#[inline]
pub fn binomial_coefficient_f64(n: usize, k: usize) -> f64 {
if k > n {
return 0.0;
}
if k == 0 || k == n {
return 1.0;
}
let k_eff = k.min(n - k);
let mut num: u128 = 1;
for j in 0..k_eff {
match num.checked_mul((n - j) as u128) {
Some(scaled) => num = scaled / (j as u128 + 1),
None => {
let mut out = num as f64;
for jj in j..k_eff {
out = out * (n - jj) as f64 / (jj + 1) as f64;
}
return out;
}
}
}
num as f64
}
#[inline]
fn horner_polynomial(x: f64, coeffs: &[f64]) -> f64 {
coeffs.iter().rev().fold(0.0, |acc, &c| acc * x + c)
}
#[inline]
pub fn stable_polynomial_times_exp_neg(x: f64, coeffs: &[f64]) -> f64 {
if coeffs.is_empty() || !x.is_finite() {
return 0.0;
}
const DIRECT_EXP_SWITCH: f64 = 600.0;
if x <= DIRECT_EXP_SWITCH {
return horner_polynomial(x, coeffs) * (-x).exp();
}
let inv_x = x.recip();
let mut tail = 0.0;
for &c in coeffs {
tail = tail * inv_x + c;
}
let degree = (coeffs.len() - 1) as f64;
let scale = (degree * x.ln() - x).exp();
scale * tail
}
const BESSEL_ASYMPTOTIC_THRESHOLD: f64 = 20.0;
const BESSEL_SERIES_MAX_TERMS: usize = 128;
const BESSEL_ASYMPTOTIC_MAX_TERMS: usize = 64;
fn bessel_ascending_series(ax: f64) -> BesselAscending {
let half = 0.5 * ax;
let quarter_square = half * half;
let mut term_i0 = 1.0_f64;
let mut i0_minus_one = 0.0_f64;
let mut term_i1 = 1.0_f64;
let mut sum_i1 = 1.0_f64;
let mut i0_minus_i1 = 1.0 - half;
for k in 1..=BESSEL_SERIES_MAX_TERMS {
let kf = k as f64;
term_i0 *= quarter_square / (kf * kf);
term_i1 *= quarter_square / (kf * (kf + 1.0));
i0_minus_one += term_i0;
sum_i1 += term_i1;
i0_minus_i1 += term_i0 * (kf + 1.0 - half) / (kf + 1.0);
if term_i0 <= f64::EPSILON * i0_minus_i1.abs()
&& term_i0 <= f64::EPSILON * (1.0 + i0_minus_one)
&& term_i1 <= f64::EPSILON * sum_i1
{
break;
}
}
BesselAscending {
i0_minus_one,
i1: half * sum_i1,
i0_minus_i1,
}
}
struct BesselAscending {
i0_minus_one: f64,
i1: f64,
i0_minus_i1: f64,
}
struct BesselAsymptotic {
s0: f64,
s1: f64,
n: f64,
s0_scaled_derivative: f64,
n_scaled_derivative: f64,
}
fn bessel_asymptotic_series(ax: f64) -> BesselAsymptotic {
let inverse = 1.0 / ax;
let mut c = 1.0_f64;
let mut b = 1.0_f64;
let mut acc = BesselAsymptotic {
s0: 1.0,
s1: 1.0,
n: 0.0,
s0_scaled_derivative: 0.0,
n_scaled_derivative: 0.0,
};
let mut power_two_back = ax;
let mut power_one_back = 1.0_f64;
let mut smallest = f64::INFINITY;
for k in 1..=BESSEL_ASYMPTOTIC_MAX_TERMS {
let kf = k as f64;
let odd = 2.0 * kf - 1.0;
c *= odd * odd / (8.0 * kf);
b *= (odd * odd - 4.0) / (8.0 * kf);
let power = power_one_back * inverse;
let term_c = c * power;
if !(term_c.abs() <= smallest) {
break;
}
smallest = term_c.abs();
let difference = b - c;
let curvature_term = (kf - 1.0) * difference * power_two_back;
acc.s0 += term_c;
acc.s1 += b * power;
acc.n += difference * power_one_back;
acc.s0_scaled_derivative -= kf * c * power_one_back;
if k >= 2 {
acc.n_scaled_derivative -= curvature_term;
}
let scale = acc.n_scaled_derivative.abs().max(acc.n.abs());
if k >= 3 && curvature_term.abs() <= f64::EPSILON * scale {
break;
}
power_two_back = power_one_back;
power_one_back = power;
}
acc
}
pub fn bessel_i0_centered_terms(eta: f64) -> (f64, f64, f64) {
let ax = eta.abs();
if ax.is_nan() {
return (f64::NAN, f64::NAN, f64::NAN);
}
if ax.is_infinite() {
return (f64::NEG_INFINITY, 1.0, -0.5);
}
if ax < BESSEL_ASYMPTOTIC_THRESHOLD {
let series = bessel_ascending_series(ax);
let i0 = 1.0 + series.i0_minus_one;
return (
series.i0_minus_one.ln_1p() - ax,
series.i1 / i0,
-ax * (series.i0_minus_i1 / i0),
);
}
let series = bessel_asymptotic_series(ax);
(
series.s0.ln() - 0.5 * (std::f64::consts::TAU.ln() + ax.ln()),
series.s1 / series.s0,
series.n / series.s0,
)
}
pub fn bessel_i0_centered_terms_from_log_abs(log_abs_eta: f64) -> (f64, f64, f64) {
if log_abs_eta.is_nan() {
return (f64::NAN, f64::NAN, f64::NAN);
}
if log_abs_eta == f64::NEG_INFINITY {
return (0.0, 0.0, 0.0);
}
if log_abs_eta <= f64::MAX.ln() {
return bessel_i0_centered_terms(log_abs_eta.exp());
}
(-0.5 * (std::f64::consts::TAU.ln() + log_abs_eta), 1.0, -0.5)
}
pub fn bessel_i0_centered_second_log_derivative_from_log_abs(log_abs_eta: f64) -> f64 {
if log_abs_eta.is_nan() {
return f64::NAN;
}
if log_abs_eta == f64::NEG_INFINITY {
return 0.0;
}
if log_abs_eta > f64::MAX.ln() {
return 0.0;
}
let eta = log_abs_eta.exp();
if eta >= BESSEL_ASYMPTOTIC_THRESHOLD {
let series = bessel_asymptotic_series(eta);
return (series.n_scaled_derivative * series.s0 - series.n * series.s0_scaled_derivative)
/ (eta * series.s0 * series.s0);
}
let (_centered, ratio, d1) = bessel_i0_centered_terms(eta);
if eta < 1.0 {
return -eta * (1.0 + d1 * (1.0 + ratio));
}
let q = d1 + 0.5;
-2.0 * eta * (q + 0.125 / eta) + q - q * q
}
pub fn bessel_i0_log_minus_abs_and_ratio(eta: f64) -> (f64, f64) {
let (centered_log_i0, ratio, _) = bessel_i0_centered_terms(eta);
(centered_log_i0, ratio)
}
pub fn bessel_i0_log_and_ratio(eta: f64) -> (f64, f64) {
let (centered_log_i0, ratio) = bessel_i0_log_minus_abs_and_ratio(eta);
(eta.abs() + centered_log_i0, ratio)
}
const POLYGAMMA_ASYMPTOTIC_THRESHOLD: f64 = 20.0;
pub fn digamma(mut x: f64) -> f64 {
if !(x.is_finite() && x > 0.0) {
return f64::NAN;
}
let mut recurrence = 0.0_f64;
while x < POLYGAMMA_ASYMPTOTIC_THRESHOLD {
recurrence -= 1.0 / x;
x += 1.0;
}
let inv = 1.0 / x;
let inv2 = inv * inv;
let series = horner_polynomial(
inv2,
&[
-1.0 / 12.0,
1.0 / 120.0,
-1.0 / 252.0,
1.0 / 240.0,
-1.0 / 132.0,
691.0 / 32_760.0,
],
);
recurrence + x.ln() - 0.5 * inv + inv2 * series
}
pub fn trigamma(mut x: f64) -> f64 {
if !(x.is_finite() && x > 0.0) {
return f64::NAN;
}
let mut recurrence = 0.0_f64;
while x < POLYGAMMA_ASYMPTOTIC_THRESHOLD {
recurrence += 1.0 / (x * x);
x += 1.0;
}
let inv = 1.0 / x;
let inv2 = inv * inv;
let series = horner_polynomial(
inv2,
&[
1.0 / 6.0,
-1.0 / 30.0,
1.0 / 42.0,
-1.0 / 30.0,
5.0 / 66.0,
-691.0 / 2_730.0,
],
);
recurrence + inv + 0.5 * inv2 + inv2 * inv * series
}
pub fn tetragamma(mut x: f64) -> f64 {
if !(x.is_finite() && x > 0.0) {
return f64::NAN;
}
let mut recurrence = 0.0_f64;
while x < POLYGAMMA_ASYMPTOTIC_THRESHOLD {
recurrence -= 2.0 / (x * x * x);
x += 1.0;
}
let inv = 1.0 / x;
let inv2 = inv * inv;
let series = horner_polynomial(
inv2,
&[
0.5,
-1.0 / 6.0,
1.0 / 6.0,
-3.0 / 10.0,
5.0 / 6.0,
-691.0 / 210.0,
],
);
recurrence - (inv2 + inv2 * inv + inv2 * inv2 * series)
}
pub fn pentagamma(mut x: f64) -> f64 {
if !(x.is_finite() && x > 0.0) {
return f64::NAN;
}
let mut recurrence = 0.0_f64;
while x < POLYGAMMA_ASYMPTOTIC_THRESHOLD {
recurrence += 6.0 / (x * x * x * x);
x += 1.0;
}
let inv = 1.0 / x;
let inv2 = inv * inv;
let series = horner_polynomial(
inv2,
&[2.0, -1.0, 4.0 / 3.0, -3.0, 10.0, -691.0 * 182.0 / 2_730.0],
);
recurrence + 2.0 * inv2 * inv + 3.0 * inv2 * inv2 + inv2 * inv2 * inv * series
}
pub fn gauss_legendre(n: usize) -> (Vec<f64>, Vec<f64>) {
let mut tmp: Vec<(f64, f64)> = Vec::with_capacity(n);
let half = n.div_ceil(2);
for i in 0..half {
let mut z = (std::f64::consts::PI * (i as f64 + 0.75) / (n as f64 + 0.5)).cos();
let legendre_value_and_slope = |z: f64| {
let mut p1 = 1.0_f64;
let mut p2 = 0.0_f64;
for j in 0..n {
let p3 = p2;
p2 = p1;
p1 = ((2.0 * j as f64 + 1.0) * z * p2 - j as f64 * p3) / (j as f64 + 1.0);
}
(p1, n as f64 * (z * p1 - p2) / (z * z - 1.0))
};
for _ in 0..200 {
let (p1, pp) = legendre_value_and_slope(z);
let z_prev = z;
z = z_prev - p1 / pp;
if (z - z_prev).abs() < 1e-15 {
break;
}
}
let (_, pp) = legendre_value_and_slope(z);
let w = 2.0 / ((1.0 - z * z) * pp * pp);
if !n.is_multiple_of(2) && i == half - 1 {
tmp.push((0.0, w));
} else {
tmp.push((-z.abs(), w));
tmp.push((z.abs(), w));
}
}
tmp.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut nodes = Vec::with_capacity(n);
let mut weights = Vec::with_capacity(n);
for (z, w) in tmp.into_iter().take(n) {
nodes.push(z);
weights.push(w);
}
(nodes, weights)
}
pub fn gauss_lobatto(n: usize) -> (Vec<f64>, Vec<f64>) {
assert!(n >= 2, "a Gauss-Lobatto rule needs at least the two endpoints");
let m = n - 1;
let legendre = |x: f64| -> (f64, f64, f64) {
let mut p1 = 1.0_f64;
let mut p2 = 0.0_f64;
for j in 0..m {
let p3 = p2;
p2 = p1;
p1 = ((2.0 * j as f64 + 1.0) * x * p2 - j as f64 * p3) / (j as f64 + 1.0);
}
if (1.0 - x * x).abs() < f64::EPSILON {
return (p1, 0.0, 0.0);
}
let derivative = m as f64 * (x * p1 - p2) / (x * x - 1.0);
let second = (2.0 * x * derivative - (m * (m + 1)) as f64 * p1) / (1.0 - x * x);
(p1, derivative, second)
};
let endpoint_weight = 2.0 / (n * m) as f64;
let mut nodes = Vec::with_capacity(n);
let mut weights = Vec::with_capacity(n);
nodes.push(-1.0);
weights.push(endpoint_weight);
for i in 1..m {
let mut x = (std::f64::consts::PI * i as f64 / m as f64).cos();
for _ in 0..200 {
let (_, derivative, second) = legendre(x);
let previous = x;
x = previous - derivative / second;
if (x - previous).abs() < 1e-15 {
break;
}
}
let (value, _, _) = legendre(x);
nodes.push(x);
weights.push(endpoint_weight / (value * value));
}
nodes.push(1.0);
weights.push(endpoint_weight);
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| nodes[a].total_cmp(&nodes[b]));
let sorted_nodes: Vec<f64> = order.iter().map(|&i| nodes[i]).collect();
let sorted_weights: Vec<f64> = order.iter().map(|&i| weights[i]).collect();
(sorted_nodes, sorted_weights)
}
#[inline]
pub fn softplus(x: f64) -> f64 {
x.max(0.0) + (-x.abs()).exp().ln_1p()
}
#[inline]
pub fn logistic(x: f64) -> f64 {
if x >= 0.0 {
1.0 / (1.0 + (-x).exp())
} else {
let e = x.exp();
e / (1.0 + e)
}
}
#[inline]
pub fn xlogy(x: f64, y: f64) -> f64 {
if x == 0.0 { 0.0 } else { x * y.ln() }
}
#[inline]
pub fn logaddexp(a: f64, b: f64) -> f64 {
let hi = a.max(b);
let lo = a.min(b);
if hi == f64::NEG_INFINITY {
f64::NEG_INFINITY
} else {
hi + (lo - hi).exp().ln_1p()
}
}
#[inline]
pub fn expm1_minus_x(x: f64) -> f64 {
if x.abs() > 0.5 {
return x.exp_m1() - x;
}
let mut term = 0.5 * x * x;
let mut sum = term;
let mut k = 2.0;
loop {
k += 1.0;
term *= x / k;
let next = sum + term;
if next == sum {
return next;
}
sum = next;
}
}
#[inline]
pub fn log1p_minus_x(x: f64) -> f64 {
if x.abs() > 0.5 {
return x.ln_1p() - x;
}
let mut power = x * x;
let mut sign = -1.0;
let mut k = 2.0;
let mut sum = sign * power / k;
loop {
power *= x;
sign = -sign;
k += 1.0;
let next = sum + sign * power / k;
if next == sum {
return next;
}
sum = next;
}
}
#[inline]
pub fn exprel(x: f64) -> f64 {
if x == 0.0 {
return 1.0;
}
if x.abs() > 0.5 {
return x.exp_m1() / x;
}
let mut term = 1.0;
let mut sum = term;
let mut k = 1.0;
loop {
k += 1.0;
term *= x / k;
let next = sum + term;
if next == sum {
return next;
}
sum = next;
}
}
#[inline]
pub fn log_exprel(x: f64) -> f64 {
if x == 0.0 {
0.0
} else if x.abs() <= 0.5 {
exprel(x).ln()
} else if x > 0.0 {
x + (-(-x).exp()).ln_1p() - x.ln()
} else {
(-x.exp()).ln_1p() - (-x).ln()
}
}
#[inline]
pub fn log_abs_one_minus_exp(x: f64) -> f64 {
if x > 0.0 {
x + crate::probability::log1mexp_positive(x)
} else {
crate::probability::log1mexp_positive(-x)
}
}
#[inline]
pub fn bd0(x: f64, m: f64) -> f64 {
if x == 0.0 {
return m;
}
if x == m {
return 0.0;
}
let hi = x.max(m);
let lo = x.min(m);
let relative_gap = (x - m).abs() / hi;
if relative_gap < 0.2 {
let v = ((x - m) / hi) / (1.0 + lo / hi);
let mut sum = (x - m) * v;
let mut ej = 2.0 * (x * v);
let v2 = v * v;
let mut denominator = 3.0;
loop {
ej *= v2;
let next = sum + ej / denominator;
if next == sum {
return next;
}
sum = next;
denominator += 2.0;
}
}
x * (x.ln() - m.ln()) + (m - x)
}
#[inline]
pub fn bernoulli_kl_from_logits(a: f64, b: f64) -> f64 {
if a == b {
return 0.0;
}
let h = b - a;
if h.abs() <= 0.5 {
let (p, local_h) = if a <= 0.0 {
(logistic(a), h)
} else {
(logistic(-a), -h)
};
let em1 = local_h.exp_m1();
let x = p * em1;
return log1p_minus_x(x) + p * expm1_minus_x(local_h);
}
if a <= 0.0 {
let p = logistic(a);
p * (a - b) + softplus(b) - softplus(a)
} else {
let q = logistic(-a);
q * (b - a) + softplus(-b) - softplus(-a)
}
}
#[inline]
pub fn positive_frexp(x: f64) -> (f64, i32) {
assert!(x.is_finite() && x > 0.0);
let bits = x.to_bits();
let raw_exp = ((bits >> 52) & 0x7ff) as i32;
let fraction = bits & ((1_u64 << 52) - 1);
if raw_exp != 0 {
let mantissa = f64::from_bits((1023_u64 << 52) | fraction);
(mantissa, raw_exp - 1023)
} else {
let leading = 63_i32 - fraction.leading_zeros() as i32;
let shift = 52_i32 - leading;
let normalized = fraction << shift;
let mantissa = f64::from_bits((1023_u64 << 52) | (normalized & ((1_u64 << 52) - 1)));
(mantissa, -1022 - shift)
}
}
#[inline]
pub fn scale_normalized_power_of_two(mut mantissa: f64, mut exponent: i32) -> f64 {
while mantissa >= 2.0 {
mantissa *= 0.5;
exponent += 1;
}
while mantissa < 1.0 {
mantissa *= 2.0;
exponent -= 1;
}
if exponent > 1023 {
return f64::INFINITY;
}
if exponent >= -1022 {
let power = f64::from_bits(((exponent + 1023) as u64) << 52);
return mantissa * power;
}
if exponent < -1075 {
return 0.0;
}
let units = mantissa * 2.0_f64.powi(exponent + 1074);
units * f64::from_bits(1)
}
#[inline]
pub fn scaled_positive_product_quotient(a: f64, b: f64, c: f64, d: f64) -> f64 {
assert!(a.is_finite() && a > 0.0);
assert!(b.is_finite() && b > 0.0);
assert!(c.is_finite() && c > 0.0);
assert!(d.is_finite() && d > 0.0);
let (ma, ea) = positive_frexp(a);
let (mb, eb) = positive_frexp(b);
let (mc, ec) = positive_frexp(c);
let (md, ed) = positive_frexp(d);
scale_normalized_power_of_two((ma * mb) * (mc / md), ea + eb + ec - ed)
}
#[cfg(test)]
mod exponential_family_kernel_tests {
use super::*;
#[test]
fn softplus_and_logistic_agree_with_their_definitions_away_from_the_tails() {
for &x in &[-3.0_f64, -0.7, 0.0, 0.4, 2.5] {
assert!((softplus(x) - (1.0 + x.exp()).ln()).abs() <= 4.0 * f64::EPSILON);
assert!((logistic(x) - 1.0 / (1.0 + (-x).exp())).abs() <= 4.0 * f64::EPSILON);
}
assert_eq!(softplus(800.0), 800.0);
assert_eq!(softplus(-800.0), 0.0);
}
#[test]
fn remainders_match_the_direct_formula_where_it_does_not_cancel() {
for &x in &[-0.75_f64, 0.6, 1.5] {
assert!((expm1_minus_x(x) - (x.exp_m1() - x)).abs() <= 8.0 * f64::EPSILON);
assert!((log1p_minus_x(x) - (x.ln_1p() - x)).abs() <= 8.0 * f64::EPSILON);
assert!((exprel(x) - x.exp_m1() / x).abs() <= 8.0 * f64::EPSILON);
assert!((log_exprel(x) - (x.exp_m1() / x).ln()).abs() <= 8.0 * f64::EPSILON);
}
let x = 1.0e-6;
let series = expm1_minus_x(x);
assert!((series - 0.5 * x * x).abs() <= 1.0e-6 * 0.5 * x * x);
assert!((log1p_minus_x(x) + 0.5 * x * x).abs() <= 1.0e-6 * 0.5 * x * x);
}
#[test]
fn bd0_is_the_poisson_bregman_divergence() {
assert_eq!(bd0(0.0, 2.5), 2.5);
assert_eq!(bd0(3.0, 3.0), 0.0);
for &(x, m) in &[(3.0_f64, 2.0_f64), (10.0, 10.5), (0.2, 7.0)] {
let direct = x * (x / m).ln() + m - x;
assert!((bd0(x, m) - direct).abs() <= 16.0 * f64::EPSILON * direct.abs().max(1.0));
}
}
#[test]
fn bernoulli_kl_from_logits_is_the_kl_divergence_between_the_two_bernoullis() {
for &(a, b) in &[(0.3_f64, -0.2_f64), (-2.0, -1.8), (4.0, 1.0), (0.0, 0.0)] {
let p = logistic(a);
let q = logistic(b);
let direct = xlogy(p, p / q) + xlogy(1.0 - p, (1.0 - p) / (1.0 - q));
assert!((bernoulli_kl_from_logits(a, b) - direct).abs() <= 1.0e-13);
}
}
#[test]
fn logaddexp_and_log_abs_one_minus_exp_handle_their_edge_cases() {
assert_eq!(logaddexp(f64::NEG_INFINITY, f64::NEG_INFINITY), f64::NEG_INFINITY);
assert!((logaddexp(1.0, 2.0) - (1.0_f64.exp() + 2.0_f64.exp()).ln()).abs() <= 4.0 * f64::EPSILON);
assert!((log_abs_one_minus_exp(-1.0) - (1.0 - (-1.0_f64).exp()).ln()).abs() <= 4.0 * f64::EPSILON);
assert!((log_abs_one_minus_exp(1.0) - (1.0_f64.exp() - 1.0).ln()).abs() <= 4.0 * f64::EPSILON);
}
#[test]
fn binary_exponent_arithmetic_round_trips_and_survives_intermediate_overflow() {
for &x in &[1.0_f64, 0.3, 1.0e300, 5.0e-320, f64::MIN_POSITIVE] {
let (mantissa, exponent) = positive_frexp(x);
assert!((1.0..2.0).contains(&mantissa));
assert_eq!(scale_normalized_power_of_two(mantissa, exponent), x);
}
let got = scaled_positive_product_quotient(1.0e-300, 1.0, 1.0e308, 1.0);
assert!((got - 1.0e8).abs() <= 4.0 * f64::EPSILON * 1.0e8);
let got = scaled_positive_product_quotient(1.0e-300, 1.0e-200, 1.0, 1.0e-300);
assert!((got - 1.0e-200).abs() <= 4.0 * f64::EPSILON * 1.0e-200);
assert_eq!(scaled_positive_product_quotient(2.0, 3.0, 5.0, 4.0), 7.5);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gauss_lobatto_includes_its_endpoints_and_is_exact_to_two_n_minus_three() {
for n in 2..=12 {
let (nodes, weights) = gauss_lobatto(n);
assert_eq!(nodes.len(), n);
assert!((nodes[0] + 1.0).abs() < 1e-15 && (nodes[n - 1] - 1.0).abs() < 1e-15);
assert!(nodes.windows(2).all(|w| w[1] > w[0]), "nodes not ascending: {nodes:?}");
assert!(weights.iter().all(|w| *w > 0.0), "weights {weights:?}");
for degree in 0..=(2 * n - 3) {
let quadrature: f64 = nodes
.iter()
.zip(weights.iter())
.map(|(x, w)| w * x.powi(degree as i32))
.sum();
let exact = if degree % 2 == 1 { 0.0 } else { 2.0 / (degree as f64 + 1.0) };
assert!(
(quadrature - exact).abs() < 1e-12 * (1.0 + exact.abs()),
"n={n} degree={degree}: {quadrature} vs {exact}"
);
}
}
}
#[test]
fn centered_bessel_log_is_finite_and_derivative_consistent() {
for eta in [0.25_f64, 1.0, 3.74, 3.76, 12.0, 900.0] {
let (centered, ratio, scaled_derivative) = bessel_i0_centered_terms(eta);
assert!(centered.is_finite());
assert!((0.0..=1.0).contains(&ratio));
let h = 1.0e-4 * eta.max(1.0);
let (plus, _) = bessel_i0_log_and_ratio(eta + h);
let (minus, _) = bessel_i0_log_and_ratio(eta - h);
let derivative = (plus - minus) / (2.0 * h);
assert!(
(derivative - ratio).abs() <= 1.0e-8,
"d/dη log I0 mismatch at eta={eta}: analytic={ratio}, finite_difference={derivative}"
);
let log_step = 1.0e-5_f64;
let (centered_plus, _, _) = bessel_i0_centered_terms(eta * log_step.exp());
let (centered_minus, _, _) = bessel_i0_centered_terms(eta * (-log_step).exp());
let finite_difference = (centered_plus - centered_minus) / (2.0 * log_step);
assert!(
(finite_difference - scaled_derivative).abs() < 1.0e-8,
"centered Bessel value/gradient mismatch at eta={eta}: analytic={scaled_derivative}, finite_difference={finite_difference}"
);
}
for eta in [1.0e20_f64, 1.0e100, 1.0e300] {
let (centered, ratio, scaled_derivative) = bessel_i0_centered_terms(eta);
let asymptotic = -0.5 * (std::f64::consts::TAU * eta).ln();
assert!(centered.is_finite() && ratio.is_finite());
assert!(
(centered - asymptotic).abs() < 1.0e-13,
"large-eta centered log must equal -½log(2πη); eta={eta:e}, centered={centered}, asymptotic={asymptotic}"
);
assert!(
(scaled_derivative + 0.5).abs() < 1.0e-15,
"large-eta centered derivative must retain its -1/2 limit; eta={eta:e}, derivative={scaled_derivative}"
);
}
assert_eq!(bessel_i0_centered_terms(0.0), (0.0, 0.0, 0.0));
let log_eta = 1_200.0;
let (centered, ratio, scaled_derivative) = bessel_i0_centered_terms_from_log_abs(log_eta);
assert!(centered.is_finite());
assert_eq!(ratio, 1.0);
assert_eq!(scaled_derivative, -0.5);
assert_eq!(centered, -0.5 * (std::f64::consts::TAU.ln() + log_eta));
}
#[test]
fn centered_bessel_second_log_derivative_matches_finite_difference() {
let first_log_derivative = |x: f64| bessel_i0_centered_terms(x).2;
for eta in [
0.02_f64, 0.05, 0.25, 0.999, 1.0, 1.001, 2.0, 3.5, 4.0, 8.0, 19.9, 20.1, 29.9, 30.1,
] {
let log_eta = eta.ln();
let analytic = bessel_i0_centered_second_log_derivative_from_log_abs(log_eta);
let log_step = 1.0e-6_f64;
let first_plus = first_log_derivative(eta * log_step.exp());
let first_minus = first_log_derivative(eta * (-log_step).exp());
let finite_difference = (first_plus - first_minus) / (2.0 * log_step);
assert!(
(analytic - finite_difference).abs() < 1.0e-8 + 1.0e-6 * analytic.abs(),
"centered Bessel second log-derivative mismatch at eta={eta}: \
analytic={analytic}, finite_difference={finite_difference}"
);
}
for eta in [50.0_f64, 200.0, 1.0e4] {
let c2 = bessel_i0_centered_second_log_derivative_from_log_abs(eta.ln());
let inverse = 1.0 / eta;
let expansion = inverse * (0.125 + inverse * (0.25 + inverse * (75.0 / 128.0)));
assert!(
c2 > 0.0 && (c2 - expansion).abs() < 8.0 * inverse.powi(4),
"large-eta centered second derivative must track its own expansion; \
eta={eta}, c2={c2}, expansion={expansion}"
);
}
assert_eq!(
bessel_i0_centered_second_log_derivative_from_log_abs(f64::NEG_INFINITY),
0.0
);
assert_eq!(
bessel_i0_centered_second_log_derivative_from_log_abs(1_200.0),
0.0
);
}
#[test]
fn bessel_primitives_match_independent_high_precision_reference() {
const REFERENCE: [[f64; 5]; 24] = [
[
1e-06,
-9.9999975e-07,
4.999999999999375e-07,
-9.999995e-07,
-9.99999e-07,
],
[
0.001,
-0.000999750000015625,
0.0004999999375000105,
-0.0009995000000625,
-0.00099900000025,
],
[
0.05,
-0.049375097629132,
0.024992190753810217,
-0.048750390462309494,
-0.04750156152399669,
],
[
0.25,
-0.23443561468661894,
0.12403350191792471,
-0.21899162452051882,
-0.18846151934987648,
],
[
0.5,
-0.4384502808145187,
0.24249961258080194,
-0.378750193709599,
-0.2647015155254598,
],
[
1.0,
-0.7640856414928213,
0.4463899658965345,
-0.5536100341034655,
-0.19926400165310923,
],
[
2.0,
-1.1760064585170438,
0.697774657964008,
-0.604450684071984,
0.05244210681284669,
],
[
3.75,
-1.5396457880279808,
0.8531704594530685,
-0.5506107770509933,
0.0764086000777509,
],
[
5.0,
-1.6953182241774665,
0.8933831370440852,
-0.5330843147795739,
0.0466642611317311,
],
[
8.0,
-1.941895744572186,
0.9352354935294386,
-0.5181160517644912,
0.02141258513583364,
],
[
12.0,
-2.1504975008971563,
0.9573814053952422,
-0.5114231352570932,
0.01260162289404047,
],
[
17.0,
-2.327961358737179,
0.9701275885919403,
-0.5078309939370159,
0.008361475455484893,
],
[
19.5,
-2.397561575434808,
0.9740118676091061,
-0.5067685816224307,
0.007160287955186735,
],
[
19.999999,
-2.410389546426233,
0.9746705066059314,
-0.5065898425518784,
0.006960420318717729,
],
[
20.0,
-2.4103895717557258,
0.9746705078898071,
-0.5065898422038575,
0.006960419930170057,
],
[
20.000001,
-2.410389597085217,
0.9746705091736827,
-0.5065898418558366,
0.006960419541622429,
],
[
25.0,
-2.5232719950007563,
0.9797914534905159,
-0.5052136627371017,
0.005442291838848013,
],
[
30.0,
-2.615298566828064,
0.9831895553653361,
-0.5043133390399173,
0.004468398461442669,
],
[
64.0,
-2.996411436485784,
0.9921564935488112,
-0.5019844128760834,
0.002016497368136742,
],
[
150.0,
-3.423420049648141,
0.9966610736828279,
-0.5008389475758167,
0.0008446213361703931,
],
[
900.0,
-4.319996948727984,
0.9994442899516907,
-0.5001390434784159,
0.0001391983371050074,
],
[
10000.0,
-5.524096218567699,
0.999949998749875,
-0.5000125012501954,
1.2502500586100053e-05,
],
[
1000000.0,
-7.826693687186747,
0.999999499999875,
-0.500000125000125,
1.2500025000058594e-07,
],
[
1000000000000.0,
-14.734449091168822,
0.9999999999995,
-0.500000000000125,
1.2500000000025e-13,
],
];
const CENTERED_TOL: f64 = 4.0e-15;
const RATIO_TOL: f64 = 4.0e-15;
const D1_TOL: f64 = 4.0e-15;
const CURVATURE_TOL: f64 = 2.0e-11;
for [eta, want_centered, want_ratio, want_d1, want_curvature] in REFERENCE {
let (centered, ratio, d1) = bessel_i0_centered_terms(eta);
let curvature = bessel_i0_centered_second_log_derivative_from_log_abs(eta.ln());
let relative = |got: f64, want: f64| (got - want).abs() / want.abs();
assert!(
relative(centered, want_centered) < CENTERED_TOL,
"log I0({eta}) − {eta}: got {centered:.17e}, want {want_centered:.17e}"
);
assert!(
relative(ratio, want_ratio) < RATIO_TOL,
"I1/I0({eta}): got {ratio:.17e}, want {want_ratio:.17e}"
);
assert!(
relative(d1, want_d1) < D1_TOL,
"η(I1/I0 − 1) at {eta}: got {d1:.17e}, want {want_d1:.17e}"
);
assert!(
relative(curvature, want_curvature) < CURVATURE_TOL,
"c''(log η) at {eta}: got {curvature:.17e}, want {want_curvature:.17e}"
);
}
}
#[test]
fn bessel_centered_terms_satisfy_their_defining_relations() {
for eta in [
0.5_f64, 1.0, 5.0, 12.0, 19.999, 20.0, 20.001, 25.0, 64.0, 900.0, 1.0e6, 1.0e12,
] {
let (_centered, ratio, d1) = bessel_i0_centered_terms(eta);
let naive = eta * (ratio - 1.0);
assert!(
(d1 - naive).abs() <= 8.0 * f64::EPSILON * eta,
"d1 must equal η(I1/I0 − 1) at eta={eta}: d1={d1:.17e}, naive={naive:.17e}"
);
if eta <= 64.0 {
let round_tripped = eta.ln().exp();
let (_, _, same_d1) = bessel_i0_centered_terms(round_tripped);
let curvature = bessel_i0_centered_second_log_derivative_from_log_abs(eta.ln());
let naive = -round_tripped * (2.0 * same_d1 + 1.0) - same_d1 * same_d1;
let budget =
8.0 * f64::EPSILON * (2.0 * round_tripped * same_d1.abs() + same_d1 * same_d1);
assert!(
(curvature - naive).abs() <= budget,
"c'' must equal −η(2d1+1) − d1² at eta={eta}: \
c2={curvature:.17e}, naive={naive:.17e}, budget={budget:.3e}"
);
}
assert!((0.0..1.0).contains(&ratio), "I1/I0({eta})={ratio} ∉ (0,1)");
assert!(
(-0.608_891_247_247_802..0.0).contains(&d1),
"η(I1/I0 − 1) at {eta} is {d1}, outside (min d1, 0)"
);
}
}
#[test]
fn bessel_branch_crossovers_have_no_step() {
for seam in [1.0_f64, 3.75, 20.0, 30.0] {
let delta = 1.0e-11 * seam;
let (below_c, below_r, below_d1) = bessel_i0_centered_terms(seam - delta);
let (above_c, above_r, above_d1) = bessel_i0_centered_terms(seam + delta);
let below_c2 =
bessel_i0_centered_second_log_derivative_from_log_abs((seam - delta).ln());
let above_c2 =
bessel_i0_centered_second_log_derivative_from_log_abs((seam + delta).ln());
let slope_budget = 2.0 * delta + 1.0e-14;
assert!(
(above_c - below_c).abs() < slope_budget,
"centered log steps at the {seam} seam: {below_c:.17e} -> {above_c:.17e}"
);
assert!(
(above_r - below_r).abs() < slope_budget,
"I1/I0 steps at the {seam} seam: {below_r:.17e} -> {above_r:.17e}"
);
assert!(
(above_d1 - below_d1).abs() < slope_budget,
"d1 steps at the {seam} seam: {below_d1:.17e} -> {above_d1:.17e}"
);
assert!(
(above_c2 - below_c2).abs() < slope_budget,
"c'' steps at the {seam} seam: {below_c2:.17e} -> {above_c2:.17e}"
);
}
}
#[test]
fn bessel_primitives_handle_boundary_arguments() {
let (centered, ratio, d1) = bessel_i0_centered_terms(f64::INFINITY);
assert_eq!((centered, ratio, d1), (f64::NEG_INFINITY, 1.0, -0.5));
let (centered, ratio, d1) = bessel_i0_centered_terms(f64::NEG_INFINITY);
assert_eq!((centered, ratio, d1), (f64::NEG_INFINITY, 1.0, -0.5));
let (centered, ratio, d1) = bessel_i0_centered_terms(f64::NAN);
assert!(centered.is_nan() && ratio.is_nan() && d1.is_nan());
assert!(bessel_i0_centered_second_log_derivative_from_log_abs(f64::NAN).is_nan());
for eta in [0.5_f64, 5.0, 25.0, 1.0e6] {
assert_eq!(
bessel_i0_centered_terms(-eta),
bessel_i0_centered_terms(eta)
);
}
}
#[test]
fn polygamma_family_matches_independent_high_precision_reference() {
const POLYGAMMA_REFERENCE: [[f64; 5]; 22] = [
[
1e-08,
-100000000.57721564,
1.0000000000000002e+16,
-2e+24,
5.999999999999999e+32,
],
[
0.0001,
-10000.577051183514,
100000001.64469367,
-2000000000002.403,
5.999999999999999e+16,
],
[
0.01,
-100.56088545786868,
10001.621213528313,
-2000002.340398677,
600000006.2510618,
],
[
0.1,
-10.423754940411076,
101.43329915079275,
-2001.8614573783436,
60004.51287679026,
],
[
0.25,
-4.2274535333762655,
17.19732915450711,
-129.32773993753693,
1538.7821440091884,
],
[
0.5,
-1.9635100260214235,
4.934802200544679,
-16.82879664423432,
97.40909103400244,
],
[
1.0,
-0.5772156649015329,
1.6449340668482264,
-2.4041138063191885,
6.493939402266829,
],
[
1.4616321449683622,
-9.241265521729427e-17,
0.9676722454476212,
-0.8855263379671844,
1.5509985657339065,
],
[
2.0,
0.42278433509846713,
0.6449340668482264,
-0.4041138063191886,
0.49393940226682914,
],
[
3.5,
1.103156640645243,
0.3303577561002349,
-0.1082040516417274,
0.07030584881725205,
],
[
7.0,
1.8727843350984672,
0.15354517795933756,
-0.023530472985855238,
0.007198198563125445,
],
[
8.0,
2.01564147795561,
0.1331370146940314,
-0.017699569195767775,
0.004699239795945104,
],
[
10.0,
2.251752589066721,
0.10516633568168575,
-0.011049834970802067,
0.0023199013042898686,
],
[
19.0,
2.9178924132947808,
0.05404090603769619,
-0.0029197100973139254,
0.0003154143837079449,
],
[
19.999,
2.9704727201051075,
0.05127345119229945,
-0.0026283917972403977,
0.00026941563155986057,
],
[
20.0,
2.970523992242149,
0.05127082293520312,
-0.0026281224023146548,
0.0002693742213396389,
],
[
20.001,
2.970575261751068,
0.05126819494748101,
-0.0026278530487948894,
0.0002693328196036835,
],
[
25.0,
3.198742512851974,
0.04081066325722558,
-0.001665279318422468,
0.0001358846365082737,
],
[
100.0,
4.600161852738087,
0.010050166663333571,
-0.00010100499983335,
2.030199990001333e-06,
],
[
10000.0,
9.210290371142849,
0.00010000500016666666,
-1.000100005e-08,
2.00030002e-12,
],
[
100000000.0,
18.420680738952367,
1.000000005e-08,
-1.00000001e-16,
2.0000000300000002e-24,
],
[
1000000000000000.0,
34.538776394910684,
1.0000000000000005e-15,
-1.000000000000001e-30,
2.000000000000003e-45,
],
];
for [x, want_psi, want_psi1, want_psi2, want_psi3] in POLYGAMMA_REFERENCE {
let checks = [
("ψ", digamma(x), want_psi),
("ψ₁", trigamma(x), want_psi1),
("ψ₂", tetragamma(x), want_psi2),
("ψ₃", pentagamma(x), want_psi3),
];
for (name, got, want) in checks {
let error = (got - want).abs();
let budget = 1e-14 * want.abs() + 1e-15;
assert!(
error <= budget,
"{name}({x}): got {got:.17e}, want {want:.17e} \
(error {error:.3e} > {budget:.3e})"
);
}
}
}
#[test]
fn polygamma_family_is_seamless_and_mutually_consistent() {
for threshold in [8.0_f64, 10.0, 20.0] {
let delta = 1.0e-11 * threshold;
for f in [digamma as fn(f64) -> f64, trigamma, tetragamma, pentagamma] {
let below = f(threshold - delta);
let above = f(threshold + delta);
assert!(
(above - below).abs() < 2.0 * delta + 1.0e-15,
"polygamma step at the {threshold} seam: {below:.17e} -> {above:.17e}"
);
}
}
for x in [0.75_f64, 1.5, 4.0, 9.0, 19.5, 21.0, 60.0] {
let h = 1.0e-4 * x;
for (name, value, derivative) in [
("ψ", digamma as fn(f64) -> f64, trigamma as fn(f64) -> f64),
("ψ₁", trigamma, tetragamma),
("ψ₂", tetragamma, pentagamma),
] {
let finite_difference = (value(x + h) - value(x - h)) / (2.0 * h);
let analytic = derivative(x);
assert!(
(finite_difference - analytic).abs() <= 1e-6 * analytic.abs().max(1e-3),
"d{name}/dx at {x}: analytic={analytic:.17e}, fd={finite_difference:.17e}"
);
}
}
for bad in [
0.0_f64,
-1.0,
-0.5,
f64::NAN,
f64::INFINITY,
f64::NEG_INFINITY,
] {
assert!(digamma(bad).is_nan(), "digamma({bad}) must be NaN");
assert!(trigamma(bad).is_nan(), "trigamma({bad}) must be NaN");
assert!(tetragamma(bad).is_nan(), "tetragamma({bad}) must be NaN");
assert!(pentagamma(bad).is_nan(), "pentagamma({bad}) must be NaN");
}
}
#[test]
fn gauss_legendre_integrates_polynomials_exactly() {
for n in [1usize, 2, 3, 5, 8, 40, 64] {
let (nodes, weights) = gauss_legendre(n);
assert_eq!(nodes.len(), n);
assert_eq!(weights.len(), n);
assert!(nodes.windows(2).all(|w| w[0] < w[1]), "nodes ascending");
if !n.is_multiple_of(2) {
assert_eq!(nodes[n / 2], 0.0, "odd-n central node is exact zero");
}
let total: f64 = weights.iter().sum();
assert!((total - 2.0).abs() < 1e-13, "∫1 dx = 2, got {total}");
if n >= 2 {
let x2: f64 = nodes.iter().zip(&weights).map(|(x, w)| w * x * x).sum();
assert!((x2 - 2.0 / 3.0).abs() < 1e-13, "∫x² dx = 2/3, got {x2}");
}
for degree in 0..(2 * n) {
let term = |(x, w): (&f64, &f64)| w * x.powi(degree as i32);
let quadrature: f64 = nodes.iter().zip(&weights).map(term).sum();
let magnitude: f64 = nodes.iter().zip(&weights).map(|p| term(p).abs()).sum();
let exact = if degree % 2 == 1 {
0.0
} else {
2.0 / (degree as f64 + 1.0)
};
let scale = magnitude.max(exact);
if scale == 0.0 {
assert_eq!(quadrature, 0.0, "n={n}, x^{degree}");
continue;
}
assert!(
(quadrature - exact).abs() / scale < 1.0e-13,
"n={n} rule must integrate x^{degree} exactly: got {quadrature:.17e}, \
want {exact:.17e}"
);
}
}
}
#[test]
fn gauss_legendre_weights_match_independent_high_precision_reference() {
const GL8: [(f64, f64); 4] = [
(0.183434642495649805, 0.362683783378361983),
(0.525532409916328986, 0.313706645877887287),
(0.796666477413626740, 0.222381034453374471),
(0.960289856497536232, 0.101228536290376259),
];
const GL16: [(f64, f64); 8] = [
(0.0950125098376374402, 0.189450610455068496),
(0.281603550779258913, 0.182603415044923589),
(0.458016777657227386, 0.169156519395002538),
(0.617876244402643748, 0.149595988816576732),
(0.755404408355003034, 0.124628971255533872),
(0.865631202387831744, 0.0951585116824927848),
(0.944575023073232576, 0.0622535239386478929),
(0.989400934991649933, 0.0271524594117540949),
];
const NODE_TOL: f64 = 4.0e-16;
const WEIGHT_TOL: f64 = 1.0e-14;
for (n, reference) in [(8usize, &GL8[..]), (16, &GL16[..])] {
let (nodes, weights) = gauss_legendre(n);
for (k, &(want_node, want_weight)) in reference.iter().enumerate() {
let index = n / 2 + k;
let (got_node, got_weight) = (nodes[index], weights[index]);
assert!(
(got_node - want_node).abs() < NODE_TOL,
"n={n} node {index}: got {got_node:.17e}, want {want_node:.17e}"
);
let relative = (got_weight - want_weight).abs() / want_weight.abs();
assert!(
relative < WEIGHT_TOL,
"n={n} weight {index}: got {got_weight:.17e}, want {want_weight:.17e}, \
rel {relative:.3e}"
);
let mirror = n / 2 - 1 - k;
assert_eq!(
nodes[mirror], -got_node,
"n={n} node {mirror} mirrors {index}"
);
assert_eq!(weights[mirror], got_weight, "n={n} weight {mirror} mirrors");
}
}
}
#[test]
fn binom_k_exceeds_n_returns_zero() {
assert_eq!(binomial_coefficient_f64(3, 5), 0.0);
assert_eq!(binomial_coefficient_f64(0, 1), 0.0);
assert_eq!(binomial_coefficient_f64(10, 11), 0.0);
}
#[test]
fn binom_k_zero_returns_one() {
assert_eq!(binomial_coefficient_f64(0, 0), 1.0);
assert_eq!(binomial_coefficient_f64(5, 0), 1.0);
assert_eq!(binomial_coefficient_f64(100, 0), 1.0);
}
#[test]
fn binom_k_equals_n_returns_one() {
assert_eq!(binomial_coefficient_f64(1, 1), 1.0);
assert_eq!(binomial_coefficient_f64(5, 5), 1.0);
assert_eq!(binomial_coefficient_f64(20, 20), 1.0);
}
#[test]
fn binom_small_exact_values() {
assert_eq!(binomial_coefficient_f64(5, 2), 10.0);
assert_eq!(binomial_coefficient_f64(10, 3), 120.0);
assert_eq!(binomial_coefficient_f64(20, 10), 184_756.0);
assert_eq!(binomial_coefficient_f64(6, 3), 20.0);
}
#[test]
fn binom_symmetry() {
assert_eq!(
binomial_coefficient_f64(10, 3),
binomial_coefficient_f64(10, 7)
);
assert_eq!(
binomial_coefficient_f64(20, 5),
binomial_coefficient_f64(20, 15)
);
assert_eq!(
binomial_coefficient_f64(54, 24),
binomial_coefficient_f64(54, 30)
);
}
#[test]
fn binom_c54_24_is_exact() {
assert_eq!(binomial_coefficient_f64(54, 24), 1_402_659_561_581_460.0);
}
#[test]
fn poly_exp_empty_coeffs_returns_zero() {
assert_eq!(stable_polynomial_times_exp_neg(1.0, &[]), 0.0);
assert_eq!(stable_polynomial_times_exp_neg(0.0, &[]), 0.0);
assert_eq!(stable_polynomial_times_exp_neg(700.0, &[]), 0.0);
}
#[test]
fn poly_exp_nonfinite_x_returns_zero() {
assert_eq!(
stable_polynomial_times_exp_neg(f64::INFINITY, &[1.0, 2.0]),
0.0
);
assert_eq!(
stable_polynomial_times_exp_neg(f64::NEG_INFINITY, &[1.0, 2.0]),
0.0
);
assert_eq!(stable_polynomial_times_exp_neg(f64::NAN, &[1.0]), 0.0);
}
#[test]
fn poly_exp_constant_at_zero() {
assert_eq!(stable_polynomial_times_exp_neg(0.0, &[5.0]), 5.0);
assert_eq!(stable_polynomial_times_exp_neg(0.0, &[3.0, 1.0, 2.0]), 3.0);
}
#[test]
fn poly_exp_constant_poly_direct_path() {
let x = 2.0;
let got = stable_polynomial_times_exp_neg(x, &[3.0]);
let expected = 3.0 * (-x).exp();
assert!(
(got - expected).abs() < 1e-14,
"got={got} expected={expected}"
);
}
#[test]
fn poly_exp_linear_poly_direct_path() {
let x = 1.5;
let (a, b) = (2.0, 3.0);
let got = stable_polynomial_times_exp_neg(x, &[a, b]);
let expected = (a + b * x) * (-x).exp();
assert!(
(got - expected).abs() < 1e-14,
"got={got} expected={expected}"
);
}
#[test]
fn poly_exp_constant_poly_asymptotic_path() {
let x = 700.0_f64;
let got = stable_polynomial_times_exp_neg(x, &[1.0]);
let expected = (-x).exp();
let rel = (got - expected).abs() / expected;
assert!(rel < 1e-12, "got={got} expected={expected} rel={rel}");
}
#[test]
fn poly_exp_quadratic_asymptotic_path() {
let x = 620.0_f64;
let got = stable_polynomial_times_exp_neg(x, &[0.0, 0.0, 1.0]);
let expected = (2.0 * x.ln() - x).exp();
let rel = (got - expected).abs() / expected.abs();
assert!(rel < 1e-12, "got={got} expected={expected} rel={rel}");
}
#[test]
fn centered_bessel_second_log_derivative_matches_high_precision_reference() {
const CASES: [(f64, f64, f64); 13] = [
(0.5, -0.2647015155254598, 1e-14),
(1.0, -0.19926400165310923, 1e-14),
(2.0, 0.05244210681284669, 1e-13),
(5.0, 0.0466642611317311, 1e-13),
(10.0, 0.015837019843595493, 1e-12),
(15.0, 0.009659446256568909, 1e-11),
(18.85, 0.00743799786561837, 1e-11),
(19.99, 0.006964307582746309, 1e-11),
(20.0, 0.006960419930170057, 1e-12),
(25.0, 0.005442291838848013, 1e-14),
(50.0, 0.0026049656149811874, 1e-15),
(200.0, 0.0006313242744933583, 1e-15),
(1e4, 1.2502500586100053e-05, 1e-15),
];
for (eta, expected, tolerance) in CASES {
let got = bessel_i0_centered_second_log_derivative_from_log_abs(eta.ln());
let relative = (got - expected).abs() / expected.abs();
assert!(
relative < tolerance,
"eta={eta}: got={got} expected={expected} rel={relative:e} tol={tolerance:e}"
);
}
}
#[test]
fn centered_bessel_second_log_derivative_is_continuous_across_the_crossover() {
const STEP: f64 = 1e-11;
let below = bessel_i0_centered_second_log_derivative_from_log_abs(
(BESSEL_ASYMPTOTIC_THRESHOLD - STEP).ln(),
);
let above =
bessel_i0_centered_second_log_derivative_from_log_abs(BESSEL_ASYMPTOTIC_THRESHOLD.ln());
assert!(
below != above,
"step {STEP:e} was rounded away; the two sides are the same evaluation"
);
let jump = (below - above).abs() / above.abs();
assert!(
jump < 3e-11,
"seam jump {jump:e}: below={below} above={above}"
);
}
}