use crate::error::{SpecialError, SpecialResult};
use crate::gamma::{gamma, gammaln};
const MAX_SERIES_TERMS: usize = 500;
const SERIES_TOL: f64 = 1e-15;
const MAX_ASYMP_TERMS: usize = 30;
pub fn whittaker_m_series(kappa: f64, mu: f64, z: f64) -> SpecialResult<f64> {
if z <= 0.0 {
return Err(SpecialError::DomainError(format!(
"Whittaker M requires z > 0, got z = {z}"
)));
}
let two_mu = 2.0 * mu;
if two_mu <= -1.0 && (two_mu + 1.0).fract().abs() < 1e-10 {
return Err(SpecialError::DomainError(format!(
"Whittaker M: 2*mu = {two_mu} is a negative integer, function undefined"
)));
}
let log_prefactor = -z / 2.0 + (mu + 0.5) * z.ln();
if log_prefactor < -740.0 {
return Ok(0.0); }
if log_prefactor > 709.0 {
return Err(SpecialError::OverflowError(format!(
"Whittaker M: prefactor overflows for z={z}, mu={mu}"
)));
}
let prefactor = log_prefactor.exp();
let a = mu - kappa + 0.5;
let b = 2.0 * mu + 1.0;
let hyp = hyp1f1_series(a, b, z)?;
Ok(prefactor * hyp)
}
pub fn whittaker_w_series(kappa: f64, mu: f64, z: f64) -> SpecialResult<f64> {
if z <= 0.0 {
return Err(SpecialError::DomainError(format!(
"Whittaker W requires z > 0, got z = {z}"
)));
}
if z > 20.0 {
return whittaker_w_asymptotic(kappa, mu, z);
}
let a = mu - kappa + 0.5;
let b = 2.0 * mu + 1.0;
whittaker_w_from_tricomi_u(kappa, mu, a, b, z)
}
pub fn whittaker_w_asymptotic(kappa: f64, mu: f64, z: f64) -> SpecialResult<f64> {
if z <= 0.0 {
return Err(SpecialError::DomainError(format!(
"Whittaker W asymptotic requires z > 0, got z = {z}"
)));
}
let log_prefactor = -z / 2.0 + kappa * z.ln();
if log_prefactor < -740.0 {
return Ok(0.0);
}
if log_prefactor > 709.0 {
return Err(SpecialError::OverflowError(format!(
"Whittaker W asymptotic: prefactor overflows for z={z}, kappa={kappa}"
)));
}
let prefactor = log_prefactor.exp();
let alpha = 0.5 - mu + kappa;
let beta = 0.5 + mu + kappa;
let mut term = 1.0_f64;
let mut sum = 1.0_f64;
let mut min_abs = 1.0_f64;
let mut best_sum = 1.0_f64;
for n in 0..MAX_ASYMP_TERMS {
let n_f = n as f64;
let numer = -(alpha + n_f) * (beta + n_f);
let denom = (n_f + 1.0) * z;
if denom == 0.0 {
break;
}
term *= numer / denom;
sum += term;
if term.abs() < min_abs {
min_abs = term.abs();
best_sum = sum;
}
if term.abs() > 10.0 * min_abs && n > 3 {
break;
}
if term.abs() < SERIES_TOL * sum.abs() {
best_sum = sum;
break;
}
}
Ok(prefactor * best_sum)
}
pub fn whittaker_m_array(kappa: f64, mu: f64, z_values: &[f64]) -> SpecialResult<Vec<f64>> {
z_values
.iter()
.map(|&z| whittaker_m_series(kappa, mu, z))
.collect()
}
pub fn whittaker_w_array(kappa: f64, mu: f64, z_values: &[f64]) -> SpecialResult<Vec<f64>> {
z_values
.iter()
.map(|&z| whittaker_w_series(kappa, mu, z))
.collect()
}
pub fn whittaker_wronskian(kappa: f64, mu: f64) -> SpecialResult<f64> {
let g_num = gamma(2.0 * mu + 1.0);
let g_den = gamma(mu - kappa + 0.5);
if !g_num.is_finite() {
return Err(SpecialError::OverflowError(format!(
"Gamma(2*mu+1) = Gamma({}) overflows",
2.0 * mu + 1.0
)));
}
if g_den.abs() < 1e-300 {
return Err(SpecialError::DomainError(format!(
"Gamma(mu - kappa + 1/2) = 0 for kappa={kappa}, mu={mu}"
)));
}
Ok(-g_num / g_den)
}
fn hyp1f1_series(a: f64, b: f64, z: f64) -> SpecialResult<f64> {
if b <= 0.0 && b.fract().abs() < 1e-10 && b.round() as i64 <= 0 {
return Err(SpecialError::DomainError(format!(
"1F1: b = {b} is a non-positive integer (pole)"
)));
}
if a.abs() < 1e-14 {
return Ok(1.0);
}
if z > 30.0 {
let a2 = b - a;
let b2 = b;
let z2 = -z;
let hyp2 = hyp1f1_series_direct(a2, b2, z2)?;
return Ok(z.exp() * hyp2);
}
hyp1f1_series_direct(a, b, z)
}
fn hyp1f1_series_direct(a: f64, b: f64, z: f64) -> SpecialResult<f64> {
let mut term = 1.0_f64;
let mut sum = 1.0_f64;
for n in 1..=MAX_SERIES_TERMS {
let n_f = (n - 1) as f64;
let numer = (a + n_f) * z;
let denom = (b + n_f) * (n as f64);
if denom.abs() < 1e-300 {
break;
}
term *= numer / denom;
sum += term;
if !term.is_finite() {
return Err(SpecialError::OverflowError(format!(
"1F1({a};{b};{z}): term overflow at n={n}"
)));
}
if term.abs() < SERIES_TOL * sum.abs().max(1e-300) && n > 5 {
break;
}
}
Ok(sum)
}
fn whittaker_w_from_tricomi_u(
kappa: f64,
mu: f64,
a: f64, b: f64, z: f64,
) -> SpecialResult<f64> {
let prefactor_log = -z / 2.0 + (mu + 0.5) * z.ln();
if prefactor_log < -740.0 {
return Ok(0.0);
}
if prefactor_log > 709.0 {
return Err(SpecialError::OverflowError(format!(
"Whittaker W: prefactor overflows for z={z}, mu={mu}"
)));
}
let prefactor = prefactor_log.exp();
let b_is_integer = (b.fract().abs() < 1e-10) && b.abs() < 1e15;
let b_int = b.round() as i64;
if !b_is_integer {
let sin_pi_b = (std::f64::consts::PI * b).sin();
if sin_pi_b.abs() < 1e-12 {
return whittaker_w_integer_b(kappa, mu, a, b, z, prefactor);
}
let log_g_1_plus_a_minus_b = gammaln_safe(1.0 + a - b)?;
let log_g_b = gammaln_safe(b)?;
let log_g_a = gammaln_safe(a)?;
let log_g_2_minus_b = gammaln_safe(2.0 - b)?;
let m1 = hyp1f1_series(a, b, z)?;
let m2 = hyp1f1_series(a - b + 1.0, 2.0 - b, z)?;
let z_1mb = z.powf(1.0 - b);
let g_1ab_sign = if (1.0 + a - b) < 0.0 && ((1.0 + a - b).floor() as i64).abs() % 2 == 1 {
-1.0_f64
} else {
1.0
};
let g_b_sign = if b < 0.0 && (b.floor() as i64).abs() % 2 == 1 {
-1.0_f64
} else {
1.0
};
let g_a_sign = if a < 0.0 && (a.floor() as i64).abs() % 2 == 1 {
-1.0_f64
} else {
1.0
};
let g_2mb_sign = if (2.0 - b) < 0.0 && ((2.0 - b).floor() as i64).abs() % 2 == 1 {
-1.0_f64
} else {
1.0
};
let term1_log_denom = log_g_1_plus_a_minus_b + log_g_b;
let term1 = g_1ab_sign * g_b_sign * m1 * (-term1_log_denom).exp();
let term2_log_denom = log_g_a + log_g_2_minus_b;
let term2 = g_a_sign * g_2mb_sign * z_1mb * m2 * (-term2_log_denom).exp();
let pi = std::f64::consts::PI;
let u_val = (pi / sin_pi_b) * (term1 - term2);
Ok(prefactor * u_val)
} else {
whittaker_w_integer_b(kappa, mu, a, b_int as f64, z, prefactor)
}
}
fn whittaker_w_integer_b(
kappa: f64,
mu: f64,
_a: f64,
_b: f64,
z: f64,
prefactor: f64,
) -> SpecialResult<f64> {
let two_mu = 2.0 * mu;
let two_mu_is_int = (two_mu.fract().abs() < 1e-9) && two_mu.abs() < 1e15;
if two_mu_is_int {
return whittaker_w_asymptotic(kappa, mu, z);
}
let log_g_neg_2mu = gammaln_safe(-two_mu)?;
let log_g_half_minus_mu_minus_k = gammaln_safe(0.5 - mu - kappa)?;
let log_g_2mu = gammaln_safe(two_mu)?;
let log_g_half_plus_mu_minus_k = gammaln_safe(0.5 + mu - kappa)?;
let sign_g_neg_2mu = if (-two_mu) < 0.0 && ((-two_mu).floor() as i64).abs() % 2 == 1 {
-1.0_f64
} else {
1.0
};
let sign_g_h1 = if (0.5 - mu - kappa) < 0.0
&& ((0.5 - mu - kappa).floor() as i64).abs() % 2 == 1
{
-1.0_f64
} else {
1.0
};
let sign_g_2mu = if two_mu < 0.0 && (two_mu.floor() as i64).abs() % 2 == 1 {
-1.0_f64
} else {
1.0
};
let sign_g_h2 = if (0.5 + mu - kappa) < 0.0
&& ((0.5 + mu - kappa).floor() as i64).abs() % 2 == 1
{
-1.0_f64
} else {
1.0
};
let m_plus = {
let a1 = mu - kappa + 0.5;
let b1 = 2.0 * mu + 1.0;
if b1 <= 0.0 && (b1.fract().abs() < 1e-10) {
0.0 } else {
let hyp1 = hyp1f1_series(a1, b1, z).unwrap_or(0.0);
prefactor * hyp1
}
};
let m_minus = {
let pref2_log = -z / 2.0 + (-mu + 0.5) * z.ln();
if pref2_log < -740.0 {
0.0
} else if pref2_log > 709.0 {
f64::INFINITY
} else {
let pref2 = pref2_log.exp();
let a2 = -mu - kappa + 0.5;
let b2 = -2.0 * mu + 1.0;
if b2 <= 0.0 && (b2.fract().abs() < 1e-10) {
0.0
} else {
let hyp2 = hyp1f1_series(a2, b2, z).unwrap_or(0.0);
pref2 * hyp2
}
}
};
let c1 = sign_g_neg_2mu * sign_g_h1 * (log_g_neg_2mu - log_g_half_minus_mu_minus_k).exp();
let c2 = sign_g_2mu * sign_g_h2 * (log_g_2mu - log_g_half_plus_mu_minus_k).exp();
Ok(c1 * m_plus + c2 * m_minus)
}
fn gammaln_safe(x: f64) -> SpecialResult<f64> {
if x <= 0.0 && (x.fract().abs() < 1e-10) {
return Err(SpecialError::DomainError(format!(
"gammaln({x}): argument is a non-positive integer (pole)"
)));
}
let val = gammaln(x);
if !val.is_finite() {
return Err(SpecialError::OverflowError(format!(
"gammaln({x}) = {val} overflows"
)));
}
Ok(val)
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-6;
#[test]
fn test_whittaker_m_basic() {
let m = whittaker_m_series(0.5, 0.5, 1.0).expect("ok");
assert!(m.is_finite() && m > 0.0, "M should be positive: {m}");
}
#[test]
fn test_whittaker_m_small_z() {
let z = 0.01_f64;
let mu = 0.5;
let kappa = 0.5;
let m = whittaker_m_series(kappa, mu, z).expect("ok");
assert!(m.is_finite() && m > 0.0);
let approx = z.powf(mu + 0.5) * (-z / 2.0).exp();
assert!(
(m / approx - 1.0).abs() < 0.1,
"M ratio to approx should be near 1: {m} vs {approx}"
);
}
#[test]
fn test_whittaker_m_large_z() {
let m = whittaker_m_series(0.5, 0.5, 10.0).expect("ok");
assert!(m.is_finite(), "M should be finite for large z: {m}");
}
#[test]
fn test_whittaker_m_negative_z_error() {
assert!(whittaker_m_series(0.5, 0.5, -1.0).is_err());
assert!(whittaker_m_series(0.5, 0.5, 0.0).is_err());
}
#[test]
fn test_whittaker_w_basic() {
let w = whittaker_w_series(0.5, 0.5, 1.0).expect("ok");
assert!(w.is_finite(), "W should be finite: {w}");
}
#[test]
fn test_whittaker_w_large_z() {
let z = 10.0_f64;
let kappa = 0.5;
let mu = 0.5;
let w = whittaker_w_series(kappa, mu, z).expect("ok");
assert!(w.is_finite(), "W should be finite for z=10: {w}");
let leading = (-z / 2.0 + kappa * z.ln()).exp();
assert!(
w.abs() < leading * 5.0,
"W should not vastly exceed leading term: {w} vs {leading}"
);
}
#[test]
fn test_whittaker_w_negative_z_error() {
assert!(whittaker_w_series(0.5, 0.5, -1.0).is_err());
}
#[test]
fn test_whittaker_w_asymptotic_accuracy() {
let kappa = 1.0;
let mu = 0.5;
let z = 50.0;
let w_asym = whittaker_w_asymptotic(kappa, mu, z).expect("ok");
assert!(w_asym.is_finite() && w_asym > 0.0, "W asymptotic: {w_asym}");
}
#[test]
fn test_whittaker_m_array() {
let zs = vec![0.5, 1.0, 2.0, 5.0];
let ms = whittaker_m_array(1.0, 0.5, &zs).expect("ok");
assert_eq!(ms.len(), 4);
for (i, &m) in ms.iter().enumerate() {
let m_scalar = whittaker_m_series(1.0, 0.5, zs[i]).expect("ok");
assert!(
(m - m_scalar).abs() < 1e-14,
"Array[{i}]={m} vs scalar={m_scalar}"
);
}
}
#[test]
fn test_whittaker_w_array() {
let zs = vec![1.0, 5.0, 10.0, 30.0];
let ws = whittaker_w_array(0.5, 0.5, &zs).expect("ok");
assert_eq!(ws.len(), 4);
for &w in &ws {
assert!(w.is_finite(), "W should be finite");
}
}
#[test]
fn test_whittaker_wronskian() {
let w = whittaker_wronskian(0.5, 1.0).expect("ok");
assert!(w.is_finite(), "Wronskian should be finite: {w}");
}
#[test]
fn test_whittaker_w_at_varying_kappa() {
let mu = 0.5;
let z = 3.0;
for &kappa in &[-1.0, 0.0, 0.5, 1.0, 2.0] {
let w = whittaker_w_series(kappa, mu, z).expect("ok");
assert!(
w.is_finite(),
"W should be finite for kappa={kappa}: {w}"
);
}
}
#[test]
fn test_whittaker_m_kummer_connection() {
let kappa = 0.5;
let mu = 0.5;
let z = 2.0;
let m_val = whittaker_m_series(kappa, mu, z).expect("ok");
let a = mu - kappa + 0.5; let b = 2.0 * mu + 1.0; let hyp = hyp1f1_series(a, b, z).expect("ok");
let pref = (-z / 2.0 + (mu + 0.5) * z.ln()).exp();
let expected = pref * hyp;
assert!(
(m_val - expected).abs() < TOL * expected.abs().max(1e-10),
"Kummer connection: M={m_val}, expected={expected}"
);
}
#[test]
fn test_hyp1f1_series_special_cases() {
let h = hyp1f1_series(0.0, 2.0, 3.0).expect("ok");
assert!((h - 1.0).abs() < 1e-14, "1F1(0;b;z) should be 1: {h}");
let h0 = hyp1f1_series(2.0, 3.0, 0.0).expect("ok");
assert!((h0 - 1.0).abs() < 1e-14, "1F1(a;b;0) should be 1: {h0}");
}
#[test]
fn test_whittaker_m_array_error_propagation() {
let zs = vec![1.0, -1.0, 2.0];
assert!(whittaker_m_array(0.5, 0.5, &zs).is_err());
}
}