In [1]:
from pathlib import Path

import numpy as np
import escape as esc

esc.require("0.9.8")
Loading material database from C:\dev\escape-core\python\src\escape\scattering\..\data\mdb\materials.db

PNR multilayer simulation¶

The stack is Fe20Co80 / (Al2O3 / Fe20Co80) x 10 / Al2O3 / Fe20Co80 on glass.

The neutron source has 4.52 A neutron wavelength and polarization device efficiencies of 0.94 for both polarizer and analyzer. The FWHM value in the source is a wavelength spread; for the plotted reflectivity curves below we use a simple Gaussian Qz resolution parameter, as in the NonColWaveGuide example.

In [2]:
src = esc.neutrons(4.52, units="angstrom")
qz_grid = np.linspace(0.02, 2.5, 800)

# SLD values come from the .ent file in 1e-6 A^-2.
feco_sld = esc.par("Fe20Co80 SLD", 4.26, scale=1e-4, units="1/nm^2")
feco_sld_im = esc.par("Fe20Co80 SLD Im", 3.3e-4, scale=1e-4, units="1/nm^2")
feco_sldm = esc.par("Fe20Co80 SLDM", 0.0, scale=1e-4, units="1/nm^2")

al2o3_sld = esc.par("Al2O3 SLD", 5.32, scale=1e-4, units="1/nm^2")
al2o3_sld_im = esc.par("Al2O3 SLD Im", 2.3e-4, scale=1e-4, units="1/nm^2")

glass_sld = esc.par("Glass SLD", 4.88, scale=1e-4, units="1/nm^2")
glass_sld_im = esc.par("Glass SLD Im", 9.77e-3, scale=1e-4, units="1/nm^2")

# Thicknesses and roughnesses are converted from Angstrom to nm.
feco_thickness = esc.par("Fe20Co80 thickness", 1.6, units="nm")
al2o3_thickness = esc.par("Al2O3 thickness", 3.66, units="nm")
interface_roughness = esc.par("Interface roughness", 0.0, userlim=[0, 1], units="nm")
substrate_roughness = esc.par("Substrate roughness", 0.0, userlim=[0, 1], units="nm")

# The .ent angles theta=90 deg and phi=90 deg put the magnetization along y.
feco_b_phi = esc.par("Fe20Co80 B phi", 90, units="deg")
feco_b_theta = esc.par("Fe20Co80 B theta", 90, units="deg")

intensity_scale = esc.par("Scale", 0.67, userlim=[0.1, 2.0])
background = esc.par("Background", 1e-6, userlim=[0, 1e-4])

Next we create two versions of the same stack. magnetic_sample contains the magnetic SLD and is used for PNR. structural_sample keeps the same nuclear structure but omits the magnetic term, which is useful for non-polarized specular reflectivity.

In [3]:
feco_magnetic = esc.generic_amorphous(
    feco_sld, feco_sld_im, sldm=feco_sldm, name="Fe20Co80 magnetic"
)
feco_structural = esc.generic_amorphous(
    feco_sld, feco_sld_im, name="Fe20Co80 structural"
)
al2o3 = esc.generic_amorphous(al2o3_sld, al2o3_sld_im, name="Al2O3")
glass = esc.generic_amorphous(glass_sld, glass_sld_im, name="Glass")

magnetic_sample = esc.multilayer(
    frgr=esc.air("Air"),
    bkgr=esc.substrate(glass, substrate_roughness, name="Substrate: glass"),
    name="FeCo/Al2O3 magnetic multilayer",
)
magnetic_sample.add(
    esc.magnetic_layer(
        feco_magnetic,
        feco_b_phi,
        feco_b_theta,
        feco_thickness,
        interface_roughness,
        name="Top Fe20Co80",
    )
)
for period in range(10):
    magnetic_sample.add(
        esc.layer(al2o3, al2o3_thickness, interface_roughness, name=f"Period {period + 1}: Al2O3")
    )
    magnetic_sample.add(
        esc.magnetic_layer(
            feco_magnetic,
            feco_b_phi,
            feco_b_theta,
            feco_thickness,
            interface_roughness,
            name=f"Period {period + 1}: Fe20Co80",
        )
    )
magnetic_sample.add(esc.layer(al2o3, al2o3_thickness, interface_roughness, name="Bottom Al2O3"))
magnetic_sample.add(
    esc.magnetic_layer(
        feco_magnetic,
        feco_b_phi,
        feco_b_theta,
        feco_thickness,
        interface_roughness,
        name="Bottom Fe20Co80",
    )
)

structural_sample = esc.multilayer(
    frgr=esc.air("Air"),
    bkgr=esc.substrate(glass, substrate_roughness, name="Substrate: glass"),
    name="FeCo/Al2O3 structural multilayer",
)
structural_sample.add(
    esc.layer(feco_structural, feco_thickness, interface_roughness, name="Top Fe20Co80")
)
for period in range(10):
    structural_sample.add(
        esc.layer(al2o3, al2o3_thickness, interface_roughness, name=f"Period {period + 1}: Al2O3")
    )
    structural_sample.add(
        esc.layer(
            feco_structural,
            feco_thickness,
            interface_roughness,
            name=f"Period {period + 1}: Fe20Co80",
        )
    )
structural_sample.add(esc.layer(al2o3, al2o3_thickness, interface_roughness, name="Bottom Al2O3"))
structural_sample.add(
    esc.layer(feco_structural, feco_thickness, interface_roughness, name="Bottom Fe20Co80")
)

magnetic_sample.show(profile="sld0re", source=src).config(
    title="Nuclear SLD profile",
    xlabel="Z [nm]",
    ylabel="SLD [1/nm^2]",
)
Out[3]:
ProfileLayout(children=(HTML(value='<h4>Nuclear SLD profile</h4>', layout=Layout(grid_area='header')), HTML(va…

For polarized neutron reflectivity we calculate the four analyzer/flipper channels. The sign of the polarizer and analyzer efficiencies defines the incoming and outgoing spin state, following the same convention as the NonColWaveGuide notebook.

In [4]:
Qz = esc.var("qz")
q_resolution_fwhm = esc.par("Q resolution FWHM", 0.02, units="1/nm", userlim=[0.001, 0.1])

polarizer_efficiency = esc.par("Polarizer efficiency", 0.94, userlim=[0.8, 1.0])
analyzer_efficiency = esc.par("Analyzer efficiency", 0.94, userlim=[0.8, 1.0])
flipper_1_efficiency = esc.par("Flipper 1 efficiency", 1.0, userlim=[0.8, 1.0])
flipper_2_efficiency = esc.par("Flipper 2 efficiency", 1.0, userlim=[0.8, 1.0])

external_field = [0, 1, 0]

Rmm = esc.pnrspec(
    Qz,
    magnetic_sample,
    external_field,
    -polarizer_efficiency,
    -analyzer_efficiency,
    source=src,
    name="R--",
)
Rpm = esc.pnrspec(
    Qz,
    magnetic_sample,
    external_field,
    polarizer_efficiency * flipper_1_efficiency,
    -analyzer_efficiency,
    source=src,
    name="R+-",
)
Rmp = esc.pnrspec(
    Qz,
    magnetic_sample,
    external_field,
    -polarizer_efficiency,
    analyzer_efficiency * flipper_2_efficiency,
    source=src,
    name="R-+",
)
Rpp = esc.pnrspec(
    Qz,
    magnetic_sample,
    external_field,
    polarizer_efficiency * flipper_1_efficiency,
    analyzer_efficiency * flipper_2_efficiency,
    source=src,
    name="R++",
)

numpoints = 15
Rpp = esc.average_normal(Rpp, q_resolution_fwhm, Qz, numpoints=numpoints)
Rmm = esc.average_normal(Rmm, q_resolution_fwhm, Qz, numpoints=numpoints)
Rpm = esc.average_normal(Rpm, q_resolution_fwhm, Qz, numpoints=numpoints)
Rmp = esc.average_normal(Rmp, q_resolution_fwhm, Qz, numpoints=numpoints)

Ipp = intensity_scale * Rpp + background
Imm = intensity_scale * Rmm + background
Ipm = intensity_scale * Rpm + background
Imp = intensity_scale * Rmp + background
In [ ]:
w_pp = Ipp.show(coordinates=qz_grid).config(
    title="Polarized reflectivity R++",
    xlabel="Q [1/nm]",
    ylabel="R++",
    ylog=True,
)
w_mm = Imm.show(coordinates=qz_grid).config(
    title="Polarized reflectivity R--",
    xlabel="Q [1/nm]",
    ylabel="R--",
    ylog=True,
)
w_pm = Ipm.show(coordinates=qz_grid).config(
    title="Polarized reflectivity R+-",
    xlabel="Q [1/nm]",
    ylabel="R+-",
    ylog=True,
)
w_mp = Imp.show(coordinates=qz_grid).config(
    title="Polarized reflectivity R-+",
    xlabel="Q [1/nm]",
    ylabel="R-+",
    ylog=True,
)

esc.show(w_pp, w_mm, w_pm, w_mp)

Ipol is the spin-summed intensity: 0.5 * scale * (Rpp + Rmm + Rpm + Rmp) + background — one background after averaging scaled R, same convention as I_nonpolarized. I_nonpolarized uses nuclear-only Parratt on structural_sample (no sldm). Strong magnetism or different stacks still makes them differ; with sldm=0 and matched nuclear geometry they should agree where R half-trace matches R_np.

In [ ]:
Ipol = 0.5 * intensity_scale * (Rpp + Rmm + Rpm + Rmp) + background


R_nonpolarized = esc.specrefl(
    Qz,
    structural_sample,
    "parratt",
    source=src,
    name="Non-polarized specular reflectivity",
)
R_nonpolarized = esc.average_normal(
    R_nonpolarized,
    q_resolution_fwhm,
    Qz,
    numpoints=numpoints,
)
I_nonpolarized = intensity_scale * R_nonpolarized + background

esc.overlay(Ipol, I_nonpolarized, coordinates=qz_grid).config(
    title="PNR spin average (magnetic) vs nuclear-only Parratt",
    xlabel="Q [1/nm]",
    ylabel="R",
    ylog=True,
)

If experimental channel files become available, replace the direct show(coordinates=...) calls with esc.data(...) and esc.model(...) objects as in the NonColWaveGuide notebook. The scale, background, polarizer, analyzer, and flipper parameters are already separated so they can be reused in a fit.