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.
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.
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]",
)
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.
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
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.
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.