In fitting, we estimate the optimum parameters in a numerical model, specifically a scattering simulation, by minimizing the difference between simulated and reference data.
BornAgain provides the simulation result. Experimental data import,
parameter management, and the residual function are ordinary Python code.
This keeps fitting scripts independent of any BornAgain-specific data format
and lets users combine BornAgain with standard packages such as
numpy, fabio, and lmfit.
In the following we will show how to fit using the BornAgain Python API.
In the following, a very simple example shows how to fit a parametric model to given data.
The model is a specular reflectometry scan, with a sample consisting of 20 alternating Ti and Ni layers on a Si substrate. Using this model, synthetic data have been generated with GenX. These data are part of the BornAgain sources, testdata/genx_alternating_layers.dat.gz. The generated example includes a copy beside the script.
From the build directory, run as:
python3 ../auto/Examples/fit/...
The fit model is identical to the model used for generating the data.
There is just one fit parameter, namely the thickness of the Ti layers.
The resulting fit is indistinguishable from the data:
|
|
The arrays alpha and exp_values contain the data to be fitted.
They are read with ba.read_columns and converted explicitly to the
units required by the simulation; see Data import.
The lmfit.Parameters object P contains the fit parameters.
The function residuals(P) is passed to the minimizer. It converts the
lmfit parameters to a plain dictionary, runs the BornAgain simulation,
extracts simulated intensities as a NumPy array, reports the completed
evaluation, and returns the residual vector.
FitPlotter describes one subplot by combining a plotting function, optional
fixed plot data, and plotting arguments. FitMonitor arranges one or more
such plots and adds the fit status. The residual function reports each complete
evaluation directly:
# Fit progress display
fit_plotter = ba.FitPlotter(
ba.plot_specular,
context_data=exp_data,
ylabel="Intensity",
)
monitor = ba.FitMonitor(
fit_plotter,
ncols=1,
show_best=True,
max_fps=1,
printer=ba.Printer(every_nth=10))
def residuals(P):
simulation = get_simulation(alpha, P.valuesdict()).simulate()
residuals = exp_values - simulation.intensities()
monitor.update(simulation, P, residuals)
return residuals
result = lmfit.minimize(residuals, P, method="leastsq")
# Recompute and report the simulation at the fitted parameters.
residuals(result.params)
# Render the just-reported evaluation as the final fit state.
monitor.render_final(result.params)
ba.plt.show()
Here exp_data remains fixed as the plotter’s context_data, while
monitor.update(simulation, P, residuals) supplies one complete evaluation.
The standard plot_specular helper draws the measured and simulated curves
on the subplot managed by FitMonitor. max_fps limits graphical redraws,
independently of the evaluation interval selected by Printer.every_nth for
terminal output. The explicit final residual call evaluates the fitted
parameters through this same update path before render_final renders them.
A custom plotting function receives the current result first:
plotter_fn(result, ax=subplot, **plot_args). If context_data is configured,
the monitor also passes it as a keyword argument. This is fixed data for that
plot, not sample or detector configuration and not fit state. The result may
also be a tuple of simulations. This keeps standard BornAgain plots and
user-defined derived plots equally usable without requiring source callbacks.
For its complete optimizer-independent interface, constructor parameters,
layout, and display helpers, see
Fit reference > Fit monitoring.
For masked detector pixels, BornAgain simulations return NaN.
Residual functions that use detector masks should exclude those entries
explicitly. The helper ba.valid_pixel_residual(exp, sim) does this and
also excludes non-finite or negative experimental values, the dead-pixel
markers of real detectors.
For a rectangular analysis window described by a boolean detector mask,
ba.crop_by_mask(data, mask) returns the part of the data inside the
smallest rectangle containing all unmasked (False) pixels.