Skip to content

Fix DivisionByZeroError in the hypergeometric family for alternating series - #492

Open
gaoflow wants to merge 1 commit into
markrogoyski:developfrom
gaoflow:fix-hypergeometric-alternating-series-divide-by-zero
Open

Fix DivisionByZeroError in the hypergeometric family for alternating series#492
gaoflow wants to merge 1 commit into
markrogoyski:developfrom
gaoflow:fix-hypergeometric-alternating-series-divide-by-zero

Conversation

@gaoflow

@gaoflow gaoflow commented Jul 28, 2026

Copy link
Copy Markdown

The shared generalizedHypergeometric() convergence loop tests

while (\abs($product / $sum) > $tol && $iteration < self::MAX_ITERATIONS);

which divides by the running partial sum $sum. For any alternating series the partial sum passes through exactly 0 on an early iteration, so this raises DivisionByZeroError on finite, well-defined inputs. It reaches the public confluentHypergeometric (₁F₁) and hypergeometric (₂F₁) entry points:

Special::confluentHypergeometric(1, 1, -1);  // ₁F₁(1;1;-1) = e⁻¹ ≈ 0.36788, throws
Special::confluentHypergeometric(2, 2, -1);  // throws
Special::hypergeometric(2, 2, 2, -0.5);      // ₂F₁(2,2,2,z) = (1-z)⁻² = 0.44444, throws

₁F₁(a;a;z) ≡ eᶻ is a textbook identity, so the first case unambiguously should return e⁻¹.

A grid sweep of ₁F₁ (a ∈ {-0.5, 0.5, 1, 1.5, 2, 3}, b ∈ {0.5, 1, 1.5, 2, 3}, z ∈ -3..5) and ₂F₁ (|z| < 1) found 15 crashing inputs, all the same root cause, all on finite values.

Fix

Compare the term magnitude to $tol * \abs($sum) (multiply) instead of \abs($product / $sum) (divide). Multiplying can never divide by zero, and for a non-zero $sum it is algebraically identical to the old test, so every already-correct result is unchanged. When the partial sum is momentarily zero the loop simply keeps iterating. The MAX_ITERATIONS guard is retained.

The sibling lowerIncompleteGammaSeries() in the same file already guards the identical divide-by-accumulator (\abs($sum) > 0 && \abs($term / $sum) < $tol); this brings the hypergeometric loop in line with it.

No regression

I compared the fixed output against the unpatched output across the full grid: all 500+ currently-working inputs are bit-for-bit identical (as expected, since the convergence test is only algebraically rearranged), and the 15 formerly-crashing inputs now match mpmath hyp1f1/hyp2f1 (dps 30 and 35 in agreement) to the series' ~1e-8 tolerance.

Tests

Added data-provider regression tests across ₁F₁ and ₂F₁ for the zero-crossing inputs, plus the ₁F₁(a;a;z) = eᶻ axiom over the sign of z. Expected values are from mpmath (dps 45 and 50 agree). Full suite is green (phpunit, phpcs, phpstan).

The shared generalizedHypergeometric() convergence loop tested
abs(product / sum) > tol, dividing by the running partial sum. For
alternating series (e.g. z < 0) that sum passes through exactly 0 on an
early partial sum, so the test raised DivisionByZeroError on finite,
well-defined values. This reached the public confluentHypergeometric
(1F1) and hypergeometric (2F1) entry points, e.g. 1F1(1,1,-1), which is
the identity e^z and should return e^-1 = 0.3679.

Test convergence by comparing the term magnitude to tol * |sum| instead:
multiplying can never divide by zero, and for a non-zero sum it is
algebraically identical to the old test, so already-correct results are
unchanged. When the partial sum is momentarily 0 the loop simply keeps
iterating. The existing MAX_ITERATIONS guard is retained.

The sibling lowerIncompleteGammaSeries() in the same file already guards
its identical divide-by-accumulator (abs(sum) > 0 && ...); this back-ports
that safety to the hypergeometric loop.

Adds regression tests across 1F1 and 2F1 for inputs whose partial sum
crosses zero, plus the 1F1(a;a;z) = e^z axiom over the sign of z. Expected
values from mpmath (dps 45 and 50 agree).
@gaoflow

gaoflow commented Jul 31, 2026

Copy link
Copy Markdown
Author

The red jobs aren't from this change: all 53 failures are ReflectionException from ReflectionMethod invocations on private Piecewise/Polynomial/Finance/chebyshevEval methods in files this PR doesn't touch, and #491 (a disjoint diff) fails with the identical list. The same tests pass locally with the repo's pinned PHPUnit 8.5.53, so it looks like a CI runner/PHP-build issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant