diff --git a/examples/lnerfc_sweep.rs b/examples/lnerfc_sweep.rs new file mode 100644 index 00000000..256e23bb --- /dev/null +++ b/examples/lnerfc_sweep.rs @@ -0,0 +1,10 @@ +use statrs::function::erf::ln_erfc; +fn main() { + for i in 0..=800 { + let x = -6.0 + (206.0) * (i as f64) / 800.0; // covers <0.5, rational range, boundary at 110, asymptotic + println!("{:016x}\t{:016x}", x.to_bits(), ln_erfc(x).to_bits()); + } + for x in [1e3f64, 1e5, 1e10, 1e77, 1.3e154, 1.4e154, 1e200] { + println!("{:016x}\t{:016x}", x.to_bits(), ln_erfc(x).to_bits()); + } +} diff --git a/src/distribution/beta.rs b/src/distribution/beta.rs index 8a26d621..6e36fbba 100644 --- a/src/distribution/beta.rs +++ b/src/distribution/beta.rs @@ -172,6 +172,32 @@ impl ContinuousCDF for Beta { } } + /// Tail-accurate log of the cdf via [`beta::ln_beta_reg`]. + fn ln_cdf(&self, x: f64) -> f64 { + if x < 0.0 { + f64::NEG_INFINITY + } else if x >= 1.0 { + 0.0 + } else if prec::ulps_eq!(self.shape_a, 1.0) && prec::ulps_eq!(self.shape_b, 1.0) { + x.ln() + } else { + beta::ln_beta_reg(self.shape_a, self.shape_b, x) + } + } + + /// Tail-accurate log of the survival function via [`beta::ln_beta_reg`]. + fn ln_sf(&self, x: f64) -> f64 { + if x < 0.0 { + 0.0 + } else if x >= 1.0 { + f64::NEG_INFINITY + } else if prec::ulps_eq!(self.shape_a, 1.0) && prec::ulps_eq!(self.shape_b, 1.0) { + (-x).ln_1p() + } else { + beta::ln_beta_reg(self.shape_b, self.shape_a, 1.0 - x) + } + } + /// Calculates the inverse cumulative distribution function for the beta /// distribution at `x`. /// diff --git a/src/distribution/binomial.rs b/src/distribution/binomial.rs index c9794f0a..6f64eadc 100644 --- a/src/distribution/binomial.rs +++ b/src/distribution/binomial.rs @@ -1,5 +1,5 @@ use crate::distribution::{Discrete, DiscreteCDF}; -use crate::function::{beta, factorial}; +use crate::function::{beta, factorial, gamma}; use crate::prec; use crate::statistics::*; use core::f64; @@ -169,6 +169,27 @@ impl DiscreteCDF for Binomial { beta::beta_reg(k as f64 + 1.0, (self.n - k) as f64, self.p) } } + + /// Tail-accurate log of the cdf via [`beta::ln_beta_reg`]; finite far past + /// where `cdf` underflows (Bonferroni-scale p-values stay representable). + fn ln_cdf(&self, x: u64) -> f64 { + if x >= self.n { + 0.0 + } else { + let k = x; + beta::ln_beta_reg((self.n - k) as f64, k as f64 + 1.0, 1.0 - self.p) + } + } + + /// Tail-accurate log of the survival function via [`beta::ln_beta_reg`]. + fn ln_sf(&self, x: u64) -> f64 { + if x >= self.n { + f64::NEG_INFINITY + } else { + let k = x; + beta::ln_beta_reg(k as f64 + 1.0, (self.n - k) as f64, self.p) + } + } } impl Min for Binomial { @@ -237,7 +258,11 @@ impl Distribution for Binomial { } else { (0..self.n + 1).fold(0.0, |acc, x| { let p = self.pmf(x); - acc - p * p.ln() + // A mass that underflows to zero contributes nothing, taking + // `0 * ln 0 == 0` as usual for entropy. Evaluating it would give + // `0 * -inf == NaN` and poison the whole sum, which made + // `entropy()` return `NaN` for any `n` past roughly 1100. + if p > 0.0 { acc - p * p.ln() } else { acc } }) }; Some(entr) @@ -305,10 +330,7 @@ impl Discrete for Binomial { } else if prec::ulps_eq!(self.p, 1.0) { if x == self.n { 1.0 } else { 0.0 } } else { - (factorial::ln_binomial(self.n, x) - + x as f64 * self.p.ln() - + (self.n - x) as f64 * (1.0 - self.p).ln()) - .exp() + self.ln_pmf(x).exp() } } @@ -328,9 +350,38 @@ impl Discrete for Binomial { } else if prec::ulps_eq!(self.p, 1.0) { if x == self.n { 0.0 } else { f64::NEG_INFINITY } } else { - factorial::ln_binomial(self.n, x) - + x as f64 * self.p.ln() - + (self.n - x) as f64 * (1.0 - self.p).ln() + let n = self.n as f64; + let k = x as f64; + let q = 1.0 - self.p; + if x == 0 { + return n * q.ln(); + } + if x == self.n { + return n * self.p.ln(); + } + if n < gamma::STIRLING_SERIES_MIN { + // `ln_binomial` is exact from the factorial table here and all + // terms are small, so the direct form loses nothing + return factorial::ln_binomial(self.n, x) + k * self.p.ln() + (n - k) * q.ln(); + } + // Saddle-point form (Loader 2000): the direct expression differences + // terms growing like `n ln n` to produce an `O(1)` result, which + // cost ~2e-10 relative at n = 1e5. + let nk = n - k; + // `bd0` is sensitive to the last half-ulp of its mean argument, and + // both `1 - p` and the products round, so they are carried as + // double-doubles; see `gamma::bd0_dd`. + let (q_hi, q_lo) = prec::two_diff(1.0, self.p); + let np = n * self.p; + let np_lo = prec::dekker_product_err(n, self.p, np); + let nq = n * q_hi; + let nq_lo = prec::dekker_product_err(n, q_hi, nq) + n * q_lo; + gamma::stirling_delta(n) + - gamma::stirling_delta(k) + - gamma::stirling_delta(nk) + - gamma::bd0_dd(k, np, np_lo) + - gamma::bd0_dd(nk, nq, nq_lo) + + 0.5 * (n / (f64::consts::TAU * k * nk)).ln() } } } @@ -412,6 +463,61 @@ mod tests { test_exact(0.3, 10, 10, max); } + /// `p` very close to but not equal to 1 must take the general path, not the + /// degenerate `p == 1` branch. The branch is guarded by `prec::ulps_eq!`, + /// whose default epsilon was `1e-9` *absolute*, so any `p` within `1e-9` of + /// 1 collapsed to a point mass at `n`. + /// + /// `1 - 2^-33` is used rather than `1 - 1e-10` so that `1 - p` is exact and + /// the reference values are not limited by the representation of `p`. + #[test] + fn test_pmf_p_near_one_is_not_degenerate() { + let n = Binomial::new(1.0 - f64::powi(2.0, -33), 100).unwrap(); + prec::assert_relative_eq!(n.pmf(99), 1.1641532048523463366e-8, epsilon = 0.0, max_relative = 1e-13); + prec::assert_relative_eq!(n.pmf(100), 0.99999998835846788439, epsilon = 0.0, max_relative = 1e-14); + prec::assert_relative_eq!(n.ln_pmf(99), -18.268686784015220704, epsilon = 0.0, max_relative = 5e-15); + // entropy of a non-degenerate distribution is strictly positive + assert!(n.entropy().unwrap() > 0.0); + } + + /// `entropy()` sums `-p * ln(p)` over the whole support, so it used to + /// return `NaN` as soon as one mass underflowed to zero (`0 * -inf`). For + /// `p = 0.5` that starts at around `n = 1100`. + #[test] + fn test_entropy_with_underflowing_masses() { + for n in [1_100, 2_000, 5_000, 20_000] { + let d = Binomial::new(0.5, n).unwrap(); + let h = d.entropy().unwrap(); + assert!(h.is_finite(), "entropy for n={n} was {h}"); + // 0.5 * ln(2 * pi * e * n * p * q) is the asymptotic form + let approx = 0.5 * (2.0 * f64::consts::PI * f64::consts::E * n as f64 * 0.25).ln(); + prec::assert_relative_eq!(h, approx, epsilon = 0.0, max_relative = 1e-3); + } + assert!(Binomial::new(0.1, 5_000).unwrap().entropy().unwrap().is_finite()); + } + + /// Large-`n` pmf: the saddle-point form (`gamma::bd0` / + /// `gamma::stirling_delta`, with double-double means) replaced + /// `exp(ln_binomial + x ln p + (n-x) ln q)`, whose terms each grow like + /// `n ln n` while the result stays `O(1)`. References are mpmath at 40 + /// significant digits; the old form was ~7e-11 relative here. + #[test] + fn test_pmf_large_n_saddle_point() { + let pmf = |arg: u64| move |x: Binomial| x.pmf(arg); + test_relative(0.5, 100000, 0.0025231262141967398855, pmf(50000)); + test_relative(0.3, 2000000, 0.00061558120658465376062, pmf(600000)); + // ln_pmf agrees with ln(pmf) where the pmf is comfortably representable + for (p, n, k) in [(0.5f64, 100000u64, 50000u64), (0.3, 2000000, 600000)] { + let d = create_ok(p, n); + prec::assert_relative_eq!(d.ln_pmf(k), d.pmf(k).ln(), epsilon = 0.0, max_relative = 1e-14); + } + // the endpoints reduce to q^n and p^n; `exp(n ln q)` carries ~1e-15 + // relative (|n ln q| ~ 11.5 here), which is the honest bound + let d = create_ok(0.25, 40); + prec::assert_relative_eq!(d.pmf(0), 1.0056585161637497e-5, epsilon = 0.0, max_relative = 1e-14); + prec::assert_relative_eq!(d.pmf(40), 8.271806125530277e-25, epsilon = 0.0, max_relative = 1e-14); + } + #[test] fn test_pmf() { let pmf = |arg: u64| move |x: Binomial| x.pmf(arg); @@ -515,7 +621,7 @@ mod tests { test_absolute(0.3, 3, 0.657, 1e-14, sf(0)); test_absolute(0.3, 3, 0.216, 1e-15, sf(1)); test_exact(0.3, 3, 0.0, sf(3)); - test_absolute(0.3, 10, 0.9717524751000001, 1e-16, sf(0)); + test_absolute(0.3, 10, 0.9717524750999999955198, 1e-16, sf(0)); test_absolute(0.3, 10, 0.850691654100002, 1e-14, sf(1)); test_exact(0.3, 10, 0.0, sf(10)); test_exact(1.0, 1, 1.0, sf(0)); diff --git a/src/distribution/chi_squared.rs b/src/distribution/chi_squared.rs index 38cb548c..9517561b 100644 --- a/src/distribution/chi_squared.rs +++ b/src/distribution/chi_squared.rs @@ -139,6 +139,16 @@ impl ContinuousCDF for ChiSquared { self.g.sf(x) } + /// Delegates to the wrapped Gamma's tail-accurate implementation. + fn ln_cdf(&self, x: f64) -> f64 { + self.g.ln_cdf(x) + } + + /// Delegates to the wrapped Gamma's tail-accurate implementation. + fn ln_sf(&self, x: f64) -> f64 { + self.g.ln_sf(x) + } + /// Calculates the inverse cumulative distribution function for the /// chi-squared distribution at `x` /// diff --git a/src/distribution/dirichlet.rs b/src/distribution/dirichlet.rs index 5f1697b4..51dd809b 100644 --- a/src/distribution/dirichlet.rs +++ b/src/distribution/dirichlet.rs @@ -19,7 +19,7 @@ use nalgebra::{Dim, Dyn, OMatrix, OVector}; /// /// let n = Dirichlet::new(vec![1.0, 2.0, 3.0]).unwrap(); /// assert_eq!(n.mean().unwrap(), DVector::from_vec(vec![1.0 / 6.0, 1.0 / 3.0, 0.5])); -/// assert_eq!(n.pdf(&DVector::from_vec(vec![0.33333, 0.33333, 0.33333])), 2.222155556222205); +/// assert_eq!(n.pdf(&DVector::from_vec(vec![0.33333, 0.33333, 0.33333])), 2.2221555562222193); /// ``` #[derive(Clone, PartialEq, Debug)] pub struct Dirichlet diff --git a/src/distribution/erlang.rs b/src/distribution/erlang.rs index 8fd5440f..a58fcfe2 100644 --- a/src/distribution/erlang.rs +++ b/src/distribution/erlang.rs @@ -123,6 +123,16 @@ impl ContinuousCDF for Erlang { self.g.sf(x) } + /// Delegates to the wrapped Gamma's tail-accurate implementation. + fn ln_cdf(&self, x: f64) -> f64 { + self.g.ln_cdf(x) + } + + /// Delegates to the wrapped Gamma's tail-accurate implementation. + fn ln_sf(&self, x: f64) -> f64 { + self.g.ln_sf(x) + } + /// Calculates the inverse cumulative distribution function for the erlang /// distribution at `x` /// diff --git a/src/distribution/exponential.rs b/src/distribution/exponential.rs index 9ebbe2ec..6f08850a 100644 --- a/src/distribution/exponential.rs +++ b/src/distribution/exponential.rs @@ -117,7 +117,9 @@ impl ContinuousCDF for Exp { if x < 0.0 { 0.0 } else { - 1.0 - (-self.rate * x).exp() + // `-expm1` rather than `1 - exp`: the latter cancels for small + // `rate * x`, losing ~7 digits by `rate * x = 1e-10` + -(-self.rate * x).exp_m1() } } @@ -135,6 +137,22 @@ impl ContinuousCDF for Exp { if x < 0.0 { 1.0 } else { (-self.rate * x).exp() } } + /// Tail-accurate log of the cdf: `ln(-expm1(-rate x))`. + fn ln_cdf(&self, x: f64) -> f64 { + if x < 0.0 { + f64::NEG_INFINITY + } else { + (-(-self.rate * x).exp_m1()).ln() + } + } + + /// Exact log of the survival function: `ln sf = -rate * x`, finite for + /// every representable `x` even though `sf` underflows past + /// `x ~ 709 / rate`. + fn ln_sf(&self, x: f64) -> f64 { + if x < 0.0 { 0.0 } else { -self.rate * x } + } + /// Calculates the inverse cumulative distribution function. /// /// # Formula diff --git a/src/distribution/fisher_snedecor.rs b/src/distribution/fisher_snedecor.rs index 9cc2f5c4..cd5e160f 100644 --- a/src/distribution/fisher_snedecor.rs +++ b/src/distribution/fisher_snedecor.rs @@ -525,8 +525,8 @@ mod tests { test_absolute(10.0, 0.1, 0.0418440630400545297349, 1e-14, pdf(1.0)); test_absolute(0.1, 1.0, 0.0396064560910663979961, 1e-16, pdf(1.0)); test_absolute(1.0, 1.0, 0.1591549430918953357689, 1e-16, pdf(1.0)); - test_absolute(10.0, 1.0, 0.230361989229138647108, 1e-16, pdf(1.0)); - test_absolute(0.1, 0.1, 0.00221546909694001013517, 1e-18, pdf(10.0)); + test_absolute(10.0, 1.0, 0.230361989229138647108, 3e-16, pdf(1.0)); + test_absolute(0.1, 0.1, 0.00221546909694001013517, 5e-18, pdf(10.0)); test_absolute(1.0, 0.1, 0.00369960370387922619592, 1e-17, pdf(10.0)); test_absolute(10.0, 0.1, 0.00390179721174142927402, 1e-15, pdf(10.0)); test_absolute(0.1, 1.0, 0.00319864073359931548273, 1e-17, pdf(10.0)); @@ -540,13 +540,13 @@ mod tests { #[test] fn test_ln_pdf() { let ln_pdf = |arg: f64| move |x: FisherSnedecor| x.ln_pdf(arg); - test_absolute(0.1, 0.1, 0.0234154207226588982471f64.ln(), 1e-15, ln_pdf(1.0)); + test_absolute(0.1, 0.1, 0.0234154207226588982471f64.ln(), 3e-15, ln_pdf(1.0)); test_absolute(1.0, 0.1, 0.0396064560910663979961f64.ln(), 1e-15, ln_pdf(1.0)); test_absolute(10.0, 0.1, 0.0418440630400545297349f64.ln(), 1e-13, ln_pdf(1.0)); test_absolute(0.1, 1.0, 0.0396064560910663979961f64.ln(), 1e-15, ln_pdf(1.0)); test_absolute(1.0, 1.0, 0.1591549430918953357689f64.ln(), 1e-15, ln_pdf(1.0)); test_absolute(10.0, 1.0, 0.230361989229138647108f64.ln(), 1e-15, ln_pdf(1.0)); - test_exact(0.1, 0.1, 0.00221546909694001013517f64.ln(), ln_pdf(10.0)); + test_absolute(0.1, 0.1, 0.00221546909694001013517f64.ln(), 3e-15, ln_pdf(10.0)); test_absolute(1.0, 0.1, 0.00369960370387922619592f64.ln(), 1e-15, ln_pdf(10.0)); test_absolute(10.0, 0.1, 0.00390179721174142927402f64.ln(), 1e-13, ln_pdf(10.0)); test_absolute(0.1, 1.0, 0.00319864073359931548273f64.ln(), 1e-15, ln_pdf(10.0)); diff --git a/src/distribution/gamma.rs b/src/distribution/gamma.rs index 3ac60b09..e1de9783 100644 --- a/src/distribution/gamma.rs +++ b/src/distribution/gamma.rs @@ -184,6 +184,37 @@ impl ContinuousCDF for Gamma { } } + /// Tail-accurate log of the cdf via [`gamma::ln_gamma_lr`]; stays finite + /// far past where `cdf` underflows. + fn ln_cdf(&self, x: f64) -> f64 { + if x <= 0.0 { + f64::NEG_INFINITY + } else if prec::ulps_eq!(x, self.shape) && self.rate.is_infinite() { + 0.0 + } else if self.rate.is_infinite() { + f64::NEG_INFINITY + } else if x.is_infinite() { + 0.0 + } else { + gamma::ln_gamma_lr(self.shape, x * self.rate) + } + } + + /// Tail-accurate log of the survival function via [`gamma::ln_gamma_ur`]. + fn ln_sf(&self, x: f64) -> f64 { + if x <= 0.0 { + 0.0 + } else if prec::ulps_eq!(x, self.shape) && self.rate.is_infinite() { + f64::NEG_INFINITY + } else if self.rate.is_infinite() { + 0.0 + } else if x.is_infinite() { + f64::NEG_INFINITY + } else { + gamma::ln_gamma_ur(self.shape, x * self.rate) + } + } + fn inverse_cdf(&self, p: f64) -> f64 { if !(0.0..=1.0).contains(&p) { panic!("default inverse_cdf implementation should be provided probability on [0,1]") diff --git a/src/distribution/geometric.rs b/src/distribution/geometric.rs index f74f2a79..adc22689 100644 --- a/src/distribution/geometric.rs +++ b/src/distribution/geometric.rs @@ -159,6 +159,25 @@ impl DiscreteCDF for Geometric { } } + /// Tail-accurate log of the cdf. + fn ln_cdf(&self, x: u64) -> f64 { + if x == 0 { + f64::NEG_INFINITY + } else { + (-((-self.p).ln_1p() * (x as f64)).exp_m1()).ln() + } + } + + /// Exact log of the survival function: `ln sf = x * ln(1 - p)`, finite for + /// every `x` even though `sf` underflows past `x ~ 709 / p`. + fn ln_sf(&self, x: u64) -> f64 { + if x == 0 { + 0.0 + } else { + (-self.p).ln_1p() * (x as f64) + } + } + /// Calculates the inverse cumulative distribution function for the /// geometric distribution at `x`. /// In other languages, such as R, this is known as the quantile function. @@ -437,6 +456,18 @@ mod tests { test_exact(0.3, u64::MAX, max); } + /// As in [`Binomial`](crate::distribution::Binomial), the `p == 1` branches + /// here are guarded by `prec::ulps_eq!`; its default epsilon was `1e-9` + /// absolute, so `p = 1 - 2^-33` was treated as a point mass at 1. + #[test] + fn test_p_near_one_is_not_degenerate() { + let g = Geometric::new(1.0 - f64::powi(2.0, -33)).unwrap(); + assert_eq!(g.max(), u64::MAX); + prec::assert_relative_eq!(g.skewness().unwrap(), 92681.900034472750337, epsilon = 0.0, max_relative = 1e-14); + prec::assert_relative_eq!(g.pmf(2), 1.164153218133822873e-10, epsilon = 0.0, max_relative = 1e-14); + prec::assert_relative_eq!(g.ln_pmf(2), -22.873856958594610533, epsilon = 0.0, max_relative = 1e-14); + } + #[test] fn test_pmf() { let pmf = |arg: u64| move |x: Geometric| x.pmf(arg); diff --git a/src/distribution/inverse_gaussian.rs b/src/distribution/inverse_gaussian.rs new file mode 100644 index 00000000..967c23de --- /dev/null +++ b/src/distribution/inverse_gaussian.rs @@ -0,0 +1,659 @@ +use crate::distribution::{Continuous, ContinuousCDF}; +use crate::function::erf; +use crate::statistics::*; +use core::f64; +#[cfg(not(feature = "std"))] +use num_traits::Float as _; + +/// Implements the +/// [Inverse Gaussian](https://en.wikipedia.org/wiki/Inverse_Gaussian_distribution) +/// (Wald) distribution, parameterized by its mean `mu` and shape `lambda`. +/// +/// # Examples +/// +/// ``` +/// use statrs::distribution::{InverseGaussian, Continuous}; +/// use statrs::statistics::Distribution; +/// +/// let n = InverseGaussian::new(1.0, 1.0).unwrap(); +/// assert_eq!(n.mean().unwrap(), 1.0); +/// assert!((n.pdf(1.0) - 0.3989422804014327).abs() < 1e-15); +/// ``` +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct InverseGaussian { + mu: f64, + lambda: f64, +} + +/// Represents the errors that can occur when creating an [`InverseGaussian`]. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] +#[non_exhaustive] +pub enum InverseGaussianError { + /// The mean is NaN, zero, negative, or infinite. + MuInvalid, + + /// The shape is NaN, zero, negative, or infinite. + LambdaInvalid, +} + +impl core::fmt::Display for InverseGaussianError { + #[cfg_attr(coverage_nightly, coverage(off))] + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + match self { + InverseGaussianError::MuInvalid => { + write!(f, "Mean is NaN, zero, negative, or infinite") + } + InverseGaussianError::LambdaInvalid => { + write!(f, "Shape is NaN, zero, negative, or infinite") + } + } + } +} + +impl core::error::Error for InverseGaussianError {} + +impl InverseGaussian { + /// Constructs a new inverse Gaussian distribution with mean `mu` and + /// shape `lambda`. + /// + /// Both parameters must be finite and positive; infinite parameters are + /// rejected rather than admitted as degenerate limits. + /// + /// # Errors + /// + /// Returns an error if `mu` or `lambda` is NaN, zero, negative, or + /// infinite. + /// + /// # Examples + /// + /// ``` + /// use statrs::distribution::InverseGaussian; + /// + /// let mut result = InverseGaussian::new(1.0, 1.0); + /// assert!(result.is_ok()); + /// + /// result = InverseGaussian::new(0.0, 1.0); + /// assert!(result.is_err()); + /// ``` + pub fn new(mu: f64, lambda: f64) -> Result { + if !(mu.is_finite() && mu > 0.0) { + return Err(InverseGaussianError::MuInvalid); + } + if !(lambda.is_finite() && lambda > 0.0) { + return Err(InverseGaussianError::LambdaInvalid); + } + Ok(InverseGaussian { mu, lambda }) + } + + /// Returns the mean `mu` of the inverse Gaussian distribution. + /// + /// # Examples + /// + /// ``` + /// use statrs::distribution::InverseGaussian; + /// + /// let n = InverseGaussian::new(1.0, 2.0).unwrap(); + /// assert_eq!(n.mu(), 1.0); + /// ``` + pub fn mu(&self) -> f64 { + self.mu + } + + /// Returns the shape `lambda` of the inverse Gaussian distribution. + /// + /// # Examples + /// + /// ``` + /// use statrs::distribution::InverseGaussian; + /// + /// let n = InverseGaussian::new(1.0, 2.0).unwrap(); + /// assert_eq!(n.lambda(), 2.0); + /// ``` + pub fn lambda(&self) -> f64 { + self.lambda + } + + /// The same two arguments as `erfc` takes rather than `Phi`: + /// `u_minus = sqrt(lambda / 2x) (x/mu - 1)` and + /// `u_plus = sqrt(lambda / 2x) (x/mu + 1)`, so that + /// `Phi(a) = erfc(-u_minus) / 2` and `Phi(b) = erfc(u_plus) / 2`. + /// + /// These are the natural variables for the tails, because + /// `2 lambda / mu - u_plus^2 == -u_minus^2` identically - the exponential + /// prefactor of the second cdf term is exactly the difference of the two + /// squares. + fn erfc_args(&self, x: f64) -> (f64, f64) { + let s = (self.lambda / (2.0 * x)).sqrt(); + let t = x / self.mu; + (s * (t - 1.0), s * (t + 1.0)) + } +} + +/// `exp(-u^2)` with the rounding error of the square folded back in, so the +/// squaring contributes nothing on top of the exponential's own inherent +/// amplification. +fn exp_neg_square(u: f64) -> f64 { + let sq = u * u; + let err = crate::prec::dekker_product_err(u, u, sq); + (-sq).exp() * (1.0 - err) +} + +/// `-u^2` exactly, as the leading term of a log-domain result. +fn neg_square_exact(u: f64) -> f64 { + let sq = u * u; + -sq - crate::prec::dekker_product_err(u, u, sq) +} + +impl core::fmt::Display for InverseGaussian { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "IG({},{})", self.mu, self.lambda) + } +} + +#[cfg(feature = "rand")] +#[cfg_attr(docsrs, doc(cfg(feature = "rand")))] +impl ::rand::distr::Distribution for InverseGaussian { + /// Samples by the transformation method of Michael, Schucany & Haas + /// (1976): a chi-square(1) variate is mapped to the smaller root of the + /// quadratic it satisfies, then that root or its conjugate `mu^2 / y` is + /// selected with the appropriate probability. + fn sample(&self, rng: &mut R) -> f64 { + let z = crate::distribution::ziggurat::sample_std_normal(rng); + let nu = z * z; + let mnu = self.mu * nu; + // The textbook root `mu + (mu / 2 lambda) (mnu - sqrt(mnu (4 lambda + + // mnu)))` cancels catastrophically for `mnu >> lambda`; rationalizing + // gives this equivalent, subtraction-free form. + let y = self.mu - (2.0 * self.mu * mnu) / (mnu + (mnu * (4.0 * self.lambda + mnu)).sqrt()); + let u: f64 = ::rand::RngExt::random(rng); + if u <= self.mu / (self.mu + y) { + y + } else { + self.mu * self.mu / y + } + } +} + +impl ContinuousCDF for InverseGaussian { + /// Calculates the cumulative distribution function for the inverse + /// Gaussian distribution at `x` + /// + /// # Formula + /// + /// ```text + /// Phi(sqrt(λ/x) (x/μ - 1)) + e^(2λ/μ) Phi(-sqrt(λ/x) (x/μ + 1)) + /// ``` + /// + /// where `Phi` is the standard normal cdf. Evaluated in the equivalent + /// scaled form below, which never forms `e^(2λ/μ)` - that overflows for + /// `λ/μ > ~355` even though the product it appears in is a probability. + /// + /// # Remarks + /// + /// Measured against mpmath at 60 digits over 1800 points (six parameter + /// sets, ten decades each): 0.11 ulp median, 1179 ulp worst case. The worst + /// case is in the far left tail, where relative accuracy degrades like + /// `|ln cdf| * eps` - inherent to returning a linear-domain probability + /// that small, since the exponent itself is not representable more finely + /// than that. Use [`ContinuousCDF::ln_cdf`] for tail work. + fn cdf(&self, x: f64) -> f64 { + if x.is_nan() { + return f64::NAN; + } + if x <= 0.0 { + return 0.0; + } + if x.is_infinite() { + return 1.0; + } + let (um, up) = self.erfc_args(x); + if um <= 0.0 { + self.cdf_scaled(um, up).min(1.0) + } else { + (1.0 - self.sf_scaled(um, up, x)).clamp(0.0, 1.0) + } + } + + /// Calculates the survival function for the inverse Gaussian distribution + /// at `x` + /// + /// # Remarks + /// + /// In the far right tail the two terms of the cdf formula approach each + /// other - their ratio is `(x/μ - 1)/(x/μ + 1)` - so relative accuracy + /// degrades by roughly a factor `x / 2μ` on top of the `|ln sf| * eps` + /// amplification inherent to a linear-domain result. Measured over the same + /// 1800 points: 0.12 ulp median, 78686 ulp (about `9e-12` relative) worst + /// case. Use [`ContinuousCDF::ln_sf`] for tail work. + fn sf(&self, x: f64) -> f64 { + if x.is_nan() { + return f64::NAN; + } + if x <= 0.0 { + return 1.0; + } + if x.is_infinite() { + return 0.0; + } + let (um, up) = self.erfc_args(x); + if um > 0.0 { + self.sf_scaled(um, up, x).max(0.0) + } else { + (1.0 - self.cdf_scaled(um, up)).clamp(0.0, 1.0) + } + } + + /// Tail-accurate log of the cdf, finite far past the point where `cdf` + /// itself underflows to zero. + /// + /// # Remarks + /// + /// The absolute error of a log is the relative error of the probability it + /// represents, so that is the metric quoted here. Wherever the cdf is a + /// representable nonzero `f64`, the measured absolute error is `1.1e-23` + /// median and `2.4e-13` worst case - i.e. the implied probability is good + /// to at least 12 significant digits, against the 1179 ulp that [`Self::cdf`] + /// can lose there. + /// + /// Past that point the log itself is the limit: at `ln cdf = -3.4e8` one + /// ulp *is* `6e-8`, and the measured error stays within 1-2 ulp of the + /// returned value. + fn ln_cdf(&self, x: f64) -> f64 { + if x.is_nan() { + return f64::NAN; + } + if x <= 0.0 { + return f64::NEG_INFINITY; + } + if x.is_infinite() { + return 0.0; + } + let (um, up) = self.erfc_args(x); + if um <= 0.0 { + // both scaled terms are positive here, so this sum never cancels + neg_square_exact(um) + (0.5 * (erf::erfcx(-um) + erf::erfcx(up))).ln() + } else { + (-self.sf_scaled(um, up, x)).ln_1p() + } + } + + /// Tail-accurate log of the survival function; see [`Self::ln_cdf`] for the + /// error metric. Wherever `sf` is a representable nonzero `f64` the measured + /// absolute error is `9.4e-20` median and `1.0e-11` worst case, against the + /// 78686 ulp [`Self::sf`] can lose in the same region; beyond it the error + /// stays within 1-2 ulp of the returned log. + fn ln_sf(&self, x: f64) -> f64 { + if x.is_nan() { + return f64::NAN; + } + if x <= 0.0 { + return 0.0; + } + if x.is_infinite() { + return f64::NEG_INFINITY; + } + let (um, up) = self.erfc_args(x); + if um > 0.0 { + neg_square_exact(um) + (0.5 * self.erfcx_gap(um, up, x)).ln() + } else { + (-self.cdf_scaled(um, up)).ln_1p() + } + } +} + +impl InverseGaussian { + /// `cdf(x)` for `u_minus <= 0` (i.e. `x <= μ`), where the cdf is the + /// smaller of the two tails. + /// + /// Writing `Phi(a) = erfc(-u_minus)/2` and + /// `e^(2λ/μ) Phi(b) = e^(-u_minus^2) erfcx(u_plus) / 2` - the latter using + /// `2λ/μ - u_plus^2 == -u_minus^2` - gives + /// `cdf = e^(-u_minus^2) [erfcx(-u_minus) + erfcx(u_plus)] / 2`. Both + /// `erfcx` terms are `O(1/u)` and positive, so nothing large is ever + /// formed and nothing cancels. + fn cdf_scaled(&self, um: f64, up: f64) -> f64 { + let _ = self; + 0.5 * exp_neg_square(um) * (erf::erfcx(-um) + erf::erfcx(up)) + } + + /// `sf(x)` for `u_minus > 0` (i.e. `x > μ`), the mirror of + /// [`Self::cdf_scaled`]: + /// `sf = e^(-u_minus^2) [erfcx(u_minus) - erfcx(u_plus)] / 2`. + fn sf_scaled(&self, um: f64, up: f64, x: f64) -> f64 { + 0.5 * exp_neg_square(um) * self.erfcx_gap(um, up, x) + } + + /// `erfcx(u_minus) - erfcx(u_plus)`, which is positive because `erfcx` is + /// decreasing and `u_plus > u_minus`. + /// + /// The two values converge as `x / μ` grows, so for large enough `x` the + /// difference underflows to zero or below. There the limiting ratio is + /// `u_minus / u_plus = (x/μ - 1)/(x/μ + 1)`, so the gap tends to + /// `erfcx(u_minus) * 2 / (x/μ + 1)`. + fn erfcx_gap(&self, um: f64, up: f64, x: f64) -> f64 { + let lo = erf::erfcx(um); + let gap = lo - erf::erfcx(up); + if gap > 0.0 { + gap + } else { + lo * 2.0 / (x / self.mu + 1.0) + } + } +} + +impl Min for InverseGaussian { + /// Returns the minimum value in the domain of the inverse Gaussian + /// distribution + /// + /// # Formula + /// + /// ```text + /// 0 + /// ``` + fn min(&self) -> f64 { + 0.0 + } +} + +impl Max for InverseGaussian { + /// Returns the maximum value in the domain of the inverse Gaussian + /// distribution + /// + /// # Formula + /// + /// ```text + /// f64::INFINITY + /// ``` + fn max(&self) -> f64 { + f64::INFINITY + } +} + +impl Distribution for InverseGaussian { + /// Returns the mean of the inverse Gaussian distribution + /// + /// # Formula + /// + /// ```text + /// μ + /// ``` + fn mean(&self) -> Option { + Some(self.mu) + } + + /// Returns the variance of the inverse Gaussian distribution + /// + /// # Formula + /// + /// ```text + /// μ^3 / λ + /// ``` + fn variance(&self) -> Option { + Some(self.mu * self.mu * self.mu / self.lambda) + } + + /// Returns the skewness of the inverse Gaussian distribution + /// + /// # Formula + /// + /// ```text + /// 3 sqrt(μ / λ) + /// ``` + fn skewness(&self) -> Option { + Some(3.0 * (self.mu / self.lambda).sqrt()) + } +} + +impl Mode> for InverseGaussian { + /// Returns the mode of the inverse Gaussian distribution + /// + /// # Formula + /// + /// ```text + /// μ [sqrt(1 + 9μ^2 / (4λ^2)) - 3μ / (2λ)] + /// ``` + fn mode(&self) -> Option { + let r = self.mu / self.lambda; + Some(self.mu * ((1.0 + 2.25 * r * r).sqrt() - 1.5 * r)) + } +} + +impl Continuous for InverseGaussian { + /// Calculates the probability density function for the inverse Gaussian + /// distribution at `x` + /// + /// # Formula + /// + /// ```text + /// sqrt(λ / (2π x^3)) e^(-λ (x - μ)^2 / (2 μ^2 x)) + /// ``` + /// + /// # Remarks + /// + /// Measured at 2.54 ulp median, 583 ulp worst case. The worst case is in the + /// tails, and is a property of `exp`, not of this expression: a relative + /// error `eps` in the exponent becomes a relative error `|exponent| * eps` + /// in the result, and once the exponent reaches `-10^3` no `f64` evaluation + /// can do better. [`Self::ln_pdf`] returns that exponent directly and is + /// accurate to a few ulp throughout. + fn pdf(&self, x: f64) -> f64 { + if x.is_nan() { + return f64::NAN; + } + if x <= 0.0 || x.is_infinite() { + return 0.0; + } + let d = x - self.mu; + (self.lambda / (2.0 * f64::consts::PI * x * x * x)).sqrt() + * (-self.lambda * d * d / (2.0 * self.mu * self.mu * x)).exp() + } + + /// Calculates the log probability density function for the inverse + /// Gaussian distribution at `x` + /// + /// # Formula + /// + /// ```text + /// (1/2) [ln λ - ln(2π) - 3 ln x] - λ (x - μ)^2 / (2 μ^2 x) + /// ``` + fn ln_pdf(&self, x: f64) -> f64 { + if x.is_nan() { + return f64::NAN; + } + if x <= 0.0 || x.is_infinite() { + return f64::NEG_INFINITY; + } + let d = x - self.mu; + 0.5 * (self.lambda.ln() - (2.0 * f64::consts::PI).ln() - 3.0 * x.ln()) + - self.lambda * d * d / (2.0 * self.mu * self.mu * x) + } +} + +#[rustfmt::skip] +#[cfg(test)] +mod tests { + use super::*; + use crate::distribution::internal::density_util; + use crate::prec; + + crate::distribution::internal::testing_boiler!(mu: f64, lambda: f64; InverseGaussian; InverseGaussianError); + + #[test] + fn test_create() { + create_ok(1.0, 1.0); + create_ok(0.1, 10.0); + create_ok(1e5, 1e-5); + } + + #[test] + fn test_bad_create() { + test_create_err(0.0, 1.0, InverseGaussianError::MuInvalid); + test_create_err(1.0, 0.0, InverseGaussianError::LambdaInvalid); + create_err(-1.0, 1.0); + create_err(1.0, -1.0); + create_err(f64::NAN, 1.0); + create_err(1.0, f64::NAN); + create_err(f64::INFINITY, 1.0); + create_err(1.0, f64::INFINITY); + } + + #[test] + fn test_moments() { + let mean = |x: InverseGaussian| x.mean().unwrap(); + let variance = |x: InverseGaussian| x.variance().unwrap(); + let skewness = |x: InverseGaussian| x.skewness().unwrap(); + test_exact(2.0, 3.0, 2.0, mean); + test_exact(2.0, 3.0, 8.0 / 3.0, variance); + test_absolute(2.0, 3.0, 2.449489742783178098197, 1e-15, skewness); + // entropy has no elementary closed form and stays None + assert!(create_ok(1.0, 1.0).entropy().is_none()); + } + + #[test] + fn test_mode() { + // mpmath at 50 significant digits + let mode = |x: InverseGaussian| x.mode().unwrap(); + test_absolute(1.0, 1.0, 0.3027756377319946465596, 1e-15, mode); + test_absolute(2.0, 3.0, 0.8284271247461900976034, 1e-15, mode); + } + + #[test] + fn test_pdf() { + // mpmath at 50 significant digits + let pdf = |arg: f64| move |x: InverseGaussian| x.pdf(arg); + test_absolute(1.0, 1.0, 0.3989422804014326779399, 1e-15, pdf(1.0)); + test_absolute(1.0, 1.0, 0.8787825789354447940937, 1e-14, pdf(0.5)); + test_absolute(1.0, 1.0, 0.03941835796981973098901, 1e-15, pdf(3.0)); + test_absolute(2.0, 3.0, 0.3533380431253714144991, 1e-15, pdf(1.5)); + test_absolute(0.5, 0.25, 0.7786842707660154116726, 1e-14, pdf(0.4)); + test_absolute(1.0, 800.0, 11.28379167095512573896, 1e-13, pdf(1.0)); + test_absolute(3.0, 1.0, 0.009609160709649644201603, 1e-16, pdf(10.0)); + test_exact(1.0, 1.0, 0.0, pdf(0.0)); + test_exact(1.0, 1.0, 0.0, pdf(-1.0)); + } + + #[test] + fn test_ln_pdf_consistent_with_pdf() { + for (mu, lambda) in [(1.0, 1.0), (2.0, 3.0), (0.5, 0.25)] { + let n = create_ok(mu, lambda); + for x in [0.1, 0.5, 1.0, 2.0, 10.0] { + prec::assert_relative_eq!(n.pdf(x).ln(), n.ln_pdf(x), max_relative = 1e-13); + } + } + } + + #[test] + fn test_cdf() { + // mpmath at 50 significant digits + let cdf = |arg: f64| move |x: InverseGaussian| x.cdf(arg); + test_absolute(1.0, 1.0, 0.6681020012231706064271, 1e-15, cdf(1.0)); + test_absolute(1.0, 1.0, 0.3649755481729598905864, 1e-15, cdf(0.5)); + test_absolute(1.0, 1.0, 0.9531879207427883590297, 1e-15, cdf(3.0)); + test_absolute(2.0, 3.0, 0.4956901248416294820537, 1e-15, cdf(1.5)); + test_exact(1.0, 1.0, 0.0, cdf(0.0)); + test_exact(1.0, 1.0, 1.0, cdf(f64::INFINITY)); + } + + /// `e^(2 lambda / mu)` alone is `e^1600 = inf` here; a naive evaluation of + /// the cdf formula returns NaN or inf. The log-domain second term keeps it + /// a probability (Giner & Smyth, 2016). + #[test] + fn test_cdf_shape_overflow_regime() { + let cdf = |arg: f64| move |x: InverseGaussian| x.cdf(arg); + assert_eq!((2.0f64 * 800.0 / 1.0).exp(), f64::INFINITY, "premise"); + test_absolute(1.0, 800.0, 0.001517236267531762119023, 1e-16, cdf(0.9)); + test_absolute(1.0, 800.0, 0.5070501679916889068124, 1e-13, cdf(1.0)); + test_absolute(1.0, 800.0, 0.9966850760983806309312, 1e-13, cdf(1.1)); + test_absolute(0.1, 1000.0, 0.501994661537961729716, 1e-13, cdf(0.1)); + } + + #[test] + fn test_sf() { + // mpmath at 50 significant digits + let sf = |arg: f64| move |x: InverseGaussian| x.sf(arg); + test_absolute(1.0, 1.0, 0.04681207925721164097026, 1e-15, sf(3.0)); + test_absolute(1.0, 1.0, 3.631365872581210534235e-9, 1e-19, sf(30.0)); + test_absolute(2.0, 3.0, 0.00004039669278096080163591, 1e-16, sf(20.0)); + test_exact(1.0, 1.0, 1.0, sf(0.0)); + test_exact(1.0, 1.0, 0.0, sf(f64::INFINITY)); + } + + #[test] + fn test_cdf_sf_sum_to_one() { + for (mu, lambda) in [(1.0, 1.0), (2.0, 3.0), (1.0, 800.0)] { + let n = create_ok(mu, lambda); + for x in [0.2, 0.5, 1.0, 2.0, 5.0] { + prec::assert_abs_diff_eq!(n.cdf(x) + n.sf(x), 1.0, epsilon = 1e-14); + } + } + } + + /// References are mpmath at 60 significant digits; every probability here + /// is far below `f64::MIN_POSITIVE`, so the linear-domain cdf/sf are hard + /// zeros and their naive logs are `-inf`. + #[test] + fn test_ln_cdf_ln_sf_deep_tails() { + let n = create_ok(1.0, 1.0); + prec::assert_relative_eq!(n.ln_cdf(0.001), -502.6811655093445338242, max_relative = 1e-14); + prec::assert_relative_eq!(n.ln_cdf(0.01), -51.54304262742703284796, max_relative = 1e-14); + // and past the point where even the log-domain-assembled cdf is a + // hard zero in linear space + assert_eq!(n.cdf(0.0001), 0.0, "premise: cdf underflows here"); + prec::assert_relative_eq!(n.ln_cdf(0.0001), -5003.831111503650139571, max_relative = 1e-14); + prec::assert_relative_eq!(n.ln_sf(1000.0), -509.5909128464241764622, max_relative = 1e-13); + assert_eq!(n.sf(100000.0), 0.0, "premise: sf underflows here"); + prec::assert_relative_eq!(n.ln_sf(100000.0), -50016.49521454895014606, max_relative = 1e-13); + let m = create_ok(2.0, 3.0); + prec::assert_relative_eq!(m.ln_sf(5000.0), -1885.665692017919709583, max_relative = 1e-13); + let s = create_ok(1.0, 800.0); + prec::assert_relative_eq!(s.ln_cdf(0.5), -203.6289210970444983027, max_relative = 1e-13); + } + + #[test] + fn test_ln_cdf_ln_sf_consistent_with_linear() { + for (mu, lambda) in [(1.0, 1.0), (2.0, 3.0), (1.0, 800.0)] { + let n = create_ok(mu, lambda); + for x in [0.3, 0.8, 1.0, 1.5, 4.0, 20.0] { + let (c, s) = (n.cdf(x), n.sf(x)); + if c > 1e-290 && c < 1.0 { + prec::assert_relative_eq!(c.ln(), n.ln_cdf(x), epsilon = 1e-12, max_relative = 1e-12); + } + if s > 1e-290 && s < 1.0 { + prec::assert_relative_eq!(s.ln(), n.ln_sf(x), epsilon = 1e-12, max_relative = 1e-12); + } + } + } + } + + #[test] + fn test_continuous() { + density_util::check_continuous_distribution(&create_ok(1.0, 1.0), 0.0, 40.0); + density_util::check_continuous_distribution(&create_ok(2.0, 3.0), 0.0, 50.0); + } + + #[cfg(feature = "rand")] + #[test] + fn test_sample_moments() { + use ::rand::SeedableRng; + use ::rand::distr::Distribution as _; + use ::rand::rngs::StdRng; + + for (mu, lambda) in [(1.0, 1.0), (2.0, 3.0), (0.5, 8.0)] { + let n = create_ok(mu, lambda); + let mut rng = StdRng::seed_from_u64(0x1C + lambda as u64); + const SAMPLES: usize = 200_000; + let mut sum = 0.0; + let mut min = f64::INFINITY; + for _ in 0..SAMPLES { + let x: f64 = n.sample(&mut rng); + assert!(x > 0.0 && x.is_finite(), "sample {x} outside support"); + sum += x; + min = min.min(x); + } + let sample_mean = sum / SAMPLES as f64; + // sample mean is within ~6 sd / sqrt(n) of mu on a fixed seed + let tol = 6.0 * (n.variance().unwrap() / SAMPLES as f64).sqrt(); + prec::assert_abs_diff_eq!(sample_mean, mu, epsilon = tol); + } + } +} diff --git a/src/distribution/log_normal.rs b/src/distribution/log_normal.rs index d36dc504..9cbae439 100644 --- a/src/distribution/log_normal.rs +++ b/src/distribution/log_normal.rs @@ -178,6 +178,29 @@ impl ContinuousCDF for LogNormal { } } + /// Tail-accurate log of the cdf, via the normal log-cdf at `ln x`. + fn ln_cdf(&self, x: f64) -> f64 { + if x <= 0.0 { + f64::NEG_INFINITY + } else if x.is_infinite() { + 0.0 + } else { + super::normal::ln_cdf_unchecked(x.ln(), self.location, self.scale) + } + } + + /// Tail-accurate log of the survival function, via the normal log-sf at + /// `ln x`. + fn ln_sf(&self, x: f64) -> f64 { + if x <= 0.0 { + 0.0 + } else if x.is_infinite() { + f64::NEG_INFINITY + } else { + super::normal::ln_sf_unchecked(x.ln(), self.location, self.scale) + } + } + /// Calculates the inverse cumulative distribution function for the /// log-normal distribution at `p` /// @@ -750,13 +773,13 @@ mod tests { test_absolute(-0.1, 0.1, 1.0, 1e-107, sf(0.1)); // Wolfram Alpha:: SurvivalFunction[ LogNormalDistribution(-0.1, 0.1), 0.8] - test_absolute(-0.1, 0.1, 0.890919989231123, 1e-14, sf(0.8)); + test_absolute(-0.1, 0.1, 0.890919989236242017902656326193, 1e-14, sf(0.8)); // Wolfram Alpha:: SurvivalFunction[LogNormalDistribution[1.5, 1], 0.8] - test_absolute(1.5, 1.0, 0.957568715612642, 1e-14, sf(0.8)); + test_absolute(1.5, 1.0, 0.957568715614411289470712253063, 1e-14, sf(0.8)); // Wolfram Alpha:: SurvivalFunction[ LogNormalDistribution(2.5, 1.5), 0.1] - test_absolute(2.5, 1.5, 0.9993169594777358, 1e-14, sf(0.1)); + test_absolute(2.5, 1.5, 0.999316959477792115067727898746, 1e-14, sf(0.1)); } #[test] diff --git a/src/distribution/mod.rs b/src/distribution/mod.rs index be948218..47ee6ba2 100644 --- a/src/distribution/mod.rs +++ b/src/distribution/mod.rs @@ -27,6 +27,7 @@ pub use self::geometric::{Geometric, GeometricError}; pub use self::gumbel::{Gumbel, GumbelError}; pub use self::hypergeometric::{Hypergeometric, HypergeometricError}; pub use self::inverse_gamma::{InverseGamma, InverseGammaError}; +pub use self::inverse_gaussian::{InverseGaussian, InverseGaussianError}; pub use self::laplace::{Laplace, LaplaceError}; pub use self::levy::{Levy, LevyError}; pub use self::log_normal::{LogNormal, LogNormalError}; @@ -70,6 +71,7 @@ mod hypergeometric; #[macro_use] mod internal; mod inverse_gamma; +mod inverse_gaussian; mod laplace; mod levy; mod log_normal; @@ -133,6 +135,26 @@ pub trait ContinuousCDF: Min + Max { /// ``` fn cdf(&self, x: K) -> T; + /// Returns the natural logarithm of the cumulative distribution function + /// at `x`. + /// + /// The default implementation is `self.cdf(x).ln()`, which loses the left + /// tail once `cdf(x)` underflows (below ~1e-308). Distributions override it + /// where a tail-accurate form exists; overrides stay finite far past the + /// underflow point, e.g. `Normal::ln_cdf(-100.0)` is about -5005.5 where + /// the default is `-inf`. + fn ln_cdf(&self, x: K) -> T { + self.cdf(x).ln() + } + + /// Returns the natural logarithm of the survival function at `x`. + /// + /// The default implementation is `self.sf(x).ln()`; see [`Self::ln_cdf`] + /// for the tail-accuracy caveat and override behaviour. + fn ln_sf(&self, x: K) -> T { + self.sf(x).ln() + } + /// Returns the survival function calculated /// at `x` for a given distribution. May panic depending /// on the implementor. @@ -248,6 +270,24 @@ pub trait DiscreteCDF: /// ``` fn cdf(&self, x: K) -> T; + /// Returns the natural logarithm of the cumulative distribution function + /// at `x`. + /// + /// The default implementation is `self.cdf(x).ln()`, which loses the left + /// tail once `cdf(x)` underflows (below ~1e-308); distributions override it + /// where a tail-accurate form exists. + fn ln_cdf(&self, x: K) -> T { + self.cdf(x).ln() + } + + /// Returns the natural logarithm of the survival function at `x`. + /// + /// The default implementation is `self.sf(x).ln()`; see [`Self::ln_cdf`] + /// for the tail-accuracy caveat. + fn ln_sf(&self, x: K) -> T { + self.sf(x).ln() + } + /// Returns the survival function calculated at `x` for /// a given distribution. May panic depending on the implementor. /// diff --git a/src/distribution/multivariate_students_t.rs b/src/distribution/multivariate_students_t.rs index 2ad69880..cee52829 100644 --- a/src/distribution/multivariate_students_t.rs +++ b/src/distribution/multivariate_students_t.rs @@ -19,7 +19,7 @@ use nalgebra::{Cholesky, Const, DMatrix, Dim, DimMin, Dyn, OMatrix, OVector}; /// let mvs = MultivariateStudent::new(vec![0., 0.], vec![1., 0., 0., 1.], 4.).unwrap(); /// assert_eq!(mvs.mean().unwrap(), DVector::from_vec(vec![0., 0.])); /// assert_eq!(mvs.variance().unwrap(), DMatrix::from_vec(2, 2, vec![2., 0., 0., 2.])); -/// assert_eq!(mvs.pdf(&DVector::from_vec(vec![1., 1.])), 0.04715702017537655); +/// assert_eq!(mvs.pdf(&DVector::from_vec(vec![1., 1.])), 0.047157020175376395); /// ``` #[derive(Debug, Clone, PartialEq)] pub struct MultivariateStudent diff --git a/src/distribution/normal.rs b/src/distribution/normal.rs index ff65737a..f920b702 100644 --- a/src/distribution/normal.rs +++ b/src/distribution/normal.rs @@ -155,6 +155,18 @@ impl ContinuousCDF for Normal { sf_unchecked(x, self.mean, self.std_dev) } + /// Tail-accurate log of the cdf via a dedicated `ln erfc`; finite for + /// every representable `x` (`ln_cdf(-100)` is about -5005.5 where + /// `cdf(-100).ln()` is `-inf`). + fn ln_cdf(&self, x: f64) -> f64 { + ln_cdf_unchecked(x, self.mean, self.std_dev) + } + + /// Tail-accurate log of the survival function; see [`Self::ln_cdf`]. + fn ln_sf(&self, x: f64) -> f64 { + ln_sf_unchecked(x, self.mean, self.std_dev) + } + /// Calculates the inverse cumulative distribution function for the /// normal distribution at `x`. /// In other languages, such as R, this is known as the the quantile function. @@ -324,16 +336,128 @@ impl Continuous for Normal { } } +use crate::prec::{dekker_product_err, two_diff}; + +/// Low half of `sqrt(2)`: `sqrt(2) == SQRT_2 + SQRT_2_LO` to double-double +/// precision. +const SQRT_2_LO: f64 = -9.667293313452913e-17; + +/// Computes `0.5 * erfc((a - b) / (std_dev * sqrt(2)))`, compensating the +/// rounding of the argument in the tails. +/// +/// `erfc` amplifies a relative error `e` in its argument `t` by `2 * t^2`, so +/// the roundings in `(a - b) / (sigma * SQRT_2)` cost ~`4 t^2` ulps - about 80 +/// ulps by `t = 6`. For `|t| > 1.5` this recovers the argument's rounding +/// residual `delta` (subtraction, division, and the double-double value of +/// `sigma * sqrt(2)`) and applies the first-order correction +/// `erfc(t + delta) ~= erfc(t) + erfc'(t) * delta`, which brings the tail +/// back to `erfc`'s intrinsic ~2 ulp. +#[inline] +fn half_erfc(a: f64, b: f64, std_dev: f64) -> f64 { + let (d, d_err) = two_diff(a, b); + let s = std_dev * f64::consts::SQRT_2; + // An infinite `x` makes `d` infinite, and a `std_dev` near `f64::MAX` makes + // `s` infinite; `d / s` is then `inf / inf == NaN`. Both limits are + // well defined, so take them directly rather than returning NaN. + if d.is_nan() { + return f64::NAN; + } + if d.is_infinite() { + return if d > 0.0 { 0.0 } else { 1.0 }; + } + if !s.is_finite() { + // sigma so large the distribution is flat over any finite difference + return 0.5; + } + let t = d / s; + let half = 0.5 * erf::erfc(t); + // Skip the correction where it cannot help or cannot be formed safely: + // * `|t| <= 1.5`: the amplification is negligible and `erfc` is + // absolutely well conditioned, so this only costs an `exp`; + // * `|t| >= 30`: `erfc` has already saturated to exactly 0 or 2; + // * `s > 1e300`: the Dekker splits below multiply by `2^27 + 1`, which + // would overflow to `inf` and turn the whole result into NaN. + // A NaN `t` fails the range test and so falls through here as well. + if !(1.5..30.0).contains(&t.abs()) || s > 1e300 { + return half; + } + // exact residual of the division: d - t * s, via a Dekker product + let p = t * s; + let r = (d - p) - dekker_product_err(t, s, p); + // s itself is short of sigma * sqrt(2) by its product rounding error and + // by sigma * SQRT_2_LO + let s_err = dekker_product_err(std_dev, f64::consts::SQRT_2, s) + std_dev * SQRT_2_LO; + let delta = (r + d_err - t * s_err) / s; + // erfc'(t) = -2/sqrt(pi) * exp(-t^2); halved for the 0.5 * erfc form + half - 0.5 * f64::consts::FRAC_2_SQRT_PI * (-t * t).exp() * delta +} + +/// Log-domain companion of [`half_erfc`]: computes +/// `ln(0.5 * erfc((a - b) / (std_dev * sqrt 2)))`, staying finite far past the +/// point where the probability itself underflows (`ln_cdf(-40)` for the +/// standard normal is about -804.6 where `cdf(-40).ln()` is `-inf`). +/// +/// The same argument-rounding compensation applies, but additively: +/// `ln erfc(t + delta) ~= ln erfc(t) + (d ln erfc / dt) delta`. +fn ln_half_erfc(a: f64, b: f64, std_dev: f64) -> f64 { + let (d, d_err) = two_diff(a, b); + let s = std_dev * f64::consts::SQRT_2; + if d.is_nan() { + return f64::NAN; + } + if d.is_infinite() { + return if d > 0.0 { f64::NEG_INFINITY } else { 0.0 }; + } + if !s.is_finite() { + // sigma so large the distribution is flat over any finite difference + return -f64::consts::LN_2; + } + let t = d / s; + let base = erf::ln_erfc(t) - f64::consts::LN_2; + if !(1.5..1e300).contains(&t) || s > 1e300 { + // t < 1.5: amplification negligible (and for t < 0, erfc -> 2 with a + // vanishing derivative). Bounds also keep the Dekker splits below away + // from arguments that would overflow them. + return base; + } + // residuals of the subtraction, the division, and sigma * sqrt(2), exactly + // as in `half_erfc` + let p = t * s; + let r = (d - p) - dekker_product_err(t, s, p); + let s_err = dekker_product_err(std_dev, f64::consts::SQRT_2, s) + std_dev * SQRT_2_LO; + let delta = (r + d_err - t * s_err) / s; + // d/dt ln erfc = -2/sqrt(pi) e^(-t^2) / erfc(t); past the representable + // range of erfc use its asymptotic -(2t + 1/t) + let dln = if t < 26.0 { + -f64::consts::FRAC_2_SQRT_PI * (-t * t).exp() / erf::erfc(t) + } else { + -(2.0 * t + 1.0 / t) + }; + base + dln * delta +} + /// performs an unchecked cdf calculation for a normal distribution /// with the given mean and standard deviation at x pub fn cdf_unchecked(x: f64, mean: f64, std_dev: f64) -> f64 { - 0.5 * erf::erfc((mean - x) / (std_dev * f64::consts::SQRT_2)) + half_erfc(mean, x, std_dev) } /// performs an unchecked sf calculation for a normal distribution /// with the given mean and standard deviation at x pub fn sf_unchecked(x: f64, mean: f64, std_dev: f64) -> f64 { - 0.5 * erf::erfc((x - mean) / (std_dev * f64::consts::SQRT_2)) + half_erfc(x, mean, std_dev) +} + +/// performs an unchecked log-cdf calculation for a normal distribution +/// with the given mean and standard deviation at x +pub fn ln_cdf_unchecked(x: f64, mean: f64, std_dev: f64) -> f64 { + ln_half_erfc(mean, x, std_dev) +} + +/// performs an unchecked log-sf calculation for a normal distribution +/// with the given mean and standard deviation at x +pub fn ln_sf_unchecked(x: f64, mean: f64, std_dev: f64) -> f64 { + ln_half_erfc(x, mean, std_dev) } /// performs an unchecked pdf calculation for a normal distribution @@ -508,6 +632,57 @@ mod tests { test_exact(-5.0, f64::INFINITY, f64::NEG_INFINITY, ln_pdf(100.0)); } + /// The tail compensation in `half_erfc` splits its arguments with Dekker's + /// algorithm, which multiplies by `2^27 + 1`; for extreme `std_dev` or `x` + /// that overflowed to `inf` and made the whole result NaN. cdf/sf must stay + /// in `[0, 1]` for every representable input. + #[test] + fn test_cdf_sf_extreme_arguments_are_not_nan() { + let cases = [ + (0.0f64, 1.0f64), + (0.0, f64::MIN_POSITIVE), + (0.0, 1e-300), + (0.0, 1e300), + (0.0, f64::MAX), + (1e300, 1e-100), + (-1e300, 1e300), + ]; + for (mean, sd) in cases { + let d = Normal::new(mean, sd).unwrap(); + for x in [-f64::MAX, -1e300, -1e10, -1.0, 0.0, 1.0, 1e10, 1e300, f64::MAX] { + let c = d.cdf(x); + let sf = d.sf(x); + assert!( + (0.0..=1.0).contains(&c), + "cdf out of range for N({mean:e},{sd:e}) at x={x:e}: {c}" + ); + assert!( + (0.0..=1.0).contains(&sf), + "sf out of range for N({mean:e},{sd:e}) at x={x:e}: {sf}" + ); + } + assert_eq!(d.cdf(f64::NEG_INFINITY), 0.0); + assert_eq!(d.cdf(f64::INFINITY), 1.0); + } + } + + /// Far-tail cdf/sf: without the argument compensation in `half_erfc` these + /// carried ~40-80 ulp from `erfc`'s `2 t^2` amplification of the argument + /// roundings. References are mpmath at 40 significant digits. + #[test] + fn test_cdf_sf_far_tail() { + let n = Normal::standard(); + prec::assert_relative_eq!(n.cdf(-8.0), 6.220960574271784123516e-16, epsilon = 0.0, max_relative = 1e-15); + prec::assert_relative_eq!(n.cdf(-6.0), 9.865876450376981407009e-10, epsilon = 0.0, max_relative = 1e-15); + prec::assert_relative_eq!(n.sf(8.0), 6.220960574271784123516e-16, epsilon = 0.0, max_relative = 1e-15); + prec::assert_relative_eq!(n.sf(6.0), 9.865876450376981407009e-10, epsilon = 0.0, max_relative = 1e-15); + // non-standard parameters: the `x - mean` subtraction rounds too and + // is compensated by the two_diff + let m = Normal::new(0.3, 1.7).unwrap(); + prec::assert_relative_eq!(m.cdf(-13.3), 6.220960574271752778279e-16, epsilon = 0.0, max_relative = 1e-14); + prec::assert_relative_eq!(m.sf(13.9), 6.220960574271762676775e-16, epsilon = 0.0, max_relative = 1e-14); + } + #[test] fn test_cdf() { let cdf = |arg: f64| move |x: Normal| x.cdf(arg); diff --git a/src/distribution/pareto.rs b/src/distribution/pareto.rs index 491047d5..be3b644a 100644 --- a/src/distribution/pareto.rs +++ b/src/distribution/pareto.rs @@ -171,6 +171,25 @@ impl ContinuousCDF for Pareto { } } + /// Tail-accurate log of the cdf: `ln1p(-(scale / x)^shape)`. + fn ln_cdf(&self, x: f64) -> f64 { + if x < self.scale { + f64::NEG_INFINITY + } else { + (-(self.scale / x).powf(self.shape)).ln_1p() + } + } + + /// Exact log of the survival function: + /// `ln sf = shape * ln(scale / x)`, finite long after `sf` underflows. + fn ln_sf(&self, x: f64) -> f64 { + if x < self.scale { + 0.0 + } else { + self.shape * (self.scale / x).ln() + } + } + /// Calculates the inverse cumulative distribution function for the Pareto /// distribution at `x` /// diff --git a/src/distribution/poisson.rs b/src/distribution/poisson.rs index fc9421ff..0bab9bc5 100644 --- a/src/distribution/poisson.rs +++ b/src/distribution/poisson.rs @@ -145,6 +145,17 @@ impl DiscreteCDF for Poisson { fn sf(&self, x: u64) -> f64 { gamma::gamma_lr(x as f64 + 1.0, self.lambda) } + + /// Tail-accurate log of the cdf via [`gamma::ln_gamma_ur`]; finite far + /// past where `cdf` underflows. + fn ln_cdf(&self, x: u64) -> f64 { + gamma::ln_gamma_ur(x as f64 + 1.0, self.lambda) + } + + /// Tail-accurate log of the survival function via [`gamma::ln_gamma_lr`]. + fn ln_sf(&self, x: u64) -> f64 { + gamma::ln_gamma_lr(x as f64 + 1.0, self.lambda) + } } impl Min for Poisson { @@ -276,7 +287,7 @@ impl Discrete for Poisson { /// /// where `λ` is the rate fn pmf(&self, x: u64) -> f64 { - (-self.lambda + x as f64 * self.lambda.ln() - factorial::ln_factorial(x)).exp() + self.ln_pmf(x).exp() } /// Calculates the log probability mass function for the poisson @@ -291,7 +302,19 @@ impl Discrete for Poisson { /// /// where `λ` is the rate fn ln_pmf(&self, x: u64) -> f64 { - -self.lambda + x as f64 * self.lambda.ln() - factorial::ln_factorial(x) + let k = x as f64; + if x == 0 { + return -self.lambda; + } + if k < gamma::STIRLING_SERIES_MIN { + // `ln_factorial` is exact from its table here and every term is + // small, so the direct form loses nothing + return -self.lambda + k * self.lambda.ln() - factorial::ln_factorial(x); + } + // Saddle-point form (Loader 2000): the direct expression is a + // difference of terms that each grow like `x ln x` while the result + // stays `O(1)`, which cost ~1e-11 relative at lambda = 1e4. + -gamma::stirling_delta(k) - gamma::bd0(k, self.lambda) - 0.5 * (f64::consts::TAU * k).ln() } } @@ -421,6 +444,24 @@ mod tests { test_exact(10.8, u64::MAX, max); } + /// Large-lambda pmf: the saddle-point form (`gamma::bd0` / + /// `gamma::stirling_delta`) replaced `exp(-lam + x ln lam - ln x!)`, whose + /// terms each grow like `x ln x` while the result stays `O(1)`. References + /// are mpmath at 40 significant digits; the old form was ~650 ulp median and + /// 1.4e7 ulp worst over this range. + #[test] + fn test_pmf_large_lambda_saddle_point() { + let pmf = |arg: u64| move |x: Poisson| x.pmf(arg); + test_relative(10000.0, 0.0039893895589628256487, pmf(10000)); + test_relative(1000000.0, 0.0003989422471562440297, pmf(1000000)); + test_relative(1000000.0, 0.00024189010120174141723, pmf(1001000)); + // ln_pmf agrees with ln(pmf) where the pmf is comfortably representable + for (lam, k) in [(10000.0f64, 10000u64), (1e6, 1001000)] { + let d = create_ok(lam); + crate::prec::assert_relative_eq!(d.ln_pmf(k), d.pmf(k).ln(), epsilon = 0.0, max_relative = 1e-14); + } + } + #[test] fn test_pmf() { let pmf = |arg: u64| move |x: Poisson| x.pmf(arg); diff --git a/src/distribution/weibull.rs b/src/distribution/weibull.rs index 5cb247e0..4e4cbd09 100644 --- a/src/distribution/weibull.rs +++ b/src/distribution/weibull.rs @@ -169,6 +169,25 @@ impl ContinuousCDF for Weibull { } } + /// Tail-accurate log of the cdf. + fn ln_cdf(&self, x: f64) -> f64 { + if x < 0.0 { + f64::NEG_INFINITY + } else { + (-(-x.powf(self.shape) * self.scale_pow_shape_inv).exp_m1()).ln() + } + } + + /// Exact log of the survival function: + /// `ln sf = -(x / scale)^shape`, finite long after `sf` underflows. + fn ln_sf(&self, x: f64) -> f64 { + if x < 0.0 { + 0.0 + } else { + -x.powf(self.shape) * self.scale_pow_shape_inv + } + } + /// Calculates the inverse cumulative distribution function for the weibull /// distribution at `x` /// diff --git a/src/function/beta.rs b/src/function/beta.rs index 970e76ed..a22a3176 100644 --- a/src/function/beta.rs +++ b/src/function/beta.rs @@ -119,62 +119,97 @@ pub fn checked_beta_inc(a: f64, b: f64, x: f64) -> Result { checked_beta_reg(a, b, x).and_then(|x| checked_beta(a, b).map(|y| x * y)) } -/// Computes the regularized lower incomplete beta function -/// `I_x(a,b) = 1/Beta(a,b) * int(t^(a-1)*(1-t)^(b-1), t=0..x)` -/// `a > 0`, `b > 0`, `1 >= x >= 0` where `a` is the first beta parameter, -/// `b` is the second beta parameter, and `x` is the upper limit of the -/// integral. +/// Computes the natural logarithm of the regularized lower incomplete beta +/// function, `ln I_x(a, b)`, staying finite far past the point where `I_x` +/// itself underflows (below ~1e-308). +/// +/// `beta_reg` computes `exp(ln prefix) * fraction` and so saturates to 0 once +/// the prefix passes the underflow limit; the log-domain form has no cliff. +/// Used by `ln_cdf`/`ln_sf` implementations on the beta family +/// (`Binomial::ln_sf(x)` is `ln_beta_reg(x+1, n-x, p)` and stays finite for +/// p-values far beyond representable). +/// +/// Accuracy: the deep tail (where the untransformed branch is taken) is +/// limited by the prefix, a few 1e-16 *absolute* in the log - i.e. the +/// *relative* error of the underlying probability. Near the centre the result +/// is `ln1p(-v)` of a well-conditioned `v` and tracks `beta_reg` itself. /// /// # Panics /// /// if `a <= 0.0`, `b <= 0.0`, `x < 0.0`, or `x > 1.0` -pub fn beta_reg(a: f64, b: f64, x: f64) -> f64 { - checked_beta_reg(a, b, x).unwrap() +pub fn ln_beta_reg(a: f64, b: f64, x: f64) -> f64 { + checked_ln_beta_reg(a, b, x).unwrap() } -/// Computes the regularized lower incomplete beta function -/// `I_x(a,b) = 1/Beta(a,b) * int(t^(a-1)*(1-t)^(b-1), t=0..x)` -/// `a > 0`, `b > 0`, `1 >= x >= 0` where `a` is the first beta parameter, -/// `b` is the second beta parameter, and `x` is the upper limit of the -/// integral. +/// Non-panicking variant of [`ln_beta_reg`]. /// /// # Errors /// /// if `a <= 0.0`, `b <= 0.0`, `x < 0.0`, or `x > 1.0` -pub fn checked_beta_reg(a: f64, b: f64, x: f64) -> Result { +pub fn checked_ln_beta_reg(a: f64, b: f64, x: f64) -> Result { if a <= 0.0 { return Err(BetaFuncError::ANotGreaterThanZero); } - if b <= 0.0 { return Err(BetaFuncError::BNotGreaterThanZero); } - if !(0.0..=1.0).contains(&x) { return Err(BetaFuncError::XOutOfRange); } + if x == 0.0 { + return Ok(f64::NEG_INFINITY); + } + if x == 1.0 { + return Ok(0.0); + } - let bt = if x == 0.0 || crate::prec::ulps_eq!(x, 1.0, epsilon = MODULE_EPS) { - 0.0 + let symm_transform = beta_reg_symm_transform(a, b, x); + let max_iters = ((8.0 * a.min(b).cbrt()) as u32).clamp(140, 1_000_000); + + let (a, b, x) = if symm_transform { + (b, a, 1.0 - x) } else { - (gamma::ln_gamma(a + b) - gamma::ln_gamma(a) - gamma::ln_gamma(b) - + a * x.ln() - + b * (1.0 - x).ln()) - .exp() + (a, b, x) }; - let symm_transform = x >= (a + 1.0) / (a + b + 2.0); - let eps = prec::F64_PREC; - let fpmin = f64::MIN_POSITIVE / eps; - let mut a = a; - let mut b = b; - let mut x = x; + // ln(bt * h / a): every piece stays representable long after `bt` itself + // underflows. + let ln_v = ln_beta_prefix(a, b, x) + (beta_reg_fraction(a, b, x, max_iters) / a).ln(); if symm_transform { - let swap = a; - x = 1.0 - x; - a = b; - b = swap; + // I = 1 - v; `exp` may underflow to 0, in which case ln I correctly + // saturates to -0. Clamp guards the truncated-recurrence regime where + // v could exceed 1 (see `finish`). + Ok((-ln_v.min(0.0).exp()).ln_1p()) + } else { + // a probability's log is never positive; > 0 only ever arises from the + // truncated-recurrence regime + Ok(ln_v.min(0.0)) } +} + +/// Whether `I_x(a, b)` should be computed via the symmetry +/// `I_x(a, b) = 1 - I_{1-x}(b, a)`; true when `x` is past the distribution's +/// bulk, where the direct continued fraction converges poorly. +fn beta_reg_symm_transform(a: f64, b: f64, x: f64) -> bool { + let denom = a + b + 2.0; + if denom.is_finite() { + x >= (a + 1.0) / denom + } else { + // `a + b` overflowed, which would collapse the threshold to zero and + // send every `x` down the transformed branch. Scaling numerator and + // denominator by `max(a, b)` keeps the ratio exact enough to compare. + let m = a.max(b); + x >= (a / m + 1.0 / m) / (a / m + b / m + 2.0 / m) + } +} + +/// The Lentz continued fraction for `I_x(a, b) * a / (x^a (1-x)^b / B(a, b))`, +/// evaluated after the symmetry transform. Shared by [`checked_beta_reg`] and +/// [`checked_ln_beta_reg`]; the value is O(1) and independent of the prefix, +/// which is what makes the log-domain variant possible. +fn beta_reg_fraction(a: f64, b: f64, x: f64, max_iters: u32) -> f64 { + let eps = prec::F64_PREC; + let fpmin = f64::MIN_POSITIVE / eps; let qab = a + b; let qap = a + 1.0; @@ -188,7 +223,7 @@ pub fn checked_beta_reg(a: f64, b: f64, x: f64) -> Result { d = 1.0 / d; let mut h = d; - for m in 1..141 { + for m in 1..=max_iters { let m = f64::from(m); let m2 = m * 2.0; let mut aa = m * (b - m) * x / ((qam + m2) * (a + m2)); @@ -223,18 +258,205 @@ pub fn checked_beta_reg(a: f64, b: f64, x: f64) -> Result { h *= del; if (del - 1.0).abs() <= eps { - return if symm_transform { - Ok(1.0 - bt * h / a) - } else { - Ok(bt * h / a) - }; + break; } } + h +} + +/// Computes the regularized lower incomplete beta function +/// `I_x(a,b) = 1/Beta(a,b) * int(t^(a-1)*(1-t)^(b-1), t=0..x)` +/// `a > 0`, `b > 0`, `1 >= x >= 0` where `a` is the first beta parameter, +/// `b` is the second beta parameter, and `x` is the upper limit of the +/// integral. +/// +/// # Panics +/// +/// if `a <= 0.0`, `b <= 0.0`, `x < 0.0`, or `x > 1.0` +pub fn beta_reg(a: f64, b: f64, x: f64) -> f64 { + checked_beta_reg(a, b, x).unwrap() +} + +/// `ln(x^a (1-x)^b / Beta(a, b))`, the prefix of the incomplete beta functions. +/// +/// Written out, this is +/// `ln_gamma(a+b) - ln_gamma(a) - ln_gamma(b) + a ln x + b ln(1-x)`, where the +/// terms grow like `(a + b) ln(a + b)` while the sum stays `O(ln(a + b))`. With +/// `n = a + b` the saddle-point form is +/// +/// ```text +/// = -bd0(a, n x) - bd0(b, n (1-x)) +/// + stirling_delta(n) - stirling_delta(a) - stirling_delta(b) +/// + ln(a b / (2 pi n)) / 2 +/// ``` +/// +/// Every piece is `O(1)` (the two `bd0` terms grow only as the true `-ln` of +/// the prefix does), so this is accurate to a few `1e-16` absolute where the +/// old form lost `~1e-10` at `a = b = 1e4`. +/// +/// For small parameters the direct form is kept: there is no cancellation left +/// to remove (every term is already `O(1)`), while `stirling_delta` would need +/// up to 16 recurrence steps per argument and so contributes more rounding than +/// it saves. +fn ln_beta_prefix(a: f64, b: f64, x: f64) -> f64 { + let y = 1.0 - x; + if a.max(b) < gamma::STIRLING_SERIES_MIN { + return gamma::ln_gamma(a + b) - gamma::ln_gamma(a) - gamma::ln_gamma(b) + + a * x.ln() + + b * y.ln(); + } + // `bd0` is sensitive to the last half-ulp of its mean argument, so `n` and + // the two products are carried as double-doubles; see `gamma::bd0_dd`. + let (n, n_lo) = prec::two_sum(a, b); + let (y_hi, y_lo) = prec::two_diff(1.0, x); + let nx = n * x; + let nx_lo = prec::dekker_product_err(n, x, nx) + n_lo * x; + let ny = n * y_hi; + let ny_lo = prec::dekker_product_err(n, y_hi, ny) + n * y_lo + n_lo * y_hi; + + // `a / n` and `b / TAU` are each individually representable, unlike + // `a * b / (2 pi n)`, which can overflow for large parameters + let scale = 0.5 * ((a / n).ln() + (b / f64::consts::TAU).ln()); + scale - gamma::bd0_dd(a, nx, nx_lo) - gamma::bd0_dd(b, ny, ny_lo) + gamma::stirling_delta(n) + - gamma::stirling_delta(a) + - gamma::stirling_delta(b) +} + +/// Computes the regularized lower incomplete beta function +/// `I_x(a,b) = 1/Beta(a,b) * int(t^(a-1)*(1-t)^(b-1), t=0..x)` +/// `a > 0`, `b > 0`, `1 >= x >= 0` where `a` is the first beta parameter, +/// `b` is the second beta parameter, and `x` is the upper limit of the +/// integral. +/// +/// # Remarks +/// +/// The leading factor is evaluated by the saddle-point decomposition in +/// [`ln_beta_prefix`], which keeps every intermediate `O(1)` however large +/// `a + b` becomes. Measured against the exact identity `I_{1/2}(a, a) == 1/2`, +/// the relative error is: +/// +/// ```text +/// min(a, b) <= 1e7 2e-13 (1e-15 at 1e4 and below) +/// min(a, b) <= 1e13 3e-10 +/// min(a, b) <= 1e16 3e-7 +/// beyond unreliable; the result is still clamped to [0, 1] +/// ``` +/// +/// The degradation past `1e7` is the continued fraction accumulating its own +/// rounding over millions of iterations, not the prefix. Evaluation is bounded +/// at roughly 10 ms in the worst case. +/// +/// Values deep in the tails carry more *relative* error - a few hundred ulp, +/// since the prefix is recovered as `exp(ln prefix)` and `|ln prefix|` is large +/// there - but their absolute error stays far below the smallest value of +/// interest. +/// +/// # Errors +/// +/// if `a <= 0.0`, `b <= 0.0`, `x < 0.0`, or `x > 1.0` +pub fn checked_beta_reg(a: f64, b: f64, x: f64) -> Result { + if a <= 0.0 { + return Err(BetaFuncError::ANotGreaterThanZero); + } + + if b <= 0.0 { + return Err(BetaFuncError::BNotGreaterThanZero); + } + + if !(0.0..=1.0).contains(&x) { + return Err(BetaFuncError::XOutOfRange); + } + + // `I_0(a, b) == 0` and `I_1(a, b) == 1` for every `a` and `b`. Handling the + // endpoints here keeps them independent of the symmetry test below, which + // otherwise mapped `x == 0` to `1.0` once `a + b` overflowed. + if x == 0.0 { + return Ok(0.0); + } + if x == 1.0 { + return Ok(1.0); + } + + let bt = if x == 0.0 || crate::prec::ulps_eq!(x, 1.0, epsilon = MODULE_EPS) { + 0.0 + } else { + ln_beta_prefix(a, b, x).exp() + }; + let symm_transform = beta_reg_symm_transform(a, b, x); + + // The result is `bt * h / a` with the continued fraction `h` of order one, + // so a prefix that has underflowed pins the answer at the corresponding + // endpoint. Returning here also avoids forming `0.0 * h`, which is NaN + // whenever the recurrence overflowed (e.g. `beta_reg(1e300, 1e-300, 0.5)`), + // and skips the recurrence entirely in the regime where the distribution + // has concentrated to a step function. + if bt == 0.0 { + return Ok(if symm_transform { 1.0 } else { 0.0 }); + } + + // Fallback for the regime where the recurrence below is truncated by + // `max_iters` and can degenerate to a non-finite value: the distribution has + // concentrated around its mean, so this is the limiting step function. It is + // only consulted when the recurrence produced something unusable - see + // `finish`. + let saturated = { + let mean = a / (a + b); + if x < mean { + 0.0 + } else if x > mean { + 1.0 + } else { + 0.5 + } + }; + + // Iterations the Lentz recurrence below needs before `del` settles. It is + // slowest at the centre of the distribution (`x ~ a / (a + b)`), where the + // worst case over `x` grows like `5 * min(a, b).cbrt()`; the bound here + // carries headroom on top of that. A fixed bound of 140 used to be applied + // regardless of `a` and `b`, which silently truncated the recurrence and + // returned a badly wrong value once `min(a, b)` passed ~1.5e4 (for example + // `I_0.5(1e6, 1e6)` came back as 0.491 instead of 0.5). The loop still + // stops as soon as it converges, so the typical few-dozen-iteration case is + // unchanged. + // + // The upper clamp bounds the work at roughly 10 ms. Past `min(a, b) ~ 1e15` + // the recurrence needs more iterations than that, but it has also stopped + // being able to deliver an accurate answer (its own rounding accumulates + // over millions of steps), so spending longer buys nothing - see the + // accuracy note on `checked_beta_reg`. + let max_iters = ((8.0 * a.min(b).cbrt()) as u32).clamp(140, 1_000_000); + + let mut a = a; + let mut b = b; + let mut x = x; if symm_transform { - Ok(1.0 - bt * h / a) + let swap = a; + x = 1.0 - x; + a = b; + b = swap; + } + + let h = beta_reg_fraction(a, b, x, max_iters); + Ok(finish(symm_transform, bt, h, a, saturated)) +} + +/// Assembles `I_x(a, b)` from the prefix and continued fraction, keeping the +/// result inside `[0, 1]`. +/// +/// Neither guard engages while the recurrence converges. Once it is truncated by +/// `max_iters` (only for `min(a, b)` past ~1e16) the raw value can drift outside +/// the unit interval - `-2.56` at `a = b = 1e20` - or become non-finite +/// entirely, and callers such as `Binomial::cdf` are contractually +/// probabilities. +fn finish(symm_transform: bool, bt: f64, h: f64, a: f64, saturated: f64) -> f64 { + let v = bt * h / a; + let v = if symm_transform { 1.0 - v } else { v }; + if v.is_finite() { + v.clamp(0.0, 1.0) } else { - Ok(bt * h / a) + saturated } } @@ -646,6 +868,81 @@ mod tests { assert!(checked_beta_reg(1.0, 1.0, 2.0).is_err()); } + /// `I_{1/2}(a, a) == 1/2` exactly, by the symmetry of the `Beta(a, a)` + /// density. That point is also where the continued fraction converges most + /// slowly, so it pins the iteration bound. With the old fixed bound of 140 + /// iterations this returned 0.49999969 at `a = 1e5`, 0.49121973 at `a = 1e6` + /// and 0.21285001 at `a = 1e7`. + #[test] + fn test_beta_reg_symmetric_midpoint_large_parameters() { + // The saddle-point prefix (`ln_beta_prefix`) took this from `6e-7` at + // `a = 1e5` and `5.7e-1` at `a = 1e7` down to the `1e-13` level. + for a in [1e2, 1e3, 1e4, 1e5, 1e6, 1e7] { + prec::assert_relative_eq!( + beta_reg(a, a, 0.5), + 0.5, + epsilon = 0.0, + max_relative = 1e-12 + ); + } + } + + /// `beta_reg` is a probability and must stay in `[0, 1]` and finite for + /// every valid input, including parameter ratios extreme enough to + /// over/underflow the intermediate quantities. Before the `bd0` ratio guard + /// and the `[0, 1]` clamp, this grid produced NaN (`a = 1e300, b = 1e-300`), + /// a silent `1.0` where the answer was `0` (`a = 1e100, b = 1e-300`), and + /// `-2.56` (`a = b = 1e20`). + #[test] + fn test_beta_reg_extreme_parameters_stay_a_probability() { + let params = [ + 1e-308f64, 1e-300, 1e-100, 1e-8, 0.5, 1.0, 20.0, 1e8, 1e20, 1e100, 1e300, 1e308, + ]; + for &a in ¶ms { + for &b in ¶ms { + // Beyond a ~1e300 parameter ratio the Lentz recurrence bottoms + // out in its own `fpmin` guards for `x` within an ulp of the + // mode, and returns NaN. That is pre-existing and unreachable + // from any distribution in the crate (`Binomial` is bounded by + // `n <= u64::MAX`), so it is excluded rather than papered over + // with a plausible-looking wrong value. + if a.max(b) / a.min(b) > 1e200 { + continue; + } + for x in [0.0f64, 1e-300, 0.25, 0.5, 0.75, 1.0] { + let v = beta_reg(a, b, x); + assert!( + v.is_finite() && (0.0..=1.0).contains(&v), + "beta_reg({a:e}, {b:e}, {x}) = {v}" + ); + } + // monotone in x, and pinned at the endpoints + assert_eq!(beta_reg(a, b, 0.0), 0.0, "beta_reg({a:e},{b:e},0)"); + assert_eq!(beta_reg(a, b, 1.0), 1.0, "beta_reg({a:e},{b:e},1)"); + } + } + } + + /// `I_x(a, b) + I_{1-x}(b, a) == 1` for every valid `a`, `b`, `x`. The two + /// sides truncate differently, so a prematurely stopped continued fraction + /// breaks the identity. + #[test] + fn test_beta_reg_complement_identity_large_parameters() { + for (a, b) in [ + (1e5, 1e5), + (1e6, 1e6), + (1e7, 1e7), + (1e6, 1e3), + (1e3, 1e6), + (2e4, 3e4), + ] { + for x in [0.1, 0.25, 0.5, 0.5 + 1e-9, 0.75, 0.9] { + let lhs = beta_reg(a, b, x) + beta_reg(b, a, 1.0 - x); + prec::assert_abs_diff_eq!(lhs, 1.0, epsilon = 1e-7); + } + } + } + #[test] fn test_error_is_sync_send() { fn assert_sync_send() {} diff --git a/src/function/erf.rs b/src/function/erf.rs index 63238712..e912f261 100644 --- a/src/function/erf.rs +++ b/src/function/erf.rs @@ -1,6 +1,7 @@ //! Provides the [error](https://en.wikipedia.org/wiki/Error_function) and //! related functions +use crate::consts; use crate::function::evaluate; use core::f64; #[cfg(not(feature = "std"))] @@ -51,6 +52,107 @@ pub fn erfc(x: f64) -> f64 { } } +/// The `1 - 1/(2z^2) + 3/(4z^4) - 15/(8z^6)` correction of the asymptotic +/// expansion `erfc(z) ~ exp(-z^2) / (z sqrt(pi)) * (...)`, returned as the +/// series *minus one* so callers can use `ln_1p`. Only used for `z >= 110`, +/// where the first dropped term is ~1e-16 of the correction. +fn erfc_asymptotic_correction(z: f64) -> f64 { + let inv2 = (z * z).recip(); + -0.5 * inv2 + 0.75 * inv2 * inv2 - 1.875 * inv2 * inv2 * inv2 +} + +/// `erfcx` calculates the scaled complementary error function +/// `exp(x^2) erfc(x)`. +/// +/// Unlike `erfc`, this does not underflow: it decays only like +/// `1 / (x sqrt(pi))`, so it stays useful across the whole positive axis where +/// `erfc` has long since become zero. For `x` below about -26.6 the `exp(x^2)` +/// factor overflows and the result is `+inf`; use [`ln_erfcx`] there. +pub fn erfcx(x: f64) -> f64 { + if x.is_nan() { + return f64::NAN; + } + if x >= 0.5 { + if x < 110.0 { + // erfc(z) = exp(-z^2)/z * (b + r), so erfcx(z) = (b + r)/z with no + // exponential involved at all + let (r, b) = erfc_fraction(x); + return (b + r) / x; + } + return (1.0 + erfc_asymptotic_correction(x)) * (0.5 * f64::consts::FRAC_2_SQRT_PI) / x; + } + // |x| < 0.5, or negative: `exp(x^2)` is harmless up to x^2 = 709 + (x * x).exp() * erfc(x) +} + +/// `ln_erfcx` calculates the natural logarithm of the scaled complementary +/// error function, `x^2 + ln erfc(x)`. +/// +/// Finite over the entire range of `f64`, unlike both `erfc` (which underflows) +/// and `erfcx` (which overflows for very negative `x`). Differences of this +/// function are the well-conditioned way to form ratios of two `erfc` values: +/// the `x^2` terms that would otherwise dominate cancel analytically. +pub fn ln_erfcx(x: f64) -> f64 { + if x.is_nan() { + return f64::NAN; + } + if x >= 0.5 { + if x < 110.0 { + let (r, b) = erfc_fraction(x); + return ((b + r) / x).ln(); + } + return -x.ln() - 0.5 * consts::LN_PI + erfc_asymptotic_correction(x).ln_1p(); + } + x * x + ln_erfc(x) +} + +/// `ln_erfc` calculates the natural logarithm of the complementary error +/// function at `x`, staying finite far past the point where `erfc` itself +/// underflows (`x ~ 27`): `ln_erfc(100)` is about -10004.14, and results remain +/// finite until `x^2` overflows near `x = 1.3e154`. +/// +/// Accuracy tracks `erfc` where both are representable (a few ulp) because it +/// reuses the same interval machinery; past the underflow point it continues +/// with the asymptotic series. +pub fn ln_erfc(x: f64) -> f64 { + if x.is_nan() { + return f64::NAN; + } + if x == f64::INFINITY { + return f64::NEG_INFINITY; + } + if x < 0.5 { + // erfc = 1 - erf with erf(x) <= erf(0.5) ~ 0.52 (and negative for + // x < 0, where erfc -> 2), so ln_1p is accurate on this whole range + return (-erf(x)).ln_1p(); + } + if x < 110.0 { + // erfc(z) = exp(-z^2) / z * (b + r); take logs. The rounding error of + // the square is recovered exactly (Dekker), and enters the log-domain + // result additively rather than through `exp`. + let sq = x * x; + let split = 134_217_729.0 * x; // 2^27 + 1; x < 110, cannot overflow + let x_hi = split - (split - x); + let x_lo = x - x_hi; + let err = ((x_hi * x_hi - sq) + 2.0 * x_hi * x_lo) + x_lo * x_lo; + let (r, b) = erfc_fraction(x); + return -sq - err + ((b + r) / x).ln(); + } + // Asymptotic series: erfc(z) ~ exp(-z^2) / (z sqrt(pi)) * + // (1 - 1/(2 z^2) + 3/(4 z^4) - 15/(8 z^6) + ...); at z = 110 the dropped + // term is ~1e-16 of the correction and utterly negligible in the total. + let series = erfc_asymptotic_correction(x); + let sq = x * x; + if !sq.is_finite() { + return f64::NEG_INFINITY; + } + let split = 134_217_729.0 * x; + let x_hi = split - (split - x); + let x_lo = x - x_hi; + let err = ((x_hi * x_hi - sq) + 2.0 * x_hi * x_lo) + x_lo * x_lo; + -sq - err - x.ln() - 0.5 * consts::LN_PI + series.ln_1p() +} + /// `erfc_inv` calculates the complementary inverse /// error function at `x`. pub fn erfc_inv(x: f64) -> f64 { @@ -569,6 +671,88 @@ const ERF_INV_IMPL_GD: &[f64] = &[ 0.231558608310259605225e-11, ]; +/// Selects the rational-approximation interval for `erfc(z) = exp(-z^2)/z * +/// (b + r)` and returns `(r, b)`. Requires `0.5 <= z < 110`. Shared by +/// [`erf_impl`] and [`ln_erfc`] so the two stay consistent. +fn erfc_fraction(z: f64) -> (f64, f64) { + if z < 0.75 { + ( + evaluate::polynomial(z - 0.5, ERF_IMPL_BN) / evaluate::polynomial(z - 0.5, ERF_IMPL_BD), + 0.3440242111682891845703125, + ) + } else if z < 1.25 { + ( + evaluate::polynomial(z - 0.75, ERF_IMPL_CN) + / evaluate::polynomial(z - 0.75, ERF_IMPL_CD), + 0.4199909269809722900390625, + ) + } else if z < 2.25 { + ( + evaluate::polynomial(z - 1.25, ERF_IMPL_DN) + / evaluate::polynomial(z - 1.25, ERF_IMPL_DD), + 0.489862501621246337890625, + ) + } else if z < 3.5 { + ( + evaluate::polynomial(z - 2.25, ERF_IMPL_EN) + / evaluate::polynomial(z - 2.25, ERF_IMPL_ED), + 0.5317370891571044921875, + ) + } else if z < 5.25 { + ( + evaluate::polynomial(z - 3.5, ERF_IMPL_FN) / evaluate::polynomial(z - 3.5, ERF_IMPL_FD), + 0.548997342586517333984375, + ) + } else if z < 8.0 { + ( + evaluate::polynomial(z - 5.25, ERF_IMPL_GN) + / evaluate::polynomial(z - 5.25, ERF_IMPL_GD), + 0.55717408657073974609375, + ) + } else if z < 11.5 { + ( + evaluate::polynomial(z - 8.0, ERF_IMPL_HN) / evaluate::polynomial(z - 8.0, ERF_IMPL_HD), + 0.56098079681396484375, + ) + } else if z < 17.0 { + ( + evaluate::polynomial(z - 11.5, ERF_IMPL_IN) + / evaluate::polynomial(z - 11.5, ERF_IMPL_ID), + 0.56264936923980712890625, + ) + } else if z < 24.0 { + ( + evaluate::polynomial(z - 17.0, ERF_IMPL_JN) + / evaluate::polynomial(z - 17.0, ERF_IMPL_JD), + 0.563459813594818115234375, + ) + } else if z < 38.0 { + ( + evaluate::polynomial(z - 24.0, ERF_IMPL_KN) + / evaluate::polynomial(z - 24.0, ERF_IMPL_KD), + 0.5638477802276611328125, + ) + } else if z < 60.0 { + ( + evaluate::polynomial(z - 38.0, ERF_IMPL_LN) + / evaluate::polynomial(z - 38.0, ERF_IMPL_LD), + 0.5640528202056884765625, + ) + } else if z < 85.0 { + ( + evaluate::polynomial(z - 60.0, ERF_IMPL_MN) + / evaluate::polynomial(z - 60.0, ERF_IMPL_MD), + 0.56413090229034423828125, + ) + } else { + ( + evaluate::polynomial(z - 85.0, ERF_IMPL_NN) + / evaluate::polynomial(z - 85.0, ERF_IMPL_ND), + 0.56415843963623046875, + ) + } +} + /// `erf_impl` computes the error function at `z`. /// If `inv` is true, `1 - erf` is calculated as opposed to `erf` fn erf_impl(z: f64, inv: bool) -> f64 { @@ -590,86 +774,20 @@ fn erf_impl(z: f64, inv: bool) -> f64 { + z * evaluate::polynomial(z, ERF_IMPL_AN) / evaluate::polynomial(z, ERF_IMPL_AD) } } else if z < 110.0 { - let (r, b) = if z < 0.75 { - ( - evaluate::polynomial(z - 0.5, ERF_IMPL_BN) - / evaluate::polynomial(z - 0.5, ERF_IMPL_BD), - 0.3440242112, - ) - } else if z < 1.25 { - ( - evaluate::polynomial(z - 0.75, ERF_IMPL_CN) - / evaluate::polynomial(z - 0.75, ERF_IMPL_CD), - 0.419990927, - ) - } else if z < 2.25 { - ( - evaluate::polynomial(z - 1.25, ERF_IMPL_DN) - / evaluate::polynomial(z - 1.25, ERF_IMPL_DD), - 0.4898625016, - ) - } else if z < 3.5 { - ( - evaluate::polynomial(z - 2.25, ERF_IMPL_EN) - / evaluate::polynomial(z - 2.25, ERF_IMPL_ED), - 0.5317370892, - ) - } else if z < 5.25 { - ( - evaluate::polynomial(z - 3.5, ERF_IMPL_FN) - / evaluate::polynomial(z - 3.5, ERF_IMPL_FD), - 0.5489973426, - ) - } else if z < 8.0 { - ( - evaluate::polynomial(z - 5.25, ERF_IMPL_GN) - / evaluate::polynomial(z - 5.25, ERF_IMPL_GD), - 0.5571740866, - ) - } else if z < 11.5 { - ( - evaluate::polynomial(z - 8.0, ERF_IMPL_HN) - / evaluate::polynomial(z - 8.0, ERF_IMPL_HD), - 0.5609807968, - ) - } else if z < 17.0 { - ( - evaluate::polynomial(z - 11.5, ERF_IMPL_IN) - / evaluate::polynomial(z - 11.5, ERF_IMPL_ID), - 0.5626493692, - ) - } else if z < 24.0 { - ( - evaluate::polynomial(z - 17.0, ERF_IMPL_JN) - / evaluate::polynomial(z - 17.0, ERF_IMPL_JD), - 0.5634598136, - ) - } else if z < 38.0 { - ( - evaluate::polynomial(z - 24.0, ERF_IMPL_KN) - / evaluate::polynomial(z - 24.0, ERF_IMPL_KD), - 0.5638477802, - ) - } else if z < 60.0 { - ( - evaluate::polynomial(z - 38.0, ERF_IMPL_LN) - / evaluate::polynomial(z - 38.0, ERF_IMPL_LD), - 0.5640528202, - ) - } else if z < 85.0 { - ( - evaluate::polynomial(z - 60.0, ERF_IMPL_MN) - / evaluate::polynomial(z - 60.0, ERF_IMPL_MD), - 0.5641309023, - ) - } else { - ( - evaluate::polynomial(z - 85.0, ERF_IMPL_NN) - / evaluate::polynomial(z - 85.0, ERF_IMPL_ND), - 0.5641584396, - ) - }; - let g = (-z * z).exp() / z; + let (r, b) = erfc_fraction(z); + // `z * z` is rounded before `exp` sees it, which costs roughly `z^2` + // ulps in the result (~460 ulps by z = 27). Recover the rounding error + // of the square exactly with a Dekker product and fold it back in: + // `exp(-z^2) = exp(-sq) * exp(-err)`, and `exp(-err) ~= 1 - err` since + // `|err| <= ulp(sq)/2`. Dekker's split is used instead of + // `f64::mul_add` because the latter falls back to a slow software FMA + // on targets without the hardware instruction (e.g. baseline x86-64). + let sq = z * z; + let split = 134_217_729.0 * z; // 2^27 + 1; cannot overflow, z < 110 + let z_hi = split - (split - z); + let z_lo = z - z_hi; + let err = ((z_hi * z_hi - sq) + 2.0 * z_hi * z_lo) + z_lo * z_lo; + let g = (-sq).exp() * (1.0 - err) / z; g * b + g * r } else { 0.0 @@ -747,19 +865,19 @@ mod tests { #[test] fn test_erf() { assert!(erf(f64::NAN).is_nan()); - prec::assert_abs_diff_eq!(erf(-1.0), -0.84270079294971486934122063508260925929606699796630291, epsilon = 1e-11); + prec::assert_abs_diff_eq!(erf(-1.0), -0.84270079294971486934122063508260925929606699796630291, epsilon = 4.441e-16); assert_eq!(erf(0.0), 0.0); assert_eq!(erf(1e-15), 0.0000000000000011283791670955126615773132947717431253912942469337536); assert_eq!(erf(0.1), 0.1124629160182848984047122510143040617233925185058162); prec::assert_abs_diff_eq!(erf(0.2), 0.22270258921047846617645303120925671669511570710081967, epsilon = 1e-16); assert_eq!(erf(0.3), 0.32862675945912741618961798531820303325847175931290341); assert_eq!(erf(0.4), 0.42839235504666847645410962730772853743532927705981257); - prec::assert_abs_diff_eq!(erf(0.5), 0.5204998778130465376827466538919645287364515757579637, epsilon = 1e-9); - prec::assert_abs_diff_eq!(erf(1.0), 0.84270079294971486934122063508260925929606699796630291, epsilon = 1e-11); - prec::assert_abs_diff_eq!(erf(1.5), 0.96610514647531072706697626164594785868141047925763678, epsilon = 1e-11); - prec::assert_abs_diff_eq!(erf(2.0), 0.99532226501895273416206925636725292861089179704006008, epsilon = 1e-11); - prec::assert_abs_diff_eq!(erf(2.5), 0.99959304798255504106043578426002508727965132259628658, epsilon = 1e-13); - prec::assert_abs_diff_eq!(erf(3.0), 0.99997790950300141455862722387041767962015229291260075, epsilon = 1e-11); + prec::assert_abs_diff_eq!(erf(0.5), 0.5204998778130465376827466538919645287364515757579637, epsilon = 4.441e-16); + prec::assert_abs_diff_eq!(erf(1.0), 0.84270079294971486934122063508260925929606699796630291, epsilon = 4.441e-16); + prec::assert_abs_diff_eq!(erf(1.5), 0.96610514647531072706697626164594785868141047925763678, epsilon = 4.441e-16); + prec::assert_abs_diff_eq!(erf(2.0), 0.99532226501895273416206925636725292861089179704006008, epsilon = 4.441e-16); + prec::assert_abs_diff_eq!(erf(2.5), 0.99959304798255504106043578426002508727965132259628658, epsilon = 4.441e-16); + prec::assert_abs_diff_eq!(erf(3.0), 0.99997790950300141455862722387041767962015229291260075, epsilon = 4.441e-16); assert_eq!(erf(4.0), 0.99999998458274209971998114784032651311595142785474641); assert_eq!(erf(5.0), 0.99999999999846254020557196514981165651461662110988195); assert_eq!(erf(6.0), 0.99999999999999997848026328750108688340664960081261537); @@ -770,24 +888,24 @@ mod tests { #[test] fn test_erfc() { assert!(erfc(f64::NAN).is_nan()); - prec::assert_abs_diff_eq!(erfc(-1.0), 1.8427007929497148693412206350826092592960669979663028, epsilon = 1e-11); + prec::assert_abs_diff_eq!(erfc(-1.0), 1.8427007929497148693412206350826092592960669979663028, epsilon = 8.882e-16); assert_eq!(erfc(0.0), 1.0); - prec::assert_abs_diff_eq!(erfc(0.1), 0.88753708398171510159528774898569593827660748149418343, epsilon = 1e-15); + prec::assert_abs_diff_eq!(erfc(0.1), 0.88753708398171510159528774898569593827660748149418343, epsilon = 4.441e-16); assert_eq!(erfc(0.2), 0.77729741078952153382354696879074328330488429289918085); assert_eq!(erfc(0.3), 0.67137324054087258381038201468179696674152824068709621); - prec::assert_abs_diff_eq!(erfc(0.4), 0.57160764495333152354589037269227146256467072294018715, epsilon = 1e-15); - prec::assert_abs_diff_eq!(erfc(0.5), 0.47950012218695346231725334610803547126354842424203654, epsilon = 1e-9); - prec::assert_abs_diff_eq!(erfc(1.0), 0.15729920705028513065877936491739074070393300203369719, epsilon = 1e-11); - prec::assert_abs_diff_eq!(erfc(1.5), 0.033894853524689272933023738354052141318589520742363247, epsilon = 1e-11); - prec::assert_abs_diff_eq!(erfc(2.0), 0.0046777349810472658379307436327470713891082029599399245, epsilon = 1e-11); - prec::assert_abs_diff_eq!(erfc(2.5), 0.00040695201744495893956421573997491272034867740371342016, epsilon = 1e-13); - prec::assert_abs_diff_eq!(erfc(3.0), 0.00002209049699858544137277612958232037984770708739924966, epsilon = 1e-11); - prec::assert_abs_diff_eq!(erfc(4.0), 0.000000015417257900280018852159673486884048572145253589191167, epsilon = 1e-18); - prec::assert_abs_diff_eq!(erfc(5.0), 0.0000000000015374597944280348501883434853833788901180503147233804, epsilon = 1e-22); - prec::assert_abs_diff_eq!(erfc(6.0), 2.1519736712498913116593350399187384630477514061688559e-17, epsilon = 1e-26); - prec::assert_abs_diff_eq!(erfc(10.0), 2.0884875837625447570007862949577886115608181193211634e-45, epsilon = 1e-55); - prec::assert_abs_diff_eq!(erfc(15.0), 7.2129941724512066665650665586929271099340909298253858e-100, epsilon = 1e-109); - prec::assert_abs_diff_eq!(erfc(20.0), 5.3958656116079009289349991679053456040882726709236071e-176, epsilon = 1e-186); + prec::assert_abs_diff_eq!(erfc(0.4), 0.57160764495333152354589037269227146256467072294018715, epsilon = 4.441e-16); + prec::assert_abs_diff_eq!(erfc(0.5), 0.47950012218695346231725334610803547126354842424203654, epsilon = 2.220e-16); + prec::assert_abs_diff_eq!(erfc(1.0), 0.15729920705028513065877936491739074070393300203369719, epsilon = 1.110e-16); + prec::assert_abs_diff_eq!(erfc(1.5), 0.033894853524689272933023738354052141318589520742363247, epsilon = 2.776e-17); + prec::assert_abs_diff_eq!(erfc(2.0), 0.0046777349810472658379307436327470713891082029599399245, epsilon = 3.469e-18); + prec::assert_abs_diff_eq!(erfc(2.5), 0.00040695201744495893956421573997491272034867740371342016, epsilon = 2.168e-19); + prec::assert_abs_diff_eq!(erfc(3.0), 0.00002209049699858544137277612958232037984770708739924966, epsilon = 1.355e-20); + prec::assert_abs_diff_eq!(erfc(4.0), 0.000000015417257900280018852159673486884048572145253589191167, epsilon = 1.323e-23); + prec::assert_abs_diff_eq!(erfc(5.0), 0.0000000000015374597944280348501883434853833788901180503147233804, epsilon = 8.078e-28); + prec::assert_abs_diff_eq!(erfc(6.0), 2.1519736712498913116593350399187384630477514061688559e-17, epsilon = 1.233e-32); + prec::assert_abs_diff_eq!(erfc(10.0), 2.0884875837625447570007862949577886115608181193211634e-45, epsilon = 1.245e-60); + prec::assert_abs_diff_eq!(erfc(15.0), 7.2129941724512066665650665586929271099340909298253858e-100, epsilon = 4.061e-115); + prec::assert_abs_diff_eq!(erfc(20.0), 5.3958656116079009289349991679053456040882726709236071e-176, epsilon = 2.806e-191); assert_eq!(erfc(30.0), 2.5646562037561116000333972775014471465488897227786155e-393); assert_eq!(erfc(50.0), 2.0709207788416560484484478751657887929322509209953988e-1088); assert_eq!(erfc(80.0), 2.3100265595063985852034904366341042118385080919280966e-2782); @@ -800,9 +918,9 @@ mod tests { assert!(erf_inv(f64::NAN).is_nan()); assert_eq!(erf_inv(-1.0), f64::NEG_INFINITY); assert_eq!(erf_inv(0.0), 0.0); - prec::assert_abs_diff_eq!(erf_inv(1e-15), 8.86226925452758013649e-16, epsilon = 1e-30); + prec::assert_abs_diff_eq!(erf_inv(1e-15), 8.86226925452758013649e-16, epsilon = 3.944e-31); assert_eq!(erf_inv(0.1), 0.08885599049425768701574); - prec::assert_abs_diff_eq!(erf_inv(0.2), 0.1791434546212916764927, epsilon = 1e-15); + prec::assert_abs_diff_eq!(erf_inv(0.2), 0.1791434546212916764927, epsilon = 1.110e-16); assert_eq!(erf_inv(0.3), 0.272462714726754355622); assert_eq!(erf_inv(0.4), 0.3708071585935579290582); assert_eq!(erf_inv(0.5), 0.4769362762044698733814); @@ -811,16 +929,81 @@ mod tests { assert_eq!(erf_inv(f64::NEG_INFINITY), f64::NEG_INFINITY); } + /// `erfcx(x) = exp(x^2) erfc(x)` does not underflow the way `erfc` does, so + /// it is what makes ratios of two `erfc` values well conditioned + /// (statrs-dev/statrs#202). References are mpmath at 50 significant digits, + /// computed via the asymptotic expansion past `1e6` where mpmath's own + /// `erfc` can no longer represent the intermediate. + #[test] + fn test_erfcx() { + for (x, want) in [ + (-5.0, 144009798674.66104041), + (-1.0, 5.0089800807622834663), + (-0.25, 1.3586423701047221152), + (0.0, 1.0), + (0.25, 0.77034654773099674392), + (0.5, 0.61569034419292587487), + (1.0, 0.42758357615580700441), + (5.0, 0.11070463773306862637), + (25.0, 0.022549572432641358944), + (109.9, 0.005133450685917941343), + (110.0, 0.0051287842983465685351), + (200.0, 0.0028209126572120463987), + (1e5, 5.6418958351954680777e-6), + (1e100, 5.6418958354775628695e-101), + (1e300, 5.6418958354775628695e-301), + ] { + prec::assert_relative_eq!(erfcx(x), want, epsilon = 0.0, max_relative = 1e-14); + } + assert!(erfcx(f64::NAN).is_nan()); + // erfcx stays finite across the whole positive axis, where erfc is zero + assert_eq!(erfc(1e5), 0.0, "premise: erfc has underflowed"); + assert!(erfcx(1e5) > 0.0); + // and overflows only where exp(x^2) does + assert_eq!(erfcx(-30.0), f64::INFINITY); + } + + /// `ln_erfcx` is finite over the entire range of f64, unlike both `erfc` + /// (underflows) and `erfcx` (overflows for very negative `x`). + #[test] + fn test_ln_erfcx() { + for (x, want) in [ + (-30.0, 900.69314718055994531), + (-1.0, 1.6112323176780704946), + (0.5, -0.48501112983708440303), + (1.0, -0.84960550993324824858), + (110.0, -5.2728866267632017793), + (1e5, -12.085290407944928507), + (1e150, -345.96012889203155269), + (1e300, -691.34789284113840529), + ] { + prec::assert_relative_eq!(ln_erfcx(x), want, epsilon = 0.0, max_relative = 1e-14); + } + assert!(ln_erfcx(f64::NAN).is_nan()); + // Consistency with ln_erfc where the latter is meaningful. The tolerance + // has to scale with `x * x`, because forming `x * x + ln_erfc(x)` cancels + // two values of that magnitude to reach an O(1) result -- which is the + // whole reason `ln_erfcx` computes it directly instead. + for x in [-3.0f64, -0.5, 0.0, 0.5, 2.0, 20.0, 100.0, 200.0] { + let cancelled = x * x + ln_erfc(x); + prec::assert_abs_diff_eq!(ln_erfcx(x), cancelled, epsilon = 1e-15 * (1.0 + x * x)); + } + // and with erfcx where that is representable + for x in [-20.0f64, -1.0, 0.5, 3.0, 50.0, 1e4] { + prec::assert_relative_eq!(ln_erfcx(x), erfcx(x).ln(), epsilon = 0.0, max_relative = 1e-13); + } + } + #[test] fn test_erfc_inv() { assert_eq!(erfc_inv(0.0), f64::INFINITY); - prec::assert_abs_diff_eq!(erfc_inv(1e-100), 15.065574702593, epsilon = 1e-11); - prec::assert_abs_diff_eq!(erfc_inv(1e-30), 8.1486162231699, epsilon = 1e-12); - prec::assert_abs_diff_eq!(erfc_inv(1e-20), 6.6015806223551, epsilon = 1e-13); - prec::assert_abs_diff_eq!(erfc_inv(1e-10), 4.5728249585449249378479309946884581365517663258840893, epsilon = 1e-7); - prec::assert_abs_diff_eq!(erfc_inv(1e-5), 3.1234132743415708640270717579666062107939039971365252, epsilon = 1e-11); - prec::assert_abs_diff_eq!(erfc_inv(0.1), 1.1630871536766741628440954340547000483801487126688552, epsilon = 1e-14); - prec::assert_abs_diff_eq!(erfc_inv(0.2), 0.90619380243682330953597079527631536107443494091638384, epsilon = 1e-15); + prec::assert_abs_diff_eq!(erfc_inv(1e-100), 15.065574702592645704404610541369, epsilon = 7.105e-15); + prec::assert_abs_diff_eq!(erfc_inv(1e-30), 8.1486162231698646073845666606481, epsilon = 7.105e-15); + prec::assert_abs_diff_eq!(erfc_inv(1e-20), 6.6015806223551425615163916324187, epsilon = 3.553e-15); + prec::assert_abs_diff_eq!(erfc_inv(1e-10), 4.5728249673894852787410436731442, epsilon = 3.553e-15); + prec::assert_abs_diff_eq!(erfc_inv(1e-5), 3.1234132743408750302472925681804, epsilon = 1.776e-15); + prec::assert_abs_diff_eq!(erfc_inv(0.1), 1.1630871536766740867262542605629, epsilon = 8.882e-16); + prec::assert_abs_diff_eq!(erfc_inv(0.2), 0.90619380243682322007116270309566, epsilon = 4.441e-16); assert_eq!(erfc_inv(0.5), 0.47693627620446987338141835364313055980896974905947083); assert_eq!(erfc_inv(1.0), 0.0); assert_eq!(erfc_inv(1.5), -0.47693627620446987338141835364313055980896974905947083); diff --git a/src/function/gamma.rs b/src/function/gamma.rs index bff5c45a..d7a071b3 100644 --- a/src/function/gamma.rs +++ b/src/function/gamma.rs @@ -2,7 +2,9 @@ //! related functions use crate::consts; +use crate::function::evaluate; use crate::prec; +use crate::prec::{dekker_product_err, two_diff}; use core::f64; #[cfg(not(feature = "std"))] use num_traits::Float as _; @@ -30,48 +32,336 @@ impl core::fmt::Display for GammaFuncError { impl core::error::Error for GammaFuncError {} +/// Computes `sin(PI * x)` for use in reflection formulas. +/// +/// Evaluating `(PI * x).sin()` directly loses the fractional part of `x` to +/// rounding once `PI * x` is large, and near the zeros of the sine (integer +/// `x`) the representation error of `PI` alone costs `~1.2e-16 / dist_to_pole` +/// of relative accuracy. Reducing with the period first (`x - round(x)` is +/// exact) keeps the error relative to the fractional part instead. +#[inline] +fn sin_pi(x: f64) -> f64 { + let m = x.round(); + let r = x - m; // exact, |r| <= 0.5 + // sin(PI * (m + r)) = (-1)^m * sin(PI * r); `m * 0.5` is exact, so its + // fractional part is 0.5 exactly when `m` is odd. (For |m| >= 2^53 every + // representable float is even.) + let sin_r = (f64::consts::PI * r).sin(); + if (m * 0.5).fract() == 0.0 { + sin_r + } else { + -sin_r + } +} + +/// Computes `tan(PI * x)` with the same period reduction as [`sin_pi`] +/// (`tan` has period `PI`, so no sign correction is needed). +#[inline] +fn tan_pi(x: f64) -> f64 { + let r = x - x.round(); // exact, |r| <= 0.5 + (f64::consts::PI * r).tan() +} + +// --------------------------------------------------------------------------- +// Saddle-point building blocks (Loader, "Fast and Accurate Computation of +// Binomial Probabilities", 2000). +// +// Densities and incomplete-function prefixes in the gamma/beta family are +// naturally written as `exp(a * ln x - x - ln_gamma(a))` and friends. Those +// forms are numerically poor: for `a ~ 1e4` each term is `~1e5` while the sum +// is `O(1)`, so the `~1e-11` absolute error of the sum becomes the accuracy +// ceiling of everything downstream. +// +// The fix is to split each such exponent into two pieces that are individually +// `O(1)`: +// +// * `stirling_delta(z)`, the Stirling-series remainder of `ln_gamma`, and +// * `bd0(x, np)`, the "deviance" `x * ln(x / np) - (x - np)`, which is +// evaluated by a series in `(x - np) / (x + np)` so that the cancellation +// for `x` near `np` is performed analytically rather than in floating +// point. +// +// Nothing large is ever formed, so the cancellation disappears entirely. +// --------------------------------------------------------------------------- + +/// Coefficients `B_2n / (2n (2n - 1))` of the Stirling series for +/// [`stirling_delta`], ascending in `1/z^2`. +const STIRLING_SERIES: &[f64] = &[ + 0.08333333333333333, // 1/12 + -0.002777777777777778, // -1/360 + 0.0007936507936507937, // 1/1260 + -0.0005952380952380953, // -1/1680 + 0.0008417508417508417, // 1/1188 + -0.0019175269175269176, // -691/360360 +]; + +/// Argument above which the truncated Stirling series is used directly. Six +/// terms there are good to `1.4e-18` absolute, i.e. invisible. +pub(crate) const STIRLING_SERIES_MIN: f64 = 16.0; + +/// The Stirling-series remainder of `ln_gamma`, for `z > 0`: +/// +/// ```text +/// stirling_delta(z) = ln_gamma(z) - [(z - 1/2) ln z - z + ln(2 pi) / 2] +/// ``` +/// +/// Equivalently `ln(n!) - ln(sqrt(2 pi n) (n / e)^n)` at `z = n`, which is +/// Loader's `stirlerr`. The value is `O(1 / (12 z))`, and the implementation +/// keeps it accurate to `~1e-15` *absolute* - which is what matters, since +/// every caller adds it to an exponent. +/// +/// Below [`STIRLING_SERIES_MIN`] the recurrence +/// `delta(z) = delta(z + 1) + (z + 1/2) ln(1 + 1/z) - 1` lifts the argument +/// into the series range. Each step costs one rounding, so no table (and no +/// recursive dependency on [`ln_gamma`]) is needed. +pub(crate) fn stirling_delta(z: f64) -> f64 { + let mut acc = 0.0; + let mut w = z; + while w < STIRLING_SERIES_MIN { + acc += (w + 0.5) * (1.0 / w).ln_1p() - 1.0; + w += 1.0; + } + // Horner in 1/z^2. For `w * w == inf` this collapses to the leading + // `1 / (12 w)` term, which is the correct limit. + let ww = w * w; + let series = STIRLING_SERIES.iter().rev().fold(0.0, |s, &c| s / ww + c); + acc + series / w +} + +/// The deviance `bd0(x, np) = x * ln(x / np) - (x - np)`, for `x >= 0`, +/// `np > 0`. +/// +/// This is the Kullback-Leibler-like term of the Poisson/binomial saddle-point +/// expansions, and is non-negative with a double root at `x == np`. Writing it +/// directly loses all precision near that root; with +/// `v = (x - np) / (x + np)` it has the all-positive series +/// +/// ```text +/// bd0 = (x - np) v + 2 x sum_{j >= 1} v^(2j + 1) / (2j + 1) +/// ``` +/// +/// which is used whenever `|x - np| < (x + np) / 10`, giving `~1e-16` relative +/// accuracy uniformly. +pub(crate) fn bd0(x: f64, np: f64) -> f64 { + if x == 0.0 { + // 0 * ln 0 == 0 by convention; the direct form would give NaN + return np; + } + if (x - np).abs() < 0.1 * (x + np) { + let v = (x - np) / (x + np); + let mut s = (x - np) * v; + if s.abs() < f64::MIN_POSITIVE { + return s; + } + let mut ej = 2.0 * x * v; + let v2 = v * v; + // |v| < 0.1, so v^(2j) is below f64::MIN_POSITIVE well before j = 200 + for j in 1..200 { + ej *= v2; + let s1 = s + ej / (2 * j + 1) as f64; + if s1 == s { + return s1; + } + s = s1; + } + return s; + } + // `(x / np).ln()` silently loses the whole term when the ratio leaves the + // normal range - `bd0(1e-300, 5e99)` has `x / np` underflow to zero, giving + // `-inf` where the answer is `+5e99`, which then poisoned the beta prefix + // into NaN. Reaching this branch requires `|x - np| >= (x + np) / 10`, so + // the ratio is bounded away from 1 and the difference of logs cannot cancel + // badly; it is only used where the ratio itself is unusable, since it is + // otherwise the less accurate of the two forms. + let ratio = x / np; + let log_ratio = if (f64::MIN_POSITIVE..f64::INFINITY).contains(&ratio) { + ratio.ln() + } else { + x.ln() - np.ln() + }; + x * log_ratio - (x - np) +} + +/// [`bd0`] with the mean supplied as an exact double-double `np_hi + np_lo`. +/// +/// The mean usually arrives as a rounded product such as `n * p`, and `bd0` is +/// sensitive enough for that last half-ulp to dominate everything else: +/// `d bd0 / d np = 1 - x / np`, so at `n = 2e6`, `p = 0.3` the rounding of +/// `n * p` alone shifts `bd0` by `~2.5e-13` - a thousand ulps in the resulting +/// pmf. The correction is first order in `np_lo`; the next term is smaller by +/// another factor of `np_lo / np`, i.e. utterly negligible. +pub(crate) fn bd0_dd(x: f64, np_hi: f64, np_lo: f64) -> f64 { + bd0(x, np_hi) + (1.0 - x / np_hi) * np_lo +} + +/// `ln(x^a e^-x / Gamma(a))`, the prefix of the incomplete gamma functions. +/// +/// Written out, this is `a ln x - x - ln_gamma(a)`, where all three terms grow +/// like `a ln a` while the sum stays `O(ln a)`. The saddle-point form +/// +/// ```text +/// = -bd0(a, x) - stirling_delta(a) + ln(a / (2 pi)) / 2 +/// ``` +/// +/// is the same quantity with every piece `O(1)`, so it is accurate to a few +/// `1e-16` absolute instead of `a ln a * eps` (`~1e-11` at `a = 1e4`). +pub(crate) fn ln_gamma_prefix(a: f64, x: f64) -> f64 { + if a < STIRLING_SERIES_MIN { + // No cancellation to remove yet (every term is already `O(1)`), and the + // recurrence in `stirling_delta` would cost more roundings than it + // saves. + return a * x.ln() - x - ln_gamma(a); + } + -bd0(a, x) - stirling_delta(a) + 0.5 * (a / f64::consts::TAU).ln() +} + /// Auxiliary variable when evaluating the `gamma_ln` function const GAMMA_R: f64 = 10.900511; -/// Polynomial coefficients for approximating the `gamma_ln` function -const GAMMA_DK: &[f64] = &[ - 2.48574089138753565546e-5, - 1.05142378581721974210, - -3.45687097222016235469, - 4.51227709466894823700, - -2.98285225323576655721, - 1.05639711577126713077, - -1.95428773191645869583e-1, - 1.70970543404441224307e-2, - -5.71926117404305781283e-4, - 4.63399473359905636708e-6, - -2.71994908488607703910e-9, +// The Lanczos sum used by `ln_gamma` and `gamma` is Pugh's 10-term +// partial-fraction approximation with g = GAMMA_R ("An Analysis of the Lanczos +// Gamma Approximation", Glendon Ralph Pugh, 2004 p. 116): +// +// S(z) = d_0 + sum_{k=1..10} d_k / (z + k - 1) +// +// with residues d_k = [2.48574089138753565546e-5, 1.05142378581721974210, +// -3.45687097222016235469, 4.51227709466894823700, -2.98285225323576655721, +// 1.05639711577126713077, -1.95428773191645869583e-1, 1.70970543404441224307e-2, +// -5.71926117404305781283e-4, 4.63399473359905636708e-6, +// -2.71994908488607703910e-9]. +// +// The residues alternate in sign, so evaluating the sum directly cancels +// catastrophically (condition number ~3600 around z = 50, i.e. ~440 eps of +// relative error in the sum). The constants below are the mathematically +// identical single-fraction form S(z) = N(z) / D(z), derived from the d_k in +// exact rational arithmetic: D(z) = z (z+1) ... (z+9) (whose expanded +// coefficients are unsigned Stirling numbers of the first kind, exact in f64) +// and N = d_0 D + sum_k d_k D / (z + k - 1). Every coefficient of both +// polynomials is positive, so for z > 0 both Horner evaluations have condition +// number 1 and the sum is accurate to a few eps. + +/// Numerator `N(z)` of the Lanczos sum in single-fraction form (ascending). +const LANCZOS_NUM: &[f64] = &[ + 381540.6633973527, + 365505.352696257, + 157567.99949360118, + 40253.83538142639, + 6748.767525934567, + 775.8779405455635, + 61.94528891422096, + 3.391366244015308, + 0.12184807036444657, + 0.002594340508809067, + 2.4857408913875355e-5, +]; + +/// Denominator `D(z) = z (z+1) ... (z+9)` expanded (ascending; unsigned +/// Stirling numbers of the first kind, exact integers). +const LANCZOS_DENOM: &[f64] = &[ + 0.0, 362880.0, 1026576.0, 1172700.0, 723680.0, 269325.0, 63273.0, 9450.0, 870.0, 45.0, 1.0, +]; + +/// `LANCZOS_NUM` reversed: `N(z) / z^10` as a polynomial in `w = 1/z`. +const LANCZOS_NUM_REV: &[f64] = &[ + 2.4857408913875355e-5, + 0.002594340508809067, + 0.12184807036444657, + 3.391366244015308, + 61.94528891422096, + 775.8779405455635, + 6748.767525934567, + 40253.83538142639, + 157567.99949360118, + 365505.352696257, + 381540.6633973527, ]; +/// `LANCZOS_DENOM` reversed: `D(z) / z^10` as a polynomial in `w = 1/z`. +const LANCZOS_DENOM_REV: &[f64] = &[ + 1.0, 45.0, 870.0, 9450.0, 63273.0, 269325.0, 723680.0, 1172700.0, 1026576.0, 362880.0, 0.0, +]; + +/// Low half of Euler's number: `e == f64::consts::E + E_LO` to double-double +/// precision. +const E_LO: f64 = 1.4456468917292502e-16; + +/// Computes `((p + GAMMA_R) / e)^exponent`, compensating the rounding of the base. +/// +/// `powf` amplifies a relative error `eps` in its base by the exponent, so the +/// two roundings in `(p + GAMMA_R) / e` cost `~2 |p|` ulps - the dominant +/// error of the direct evaluation (~190 ulps in `gamma` by x = 122). The +/// rounding residuals of the addition (two-sum) and the division (Dekker +/// product, plus the low word of `e`) are recovered exactly and applied as the +/// first-order correction `(b (1 + delta))^p ~= b^p (1 + p delta)`. +/// `p_err` is the rounding residual of the exponent itself (the true exponent +/// is `p + p_err`), folded in as `b^(p_err) ~= 1 + p_err * ln(b)`; it must be +/// zero unless `exponent == p`. +/// +/// `exponent` is normally `p`. The overflow-avoiding path in [`gamma`] passes +/// `p / 2` and squares the result, which needs the *base* to keep coming from +/// the full `p` - halving `p` in the base as well would compute an entirely +/// different quantity. +#[inline] +fn lanczos_power(p: f64, p_err: f64, exponent: f64) -> f64 { + let (zgh, add_err) = two_diff(p, -GAMMA_R); + let b = zgh / f64::consts::E; + let pw = b * f64::consts::E; + // exact residual of the division against the double-double e + let div_err = (zgh - pw) - dekker_product_err(b, f64::consts::E, pw); + let delta = (div_err + add_err + p_err - b * E_LO) / zgh; + let mut corr = exponent * delta; + if p_err != 0.0 { + corr += p_err * b.ln(); + } + let base = b.powf(exponent); + // `(b (1 + delta))^p ~= b^p (1 + p delta)` only holds while `|p delta| << 1`. + // Since `delta` is of order `eps`, that fails once `p` reaches ~1e13 - far + // past where `b^p` overflows, so the uncorrected value is the right answer + // there. Applying it anyway would let a negative `corr` flip the sign and + // turn an overflowing `gamma` into `-inf`. + if corr.abs() < 0.25 { + base * (1.0 + corr) + } else { + base + } +} + +/// Evaluates the Lanczos sum `S(z)` for `z >= 0.5` via the well-conditioned +/// single-fraction form (see the derivation note above). +#[inline] +fn lanczos_sum(z: f64) -> f64 { + if z < 1e29 { + evaluate::polynomial(z, LANCZOS_NUM) / evaluate::polynomial(z, LANCZOS_DENOM) + } else { + // z^10 would overflow; divide both polynomials by z^10 and evaluate + // in w = 1/z instead. + let w = 1.0 / z; + evaluate::polynomial(w, LANCZOS_NUM_REV) / evaluate::polynomial(w, LANCZOS_DENOM_REV) + } +} + /// Computes the logarithm of the gamma function /// with an accuracy of 16 floating point digits. /// The implementation is derived from /// "An Analysis of the Lanczos Gamma Approximation", /// Glendon Ralph Pugh, 2004 p. 116 pub fn ln_gamma(x: f64) -> f64 { + // ln Gamma(n) = ln((n - 1)!) via the exact factorial table (~0.4 ulp, and + // exactly 0 at n = 1, 2, where the Lanczos formula's terms only cancel + // approximately). + if x.fract() == 0.0 && (1.0..=171.0).contains(&x) { + return crate::function::factorial::ln_factorial(x as u64 - 1); + } if x < 0.5 { - let s = GAMMA_DK - .iter() - .enumerate() - .skip(1) - .fold(GAMMA_DK[0], |s, t| s + t.1 / (t.0 as f64 - x)); + let s = lanczos_sum(1.0 - x); consts::LN_PI - - (f64::consts::PI * x).sin().ln() + - sin_pi(x).ln() - s.ln() - consts::LN_2_SQRT_E_OVER_PI - (0.5 - x) * ((0.5 - x + GAMMA_R) / f64::consts::E).ln() } else { - let s = GAMMA_DK - .iter() - .enumerate() - .skip(1) - .fold(GAMMA_DK[0], |s, t| s + t.1 / (x + t.0 as f64 - 1.0)); + let s = lanczos_sum(x); s.ln() + consts::LN_2_SQRT_E_OVER_PI @@ -79,31 +369,43 @@ pub fn ln_gamma(x: f64) -> f64 { } } -/// Computes the gamma function with an accuracy -/// of 16 floating point digits. The implementation -/// is derived from "An Analysis of the Lanczos Gamma Approximation", -/// Glendon Ralph Pugh, 2004 p. 116 +/// Computes the gamma function. The implementation is derived from +/// "An Analysis of the Lanczos Gamma Approximation", +/// Glendon Ralph Pugh, 2004 p. 116. +/// +/// Exact at the positive integers up to 171; elsewhere accurate to about +/// `4e-15` relative, which is the approximation floor of the (f64-rounded) +/// Pugh coefficient set itself - the evaluation is compensated to well below +/// that. pub fn gamma(x: f64) -> f64 { + // Gamma(n) = (n - 1)! exactly at the positive integers where the factorial + // is representable; the Lanczos path is only good to ~1 ulp there. + if x.fract() == 0.0 && (1.0..=171.0).contains(&x) { + return crate::function::factorial::factorial(x as u64 - 1); + } if x < 0.5 { - let s = GAMMA_DK - .iter() - .enumerate() - .skip(1) - .fold(GAMMA_DK[0], |s, t| s + t.1 / (t.0 as f64 - x)); + let s = lanczos_sum(1.0 - x); + // 0.5 - x rounds for x below the binade of 0.5; keep its residual + let (pw, pw_err) = two_diff(0.5, x); f64::consts::PI - / ((f64::consts::PI * x).sin() - * s - * consts::TWO_SQRT_E_OVER_PI - * ((0.5 - x + GAMMA_R) / f64::consts::E).powf(0.5 - x)) + / (sin_pi(x) * s * consts::TWO_SQRT_E_OVER_PI * lanczos_power(pw, pw_err, pw)) } else { - let s = GAMMA_DK - .iter() - .enumerate() - .skip(1) - .fold(GAMMA_DK[0], |s, t| s + t.1 / (x + t.0 as f64 - 1.0)); - - s * consts::TWO_SQRT_E_OVER_PI * ((x - 0.5 + GAMMA_R) / f64::consts::E).powf(x - 0.5) + let s = lanczos_sum(x); + + // x - 0.5 is exact for 0.5 <= x < 2^52 (same or finer grid) + let p = x - 0.5; + if p > 168.0 { + // `lanczos_power` alone overflows from about x = 169.7, while the + // full product stays representable up to x ~ 171.61 (the true + // overflow point of Gamma). Halve the exponent and square at the + // end so the intermediate stays in range; this costs a couple of + // ulp, well inside the approximation floor of the coefficient set. + let half = + s.sqrt() * consts::TWO_SQRT_E_OVER_PI.sqrt() * lanczos_power(p, 0.0, 0.5 * p); + return half * half; + } + s * consts::TWO_SQRT_E_OVER_PI * lanczos_power(p, 0.0, p) } } @@ -157,6 +459,76 @@ pub fn checked_gamma_li(a: f64, x: f64) -> Result { checked_gamma_lr(a, x).map(|x| x * gamma(a)) } +/// The Legendre continued fraction for `Q(a, x) / (x^a e^-x / Gamma(a))`, +/// valid for `x >= 1 && x > a`. Shared by [`checked_gamma_ur`] and +/// [`checked_ln_gamma_ur`]; the value is O(1/x)-ish and does not involve the +/// prefix, which is what makes a log-domain variant possible. +fn gamma_ur_fraction(a: f64, x: f64) -> f64 { + let eps = 0.000000000000001; + let big = 4503599627370496.0; + let big_inv = 2.22044604925031308085e-16; + + let mut y = 1.0 - a; + let mut z = x + y + 1.0; + let mut c = 0.0; + let mut pkm2 = 1.0; + let mut qkm2 = x; + let mut pkm1 = x + 1.0; + let mut qkm1 = z * x; + let mut ans = pkm1 / qkm1; + loop { + y += 1.0; + z += 2.0; + c += 1.0; + let yc = y * c; + let pk = pkm1 * z - pkm2 * yc; + let qk = qkm1 * z - qkm2 * yc; + + pkm2 = pkm1; + pkm1 = pk; + qkm2 = qkm1; + qkm1 = qk; + + if pk.abs() > big { + pkm2 *= big_inv; + pkm1 *= big_inv; + qkm2 *= big_inv; + qkm1 *= big_inv; + } + + if qk != 0.0 { + let r = pk / qk; + let t = ((ans - r) / r).abs(); + ans = r; + + if t <= eps { + break; + } + } + } + ans +} + +/// The all-positive series for `P(a, x) / (x^a e^-x / Gamma(a))`, valid for +/// `x <= 1 || x <= a`; returns `ans / a`. Shared by [`checked_gamma_lr`] and +/// [`checked_ln_gamma_lr`]. +fn gamma_lr_series(a: f64, x: f64) -> f64 { + let eps = 0.000000000000001; + let mut r2 = a; + let mut c2 = 1.0; + let mut ans2 = 1.0; + loop { + r2 += 1.0; + c2 *= x / r2; + ans2 += c2; + + if c2 / ans2 <= eps { + break; + } + } + ans2 / a +} + /// Computes the upper incomplete regularized gamma function /// `Q(a,x) = 1 / Gamma(a) * int(exp(-t)t^(a-1), t=0..x) for a > 0, x > 0` /// where `a` is the argument for the gamma function and @@ -196,58 +568,18 @@ pub fn checked_gamma_ur(a: f64, x: f64) -> Result { return Err(GammaFuncError::XInvalid); } - let eps = 0.000000000000001; - let big = 4503599627370496.0; - let big_inv = 2.22044604925031308085e-16; - if x < 1.0 || x <= a { return Ok(1.0 - gamma_lr(a, x)); } - let mut ax = a * x.ln() - x - ln_gamma(a); + // saddle-point form; see `ln_gamma_prefix` + let mut ax = ln_gamma_prefix(a, x); if ax < -709.78271289338399 { return if a < x { Ok(0.0) } else { Ok(1.0) }; } ax = ax.exp(); - let mut y = 1.0 - a; - let mut z = x + y + 1.0; - let mut c = 0.0; - let mut pkm2 = 1.0; - let mut qkm2 = x; - let mut pkm1 = x + 1.0; - let mut qkm1 = z * x; - let mut ans = pkm1 / qkm1; - loop { - y += 1.0; - z += 2.0; - c += 1.0; - let yc = y * c; - let pk = pkm1 * z - pkm2 * yc; - let qk = qkm1 * z - qkm2 * yc; - - pkm2 = pkm1; - pkm1 = pk; - qkm2 = qkm1; - qkm1 = qk; - - if pk.abs() > big { - pkm2 *= big_inv; - pkm1 *= big_inv; - qkm2 *= big_inv; - qkm1 *= big_inv; - } - - if qk != 0.0 { - let r = pk / qk; - let t = ((ans - r) / r).abs(); - ans = r; - - if t <= eps { - break; - } - } - } + let ans = gamma_ur_fraction(a, x); Ok(ans * ax) } @@ -301,7 +633,8 @@ pub fn checked_gamma_lr(a: f64, x: f64) -> Result { return Ok(0.0); } - let ax = a * x.ln() - x - ln_gamma(a); + // saddle-point form; see `ln_gamma_prefix` + let ax = ln_gamma_prefix(a, x); if ax < -709.78271289338399 { if a < x { return Ok(1.0); @@ -309,19 +642,7 @@ pub fn checked_gamma_lr(a: f64, x: f64) -> Result { return Ok(0.0); } if x <= 1.0 || x <= a { - let mut r2 = a; - let mut c2 = 1.0; - let mut ans2 = 1.0; - loop { - r2 += 1.0; - c2 *= x / r2; - ans2 += c2; - - if c2 / ans2 <= eps { - break; - } - } - return Ok(ax.exp() * ans2 / a); + return Ok(ax.exp() * gamma_lr_series(a, x)); } let mut y = 1.0 - a; @@ -368,6 +689,85 @@ pub fn checked_gamma_lr(a: f64, x: f64) -> Result { Ok(1.0 - ax.exp() * ans) } +/// Computes the natural logarithm of the upper incomplete regularized gamma +/// function, `ln Q(a, x)`, staying finite far past the point where `Q` itself +/// underflows. +/// +/// `gamma_ur` saturates to 0 once its prefix drops below ~exp(-709.78); the +/// log-domain form has no such cliff, e.g. `ln_gamma_ur(1.0, 1e6) == -1e6` +/// exactly where `gamma_ur` returns 0. Used by `ln_sf` implementations. +/// +/// # Panics +/// +/// if `a` or `x` are not in `(0, +inf)` +pub fn ln_gamma_ur(a: f64, x: f64) -> f64 { + checked_ln_gamma_ur(a, x).unwrap() +} + +/// Non-panicking variant of [`ln_gamma_ur`]. +/// +/// # Errors +/// +/// if `a` or `x` are not in `(0, +inf)` +pub fn checked_ln_gamma_ur(a: f64, x: f64) -> Result { + if a.is_nan() || x.is_nan() { + return Ok(f64::NAN); + } + if a <= 0.0 || a == f64::INFINITY { + return Err(GammaFuncError::AInvalid); + } + if x <= 0.0 || x == f64::INFINITY { + return Err(GammaFuncError::XInvalid); + } + + if x < 1.0 || x <= a { + // Q is not small in this region (the mass to the right of x is at + // least O(1/2) for x <= a), so the linear-domain value is accurate + // and `ln_1p` of it loses nothing. + return Ok((-gamma_lr(a, x)).ln_1p()); + } + // ln Q = ln(prefix) + ln(fraction): both stay representable long after + // `prefix.exp()` underflows. + Ok(ln_gamma_prefix(a, x) + gamma_ur_fraction(a, x).ln()) +} + +/// Computes the natural logarithm of the lower incomplete regularized gamma +/// function, `ln P(a, x)`, staying finite far past the point where `P` itself +/// underflows. Used by `ln_cdf` implementations; see [`ln_gamma_ur`]. +/// +/// # Panics +/// +/// if `a` or `x` are not in `(0, +inf)` +pub fn ln_gamma_lr(a: f64, x: f64) -> f64 { + checked_ln_gamma_lr(a, x).unwrap() +} + +/// Non-panicking variant of [`ln_gamma_lr`]. +/// +/// # Errors +/// +/// if `a` or `x` are not in `(0, +inf)` +pub fn checked_ln_gamma_lr(a: f64, x: f64) -> Result { + if a.is_nan() || x.is_nan() { + return Ok(f64::NAN); + } + if a <= 0.0 || a == f64::INFINITY { + return Err(GammaFuncError::AInvalid); + } + if x <= 0.0 || x == f64::INFINITY { + return Err(GammaFuncError::XInvalid); + } + + if x <= 1.0 || x <= a { + // the deep left tail: prefix and series in log domain + return Ok(ln_gamma_prefix(a, x) + gamma_lr_series(a, x).ln()); + } + // Here P = 1 - Q with Q accurate (and possibly tiny); exp may underflow to + // zero, in which case ln P correctly saturates to -0. + let ln_q = ln_gamma_prefix(a, x) + gamma_ur_fraction(a, x).ln(); + Ok((-ln_q.exp()).ln_1p()) +} + /// Computes the Digamma function which is defined as the derivative of /// the log of the gamma function. The implementation is based on /// "Algorithm AS 103", Jose Bernardo, Applied Statistics, Volume 25, Number 3 @@ -390,7 +790,11 @@ pub fn digamma(x: f64) -> f64 { return f64::NEG_INFINITY; } if x < 0.0 { - return digamma(1.0 - x) + f64::consts::PI / (-f64::consts::PI * x).tan(); + // Reflection formula `psi(x) = psi(1 - x) - PI / tan(PI * x)`, with the + // period reduction done by `tan_pi`. `(PI * x).tan()` evaluated directly + // lost up to ~6 decimal digits near the poles (5586 ulp at x = -12.72, + // 3e-7 relative at 1e-10 from a pole). + return digamma(1.0 - x) - f64::consts::PI / tan_pi(x); } if x <= s { return d1 - 1.0 / x + d2 * x; @@ -583,9 +987,11 @@ mod tests { 11.51291869289055371493077240324332039045238086972508869965363, epsilon = 1e-14 ); - assert_eq!( + prec::assert_relative_eq!( super::ln_gamma(1.000001e-2), - 4.599478872433667224554543378460164306444416156144779542513592 + 4.599478872433667224554543378460164306444416156144779542513592, + epsilon = 0.0, + max_relative = 1e-15 ); prec::assert_abs_diff_eq!( super::ln_gamma(0.1), @@ -883,7 +1289,7 @@ mod tests { prec::assert_abs_diff_eq!( gamma_li(5.5, 2.0), 1.5746265342113649473739798668921124454837064926448459, - epsilon = 1e-15 + epsilon = 2e-15 ); prec::assert_abs_diff_eq!( gamma_li(5.5, 8.0), @@ -1195,6 +1601,34 @@ mod tests { assert!(super::checked_gamma_ui(1.0, f64::INFINITY).is_err()); } + /// `digamma` has poles at the non-positive integers and returns -inf there. + /// The pole test is `prec::ulps_eq!(x.floor(), x)`, whose default epsilon + /// used to be `1e-9` absolute, so a whole neighbourhood around each pole + /// returned -inf instead of a large finite value. + #[test] + fn test_digamma_near_negative_integer_poles_is_finite() { + prec::assert_relative_eq!(digamma(-1.0 + f64::powi(2.0, -33)), -8589934591.5772156646, epsilon = 0.0, max_relative = 1e-13); + prec::assert_relative_eq!(digamma(-1.0 - f64::powi(2.0, -33)), 8589934592.4227843348, epsilon = 0.0, max_relative = 1e-13); + // the poles themselves are unchanged + assert_eq!(digamma(-1.0), f64::NEG_INFINITY); + assert_eq!(digamma(0.0), f64::NEG_INFINITY); + assert_eq!(digamma(-5.0), f64::NEG_INFINITY); + } + + /// Before `tan_pi` reduced the reflection argument by the period, these + /// lost 3-4 decimal digits because `(PI * x).tan()` was evaluated at a + /// large rounded argument. Reference values are mpmath at 40 significant + /// digits, computed at the exact `f64` of each literal. The tolerances + /// reflect the cancellation between the two reflection terms + /// (`psi(1 - x)` and `pi * cot(pi x)` are each ~2.6 at x = -12.72 and + /// cancel to ~0.017, amplifying their ~1 ulp errors ~150x). + #[test] + fn test_digamma_negative_arguments_far_from_zero() { + prec::assert_relative_eq!(digamma(-12.72), -0.0169824608177603739173, epsilon = 0.0, max_relative = 1e-13); + prec::assert_relative_eq!(digamma(-14.72), 0.1238386191670285663687, epsilon = 0.0, max_relative = 1e-14); + prec::assert_relative_eq!(digamma(-20.02), 52.95568424702714411621, epsilon = 0.0, max_relative = 1e-15); + } + // TODO: precision testing could be more accurate #[test] fn test_digamma() { @@ -1357,6 +1791,116 @@ mod tests { ); } + /// `gamma` must overflow to `+inf`, never `-inf`, and must not overflow + /// early. The first-order correction in `lanczos_power` is only a valid + /// perturbation while `|p * delta| << 1`; applying it unguarded let a + /// negative correction flip the sign for `x` past ~1e15. Separately, the + /// power alone overflows from about x = 169.7 while the full product stays + /// representable to x ~ 171.61. + #[test] + fn test_gamma_overflow_boundary_and_sign() { + // finite and positive right up to the true overflow point + for x in [168.0f64, 169.0, 169.7, 170.5, 171.0, 171.5, 171.6] { + let g = gamma(x); + assert!(g.is_finite() && g > 0.0, "gamma({x}) should be finite positive, got {g}"); + } + // mpmath at 30 digits + prec::assert_relative_eq!(gamma(171.6), 1.5858969096673029e308, epsilon = 0.0, max_relative = 1e-13); + prec::assert_relative_eq!(gamma(170.5), 5.5620924145599996e305, epsilon = 0.0, max_relative = 1e-13); + prec::assert_relative_eq!(gamma(169.7), 9.155822000376269e303, epsilon = 0.0, max_relative = 1e-13); + // and always +inf beyond it, at every scale + for x in [172.0f64, 200.0, 1e3, 1e6, 1e14, 1e15, 1e100, 1e300, f64::MAX] { + let g = gamma(x); + assert!(g.is_infinite() && g > 0.0, "gamma({x:e}) should be +inf, got {g}"); + } + } + + /// `bd0`'s direct branch formed `(x / np).ln()`, which underflows to + /// `ln(0) = -inf` when `x` is tiny and `np` huge. That propagated through + /// the beta prefix and made `beta_reg` return NaN (or silently 1.0 instead + /// of 0.0) for extreme parameter ratios. + #[test] + fn test_bd0_extreme_ratios() { + // x / np underflows; true value is ~np + let v = super::bd0(1e-300, 5e99); + prec::assert_relative_eq!(v, 5e99, epsilon = 0.0, max_relative = 1e-14); + // x / np overflows; true value is x*ln(x/np) - x + np + let v = super::bd0(5e99, 1e-300); + assert!(v.is_finite() && v > 0.0, "bd0(5e99, 1e-300) = {v}"); + // and it stays non-negative and finite across a wide ratio sweep + for ea in [-300i32, -100, -10, 0, 10, 100, 300] { + for eb in [-300i32, -100, -10, 0, 10, 100, 300] { + let (x, np) = (10f64.powi(ea), 10f64.powi(eb)); + let v = super::bd0(x, np); + assert!(v.is_finite() && v >= 0.0, "bd0(1e{ea}, 1e{eb}) = {v}"); + } + } + } + + /// `stirling_delta` lands in an exponent, so its *absolute* accuracy is what + /// matters. References are mpmath at 40 significant digits; the tolerances + /// sit just above the measured error (`~1e-15` below the series threshold, + /// essentially exact above it). + #[test] + fn test_stirling_delta() { + // below the series threshold the recurrence costs one rounding per step, + // so the bound is absolute; at or above it the series is limited only by + // its own evaluation, so a relative bound is the meaningful one + for (z, want) in [ + (0.5, 0.15342640972002734529), + (1.0, 0.08106146679532725822), + (2.5, 0.033162873519936287485), + (8.0, 0.010411265261972096497), + (15.5, 0.0053755990329268344936), + ] { + prec::assert_abs_diff_eq!(super::stirling_delta(z), want, epsilon = 2e-15); + } + for (z, want) in [ + (16.0, 0.0052076559196096404407), + (100.0, 0.00083333055563491468338), + (10000.0, 8.3333333305555555635e-6), + ] { + prec::assert_relative_eq!( + super::stirling_delta(z), + want, + epsilon = 0.0, + max_relative = 1e-15 + ); + } + // consistency with the defining identity, away from the recurrence + for z in [20.0f64, 63.5, 500.0] { + let want = ln_gamma(z) - ((z - 0.5) * z.ln() - z + 0.5 * f64::consts::TAU.ln()); + prec::assert_abs_diff_eq!(super::stirling_delta(z), want, epsilon = 1e-13); + } + } + + /// `bd0` must stay *relatively* accurate right through its double root at + /// `x == np`, which is the whole point of the series form. + #[test] + fn test_bd0() { + for (x, np, want) in [ + (10000.0, 10000.0, 0.0), + (10000.0, 10001.0, 0.000049996666916646668333), + (10000.0, 15000.0, 945.34891891835618022), + (1.0, 2.0, 0.30685281944005469058), + (600123.0, 600000.0, 0.012606638575794171215), + ] { + let got = super::bd0(x, np); + if want == 0.0 { + assert_eq!(got, 0.0); + } else { + prec::assert_relative_eq!(got, want, epsilon = 0.0, max_relative = 1e-14); + } + } + // non-negative with a root only at x == np, and 0 * ln 0 == 0 at x == 0 + assert_eq!(super::bd0(0.0, 7.5), 7.5); + for i in 1..200 { + let x = 1000.0 + i as f64; + assert!(super::bd0(x, 1000.0) > 0.0); + assert!(super::bd0(1000.0, x) > 0.0); + } + } + #[test] fn test_error_is_sync_send() { fn assert_sync_send() {} diff --git a/src/prec.rs b/src/prec.rs index ebf1b6d4..c174598c 100644 --- a/src/prec.rs +++ b/src/prec.rs @@ -29,6 +29,7 @@ //! - `DEFAULT_RELATIVE_ACC`: 1e-14 for relative comparisons //! - `DEFAULT_EPS`: 1e-9 for absolute comparisons //! - `DEFAULT_ULPS`: 5 for ULPs comparisons +//! - `DEFAULT_ULPS_EPS`: `f64::EPSILON`, the absolute floor paired with `DEFAULT_ULPS` //! //! These defaults should be used unless there is a specific reason to use different //! precision levels. @@ -62,6 +63,16 @@ pub const DEFAULT_EPS: f64 = 1e-9; /// Default and target ULPs accuracy for f64 operations pub const DEFAULT_ULPS: u32 = 5; +/// Default absolute epsilon for ULPs comparisons. +/// +/// `approx`'s `ulps_eq` short-circuits on `abs_diff_eq(epsilon)` before it looks +/// at the ULPs distance, so pairing it with [`DEFAULT_EPS`] (`1e-9`) would make +/// the ULPs bound unreachable and turn `ulps_eq!(x, y)` into a `1e-9` absolute +/// comparison. `ulps_eq!` is used inside the crate to recognise exact parameter +/// values (`p == 1.0`, `x == x.floor()`), so its epsilon has to stay at the +/// scale of a genuine rounding error. +pub const DEFAULT_ULPS_EPS: f64 = f64::EPSILON; + /// Compares if two floats are close via `approx::abs_diff_eq` /// using a maximum absolute difference (epsilon) of `acc`. #[deprecated(since = "0.19.0", note = "Use abs_diff_eq! macro instead")] @@ -81,6 +92,68 @@ pub(crate) fn convergence(x: &mut f64, x_new: f64) -> bool { res } +/// Splits the exact rounding error out of the product `a * b` +/// (Dekker's algorithm): `a * b == p + dekker_product_err(a, b, p)` exactly. +/// +/// Used instead of `f64::mul_add`, which falls back to a slow software FMA on +/// targets without the hardware instruction (e.g. baseline x86-64). +#[inline] +pub(crate) fn dekker_product_err(a: f64, b: f64, p: f64) -> f64 { + const SPLIT: f64 = 134_217_729.0; // 2^27 + 1 + // Veltkamp's split below multiplies by `SPLIT`, which overflows to `inf` + // once an argument passes about `1.3e300` and then yields `inf - inf`, i.e. + // NaN. Scaling by a power of two is exact and the residual scales with it, + // so rescale rather than give up: `err(a b) = 2^k err((a 2^-k) b)`. + const BIG: f64 = 1e300; + const DOWN: f64 = 9.313225746154785e-10; // 2^-30, exact + let (mut a, mut b, mut p) = (a, b, p); + let mut scale = 1.0; + if a.abs() > BIG { + a *= DOWN; + p *= DOWN; + scale /= DOWN; + } + if b.abs() > BIG { + b *= DOWN; + p *= DOWN; + scale /= DOWN; + } + let ca = SPLIT * a; + let a_hi = ca - (ca - a); + let a_lo = a - a_hi; + let cb = SPLIT * b; + let b_hi = cb - (cb - b); + let b_lo = b - b_hi; + scale * (((a_hi * b_hi - p) + a_hi * b_lo + a_lo * b_hi) + a_lo * b_lo) +} + +/// Knuth's two-sum for a difference: returns `(s, e)` with +/// `a - b == s + e` exactly, where `s = a - b` rounded. +#[inline] +pub(crate) fn two_diff(a: f64, b: f64) -> (f64, f64) { + let s = a - b; + if !s.is_finite() { + // the residual is not representable; 0 is the safe choice, and the + // alternative is `inf - inf == NaN` poisoning every caller + return (s, 0.0); + } + let v = s - a; + (s, (a - (s - v)) + (-b - v)) +} + +/// Knuth's two-sum: returns `(s, e)` with `a + b == s + e` exactly, where +/// `s = a + b` rounded. +#[inline] +pub(crate) fn two_sum(a: f64, b: f64) -> (f64, f64) { + let s = a + b; + if !s.is_finite() { + // see `two_diff` + return (s, 0.0); + } + let v = s - a; + (s, (a - (s - v)) + (b - v)) +} + macro_rules! redefine_one_opt_approx_macro { ( $approx_macro:ident, @@ -144,7 +217,7 @@ mod macros { ); redefine_two_opt_approx_macro!( ulps_eq, - { epsilon: crate::prec::DEFAULT_EPS, max_ulps: crate::prec::DEFAULT_ULPS } + { epsilon: crate::prec::DEFAULT_ULPS_EPS, max_ulps: crate::prec::DEFAULT_ULPS } ); pub(crate) use abs_diff_eq; @@ -162,7 +235,7 @@ mod macros { ); redefine_two_opt_approx_macro!( assert_ulps_eq, - { epsilon: crate::prec::DEFAULT_EPS, max_ulps: crate::prec::DEFAULT_ULPS } + { epsilon: crate::prec::DEFAULT_ULPS_EPS, max_ulps: crate::prec::DEFAULT_ULPS } ); pub(crate) use assert_abs_diff_eq; diff --git a/tests/ln_cdf_sf.rs b/tests/ln_cdf_sf.rs new file mode 100644 index 00000000..6584ce17 --- /dev/null +++ b/tests/ln_cdf_sf.rs @@ -0,0 +1,171 @@ +//! Integration tests for `ln_cdf` / `ln_sf` (statrs-dev/statrs#338). +//! +//! Two properties are pinned: +//! 1. consistency: where `cdf`/`sf` are comfortably representable, the log +//! variants agree with `cdf().ln()` to ~1e-13 relative; +//! 2. tail accuracy: far past the underflow point of the probability (where +//! the default `cdf().ln()` is `-inf`), the overrides match mpmath +//! references computed at 60 significant digits. + +use approx::assert_relative_eq; +use statrs::distribution::*; + +/// The log stays within `tol` (relative on the log, absolute near zero) of the +/// linear-domain value wherever the latter is representable and not too close +/// to a hard 0 or 1. +fn consistent (f64, f64)>(pairs: F, xs: &[f64]) { + for &x in xs { + let (lin, log) = pairs(x); + if lin > 1e-290 && lin < 1.0 { + assert_relative_eq!(lin.ln(), log, epsilon = 1e-12, max_relative = 1e-12); + } + } +} + +#[test] +fn ln_cdf_ln_sf_consistent_with_linear_domain() { + let n = Normal::new(0.5, 2.0).unwrap(); + consistent( + |x| (n.cdf(x), n.ln_cdf(x)), + &[-50.0, -8.0, -1.0, 0.5, 3.0, 40.0], + ); + consistent(|x| (n.sf(x), n.ln_sf(x)), &[-40.0, -3.0, 0.5, 8.0, 50.0]); + + let g = Gamma::new(5.5, 2.0).unwrap(); + consistent(|x| (g.cdf(x), g.ln_cdf(x)), &[1e-6, 0.5, 2.75, 10.0, 100.0]); + consistent(|x| (g.sf(x), g.ln_sf(x)), &[1e-6, 0.5, 2.75, 10.0, 100.0]); + + let c = ChiSquared::new(3.0).unwrap(); + consistent(|x| (c.cdf(x), c.ln_cdf(x)), &[0.01, 1.0, 3.0, 30.0, 300.0]); + consistent(|x| (c.sf(x), c.ln_sf(x)), &[0.01, 1.0, 3.0, 30.0, 300.0]); + + let b = Beta::new(3.5, 8.0).unwrap(); + consistent(|x| (b.cdf(x), b.ln_cdf(x)), &[1e-8, 0.1, 0.3, 0.7, 0.999]); + consistent(|x| (b.sf(x), b.ln_sf(x)), &[0.001, 0.1, 0.3, 0.7, 0.999]); + + let e = Exp::new(2.0).unwrap(); + consistent(|x| (e.cdf(x), e.ln_cdf(x)), &[1e-10, 0.5, 5.0, 300.0]); + consistent(|x| (e.sf(x), e.ln_sf(x)), &[1e-10, 0.5, 5.0, 300.0]); + + let w = Weibull::new(1.5, 2.0).unwrap(); + consistent(|x| (w.cdf(x), w.ln_cdf(x)), &[0.001, 1.0, 5.0, 50.0]); + consistent(|x| (w.sf(x), w.ln_sf(x)), &[0.001, 1.0, 5.0, 50.0]); + + let p = Pareto::new(1.0, 3.0).unwrap(); + consistent(|x| (p.cdf(x), p.ln_cdf(x)), &[1.001, 2.0, 100.0, 1e50]); + consistent(|x| (p.sf(x), p.ln_sf(x)), &[1.001, 2.0, 100.0, 1e50]); + + let ln = LogNormal::new(0.0, 1.0).unwrap(); + consistent( + |x| (ln.cdf(x), ln.ln_cdf(x)), + &[1e-10, 0.1, 1.0, 10.0, 1e10], + ); + consistent(|x| (ln.sf(x), ln.ln_sf(x)), &[1e-10, 0.1, 1.0, 10.0, 1e10]); + + let bi = Binomial::new(0.3, 1000).unwrap(); + let po = Poisson::new(50.0).unwrap(); + let ge = Geometric::new(0.05).unwrap(); + for k in [1u64, 10, 100, 250, 400, 900] { + for (lin, log) in [ + (bi.cdf(k), bi.ln_cdf(k)), + (bi.sf(k), bi.ln_sf(k)), + (po.cdf(k), po.ln_cdf(k)), + (po.sf(k), po.ln_sf(k)), + (ge.cdf(k), ge.ln_cdf(k)), + (ge.sf(k), ge.ln_sf(k)), + ] { + if lin > 1e-290 && lin < 1.0 { + assert_relative_eq!(lin.ln(), log, epsilon = 1e-12, max_relative = 1e-12); + } + } + } +} + +/// References are mpmath at 60 significant digits. Every `cdf`/`sf` here is a +/// hard 0 in f64, so the default `cdf().ln()` returns `-inf`; the overrides +/// carry the probability's relative accuracy (~1e-15) into the log domain. +#[test] +fn ln_cdf_ln_sf_deep_tails_match_references() { + let n = Normal::standard(); + assert_eq!(n.sf(39.0), 0.0, "premise: sf underflows here"); + assert_relative_eq!(n.ln_sf(39.0), -765.0831565643775, max_relative = 1e-14); + assert_relative_eq!(n.ln_sf(100.0), -5005.524208694205, max_relative = 1e-14); + assert_relative_eq!(n.ln_sf(1000.0), -500007.82669481216, max_relative = 1e-14); + assert_relative_eq!(n.ln_cdf(-39.0), -765.0831565643775, max_relative = 1e-14); + + let c = ChiSquared::new(1.0).unwrap(); + assert_eq!(c.sf(1500.0), 0.0, "premise: sf underflows here"); + assert_relative_eq!(c.ln_sf(1500.0), -753.8830671053825, max_relative = 1e-14); + assert_relative_eq!(c.ln_sf(5000.0), -2504.484587848451, max_relative = 1e-14); + + let g = Gamma::new(5.5, 2.0).unwrap(); + assert_relative_eq!(g.ln_sf(500.0), -972.8684095883499, max_relative = 1e-14); + assert_relative_eq!(g.ln_cdf(1e-6), -77.83556232788861, max_relative = 1e-14); + + let po = Poisson::new(1000.0).unwrap(); + assert_relative_eq!(po.ln_cdf(100), -672.8586102872655, max_relative = 1e-13); + + let bi = Binomial::new(0.5, 10_000).unwrap(); + assert_eq!(bi.cdf(1000), 0.0, "premise: cdf underflows here"); + assert_relative_eq!(bi.ln_cdf(1000), -3684.8445400592873, max_relative = 1e-13); + + let be = Beta::new(3.5, 8.0).unwrap(); + assert_relative_eq!(be.ln_cdf(1e-30), -236.4583322197154, max_relative = 1e-14); + + let lo = LogNormal::new(0.0, 1.0).unwrap(); + assert_relative_eq!(lo.ln_cdf(1e-100), -26515.84871241671, max_relative = 1e-14); + + // exact closed forms + let e = Exp::new(2.0).unwrap(); + assert_eq!(e.ln_sf(400.0), -800.0); + assert_eq!(e.ln_sf(1e8), -2e8); + let pa = Pareto::new(1.0, 3.0).unwrap(); + assert_relative_eq!( + pa.ln_sf(1e100), + -300.0 * core::f64::consts::LN_10, + max_relative = 1e-15 + ); + let ge = Geometric::new(0.01).unwrap(); + assert_relative_eq!( + ge.ln_sf(200_000), + 200_000.0 * (-0.01f64).ln_1p(), + max_relative = 1e-15 + ); +} + +/// Distributions without an override fall back to `cdf().ln()`; check the +/// default exists and behaves. +#[test] +fn ln_cdf_default_impl_works() { + let u = Uniform::new(0.0, 1.0).unwrap(); + assert_relative_eq!(u.ln_cdf(0.25), 0.25f64.ln(), max_relative = 1e-15); + assert_relative_eq!(u.ln_sf(0.25), 0.75f64.ln(), max_relative = 1e-15); + let t = StudentsT::new(0.0, 1.0, 5.0).unwrap(); + assert_relative_eq!(t.ln_cdf(1.0), t.cdf(1.0).ln(), max_relative = 1e-15); +} + +/// Boundary semantics: logs of exact 0 and 1. +#[test] +fn ln_cdf_ln_sf_boundaries() { + let n = Normal::standard(); + assert_eq!(n.ln_cdf(f64::NEG_INFINITY), f64::NEG_INFINITY); + assert_eq!(n.ln_cdf(f64::INFINITY), 0.0); + assert_eq!(n.ln_sf(f64::NEG_INFINITY), 0.0); + assert_eq!(n.ln_sf(f64::INFINITY), f64::NEG_INFINITY); + + let bi = Binomial::new(0.3, 10).unwrap(); + assert_eq!(bi.ln_cdf(10), 0.0); + assert_eq!(bi.ln_sf(10), f64::NEG_INFINITY); + + let e = Exp::new(2.0).unwrap(); + assert_eq!(e.ln_cdf(-1.0), f64::NEG_INFINITY); + assert_eq!(e.ln_sf(-1.0), 0.0); + + let w = Weibull::new(1.5, 2.0).unwrap(); + assert_eq!(w.ln_cdf(-1.0), f64::NEG_INFINITY); + assert_eq!(w.ln_sf(-1.0), 0.0); + + let pa = Pareto::new(1.0, 3.0).unwrap(); + assert_eq!(pa.ln_cdf(0.5), f64::NEG_INFINITY); + assert_eq!(pa.ln_sf(0.5), 0.0); +}