fix(math): regularize removable singularity in hyp2f1 when c-a-b is an integer#2026
Open
Sumu004 wants to merge 1 commit into
Open
fix(math): regularize removable singularity in hyp2f1 when c-a-b is an integer#2026Sumu004 wants to merge 1 commit into
Sumu004 wants to merge 1 commit into
Conversation
…n integer
hyp2f1_small_argument returns nan for z near 1 whenever c - a - b is
exactly an integer, e.g. hyp2f1(1, -0.5, 2.5, 0.9901961) -> nan instead
of ~0.753603.
Root cause: _hyp2f1_z_near_one computes d = c - a - b and uses the
standard Gauss connection formula, which needs both lgamma(d) and
lgamma(-d). One of these always hits a nonpositive-integer pole of
Gamma whenever d is an integer of either sign (lgamma(d) for d <= 0,
lgamma(-d) for d >= 0), producing nan even though 2F1 itself is finite
and analytic at these points -- it is a removable singularity in the
formula, not in the function.
The exact value at such a point is given by a distinct closed form
involving digamma corrections and an extra log(1-z) term (DLMF 15.8.8
for d >= 0, 15.8.9 for d < 0). Rather than hand-derive and separately
validate that closed form (verified by hand against mpmath, one
transcription attempt for the d < 0 direction did not reproduce
mpmath to more than 3 digits before being caught), this fix instead
regularizes by nudging c off the exact degenerate point by
epsilon = sqrt(machine epsilon) -- the standard step size balancing
truncation against rounding error in a one-sided numerical derivative.
Since 2F1 is analytic in c away from its own poles, this recovers the
true value at a controlled, first-order-in-epsilon rate.
Verified against mpmath (50-digit precision) across m = c-a-b in
{-4..4} and 300 randomized (a, b, z) combinations: worst-case relative
error 8.6e-7, consistent with the expected O(epsilon) bound at
epsilon ~ 1.5e-8 (float64). The perturbation also transitively
resolves the same degeneracy in the two recursive
_hyp2f1_internal(..., 1 - d, ...) / (..., d + 1, ...) calls, whose c
parameters land on the exact same integer boundary.
Fixes tensorflow#2001
|
Thanks for this patch! |
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.
What
hyp2f1_small_argumentreturnsnanforznear 1 wheneverc - a - bis exactly an integer:Root cause
_hyp2f1_z_near_onecomputesd = c - a - band evaluates the standard Gauss connection formula forznear 1, which needs bothlgamma(d)andlgamma(-d). Wheneverdis an integer of either sign, one of these two always lands on a nonpositive-integer pole of the Gamma function (lgamma(d)ford <= 0,lgamma(-d)ford >= 0), producingnan— even though2F1itself is finite and analytic at these points. It's a removable singularity in the formula, not in the function.Why not the closed form?
The exact value at
dinteger is given by a distinct closed form involving digamma corrections and an extralog(1-z)term (DLMF 15.8.8 ford >= 0, 15.8.9 ford < 0). I initially hand-derived and implemented this directly, verified it exactly againstmpmathat 50-digit precision form=2— and then found it silently diverged frommpmath(only 3 correct digits) form=3with a different parameter set, indicating a transcription error in my own derivation that a quick spot-check would have missed. Given how easy it is to introduce a subtle-but-wrong error in a delicate closed form like this, I moved to a different, independently-verifiable approach instead of debugging the series further:Fix
Regularize by nudging
coff the exact degenerate point before evaluating the (already-correct, well-tested) existing formula:epsilon = sqrt(machine epsilon)is the standard step size balancing truncation against rounding error in a one-sided numerical derivative. Since2F1is analytic incaway from its own poles, this recovers the true value at a controlled, first-order-in-epsilonrate — no new closed form to get subtly wrong, and it reuses the exact code path already validated by the surrounding test suite for non-degenerated.As a side effect, the same nudge transitively resolves the identical degeneracy in the two recursive
_hyp2f1_internal(..., 1 - d, ...)/(..., d + 1, ...)calls a few lines down, whose owncparameters land on the exact same integer boundary (this recursion is what actually crashes today — dividing by an exact-zero series denominator — rather than surfacing as a "clean"nanfromlgammaalone).Verification
Checked against
mpmathat 50-digit precision:m=2): matches to the reported expected value0.753603006025111.mswept across{-4, ..., 4}(covering both DLMF 15.8.8 and 15.8.9 regimes) with hand-picked test cases: relative error consistently ~1e-8 to 1e-9, matching the expectedO(epsilon)bound.(a, b, m, z)combinations atepsilon=1e-8: worst-case relative error8.6e-7, still within the expected bound.Checklist
testHyp2F1DegenerateCMinusAMinusBregression test: the exact reported repro plus 5 additional cases sweepingm = c-a-bthrough{-3, -1, 0, 1, 3}, checked againstscipy.special.hyp2f1at both float32 and float64dis exactly an integerFixes #2001