use super::{
arithmetic::two_sum,
elementary::{dd_cos, dd_exp, dd_log, dd_pow, dd_sin, dd_sqrt},
DoubleDouble, DD_HALF, DD_LN2, DD_ONE, DD_PI, DD_TWO, DD_TWO_PI, DD_ZERO,
};
const LANCZOS_G: f64 = 7.0;
const LANCZOS_COEFF: [f64; 9] = [
0.999_999_999_999_809_93,
676.520_368_121_885_1,
-1_259.139_216_722_402_9,
771.323_428_777_653_1,
-176.615_029_162_140_6,
12.507_343_278_686_905,
-0.138_571_095_265_720_12,
9.984_369_578_019_572e-6,
1.505_632_735_149_311_6e-7,
];
pub fn dd_gamma(x: DoubleDouble) -> DoubleDouble {
if x.is_zero() {
return DoubleDouble::new(f64::INFINITY, 0.0);
}
if x.is_negative() {
let xi = x.hi.round();
if (x.hi - xi).abs() < 1e-15 && x.lo.abs() < 1e-30 && xi <= 0.0 {
return DoubleDouble::new(f64::INFINITY, 0.0);
}
}
if x.hi < 0.5 {
let sin_px = dd_sin(DD_PI * x);
let g1mx = dd_gamma(DD_ONE - x);
return DD_PI / (sin_px * g1mx);
}
let z = x - DD_ONE;
let dd_g = DoubleDouble::from(LANCZOS_G);
let mut ag = DoubleDouble::from(LANCZOS_COEFF[0]);
for i in 1..LANCZOS_COEFF.len() {
let coeff = DoubleDouble::from(LANCZOS_COEFF[i]);
ag = ag + coeff / (z + DoubleDouble::from(i as f64));
}
let t = z + dd_g + DD_HALF;
let sqrt_2pi = dd_sqrt(DD_TWO_PI);
sqrt_2pi * dd_pow(t, z + DD_HALF) * dd_exp(-t) * ag
}
pub fn dd_lgamma(x: DoubleDouble) -> DoubleDouble {
if x.is_zero() || (x.is_negative() && x.hi == x.hi.round() && x.lo.abs() < 1e-30) {
return DoubleDouble::new(f64::INFINITY, 0.0);
}
if x.hi < 0.5 {
let ln_pi = dd_log(DD_PI);
let sin_px = dd_sin(DD_PI * x).abs();
let ln_sin = dd_log(sin_px);
return ln_pi - ln_sin - dd_lgamma(DD_ONE - x);
}
let z = x - DD_ONE;
let dd_g = DoubleDouble::from(LANCZOS_G);
let mut ag = DoubleDouble::from(LANCZOS_COEFF[0]);
for i in 1..LANCZOS_COEFF.len() {
let coeff = DoubleDouble::from(LANCZOS_COEFF[i]);
ag = ag + coeff / (z + DoubleDouble::from(i as f64));
}
let t = z + dd_g + DD_HALF;
let ln_sqrt_2pi = dd_log(dd_sqrt(DD_TWO_PI));
ln_sqrt_2pi + (z + DD_HALF) * dd_log(t) - t + dd_log(ag)
}
impl DoubleDouble {
#[inline]
pub fn gamma(self) -> Self {
dd_gamma(self)
}
#[inline]
pub fn lgamma(self) -> Self {
dd_lgamma(self)
}
}
pub fn dd_erf(x: DoubleDouble) -> DoubleDouble {
if x.is_zero() {
return DD_ZERO;
}
let abs_x = x.abs();
if abs_x.hi > 6.0 {
let result = DD_ONE - dd_erfc_large(abs_x);
return if x.is_negative() { -result } else { result };
}
if abs_x.hi > 2.5 {
let result = DD_ONE - dd_erfc_cf(abs_x);
return if x.is_negative() { -result } else { result };
}
let x2 = x.sqr();
let mut term = x; let mut sum = x;
for n in 1..=40 {
term = term * x2 * (-DD_ONE) / DoubleDouble::from(n as f64);
let contrib = term / DoubleDouble::from((2 * n + 1) as f64);
sum = sum + contrib;
if contrib.abs().hi < 1e-35 {
break;
}
}
let two_over_sqrt_pi = DD_TWO / dd_sqrt(DD_PI);
let result = two_over_sqrt_pi * sum;
if result.hi > 1.0 {
DD_ONE
} else if result.hi < -1.0 {
-DD_ONE
} else {
result
}
}
fn dd_erfc_cf(x: DoubleDouble) -> DoubleDouble {
let x2 = x.sqr();
let exp_neg_x2 = dd_exp(-x2);
let mut cf = x;
for k in (1..=30).rev() {
let ak = DoubleDouble::from(k as f64) * DD_HALF;
cf = x + ak / cf;
}
exp_neg_x2 / (dd_sqrt(DD_PI) * cf)
}
fn dd_erfc_large(x: DoubleDouble) -> DoubleDouble {
let x2 = x.sqr();
let exp_neg_x2 = dd_exp(-x2);
let inv_2x2 = DD_ONE / (DD_TWO * x2);
let mut term = DD_ONE;
let mut sum = DD_ONE;
for k in 1..=20 {
term = term * DoubleDouble::from((2 * k - 1) as f64) * (-inv_2x2);
let old_sum = sum;
sum = sum + term;
if term.abs().hi > (old_sum - sum).abs().hi * 2.0 {
sum = old_sum;
break;
}
if term.abs().hi < 1e-34 {
break;
}
}
exp_neg_x2 / (x * dd_sqrt(DD_PI)) * sum
}
pub fn dd_erfc(x: DoubleDouble) -> DoubleDouble {
if x.is_zero() {
return DD_ONE;
}
let abs_x = x.abs();
if abs_x.hi > 6.0 {
let r = dd_erfc_large(abs_x);
return if x.is_negative() { DD_TWO - r } else { r };
}
if abs_x.hi > 2.5 {
let r = dd_erfc_cf(abs_x);
return if x.is_negative() { DD_TWO - r } else { r };
}
DD_ONE - dd_erf(x)
}
impl DoubleDouble {
#[inline]
pub fn erf(self) -> Self {
dd_erf(self)
}
#[inline]
pub fn erfc(self) -> Self {
dd_erfc(self)
}
}
pub fn dd_beta(a: DoubleDouble, b: DoubleDouble) -> DoubleDouble {
let lg_a = dd_lgamma(a);
let lg_b = dd_lgamma(b);
let lg_ab = dd_lgamma(a + b);
dd_exp(lg_a + lg_b - lg_ab)
}
impl DoubleDouble {
#[inline]
pub fn beta(self, b: Self) -> Self {
dd_beta(self, b)
}
}
pub fn dd_bessel_j0(x: DoubleDouble) -> DoubleDouble {
let abs_x = x.abs();
if abs_x.is_zero() {
return DD_ONE;
}
if abs_x.hi > 20.0 {
return bessel_j0_asymptotic(abs_x);
}
let x_half = abs_x * DD_HALF;
let x_half_sq = x_half.sqr();
let mut term = DD_ONE; let mut sum = DD_ONE;
for k in 1..=40 {
let kf = DoubleDouble::from(k as f64);
term = term * (-x_half_sq) / kf.sqr();
sum = sum + term;
if term.abs().hi < 1e-35 {
break;
}
}
sum
}
fn bessel_j0_asymptotic(x: DoubleDouble) -> DoubleDouble {
let theta = x - DD_PI * DoubleDouble::from(0.25);
let cos_theta = dd_cos(theta);
let sin_theta = dd_sin(theta);
let inv_8x = DD_ONE / (DoubleDouble::from(8.0) * x);
let inv_8x_sq = inv_8x.sqr();
let mut p0 = DD_ONE;
let mut p_term = DD_ONE;
let mut q_term = DD_ONE;
for k in 1..=10 {
let k2 = 2 * k;
let num = DoubleDouble::from(((k2 - 1) * (k2 - 1)) as f64);
p_term = p_term * (-num) / DoubleDouble::from((2 * k * (2 * k - 1)) as f64) * inv_8x_sq;
p0 = p0 + p_term;
let num_q = DoubleDouble::from(((k2 - 1) * (k2 + 1)) as f64);
q_term = q_term * (-num_q) / DoubleDouble::from(((2 * k) * (2 * k + 1)) as f64) * inv_8x_sq;
}
let q0 = -inv_8x * q_term;
let prefactor = dd_sqrt(DD_TWO / (DD_PI * x));
prefactor * (p0 * cos_theta - q0 * sin_theta)
}
impl DoubleDouble {
#[inline]
pub fn bessel_j0(self) -> Self {
dd_bessel_j0(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gamma_half_is_sqrt_pi() {
let g = dd_gamma(DD_HALF);
let sqrt_pi = dd_sqrt(DD_PI);
let diff = (g - sqrt_pi).abs();
assert!(
diff.to_f64() < 1e-14,
"Gamma(0.5) - sqrt(pi) = {:e}",
diff.to_f64()
);
}
#[test]
fn test_gamma_integers() {
let g1 = dd_gamma(DD_ONE);
assert!(
(g1 - DD_ONE).abs().to_f64() < 1e-14,
"Gamma(1) = {:e}",
g1.to_f64()
);
let g2 = dd_gamma(DD_TWO);
assert!(
(g2 - DD_ONE).abs().to_f64() < 1e-14,
"Gamma(2) = {:e}",
g2.to_f64()
);
let g5 = dd_gamma(DoubleDouble::from(5.0));
let diff = (g5 - DoubleDouble::from(24.0)).abs();
assert!(diff.to_f64() < 1e-12, "Gamma(5) - 24 = {:e}", diff.to_f64());
}
#[test]
fn test_lgamma_positive() {
let lg1 = dd_lgamma(DD_ONE);
assert!(lg1.abs().to_f64() < 1e-14, "lgamma(1) = {:e}", lg1.to_f64());
let lg2 = dd_lgamma(DD_TWO);
assert!(lg2.abs().to_f64() < 1e-14, "lgamma(2) = {:e}", lg2.to_f64());
}
#[test]
fn test_lgamma_large() {
let lg10 = dd_lgamma(DoubleDouble::from(10.0));
let expected = dd_log(DoubleDouble::from(362_880.0));
let diff = (lg10 - expected).abs();
assert!(
diff.to_f64() < 1e-12,
"lgamma(10) - ln(362880) = {:e}",
diff.to_f64()
);
}
#[test]
fn test_erf_zero() {
let e = dd_erf(DD_ZERO);
assert!(e.abs().to_f64() < 1e-31, "erf(0) = {:e}", e.to_f64());
}
#[test]
fn test_erf_large_approaches_one() {
let e = dd_erf(DoubleDouble::from(5.0));
let diff = (e - DD_ONE).abs();
assert!(diff.to_f64() < 1e-10, "erf(5) - 1 = {:e}", diff.to_f64());
}
#[test]
fn test_erf_symmetry() {
let x = DoubleDouble::from(1.0);
let ep = dd_erf(x);
let en = dd_erf(-x);
let sum = ep + en;
assert!(
sum.abs().to_f64() < 1e-28,
"erf(1) + erf(-1) = {:e}",
sum.to_f64()
);
}
#[test]
fn test_erf_known_value() {
let e = dd_erf(DD_ONE);
let expected = 0.842_700_792_949_714_9;
let diff = (e.to_f64() - expected).abs();
assert!(diff < 1e-14, "erf(1) = {:e}, diff = {:e}", e.to_f64(), diff);
}
#[test]
fn test_erfc_zero() {
let ec = dd_erfc(DD_ZERO);
let diff = (ec - DD_ONE).abs();
assert!(diff.to_f64() < 1e-31);
}
#[test]
fn test_erfc_large() {
let ec = dd_erfc(DoubleDouble::from(5.0));
assert!(ec.to_f64() > 0.0);
assert!(ec.to_f64() < 1e-10);
}
#[test]
fn test_erf_plus_erfc_equals_one() {
for &xf in &[0.5, 1.0, 2.0, 3.0] {
let x = DoubleDouble::from(xf);
let sum = dd_erf(x) + dd_erfc(x);
let diff = (sum - DD_ONE).abs();
assert!(
diff.to_f64() < 1e-25,
"erf({xf}) + erfc({xf}) - 1 = {:e}",
diff.to_f64()
);
}
}
#[test]
fn test_beta_function() {
let b11 = dd_beta(DD_ONE, DD_ONE);
let diff = (b11 - DD_ONE).abs();
assert!(diff.to_f64() < 1e-14, "B(1,1) - 1 = {:e}", diff.to_f64());
}
#[test]
fn test_beta_half_half() {
let b = dd_beta(DD_HALF, DD_HALF);
let diff = (b - DD_PI).abs();
assert!(
diff.to_f64() < 1e-13,
"B(0.5, 0.5) - pi = {:e}",
diff.to_f64()
);
}
#[test]
fn test_bessel_j0_zero() {
let j = dd_bessel_j0(DD_ZERO);
let diff = (j - DD_ONE).abs();
assert!(diff.to_f64() < 1e-31);
}
#[test]
fn test_bessel_j0_known_values() {
let j = dd_bessel_j0(DD_ONE);
let expected = 0.765_197_686_557_966_6;
let diff = (j.to_f64() - expected).abs();
assert!(diff < 1e-14, "J0(1) = {:e}, diff = {:e}", j.to_f64(), diff);
}
#[test]
fn test_bessel_j0_symmetry() {
let x = DoubleDouble::from(2.5);
let jp = dd_bessel_j0(x);
let jn = dd_bessel_j0(-x);
let diff = (jp - jn).abs();
assert!(
diff.to_f64() < 1e-28,
"J0(2.5) - J0(-2.5) = {:e}",
diff.to_f64()
);
}
#[test]
fn test_gamma_negative() {
let g = dd_gamma(DoubleDouble::from(-0.5));
let expected = DD_TWO * dd_sqrt(DD_PI) * (-DD_ONE);
let diff = (g - expected).abs();
assert!(
diff.to_f64() < 1e-13,
"Gamma(-0.5) error = {:e}",
diff.to_f64()
);
}
}