Minimizer

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.

Fit parameter setup with lmfit

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)

Running the fit

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.

Two-stage fit (global + local)

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.

Further resources