Conversation
`x^2` underflows to `0` for small `x`, so the function returned `-Infinity` although the log-CDF is `ln(x^2/(2*sigma^2))` to working precision there. Return `2*ln(x) - ln(2*sigma^2)` once it is below `-36`.
Coverage Report
The above coverage report was generated for the changes in this PR. |
| return NINF; | ||
| } | ||
| s2 = pow( sigma, 2.0 ); | ||
| lt = ( 2.0*ln( x ) ) - ln( 2.0*s2 ); |
There was a problem hiding this comment.
| lt = ( 2.0*ln( x ) ) - ln( 2.0*s2 ); | |
| r = x / sigma; | |
| if ( r < FLOAT64_SMALLEST_NORMAL ) { | |
| lt = ( 2.0*( ln( x ) - ln( sigma ) ) ) - LN2; | |
| } else { | |
| lt = ( 2.0*ln( r ) ) - LN2; | |
| } |
2*ln(x) - ln(2*s2) cancels when sigma is far from 1. logcdf( 1e-108, 1e-100 ) is now 5 ULP off (correctly rounded on develop). logcdf( 1e-170, 1e-160 ) is wrong because sigma^2 is subnormal, and logcdf( 1e-300, 1e200 ) still returns -Infinity. With x/sigma, as in #15797, all of these are within 1 ULP of mpmath for sigma in [1e-300, 1e300].
Same in lib/factory.js:82. Needs var r;, @stdlib/constants/float64/ln-two and @stdlib/constants/float64/smallest-normal.
| return STDLIB_CONSTANT_FLOAT64_NINF; | ||
| } | ||
| s2 = stdlib_base_pow( sigma, 2.0 ); | ||
| lt = ( 2.0 * stdlib_base_ln( x ) ) - stdlib_base_ln( 2.0 * s2 ); |
There was a problem hiding this comment.
| lt = ( 2.0 * stdlib_base_ln( x ) ) - stdlib_base_ln( 2.0 * s2 ); | |
| r = x / sigma; | |
| if ( r < STDLIB_CONSTANT_FLOAT64_SMALLEST_NORMAL ) { | |
| lt = ( 2.0 * ( stdlib_base_ln( x ) - stdlib_base_ln( sigma ) ) ) - STDLIB_CONSTANT_FLOAT64_LN2; | |
| } else { | |
| lt = ( 2.0 * stdlib_base_ln( r ) ) - STDLIB_CONSTANT_FLOAT64_LN2; | |
| } |
Same as lib/main.js:85. Also needs double r;, the ln_two.h/smallest_normal.h includes, and both deps in manifest.json.
| x = exp10.( range( -300.0, stop = -2.0, length = 1000 ) ); | ||
| sigma = ( rand( 1000 ) .* 10.0 ) .+ 0.1; |
There was a problem hiding this comment.
| x = exp10.( range( -300.0, stop = -2.0, length = 1000 ) ); | |
| sigma = ( rand( 1000 ) .* 10.0 ) .+ 0.1; | |
| sigma = exp10.( ( rand( 1000 ) .* 200.0 ) .- 100.0 ); | |
| x = sigma .* exp10.( range( -150.0, stop = -4.0, length = 1000 ) ); |
With sigma in [0.1, 10.1], this fixture can't catch the cancellation above. With this change, the current code fails 49/1000 points at 1 ULP and the x/sigma version passes all of them.
Co-authored-by: 0PrashantYadav0 <0PrashantYadav0@users.noreply.github.com>
|
@0PrashantYadav0 thanks, applied in 0dbba04:
On the fixtures: yes, that was intentional. The old Distributions fixtures here never reach the new branch ( |
|
@Abhist17 Consistency is preferred but we have to discuss this with others first ( to find out which one is required according to our needs ) |
Description
This pull request:
x^2for smallxinstats/base/dists/rayleigh/logcdf(JS, factory and C).For
xbelow about1e-154,x^2underflows to0and the function returns-Infinity, although the log-CDF is finite there:With
t = x^2/(2*sigma^2),ln(1 - e^{-t}) = ln(t) + ln(1 - t/2 + ...). Onceln(t) = 2*ln(x) - ln(2*sigma^2)is below-36, the correction is less than half an ULP ofln(t). So the function returnsln(t)there, which never underflows. Above it, the existinglog1p/expm1paths are unchanged, andx = 0still gives-Infinity. This is the same change as #15797 forweibull/logcdf.Tests:
runner.jlgains agen_smallfunction. It evaluates the textbook formula in 2048-bitBigFloat(written withexpm1, so it stays exact at that precision) and writessmall_x.json, withxlog-spaced over[1e-300, 0.01]. The new tests pass at 1 ULP in JS and native. Ondevelop, 487 of the 4,011 assertions intest.logcdf.jsfail, most of them returning-Infinity. The existing fixtures never reach the new branch and still pass at 0 ULP.Related Issues
None. Related to #15772 (
rayleigh/cdf).Questions
No.
Other
No.
Checklist
AI Assistance
If you answered "yes" above, how did you use AI assistance?
Disclosure
This PR was written primarily by Claude Code. I found the bug by evaluating every
stats/base/distslogcdf just above the lower end of its support.@stdlib-js/reviewers