Overview

In fitting, we estimate the optimum parameters in a numerical model, specifically a scattering simulation, by minimizing the difference between simulated and reference data.

BornAgain supports

  • a variety of minimization algorithms;
  • choice of fitting parameters, their properties and correlations;
  • full control over objective function calculations, including the use of different normalizations and assignments of different masks and weights to different areas of the reference data;
  • possibility to fit simultaneously an arbitrary number of data sets.

In the following we will show how to fit using the BornAgain Python API. For fitting through the graphical user interface, see GUI > Fitting.

Introductory example

In the following, a very simple example shows how to fit a parametric model to given data.

The model is a specular reflectometry scan, with a sample consisting of 20 alternating Ti and Ni layers on a Si substrate. Using this model, synthetic data have been generated with GenX. These data are part of the BornAgain sources, testdata/genx_alternating_layers.dat.gz. To make them findable by the script, the environment variable BA_DATA_DIR must point to a local copy of directory testdata.

The fit model is identical to the model used for generating the data. There is just one fit parameter, namely the thickness of the Ti layers. The resulting fit is indistinguishable from the data:

Script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#!/usr/bin/env python3
"""
Basic example how to fit specular data.
The sample consists of twenty alternating Ti and Ni layers.
Reference data was generated with GenX.
We fit just one parameter, the thickness of the Ti layers,
which has an original value of 30 angstroms.
"""

import os
import numpy as np
from matplotlib import pyplot as plt
import bornagain as ba
from bornagain import angstrom, ba_fitmonitor


def load_data():
    datadir = os.getenv('BA_DATA_DIR', '')
    data_fname = os.path.join(datadir, "genx_alternating_layers.dat.gz")

    expdata = np.loadtxt(data_fname, usecols=(0, 1), skiprows=3)
    # convert double incident angle (degs) to incident angle (radians)
    expdata[:, 0] *= np.pi/360

    return (expdata[:, 0], expdata[:, 1])


def get_sample(P):
    # Materials
    vacuum = ba.MaterialBySLD()
    material_Ti = ba.MaterialBySLD("Ti", -1.9493e-06, 0)
    material_Ni = ba.MaterialBySLD("Ni", 9.4245e-06, 0)
    material_Si = ba.MaterialBySLD("Si", 2.0704e-06, 0)

    # Multilayer
    layer_Ti = ba.Layer(material_Ti, P["thickness_Ti"])
    layer_Ni = ba.Layer(material_Ni, 70*angstrom)

    sample = ba.MultiLayer()
    sample.addLayer(ba.Layer(vacuum))
    for _ in range(10):
        sample.addLayer(layer_Ti)
        sample.addLayer(layer_Ni)
    sample.addLayer(ba.Layer(material_Si))

    return sample


def get_simulation(P):
    scan = ba.AlphaScan(ba.ListScan("", exp_x))
    scan.setWavelength(1.54*angstrom)
    sample = get_sample(P)

    return ba.SpecularSimulation(scan, sample)


if __name__ == '__main__':
    global exp_x
    exp_x, exp_y = load_data()

    P = ba.Parameters()
    P.add("thickness_Ti", 50*angstrom, min=10*angstrom, max=60*angstrom)

    fit_objective = ba.FitObjective()
    fit_objective.addSimulationAndData(get_simulation, exp_y, 1)

    fit_objective.initPrint(10)
    plot_observer = ba_fitmonitor.PlotterSpecular(pause=0.5)
    fit_objective.initPlot(10, plot_observer)

    minimizer = ba.Minimizer()
    result = minimizer.minimize(fit_objective.evaluate, P)

    fit_objective.finalize(result)

    plot_observer.show()
auto/Examples/fit/specular/Specular1Par.py

Explanations

The arrays (Python lists) exp_x, exp_y contain the data to be fitted.

The dictionary P contains the fit parameter.

An instance of class FitObjective defines the objective function in terms of data $y(x)$ and fit model $f(x;P)$. By default, it is a $\chi^2$ function, weighted with the inverse standard deviation.

The function call initPrint(10) means that values of $\chi^2$ and of all fit parameters are printed to the terminal every 10 fit iterations. To suppress the printing, set the function argument to 0.

Similarly, the function call initPlot(10, ...) means that data and fit are replotted every 10 fit iterations. In the PlotterSpecular, the argument pause=0.5 means that after refreshing the plot the program is put to sleep for 0.5 s. This enables humans to observe the evolution of the plots in real time. With pause=0, the program executes so fast that one only sees the final plot.

The actual fit is controlled by class Minimizer, and launched by the function call minimize(...).