use num_complex::Complex64;
use std::f64::consts::PI;
use crate::material::DispersiveMaterial;
use crate::units::conversion::SPEED_OF_LIGHT;
use crate::units::{RefractiveIndex, Wavelength};
const EPSILON_0: f64 = 8.854_187_817e-12;
#[derive(Debug, Clone)]
pub struct PmlCellProfiles {
pub sigma: Vec<f64>,
pub kappa: Vec<f64>,
pub alpha: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct Pml {
pub thickness: f64,
pub polynomial_order: u32,
pub kappa_max: f64,
pub alpha_max: f64,
pub alpha_grading_order: u32,
pub sigma_max: Option<f64>,
pub host_eps_r: f64,
}
impl Pml {
pub fn new(thickness: f64, max_conductivity: f64) -> Self {
Self {
thickness,
polynomial_order: 3,
kappa_max: 1.0,
alpha_max: 0.0,
alpha_grading_order: 1,
sigma_max: Some(max_conductivity),
host_eps_r: 1.0,
}
}
pub fn new_optimal(thickness: f64, host_eps_r: f64) -> Self {
Self {
thickness,
polynomial_order: 3,
kappa_max: 15.0,
alpha_max: 0.05 * EPSILON_0,
alpha_grading_order: 1,
sigma_max: None,
host_eps_r,
}
}
}
impl Pml {
pub fn sigma_max_optimal(&self, dx: f64) -> f64 {
(self.polynomial_order as f64 + 1.0) / (150.0 * PI * self.host_eps_r.sqrt() * dx)
}
pub fn sigma_max_resolved(&self, dx: f64) -> f64 {
self.sigma_max.unwrap_or_else(|| self.sigma_max_optimal(dx))
}
pub fn sigma(&self, s_over_d: f64, sigma_max: f64) -> f64 {
sigma_max * s_over_d.powf(self.polynomial_order as f64)
}
pub fn kappa(&self, s_over_d: f64) -> f64 {
1.0 + (self.kappa_max - 1.0) * s_over_d.powf(self.polynomial_order as f64)
}
pub fn alpha(&self, s_over_d: f64) -> f64 {
self.alpha_max * (1.0 - s_over_d).powf(self.alpha_grading_order as f64)
}
pub fn cell_profiles(&self, n_cells: usize, dx: f64) -> PmlCellProfiles {
let sig_max = self.sigma_max_resolved(dx);
let n = n_cells as f64;
let sigma: Vec<f64> = (0..n_cells)
.map(|i| {
let s = (i as f64 + 0.5) / n;
self.sigma(s, sig_max)
})
.collect();
let kappa: Vec<f64> = (0..n_cells)
.map(|i| {
let s = (i as f64 + 0.5) / n;
self.kappa(s)
})
.collect();
let alpha: Vec<f64> = (0..n_cells)
.map(|i| {
let s = (i as f64 + 0.5) / n;
self.alpha(s)
})
.collect();
PmlCellProfiles {
sigma,
kappa,
alpha,
}
}
pub fn complex_eps_eff(&self, s_over_d: f64, omega: f64, dx: f64) -> Complex64 {
let sig = self.sigma(s_over_d, self.sigma_max_resolved(dx));
let kap = self.kappa(s_over_d);
let alp = self.alpha(s_over_d);
let denom = Complex64::new(alp, omega); let stretch = Complex64::new(kap, 0.0) + Complex64::new(sig / EPSILON_0, 0.0) / denom;
Complex64::new(self.host_eps_r, 0.0) * stretch
}
}
impl DispersiveMaterial for Pml {
fn refractive_index(&self, wavelength: Wavelength) -> RefractiveIndex {
const REF_DX: f64 = 1e-9; let omega = 2.0 * PI * SPEED_OF_LIGHT / wavelength.0;
let eps_eff = self.complex_eps_eff(0.5, omega, REF_DX);
let n_complex = eps_eff.sqrt();
let n_absorbing = if n_complex.im >= 0.0 {
n_complex
} else {
-n_complex
};
RefractiveIndex {
n: n_absorbing.re.abs(),
k: n_absorbing.im.abs(),
}
}
fn name(&self) -> &str {
"CFS-PML"
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn sigma_profile_is_polynomial() {
let pml = Pml::new_optimal(1e-6, 1.0);
let sig_max = pml.sigma_max_optimal(1e-8);
let s = 0.5_f64;
let expected = sig_max * s.powi(3);
assert_relative_eq!(pml.sigma(s, sig_max), expected, epsilon = 1e-10);
}
#[test]
fn kappa_profile_is_one_at_inner_face() {
let pml = Pml::new_optimal(1e-6, 1.0);
assert_relative_eq!(pml.kappa(0.0), 1.0, epsilon = 1e-12);
}
#[test]
fn kappa_profile_is_kappa_max_at_outer_face() {
let pml = Pml::new_optimal(1e-6, 1.0);
assert_relative_eq!(pml.kappa(1.0), pml.kappa_max, epsilon = 1e-12);
}
#[test]
fn alpha_profile_is_alpha_max_at_inner_face() {
let pml = Pml::new_optimal(1e-6, 1.0);
assert_relative_eq!(pml.alpha(0.0), pml.alpha_max, epsilon = 1e-30);
}
#[test]
fn alpha_profile_is_zero_at_outer_face() {
let pml = Pml::new_optimal(1e-6, 1.0);
assert!(pml.alpha(1.0).abs() < 1e-30);
}
#[test]
fn eps_eff_yields_absorbing_refractive_index() {
let pml = Pml::new_optimal(1e-6, 1.0);
let omega = 2.0 * PI * SPEED_OF_LIGHT / 1550e-9;
let eps = pml.complex_eps_eff(0.5, omega, 1e-8);
assert!(
eps.im.abs() > 0.0,
"PML ε_eff must have nonzero imaginary part (lossy medium)"
);
let n_sq = eps.sqrt();
assert!(
n_sq.im.abs() > 0.0,
"Im(n_eff) must be nonzero for PML to absorb; got {:.3e}",
n_sq.im
);
}
#[test]
fn optimal_sigma_formula() {
let pml = Pml::new_optimal(1e-6, 1.0);
let sig = pml.sigma_max_optimal(1e-8);
let expected = 4.0 / (150.0 * PI * 1e-8);
assert_relative_eq!(sig, expected, epsilon = 1e-6 * expected);
}
}