fix: safeguard Gamma::inverse_cdf Newton step to avoid NaN for shape < 1#382
Merged
YeungOnion merged 2 commits intoJul 18, 2026
Merged
Conversation
inverse_cdf refined its bisection bracket with an unguarded Newton–Raphson loop. For shape < 1 the quantile lies very close to 0, so the first Newton step overshoots below the support (where pdf == 0), the division blows up and the iteration diverges to NaN. ChiSquared (df < 2) and Erlang delegate to this solver and were affected too. Keep the bracket as an invariant through the Newton loop (rtsafe): take a Newton step only when it is finite and strictly inside [low, high], otherwise bisect. The iterate can no longer leave the support. Fixes statrs-dev#373 and statrs-dev#361.
Contributor
|
I like the solution, we might benefit from this kind of search for other inverse cdf steps for float domains. Thanks for this! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Gamma::inverse_cdf(p)returnsNaNfor any shape < 1 oncepis small enough.ChiSquared(df < 2) andErlangdelegate to the same solver and inherit it — this is the NaN reported in #361 and #373.Root cause
inverse_cdfbuilds a valid bracket[low, high]by bisection and then refines it with an unguarded Newton–Raphson loop. For shape < 1 the true quantile sits extremely close to 0 while the post-bisection iterate is still far from it, so the first Newton step overshoots tox < 0, outside the support. Therecdf(x) == 0andpdf(x) == 0, so the update is(0 - p) / 0 = -inf; the next step isinf - inf = NaN, which propagates because the loop never re-brackets. The bracket invariant from the bisection phase is discarded by the Newton loop.Fix
Merge the two phases into one safeguarded loop (Numerical Recipes'
rtsafe): keep[low, high]as an invariant, tighten it by the sign ofcdf(x) - peach step, and accept a Newton step only when it is finite and strictly inside the bracket, otherwise take a bisection step. The iterate can no longer leave the support, so the result is always finite; shape ≥ 1 results are unchanged.I diff-tested the whole
inverse_cdfsurface against scipy.stats / mpmath (dps=60) to scope this: the Gamma family (Gamma/ChiSquared/Erlang) was the only one producing NaN. Beta/StudentsT/FisherSnedecor go through the separately-safeguarded AS 64inv_beta_reg, and Chi/InverseGamma use the bracketed default search — none share this failure — so the fix stays in the Gamma solver.Tests
Added regression tests in
gamma.rs(all NaN/panic before the fix): shape < 1 quantiles are finite; values checked against scipy/mpmath constants; round-tripcdf(inverse_cdf(p)) ≈ pand monotonicity across the shape/pgrid; and ChiSquared/Erlang delegation, including the large-df upper-tail case from #361. Full test suite,cargo fmt --checkandcargo clippyare clean.