Spin asymmetry of a magnetic spinel film

This example simulates the polarized reflectivity of a magnesium aluminum ferrite layer (MAFO, from Mg-Al-Fe-O; MgAl${0.5}$Fe${1.5}$O$_4$) on a magnesium aluminate (MAO, MgAl$_2$O$_4$) substrate, and compares it with data measured at NIST (Magnetically Dead Layers in Spinel Films). The reduced model used here deliberately omits the magnetically dead layer of the original study.

The sample parameters are results of our own fit, described in the companion fit example.

Spin asymmetry

The spin asymmetry is defined in terms of the two non-spin-flip reflectivities as

$$S = \frac{R^{++} - R^{–}}{R^{++} + R^{–}}$$

It is computed from one simulation for each non-spin-flip channel. For the experimental data, assuming independent uncertainties $\Delta R^{\pm\pm}$ of the two channels, the propagated uncertainty is

$$\Delta S = \frac{2\sqrt{ \left(R^{–}\right)^2 \left( \Delta R^{++} \right)^2 + \left(R^{++}\right)^2 \left( \Delta R^{–}\right)^2 }}{ \left( R^{++} + R^{–}\right)^2 }$$

Both formulas are implemented in the functions spin_asymmetry and spin_asymmetry_error of the example script.

Instrument corrections

The fourth column of each data file supplies a pointwise Gaussian standard deviation of $q$, including the angular and wavelength contributions of the instrument. As in the source Refl1D model, the simulation reconstructs the instrument angular FWHM, adds the fitted sample_broadening linearly, and then converts the combined resolution back to a standard deviation of $q$. The fitted parameter q_offset accommodates experimental uncertainty in the measurement of $\theta$.

Simulation result

Reflectivities and spin asymmetry

The separation of $R^{++}$ and $R^{–}$ appears directly as the nonzero, oscillatory spin asymmetry.

Running the example

This example requires experimental data files from the BornAgain repository (testdata/specular/MAFO_Saturated_pp.tab and testdata/specular/MAFO_Saturated_mm.tab).

The environment variable BA_DATA_DIR must point to the testdata/ directory. From the build directory, run:

BA_DATA_DIR=../testdata python3 ../auto/Examples/specular/SpinAsymmetry.py

Here is the complete 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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["bornagain>=25,<26"]
# ///
"""
Spin asymmetry of a magnetic spinel film.

Simulates the polarized non-spin-flip reflectivities R++ and R-- of a
magnesium aluminum ferrite (MAFO, from Mg-Al-Fe-O; MgAl0.5Fe1.5O4) layer
on a magnesium aluminate (MAO, MgAl2O4) substrate, and compares them and
their spin asymmetry with data measured at NIST
(https://www.nist.gov/ncnr/magnetically-dead-layers-spinel-films).
The magnetically dead layer of the original study is omitted from this
reduced model. Sample parameters are results of our own fit, see the
companion example SpinAsymmetryFit.
"""

import os
import numpy as np
import bornagain as ba
ba.require_versions("bornagain>=25,<26")
from bornagain import nm, ba_io, ba_plot as bp, R3

datadir = ba_io.data_dir()
fname_stem = os.path.join(datadir, "specular/MAFO_Saturated_")

MAO_SLD = (5.377e-06, 0)  # SLD in Angstrom^-2
# Magnetic SLD in Angstrom^-2 per magnetization in A/m.
MAGNETIC_SLD_PER_MAGNETIZATION = 2.910429812376859e-12

def total_q_resolution(q_axis, dq_pointwise, sample_broadening):
    """
    Combines pointwise instrument resolution with sample broadening.

    The pointwise dQ is a standard deviation in 1/nm. The sample broadening is
    an angular FWHM in degrees and is added linearly to the reconstructed
    instrument angular FWHM, as in Refl1D. The wavelength and its resolution
    are fixed by the MAFO experiment.
    """
    wavelength = 0.475*nm
    wavelength_resolution = 0.003*nm
    fwhm_scale = np.sqrt(8*np.log(2))

    theta = np.arcsin(q_axis*wavelength/(4*np.pi))
    dq_spectral = q_axis*wavelength_resolution/wavelength
    dq_angular = np.sqrt(dq_pointwise**2 - dq_spectral**2)
    angular_fwhm = (
        dq_angular*wavelength*fwhm_scale/(4*np.pi*np.cos(theta)))
    angular_fwhm += np.deg2rad(sample_broadening)

    dq_angular_broadened = (
        (4*np.pi/wavelength)*np.cos(theta)*angular_fwhm/fwhm_scale)
    return np.hypot(dq_spectral, dq_angular_broadened)

def get_sample(parameters):
    """
    Magnesium aluminum ferrite (MAFO, from Mg-Al-Fe-O; MgAl0.5Fe1.5O4)
    layer on a magnesium aluminate (MAO, MgAl2O4) substrate.
    """
    magnetic_sld = parameters["mafo_magnetic_sld"]*1e-6
    magnetization = R3(0, magnetic_sld/MAGNETIC_SLD_PER_MAGNETIZATION, 0)

    vacuum = ba.Vacuum()
    film_color = (0.45, 0.32, 0.80)
    film_sld = parameters["mafo_sld"]*1e-6
    film_material = ba.SLDMaterial(
        "MgAl0.5Fe1.5O4", film_color, film_sld, 0, magnetization)
    substrate_color = (0.28, 0.57, 0.82)
    substrate_material = ba.SLDMaterial("MgAl2O4", substrate_color, *MAO_SLD)

    film_autocorr = ba.SelfAffineFractalModel(
        parameters["mafo_roughness"]*nm, 0.7, 25*nm)
    substrate_autocorr = ba.SelfAffineFractalModel(
        parameters["mao_roughness"]*nm, 0.7, 25*nm)

    transient = ba.TanhTransient()

    film_roughness = ba.Roughness(film_autocorr, transient)
    substrate_roughness = ba.Roughness(substrate_autocorr, transient)

    ambient_layer = ba.Layer(vacuum)
    film_layer = ba.Layer(
        film_material, parameters["mafo_thickness"]*nm, film_roughness)
    substrate_layer = ba.Layer(substrate_material, substrate_roughness)

    sample = ba.Sample()
    sample.addLayer(ambient_layer)
    sample.addLayer(film_layer)
    sample.addLayer(substrate_layer)

    return sample


def get_simulation(q_axis, q_resolution, parameters, spin_sign):
    """
    Polarized specular simulation of one non-spin-flip channel.
    q_axis and q_resolution contain one value per point, both in 1/nm.
    spin_sign is +1 for the ++ channel, -1 for the -- channel.
    """
    resolution_profile = ba.DistributionGaussian(0., 1., 25, 4.)

    scan = ba.QzScan(q_axis)
    scan.setOffset(parameters["q_offset"]/nm)
    q_resolution = total_q_resolution(
        q_axis, q_resolution, parameters["sample_broadening"])
    scan.setVectorResolution(resolution_profile, q_resolution)

    channel = R3(0, spin_sign, 0)
    scan.setPolarization(channel)
    scan.setAnalyzer(channel)

    return ba.SpecularSimulation(scan, get_sample(parameters))


def load_data(fname):
    """
    Reads dimensionless reflectivity, its uncertainty, and q resolution.
    Interprets q and its pointwise standard deviation as 1/nm.
    """
    q, reflectivity, uncertainty, q_resolution = ba_io.read_columns(
        fname, usecols=(0, 1, 2, 3))
    return q/nm, reflectivity, uncertainty, q_resolution/nm


def qz_datafield(q, values, errors=()):
    """
    Wraps dimensionless values and errors on a q_z axis given in 1/nm.
    """
    return ba.Datafield(ba.Frame(ba.ListScan("q_z (1/nm)", list(q))),
                        np.asarray(values, dtype=float).tolist(),
                        np.asarray(errors, dtype=float).tolist())


def spin_asymmetry(r_pp, r_mm):
    """
    Spin asymmetry S = (R++ - R--)/(R++ + R--); NaN where undefined.
    """
    denominator = r_pp + r_mm
    with np.errstate(divide='ignore', invalid='ignore'):
        return np.where(denominator != 0, (r_pp - r_mm)/denominator, np.nan)


def spin_asymmetry_error(r_pp, r_mm, sigma_pp, sigma_mm):
    """
    Uncertainty of the spin asymmetry, assuming independent channels.
    """
    denominator = (r_pp + r_mm)**2
    with np.errstate(divide='ignore', invalid='ignore'):
        return np.where(denominator != 0,
                        2*np.sqrt(r_mm**2*sigma_pp**2
                                  + r_pp**2*sigma_mm**2)/denominator,
                        np.nan)

if __name__ == '__main__':
    q_pp, r_pp, sigma_pp, q_res_pp = load_data(fname_stem + "pp.tab")
    q_mm, r_mm, sigma_mm, q_res_mm = load_data(fname_stem + "mm.tab")
    if not np.array_equal(q_pp, q_mm):
        raise ValueError(
            "Spin asymmetry requires both channels on the same q grid")
    qz_data = q_pp

    # sample parameters, fitted in the companion example SpinAsymmetryFit
    parameters = {
        'sample_broadening': 0.03686265,  # angular FWHM (deg)
        'q_offset': 8.8849e-05,  # q-axis shift (1/nm)
        'mafo_sld': 6.36867341,  # SLD of the film (1e-6 Å⁻²)
        'mafo_magnetic_sld': 0.27388649,  # magnetic SLD (1e-6 Å⁻²)
        'mafo_thickness': 13.7518675,  # (nm)
        'mao_roughness': 0.96885198,  # substrate roughness (nm)
        'mafo_roughness': 0.43123693,  # film roughness (nm)
    }

    ba.showSample3D(get_sample(parameters), sample_size=120*nm, seed=0)
    # Evaluate smooth model curves on a denser q grid than the measured data.
    qmin, qmax = 0.05997/nm, 1.96/nm
    scan_size = 1500
    qz_plot = np.linspace(qmin, qmax, scan_size)
    q_res_plot_pp = np.interp(qz_plot, q_pp, q_res_pp)
    q_res_plot_mm = np.interp(qz_plot, q_mm, q_res_mm)
    result_pp = get_simulation(
        qz_plot, q_res_plot_pp, parameters, +1).simulate()
    result_mm = get_simulation(
        qz_plot, q_res_plot_mm, parameters, -1).simulate()

    # measured and simulated spin asymmetry
    sa_data = spin_asymmetry(r_pp, r_mm)
    sa_data_error = spin_asymmetry_error(r_pp, r_mm, sigma_pp, sigma_mm)
    sa_simulated = spin_asymmetry(
        result_pp.intensities(), result_mm.intensities())

    fig, (ax_r, ax_sa) = bp.plt.subplots(1, 2, figsize=(12, 5))
    bp.plot_specular_curves(
        [("$R^{++}$", qz_datafield(qz_data, r_pp, sigma_pp), result_pp),
         ("$R^{--}$", qz_datafield(qz_data, r_mm, sigma_mm), result_mm)],
        ax=ax_r, ylabel="$R$")
    ax_r.legend()
    bp.plot_specular_curves(
        [(None, qz_datafield(qz_data, sa_data, sa_data_error),
          qz_datafield(qz_plot, sa_simulated))],
        ax=ax_sa, yscale='linear', ylim=(-0.3, 0.5),
        ylabel="Spin asymmetry")
    fig.tight_layout()

    bp.plt.show()
auto/Examples/specular/SpinAsymmetry.py