Skip to content

Use the NSWC GAMLN1 rational approximation in the incomplete beta routines - #543

Merged
andreasnoack merged 3 commits into
masterfrom
an/beta-inc-gamln1
Aug 17, 2026
Merged

Use the NSWC GAMLN1 rational approximation in the incomplete beta routines#543
andreasnoack merged 3 commits into
masterfrom
an/beta-inc-gamln1

Conversation

@andreasnoack

Copy link
Copy Markdown
Member

Second follow-up from JuliaStats/StatsFuns.jl#228 (companion to the truncation PR; the two are independent and conflict only trivially in one hunk).

Where the original NSWC BPSER/BRCOMP routines call GAMLN1 — a pair of minimax rational approximations of log(Γ(1+a)) on -0.2 ≤ a ≤ 1.25 — the beta_inc translation substituted loggamma1p from the incomplete gamma implementation (Gil–Segura–Temme: an 18-term Chebyshev recurrence via auxgam plus a log1p). loggamma1p is the more accurate function, but it is ~8× more expensive (30 ns vs 4 ns measured), and inside the incomplete beta routines the extra accuracy cannot reach the final result. This PR ports GAMLN1 from the NSWC source (https://github.com/jacobwilliams/nswc) and uses it at the four beta_inc.jl call sites. loggamma1p itself is untouched and remains in use by gamma_inc/gamma_inc_inv, where its wider domain and higher relative accuracy matter.

Why it is safe here (also documented in the docstring):

  • Domain: all four call sites pass a0 = min(a, b) inside an a0 < 1 branch, with zero arguments handled earlier, so the argument is always in (0, 1) — well inside GAMLN1's designed domain [-0.2, 1.25] and away from the negative edge where its error grows.
  • Error propagation: on (0, 1), |log Γ(1+a)| ≤ 0.1215. A 200k-point sweep against a 256-bit reference gives max 8.0 ulp / mean 2.6 ulp for gamln1 (vs 4.6 / 0.9 for loggamma1p) — but relative to a value bounded by 0.1215, 8 ulps is an absolute error below 2e-16. The result is only ever consumed through exp(z - u), where an absolute perturbation of that size changes the final incomplete beta value by less than one ulp.

Consistent with that, all results in the test suite and in spot checks are bit-identical before and after.

Benchmarks (Apple Silicon, @belapsed, this branch alone vs current master):

case master this PR with the truncation PR as well
beta_inc(0.5, 2.5, 0.3103) 217 ns 168 ns (−23%) 127 ns
StatsFuns.tdistcdf(5, 1.5) 223 ns 171 ns (−23%) 129 ns
StatsFuns.tdistinvcdf(5, 1e-8) 1642 ns 1242 ns (−24%) 810 ns

Disclosure: this PR was prepared by Claude Code at my direction; I have reviewed the changes, the accuracy sweep, and the benchmark methodology.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.50%. Comparing base (1d104a8) to head (9c67158).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #543      +/-   ##
==========================================
+ Coverage   94.49%   94.50%   +0.01%     
==========================================
  Files          14       14              
  Lines        3016     3023       +7     
==========================================
+ Hits         2850     2857       +7     
  Misses        166      166              
Flag Coverage Δ
unittests 94.50% <100.00%> (+0.01%) ⬆️

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 requested a review from devmotion August 17, 2026 08:30
…tines

The incomplete beta port substituted loggamma1p (from the incomplete
gamma implementation) where the original NSWC routines call GAMLN1.
loggamma1p is more accurate but roughly eight times more expensive: an
18-term Chebyshev recurrence plus a log1p, versus a single rational
function evaluation.

Within the incomplete beta routines the extra accuracy does not affect
the result: the argument is always a0 = min(a, b) in (0, 1), where
|log(gamma(1 + a))| <= 0.1215, so even a few ulps of relative error is
an absolute error below 2e-16, and the value is only consumed through
exp(z - u). Test suite results are unchanged.

This makes beta_inc about 25% faster on the min(a, b) < 1 branches.

@devmotion devmotion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM apart from the comments below. I compared the coefficients and implementation with the Fortran source, and everything seems to match.

Comment thread src/beta_inc.jl Outdated
Comment thread src/beta_inc.jl Outdated
Comment thread src/beta_inc.jl Outdated
Comment thread src/beta_inc.jl Outdated
Comment thread src/beta_inc.jl Outdated
Review feedback: replace the @evalpoly macro with the evalpoly function
(coefficients passed as tuples, since evalpoly has no vararg method),
and add tests backing the docstring claims: gamln1 matches a 256-bit
loggamma(1 + a) reference to within 2e-16 absolute error on [0, 1]
(5e-16 on the full [-0.2, 1.25] domain), and substituting gamln1 for
loggamma1p perturbs exp(z - u) by less than one ulp.
Comment thread test/beta_inc.jl Outdated
Comment thread src/beta_inc.jl Outdated
Docstring: the domain argument now rests on beta_inc's input validation
rather than on "all call sites" (beta_integrand is also reachable from
ncbeta with unvalidated parameters, harmlessly since beta_inc throws a
DomainError for the same input on the next line), the perturbation claim
is stated as "at most about one ulp", and the measured ulp errors and
cost ratio are quoted.

Tests: assert both the absolute bound (binding near the minimum of
loggamma(1 + a)) and a 16/32 ulp bound (binding near the zeros at
a = 0 and a = 1), add log-spaced grids for a -> 0 and a -> 1, compare
directly against the BigFloat reference, loosen the full-domain
absolute bound from 5e-16 to a bit-portable 8e-16 (FMA contraction in
evalpoly is target-dependent), and drop the expm1 cross-check against
loggamma1p, which asserted agreement with another approximation rather
than with the true value.
@andreasnoack
andreasnoack merged commit 10e4883 into master Aug 17, 2026
15 checks passed
@andreasnoack
andreasnoack deleted the an/beta-inc-gamln1 branch August 17, 2026 19:51
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