use crate::error::{SpecialError, SpecialResult};
use crate::gamma::{gamma, gammaln};
use scirs2_core::numeric::{Float, FromPrimitive};
use std::fmt::Debug;
const MAX_DOUBLE_SERIES_TERMS: usize = 100;
const MAX_TOTAL_TERMS: usize = 5000;
const CONVERGENCE_TOL: f64 = 1e-15;
#[inline(always)]
fn const_f64<F: Float + FromPrimitive>(value: f64) -> F {
F::from(value).unwrap_or_else(|| {
if value > 0.0 {
F::infinity()
} else if value < 0.0 {
F::neg_infinity()
} else {
F::zero()
}
})
}
pub fn appell_f1(a: f64, b1: f64, b2: f64, c: f64, x: f64, y: f64) -> SpecialResult<f64> {
if c <= 0.0 && c.fract() == 0.0 {
return Err(SpecialError::DomainError(format!(
"c must not be 0 or negative integer, got {c}"
)));
}
if x.abs() >= 1.0 || y.abs() >= 1.0 {
return Err(SpecialError::DomainError(
"Appell F1 requires |x| < 1 and |y| < 1".to_string(),
));
}
if x.abs() < 1e-300 && y.abs() < 1e-300 {
return Ok(1.0);
}
if y.abs() < 1e-300 {
return crate::hypergeometric::hyp2f1(a, b1, c, x).map(|v: f64| v);
}
if x.abs() < 1e-300 {
return crate::hypergeometric::hyp2f1(a, b2, c, y).map(|v: f64| v);
}
let mut result = 0.0;
let mut total_terms = 0usize;
let mut x_pow_m = 1.0; let mut pochhammer_b1_m = 1.0; let mut m_factorial = 1.0;
for m in 0..MAX_DOUBLE_SERIES_TERMS {
if m > 0 {
x_pow_m *= x;
pochhammer_b1_m *= b1 + (m - 1) as f64;
m_factorial *= m as f64;
}
if x_pow_m.abs() < 1e-300 && m > 0 {
break;
}
let mut y_pow_n = 1.0;
let mut pochhammer_b2_n = 1.0;
let mut n_factorial = 1.0;
let mut inner_sum = 0.0;
let mut pochhammer_a_mn = pochhammer_a_precompute(a, m);
let mut pochhammer_c_mn = pochhammer_a_precompute(c, m);
for n in 0..MAX_DOUBLE_SERIES_TERMS {
if n > 0 {
y_pow_n *= y;
pochhammer_b2_n *= b2 + (n - 1) as f64;
n_factorial *= n as f64;
pochhammer_a_mn *= a + (m + n - 1) as f64;
pochhammer_c_mn *= c + (m + n - 1) as f64;
}
total_terms += 1;
if total_terms > MAX_TOTAL_TERMS {
break;
}
if pochhammer_c_mn.abs() < 1e-300 {
return Err(SpecialError::DomainError(
"Division by zero: (c)_{m+n} = 0".to_string(),
));
}
let term = pochhammer_a_mn * pochhammer_b1_m * pochhammer_b2_n
/ (pochhammer_c_mn * m_factorial * n_factorial)
* x_pow_m
* y_pow_n;
inner_sum += term;
if n > 0 && term.abs() < CONVERGENCE_TOL * inner_sum.abs() {
break;
}
}
result += inner_sum;
if m > 0 && inner_sum.abs() < CONVERGENCE_TOL * result.abs() {
break;
}
if total_terms > MAX_TOTAL_TERMS {
break;
}
}
Ok(result)
}
pub fn appell_f2(a: f64, b1: f64, b2: f64, c1: f64, c2: f64, x: f64, y: f64) -> SpecialResult<f64> {
if (c1 <= 0.0 && c1.fract() == 0.0) || (c2 <= 0.0 && c2.fract() == 0.0) {
return Err(SpecialError::DomainError(
"c1 and c2 must not be 0 or negative integers".to_string(),
));
}
if x.abs() + y.abs() >= 1.0 {
return Err(SpecialError::DomainError(
"Appell F2 requires |x| + |y| < 1".to_string(),
));
}
if x.abs() < 1e-300 && y.abs() < 1e-300 {
return Ok(1.0);
}
if y.abs() < 1e-300 {
return crate::hypergeometric::hyp2f1(a, b1, c1, x).map(|v: f64| v);
}
if x.abs() < 1e-300 {
return crate::hypergeometric::hyp2f1(a, b2, c2, y).map(|v: f64| v);
}
let mut result = 0.0;
let mut total_terms = 0usize;
let mut x_pow_m = 1.0;
let mut pochhammer_b1_m = 1.0;
let mut pochhammer_c1_m = 1.0;
let mut m_factorial = 1.0;
for m in 0..MAX_DOUBLE_SERIES_TERMS {
if m > 0 {
x_pow_m *= x;
pochhammer_b1_m *= b1 + (m - 1) as f64;
pochhammer_c1_m *= c1 + (m - 1) as f64;
m_factorial *= m as f64;
}
if pochhammer_c1_m.abs() < 1e-300 && m > 0 {
return Err(SpecialError::DomainError("(c1)_m = 0".to_string()));
}
let mut y_pow_n = 1.0;
let mut pochhammer_b2_n = 1.0;
let mut pochhammer_c2_n = 1.0;
let mut n_factorial = 1.0;
let mut inner_sum = 0.0;
let mut pochhammer_a_mn = pochhammer_a_precompute(a, m);
for n in 0..MAX_DOUBLE_SERIES_TERMS {
if n > 0 {
y_pow_n *= y;
pochhammer_b2_n *= b2 + (n - 1) as f64;
pochhammer_c2_n *= c2 + (n - 1) as f64;
n_factorial *= n as f64;
pochhammer_a_mn *= a + (m + n - 1) as f64;
}
total_terms += 1;
if total_terms > MAX_TOTAL_TERMS {
break;
}
if pochhammer_c2_n.abs() < 1e-300 && n > 0 {
return Err(SpecialError::DomainError("(c2)_n = 0".to_string()));
}
let denom = pochhammer_c1_m * pochhammer_c2_n * m_factorial * n_factorial;
if denom.abs() < 1e-300 {
continue;
}
let term =
pochhammer_a_mn * pochhammer_b1_m * pochhammer_b2_n / denom * x_pow_m * y_pow_n;
inner_sum += term;
if n > 0 && term.abs() < CONVERGENCE_TOL * inner_sum.abs().max(1e-300) {
break;
}
}
result += inner_sum;
if m > 0 && inner_sum.abs() < CONVERGENCE_TOL * result.abs().max(1e-300) {
break;
}
if total_terms > MAX_TOTAL_TERMS {
break;
}
}
Ok(result)
}
pub fn meijer_g_01_10(z: f64, b: f64) -> SpecialResult<f64> {
if z <= 0.0 {
return Err(SpecialError::DomainError(
"z must be positive for this Meijer G case".to_string(),
));
}
let gamma_bp1 = gamma(b + 1.0);
if gamma_bp1.abs() < 1e-300 || !gamma_bp1.is_finite() {
return Err(SpecialError::OverflowError(
"Gamma(b+1) overflow or zero in Meijer G".to_string(),
));
}
Ok((-z).exp() * z.powf(b) / gamma_bp1)
}
pub fn meijer_g_10_01(z: f64, a: f64) -> SpecialResult<f64> {
if z < 0.0 {
return Err(SpecialError::DomainError(
"z must be non-negative for this Meijer G case".to_string(),
));
}
if z == 0.0 {
if a > 0.0 {
return Ok(0.0);
} else if a == 0.0 {
return Ok(1.0);
} else {
return Ok(f64::INFINITY);
}
}
Ok(z.powf(a) * (-z).exp())
}
pub fn meijer_g_11_11(z: f64, a: f64, b: f64) -> SpecialResult<f64> {
if z <= 0.0 {
return Err(SpecialError::DomainError(
"z must be positive for G_{1,1}^{1,1}".to_string(),
));
}
let cond = 1.0 - a + b;
if cond <= 0.0 {
return Err(SpecialError::DomainError(format!(
"Need 1-a+b > 0, got {cond}"
)));
}
let gamma_cond = gamma(cond);
let gamma_a = gamma(a);
if gamma_a.abs() < 1e-300 || !gamma_a.is_finite() {
return Err(SpecialError::OverflowError(
"Gamma(a) overflow in Meijer G".to_string(),
));
}
Ok(gamma_cond * z.powf(b) * (1.0 + z).powf(a - b - 1.0) / gamma_a)
}
pub fn hyp_pfq(a_params: &[f64], b_params: &[f64], z: f64) -> SpecialResult<f64> {
let p = a_params.len();
let q = b_params.len();
for (i, &b) in b_params.iter().enumerate() {
if b <= 0.0 && b.fract() == 0.0 {
return Err(SpecialError::DomainError(format!(
"b_{} = {b} is a non-positive integer",
i + 1
)));
}
}
if p > q + 1 {
let is_terminating = a_params.iter().any(|&a| a <= 0.0 && a.fract() == 0.0);
if !is_terminating {
return Err(SpecialError::DomainError(format!(
"p={p} > q+1={}: series diverges for non-terminating case",
q + 1
)));
}
}
if p == q + 1 && z.abs() >= 1.0 {
let is_terminating = a_params.iter().any(|&a| a <= 0.0 && a.fract() == 0.0);
if !is_terminating {
return Err(SpecialError::DomainError(
"p = q+1 and |z| >= 1: series may not converge".to_string(),
));
}
}
if z.abs() < 1e-300 {
return Ok(1.0);
}
let mut result = 1.0;
let mut term = 1.0;
let max_terms = 1000;
for n in 1..=max_terms {
let n_f = n as f64;
let mut numer_factor = 1.0;
for &a in a_params {
numer_factor *= a + n_f - 1.0;
}
let mut denom_factor = 1.0;
for &b in b_params {
let bf = b + n_f - 1.0;
if bf.abs() < 1e-300 {
return Err(SpecialError::DomainError(
"Denominator parameter hit zero".to_string(),
));
}
denom_factor *= bf;
}
term *= numer_factor * z / (denom_factor * n_f);
result += term;
if numer_factor.abs() < 1e-300 {
break;
}
if term.abs() < CONVERGENCE_TOL * result.abs() {
break;
}
if !result.is_finite() {
return Err(SpecialError::OverflowError(
"Overflow in pFq series".to_string(),
));
}
}
Ok(result)
}
pub fn hyp3f2(a1: f64, a2: f64, a3: f64, b1: f64, b2: f64, z: f64) -> SpecialResult<f64> {
hyp_pfq(&[a1, a2, a3], &[b1, b2], z)
}
pub fn pochhammer_real(a: f64, x: f64) -> SpecialResult<f64> {
if x == 0.0 {
return Ok(1.0);
}
if x > 0.0 && x == x.floor() && x < 100.0 {
let n = x as usize;
let mut result = 1.0;
for k in 0..n {
result *= a + k as f64;
}
return Ok(result);
}
let gamma_ax = gamma(a + x);
let gamma_a = gamma(a);
if gamma_a.abs() < 1e-300 {
return Err(SpecialError::DomainError(format!(
"Gamma({a}) is zero or near-zero"
)));
}
if !gamma_ax.is_finite() || !gamma_a.is_finite() {
let log_result = gammaln(a + x) - gammaln(a);
if log_result.is_finite() {
return Ok(log_result.exp());
}
}
Ok(gamma_ax / gamma_a)
}
pub fn pochhammer_regularized(a: f64, n: usize) -> SpecialResult<f64> {
let gamma_a = gamma(a);
if gamma_a.abs() < 1e-300 || !gamma_a.is_finite() {
return Err(SpecialError::DomainError(format!(
"Gamma({a}) is not finite or zero"
)));
}
Ok(1.0 / gamma_a)
}
pub fn hyp1f1_asymptotic(a: f64, b: f64, z: f64) -> SpecialResult<f64> {
if b <= 0.0 && b.fract() == 0.0 {
return Err(SpecialError::DomainError(format!(
"b must not be 0 or negative integer, got {b}"
)));
}
if z.abs() < 50.0 {
return crate::hypergeometric::hyp1f1(a, b, z);
}
if z > 0.0 {
let log_gamma_b = gammaln(b);
let log_gamma_a = gammaln(a);
let log_gamma_ba = gammaln(b - a);
let log_term1 = log_gamma_b - log_gamma_a + z + (a - b) * z.ln();
let log_term2 = log_gamma_b - log_gamma_ba + (-a) * (-z).abs().ln();
let mut sum1 = 1.0;
let mut coeff1 = 1.0;
for s in 1..20 {
let s_f = s as f64;
coeff1 *= (b - a + s_f - 1.0) * (1.0 - a + s_f - 1.0) / (s_f * z);
if coeff1.abs() < 1e-15 {
break;
}
sum1 += coeff1;
if coeff1.abs() > sum1.abs() {
break;
}
}
let mut sum2 = 1.0;
let mut coeff2 = 1.0;
for s in 1..20 {
let s_f = s as f64;
coeff2 *= (a + s_f - 1.0) * (a - b + s_f) / (s_f * (-z));
if coeff2.abs() < 1e-15 {
break;
}
sum2 += coeff2;
if coeff2.abs() > sum2.abs() {
break;
}
}
let term1 = if log_term1.is_finite() {
log_term1.exp() * sum1
} else {
0.0
};
let sign2 = if ((-a) as i64) % 2 == 0 { 1.0 } else { -1.0 };
let term2 = if log_term2.is_finite() {
sign2 * log_term2.exp() * sum2
} else {
0.0
};
return Ok(term1 + term2);
}
let result = crate::hypergeometric::hyp1f1(b - a, b, -z)?;
Ok(z.exp() * result)
}
pub fn tricomi_u(a: f64, b: f64, z: f64) -> SpecialResult<f64> {
if z <= 0.0 {
return Err(SpecialError::DomainError(
"z must be positive for Tricomi U".to_string(),
));
}
crate::hypergeometric::hyperu(a, b, z)
}
pub fn kummer_transform<
F: Float + FromPrimitive + Debug + std::ops::AddAssign + std::ops::MulAssign,
>(
a: F,
b: F,
z: F,
) -> SpecialResult<F> {
let b_minus_a = b - a;
let neg_z = -z;
let m_val: F = crate::hypergeometric::hyp1f1(b_minus_a, b, neg_z)?;
Ok(z.exp() * m_val)
}
fn pochhammer_a_precompute(a: f64, n: usize) -> f64 {
if n == 0 {
return 1.0;
}
let mut result = 1.0;
for k in 0..n {
result *= a + k as f64;
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_appell_f1_zero() {
let val = appell_f1(1.0, 2.0, 3.0, 4.0, 0.0, 0.0).expect("failed");
assert_relative_eq!(val, 1.0, epsilon = 1e-14);
}
#[test]
fn test_appell_f1_y_zero() {
let x = 0.3;
let f1 = appell_f1(1.0, 2.0, 3.0, 4.0, x, 0.0).expect("failed");
let h: f64 = crate::hypergeometric::hyp2f1(1.0, 2.0, 4.0, x).expect("failed");
assert_relative_eq!(f1, h, epsilon = 1e-10);
}
#[test]
fn test_appell_f1_x_zero() {
let y = 0.3;
let f1 = appell_f1(1.0, 2.0, 3.0, 4.0, 0.0, y).expect("failed");
let h: f64 = crate::hypergeometric::hyp2f1(1.0, 3.0, 4.0, y).expect("failed");
assert_relative_eq!(f1, h, epsilon = 1e-10);
}
#[test]
fn test_appell_f1_small_args() {
let val = appell_f1(1.0, 1.0, 1.0, 2.0, 0.1, 0.1).expect("failed");
assert!(val > 1.0, "F1 should be > 1 for positive args");
assert!(val < 2.0, "F1 should be bounded for small args");
}
#[test]
fn test_appell_f1_domain_error() {
assert!(appell_f1(1.0, 2.0, 3.0, 4.0, 1.5, 0.0).is_err());
assert!(appell_f1(1.0, 2.0, 3.0, 0.0, 0.5, 0.5).is_err());
}
#[test]
fn test_appell_f2_zero() {
let val = appell_f2(1.0, 2.0, 3.0, 4.0, 5.0, 0.0, 0.0).expect("failed");
assert_relative_eq!(val, 1.0, epsilon = 1e-14);
}
#[test]
fn test_appell_f2_y_zero() {
let x = 0.2;
let f2 = appell_f2(1.0, 2.0, 3.0, 4.0, 5.0, x, 0.0).expect("failed");
let h: f64 = crate::hypergeometric::hyp2f1(1.0, 2.0, 4.0, x).expect("failed");
assert_relative_eq!(f2, h, epsilon = 1e-10);
}
#[test]
fn test_appell_f2_x_zero() {
let y = 0.2;
let f2 = appell_f2(1.0, 2.0, 3.0, 4.0, 5.0, 0.0, y).expect("failed");
let h: f64 = crate::hypergeometric::hyp2f1(1.0, 3.0, 5.0, y).expect("failed");
assert_relative_eq!(f2, h, epsilon = 1e-10);
}
#[test]
fn test_appell_f2_domain_error() {
assert!(appell_f2(1.0, 2.0, 3.0, 4.0, 5.0, 0.6, 0.5).is_err());
}
#[test]
fn test_meijer_g_01_10_exp() {
let val = meijer_g_01_10(1.0, 0.0).expect("failed");
assert_relative_eq!(val, (-1.0_f64).exp(), epsilon = 1e-12);
}
#[test]
fn test_meijer_g_01_10_b1() {
let z = 2.0;
let val = meijer_g_01_10(z, 1.0).expect("failed");
assert_relative_eq!(val, z * (-z).exp(), epsilon = 1e-12);
}
#[test]
fn test_meijer_g_10_01() {
let z = 2.0;
let val = meijer_g_10_01(z, 1.0).expect("failed");
assert_relative_eq!(val, z * (-z).exp(), epsilon = 1e-12);
}
#[test]
fn test_meijer_g_11_11() {
let z = 2.0;
let a = 0.5;
let b = 0.0;
let val = meijer_g_11_11(z, a, b).expect("failed");
let expected = gamma(1.0 - a + b) * z.powf(b) * (1.0 + z).powf(a - b - 1.0) / gamma(a);
assert_relative_eq!(val, expected, epsilon = 1e-12);
}
#[test]
fn test_meijer_g_domain_errors() {
assert!(meijer_g_01_10(-1.0, 0.0).is_err());
assert!(meijer_g_10_01(-1.0, 1.0).is_err());
assert!(meijer_g_11_11(-1.0, 1.0, 0.0).is_err());
}
#[test]
fn test_hyp_pfq_1f0() {
let val = hyp_pfq(&[1.0], &[], 0.5).expect("failed");
assert_relative_eq!(val, 2.0, epsilon = 1e-10);
}
#[test]
fn test_hyp_pfq_0f0() {
let val = hyp_pfq(&[], &[], 1.0).expect("failed");
assert_relative_eq!(val, 1.0_f64.exp(), epsilon = 1e-10);
}
#[test]
fn test_hyp_pfq_0f1() {
let val = hyp_pfq(&[], &[1.0], 0.5).expect("failed");
assert!(val.is_finite());
}
#[test]
fn test_hyp_pfq_zero() {
let val = hyp_pfq(&[1.0, 2.0], &[3.0], 0.0).expect("failed");
assert_relative_eq!(val, 1.0, epsilon = 1e-14);
}
#[test]
fn test_hyp3f2_finite() {
let val = hyp3f2(1.0, 1.0, 1.0, 2.0, 2.0, 0.5).expect("failed");
assert!(val.is_finite());
assert!(val > 1.0);
}
#[test]
fn test_hyp_pfq_terminating() {
let val = hyp_pfq(&[-2.0, 1.0], &[1.0], 0.5).expect("failed");
assert_relative_eq!(val, 0.25, epsilon = 1e-10);
}
#[test]
fn test_hyp_pfq_domain_error() {
assert!(hyp_pfq(&[1.0], &[-1.0], 0.5).is_err());
}
#[test]
fn test_pochhammer_real_integer() {
let val = pochhammer_real(1.0, 3.0).expect("failed");
assert_relative_eq!(val, 6.0, epsilon = 1e-10);
}
#[test]
fn test_pochhammer_real_half() {
let val = pochhammer_real(0.5, 2.0).expect("failed");
assert_relative_eq!(val, 0.75, epsilon = 1e-10);
}
#[test]
fn test_pochhammer_real_zero_index() {
let val = pochhammer_real(5.0, 0.0).expect("failed");
assert_relative_eq!(val, 1.0, epsilon = 1e-14);
}
#[test]
fn test_pochhammer_regularized() {
let val = pochhammer_regularized(2.0, 3).expect("failed");
assert_relative_eq!(val, 1.0, epsilon = 1e-12);
}
#[test]
fn test_hyp1f1_asymptotic_small_z() {
let a = 1.0;
let b = 2.0;
let z = 0.5;
let val = hyp1f1_asymptotic(a, b, z).expect("failed");
let expected: f64 = crate::hypergeometric::hyp1f1(a, b, z).expect("failed");
assert_relative_eq!(val, expected, epsilon = 1e-10);
}
#[test]
fn test_hyp1f1_asymptotic_domain() {
assert!(hyp1f1_asymptotic(1.0, 0.0, 1.0).is_err());
assert!(hyp1f1_asymptotic(1.0, -1.0, 1.0).is_err());
}
#[test]
fn test_tricomi_u_basic() {
let val = tricomi_u(1.0, 0.5, 3.0).expect("failed");
assert!(val.is_finite());
assert!(
(val - 1.0 / 3.0).abs() < 0.2,
"U(1, 0.5, 3) ~ {val}, expected ~0.333"
);
}
#[test]
fn test_tricomi_u_domain() {
assert!(tricomi_u(1.0, 1.0, -1.0).is_err());
}
#[test]
fn test_kummer_transform() {
let a = 1.0f64;
let b = 2.0f64;
let z = 0.5f64;
let direct: f64 = crate::hypergeometric::hyp1f1(a, b, z).expect("failed");
let via_kummer: f64 = kummer_transform(a, b, z).expect("failed");
assert_relative_eq!(direct, via_kummer, epsilon = 1e-8);
}
}