use libm::{erf, erfc};
use statrs::function::{
beta::{beta_reg, inv_beta_reg, ln_beta},
gamma::gamma_ur,
};
const INV_SQRT_PI: f64 = 0.564_189_583_547_756_3;
const SQRT_2_OVER_PI: f64 = 0.797_884_560_802_865_4;
pub fn beta_quantile(p: f64, a: f64, b: f64) -> f64 {
if !(a.is_finite() && a > 0.0 && b.is_finite() && b > 0.0) {
return f64::NAN;
}
if !p.is_finite() || p <= 0.0 {
return 0.0;
}
if p >= 1.0 {
return 1.0;
}
match lower_tail_beta_quantile(p, a, b) {
Some(x) => x,
None => inv_beta_reg(a, b, p),
}
}
fn lower_tail_beta_quantile(p: f64, a: f64, b: f64) -> Option<f64> {
let ln_b = ln_beta(a, b);
if !ln_b.is_finite() {
return None;
}
let mut y = (p.ln() + a.ln() + ln_b) / a;
if !y.is_finite() {
return None;
}
let ratio_bound = (0.5_f64).ln() - b.max(1.0).ln();
if !(y <= ratio_bound) {
return None;
}
let ln_p = p.ln();
for _ in 0..BETA_NEWTON_MAX_STEPS {
let x = y.exp();
if x * b.max(1.0) > 0.5 {
return None;
}
let (sum, derivative_sum) = beta_ascending_series(x, a, b)?;
if !(sum.is_finite() && sum > 0.0 && derivative_sum.is_finite()) {
return None;
}
let g = a * y - ln_b + sum.ln() - ln_p;
let g_prime = a + x * derivative_sum / sum;
if !(g.is_finite() && g_prime.is_finite() && g_prime > 0.0) {
return None;
}
let step = g / g_prime;
if !step.is_finite() {
return None;
}
y -= step;
if step.abs() <= f64::EPSILON * y.abs().max(1.0) {
break;
}
}
let x = y.exp();
if x.is_finite() && (0.0..=1.0).contains(&x) {
Some(x)
} else {
None
}
}
fn beta_ascending_series(x: f64, a: f64, b: f64) -> Option<(f64, f64)> {
let mut pochhammer_over_factorial = 1.0_f64;
let mut power = 1.0_f64;
let mut sum = 1.0 / a;
let mut derivative_sum = 0.0_f64;
for k in 1..=BETA_SERIES_MAX_TERMS {
let kf = k as f64;
pochhammer_over_factorial *= (kf - b) / kf;
let coefficient = pochhammer_over_factorial / (a + kf);
derivative_sum += kf * coefficient * power;
power *= x;
let term = coefficient * power;
sum += term;
if term.abs() <= f64::EPSILON * sum.abs() {
return Some((sum, derivative_sum));
}
}
None
}
fn regularized_beta_lower_from_log_x(log_x: f64, a: f64, b: f64) -> f64 {
if !(a.is_finite() && a > 0.0 && b.is_finite() && b > 0.0)
|| log_x.is_nan()
|| log_x > 0.0
{
return f64::NAN;
}
if log_x == 0.0 {
return 1.0;
}
if log_x == f64::NEG_INFINITY {
return 0.0;
}
let series_limit = (0.5_f64).ln() - b.max(1.0).ln();
if log_x <= series_limit {
let x = log_x.exp();
let Some((sum, _)) = beta_ascending_series(x, a, b) else {
return f64::NAN;
};
let log_beta = ln_beta(a, b);
if !(sum.is_finite() && sum > 0.0 && log_beta.is_finite()) {
return f64::NAN;
}
return (a * log_x - log_beta + sum.ln()).exp();
}
beta_reg(a, b, log_x.exp())
}
#[inline]
fn log_reciprocal_one_plus_exp(log_ratio: f64) -> f64 {
if log_ratio <= 0.0 {
-log_ratio.exp().ln_1p()
} else {
-log_ratio - (-log_ratio).exp().ln_1p()
}
}
const BETA_SERIES_MAX_TERMS: usize = 128;
const BETA_NEWTON_MAX_STEPS: usize = 32;
#[inline]
fn square_residual(x: f64, rounded_square: f64) -> f64 {
x.mul_add(x, -rounded_square)
}
#[inline]
pub fn normal_pdf(x: f64) -> f64 {
const INV_SQRT_2PI: f64 = 0.398_942_280_401_432_7;
let rounded_square = x * x;
let head = INV_SQRT_2PI * (-0.5 * rounded_square).exp();
if head == 0.0 || head.is_nan() {
return head;
}
let residual = square_residual(x, rounded_square);
head.mul_add(-0.5 * residual, head)
}
#[inline]
pub fn normal_cdf(x: f64) -> f64 {
0.5 * erfc(-x / std::f64::consts::SQRT_2)
}
#[inline]
pub fn normal_two_sided_probability(z: f64) -> f64 {
erfc(z.abs() / std::f64::consts::SQRT_2)
}
pub fn student_t_two_sided_probability(t: f64, degrees_of_freedom: f64) -> f64 {
let half_df = 0.5 * degrees_of_freedom;
if t.is_nan()
|| !(degrees_of_freedom.is_finite()
&& degrees_of_freedom > 0.0
&& half_df > 0.0)
{
return f64::NAN;
}
if t.is_infinite() {
return 0.0;
}
let log_t_squared_over_df = 2.0 * t.abs().ln() - degrees_of_freedom.ln();
let log_x = log_reciprocal_one_plus_exp(log_t_squared_over_df);
regularized_beta_lower_from_log_x(log_x, half_df, 0.5)
}
pub fn chi_square_sf(statistic: f64, degrees_of_freedom: f64) -> f64 {
let half_df = 0.5 * degrees_of_freedom;
if statistic.is_nan()
|| statistic < 0.0
|| !(degrees_of_freedom.is_finite()
&& degrees_of_freedom > 0.0
&& half_df > 0.0)
{
return f64::NAN;
}
if statistic == 0.0 {
return 1.0;
}
if statistic == f64::INFINITY {
return 0.0;
}
gamma_ur(half_df, 0.5 * statistic)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WeightedChiSquareTerm {
pub weight: f64,
pub degrees_of_freedom: f64,
}
pub fn signed_weighted_chi_square_sf_to_tolerance(
terms: &[WeightedChiSquareTerm],
statistic: f64,
absolute_tolerance: f64,
) -> (f64, f64) {
let tolerance = if absolute_tolerance.is_finite() && absolute_tolerance > 0.0 {
absolute_tolerance
} else {
WEIGHTED_CHI_SQUARE_TOLERANCE
};
if statistic.is_nan() {
return (f64::NAN, f64::NAN);
}
let mut active = Vec::with_capacity(terms.len());
for term in terms {
if !term.weight.is_finite()
|| !(term.degrees_of_freedom.is_finite() && term.degrees_of_freedom > 0.0)
{
return (f64::NAN, f64::NAN);
}
if term.weight != 0.0 {
active.push(*term);
}
}
if active.is_empty() {
return (if statistic < 0.0 { 1.0 } else { 0.0 }, 0.0);
}
let all_positive = active.iter().all(|term| term.weight > 0.0);
let all_negative = active.iter().all(|term| term.weight < 0.0);
if all_positive && statistic <= 0.0 {
return (1.0, 0.0);
}
if all_negative && statistic >= 0.0 {
return (0.0, 0.0);
}
let first = active[0].weight;
if active.iter().all(|term| term.weight == first) {
let total_df: f64 = active.iter().map(|term| term.degrees_of_freedom).sum();
let scaled = statistic / first;
let tail = if first > 0.0 {
chi_square_sf(scaled, total_df)
} else {
1.0 - chi_square_sf(scaled, total_df)
};
return (tail, 0.0);
}
imhof_survival(&active, statistic, tolerance)
}
pub const WEIGHTED_CHI_SQUARE_TOLERANCE: f64 = 1e-11;
const GAUSS_LEGENDRE_16: [(f64, f64); 8] = [
(0.095_012_509_837_637_44, 0.189_450_610_455_068_64),
(0.281_603_550_779_258_9, 0.182_603_415_044_923_64),
(0.458_016_777_657_227_37, 0.169_156_519_395_002_65),
(0.617_876_244_402_643_8, 0.149_595_988_816_576_7),
(0.755_404_408_355_003, 0.124_628_971_255_534_07),
(0.865_631_202_387_831_8, 0.095_158_511_682_492_6),
(0.944_575_023_073_232_6, 0.062_253_523_938_647_456),
(0.989_400_934_991_649_9, 0.027_152_459_411_754_176),
];
#[inline]
fn imhof_integrand(terms: &[WeightedChiSquareTerm], statistic: f64, u: f64) -> f64 {
if u == 0.0 {
let mean: f64 = terms
.iter()
.map(|term| term.weight * term.degrees_of_freedom)
.sum();
return 0.5 * (mean - statistic);
}
let mut phase = -0.5 * statistic * u;
let mut log_rho = 0.0;
for term in terms {
let wu = term.weight * u;
phase += 0.5 * term.degrees_of_freedom * wu.atan();
log_rho += 0.25 * term.degrees_of_freedom * wu.mul_add(wu, 1.0).ln();
}
phase.sin() / (u * log_rho.exp())
}
#[inline]
fn imhof_log_rho(terms: &[WeightedChiSquareTerm], u: f64) -> f64 {
terms
.iter()
.map(|term| {
let wu = term.weight * u;
0.25 * term.degrees_of_freedom * wu.mul_add(wu, 1.0).ln()
})
.sum()
}
#[inline]
fn imhof_phase_slack(terms: &[WeightedChiSquareTerm], u: f64) -> f64 {
terms
.iter()
.map(|term| {
let wu = term.weight * u;
0.5 * term.degrees_of_freedom * term.weight.abs() / wu.mul_add(wu, 1.0)
})
.sum()
}
#[inline]
fn imhof_amplitude_panel(max_abs_weight: f64, tolerance: f64) -> f64 {
let node_count = 2.0 * GAUSS_LEGENDRE_16.len() as f64;
let bernstein = tolerance.recip().powf(0.5 / node_count);
let semi_minor_ratio = 0.5 * (bernstein - bernstein.recip());
if !(semi_minor_ratio > 0.0 && max_abs_weight > 0.0) {
return f64::INFINITY;
}
2.0 / (max_abs_weight * semi_minor_ratio)
}
#[inline]
fn imhof_amplitude_bound(terms: &[WeightedChiSquareTerm], u: f64) -> Option<f64> {
let active_df: f64 = terms
.iter()
.filter(|term| term.weight.abs() * u >= 1.0)
.map(|term| term.degrees_of_freedom)
.sum();
(active_df > 0.0).then(|| 4.0 / (active_df * imhof_log_rho(terms, u).exp()))
}
pub const IMHOF_MAX_PANELS: usize = 1 << 21;
fn imhof_survival(
terms: &[WeightedChiSquareTerm],
statistic: f64,
tolerance: f64,
) -> (f64, f64) {
let rate: f64 = terms
.iter()
.map(|term| term.degrees_of_freedom * term.weight.abs())
.sum();
let phase_panel = 4.0 * std::f64::consts::PI / (statistic.abs() + rate);
let max_abs_weight = terms
.iter()
.map(|term| term.weight.abs())
.fold(0.0_f64, f64::max);
let panel = phase_panel.min(imhof_amplitude_panel(max_abs_weight, tolerance));
let mut integral = 0.0_f64;
let mut lower = 0.0_f64;
let mut bound = f64::INFINITY;
for _ in 0..IMHOF_MAX_PANELS {
let upper = lower + panel;
let half = 0.5 * (upper - lower);
let mid = 0.5 * (upper + lower);
let mut panel_value = 0.0;
for &(node, weight) in &GAUSS_LEGENDRE_16 {
let offset = half * node;
panel_value += weight
* (imhof_integrand(terms, statistic, mid + offset)
+ imhof_integrand(terms, statistic, mid - offset));
}
integral += half * panel_value;
lower = upper;
bound = imhof_amplitude_bound(terms, lower).unwrap_or(f64::INFINITY);
if statistic > 0.0 && imhof_phase_slack(terms, lower) <= 0.25 * statistic {
let oscillatory =
16.0 / (statistic * lower * imhof_log_rho(terms, lower).exp());
bound = bound.min(oscillatory);
}
if bound <= tolerance {
break;
}
}
(
(0.5 + integral / std::f64::consts::PI).clamp(0.0, 1.0),
bound,
)
}
pub fn fisher_snedecor_sf(
statistic: f64,
numerator_degrees_of_freedom: f64,
denominator_degrees_of_freedom: f64,
) -> f64 {
let beta_a = 0.5 * denominator_degrees_of_freedom;
let beta_b = 0.5 * numerator_degrees_of_freedom;
if statistic.is_nan()
|| statistic < 0.0
|| !(numerator_degrees_of_freedom.is_finite()
&& numerator_degrees_of_freedom > 0.0
&& denominator_degrees_of_freedom.is_finite()
&& denominator_degrees_of_freedom > 0.0
&& beta_a > 0.0
&& beta_b > 0.0)
{
return f64::NAN;
}
if statistic == 0.0 {
return 1.0;
}
if statistic == f64::INFINITY {
return 0.0;
}
let log_ratio = numerator_degrees_of_freedom.ln() + statistic.ln()
- denominator_degrees_of_freedom.ln();
let log_x = log_reciprocal_one_plus_exp(log_ratio);
regularized_beta_lower_from_log_x(log_x, beta_a, beta_b)
}
#[inline]
pub fn erfcx_nonnegative(x: f64) -> f64 {
if x.is_nan() || x < 0.0 {
return f64::NAN;
}
if x == f64::INFINITY {
return 0.0;
}
if x < 26.0 {
let rounded_square = x * x;
let head = rounded_square.exp() * erfc(x);
head.mul_add(square_residual(x, rounded_square), head)
} else {
let inv = 1.0 / x;
let inv2 = inv * inv;
let poly = 1.0
+ inv2
* (-0.5
+ inv2
* (0.75
+ inv2
* (-1.875
+ inv2 * (6.5625 + inv2 * (-29.53125 + inv2 * 162.421875)))));
inv * poly * INV_SQRT_PI
}
}
#[inline]
pub fn log1mexp_positive(a: f64) -> f64 {
assert!(a >= 0.0, "log1mexp_positive requires a >= 0: a={a}");
if a == f64::INFINITY {
return 0.0;
}
if a > core::f64::consts::LN_2 {
(-(-a).exp()).ln_1p()
} else if a > 0.0 {
(-(-a).exp_m1()).ln()
} else {
f64::NEG_INFINITY
}
}
const EXACT_BINARY64_SUM_WORDS: usize = 33;
const EXACT_BINARY64_SUM_MAX_TERMS: usize = (1 << 14) - 1;
const _: () = assert!(EXACT_BINARY64_SUM_WORDS * 64 == 2112);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExactBinary64SumSignError {
NonFiniteTerm { index: usize },
TermCapacityExceeded { maximum: usize },
}
impl std::fmt::Display for ExactBinary64SumSignError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NonFiniteTerm { index } => {
write!(formatter, "exact binary64 sum term {index} is not finite")
}
Self::TermCapacityExceeded { maximum } => write!(
formatter,
"exact binary64 sum exceeds its structural {maximum}-term capacity"
),
}
}
}
impl std::error::Error for ExactBinary64SumSignError {}
pub fn exact_binary64_sum_sign(
values: impl IntoIterator<Item = f64>,
) -> Result<std::cmp::Ordering, ExactBinary64SumSignError> {
fn add_magnitude(
accumulator: &mut [u64; EXACT_BINARY64_SUM_WORDS],
value: f64,
) -> Result<(), ExactBinary64SumSignError> {
let magnitude_bits = value.to_bits() & !(1_u64 << 63);
let exponent_bits = ((magnitude_bits >> 52) & 0x7ff) as usize;
let fraction = magnitude_bits & ((1_u64 << 52) - 1);
let (significand, shift) = if exponent_bits == 0 {
(fraction, 0usize)
} else {
((1_u64 << 52) | fraction, exponent_bits - 1)
};
if significand == 0 {
return Ok(());
}
let mut word = shift / 64;
let offset = shift % 64;
let (low_sum, low_carry) =
accumulator[word].overflowing_add(significand << offset);
accumulator[word] = low_sum;
word += 1;
let high = if offset == 0 {
0
} else {
significand >> (64 - offset)
};
let (high_sum, high_carry) = accumulator[word].overflowing_add(high);
let (high_sum, carry_carry) = high_sum.overflowing_add(u64::from(low_carry));
accumulator[word] = high_sum;
let mut carry = high_carry || carry_carry;
word += 1;
while carry {
if word == EXACT_BINARY64_SUM_WORDS {
return Err(ExactBinary64SumSignError::TermCapacityExceeded {
maximum: EXACT_BINARY64_SUM_MAX_TERMS,
});
}
let (sum, next_carry) = accumulator[word].overflowing_add(1);
accumulator[word] = sum;
carry = next_carry;
word += 1;
}
Ok(())
}
let mut positive = [0_u64; EXACT_BINARY64_SUM_WORDS];
let mut negative = [0_u64; EXACT_BINARY64_SUM_WORDS];
for (index, value) in values.into_iter().enumerate() {
if index == EXACT_BINARY64_SUM_MAX_TERMS {
return Err(ExactBinary64SumSignError::TermCapacityExceeded {
maximum: EXACT_BINARY64_SUM_MAX_TERMS,
});
}
if !value.is_finite() {
return Err(ExactBinary64SumSignError::NonFiniteTerm { index });
}
let target = if value.is_sign_negative() {
&mut negative
} else {
&mut positive
};
add_magnitude(target, value)?;
}
for index in (0..EXACT_BINARY64_SUM_WORDS).rev() {
match positive[index].cmp(&negative[index]) {
std::cmp::Ordering::Less => return Ok(std::cmp::Ordering::Less),
std::cmp::Ordering::Greater => return Ok(std::cmp::Ordering::Greater),
std::cmp::Ordering::Equal => {}
}
}
Ok(std::cmp::Ordering::Equal)
}
pub fn signed_log_sum_exp(log_mags: &[f64], signs: &[f64]) -> (f64, f64) {
let mut has_pos_inf = false;
let mut has_neg_inf = false;
for (idx, &lm) in log_mags.iter().enumerate() {
if lm == f64::INFINITY {
if signs[idx] > 0.0 {
has_pos_inf = true;
} else if signs[idx] < 0.0 {
has_neg_inf = true;
}
}
}
match (has_pos_inf, has_neg_inf) {
(true, true) => return (f64::NAN, 0.0),
(true, false) => return (f64::INFINITY, 1.0),
(false, true) => return (f64::INFINITY, -1.0),
(false, false) => {}
}
let mut pos_max = f64::NEG_INFINITY;
let mut neg_max = f64::NEG_INFINITY;
for (idx, &lm) in log_mags.iter().enumerate() {
if signs[idx] > 0.0 {
pos_max = pos_max.max(lm);
} else if signs[idx] < 0.0 {
neg_max = neg_max.max(lm);
}
}
if pos_max == f64::NEG_INFINITY && neg_max == f64::NEG_INFINITY {
return (f64::NEG_INFINITY, 0.0);
}
let common_max = pos_max.max(neg_max);
let mut signed_head = 0.0_f64;
let mut signed_tail = 0.0_f64;
let mut absolute_scaled_sum = 0.0_f64;
let mut finite_term_count = 0usize;
for (idx, &lm) in log_mags.iter().enumerate() {
if !lm.is_finite() || !(signs[idx] > 0.0 || signs[idx] < 0.0) {
continue;
}
let magnitude = (lm - common_max).exp();
let term = if signs[idx] > 0.0 {
magnitude
} else {
-magnitude
};
let combined = signed_head + term;
let shifted = combined - signed_head;
let residual = (signed_head - (combined - shifted)) + (term - shifted);
signed_head = combined;
signed_tail += residual;
absolute_scaled_sum += magnitude;
finite_term_count += 1;
}
let signed_scaled_sum = signed_head + signed_tail;
let direct_error_bound =
(finite_term_count as f64 + 2.0) * f64::EPSILON * absolute_scaled_sum;
if signed_scaled_sum.abs() > direct_error_bound {
return (
common_max + signed_scaled_sum.abs().ln(),
signed_scaled_sum.signum(),
);
}
let mut pos_sum = 0.0_f64;
let mut pos_tail = 0.0_f64;
let mut neg_sum = 0.0_f64;
let mut neg_tail = 0.0_f64;
for (idx, &lm) in log_mags.iter().enumerate() {
if !lm.is_finite() {
continue;
}
if signs[idx] > 0.0 {
let term = (lm - pos_max).exp();
let combined = pos_sum + term;
let shifted = combined - pos_sum;
pos_tail += (pos_sum - (combined - shifted)) + (term - shifted);
pos_sum = combined;
} else if signs[idx] < 0.0 {
let term = (lm - neg_max).exp();
let combined = neg_sum + term;
let shifted = combined - neg_sum;
neg_tail += (neg_sum - (combined - shifted)) + (term - shifted);
neg_sum = combined;
}
}
pos_sum += pos_tail;
neg_sum += neg_tail;
let log_pos = if pos_sum > 0.0 {
pos_max + pos_sum.ln()
} else {
f64::NEG_INFINITY
};
let log_neg = if neg_sum > 0.0 {
neg_max + neg_sum.ln()
} else {
f64::NEG_INFINITY
};
if log_neg == f64::NEG_INFINITY {
return (log_pos, 1.0);
}
if log_pos == f64::NEG_INFINITY {
return (log_neg, -1.0);
}
if log_pos > log_neg {
let gap = log_pos - log_neg;
(log_pos + log1mexp_positive(gap), 1.0)
} else if log_neg > log_pos {
let gap = log_neg - log_pos;
(log_neg + log1mexp_positive(gap), -1.0)
} else {
(f64::NEG_INFINITY, 0.0)
}
}
#[inline]
pub fn normal_logcdf(x: f64) -> f64 {
if x == f64::INFINITY {
return 0.0;
}
if x == f64::NEG_INFINITY {
return f64::NEG_INFINITY;
}
if x.is_nan() {
return f64::NAN;
}
if x < 0.0 {
let (u, scaled_tail) = negative_normal_tail_components(x);
negative_normal_logcdf_from_scaled_tail(u, scaled_tail)
} else {
let upper_tail = 0.5 * erfc(x / std::f64::consts::SQRT_2);
(-upper_tail).ln_1p()
}
}
#[inline]
pub fn normal_logsf(x: f64) -> f64 {
normal_logcdf(-x)
}
#[inline]
pub fn signed_probit_logcdf_and_mills_ratio(x: f64) -> (f64, f64) {
if x == f64::INFINITY {
return (0.0, 0.0);
}
if x == f64::NEG_INFINITY {
return (f64::NEG_INFINITY, f64::INFINITY);
}
if x.is_nan() {
return (f64::NAN, f64::NAN);
}
if x < 0.0 {
let (u, scaled_tail) = negative_normal_tail_components(x);
(
negative_normal_logcdf_from_scaled_tail(u, scaled_tail),
SQRT_2_OVER_PI / scaled_tail,
)
} else {
let upper_tail = 0.5 * erfc(x / std::f64::consts::SQRT_2);
let cdf = 1.0 - upper_tail;
let lambda = normal_pdf(x) / cdf;
((-upper_tail).ln_1p(), lambda)
}
}
#[inline]
fn negative_normal_tail_components(x: f64) -> (f64, f64) {
assert!(x.is_finite() && x < 0.0);
let u = -x / std::f64::consts::SQRT_2;
(u, erfcx_nonnegative(u))
}
#[inline]
fn negative_normal_logcdf_from_scaled_tail(u: f64, scaled_tail: f64) -> f64 {
-u * u + scaled_tail.ln() - std::f64::consts::LN_2
}
#[inline]
pub fn normal_logcdf_derivatives(x: f64) -> [f64; 5] {
if x.is_nan() {
return [f64::NAN; 5];
}
if x == f64::INFINITY {
return [0.0; 5];
}
if x == f64::NEG_INFINITY {
return [f64::NEG_INFINITY, f64::INFINITY, -1.0, 0.0, 0.0];
}
const RIGHT_LOG_MAGNITUDE_SWITCH: f64 = 8.0;
if x <= LEFT_CONTINUED_FRACTION_SWITCH {
return normal_logcdf_derivatives_left_tail(x);
}
if x >= RIGHT_LOG_MAGNITUDE_SWITCH {
return normal_logcdf_derivatives_right_tail(x);
}
let (log_cdf, lambda) = signed_probit_logcdf_and_mills_ratio(x);
let x2 = x * x;
if x < 0.0 {
let q = lambda + x;
let q2 = q * q;
return [
log_cdf,
lambda,
-lambda * q,
lambda * (2.0 * q2 - x * q - 1.0),
lambda * (-6.0 * q2 * q + 6.0 * x * q2 + (4.0 - x2) * q - x),
];
}
let lambda2 = lambda * lambda;
let lambda3 = lambda2 * lambda;
[
log_cdf,
lambda,
-lambda * (x + lambda),
lambda * (x2 - 1.0 + 3.0 * x * lambda + 2.0 * lambda2),
-lambda
* ((x * x2 - 3.0 * x) + (7.0 * x2 - 4.0) * lambda + 12.0 * x * lambda2 + 6.0 * lambda3),
]
}
#[derive(Clone, Copy)]
struct MillsCorrectionDerivatives {
value: f64,
first: f64,
second: f64,
third: f64,
}
const LEFT_CONTINUED_FRACTION_SWITCH: f64 = -4.0;
#[inline]
fn mills_correction_continued_fraction(t: f64) -> MillsCorrectionDerivatives {
assert!(t.is_finite() && t >= 4.0);
let mut q = MillsCorrectionDerivatives {
value: 0.0,
first: 0.0,
second: 0.0,
third: 0.0,
};
for n in (1..=64).rev() {
let denominator = t + q.value;
let inv_denominator = denominator.recip();
let value = f64::from(n) / denominator;
let denominator_first = 1.0 + q.first;
let a = denominator_first * inv_denominator;
let b = q.second * inv_denominator;
let c = q.third * inv_denominator;
q = MillsCorrectionDerivatives {
value,
first: -value * denominator_first / denominator,
second: value * (2.0 * a * a - b),
third: value * (-6.0 * a * a * a + 6.0 * a * b - c),
};
}
q
}
#[inline]
fn normal_logcdf_derivatives_left_tail(x: f64) -> [f64; 5] {
assert!(x.is_finite() && x <= LEFT_CONTINUED_FRACTION_SWITCH);
let t = -x;
let q = mills_correction_continued_fraction(t);
[
normal_logcdf(x),
t + q.value,
-(1.0 + q.first),
q.second,
-q.third,
]
}
#[inline]
fn normal_logcdf_derivatives_right_tail(x: f64) -> [f64; 5] {
assert!(x.is_finite() && x >= 8.0);
const LOG_SQRT_2PI: f64 = 0.918_938_533_204_672_7;
let log_cdf = normal_logcdf(x);
let u = x / std::f64::consts::SQRT_2;
let log_lambda = -u * u - LOG_SQRT_2PI - log_cdf;
let log_x = x.ln();
let inv_x2 = x.recip() * x.recip();
let first = log_lambda.exp();
let second = signed_exp_sum(&[log_x + log_lambda, 2.0 * log_lambda], &[-1.0, -1.0]);
let third = signed_exp_sum(
&[
2.0 * log_x + (-inv_x2).ln_1p() + log_lambda,
3.0_f64.ln() + log_x + 2.0 * log_lambda,
2.0_f64.ln() + 3.0 * log_lambda,
],
&[1.0, 1.0, 1.0],
);
let fourth = signed_exp_sum(
&[
3.0 * log_x + (-3.0 * inv_x2).ln_1p() + log_lambda,
7.0_f64.ln() + 2.0 * log_x + (-(4.0 / 7.0) * inv_x2).ln_1p() + 2.0 * log_lambda,
12.0_f64.ln() + log_x + 3.0 * log_lambda,
6.0_f64.ln() + 4.0 * log_lambda,
],
&[-1.0, -1.0, -1.0, -1.0],
);
[log_cdf, first, second, third, fourth]
}
#[inline]
fn signed_exp_sum(log_magnitudes: &[f64], signs: &[f64]) -> f64 {
let (log_magnitude, sign) = signed_log_sum_exp(log_magnitudes, signs);
if sign == 0.0 {
0.0
} else {
sign * log_magnitude.exp()
}
}
#[inline]
fn acklam_lower_tail_quantile_from_log_probability(log_p: f64) -> f64 {
const C: [f64; 6] = [
-7.784_894_002_430_293e-3,
-3.223_964_580_411_365e-1,
-2.400_758_277_161_838,
-2.549_732_539_343_734,
4.374_664_141_464_968,
2.938_163_982_698_783,
];
const D: [f64; 4] = [
7.784_695_709_041_462e-3,
3.224_671_290_700_398e-1,
2.445_134_137_142_996,
3.754_408_661_907_416,
];
let q = (-2.0 * log_p).sqrt();
(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
/ ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
}
#[inline]
pub fn standard_normal_quantile(p: f64) -> Result<f64, String> {
if !(p.is_finite() && p > 0.0 && p < 1.0) {
return Err(format!("normal quantile requires p in (0,1), got {p}"));
}
const A: [f64; 6] = [
-3.969_683_028_665_376e1,
2.209_460_984_245_205e2,
-2.759_285_104_469_687e2,
1.383_577_518_672_69e2,
-3.066_479_806_614_716e1,
2.506_628_277_459_239,
];
const B: [f64; 5] = [
-5.447_609_879_822_406e1,
1.615_858_368_580_409e2,
-1.556_989_798_598_866e2,
6.680_131_188_771_972e1,
-1.328_068_155_288_572e1,
];
const P_LOW: f64 = 0.02425;
const P_HIGH: f64 = 1.0 - P_LOW;
let mut x = if p < P_LOW {
acklam_lower_tail_quantile_from_log_probability(p.ln())
} else if p <= P_HIGH {
let q = p - 0.5;
let r = q * q;
(((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
/ (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
} else {
-acklam_lower_tail_quantile_from_log_probability((1.0 - p).ln())
};
for _ in 0..2 {
let density = normal_pdf(x);
if !(density.is_finite() && density > 0.0) {
break;
}
let residual = if (0.25..=0.75).contains(&p) {
0.5 * erf(x / std::f64::consts::SQRT_2) - (p - 0.5)
} else if x > 0.0 {
(1.0 - p) - 0.5 * erfc(x / std::f64::consts::SQRT_2)
} else {
normal_cdf(x) - p
};
let correction = residual / density;
let denominator = 1.0 + 0.5 * x * correction;
if !(correction.is_finite() && denominator.is_finite() && denominator != 0.0) {
break;
}
let step = correction / denominator;
if !step.is_finite() {
break;
}
x -= step;
if step.abs() <= 2.0 * f64::EPSILON * x.abs().max(1.0) {
break;
}
}
Ok(x)
}
#[inline]
pub fn standard_normal_quantile_from_log_cdf(log_p: f64) -> Result<f64, String> {
if !(log_p.is_finite() && log_p < 0.0) {
return Err(format!(
"normal log-quantile requires finite log_p < 0, got {log_p}"
));
}
if log_p > -std::f64::consts::LN_2 {
let log_q = (-log_p.exp_m1()).ln();
return standard_normal_quantile_from_log_cdf(log_q).map(|x| -x);
}
let p = log_p.exp();
let mut x = if p > 0.0 {
standard_normal_quantile(p)?
} else {
acklam_lower_tail_quantile_from_log_probability(log_p)
};
for _ in 0..4 {
let (current_log_p, mills_ratio) = signed_probit_logcdf_and_mills_ratio(x);
if !(current_log_p.is_finite() && mills_ratio.is_finite() && mills_ratio > 0.0) {
break;
}
let step = (current_log_p - log_p) / mills_ratio;
if !step.is_finite() {
break;
}
x -= step;
if step.abs() <= 2.0 * f64::EPSILON * x.abs().max(1.0) {
break;
}
}
Ok(x)
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-12;
fn rel_err(got: f64, expected: f64) -> f64 {
(got - expected).abs() / expected.abs().max(1e-300)
}
#[test]
fn student_t_primitives_keep_the_tail_that_one_minus_the_cdf_destroys() {
const ROWS: [(f64, f64, f64); 10] = [
(5.0, 20.0, 2.887758186612086e-6),
(5.0, 40.0, 9.205981085886477e-8),
(30.0, 10.0, 2.2876257041148065e-11),
(30.0, 20.0, 3.3745418328856434e-19),
(30.0, 40.0, 6.863022597203202e-28),
(500.0, 8.0, 4.3648313969400955e-15),
(500.0, 10.0, 6.930246799119958e-22),
(500.0, 20.0, 4.056001518093838e-66),
(500.0, 40.0, 3.14532145912912e-158),
(10000.0, 10.0, 9.816403714331914e-24),
];
let bar = 1.0e-11;
for (nu, t, want) in ROWS {
let got = student_t_sf(t, nu);
let rel = ((got - want) / want).abs();
assert!(
rel <= bar,
"student_t_sf({t}, {nu}) = {got:e}, want {want:e}, relative {rel:e} > {bar:e}"
);
let got_two_sided = student_t_two_sided_probability(t, nu);
let two_sided_rel = ((got_two_sided - 2.0 * want) / (2.0 * want)).abs();
assert!(
two_sided_rel <= bar,
"student_t_two_sided_probability({t}, {nu}) = {got_two_sided:e}, \
want {:e}, relative {two_sided_rel:e} > {bar:e}",
2.0 * want
);
let lower = student_t_sf(-t, nu);
assert!(
(lower - (1.0 - want)).abs() <= 2.0 * f64::EPSILON,
"student_t_sf({}, {nu}) = {lower}, want {}",
-t,
1.0 - want
);
}
for nu in [1.0_f64, 5.0, 1e4] {
assert!(
(student_t_sf(0.0, nu) - 0.5).abs() <= f64::EPSILON,
"median at nu = {nu}"
);
}
assert!(student_t_sf(1.0, 0.0).is_nan(), "nu = 0 is not a t");
assert!(
student_t_sf(1.0, f64::INFINITY).is_nan(),
"nu = inf is not a t"
);
assert_eq!(student_t_sf(f64::INFINITY, 5.0), 0.0, "tail beyond +inf");
assert_eq!(
student_t_sf(f64::NEG_INFINITY, 5.0),
1.0,
"tail beyond -inf"
);
}
#[test]
fn normal_sf_keeps_the_upper_tail_that_one_minus_the_cdf_destroys() {
const ROWS: [(f64, f64); 13] = [
(0.5, 0.3085375387259869),
(2.0, 0.02275013194817921),
(4.0, 3.1671241833119924e-5),
(5.0, 2.866515718791933e-7),
(6.0, 9.86587645037698e-10),
(7.0, 1.279812543885835e-12),
(8.0, 6.220960574271784e-16),
(8.3, 5.205569744890254e-17),
(9.0, 1.1285884059538405e-19),
(12.0, 1.776482112077679e-33),
(20.0, 2.7536241186062337e-89),
(30.0, 4.906713927148187e-198),
(37.0, 5.725571222524577e-300),
];
for (x, want) in ROWS {
let bar = (x * x + 2.0) * f64::EPSILON;
let got = normal_sf(x);
let rel = ((got - want) / want).abs();
assert!(
rel <= bar,
"normal_sf({x}) = {got:e}, want {want:e}, relative {rel:e} > {bar:e}"
);
let got_two_sided = normal_two_sided_probability(x);
let two_sided_rel = ((got_two_sided - 2.0 * want) / (2.0 * want)).abs();
assert!(
two_sided_rel <= bar,
"normal_two_sided_probability({x}) = {got_two_sided:e}, \
want {:e}, relative {two_sided_rel:e} > {bar:e}",
2.0 * want
);
if x >= 8.3 {
assert_eq!(
1.0 - normal_cdf(x),
0.0,
"1 - normal_cdf({x}) is expected to have saturated"
);
}
}
for x in [-3.0_f64, -0.25, 0.0, 0.25, 3.0] {
let sum = normal_sf(x) + normal_cdf(x);
assert!((sum - 1.0).abs() <= 2.0 * f64::EPSILON, "sf + cdf = {sum}");
assert_eq!(normal_sf(-x), normal_cdf(x), "sf(-x) != cdf(x) at {x}");
}
}
#[test]
fn normal_two_sided_tail_retains_subnormal_edge() {
const EXPECTED_AT_38: f64 = 5.770_856_702_007_929e-316;
let got = normal_two_sided_probability(38.0);
let ulps = got.to_bits().abs_diff(EXPECTED_AT_38.to_bits());
assert!(
got.is_subnormal() && ulps <= 128,
"two-sided normal tail at z=38: got {got:.17e}, \
expected {EXPECTED_AT_38:.17e}, ulps {ulps}"
);
assert_eq!(normal_two_sided_probability(40.0), 0.0);
assert_eq!(normal_two_sided_probability(f64::INFINITY), 0.0);
assert!(normal_two_sided_probability(f64::NAN).is_nan());
}
#[test]
fn student_t_two_sided_tail_retains_subnormal_cauchy_edge() {
const EXPECTED: f64 = 3.541_315_033_259_774_5e-309;
let got = student_t_two_sided_probability(f64::MAX, 1.0);
let analytic = 2.0 * (1.0 / f64::MAX).atan() / std::f64::consts::PI;
let pinned_ulps = got.to_bits().abs_diff(EXPECTED.to_bits());
let analytic_ulps = got.to_bits().abs_diff(analytic.to_bits());
assert!(
got.is_subnormal() && pinned_ulps <= 512 && analytic_ulps <= 512,
"Cauchy tail at f64::MAX: got {got:.17e}, pinned {EXPECTED:.17e}, \
analytic {analytic:.17e}, pinned ulps {pinned_ulps}, \
analytic ulps {analytic_ulps}"
);
}
#[test]
fn distribution_survival_primitives_define_boundaries_and_identities() {
assert_eq!(normal_sf(f64::INFINITY), 0.0);
assert_eq!(normal_sf(f64::NEG_INFINITY), 1.0);
assert!(normal_sf(f64::NAN).is_nan());
assert_eq!(student_t_two_sided_probability(0.0, 7.0), 1.0);
assert_eq!(student_t_sf(0.0, 7.0), 0.5);
assert!(student_t_sf(f64::NAN, 7.0).is_nan());
assert_eq!(chi_square_sf(0.0, 3.0), 1.0);
assert_eq!(chi_square_sf(f64::INFINITY, 3.0), 0.0);
assert!(chi_square_sf(-1.0, 3.0).is_nan());
assert!(chi_square_sf(1.0, 0.0).is_nan());
assert_eq!(fisher_snedecor_sf(0.0, 3.0, 20.0), 1.0);
assert_eq!(
fisher_snedecor_sf(f64::INFINITY, 3.0, 20.0),
0.0
);
assert!(fisher_snedecor_sf(-1.0, 3.0, 20.0).is_nan());
assert!(fisher_snedecor_sf(1.0, 0.0, 20.0).is_nan());
assert!(fisher_snedecor_sf(1.0, 3.0, 0.0).is_nan());
let statistic = 160.0_f64;
let chi_expected = normal_two_sided_probability(statistic.sqrt());
let chi_got = chi_square_sf(statistic, 1.0);
assert!(rel_err(chi_got, chi_expected) <= 2.0e-13);
let f_expected = student_t_two_sided_probability(statistic.sqrt(), 1.0);
let f_got = fisher_snedecor_sf(statistic, 1.0, 1.0);
assert!(rel_err(f_got, f_expected) <= 2.0e-13);
}
#[test]
fn beta_quantile_resolves_the_lower_tail_below_the_solver_floor() {
const CASES: [(f64, f64, f64, f64); 8] = [
(0.04, 3.96, 0.025, 1.4749755854885786e-41),
(0.01, 0.99, 0.025, 6.326229749489128e-161),
(
0.046666666666666666,
2.2866666666666666,
0.025,
1.488779171021457e-35,
),
(0.05, 0.95, 0.025, 9.875267916846768e-33),
(0.1, 0.9, 0.025, 1.12479965068234e-16),
(0.3, 0.7, 0.025, 7.6005358168401896e-6),
(0.5, 0.5, 0.025, 1.5413331334360133e-3),
(0.1, 0.1, 1.0e-4, 8.869280655550463e-38),
];
let mut worst = 0.0_f64;
for (a, b, p, want) in CASES {
let got = beta_quantile(p, a, b);
let relative = ((got - want) / want).abs();
assert!(
relative <= 16.0 * f64::EPSILON,
"beta_quantile({p}, {a}, {b}) = {got:e}, want {want:e}, relative {relative:e}"
);
worst = worst.max(relative);
}
println!("worst relative error over the lower-tail table: {worst:e}");
let underflowed = beta_quantile(0.025, 0.0023333333333333335, 2.3310000000000004);
assert!(
underflowed == 0.0,
"a quantile below MIN_POSITIVE must round to zero, got {underflowed:e}"
);
const UPPER: f64 = 0.12274676682071068;
let upper = beta_quantile(0.975, 0.04, 3.96);
assert!(
((upper - UPPER) / UPPER).abs() <= 1.0e-11,
"upper tail moved: {upper:e}, want {UPPER:e}"
);
}
#[test]
fn beta_quantile_matches_known_reference_values() {
let cases: [(f64, f64, f64, f64); 8] = [
(0.025, 2.0, 2.0, 0.094_299_3),
(0.975, 2.0, 2.0, 0.905_700_7),
(0.5, 2.0, 2.0, 0.5),
(0.025, 0.8, 4.0, 0.002_339_1),
(0.975, 0.8, 4.0, 0.564_717_3),
(0.025, 5.0, 1.5, 0.408_549_1),
(0.5, 20.0, 80.0, 0.197_994_8),
(0.975, 20.0, 80.0, 0.283_367_6),
];
for (p, a, b, expected) in cases {
let got = beta_quantile(p, a, b);
let abs = (got - expected).abs();
assert!(
abs < 1e-5,
"beta_quantile(p={p}, a={a}, b={b}) = {got}, expected ≈ {expected} (abs err {abs})"
);
}
}
#[test]
fn beta_quantile_boundaries_and_degeneracy() {
assert_eq!(beta_quantile(0.0, 2.0, 3.0), 0.0);
assert_eq!(beta_quantile(-0.5, 2.0, 3.0), 0.0);
assert_eq!(beta_quantile(1.0, 2.0, 3.0), 1.0);
assert_eq!(beta_quantile(1.5, 2.0, 3.0), 1.0);
assert!(beta_quantile(0.5, -1.0, 3.0).is_nan());
assert!(beta_quantile(0.5, 2.0, 0.0).is_nan());
assert!(beta_quantile(0.5, f64::NAN, 3.0).is_nan());
let mut prev = 0.0;
for i in 1..100 {
let p = i as f64 / 100.0;
let q = beta_quantile(p, 3.0, 5.0);
assert!(q > prev, "beta quantile not increasing at p={p}");
prev = q;
}
}
#[test]
fn normal_pdf_at_zero() {
let expected = 1.0 / (2.0 * std::f64::consts::PI).sqrt();
assert!((normal_pdf(0.0) - expected).abs() < TOL);
}
#[test]
fn normal_pdf_symmetry() {
for &x in &[0.5, 1.0, 2.0, 3.0, 5.0] {
assert_eq!(normal_pdf(x), normal_pdf(-x), "symmetry failed at x={x}");
}
}
#[test]
fn square_residual_completes_the_rounded_square_exactly() {
const SPLIT: f64 = 134_217_729.0;
let mut saw_amplified = false;
for &x in &[
0.1, 0.7, 1.3, 2.9, 6.1, 10.5, 14.3, 19.7, 23.9, 25.9999, 34.7,
] {
let rounded = x * x;
let residual = square_residual(x, rounded);
let c = x * SPLIT;
let head = c - (c - x);
let tail = x - head;
let dekker = ((head * head - rounded) + 2.0 * head * tail) + tail * tail;
assert_eq!(
residual, dekker,
"x={x}: mul_add residual {residual:e} != Dekker residual {dekker:e}"
);
let amplified = (residual / rounded).abs() * rounded;
if amplified > f64::EPSILON {
saw_amplified = true;
}
}
assert!(
saw_amplified,
"no test argument had a residual `exp` could amplify past one ulp; \
the correction under test would be untested"
);
}
#[test]
fn normal_pdf_matches_high_precision_reference() {
const TOLERANCE: f64 = 1.5e-15;
let refs: &[(f64, f64)] = &[
(0.5, 0.35206532676429947),
(1.0, 0.24197072451914334),
(2.5, 0.017528300493568537),
(4.0, 0.00013383022576488534),
(7.3, 1.0693837871541648e-12),
(11.9, 7.090702668428078e-32),
(17.4, 7.201308152719057e-67),
(23.6, 4.555989824112156e-122),
(29.1, 5.229437243665329e-185),
(34.7, 1.368008224488383e-262),
];
for &(x, reference) in refs {
assert!(
x <= 5.0 || square_residual(x, x * x) != 0.0,
"x={x} squares exactly, so it cannot exercise the correction"
);
let rel = rel_err(normal_pdf(x), reference);
assert!(
rel < TOLERANCE,
"normal_pdf({x}) = {:.17e}, reference {reference:.17e}, rel {rel:.3e}",
normal_pdf(x)
);
}
}
#[test]
fn normal_pdf_nonfinite_and_underflowed_arguments() {
assert_eq!(normal_pdf(f64::INFINITY), 0.0);
assert_eq!(normal_pdf(f64::NEG_INFINITY), 0.0);
assert!(normal_pdf(f64::NAN).is_nan());
assert_eq!(normal_pdf(40.0), 0.0);
assert_eq!(normal_pdf(-40.0), 0.0);
assert_eq!(normal_pdf(f64::MAX), 0.0);
let edge = normal_pdf(38.0);
assert!(edge > 0.0 && edge.is_subnormal(), "phi(38) = {edge:e}");
}
#[test]
fn normal_pdf_positive() {
for &x in &[-5.0, -1.0, 0.0, 1.0, 5.0] {
assert!(normal_pdf(x) > 0.0, "pdf should be positive at x={x}");
}
}
#[test]
fn normal_cdf_at_zero_is_half() {
assert!((normal_cdf(0.0) - 0.5).abs() < TOL);
}
#[test]
fn normal_cdf_symmetry() {
for &x in &[0.5, 1.0, 2.0, 3.0] {
let sum = normal_cdf(x) + normal_cdf(-x);
assert!(
(sum - 1.0).abs() < TOL,
"cdf symmetry failed at x={x}: sum={sum}"
);
}
}
#[test]
fn normal_cdf_bounds() {
assert!(normal_cdf(10.0) > 0.9999);
assert!(normal_cdf(-10.0) < 1e-22);
assert!(normal_cdf(0.0) > 0.0);
assert!(normal_cdf(0.0) < 1.0);
}
#[test]
fn normal_cdf_at_1_96_near_0975() {
let p = normal_cdf(1.959_963_985);
assert!((p - 0.975).abs() < 1e-8, "p={p}");
}
#[test]
fn erfcx_zero_is_one_and_negative_domain_is_rejected() {
assert_eq!(erfcx_nonnegative(0.0), 1.0);
assert!(erfcx_nonnegative(-f64::MIN_POSITIVE).is_nan());
assert!(erfcx_nonnegative(-1.0).is_nan());
assert!(erfcx_nonnegative(f64::NEG_INFINITY).is_nan());
}
#[test]
fn erfcx_positive_inf_returns_zero() {
assert_eq!(erfcx_nonnegative(f64::INFINITY), 0.0);
}
#[test]
fn erfcx_nan_propagates() {
assert!(erfcx_nonnegative(f64::NAN).is_nan());
}
#[test]
fn erfcx_small_positive_matches_direct() {
use libm::erfc;
for &x in &[0.1_f64, 0.5, 1.0, 5.0, 10.0, 25.0] {
let got = erfcx_nonnegative(x);
let expected = (x * x).exp() * erfc(x);
let err = rel_err(got, expected);
assert!(
err < 1e-10,
"x={x}: got={got} expected={expected} rel={err}"
);
}
}
#[test]
fn erfcx_large_x_positive_and_finite() {
let got = erfcx_nonnegative(50.0);
assert!(got.is_finite() && got > 0.0, "erfcx(50)={got}");
let asymptotic = 1.0 / (50.0 * std::f64::consts::PI.sqrt());
assert!(
rel_err(got, asymptotic) < 1e-3,
"got={got} asymptotic={asymptotic}"
);
}
#[test]
fn erfcx_asymptotic_switch_matches_finite_direct_identity() {
let switch = 26.0_f64;
assert_eq!(
square_residual(switch, switch * switch),
0.0,
"676 must be exact for the direct form below to be an oracle"
);
let direct = (switch * switch).exp() * erfc(switch);
let asymptotic = erfcx_nonnegative(switch);
assert!(
rel_err(asymptotic, direct) < 1.0e-15,
"switch mismatch: asymptotic={asymptotic:.17e}, direct={direct:.17e}"
);
let immediately_below = f64::from_bits(switch.to_bits() - 1);
let below = erfcx_nonnegative(immediately_below);
let step = 2.0 * switch * (switch - immediately_below);
assert!(
rel_err(asymptotic, below) < 2.0 * step,
"discontinuous switch: below={below:.17e}, at={asymptotic:.17e}, \
one-ulp travel {step:.3e}"
);
}
#[test]
fn erfcx_preserves_representable_subnormal_tail() {
let tail = erfcx_nonnegative(f64::MAX);
assert!(tail > 0.0 && tail.is_subnormal(), "erfcx(MAX)={tail:e}");
}
#[test]
fn erfcx_matches_high_precision_reference() {
const TOLERANCE: f64 = 1.5e-15;
let refs: &[(f64, f64)] = &[
(0.1, 0.8964569799691267),
(0.5, 0.6156903441929259),
(1.0, 0.427583576155807),
(2.0, 0.25539567631050575),
(3.5, 0.1552936556088943),
(6.0, 0.09277656780053835),
(9.0, 0.06230772403777468),
(10.5, 0.05349189974656412),
(13.0, 0.043271921864609694),
(14.3, 0.0393580473372741),
(18.0, 0.03129571781590521),
(19.7, 0.028602309402825203),
(22.0, 0.025618570005879453),
(23.9, 0.023585649371803793),
(25.5, 0.022108108052519827),
(25.9999, 0.021683668126369115),
];
for &(x, reference) in refs {
let got = erfcx_nonnegative(x);
let rel = rel_err(got, reference);
assert!(
rel < TOLERANCE,
"erfcx({x}) = {got:.17e}, reference {reference:.17e}, rel {rel:.3e}"
);
}
let inexact = refs
.iter()
.filter(|&&(x, _)| square_residual(x, x * x) != 0.0)
.count();
assert!(
inexact >= 4,
"only {inexact} of {} reference arguments have an inexact square",
refs.len()
);
}
#[test]
fn log1mexp_at_zero_is_neg_inf() {
assert_eq!(log1mexp_positive(0.0), f64::NEG_INFINITY);
}
#[test]
fn log1mexp_recovers_log_one_minus_exp() {
for &a in &[0.001_f64, 0.5, std::f64::consts::LN_2, 1.0, 5.0, 20.0] {
let lm = log1mexp_positive(a);
let roundtrip = lm.exp() + (-a).exp();
assert!(
(roundtrip - 1.0).abs() < 1e-14,
"a={a}: exp(log1mexp(a)) + exp(-a) = {roundtrip}, expected 1.0"
);
}
}
#[test]
fn log1mexp_at_ln2_is_neg_ln2() {
let ln2 = std::f64::consts::LN_2;
let got = log1mexp_positive(ln2);
assert!((got - (-ln2)).abs() < TOL, "got={got}");
}
#[test]
fn slse_all_positive_single() {
let (lm, sg) = signed_log_sum_exp(&[2.0], &[1.0]);
assert!((lm - 2.0).abs() < TOL);
assert!((sg - 1.0).abs() < TOL);
}
#[test]
fn slse_difference_recovers_log2() {
let log3 = 3.0_f64.ln();
let log1 = 0.0_f64; let (lm, sg) = signed_log_sum_exp(&[log3, log1], &[1.0, -1.0]);
assert!((lm - 2.0_f64.ln()).abs() < TOL, "lm={lm}");
assert!((sg - 1.0).abs() < TOL, "sg={sg}");
}
#[test]
fn slse_cancellation_gives_neg_inf() {
let ln2 = 2.0_f64.ln();
let (lm, sg) = signed_log_sum_exp(&[ln2, ln2], &[1.0, -1.0]);
assert_eq!(lm, f64::NEG_INFINITY);
assert_eq!(sg, 0.0);
}
#[test]
fn slse_compensated_signed_reduction_preserves_conditioned_residual() {
let log_magnitudes = [
-8.752777116220523,
-8.741767521635955,
-8.77021076826994,
-8.75153786858979,
-8.754172660745834,
-8.768217028174623,
-8.756625396724502,
-8.737312647396818,
];
let signs = [1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0];
let (log_magnitude, sign) = signed_log_sum_exp(&log_magnitudes, &signs);
let got = sign * log_magnitude.exp();
let truth = -7.141194316117315e-13;
let legacy = -7.141196119493781e-13;
assert_eq!(sign, -1.0);
assert!(
(got - truth).abs() < (legacy - truth).abs(),
"compensated signed reduction did not improve the conditioned residual: \
got={got:.17e}, truth={truth:.17e}, legacy={legacy:.17e}"
);
}
#[test]
fn slse_log_domain_branch_retains_sub_ulp_two_term_gap() {
let gap = f64::EPSILON * 0.25;
let (log_magnitude, sign) = signed_log_sum_exp(&[0.0, -gap], &[1.0, -1.0]);
assert_eq!(sign, 1.0);
assert_eq!(log_magnitude, log1mexp_positive(gap));
}
#[test]
fn exact_binary64_sum_sign_resolves_midpoint_and_both_adjacent_sides() {
let half_upper_ulp_at_one = 2.0_f64.powi(-53);
let least_subnormal = f64::from_bits(1);
assert_eq!(
exact_binary64_sum_sign([
1.0,
half_upper_ulp_at_one,
-1.0,
-half_upper_ulp_at_one,
]),
Ok(std::cmp::Ordering::Equal),
"an exact rounding midpoint must compare equal"
);
assert_eq!(
exact_binary64_sum_sign([
1.0,
half_upper_ulp_at_one,
least_subnormal,
-1.0,
-half_upper_ulp_at_one,
]),
Ok(std::cmp::Ordering::Greater),
"one binary lattice quantum above the midpoint must compare positive"
);
assert_eq!(
exact_binary64_sum_sign([
1.0,
half_upper_ulp_at_one,
-least_subnormal,
-1.0,
-half_upper_ulp_at_one,
]),
Ok(std::cmp::Ordering::Less),
"one binary lattice quantum below the midpoint must compare negative"
);
}
#[test]
fn exact_binary64_sum_sign_enforces_its_finite_structural_contract() {
assert_eq!(
exact_binary64_sum_sign([f64::MAX, -f64::MAX, f64::from_bits(1)]),
Ok(std::cmp::Ordering::Greater),
);
assert_eq!(
exact_binary64_sum_sign([0.0, f64::NAN]),
Err(ExactBinary64SumSignError::NonFiniteTerm { index: 1 }),
);
assert_eq!(
exact_binary64_sum_sign(
std::iter::repeat_n(1.0, EXACT_BINARY64_SUM_MAX_TERMS + 1)
),
Err(ExactBinary64SumSignError::TermCapacityExceeded {
maximum: EXACT_BINARY64_SUM_MAX_TERMS,
}),
);
}
#[test]
fn slse_empty_returns_neg_inf_with_zero_sign() {
let (lm, sg) = signed_log_sum_exp(&[], &[]);
assert_eq!(lm, f64::NEG_INFINITY);
assert_eq!(sg, 0.0);
}
#[test]
fn slse_all_zero_signs_return_zero_sign() {
let (lm, sg) = signed_log_sum_exp(&[0.0], &[0.0]);
assert_eq!(lm, f64::NEG_INFINITY);
assert_eq!(sg, 0.0);
}
#[test]
fn slse_all_neg_inf_magnitudes_return_zero_sign() {
let (lm, sg) = signed_log_sum_exp(&[f64::NEG_INFINITY, f64::NEG_INFINITY], &[1.0, -1.0]);
assert_eq!(lm, f64::NEG_INFINITY);
assert_eq!(sg, 0.0);
}
#[test]
fn slse_pos_inf_dominates() {
let (lm, sg) = signed_log_sum_exp(&[f64::INFINITY, 1.0], &[1.0, -1.0]);
assert_eq!(lm, f64::INFINITY);
assert_eq!(sg, 1.0);
}
#[test]
fn slse_neg_inf_dominates() {
let (lm, sg) = signed_log_sum_exp(&[f64::INFINITY, 1.0], &[-1.0, 1.0]);
assert_eq!(lm, f64::INFINITY);
assert_eq!(sg, -1.0);
}
#[test]
fn slse_both_inf_signs_gives_nan() {
let (lm, sg) = signed_log_sum_exp(&[f64::INFINITY, f64::INFINITY], &[1.0, -1.0]);
assert!(lm.is_nan());
assert_eq!(sg, 0.0);
}
#[test]
fn logcdf_at_zero_is_log_half() {
let got = normal_logcdf(0.0);
let expected = 0.5_f64.ln();
assert!((got - expected).abs() < TOL, "got={got}");
}
#[test]
fn logcdf_pos_inf_is_zero() {
assert_eq!(normal_logcdf(f64::INFINITY), 0.0);
}
#[test]
fn logcdf_neg_inf_is_neg_inf() {
assert_eq!(normal_logcdf(f64::NEG_INFINITY), f64::NEG_INFINITY);
}
#[test]
fn logcdf_nan_is_nan() {
assert!(normal_logcdf(f64::NAN).is_nan());
}
#[test]
fn logcdf_matches_log_cdf_for_moderate_x() {
for &x in &[-2.0_f64, -1.0, 0.0, 1.0, 2.0, 3.0] {
let got = normal_logcdf(x);
let expected = normal_cdf(x).ln();
assert!(
(got - expected).abs() < 1e-10,
"x={x}: got={got} expected={expected}"
);
}
}
#[test]
fn logcdf_deep_left_tail_stays_finite() {
let got = normal_logcdf(-20.0);
assert!(got.is_finite() && got < -100.0, "logcdf(-20)={got}");
}
#[test]
fn logcdf_positive_tail_does_not_round_through_unit_cdf() {
let x = 10.0_f64;
let got = normal_logcdf(x);
let expected = (-0.5 * erfc(x / std::f64::consts::SQRT_2)).ln_1p();
assert!(
got < 0.0,
"logcdf(10) must retain its negative tail: {got:e}"
);
assert_eq!(got.to_bits(), expected.to_bits());
}
#[test]
fn log_cdf_quantile_round_trips_both_unrepresentable_tails() {
for x in [-1.0e6, -40.0, -10.0, -2.0, 0.0, 2.0, 10.0] {
let log_p = normal_logcdf(x);
let recovered = standard_normal_quantile_from_log_cdf(log_p)
.expect("finite strict log-CDF has a quantile");
assert!(
(recovered - x).abs() <= 2.0e-12 * x.abs().max(1.0),
"log-quantile round trip at x={x}: log_p={log_p}, recovered={recovered}"
);
}
}
#[test]
fn logsf_at_zero_is_log_half() {
let got = normal_logsf(0.0);
let expected = 0.5_f64.ln();
assert!((got - expected).abs() < TOL, "got={got}");
}
#[test]
fn logsf_mirrors_logcdf() {
for &x in &[-3.0_f64, -1.0, 0.0, 1.0, 3.0] {
assert_eq!(normal_logsf(x), normal_logcdf(-x));
}
}
#[test]
fn probit_at_pos_inf() {
let (lc, mr) = signed_probit_logcdf_and_mills_ratio(f64::INFINITY);
assert_eq!(lc, 0.0);
assert_eq!(mr, 0.0);
}
#[test]
fn probit_at_neg_inf() {
let (lc, mr) = signed_probit_logcdf_and_mills_ratio(f64::NEG_INFINITY);
assert_eq!(lc, f64::NEG_INFINITY);
assert_eq!(mr, f64::INFINITY);
}
#[test]
fn probit_nan_propagates() {
let (lc, mr) = signed_probit_logcdf_and_mills_ratio(f64::NAN);
assert!(lc.is_nan() && mr.is_nan());
}
#[test]
fn probit_at_zero_logcdf_and_mills() {
let (lc, mr) = signed_probit_logcdf_and_mills_ratio(0.0);
assert!((lc - 0.5_f64.ln()).abs() < TOL, "lc={lc}");
assert!((mr - 0.797_884_560_802_865).abs() < 1e-10, "mr={mr}");
}
#[test]
fn probit_positive_branch_matches_logcdf() {
for &x in &[0.5_f64, 1.0, 2.0, 3.0] {
let (lc, mr) = signed_probit_logcdf_and_mills_ratio(x);
let lc_ref = normal_logcdf(x);
let mr_ref = normal_pdf(x) / normal_cdf(x);
assert!(
(lc - lc_ref).abs() < 1e-10,
"x={x}: lc={lc} lc_ref={lc_ref}"
);
assert!(
(mr - mr_ref).abs() < 1e-10,
"x={x}: mr={mr} mr_ref={mr_ref}"
);
}
}
#[test]
fn probit_negative_branch_matches_logcdf() {
for &x in &[-0.5_f64, -1.0, -2.0, -5.0] {
let (lc, mr) = signed_probit_logcdf_and_mills_ratio(x);
let lc_ref = normal_logcdf(x);
assert!(
(lc - lc_ref).abs() < 1e-10,
"x={x}: lc={lc} lc_ref={lc_ref}"
);
assert!(mr.is_finite() && mr > 0.0, "x={x}: mr={mr}");
}
}
#[test]
fn probit_mills_ratio_has_no_deep_tail_floor() {
let x = -1.0e305_f64;
let (log_cdf, mills_ratio) = signed_probit_logcdf_and_mills_ratio(x);
assert_eq!(log_cdf, f64::NEG_INFINITY);
assert!(mills_ratio.is_finite());
assert!(
((mills_ratio / -x) - 1.0).abs() < 5.0e-15,
"mills({x:e})={mills_ratio:e}"
);
}
#[test]
fn normal_logcdf_derivative_stack_has_honest_infinite_limits() {
assert_eq!(normal_logcdf_derivatives(f64::INFINITY), [0.0; 5]);
assert_eq!(
normal_logcdf_derivatives(f64::NEG_INFINITY),
[f64::NEG_INFINITY, f64::INFINITY, -1.0, 0.0, 0.0]
);
assert!(
normal_logcdf_derivatives(f64::NAN)
.into_iter()
.all(f64::is_nan)
);
for x in [-1.0e200_f64, 1.0e200_f64] {
let derivatives = normal_logcdf_derivatives(x);
assert!(
derivatives.into_iter().all(|value| !value.is_nan()),
"NaN derivative at x={x:e}: {derivatives:?}"
);
}
}
#[test]
fn normal_logcdf_left_tail_derivatives_do_not_cancel() {
let x = -1.0e100_f64;
let derivatives = normal_logcdf_derivatives(x);
assert_eq!(derivatives[2], -1.0);
assert!(derivatives[3] > 0.0 && derivatives[3].is_finite());
assert!(
(derivatives[3] / 2.0e-300 - 1.0).abs() < 2.0e-14,
"third derivative={:e}",
derivatives[3]
);
assert_eq!(derivatives[4], 0.0);
}
#[test]
fn normal_logcdf_right_tail_preserves_weighted_subnormal_derivatives() {
let derivatives = normal_logcdf_derivatives(38.6);
assert_eq!(derivatives[1], 0.0);
assert!(derivatives[2] < 0.0 && derivatives[2].is_subnormal());
assert!(derivatives[3] > 0.0 && derivatives[3].is_subnormal());
assert!(derivatives[4] < 0.0 && derivatives[4].is_subnormal());
}
#[test]
fn normal_logcdf_tail_stack_is_finite_difference_consistent() {
let h = 1.0e-4_f64;
for x in [-8.0_f64, -4.0, 8.0, 20.0] {
let center = normal_logcdf_derivatives(x);
let left = normal_logcdf_derivatives(x - h);
let right = normal_logcdf_derivatives(x + h);
for order in 1..=3 {
let finite_difference = (right[order] - left[order]) / (2.0 * h);
let expected = center[order + 1];
let relative = (finite_difference - expected).abs() / expected.abs().max(1.0e-300);
assert!(
relative < 2.0e-5,
"x={x}, order={order}: fd={finite_difference:e}, expected={expected:e}, rel={relative:e}"
);
}
}
}
#[test]
fn normal_logcdf_derivative_tower_matches_high_precision_reference() {
let refs: &[(f64, [f64; 5])] = &[
(
-4.0,
[
-10.360101486527291,
4.2256071444894711,
-0.95332716160257737,
0.017856339307658426,
0.0095065764315958691,
],
),
(
-10.0,
[
-53.231285150512471,
10.098093233962512,
-0.99055462217434374,
0.0017864003921165069,
0.00049785382237944016,
],
),
(
-6.0,
[
-20.736768949974706,
6.1584826045445989,
-0.97601236321083323,
0.0069535374991643118,
0.0028992056785575027,
],
),
(
-2.0,
[
-3.7831843336820319,
2.3732155328228409,
-0.88572089958591874,
0.059355861291565813,
0.039421993865946813,
],
),
(
-1.0,
[
-1.8410216450092635,
1.5251352761609812,
-0.80090233442965121,
0.11693119540604883,
0.07917498368074563,
],
),
(
-0.3,
[
-0.96210281816885066,
0.99816596885848332,
-0.69688551072964971,
0.18398317992442132,
0.11037564722092704,
],
),
(
0.5,
[
-0.36894641528865639,
0.50916043383703349,
-0.5138245643036329,
0.27099012446870783,
0.088167801929197554,
],
),
(
2.0,
[
-0.023012909328963488,
0.055247862678989959,
-0.11354805168857645,
0.18439481503247759,
-0.18785468561160969,
],
),
];
for &(x, reference) in refs {
let got = normal_logcdf_derivatives(x);
for (order, (&g, &r)) in got.iter().zip(reference.iter()).enumerate() {
let rel = (g - r).abs() / r.abs().max(1.0e-3);
assert!(
rel < 1.0e-11,
"normal_logcdf_derivatives({x})[{order}] = {g:.17e}, reference {r:.17e}, \
rel {rel:.3e} >= 1e-11"
);
}
}
}
#[test]
fn quantile_rejects_out_of_range() {
assert!(standard_normal_quantile(0.0).is_err());
assert!(standard_normal_quantile(1.0).is_err());
assert!(standard_normal_quantile(-0.1).is_err());
assert!(standard_normal_quantile(1.1).is_err());
assert!(standard_normal_quantile(f64::NAN).is_err());
}
#[test]
fn quantile_at_half_is_near_zero() {
let q = standard_normal_quantile(0.5).unwrap();
assert!(q.abs() < 1e-10, "quantile(0.5)={q}");
}
#[test]
fn quantile_at_0975_is_near_196() {
let q = standard_normal_quantile(0.975).unwrap();
assert!((q - 1.959_963_984_540_054).abs() < 1e-14, "q={q}");
}
#[test]
fn normal_quantile_is_ulp_accurate_through_the_median() {
const CENTRAL_REFERENCE: [[f64; 2]; 19] = [
[0.5000000000000284, 7.124266047159724e-14],
[0.4999999999999716, -7.124266047159724e-14],
[0.5000000009313226, 2.3344794983332983e-09],
[0.4999999990686774, -2.3344794983332983e-09],
[0.5000009536743164, 2.390507006295574e-06],
[0.500000001, 2.5066282037387115e-09],
[0.4999999999, -2.506628482030354e-10],
[0.5001, 0.00025066283008800747],
[0.4999, -0.00025066283008800747],
[0.51, 0.025068908258711057],
[0.49, -0.025068908258711057],
[0.55, 0.12566134685507416],
[0.45, -0.12566134685507402],
[0.6, 0.2533471031357997],
[0.4, -0.2533471031357997],
[0.7, 0.5244005127080407],
[0.3, -0.5244005127080408],
[0.75, 0.6744897501960817],
[0.25, -0.6744897501960817],
];
let bar = 4.0 * f64::EPSILON;
let mut worst = 0.0_f64;
let mut worst_at = f64::NAN;
for [p, expected] in CENTRAL_REFERENCE {
let got = standard_normal_quantile(p).expect("central p is in (0,1)");
let relative = ((got - expected) / expected).abs();
if relative > worst {
worst = relative;
worst_at = p;
}
assert!(
relative <= bar,
"Phi^-1({p}) = {got}, expected {expected}, relative {relative:e} > {bar:e}"
);
}
println!("central quantile worst relative {worst:e} at p = {worst_at}");
}
#[test]
fn normal_quantiles_match_independent_high_precision_reference() {
const QUANTILE_REFERENCE: [[f64; 2]; 22] = [
[1e-300, -37.0470962993612],
[1e-100, -21.273453560965326],
[1e-20, -9.262340089798407],
[1e-08, -5.612001244174789],
[0.001, -3.0902323061678136],
[0.02424, -1.9731366119445441],
[0.02425, -1.972961051311885],
[0.02426, -1.9727855514678605],
[0.05, -1.6448536269514726],
[0.1, -1.2815515655446004],
[0.25, -0.6744897501960817],
[0.4, -0.2533471031357997],
[0.5, 0.0],
[0.6, 0.2533471031357997],
[0.75, 0.6744897501960817],
[0.9, 1.2815515655446006],
[0.95, 1.6448536269514722],
[0.975, 1.9599639845400538],
[0.99, 2.3263478740408408],
[0.999, 3.090232306167813],
[0.99999999, 5.612001243305505],
[0.9999999999999999, 8.209536151601387],
];
for [p, want] in QUANTILE_REFERENCE {
let got = standard_normal_quantile(p).expect("p in (0,1) has a quantile");
let error = (got - want).abs();
let budget = if want == 0.0 {
1e-16
} else {
4e-15 * want.abs()
};
assert!(
error <= budget,
"Φ⁻¹({p}): got {got:.17e}, want {want:.17e} (error {error:.3e} > {budget:.3e})"
);
}
const LOG_CDF_QUANTILE_REFERENCE: [[f64; 2]; 9] = [
[-0.7, -0.008559478582480282],
[-2.0, -1.1015196284987503],
[-10.0, -3.913946240531893],
[-50.0, -9.674825283612357],
[-200.0, -19.803669380301212],
[-1000.0, -44.6157477319694],
[-10000.0, -141.37983987312717],
[-100000.0, -447.1978936785251],
[-1000000.0, -1414.2077829910174],
];
for [log_p, want] in LOG_CDF_QUANTILE_REFERENCE {
let got =
standard_normal_quantile_from_log_cdf(log_p).expect("finite log_p < 0 has a root");
let error = (got - want).abs();
let conditioning = 8.0 * f64::EPSILON * log_p.abs() / want.abs().max(0.8);
let budget = 4e-15 * want.abs() + conditioning;
assert!(
error <= budget,
"Φ⁻¹(exp({log_p})): got {got:.17e}, want {want:.17e} \
(error {error:.3e} > {budget:.3e})"
);
}
}
#[test]
fn quantile_antisymmetry() {
let q_lo = standard_normal_quantile(0.1).unwrap();
let q_hi = standard_normal_quantile(0.9).unwrap();
assert!((q_lo + q_hi).abs() < 1e-10, "q_lo={q_lo} q_hi={q_hi}");
}
#[test]
fn quantile_roundtrip_cdf() {
for &p in &[
0.001, 0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 0.999,
] {
let q = standard_normal_quantile(p).unwrap();
let p_back = normal_cdf(q);
assert!(
(p_back - p).abs() <= 1e-14 * p,
"roundtrip failed at p={p}: q={q} p_back={p_back}"
);
}
}
}
#[inline]
pub fn normal_sf(x: f64) -> f64 {
0.5 * erfc(x / std::f64::consts::SQRT_2)
}
pub fn student_t_sf(t: f64, degrees_of_freedom: f64) -> f64 {
let two_sided = student_t_two_sided_probability(t, degrees_of_freedom);
if t < 0.0 {
1.0 - 0.5 * two_sided
} else {
0.5 * two_sided
}
}
#[cfg(test)]
mod signed_weighted_chi_square_tests {
use super::*;
fn term(weight: f64, degrees_of_freedom: f64) -> WeightedChiSquareTerm {
WeightedChiSquareTerm {
weight,
degrees_of_freedom,
}
}
#[test]
fn the_f_tail_is_the_two_term_signed_combination_at_zero() {
let mut worst = 0.0_f64;
for &(a, b) in &[
(1.0_f64, 5.0_f64),
(2.0, 17.0),
(3.0, 26.0),
(0.7, 24.0),
(5.4, 191.0),
(11.0, 4.0),
] {
for &f in &[0.05_f64, 0.5, 1.0, 2.5, 9.0, 40.0] {
let terms = [term(1.0, a), term(-f * a / b, b)];
let (got, bound) = signed_weighted_chi_square_sf_to_tolerance(
&terms,
0.0,
WEIGHTED_CHI_SQUARE_TOLERANCE,
);
let want = fisher_snedecor_sf(f, a, b);
let error = (got - want).abs();
worst = worst.max(error);
assert!(
error <= 1e-9 + bound,
"F({a},{b}) at {f}: imhof {got} vs beta {want} \
(error {error:.3e}, certified bound {bound:.3e})"
);
}
}
println!("worst |imhof − F| over the grid: {worst:.3e}");
}
#[test]
fn the_amplitude_bound_certifies_the_zero_statistic_answer() {
let cases: [&[WeightedChiSquareTerm]; 3] = [
&[term(1.0, 1.0), term(-0.05, 26.0)],
&[term(0.9, 1.0), term(0.2, 3.0), term(-0.01, 191.0)],
&[term(1.0, 5.4), term(-2.5, 1.0), term(-0.004, 44.0)],
];
for terms in cases {
let (reference, reference_bound) =
signed_weighted_chi_square_sf_to_tolerance(terms, 0.0, 1e-14);
for tolerance in [1e-4_f64, 1e-7, 1e-10] {
let (got, bound) =
signed_weighted_chi_square_sf_to_tolerance(terms, 0.0, tolerance);
assert!(
bound <= tolerance,
"asked {tolerance:.0e}, certified {bound:.3e} on {terms:?}"
);
assert!(
(got - reference).abs() <= bound + reference_bound,
"{got} vs {reference} exceeds the certified {bound:.3e} + \
{reference_bound:.3e} on {terms:?}"
);
}
}
}
#[test]
fn the_quadrature_resolves_the_amplitude_not_only_the_phase() {
let cases: [&[WeightedChiSquareTerm]; 5] = [
&[term(1.0, 1.0), term(-0.01, 5.0)],
&[term(1.0, 1.0), term(-0.2, 2.0)],
&[term(1.0, 3.0), term(-1.0, 3.0)],
&[term(0.9, 1.0), term(0.2, 4.0), term(-0.05, 26.0)],
&[term(1.0, 0.7), term(-0.006, 24.0)],
];
let mut worst = 0.0_f64;
for terms in cases {
for &statistic in &[0.0_f64, 0.3, -0.2] {
let (reference, reference_bound) =
signed_weighted_chi_square_sf_to_tolerance(terms, statistic, 1e-15);
let (got, bound) = signed_weighted_chi_square_sf_to_tolerance(
terms,
statistic,
WEIGHTED_CHI_SQUARE_TOLERANCE,
);
let error = (got - reference).abs();
worst = worst.max(error);
assert!(
error <= bound + reference_bound,
"{terms:?} at {statistic}: {got} vs {reference} differs by {error:.3e}, \
above the certified {bound:.3e} + {reference_bound:.3e}"
);
}
}
println!("worst discretization error against the fine-panel reference: {worst:.3e}");
}
}