# ScopeDOPE Ballistics Engine — Validation Reference

**Date:** 2026-05-29
**Status:** ⚠️ Superseded (2026-05-30) — the Python harness has been retired; engine validation is now the Kotlin/JVM `:shared:ballistics:jvmTest` suite
**Audience:** Developer working on the ballistics engine

---

> **⚠️ SUPERSEDED (2026-05-30):** This document originally described the **Python** validation harness (`scopedope-ballistics-test/`), which has been **retired and deleted**. Engine validation is now the Kotlin/JVM **`:shared:ballistics:jvmTest`** suite, which exercises `BallisticsEngine.kt` directly through the shared `:ballistics` KMP module (no Python port). The 22 test datasets now live at `scopedope-shared/ballistics-test-data/test_cases/`. The datasets, tier system, methodology, and provenance below remain valid; run instructions have been updated to the gradle suite. See `SPEC_ballistics_kmp.md` for the current suite.

## 1. What You Are Testing

ScopeDOPE's ballistics engine is a from-scratch Kotlin Multiplatform implementation of a G1/G7 point-mass trajectory solver with DOPE bias-correction via monotone cubic spline interpolation. It lives in the `:ballistics` KMP module.

**You are validating two things:**

1. **Physics accuracy** — does the pure-physics trajectory (no DOPE entries) match external reference calculators (JBM, ShootersCalculator) and, where available, confirmed field-fired data?
2. **DOPE interpolation accuracy** — when N−1 confirmed DOPE points are fed in as known impacts, does the engine predict the Nth point more accurately than pure physics alone?

**Current validation status:** See `BALLISTICS_ENGINE_ACCURACY_ANALYSIS.md` for the full findings from the initial C(N,k) validation run (2026-05-29). Three findings: G7 systematic under-prediction at long range (High), G1 subsonic over-prediction for .22 LR (Medium), DOPE spline degradation at near-boundary interior points (Low).

---

## 2. Running the Test Suite

The validation runs as the Kotlin/JVM `:shared:ballistics:jvmTest` suite:

```bash
# from within scopedope-android/
./gradlew :shared:ballistics:jvmTest
```

Key files (in `scopedope-android/shared/ballistics/src/jvmTest/`): `ExhaustiveCVRunner.kt` (C(N,k) cross-validation runner), `BallisticsValidationTest.kt` (JUnit entry point), `ReportWriter.kt`, and `TestCaseModel.kt`. The suite tests `BallisticsEngine.kt` from `commonMain` directly — no Python port. Test cases follow the schema in §5.1 and live in `scopedope-shared/ballistics-test-data/test_cases/`.

---

## 3. Engine Algorithm Reference

Key functions in the `:ballistics` module (all in `commonMain`):

| Function | Location | Description |
|---|---|---|
| `CdCurve` | `BallisticsEngine.kt` | Fritsch-Carlson monotone cubic Hermite interpolator for Cd vs Mach |
| `speedOfSoundFps(tempF)` | `BallisticsEngine.kt` | ISA troposphere: `1116.45 × √((tempF+459.67)/518.67)` |
| `airDensityRatio(...)` | `BallisticsEngine.kt` | ICAO atmosphere + Magnus humidity correction |
| `windComponents(speedFps, angleDeg)` | `BallisticsEngine.kt` | Clock-convention decomposition to headwind/crosswind |
| `integrate(mv, bcEff, cdFn, dist, sos, hw, gravity)` | `BallisticsEngine.kt` | RK4 spatial integrator, 0.5-yd steps |
| `coriolisMils(lat, az, dist, tof, mv0, mv1)` | `BallisticsEngine.kt` | Coriolis elev + wind corrections |
| `millerSg(twist, weight, diam, length, mv)` | `BallisticsEngine.kt` | Miller 1983 gyroscopic stability |
| `spinDriftMils(twist, dir, tof, dist, ...)` | `BallisticsEngine.kt` | Litz approximation with Miller Sg |
| `trajectoryMils(dropD, dropZ, dist, zero, sh)` | `BallisticsEngine.kt` | Mil output from drop and zero drop |
| `calculateTrajectory(params, distances)` | `BallisticsEngine.kt` | Full physics pass |
| `entryBias(entry, params)` | `BallisticsEngine.kt` | Residual between observed DOPE and physics — `internal` |
| `interpolateDopeAll(entries, distances, params)` | `BallisticsEngine.kt` | Batch DOPE-corrected results — `internal` |
| `monotoneCubicInterp(xs, ys, xi)` | `BallisticsEngine.kt` | Fritsch-Carlson spline with linear fallback for n<3 — `internal` |

**Critical implementation notes:**
- Drag table integration uses RK4 with `dvds(v) = -KD × Cd(v/sos) × v² / (bcEff × v)` where `KD = 0.0764/288.0`
- `bcEff = bc / airDensityRatio` — BC is scaled by density ratio before integration
- Zero integration runs at zeroing conditions (separate temp/altitude/pressure if provided), NOT range-day conditions
- `gEff = 9.80665 × cos(firingAngleDeg)` for inclined fire (rifleman's rule); zero always uses full gravity
- Wind: headwind component affects bullet velocity; crosswind uses Didion lag-rule
- Bias spline: constant extrapolation outside the DOPE range; linear fallback for n=2; cubic for n≥3
- `entryBias` and `applyBias` are `internal` — not accessible outside the `:ballistics` module

Key constants:
```kotlin
const val STANDARD_TEMP_F  = 59.0
const val GRAVITY_FPS2     = 32.174
const val STD_PRESSURE_HPA = 1013.25
const val EARTH_OMEGA      = 7.2921e-5
const val KD               = 0.0764 / 288.0
const val MAX_DIST_YD      = 3500.0
const val STEP_YD          = 0.5
```

---

## 4. Data Completeness Tier System

Every test case **must** be classified using this tier system. Tier assignment is based on two axes: environmental data completeness and whether confirmed field-fired impacts exist.

| Tier | Environmental Data Completeness | Confirmed Impacts | What It Validates |
|---|---|---|---|
| **1** | ≥ 85% of fields known | Required | Engine accuracy — direct comparison valid; deviations are actionable |
| **2** | 40–84% of fields known | Required | Partial accuracy signal; deviations noted with caveats about unknown conditions |
| **3** | < 40% of fields known | Required | Robustness only — graceful degradation and cross-calculator consistency |
| **Sub-3** | Any completeness | None (calculated only) | Cross-calculator comparison only — cannot validate real-world accuracy |

**Environmental fields (4 total):**
1. Altitude (ft) — known from source vs. assumed sea level
2. Temperature (°F) — known from source vs. assumed 59°F
3. Pressure (hPa or inHg) — known from source vs. derived from altitude via ICAO
4. Humidity (%) — known from source vs. assumed 0%

Completeness % = confirmed fields / 4. MV source quality (fired vs. manufacturer-stated) is reported separately and does not affect tier assignment but must be noted.

**Reporting rule:** If a test case has no confirmed fired impacts, it is Sub-Tier-3 and its deviations are labeled "cross-calculator Δ" rather than "real-world Δ." This distinction must appear in the report header for every test case.

---

## 5. Test Datasets

### 5.1 Test Case JSON Schema

Each test case is a JSON file in `scopedope-shared/ballistics-test-data/test_cases/`. The schema:

```json
{
  "id": "unique_string",
  "caliber": ".22 LR",
  "load_name": "CCI Standard Velocity",
  "bc_model": "G1",
  "bc": 0.120,
  "bc_source": "community_consensus",
  "mv_fps": 1070,
  "mv_source": "chronograph_fired",
  "bullet_weight_gr": 40.0,
  "zero_yards": 50,
  "sight_height_in": 1.5,
  "twist_rate_in": 16.0,
  "conditions": {
    "altitude_ft": 0,
    "temp_f": 59.0,
    "pressure_hpa": 0,
    "humidity_pct": 0,
    "confirmed_fields": []
  },
  "dope_points": [
    {"yards": 25,  "mil": -0.35, "moa": -1.19, "source": "calculated", "fired": false},
    {"yards": 75,  "mil":  0.95, "moa":  3.27,  "source": "calculated", "fired": false},
    {"yards": 100, "mil":  2.11, "moa":  7.27,  "source": "calculated", "fired": false},
    {"yards": 125, "mil":  3.40, "moa": 11.69,  "source": "calculated", "fired": false},
    {"yards": 150, "mil":  4.78, "moa": 16.43,  "source": "calculated", "fired": false},
    {"yards": 200, "mil":  7.78, "moa": 26.73,  "source": "calculated", "fired": false}
  ],
  "tier": 3,
  "data_notes": "Sub-Tier-3 (cross-calculator comparison only).",
  "source_urls": []
}
```

`confirmed_fields` lists which of `["altitude_ft","temp_f","pressure_hpa","humidity_pct"]` are actually known (not assumed). `fired: true` means the dope_point came from a physical impact, not a calculator.

---

### 5.2 Rimfire Test Cases

#### .22 LR — NRL22 Distances (50-yd zero; 25, 50, 75, 100, 125, 150, 200 yd)

All .22 LR data is Sub-Tier-3. These cases test cross-calculator consistency and graceful subsonic handling.

**Important subsonic warning:** .22 LR at ~1070–1085 fps crosses the sound barrier (≈1116 fps) before the muzzle. G1 drag models are calibrated for supersonic flight and degrade in accuracy once the bullet goes subsonic. Real-world truing is required; do not treat deviations as purely engine error.

| Load | MV (fps) | BC G1 | MV Source | BC Source |
|---|---|---|---|---|
| CCI Standard Velocity | 1070 | 0.120 | Chronograph (community, mean) | Calculator consensus |
| Eley Match | 1085 | 0.112 | Manufacturer (RA4 model, approximation) | Eley RA4 model |
| Lapua Center-X | 1073 | 0.172 | Manufacturer (26" test barrel) | Lapua published |
| SK Standard Plus | 1073 | 0.172 | Manufacturer (26" test barrel) | Community consensus |
| Federal AutoMatch | 1200 | 0.138 | Fired (TFB review, 20" barrel, mean 1196 fps) | Federal published |

All use 50-yd zero, 1.5" sight height, sea level, 59°F, 0% humidity (all assumed).

**Trajectory tables (ShootersCalculator, Sub-Tier-3):**

CCI SV (G1 0.120, 1070 fps, 50-yd zero):
```
25 yd:  -1.19 MOA  / -0.35 mil
50 yd:   0.00       /  0.00      (zero)
75 yd:   3.27 MOA  /  0.95 mil
100 yd:  7.27 MOA  /  2.11 mil
125 yd: 11.69 MOA  /  3.40 mil
150 yd: 16.43 MOA  /  4.78 mil
200 yd: 26.73 MOA  /  7.78 mil
```

Lapua Center-X / SK Standard Plus (G1 0.172, 1073 fps, 50-yd zero — identical):
```
25 yd:  -1.05 MOA  / -0.31 mil
75 yd:   3.08 MOA  /  0.89 mil
100 yd:  6.81 MOA  /  1.98 mil
125 yd: 10.90 MOA  /  3.17 mil
150 yd: 15.24 MOA  /  4.43 mil
200 yd: 24.54 MOA  /  7.14 mil
```
Field note: At 200 yd, one SK Long Range Match field report needed 7.0 MRAD; calculator predicts 7.14 MRAD — consistent.

Eley Match (G1 0.112, 1085 fps, 50-yd zero):
```
25 yd:  -1.12 MOA  / -0.33 mil
75 yd:   3.22 MOA  /  0.94 mil
100 yd:  7.19 MOA  /  2.09 mil
125 yd: 11.59 MOA  /  3.37 mil
150 yd: 16.33 MOA  /  4.75 mil
200 yd: 26.70 MOA  /  7.77 mil
```

Federal AutoMatch (G1 0.138, 1200 fps, 50-yd zero):
```
25 yd:  -0.43 MOA  / -0.12 mil
75 yd:   2.51 MOA  /  0.73 mil
100 yd:  5.74 MOA  /  1.67 mil
125 yd:  9.37 MOA  /  2.73 mil
150 yd: 13.31 MOA  /  3.87 mil
200 yd: 21.94 MOA  /  6.38 mil
```

---

#### .22 WMR — 50–200 yd (100-yd zero)

All Sub-Tier-3. Three entries are in `ammo_builtin.csv`.

| Load | MV (fps) | BC G1 | BC G7 | Weight | Notes |
|---|---|---|---|---|---|
| CCI Maxi-Mag 40gr JHP | 1875 | 0.116 | 0.059 | 40 gr | ATG/manufacturer confirmed |
| Hornady V-Max 30gr | 2200 | 0.095 | 0.048 | 30 gr | Manufacturer; use G1 — not a VLD bullet |
| Federal Game-Shok 50gr JHP | 1530 | 0.161 | 0.082 | 50 gr | Manufacturer |

Trajectory tables (100-yd zero, sea level, 59°F, G1 model):

CCI Maxi-Mag 40gr (G1 0.116, 1875 fps):
```
50 yd:  ~-1.96 MOA / ~-0.57 mil  (above line of sight)
100 yd:  0.00       /  0.00       (zero)
125 yd:  1.72 MOA  /  0.50 mil
150 yd:  3.80 MOA  /  1.11 mil
175 yd:  6.20 MOA  /  1.80 mil
200 yd:  8.91 MOA  /  2.59 mil
```

Hornady V-Max 30gr (G1 0.095, 2200 fps):
```
100 yd:  0.00
125 yd:  1.34 MOA /  0.39 mil
150 yd:  3.04 MOA /  0.88 mil
175 yd:  5.10 MOA /  1.48 mil
200 yd:  7.51 MOA /  2.18 mil
```

Federal Game-Shok 50gr (G1 0.161, 1530 fps):
```
100 yd:  0.00
150 yd: ~5.50 MOA / ~1.60 mil
200 yd: ~12.5 MOA / ~3.64 mil
```
Note: Only 3 data points. Use primarily as Sub-Tier-3 graceful-degradation test; skip LOO.

---

#### .17 HMR — 50–300 yd (100-yd zero)

This caliber provides the **only confirmed-fired impact data** in this dataset (Tier 3). Three entries are in `ammo_builtin.csv`.

| Load | MV (fps) | BC G1 | Weight | MV Source |
|---|---|---|---|---|
| Hornady 17gr V-Max | 2550 | 0.125 | 17 gr | Manufacturer; Varmint Al measured 2594 fps (22" T/C) |
| Federal V-Shok 17gr V-Max | 2530 | 0.125 | 17 gr | Manufacturer |
| CCI V-Max 17gr | 2550 | 0.125 | 17 gr | Manufacturer (same projectile as Hornady) |

**Confirmed-fired impact data (Varmint Al field test — Tier 3):**
Source: varmintal.com/17hmr.htm — measured click adjustments with 0.125 MOA/click scope, T/C Contender 22" Bullberry barrel, measured MV 2594 fps.
No environmental conditions stated (rural California — altitude and temperature unknown).
```
150 yd:  14 clicks × 0.125 MOA = 1.75 MOA  (0.509 mil)  [FIRED]
200 yd:  35 clicks × 0.125 MOA = 4.375 MOA (1.272 mil)  [FIRED]
300 yd:  96 clicks × 0.125 MOA = 12.0 MOA  (3.491 mil)  [FIRED]
```

**Calculated trajectory (ShootersCalculator, G1 0.125, 2550 fps, 100-yd zero, sea level, 59°F — Sub-Tier-3):**
```
50 yd:   hold under 0.29 MOA  / -0.08 mil
75 yd:   hold under 0.43 MOA  / -0.12 mil
100 yd:   0.00                 /  0.00
125 yd:   0.71 MOA             /  0.21 mil
150 yd:   1.63 MOA             /  0.48 mil  (fired: 1.75 MOA / 0.509 mil)
175 yd:   2.75 MOA             /  0.80 mil
200 yd:   4.05 MOA             /  1.18 mil  (fired: 4.375 MOA / 1.272 mil)
225 yd:   5.54 MOA             /  1.61 mil
250 yd:   7.25 MOA             /  2.11 mil
275 yd:   9.18 MOA             /  2.67 mil
300 yd:  11.35 MOA             /  3.30 mil  (fired: 12.0 MOA / 3.491 mil)
```

**Test design:** Use the 9 calculated points (50–250 yd) as DOPE training entries. Validate against the 3 fired impact points (150, 200, 300 yd). This produces a Tier 3 validation.

**Subsonic warning:** The 17gr V-Max crosses ~1116 fps between 250–275 yd. Prediction quality degrades sharply above 250 yd. Report whether the engine handles the transonic transition gracefully.

---

### 5.3 Centerfire Test Cases

#### 6.5 Creedmoor — 100–1000 yd (100-yd zero)

All Sub-Tier-3. MV sources are confirmed-fired (LabRadar, n=40 from PrecisionRifleBlog).

| Load | MV (fps) | BC G7 | MV Source |
|---|---|---|---|
| Hornady Match 140gr ELD-M | 2674 | 0.326 | FIRED — PRB LabRadar, n=40 (24" barrel) |
| Berger 140gr Hybrid Target | 2779 | 0.311 | FIRED — PRB LabRadar, n=40 |
| Federal Gold Medal 140gr SMK | 2681 | 0.253 | FIRED — PRB LabRadar, n=40 |

Use **G7 BC model** for all 6.5 CM loads.

Trajectory tables (G7 model, 100-yd zero, sea level, 59°F):

Hornady 140gr ELD-M (G7 0.326, MV 2674 fps):
```
100 yd:   0.00 mil
200 yd:   0.53 mil
300 yd:   1.26 mil
400 yd:   2.08 mil
500 yd:   2.99 mil
600 yd:   3.97 mil
700 yd:   5.05 mil
800 yd:   6.22 mil
900 yd:   7.50 mil
1000 yd:  8.90 mil
```

Berger 140gr Hybrid (G7 0.311, MV 2779 fps):
```
100 yd:   0.00 mil
200 yd:   0.50 mil
300 yd:   1.20 mil
400 yd:   2.00 mil
500 yd:   2.87 mil
600 yd:   3.83 mil
700 yd:   4.89 mil
800 yd:   6.04 mil
900 yd:   7.30 mil
1000 yd:  8.69 mil
```

Federal 140gr SMK (G7 0.253, MV 2681 fps):
```
100 yd:   0.00 mil
200 yd:   0.57 mil
300 yd:   1.35 mil
400 yd:   2.24 mil
500 yd:   3.23 mil
600 yd:   4.32 mil
700 yd:   5.52 mil
800 yd:   6.85 mil
900 yd:   8.31 mil
1000 yd:  9.94 mil
```

---

#### .308 Winchester — 100–1000 yd (100-yd zero)

| Load | MV (fps) | BC G7 | MV Source |
|---|---|---|---|
| Federal 175gr Gold Medal SMK | 2613 | 0.243 | FIRED — Sniper Central (FN SPR A3G, 2015-07-24, 3200 ft ASL, 64°F) |
| Federal 168gr Gold Medal SMK | 2650 | 0.224 | Manufacturer-stated |
| Hornady Match 168gr ELD-M | 2650 | 0.263 | Manufacturer |

Run the Federal 175gr twice: once at sea level/59°F and once at 3200 ft / 64°F. The delta demonstrates altitude/temperature correction behavior.

Trajectory tables (G7 model, 100-yd zero, sea level, 59°F):

Federal 175gr SMK (G7 0.243, MV 2613 fps):
```
100 yd:   0.00 mil
200 yd:   0.62 mil
300 yd:   1.48 mil
400 yd:   2.46 mil
500 yd:   3.58 mil
600 yd:   4.83 mil
700 yd:   6.23 mil
800 yd:   7.82 mil
900 yd:   9.67 mil
1000 yd: 11.80 mil
```

Federal 168gr SMK (G7 0.224, MV 2650 fps):
```
100 yd:   0.00 mil
200 yd:   0.58 mil
300 yd:   1.38 mil
400 yd:   2.29 mil
500 yd:   3.33 mil
600 yd:   4.50 mil
700 yd:   5.82 mil
800 yd:   7.31 mil
900 yd:   9.00 mil
1000 yd: 11.00 mil
```

Hornady 168gr ELD-M (G7 0.263, MV 2650 fps):
```
100 yd:   0.00 mil
200 yd:   0.58 mil
300 yd:   1.37 mil
400 yd:   2.28 mil
500 yd:   3.30 mil
600 yd:   4.44 mil
700 yd:   5.71 mil
800 yd:   7.14 mil
900 yd:   8.75 mil
1000 yd: 10.56 mil
```

---

#### 6mm Creedmoor — 100–1000 yd (100-yd zero)

| Load | MV (fps) | BC G7 | MV Source |
|---|---|---|---|
| Hornady Match 108gr ELD-M | 2960 | 0.270 | Manufacturer (24" barrel) |
| Berger 105gr Hybrid / Federal GM 105gr Berger | 2950 | 0.278 | Manufacturer/Federal tested |

Trajectory tables (G7, 100-yd zero, sea level, 59°F):

Hornady 108gr ELD-M (G7 0.270, MV 2960 fps):
```
100 yd:   0.00 mil
200 yd:   0.42 mil
300 yd:   1.02 mil
400 yd:   1.73 mil
500 yd:   2.51 mil
600 yd:   3.39 mil
700 yd:   4.35 mil
800 yd:   5.42 mil
900 yd:   6.62 mil
1000 yd:  7.95 mil
```

Berger/Federal 105gr Hybrid (G7 0.278, MV 2950 fps):
```
100 yd:   0.00 mil
200 yd:   0.42 mil
300 yd:   1.03 mil
400 yd:   1.73 mil
500 yd:   2.51 mil
600 yd:   3.38 mil
700 yd:   4.33 mil
800 yd:   5.39 mil
900 yd:   6.56 mil
1000 yd:  7.86 mil
```

---

#### .300 Win Magnum — 300–1500 yd (100-yd zero)

| Load | MV (fps) | BC G7 | MV Source |
|---|---|---|---|
| Federal 190gr Gold Medal SMK | 2900 | 0.275 | Manufacturer |
| Hornady 200gr ELD-X Precision Hunter | 2860 | 0.301 | Manufacturer (24" barrel) |

Federal 190gr SMK (G7 0.275, MV 2900 fps):
```
300 yd:   1.07 mil
400 yd:   1.81 mil
500 yd:   2.62 mil
600 yd:   3.53 mil
700 yd:   4.53 mil
800 yd:   5.64 mil
900 yd:   6.87 mil
1000 yd:  8.24 mil
1100 yd:  9.78 mil
1200 yd: 11.51 mil
1300 yd: 13.47 mil
1400 yd: 15.68 mil
1500 yd: 18.16 mil
```

Hornady 200gr ELD-X (G7 0.301, MV 2860 fps):
```
300 yd:   1.09 mil
400 yd:   1.83 mil
500 yd:   2.64 mil
600 yd:   3.53 mil
700 yd:   4.51 mil
800 yd:   5.59 mil
900 yd:   6.77 mil
1000 yd:  8.07 mil
1100 yd:  9.50 mil
1200 yd: 11.10 mil
1300 yd: 12.88 mil
1400 yd: 14.87 mil
1500 yd: 17.09 mil
```

---

### 5.4 Dataset Summary Table

| ID | Caliber | Load | Points | Has Fired Impacts | Env Complete % | Tier |
|---|---|---|---|---|---|---|
| 22lr_cci_sv | .22 LR | CCI Standard Velocity | 7 | No | 0% | Sub-3 |
| 22lr_eley_match | .22 LR | Eley Match | 7 | No | 0% | Sub-3 |
| 22lr_lapua_cx | .22 LR | Lapua Center-X | 7 | No | 0% | Sub-3 |
| 22lr_sk_stdplus | .22 LR | SK Standard Plus | 7 | No | 0% | Sub-3 |
| 22lr_federal_am | .22 LR | Federal AutoMatch | 7 | No | 0% | Sub-3 |
| 22wmr_cci_mm | .22 WMR | CCI Maxi-Mag 40gr | 6 | No | 0% | Sub-3 |
| 22wmr_hornady_vm | .22 WMR | Hornady V-Max 30gr | 5 | No | 0% | Sub-3 |
| 22wmr_federal_gs | .22 WMR | Federal Game-Shok 50gr | 3 | No | 0% | Sub-3 |
| 17hmr_hornady_vm | .17 HMR | Hornady 17gr V-Max | 11 calc + 3 fired | **Yes** | ~0% | **Tier 3** |
| 17hmr_federal_vs | .17 HMR | Federal V-Shok 17gr | 9 | No | 0% | Sub-3 |
| 17hmr_cci_vm | .17 HMR | CCI V-Max 17gr | 9 | No | 0% | Sub-3 |
| 65cm_hornady_eldm | 6.5 CM | Hornady 140gr ELD-M | 10 | No | 0% | Sub-3 |
| 65cm_berger_hybrid | 6.5 CM | Berger 140gr Hybrid | 10 | No | 0% | Sub-3 |
| 65cm_federal_smk | 6.5 CM | Federal 140gr SMK | 10 | No | 0% | Sub-3 |
| 308_fed175_smk | .308 Win | Federal 175gr SMK | 10 | No | 50%* | Sub-3† |
| 308_fed168_smk | .308 Win | Federal 168gr SMK | 10 | No | 0% | Sub-3 |
| 308_horn168_eldm | .308 Win | Hornady 168gr ELD-M | 10 | No | 0% | Sub-3 |
| 6cm_hornady_eldm | 6mm CM | Hornady 108gr ELD-M | 10 | No | 0% | Sub-3 |
| 6cm_berger_hybrid | 6mm CM | Berger 105gr Hybrid | 10 | No | 0% | Sub-3 |
| 300wm_fed190_smk | .300 WM | Federal 190gr SMK | 13 | No | 0% | Sub-3 |
| 300wm_horn200_eldx | .300 WM | Hornady 200gr ELD-X | 13 | No | 0% | Sub-3 |

*308_fed175: altitude (3200 ft) and temperature (64°F) are known from Sniper Central's 2015 test — but no per-yard fired DOPE table was published, so it is Sub-3 on impact data.
†Run this case twice: once at sea level/59°F and once at 3200 ft / 64°F.

---

## 6. Test Methodology

### 6.1 Leave-One-Out (LOO) Validation

For each test case with ≥ 4 usable data points:

1. Sort all DOPE points by distance.
2. For each point `i` (the holdout):
   a. Feed all points except `i` as DOPE training entries.
   b. Run `interpolateDopeAll` to predict elevation at the holdout distance.
   c. Record: `predicted_mil`, `actual_mil`, `delta_mil = predicted - actual`.
3. Also run `calculateTrajectory` (pure physics, no DOPE entries) for the holdout distance and record `physics_mil`.

For test cases with < 4 usable points (22wmr_federal_gs has only 2 after the zero-distance guard): skip cross-validation, run as robustness-only (does not crash with minimal data?). Report this explicitly.

### 6.2 Exhaustive C(N,k) Cross-validation

For a full validation run, test every subset of held-out points for k = 1 .. N−3 (always leaving ≥3 training points). This produces C(N,k) combinations per test case — 4 to 8,099 for the datasets in §5.4. The primary verdict uses k=1 interior leave-one-out (the realistic interpolation case); the worst case across all k is reported as the stress-test verdict (graceful-degradation limit under data sparsity).

### 6.3 Fired Impact Validation (.17 HMR Tier 3)

For `17hmr_hornady_vm`:
1. Use the 9 calculated points (50–250 yd) as training DOPE entries.
2. Predict elevation at 150, 200, and 300 yd.
3. Compare against the 3 fired impacts (Varmint Al data).
4. Report delta in MOA and mils. Note that unknown conditions are a confounding factor.
5. Also run pure physics (no DOPE) against the same fired points.

### 6.4 Altitude/Temperature Sensitivity (.308 Fed 175gr)

Run `308_fed175_smk` twice:
- **Run A:** sea level, 59°F
- **Run B:** 3200 ft altitude, 64°F, pressure ICAO-derived

Report the delta at each distance to validate the atmosphere model.

---

## 7. Error Handling Requirements

The test suite **must not crash** on bad input:

1. **Missing BC:** If `bc == 0`, skip with logged warning.
2. **Missing MV:** If `mv_fps <= 0`, skip.
3. **Insufficient DOPE points:** If `len(dope_points) < 2`, run pure-physics only, mark `LOO_SKIPPED`.
4. **Single DOPE point:** Verify engine falls back to linear extrapolation.
5. **Subsonic / transonic flag:** For .22 LR and .17 HMR above 200 yd / 250 yd, flag each result `TRANSONIC_ZONE` and apply 2× tolerance.
6. **NaN / Inf guard:** Wrap every engine call. Log and move to next case.
7. **Zero-distance guard:** Skip `dope_point` where `yards == 0` or `yards == zero_yards`.

---

## 8. Report Format

The suite writes a Markdown report (`ReportWriter.kt`) to `ballistics/build/reports/ballistics-validation.md`. Structure:

```markdown
# ScopeDOPE Ballistics Engine Validation Report
Generated: {datetime}
Engine version: BallisticsEngine.kt (ScopeDOPE Android v0.9.x)
Run: ./gradlew :shared:ballistics:jvmTest  (from within scopedope-android/)

## Executive Summary
- Total test cases: N
- Tier 3 (confirmed fired impacts): N
- Sub-Tier-3 (cross-calculator): N
- Skipped/errors: N
- Cross-validation method: exhaustive C(N,k) for k=1..N-3 (min 3 training points)
- Max |Δ| Tier 3 (fired): X.XX mil at {load} {dist}yd
- Max |Δ| Sub-3 (all k):  X.XX mil at {load} {dist}yd
- **Primary verdict (k=1 interior LOO):  [PASS / INVESTIGATE / FAIL]**
- **Stress-test verdict (all k):         [PASS / INVESTIGATE / FAIL]**

## Per-Case Results

### {case_id} — {caliber} {load_name}
**Tier:** {tier} | **Env completeness:** {pct}% | **Confirmed impacts:** {yes/no}

k=1 (LOO) — interior holdouts — Primary Verdict:

| Dist (yd) | Actual (mil) | Δ Physics | max|Δ| interior | Verdict (int) | max|Δ| extrap | Verdict (ext) | Flags |
|---|---|---|---|---|---|---|---|
...

**LOO (k=1) summary:** mean |Δ| = X.XX mil | max |Δ| = X.XX mil at {dist}yd
```

**Pass/fail thresholds:**

| Tier | OK | INVESTIGATE | FAIL |
|---|---|---|---|
| Tier 3 (confirmed fired) | < 0.05 mil | 0.05–0.10 mil | ≥ 0.10 mil |
| Sub-Tier-3 (cross-calculator) | < 0.025 mil | 0.025–0.05 mil | ≥ 0.05 mil |

TRANSONIC_ZONE flagged points: apply 2× tolerance to all bands.

---

## 9. Ammo DB

The following entries are in `scopedope-shared/assets/ammo_builtin.csv` (added during initial validation preparation):

| Caliber | Load | MV | BC G1 | BC G7 | Weight |
|---|---|---|---|---|---|
| .17 HMR | Hornady 17gr V-Max | 2550 fps | 0.125 | 0.063 | 17 gr |
| .17 HMR | Federal V-Shok 17gr V-Max | 2530 fps | 0.125 | 0.063 | 17 gr |
| .17 HMR | CCI V-Max 17gr | 2550 fps | 0.125 | 0.063 | 17 gr |
| .22 WMR | CCI Maxi-Mag 40gr JHP | 1875 fps | 0.116 | 0.059 | 40 gr |
| .22 WMR | Federal Game-Shok 50gr JHP | 1530 fps | 0.161 | 0.082 | 50 gr |
| .22 WMR | Hornady Varmint Express 30gr V-Max | 2200 fps | 0.095 | 0.048 | 30 gr |

To rebuild the DB after CSV changes:
```bash
cd /home/jschwefel/repositories/ColdBoreBallistics/scopedope-shared/assets
# Confirm IDENTITY_HASH in build-ammo-db.py matches app/schemas/.../BuiltinAmmoDatabase/4.json
python3 build-ammo-db.py
# Commit both ammo_builtin.csv and databases/ammo_builtin.db
```

CSV columns: `caliber,name,velocity_fps,bc_g1,bc_g7,bullet_weight_gr,mv_sensitivity_fps_per_f,mv_reference_temp_f`

---

## 10. Data Provenance — Honest Accounting

The most important finding from dataset preparation: **truly controlled, confirmed-fired DOPE cards are not publicly available** for any of these loads at the detail level required for Tier 1 or Tier 2 validation.

What was found:
- **Varmint Al .17 HMR:** 3 confirmed fired impact points (Tier 3) — best real-world data in the set
- **PRB 6.5 CM / .308:** Confirmed-fired muzzle velocities (LabRadar, n=40) — high-quality MV, but no per-yard corrections
- **Sniper Central .308 175gr:** Confirmed fired MV + known altitude + temperature — but no DOPE card
- **Everything else:** Manufacturer velocity data + calculator-predicted trajectories

**If you have access to actual fired DOPE data:** Add it as additional test cases with `"fired": true` on the relevant points and `"confirmed_fields"` populated. Any fired data immediately upgrades the case to Tier 2 or Tier 3.

---

## Appendix — Sources

- Varmint Al .17 HMR field test (fired): https://www.varmintal.com/17hmr.htm
- PrecisionRifleBlog 6.5 CM ammo live-fire MV test (2021): https://precisionrifleblog.com/2021/05/28/6-5-creedmoor-ammo-review-muzzle-velocity/
- Sniper Central Federal 175gr GMM fired test (2015-07-24): https://snipercentral.com/federal-175gr-gold-medal-match-test-results/
- Lapua Center-X ballistic data: https://www.lapua.com/cartridges/center-x-22-lr/
- Hornady BC page: https://www.hornady.com/bc
- Berger bullets product pages: https://bergerbullets.com/
- Federal Premium product pages: https://www.federalpremium.com/
- ShootersCalculator trajectory tool: https://shooterscalculator.com/
- TFB Federal AutoMatch fired review: https://www.thefirearmblog.com/blog/2023/01/09/rimfire-report-federal-automatch/
- SK ammunition ballistic data: https://sk-ammunition.com/rimfire/ballistic-data/

---

## Document Control

| Field | Value |
|---|---|
| **Document owner** | `jschwefel@coldboreballisticsllc.com` |
| **Classification** | Highly Confidential (04_-_Highly_Confidential/) |
| **Location** | `OneDrive/ScopeDOPE/04_-_Highly_Confidential/Engine_Documentation/Obsolete/BALLISTICS_TEST_HANDOFF.md` |
| **Cross-references** | `BALLISTICS_ENGINE_ACCURACY_ANALYSIS.md`; `SCOPEDOPE_BALLISTICS_ENGINE_TECHNICAL_WHITE_PAPER.md` |
| **Created** | 2026-06-01 |
| **Last updated** | 2026-06-01 |
| **Next full review due** | 2027-06-01 |

---

## Revision Control Block

| Rev | Date | Changed By | Reviewed By | Approved By | Summary |
|-----|------|------------|-------------|-------------|---------|
| 0.9 | 2026-06-01 | jschwefel | — | — | Initial release |
