A multilayer fitting model#

One of the main tools in easyreflectometry is the assemblies library. This allows the user to define their model, using specific parameters for their system of interest (if it is included in the assemblies library). These assemblies will impose necessary constraints and computational efficiencies based on the assembly that is used.

In this tutorial, we will look at one of these assemblies, that of a RepeatingMultilayer (documented here). This tutorial is based on an example from the BornAgain documentation looking at specular reflectivity analysis. Before performing analysis, we should import the packages that we need.

First configure matplotlib to place figures in notebook and import needed modules

[1]:
%matplotlib inline

import numpy as np
import scipp as sc
import pooch
import refl1d

import easyreflectometry

from easyreflectometry.data import load
from easyreflectometry.sample import Layer
from easyreflectometry.sample import Sample
from easyreflectometry.sample import Material
from easyreflectometry.sample import RepeatingMultilayer
from easyreflectometry.experiment import Model
from easyreflectometry.experiment import PercentageFhwm
from easyreflectometry.calculators import CalculatorFactory
from easyreflectometry.fitting import Fitter
from easyreflectometry.plot import plot
from easyscience.fitting import AvailableMinimizers

As mentioned in the previous tutorial, we share the version of the software packages we will use.

[2]:
print(f'numpy: {np.__version__}')
print(f'scipp: {sc.__version__}')
print(f'easyreflectometry: {easyreflectometry.__version__}')
print(f'Refl1D: {refl1d.__version__}')
numpy: 1.26.0
scipp: 24.02.0
easyreflectometry: 1.1.1
Refl1D: 0.8.16

Reading in experimental data#

The data that we will investigate in this tutorial was generated with GenX and is stored in an .ort format file. We use pooch to fetch the file from the repository.

[3]:
file_path = pooch.retrieve(
    # URL to one of Pooch's test files
    url="https://raw.githubusercontent.com/EasyScience/EasyReflectometryLib/master/docs/src/tutorials/fitting/repeating_layers.ort",
    known_hash="a5ffca9fd24f1d362266251723aec7ce9f34f123e39a38dfc4d829c758e6bf90",
)
data = load(file_path)
Downloading data from 'https://raw.githubusercontent.com/EasyScience/EasyReflectometryLib/master/docs/src/tutorials/fitting/repeating_layers.ort' to file '/home/runner/.cache/pooch/13c1608abd74b50f2c27c72af9126344-repeating_layers.ort'.

This data is very featureful, with many fringes present (arising from the multilayer structure)

[4]:
plot(data)
../../_images/tutorials_fitting_repeating_8_0.png

Building our model#

The system that was used to produce the data shown above is based on a silicon subphase, with a repeating multilayer of nickel and titanium grown upon it. Typcially, under experimental conditions, the producer of the sample will know how many repeats there will be of the multilayer system (as these are grown using some vapour disposition or sputtering method that the producer controls). We show the model that will be used graphically below.

A slab model description of the repeating multilayer system.

A slab model description of the repeating multilayer, showing the four layers of vacuum, titanium, nickel and silicon, with the titanium/nickel layers being repeated 10 times.

To construct such a layer structure, first we create each of the materials and associated layers

[5]:
vacuum = Material(sld=0, isld=0, name='Vacuum')
ti = Material(sld=-1.9493, isld=0, name='Ti')
ni = Material(sld=9.4245, isld=0, name='Ni')
si = Material(sld=2.0704, isld=0, name='Si')
[6]:
superphase = Layer(material=vacuum, thickness=0, roughness=0, name='Vacuum Superphase')
ti_layer = Layer(material=ti, thickness=40, roughness=0, name='Ti Layer')
ni_layer = Layer(material=ni, thickness=70, roughness=0, name='Ni Layer')
subphase = Layer(material=si, thickness=0, roughness=0, name='Si Subphase')

Then, to produce the repeating multilayer, we use the RepeatingMultilayer assembly type. This can be constructed in a range of different ways, however here we pass a list of Layer type objects and a number of repetitions.

[7]:
rep_multilayer = RepeatingMultilayer([ti_layer, ni_layer], repetitions=10, name='NiTi Multilayer')
rep_multilayer
[7]:
NiTi Multilayer:
  Ti Layer/Ni Layer:
  - Ti Layer:
      material:
        Ti:
          sld: -1.949e-6 1/Å^2
          isld: 0.000e-6 1/Å^2
      thickness: 40.000 Å
      roughness: 0.000 Å
  - Ni Layer:
      material:
        Ni:
          sld: 9.425e-6 1/Å^2
          isld: 0.000e-6 1/Å^2
      thickness: 70.000 Å
      roughness: 0.000 Å
  repetitions: 10.0

From these objects, we can construct our structure and combine this with a scaling, background and resolution (since this data is simulated there is no background or resolution smearing).

[8]:
resolution_function = PercentageFhwm(0)
sample = Sample(superphase, rep_multilayer, subphase, name='Multilayer Structure')
model = Model(
    sample=sample,
    scale=1,
    background=0,
    resolution_function=resolution_function,
    name='Multilayer Model'
)

In the analysis, we will only vary a single parameter, the thickness of titanium layer.

[9]:
ti_layer.thickness.bounds = (10, 60)

Choosing our calculation engine#

In the previous tutorial, we used the default refnx engine for our analysis. Here, we will change our engine to be Refl1D. This is achieved with the interface.switch('refl1d') method below.

[10]:
interface = CalculatorFactory()
interface.switch('refl1d')
model.interface = interface
print(interface.current_interface.name)
refl1d

Performing an optimisation#

The easyScience framework allows us to access a broad range of optimisation methods. Below, we have selected the differential evolution method from lmfit.

[11]:
fitter = Fitter(model)
fitter.switch_minimizer(AvailableMinimizers.LMFit_differential_evolution)
analysed = fitter.fit(data)
analysed
[11]:
  • data
    dict
    ()
    {'R_0': <scipp.Variable> (Qz_0: 400) float64 [dimensionless] [1, 1, ..., 8....
  • coords
    dict
    ()
    {'Qz_0': <scipp.Variable> (Qz_0: 400) float64 [1/Å] [0.000712093, ...
  • attrs
    dict
    ()
    {'R_0': {'orso_header': <scipp.Variable> () PyObject <no unit> {'data_...
  • R_0_model
    scipp
    Variable
    (Qz_0: 400)
    float64
    𝟙
    1.000, 1.000, ..., 9.474e-08, 4.953e-09
  • SLD_0
    scipp
    Variable
    (z_0: 10200)
    float64
    1/Å^2
    8.882e-22, 8.882e-22, ..., 2.070e-06, 2.070e-06

We can visualise the analysed model and SLD profile with the plot function.

[12]:
plot(analysed)
../../_images/tutorials_fitting_repeating_23_0.png

The value of the titanium layer thickness that gives this best fit can be found from the relavant object. Note that the uncertainty of 0 is due to the use of the lmfit differential evolution algorithm, which does not include uncertainty analysis.

[13]:
ti_layer.thickness
[13]:
<Parameter 'thickness': 29.9879 Å, bounds=[10.0:60.0]>

This result of a thickness of 30 Å is the same as that which is used to produce the data.