use num_complex::Complex64;
use std::f64::consts::PI;
use crate::error::OxiPhotonError;
#[derive(Debug, Clone)]
pub struct MaxwellGarnett {
pub eps_host: Complex64,
pub eps_inclusion: Complex64,
pub fill_fraction: f64,
}
impl MaxwellGarnett {
pub fn new(
eps_host: Complex64,
eps_inclusion: Complex64,
fill_fraction: f64,
) -> Result<Self, OxiPhotonError> {
if !(0.0..=1.0).contains(&fill_fraction) {
return Err(OxiPhotonError::NumericalError(format!(
"fill_fraction must be in [0, 1], got {fill_fraction}"
)));
}
Ok(Self {
eps_host,
eps_inclusion,
fill_fraction,
})
}
pub fn effective_permittivity(&self) -> Complex64 {
let f = self.fill_fraction;
let eh = self.eps_host;
let ei = self.eps_inclusion;
let delta = ei - eh;
let numerator = Complex64::new(3.0 * f, 0.0) * delta;
let denominator = ei + Complex64::new(2.0, 0.0) * eh - Complex64::new(f, 0.0) * delta;
eh * (Complex64::new(1.0, 0.0) + numerator / denominator)
}
pub fn effective_index(&self) -> Complex64 {
let eps_eff = self.effective_permittivity();
let root = eps_eff.sqrt();
if root.re >= 0.0 {
root
} else {
-root
}
}
pub fn layered_parallel(&self) -> Complex64 {
let f = self.fill_fraction;
Complex64::new(f, 0.0) * self.eps_inclusion + Complex64::new(1.0 - f, 0.0) * self.eps_host
}
pub fn layered_perpendicular(&self) -> Complex64 {
let f = self.fill_fraction;
let inv = Complex64::new(f, 0.0) / self.eps_inclusion
+ Complex64::new(1.0 - f, 0.0) / self.eps_host;
Complex64::new(1.0, 0.0) / inv
}
pub fn cylindrical_effective_permittivity(&self) -> Complex64 {
let f = self.fill_fraction;
let eh = self.eps_host;
let ei = self.eps_inclusion;
let delta = ei - eh;
let numerator = Complex64::new(2.0 * f, 0.0) * delta;
let denominator = ei + eh - Complex64::new(f, 0.0) * delta;
eh * (Complex64::new(1.0, 0.0) + numerator / denominator)
}
pub fn sweep_fill_fraction(&self, n_points: usize) -> Vec<(f64, Complex64)> {
(0..n_points)
.map(|i| {
let f = i as f64 / (n_points.saturating_sub(1).max(1)) as f64;
let mg = MaxwellGarnett {
eps_host: self.eps_host,
eps_inclusion: self.eps_inclusion,
fill_fraction: f,
};
(f, mg.effective_permittivity())
})
.collect()
}
pub fn fill_fraction_for_target_eps(&self, target_eps_real: f64) -> Option<f64> {
let eps_at_f = |f: f64| -> f64 {
let mg = MaxwellGarnett {
eps_host: self.eps_host,
eps_inclusion: self.eps_inclusion,
fill_fraction: f,
};
mg.effective_permittivity().re
};
let lo_val = eps_at_f(0.0);
let hi_val = eps_at_f(1.0);
let (low_f, high_f) = if lo_val <= hi_val {
if target_eps_real < lo_val || target_eps_real > hi_val {
return None;
}
(0.0_f64, 1.0_f64)
} else {
if target_eps_real < hi_val || target_eps_real > lo_val {
return None;
}
(0.0_f64, 1.0_f64)
};
let mut lo = low_f;
let mut hi = high_f;
for _ in 0..60 {
let mid = 0.5 * (lo + hi);
let val = eps_at_f(mid);
if (val - target_eps_real).abs() < 1e-12 {
return Some(mid);
}
let lo_val_now = eps_at_f(lo);
if (lo_val_now - target_eps_real) * (val - target_eps_real) <= 0.0 {
hi = mid;
} else {
lo = mid;
}
}
Some(0.5 * (lo + hi))
}
}
#[derive(Debug, Clone)]
pub struct Bruggeman {
pub eps_a: Complex64,
pub eps_b: Complex64,
pub fill_fraction_a: f64,
}
impl Bruggeman {
pub fn new(
eps_a: Complex64,
eps_b: Complex64,
fill_fraction_a: f64,
) -> Result<Self, OxiPhotonError> {
if !(0.0..=1.0).contains(&fill_fraction_a) {
return Err(OxiPhotonError::NumericalError(format!(
"fill_fraction_a must be in [0, 1], got {fill_fraction_a}"
)));
}
Ok(Self {
eps_a,
eps_b,
fill_fraction_a,
})
}
pub fn effective_permittivity(&self) -> Result<Complex64, OxiPhotonError> {
let f = self.fill_fraction_a;
let ea = self.eps_a;
let eb = self.eps_b;
let two = Complex64::new(2.0, 0.0);
let one = Complex64::new(1.0, 0.0);
let fa = Complex64::new(f, 0.0);
let fb = Complex64::new(1.0 - f, 0.0);
let coeff_b = -((Complex64::new(3.0, 0.0) * fa - one) * ea
+ (Complex64::new(3.0, 0.0) * fb - one) * eb);
let discriminant = coeff_b * coeff_b + Complex64::new(4.0 * 2.0, 0.0) * ea * eb;
let sqrt_disc = discriminant.sqrt();
let root1 = (-coeff_b + sqrt_disc) / Complex64::new(8.0, 0.0);
let root2 = (-coeff_b - sqrt_disc) / Complex64::new(8.0, 0.0);
let eps0 = if root1.im >= root2.im { root1 } else { root2 };
let bruggeman_f = |e: Complex64| -> Complex64 {
fa * (ea - e) / (ea + two * e) + fb * (eb - e) / (eb + two * e)
};
let bruggeman_df = |e: Complex64| -> Complex64 {
let da = ea + two * e;
let db = eb + two * e;
-fa * (ea + two * e + two * (ea - e)) / (da * da)
- fb * (eb + two * e + two * (eb - e)) / (db * db)
};
let mut eps = eps0;
for iter in 0..200 {
let fval = bruggeman_f(eps);
if fval.norm() < 1e-14 {
break;
}
let dfval = bruggeman_df(eps);
if dfval.norm() < 1e-30 {
return Err(OxiPhotonError::NumericalError(
"Bruggeman Newton step: zero derivative".to_string(),
));
}
let step = fval / dfval;
eps -= step;
if iter == 199 {
return Err(OxiPhotonError::NumericalError(
"Bruggeman iteration did not converge after 200 steps".to_string(),
));
}
}
if eps.re.is_nan() || eps.im.is_nan() {
return Err(OxiPhotonError::NumericalError(
"Bruggeman converged to NaN".to_string(),
));
}
Ok(eps)
}
pub fn effective_conductivity(&self, sigma_a: f64, sigma_b: f64) -> f64 {
let f = self.fill_fraction_a;
let term1 = (3.0 * f - 1.0) * sigma_a + (2.0 - 3.0 * f) * sigma_b;
let discriminant = term1 * term1 + 8.0 * sigma_a * sigma_b;
(term1 + discriminant.sqrt()) / 4.0
}
pub fn percolation_threshold() -> f64 {
1.0 / 3.0
}
pub fn is_percolating(&self) -> bool {
self.fill_fraction_a > Self::percolation_threshold()
}
}
#[derive(Debug, Clone)]
pub struct MieSphere {
pub radius_nm: f64,
pub eps_sphere: Complex64,
pub eps_medium: Complex64,
}
impl MieSphere {
pub fn new(radius_nm: f64, eps_sphere: Complex64, eps_medium: Complex64) -> Self {
Self {
radius_nm,
eps_sphere,
eps_medium,
}
}
pub fn size_parameter(&self, lambda_nm: f64) -> f64 {
let n_m = self.eps_medium.sqrt().re.max(0.0);
2.0 * PI * n_m * self.radius_nm / lambda_nm
}
fn relative_index(&self) -> Complex64 {
let n_sph = self.eps_sphere.sqrt();
let n_med = self.eps_medium.sqrt();
let n_sph = if n_sph.re >= 0.0 { n_sph } else { -n_sph };
let n_med = if n_med.re >= 0.0 { n_med } else { -n_med };
n_sph / n_med
}
fn riccati_bessel_j(z: Complex64, n_max: usize) -> Vec<Complex64> {
let mut psi = vec![Complex64::new(0.0, 0.0); n_max + 2];
psi[0] = z.sin();
if n_max == 0 {
return psi;
}
psi[1] = z.sin() / z - z.cos();
for n in 1..n_max {
psi[n + 1] = Complex64::new((2 * n + 1) as f64, 0.0) / z * psi[n] - psi[n - 1];
}
psi
}
fn riccati_hankel(z: Complex64, n_max: usize) -> Vec<Complex64> {
let i = Complex64::new(0.0, 1.0);
let mut xi = vec![Complex64::new(0.0, 0.0); n_max + 2];
let psi = Self::riccati_bessel_j(z, n_max);
let mut chi = vec![Complex64::new(0.0, 0.0); n_max + 2];
chi[0] = -z.cos();
if n_max >= 1 {
chi[1] = -z.cos() / z - z.sin();
}
for n in 1..n_max {
chi[n + 1] = Complex64::new((2 * n + 1) as f64, 0.0) / z * chi[n] - chi[n - 1];
}
for n in 0..=n_max + 1 {
xi[n] = psi[n] + i * chi[n];
}
xi
}
fn mie_coefficients(&self, lambda_nm: f64) -> (Vec<Complex64>, Vec<Complex64>) {
let x = self.size_parameter(lambda_nm);
let m = self.relative_index();
let n_max = (x + 4.0 * x.cbrt() + 2.0).ceil() as usize + 1;
let n_max = n_max.max(2);
let mx = m * Complex64::new(x, 0.0);
let x_c = Complex64::new(x, 0.0);
let psi_x = Self::riccati_bessel_j(x_c, n_max);
let psi_mx = Self::riccati_bessel_j(mx, n_max);
let xi_x = Self::riccati_hankel(x_c, n_max);
let dpsi_x: Vec<Complex64> = (0..=n_max)
.map(|n| {
if n == 0 {
psi_x[1]
} else {
psi_x[n - 1] - Complex64::new(n as f64, 0.0) / x_c * psi_x[n]
}
})
.collect();
let dpsi_mx: Vec<Complex64> = (0..=n_max)
.map(|n| {
if n == 0 {
psi_mx[1]
} else {
psi_mx[n - 1] - Complex64::new(n as f64, 0.0) / mx * psi_mx[n]
}
})
.collect();
let dxi_x: Vec<Complex64> = (0..=n_max)
.map(|n| {
if n == 0 {
xi_x[1]
} else {
xi_x[n - 1] - Complex64::new(n as f64, 0.0) / x_c * xi_x[n]
}
})
.collect();
let mut a_n = Vec::with_capacity(n_max);
let mut b_n = Vec::with_capacity(n_max);
for n in 1..=n_max {
let an_num = m * psi_mx[n] * dpsi_x[n] - psi_x[n] * dpsi_mx[n];
let an_den = m * psi_mx[n] * dxi_x[n] - xi_x[n] * dpsi_mx[n];
let an = if an_den.norm() > 1e-30 {
an_num / an_den
} else {
Complex64::new(0.0, 0.0)
};
a_n.push(an);
let bn_num = psi_mx[n] * dpsi_x[n] - m * psi_x[n] * dpsi_mx[n];
let bn_den = psi_mx[n] * dxi_x[n] - m * xi_x[n] * dpsi_mx[n];
let bn = if bn_den.norm() > 1e-30 {
bn_num / bn_den
} else {
Complex64::new(0.0, 0.0)
};
b_n.push(bn);
}
(a_n, b_n)
}
pub fn scattering_efficiency(&self, lambda_nm: f64) -> f64 {
let x = self.size_parameter(lambda_nm);
let (a_n, b_n) = self.mie_coefficients(lambda_nm);
let sum: f64 = a_n
.iter()
.zip(b_n.iter())
.enumerate()
.map(|(i, (a, b))| {
let n = (i + 1) as f64;
(2.0 * n + 1.0) * (a.norm_sqr() + b.norm_sqr())
})
.sum();
(2.0 / (x * x)) * sum
}
pub fn extinction_efficiency(&self, lambda_nm: f64) -> f64 {
let x = self.size_parameter(lambda_nm);
let (a_n, b_n) = self.mie_coefficients(lambda_nm);
let sum: f64 = a_n
.iter()
.zip(b_n.iter())
.enumerate()
.map(|(i, (a, b))| {
let n = (i + 1) as f64;
(2.0 * n + 1.0) * (a.re + b.re)
})
.sum();
(2.0 / (x * x)) * sum
}
pub fn absorption_efficiency(&self, lambda_nm: f64) -> f64 {
(self.extinction_efficiency(lambda_nm) - self.scattering_efficiency(lambda_nm)).max(0.0)
}
pub fn scattering_cross_section_nm2(&self, lambda_nm: f64) -> f64 {
self.scattering_efficiency(lambda_nm) * PI * self.radius_nm * self.radius_nm
}
pub fn dipole_scattering_efficiency(&self, lambda_nm: f64) -> f64 {
let x = self.size_parameter(lambda_nm);
let eps_r = self.eps_sphere / self.eps_medium; let one = Complex64::new(1.0, 0.0);
let two = Complex64::new(2.0, 0.0);
let k = (eps_r - one) / (eps_r + two);
(8.0 / 3.0) * x.powi(4) * k.norm_sqr()
}
pub fn lspr_condition(&self) -> f64 {
-2.0 * self.eps_medium.re
}
pub fn asymmetry_parameter(&self, lambda_nm: f64) -> f64 {
let x = self.size_parameter(lambda_nm);
let (a_n, b_n) = self.mie_coefficients(lambda_nm);
let q_sca = self.scattering_efficiency(lambda_nm);
if q_sca < 1e-20 {
return 0.0;
}
let len = a_n.len();
let mut sum = 0.0;
for n in 1..=len {
let an = a_n[n - 1];
let bn = b_n[n - 1];
let nf = n as f64;
if n < len {
let an1 = a_n[n];
let bn1 = b_n[n];
let cross =
nf * (nf + 2.0) / (nf + 1.0) * ((an * an1.conj()).re + (bn * bn1.conj()).re);
sum += cross;
}
let self_term = (2.0 * nf + 1.0) / (nf * (nf + 1.0)) * (an * bn.conj()).re;
sum += self_term;
}
(4.0 / (q_sca * x * x)) * sum
}
}
#[derive(Debug, Clone)]
pub enum PeriodicGeometry {
Lamellar { fill_a: f64 },
Cylinders { radius_fraction: f64 },
Spheres { radius_fraction: f64 },
Holes { radius_fraction: f64 },
}
#[derive(Debug, Clone)]
pub struct PeriodicEMT {
pub lattice_constant_nm: f64,
pub eps_a: Complex64,
pub eps_b: Complex64,
pub geometry: PeriodicGeometry,
}
impl PeriodicEMT {
pub fn new(
lattice_constant_nm: f64,
eps_a: Complex64,
eps_b: Complex64,
geometry: PeriodicGeometry,
) -> Self {
Self {
lattice_constant_nm,
eps_a,
eps_b,
geometry,
}
}
fn fill_fraction_a(&self) -> f64 {
match &self.geometry {
PeriodicGeometry::Lamellar { fill_a } => *fill_a,
PeriodicGeometry::Cylinders { radius_fraction } => {
PI * radius_fraction * radius_fraction
}
PeriodicGeometry::Spheres { radius_fraction } => {
(4.0 / 3.0) * PI * radius_fraction.powi(3)
}
PeriodicGeometry::Holes { radius_fraction } => {
1.0 - PI * radius_fraction * radius_fraction
}
}
}
pub fn effective_permittivity_parallel(&self) -> Complex64 {
let f = self.fill_fraction_a().clamp(0.0, 1.0);
match &self.geometry {
PeriodicGeometry::Lamellar { .. } | PeriodicGeometry::Holes { .. } => {
Complex64::new(f, 0.0) * self.eps_a + Complex64::new(1.0 - f, 0.0) * self.eps_b
}
PeriodicGeometry::Cylinders { .. } => {
let mg = MaxwellGarnett {
eps_host: self.eps_b,
eps_inclusion: self.eps_a,
fill_fraction: f,
};
mg.cylindrical_effective_permittivity()
}
PeriodicGeometry::Spheres { .. } => {
let mg = MaxwellGarnett {
eps_host: self.eps_b,
eps_inclusion: self.eps_a,
fill_fraction: f,
};
mg.effective_permittivity()
}
}
}
pub fn effective_permittivity_perpendicular(&self) -> Complex64 {
let f = self.fill_fraction_a().clamp(0.0, 1.0);
match &self.geometry {
PeriodicGeometry::Lamellar { .. } | PeriodicGeometry::Holes { .. } => {
let inv =
Complex64::new(f, 0.0) / self.eps_a + Complex64::new(1.0 - f, 0.0) / self.eps_b;
Complex64::new(1.0, 0.0) / inv
}
PeriodicGeometry::Cylinders { .. } | PeriodicGeometry::Spheres { .. } => {
let mg = MaxwellGarnett {
eps_host: self.eps_b,
eps_inclusion: self.eps_a,
fill_fraction: f,
};
mg.effective_permittivity()
}
}
}
pub fn effective_index_te(&self) -> Complex64 {
let eps = self.effective_permittivity_parallel();
let root = eps.sqrt();
if root.re >= 0.0 {
root
} else {
-root
}
}
pub fn effective_index_tm(&self) -> Complex64 {
let eps = self.effective_permittivity_perpendicular();
let root = eps.sqrt();
if root.re >= 0.0 {
root
} else {
-root
}
}
pub fn form_birefringence(&self) -> f64 {
let n_te = self.effective_index_te();
let n_tm = self.effective_index_tm();
(n_te.re - n_tm.re).abs()
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_maxwell_garnett_dilute_limit() {
let eh = Complex64::new(2.25, 0.0);
let ei = Complex64::new(12.0, 0.5);
let mg = MaxwellGarnett::new(eh, ei, 1e-6).unwrap();
let eps_eff = mg.effective_permittivity();
assert_abs_diff_eq!(eps_eff.re, eh.re, epsilon = 1e-4);
assert_abs_diff_eq!(eps_eff.im, eh.im, epsilon = 1e-4);
}
#[test]
fn test_maxwell_garnett_dense_limit() {
let eh = Complex64::new(2.25, 0.0);
let ei = Complex64::new(12.0, 0.5);
let mg = MaxwellGarnett::new(eh, ei, 1.0 - 1e-8).unwrap();
let eps_eff = mg.effective_permittivity();
assert!(
eps_eff.re > eh.re,
"ε_eff should be > ε_host at high fill fraction"
);
}
#[test]
fn test_layered_parallel_weighted_average() {
let eh = Complex64::new(2.25, 0.0);
let ei = Complex64::new(9.0, 0.0);
let f = 0.3;
let mg = MaxwellGarnett::new(eh, ei, f).unwrap();
let eps_par = mg.layered_parallel();
let expected = f * ei + (1.0 - f) * eh;
assert_abs_diff_eq!(eps_par.re, expected.re, epsilon = 1e-12);
assert_abs_diff_eq!(eps_par.im, expected.im, epsilon = 1e-12);
}
#[test]
fn test_maxwell_garnett_invalid_fill_fraction() {
let eps = Complex64::new(2.0, 0.0);
assert!(MaxwellGarnett::new(eps, eps, 1.5).is_err());
assert!(MaxwellGarnett::new(eps, eps, -0.1).is_err());
}
#[test]
fn test_bruggeman_symmetric() {
let ea = Complex64::new(2.25, 0.0);
let eb = Complex64::new(9.0, 0.1);
let f = 0.4;
let bg1 = Bruggeman::new(ea, eb, f).unwrap();
let eps1 = bg1.effective_permittivity().unwrap();
let bg2 = Bruggeman::new(eb, ea, 1.0 - f).unwrap();
let eps2 = bg2.effective_permittivity().unwrap();
assert_abs_diff_eq!(eps1.re, eps2.re, epsilon = 1e-8);
assert_abs_diff_eq!(eps1.im, eps2.im, epsilon = 1e-8);
}
#[test]
fn test_bruggeman_percolation_threshold() {
assert_abs_diff_eq!(
Bruggeman::percolation_threshold(),
1.0 / 3.0,
epsilon = 1e-15
);
}
#[test]
fn test_bruggeman_is_percolating() {
let ea = Complex64::new(1e6, 0.0); let eb = Complex64::new(2.0, 0.0); let bg_above = Bruggeman::new(ea, eb, 0.4).unwrap();
let bg_below = Bruggeman::new(ea, eb, 0.2).unwrap();
assert!(bg_above.is_percolating());
assert!(!bg_below.is_percolating());
}
#[test]
fn test_mie_small_sphere_dipole_approx() {
let radius_nm = 5.0; let eps_sphere = Complex64::new(4.0, 0.1);
let eps_medium = Complex64::new(1.0, 0.0);
let mie = MieSphere::new(radius_nm, eps_sphere, eps_medium);
let lambda_nm = 600.0;
let x = mie.size_parameter(lambda_nm);
assert!(
x < 0.2,
"size parameter should be < 0.2 for dipole regime, got {x}"
);
let q_full = mie.scattering_efficiency(lambda_nm);
let q_dipole = mie.dipole_scattering_efficiency(lambda_nm);
let rel_err = (q_full - q_dipole).abs() / q_full.max(1e-30);
assert!(
rel_err < 0.2,
"dipole vs full Mie relative error {rel_err:.4} > 20%"
);
}
#[test]
fn test_mie_efficiency_positive() {
let mie = MieSphere::new(
50.0,
Complex64::new(-10.0, 1.5), Complex64::new(2.25, 0.0),
);
for lambda_nm in [400.0, 500.0, 600.0, 800.0] {
let q_sca = mie.scattering_efficiency(lambda_nm);
let q_abs = mie.absorption_efficiency(lambda_nm);
let q_ext = mie.extinction_efficiency(lambda_nm);
assert!(q_sca >= -1e-10, "Q_sca < 0 at λ={lambda_nm}");
assert!(q_abs >= -1e-10, "Q_abs < 0 at λ={lambda_nm}");
assert!(q_ext >= -1e-10, "Q_ext < 0 at λ={lambda_nm}");
}
}
#[test]
fn test_periodic_emt_lamellar_form_birefringence() {
let ea = Complex64::new(4.0, 0.0);
let eb = Complex64::new(1.0, 0.0);
let pemt = PeriodicEMT::new(500.0, ea, eb, PeriodicGeometry::Lamellar { fill_a: 0.5 });
let fb = pemt.form_birefringence();
assert!(
fb > 1e-6,
"form birefringence should be non-zero for ε_a ≠ ε_b, got {fb}"
);
}
#[test]
fn test_periodic_emt_equal_eps_zero_birefringence() {
let eps = Complex64::new(3.0, 0.0);
let pemt = PeriodicEMT::new(400.0, eps, eps, PeriodicGeometry::Lamellar { fill_a: 0.5 });
let fb = pemt.form_birefringence();
assert!(
fb < 1e-12,
"form birefringence should vanish when ε_a = ε_b, got {fb}"
);
}
}