From 3ad282cf16e8f01590808fe96f3d4596f13136ec Mon Sep 17 00:00:00 2001 From: Philipp Burckhardt Date: Thu, 27 Aug 2026 23:14:37 -0500 Subject: [PATCH] fix: prevent non-terminating searches in `stats/base/dists/negative-binomial/quantile` Guards the quantile search against three previously non-terminating inputs: a success probability equal to zero (now returns NaN, matching R and Boost domain conventions), a distribution mean overflowing double-precision (now saturates to positive infinity), and unit search steps which no longer advance the search position (e.g., for search positions exceeding 2^53). Fixes are applied to both the JavaScript and C implementations. Also corrects REPL doctest examples which had been copied from the CDF documentation. Co-Authored-By: Claude Fable 5 --- type: pre_commit_static_analysis_report description: Results of running static analysis checks when committing changes. report: - task: lint_filenames status: passed - task: lint_editorconfig status: passed - task: lint_markdown_pkg_readmes status: passed - task: lint_markdown_docs status: na - task: lint_markdown status: na - task: lint_package_json status: na - task: lint_repl_help status: passed - task: lint_javascript_src status: passed - task: lint_javascript_cli status: na - task: lint_javascript_examples status: na - task: lint_javascript_tests status: passed - task: lint_javascript_benchmarks status: na - task: lint_python status: na - task: lint_r status: na - task: lint_c_src status: passed - task: lint_c_examples status: na - task: lint_c_benchmarks status: na - task: lint_c_tests_fixtures status: na - task: lint_shell status: na - task: lint_typescript_declarations status: passed - task: lint_typescript_tests status: na - task: lint_license_headers status: passed --- --- .../negative-binomial/quantile/README.md | 7 +++ .../negative-binomial/quantile/docs/repl.txt | 11 +++-- .../negative-binomial/quantile/lib/factory.js | 7 ++- .../negative-binomial/quantile/lib/main.js | 11 ++++- .../negative-binomial/quantile/lib/native.js | 4 ++ .../negative-binomial/quantile/lib/search.js | 18 +++++++- .../negative-binomial/quantile/src/main.c | 29 +++++++++--- .../quantile/test/test.factory.js | 45 +++++++++++++++++++ .../quantile/test/test.main.js | 36 +++++++++++++++ .../quantile/test/test.native.js | 36 +++++++++++++++ 10 files changed, 190 insertions(+), 14 deletions(-) diff --git a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/README.md b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/README.md index 892ea475d535..6eddd8678071 100644 --- a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/README.md +++ b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/README.md @@ -124,6 +124,13 @@ y = quantile( 0.3, 20.0, 1.5 ); // returns NaN ``` +If provided a success probability `p` equal to `0`, the function returns `NaN`, as the number of failures before observing `r` successes is unbounded. + +```javascript +var y = quantile( 0.5, 20.0, 0.0 ); +// returns NaN +``` + #### quantile.factory( r, p ) Returns a function for evaluating the [quantile function][quantile-function] for a [negative binomial][negative-binomial-distribution] distribution with number of successes until experiment is stopped `r` and success probability `p`. diff --git a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/docs/repl.txt b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/docs/repl.txt index 7726004d33ef..4330b98908bd 100644 --- a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/docs/repl.txt +++ b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/docs/repl.txt @@ -14,6 +14,9 @@ If provided a success probability `p` outside of `[0,1]`, the function returns `NaN`. + If provided a success probability `p` equal to `0`, the function returns + `NaN`. + Parameters ---------- k: number @@ -46,10 +49,10 @@ > y = {{alias}}( -0.1, 20.0, 0.5 ) NaN - > y = {{alias}}( 21.0, 15.5, 0.5 ) - 12 - > y = {{alias}}( 5.0, 7.4, 0.4 ) - 10 + > y = {{alias}}( 0.8, 15.5, 0.5 ) + 20 + > y = {{alias}}( 0.4, 7.4, 0.4 ) + 9 > y = {{alias}}( 0.5, 0.0, 0.5 ) NaN diff --git a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/lib/factory.js b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/lib/factory.js index 69e34843a445..6e7eef33833d 100644 --- a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/lib/factory.js +++ b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/lib/factory.js @@ -57,7 +57,7 @@ function factory( r, p ) { isnan( r ) || isnan( p ) || r <= 0.0 || - p < 0.0 || + p <= 0.0 || p > 1.0 ) { return constantFunction( NaN ); @@ -94,7 +94,10 @@ function factory( r, p ) { if ( k === 1.0 ) { return PINF; } - + // If the mean overflows double-precision floating-point format, the sought quantile saturates at positive infinity: + if ( mu === PINF ) { + return PINF; + } // Cornish-Fisher expansion: if ( k < 0.5 ) { x = -erfcinv( 2.0 * k ) * SQRT2; diff --git a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/lib/main.js b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/lib/main.js index 50aba0cdcb8b..d58c5431f8d9 100644 --- a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/lib/main.js +++ b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/lib/main.js @@ -73,6 +73,10 @@ var search = require( './search.js' ); * // returns NaN * * @example +* var y = quantile( 0.5, 20.0, 0.0 ); +* // returns NaN +* +* @example * var y = quantile( 0.3, 20.0, -1.0 ); * // returns NaN * @@ -107,7 +111,7 @@ function quantile( k, r, p ) { isnan( p ) || isnan( k ) || r <= 0.0 || - p < 0.0 || + p <= 0.0 || p > 1.0 || k < 0.0 || k > 1.0 @@ -122,6 +126,11 @@ function quantile( k, r, p ) { } q = 1.0 - p; mu = ( r * q ) / p; + + // If the mean overflows double-precision floating-point format, the sought quantile saturates at positive infinity: + if ( mu === PINF ) { + return PINF; + } sigma = sqrt( r * q ) / p; sigmaInv = 1.0 / sigma; diff --git a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/lib/native.js b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/lib/native.js index a644c97182ec..9378c9578bb1 100644 --- a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/lib/native.js +++ b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/lib/native.js @@ -67,6 +67,10 @@ var addon = require( './../src/addon.node' ); * // returns NaN * * @example +* var y = quantile( 0.5, 20.0, 0.0 ); +* // returns NaN +* +* @example * var y = quantile( 0.3, 20.0, -1.0 ); * // returns NaN * diff --git a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/lib/search.js b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/lib/search.js index 549bed04cb21..ad3bf1812d39 100644 --- a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/lib/search.js +++ b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/lib/search.js @@ -40,11 +40,18 @@ var methods; * @returns {NonNegativeInteger} `k` quantile of the specified distribution */ function searchLeft( x, k, r, p ) { + var xp; while ( true ) { if ( x === 0 || cdf( x - 1.0, r, p ) < k ) { return x; } - x -= 1; + xp = x - 1.0; + + // Guard against non-advancing updates (e.g., when `x` is `NaN` or is so large that decrementing does not change its value) in order to avoid an infinite loop: + if ( !( xp < x ) ) { + return x; + } + x = xp; } } @@ -58,8 +65,15 @@ function searchLeft( x, k, r, p ) { * @returns {NonNegativeInteger} `k` quantile of the specified distribution */ function searchRight( x, k, r, p ) { + var xn; while ( true ) { - x += 1; + xn = x + 1.0; + + // Guard against non-advancing updates (e.g., when `x` is `NaN` or is so large that incrementing does not change its value) in order to avoid an infinite loop: + if ( !( xn > x ) ) { + return x; + } + x = xn; if ( cdf( x, r, p ) >= k ) { return x; } diff --git a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/src/main.c b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/src/main.c index e02e7aaba80b..1e62a238838d 100644 --- a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/src/main.c +++ b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/src/main.c @@ -35,8 +35,15 @@ * @return `k` quantile of the specified distribution */ static double search_left( double x, const double k, const double r, const double p ) { + double xp; while ( x > 0.0 && stdlib_base_dists_negative_binomial_cdf( x - 1.0, r, p ) >= k ) { - x -= 1.0; + xp = x - 1.0; + + // Guard against non-advancing updates (e.g., when `x` is `NaN` or is so large that decrementing does not change its value) in order to avoid an infinite loop: + if ( !( xp < x ) ) { + return x; + } + x = xp; } return x; } @@ -51,9 +58,16 @@ static double search_left( double x, const double k, const double r, const doubl * @return `k` quantile of the specified distribution */ static double search_right( double x, const double k, const double r, const double p ) { - x += 1.0; - while ( stdlib_base_dists_negative_binomial_cdf( x, r, p ) < k ) { - x += 1.0; + double xn; + + // Guard against non-advancing updates (e.g., when `x` is `NaN` or is so large that incrementing does not change its value) in order to avoid an infinite loop: + xn = x + 1.0; + while ( xn > x ) { + x = xn; + if ( stdlib_base_dists_negative_binomial_cdf( x, r, p ) >= k ) { + return x; + } + xn = x + 1.0; } return x; } @@ -85,7 +99,7 @@ double stdlib_base_dists_negative_binomial_quantile( const double k, const doubl stdlib_base_is_nan( p ) || stdlib_base_is_nan( k ) || r <= 0.0 || - p < 0.0 || + p <= 0.0 || p > 1.0 || k < 0.0 || k > 1.0 @@ -100,6 +114,11 @@ double stdlib_base_dists_negative_binomial_quantile( const double k, const doubl } q = 1.0 - p; mu = ( r * q ) / p; + + // If the mean overflows double-precision floating-point format, the sought quantile saturates at positive infinity: + if ( mu == STDLIB_CONSTANT_FLOAT64_PINF ) { + return STDLIB_CONSTANT_FLOAT64_PINF; + } sigma = stdlib_base_sqrt( r * q ) / p; sigmaInv = 1.0 / sigma; diff --git a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/test/test.factory.js b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/test/test.factory.js index 9359caeddd63..2c245b5c2f3a 100644 --- a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/test/test.factory.js +++ b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/test/test.factory.js @@ -141,6 +141,51 @@ tape( 'if provided a `r` which is not a positive integer, the created function a t.end(); }); +tape( 'if provided a success probability `p` equal to `0`, the created function always returns `NaN`', function test( t ) { + var quantile; + var y; + + quantile = factory( 20.0, 0.0 ); + + y = quantile( 0.5 ); + t.strictEqual( isnan( y ), true, 'returns expected value' ); + + y = quantile( 0.3 ); + t.strictEqual( isnan( y ), true, 'returns expected value' ); + + t.end(); +}); + +tape( 'if the distribution mean overflows double-precision floating-point format, the created function returns `+infinity`', function test( t ) { + var quantile; + var y; + + quantile = factory( 1.0e308, 0.1 ); + + y = quantile( 0.3 ); + t.strictEqual( y, PINF, 'returns expected value' ); + + y = quantile( 0.9 ); + t.strictEqual( y, PINF, 'returns expected value' ); + + t.end(); +}); + +tape( 'the created function terminates for very large `r` for which unit increments can no longer advance the search', function test( t ) { + var quantile; + var y; + + quantile = factory( 1.0e300, 0.5 ); + + y = quantile( 0.9 ); + t.strictEqual( y, 1.0e300, 'returns expected value' ); + + y = quantile( 0.1 ); + t.strictEqual( y, 1.0e300, 'returns expected value' ); + + t.end(); +}); + tape( 'if provided a valid `r` and `p`, the function returns a function which accurately computes the 0% and 100% quantiles', function test( t ) { var quantile; var y; diff --git a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/test/test.main.js b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/test/test.main.js index 5f0b89e031d9..1eee11a4a2a2 100644 --- a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/test/test.main.js +++ b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/test/test.main.js @@ -94,6 +94,42 @@ tape( 'if provided a success probability `p` outside `[0,1]`, the function retur t.end(); }); +tape( 'if provided a success probability `p` equal to `0`, the function returns `NaN`', function test( t ) { + var y; + + y = quantile( 0.5, 20, 0.0 ); + t.strictEqual( isnan( y ), true, 'returns expected value' ); + + y = quantile( 0.3, 10, 0.0 ); + t.strictEqual( isnan( y ), true, 'returns expected value' ); + + t.end(); +}); + +tape( 'if the distribution mean overflows double-precision floating-point format, the function returns `+infinity`', function test( t ) { + var y; + + y = quantile( 0.3, 1.0e308, 0.1 ); + t.strictEqual( y, PINF, 'returns expected value' ); + + y = quantile( 0.9, 1.0e308, 0.1 ); + t.strictEqual( y, PINF, 'returns expected value' ); + + t.end(); +}); + +tape( 'the function terminates for very large `r` for which unit increments can no longer advance the search', function test( t ) { + var y; + + y = quantile( 0.9, 1.0e300, 0.5 ); + t.strictEqual( y, 1.0e300, 'returns expected value' ); + + y = quantile( 0.1, 1.0e300, 0.5 ); + t.strictEqual( y, 1.0e300, 'returns expected value' ); + + t.end(); +}); + tape( 'if provided a valid `r` and `p`, the function accurately computes the 0% and 100% quantiles', function test( t ) { var y; diff --git a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/test/test.native.js b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/test/test.native.js index 190c8ad166d2..74cecf32840e 100644 --- a/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/test/test.native.js +++ b/lib/node_modules/@stdlib/stats/base/dists/negative-binomial/quantile/test/test.native.js @@ -103,6 +103,42 @@ tape( 'if provided a success probability `p` outside `[0,1]`, the function retur t.end(); }); +tape( 'if provided a success probability `p` equal to `0`, the function returns `NaN`', opts, function test( t ) { + var y; + + y = quantile( 0.5, 20, 0.0 ); + t.strictEqual( isnan( y ), true, 'returns expected value' ); + + y = quantile( 0.3, 10, 0.0 ); + t.strictEqual( isnan( y ), true, 'returns expected value' ); + + t.end(); +}); + +tape( 'if the distribution mean overflows double-precision floating-point format, the function returns `+infinity`', opts, function test( t ) { + var y; + + y = quantile( 0.3, 1.0e308, 0.1 ); + t.strictEqual( y, PINF, 'returns expected value' ); + + y = quantile( 0.9, 1.0e308, 0.1 ); + t.strictEqual( y, PINF, 'returns expected value' ); + + t.end(); +}); + +tape( 'the function terminates for very large `r` for which unit increments can no longer advance the search', opts, function test( t ) { + var y; + + y = quantile( 0.9, 1.0e300, 0.5 ); + t.strictEqual( y, 1.0e300, 'returns expected value' ); + + y = quantile( 0.1, 1.0e300, 0.5 ); + t.strictEqual( y, 1.0e300, 'returns expected value' ); + + t.end(); +}); + tape( 'if provided a valid `r` and `p`, the function accurately computes the 0% and 100% quantiles', opts, function test( t ) { var y;