@@ -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 {
365390mod 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