# OpenRocket flight parity
`Apogee.java` flies OpenRocket's own test fixture — the Estes Alpha III from
`TestRockets.makeEstesAlphaIII()` — on its A8 and D21 test motors with zero
wind and prints both apogees plus the component masses. The apogees are
pinned as constants in `src/command.rs`
(`openrocket_flight_parity_alpha_iii_{a8,d21}`), which fly the same
configuration through the `rockets` CLI and require agreement within 10 %.
## Reproduce
```sh
curl -LO https://github.com/openrocket/openrocket/releases/download/release-24.12/OpenRocket-24.12.jar
javac -cp OpenRocket-24.12.jar Apogee.java
java -cp OpenRocket-24.12.jar:. Apogee
```
CI runs exactly this on every push (the `openrocket-parity` job in
`.github/workflows/ci.yml`): it downloads the pinned jar, runs the driver,
sanity-checks both references within 5 % of the recorded values, then
injects the live apogees into the parity tests via
`ROCKETS_OPENROCKET_APOGEE_M` (A8) and `ROCKETS_OPENROCKET_D21_APOGEE_M`.
Local test runs use the pinned constants when the variables are unset.
Reference results (OpenRocket 24.12, 2026-10-25, rounded fins / SMOOTH
finish / no launch lug):
| A8 (9 N·s) | subsonic, max Mach 0.42 | 409.64 m | 390.11 m | −4.8 % |
| D21 (32 N·s) | transonic, max Mach 0.85 | 697.02 m | 678.12 m | −2.7 % |
| G64 (128 N·s, synthetic) | supersonic, max Mach 1.60 | 1112.14 m | 1085.71 m | −2.4 % |
The G64 motor is synthetic by design: parity needs identical inputs, not a
real motor file. It exists to push the Alpha III airframe past Mach 1.
## What the G64 fixture taught us
Comparing the drag decomposition at matched Mach numbers (OpenRocket's
friction/pressure/base split against ours) surfaced two model errors,
both fixed:
- **Fin form drag scales with the fin count.** OpenRocket instances a fin
set per fin (`InstanceMap` × `FinSet.getInstanceCount()`), so the set's
form drag is n × the single-fin value from
`FinSetCalc.calculatePressureCD`. We had one fin's worth.
- **Nose `sinphi` is the surface angle at 99% of the nose length**, not the
secant half-angle (`SymmetricComponentCalc`). For a cone the two agree;
for a tangent ogive the base is nearly axial (sinphi ≈ 0.0015 here), so
the ogive's wave drag is effectively zero — we had been charging it the
secant value.
- Also caught: the supersonic leading-edge exponent is 0.502/M² +
0.1095/M⁴, not M⁴/M⁸.
With those, all three flights land within ±5 % of OpenRocket.
## Configuration
The driver boots the OpenRocket core headless (Guice `CoreModule`, like the
unit tests), then modifies the fixture so both sides model the same rocket:
- **Rounded fin cross-section** — the fixture's default SQUARE section adds
~(t/MAC)·A_fins/A_ref ≈ 1.0 of pressure drag (OpenRocket's
`FinSetCalc.calculatePressureCD`); our modeler assumes streamlined fins
(the rounded-section form of that same formula is what we implement).
- **SMOOTH finish** — ≈20 µm paint in OpenRocket's finish table, matching
our modeler's default roughness.
- **Launch lug removed** — our geometry modeler has no lug concept.
- **Zero wind, ISA atmosphere, vertical rod** — a clean ascent; the
parachute ejection delay is set to 1000 s so it never fires.
Masses for the Rust side come from the printed flight mass: structure
24.74 g (nose 4.27 g, fins 13.06 g, everything else 7.41 g), plus the
motor's casing (A8 13.1 g, D21 15.4 g).
## Known remaining differences
- Fin-body interference drag: none on either side. Niskanen's thesis
explicitly ignores it "as small compared to the other sources" — earlier
revisions of this note wrongly listed it as a gap on our side.
- Skin friction matches OpenRocket's fully-turbulent branch exactly
(Schoenherr Cf, roughness limit, and the c1/c2 compressibility blends);
the residual few percent is the transonic interpolator details and
atmosphere-table differences.