JuliaMath / HypergeometricFunctions.jl

A Julia package for calculating hypergeometric functions

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`_₂F₁` returns wrong (and negative!) result for large parameter values

devmotion opened this issue · comments

The following example causes issues in StatsFuns:

julia> using HypergeometricFunctions

julia> _₂F₁(6041, -2495, 6042, 0.1)
-1.8601060304844724e86

According to Mathematica the result should be around 7.169e-115 (see, e.g., WolframAlpha).

That's a tough one since it's so ill-conditioned!

Even a switch to BigFloat doesn't give the true result with 256 bits.

julia> for d in (64, 128, 256, 512, 1024, 2048)
           setprecision(d)
           println("With $d bits: ", _₂F₁(big(6041), big(-2495), big(6042), big"0.1"))
       end
With 64 bits: -8.99485706711407646963e+82
With 128 bits: -3.768364622629911259387373377831295071159e+63
With 256 bits: 7.643085578305798769538104224081012613229772525309794740110258747017712951371203e+24
With 512 bits: -4.4264798677310339256173502040188038311890682446912080069351277321056166885881673669408836469214270669989163506221614309672646227499147896742387972120237901e-53
With 1024 bits: 7.169000864829757581421226521118341577071490491462656813054559391246650807976143869415974650424976180664819945767536481261295942933662658520034114786078310397596431648363554816973760905648122364023602984747002471585997491094811128251440473364079854567611045461854182756393668302607360226003994524772703104486931e-115
With 2048 bits: 7.16900086482975758142122652111834157707149049146265681305455939124665080797614386941597465045815641567402422575960186149900525065034794034245254231059789333329885896628183155673583792646799563492848650702878239420924841585546950533124023812208251395253944943499468495262977909104723971416703356838132623167890376330260360041712490878775613493723688176758406527219283758417803787986529538500458406631271909953012349496024826178457978964413609239990864787077478180426443391470025827655872136792733048071405467264884944257127728627481742532104296302397542778585748814649159904153814745638294080221721631746185393466297522e-115

Please be advised this package is not symbolic in any way. But it does support some rational approximations that turn out to be a bit better:

julia> for d in (64, 128, 256, 512, 1024, 2048)
           setprecision(d)
           println("With $d bits: ", HypergeometricFunctions.pFqweniger([big(6041), big(-2495)], [big(6042)], big"0.1"))
       end
With 64 bits: 7.33313041116102000485e-18
With 128 bits: -1.315752681882871685499142091512016853179e-37
With 256 bits: -5.88011503082003285608549269073787834531678494727957553278509703250723096730728e-76
With 512 bits: 7.16900086482975758142122652111834157693588006812966718922489682313836019469317922584726132521494747133919436666829291677405009314509901237278755322399783044e-115
With 1024 bits: 7.169000864829757581421226521118341577071490491462656813054559391246650807976143869415974650458156415674024225759601861499005250650347940342452542310597893333298858966281831556735837926467995641393151209833000746729910324899663179564716640497352333266144750526037598526100148693723982529553736819701265169463323e-115
With 2048 bits: 7.16900086482975758142122652111834157707149049146265681305455939124665080797614386941597465045815641567402422575960186149900525065034794034245254231059789333329885896628183155673583792646799563492848650702878239420924841585546950533124023812208251395253944943499468495262977909104723971416703356838132623167890376330260360041712490878775613493723688176758406527219283758417803787986529538500458406631294807307181301996737860383586928193338206438102619894396979075216057868001561137227723845183386807553903964830789880785321005000975898230383185412057088684678130334133157201835739033936199142693848081407373683764397366e-115

Looking at the betalogcdf implementation, maybe there's a reason to use https://dlmf.nist.gov/8.17#E8 instead

julia> a = 6041
6041

julia> b = 2496
2496

julia> x = 0.1
0.1

julia> (1-x)^b * HypergeometricFunctions.pFqweniger([1, a+b], [a+1], x)
7.169000864830204e-115

I think with a,b>0, it's true that this is positive, something that can't be said about the terminating form with alternating coefficients.
Probably this case converts the DLMF's E8 into E7 for the _₂F₁, but the rational algorithm isn't cased out.

Yes, maybe we should use a different algorithm in StatsFuns - in particular since one problem with these user-provided parameter values is that it leads to a DomainError due to the negative result in HypergeometricFunctions. Thanks for looking into the issue!

I think with a,b>0, it's true that this is positive, something that can't be said about the terminating form with alternating coefficients.

Do you know any (immediate) disadvantages or possible problems with this alternative approach based on pFqweniger? I'm not familiar with the different algorithms in HypergeometricFunctions, are there e.g. any performance difference or possible numerical issues for certain parameter ranges?

But it does support some rational approximations that turn out to be a bit better:
Probably this case converts the DLMF's E8 into E7 for the _₂F₁, but the rational algorithm isn't cased out.

I think, from the perspective of a downstream user it would be easiest if HypergeometricFunctions would automatically use the algorithm with the best trade-off between performance and accuracy even for such hard parameter choices. I would also like to avoid calling internal functions such as pFqweniger in StatsFuns.

(BTW while these parameter values may seem a bit unlikely it's easy to end up with such values eg in a simple Bayesian coinflip example: https://turing.ml/dev/tutorials/00-introduction/)

In the general setting, it's not at all obvious to me what's best: if one of the top parameters is a real negative integer, the series is terminating. If all the parameters and the argument are positive, the series is positive. How can one even begin to weigh these two properties?

Maybe I could add a method positive₂F₁ just for the important scenario of a CDF: real argument clamped between 0 and 1, inclusive, and positive parameters?

Just tagged a new version with this exported function positive₂F₁ with argument support that includes the use for the beta log CDF. Let me know if it works for StatsFuns with https://dlmf.nist.gov/8.17#E8

Wow, thanks a lot for the quick fix! So far it seems to solve the issues in StatsFuns: JuliaStats/StatsFuns.jl#144

Hmm, it seems to cause downstream issues for some non-finite special cases: https://github.com/JuliaStats/StatsFuns.jl/runs/6414587037?check_suite_focus=true#step%3A6%3A487 I'll check how we can fix them. The original problem is resolved at least 🙂

Does it have to do with argument unity?

The problem are non-finite parameter values, which can occur since we reduce computation of logccdf(Binomial(n, p), k) to logcdf(Beta(max(0, k + 1), max(0, n - k)), p). We want logccdf(Binomial(n, p), Inf) = log(0.0) = -Inf. This worked with E7 since then only one of the terms in the branch in betalogcdf (the one that calls HypergeometricFunctions) is non-finite, but now the call of positive₂F₁ also returns a non-finite value with different sign, so we end up with NaN. Edit: I was using the wrong formula (not in the PR, just this comment), it seems the NaN arises directly from the call to positive₂F₁:

As a concrete example, we have

julia> binomlogccdf(5.0, 0.5, Inf) # should be -Inf
NaN

julia> betalogcdf(Inf, 0.0, 0.5) # called by `binomlogccdf`
NaN

julia> positive₂F₁(Inf, 1.0, Inf, 0.5) # non-finite term introduced in the PR
NaN

julia> _₂F₁(Inf, 1.0, Inf, 0.5) # finite term in the current version
2.0

Maybe we have to handle this special case separately.

Maybe we have to handle this special case separately.

On the other hand, maybe this should be fixed in HypergeometricFunctions since it worked before and we should still get

julia> positive₂F₁(Inf, 1.0, Inf, 0.5) 
2.0

as before. I guess maybe a branch in the definition of positive₂F₁ would be sufficient?

We can do that: when a top and bottom parameter are equal, we remove them both

elseif isequal(a, c) # 1. 15.4.6
return exp(-b*log1p(-z))
elseif isequal(b, c) # 1. 15.4.6
return exp(-a*log1p(-z))

Ok, so I changed it again 55bd4f2. Instead of positive2F1, there's a method keyword which can guarantee positivity (though that certainly comes at a computational cost for z near 1) _₂F₁(a, b, c, z; method = :positive).

This lets the code reuse all the special cases based on parameter values (including the removal of two equal values in top/bottom) and I also added an argument unity case.

Thank you! It seems this fixed the downstream tests in StatsFuns. I'll close this issue.