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.
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:
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))
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))