use num_complex::Complex64;
pub struct MieCoefficients {
pub a: Vec<Complex64>,
pub b: Vec<Complex64>,
}
pub fn mie(x: f64, m: Complex64) -> MieCoefficients {
assert!(x > 0.0, "size parameter must be positive");
let nstop = (x + 4.0 * x.powf(1.0 / 3.0) + 2.0) as usize;
let nmx = (nstop.max((m.norm() * x) as usize) + 15) as usize;
let mx = m * x;
let mut d = vec![Complex64::new(0.0, 0.0); nmx + 1];
for n in (1..nmx).rev() {
let rn = Complex64::new((n as f64) + 1.0, 0.0) / mx;
d[n - 1] = rn - Complex64::new(1.0, 0.0) / (d[n] + rn);
}
let mut psi_prev = x.sin();
let mut psi = x.sin() / x - x.cos();
let mut chi_prev = x.cos();
let mut chi = x.cos() / x + x.sin();
let mut a = Vec::with_capacity(nstop);
let mut b = Vec::with_capacity(nstop);
for n in 1..=nstop {
let nf = n as f64;
let xi_prev = Complex64::new(psi_prev, -chi_prev);
let xi = Complex64::new(psi, -chi);
let dn = d[n - 1];
let numer_a = (dn / m + Complex64::new(nf / x, 0.0)) * Complex64::new(psi, 0.0)
- Complex64::new(psi_prev, 0.0);
let denom_a = (dn / m + Complex64::new(nf / x, 0.0)) * xi - xi_prev;
a.push(numer_a / denom_a);
let numer_b = (m * dn + Complex64::new(nf / x, 0.0)) * Complex64::new(psi, 0.0)
- Complex64::new(psi_prev, 0.0);
let denom_b = (m * dn + Complex64::new(nf / x, 0.0)) * xi - xi_prev;
b.push(numer_b / denom_b);
let psi_new = (2.0 * nf + 1.0) / x * psi - psi_prev;
psi_prev = psi;
psi = psi_new;
let chi_new = (2.0 * nf + 1.0) / x * chi - chi_prev;
chi_prev = chi;
chi = chi_new;
}
MieCoefficients { a, b }
}
pub fn qsca(x: f64, m: Complex64) -> f64 {
let coeffs = mie(x, m);
let mut q = 0.0;
for (n, (an, bn)) in coeffs.a.iter().zip(coeffs.b.iter()).enumerate() {
let nf = (n + 1) as f64;
q += (2.0 * nf + 1.0) * (an.norm_sqr() + bn.norm_sqr());
}
(2.0 / (x * x)) * q
}
pub fn qext(x: f64, m: Complex64) -> f64 {
let coeffs = mie(x, m);
let mut q = 0.0;
for (n, (an, bn)) in coeffs.a.iter().zip(coeffs.b.iter()).enumerate() {
let nf = (n + 1) as f64;
q += (2.0 * nf + 1.0) * (an.re + bn.re);
}
(2.0 / (x * x)) * q
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rayleigh_limit_small_sphere() {
let x = 0.01;
let m = Complex64::new(1.33, 0.0);
let q_sca = qsca(x, m);
let m2 = m * m;
let alpha = (m2 - Complex64::new(1.0, 0.0)) / (m2 + Complex64::new(2.0, 0.0));
let expected = (8.0 / 3.0) * x.powi(4) * alpha.norm_sqr();
let rel_err = (q_sca - expected).abs() / expected;
assert!(
rel_err < 1e-5,
"Rayleigh Q_sca off: got {q_sca:.6e}, expected {expected:.6e}, rel err {rel_err:.2e}"
);
}
#[test]
fn perfectly_conducting_q_matches_table() {
let q_ext = qext(3.0, Complex64::new(1.5, 0.001));
assert!(q_ext > 2.0 && q_ext < 5.0);
}
}
#[cfg(test)]
mod spot_tests {
use super::*;
#[test]
fn bhmie_x1_m15() {
let q = qsca(1.0, Complex64::new(1.5, 0.0));
let rel = (q - 0.215098).abs() / 0.215098;
assert!(rel < 1e-4, "Q_sca(x=1,m=1.5) = {q:.6}, rel err {rel:.2e}");
}
#[test]
fn bhmie_x3_m15() {
let q = qsca(3.0, Complex64::new(1.5, 0.0));
let rel = (q - 3.418056).abs() / 3.418056;
assert!(rel < 1e-4, "Q_sca(x=3,m=1.5) = {q:.6}, rel err {rel:.2e}");
}
#[test]
fn absorbing_large_sphere() {
let qs = qsca(10.0, Complex64::new(1.33, 0.01));
let qe = qext(10.0, Complex64::new(1.33, 0.01));
let rel_s = (qs - 1.872112).abs() / 1.872112;
let rel_e = (qe - 2.249241).abs() / 2.249241;
assert!(
rel_s < 1e-4,
"Q_sca(x=10,m=1.33+0.01i) = {qs:.6}, rel err {rel_s:.2e}"
);
assert!(
rel_e < 1e-4,
"Q_ext(x=10,m=1.33+0.01i) = {qe:.6}, rel err {rel_e:.2e}"
);
assert!(qe > qs, "Q_ext must exceed Q_sca for absorbing sphere");
}
}