use crate::error::{OxiPhotonError, Result};
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct XrayMaterial {
pub name: String,
pub delta: f64,
pub beta: f64,
pub density: f64,
}
impl XrayMaterial {
pub fn new(name: impl Into<String>, delta: f64, beta: f64, density: f64) -> Self {
Self {
name: name.into(),
delta,
beta,
density,
}
}
pub fn molybdenum_13_5nm() -> Self {
Self::new("Mo@13.5nm", 0.077, 0.00607, 10.28)
}
pub fn silicon_13_5nm() -> Self {
Self::new("Si@13.5nm", 0.001, 1.71e-4, 2.33)
}
pub fn tungsten_0_1nm() -> Self {
Self::new("W@0.1nm", 4.8e-5, 3.6e-6, 19.3)
}
pub fn carbon_0_1nm() -> Self {
Self::new("C@0.1nm", 2.4e-6, 4.1e-10, 2.0)
}
pub fn ruthenium_13_5nm() -> Self {
Self::new("Ru@13.5nm", 0.0274, 0.00623, 12.4)
}
pub fn refractive_index(&self) -> (f64, f64) {
(1.0 - self.delta, self.beta)
}
pub fn absorption_length_m(&self, wavelength_m: f64) -> f64 {
if self.beta <= 0.0 || wavelength_m <= 0.0 {
return f64::INFINITY;
}
wavelength_m / (4.0 * PI * self.beta)
}
}
#[derive(Debug, Clone)]
pub struct MultilayerMirror {
pub bilayer_period: f64,
pub n_bilayers: usize,
pub gamma: f64,
pub layer_a: XrayMaterial,
pub layer_b: XrayMaterial,
pub wavelength: f64,
pub incidence_angle: f64,
}
impl MultilayerMirror {
pub fn new(
bilayer_period: f64,
n_bilayers: usize,
gamma: f64,
layer_a: XrayMaterial,
layer_b: XrayMaterial,
wavelength: f64,
incidence_angle: f64,
) -> Result<Self> {
if bilayer_period <= 0.0 || !bilayer_period.is_finite() {
return Err(OxiPhotonError::NumericalError(
"bilayer_period must be positive and finite".into(),
));
}
if n_bilayers == 0 {
return Err(OxiPhotonError::NumericalError(
"n_bilayers must be at least 1".into(),
));
}
if !(0.0..=1.0).contains(&gamma) {
return Err(OxiPhotonError::NumericalError(
"gamma must be in [0, 1]".into(),
));
}
if !wavelength.is_finite() || wavelength <= 0.0 {
return Err(OxiPhotonError::InvalidWavelength(wavelength));
}
Ok(Self {
bilayer_period,
n_bilayers,
gamma,
layer_a,
layer_b,
wavelength,
incidence_angle,
})
}
pub fn new_mo_si_euv(n_bilayers: usize) -> Self {
Self {
bilayer_period: 6.9e-9,
n_bilayers,
gamma: 0.4,
layer_a: XrayMaterial::molybdenum_13_5nm(),
layer_b: XrayMaterial::silicon_13_5nm(),
wavelength: 13.5e-9,
incidence_angle: 0.0, }
}
pub fn bragg_wavelength(&self, order: usize) -> f64 {
if order == 0 {
return f64::INFINITY;
}
2.0 * self.bilayer_period * self.incidence_angle.cos() / order as f64
}
fn bilayer_amplitude(&self) -> f64 {
let da = self.layer_a.delta;
let db = self.layer_b.delta;
let denom = 2.0 - da - db;
if denom.abs() < 1e-30 {
return 0.0;
}
(da - db).abs() / denom
}
pub fn peak_reflectivity(&self) -> f64 {
let r = self.bilayer_amplitude();
let x = self.n_bilayers as f64 * r;
x.tanh().powi(2)
}
pub fn reflectivity_at(&self, lambda: f64) -> f64 {
let lambda_b = self.bragg_wavelength(1);
let fwhm = self.bandwidth_fwhm();
if fwhm <= 0.0 {
if (lambda - lambda_b).abs() < 1e-30 {
return self.peak_reflectivity();
}
return 0.0;
}
let half = fwhm / 2.0;
let x = (lambda - lambda_b) / half;
self.peak_reflectivity() / (1.0 + x * x)
}
pub fn bandwidth_fwhm(&self) -> f64 {
let da = self.layer_a.delta;
let db = self.layer_b.delta;
let d_avg = self.gamma * da + (1.0 - self.gamma) * db;
if d_avg <= 0.0 {
return 0.0;
}
let relative = 2.0 * self.gamma * (1.0 - self.gamma) * (da - db).abs() / d_avg;
let lambda_b = self.bragg_wavelength(1);
relative * lambda_b
}
pub fn penetration_depth(&self) -> f64 {
let r = self.bilayer_amplitude();
if r <= 0.0 {
return f64::INFINITY;
}
let n_pen = 1.0 / (2.0 * r);
n_pen * self.bilayer_period
}
pub fn thermal_shift(&self, delta_temp: f64, thermal_expansion: f64) -> f64 {
self.bilayer_period * thermal_expansion * delta_temp
}
pub fn effective_bilayers(&self) -> f64 {
let r = self.bilayer_amplitude();
if r <= 0.0 {
return self.n_bilayers as f64;
}
let n_pen = 1.0 / (2.0 * r); (self.n_bilayers as f64).min(n_pen)
}
}
#[derive(Debug, Clone)]
pub struct EuvMirror {
pub multilayer: MultilayerMirror,
pub substrate: String,
pub capping_layer_nm: f64,
}
impl EuvMirror {
pub fn new_standard() -> Self {
Self {
multilayer: MultilayerMirror::new_mo_si_euv(40),
substrate: "ULE glass".to_string(),
capping_layer_nm: 2.5,
}
}
pub fn peak_reflectivity_with_capping(&self) -> f64 {
let lambda = self.multilayer.wavelength;
let d_cap = self.capping_layer_nm * 1e-9;
let ru = XrayMaterial::ruthenium_13_5nm();
let mu_cap = 4.0 * PI * ru.beta / lambda; let t_cap_sq = (-2.0 * mu_cap * d_cap).exp();
t_cap_sq * self.multilayer.peak_reflectivity()
}
pub fn n_mirror_transmission(&self, n_mirrors: usize) -> f64 {
self.peak_reflectivity_with_capping().powi(n_mirrors as i32)
}
pub fn six_mirror_scanner_transmission(&self) -> f64 {
self.n_mirror_transmission(6)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn mo_si_bragg_wavelength_normal_incidence() {
let mirror = MultilayerMirror::new_mo_si_euv(40);
let lambda_b = mirror.bragg_wavelength(1);
assert!(
(lambda_b - 13.8e-9).abs() < 1e-11,
"λ_B should be ~13.8 nm, got {:.3e} m",
lambda_b
);
}
#[test]
fn mo_si_peak_reflectivity_reasonable() {
let mirror = MultilayerMirror::new_mo_si_euv(40);
let r = mirror.peak_reflectivity();
assert!(r > 0.5, "peak reflectivity should exceed 0.5, got {r:.4}");
assert!(r <= 1.0, "reflectivity cannot exceed 1, got {r:.4}");
}
#[test]
fn multilayer_construction_bad_period() {
let mat = XrayMaterial::molybdenum_13_5nm();
let res = MultilayerMirror::new(-1e-9, 40, 0.4, mat.clone(), mat, 13.5e-9, 0.0);
assert!(res.is_err());
}
#[test]
fn multilayer_construction_bad_gamma() {
let mat = XrayMaterial::molybdenum_13_5nm();
let res = MultilayerMirror::new(6.9e-9, 40, 1.5, mat.clone(), mat, 13.5e-9, 0.0);
assert!(res.is_err());
}
#[test]
fn bandwidth_increases_with_contrast() {
let mo = XrayMaterial::molybdenum_13_5nm(); let si = XrayMaterial::silicon_13_5nm(); let mut high_contrast = MultilayerMirror::new_mo_si_euv(40);
high_contrast.layer_a = mo;
high_contrast.layer_b = si.clone();
let low_contrast = MultilayerMirror {
layer_a: XrayMaterial::new("Lo_A", 0.01, 1e-4, 5.0),
layer_b: XrayMaterial::new("Lo_B", 0.009, 1e-4, 5.0),
..MultilayerMirror::new_mo_si_euv(40)
};
assert!(
high_contrast.bandwidth_fwhm() > low_contrast.bandwidth_fwhm(),
"higher contrast should yield wider bandwidth"
);
}
#[test]
fn penetration_depth_finite_for_nonzero_contrast() {
let mirror = MultilayerMirror::new_mo_si_euv(40);
let depth = mirror.penetration_depth();
assert!(depth.is_finite() && depth > 0.0);
}
#[test]
fn euv_mirror_capping_reduces_reflectivity() {
let euv = EuvMirror::new_standard();
let r_with = euv.peak_reflectivity_with_capping();
let r_without = euv.multilayer.peak_reflectivity();
assert!(
r_with <= r_without,
"capping layer should reduce reflectivity: {r_with:.4} vs {r_without:.4}"
);
}
#[test]
fn n_mirror_transmission_monotone_decreasing() {
let euv = EuvMirror::new_standard();
let t6 = euv.n_mirror_transmission(6);
let t8 = euv.n_mirror_transmission(8);
assert!(t8 < t6, "more mirrors → less transmission");
}
#[test]
fn thermal_shift_proportional() {
let mirror = MultilayerMirror::new_mo_si_euv(40);
let alpha = 1e-6; let dt1 = 10.0;
let dt2 = 20.0;
let s1 = mirror.thermal_shift(dt1, alpha);
let s2 = mirror.thermal_shift(dt2, alpha);
assert_abs_diff_eq!(s2, 2.0 * s1, epsilon = 1e-25);
}
#[test]
fn material_absorption_length_silicon() {
let si = XrayMaterial::silicon_13_5nm();
let l_abs = si.absorption_length_m(13.5e-9);
let expected = 13.5e-9 / (4.0 * PI * 1.71e-4);
assert_abs_diff_eq!(l_abs, expected, epsilon = 1e-10);
}
}