Minimizer

BornAgain fitting uses standard Python minimization packages. The recommended choice is lmfit, which provides a convenient parameter interface and several algorithms. For global search (useful when parameters are far from their optimal values), scipy.optimize.differential_evolution is a good option.

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:

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)
print(lmfit.fit_report(result))

Two-stage fit (global + local)

For difficult problems, combine a global search with a local refinement:

import scipy

# Stage 1: global search
bounds = [(1*nm, 10*nm), (8*nm, 14*nm)]
def objective(x):
    P = {"radius": x[0], "length": x[1]}
    r = exp_values - run_simulation(P).simulate().intensities()
    return float(np.sum(r*r))

result1 = scipy.optimize.differential_evolution(
    objective, bounds, seed=42, tol=1e-4, maxiter=100)

# Stage 2: local refinement seeded from stage 1
P["radius"].value = result1.x[0]
P["length"].value = result1.x[1]
result2 = lmfit.minimize(residuals, P)
print(lmfit.fit_report(result2))

Further resources