use crate::error::{SpecialError, SpecialResult};
fn heun_series_coeffs(
a: f64,
q: f64,
alpha: f64,
beta: f64,
gamma: f64,
delta: f64,
epsilon: f64,
n_terms: usize,
) -> SpecialResult<Vec<f64>> {
if a == 0.0 {
return Err(SpecialError::DomainError(
"Heun singular point 'a' must be non-zero".to_string(),
));
}
if gamma == 0.0 || gamma.fract() < 0.0 && (-gamma).fract() == 0.0 {
}
let mut c = vec![0.0f64; n_terms];
if n_terms == 0 {
return Ok(c);
}
c[0] = 1.0;
if n_terms == 1 {
return Ok(c);
}
let p0 = a * gamma;
if p0.abs() < f64::EPSILON * 1e3 {
return Err(SpecialError::DomainError(
"Denominator a*gamma is too small; series ill-defined".to_string(),
));
}
c[1] = q * c[0] / p0;
for n in 1..(n_terms - 1) {
let nf = n as f64;
let p_n = a * (nf + 1.0) * (nf + gamma);
let q_n = nf * ((nf - 1.0 + gamma + delta) * a + (nf - 1.0 + gamma + epsilon)) + q;
let r_n = (nf - 1.0 + alpha) * (nf - 1.0 + beta);
if p_n.abs() < f64::MIN_POSITIVE {
break;
}
c[n + 1] = (q_n * c[n] - r_n * c[n - 1]) / p_n;
if !c[n + 1].is_finite() {
c[n + 1] = 0.0;
break;
}
}
Ok(c)
}
fn eval_power_series(coeffs: &[f64], x: f64) -> f64 {
let n = coeffs.len();
if n == 0 {
return 0.0;
}
let mut result = coeffs[n - 1];
for k in (0..n - 1).rev() {
result = result * x + coeffs[k];
}
result
}
pub fn heun_local(
a: f64,
q: f64,
alpha: f64,
beta: f64,
gamma: f64,
delta: f64,
x: f64,
) -> SpecialResult<f64> {
let epsilon = alpha + beta + 1.0 - gamma - delta;
let radius = (1.0_f64).min(a.abs());
if x.abs() >= radius {
return Err(SpecialError::DomainError(format!(
"x={x} is outside the radius of convergence ({radius}) for heun_local"
)));
}
let coeffs = heun_series_coeffs(a, q, alpha, beta, gamma, delta, epsilon, 80)?;
Ok(eval_power_series(&coeffs, x))
}
pub fn heun_general(
a: f64,
q: f64,
alpha: f64,
beta: f64,
gamma: f64,
delta: f64,
epsilon: f64,
x: f64,
) -> SpecialResult<f64> {
let fuchsian_check = (gamma + delta + epsilon - alpha - beta - 1.0).abs();
if fuchsian_check > 1e-10 {
return Err(SpecialError::ValueError(format!(
"Fuchsian condition γ+δ+ε = α+β+1 violated: residual = {fuchsian_check:.3e}"
)));
}
let radius = (1.0_f64).min(a.abs());
if x.abs() >= radius {
return Err(SpecialError::DomainError(format!(
"x={x} is outside the radius of convergence ({radius}) for heun_general"
)));
}
let coeffs = heun_series_coeffs(a, q, alpha, beta, gamma, delta, epsilon, 80)?;
Ok(eval_power_series(&coeffs, x))
}
pub fn confluent_heun(
a: f64,
q: f64,
alpha: f64,
gamma: f64,
delta: f64,
x: f64,
) -> SpecialResult<f64> {
if x.abs() >= 1.0 {
return Err(SpecialError::DomainError(format!(
"confluent_heun: x={x} must satisfy |x| < 1 for series convergence"
)));
}
let n_terms = 80usize;
let mut d = vec![0.0f64; n_terms];
d[0] = 1.0;
if n_terms == 1 {
return Ok(eval_power_series(&d, x));
}
let a0 = gamma;
if a0.abs() < f64::MIN_POSITIVE * 1e10 {
return Err(SpecialError::DomainError(
"confluent_heun: gamma=0 makes first recurrence step ill-defined".to_string(),
));
}
let b0 = -(a * alpha + q);
d[1] = -b0 * d[0] / a0;
for n in 1..(n_terms - 1) {
let nf = n as f64;
let a_n = (nf + 1.0) * (nf + gamma);
let b_n = -(nf * (nf + gamma + delta - 1.0) + a * alpha + q);
let c_n = nf - 1.0 + alpha;
if a_n.abs() < f64::MIN_POSITIVE {
break;
}
d[n + 1] = -(b_n * d[n] + c_n * d[n - 1]) / a_n;
if !d[n + 1].is_finite() {
d[n + 1] = 0.0;
break;
}
}
Ok(eval_power_series(&d, x))
}
pub fn double_confluent_heun(
a: f64,
b: f64,
alpha: f64,
gamma: f64,
x: f64,
) -> SpecialResult<f64> {
let n_terms = 60usize;
let mut e = vec![0.0f64; n_terms];
e[0] = 1.0;
if n_terms == 1 {
return Ok(eval_power_series(&e, x));
}
e[1] = -gamma * e[0];
for n in 1..(n_terms - 1) {
let nf = n as f64;
let coeff_n = b * nf + gamma;
let coeff_nm1 = a * (nf - 1.0) + alpha;
e[n + 1] = (-coeff_n * e[n] - coeff_nm1 * e[n - 1]) / (nf + 1.0);
if !e[n + 1].is_finite() {
e[n + 1] = 0.0;
break;
}
}
Ok(eval_power_series(&e, x))
}
pub fn mathieu_via_heun(
a_mat: f64,
q_mat: f64,
z: f64,
n_terms: usize,
) -> SpecialResult<f64> {
if n_terms == 0 {
return Err(SpecialError::ValueError(
"n_terms must be at least 1".to_string(),
));
}
let n = n_terms.min(50);
let mut coeffs = vec![0.0f64; n + 2];
coeffs[n] = 0.0;
coeffs[n + 1] = 1e-30;
for k in (1..=n).rev() {
let kf = (2 * k) as f64;
let kf_prev = (2 * (k - 1)) as f64;
let denom = a_mat - kf_prev * kf_prev;
if denom.abs() < f64::EPSILON * 1e6 {
coeffs[k - 1] = 0.0;
} else {
coeffs[k - 1] = (denom * coeffs[k] - q_mat * coeffs[k + 1]) / q_mat.max(f64::EPSILON);
}
}
let norm = coeffs[0].abs().max(1e-300);
let mut result = 0.0;
for k in 0..n {
let arg = (2 * k) as f64 * z;
result += (coeffs[k] / norm) * arg.cos();
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_heun_local_at_zero() {
let v = heun_local(2.0, 0.0, 0.5, 0.5, 1.0, 1.0, 0.0).expect("heun_local x=0");
assert!((v - 1.0).abs() < 1e-14, "Hl(x=0) = {v}");
}
#[test]
fn test_heun_local_small_x() {
let a = 2.0;
let q = 1.0;
let gamma = 1.0;
let alpha = 0.5;
let beta = 0.5;
let delta = 1.0;
let x = 0.01;
let v = heun_local(a, q, alpha, beta, gamma, delta, x).expect("heun_local small x");
let expected_linear = 1.0 + q / (a * gamma) * x;
assert!((v - expected_linear).abs() < 1e-6, "linear approx: {v} vs {expected_linear}");
}
#[test]
fn test_heun_general_fuchsian_check() {
let result = heun_general(2.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.1);
assert!(result.is_err());
}
#[test]
fn test_heun_general_fuchsian_satisfied() {
let v = heun_general(2.0, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 0.1).expect("heun_general valid");
assert!(v.is_finite(), "heun_general: {v}");
}
#[test]
fn test_confluent_heun_at_zero() {
let v = confluent_heun(1.0, 0.5, 1.0, 1.0, 1.0, 0.0).expect("confluent x=0");
assert!((v - 1.0).abs() < 1e-14, "HeunC(x=0) = {v}");
}
#[test]
fn test_confluent_heun_small_x() {
let v1 = confluent_heun(1.0, 0.5, 1.0, 1.0, 1.0, 0.1).expect("confluent 0.1");
let v2 = confluent_heun(1.0, 0.5, 1.0, 1.0, 1.0, 0.2).expect("confluent 0.2");
assert!(v1.is_finite() && v2.is_finite(), "both finite: {v1}, {v2}");
assert!((v2 - v1).abs() < 1.0, "continuity check: {v1} vs {v2}");
}
#[test]
fn test_double_confluent_at_zero() {
let v = double_confluent_heun(0.1, 0.2, 0.3, 0.4, 0.0).expect("double confluent x=0");
assert!((v - 1.0).abs() < 1e-14, "HeunDC(x=0) = {v}");
}
#[test]
fn test_double_confluent_small_x() {
let x = 0.01;
let v = double_confluent_heun(0.1, 0.2, 0.3, 0.4, x).expect("double confluent small x");
let expected = 1.0 - 0.4 * x;
assert!((v - expected).abs() < 1e-6, "linear approx: {v} vs {expected}");
}
#[test]
fn test_heun_outside_radius() {
let result = heun_local(2.0, 0.0, 0.5, 0.5, 1.0, 1.0, 1.5);
assert!(result.is_err());
}
#[test]
fn test_mathieu_via_heun() {
let v = mathieu_via_heun(0.0, 0.0, 0.5, 10).expect("mathieu_via_heun q=0");
assert!(v.is_finite(), "mathieu q=0: {v}");
}
#[test]
fn test_heun_hypergeometric_reduction() {
let a = 3.0;
let alpha = 0.5;
let beta = 0.5;
let gamma = 1.0;
let delta = 0.5; let q = 0.1;
let x = 0.3;
let v = heun_local(a, q, alpha, beta, gamma, delta, x).expect("heun reduction test");
assert!(v.is_finite(), "Heun reduction test: {v}");
}
}