SciPy basics

This example runs a BornAgain fitting problem directly through scipy.optimize instead of lmfit. The objective receives a NumPy array of candidate parameters, runs the corresponding simulation, and returns the sum of squared residuals as one scalar value.

scipy.optimize.differential_evolution explores the bounded parameter region and returns a SciPy OptimizeResult. The script reports convergence, the initial and final objective values, the fitted parameters, and the number of function evaluations.

Result

SciPy basics result

Sample

SciPy basics sample

Python 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
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["bornagain>=25,<26", "scipy>=1.7"]
# ///
"""
SciPy differential evolution for BornAgain fits.
"""
import bornagain as ba
ba.require_versions("bornagain>=25,<26")
from bornagain import deg, nm
import numpy as np
import scipy.optimize


def get_sample(P):
    """
    Spheres on a hexagonal lattice, parameterized for fitting.
    """
    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)

    particle = ba.Particle(particle_mat, ba.Sphere(P["radius"]))

    lattice = ba.HexagonalLattice2D(P["length"], 0)
    struct = ba.Crystal2D(particle, lattice)
    struct.setDecayFunction(ba.Profile2DCauchy(100*nm, 100*nm, 0))

    particle_layer = ba.Layer(ba.Vacuum())
    particle_layer.deposit2D(struct)

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


def get_simulation(P):
    """
    GISAS simulation for the parameterized hexagonal lattice.
    """
    n_pix = 100

    beam = ba.Beam(1e8, 0.1*nm, 0.2*deg)
    detector = ba.SphericalDetector(n_pix, -1*deg, 1*deg, n_pix, 0, 2*deg)

    simulation = ba.ScatteringSimulation(beam, get_sample(P), detector)
    return simulation


def fake_data():
    """
    Noisy synthetic data for a known hexagonal lattice.
    """
    P = {"radius": 5*nm, "length": 14*nm}
    return get_simulation(P).simulate().noisy(0.1, 0.1)


def print_result(result, initial_objective):
    """
    Prints selected fields of the SciPy OptimizeResult.
    """
    radius, length = result.x
    print(f"Success: {result.success}")
    print(f"Message: {result.message}")
    print(f"Initial objective: {initial_objective:.6g}")
    print(f"Objective: {result.fun:.6g}")
    print(f"Function evaluations: {result.nfev}")
    print(f"radius: {radius:.6g}")
    print(f"length: {length:.6g}")


if __name__ == '__main__':
    # Generate synthetic data for the fit target.
    data = fake_data()
    flat_exp_values = data.intensities().ravel()

    def scalar_objective(values):
        """
        Runs a simulation and returns the sum of squared residuals.
        """
        radius, length = values
        parameters = {"radius": radius, "length": length}
        simulation = get_simulation(parameters)
        result = simulation.simulate()
        flat_sim_values = result.intensities().ravel()
        residuals = flat_sim_values - flat_exp_values
        return np.sum(residuals*residuals)

    # Define SciPy's initial candidate and bounds.
    initial_values = np.array([4.5*nm, 13.5*nm])
    bounds = [(4*nm, 6*nm), (13*nm, 15*nm)]
    initial_objective = scalar_objective(initial_values)

    # differential_evolution expects a scalar objective value.
    result = scipy.optimize.differential_evolution(
        scalar_objective,
        bounds=bounds,
        x0=initial_values,
        maxiter=30,
        popsize=6,
        polish=True,
        seed=0)

    print_result(result, initial_objective)
    radius, length = result.x
    final_parameters = {"radius": radius, "length": length}
    final_result = get_simulation(final_parameters).simulate()
    ba.showSample3D(get_sample(final_parameters), sample_size=300*nm, seed=0)
    ba.plot_datafield(final_result, unit_aspect=1)
    ba.plt.show()
auto/Examples/fit/gisas/scipy_basics.py