In the left tail, LogNormal::pdf(x) and ln_pdf(x).exp() on the same object disagree over two adjacent bands of x:
- a band where
pdf returns 0.0 and ln_pdf(x).exp() returns an ordinary f64,
- immediately above it, a band where
pdf returns a finite value that differs from the density by up to a factor of two.
Both bands sit well above the point where the density underflows, both widen with scale, and both move with location. scipy.stats.lognorm and mpmath agree with ln_pdf(x).exp() throughout.
Environment: statrs main on 128c9ae6d52c2be4574b6d8669136e344b87f65c, rustc 1.97.1, aarch64-apple-darwin.
Reproducer
use statrs::distribution::{Continuous, LogNormal};
fn main() {
// 1. pdf returns 0.0 where the density is an ordinary double
let d = LogNormal::new(0.0, 15.0).unwrap();
println!("{:e}\t{:e}", d.pdf(1e-252), d.ln_pdf(1e-252).exp());
// 2. just above that band, pdf returns a finite value about 1.9x the density
let d = LogNormal::new(0.0, 10.0).unwrap();
println!("{:e}\t{:e}", d.pdf(2.24e-168), d.ln_pdf(2.24e-168).exp());
// 3. the case this came out of
let d = LogNormal::new(0.0, 5.0).unwrap();
println!("{:e}\t{:e}", d.pdf(1.38e-87), d.ln_pdf(1.38e-87).exp());
}
0e0 3.047968563730997e-75
8.79927122410328e-158 4.605967253151374e-158
0e0 2.0733810257862521e-262
Expected
| case |
pdf |
scipy.stats.lognorm(s=sigma).pdf |
mpmath, mp.dps = 60 |
LogNormal(0, 15), x = 1e-252 |
0e0 |
3.047969e-75 |
3.047969e-75 |
LogNormal(0, 10), x = 2.24e-168 |
8.799271e-158 |
4.605967e-158 |
4.605967e-158 |
LogNormal(0, 5), x = 1.38e-87 |
0e0 |
2.073381e-262 |
2.073381e-262 |
As those above seems like "lucky" findings, I asked Claude to run a full sweep. What follows is generated by it.
Claude Opus 5.0 analysis
How far the two bands reach
LogNormal(0, sigma), scanning x at 200 points per power of 10. "Width" is the span of band 1 in powers of 10: 1e-96 to 1e-84 is 12 of them.
sigma |
band 1: pdf returns 0.0 |
width |
largest density it returns 0.0 for |
band 2: worst relative error |
1 |
6.4e-18 to 1.7e-17 |
0.4 |
3.886588e-308 |
0.91 at x = 1.72e-17 |
2 |
5.3e-36 to 2.9e-34 |
1.7 |
1.412756e-291 |
0.91 at x = 2.95e-34 |
3 |
5.2e-55 to 5.0e-51 |
4.0 |
5.904035e-275 |
0.91 at x = 5.07e-51 |
5 |
5.8e-96 to 1.5e-84 |
11.4 |
1.273613e-241 |
0.91 at x = 1.50e-84 |
7 |
4.7e-141 to 4.4e-118 |
23.0 |
3.161836e-208 |
0.91 at x = 4.42e-118 |
10 |
5.4e-217 to 2.2e-168 |
48.6 |
4.449485e-158 |
0.91 at x = 2.24e-168 |
15 |
5e-324 to 3.3e-252 |
71.8 or more |
1.975989e-74 |
0.97 at x = 3.31e-252 |
19 |
5e-324 to 2.9e-319 |
4.8 or more |
1.813587e-07 |
0.96 at x = 2.88e-319 |
Reading the sigma = 10 row: pdf returns 0.0 over 48.6 powers of 10, the largest density it calls zero is 4.4e-158, and the first x above that band, 2.24e-168, is the second line of the reproducer, where pdf returns 8.80e-158 against a true 4.61e-158.
Four notes on that scan:
- Band 2 is wide too. Measured out to where pdf comes back within 1e-9 relative, it spans 0.2 powers of 10 at
sigma = 1, 1.1 at sigma = 5, 2.3 at sigma = 10 and 3.4 at sigma = 15, starting at the top of band 1 and easing back to ordinary 1e-14 agreement above it.
- The
sigma = 15 and sigma = 19 bands run off the bottom of the f64 range, so those two widths are floors rather than measurements.
- Below band 1 the density really does underflow (
LogNormal(0, 5).pdf(1e-100) is about 2.4e-362), so 0.0 is the right answer there.
- Stepping
sigma by 0.01, the largest one with a band is 19.28, and there is none at 19.5 or above. Every sigma I sampled from 0.05 upward has one, but below sigma around 0.6 the affected densities are all under 1e-313, so that end looks immaterial in practice.
location moves both bands
(location, scale) |
band 1: pdf returns 0.0 |
width |
largest density it returns 0.0 for |
(0, 5) |
5.8e-96 to 1.5e-84 |
11.4 |
1.273613e-241 |
(-100, 5) |
8.2e-145 to 5.5e-128 |
16.8 |
3.394508e-198 |
(100, 5) |
9.4e-47 to 4.0e-41 |
5.6 |
4.778570e-285 |
Band 2 moves with it, with worst relative errors of 0.93 and 0.90 on the two shifted rows. With location = 100 the first x that returns 0.0 is 4.0e-41, much closer to ordinary magnitudes than any location = 0 case.
Where the same scan finds nothing
LogNormal(0, 5).pdf(1e84) and LogNormal(0, 10).pdf(1e200) both return 0.0, and mpmath puts both densities below the f64 range, so the right tail is fine. Normal::pdf for std_dev in {1e-3, 1, 100}, over the same grid across both tails, agrees with ln_pdf().exp() to 4.9e-14 or better everywhere.
Two more corners of the same tail, with subnormal x
Less likely to matter than the two bands, but they came out of the same scan:
| call |
returns |
mpmath, mp.dps = 60 |
LogNormal::new(0.0, 0.1).unwrap().pdf(5e-324) |
NaN |
density is 0 (log density -2.77e7) |
LogNormal::new(0.0, 0.1).unwrap().ln_pdf(1e-323) |
inf |
-2.765723e7 |
LogNormal::new(0.0, 20.0).unwrap().pdf(5e-324) |
4.736269e20 |
5.668494e20 |
The NaN and the inf show up for scale <= 0.5, and scipy.stats.lognorm returns inf for those same two calls, so mpmath is the only reference I have there. The third row is 16% off with no band 1 anywhere at that scale (same at scale = 50: 9.856067e272 against 1.179601e273), and ln_pdf().exp() matches mpmath to 2e-13 on both.
In the left tail,
LogNormal::pdf(x)andln_pdf(x).exp()on the same object disagree over two adjacent bands ofx:pdfreturns0.0andln_pdf(x).exp()returns an ordinaryf64,pdfreturns a finite value that differs from the density by up to a factor of two.Both bands sit well above the point where the density underflows, both widen with
scale, and both move withlocation.scipy.stats.lognormand mpmath agree withln_pdf(x).exp()throughout.Environment: statrs main on
128c9ae6d52c2be4574b6d8669136e344b87f65c, rustc 1.97.1, aarch64-apple-darwin.Reproducer
Expected
pdfscipy.stats.lognorm(s=sigma).pdfmp.dps = 60LogNormal(0, 15),x = 1e-2520e03.047969e-753.047969e-75LogNormal(0, 10),x = 2.24e-1688.799271e-1584.605967e-1584.605967e-158LogNormal(0, 5),x = 1.38e-870e02.073381e-2622.073381e-262As those above seems like "lucky" findings, I asked Claude to run a full sweep. What follows is generated by it.
Claude Opus 5.0 analysis
How far the two bands reach
LogNormal(0, sigma), scanningxat 200 points per power of 10. "Width" is the span of band 1 in powers of 10:1e-96to1e-84is 12 of them.sigmapdfreturns0.00.0for16.4e-18to1.7e-173.886588e-3080.91atx = 1.72e-1725.3e-36to2.9e-341.412756e-2910.91atx = 2.95e-3435.2e-55to5.0e-515.904035e-2750.91atx = 5.07e-5155.8e-96to1.5e-841.273613e-2410.91atx = 1.50e-8474.7e-141to4.4e-1183.161836e-2080.91atx = 4.42e-118105.4e-217to2.2e-1684.449485e-1580.91atx = 2.24e-168155e-324to3.3e-2521.975989e-740.97atx = 3.31e-252195e-324to2.9e-3191.813587e-070.96atx = 2.88e-319Reading the
sigma = 10row:pdfreturns0.0over 48.6 powers of 10, the largest density it calls zero is4.4e-158, and the firstxabove that band,2.24e-168, is the second line of the reproducer, wherepdfreturns8.80e-158against a true4.61e-158.Four notes on that scan:
sigma = 1, 1.1 atsigma = 5, 2.3 atsigma = 10and 3.4 atsigma = 15, starting at the top of band 1 and easing back to ordinary1e-14agreement above it.sigma = 15andsigma = 19bands run off the bottom of thef64range, so those two widths are floors rather than measurements.LogNormal(0, 5).pdf(1e-100)is about2.4e-362), so0.0is the right answer there.sigmaby0.01, the largest one with a band is19.28, and there is none at19.5or above. EverysigmaI sampled from0.05upward has one, but belowsigmaaround0.6the affected densities are all under1e-313, so that end looks immaterial in practice.locationmoves both bands(location, scale)pdfreturns0.00.0for(0, 5)5.8e-96to1.5e-841.273613e-241(-100, 5)8.2e-145to5.5e-1283.394508e-198(100, 5)9.4e-47to4.0e-414.778570e-285Band 2 moves with it, with worst relative errors of
0.93and0.90on the two shifted rows. Withlocation = 100the firstxthat returns0.0is4.0e-41, much closer to ordinary magnitudes than anylocation = 0case.Where the same scan finds nothing
LogNormal(0, 5).pdf(1e84)andLogNormal(0, 10).pdf(1e200)both return0.0, and mpmath puts both densities below thef64range, so the right tail is fine.Normal::pdfforstd_devin{1e-3, 1, 100}, over the same grid across both tails, agrees withln_pdf().exp()to4.9e-14or better everywhere.Two more corners of the same tail, with subnormal
xLess likely to matter than the two bands, but they came out of the same scan:
mp.dps = 60LogNormal::new(0.0, 0.1).unwrap().pdf(5e-324)NaN0(log density-2.77e7)LogNormal::new(0.0, 0.1).unwrap().ln_pdf(1e-323)inf-2.765723e7LogNormal::new(0.0, 20.0).unwrap().pdf(5e-324)4.736269e205.668494e20The
NaNand theinfshow up forscale <= 0.5, andscipy.stats.lognormreturnsinffor those same two calls, so mpmath is the only reference I have there. The third row is 16% off with no band 1 anywhere at thatscale(same atscale = 50:9.856067e272against1.179601e273), andln_pdf().exp()matches mpmath to2e-13on both.