Skip to content

Why the wheel, and what it costs to do without it

The question anyone installing this package eventually asks: there are two engines — what do I lose by running the one that needs no compiler? Measured, not estimated, over the 23-case benchmark that ships with the tests.

The short answer: you lose speed — one to two orders of magnitude — and you lose nothing else. The two engines run the same algorithm with the same optimiser and reach the same optimum to the eighth decimal.


1. Efficacy first, because it is the one that matters

If the answers differed, speed would be irrelevant. They do not:

largest |Δ log-likelihood| over 23 real models, Python vs C 0.0002
in all but one case < 0.00005

Same likelihood (elfvarma.py translates elfvarma.c step by step, and both are checked against the published algorithms — docs/PROVENANCE.md), same optimiser (raxopt in both, docs/PORT.md §3.1), same stopping rules. The one case that moves, IPC-T/Coint/R.4, is the fit that never converged in the first place — termcode 2 with a gradient of 1e5 — so a difference in the eighth decimal there is the flat valley, not the engine.

2. Efficiency, over the 23-case benchmark

Measured 13 August 2026, Linux x86-64, CPython 3.12, gcc -O2. Reproduce with

python -m pytest tests/test_performance.py::test_summary -s
C (the wheel) Python + raxopt Python + L-BFGS-B
slowdown vs C, median ×90 ×282
slowdown vs C, range ×29 … ×384 ×29 … ×1178

In wall-clock terms, on the models people actually fit:

case freq n npar C Python + raxopt
PCE/SF/R.1 4 68 1 0.1 ms 4.1 ms
IPC-T/R.1 4 68 4 0.3 ms 21.2 ms
IPC-T/Coint/R.4 4 68 5 1.6 ms 134 ms
RIPC.4 12 78 15 8.8 ms 1.8 s
RIPC.1 12 72 14 38 ms 7.0 s
RIPC.3 12 78 17 14 ms 2.7 s

So: a single quarterly model is imperceptible either way. A monthly model with fifteen parameters is 9 ms against 2 seconds — and that is the difference between an interactive session and a wait. Multiply by the model ladder of a real analysis, or by a Monte Carlo, and the wheel stops being a convenience.

Where the time goes, profiled on SFNY.2: cast_us_py 63%, flikam_scalar 19%, the rest below 4% each. It is the casting, not the likelihood, and not scipy: the Python loops that rebuild the model structure at every function evaluation are what the C does in compiled form.

3. The other study: raxopt against L-BFGS-B

Both are available in the Python engine (estimate_py uses raxopt; optimizer="lbfgsb" switches). The comparison was worth making because the intuition — a modern library optimiser must beat a 1995 translation — is wrong on both counts here:

  • Speed. raxopt is faster in 21 of the 23 cases, by a median factor of 2.4× and up to 7.3×. L-BFGS-B evaluates the objective more times, and every evaluation carries the Python casting overhead of §2.
  • The optimum. Δ log-likelihood is ≈ 0 for raxopt against the C on every case. L-BFGS-B is not uniformly worse — in June 2026 it escaped a local optimum on IPC-T/Coint/R.4 where raxopt stayed (251.68 against 211.21) — which is exactly why it is kept as an option rather than removed.

That asymmetry is the honest summary: raxopt for estimation, L-BFGS-B as a second opinion when a fit looks trapped. bugs/BUG-0005 is the open defect about spurious optima on multimodal seasonal-AR likelihoods, and it is the situation where trying the other optimiser is the right reflex.

4. What the wheel actually contains

pip install fue fetches a wheel with the C engine compiled in and GSL linked statically, so there is nothing else to install — on Linux (x86_64 and aarch64, glibc and musl), macOS on Apple Silicon, and Windows AMD64.

Intel macOS is the one target not built, and that is policy: GitHub's Intel-mac runners are chronically starved — a job once queued nine hours without one — and every current Mac is arm64. Building from source needs a C compiler and GSL headers; if neither is available:

FUE_SKIP_C=1 pip install fue      # skip the extension: pure Python, permanently

Everything except the estimator is pure Python in both cases — the .inp parser, the reports, the forecasts, the diagnostics, the plots. The extension is the estimator and nothing else.

5. What was considered and not done

Three hybrid architectures were costed before settling on "ship the C, keep the translation":

option projected speed dependency verdict
Python optimiser + C objective function (fue_objcfunc) ≈ C still GSL the interesting one, if the optimiser ever needs extending
Python optimiser + Python casting + C flikam only ×2.5 over pure Python still GSL rejected: flikam is 19% of the time, the casting is 63%
vectorised pure Python (np.convolve, lfilter) ×20-50 vs C none not done; it is a rewrite of the loops, and the algorithms are published

The first remains the candidate if the day comes when the optimiser has to grow constraints or alternative criteria — the C would keep the inner loop and the search would move to Python without the ×90.


The full measurement log, including every migration stage since June 2026, is in PERFORMANCE.md at the repository root; this document is the part a user needs in order to decide.