use num_complex::Complex64;
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct MieResult {
pub q_ext: f64,
pub q_scat: f64,
pub q_abs: f64,
pub q_back: f64,
pub size_parameter: f64,
}
pub struct SphereScatter {
pub radius: f64,
pub n_sphere: f64,
pub k_sphere: f64,
pub n_medium: f64,
}
impl SphereScatter {
pub fn new(radius: f64, n_sphere: f64, k_sphere: f64, n_medium: f64) -> Self {
Self {
radius,
n_sphere,
k_sphere,
n_medium,
}
}
pub fn compute(&self, lambda_m: f64) -> MieResult {
debug_assert!(lambda_m > 0.0, "wavelength must be positive");
debug_assert!(self.radius > 0.0, "radius must be positive");
debug_assert!(self.n_medium > 0.0, "n_medium must be positive");
let x = 2.0 * PI * self.n_medium * self.radius / lambda_m;
let m = Complex64::new(self.n_sphere / self.n_medium, self.k_sphere / self.n_medium);
if x < 1e-10 {
return SphereScatter::rayleigh_limit(x, m);
}
let (a_vec, b_vec) = Self::mie_coefficients(x, m);
let n_terms = a_vec.len();
let mut q_ext_sum = 0.0_f64;
let mut q_scat_sum = 0.0_f64;
let mut q_back_sum = Complex64::new(0.0, 0.0);
for idx in 0..n_terms {
let n = (idx + 1) as f64; let weight = 2.0 * n + 1.0;
let an = a_vec[idx];
let bn = b_vec[idx];
q_ext_sum += weight * (an.re + bn.re);
q_scat_sum += weight * (an.norm_sqr() + bn.norm_sqr());
let sign = if (idx + 1) % 2 == 1 { 1.0 } else { -1.0 }; q_back_sum += (an - bn) * Complex64::new(weight * sign, 0.0);
}
let inv_x2 = 1.0 / (x * x);
let q_ext = 2.0 * inv_x2 * q_ext_sum;
let q_scat = 2.0 * inv_x2 * q_scat_sum;
let q_abs = q_ext - q_scat;
let q_back = inv_x2 * q_back_sum.norm_sqr();
MieResult {
q_ext,
q_scat,
q_abs,
q_back,
size_parameter: x,
}
}
pub fn cross_sections(&self, lambda_m: f64) -> (f64, f64, f64) {
let geometric_area = PI * self.radius * self.radius;
let r = self.compute(lambda_m);
(
r.q_ext * geometric_area,
r.q_scat * geometric_area,
r.q_abs * geometric_area,
)
}
fn mie_coefficients(x: f64, m: Complex64) -> (Vec<Complex64>, Vec<Complex64>) {
let mx = m * x;
let n_max = ((mx.norm().max(x)).ceil() as usize) + 16;
let d_mx = Self::log_deriv_d(mx, n_max);
let sin_x = x.sin();
let cos_x = x.cos();
let mut psi_prev = sin_x; let mut psi_curr = sin_x / x - cos_x;
let xi_0 = Complex64::new(sin_x, -cos_x);
let xi_1 = Complex64::new(sin_x / x - cos_x, -(cos_x / x + sin_x));
let mut xi_prev = xi_0;
let mut xi_curr = xi_1;
let mut a_vec: Vec<Complex64> = Vec::new();
let mut b_vec: Vec<Complex64> = Vec::new();
let mut consecutive_converged: usize = 0;
const CONVERGENCE_THRESHOLD: f64 = 1e-12;
const CONVERGENCE_RUNS: usize = 3;
#[allow(clippy::needless_range_loop)]
for n in 1..=n_max {
let nf = n as f64;
let d_n_mx = if n <= n_max {
d_mx[n]
} else {
Complex64::new(0.0, 0.0)
};
let psi_n_m1 = Complex64::new(psi_prev, 0.0); let psi_n = Complex64::new(psi_curr, 0.0); let xi_n_m1 = xi_prev; let xi_n = xi_curr;
let n_over_x = Complex64::new(nf / x, 0.0);
let coeff_a = d_n_mx / m + n_over_x; let coeff_b = d_n_mx * m + n_over_x;
let num_a = coeff_a * psi_n - psi_n_m1;
let den_a = coeff_a * xi_n - xi_n_m1;
let num_b = coeff_b * psi_n - psi_n_m1;
let den_b = coeff_b * xi_n - xi_n_m1;
let a_n = if den_a.norm() > f64::MIN_POSITIVE * 1e3 {
num_a / den_a
} else {
Complex64::new(0.0, 0.0)
};
let b_n = if den_b.norm() > f64::MIN_POSITIVE * 1e3 {
num_b / den_b
} else {
Complex64::new(0.0, 0.0)
};
a_vec.push(a_n);
b_vec.push(b_n);
if a_n.norm() < CONVERGENCE_THRESHOLD && b_n.norm() < CONVERGENCE_THRESHOLD {
consecutive_converged += 1;
if consecutive_converged >= CONVERGENCE_RUNS {
break;
}
} else {
consecutive_converged = 0;
}
let factor = (2.0 * nf + 1.0) / x;
let psi_next = factor * psi_curr - psi_prev;
psi_prev = psi_curr;
psi_curr = psi_next;
let xi_next = Complex64::new(factor, 0.0) * xi_curr - xi_prev;
xi_prev = xi_curr;
xi_curr = xi_next;
}
(a_vec, b_vec)
}
fn log_deriv_d(rho: Complex64, n_max: usize) -> Vec<Complex64> {
let mut d = vec![Complex64::new(0.0, 0.0); n_max + 2];
for n in (1..=n_max).rev() {
let nf = n as f64;
let n_over_rho = Complex64::new(nf, 0.0) / rho;
let denominator = d[n] + n_over_rho;
d[n - 1] = n_over_rho
- if denominator.norm() > f64::MIN_POSITIVE * 1e6 {
Complex64::new(1.0, 0.0) / denominator
} else {
Complex64::new(0.0, 0.0)
};
}
d
}
fn rayleigh_limit(x: f64, m: Complex64) -> MieResult {
let m2 = m * m;
let k = (m2 - Complex64::new(1.0, 0.0)) / (m2 + Complex64::new(2.0, 0.0));
let q_ext = -4.0 * x * k.im;
let q_scat = (8.0 / 3.0) * x.powi(4) * k.norm_sqr();
let q_abs = q_ext - q_scat;
let q_back = 1.5 * q_scat;
MieResult {
q_ext: q_ext.max(0.0),
q_scat,
q_abs: q_abs.max(0.0),
q_back,
size_parameter: x,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rel_err(got: f64, expected: f64) -> f64 {
(got - expected).abs() / expected.abs()
}
#[test]
fn mie_bh_table4_1_x01_real_index() {
let radius = 0.1 / (2.0 * PI); let s = SphereScatter::new(radius, 1.5, 0.0, 1.0);
let r = s.compute(1.0);
assert!(r.q_abs.abs() < 1e-5, "Q_abs={:.2e}", r.q_abs);
assert!(r.q_ext > 0.0, "Q_ext={:.6}", r.q_ext);
let m2 = 1.5_f64.powi(2); let k_val = (m2 - 1.0) / (m2 + 2.0); let q_scat_expected = (8.0 / 3.0) * 0.1_f64.powi(4) * k_val * k_val;
assert!(
rel_err(r.q_scat, q_scat_expected) < 1e-3,
"Q_scat={:.6e} expected≈{:.6e}",
r.q_scat,
q_scat_expected
);
}
#[test]
fn mie_bh_x1_m15_energy_conservation() {
let s = SphereScatter::new(1.0 / (2.0 * PI), 1.5, 0.0, 1.0); let r = s.compute(1.0);
assert!(r.q_abs.abs() < 1e-6, "Q_abs={:.2e}", r.q_abs);
assert!(
(r.q_ext - r.q_scat).abs() < 1e-8,
"|Q_ext-Q_scat|={:.2e}",
(r.q_ext - r.q_scat).abs()
);
assert!(
(r.q_ext - 0.2151).abs() < 0.002,
"Q_ext={:.5} expected ≈ 0.2151",
r.q_ext
);
}
#[test]
fn mie_energy_conservation_real_index() {
let s = SphereScatter::new(100e-9, 1.5, 0.0, 1.0);
let r = s.compute(532e-9);
assert!(r.q_abs.abs() < 1e-6, "Q_abs={}", r.q_abs);
assert!(r.q_ext > 0.0);
assert!(r.q_scat > 0.0);
assert!((r.q_ext - r.q_scat - r.q_abs).abs() < 1e-10);
}
#[test]
fn mie_small_sphere_rayleigh_scaling() {
let r1 = SphereScatter::new(5e-9, 1.5, 0.0, 1.0).compute(532e-9); let r2 = SphereScatter::new(10e-9, 1.5, 0.0, 1.0).compute(532e-9); let ratio = r2.q_ext / r1.q_ext;
assert!(
(ratio - 16.0).abs() < 2.0,
"Rayleigh scaling ratio={:.4} (expected ≈ 16)",
ratio
);
}
#[test]
fn mie_absorbing_sphere_positive_q_abs() {
let s = SphereScatter::new(100e-9, 1.5, 1.0, 1.0);
let r = s.compute(532e-9);
assert!(r.q_abs > 0.0, "Q_abs={:.6} should be positive", r.q_abs);
assert!(
r.q_ext > r.q_scat,
"Q_ext={:.6} should exceed Q_scat={:.6}",
r.q_ext,
r.q_scat
);
}
#[test]
fn mie_cross_sections_scale_with_area() {
let r1 = SphereScatter::new(50e-9, 1.5, 0.0, 1.0);
let r2 = SphereScatter::new(100e-9, 1.5, 0.0, 1.0);
let lambda = 314e-9;
let (c1_ext, _, _) = r1.cross_sections(lambda);
let lambda2 = 628e-9;
let (c2_ext, _, _) = r2.cross_sections(lambda2);
let ratio = c2_ext / c1_ext;
assert!(
(ratio - 4.0).abs() < 0.1,
"C2/C1 ratio={:.4} (expected ≈ 4.0)",
ratio
);
}
#[test]
fn mie_large_size_parameter_converges() {
let s = SphereScatter::new(848e-9, 1.5, 0.0, 1.0);
let r = s.compute(1000e-9);
assert!(
r.q_ext > 1.0,
"large sphere Q_ext should exceed 1.0, got {}",
r.q_ext
);
assert!(
r.q_scat < r.q_ext + 1e-6,
"Q_scat={} Q_ext={}",
r.q_scat,
r.q_ext
);
}
#[test]
fn mie_q_back_nonnegative() {
let s = SphereScatter::new(100e-9, 1.5, 0.5, 1.0);
let r = s.compute(532e-9);
assert!(
r.q_back >= 0.0,
"Q_back={:.6} must be non-negative",
r.q_back
);
}
#[test]
fn mie_energy_balance_absorbing() {
let s = SphereScatter::new(100e-9, 2.5, 0.8, 1.3);
let r = s.compute(400e-9);
let balance = (r.q_ext - r.q_scat - r.q_abs).abs();
assert!(balance < 1e-9, "energy balance violation: {:.2e}", balance);
}
#[test]
fn mie_nonabsorbing_in_medium() {
let s = SphereScatter::new(50e-9, 2.0, 0.0, 1.5);
let r = s.compute(633e-9);
assert!(
r.q_abs.abs() < 1e-6,
"Q_abs={:.2e} for non-absorbing sphere",
r.q_abs
);
assert!(r.q_ext >= 0.0);
assert!(r.q_scat >= 0.0);
}
#[test]
fn mie_rayleigh_regime_x4_scaling() {
let lambda1 = 1.0 / (2.0 * PI * 0.001); let lambda2 = 1.0 / (2.0 * PI * 0.01); let r1 = SphereScatter::new(1.0, 1.5, 0.0, 1.0).compute(lambda1);
let r2 = SphereScatter::new(1.0, 1.5, 0.0, 1.0).compute(lambda2);
let ratio = r2.q_ext / r1.q_ext;
assert!(
(ratio - 1e4).abs() / 1e4 < 0.01,
"Rayleigh x^4 scaling: Q_ext(x=0.01)/Q_ext(x=0.001) = {:.4} (expected ≈ 1e4)",
ratio
);
}
#[test]
fn mie_size_parameter_value() {
let radius = 100e-9;
let lambda = 500e-9;
let n_med = 1.33;
let s = SphereScatter::new(radius, 1.5, 0.0, n_med);
let r = s.compute(lambda);
let x_expected = 2.0 * PI * n_med * radius / lambda;
assert!(
(r.size_parameter - x_expected).abs() < 1e-12,
"x={} expected={}",
r.size_parameter,
x_expected
);
}
#[test]
fn mie_cross_section_vs_efficiency() {
let radius = 80e-9;
let lambda = 400e-9;
let s = SphereScatter::new(radius, 1.8, 0.3, 1.0);
let r = s.compute(lambda);
let (c_ext, c_scat, c_abs) = s.cross_sections(lambda);
let area = PI * radius * radius;
assert!((c_ext - r.q_ext * area).abs() < 1e-30, "C_ext mismatch");
assert!((c_scat - r.q_scat * area).abs() < 1e-30, "C_scat mismatch");
assert!((c_abs - r.q_abs * area).abs() < 1e-30, "C_abs mismatch");
}
#[test]
fn mie_metallic_sphere_strong_absorption() {
let s = SphereScatter::new(40e-9, 0.15, 3.5, 1.0);
let r = s.compute(532e-9);
assert!(r.q_abs > 0.0, "metallic sphere must absorb");
assert!(r.q_ext > 0.0);
assert!((r.q_ext - r.q_scat - r.q_abs).abs() < 1e-9);
}
#[test]
fn mie_extinction_paradox_large_sphere() {
let r_val = 1.0e-6;
let lambda_val = 2.0 * PI * r_val / 20.0;
let s = SphereScatter::new(r_val, 1.5, 0.0, 1.0);
let result = s.compute(lambda_val);
assert!(
result.q_ext > 1.0 && result.q_ext < 3.5,
"x=20 Q_ext={:.5} outside expected range",
result.q_ext
);
}
#[test]
fn mie_q_abs_consistency() {
let s = SphereScatter::new(120e-9, 1.7, 0.4, 1.2);
let r = s.compute(600e-9);
let q_abs_direct = r.q_ext - r.q_scat;
assert!(
(r.q_abs - q_abs_direct).abs() < 1e-10,
"Q_abs internal inconsistency: stored={:.2e}, computed={:.2e}",
r.q_abs,
q_abs_direct
);
}
}