If you consume libstats as an installed package, this release fixes a silent accuracy defect that affected you. See #97 below.
Why 2.2.0 and not 2.1.1
The fixes here are patch-shaped, but three things landed alongside them that break drop-in:
- CMake minimum rises 3.20 → 3.25
- Install paths move to GNUInstallDirs (so
lib64on distros that use it) - A new installed header,
libstats/libstats_config.h, joins the public set
The library API itself is unchanged. Upgrading from v2.1.0 is a recompile, not a code change.
Fixed
#97 — installed packages silently compiled the Tier 2 Bessel fallback. LIBSTATS_HAS_CXX17_BESSEL selects between two bodies of the same inline functions in libstats/core/bessel.h, but was applied as a target_compile_definitions on an interface target the library pulls in as $<LINK_ONLY:...>. LINK_ONLY propagates the link and strips usage requirements, so the macro reached libstats' own TUs and the in-tree tests, and never a find_package consumer. Every consumer on every platform got the low-accuracy tier: measured against a clean install tree, bessel_i0(10) went from 1.3×10⁻⁸ relative to 1.5×10⁻¹⁶. Because the two bodies differed inside inline functions linked into one program, it was also an ODR violation. The probe result now travels in a generated, installed libstats_config.h, which also reaches pkg-config and plain-include-path consumers.
#97 — std::terminate on negative Bessel arguments under libstdc++. Tier 1 called std::cyl_bessel_i raw, which libstdc++ answers with a thrown std::domain_error for x < 0 while MSVC does not — so the noexcept wrappers aborted on GCC only. Latent until the fix above, since no consumer was reaching Tier 1.
#96 — top three coefficients of the I1/I0 complement series were wrong, c10 by 0.199. Re-derived by exact rational series division of the two Hankel expansions. The cause is recorded in the source because the original validation looked convincing: a Vandermonde solve is ill-conditioned and degrades at high order while staying exact at low order, so checking that the low coefficients came out dyadic confirmed precisely the half a bad solve gets right.
#92 — log I0 was discontinuous at x = 700. The asymptotic above the cut carried only two terms, truncating at O(x⁻³) — exactly the size of the 2.14×10⁻¹⁰ step. Extended to five terms; the seam goes from 0.4→1881 ULP down to 0.4→0.8.
#93 — circular variance lost ~9 bits at large κ. 1 - I1/I0 cancels however accurately the ratio is computed. Adds bessel_i1_i0_complement and bessel_i1_over_i0, each computed directly in its own regime.
#90 — reconfiguring a build directory dropped Threads::Threads from the installed export. Cache variables persist across configure passes; imported targets do not. The same commit and prefix could produce a different package depending on how many times its build directory had been configured.
#94 — the Ninja generator could not configure the repo, and run_tests had been running the entire suite rather than the 41 tests its own comment described.
Added
libstats/libstats_config.h— generated and installed, carrying the build's Bessel tiertests/test_bessel_tier.cpp— a two-sided tier canary plus a negative-argument symmetry test
Validation
Native run on AMD Zen 4 with AVX-512 confirmed active, MSVC 14.51 Release, clean build tree: 49/49 correctness. One timing test (UniformEnhancedTest.SIMDAndParallelBatchImplementations) is flaky on that machine — 2 failures in 3 runs against an adaptive speedup threshold — and is pre-existing; nothing in this release touches uniform or the dispatch thresholds. The NEON and AVX2 legs are CI-validated but not natively re-run for this release, which carries no SIMD kernel change.
Full detail in CHANGELOG.md.