Skip to content

Commit dfe65e0

Browse files
authored
Merge branch 'main' into feat/fix-beta-inverse-extremes
2 parents b793093 + b152c47 commit dfe65e0

4 files changed

Lines changed: 403 additions & 21 deletions

File tree

src/distribution/gamma.rs

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -361,13 +361,10 @@ impl Continuous<f64, f64> for Gamma {
361361
0.0
362362
} else if self.shape == 1.0 {
363363
self.rate * (-self.rate * x).exp()
364-
} else if self.shape > 160.0 {
365-
self.ln_pdf(x).exp()
366364
} else if x.is_infinite() {
367365
0.0
368366
} else {
369-
self.rate.powf(self.shape) * x.powf(self.shape - 1.0) * (-self.rate * x).exp()
370-
/ gamma::gamma(self.shape)
367+
self.ln_pdf(x).exp()
371368
}
372369
}
373370

@@ -392,15 +389,29 @@ impl Continuous<f64, f64> for Gamma {
392389
f64::NEG_INFINITY
393390
} else if self.shape == 1.0 {
394391
self.rate.ln() - self.rate * x
392+
} else if x == 0.0 {
393+
if self.rate.is_infinite() {
394+
f64::NAN
395+
} else if self.shape < 1.0 {
396+
f64::INFINITY
397+
} else {
398+
f64::NEG_INFINITY
399+
}
395400
} else if x.is_infinite() {
396401
f64::NEG_INFINITY
402+
} else if self.rate.is_infinite() {
403+
f64::NAN
397404
} else {
398-
self.shape * self.rate.ln() + (self.shape - 1.0) * x.ln()
405+
let (m1, e1) = prec::frexp(self.rate);
406+
let (m2, e2) = prec::frexp(x);
407+
let ln_product = (m1 * m2).ln() + (e1 + e2) as f64 * core::f64::consts::LN_2;
408+
(self.shape - 1.0) * ln_product + self.rate.ln()
399409
- self.rate * x
400410
- gamma::ln_gamma(self.shape)
401411
}
402412
}
403413
}
414+
404415
/// Samples from a gamma distribution with a shape of `shape` and a
405416
/// rate of `rate` using `rng` as the source of randomness. Implementation from:
406417
///
@@ -610,6 +621,48 @@ mod tests {
610621
// (10.0, f64::INFINITY, f64::INFINITY, 0.0, pdf(f64::INFINITY)),];
611622
}
612623

624+
#[test]
625+
fn test_pdf_with_underflowing_rate_power() {
626+
let expected = 4.455666577035095e-7;
627+
test_absolute(80.0, 1e-5, expected, expected * 2e-13, |dist| dist.pdf(8e6));
628+
}
629+
630+
#[test]
631+
fn test_pdf_ln_pdf_near_unit_rate_x_product() {
632+
// `rate` and `x` differ by many orders of magnitude while their product
633+
// stays near 1.0. Computing `ln(rate) + ln(x)` directly, even with a
634+
// two-sum compensation, loses accuracy here because each term is
635+
// individually huge and they nearly cancel. Reference values are from
636+
// mpmath at 60 digits of precision.
637+
let cases = [
638+
(
639+
10.0,
640+
9124.13416510371,
641+
0.00020282465462814574,
642+
0.0058375360020384311786,
643+
1.0058546076178841054,
644+
),
645+
(
646+
10.0,
647+
147313129.0117983,
648+
3.6626428448745405e-9,
649+
-0.086400524503597070522,
650+
0.91722678583654005927,
651+
),
652+
(
653+
10.0,
654+
218565082552.56137,
655+
1.0763838473319353e-12,
656+
0.04968343200871080629,
657+
1.0509383502679062755,
658+
),
659+
];
660+
for (shape, rate, x, expected_ln_pdf, expected_pdf) in cases {
661+
test_absolute(shape, rate, expected_ln_pdf, 1.5e-14, |dist| dist.ln_pdf(x));
662+
test_absolute(shape, rate, expected_pdf, 1.5e-14, |dist| dist.pdf(x));
663+
}
664+
}
665+
613666
#[test]
614667
fn test_pdf_at_zero() {
615668
test_relative(1.0, 0.1, 0.1, |x| x.pdf(0.0));
@@ -624,6 +677,28 @@ mod tests {
624677
test_exact(1.0 - 5e-10, 1.0, f64::INFINITY, |dist| dist.ln_pdf(0.0));
625678
}
626679

680+
#[test]
681+
fn test_pdf_at_zero_with_infinite_shape() {
682+
test_exact(f64::INFINITY, 1.0, 0.0, |dist| dist.pdf(0.0));
683+
test_exact(f64::INFINITY, 1.0, f64::NEG_INFINITY, |dist| dist.ln_pdf(0.0));
684+
}
685+
686+
#[test]
687+
fn test_pdf_at_zero_with_infinite_rate() {
688+
test_is_nan(0.5, f64::INFINITY, |dist| dist.pdf(0.0));
689+
test_is_nan(0.5, f64::INFINITY, |dist| dist.ln_pdf(0.0));
690+
test_is_nan(2.0, f64::INFINITY, |dist| dist.pdf(0.0));
691+
test_is_nan(2.0, f64::INFINITY, |dist| dist.ln_pdf(0.0));
692+
test_is_nan(1.0, f64::INFINITY, |dist| dist.pdf(0.0));
693+
test_is_nan(1.0, f64::INFINITY, |dist| dist.ln_pdf(0.0));
694+
}
695+
696+
#[test]
697+
fn test_pdf_with_infinite_rate() {
698+
test_is_nan(2.0, f64::INFINITY, |dist| dist.pdf(1.0));
699+
test_is_nan(2.0, f64::INFINITY, |dist| dist.ln_pdf(1.0));
700+
}
701+
627702
#[test]
628703
fn test_ln_pdf() {
629704
let f = |arg: f64| move |x: Gamma| x.ln_pdf(arg);

src/distribution/log_normal.rs

Lines changed: 173 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,20 @@ impl Continuous<f64, f64> for LogNormal {
336336
0.0
337337
} else {
338338
let d = (x.ln() - self.location) / self.scale;
339-
(-0.5 * d * d).exp() / (x * consts::SQRT_2PI * self.scale)
339+
let numer = (-0.5 * d * d).exp();
340+
// grouped so that a subnormal `x` is rounded once instead of twice
341+
let denom = x * (consts::SQRT_2PI * self.scale);
342+
if numer.is_normal() && denom.is_normal() {
343+
numer / denom
344+
} else {
345+
// Either end of the quotient has left the range where dividing is
346+
// accurate: `numer` underflows to zero, or to a subnormal holding
347+
// only a few significant bits, long before the density itself does,
348+
// and `denom` overflows to infinity or underflows for extreme `x`.
349+
// Both collapse the quotient to zero where the density is still a
350+
// representable f64, so divide in log space and exponentiate once.
351+
self.ln_pdf(x).exp()
352+
}
340353
}
341354
}
342355

@@ -354,8 +367,20 @@ impl Continuous<f64, f64> for LogNormal {
354367
if x <= 0.0 || x.is_infinite() {
355368
f64::NEG_INFINITY
356369
} else {
357-
let d = (x.ln() - self.location) / self.scale;
358-
(-0.5 * d * d) - consts::LN_SQRT_2PI - (x * self.scale).ln()
370+
let ln_x = x.ln();
371+
let d = (ln_x - self.location) / self.scale;
372+
let x_scale = x * self.scale;
373+
// `x * σ` overflows to infinity or underflows to zero at the extremes
374+
// of the support, and `ln` then hands back `±inf`, so `ln(xσ)` is split
375+
// into `ln(x) + ln(σ)` there. Everywhere else the product keeps its
376+
// full precision and is the more accurate of the two, because it only
377+
// rounds once and `ln(x)` and `ln(σ)` can be much larger than the sum.
378+
let ln_x_scale = if x_scale.is_normal() {
379+
x_scale.ln()
380+
} else {
381+
ln_x + self.scale.ln()
382+
};
383+
(-0.5 * d * d) - consts::LN_SQRT_2PI - ln_x_scale
359384
}
360385
}
361386
}
@@ -365,6 +390,7 @@ impl Continuous<f64, f64> for LogNormal {
365390
mod tests {
366391
use super::*;
367392
use crate::distribution::internal::density_util;
393+
use crate::prec;
368394

369395
testing_boiler!(location: f64, scale: f64; LogNormal; LogNormalError);
370396

@@ -579,7 +605,7 @@ mod tests {
579605
test_absolute(-0.1, 1.5, 0.90492497850024368541682348133921492204585092983646, 1e-15, pdf(0.1));
580606
test_absolute(-0.1, 1.5, 0.49191985207660942803818797602364034466489243416574, 1e-16, pdf(0.5));
581607
test_exact(-0.1, 1.5, 0.33133347214343229148978298237579567194870525187207, pdf(0.8));
582-
test_exact(-0.1, 2.5, 1.0824698632626565182080576574958317806389057196768, pdf(0.1));
608+
test_absolute(-0.1, 2.5, 1.0824698632626565182080576574958317806389057196768, 1e-15, pdf(0.1));
583609
test_absolute(-0.1, 2.5, 0.31029619474753883558901295436486123689563749784867, 1e-16, pdf(0.5));
584610
test_absolute(-0.1, 2.5, 0.19922929916156673799861939824205622734205083805245, 1e-16, pdf(0.8));
585611

@@ -594,7 +620,7 @@ mod tests {
594620
test_absolute(1.5, 1.5, 0.17185785323404088913982425377565512294017306418953, 1e-16, pdf(0.8));
595621
test_absolute(1.5, 2.5, 0.50186885259059181992025035649158160252576845315332, 1e-15, pdf(0.1));
596622
test_absolute(1.5, 2.5, 0.21721369314437986034957451699565540205404697589349, 1e-16, pdf(0.5));
597-
test_exact(1.5, 2.5, 0.15729636000661278918949298391170443742675565300598, pdf(0.8));
623+
test_absolute(1.5, 2.5, 0.15729636000661278918949298391170443742675565300598, 1e-16, pdf(0.8));
598624
test_exact(2.5, 0.1, 5.6836826548848916385760779034504046896805825555997e-500, pdf(0.1));
599625
test_absolute(2.5, 0.1, 3.1225608678589488061206338085285607881363155340377e-221, 1e-233, pdf(0.5));
600626
test_absolute(2.5, 0.1, 4.6994713794671660918554320071312374073172560048297e-161, 1e-173, pdf(0.8));
@@ -612,6 +638,148 @@ mod tests {
612638
test_exact(0.0, 1.0, 0.0, pdf(0.0));
613639
}
614640

641+
#[test]
642+
fn test_pdf_left_tail() {
643+
// Expected values are exact arithmetic at 80 decimal digits, rounded to
644+
// binary64, from pdf(x) = exp(-((ln x - mu)/sigma)^2 / 2) / (x * sigma * sqrt(2 pi)).
645+
// Not SciPy: SciPy computes this the same way we do, so it agrees with us
646+
// rather than checking us.
647+
// Each density is an ordinary (non-subnormal) f64, far above the
648+
// underflow threshold of the distribution itself.
649+
let cases = [
650+
(0.0, 15.0, 1e-252, 3.04796856373058623e-75),
651+
(0.0, 10.0, 2.24e-168, 4.60596725315129703e-158),
652+
(0.0, 5.0, 1.38e-87, 2.07338102578635509e-262),
653+
];
654+
655+
for (location, scale, x, expected) in cases {
656+
let d = create_ok(location, scale);
657+
let got = d.pdf(x);
658+
let rel = ((got - expected) / expected).abs();
659+
assert!(
660+
prec::relative_eq!(got, expected, epsilon = 0.0, max_relative = 1e-12),
661+
"LogNormal({location}, {scale}).pdf({x:e}): got {got:e}, expected {expected:e}, rel err {rel:e}"
662+
);
663+
}
664+
}
665+
666+
#[test]
667+
fn test_pdf_right_tail() {
668+
// The same expression covers the right tail. Expected values are exact
669+
// arithmetic at 60 decimal digits, rounded to binary64, not SciPy.
670+
// These are all reachable without exp(-d^2/2)
671+
// underflowing, so they pin the density down where it always worked.
672+
let cases = [
673+
(0.0, 1.0, 1e5, 6.5856159926167960017e-35),
674+
(0.0, 5.0, 1e30, 2.8537004754025084595e-73),
675+
(0.0, 15.0, 1e100, 1.8041024017455373079e-153),
676+
];
677+
678+
for (location, scale, x, expected) in cases {
679+
let got = create_ok(location, scale).pdf(x);
680+
prec::assert_relative_eq!(got, expected, epsilon = 0.0, max_relative = 1e-13);
681+
}
682+
683+
// further out the density itself underflows, and must stay exactly
684+
// zero rather than becoming a NaN or a spurious subnormal
685+
let pdf = |arg: f64| move |x: LogNormal| x.pdf(arg);
686+
test_exact(0.0, 15.0, 0.0, pdf(1e250));
687+
test_exact(0.0, 15.0, 0.0, pdf(1e300));
688+
test_exact(2.5, 10.0, 0.0, pdf(1e200));
689+
// exp(-d^2/2) underflows and x * sigma * sqrt(2 pi) overflows at the same
690+
// time here, which a naive inf / inf would turn into a NaN
691+
test_exact(0.0, 1.0, 0.0, pdf(f64::MAX));
692+
}
693+
694+
#[test]
695+
fn test_pdf_boundaries() {
696+
// x = 0.0 is covered by test_neg_pdf
697+
let pdf = |arg: f64| move |x: LogNormal| x.pdf(arg);
698+
test_exact(0.0, 1.0, 0.0, pdf(-1.0));
699+
test_exact(0.0, 1.0, 0.0, pdf(f64::NEG_INFINITY));
700+
test_exact(0.0, 1.0, 0.0, pdf(f64::INFINITY));
701+
test_is_nan(0.0, 1.0, pdf(f64::NAN));
702+
// an infinite scale is accepted by ::new, and spreads the density to zero
703+
test_exact(0.0, f64::INFINITY, 0.0, pdf(1.0));
704+
}
705+
706+
#[test]
707+
fn test_pdf_denominator_overflow() {
708+
// x * sigma * sqrt(2 pi) overflows to infinity for x near f64::MAX, which
709+
// collapses the quotient to zero even though exp(-d^2/2) is an ordinary
710+
// float and the density is still representable. Expected values are exact
711+
// arithmetic at 150 decimal digits on the binary64 inputs, rounded to
712+
// binary64, from pdf(x) = exp(-((ln x - mu)/sigma)^2 / 2) / (x * sigma * sqrt(2 pi)).
713+
// The density is subnormal, so 1e-12 is far tighter than the 1.0 that the
714+
// plain quotient is off by, and still loose enough for the log-space path,
715+
// whose absolute error is a few ulp of |ln_pdf| ~ 710.
716+
let got = create_ok(709.0, 1.0).pdf(f64::MAX);
717+
prec::assert_relative_eq!(got, 1.6336594696904199462e-309,
718+
epsilon = 0.0, max_relative = 1e-12);
719+
}
720+
721+
#[test]
722+
fn test_pdf_denominator_underflow() {
723+
// The mirror image: x * sigma * sqrt(2 pi) is subnormal for x near
724+
// f64::MIN_POSITIVE, so the quotient divides by a denominator that holds
725+
// only a handful of significant bits. Dividing directly is off by 2.8e-4
726+
// here. Expected value is exact arithmetic at 150 decimal digits on the
727+
// binary64 inputs. 1e-10 leaves room for the log-space path, which sums
728+
// terms of size |ln x| ~ 737, and for a platform `ln` or `exp` that is an
729+
// ulp or two off; the plain quotient misses by 2.8e-4 either way.
730+
let got = create_ok(-747.0, 0.5).pdf(1e-320);
731+
prec::assert_relative_eq!(got, 1.0375087109168858740e230,
732+
epsilon = 0.0, max_relative = 1e-10);
733+
}
734+
735+
#[test]
736+
fn test_pdf_guard_boundaries() {
737+
// Straddles both switches between the quotient and the log-space path, so
738+
// the density stays continuous across them. For location 0 and scale 1 the
739+
// numerator leaves the normal range just below x = 4.5e-17; for location
740+
// 709 and scale 1 the denominator overflows just above x = 7.17e307.
741+
// Expected values are exact arithmetic at 150 decimal digits on the
742+
// binary64 inputs, rounded to binary64.
743+
let cases = [
744+
(0.0, 1.0, 5e-17, 9.4703106003006510660e-291), // quotient
745+
(0.0, 1.0, 4e-17, 2.6606766015573320120e-294), // log space
746+
(709.0, 1.0, 7e307, 5.6262704857806850227e-309), // quotient
747+
(709.0, 1.0, 7.3e307, 5.4267255927382909466e-309), // log space
748+
];
749+
750+
for (location, scale, x, expected) in cases {
751+
let got = create_ok(location, scale).pdf(x);
752+
prec::assert_relative_eq!(got, expected, epsilon = 0.0, max_relative = 1e-12);
753+
}
754+
}
755+
756+
#[test]
757+
fn test_ln_pdf_extreme_x() {
758+
// x * sigma underflows to zero for subnormal x and overflows to infinity
759+
// for x near f64::MAX, and ln of either flips the sign of the result: the
760+
// first and last of these used to come back as +inf and -inf. The middle
761+
// one keeps x * sigma subnormal but nonzero, and has to stay put. Expected
762+
// values are exact arithmetic at 150 decimal digits on the binary64
763+
// inputs, rounded to binary64, from
764+
// ln_pdf(x) = -((ln x - mu)/sigma)^2 / 2 - ln(sqrt(2 pi)) - ln x - ln sigma.
765+
let cases = [
766+
(0.0, 0.25, 5e-324, -4432783.2580307411557),
767+
(0.0, 1.0, 1e-320, -270721.28315714487534),
768+
(0.0, 1e10, f64::MAX, -733.72750235652912883),
769+
];
770+
771+
for (location, scale, x, expected) in cases {
772+
let got = create_ok(location, scale).ln_pdf(x);
773+
prec::assert_relative_eq!(got, expected, epsilon = 0.0, max_relative = 1e-14);
774+
}
775+
776+
// an infinite scale is accepted by ::new; x * sigma is then infinite for
777+
// every x, and the log density is -inf rather than +inf
778+
let ln_pdf = |arg: f64| move |x: LogNormal| x.ln_pdf(arg);
779+
test_exact(0.0, f64::INFINITY, f64::NEG_INFINITY, ln_pdf(1.0));
780+
test_is_nan(0.0, 1.0, ln_pdf(f64::NAN));
781+
}
782+
615783
#[test]
616784
fn test_ln_pdf() {
617785
let ln_pdf = |arg: f64| move |x: LogNormal| x.ln_pdf(arg);

0 commit comments

Comments
 (0)