Basic fitting tutorial

In this section we are going to go through a complete example of fitting using BornAgain. Each step will be associated with a detailed piece of code written in Python. The script can also be found in the Fit Cylinders and Prisms example.

This example uses the same sample geometry as in Basic simulation tutorial. Cylindrical and prismatic particles in equal proportion are deposited on a substrate layer, with no interference between the particles. We consider the following parameters to be unkown

  • the radius of cylinders,
  • the height of cylinders,
  • the length of the prisms’ triangular basis,
  • the height of prisms.

Our reference data are a “noisy” two-dimensional intensity map obtained from the simulation of the same geometry with a fixed value of 5nmfor the height and radius of cylinders and for the height of prisms which have a 10-nanometer-long side length. Then we run our fitting using default minimizer settings starting with a cylinder’s height of 4nm, a cylinder’s radius of 6nm, a prism’s half side of 6nm and a height equal to 4nm. As a result, the fitting procedure is able to find the correct value of 5nm for all four parameters.

Importing python libraries

from bornagain import * 

We start from import of the BornAgain Python API.

Building the sample

def get_sample():
    """
    Build the sample representing cylinders and pyramids on top of
    substrate without interference.
    """
    # defining materials
    m_air = HomogeneousMaterial("Air", 0.0, 0.0)
    m_substrate = HomogeneousMaterial("Substrate", 6e-6, 2e-8)
    m_particle = HomogeneousMaterial("Particle", 6e-4, 2e-8)

    # collection of particles
    cylinder_ff = FormFactorCylinder(1.0*nanometer, 1.0*nanometer)
    cylinder = Particle(m_particle, cylinder_ff)
    prism_ff = FormFactorPrism3(1.0*nanometer, 1.0*nanometer)
    prism = Particle(m_particle, prism_ff)
    particle_layout = ParticleLayout()
    particle_layout.addParticle(cylinder, 0.5)
    particle_layout.addParticle(prism, 0.5)
    interference = InterferenceFunctionNone()
    particle_layout.addInterferenceFunction(interference)

    # air layer with particles and substrate form multi layer
    air_layer = Layer(m_air)
    air_layer.addLayout(particle_layout)
    substrate_layer = Layer(m_substrate, 0)
    multi_layer = MultiLayer()
    multi_layer.addLayer(air_layer)
    multi_layer.addLayer(substrate_layer)
    return multi_layer

The function starting at line 4 creates a multilayered sample with cylinders and prisms using arbitrary 1nm value for all size’s of particles. The details about the generation of this
multilayered sample are given in the Basic simulation tutorial.

Creating the simulation

def get_simulation():
    """
    Create GISAXS simulation with beam and detector defined
    """
    simulation = GISASSimulation()
    simulation.setDetectorParameters(100, -1.0*degree, 1.0*degree, 100, 0.0*degree, 2.0*degree)
    simulation.setBeamParameters(1.0*angstrom, 0.2*degree, 0.0*degree)
    return simulation

The function starting at line 35 creates the simulation object with the definition of the beam and detector parameters.

Preparing the fitting pair

def run_fitting():
    """
    run fitting
    """
    sample = get_sample()
    simulation = get_simulation()
    simulation.setSample(sample)

    real_data = IntensityDataIOFactory.readIntensityData('refdata_fitcylinderprisms.int.gz')

Lines 49-51 generate the sample and simulation description and assign the sample to the simulation. Our reference data are contained in the file ’refdata_fitcylinderprisms.int.gz’. This reference had been generated by adding noise on the scattered intensity froma numerical sample with a fixed length of 5nm for the four fitting parameters (i.e. the dimensions of the cylinders and prisms). Line 53 creates the real data object by loading the ASCII data fromthe input file.

Setting up FitSuite

    fit_suite = FitSuite()
    fit_suite.addSimulationAndRealData(simulation, real_data)
    fit_suite.initPrint(10)

Line 55 creates a FitSuite object which provides the main interface to the minimization kernel of BornAgain. Line 56 submits simulation description and real data pair to the
subsequent fitting. Line 57 sets up FitSuite to print on the screen the information about fit progress once per 10 iterations.

    fit_suite.addFitParameter("*FormFactorCylinder/height", 4.*nanometer, AttLimits.lowerLimited(0.01))
    fit_suite.addFitParameter("*FormFactorCylinder/radius", 6.*nanometer, AttLimits.lowerLimited(0.01))
    fit_suite.addFitParameter("*FormFactorPrism3/height", 4.*nanometer, AttLimits.lowerLimited(0.01))
    fit_suite.addFitParameter("*FormFactorPrism3/length", 12.*nanometer, AttLimits.lowerLimited(0.01))

Lines 60–63 enter the list of fitting parameters. Here we use the cylinders’ height and radius and the prisms’ height and half-side length. The cylinder’s length and prism half-side are initially equal to 4nm, whereas the cylinder’s radius and the prism half-side length are equal to 6nm before the minimization. For each fit parameter the lower boundary is imposed to be equal to 0.01nm.

Running the fit and accessing the results

    fit_suite.runFit()

    print "Fitting completed."
    fit_suite.printResults()
    print "chi2:", fit_suite.getChi2()
    fitpars = fit_suite.getFitParameters()
    for i in range(0, fitpars.size()):
        print fitpars[i].getName(), fitpars[i].getValue(), fitpars[i].getError()

Line 66 shows the command to start the fitting process. During the fitting the progress will be displayed on the screen. Lines 69–73 show different ways of accessing the fit results.