Skip to content

Generate Bessel I0 coefficients in Python and fix cancellations while evaluating the Kaiser-Bessel window. - #248

Draft
jenskeiner wants to merge 2 commits into
feature/kaiser-bessel-windowfrom
feature/bessel
Draft

Generate Bessel I0 coefficients in Python and fix cancellations while evaluating the Kaiser-Bessel window.#248
jenskeiner wants to merge 2 commits into
feature/kaiser-bessel-windowfrom
feature/bessel

Conversation

@jenskeiner

@jenskeiner jenskeiner commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Replace the Mathematica notebook that produced the I0 coefficient tables with tools/besselgen, a deterministic mpmath minimax fitter, and rework the evaluation.

Generator (tools/besselgen)

  • Weighted Remez in the Chebyshev basis, the weight chosen so the relative error of the final expression equioscillates.
  • Emits kernel/util/bessel_i0_data.h for all four floating-point formats.

I0 (kernel/util/bessel_i0.c)

x <= split

Evaluate I0(x) = 1 + y*P1(y), y = (x/2)^2, Horner over positive coefficients. Replaces a Chebyshev-basis rational evaluated at y = x*x, far outside the domain that basis is conditioned for.

bessel_i0_log and bessel_i0_logtail use LOG1P, i.e. log(I0(x)) = LOG1P(y*P1(y)) which avoids cancellations where log I0(x) ~ x^2/4 near zero.

x > split

Evaluate I0(x) = exp(x) / sqrt(x) * p. For p map (split, inf) to (-1,1) and use Clenshaw. Use a slightly different order of evaluation of exp(x) alone would already overflow. This correctly evaluates results where I0(x) itself is still representable.

Kaiser-Bessel window (kernel/util/window.c)

Phi_hut now always takes the log form. Several other improvements, e.g. use (b-t)(b+t) and (m-nx)(m+nx) instead of differences of squares, and EXPM1 instead of a difference of exponentials for sinh.

Tests

  • New kaiser_bessel_cancellation case: phi_hut against 60-digit reference values for the near-peak bins and the band edge.
  • bessel_i0 bounds tightened from the measured worst case: 24 -> 8 single, 58 -> 16 quadruple.

See bessel-i0-kaiser-bessel-accuracy.html for more details.

Comment thread tools/besselgen/remez.py Fixed
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown

Accuracy report

2213 unchanged · 95 improved · 0 regressed

module improved regressed
nfct 13 0
nfft 62 0
nfst 20 0

📊 Full accuracy report

@jenskeiner
jenskeiner marked this pull request as draft August 26, 2026 11:00
@jenskeiner

Copy link
Copy Markdown
Contributor Author

Leaving as draft until benchmarks show no regressions.

@jenskeiner jenskeiner changed the title Generate Bessel I0 coefficients in Python, and fix its cancellations Generate Bessel I0 coefficients in Python and fix cancellations while evaluating the Kaiser-Bessel window. Aug 27, 2026
The generated I0 was accurate but slow: 10.8 ns/call against 2.5 for the
hand-written tables it replaced, and phi_hut inherited a factor of six. The
coefficients were never the problem, the evaluation shape was.

bessel_i0.c
- Branch 2 ships as monomial coefficients in u = 1/x rather than a Chebyshev
  series in t. A sum can be regrouped; the Clenshaw recurrence cannot, and that
  alone cost the asymptotic branch a factor of four.
- Both branches sum as four independent chains over the coefficients sharing a
  residue mod 4 (poly4), so the dependent-operation count is a quarter of a
  Horner pass. Table lengths are multiples of four, so the loop needs no
  prologue.
- exp(x)*sqrt(u) instead of a second divide by sqrt(x), and log I0 = x +
  LOG(SQRT(u)*p) instead of two separate logarithms.

Regrouping a sum is only safe when it cannot cancel, and that turned out to pin
the split. P1 is fine at any degree, its coefficients all positive. P2 is not:
its monomial coefficients grow with the degree, and past roughly degree 24 the
growth factor leaves 1 and climbs -- 466 at MANT_DIG 64 with a split of 10,
1.2e9 at 113 with a split of 15. The split now moves per format to the smallest
value holding that factor at 1, and branch 1 absorbs the degree. Long double
splits at 15 and quadruple at 25.

window.c
- phi_hut used the log domain for I0(a)/I0(m b) to keep the peak from
  overflowing, which cost a LOG. Adding bessel_i0_exp_scaled -- exp(-x)*I0(x),
  in (0, 1] -- lets it be exp(a - m b) * i0e(a) / i0e(m b) instead. Above the
  split the exponential cancels against the asymptotic form, so i0e is cheaper
  than I0 itself, and a - m b = -m t^2/(ra + b) is still formed without the
  cancellation the log domain was there to avoid.
- 1/i0e(m b) is cached per axis, so the call carries no division. Both factors
  are bounded, so the peak normalisation cannot overflow at any m b.

Measured (double, aarch64, glibc, ns/call):

  bessel_i0        [0,18.85]  2.76 old  6.37 before  2.30 now
                   [0,94.2]   2.51 old 10.82 before  3.00 now
  bessel_i0_log               ~4.5 old 15.27 before  4.65 now
  kb_phi_hut m=11             4.08 old 23.66 before  5.79 now
  kb_phi     m=11             3.83 old  4.01 before  3.63 now

Accuracy is unchanged or better. bessel_i0 is 2.31 ulp on (0,100] against 8.20
for the pre-generator tables and 2.39 before this change; kb_phi_hut matches the
log-domain form it replaces to within noise (5.76 vs 5.25 ulp at m=11 sigma=2)
and stays 10-20x clear of the naive I0(a)/I0(m b) ratio.

tests/besselgen
- emulate mirrors poly4, so the fixed-precision emulation still reproduces the
  arithmetic the C performs.
- The precision-parametrised tests cover 64 and 113 bits as well, at the
  working precision the generator uses. That is the only cover Intel double
  extended gets on a host whose long double is binary128.
- New tests for the two invariants the scheme now rests on: P2 cannot cancel at
  any pinned split, and every table length is a multiple of four.

Also fixes an unterminated comment in window.c that had swallowed the
kb_inv_peak header.

Claude-Session: https://claude.ai/code/session_01GPi5g9NWB2rgkdr53xN92D
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