use crate::error::{OxiPhotonError, Result};
use std::f64::consts::PI;
fn beryllium_delta(energy_kev: f64) -> f64 {
let lambda_nm = 1.23984193 / energy_kev; 2.37e-6 * lambda_nm * lambda_nm
}
fn beryllium_beta(energy_kev: f64) -> f64 {
let lambda_nm = 1.23984193 / energy_kev;
3.8e-10 * lambda_nm * lambda_nm * lambda_nm
}
fn aluminum_delta(energy_kev: f64) -> f64 {
let lambda_nm = 1.23984193 / energy_kev;
7.6e-6 * lambda_nm * lambda_nm
}
fn aluminum_beta(energy_kev: f64) -> f64 {
let lambda_nm = 1.23984193 / energy_kev;
1.1e-8 * lambda_nm * lambda_nm * lambda_nm
}
#[derive(Debug, Clone)]
pub struct FresnelZonePlate {
pub wavelength: f64,
pub focal_length: f64,
pub n_zones: usize,
pub efficiency_order: i32,
pub outermost_zone_width: f64,
}
impl FresnelZonePlate {
pub fn new(wavelength: f64, focal_length: f64, n_zones: usize) -> Result<Self> {
if !wavelength.is_finite() || wavelength <= 0.0 {
return Err(OxiPhotonError::InvalidWavelength(wavelength));
}
if !focal_length.is_finite() || focal_length <= 0.0 {
return Err(OxiPhotonError::NumericalError(
"focal_length must be positive and finite".into(),
));
}
if n_zones == 0 {
return Err(OxiPhotonError::NumericalError(
"n_zones must be at least 1".into(),
));
}
let delta_r = (wavelength * focal_length / n_zones as f64).sqrt();
Ok(Self {
wavelength,
focal_length,
n_zones,
efficiency_order: 1,
outermost_zone_width: delta_r,
})
}
pub fn zone_radius(&self, n: usize) -> Option<f64> {
if n > self.n_zones {
return None;
}
let nl = n as f64 * self.wavelength;
Some((nl * self.focal_length + nl * nl / 4.0).sqrt())
}
pub fn outermost_zone_width(&self) -> f64 {
match self.zone_radius(self.n_zones) {
Some(r_n) => r_n / (2.0 * self.n_zones as f64),
None => self.outermost_zone_width,
}
}
pub fn diameter(&self) -> f64 {
self.zone_radius(self.n_zones)
.unwrap_or(2.0 * self.n_zones as f64 * self.outermost_zone_width)
* 2.0
}
pub fn numerical_aperture(&self) -> f64 {
self.wavelength / (2.0 * self.outermost_zone_width())
}
pub fn depth_of_focus(&self) -> f64 {
let dr = self.outermost_zone_width();
2.0 * dr * dr / self.wavelength
}
pub fn diffraction_efficiency(&self, order: i32) -> f64 {
if order == 0 {
return 0.25;
}
if order % 2 == 0 {
return 0.0;
}
1.0 / (order as f64 * PI).powi(2)
}
pub fn chromatic_aberration(&self, delta_lambda: f64) -> f64 {
self.focal_length * delta_lambda / self.wavelength
}
pub fn resolution(&self) -> f64 {
1.22 * self.outermost_zone_width()
}
pub fn phase_zone_plate_efficiency(&self, phase_shift: f64) -> f64 {
let s = (phase_shift / 2.0).sin();
(2.0 / PI).powi(2) * s * s
}
pub fn focal_length_order(&self, order: i32) -> Option<f64> {
if order == 0 {
return None;
}
Some(self.focal_length / order as f64)
}
pub fn first_order_throughput(&self) -> f64 {
self.diffraction_efficiency(1)
}
}
#[derive(Debug, Clone)]
pub struct CompoundRefractiveLens {
pub n_lenses: usize,
pub radius: f64,
pub delta: f64,
pub beta: f64,
pub material_thickness: f64,
}
impl CompoundRefractiveLens {
pub fn new_beryllium(n_lenses: usize, radius: f64, energy_kev: f64) -> Self {
let delta = beryllium_delta(energy_kev);
let beta = beryllium_beta(energy_kev);
let material_thickness = n_lenses as f64 * 3e-4;
Self {
n_lenses,
radius,
delta,
beta,
material_thickness,
}
}
pub fn new_aluminum(n_lenses: usize, radius: f64, energy_kev: f64) -> Self {
let delta = aluminum_delta(energy_kev);
let beta = aluminum_beta(energy_kev);
let material_thickness = n_lenses as f64 * 5e-4;
Self {
n_lenses,
radius,
delta,
beta,
material_thickness,
}
}
pub fn focal_length(&self) -> Option<f64> {
if self.delta == 0.0 || self.n_lenses == 0 {
return None;
}
Some(self.radius / (2.0 * self.n_lenses as f64 * self.delta))
}
pub fn transmission(&self, wavelength: f64) -> f64 {
if wavelength <= 0.0 {
return 0.0;
}
let mu = 4.0 * PI * self.beta / wavelength; (-mu * self.material_thickness).exp()
}
pub fn effective_aperture(&self, wavelength: f64) -> f64 {
if self.beta <= 0.0 || self.n_lenses == 0 || wavelength <= 0.0 {
return 0.0;
}
let denom = 4.0 * PI * self.beta * self.n_lenses as f64 / self.radius;
if denom <= 0.0 {
return 0.0;
}
(wavelength / denom).sqrt()
}
pub fn gain(&self, wavelength: f64) -> f64 {
let t = self.transmission(wavelength);
let r_eff = self.effective_aperture(wavelength);
match self.focal_length() {
Some(f) if f > 0.0 && wavelength > 0.0 => t * PI * r_eff * r_eff / (wavelength * f),
_ => 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct KbMirror {
pub mirror_length: f64,
pub grazing_angle_rad: f64,
pub focal_length_h: f64,
pub focal_length_v: f64,
pub surface_roughness_nm: f64,
}
impl KbMirror {
pub fn new(length: f64, angle_mrad: f64, f_h: f64, f_v: f64) -> Self {
Self {
mirror_length: length,
grazing_angle_rad: angle_mrad * 1e-3,
focal_length_h: f_h,
focal_length_v: f_v,
surface_roughness_nm: 0.3, }
}
pub fn critical_angle_rad(&self, delta: f64) -> f64 {
(2.0 * delta).sqrt()
}
pub fn reflectivity(&self, _wavelength: f64, delta: f64, beta: f64) -> f64 {
use num_complex::Complex64;
let theta = self.grazing_angle_rad;
let theta2 = theta * theta;
let under = Complex64::new(theta2 - 2.0 * delta, 2.0 * beta);
let sq = under.sqrt();
let num = Complex64::new(theta, 0.0) - sq;
let den = Complex64::new(theta, 0.0) + sq;
if den.norm() < 1e-30 {
return 1.0;
}
(num / den).norm_sqr()
}
pub fn scattering_loss(&self, wavelength: f64) -> f64 {
if wavelength <= 0.0 {
return 0.0;
}
let sigma = self.surface_roughness_nm * 1e-9;
let theta = self.grazing_angle_rad;
let exponent = (4.0 * PI * sigma * theta.sin() / wavelength).powi(2);
(-exponent).exp()
}
pub fn spot_size(&self, wavelength: f64) -> f64 {
let na = self.grazing_angle_rad.sin() * self.mirror_length / (2.0 * self.focal_length_v);
if na <= 0.0 || wavelength <= 0.0 {
return f64::INFINITY;
}
0.886 * wavelength / na
}
pub fn numerical_aperture(&self) -> f64 {
self.grazing_angle_rad.sin() * self.mirror_length / (2.0 * self.focal_length_v)
}
pub fn throughput(&self, wavelength: f64, delta: f64, beta: f64) -> f64 {
let r = self.reflectivity(wavelength, delta, beta);
let dw = self.scattering_loss(wavelength);
(r * dw).powi(2)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
const LAMBDA: f64 = 1e-9;
const FOCAL: f64 = 0.1; const N: usize = 1000;
#[test]
fn fzp_construction_valid() {
let fzp = FresnelZonePlate::new(LAMBDA, FOCAL, N).expect("valid FZP");
let expected = (LAMBDA * FOCAL / N as f64).sqrt();
assert_abs_diff_eq!(fzp.outermost_zone_width, expected, epsilon = 1e-15);
}
#[test]
fn fzp_construction_bad_wavelength() {
assert!(FresnelZonePlate::new(-1e-9, FOCAL, N).is_err());
assert!(FresnelZonePlate::new(0.0, FOCAL, N).is_err());
assert!(FresnelZonePlate::new(f64::NAN, FOCAL, N).is_err());
}
#[test]
fn fzp_zone_radius_paraxial() {
let fzp = FresnelZonePlate::new(LAMBDA, FOCAL, N).expect("valid FZP");
let r1 = fzp.zone_radius(1).expect("zone 1 exists");
let paraxial = (LAMBDA * FOCAL).sqrt();
assert_abs_diff_eq!(r1, paraxial, epsilon = paraxial * 1e-6);
}
#[test]
fn fzp_efficiency_binary_amplitude() {
let fzp = FresnelZonePlate::new(LAMBDA, FOCAL, N).expect("valid FZP");
let eta1 = fzp.diffraction_efficiency(1);
assert_abs_diff_eq!(eta1, 1.0 / PI.powi(2), epsilon = 1e-12);
assert_abs_diff_eq!(fzp.diffraction_efficiency(2), 0.0, epsilon = 1e-15);
assert_abs_diff_eq!(fzp.diffraction_efficiency(0), 0.25, epsilon = 1e-15);
}
#[test]
fn fzp_phase_efficiency_at_pi() {
let fzp = FresnelZonePlate::new(LAMBDA, FOCAL, N).expect("valid FZP");
let eta = fzp.phase_zone_plate_efficiency(PI);
assert_abs_diff_eq!(eta, (2.0 / PI).powi(2), epsilon = 1e-12);
}
#[test]
fn fzp_resolution_and_na() {
let fzp = FresnelZonePlate::new(LAMBDA, FOCAL, N).expect("valid FZP");
let na = fzp.numerical_aperture();
let res = fzp.resolution();
assert_abs_diff_eq!(res, 1.22 * LAMBDA / (2.0 * na), epsilon = 1e-15);
}
#[test]
fn crl_be_focal_length() {
let crl = CompoundRefractiveLens::new_beryllium(20, 1e-3, 10.0);
let f = crl.focal_length().expect("valid focal length");
let lambda_nm = 1.23984193 / 10.0;
let delta = 2.37e-6 * lambda_nm * lambda_nm;
let expected = 1e-3 / (2.0 * 20.0 * delta);
assert_abs_diff_eq!(f, expected, epsilon = 1.0);
}
#[test]
fn crl_transmission_monotone_in_beta() {
let crl_low_beta = CompoundRefractiveLens {
n_lenses: 10,
radius: 1e-3,
delta: 1e-6,
beta: 1e-10,
material_thickness: 1e-3,
};
let crl_high_beta = CompoundRefractiveLens {
n_lenses: 10,
radius: 1e-3,
delta: 1e-6,
beta: 1e-8,
material_thickness: 1e-3,
};
let lambda = 1e-10; assert!(crl_high_beta.transmission(lambda) < crl_low_beta.transmission(lambda));
}
#[test]
fn kb_critical_angle() {
let kb = KbMirror::new(0.1, 5.0, 0.2, 0.2);
let delta = 4.5e-5;
let tc = kb.critical_angle_rad(delta);
assert_abs_diff_eq!(tc, (2.0 * delta).sqrt(), epsilon = 1e-12);
}
#[test]
fn kb_reflectivity_below_critical_angle() {
let kb = KbMirror {
mirror_length: 0.1,
grazing_angle_rad: 1e-4, focal_length_h: 0.5,
focal_length_v: 0.5,
surface_roughness_nm: 0.0,
};
let delta = 1e-2; let beta = 1e-4;
let r = kb.reflectivity(1e-10, delta, beta);
assert!(r > 0.95, "expected near-total reflection, got R={r:.4}");
}
#[test]
fn kb_debye_waller_zero_roughness() {
let mut kb = KbMirror::new(0.1, 5.0, 0.2, 0.2);
kb.surface_roughness_nm = 0.0;
let loss = kb.scattering_loss(1e-10);
assert_abs_diff_eq!(loss, 1.0, epsilon = 1e-15);
}
#[test]
fn fzp_chromatic_aberration_sign() {
let fzp = FresnelZonePlate::new(LAMBDA, FOCAL, N).expect("valid FZP");
let da = fzp.chromatic_aberration(1e-12);
assert!(da > 0.0);
}
}