# ScopeDOPE — Shared Assets

Cross-platform assets shared across all ScopeDOPE platform repositories.

## Contents

| Path | Description |
|---|---|
| `SPEC.md` | Authoritative product specification — master index (§1–4) |
| `SPEC_data_engine.md` | Specification §5–9: ammo, ballistics engine, DOPE, environment, sensors |
| `SPEC_features.md` | Specification §10–15: calculation, labels, printing, session management, UI |
| `SPEC_nonfunc_appendices.md` | Specification §16–20 + Appendices A–D (NFRs, schema, AppPreferences) |
| `assets/ammo_builtin.csv` | Ammo data source — edit this to add/change/remove entries |
| `assets/build-ammo-db.py` | Regenerates `ammo_builtin.db` from the CSV |
| `assets/databases/ammo_builtin.db` | Built-in ammo SQLite DB consumed at runtime (generated — do not edit directly) |
| `brand/` | Brand assets (logos, color palette) |
| `ballistics/` | KMP `:ballistics` module — engine source, JVM validation tests, and persistent reports |
| `ballistics-test-data/test_cases/` | Validation cases — calibrated reference + real-world fired DOPE (`real_*.json`) |
| `ballistics-test-data/tools/` | Helper scripts (reference regeneration, real-data import) |

## Usage

This repository is included in platform repos as a git submodule at `shared/`:

```bash
# When cloning a platform repo for the first time
git clone --recurse-submodules git@github.com:ColdBoreBallistics/scopedope-android.git

# Or if already cloned without submodules
git submodule update --init
```

## Updating the Ammo Database

Edit `assets/ammo_builtin.csv` — one row per load. Columns:

```
caliber,name,velocity_fps,bc_g1,bc_g7,bullet_weight_gr,mv_sensitivity_fps_per_f,mv_reference_temp_f
```

Then regenerate the SQLite DB:

```bash
cd assets
python3 build-ammo-db.py
```

The script recreates `databases/ammo_builtin.db` with the exact Room schema and identity hash expected by the Android app. After any schema change, bump the Room DB version, write a migration, and update the `IDENTITY_HASH` constant in `build-ammo-db.py` to match the new schema JSON at `app/schemas/.../BuiltinAmmoDatabase/<version>.json`. Commit both the updated CSV and the regenerated DB.

See `SPEC_nonfunc_appendices.md` Appendix A for the full database schema.

---

## Ballistics Engine Validation

How to run, read, and extend the ballistics engine validation suite. This guide is
operational only; it does not document engine internals.

The suite checks two things, treating the engine as a black box:

1. **Physics accuracy** — does the engine's pure-physics trajectory agree with trusted
   references (independent solvers, and where available confirmed field-fired data)?
2. **DOPE-correction accuracy** — given recorded come-ups, does the engine predict a
   held-out point more accurately than pure physics alone?

### Quick start

Open a terminal and run the validation from either repo (the project prefix differs —
see [Running the validation](#running-the-validation)):

```sh
cd scopedope-android   &&  ./gradlew :shared:ballistics:jvmTest
# or, from the standalone shared repo:
cd scopedope-shared    &&  ./gradlew :ballistics:jvmTest
```

One run validates the whole suite — calibrated reference cases **and** the imported
real-world fired-DOPE cases, each scored against the band appropriate to it. A passing run
prints `BUILD SUCCESSFUL` and writes a timestamped report to
`ballistics/reports/ballistics-validation-<timestamp>.md`. No setup beyond a supported
JDK — see [Prerequisites](#prerequisites).

### Where things live

| Path | What |
|---|---|
| `ballistics/src/commonMain/…` | The engine under test (KMP `:ballistics` module) |
| `ballistics/src/jvmTest/…` | The validation test + cross-validation runner + report writer |
| `ballistics-test-data/test_cases/` | All validation cases — calibrated reference + imported real-world fired DOPE (prefixed `real_`) |
| `ballistics-test-data/tools/` | Helper scripts (reference regeneration, real-data import) |
| `ballistics/reports/…` | Timestamped Markdown validation reports (persistent — survive `gradle clean`) |
| `ballistics/build/…` | Gradle's JUnit XML + auto-generated HTML test reports (git-ignored, ephemeral) |

`scopedope-shared` is consumed by `scopedope-android` as the `shared/` git submodule, so
the same files also appear under `scopedope-android/shared/…`.

### Prerequisites

A supported JDK (the build runs on **JDK 21**; the system default may be newer than the
build tooling supports). The Gradle JVM is pinned in `gradle.properties`, so no manual
`JAVA_HOME` is needed when running from this repo.

### Running the validation

A single task runs the whole suite. **The task path depends on which repo you run from:**

| Run from | Command |
|---|---|
| `scopedope-shared` | `./gradlew :ballistics:jvmTest` |
| `scopedope-android` | `./gradlew :shared:ballistics:jvmTest` |

It runs every case in `test_cases/` — calibrated and real-world — and **fails the build**
if the primary verdict is `FAIL`. Each case is scored against the band appropriate to it
(see [Verdict bands](#verdict-bands)), so noisy real-world data is held to field-realistic
tolerances rather than the strict reference band.

The task **always re-runs** (it is a validation/report task, not a cached unit test), so
each invocation executes the suite and writes a fresh timestamped report — you do not need
`--rerun-tasks`.

### The dataset

All cases live in `test_cases/` and run in one suite. They come in two kinds, distinguished
by the `realworld` flag in each case (see schema below):

| | Calibrated reference | Real-world (`realworld: true`) |
|---|---|---|
| Source | Curated reference trajectories + a little confirmed-fired data | Verified user-attributed field DOPE, imported from the separate real-data dataset |
| File prefix | caliber (e.g. `308_…`) | `real_` |
| Tolerance band | Strict (solver/click floor) | Wide (scope-click reality) |
| Authoring | By hand | Generated — re-run the importer, do not hand-edit |

Both kinds gate the build, each against its own band.

### Test-case JSON schema

Each case is one JSON file. Required fields unless noted:

```json
{
  "id": "308_fed175_smk",
  "caliber": ".308 Winchester",
  "load_name": "Federal 175gr Gold Medal Match SMK",
  "bc_model": "G7",                 // "G1" or "G7"
  "bc": 0.243,
  "bc_source": "…",                 // optional, free text
  "mv_fps": 2613,
  "mv_source": "…",                 // optional, free text
  "bullet_weight_gr": 175.0,
  "zero_yards": 100,
  "sight_height_in": 1.5,
  "twist_rate_in": 10.0,            // inches per turn; 0 if unknown
  "conditions": {
    "altitude_ft": 0,
    "temp_f": 59.0,
    "pressure_hpa": 0,              // 0 = derive from altitude
    "humidity_pct": 0,
    "confirmed_fields": []          // which of the 4 fields are actually known
  },
  "dope_points": [
    { "yards": 200, "mil": 0.62, "moa": 2.13, "source": "…", "fired": false }
  ],
  "tier": "Sub-3",                  // "1" | "2" | "3" | "Sub-3"
  "realworld": false,               // true = real field DOPE; use the wider field band
  "data_notes": "…",
  "source_urls": []                 // optional
}
```

Notes:
- **`mil`** is the elevation come-up (positive = dial up). `moa` is the same value in MOA.
- **`fired: true`** marks a point that came from a physical impact (not a calculator).
- **`tier`** reflects data quality, not engine quality:
  - `1` / `2` / `3` — has confirmed fired impacts, with decreasing environmental completeness.
  - `Sub-3` — no confirmed fired impacts (reference comparison only).
- `confirmed_fields` lists which of `altitude_ft`, `temp_f`, `pressure_hpa`, `humidity_pct`
  are actually known versus assumed.

### How cases are scored

For each case the runner performs **exhaustive C(N,k) cross-validation**: it repeatedly
hides subsets of the recorded points, predicts them from the rest, and measures the error.

- **Primary verdict — `k=1` interior leave-one-out:** hide one point that the remaining
  points bracket, and predict it. This is the realistic interpolation scenario and is what
  gates the build.
- **Stress-test verdict — all `k`:** the worst case across every subset size, down to the
  minimum training set. Larger errors here are expected and document graceful degradation,
  not regressions.

Cases with fewer than 4 usable points are reported but skipped for cross-validation.

#### Verdict bands

Each prediction error is labeled `OK` / `INVESTIGATE` / `FAIL`. Transonic-zone points get
2× the tolerance.

The band a case is scored against is chosen **per case**:

| Case kind | OK | INVESTIGATE | FAIL |
|---|---|---|---|
| Calibrated reference (`tier` ≠ `3`) | < 0.025 mil | 0.025–0.05 mil | ≥ 0.05 mil |
| Calibrated confirmed-fired (`tier` = `3`) | < 0.05 mil | 0.05–0.10 mil | ≥ 0.10 mil |
| Real-world (`realworld: true`) | < 0.10 mil | 0.10–0.20 mil | ≥ 0.20 mil |

The real-world band is intentionally wider: field DOPE is recorded to scope-click resolution
(0.1 mil = one click) and is sparser, so it cannot meet the reference bands.

### Reading the report

After a run, open the timestamped Markdown report:

- `ballistics/reports/ballistics-validation-<timestamp>.md`

It contains an executive summary (case counts, max deltas, primary/stress verdicts), a tier
table, and a per-case breakdown of interior vs. extrapolated predictions with verdicts.
JUnit XML for CI is under `ballistics/build/test-results/`.

> Sparse cases (few points spread over wide range gaps) can show large interior errors purely
> from interpolating across the gaps — that is a data-density limitation, not an engine fault.
> Real-world cases that cannot be cross-validated within the field band are excluded by the
> importer (see below) rather than allowed to fail the gate.

### Importing real-world DOPE

`tools/import_real_dope.py` converts verified field DOPE from the separate real-data dataset
into `test_cases/` as `real_*.json` (each stamped `realworld: true`). It is idempotent and
dry-runs by default:

```sh
cd ballistics-test-data/tools
python3 import_real_dope.py            # dry run — prints the cases it would generate
python3 import_real_dope.py --write    # write real_*.json into ../test_cases/
python3 import_real_dope.py --db /path/to/dataset.db   # custom dataset location
```

Sessions too sparse/rounded to cross-validate within the field band are skipped via the
tool's `EXCLUDE_SESSIONS` set, so the merged gating suite stays clean.

It selects cross-validation-ready sessions (verified provenance, enough points, a recorded
zero and muzzle velocity), normalizes units and sign conventions, classifies the tier from
environmental completeness, and writes one file per session. Re-run it whenever the source
dataset changes. (The source dataset itself lives outside this repo and is not required to
run the validation tasks.)

### Adding a case by hand

1. Create a JSON file in `test_cases/` following the schema above.
2. Provide at least 4 usable points (beyond the zero) for cross-validation.
3. Set `tier` honestly and list any known environment in `confirmed_fields`.
4. Re-run the calibrated suite and review the report.

### Troubleshooting

| Symptom | Fix |
|---|---|
| Gradle fails immediately with a Java version string (e.g. `25.0.x`) | The pinned JDK in `gradle.properties` is missing/invalid — fix the path or set `JAVA_HOME` to a supported JDK. |
| `WARNING: …restricted method…` / `openjdk version 25` lines, then it seems to do nothing | Harmless. The `gradlew` launcher is on your shell's newer JDK (the warnings), but the build runs on the pinned JDK. The suite still executes and prints `Loaded N test cases` + verdicts; to silence the warnings, `export JAVA_HOME` to the pinned JDK before running. |
| `project 'ballistics' not found` | Wrong task path for the repo you are in — use `:ballistics:…` from `scopedope-shared`, `:shared:ballistics:…` from `scopedope-android`. |
| `No test cases found` | The data directory is empty or the wrong path is wired — confirm `test_cases/` is populated. |
| New cases not picked up in `scopedope-android` | The `shared/` submodule is pinned — commit in `scopedope-shared` and update the submodule pointer. |

---

*© 2026 Cold Bore Ballistics LLC. Proprietary and Confidential. All rights reserved. Unauthorized reproduction, distribution, or disclosure of this material is strictly prohibited without the express written consent of Cold Bore Ballistics LLC.*
