Consecutive fitting

This example demonstrates two consecutive fits through the same lmfit API.

  • In this example we are looking for the radius and height of cylindrical nano particles randomly distributed on a surface.
  • The first fit uses differential evolution to explore the bounded parameter space globally.
  • The second fit passes the resulting lmfit.Parameters directly to a local least-squares algorithm to refine the minimum.
  • Both stages use the same residual interface; only the method changes.
  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
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["bornagain>=25,<26", "lmfit"]
# ///
"""
Fitting example: two-stage global+local fitting.

Stage 1 uses differential evolution for a global search over a large
parameter space. Stage 2 uses a local least-squares method to refine the
result to a precise minimum. Both stages use the same residual vector and
the lmfit API.
"""

from itertools import count
from matplotlib import pyplot as plt
import bornagain as ba
ba.require_versions("bornagain>=25,<26")
from bornagain import deg, angstrom, nm, nm2
import lmfit


def get_sample(P):
    """
    A sample with uncorrelated cylinders and pyramids on a substrate.
    """
    radius = P["radius"]
    height = P["height"]

    substrate_color = (0.28, 0.57, 0.82)
    substrate_mat = ba.RefractiveMaterial("Substrate", substrate_color, 6e-6, 2e-8)

    particle_color = (0.86, 0.24, 0.18)
    particle_mat = ba.RefractiveMaterial("Particle", particle_color, 6e-4, 2e-8)

    ff = ba.Cylinder(radius, height)
    particle = ba.Particle(particle_mat, ff)

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

    substrate_layer = ba.Layer(substrate_mat)
    sample = ba.Sample()
    sample.addLayer(vacuum_layer)
    sample.addLayer(substrate_layer)
    return sample


def get_simulation(P):
    """
    A GISAXS simulation with beam and detector defined.
    """
    beam = ba.Beam(1e8, 1*angstrom, 0.2*deg)
    n = 100
    detector = ba.SphericalDetector(n, 0., 2*deg, n, 0., 2*deg)
    simulation = ba.ScatteringSimulation(beam, get_sample(P), detector)
    return simulation


def fake_data():
    """
    Generating "real" data by adding noise to the simulated data.
    """
    P = {'radius': 5*nm, 'height': 5*nm}

    simulation = get_simulation(P)
    result = simulation.simulate()

    return result.noisy(0.3, 0.5)


def run_fitting():
    data = fake_data()
    flat_exp_values = data.intensities().ravel()
    flat_exp_errors = data.errors().ravel()

    P = lmfit.Parameters()
    P.add("height", value=10*nm, min=0.01*nm, max=15*nm)
    P.add("radius", value=10*nm, min=0.01*nm, max=15*nm)

    def residuals(P):
        """
        Returns residuals normalized by the synthetic data uncertainties.
        """
        sim_values = get_simulation(P.valuesdict()).simulate().intensities()
        flat_sim_values = sim_values.ravel()
        return (flat_sim_values - flat_exp_values)/flat_exp_errors

    # Stage 1: global search with differential evolution
    n_generations = 20
    generations = count(1)

    def stop_callback(*_args, **_kwargs):
        return next(generations) >= n_generations

    de_result = lmfit.minimize(
        residuals,
        P,
        method="differential_evolution",
        callback=stop_callback,  # stops the search after n_generations
        popsize=15,
        max_nfev=1000,  # emergency evaluation cap
        polish=False,
        seed=42)

    # Stage 2: local refinement seeded from global search result
    result = lmfit.minimize(
        residuals,
        de_result.params,
        method="leastsq")

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


if __name__ == '__main__':
    run_fitting()
    plt.show()
auto/Examples/fit/scatter2d/consecutive_fitting.py