Fix erf/erfc catastrophic cancellation producing impossible values (erf > 1, erfc < 0, negative Normal survival) - #491
Conversation
The erf/erfc dispatch produced mathematically impossible results near the
tail because of the medium branch of errorFunction():
Special::erf(3.99) = 1.0000004145 (erf must be < 1)
Special::erfc(3.9) = -6.78e-9 (erfc must be >= 0)
(new Normal(0,1))->cdf(5.6) survival = -8.8e-8 (probability < 0)
Root cause (one family, one file):
- For 0.01 < |x| <= 4, erf was summed from the alternating Maclaurin series
Sum (-1)^n x^(2n+1)/(n!(2n+1)). Its terms grow to about e^(x^2) before
decaying, so near x = 4 they reach ~1e7 while the sum is ~1, losing about
11 digits. erfc(x) = 1 - erf(x) then dumps that error into a value ~1e-8,
flipping its sign.
- The |x| > 4 branch used erfcAsymptoticSeries() truncated at 4 terms
(dropping +105/16x^8), giving ~2.5% error at the x = 4 threshold and
~1.5e-5 at x = 5 of a divergent series.
- Normal::cdf() calls erf, so the whole tail/survival surface and every
Normal-derived path inherited the defect.
Fix: route erf/erfc through the file's own stable continued-fraction gamma
via the identity erfc(x) = Γ(1/2, x²)/√π (x >= 0), reusing the tested
upperIncompleteGamma() rather than a new series. The 8-term Maclaurin branch
is kept for |x| <= 0.01, where 1 - erfc would instead cancel. Reflection
erfc(-x) = 2 - erfc(x) and oddness erf(-x) = -erf(x) stay exact.
Verified against mpmath (dps 40, cross-checked at dps 35) across every
branch: relative error < 1e-12 for both erf and erfc, including the
previously-correct small-x region (no regression), and the bounds
erf in [-1,1], erfc in [0,2] hold on a dense sweep. Tests added for the
reference values, the invariants, and the non-negative Normal tail survival.
|
Hi @gaoflow, Thanks for your interest in MathPHP. And thanks for the PR to improve the code. I'm currently in the middle of a version upgrade of the library, so please be patient as it may take me some time to get to this and decide whether to apply any fixes forward or to the current version as well. Again, thanks for the PR. And apologies if it takes me a bit to look through this. |
|
The red jobs aren't from this change: all 53 failures are |
Problem
Special::erf/Special::erfcreturn mathematically impossible values in the tail, and this propagates intoNormal::cdf:No reference implementation is needed to see these are wrong:
erfis bounded in (-1, 1),erfcin [0, 2], and a probability cannot be negative.Root cause
All three come from one place,
errorFunction():0.01 < |x| <= 4, erf is summed from the alternating Maclaurin seriesΣ (-1)^n x^(2n+1) / (n!(2n+1)). Its terms grow to aboute^(x^2)before decaying, so nearx = 4individual terms reach ~1e7 while the sum is ~1 — losing about 11 digits to cancellation.erfc(x) = 1 - erf(x)then dumps that error straight into a value of order 1e-8, flipping its sign.|x| > 4branch usederfcAsymptoticSeries()truncated at 4 terms (it drops the+105/16 x^8term). That divergent series is worst at thex = 4threshold (~2.5%) and still ~1.5e-5 atx = 5.Normal::cdf()callserf, so the tail CDF / survival and every Normal-derived path inherit the defect.The existing tests missed it because none exercise
3 < |x| < 4, and theerfctests nearx = 5use an absolute tolerance far larger than the value.Fix
Route erf/erfc through the file's own numerically stable continued-fraction gamma, using the identity
erfc(x) = Γ(1/2, x²)/√πforx >= 0(reusing the already-testedupperIncompleteGamma) instead of an unstable/truncated series. The 8-term Maclaurin branch is kept for|x| <= 0.01, where1 - erfcwould instead cancel.erfc(-x) = 2 - erfc(x)anderf(-x) = -erf(x)stay exact. Net effect is less code, not more.Verification
Checked against mpmath (
mp.dps = 40, cross-checked at dps 35) across every dispatch branch:< 1e-12for both erf and erfc at every grid point, including the small-x region that was already correct (no regression);erf in [-1, 1]anderfc in [0, 2]on a dense sweep0 <= x <= 8(both signs);1 - cdf(z)is non-negative forzin the previously-broken 4.5-7 range.Added tests cover the mpmath reference values, the invariants, and the Normal tail. Full suite green locally (
AllButLinearAlgebra+LinearAlgebra), plusparallel-lint,phpcs, andphpstan.