Skip to content

Accuracy characterization vs mpmath (#46) - #101

Merged
OldCrow merged 4 commits into
mainfrom
feature/v2.3-accuracy-characterization
Aug 21, 2026
Merged

Accuracy characterization vs mpmath (#46)#101
OldCrow merged 4 commits into
mainfrom
feature/v2.3-accuracy-characterization

Conversation

@OldCrow

@OldCrow OldCrow commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Closes #46.

What this is

The v2.3.0 accuracy-characterization deliverable: a deterministic bit-exact sweep binary (tools/accuracy_sweep.cpp), an mp.dps-50 mpmath oracle/report generator (tools/accuracy_vs_mpmath.py, 42 unconditional self-checks), and the generated docs/ACCURACY_CHARACTERIZATION.md with per-distribution error tables and a durable contract-findings appendix. Replaces the issue's pylibstats route, which pins to released v2.2.0 and would characterize the wrong code.

Highlights

  • Sweep: 5,928 rows — 19 distributions × 3 instances, scalar + FORCE_VECTORIZED batch, pdf/logpdf/cdf/quantile, support-aware grids to p = 1e-300; byte-identical across independent processes.
  • Oracle large-parameter machinery (each failure was hit by real sweep rows in sequence): safeguarded log-log false-position quantile inversion (secant stalls on deep-tail plateaus; pure bisection costs hours at large parameters), clean-room continued-fraction incomplete beta where mp.betainc hangs or raises (min(a,b) >= 5000 — binomial n=1e6 at the mean hangs mpmath even centrally), upper-gamma complement right of the mean (chi-squared k=1e5 raises NoConvergence), far-tail asymptotic guards, boundary-limit tri-states. Cross-validated against direct quadrature at 1e-44.
  • Gate reconciliation: gaussian law_frac 1.319 ↔ pinned 0.287 of the 1e-15 budget (≈1.29 bare-law); lognormal 1.784 ≤ ~2.2 equivalent; von Mises max_abs 1.7e-16 / 5.1e-16 vs the 2.2e-16 / 8.9e-16 absolute gates.
  • 86 contract findings (doc Findings section + appendix): inconsistent batch NaN propagation across 8 distributions; NaN returns at ±inf where limits exist; batch logpdf(+inf) clamp −4605.0; quantile NaN/saturation at extreme p; large-parameter CDF limits (binomial n=1e6 off 1.3e-2 at the mean — the parked corvus adoption items bessel.h Tier 2 fallback (A&S polynomial) limits VonMises accuracy to ~10⁻⁷ on macOS/AppleClang #47/BinomialDistribution CDF slower than scipy; PDF near-parity — PMF summation and scalar lgamma are limiting factors #52 are the remedy class).

Follow-up candidates (not filed yet)

  1. Batch NaN propagation (8 distributions)
  2. ±inf input limit returns (scalar NaN, batch clamp)
  3. Quantile extreme-p contract (NaN/±inf returns)

Notes

  • test_uniform_enhanced's timing assertion fails pre-existing on clean main (SIMD speedup 1.44x vs adaptive 1.8x); this branch touches only tools/ and docs/.
  • Doc is marked PROVISIONAL: Zen 4 / MSVC / AVX-512 only; Kaby Lake and M1 legs pending.

🤖 Generated with Claude Code

OldCrow and others added 3 commits August 20, 2026 20:32
Adds the oracle/report half of #46 (SIMD accuracy characterization vs
mpmath): tools/accuracy_vs_mpmath.py and docs/ACCURACY_CHARACTERIZATION.md.
The sweep-generation half (tools/accuracy_sweep.cpp, tools/CMakeLists.txt)
is owned by a sibling change on this branch.

tools/accuracy_vs_mpmath.py:
- mpmath (mp.dps=50) reference pdf/logpdf/cdf/quantile for all 19
  distributions, parameter order read from each
  include/libstats/distributions/<dist>.h Doxygen constructor comment.
- Gaussian/LogNormal CDF reuse the erfc closed form from
  scripts/gen_gaussian_cdf_vectors.py / gen_lognormal_cdf_vectors.py.
  Von Mises CDF reuses scripts/gen_vonmises_cdf_vectors.py's direct
  quadrature and its bit-exact double wrap_to_pi (including the PI_D
  anchoring rationale, copied rather than rediscovered).
- Discrete distributions (poisson, binomial, negative_binomial,
  geometric, discrete) use mpmath's regularized incomplete gamma/beta
  identities for pmf/cdf; quantile by monotone integer search.
- Deep-tail cdf rows (F < 1e-3) report law_frac against the
  achievable-accuracy law rel(F) ~ |ln F| * 2^-52 from
  tests/test_lognormal_cdf_accuracy.cpp's law_budget comment, kept
  separate from max_rel so a law-limited tail doesn't read as a defect.
- NaN/Inf rows are contract-checked (NaN in -> NaN out, etc.) separately
  from the error statistics.
- 38 oracle self-checks (2+ per distribution: median/symmetry identities
  and closed-form spot values) run before any row is compared; the
  process exits non-zero if any fails. All 38 pass.

Verified against a hand-made 34-row fixture (not checked in; built with
mpmath-rounded scalar/batch values so error is near the double-rounding
floor, ~1e-17 to ~1e-16, across every distribution/method combination,
plus a deep-tail lognormal cdf row, a quantile row with batch_bits="-",
and a NaN-input row):
- Every legitimate row measured max_rel in the 1e-18 to 1e-16 range, as
  expected for a fixture built from the oracle's own rounded output.
- One row (exponential cdf, x=1.5) was deliberately sabotaged
  (scalar_bits inflated ~25%) and the report correctly isolated it:
  max_rel=0.2526 against every neighboring row's ~1e-17, with
  batch_vs_scalar flagging the same row (abs=0.24, rel=0.2017) since
  only scalar_bits was corrupted.
- A separate negative test corrupted a NaN row's scalar_bits to a finite
  value; the contract-violation check caught it ("NaN input did not
  produce NaN scalar output") and excluded it from the error stats, as
  designed.

docs/ACCURACY_CHARACTERIZATION.md: title, PROVISIONAL banner (one
machine -- Zen 4/AVX-512, MSVC Release; scalar libm = MSVC UCRT;
Kaby Lake/AVX2 and M1/NEON legs pending), regeneration recipe, the
law-of-F caveat for deep-tail cdf columns, cross-references to the four
existing pinned accuracy gates (test_trig_ulp_gates, test_vonmises_cdf_
accuracy, test_lognormal_cdf_accuracy, test_gaussian_cdf_accuracy), and
an empty BEGIN/END GENERATED block for the orchestrator's verification
pass to populate against the real sweep.

Judgment calls (see final report for the full list): Von Mises quantile
oracle bisects the mu=0-centered CDF then shifts/wraps, which mirrors
the library's approach but not its 2049-point grid interpolation, so
scalar-vs-oracle disagreement there is expected to include interpolation
error, not just double rounding; Student's t CDF uses the standard
regularized-incomplete-beta closed form (self-checked via symmetry and
cdf(0)=0.5 rather than derived from first principles in this message).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds tools/accuracy_sweep.cpp: a standalone binary that evaluates every
distribution's scalar AND batch (span/SIMD, FORCE_VECTORIZED) pdf/logpdf/
cdf/quantile paths over fixed characterization grids and dumps bit-exact
results (uint64 hex of the IEEE-754 bit pattern) to a CSV, for the sibling
tools/accuracy_vs_mpmath.py oracle to compare against mpmath. This
replaces the issue's original pylibstats approach: pylibstats pins to a
released libstats and would characterize the wrong code, not the commit
under test.

Grid construction is support-aware for free: continuous x-grids are built
by calling each distribution's own getQuantile() at a fixed, symmetric,
log-spaced probability grid (down to p=1e-300 in both tails), so no
per-family tail formulas are hand-derived. Beta/uniform (the only
both-sides-bounded continuous supports) additionally get explicit
boundary +/- {1e-15, 1e-9, 1e-4} points. Discrete grids are landmark
points (0, 1, 2, mean, mean +/- t*sd for a deterministic multiplier
ladder up to 16000 sd, and support edges) driven by each distribution's
own getMean()/getVariance(), not sampling. NaN/+inf/-inf rows are added
for every continuous dist's pdf/logpdf/cdf. Batch quantile is always "-"
-- no distribution exposes a span quantile overload. 3 parameter
instances per distribution (typical / small-boundary / stressed, e.g.
von Mises kappa=100, lognormal sigma=2) within each header's documented
valid range.

Registers accuracy_sweep in tools/CMakeLists.txt via add_libstats_tool,
next to the other correctness/stress tools.

Determinism: ran twice into separate files and diffed byte-for-byte
identical (5928 rows, 0 quantile skips, MSVC Release/AVX-512). Spot-
checked by hand: gaussian pdf(0; mean=0,sigma=1) = 0x3fd9884533d43650
(0.3989422804014327 = 1/sqrt(2pi), matches to the last bit against an
independent computation); exponential cdf(x=9.992...e-16; lambda=1)
matches 1-exp(-x) to 6 ULP (expected -- the kernel uses an expm1-style
form for small-x accuracy, which is more accurate than the naive
reference, not less); gaussian quantile(p=0.5; mean=0,sigma=1) = 0x0
(median at the mean). Format cross-checked against tools/
accuracy_vs_mpmath.py's parse_csv()/DIST_PARAMS (already landed by the
sibling change): column order, "0x0" p2 sentinel for 1-parameter dists,
"-" batch sentinel, dist/method vocabulary, and the literal header line
all match its input contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…results

Verification pass over the two agent-built halves of #46. The sweep tool
survived unchanged (two-process byte-identical determinism confirmed);
the mpmath oracle needed substantial hardening before it could survive
the real 5928-row sweep -- every failure below was hit in sequence by
actual sweep rows, none by the fixture:

- Quantile inversion: mp.findroot secant stalls permanently on deep-tail
  plateaus (gamma at p ~ 1-1e-16: |f| ~ 1e-15 vs tol 2.6e-54). Pure
  log-space bisection fixed that but costs ~200 CDF evaluations/row --
  hours when one evaluation is a large-parameter incomplete beta.
  Replaced with safeguarded false position in (log x, log F) coordinates
  (bracket invariant kept, anti-stagnation forcing, exact in one step on
  power-law tails), doubling bracket expansion with an exponent cap
  (gamma alpha=0.01 at p=1e-300 has its quantile near 1e-30000), and
  asymptotic-normal seeds where erfinv degenerates (2p-1 rounds to +-1
  at dps 50 below p ~ 1e-50).
- mp.betainc is unusable at large parameters: huge-a far-from-root
  escalates hypercomb precision for minutes then raises ValueError;
  large-BOTH-parameters (binomial n=1e6 at the mean) hangs even in the
  central region. Added _betainc_reg: far-tail lead guard (exp(lead)
  below every positive double; also the exact log-coordinate the solver
  interpolates on) + clean-room Lentz continued-fraction incomplete beta
  for min(a,b) >= 5000, written from the textbook recurrence, converging
  in O(sqrt(ab/(a+b))) mpf iterations. Anchored by new self-checks
  (bit-agreement vs mp.betainc at moderate parameters, I_{1/2}(a,a)=1/2
  at a=1e4) and cross-validated against direct quadrature at 1e-44.
- Lower gammainc raises NoConvergence (mpmath's own class, NOT a
  ValueError) right of the mean at large alpha (chi-squared k=1e5):
  route y > alpha through the upper-gamma complement; guard the lower
  tail with the same lead form.
- Von Mises quantile findroot call passed the bisect bracket twice
  (TypeError on mpmath 1.4); boundary-limit fixes: beta pdf/logpdf edge
  tri-state (integrable singularity for shape < 1 -- the library's +inf
  at x=1 was correct, the oracle's -inf was not), weibull logpdf(0)
  tri-state, geometric logpdf k=0 at p=1 (0*log(0) NaN).
- Report: +-inf inputs are scored against their universal limits
  (pdf->0, logpdf->-inf, cdf->0/1) instead of NaN-ing in the reference
  formulas; von Mises +-inf rows check scalar/batch consistency only
  (periodic: no limit exists; the library's saturation convention is
  recorded as such); non-finite library values against finite references
  are contract rows, never folded into error stats; batch values are now
  checked on non-finite-reference rows too; the full violation list is
  emitted as a durable appendix in the generated doc block.

Results (docs/ACCURACY_CHARACTERIZATION.md, generated + Findings prose):
the four pinned gates reconcile -- gaussian law_frac 1.319 vs pinned
0.287 of the 1e-15 budget (= 1.29 bare-law), lognormal 1.784 <= 2.2
equivalent, von Mises max_abs 1.7e-16/5.1e-16 vs 2.2e-16/8.9e-16
budgets. 86 contract findings: batch NaN propagation inconsistent across
8 distributions (uniform batch pdf(NaN) returns the in-support density),
NaN returns at +-inf where limits exist, batch logpdf(+inf) clamp
-4605.0, quantile NaN/saturation at extreme p, and large-parameter CDF
accuracy limits (binomial n=1e6 off 1.3e-2 at the mean; the parked
corvus adoption items #47/#52 are the remedy class for these).

test_uniform_enhanced's timing assertion fails pre-existing on clean
main (SIMD speedup 1.44x vs adaptive 1.8x threshold); this change set
touches only tools/ and docs/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GCC 13's -Werror=strict-overflow fires a pointer-wraparound false
positive ("assuming pointer wraparound does not occur when comparing
P +- C1 with P +- C2") when it constexpr-expands std::sort's introsort
inside xConstructionPGrid()'s immediately-invoked initializer, failing
the strict-warnings runner. The grid is strictly ascending by
construction (both exponent lists descend, and 10^-0.52 ~ 0.302 < 0.35),
so the sort+unique was purely defensive; an assert loop keeps the check
without giving GCC a sort to expand. Regenerated CSV verified
byte-identical to the pre-change sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@OldCrow
OldCrow merged commit 57da00a into main Aug 21, 2026
21 checks passed
@OldCrow
OldCrow deleted the feature/v2.3-accuracy-characterization branch August 21, 2026 03:47
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.

Benchmark: SIMD accuracy characterization vs arbitrary-precision reference (mpmath)

1 participant