BornAgain fitting uses standard Python minimization packages.
The examples use lmfit, which provides
one parameter and objective interface for local and global algorithms.
Most of these algorithms are implemented by scipy.optimize underneath.
The lmfit.Parameters class defines a collection of fit parameters.
Each parameter has a unique name, starting value, and optional bounds.
import lmfit
P = lmfit.Parameters()
P.add("radius", value=5*nm, min=1*nm, max=10*nm)
P.add("length", value=10*nm, min=8*nm, max=14*nm)
P.add("density", value=1e-4, vary=False)
Pass a user-defined residual function and the parameters to lmfit.minimize.
The method argument selects the algorithm without changing the model:
exp_values = exp_data.intensities()
def residuals(P):
sim = run_simulation(P.valuesdict()).simulate().intensities()
return (exp_values - sim).ravel()
result = lmfit.minimize(residuals, P, method="leastsq")
print(lmfit.fit_report(result))
For scalar optimization methods, including differential evolution, lmfit
minimizes the sum of squares of the returned residual vector. Parameter
bounds and fixed parameters (vary=False) remain part of P. Differential
evolution requires finite lower and upper bounds for every varying parameter.
Keeping the residual vector also lets a subsequent least-squares method refine
the same objective. A scalar objective is only suitable for scalar methods.
For difficult problems, combine a global search with a local refinement:
from itertools import count
# Stage 1: bounded global search
n_generations = 100
generations = count(1)
def stop_callback(*_args, **_kwargs):
return next(generations) >= n_generations
result1 = lmfit.minimize(
residuals,
P,
method="differential_evolution",
callback=stop_callback,
max_nfev=100000, # emergency evaluation cap
polish=False,
seed=42)
# Stage 2: local refinement, starting from the global result
result2 = lmfit.minimize(
residuals, result1.params, method="leastsq")
print(lmfit.fit_report(result2))
SciPy calls stop_callback once per generation. It is separate from lmfit’s
iter_cb, which monitors every objective-function evaluation. The global
stage disables SciPy’s built-in polishing because the second stage performs
the local refinement explicitly.
Set max_nfev well above the callback’s expected evaluation budget. lmfit
treats it as a hard abort limit, so reaching it skips polishing and may not
preserve SciPy’s best population member.
When differential evolution is the only optimization stage, keep
polish=True to refine its best population member locally.
Direct scipy.optimize remains available for algorithms or low-level options
that lmfit does not expose.