Skip to content

Commit 0890f5e

Browse files
Fix #11103: keep small lower-tail targets in betaIncompleteInv (#11117)
Interval halving switched to the complement (a and b swapped, y0 = 1 - yy0) once x passed 0.95. For a small yy0 that subtraction rounds (to exactly 1 for 1e-20), so the target was lost. Switch only when 1 - yy0 is exact, or to undo an earlier switch, and cap the unswitched search at 1 - real.epsilon as the switched one already is. * Fix #11103: switch to the complement unless 1 - yy0 loses yy0 Blocking the switch for every yy0 <= 0.5 made the search stay near 1 without the complement, which lost precision with 64-bit reals: betaIncompleteInv(1e10, 1, 0.01) gave 0.99999991898 instead of 0.99999999954. Switch as before, and skip it only when 1 - yy0 rounds away yy0. Drop the 1 - real.epsilon cap: the search now returns nextDown(1), which is closer, since the CDF there is still below the probability in the coverage tests. * Fix #11103: solve for 1 - x when 1 - yy0 loses yy0 Skipping the complement switch kept the search in x near 1, which still lost precision: betaIncompleteInv(1e11, 1, 1e-6) with 64-bit reals and (1e12, 1, 0.007) with 80-bit reals were off from the 7th digit. When 1 - yy0 is not exact, bisect t = 1 - x in [1 - x1, 1 - x0] instead, with betaIncompleteComplSmallX for the CDF, so neither yy0 nor the CDF is subtracted from 1. As suggested by jmh530. Signed-off-by: Sarthak <sarthakshreshtha345@gmail.com>
1 parent 3a75e5f commit 0890f5e

1 file changed

Lines changed: 43 additions & 2 deletions

File tree

‎std/internal/math/gammafunction.d‎

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,6 +1439,26 @@ private real betaSmallShapeComplement(real a, real b, real x)
14391439
return -expm1(a * (logBx + gammaRatioCorrection - logGammaOverA + seriesOverA) - shift);
14401440
}
14411441

1442+
// Solve betaIncomplete(aa, bb, 1 - t) = yy0 for t in [tlo, thi] by bisection
1443+
// and return 1 - t. The CDF near 1 comes from betaIncompleteComplSmallX, so
1444+
// neither yy0 nor the CDF is ever subtracted from 1.
1445+
private real betaIncompleteInvTail(real aa, real bb, real yy0, real tlo, real thi)
1446+
{
1447+
for (int i = 0; i < 200; i++)
1448+
{
1449+
const t = tlo + 0.5L * (thi - tlo);
1450+
if (t <= tlo || t >= thi || 1.0L - tlo == 1.0L - thi)
1451+
break;
1452+
if (betaIncompleteComplSmallX(bb, aa, t) > yy0)
1453+
tlo = t;
1454+
else
1455+
thi = t;
1456+
}
1457+
const x = 1.0L - (tlo + 0.5L * (thi - tlo));
1458+
// The quantile of a probability below 1 is below 1.
1459+
return x < 1.0L ? x : nextDown(1.0L);
1460+
}
1461+
14421462
/** Inverse of incomplete beta integral
14431463
*
14441464
* Given y, the function finds x such that
@@ -1562,6 +1582,14 @@ ihalve:
15621582
else
15631583
di = (y0 - y)/(yh - yl);
15641584
dir += 1;
1585+
if ( x0 > 0.95L && rflg == 0
1586+
&& fabs((1.0L - (1.0L - yy0)) - yy0) > 8 * real.epsilon * yy0 )
1587+
{
1588+
// 1 - yy0 has lost part of yy0 (all of it for yy0 = 1e-20),
1589+
// so do not switch to the complement: solve for 1 - x instead.
1590+
x = betaIncompleteInvTail(aa, bb, yy0, 1.0L - x1, 1.0L - x0);
1591+
goto done;
1592+
}
15651593
if ( x0 > 0.95L )
15661594
{
15671595
if ( rflg == 1 )
@@ -1759,6 +1787,16 @@ done:
17591787
assert(feqrel(betaIncompleteInv(5, 10, 0.2000002972865658842L), 0.229121208190918L) >= real.mant_dig - 3);
17601788
assert(feqrel(betaIncompleteInv(4, 7, 0.8000002209179505L), 0.483657360076904L) >= real.mant_dig - 3);
17611789

1790+
// https://github.com/dlang/phobos/issues/11103
1791+
// small lower-tail probabilities whose quantile is close to 1
1792+
assert(isClose(betaIncompleteInv(1000, 1, 1e-20L), pow(1e-20L, 1.0L / 1000), 1e-15L));
1793+
assert(isClose(betaIncompleteInv(1000, 1, 1e-10L), pow(1e-10L, 1.0L / 1000), 1e-15L));
1794+
assert(isClose(betaIncomplete(1000, 2, betaIncompleteInv(1000, 2, 1e-20L)), 1e-20L, 1e-10L));
1795+
// a moderate probability whose quantile is very close to 1 (also with 64-bit reals)
1796+
assert(isClose(betaIncompleteInv(1e10L, 1, 0.01L), pow(0.01L, 1.0L / 1e10L), 1e-14L));
1797+
assert(isClose(betaIncompleteInv(1e11L, 1, 1e-6L), pow(1e-6L, 1.0L / 1e11L), 1e-15L));
1798+
assert(isClose(betaIncompleteInv(1e12L, 1, 0.007L), pow(0.007L, 1.0L / 1e12L), 1e-15L));
1799+
17621800
// Coverage tests. I don't have correct values for these tests, but
17631801
// these values cover most of the code, so they are useful for
17641802
// regression testing.
@@ -1767,8 +1805,11 @@ done:
17671805
// significant improvement over the original CEPHES code.
17681806
static if (real.mant_dig == 64) // 80-bit reals
17691807
{
1770-
assert(betaIncompleteInv(0.01L, 8e-48L, 5.45464e-20L) == 1-real.epsilon);
1771-
assert(betaIncompleteInv(0.01L, 8e-48L, 9e-26L) == 1-real.epsilon);
1808+
// The CDF at nextDown(1) is still below these probabilities, so the
1809+
// quantile lies above it and nextDown(1) is the closest result below 1.
1810+
assert(betaIncomplete(0.01L, 8e-48L, nextDown(1.0L)) < 9e-26L);
1811+
assert(betaIncompleteInv(0.01L, 8e-48L, 5.45464e-20L) == nextDown(1.0L));
1812+
assert(betaIncompleteInv(0.01L, 8e-48L, 9e-26L) == nextDown(1.0L));
17721813

17731814
// Beware: a one-bit change in pow() changes almost all digits in the result!
17741815
// scipy says that this is 0.99999_99995_89020_6 (0x1.ffff_fffc_783f_2a7ap-1)

0 commit comments

Comments
 (0)