← back to methodology & accuracy

Accuracy: data and methods

Published 2026-11-11. Every figure cites the artifact it came from, and the artifact hashes name their inputs. The frozen claim first, verbatim from the methodology page:

Every prediction is checked against published load data: 30,327 printed loads, and our velocity calls miss by about 7.5% on average. The table below shows the error rate for each powder we serve, worst rows included. Low-fill pistol and magnum loads are where we miss most. When a powder improves, the new numbers go up here with a date.

The engine

IBC is a first-principles interior-ballistics integrator. It steps the shot in half-microsecond increments and keeps an energy ledger. Burning powder adds energy. Bullet travel, heat into the steel, rifling friction, and gas leaking past the projectile subtract it. Pressure follows from the energy left in the gas and the volume it must fill. The gas obeys a Noble-Abel equation of state, so molecules that take up space shrink the room they can expand into. That is why compressed and near-compressed loads behave differently. Burn rates come from propellant coefficients fit to closed-bomb measurements. The propellant physics is grounded in three measured sources: STANAG 4115 Ed. 2, ADA049465, and ADA121669. The served engine is v1.1.14, commit da7ac091effacaadd31144bb1537206f3936bbd9 (web/population-accuracy.json; web/freeze-record.json). The frozen record was measured with the deployed serving binary, sha256 41ff483b19877bb515fe3381d84a1c62277bc1ea3ce7115964aef961202f25ea.

Drop tables: the exterior half

The interior engine stops at the muzzle. Drop tables are a separate, purely external calculation. A point-mass model, drag plus gravity and nothing else, integrates the flight from the muzzle velocity the interior engine predicts. No spin drift, Coriolis, lift, cant, or pitch damping (app/trajectory.py; app/drag_tables.py).

Drag uses the standard G1 and G7 reference tables (the classic McCoy/Ingalls curves, mechanically transcribed from py-ballisticcalc's published tables) scaled by the catalog ballistic coefficient. The BC is a manufacturer figure, not one of our measurements. The atmosphere is dry air in ICAO form, rho = p/(Rd*T), at the user's temperature and pressure with 15 C and 1013 hPa defaults; wind is one full-value crosswind, left to right. The launch angle is solved by bisection so the path crosses the line of sight at the zero range.

We cross-checked the solver against an independent implementation, py-ballisticcalc 3.1.1 (RK4 integration), run with identical inputs: a G1 case (.308, 168 gr, BC .462, 2650 fps) and a G7 case (6.5 mm, 140 gr, BC .305, 2720 fps, 10 mph wind). Velocity, drop, time of flight, and wind drift agreed to under 0.1 fps and 0.02 inches at every range compared. The physics pins in tests/test_trajectory.py freeze that agreement (cross-validated 2026-11-01). The cross-check validates the solver, not the BC: if the catalog coefficient is off, the table is off with it. Drop tables are a planning aid; confirm at distance.

The data

The graded corpus is published reloading-manual data, read exactly as printed. We collect charge, barrel, bullet, printed velocity, and printed pressure, and we grade every prediction against the printed figures.

The corpus of record (the oldest lane, and the one the per-deploy artifacts headline) holds 10,883 collected rows. 9,898 resolved to a powder, bullet, and caliber we can simulate; 985 were excluded as unresolvable; 9,803 were graded after dropping 95 source-disclaimed rows. The published figures grade the 9,796 rows that sit inside the quarantine band (web/population-accuracy.json). Of the graded rows, the charge weight was printed in the book in most, recovered from cartridge and bullet class data where a row omitted it, and assumed where neither was possible; the exact split is in the artifact's charge_weight block.

Two expansion corpora joined before launch, graded the same way at each load's printed charge, bullet and barrel, every load verified identical across at least two printings of the same manual line:

graded (web/corpus-accuracy.json, powders.manual_corpus).

simulated, 4,025 graded. This is the corpus that replaced the two known-broken fits (Accurate 4064, Ramshot Enforcer) with measured calibrations (web/corpus-accuracy.json, powders.manual_corpus_winacc).

Pressure is graded in two classes, because books do not agree. Older manuals use the copper-crusher method and print CUP. Modern manuals use a transducer and print psi. We never convert between them.

The powder models carry their own 216-load calibration set, graded separately as a consistency check. It is not an accuracy headline. On max charges it returns a mean absolute velocity error of 4.0%, with 91.2% of loads inside 10% (web/accuracy.json).

How we grade it

Every graded row gets a signed error. Positive means the model reads high. Negative means it reads low. We publish the distribution, including the worst rows. We publish our misses.

On the corpus of record, the velocity median error is -5.27% and the mean absolute error is 10.27%; 57.7% of rows land inside 10% (either sign), over 9,796 graded rows (web/population-accuracy.json). On the calibrated pressure scale — the same single fixed correction the calculator applies to displayed pressure percentages — simulated peak pressure comes back 7.38% low on crusher (CUP) printings (n=1,679) and 0.91% low on transducer (PSI) printings (n=3,615) (web/population-accuracy.json, claims block).

Some printed rows are not trustworthy. A row whose velocity or pressure error exceeds 60% in magnitude is corpus-suspect. Suspect rows are excluded from every published figure and listed in full for provenance (web/corpus-accuracy.json, quarantine block). The quarantine now holds 83 rows. It was 106 before the book-verified repair passes. Phase 2 relabeled 47 rows against the print and took it to 82. Pass 6 repaired 425 truncated bullet weights and 55 barrel lengths and took it to 78. The owner checked the print against 8 suspect loads and confirmed all 8 were bad; 10 rows were removed and 2 corrected, each with a recorded disposition in the private corpus workspace's repair log.

The combined corpus

The frozen headline at the top of this paper grades all three corpora together. Before launch we ran one combined battery: every graded row from all three lanes, simulated exactly as printed by the deployed serving binary, per-row parity checked against the committed lane residuals.

lanesimulatedgradedvelocity MAE
corpus of record9,8989,88710.37%
Annual Manuals AM17-2516,41816,4155.76%
Winchester / Accurate / Ramshot AM22-254,0324,0257.95%
combined30,34830,3277.55%

Combined, the median velocity error is -3.21%, the mean absolute error 7.55%, and 73.4% of predictions land inside ±10% (web/freeze-record.json). Per-row parity against the committed lanes was byte-exact (9,898/9,898, 16,418/16,418, 4,030/4,032; the two diffs are sub-ignition charges that produce no usable output in either run and sit outside the graded band either way).

The corpus of record grades worse than the two expansion lanes, and the frozen headline is better for it: the older lane carries more of the light-fill pistol and revolver rows where the model misses worst, so the combined number sits well under the corpus-of-record headline. Both are published — the per-deploy artifacts keep the corpus-of-record figures, the frozen record carries the combined one, and neither is cherry-picked.

These figures are the launch measurement. Per-powder numbers update on this page whenever a powder improves.

The accuracy floor

The engine is held to a floor it must clear: on max-charge loads the mean velocity error stays within 5% of published figures and at least 90% of loads land inside ±10%. A model change that misses either number is rolled back. The floor is judged on the 216-load calibration set described above, and the current artifact clears it: mean absolute error 4.0%, 91.2% inside ±10% (web/accuracy.json, max block). It is a rollback rule about which model changes ship, not a warranty on any individual load. The same artifact shows individual max-charge rows from -18.4% to +23.7%, so a single load can sit outside the floor band while the engine as a whole clears it.

Calibration coverage and the two code paths

The engine has two burn-rate paths. A powder listed in the burn-rate calibration table gets a per-powder fill-density term and the ignition transient. A powder absent from the table falls back to a legacy band multiplier. The fallback has no fill-density term and no ignition transient. The lookup is a case-insensitive manufacturer-and-name match. A miss is silent, and the output stays plausible (engine/src/ballistics.c:341-400, :1365, :1409).

The served table holds 48 entries (engine/data/powder_ba_calibration_grt.csv). The corpus of record carries graded loads on 23 powders. Twelve of them are absent from the table. Those 12 run the fallback path (the residual atlas and the BA-coverage probe evidence in the private corpus workspace).

The raw-parameters counterfactual

The two code paths let us measure what the fitted layers add. We re-ran the full battery with the fitted layers switched off through environment flags. Same loads, same engine build, same grading rules; only the switches differ. The calibrated arm reproduces the 2026-11-08 corpus-of-record battery byte for byte, so none of that battery's published figures move. The baseline arm disables both fitted layers together; single-layer arms disabled each one alone to split the two apart (paired-baseline evidence, private corpus workspace, 2026-11-09).

Framing, stated plainly: these baseline arms run the GRT database's vendor parameters through OUR solver. They are not GRT's predictions, and nothing in them is derived from GRT's model. The switches only disable layers we fitted on top of those parameters.

The numbers below are corpus-of-record (2026-11-08 battery) figures, not combined-corpus figures — the counterfactual ran before the expansion corpora joined.

The burn-rate calibration layer carries nearly all of the difference. With it off, the graded corpus moves from -7.42% median velocity error to -12.16%, and mean absolute error grows from 11.41% to 14.19%. Median pressure error moves from -26.9% to -36.8%. On the calibrated population (2,551 graded velocity rows) the layer is worth 8.8 points of velocity median (-3.4% with it, -12.2% without) and 28 points of pressure median (-6.1% vs -34.1%). Per powder it is worth +6.9 to +14.9 points of median velocity (Accurate 2230 to Ramshot Hunter) and up to +41.5 points of median pressure (Ramshot Magnum).

The ignition transient is nearly neutral here, and that is expected: it is anchored to zero at each load's own maximum charge, and the corpus is maximum-charge loads. With it off alone, velocity median improves from -7.42% to -6.66% and pressure from -26.9% to -24.4%.

The 12 unanchored powders are byte-identical in every arm. They already run the raw parameters, so their rows are the baseline itself: -12.6% median velocity and -34.7% median pressure over 4,290 velocity rows. That is the regime split described in the next section, computed on the trustworthy subset (the -34.97% figure in the residual atlas includes quarantined rows; this one excludes them).

The two-regime finding

The pressure error is not uniform. On the raw printed-scale frame over 5,387 pressure rows, the median is -27.33% (residual atlas, private corpus workspace, section 2). Split by calibration coverage, the unanchored population (2,872 rows, 53%) sits at -34.97% median. The calibrated population (2,515 rows) sits at -13.29%. That gap is the dominant axis.

The second axis is geometry. Condition on loading density below about 0.55 g/cc and revolver-class chamber geometry, and the worst band is enriched 33 times. Inside that cell, 10.8% of rows land at -60% or worse. Outside, 0.33% do. The cell holds 59 of the 75 signature rows. In those rows the velocity median is -21.2%, so pressure is under-predicted about three times as hard as velocity. That is the signature of a missing pressure-side term, not a burn-rate error. Rescaling the burn rate closes the pressure only by blowing the velocity (residual atlas, sections 3-4).

What we did about it

We tried to close the calibration gap. The BA-coverage lane re-probed all 12 unanchored powders on the repaired corpus. Every candidate failed the adoption bar. Adopting the two cleanest twins anyway moved the corpus pressure MAE from 27.29% to 26.48% and the -60% tail from 75 rows to 73. That is about 4% of the promised improvement (BA-coverage pass evidence, private corpus workspace, 2026-11-07).

The discipline that rejected those candidates is the same one that rejected the phase-2 and phase-3 rows. A candidate must clear the holdout velocity, the tail count, and the quarantine count. It cannot trade a better median for a worse worst row. Phase 2 adopted 2 of 12 probed powders and held or rejected the rest. Phase 3 adopted none of the donor-route candidates (phase-4 and phase-3 evidence files, private corpus workspace).

Limitations

Our engine under-predicts published peak pressures on the calibration corpus by 27% at the median, and this error is not uniform. Most of it (median -35% over 53% of the corpus) tracks one traceable coverage gap: a set of powders whose ignition-and-fill calibration rows we have not yet fitted run on a legacy code path without those terms. Where that calibration exists, the same loads sit at -13% median. The remaining error concentrates in a physically coherent regime: low-loading-density charges in revolver and other small-chamber, large-bore geometries, where our fill-density and ignition correlations were never calibrated. They are fit in the dense-fill band, roughly 0.65 to 0.85 g/cc of charge over case capacity. There the deficit reaches -60 to -71% while predicted velocity stays roughly plausible. The failure is in the pressure-side terms, not the burn rate. We publish loads in this regime with the known-bias caveat rather than a silent extrapolation. We do not yet model cylinder-gap heat loss in revolvers, which we expect to account for part of the residual (residual atlas, section 6).

A small number of published loads run the other way. For the fastest pistol powders at charge weights that nearly fill a short, wide case, our model reads high. The clearest example is the 9 mm Makarov with Accurate No 5, where we over-predict the printed pressure by 100-112% on four manual loads. These loads share one geometric property: the powder column fills roughly three quarters or more of the space left in the case after the bullet is seated. Our burn-rate corrections for this powder were fitted on longer-case loads and do not account for that packing, so the pressure peak arrives early and high. We flag the affected loads rather than silent extrapolate: they are excluded from every headline statistic and listed in the quarantine block. No card-level calibration change we tested removes the bias without breaking the longer-case loads the same correction was fitted on, so we report it as a known limitation of the current model form (over-prediction tail analysis, private corpus workspace).

Frame note: the regime and limitation numbers above are raw printed-scale figures and predate the calibrated display scale; the pressure figures in "How we grade it" are quoted in the calibrated frame the product displays. Both are labeled, and neither frame is mixed into the other's sentences.

Publication record

prose moved here (owner direction).

evidence and residuals in the private corpus workspace; hashes published in web/freeze-record.json), and the frozen record shipped at the top of /methodology.html with the measured 7.55% headline (owner-approved).

pre-freeze row counts and quote the floor as "published on /methodology.html verbatim". Owner-gated text; the pointer is being reconciled with the owner and this paragraph will be updated when it lands.