use rug::Float;
pub const EULER_GAMMA: f64 = 0.577_215_664_901_532_9;
pub fn lambert_w0_domain_min() -> f64 {
-std::f64::consts::E.recip()
}
pub fn lambert_w0(x: f64) -> Option<f64> {
if x.is_nan() {
return None;
}
let em = lambert_w0_domain_min();
if x < em - 1e-15 {
return None;
}
if x == 0.0 {
return Some(0.0);
}
if (x - em).abs() < 1e-15 {
return Some(-1.0);
}
let mut w = if x < 0.0 {
let p = (2.0 * (x - em)).max(0.0).sqrt();
-1.0 + p * (1.0 - p / 3.0)
} else if x <= 1.0 {
x
} else {
let l1 = x.ln();
l1 - l1.ln()
};
for _ in 0..64 {
let ew = w.exp();
let f = w * ew - x;
if f.abs() < 1e-15 * x.abs().max(1.0) {
return Some(w);
}
let fp = ew * (w + 1.0);
let fpp = ew * (2.0 + w);
let denom = 2.0 * fp * fp - f * fpp;
w = if denom.abs() < 1e-300 {
w - f / fp
} else {
w - 2.0 * f * fp / denom
};
if !w.is_finite() {
return None;
}
}
Some(w)
}
pub fn digamma(x: f64) -> Option<f64> {
if x.is_nan() {
return None;
}
if x <= 0.0 && x.fract() == 0.0 {
return None;
}
let mut f = Float::with_val(53, x);
f.digamma_mut();
Some(f.to_f64())
}
pub fn bessel_j0(x: f64) -> f64 {
Float::with_val(53, x).jn(0).to_f64()
}
pub fn bessel_j1(x: f64) -> f64 {
Float::with_val(53, x).jn(1).to_f64()
}
const BERNOULLI_2N: [(i64, i64); 15] = [
(1, 6),
(-1, 30),
(1, 42),
(-1, 30),
(5, 66),
(-691, 2730),
(7, 6),
(-3617, 510),
(43867, 798),
(-174611, 330),
(854513, 138),
(-236364091, 2730),
(8553103, 6),
(-23749461029, 870),
(8615841276005, 14322),
];
pub fn trigamma(x: &Float) -> Option<Float> {
if !x.is_finite() {
return None;
}
let prec = x.prec();
let w = prec + 32;
if *x <= 0u32 {
if x.is_integer() {
return None;
}
let pi = Float::with_val(w, rug::float::Constant::Pi);
let s = Float::with_val(w, Float::with_val(w, &pi * x).sin());
if s.is_zero() {
return None;
}
let refl = Float::with_val(
w,
Float::with_val(w, &pi * &pi) / Float::with_val(w, &s * &s),
);
let one_minus = Float::with_val(w, Float::with_val(w, 1u32) - x);
let other = trigamma(&one_minus)?;
return Some(Float::with_val(prec, refl - other));
}
let target = f64::from(w + 34) / 33.0;
let target = target.exp2().max(20.0);
let mut z = Float::with_val(w, x);
let mut head = Float::new(w);
let mut guard = 0u32;
while z.to_f64() < target {
let inv = Float::with_val(w, Float::with_val(w, 1u32) / &z);
head += Float::with_val(w, &inv * &inv);
z += 1u32;
guard += 1;
if guard > 1_000_000 {
return None;
}
}
let zinv = Float::with_val(w, Float::with_val(w, 1u32) / &z);
let zinv2 = Float::with_val(w, &zinv * &zinv);
let mut acc = Float::with_val(w, &zinv + Float::with_val(w, &zinv2 / 2u32));
let mut pw = Float::with_val(w, &zinv2 * &zinv);
let mut prev = Float::with_val(w, f64::INFINITY);
for &(num, den) in BERNOULLI_2N.iter() {
let b = Float::with_val(w, num) / Float::with_val(w, den);
let term = Float::with_val(w, b * &pw);
let mag = Float::with_val(w, term.abs_ref());
if mag >= prev {
break;
}
acc += &term;
prev = mag;
pw = Float::with_val(w, &pw * &zinv2);
}
Some(Float::with_val(prec, head + acc))
}
pub fn trigamma_f64(x: f64) -> Option<f64> {
if !x.is_finite() {
return None;
}
trigamma(&Float::with_val(96, x)).map(|v| v.to_f64())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lambert_w0_at_one() {
assert!(lambert_w0(1.0).is_some());
}
#[test]
fn lambert_w0_special_values() {
assert_eq!(lambert_w0(0.0), Some(0.0));
assert!((lambert_w0(lambert_w0_domain_min()).unwrap() + 1.0).abs() < 1e-12);
}
#[test]
fn digamma_integer_values() {
let psi1 = digamma(1.0).unwrap();
assert!((psi1 + EULER_GAMMA).abs() < 1e-12);
let psi2 = digamma(2.0).unwrap();
assert!((psi2 - (1.0 - EULER_GAMMA)).abs() < 1e-12);
}
#[test]
fn bessel_j0_at_zero() {
assert!((bessel_j0(0.0) - 1.0).abs() < 1e-12);
}
#[test]
fn trigamma_closed_forms() {
let pi2 = std::f64::consts::PI * std::f64::consts::PI;
let got = trigamma_f64(1.0).unwrap();
assert!((got - pi2 / 6.0).abs() < 1e-14, "ψ₁(1) = {got}");
let got = trigamma_f64(0.5).unwrap();
assert!((got - pi2 / 2.0).abs() < 1e-13, "ψ₁(1/2) = {got}");
let got = trigamma_f64(2.0).unwrap();
assert!((got - (pi2 / 6.0 - 1.0)).abs() < 1e-14, "ψ₁(2) = {got}");
}
#[test]
fn trigamma_satisfies_its_recurrence() {
for x in [0.3_f64, 1.0, 2.5, 7.75, 40.0, 1e4] {
let a = trigamma_f64(x + 1.0).unwrap();
let b = trigamma_f64(x).unwrap() - 1.0 / (x * x);
assert!(
(a - b).abs() < 1e-13 * a.abs().max(1.0),
"ψ₁({x}+1) = {a} vs ψ₁({x}) − 1/x² = {b}"
);
}
}
#[test]
fn trigamma_reflection_holds_on_the_negative_axis() {
let pi = std::f64::consts::PI;
for x in [-0.25_f64, -0.5, -1.5, -2.75, -10.3] {
let lhs = trigamma_f64(x).unwrap() + trigamma_f64(1.0 - x).unwrap();
let rhs = pi * pi / (pi * x).sin().powi(2);
assert!(
(lhs - rhs).abs() < 1e-10 * rhs.abs(),
"reflection at {x}: {lhs} vs {rhs}"
);
}
}
#[test]
fn trigamma_is_the_derivative_of_digamma() {
let h = 1e-6;
for x in [0.4_f64, 1.0, 2.5, 9.0] {
let fd = (digamma(x + h).unwrap() - digamma(x - h).unwrap()) / (2.0 * h);
let got = trigamma_f64(x).unwrap();
assert!(
(got - fd).abs() < 1e-6 * got.abs(),
"ψ₁({x}): {got} vs {fd}"
);
}
}
#[test]
fn trigamma_declines_at_its_double_poles() {
for pole in [0.0_f64, -1.0, -2.0, -17.0] {
assert!(trigamma_f64(pole).is_none(), "{pole}");
}
for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(trigamma_f64(bad).is_none());
}
}
}