Fitting a simple slab model#

In order to show one of the simplest analyses that EasyReflectometry can perform, we will use the great example from the refnx documentation. This involves the analysis of a single neutron reflectometry dataset from a hydrated polymer film system. Before we start on any analysis, we will import the necessary packages and functions.

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

[1]:
%matplotlib inline

import EasyReflectometry
import refnx
from EasyReflectometry.data import load
from EasyReflectometry.sample import Layer
from EasyReflectometry.sample import Sample
from EasyReflectometry.sample import Material
from EasyReflectometry.sample import Multilayer
from EasyReflectometry.experiment.model import Model
from EasyReflectometry.calculators import CalculatorFactory
from EasyReflectometry.fitting import Fitter
from EasyReflectometry.plot import plot

One of benefits of using a Jupyter Notebook for our analysis is improved reproducibility, to ensure this, below we share the version of the software packages being used.

[2]:
print(f'EasyReflectometry: {EasyReflectometry.__version__}')
print(f'refnx: {refnx.__version__}')
EasyReflectometry: 0.0.0
refnx: 0.1.43

Reading in experimental data#

EasyReflectometry has support for the .ort file format, a standard file format for reduced reflectivity data developed by the Open Reflectometry Standards Organisation. To load in a dataset, we use the load function.

[3]:
data = load('_static/example.ort')

The function about will load the file into a scipp Dataset object. This offers some nice visualisations of the data, including the HTML view.

[4]:
data
[4]:
  • data
    dict
    ()
    {'R_0': <scipp.Variable> (Qz_0: 408) float64 [dimensionless] [0.709581, 0.8...
  • coords
    dict
    ()
    {'Qz_0': <scipp.Variable> (Qz_0: 408) float64 [1/Å] [0.00806022, 0...
  • attrs
    dict
    ()
    {'R_0': {'orso_header': <scipp.Variable> () PyObject <no unit> {'data_...

EasyReflectometry also includes a custom plotting function for the data.

[5]:
plot(data)
../_images/tutorials_simple_fitting_10_0.png

Building our model#

Now that we have read in the experimental data that we want to analyse, it is necessary that we construct some model that describes what we think the system looks like. The construction of this models is discussed in detail in the model-dependent analysis and reflectometry slab models sections of the ISIS Virtual Reflectometry Training Course on neutron reflectometry fitting.

The system that we are investigating consists of four layers (with the top and bottom as semi-finite super- and sub-phases). The super-phase (where the neutrons are incident first) is a silicon (Si) wafer and as a process of the sample preparation there is anticipated to by a layer of silicon dioxide (SiO2) on this material. Then a polymer film has been attached to the silicon dioxide by some chemical method and this polymer film is solvated in a heavy water (D2O) which also makes up the sub-phase of the system. This is shown pictorially below, as a slab model.

A slab model description of the polymer film system.

A slab model description of the polymer film system (note that the layers are not to scale), showing the four layers of silicon, silicon dioxide, the polymer film and the heavy water subphase.

In order to constuct this model in EasyReflecotmetry, first we must construct objects for each of the materials that will compose the layers. These objects should be of type Material, when constructed from_pars the arguments are the real and imaginary components of the scattering length density (in units of 10-6Å-2) and some name for the material.

[6]:
si = Material.from_pars(2.07, 0, 'Si')
sio2 = Material.from_pars(3.47, 0, 'SiO2')
film = Material.from_pars(2.0, 0, 'Film')
d2o = Material.from_pars(6.36, 0, 'D2O')

We can investigate the properties of one of these objects as follows.

[7]:
film
[7]:
Film:
  sld: 2.000e-6 1 / angstrom ** 2
  isld: 0.000e-6 1 / angstrom ** 2

Next we will produce layers from each of these materials, of type Layer. The from_pars constructor for these take the material, a thickness and a interfacial roughness (on the top of the layer). The thickness and roughness values are both in Å.

[8]:
si_layer = Layer.from_pars(si, 0, 0, 'Si layer')
sio2_layer = Layer.from_pars(sio2, 30, 3, 'SiO2 layer')
film_layer = Layer.from_pars(film, 250, 3, 'Film Layer')
subphase = Layer.from_pars(d2o, 0, 3, 'D2O Subphase')

Again, we can probe the properties of the layer as such.

[9]:
film_layer
[9]:
Film Layer:
  material:
    Film:
      sld: 2.000e-6 1 / angstrom ** 2
      isld: 0.000e-6 1 / angstrom ** 2
  thickness: 250.000 angstrom
  roughness: 3.000 angstrom

Given that the silicon and silicon dioxide layer both compose the solid subphase, it can be helpful to combine these as a Multilayer assembly type in our code.

[10]:
superphase = Multilayer.from_pars([si_layer, sio2_layer], name='Si/SiO2 Superphase')

These objects are then combined as a Sample, where the constructor takes a series of layers (or some more complex EasyReflectometry assemblies) and, optionally, some name for the sample.

[11]:
sample = Sample.from_pars(superphase, film_layer, subphase, name='Film Structure')

This sample can be investigated from the string representation like the other objects.

[12]:
sample
[12]:
Film Structure:
- Si/SiO2 Superphase:
    Si layer/SiO2 layer:
    - Si layer:
        material:
          Si:
            sld: 2.070e-6 1 / angstrom ** 2
            isld: 0.000e-6 1 / angstrom ** 2
        thickness: 0.000 angstrom
        roughness: 0.000 angstrom
    - SiO2 layer:
        material:
          SiO2:
            sld: 3.470e-6 1 / angstrom ** 2
            isld: 0.000e-6 1 / angstrom ** 2
        thickness: 30.000 angstrom
        roughness: 3.000 angstrom
- Film Layer:
    material:
      Film:
        sld: 2.000e-6 1 / angstrom ** 2
        isld: 0.000e-6 1 / angstrom ** 2
    thickness: 250.000 angstrom
    roughness: 3.000 angstrom
- D2O Subphase:
    material:
      D2O:
        sld: 6.360e-6 1 / angstrom ** 2
        isld: 0.000e-6 1 / angstrom ** 2
    thickness: 0.000 angstrom
    roughness: 3.000 angstrom

Constructing the model#

The structure of the system under investigation is just part of the analysis story. It is also necessary to describe the instrumental parameters, namely the background level, the resolution and some option to scale the data in the y-axis.

Note

Currently, only constant with resolution is supported. We are working to include more complex resolution in future.

the Model constructor takes our smple, a scale factor, a uniform background level and a resolution width.

[13]:
model = Model.from_pars(sample, 1, 1e-6, 0.02, 'Film Model')

From this object, we can investigate all of the parameters of our model.

[14]:
model
[14]:
Film Model:
  scale: 1.0
  background: 1.0e-06
  resolution: 0.02 %
  sample:
    Film Structure:
    - Si/SiO2 Superphase:
        Si layer/SiO2 layer:
        - Si layer:
            material:
              Si:
                sld: 2.070e-6 1 / angstrom ** 2
                isld: 0.000e-6 1 / angstrom ** 2
            thickness: 0.000 angstrom
            roughness: 0.000 angstrom
        - SiO2 layer:
            material:
              SiO2:
                sld: 3.470e-6 1 / angstrom ** 2
                isld: 0.000e-6 1 / angstrom ** 2
            thickness: 30.000 angstrom
            roughness: 3.000 angstrom
    - Film Layer:
        material:
          Film:
            sld: 2.000e-6 1 / angstrom ** 2
            isld: 0.000e-6 1 / angstrom ** 2
        thickness: 250.000 angstrom
        roughness: 3.000 angstrom
    - D2O Subphase:
        material:
          D2O:
            sld: 6.360e-6 1 / angstrom ** 2
            isld: 0.000e-6 1 / angstrom ** 2
        thickness: 0.000 angstrom
        roughness: 3.000 angstrom

Setting varying parameters#

Now that the model is fully constructed, we can select the parameters in our model that should be varied. Below we set the thickness of the SiO2 and film layers to vary along with the real scattering length density of the film and all of the roughnesses.

[15]:
# Thicknesses
sio2_layer.thickness.bounds = (15, 50)
film_layer.thickness.bounds = (200, 300)
# Roughnesses
sio2_layer.roughness.bounds = (1, 15)
film_layer.roughness.bounds = (1, 15)
subphase.roughness.bounds = (1, 15)
# Scattering length density
film_layer.material.sld.bounds = (0.1, 3)

In addition to these variables of the structure, we will also vary the background level and scale factor.

[16]:
# Background
model.background.bounds = (1e-8, 1e-5)
# Scale
model.scale.bounds = (0.5, 1.5)

Choosing our calculation engine#

The EasyReflectometry package enables the calculation of the reflectometry profile using either refnx or Refl1D. For this tutorial, we will stick to the current default, which is refnx. The calculator must be created and associated with the model that we are to fit.

[17]:
interface = CalculatorFactory()
model.interface = interface

We can check the calculation engine currently in use as follows.

[18]:
print(interface.current_interface.name)
refnx

Performing an optimisation#

The optimisation of our model is achieved with a Fitter, which takes our model and calculator.

[19]:
fitter = Fitter(model)

To actually perform the optimisation, we must pass our data object created from the experimental data. This will return a new sc.Dataset with the result of out analysis, and the model will be updated in place.

[20]:
analysed = fitter.fit(data)
[21]:
analysed
[21]:
  • data
    dict
    ()
    {'R_0': <scipp.Variable> (Qz_0: 408) float64 [dimensionless] [0.709581, 0.8...
  • coords
    dict
    ()
    {'Qz_0': <scipp.Variable> (Qz_0: 408) float64 [1/Å] [0.00806022, 0...
  • attrs
    dict
    ()
    {'R_0': {'orso_header': <scipp.Variable> () PyObject <no unit> {'data_...
  • R_0_model
    scipp
    Variable
    (Qz_0: 408)
    float64
    𝟙
    0.874, 0.874, ..., 3.925e-07, 3.922e-07
  • SLD_0
    scipp
    Variable
    (z_0: 500)
    float64
    1/Å^2
    2.070e-06, 2.070e-06, ..., 6.360e-06, 6.360e-06

The same plot function that was used on the raw data can be used for this analysed object and will show the best fit simulated data and the associated scattering length density profile.

[22]:
plot(analysed)
../_images/tutorials_simple_fitting_43_0.png

Finally, from the string representation of the parameters we can obtain information about the optimised values.

[23]:
model
[23]:
Film Model:
  scale: 0.8739516983234926
  background: 3.8894921855381854e-07
  resolution: 0.02 %
  sample:
    Film Structure:
    - Si/SiO2 Superphase:
        Si layer/SiO2 layer:
        - Si layer:
            material:
              Si:
                sld: 2.070e-6 1 / angstrom ** 2
                isld: 0.000e-6 1 / angstrom ** 2
            thickness: 0.000 angstrom
            roughness: 0.000 angstrom
        - SiO2 layer:
            material:
              SiO2:
                sld: 3.470e-6 1 / angstrom ** 2
                isld: 0.000e-6 1 / angstrom ** 2
            thickness: 38.727 angstrom
            roughness: 8.076 angstrom
    - Film Layer:
        material:
          Film:
            sld: 2.360e-6 1 / angstrom ** 2
            isld: 0.000e-6 1 / angstrom ** 2
        thickness: 258.510 angstrom
        roughness: 10.343 angstrom
    - D2O Subphase:
        material:
          D2O:
            sld: 6.360e-6 1 / angstrom ** 2
            isld: 0.000e-6 1 / angstrom ** 2
        thickness: 0.000 angstrom
        roughness: 3.511 angstrom

We note here that the results obtained are very similar to those from the refnx tutorial, which is hardly surprising given that we have used the refnx engine in this example.