Repository navigation
Conversation
`ln( 1 - (1 - x^a)^b )` cancels twice: `1 - x^a` rounds away the low-order bits of a small `x^a`, and `1 - (1 - x^a)^b` loses the digits of a small CDF, as `ln` of a value close to `1` does for a CDF close to `1`. Evaluate `ln(1-x^a)` with `log1p` or `expm1`, depending on the size of `x^a`, and finish with `log1mexp`. Add fixtures generated from the textbook formula in extended precision.
Coverage Report
The above coverage report was generated for the changes in this PR. |
| u = pow( x, a ); | ||
| if ( u < 0.5 ) { | ||
| v = log1p( -u ); | ||
| } else { | ||
| v = ln( -expm1( a * ln( x ) ) ); | ||
| } | ||
| return log1mexp( b * v ); |
There was a problem hiding this comment.
| u = pow( x, a ); | |
| if ( u < 0.5 ) { | |
| v = log1p( -u ); | |
| } else { | |
| v = ln( -expm1( a * ln( x ) ) ); | |
| } | |
| return log1mexp( b * v ); | |
| u = pow( x, a ); | |
| if ( u < FLOAT64_SMALLEST_NORMAL ) { | |
| // `x^a` is subnormal or underflows, so `ln(1-(1-x^a)^b) = ln(b) + a*ln(x)`: | |
| return ln( b ) + ( a * ln( x ) ); | |
| } | |
| if ( u < 0.5 ) { | |
| v = log1p( -u ); | |
| } else { | |
| v = ln( -expm1( a * ln( x ) ) ); | |
| } | |
| t = b * v; | |
| if ( t > -FLOAT64_SMALLEST_NORMAL ) { | |
| return ln( b ) + ln( -v ); | |
| } | |
| return log1mexp( t ); |
On your question: yes, let's add it here, plus the b*v underflow (same as #15797 (comment)). logcdf( 1.0e-200, 3.0, 2.0 ), logcdf( 0.9, 1.0e5, 2.0 ) and logcdf( 1.0e-100, 2.0, 1.0e-200 ) return -Infinity (exact ≈ -1380.858, -10535.358, -921.034); with this they're within 2 ULP. Same in lib/factory.js:86-92 and src/main.c:59-65; needs var t; and FLOAT64_SMALLEST_NORMAL (plus the manifest.json dep).
| # Small `x` (with `a` small enough that `x^a` does not underflow): | ||
| x = exp10.( range( -100.0, stop = -1.0, length = 1000 ) ); | ||
| a = ( rand( 1000 ) .* 2.5 ) .+ 0.5; |
There was a problem hiding this comment.
| # Small `x` (with `a` small enough that `x^a` does not underflow): | |
| x = exp10.( range( -100.0, stop = -1.0, length = 1000 ) ); | |
| a = ( rand( 1000 ) .* 2.5 ) .+ 0.5; | |
| # Small `x`: | |
| x = exp10.( range( -300.0, stop = -1.0, length = 1000 ) ); | |
| a = ( rand( 1000 ) .* 5.0 ) .+ 0.5; |
So the fixture covers the underflow above. Needs setprecision( BigFloat, 8192 ) on line 36 (at 2048 bits 1 - x^a rounds to 1 for x^a < 2^-2048), and the small_x tolerance goes to 2 ULP.
…wamy/logcdf` Per review: when `x^a` is subnormal the log-CDF is `ln(b) + a*ln(x)`, and when `b*v` underflows it is `ln(b) + ln(-v)`. `logcdf( 1e-200, 3, 2 )`, `logcdf( 0.9, 1e5, 2 )` and `logcdf( 1e-100, 2, 1e-200 )` returned -Infinity. The small-`x` fixture now reaches `1e-300` with `a` up to 5.5, evaluated at 8192 bits, at 2 ULP.
|
@0PrashantYadav0 thanks — applied in c83cde0:
|
Description
This pull request:
stats/base/dists/kumaraswamy/logcdf(JS, factory and C),TODOfor.The function computed
ln( 1 - pow( 1 - pow( x, a ), b ) ):x^a,1 - x^arounds away its low-order bits, and1 - (1 - x^a)^bcancels. Forx^a < 2^-53the result is-Infinity.xclose to1,1 - x^acancels (x^awas already rounded), andlnof a value close to1loses the digits of a small result. It returns0once(1 - x^a)^b < 2^-53.The function now computes
v = ln(1 - x^a)aslog1p( -x^a )whenx^a < 0.5, and asln( -expm1( a*ln(x) ) )otherwise, so1 - x^ais never formed. It then returnslog1mexp( b*v ).Tests:
There's a new
test/fixtures/julia/runner.jlthat evaluates the textbook formulaln(1 - (1 - x^a)^b)in 2048-bitBigFloat. It writes three fixtures:data.json: randomx, withaandbin[0.5, 5.5],small_x.json:xlog-spaced over[1e-100, 0.1], witha <= 3so thatx^adoesn't underflow,large_x.json:1 - xlog-spaced over[1e-15, 0.1].The "evaluates the logcdf" tests only checked that the result is a number (
// TODO: Add fixtures). They are replaced with tests against the fixtures.Max error, JS and native, which sets the tolerances:
developdatasmall_x-Infinitylarge_x0The remaining error comes from rounding in
b * v. A 1-ULP change inbmoves the exact result by|b ln(1 - x^a)|ULP, which reaches ~150 at the far end oflarge_x. So this is about as close as double precision gets without extended arithmetic. Ondevelop, 2,140 of the 3,027 assertions intest.logcdf.jsfail. With this change they all pass.Related Issues
None. Companion to #15767 (
kumaraswamy/cdf).Questions
When
x^aunderflows (e.g.x = 1e-100,a = 5), the result is still-Infinity, as ondevelop. It could returnln(b) + a*ln(x)there. I left that out to keep this PR to the cancellation, but I'm happy to add it.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 reading the
stats/base/distsfunctions for1 - ucancellation.@stdlib-js/reviewers