Ben Rached, Hoel and Meo on the FFT accuracy floor

Reproducing Ben Rached, Hoel and Meo (2024), and testing its central claim against aggregate

Author

Stephen J. Mildenhall

Published

2026-10-22 19:55 commit b5ca906+

1 Introduction

Ben Rached et al. (2024) is a paper about aggregate’s FFT-based method, and its central claim is a criticism of it.

The authors compute \(\alpha = \mathsf P(\sum_{i=1}^n X_i < \gamma)\) for small \(\gamma\) by discretizing the severity density on a uniform mesh and convolving it \(n\) times. That is what aggregate does. The difference is the convolution: they use direct summation, and they argue, with a theorem and with numerical evidence, that using the FFT instead loses relative accuracy in the deep left tail.

We further compare direct-based convolution to FFT-based convolution, and show by formal theoretical arguments and in numerical experiments that FFT-based convolution is more sensitive to rounding errors and a less reliable method when the magnitude of the probability of failure is sufficiently small.

Their Lemma 4 and Theorem 3 make this quantitative. The FFT error is bounded in absolute terms, proportional to machine epsilon times the total mass, whereas direct convolution admits a relative bound, because summing and multiplying non-negative floating point numbers never cancels:

when \(\alpha \ll 1\), the result indicates that for a given resolution \(N\), the relative error may be substantially smaller for direct-based convolution than for FFT-based convolution.

So this example has two jobs. First, to reproduce what the paper publishes. And second, take the criticism seriously and find out exactly where aggregate stops working.

2 What is reproducible

Three published tables carry values rather than convergence rates.

  • Table 6, Figure 1: CDF and PDF of \(S_{16}\) for \(X_i \sim \mathrm{LN}(0, 0.125^2)\), ten values of \(x\) with \(\gamma = 16x\), quoted to four significant figures, computed two independent ways (their convolution method and their implementation of the saddlepoint method of Asmussen et al. (2016)) which agree.
  • Table 3, Figure 2: a reference solution column for the same distribution, computed at 512-bit precision, spanning \(\alpha\) from \(1.63 \times 10^{-4}\) down to \(2.05 \times 10^{-83}\), alongside the measured relative error of 64-bit direct convolution and of FFT at four precisions.
  • Table 4, Figure 3: the same for Lévy summands, where the exact answer is known in closed form because the Lévy distribution is stable.

Figures 1 to 5 are convergence plots and are not reproduced here.

3 Setup

%xmode minimal
import numpy as np
import pandas as pd
from functools import partial

from bookstyle import GT, build

# Tail probabilities and relative errors read as powers of ten, not SI prefixes.
GT = partial(GT, eng_style='exp')
from scipy.special import erfc

def lognormal_sum(n, sigma, log2=22, bs=2.0 ** -13):
    return build(f'agg S dfreq [{n}] sev 1 * lognorm {sigma}',
                 log2=log2, bs=bs, normalize=False)

S16 = lognormal_sum(16, 0.125)
print(f'E[S_16] from agg  {S16.est_m:.10f}')
print(f'E[S_16] exact     {16 * np.exp(0.125 ** 2 / 2):.10f}')
Exception reporting mode: Minimal
E[S_16] from agg  16.1254895553
E[S_16] exact     16.1254895553

4 Table 6: CDF and PDF

The problem is \(\alpha = F_Y(\gamma)\) with \(\gamma = 16x\), \(Y = \sum_{i=1}^{16} X_i\), \(X_i \sim \mathrm{LN}(0, \sigma^2)\) and \(\sigma = 0.125\). The paper notes a caveat on its own saddlepoint column:

Note that the values calculated with our implementation of the saddlepoint method gives slightly different values compared with the values listed in the paper (Asmussen et al. 2016) (\(\pm 1\) in the third digit of the significand).

Figure 1: Ben Rached, Hoel and Meo (2024), Table 6, approximations of the CDF and PDF of \(Y\).
paper = [(0.70, 1.761e-31, 5.873e-30), (0.80, 9.806e-14, 1.829e-12),
         (0.85, 3.031e-08, 3.975e-07), (0.90, 1.631e-04, 1.388e-03),
         (0.91, 5.955e-04, 4.577e-03), (0.92, 1.911e-03, 1.318e-02),
         (0.93, 5.423e-03, 3.332e-02), (0.94, 1.368e-02, 7.416e-02),
         (0.95, 3.081e-02, 1.460e-01), (0.98, 1.901e-01, 5.520e-01)]

rows = []
for x, cdf_paper, pdf_paper in paper:
    gamma = 16 * x
    cdf, pdf = S16.cdf(gamma), S16.pdf(gamma)
    rows.append({'x': x, 'gamma': gamma,
                 'paper CDF': cdf_paper, 'agg CDF': cdf,
                 'CDF rel err': abs(cdf / cdf_paper - 1),
                 'paper PDF': pdf_paper, 'agg PDF': pdf,
                 'PDF rel err': abs(pdf / pdf_paper - 1)})
GT(pd.DataFrame(rows))
Table 1: Table 6, the CDF and PDF of \(S_{16}\) at ten thresholds, against the published values.
xgammapaper CDFagg CDFCDF rel errpaper PDFagg PDFPDF rel err
0.7000011.20176.100e-33-49.396e-182.80e+145.873e-30-4.018e-186.84e+11
0.8000012.8098.060e-1598.010e-155.06e-041.829e-121.829e-122.67e-04
0.8500013.6030.310e-0930.325e-094.89e-04397.500e-09397.542e-091.05e-04
0.9000014.40163.100e-06163.093e-064.26e-051.388e-031.388e-032.63e-04
0.9100014.56595.500e-06595.518e-062.97e-054.577e-034.577e-036.05e-05
0.9200014.721.911e-031.912e-034.75e-0413.180e-0313.185e-033.73e-04
0.9300014.885.423e-035.422e-032.60e-0433.320e-0333.319e-033.22e-05
0.9400015.0413.680e-0313.674e-034.61e-0474.160e-0374.160e-034.58e-06
0.9500015.2030.810e-0330.814e-031.35e-04146.000e-03145.958e-032.89e-04
0.9800015.68190.100e-03190.099e-035.31e-06552.000e-03552.043e-037.81e-05

In Table 1 nine of the ten rows reproduce to between \(5 \times 10^{-6}\) and \(1.2 \times 10^{-3}\) relative, in both the CDF and the PDF. Since the paper quotes four significant figures, that is agreement to the last published digit almost everywhere.

The first row fails completely. The target is \(1.761 \times 10^{-31}\) and aggregate returns \(3.4 \times 10^{-17}\), wrong by fourteen orders of magnitude. That is precisely the effect the paper was written about, and the next section measures it.

5 Table 3: locating the floor

Table 3 is the experiment that motivates the paper. The reference solution is computed with 512-bit arithmetic, and the other columns give relative error for 64-bit direct convolution and for FFT at 32, 64, 128 and 256 bits. The published 64-bit FFT column is reproduced here so that aggregate can be placed against it.

Figure 2: Ben Rached, Hoel and Meo (2024), Table 3.
# gamma, reference solution, published relative error of the authors' 64-bit FFT
paper = [(8.8, 2.05e-83, 1.67e+67), (9.6, 1.02e-61, 1.27e+45),
         (10.4, 1.04e-44, 2.23e+28), (11.2, 1.76e-31, 2.94e+15),
         (12.0, 2.45e-21, 3.20e+04), (12.8, 9.81e-14, 2.37e-03),
         (13.6, 3.03e-08, 1.23e-08), (14.4, 1.63e-04, 4.37e-13)]

rows = []
for gamma, ref, fft64_rel in paper:
    val = S16.cdf(gamma)
    rows.append({'gamma': gamma, 'reference': ref, 'agg': val,
                 'agg rel err': abs(val / ref - 1),
                 'agg abs err': abs(val - ref),
                 'paper FFT64 rel err': fft64_rel,
                 'paper FFT64 abs err': fft64_rel * ref})
GT(pd.DataFrame(rows))
Table 2: Table 3, the 512-bit reference solution against the library and the paper’s own 64-bit FFT, in relative and absolute terms.
gammareferenceaggagg rel erragg abs errpaper FFT64 rel errpaper FFT64 abs err
8.8020.500e-8430.241e-181.48e+663.02e-171.67e+673.42e-16
9.60102.000e-6322.145e-182.17e+442.21e-171.27e+451.30e-16
10.4010.400e-45-19.935e-181.92e+271.99e-172.23e+282.32e-16
11.20176.000e-33-49.396e-182.81e+144.94e-172.94e+155.17e-16
12.002.450e-21-41.301e-181.69e+044.13e-173.20e+047.84e-17
12.8098.100e-1598.010e-159.13e-048.96e-172.37e-032.32e-16
13.6030.300e-0930.325e-098.19e-042.48e-111.23e-083.73e-16
14.40163.000e-06163.093e-065.71e-049.31e-084.37e-137.12e-17

Read the last two columns of Table 2 together. The paper’s own 64-bit FFT has a relative error that explodes as \(\gamma\) falls, from \(4 \times 10^{-13}\) to \(1.7 \times 10^{67}\), but its absolute error never leaves the band \(7 \times 10^{-17}\) to \(5 \times 10^{-16}\). aggregate shows the identical signature, with an absolute error pinned between \(9 \times 10^{-18}\) and \(7 \times 10^{-17}\) on every row where the target is smaller than that. The two implementations differ in almost every detail, and they land on the same wall, because the wall is machine epsilon and not the implementation.

The \(\gamma = 12\) row is the sharpest single check. The paper’s FFT64 relative error is \(3.20 \times 10^{4}\) against a reference of \(2.45 \times 10^{-21}\), implying an absolute error of \(7.8 \times 10^{-17}\). aggregate returns \(7.07 \times 10^{-17}\) where the true answer is \(2.45 \times 10^{-21}\), so its absolute error is also \(7.07 \times 10^{-17}\). Two unrelated FFT codes agreeing to 10% on the magnitude of their own roundoff noise is a strong confirmation of Theorem 3.

5.1 The floor does not move

If this were a resolution problem, refining the grid would fix it. Table 3 shows it is not.

rows = []
for log2, bs, padding in [(22, 2.0 ** -13, 1), (22, 2.0 ** -13, 2),
                          (23, 2.0 ** -13, 2), (24, 2.0 ** -14, 2)]:
    obj = build('agg S dfreq [16] sev 1 * lognorm 0.125',
                log2=log2, bs=bs, padding=padding, normalize=False)
    rows.append({'log2': log2, 'bs': f'2^{int(np.log2(bs))}',
                 'padding': padding,
                 'F(12.0)  [true 2.45e-21]': obj.cdf(12.0),
                 'F(12.8)  [true 9.81e-14]': obj.cdf(12.8),
                 'F(14.4)  [true 1.63e-04]': obj.cdf(14.4)})
GT(pd.DataFrame(rows))
Table 3: Three probabilities across four grid settings, showing which are stable under refinement and which are noise.
log2bspaddingF(12.0) [true 2.45e-21]F(12.8) [true 9.81e-14]F(14.4) [true 1.63e-04]
222^-131-41.301e-1898.010e-15163.093e-06
222^-132-81.443e-1897.993e-15163.093e-06
232^-132-99.080e-1897.955e-15163.093e-06
242^-14216.561e-1898.126e-15163.135e-06

In Table 3, across a factor of four in resolution and both padding settings, F(12.0) wanders between \(-8.4 \times 10^{-17}\) and \(+1.0 \times 10^{-16}\), changing sign. A quantity that flips sign under refinement is not converging to anything; it is noise. Meanwhile F(12.8) is stable to three figures and F(14.4) to six.

So the operating range is clear. For this problem aggregate is accurate to roughly four significant figures down to \(\alpha \approx 10^{-13}\), and returns nothing usable below that. The transition is abrupt because the error is absolute: once the target drops below the noise, there is no partial credit.

6 Table 4: Lévy summands, and a harder limit

Table 4 replaces the log-normal with \(X_i \sim \text{Lévy}(0, 0.1)\). This is a stable law, so \(Y = \sum_{i=1}^{16} X_i \sim \text{Lévy}(0, 25.6)\) and the answer is exact:

\[\alpha = \mathsf P(Y \le \gamma) = \operatorname{erfc}\!\left(\sqrt{12.8 / \gamma}\right).\]

Figure 3: Ben Rached, Hoel and Meo (2024), Table 4.
levy = build('agg L dfreq [16] sev 0.1 * levy',
             log2=22, bs=2.0 ** -12, normalize=False)

rows = []
for gamma, published in [(0.05, 2.33e-113), (0.1, 1.28e-57), (0.2, 1.12e-29),
                         (0.5, 8.34e-13), (1.0, 4.20e-07)]:
    rows.append({'gamma': gamma, 'paper': published,
                 'exact erfc': erfc(np.sqrt(12.8 / gamma)),
                 'agg': levy.cdf(gamma)})
GT(pd.DataFrame(rows))
V:\worktrees\aggregate_REFACTOR\src\aggregate\_aggregate.py:4481: DefectiveDistributionWarning: L: aggregate PMF deficit 1.256e-01 (Σp = 1 − 1.256e-01 < 1); forwards and backwards S diverge by the deficit (forwards > backwards). Further occurrences this session are suppressed; each object records its own verdict in validation.
  rv = self.update_work(xs, debug=debug, x_min=x_min, x_max=x_max,
Table 4: Table 4, Lévy summands, where the closed form is exact and the library fails on every row.
gammapaperexact erfcagg
0.0500023.300e-11423.285e-11410.453e-09
0.100001.280e-571.278e-5720.900e-09
0.2000011.200e-3011.224e-3041.780e-09
0.50000834.000e-15834.186e-15104.249e-09
1.00000420.000e-09420.039e-09628.666e-09

In Table 4 aggregate fails on every row, and this time the reason is not roundoff. The Lévy density has \(\mathsf P(X > x) \sim \sqrt{2c/(\pi x)}\), a right tail so heavy that the mean is infinite. No uniform grid of finite extent can carry it: aggregate raises DefectiveDistributionWarning and reports a mass deficit of about 12.6%, and the missing mass wraps around the circular convolution straight into the region being measured.

The values returned, between \(10^{-8}\) and \(6 \times 10^{-7}\), are that wrapped mass and nothing else. Note that they barely vary with \(\gamma\) across sixteen orders of magnitude of true probability, which is the giveaway.

This is a genuine and structural limitation, not a tuning problem, and it is worth stating plainly. The direct convolution of Ben Rached et al. (2024) does not suffer it because the method never represents the distribution outside \([0, \gamma]\) at all. It builds the left tail without ever needing to know where the bulk is.

7 Discussion

Two conclusions, one comfortable and one not.

The comfortable one is that Table 6 reproduces almost exactly. Nine rows, CDF and PDF, four significant figures, from one build call, against a method the authors cross-validated against an independent saddlepoint implementation. For probabilities down to about \(10^{-13}\), aggregate is doing what a purpose-built rare-event method does.

The uncomfortable one is that the authors are right about the FFT. Their Theorem 3 predicts an absolute error floor of order machine epsilon, and aggregate sits on that floor to within a factor of a few. Below \(\alpha \approx 10^{-13}\) the output is noise, it changes sign under refinement, and no amount of grid refinement or padding recovers it. Anyone using aggregate for very small left-tail probabilities needs to know this, and needs a way to detect it. The refinement test above is that way: build twice at different resolutions and compare. Stability to several figures is evidence; a sign change is proof of failure.

It is worth being clear about what the tradeoff buys, because the comparison is not one-sided. The direct convolution method is \(O(N^2 \log_2 n)\) against \(O(N \log N \log_2 n)\) for the FFT, and it obtains its relative accuracy by restricting attention to \([0, \gamma]\), which means it answers one question about one threshold. aggregate carries the whole distribution, which is what makes every quantile, every tail expectation, every distortion premium and every allocation a lookup rather than a re-solve. The floor is the price of the bulk. For actuarial work, where the interesting probabilities are between \(10^{-1}\) and \(10^{-6}\) and the interesting questions are about the whole distribution rather than one point, that is a good trade. For a \(10^{-31}\) outage probability at a single threshold it is the wrong tool, and this paper says so correctly.

There is an obvious way to extend the reach, and it is the one Asmussen et al. (2016) uses: tilt. Replace the severity density \(f\) by the Esscher transform \(f_\theta(x) = e^{-\theta x} f(x) / L(\theta)\), build the aggregate from the tilted severity, and recover the true probability from the likelihood ratio \(L(\theta)^n e^{\theta \gamma}\). Under the tilted measure the left tail is no longer rare, so the FFT never has to resolve a \(10^{-31}\) number, and the ratio of bulk to tail that creates the floor disappears. That is not implemented here, and it would need care with the choice of \(\theta\) and with the normalization, but it is the natural route if the deep tail ever matters.