Skip to content

Commit 248621c

Browse files
committed
fix: avoid underflow for small x in stats/base/dists/weibull/logcdf
`(x/lambda)^k` underflows to `0` for small `x`, so the function returned `-Infinity` although the log-CDF is `k*ln(x/lambda)` to working precision there. Return `k*ln(x/lambda)` once it is below `-36`. The fixtures are now generated from the textbook formula in extended precision.
1 parent e9f509b commit 248621c

12 files changed

Lines changed: 103 additions & 17 deletions

File tree

‎lib/node_modules/@stdlib/stats/base/dists/weibull/logcdf/lib/factory.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,19 @@ function factory( k, lambda ) {
7171
* // returns <number>
7272
*/
7373
function logcdf( x ) {
74+
var lt;
7475
var p;
7576
if ( isnan( x ) ) {
7677
return NaN;
7778
}
7879
if ( x < 0.0 ) {
7980
return NINF;
8081
}
82+
lt = k * ln( x / lambda );
83+
if ( lt < -36.0 ) {
84+
// For `t = (x/lambda)^k < e^-36`, `ln(1 - e^-t) = ln(t) + ln(1 - t/2 + ...)` and the correction is below half an ULP of `ln(t)`. Using `ln(t)` directly avoids `t` underflowing to `0`:
85+
return lt;
86+
}
8187
p = -pow( x / lambda, k );
8288
return ( p < LNHALF ) ? log1p( -exp( p ) ) : ln( -expm1( p ) );
8389
}

‎lib/node_modules/@stdlib/stats/base/dists/weibull/logcdf/lib/main.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ var NINF = require( '@stdlib/constants/float64/ninf' );
7373
* // returns NaN
7474
*/
7575
function logcdf( x, k, lambda ) {
76+
var lt;
7677
var p;
7778
if (
7879
isnan( k ) ||
@@ -85,6 +86,11 @@ function logcdf( x, k, lambda ) {
8586
if ( x < 0.0 ) {
8687
return NINF;
8788
}
89+
lt = k * ln( x / lambda );
90+
if ( lt < -36.0 ) {
91+
// For `t = (x/lambda)^k < e^-36`, `ln(1 - e^-t) = ln(t) + ln(1 - t/2 + ...)` and the correction is below half an ULP of `ln(t)`. Using `ln(t)` directly avoids `t` underflowing to `0`:
92+
return lt;
93+
}
8894
p = -pow( x / lambda, k );
8995
return ( p < LNHALF ) ? log1p( -exp( p ) ) : ln( -expm1( p ) );
9096
}

‎lib/node_modules/@stdlib/stats/base/dists/weibull/logcdf/src/main.c‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
* // returns ~-0.145
4040
*/
4141
double stdlib_base_dists_weibull_logcdf( const double x, const double k, const double lambda ) {
42+
double lt;
4243
double p;
4344
if (
4445
stdlib_base_is_nan( x ) ||
@@ -51,6 +52,11 @@ double stdlib_base_dists_weibull_logcdf( const double x, const double k, const d
5152
if ( x < 0.0 ) {
5253
return STDLIB_CONSTANT_FLOAT64_NINF;
5354
}
55+
lt = k * stdlib_base_ln( x / lambda );
56+
if ( lt < -36.0 ) {
57+
// For `t = (x/lambda)^k < e^-36`, `ln(1 - e^-t) = ln(t) + ln(1 - t/2 + ...)` and the correction is below half an ULP of `ln(t)`. Using `ln(t)` directly avoids `t` underflowing to `0`:
58+
return lt;
59+
}
5460
p = -stdlib_base_pow( x / lambda, k );
5561
return ( p < STDLIB_CONSTANT_FLOAT64_LN_HALF ) ? stdlib_base_log1p( -stdlib_base_exp( p ) ) : stdlib_base_ln( -stdlib_base_expm1( p ) );
5662
}
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
Distributions 0.23.8
21
julia 1.5
32
JSON 0.21

‎lib/node_modules/@stdlib/stats/base/dists/weibull/logcdf/test/fixtures/julia/both_large.json‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎lib/node_modules/@stdlib/stats/base/dists/weibull/logcdf/test/fixtures/julia/large_scale.json‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎lib/node_modules/@stdlib/stats/base/dists/weibull/logcdf/test/fixtures/julia/large_shape.json‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎lib/node_modules/@stdlib/stats/base/dists/weibull/logcdf/test/fixtures/julia/runner.jl‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@
1616
# See the License for the specific language governing permissions and
1717
# limitations under the License.
1818

19-
import Distributions: logcdf, Weibull
2019
import JSON
2120

2221
"""
2322
gen( x, k, lambda, name )
2423
2524
Generate fixture data and write to file.
2625
26+
The logarithm of the CDF is evaluated in extended precision from the textbook formula `ln(1 - exp(-(x/lambda)^k))`, written with `expm1` so that it stays exact when `(x/lambda)^k` is far below the working precision, as `(x/lambda)^k` underflows in double precision for small `x`.
27+
2728
# Arguments
2829
2930
* `x`: input value
@@ -41,9 +42,8 @@ julia> gen( x, k, lambda, "data.json" );
4142
```
4243
"""
4344
function gen( x, k, lambda, name )
44-
z = Array{Float64}( undef, length(x) );
45-
for i in eachindex(x)
46-
z[ i ] = logcdf( Weibull( k[i], lambda[i] ), x[i] );
45+
z = setprecision( BigFloat, 2048 ) do
46+
Float64.( log.( .-expm1.( .-( ( BigFloat.( x ) ./ BigFloat.( lambda ) ) .^ BigFloat.( k ) ) ) ) )
4747
end
4848

4949
# Store data to be written to file as a collection:
@@ -87,3 +87,9 @@ x = rand( 1000 ) .* 5.0;
8787
lambda = ( rand( 1000 ) .* 5.0 ) .+ 10.0;
8888
k = ( rand( 1000 ) .* 5.0 ) .+ 10.0;
8989
gen( x, k, lambda, "both_large.json" );
90+
91+
# Small `x`:
92+
x = exp10.( range( -300.0, stop = -2.0, length = 1000 ) );
93+
lambda = ( rand( 1000 ) .* 5.0 ) .+ 0.5;
94+
k = ( rand( 1000 ) .* 5.0 ) .+ 0.5;
95+
gen( x, k, lambda, "small_x.json" );

‎lib/node_modules/@stdlib/stats/base/dists/weibull/logcdf/test/fixtures/julia/small_x.json‎

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

‎lib/node_modules/@stdlib/stats/base/dists/weibull/logcdf/test/test.factory.js‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ var factory = require( './../lib/factory.js' );
3131
// FIXTURES //
3232

3333
var largeScale = require( './fixtures/julia/large_scale.json' );
34+
var smallX = require( './fixtures/julia/small_x.json' );
3435
var largeShape = require( './fixtures/julia/large_shape.json' );
3536
var bothLarge = require( './fixtures/julia/both_large.json' );
3637

@@ -192,7 +193,7 @@ tape( 'the created function evaluates the logcdf for `x` given large `lambda` an
192193
for ( i = 0; i < x.length; i++ ) {
193194
logcdf = factory( k[i], lambda[i] );
194195
y = logcdf( x[i] );
195-
t.strictEqual( isAlmostSameValue( y, expected[ i ], 0 ), true, 'returns expected value' );
196+
t.strictEqual( isAlmostSameValue( y, expected[ i ], 1 ), true, 'returns expected value' );
196197
}
197198
t.end();
198199
});
@@ -213,7 +214,7 @@ tape( 'the created function evaluates the logcdf for `x` given large shape param
213214
for ( i = 0; i < x.length; i++ ) {
214215
logcdf = factory( k[i], lambda[i] );
215216
y = logcdf( x[i] );
216-
t.strictEqual( isAlmostSameValue( y, expected[ i ], 0 ), true, 'returns expected value' );
217+
t.strictEqual( isAlmostSameValue( y, expected[ i ], 3236 ), true, 'returns expected value' );
217218
}
218219
t.end();
219220
});
@@ -234,7 +235,28 @@ tape( 'the created function evaluates the logcdf for `x` given large scale param
234235
for ( i = 0; i < x.length; i++ ) {
235236
logcdf = factory( k[i], lambda[i] );
236237
y = logcdf( x[i] );
237-
t.strictEqual( isAlmostSameValue( y, expected[ i ], 0 ), true, 'returns expected value' );
238+
t.strictEqual( isAlmostSameValue( y, expected[ i ], 2 ), true, 'returns expected value' );
239+
}
240+
t.end();
241+
});
242+
243+
tape( 'the created function evaluates the logcdf for `x` close to `0`', function test( t ) {
244+
var expected;
245+
var logcdf;
246+
var lambda;
247+
var i;
248+
var k;
249+
var x;
250+
var y;
251+
252+
expected = smallX.expected;
253+
x = smallX.x;
254+
lambda = smallX.lambda;
255+
k = smallX.k;
256+
for ( i = 0; i < x.length; i++ ) {
257+
logcdf = factory( k[i], lambda[i] );
258+
y = logcdf( x[i] );
259+
t.strictEqual( isAlmostSameValue( y, expected[ i ], 1 ), true, 'returns expected value' );
238260
}
239261
t.end();
240262
});

0 commit comments

Comments
 (0)