use crate::error::{SpecialError, SpecialResult};
const MAX_SINGLE_DIM_TERMS: usize = 80;
const MAX_TOTAL_TERMS: usize = 10_000;
const CONV_TOL: f64 = 1e-14;
#[inline]
fn pochhammer(a: f64, n: usize) -> f64 {
if n == 0 {
return 1.0;
}
let mut result = 1.0_f64;
for k in 0..n {
result *= a + k as f64;
if result == 0.0 {
return 0.0;
}
}
result
}
pub fn appell_f1(
a: f64,
b1: f64,
b2: f64,
c: f64,
x: f64,
y: f64,
n_terms: usize,
) -> SpecialResult<f64> {
if c <= 0.0 && (c.fract().abs() < 1e-10) && c.round() as i64 <= 0 {
return Err(SpecialError::DomainError(format!(
"Appell F1: c = {c} must not be a non-positive integer"
)));
}
if x.abs() >= 1.0 {
return Err(SpecialError::DomainError(format!(
"Appell F1: |x| = {} >= 1 is outside the convergence region",
x.abs()
)));
}
if y.abs() >= 1.0 {
return Err(SpecialError::DomainError(format!(
"Appell F1: |y| = {} >= 1 is outside the convergence region",
y.abs()
)));
}
if x.abs() < f64::EPSILON && y.abs() < f64::EPSILON {
return Ok(1.0);
}
let max_terms = n_terms.min(MAX_SINGLE_DIM_TERMS);
let mut result = 0.0_f64;
let mut total = 0usize;
let mut x_m = 1.0_f64; let mut poch_b1_m = 1.0_f64; let mut fact_m = 1.0_f64;
for m in 0..max_terms {
if m > 0 {
x_m *= x;
poch_b1_m *= b1 + (m - 1) as f64;
fact_m *= m as f64;
}
if x_m.abs() < f64::EPSILON * 1e-10 && m > 0 {
break;
}
let mut y_n = 1.0_f64;
let mut poch_b2_n = 1.0_f64;
let mut fact_n = 1.0_f64;
let mut poch_a_mn = pochhammer(a, m);
let mut poch_c_mn = pochhammer(c, m);
let mut col_sum = 0.0_f64;
for n in 0..max_terms {
if n > 0 {
y_n *= y;
poch_b2_n *= b2 + (n - 1) as f64;
fact_n *= n as f64;
poch_a_mn *= a + (m + n - 1) as f64;
poch_c_mn *= c + (m + n - 1) as f64;
}
total += 1;
if total > MAX_TOTAL_TERMS {
break;
}
if poch_c_mn.abs() < 1e-300 {
return Err(SpecialError::DomainError(
"Appell F1: (c)_{m+n} = 0; denominator vanishes".to_string(),
));
}
let term = poch_a_mn * poch_b1_m * poch_b2_n
/ (poch_c_mn * fact_m * fact_n)
* x_m
* y_n;
col_sum += term;
if n > 2 && term.abs() < CONV_TOL * col_sum.abs().max(1e-300) {
break;
}
}
result += col_sum;
if m > 2 && col_sum.abs() < CONV_TOL * result.abs().max(1e-300) {
break;
}
if total > MAX_TOTAL_TERMS {
break;
}
}
Ok(result)
}
pub fn appell_f2(
a: f64,
b1: f64,
b2: f64,
c1: f64,
c2: f64,
x: f64,
y: f64,
n_terms: usize,
) -> SpecialResult<f64> {
for (ci, name) in &[(c1, "c1"), (c2, "c2")] {
if *ci <= 0.0 && (ci.fract().abs() < 1e-10) && ci.round() as i64 <= 0 {
return Err(SpecialError::DomainError(format!(
"Appell F2: {name} = {ci} must not be a non-positive integer"
)));
}
}
if x.abs() + y.abs() >= 1.0 {
return Err(SpecialError::DomainError(format!(
"Appell F2: |x|+|y| = {} >= 1 is outside the convergence region",
x.abs() + y.abs()
)));
}
if x.abs() < f64::EPSILON && y.abs() < f64::EPSILON {
return Ok(1.0);
}
let max_terms = n_terms.min(MAX_SINGLE_DIM_TERMS);
let mut result = 0.0_f64;
let mut total = 0usize;
let mut x_m = 1.0_f64;
let mut poch_b1_m = 1.0_f64;
let mut poch_c1_m = 1.0_f64;
let mut fact_m = 1.0_f64;
for m in 0..max_terms {
if m > 0 {
x_m *= x;
poch_b1_m *= b1 + (m - 1) as f64;
poch_c1_m *= c1 + (m - 1) as f64;
fact_m *= m as f64;
}
if poch_c1_m.abs() < 1e-300 && m > 0 {
return Err(SpecialError::DomainError("Appell F2: (c1)_m = 0".to_string()));
}
let mut y_n = 1.0_f64;
let mut poch_b2_n = 1.0_f64;
let mut poch_c2_n = 1.0_f64;
let mut fact_n = 1.0_f64;
let mut poch_a_mn = pochhammer(a, m);
let mut col_sum = 0.0_f64;
for n in 0..max_terms {
if n > 0 {
y_n *= y;
poch_b2_n *= b2 + (n - 1) as f64;
poch_c2_n *= c2 + (n - 1) as f64;
fact_n *= n as f64;
poch_a_mn *= a + (m + n - 1) as f64;
}
total += 1;
if total > MAX_TOTAL_TERMS {
break;
}
if poch_c2_n.abs() < 1e-300 && n > 0 {
return Err(SpecialError::DomainError("Appell F2: (c2)_n = 0".to_string()));
}
let denom = poch_c1_m * poch_c2_n * fact_m * fact_n;
if denom.abs() < 1e-300 {
continue;
}
let term = poch_a_mn * poch_b1_m * poch_b2_n / denom * x_m * y_n;
col_sum += term;
if n > 2 && term.abs() < CONV_TOL * col_sum.abs().max(1e-300) {
break;
}
}
result += col_sum;
if m > 2 && col_sum.abs() < CONV_TOL * result.abs().max(1e-300) {
break;
}
if total > MAX_TOTAL_TERMS {
break;
}
}
Ok(result)
}
pub fn appell_f3(
a1: f64,
a2: f64,
b1: f64,
b2: f64,
c: f64,
x: f64,
y: f64,
n_terms: usize,
) -> SpecialResult<f64> {
if c <= 0.0 && (c.fract().abs() < 1e-10) && c.round() as i64 <= 0 {
return Err(SpecialError::DomainError(format!(
"Appell F3: c = {c} must not be a non-positive integer"
)));
}
if x.abs() >= 1.0 || y.abs() >= 1.0 {
return Err(SpecialError::DomainError(format!(
"Appell F3: |x|={}, |y|={} must be < 1",
x.abs(),
y.abs()
)));
}
if x.abs() < f64::EPSILON && y.abs() < f64::EPSILON {
return Ok(1.0);
}
let max_terms = n_terms.min(MAX_SINGLE_DIM_TERMS);
let mut result = 0.0_f64;
let mut total = 0usize;
let mut x_m = 1.0_f64;
let mut poch_a1_m = 1.0_f64;
let mut poch_b1_m = 1.0_f64;
let mut fact_m = 1.0_f64;
for m in 0..max_terms {
if m > 0 {
x_m *= x;
poch_a1_m *= a1 + (m - 1) as f64;
poch_b1_m *= b1 + (m - 1) as f64;
fact_m *= m as f64;
}
let mut y_n = 1.0_f64;
let mut poch_a2_n = 1.0_f64;
let mut poch_b2_n = 1.0_f64;
let mut fact_n = 1.0_f64;
let mut poch_c_mn = pochhammer(c, m);
let mut col_sum = 0.0_f64;
for n in 0..max_terms {
if n > 0 {
y_n *= y;
poch_a2_n *= a2 + (n - 1) as f64;
poch_b2_n *= b2 + (n - 1) as f64;
fact_n *= n as f64;
poch_c_mn *= c + (m + n - 1) as f64;
}
total += 1;
if total > MAX_TOTAL_TERMS {
break;
}
if poch_c_mn.abs() < 1e-300 {
return Err(SpecialError::DomainError("Appell F3: (c)_{m+n} = 0".to_string()));
}
let denom = poch_c_mn * fact_m * fact_n;
let term = poch_a1_m * poch_a2_n * poch_b1_m * poch_b2_n / denom * x_m * y_n;
col_sum += term;
if n > 2 && term.abs() < CONV_TOL * col_sum.abs().max(1e-300) {
break;
}
}
result += col_sum;
if m > 2 && col_sum.abs() < CONV_TOL * result.abs().max(1e-300) {
break;
}
if total > MAX_TOTAL_TERMS {
break;
}
}
Ok(result)
}
pub fn appell_f4(
a: f64,
b: f64,
c1: f64,
c2: f64,
x: f64,
y: f64,
n_terms: usize,
) -> SpecialResult<f64> {
for (ci, name) in &[(c1, "c1"), (c2, "c2")] {
if *ci <= 0.0 && (ci.fract().abs() < 1e-10) && ci.round() as i64 <= 0 {
return Err(SpecialError::DomainError(format!(
"Appell F4: {name} = {ci} must not be a non-positive integer"
)));
}
}
if x.abs().sqrt() + y.abs().sqrt() >= 1.0 {
return Err(SpecialError::DomainError(format!(
"Appell F4: sqrt(|x|)+sqrt(|y|) = {} >= 1 is outside convergence region",
x.abs().sqrt() + y.abs().sqrt()
)));
}
if x.abs() < f64::EPSILON && y.abs() < f64::EPSILON {
return Ok(1.0);
}
let max_terms = n_terms.min(MAX_SINGLE_DIM_TERMS);
let mut result = 0.0_f64;
let mut total = 0usize;
let mut x_m = 1.0_f64;
let mut poch_c1_m = 1.0_f64;
let mut fact_m = 1.0_f64;
for m in 0..max_terms {
if m > 0 {
x_m *= x;
poch_c1_m *= c1 + (m - 1) as f64;
fact_m *= m as f64;
}
if poch_c1_m.abs() < 1e-300 && m > 0 {
return Err(SpecialError::DomainError("Appell F4: (c1)_m = 0".to_string()));
}
let mut y_n = 1.0_f64;
let mut poch_c2_n = 1.0_f64;
let mut fact_n = 1.0_f64;
let mut poch_a_mn = pochhammer(a, m);
let mut poch_b_mn = pochhammer(b, m);
let mut col_sum = 0.0_f64;
for n in 0..max_terms {
if n > 0 {
y_n *= y;
poch_c2_n *= c2 + (n - 1) as f64;
fact_n *= n as f64;
poch_a_mn *= a + (m + n - 1) as f64;
poch_b_mn *= b + (m + n - 1) as f64;
}
total += 1;
if total > MAX_TOTAL_TERMS {
break;
}
if poch_c2_n.abs() < 1e-300 && n > 0 {
return Err(SpecialError::DomainError("Appell F4: (c2)_n = 0".to_string()));
}
let denom = poch_c1_m * poch_c2_n * fact_m * fact_n;
if denom.abs() < 1e-300 {
continue;
}
let term = poch_a_mn * poch_b_mn / denom * x_m * y_n;
col_sum += term;
if n > 2 && term.abs() < CONV_TOL * col_sum.abs().max(1e-300) {
break;
}
}
result += col_sum;
if m > 2 && col_sum.abs() < CONV_TOL * result.abs().max(1e-300) {
break;
}
if total > MAX_TOTAL_TERMS {
break;
}
}
Ok(result)
}
pub fn horn_h1(
a: f64,
b: f64,
b_prime: f64,
c: f64,
x: f64,
y: f64,
n_terms: usize,
) -> SpecialResult<f64> {
if c <= 0.0 && (c.fract().abs() < 1e-10) && c.round() as i64 <= 0 {
return Err(SpecialError::DomainError(format!(
"Horn H1: c = {c} must not be a non-positive integer"
)));
}
if x.abs() < f64::EPSILON && y.abs() < f64::EPSILON {
return Ok(1.0);
}
let max_terms = n_terms.min(MAX_SINGLE_DIM_TERMS);
let mut result = 0.0_f64;
let mut total = 0usize;
let mut x_m = 1.0_f64;
let mut poch_b_m = 1.0_f64;
let mut poch_c_m = 1.0_f64;
let mut fact_m = 1.0_f64;
for m in 0..max_terms {
if m > 0 {
x_m *= x;
poch_b_m *= b + (m - 1) as f64;
poch_c_m *= c + (m - 1) as f64;
fact_m *= m as f64;
}
if poch_c_m.abs() < 1e-300 && m > 0 {
return Err(SpecialError::DomainError("Horn H1: (c)_m = 0".to_string()));
}
let mut y_n = 1.0_f64;
let mut poch_bprime_n = 1.0_f64;
let mut fact_n = 1.0_f64;
let mut col_sum = 0.0_f64;
for n in 0..max_terms {
if n > 0 {
y_n *= y;
poch_bprime_n *= b_prime + (n - 1) as f64;
fact_n *= n as f64;
}
total += 1;
if total > MAX_TOTAL_TERMS {
break;
}
let shift = m as i64 - n as i64;
let poch_a_mminusn = pochhammer_shifted(a, shift);
let denom = poch_c_m * fact_m * fact_n;
if denom.abs() < 1e-300 {
continue;
}
let term = poch_a_mminusn * poch_b_m * poch_bprime_n / denom * x_m * y_n;
col_sum += term;
if n > 2 && term.abs() < CONV_TOL * col_sum.abs().max(1e-300) {
break;
}
}
result += col_sum;
if m > 2 && col_sum.abs() < CONV_TOL * result.abs().max(1e-300) {
break;
}
if total > MAX_TOTAL_TERMS {
break;
}
}
Ok(result)
}
pub fn lauricella_fd(
a: f64,
b_vec: &[f64],
c: f64,
x_vec: &[f64],
n_terms: usize,
) -> SpecialResult<f64> {
if b_vec.len() != x_vec.len() {
return Err(SpecialError::ValueError(format!(
"lauricella_fd: b_vec.len()={} != x_vec.len()={}",
b_vec.len(),
x_vec.len()
)));
}
let n_vars = b_vec.len();
if c <= 0.0 && (c.fract().abs() < 1e-10) && c.round() as i64 <= 0 {
return Err(SpecialError::DomainError(format!(
"Lauricella FD: c = {c} must not be a non-positive integer"
)));
}
for (i, &xi) in x_vec.iter().enumerate() {
if xi.abs() >= 1.0 {
return Err(SpecialError::DomainError(format!(
"Lauricella FD: |x[{i}]| = {} >= 1 (outside convergence region)",
xi.abs()
)));
}
}
if n_vars == 0 {
return Ok(1.0);
}
if x_vec.iter().all(|&xi| xi.abs() < f64::EPSILON) {
return Ok(1.0);
}
let max_per_dim = n_terms.min(MAX_SINGLE_DIM_TERMS);
let max_order = (max_per_dim as f64).powf(1.0 / n_vars as f64).ceil() as usize + 2;
lauricella_fd_recursive(a, b_vec, c, x_vec, n_vars, max_order)
}
fn lauricella_fd_recursive(
a: f64,
b_vec: &[f64],
c: f64,
x_vec: &[f64],
n_vars: usize,
max_order: usize,
) -> SpecialResult<f64> {
match n_vars {
1 => {
lauricella_fd_1d(a, b_vec[0], c, x_vec[0], max_order)
}
2 => {
appell_f1(a, b_vec[0], b_vec[1], c, x_vec[0], x_vec[1], max_order)
}
_ => {
lauricella_fd_general(a, b_vec, c, x_vec, n_vars, max_order)
}
}
}
fn lauricella_fd_1d(a: f64, b: f64, c: f64, x: f64, max_order: usize) -> SpecialResult<f64> {
let mut term = 1.0_f64;
let mut sum = 1.0_f64;
let mut x_k = 1.0_f64;
for k in 1..=max_order {
let k_f = (k - 1) as f64;
x_k *= x;
let numer = (a + k_f) * (b + k_f);
let denom = (c + k_f) * (k as f64);
if denom.abs() < 1e-300 {
break;
}
term *= numer / denom;
sum += term * x_k / x_k * x;
let _ = x_k;
break; }
let mut term2 = 1.0_f64;
let mut sum2 = 1.0_f64;
for k in 1..=max_order {
let kf = (k - 1) as f64;
let numer = (a + kf) * (b + kf) * x;
let denom = (c + kf) * (k as f64);
if denom.abs() < 1e-300 {
break;
}
term2 *= numer / denom;
sum2 += term2;
if term2.abs() < CONV_TOL * sum2.abs().max(1e-300) {
break;
}
}
let _ = (term, sum); Ok(sum2)
}
fn lauricella_fd_general(
a: f64,
b_vec: &[f64],
c: f64,
x_vec: &[f64],
n_vars: usize,
max_order: usize,
) -> SpecialResult<f64> {
let result = lauricella_fd_inner(a, b_vec, c, x_vec, 0, n_vars, 0, 1.0, 1.0, 1.0, max_order)?;
Ok(result)
}
fn lauricella_fd_inner(
a: f64,
b_vec: &[f64],
c: f64,
x_vec: &[f64],
depth: usize,
n_vars: usize,
m_sum: usize,
poch_a: f64,
poch_c: f64,
prefix_coeff: f64,
max_order: usize,
) -> SpecialResult<f64> {
if depth == n_vars {
if poch_c.abs() < 1e-300 {
return Ok(0.0);
}
return Ok(poch_a / poch_c * prefix_coeff);
}
let b_k = b_vec[depth];
let x_k = x_vec[depth];
let mut sum = 0.0_f64;
let mut x_k_mk = 1.0_f64; let mut poch_b_mk = 1.0_f64; let mut fact_mk = 1.0_f64; let mut poch_a_cur = poch_a; let mut poch_c_cur = poch_c;
for m_k in 0..=max_order {
if m_k > 0 {
x_k_mk *= x_k;
poch_b_mk *= b_k + (m_k - 1) as f64;
fact_mk *= m_k as f64;
poch_a_cur *= a + (m_sum + m_k - 1) as f64;
poch_c_cur *= c + (m_sum + m_k - 1) as f64;
}
let coeff_k = poch_b_mk * x_k_mk / fact_mk;
let new_prefix = prefix_coeff * coeff_k;
if new_prefix.abs() < 1e-300 * prefix_coeff.abs().max(1e-300) && m_k > 0 {
break;
}
let sub_result = lauricella_fd_inner(
a,
b_vec,
c,
x_vec,
depth + 1,
n_vars,
m_sum + m_k,
poch_a_cur,
poch_c_cur,
new_prefix,
max_order,
)?;
sum += sub_result;
if m_k > 2 && sub_result.abs() < CONV_TOL * sum.abs().max(1e-300) {
break;
}
}
Ok(sum)
}
pub fn lauricella_fc(
a: f64,
b: f64,
c_vec: &[f64],
x_vec: &[f64],
n_terms: usize,
) -> SpecialResult<f64> {
if c_vec.len() != 2 || x_vec.len() != 2 {
return Err(SpecialError::ValueError(
"lauricella_fc: currently only 2-variable case supported".to_string(),
));
}
appell_f4(a, b, c_vec[0], c_vec[1], x_vec[0], x_vec[1], n_terms)
}
#[inline]
fn pochhammer_shifted(a: f64, k: i64) -> f64 {
if k == 0 {
return 1.0;
}
if k > 0 {
let mut result = 1.0_f64;
for j in 0..k {
result *= a + j as f64;
}
result
} else {
let n = (-k) as u64;
let mut denom = 1.0_f64;
let sign = if n % 2 == 1 { -1.0_f64 } else { 1.0 };
for j in 0..n {
denom *= (1.0 - a) + j as f64;
}
if denom.abs() < 1e-300 {
return 0.0;
}
sign / denom
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-10;
#[test]
fn test_appell_f1_at_origin() {
let val = appell_f1(1.0, 2.0, 3.0, 4.0, 0.0, 0.0, 50).expect("ok");
assert!((val - 1.0).abs() < 1e-14, "F1(0,0)=1: got {val}");
}
#[test]
fn test_appell_f1_reduces_to_2f1_y_zero() {
let val = appell_f1(1.0, 1.0, 999.0, 2.0, 0.5, 0.0, 100).expect("ok");
let expected = 2.0 * 2.0_f64.ln(); assert!(
(val - expected).abs() < TOL,
"F1 ~ 2F1 when y=0: got {val}, expected {expected}"
);
}
#[test]
fn test_appell_f1_reduces_to_2f1_x_zero() {
let val = appell_f1(1.0, 999.0, 1.0, 2.0, 0.0, 0.3, 100).expect("ok");
let expected = -(1.0_f64 - 0.3).ln() / 0.3; assert!(
(val - expected).abs() < TOL,
"F1 ~ 2F1 when x=0: got {val}, expected {expected}"
);
}
#[test]
fn test_appell_f1_domain_error_c() {
assert!(appell_f1(1.0, 2.0, 3.0, 0.0, 0.1, 0.1, 50).is_err());
assert!(appell_f1(1.0, 2.0, 3.0, -1.0, 0.1, 0.1, 50).is_err());
}
#[test]
fn test_appell_f1_domain_error_xy() {
assert!(appell_f1(1.0, 2.0, 3.0, 4.0, 1.0, 0.1, 50).is_err());
assert!(appell_f1(1.0, 2.0, 3.0, 4.0, 0.1, -1.5, 50).is_err());
}
#[test]
fn test_appell_f1_symmetry() {
let v1 = appell_f1(2.0, 1.0, 3.0, 5.0, 0.3, 0.2, 60).expect("ok");
let v2 = appell_f1(2.0, 3.0, 1.0, 5.0, 0.2, 0.3, 60).expect("ok");
assert!(
(v1 - v2).abs() < 1e-8,
"F1 symmetry: F1(a;b1,b2;c;x,y)=F1(a;b2,b1;c;y,x): {v1} vs {v2}"
);
}
#[test]
fn test_appell_f2_at_origin() {
let val = appell_f2(1.0, 2.0, 3.0, 4.0, 5.0, 0.0, 0.0, 50).expect("ok");
assert!((val - 1.0).abs() < 1e-14, "F2(0,0)=1: {val}");
}
#[test]
fn test_appell_f2_domain_error() {
assert!(appell_f2(1.0, 1.0, 1.0, 0.0, 2.0, 0.1, 0.1, 50).is_err());
assert!(appell_f2(1.0, 1.0, 1.0, 2.0, 0.0, 0.1, 0.1, 50).is_err());
assert!(appell_f2(1.0, 1.0, 1.0, 2.0, 2.0, 0.6, 0.5, 50).is_err());
}
#[test]
fn test_appell_f2_reduces_to_2f1() {
let val = appell_f2(1.0, 1.0, 999.0, 2.0, 3.0, 0.3, 0.0, 100).expect("ok");
let expected = -(1.0_f64 - 0.3).ln() / 0.3;
assert!(
(val - expected).abs() < TOL,
"F2 ~ 2F1 when y=0: got {val}, expected {expected}"
);
}
#[test]
fn test_appell_f3_at_origin() {
let val = appell_f3(1.0, 2.0, 3.0, 4.0, 5.0, 0.0, 0.0, 50).expect("ok");
assert!((val - 1.0).abs() < 1e-14);
}
#[test]
fn test_appell_f3_domain_error() {
assert!(appell_f3(1.0, 1.0, 1.0, 1.0, -1.0, 0.1, 0.1, 50).is_err());
assert!(appell_f3(1.0, 1.0, 1.0, 1.0, 2.0, 1.5, 0.1, 50).is_err());
}
#[test]
fn test_appell_f4_at_origin() {
let val = appell_f4(1.0, 1.0, 2.0, 2.0, 0.0, 0.0, 50).expect("ok");
assert!((val - 1.0).abs() < 1e-14);
}
#[test]
fn test_appell_f4_domain_error() {
assert!(appell_f4(1.0, 1.0, 2.0, 2.0, 0.5, 0.5, 50).is_err());
assert!(appell_f4(1.0, 1.0, 0.0, 2.0, 0.1, 0.0, 50).is_err());
}
#[test]
fn test_horn_h1_at_origin() {
let val = horn_h1(1.0, 2.0, 3.0, 4.0, 0.0, 0.0, 30).expect("ok");
assert!((val - 1.0).abs() < 1e-14);
}
#[test]
fn test_horn_h1_domain_error_c() {
assert!(horn_h1(1.0, 2.0, 3.0, 0.0, 0.1, 0.1, 30).is_err());
}
#[test]
fn test_horn_h1_small_values() {
let val = horn_h1(0.5, 1.0, 1.0, 2.0, 0.01, 0.01, 30).expect("ok");
assert!(
(val - 1.0).abs() < 0.1,
"H1 near origin should be close to 1: {val}"
);
}
#[test]
fn test_lauricella_fd_at_origin() {
let val = lauricella_fd(1.0, &[2.0, 3.0], 4.0, &[0.0, 0.0], 30).expect("ok");
assert!((val - 1.0).abs() < 1e-14);
}
#[test]
fn test_lauricella_fd_n1_is_2f1() {
let val = lauricella_fd(1.0, &[1.0], 2.0, &[0.4], 100).expect("ok");
let expected = -(1.0_f64 - 0.4_f64).ln() / 0.4;
assert!(
(val - expected).abs() < TOL,
"FD n=1 should equal 2F1: got {val}, expected {expected}"
);
}
#[test]
fn test_lauricella_fd_n2_is_appell_f1() {
let v_fd = lauricella_fd(2.0, &[1.0, 1.0], 3.0, &[0.2, 0.3], 50).expect("ok");
let v_f1 = appell_f1(2.0, 1.0, 1.0, 3.0, 0.2, 0.3, 50).expect("ok");
assert!(
(v_fd - v_f1).abs() < 1e-8,
"FD n=2 should equal Appell F1: {v_fd} vs {v_f1}"
);
}
#[test]
fn test_lauricella_fd_n3() {
let val = lauricella_fd(1.0, &[1.0, 1.0, 1.0], 2.0, &[0.2, 0.1, 0.15], 20).expect("ok");
assert!(val.is_finite(), "FD n=3 should be finite: {val}");
assert!(val > 1.0, "FD n=3 with positive args should exceed 1: {val}");
}
#[test]
fn test_lauricella_fd_domain_error() {
assert!(lauricella_fd(1.0, &[2.0], 0.0, &[0.1], 30).is_err()); assert!(lauricella_fd(1.0, &[2.0], 3.0, &[1.5], 30).is_err()); assert!(lauricella_fd(1.0, &[2.0, 3.0], 4.0, &[0.1], 30).is_err()); }
#[test]
fn test_lauricella_fc_at_origin() {
let val = lauricella_fc(1.0, 2.0, &[3.0, 4.0], &[0.0, 0.0], 50).expect("ok");
assert!((val - 1.0).abs() < 1e-14);
}
#[test]
fn test_lauricella_fc_wrong_size() {
assert!(lauricella_fc(1.0, 2.0, &[3.0], &[0.0, 0.0], 50).is_err());
assert!(lauricella_fc(1.0, 2.0, &[3.0, 4.0, 5.0], &[0.0, 0.0], 50).is_err());
}
#[test]
fn test_pochhammer_shifted_zero() {
assert_eq!(pochhammer_shifted(5.0, 0), 1.0);
assert_eq!(pochhammer_shifted(-3.0, 0), 1.0);
}
#[test]
fn test_pochhammer_shifted_positive() {
assert!((pochhammer_shifted(2.0, 3) - 24.0).abs() < 1e-14);
}
#[test]
fn test_pochhammer_shifted_negative() {
let a = 3.0;
let val = pochhammer_shifted(a, -1);
let expected = -1.0 / (1.0 - a); assert!(
(val - expected).abs() < 1e-14,
"(3)_{{-1}} should be {expected}, got {val}"
);
}
}