Fitting GISAS Data

This example demonstrates an lmfit fit of synthetic 2D GISAS data.

Fit 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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["bornagain>=25,<26", "lmfit"]
# ///
"""
Basic GISAS 2D fitting example.
Fake experimental data are generated from a known sample model.
"""

import bornagain as ba
ba.require_versions("bornagain>=25,<26")
from bornagain import ba_fitmonitor, deg, nm, nm2
import lmfit
from matplotlib import pyplot as plt
import numpy as np


def get_sample(P):
    """
    Dilute cylinders on a substrate, parameterized for GISAS fitting.
    """
    cylinder_radius = P["cylinder_radius"]
    cylinder_height = P["cylinder_height"]

    substrate_color = (0.28, 0.57, 0.82)
    particle_color = (0.86, 0.24, 0.18)

    substrate_mat = ba.RefractiveMaterial("Substrate", substrate_color, 6e-6, 2e-8)
    particle_mat = ba.RefractiveMaterial("Particle", particle_color, 6e-4, 2e-8)
    cylinder_ff = ba.Cylinder(cylinder_radius, cylinder_height)
    particle = ba.Particle(particle_mat, cylinder_ff)

    particle_layer = ba.Layer(ba.Vacuum())
    # Modest coverage keeps material averaging physical over the fit bounds.
    particle_layer.deposit2D(ba.Dilute2D(1e-3/nm2, particle))

    sample = ba.Sample()
    sample.addLayer(particle_layer)
    sample.addLayer(ba.Layer(substrate_mat))
    return sample


def get_simulation(P):
    """
    GISAS simulation with fitted beam intensity and background.
    """
    beam = ba.Beam(10**P["lg_intensity"], 0.1*nm, 0.2*deg)
    detector = ba.SphericalDetector(100, -1.5*deg, 1.5*deg, 100, 0, 3*deg)
    simulation = ba.ScatteringSimulation(beam, get_sample(P), detector)
    simulation.setBackground(ba.ConstantBackground(10**P["lg_background"]))
    return simulation


if __name__ == '__main__':
    fake_params = {
        "lg_intensity": 5,
        "lg_background": 1,
        "cylinder_height": 5.*nm,
        "cylinder_radius": 5.*nm,
    }
    data = get_simulation(fake_params).simulate()
    flat_exp_values = data.intensities().ravel()
    sim_result = None  # latest simulation, shared with the plot callback

    def residuals(P):
        global sim_result
        sim_result = get_simulation(P.valuesdict()).simulate()
        flat_sim_values = sim_result.intensities().ravel()
        return flat_exp_values - flat_sim_values

    observer = ba_fitmonitor.PlotterGISAS()

    def plot_iteration(P, iteration, resid):
        if iteration % 10 == 0 and sim_result is not None:
            observer.plot(data, sim_result, P, float(np.sum(resid*resid)))

    P = lmfit.Parameters()
    P.add("lg_intensity", value=4.3, min=0, max=15)  # (dimensionless)
    P.add("lg_background", value=2.0, min=-2, max=10)  # (dimensionless)
    P.add("cylinder_height", value=8*nm, min=0.01*nm)
    P.add("cylinder_radius", value=3.5*nm, min=0.01*nm, max=8*nm)
    result = lmfit.minimize(
        residuals,
        P,
        method="leastsq",
        iter_cb=plot_iteration)

    print("Fit completed.")
    print(lmfit.fit_report(result))
    finalP = result.params.valuesdict()
    ba.showSample3D(get_sample(finalP), sample_size=120*nm, seed=0)

    plt.show()
auto/Examples/fit/scatter2d/fit_gisas.py

Explanation

The function get_sample is basically the same as in our basic GISAS simulation example, except that radius and height of the cylindrical disks are now supplied as external parameters. These parameters are passed through a Python dictionary.

The function get_simulation has the same function argument params, from which it takes beam intensity and background level. These two parameters are on a logarithmic scale.

The residual function is defined directly in the script. It converts lmfit.Parameters to a dictionary, runs the simulation, extracts simulated intensities as a NumPy array, and returns the flattened difference to the experimental data. This residual function is passed to lmfit.minimize. In interactive mode, the lmfit iteration callback updates the fit monitor while the fit is running.