use std::f64::consts::PI;
use crate::error::{SpecialError, SpecialResult};
use crate::mathieu::advanced::{tridiag_eigenvector, tridiag_eigenvalues};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SpheroidType {
Prolate,
Oblate,
}
#[derive(Debug, Clone)]
pub struct SpheroidalConfig {
pub n_expansion: usize,
pub tol: f64,
}
impl Default for SpheroidalConfig {
fn default() -> Self {
SpheroidalConfig {
n_expansion: 40,
tol: 1e-12,
}
}
}
pub fn associated_legendre(l: usize, m: usize, x: f64) -> f64 {
if m > l {
return 0.0;
}
let factor = (1.0 - x * x).max(0.0).sqrt();
let mut pmm = 1.0_f64;
for i in 0..m {
pmm *= -((2 * i + 1) as f64) * factor;
}
if l == m {
return pmm;
}
let mut pmm1 = x * (2 * m + 1) as f64 * pmm;
if l == m + 1 {
return pmm1;
}
let mut pl_prev = pmm;
let mut pl = pmm1;
for k in (m + 1)..l {
let pk1 = ((2 * k + 1) as f64 * x * pl - (k + m) as f64 * pl_prev)
/ (k - m + 1) as f64;
pl_prev = pl;
pl = pk1;
}
let _ = pmm1; pl
}
fn build_spheroidal_tridiag(
m: usize,
n: usize,
c: f64,
config: &SpheroidalConfig,
) -> (Vec<f64>, Vec<f64>) {
let c2 = c * c;
let parity = (n - m) % 2; let nf = config.n_expansion;
let mut diag = vec![0.0_f64; nf];
let mut off = vec![0.0_f64; nf.saturating_sub(1)];
for p in 0..nf {
let k = 2 * p + parity; let ell = (m + k) as f64; let alpha_p = if k >= 2 {
let ll = ell; -c2 * (ll + m as f64) * (ll + m as f64 - 1.0)
/ ((2.0 * ll - 1.0) * (2.0 * ll + 1.0))
} else {
0.0
};
let gamma_p = -c2 * (ell - m as f64 + 1.0) * (ell - m as f64 + 2.0)
/ ((2.0 * ell + 1.0) * (2.0 * ell + 3.0));
diag[p] = ell * (ell + 1.0) + alpha_p + gamma_p;
if p + 1 < nf {
let ll = ell;
let off_val = -c2 * (ll - m as f64 + 1.0) * (ll - m as f64 + 2.0)
/ ((2.0 * ll + 1.0) * (2.0 * ll + 3.0));
off[p] = off_val;
}
}
(diag, off)
}
pub fn spheroidal_eigenvalue(
m: usize,
n: usize,
c: f64,
stype: &SpheroidType,
config: &SpheroidalConfig,
) -> SpecialResult<f64> {
if n < m {
return Err(SpecialError::DomainError(format!(
"spheroidal_eigenvalue: n={n} must be >= m={m}"
)));
}
let c_eff = match stype {
SpheroidType::Prolate => c,
SpheroidType::Oblate => c, };
let (mut diag, mut off) = build_spheroidal_tridiag(m, n, c_eff, config);
if matches!(stype, SpheroidType::Oblate) {
let (diag0, off0) = build_spheroidal_tridiag(m, n, 0.0, config);
for (i, (d, d0)) in diag.iter_mut().zip(diag0.iter()).enumerate() {
*d = 2.0 * d0 - *d; let _ = i;
}
for (o, o0) in off.iter_mut().zip(off0.iter()) {
*o = 2.0 * o0 - *o;
}
}
let eigenvalues = tridiag_eigenvalues(&diag, &off);
let target = (n - m) / 2;
eigenvalues
.get(target)
.copied()
.ok_or_else(|| {
SpecialError::ComputationError(format!(
"spheroidal_eigenvalue: failed to find eigenvalue for m={m}, n={n}, c={c}"
))
})
}
pub fn angular_spheroidal(
m: usize,
n: usize,
c: f64,
x: f64,
stype: &SpheroidType,
config: &SpheroidalConfig,
) -> SpecialResult<f64> {
if n < m {
return Err(SpecialError::DomainError(format!(
"angular_spheroidal: n={n} must be >= m={m}"
)));
}
if x.abs() > 1.0 + 1e-12 {
return Err(SpecialError::DomainError(format!(
"angular_spheroidal: x={x} must satisfy |x| <= 1"
)));
}
let x_clamped = x.clamp(-1.0, 1.0);
let (diag, off) = build_spheroidal_tridiag(m, n, c, config);
let eigenvalues = tridiag_eigenvalues(&diag, &off);
let target = (n - m) / 2;
let eigenval = eigenvalues.get(target).copied().ok_or_else(|| {
SpecialError::ComputationError(format!(
"angular_spheroidal: no eigenvalue for m={m}, n={n}"
))
})?;
let coeffs = tridiag_eigenvector(&diag, &off, eigenval);
let parity = (n - m) % 2;
let mut result = 0.0_f64;
for (p, &d) in coeffs.iter().enumerate() {
let k = 2 * p + parity;
let l = m + k;
let p_val = associated_legendre(l, m, x_clamped);
result += d * p_val;
}
let _ = stype;
Ok(result)
}
pub fn spherical_bessel_j(l: usize, x: f64) -> f64 {
if x.abs() < 1e-300 {
if l == 0 {
return 1.0; } else {
return 0.0;
}
}
let j0 = x.sin() / x;
if l == 0 {
return j0;
}
let j1 = x.sin() / (x * x) - x.cos() / x;
if l == 1 {
return j1;
}
let mut jm1 = j0;
let mut jc = j1;
for k in 1..l {
let jp1 = (2 * k + 1) as f64 / x * jc - jm1;
jm1 = jc;
jc = jp1;
}
jc
}
pub fn radial_spheroidal_1(
m: usize,
n: usize,
c: f64,
xi: f64,
config: &SpheroidalConfig,
) -> SpecialResult<f64> {
if n < m {
return Err(SpecialError::DomainError(format!(
"radial_spheroidal_1: n={n} must be >= m={m}"
)));
}
if xi < 1.0 - 1e-12 {
return Err(SpecialError::DomainError(format!(
"radial_spheroidal_1: xi={xi} must be >= 1"
)));
}
let (diag, off) = build_spheroidal_tridiag(m, n, c, config);
let eigenvalues = tridiag_eigenvalues(&diag, &off);
let target = (n - m) / 2;
let eigenval = eigenvalues.get(target).copied().ok_or_else(|| {
SpecialError::ComputationError(format!(
"radial_spheroidal_1: no eigenvalue for m={m}, n={n}"
))
})?;
let coeffs = tridiag_eigenvector(&diag, &off, eigenval);
let parity = (n - m) % 2;
let norm_idx = target; let d_norm = if norm_idx < coeffs.len() {
let v = coeffs[norm_idx];
if v.abs() < 1e-15 { 1.0 } else { v }
} else {
1.0
};
let xi2m1 = (xi * xi - 1.0).max(0.0);
let factor = xi2m1.powf(m as f64 / 2.0);
let mut sum = 0.0_f64;
for (p, &d) in coeffs.iter().enumerate() {
let k = 2 * p + parity;
let l = m + k;
let jl = spherical_bessel_j(l, c * xi);
sum += d * jl;
}
Ok(factor * sum / d_norm)
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_associated_legendre_p00() {
for &x in &[-0.5, 0.0, 0.5, 1.0] {
let val = associated_legendre(0, 0, x);
assert!((val - 1.0).abs() < 1e-14, "P_0^0({x}) should be 1, got {val}");
}
}
#[test]
fn test_associated_legendre_p10() {
for &x in &[-0.7, -0.3, 0.0, 0.4, 0.8] {
let val = associated_legendre(1, 0, x);
assert!((val - x).abs() < 1e-14, "P_1^0({x}) should be {x}, got {val}");
}
}
#[test]
fn test_associated_legendre_p11() {
for &x in &[-0.7, -0.3, 0.0, 0.4, 0.8] {
let val = associated_legendre(1, 1, x);
let expected = -(1.0 - x * x).sqrt();
assert!(
(val - expected).abs() < 1e-13,
"P_1^1({x}) should be {expected}, got {val}"
);
}
}
#[test]
fn test_associated_legendre_p20() {
for &x in &[-0.7, 0.0, 0.5, 1.0] {
let val = associated_legendre(2, 0, x);
let expected = (3.0 * x * x - 1.0) / 2.0;
assert!(
(val - expected).abs() < 1e-13,
"P_2^0({x}) = {expected}, got {val}"
);
}
}
#[test]
fn test_spherical_bessel_j0_at_pi() {
let val = spherical_bessel_j(0, PI);
assert!(val.abs() < 1e-14, "j_0(π) ≈ 0, got {val}");
}
#[test]
fn test_spherical_bessel_j0_near_zero() {
let val = spherical_bessel_j(0, 1e-300);
assert!((val - 1.0).abs() < 1e-12, "j_0(0) → 1, got {val}");
}
#[test]
fn test_spherical_bessel_j1() {
let x = 1.5;
let val = spherical_bessel_j(1, x);
let expected = x.sin() / (x * x) - x.cos() / x;
assert!(
(val - expected).abs() < 1e-13,
"j_1({x}) = {expected}, got {val}"
);
}
#[test]
fn test_spheroidal_eigenvalue_c_zero() {
let config = SpheroidalConfig::default();
for n in [2usize, 3, 4] {
let m = 0;
let lam = spheroidal_eigenvalue(m, n, 0.0, &SpheroidType::Prolate, &config).unwrap();
let expected = (n * (n + 1)) as f64;
assert!(
(lam - expected).abs() < 0.5,
"λ_{{m={m},n={n}}}(0) should be n(n+1)={expected}, got {lam}"
);
}
}
#[test]
fn test_angular_spheroidal_c_zero_legendre() {
let config = SpheroidalConfig { n_expansion: 20, tol: 1e-12 };
let x = 0.5;
let s = angular_spheroidal(0, 2, 0.0, x, &SpheroidType::Prolate, &config).unwrap();
assert!(s.is_finite(), "angular_spheroidal c=0 should be finite, got {s}");
}
#[test]
fn test_radial_spheroidal_1_finite() {
let config = SpheroidalConfig::default();
let val = radial_spheroidal_1(0, 2, 1.0, 1.5, &config).unwrap();
assert!(val.is_finite(), "radial_spheroidal_1 should be finite, got {val}");
}
}