Depth-probe simulation tutorial

Depth-probe simulation is an auxiliary simulation type, which helps to visualize the total intensity in dependence on the beam incidence angle and the position in the sample.

Here we will consider the intensity map produced by a neutron resonator, composed of one Ti/Pt bilayer.

Used conventions

Depth-probe simulation takes into account the position across the surface of the sample. This position will be denoted as “z” and measured in nanometers. The surface of the sample will be assigned to $z = 0$, while the direction of z-axis will be from the bulk of the sample out to the ambient. That is, $z < 0$ corresponds to the region from the substrate to the surface of the sample, and $z > 0$ corresponds to the ambient media.

Sample description

The layout of the sample is presented in the figure above. A Ti-Pt sample on a silicon substrate is placed in $D_2 O$ environment and is irradiated by a diagnostic beam. Note that in this layout the beam enters the sample from the substrate side. For this reason the sample has reversed structure, with silicon being defined as the “ambient media” and $D_2 O$ being defined as the “substrate”.

Before constructing the sample we will import bornagain library and units which will be used throughout our tutorial:

import bornagain as ba
from bornagain import deg, angstrom, nm

The process of constructing the sample is the same as the one described in
GISAS and reflectometry tutorials. First of all we will define the get_sample function:

def get_sample():
    """
    Constructs a sample with one resonating Ti/Pt bilayer
    """

Then we will create the materials of the sample, substrate and the ambient media:

    # define materials
    m_Si = ba.HomogeneousMaterial("Si", 3.3009e-05, 0.0)
    m_Ti = ba.HomogeneousMaterial("Ti", -3.0637e-05, 1.5278e-08)
    m_TiO2 = ba.HomogeneousMaterial("TiO2", 4.1921e-05, 8.1293e-09)
    m_Pt = ba.HomogeneousMaterial("Pt", 1.0117e-04, 3.01822e-08)
    m_D2O = ba.HomogeneousMaterial("D2O", 1.0116e-04, 1.8090e-12)

Note, that here we set the attenuation factor for silicon to zero. It is necessary, since this material will serve as the media of the beam propagation and is assumed to be semi-infinite. Thus setting a non-zero attenuation factor will lead to complete dissipation of the beam even before its entering the sample.

After the materials are defined, it is necessary to create the layers and the MultiLayer object representing the whole sample:

    # create layers
    l_Si = ba.Layer(m_Si)
    l_Ti = ba.Layer(m_Ti, 130.0 * angstrom)
    l_Pt = ba.Layer(m_Pt, 320.0 * angstrom)
    l_Ti_top = ba.Layer(m_Ti, 100.0 * angstrom)
    l_TiO2 = ba.Layer(m_TiO2, 30.0 * angstrom)
    l_D2O = ba.Layer(m_D2O)

    # construct sample from top to bottom
    sample = ba.MultiLayer()
    sample.addLayer(l_Si)

    sample.addLayer(l_Ti)
    sample.addLayer(l_Pt)

    sample.addLayer(l_Ti_top)
    sample.addLayer(l_TiO2)
    sample.addLayer(l_D2O)

    return sample

Setting up the depth-probe simulation

Depth-probe simulation can be created with the command DepthProbeSimulation(), while its parameters are set with setBeamParameters and setZSpan. The signature of setBeamParameters coincides completely with the signature of analogous method in SpecularSimulation:

<simulation>.setBeamParameters(wavelength, n_bins, angle_min, angle_max)

Here <simulation> is the simulation object created with DepthProbeSimulation command, wavelength is the wavelength of the incident beam (in nanometers by default), angle_min and angle_max — minimum and maximum incident angles (in radians by default). Units of the input arguments can be adjusted by multiplying factors deg, angstrom, etc. n_bins defines the number of points uniformly distributed from angle_min to angle_max.

setZSpan allows to define the coordinates across the sample surface, at which the signal intensities should be calculated. The signature of the command is

<simulation>.setZSpan(n_z_bins, z_min, z_max)

Again, <simulation> is the object created with DepthProbeSimulation command, z_min and z_max denote the limits of the z-axis (in nanometers by default), while n_z_bins defines the number of points uniformly distributed between z_min and z_max.

Let us write down get_simulation function, which creates a depth-probe simulation with incident angle range from $0^{\circ}$ to $1^{\circ}$ and 5000 points in between. The z-axis will be defined as the range $[-100, 100]$ nm with 500 points.

def get_simulation():
    """
    Returns a depth-probe simulation.
    """
    simulation = ba.DepthProbeSimulation()
    simulation.setBeamParameters(10 * angstrom, 5000, 0.0 * deg, 1.0 * deg)
    simulation.setZSpan(500, -100 * nm, 100 * nm)
    return simulation

Running the simulation and plotting the results

Running the simulation does not differ from doing it with any other type of simulation in BornAgain:

def run_simulation():
    """
    Runs simulation and returns its result.
    """
    sample = get_sample()
    simulation = get_simulation()
    simulation.setSample(sample)
    simulation.runSimulation()
    return simulation.result()

Plotting can be done with built-in BornAgain function plot_simulation_result:

if __name__ == '__main__':
    result = run_simulation()
    ba.plot_simulation_result(result)

As a result of executing the script, the following image should be displayed on the screen

In this figure the y-axis corresponds to the position across the sample surface (in nanometers), while the x-axis corresponds to the incident angle values $\alpha_i$.

Complete 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
115
"""
Basic example of depth-probe simulation with BornAgain.
Sample structure:
----------------------- inf

                        Si

----------------------- 0 nm
                        Ti
----------------------- -13 nm
                        Pt
----------------------- -45 nm
                        Ti
----------------------- -55 nm
                        TiO2
----------------------- -58 nm

                        D2O

----------------------- -inf
Beam comes from silicon side.
z axis is directed up and perpendicularly
to the sample, z = 0 corresponding to the sample
surface
"""
import bornagain as ba
from bornagain import deg, angstrom, nm

# layer thicknesses in angstroms
t_Ti = 130.0 * angstrom
t_Pt = 320.0 * angstrom
t_Ti_top = 100.0 * angstrom
t_TiO2 = 30.0 * angstrom

#  beam data
ai_min = 0.0 * deg  # minimum incident angle
ai_max = 1.0 * deg  # maximum incident angle
n_ai_bins = 5000    # number of bins in incident angle axis
beam_sample_ratio = 0.01  # beam-to-sample size ratio
wl = 10 * angstrom  # wavelength in angstroms

# convolution parameters
d_ang = 0.01 * ba.deg  # spread width for incident angle
n_points = 25  # number of points to convolve over
n_sig = 3  # number of sigmas to convolve over

#  depth position span
z_min = -100 * nm
z_max = 100 * nm
n_z_bins = 500


def get_sample():
    """
    Constructs a sample with one resonating Ti/Pt layer
    """

    # define materials
    m_Si = ba.HomogeneousMaterial("Si", 3.3009e-05, 0.0)
    m_Ti = ba.HomogeneousMaterial("Ti", -3.0637e-05, 1.5278e-08)
    m_TiO2 = ba.HomogeneousMaterial("TiO2", 4.1921e-05, 8.1293e-09)
    m_Pt = ba.HomogeneousMaterial("Pt", 1.0117e-04, 3.01822e-08)
    m_D2O = ba.HomogeneousMaterial("D2O", 1.0116e-04, 1.8090e-12)

    # create layers
    l_Si = ba.Layer(m_Si)
    l_Ti = ba.Layer(m_Ti, 130.0 * angstrom)
    l_Pt = ba.Layer(m_Pt, 320.0 * angstrom)
    l_Ti_top = ba.Layer(m_Ti, 100.0 * angstrom)
    l_TiO2 = ba.Layer(m_TiO2, 30.0 * angstrom)
    l_D2O = ba.Layer(m_D2O)

    # construct sample
    sample = ba.MultiLayer()
    sample.addLayer(l_Si)

    sample.addLayer(l_Ti)
    sample.addLayer(l_Pt)

    sample.addLayer(l_Ti_top)
    sample.addLayer(l_TiO2)
    sample.addLayer(l_D2O)

    return sample


def get_simulation():
    """
    Returns a depth-probe simulation.
    """
    alpha_distr = ba.DistributionGaussian(0.0, d_ang)
    footprint = ba.FootprintFactorSquare(beam_sample_ratio)
    simulation = ba.DepthProbeSimulation()
    simulation.setBeamParameters(wl, n_ai_bins, ai_min, ai_max, footprint)
    simulation.setZSpan(n_z_bins, z_min, z_max)
    simulation.addParameterDistribution("*/Beam/InclinationAngle", alpha_distr,
                                        n_points, n_sig)
    return simulation


def run_simulation():
    """
    Runs simulation and returns its result.
    """
    sample = get_sample()
    simulation = get_simulation()
    simulation.setSample(sample)
    simulation.runSimulation()
    return simulation.result()


if __name__ == '__main__':
    result = run_simulation()
    ba.plot_simulation_result(result, cmap='jet', aspect='auto')
DepthProbe.py