Fit monitoring

BornAgain’s fit-monitoring helpers display fit results, parameter values, and the objective while a fit is running. Their progress interface is independent of the minimizer package: the residual function reports the result, parameters, and residuals from each completed evaluation. The monitor numbers these evaluations itself.

For an introductory fit, see Python scripting > Fitting.

FitMonitor

The constructor is

ba.FitMonitor(
    plotters,
    ncols,
    show_best=True,
    live=True,
    figsize=None,
    max_fps=1,
    printer=None)
Parameter Default Meaning
plotters required One FitPlotter or an iterable of them.
ncols required Positive integer number of plot columns.
show_best True Show the best rather than current live state.
live True Draw and display states during the fit.
figsize None Matplotlib figure size in inches, or automatic.
max_fps 1 Maximum number of live redraws per second.
printer None Optional callback for textual fit progress.

ncols is used exactly as given. It may exceed the number of plots, leaving space in the grid for the fit status.

max_fps limits only redraws during the fit. With live=True, the first state is drawn immediately. render_final always draws the last reported state. Drawing is synchronous: max_fps limits its frequency but does not guarantee a fixed rendering overhead.

Text output has an independent evaluation-based cadence. To retain a terminal history alongside the graphical monitor, supply a Printer:

monitor = ba.FitMonitor(
    plotters,
    ncols=2,
    show_best=True,
    max_fps=1,
    printer=ba.Printer(every_nth=10))

Here the figure is redrawn at most once per second. The first evaluation and every tenth numbered evaluation are printed, and render_final always prints the final state. Printing remains active with live=False. Each report contains the evaluation number, squared-residual objective, and current parameter names and values.

evaluation      1  objective 3.18472916e+06  initial
parameters  radius= 5.000000e+00
evaluation     20  objective 2.91662381e+06
parameters  radius= 5.004830e+00
evaluation     37  objective 2.90184272e+06  final
parameters  radius= 5.008150e+00

The displayed objective is the sum of the squared residual values, written in scientific notation with eight digits after the decimal point. Its fixed width keeps the status typography independent of redraw frequency. With show_best=True, the monitor displays the result and parameters that produced the lowest finite objective seen so far. The monitor does not infer a different scalar reduction used internally by an optimizer.

Optimizer-independent interface

Report every completed fit evaluation with one call:

monitor.update(result, parameters, residuals)

The parameters may be a mapping or any object that provides valuesdict(). The result is opaque to the monitor and is passed unchanged to every plotter. For multiple simulations, use an explicit tuple or another container:

monitor.update((simulation1, simulation2), parameters, residuals)

update must run in the process and thread that created the monitor. Parallel evaluation has no single ordered state to display and is not supported. The evaluation count is the number of completed update calls. It can differ from an optimizer’s reported nfev when that optimizer evaluates the residual function during setup or finalization. The count remains monotonic when the same monitor and residual function are used across several sequential optimization stages. Best-state comparisons across stages are meaningful only while the residual definition stays unchanged.

After optimization, call the residual function once with the final parameters. This reports a fresh result through the same update path. Then mark that last evaluation as final:

# 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()

The explicit final residual call adds one reported evaluation. render_final rejects parameters that do not match the last update, preventing an optimizer’s last trial or derivative probe from being presented as the fitted result. render_final(..., filename="fit.png") also writes the final figure to a file. The standard Matplotlib show call is needed at the end of a standalone interactive script so that the process remains open. Omit it when saving a figure in headless mode or when the interactive environment owns the display.

Use with lmfit

The residual function can report each complete evaluation directly:

def residuals(P):
    simulation = get_simulation(P.valuesdict()).simulate()
    residuals = exp_values - simulation.intensities()
    monitor.update(simulation, P, residuals)
    return residuals

result = lmfit.minimize(residuals, P, method="leastsq")

This does not occupy lmfit’s iter_cb; applications remain free to use that callback for another purpose.

Use with another optimizer

The same direct reporting works with another sequential optimizer:

def objective(values):
    parameters, simulation, residuals = evaluate(values)
    monitor.update(simulation, parameters, residuals)
    return np.sum(residuals*residuals)

No optimizer callback or external evaluation counter is required.

FitPlotter

A FitPlotter describes one subplot:

ba.FitPlotter(plotter_fn, *, context_data=None, **plot_args)
Parameter Default Meaning
plotter_fn required Function that draws one subplot.
context_data None Optional fixed data passed to plotter_fn.
plot_args none Keyword arguments forwarded to plotter_fn.

The plotting function is called as plotter_fn(result, ax=subplot, **plot_args). The current result comes from FitMonitor.update. If context_data is not None, it is also passed as a keyword argument. It is fixed data needed by that plot, not sample or detector configuration and not fit state. For example, the standard specular plot needs experimental data as context_data and the current simulation as result:

plotter = ba.FitPlotter(
    ba.plot_specular,
    context_data=experimental,
    ylabel="Intensity",
)

Custom functions use the same result-first signature and declare the keyword-only context_data argument only when they need fixed plot data. When several simulations are fitted together, pass them as one tuple result and select from that tuple in a custom function. The result-consuming standard helpers expect one simulation; plot_experimental remains usable because it only reads context_data. Plotting functions should only derive display data; they must not run simulations or modify the supplied objects. FitMonitor supplies the subplot internally. When a custom function calls ba.plot_heatmap, pass with_cb=True to give that subplot its own colorbar. The ax argument is therefore reserved and cannot be supplied to the FitPlotter constructor. The name context_data is likewise reserved. Leaving it as None means that the keyword is not passed to the plotting function.

Status layout

The status uses free cells in an incomplete plot row when its parameter table fits. Otherwise it occupies a row below the plots. Up to eight parameters whose names have at most 15 characters first try one column in a free cell; larger parameter sets use two columns in the separate status row.

The rendered text extents are checked after drawing. The figure may grow to keep all status text visible. If bounded growth cannot make it fit, the monitor emits a RuntimeWarning and continues with clipped status text.

Display helpers

ba.intensity_norm(data, zmin=None, zmax=None) creates a fixed logarithmic normalization shared by several heat maps. Without zmin, its range spans at most six decades below the maximum positive intensity. Explicit zmin and zmax values override the inferred limits.

ba.relative_difference(experimental, simulated) returns 2*(simulation-experiment)/(|simulation|+|experiment|). Non-finite pixels remain NaN; a finite zero divided by zero is defined as zero.

The standard FitPlotter functions are:

  • ba.plot_experimental(result, ..., context_data=experimental) plots fixed experimental data.
  • ba.plot_heatmap plots the current simulation directly and needs no context_data.
  • ba.plot_difference(simulation, ..., context_data=experimental) plots the signed relative difference on the default [-2, 2] color scale.
  • ba.plot_masked_experimental(simulation, ..., context_data=experimental) plots the experimental data and dims pixels that are non-finite in the simulation.
  • ba.plot_specular(simulation, ..., context_data=experimental) plots a measured and simulated specular curve pair.

For text-only progress, pass ba.Printer(every_nth=10) directly to the optimizer instead of creating a FitMonitor. Printer displays the evaluation number supplied by its caller: FitMonitor supplies its complete update count, while an optimizer callback supplies the optimizer’s own nfev. Since an optimizer callback cannot know when optimization has ended, call printer.final(params, nfev, residuals) explicitly to print the final text-only state.