Fitting (LiveFit)
Similar to LivePlot, ibex_bluesky_core.callbacks.LiveFit provides a thin wrapper around Bluesky’s bluesky.callbacks.LiveFit class, enhancing it with additional functionality to better support real-time data fitting. This wrapper not only offers a wide selection of models to fit your data on, but also introduces guess generation for fit parameters. As new data points are acquired, the wrapper refines these guesses dynamically, improving the accuracy of the fit with each additional piece of data, allowing for more efficient and adaptive real-time fitting workflows.
In order to use the wrapper, import LiveFit is imported from ibex_bluesky_core.callbacks.LiveFit rather than bluesky.callbacks.LiveFit:
from ibex_bluesky_core.callbacks import LiveFit
Configuration
Below is a full example showing how to use standard matplotlib & bluesky functionality to apply fitting to a scan, using LivePlot and LiveFit. The fitting callback is set to expect data to take the form of a Gaussian, using the Gaussian model.
import matplotlib.pyplot as plt
from ibex_bluesky_core.callbacks import LiveFit, LivePlot
from ibex_bluesky_core.fitting import Gaussian
from bluesky.callbacks import LiveFitPlot
# Create a new figure to plot onto.
plt.figure()
# Make a new set of axes on that figure
ax = plt.gca()
# ax is shared by fit_callback and plot_callback
plot_callback = LivePlot(y="y_signal", x="x_signal", ax=ax, yerr="yerr_signal")
fit_callback = LiveFit(Gaussian.fit(), y="y_signal", x="x_signal", yerr="yerr_signal", update_every=0.5)
# Using the yerr parameter allows you to use error bars.
# update_every = in seconds, how often to recompute the fit.
# If `None`, do not compute until the end. Default is 1.
fit_plot_callback = LiveFitPlot(fit_callback, ax=ax, color="r")
Note
The LiveFit callback doesn’t perform plotting. It will return fitted parameters; a LiveFit object must be passed to bluesky.callbacks.mpl_plotting.LiveFitPlot in order to plot.
Using the yerr argument allows you to pass uncertainties via a signal to LiveFit, so that the “weight” of each point influences the fit produced. By not providing a signal name you choose not to use uncertainties/weighting in the fitting calculation. Each weight is computed as 1/(standard deviation at point) and is taken into account to determine how much a point affects the overall fit of the data. Same as the rest of LiveFit, the fit will be updated after every new point collected now taking into account the weights of each point. Uncertainty data is collected from Bluesky event documents after each new point.
The plot_callback and fit_plot_callback objects can then be subscribed to the RunEngine, for example using subs_decorator:
from bluesky.preprocessors import subs_decorator
@subs_decorator(
[
fit_plot_callback,
plot_callback
]
)
def plan():
...
Models
We support standard fits for the following trends in data. See Standard Fits for more information on the behaviour of these fits.
Trend |
Class Name in |
Arguments |
|---|---|---|
Linear |
None |
|
Polynomial |
Polynomial Degree (int) |
|
Gaussian |
None |
|
Lorentzian |
None |
|
Damped Oscillator |
None |
|
Slit Scan Fit |
None |
|
Error Function |
None |
|
Complementary Error Function |
None |
|
Top Hat |
None |
|
Trapezoid |
None |
|
Negative Trapezoid |
None |
|
Muon Momentum |
None |
|
PeakStats (COM) * |
- |
- |
Bluesky additionally provides a bluesky.callbacks.fitting.PeakStats callback which computes peak statistics after a run finishes. Similar to LiveFit, PeakStats does not plot by itself. The plot_peak_stats function can be used to draw results of a PeakStats on a plot.
Each of the above fit classes has a .fit() which returns an object of type FitMethod. This tells LiveFit how to perform fitting on the data.
There are two ways that you can choose how to fit a model to your data:
Standard Models
When only using the standard fits provided by the ibex_bluesky_core.fitting module, the following syntax can be used, replacing [FIT] with your chosen model from ibex_bluesky_core.fitting:
from bluesky.callbacks import LiveFitPlot
from ibex_bluesky_core.fitting import [FIT]
# Pass [FIT].fit() to the first parameter of LiveFit
lf = LiveFit([FIT].fit(), y="y_signal", x="x_signal", update_every=0.5)
# Then subscribe to LiveFitPlot(lf, ...)
The [FIT].fit() function will pass the FitMethod object straight to the LiveFit class.
Note
For the fits in the above table that require parameters, you will need to pass value(s) to their .fit method. For example, for a Polynomial model:
# For a polynomial of degree 3
lf = LiveFit(Polynomial.fit(3), y="y_signal", x="x_signal", update_every=0.5)
Custom Models
If you wish, you can define your own non-standard FitMethod object. The FitMethod class takes two function arguments as follows:
modelA function representing the behaviour of the model.
Returns the
yvalue (float) at the givenxvalue and model parameters.
guessA function that must take two
numpy.ndarrayarrays, representingxdata and respectiveydata, of typefloatas arguments and must return adictwithstrkeys andlmfit.parameter.Parametervalues.This will be called to guess a set of initial values for the fit, given the data already collected in the bluesky run.
See the following example on how to define these.
# Linear Fitting
import numpy.typing as npt
import numpy as np
import lmfit
from ibex_bluesky_core.fitting import FitMethod
from ibex_bluesky_core.callbacks import LiveFit
def model(x: float, c1: float, c0: float) -> float:
return c1 * x + c0 # y = mx + c
def guess(x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]) -> dict[str, lmfit.Parameter]:
# Linear regression calculation
# x = set of x data
# y = set of respective y data
# x[n] makes a pair with y[n]
numerator = sum(x * y) - sum(x) * sum(y)
denominator = sum(x**2) - sum(x) ** 2
c1 = numerator / denominator
c0 = (sum(y) - c1 * sum(x)) / len(x)
init_guess = {
"c1": lmfit.Parameter("c1", c1), # gradient
"c0": lmfit.Parameter("c0", c0), # y - intercept
}
return init_guess
fit_method = FitMethod(model, guess)
#Pass the model and guess function to FitMethod
lf = LiveFit(fit_method, y="y_signal", x="x_signal", update_every=0.5)
# Then subscribe to LiveFitPlot(lf, ...)
Note
The parameters returned from the guess function must allocate to the arguments to the model function, ignoring the independent variable e.g x in this case. Array-like structures are not allowed. See the Parameter and Parameters for more information.
Each FitMethod in ibex_bluesky_core.fitting has a .model() and .guess(), which make up their fitting method. These are publicly accessible class methods.
This means that as long as the parameters returned from the guess function match to the arguments of the model, you may mix and match user-defined and standard models and guess functions:
from ibex_bluesky_core.callbacks import LiveFit
from ibex_bluesky_core.fitting import FitMethod, Linear
def different_model(x: float, c1: float, c0: float) -> float:
return c1 * x + c0 ** 2 # y = mx + (c ** 2)
fit_method = FitMethod(different_model, Linear.guess())
# Uses the user defined model and the standard Guessing. function for linear models
lf = LiveFit(fit_method, y="y_signal", x="x_signal", update_every=0.5)
# Then subscribe to LiveFitPlot(lf, ...)
Or the other way round …
import lmfit
from ibex_bluesky_core.callbacks import LiveFit
from ibex_bluesky_core.fitting import FitMethod, Linear
# This Guessing. function isn't very good because it's return values don't change on the data already collected in the Bluesky run
# It always guesses that the linear function is y = x
def different_guess(x: float, c1: float, c0: float) -> float:
init_guess = {
"c1": lmfit.Parameter("c1", 1), # gradient
"c0": lmfit.Parameter("c0", 0), # y - intercept
}
return init_guess
fit_method = FitMethod(Linear.model(), different_guess)
# Uses the standard linear model and the user defined Guessing. function
lf = LiveFit(fit_method, y="y_signal", x="x_signal", update_every=0.5)
# Then subscribe to LiveFitPlot(lf, ...)
Or you can create a completely user-defined fitting method.
Note
For fits that require arguments, you will need to pass values to their respective .model and .guess functions. E.g for Polynomial fitting:
fit_method = FitMethod(Polynomial.model(3), different_guess) # If using a custom guess function
lf = LiveFit(fit_method, ...)