Experiment at GALAXI

This is an example of a real data fit. We use our own measurements performed at the laboratory diffractometer GALAXI in Forschungszentrum Jülich.

Real-space model

Fit window

  • The sample represents a 4 layer system (substrate, teflon, hmdso and air) with Ag nanoparticles placed inside the hmdso layer on top of the teflon layer.
  • The sample is generated with the help of a SampleBuilder, which is able to create samples depending on parameters defined in the constructor and passed through to the create_sample method.
  • The nanoparticles have a broad log-normal size distribution.
  • The rectangular detector is created to represent the PILATUS detector from the experiment (line 19).
  • In the simulation settings the beam is initialized and the detector is assigned to the simulation. A region of interest is assigned at line 39 to simulate only a small rectangular window. Additionally, a rectangular mask is added to exclude the reflected beam from the analysis (line 40).
  • The real data is loaded from a tiff file into a histogram representing the detector’s channels.
  • The run_fitting() function contains the initialization of the fitting kernel: loading experimental data, assignment of fit pair, fit parameters selection (line 62).

Experiment description

To successfully simulate and fit results of some real experiment it is important to have

  • A good guess about the sample structure and the initial values of the sample parameters.
  • Full information about the instrument geometry: size and exact orientation of the detector.
  • A 2D numpy array containing the intensities measured in the detector channels.

Experiment

As an example we will use our own measurements performed at the laboratory diffractometer GALAXI in Forschungszentrum Jülich.

Our sample represents a 3-layer system (substrate, teflon and air) with Ag nanoparticles sitting on top of the teflon layer. The PILATUS 1M detector was placed at a distance of 1730 mm from the sample.

The results of the measurement are represented by the intensity image taken in certain conditions (beam wavelength, inclination angle, detector position) and stored in a 32-bit tiff file. To be able to fit these data we have to

  • prepare a description of the simulation
  • load the experimental data in BornAgain’s fitting engine

Preparing the simulation description

From the experimental setup we know the following:

  • detector geometry: number of pixels, pixel size
  • detector orientation: perpendicular to the direct beam
  • detector position: distance to the sample, coordinates of direct beam hitting the detector plane

In BornAgain, we will represent this setup using the FlatDetector object. First, we create a detector corresponding to a PILATUS detector by providing the number of detector bins and the detector’s size in millimeters:

npx, npy = 981, 1043
pixel_size = 0.172  # in mm
width = npx*pixel_size
height = npy*pixel_size

detector = FlatDetector(npx, width, npy, height)

Then we define the position of the direct beam in local detector coordinates (i.e. millimeters) and set the detector perpendicular to the direct beam at a certain distance:

detector_distance = 1730.0  # in mm

# position of direct beam in pixels, (0,0) corresponds to lower left corner of the image
beam_xpos, beam_ypos = 597.1, 323.4  # in pixels

# position of direct beam in local detector coordinates
u0 = beam_xpos*pixel_size  # in mm
v0 = beam_ypos*pixel_size  # in mm

detector.setPerpendicularToDirectBeam(detector_distance, u0, v0)

See also the Rectangular detector tutorial.

Setting the region of interest

To speed-up the simulation and to avoid an influence from uninteresting areas on the fit flow it is often convenient to define a certain region of interest roi. In our example we set the roi to the rectangle with lower left corner coordinates (85.0, 70.0) and upper right corner coordinates (120.0, 92.0), where coordinates are expressed in native detector units (mm for FlatDetector)

simulation.setRegionOfInterest(85.0, 70.0, 120.0, 92.)

The final simulation setup looks as follows:

simulation = ScatteringSimulation()
simulation.setDetector(detector)  # this is our rectangular detector
simulation.setSample(sample)  # sample creation is not covered by this tutorial
simulation.setBeamParameters(1.34*angstrom, 0.463*degree, 0.0)
simulation.setBeamIntensity(1.2e7)
simulation.setRegionOfInterest(85.0, 70.0, 120.0, 92.)

During the fit, only the part of the detector corresponding to the roi will be simulated and used for $\chi^2$ calculations.

Importing the real data using Fabio library

The Fabio library provides a convenient way to import experimental data in the form of a numpy array.

import fabio

img = fabio.open("galaxi_data.tif.gz")
data = img.data.astype("float64")

The main requirement is that the shape of the numpy array coincides with the number of detector channels (i.e. npx, npy = 981, 1043 for given example).

  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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#!/usr/bin/env python3
"""
Fitting experimental data: spherical nanoparticles with size distribution
in 3 layers system (experiment at GALAXI).
"""
import os
import bornagain as ba
from bornagain import deg, nm, ba_fitmonitor
from matplotlib import pyplot as plt

wavelength = 1.34*ba.angstrom
alpha_i = 0.463*ba.deg

# detector setup as given from instrument responsible
pilatus_npx, pilatus_npy = 981, 1043
# pilatus_pixel_size = 0.172  # in mm
# detector_distance = 1730.0  # in mm
# beam_xpos, beam_ypos = 597.1, 323.4  # in pixels

datadir = os.getenv('BA_DATA_DIR', '')
if not datadir:
    raise Exception("Environment variable BA_DATA_DIR not set")

# sample model

radius = 5.75*ba.nm
sigma = 0.4
distance = 53.6*ba.nm
disorder = 10.5*ba.nm
kappa = 17.5
ptfe_thickness = 22.1*ba.nm
hmdso_thickness = 18.5*ba.nm

def get_sample(P):
    radius = P["radius"]
    sigma = P["sigma"]
    distance = P["distance"]

    # defining materials
    vacuum = ba.RefractiveMaterial("Vacuum", 0, 0)
    material_Si = ba.RefractiveMaterial("Si", 5.7816e-6, 1.0229e-7)
    material_Ag = ba.RefractiveMaterial("Ag", 2.2475e-5, 1.6152e-6)
    material_PTFE = ba.RefractiveMaterial("PTFE", 5.20509e-6, 1.9694e-8)
    material_HMDSO = ba.RefractiveMaterial("HMDSO", 2.0888e-6, 1.3261e-8)

    # collection of particles with size distribution
    nparticles = 20
    nfwhm = 2.0
    sphere_ff = ba.Sphere(radius)

    sphere = ba.Particle(material_Ag, sphere_ff)
    position = (0, 0, -1*hmdso_thickness)
    sphere.translate(position)
#     ln_distr = ba.DistributionLogNormal(radius, sigma)
#     par_distr = ba.ParameterDistribution(
#         "/Particle/Sphere/Radius", ln_distr, nparticles, nfwhm,
#     part_coll = ba.ParticleDistribution(sphere, par_distr)

    # interference function
    interference = ba.InterferenceRadialParacrystal(
        distance, 1e6*ba.nm)
    interference.setKappa(kappa)
    interference.setDomainSize(2e4*nm)
    pdf = ba.Profile1DGauss(disorder)
    interference.setProbabilityDistribution(pdf)

    # assembling particle layout
    layout = ba.ParticleLayout()
    layout.addParticle(sphere, 1)
    layout.setInterference(interference)
    layout.setTotalParticleSurfaceDensity(1)

    # roughness
    r_ptfe = ba.LayerRoughness(2.3*ba.nm, 0.3, 5*ba.nm)
    r_hmdso = ba.LayerRoughness(1.1*ba.nm, 0.3, 5*ba.nm)

    # layers
    vacuum_layer = ba.Layer(vacuum)
    hmdso_layer = ba.Layer(material_HMDSO, hmdso_thickness, r_hmdso)
    hmdso_layer.addLayout(layout)
    ptfe_layer = ba.Layer(material_PTFE, ptfe_thickness, r_ptfe)
    substrate_layer = ba.Layer(material_Si)

    # assembling sample
    sample = ba.Sample()
    sample.addLayer(vacuum_layer)
    sample.addLayer(hmdso_layer)
    sample.addLayer(ptfe_layer)
    sample.addLayer(substrate_layer)

    return sample

def create_detector(beam):
    """
    A model of the GALAXY detector
    """
    nx = pilatus_npx
    ny = pilatus_npy
    return ba.SphericalDetector(nx, -1.7*deg, 1.7*deg, ny, -0.6*deg, 1.24*deg)


def create_simulation(P):
    """
    Creates and returns GISAS simulation with beam and detector defined
    """
    beam = ba.Beam(1.2e7, wavelength, alpha_i)
    sample = get_sample(P)
    detector = create_detector(beam)
    simulation = ba.ScatteringSimulation(beam, sample, detector)

    # simulation.detector().setRegionOfInterest(85, 70, 120, 92.)
    # beamstop:
    # simulation.detector().addMask(ba.Rectangle(101.9, 82.1, 103.7, 85.2), True)

    return simulation


def load_data(filename):
    """
    Loads experimental data and returns numpy array.
    """
    filepath = os.path.join(datadir, filename)
    return ba.readData2D(filepath)


def run_fitting():
    data = load_data("scatter2d/galaxi_data.tif.gz")

    fit_objective = ba.FitObjective()
    fit_objective.addFitPair(create_simulation, data, 1)
    fit_objective.initPrint(10)
    observer = ba_fitmonitor.PlotterGISAS()

    fit_objective.initPlot(10, observer)

    P = ba.Parameters()
    P.add("radius", 5.*nm, min=4, max=6, step=0.1*nm)
    P.add("sigma", 0.55, min=0.2, max=0.8, step=0.01)
    P.add("distance", 27.*nm, min=20, max=70)

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


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

Data to be fitted: galaxi_data.tif.gz