Skip to content

Correct the scale of the continued fraction's convergence test - #553

Merged
stevengj merged 2 commits into
fix/expint-same-precisionfrom
fix/expint-cf-criterion
Aug 25, 2026
Merged

Correct the scale of the continued fraction's convergence test#553
stevengj merged 2 commits into
fix/expint-same-precisionfrom
fix/expint-cf-criterion

Conversation

@andreasnoack

@andreasnoack andreasnoack commented Aug 23, 2026

Copy link
Copy Markdown
Member

Stacked on #550.

En_cf_nogamma's convergence test compared abs(Aprev*B - A*Bprev) against 10*eps(real(B))*abs(B*Bprev). Three things are wrong with that:

  • eps(real(B)) is eps of the value z + ν, not of the type, so the tolerance grows with |z| — 7.1e-15 at |z| = 40.
  • Dividing through by |B*Bprev| shows the test to be |Aprev/Bprev - A/B| < ϵ, i.e. absolute, on a convergent of size |e^z E_ν(z)| ≈ 1/|z|. With the first point that gives an effective relative tolerance of about 10*eps(one(T))*|z|².
  • B*Bprev is the largest of the three products (|B| ≈ |z||A|), so it reaches floatmax while the numerator's mixed A·B products are still finite, and finite < Inf then succeeds unconditionally.

The last one is #545. Running the original loop on the Float32 seed points the real(z) < 0 branch picks for the reported cases, every one exits on an overflowed threshold:

seed z iters rescales rel. err
−3.5 + 0.00121i 34 1 0.874
−4.0 + 0.00138i 221 21 0.581
−6.0 + 0.00207i 196 18 0.174
−8.0 + 0.00276i 78 5 0.0554
−10.0 + 0.00345i 20 0 0.0179

The fix uses eps(T) and abs(A*Bprev), moves the rescaling above the test, tests both |A| and |B|, and lowers the threshold to sqrt(floatmax(T))/4. Each of those is load-bearing: the bound has to hold when the test consumes the products rather than one iteration earlier (for Float16, floatmax^(1/4) is 16, so one iteration's growth can overflow them); A overtakes B by up to 112x near the cut, so checking A alone understates the maximum by |z|; and the margin covers the overshoot between checks, there being two recurrence steps per loop iteration.

Effect

Against Complex{BigFloat} references at 200 bits, grid ±60 × ±30:

ν worst before worst after mean before mean after
1 2.01e-12 4.25e-15 7.96e-14 3.68e-16
2 1.55e-12 2.62e-15 1.14e-13 3.48e-16
2.5 1.93e-12 2.63e-15 1.21e-13 3.79e-16
1+i 1.17e-12 4.05e-15 1.03e-13 3.39e-16

Both half-planes improve by comparable factors — the right half-plane never touches the real(z) < 0 walk, so the fix stands on its own there.

Narrow types, which is what #545 reports:

before after
#545's eight rows (Float32) worst 0.851 worst 8.79e-7
Float32 grid, ν = 2.5, 844 points 83 above 1e-5, worst 0.864 0 above 1e-5, worst 1.23e-6
Float16 grid, ν = 2, 458 points 90 above 1e-2, worst 0.651 0 above 1e-2, median 1.8 ulps

Cost: unchanged per iteration (3.50 → 3.88 ns real, 16.7 → 17.0 ns complex, for En_cf at ν = 2.5); the ~15% more iterations are the ones the old test skipped.

Tests

A new expint continued fraction testset covers nine points with abs2(z) > 9, for ν = 2, 2.5, 3, 5 and 10, against MPFR's incomplete gamma at rtol = 50*eps(Float64). All nine fail on the base branch, by 74 to 1313 ulps, and pass here, within 8.

Full suite passes: expint 6373 pass, 1 pre-existing @test_broken.

Not fixed here

#453 improves about 5x (worst 2.75e-13 at z = 31.8 → 5.54e-14 at z = 2.89) but stays open: its residual is the cancellation in the series about the origin below |z| = 3, which is #551's mechanism. #546 is untouched. Details and measurements in #453 (comment).

🤖 Generated with Claude Code

@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.68%. Comparing base (ef23f33) to head (b10bc46).

Additional details and impacted files
@@                    Coverage Diff                     @@
##           fix/expint-same-precision     #553   +/-   ##
==========================================================
  Coverage                      94.68%   94.68%           
==========================================================
  Files                             14       14           
  Lines                           3010     3010           
==========================================================
  Hits                            2850     2850           
  Misses                           160      160           
Flag Coverage Δ
unittests 94.68% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@andreasnoack
andreasnoack force-pushed the fix/expint-same-precision branch from 523a092 to ef23f33 Compare August 23, 2026 18:32
`En_cf_nogamma` compared `abs(Aprev*B - A*Bprev)` against
`10*eps(real(B))*abs(B*Bprev)`, which is wrong on three counts.

`eps(real(B))` is `eps` of the value `z + ν` rather than of the type, so the
tolerance grew with `|z|`: 7.1e-15 at `|z| = 40` instead of 2.2e-16. And
dividing the comparison through by `|B*Bprev|` shows it to be
`|Aprev/Bprev - A/B| < ϵ`, an absolute test on a convergent of size
`|e^z E_ν(z)| ≈ 1/|z|`. Together these give an effective relative tolerance of
about `10*eps(one(T))*|z|²`, an error floor that grows with the argument:
2.8e-13 by `z = 32` for `ν = 2` on the positive real axis, against 1.8e-15 now.

`B*Bprev` is also the largest of the three products, since `|B| ≈ |z||A|`, so it
reaches `floatmax` while the numerator's mixed `A·B` products are still finite,
and the comparison then succeeds against `Inf`. That is #545: every `Float32`
seed point the `real(z) < 0` branch picks for the cases reported there exits
that way, after 20 to 221 iterations, with relative errors from 0.0179 to 0.874.
The rescaling below the test cannot prevent it, having fired at most once by
then.

The test now uses `eps(T)` and `abs(A*Bprev)`, so the threshold is the size of
the terms it is compared against, and the rescaling moves above it. The
placement matters: the bound has to hold when the test consumes the products,
not one iteration earlier, and for `Float16` `floatmax^(1/4)` is only 16, so a
single iteration's growth can overflow them. The rescaling also has to consider
`B` as well as `A`, since near the negative real axis the two sequences decouple
and `A` leads by up to 112x, so testing `A` alone understates the maximum by a
factor of `|z|`. `sqrt(floatmax(T))` becomes `sqrt(floatmax(T))/4` to leave
margin for the overshoot between checks, there being two recurrence steps per
loop iteration.

Over a grid of ±60 × ±30 for `ν = 1, 2, 2.5, 1+i` the worst relative error drops
from 2.0e-12 to 4.3e-15 and the mean from 8.0e-14 to 3.7e-16, in both half
planes, and the `Float32` and `Float16` cases of #545 return to the resolution
of their types. The cost per iteration is unchanged; the roughly 15 percent more
iterations are the ones the old test skipped.

Fixes #545.
Nine points with `abs2(z) > 9`, checked against MPFR's incomplete gamma at
`rtol = 50*eps(Float64)`. All nine fail before the previous commit, by 74 to 1313
ulps, and pass after it, within 8.
@andreasnoack
andreasnoack force-pushed the fix/expint-cf-criterion branch from 55b92e4 to b10bc46 Compare August 23, 2026 19:22
@stevengj

Copy link
Copy Markdown
Member

LGTM.

@stevengj
stevengj merged commit 87a4eb6 into fix/expint-same-precision Aug 25, 2026
15 checks passed
@stevengj
stevengj deleted the fix/expint-cf-criterion branch August 25, 2026 15:40
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.

2 participants