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
Zeeman-corrected PNR for Maranville et al. Pd/Ni-Fe/Si¶
This notebook models the high-field polarized neutron reflectometry example from Maranville et al., J. Appl. Cryst. 49, 1121-1129 (2016): Pd (200 A) / Ni-Fe (600 A) / Si in an applied field of 244 mT.
The goal is to show the same sample with the historical PNR calculation (zeeman_mode=False) and with the external-field Zeeman correction (zeeman_mode=True). The corrected mode adds the external field as a neutron magnetic SLD, rho_B = 2.31605e-6 * B[T] A^-2, and applies the spin-flip outgoing-flux correction.
The publication gives the layer geometry, the applied field, and the corrected-fit Ni-Fe magnetic SLD. It does not tabulate every fitted nuclear SLD and roughness value, so this notebook uses the reported Pd value of 4.1e-6 A^-2, the standard Si neutron SLD, and a nominal Ni-Fe nuclear SLD for the plotted simulation. Thicknesses are converted from Angstrom to nm, and SLD values use scale=1e-4 so the ESCAPE model is in 1/nm^2.
# PBR is a monochromatic neutron reflectometer. The paper plots the model
# versus kz(in), so the exact wavelength only enters the source object here.
src = esc.neutrons(4.75, units="angstrom")
# ESCAPE pnrspec uses qz = 2*kz. The paper discusses fits as a function of
# kz(in); this grid is therefore labelled as Qz = 2*kz(in) below.
qz_grid = np.linspace(0.01, 1.2, 800)
Qz = esc.var("qz")
# Nuclear SLDs are in 1e-6 A^-2 in the paper/literature. ESCAPE uses nm as
# the length unit in this notebook, so scale=1e-4 converts to 1/nm^2.
pd_sld = esc.par("Pd SLD", 4.1, scale=1e-4, units="1/nm^2")
nife_sld = esc.par("Ni-Fe SLD", 8.0, scale=1e-4, units="1/nm^2")
si_sld = esc.par("Si SLD", 2.07, scale=1e-4, units="1/nm^2")
zero_sld_im = esc.par("Absorption SLD Im", 0.0, scale=1e-4, units="1/nm^2")
# Maranville et al. report a corrected-fit in-plane Ni-Fe magnetic SLD of
# 1.12e-6 A^-2 and infer Mx = 385 kA/m. The hard-axis response gives
# Mz/Ms ~= Ha/Hk = 0.244/0.551, so the total magnetic SLD magnitude is set
# so that its x component is the reported 1.12e-6 A^-2.
applied_field_t = esc.par("Applied field", 0.244, units="T")
hard_axis_field_t = esc.par("Ni-Fe hard-axis saturation field", 0.551, units="T")
nife_mz_fraction = 0.244 / 0.551
nife_mx_fraction = np.sqrt(1.0 - nife_mz_fraction * nife_mz_fraction)
nife_sldm_total = esc.par(
"Ni-Fe total magnetic SLD",
1.12 / nife_mx_fraction,
scale=1e-4,
units="1/nm^2",
)
# Layer thicknesses from Fig. 2 of the publication: Pd(200 A)/Ni-Fe(600 A)/Si.
pd_thickness = esc.par("Pd thickness", 20.0, units="nm")
nife_thickness = esc.par("Ni-Fe thickness", 68.0, units="nm")
interface_roughness = esc.par("Interface roughness", 5.0, units="nm", userlim=[0, 10])
substrate_roughness = esc.par("Substrate roughness", 5.0, units="nm", userlim=[0, 10])
# ESCAPE magnetic_layer angles map to Bx = cos(phi)*sin(theta),
# By = sin(phi), Bz = cos(phi)*cos(theta). The film magnetization is kept
# mostly in plane by shape anisotropy, with a coherent hard-axis component.
nife_b_phi = esc.par("Ni-Fe B phi", 0.0, units="deg")
nife_b_theta = esc.par("Ni-Fe B theta", np.degrees(np.arccos(nife_mz_fraction)), units="deg")
# The applied field is slightly tilted with respect to the surface normal, as
# shown in Fig. 2. The exact tilt is not tabulated, so this keeps the same
# qualitative geometry: mostly out of plane with a small in-plane component.
field_tilt_from_surface_normal_deg = 10.0
field_tilt_rad = np.radians(field_tilt_from_surface_normal_deg)
external_field = [
0.244 * np.sin(field_tilt_rad),
0.0,
0.244 * np.cos(field_tilt_rad),
]
intensity_scale = esc.par("Scale", 1.0, userlim=[0.1, 2.0])
background = esc.par("Background", 1e-7, userlim=[0, 1e-4])
The stack below follows the paper from air/fronting medium to substrate: Pd cap, magnetic Ni-Fe layer, and single-crystal Si substrate.
pd = esc.generic_amorphous(pd_sld, zero_sld_im, name="Pd cap")
nife = esc.generic_amorphous(nife_sld, zero_sld_im, sldm=nife_sldm_total, name="Ni-Fe magnetic")
si = esc.generic_amorphous(si_sld, zero_sld_im, name="Si")
sample = esc.multilayer(
frgr=esc.air("Air / fronting"),
bkgr=esc.substrate(si, substrate_roughness, name="Si substrate"),
name="Maranville Pd/Ni-Fe/Si",
)
sample.add(esc.layer(pd, pd_thickness, interface_roughness, name="Pd cap"))
sample.add(
esc.magnetic_layer(
nife,
nife_b_phi,
nife_b_theta,
nife_thickness,
interface_roughness,
name="Ni-Fe magnetic layer",
)
)
sample.show(profile="sld0re", source=src).config(
title="Maranville Pd/Ni-Fe/Si nuclear SLD profile",
xlabel="Z [nm]",
ylabel="SLD [1/nm^2]",
)
ProfileLayout(children=(HTML(value='<h4>Maranville Pd/Ni-Fe/Si nuclear SLD profile</h4>', layout=Layout(grid_a…
The next cell creates the four polarized channels twice. The off curves preserve the historical behavior where external_field defines only the spin quantization axis. The all_layers curves include the external field as a Zeeman magnetic SLD throughout the fronting/sample/substrate region and apply the spin-flip outgoing-flux correction.
polarizer_efficiency = esc.par("Polarizer efficiency", 1.0, userlim=[0.8, 1.0])
analyzer_efficiency = esc.par("Analyzer efficiency", 1.0, 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])
Rpp_off = esc.pnrspec(
Qz, sample, external_field,
polarizer_efficiency * flipper_1_efficiency,
analyzer_efficiency * flipper_2_efficiency,
source=src, name="R++ no Zeeman", zeeman_mode=False,
)
Rmm_off = esc.pnrspec(
Qz, sample, external_field,
-polarizer_efficiency,
-analyzer_efficiency,
source=src, name="R-- no Zeeman", zeeman_mode=False,
)
Rpm_off = esc.pnrspec(
Qz, sample, external_field,
polarizer_efficiency * flipper_1_efficiency,
-analyzer_efficiency,
source=src, name="R+- no Zeeman", zeeman_mode=False,
)
Rmp_off = esc.pnrspec(
Qz, sample, external_field,
-polarizer_efficiency,
analyzer_efficiency * flipper_2_efficiency,
source=src, name="R-+ no Zeeman", zeeman_mode=False,
)
Rpp_zee = esc.pnrspec(
Qz, sample, external_field,
polarizer_efficiency * flipper_1_efficiency,
analyzer_efficiency * flipper_2_efficiency,
source=src, name="R++ Zeeman", zeeman_mode=True,
)
Rmm_zee = esc.pnrspec(
Qz, sample, external_field,
-polarizer_efficiency,
-analyzer_efficiency,
source=src, name="R-- Zeeman", zeeman_mode=True,
)
Rpm_zee = esc.pnrspec(
Qz, sample, external_field,
polarizer_efficiency * flipper_1_efficiency,
-analyzer_efficiency,
source=src, name="R+- Zeeman", zeeman_mode=True,
)
Rmp_zee = esc.pnrspec(
Qz, sample, external_field,
-polarizer_efficiency,
analyzer_efficiency * flipper_2_efficiency,
source=src, name="R-+ Zeeman", zeeman_mode=True,
)
Ipp_off_mat = intensity_scale * Rpp_off + background
Imm_off_mat = intensity_scale * Rmm_off + background
Ipm_off_mat = intensity_scale * Rpm_off + background
Imp_off_mat = intensity_scale * Rmp_off + background
Ipp_zee_mat = intensity_scale * Rpp_zee + background
Imm_zee_mat = intensity_scale * Rmm_zee + background
Ipm_zee_mat = intensity_scale * Rpm_zee + background
Imp_zee_mat = intensity_scale * Rmp_zee + background
Rpp_off = esc.pnrmaranville(
Qz, sample, external_field,
polarizer_efficiency * flipper_1_efficiency,
analyzer_efficiency * flipper_2_efficiency,
source=src, name="R++ no Zeeman", zeeman_mode=False,
)
Rmm_off = esc.pnrmaranville(
Qz, sample, external_field,
-polarizer_efficiency,
-analyzer_efficiency,
source=src, name="R-- no Zeeman", zeeman_mode=False,
)
Rpm_off = esc.pnrmaranville(
Qz, sample, external_field,
polarizer_efficiency * flipper_1_efficiency,
-analyzer_efficiency,
source=src, name="R+- no Zeeman", zeeman_mode=False,
)
Rmp_off = esc.pnrmaranville(
Qz, sample, external_field,
-polarizer_efficiency,
analyzer_efficiency * flipper_2_efficiency,
source=src, name="R-+ no Zeeman", zeeman_mode=False,
)
Rpp_zee = esc.pnrmaranville(
Qz, sample, external_field,
polarizer_efficiency * flipper_1_efficiency,
analyzer_efficiency * flipper_2_efficiency,
source=src, name="R++ Zeeman", zeeman_mode=True,
)
Rmm_zee = esc.pnrmaranville(
Qz, sample, external_field,
-polarizer_efficiency,
-analyzer_efficiency,
source=src, name="R-- Zeeman", zeeman_mode=True,
)
Rpm_zee = esc.pnrmaranville(
Qz, sample, external_field,
polarizer_efficiency * flipper_1_efficiency,
-analyzer_efficiency,
source=src, name="R+- Zeeman", zeeman_mode=True,
)
Rmp_zee = esc.pnrmaranville(
Qz, sample, external_field,
-polarizer_efficiency,
analyzer_efficiency * flipper_2_efficiency,
source=src, name="R-+ Zeeman", zeeman_mode=True,
)
Ipp_off = intensity_scale * Rpp_off + background
Imm_off = intensity_scale * Rmm_off + background
Ipm_off = intensity_scale * Rpm_off + background
Imp_off = intensity_scale * Rmp_off + background
Ipp_zee = intensity_scale * Rpp_zee + background
Imm_zee = intensity_scale * Rmm_zee + background
Ipm_zee = intensity_scale * Rpm_zee + background
Imp_zee = intensity_scale * Rmp_zee + background
wpp_off = esc.overlay(Ipp_off, Ipp_off_mat, coordinates=qz_grid).config(
labels=["R++ without Zeeman correction", "R++ without Zeeman correction matrix"],
ylog=True,
xlabel="Qz = 2 kz(in) [1/nm]",
ylabel="R++",
title="Maranville Pd/Ni-Fe/Si polarized neutron reflectometry"
)
wmm_off = esc.overlay(Imm_off, Imm_off_mat, coordinates=qz_grid).config(
labels=["R-- without Zeeman correction", "R-- without Zeeman correction matrix"],
ylog=True,
xlabel="Qz = 2 kz(in) [1/nm]",
ylabel="R--",
title="Maranville Pd/Ni-Fe/Si polarized neutron reflectometry"
)
wpp = esc.overlay(Ipp_zee, Ipp_zee_mat, coordinates=qz_grid).config(
labels=["R++ with Zeeman correction", "R++ with Zeeman correction matrix"],
ylog=True,
xlabel="Qz = 2 kz(in) [1/nm]",
ylabel="R++",
title="Maranville Pd/Ni-Fe/Si polarized neutron reflectometry"
)
wmm = esc.overlay(Imm_zee, Imm_zee_mat, coordinates=qz_grid).config(
labels=["R-- with Zeeman correction", "R-- with Zeeman correction matrix"],
ylog=True,
xlabel="Qz = 2 kz(in) [1/nm]",
ylabel="R--",
title="Maranville Pd/Ni-Fe/Si polarized neutron reflectometry"
)
display(wpp_off)
display(wmm_off)
display(wpp)
display(wmm)
OverlayLayout(children=(HTML(value='<h4>Maranville Pd/Ni-Fe/Si polarized neutron reflectometry</h4>', layout=L…
OverlayLayout(children=(HTML(value='<h4>Maranville Pd/Ni-Fe/Si polarized neutron reflectometry</h4>', layout=L…
OverlayLayout(children=(HTML(value='<h4>Maranville Pd/Ni-Fe/Si polarized neutron reflectometry</h4>', layout=L…
OverlayLayout(children=(HTML(value='<h4>Maranville Pd/Ni-Fe/Si polarized neutron reflectometry</h4>', layout=L…
wmp = esc.overlay(Imp_off, Imp_zee, coordinates=qz_grid).config(
labels=["R-+ without Zeeman correction", "R-+ with Zeeman correction"],
ylog=True,
xlabel="Qz = 2 kz(in) [1/nm]",
ylabel="R-+",
title="Maranville Pd/Ni-Fe/Si polarized neutron reflectometry"
)
wpm = esc.overlay(Ipm_off, Ipm_zee, coordinates=qz_grid).config(
labels=["R+- without Zeeman correction", "R+- with Zeeman correction"],
ylog=True,
xlabel="Qz = 2 kz(in) [1/nm]",
ylabel="R+-",
title="Maranville Pd/Ni-Fe/Si polarized neutron reflectometry"
)
display(wmp)
display(wpm)
OverlayLayout(children=(HTML(value='<h4>Maranville Pd/Ni-Fe/Si polarized neutron reflectometry</h4>', layout=L…
OverlayLayout(children=(HTML(value='<h4>Maranville Pd/Ni-Fe/Si polarized neutron reflectometry</h4>', layout=L…
Near the critical edge the corrected and uncorrected models separate most strongly, matching the behavior highlighted in the paper. The spin-flip channels are the most direct probe of the correction because a spin flip changes the neutron Zeeman energy and therefore the outgoing kz accepted by the detector.