Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Validation plan and reference inventory

This inventory was checked on 2026-09-16. The sources cargo xtask refs fetch downloads were pinned on 2026-09-17 in validation/refs.lock.toml (commits and sha256 values; ADR-002 decision record), which is the source of truth for versions and URLs. Everything downloaded goes to the gitignored refs/; only small extracted fixtures with a clear license are committed, each with its provenance.

Principles

  • Four levels of evidence:

    1. Analytic and unit tests.
    2. Component-level references (tables and worked examples).
    3. Code-to-code (RocketPy, OpenRocket).
    4. Real flights.

    Code-to-code agreement is not truth. Real flights are the final arbiter, and their uncertainty (weather, as-built mass, motor variation) must be stated.

  • Stored references with provenance. Every reference output records the tool, version, date, inputs hash and command, so CI can compare without Java or Python. A manually triggered workflow regenerates the references.

  • Per-case tolerances in files. Tolerances live in the case file, next to the reason for their size. A regression beyond tolerance fails CI.

  • Always regenerated. validation/reports/latest.md (plus JSON) is regenerated by cargo xtask validate. It lists every case, error, tolerance and verdict. The accuracy census (validation/reports/census.md, the census) counts the numbers the committed reports hold hpr to.

  • Initial targets (targets, not gates, until the first report exists):

    • Code-to-code apogee within 3% (subsonic) and 5% (transonic/supersonic).
    • Real-flight apogee mean absolute error at or below 5% on well-characterized flights.
    • Every miss is explained in the report.

    Since the first report (M2.1a milestone), the code-to-code 3% is a gate in same-drag mode and stays a target in predicted mode, where neither code’s drag is the truth (ADR-023 decision record); a predicted miss is explained in its case file, and the set of misses is pinned by a test.

Operating envelope

The operating envelope says which flights the accuracy work serves first. It sets the order of the work. It does not limit what hpr flies (ADR-143 decision record). The bands are set by the Mach number alone:

termMach numberwhat it means
Core band0 to 2.5Accuracy work goes here first. Nearly every flight on commercial motors is in it.
Extended band2.5 to 3.5Record flights on the largest commercial motors. hpr flies it; its accuracy is checked less than the core band’s.
The envelope0 to 3.5Every flight up to Mach 3.5, at any angle of attack.
Beyond the envelopepast 3.5Deferred, not dropped. These flights still fly.

The angle of attack is a separate condition. Accuracy work assumes it stays at 15° or less. A flight above 15° more than 1 s after leaving the rail is at high angle of attack, at any Mach number. The first second does not count, as a rocket meets the air at a steep angle then, for a moment (below). That 1 s was chosen, not measured; M1.14e, on large angles of attack, measures how long the transient lasts and studies high angles.

Every flight still flies, but one that goes past an edge carries a flag, each tested at its edge (M1.14a1, the envelope flags; ADR-179). A flag only marks numbers to trust less; it changes none. A flight exactly at an edge, such as Mach 2.5, raises no flag; only one past it does. The flags stack: a flight past Mach 3.5 raises all three Mach flags.

  • Beyond the validated range: the flight goes faster than the fastest public whole-flight reference, meaning any committed comparison of a public flight against an independent reference, gated or not. Today that is OpenRocket’s Dual parachute deployment example at Mach 1.1467, OpenRocket’s own top speed for it. The number is a constant in the code, and a test reads the committed reports and fails when it isn’t their fastest, so it rises as references are added. Private flights never set it.

  • At high angle of attack: above 15° more than 1 s after leaving the rail and before apogee or the first deployment, at an instant where a 15° angle would give a normal force of at least a fifth of the rocket’s weight (Flight metrics: the largest angle of attack).

    Without that floor, every flight whose path turns over would raise it: near apogee the rocket slows, and its angle swings past 15° where the air is too weak to turn it (there a 15° angle gives 0.7% to 5.0% of the weight on the tests’ rockets; a wind layer met at speed, 62% to 141%). The fifth is chosen, not measured, like the 1 s.

  • Outside the core band: past Mach 2.5.

  • Beyond the envelope: past Mach 3.5.

hpr sim prints a warning: envelope: line for each and lists them under flags in its JSON; the library has Flight::envelope_flags, and Python Flight.envelope_flags. Of the public designs hpr sim flies offline, 25 .ork files and 12 validation designs, flown off an 85° rail in calm air and in 8 m/s of wind on 2026-10-06 (hpr sim <design> --offline --inclination 85 --wind 0, then --wind 8), only one raises a flag: beyond the validated range, at Mach 1.61. It is an invented two-stage rocket, synthetic-two-stage-75mm-54mm.json, which hpr sim flies as one stack.

unstable_under_power and unstable_without_margin are reported with the envelope’s flags, but neither is an edge of the envelope. A flight whose static margin falls below zero while a motor burns, before apogee or the first deployment, is unstable under power. Where hpr can give no margin, a pitching-moment slope C_mα above zero under power says the same and raises unstable_without_margin instead, so a flight raises one of the two at most. hpr sim prints a warning: unstable: line and marks its apogee as not a prediction (Flight metrics: unstable under power; #335). A margin or C_mα of exactly zero raises nothing. When this was written (2026-10-06), none of the 37 designs above raised either on its default configuration, in either wind; no test or report pins that count. Two of the four configurations of OpenRocket’s Pods–powered with recovery deployment raise unstable_under_power, at −4.21 calibres.

A flight also warns, by issue number, when it meets one of the known errors in hpr’s drag (M1.14a2, the drag issue warnings; ADR-180). #18 warns on every flight, so it names no shape and no Mach edge; #73 has a shape but no Mach edge; the rest have both, each edge exclusive as the flags’ are:

issuewhat reads wrongwhen it warns
#67the pressure drag of a cone, an ogive, or a power or parabolic series close to a cone: high from Mach 0.8, about twice Stoney’s measured cone at Mach 0.85, 2 to 9 times MIL-HDBK-762’s ogive from Mach 0.9 to 1.2, still +15% at 1.5such a nose, or such a shoulder (a transition widening aft), past Mach 0.8
#68base drag: high from Mach 0.8 to 1.2 against MIL-HDBK-762’s data, low from 1.5 against Love’s correlation (17% at Mach 2); the sign between is unmeasuredevery rocket, past Mach 0.8
#70a sharp fin’s drag, high faster than sound; hpr has no sharp sectionairfoil fins, past Mach 1
#72a steep boattail’s drag, high faster than sound against NASA’s measured models (+13.5% to +50.8% at 15°, +26% to +54% at 16°)a boattail steeper than 10°, past Mach 1
#222supersonic pressure drag, about twice OpenRocket’s, a comparison between two codes with no measurement to say which is rightan ogive nose or airfoil fins, past Mach 1
#73a boattail’s drag below Mach 0.8, by Niskanen’s rule: from −40.8% to +41.7% on Cubbage’s measured 16° boattails (NACA RM L57B21), high on the Arcas Robin’s 15° onea boattail steeper than 9.5° (atan(1/6), where the rule’s length ratio falls below 3 and it starts to give the boattail drag), on any flight
#18skin friction, taken as fully turbulent: where a smooth surface keeps a laminar run, Barrowman’s transitional term takes 3.6% off the friction on RocketPy’s Calisto at Mach 0.3, about 2% of its drag at zero lift; nothing says how rough a surface must be to trip the flow at onceevery flight, at any speed and on any finish (ADR-190)

“Past Mach 0.8” means the flight’s top Mach number is above 0.8; one at exactly 0.8 doesn’t warn. Drag that reads high makes the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are. Each warning says so. #68’s adds, for a flight past Mach 1.2, that from Mach 1.5 its error turns the other way. #70’s and #222’s are conditional: hpr has no sharp fin section, so #70 applies if your airfoil fins are really sharp-edged, and #222 if hpr rather than OpenRocket is the one off. #73 warns at any speed, since every flight is subsonic for a while; its error runs both ways, and the warning covers the side where the drag reads high. Where an issue leaves a range unmeasured, such as boattails between 10° and 15°, the warning takes it in. A flight flown on a drag table or model of your own warns of none, as its drag isn’t hpr’s. hpr sim prints a warning: drag: line for each and lists them under issues in its JSON; the library has Flight::issue_warnings, and Python Flight.issue_warnings. #18 warns on every flight on hpr’s drag: Barrowman (1967) gives no rule for how rough a surface must be to trip its flow at once, so all 37 public designs raise it. Of the same 37 flown off an 85° rail in calm air, 8 raise at least one other: #68 on all 8, #67 on 4 and #222 on 2. #70 and #72 are raised on none, as no public design flown past Mach 1 has airfoil fins or a steep boattail; unit tests pin their edges. That count was taken before #73 and #18 joined the list (M10.1d2, the missing warnings). Seven more warn where the stability margin reads high (M1.14a3, the stability issue warnings; ADR-181):

issuewhat reads wrongwhen it warns
#87a step in radius takes the whole body off the supersonic shock-expansion method, so slender-body theory puts the center of pressure aft (on the tests’ rocket at Mach 3 and 4°, −8.65% of the normal force and 1.03 calibres)a step that stopped the method, past Mach 1.2
#120the same, for a lip behind a boattail longer than the boattail’s drop in diameter (−33.0% and 1.77 calibres)such a lip, past Mach 1.2
#121the same, for a pointed tip steeper than the cone tables’ 30° (−7.7% and 0.81 calibres)such a tip, past Mach 1.2
#172the static margin read up to 0.1108 calibres higher than OpenRocket 24.12’s on four private designs (0.0350 to 0.1108), for a reason not yet foundevery flight with a static margin
#64a fin whose leading edge sweeps forward: supersonic linear theory’s slope rises by up to 7.7% past Mach 1.2, where it should fall, so the center of pressure sits aftsuch a fin (a trapezoid’s tip ahead of its root, or a freeform point ahead of the root’s leading edge), past Mach 1
#325fin sets at one station are counted apart for fin–fin interference, where OpenRocket counts them together: about 0.029 of the 0.071 to 0.076 calibres hpr reads above OpenRocket on its Pods–airframes and winglets examplefin sets whose roots overlap or touch along the axis, on one airframe or pod set, with more than four fins between them, on any flight
#326a kinked freeform fin’s center of pressure sits 1.6 mm aft of OpenRocket 24.12’s, about 0.047 calibres of that example’s gap; other freeform outlines are unprobedevery freeform fin set, at any speed (ADR-190)

Mach 1.2 is where the method first joins slender-body theory, so the three switches can’t move a number below it. The aerodynamics report which switch stopped the method, so the warning follows the code that switches. #172 has no known condition; it gives the gap’s size and sets no threshold, since the margin you need is the RSO’s and the safety code’s call. #325’s rule in OpenRocket is unsized between overlapping roots and a common aft station, so the warning takes the wider: roots that overlap or touch. One more comes from the design checks, not the flight: a packed part drawn too wide for the room in a nose cone or a shoulder, where the room widens aft of it, names #367, because its mass could sit only farther aft than drawn and the margin may read high. These print as warning: stability: lines, with kind "stability" in the JSON and in Python. A flight on a drag of your own keeps them, as its normal force is still hpr’s; a model given a normal-force table of its own prints none, #172 included. A staged flight asks the whole stack’s body, the upper stage at its front, so a switch in the booster can warn with the upper stage’s top speed: it can over-warn, and whether it can miss one is not yet checked, a limit of this increment.

Why these edges:

  • NASA Student Launch (1,220 to 1,830 m, or 4,000 to 6,000 ft) and the American Rocketry Challenge (229 m, or 750 ft, its 2026-season target) fly below the speed of sound.
  • Spaceport America Cup teams in the 9,144 m (30,000 ft) commercial-motor category fly Mach 1.6 to 2.1: Concordia’s 2018 report gives Mach 1.64, and UC Aerospace’s 2024 flight went “just over Mach 2”.
  • The fastest commercial-motor flights, minimum-diameter record builds such as CTI O3400 and N5800 flights, reach about Mach 3.5. Tripoli’s single-stage commercial altitude records were 13,885 m (45,554 ft) on an M motor, 15,614 m (51,228 ft) on an N and 20,040 m (65,748 ft) on an O, in a 2016 snapshot.
  • At the legal wind limit (20 mph, or 8.9 m/s, in NFPA 1127), a rocket leaving the rail at 50 to 100 ft/s (15 to 30 m/s) meets the air at about 16° to 30°, the slower the steeper. So 15° covers the climb, not the first instant off the rail.

What is checked today. Code-to-code whole flights mostly stay below Mach 1.15 and 4 km; the fastest public one is OpenRocket’s example at Mach 1.147 (Accuracy: results by model; hpr’s flights against OpenRocket’s). One private flight is supersonic, and reads 13.60% high in apogee against OpenRocket. Real flights reach about Mach 1.0 and 3.9 km (Accuracy: real flights; hpr’s peak Mach for each flight is the last column of the real-flight report). So most of the core band is checked part by part, not as whole flights.

The stop rule. An accuracy step makes progress when it shrinks a measured error against an independent reference, or adds a reference that will. Either counts as progress; two steps in a row that do neither end the milestone, gaps written down. Each step names the band it serves, and work outside the core band needs a stated reason.

The validation harness

This section covers the M2.1a validation-harness milestone.

cargo xtask validate [--fast|--check] runs every case in validation/cases/lock.toml and writes validation/reports/latest.md and latest.json. --check writes nothing (see below); it runs the whole suite, so it cannot be combined with --fast. A case (validation/cases/<id>.toml) says what to fly and which metrics to compare, each with its own tolerance, against which reference (validation/fixtures/**, written by a generator under validation/oracles/). Decisions: ADR-015 decision record; code: crates/hpr-validate/.

The rules the harness enforces, each from a Loft lesson:

  • A run reads references and never writes them: a reference moves only when its generator runs (L76 Loft lesson). There is no flag to update one.
  • Every reference value carries a source naming the oracle, the generator and the field, and the report carries the reference file’s SHA-256, so an edited reference shows up in the report (L77 Loft lesson).
  • Every metric a case reports is either held to a tolerance that bounds something or declared, in writing, not scored; a case is refused if hpr measures a metric it does not account for, or if the reference publishes one the case ignores (L79 Loft lesson).
  • The cases that must run are locked; a missing one fails, a committed case that is not locked fails, and --fast may only leave out cases the lock marks slow and names them (L78 Loft lesson).
  • A case’s inputs come from the reference’s own record of what the oracle flew, never from hpr’s output, including which design it flew, what that weighed and, for a whole flight, the area its drag table is on (L75 Loft lesson, hpr_validate::rocketpy::tests::oracle_inputs_come_from_the_case_file_not_hpr_outputs).
  • A case may declare a known gap, a limit of hpr’s it runs into. The only one accepted is hpr’s refusal of a Mach number past its models’ range, which since M1.8b1 milestone ends at Mach 5 for the normal force and the drag buildup alike: the reference must reach Mach 5, hpr must refuse the flight with that error, and a gap that hpr starts flying fails the run (L85 Loft lesson). A gap scores nothing and is listed in the report’s own section, and the set is pinned (ADR-021 decision record). No case declares one. Prometheus 2022 was one on the declared drag until M1.8a milestone flew it (ADR-027 decision record), and on its own drag, refused at Mach 1 by the drag buildup, until M1.8b1 milestone did (ADR-028 decision record).

Not scored is the harness’s one escape hatch, and it is deliberately uncomfortable: the case has to write down why, a blank reason fails outright, the metric is still measured and still printed with both numbers and the difference, it never counts as a pass, and the whole excused set is pinned by hpr_validate::tests::the_metrics_that_are_not_scored_are_these_and_no_others. It was written for Valetudo’s northward drift, which read 28x RocketPy’s, and the right answer there turned out to be to fix the comparison rather than to excuse the number. So did the drifts in wind (issue #50): RocketPy’s equations were at fault, and the comparison now flies them corrected (ADR-026 decision record). Eleven whole-flight metrics use it today (report), each argued in its case file (ADR-021 decision record, ADR-026 decision record): Juno III’s, Bella Lui’s and Prometheus 2022’s drifts in wind and NDRT 2020’s apogee drift, measured model differences (hpr’s body lift at the rail exit’s angle of attack, which RocketPy leaves out, and its release at the last rail button; with both added to RocketPy, wind_response.py lands every windy drift within 1.3% of hpr’s), Calisto’s time of peak acceleration in wind and in calm air, whose two peaks are 0.9% apart, and NDRT 2020’s and Prometheus 2022’s whole-flight peaks, which are their main openings, where RocketPy has added mass and hpr has none. No case carries an absolute floor: every gate is the milestone’s 3%.

In CI, and regenerating the references

This section covers the M2.1c1 validation-in-CI milestone.

Every pull request runs cargo xtask validate --check on macOS, Windows and Linux, in continuous integration (CI): the validate job in .github/workflows/ci.yml. It flies every locked case, compares each result with the stored RocketPy numbers (RocketPy itself is not run), and writes nothing. It fails if a scored metric is outside its tolerance, or if the committed report differs from this run’s. Numbers may differ in their last digits, because platforms round differently: hpr’s value and the reference’s, read at full precision from latest.json, by up to 2e-6 or 1e-7 of the value, whichever is larger. Everything else must match exactly: the cases, sources, tolerances, verdicts, notes, known gaps and the harness version, and latest.md must be latest.json’s own rendering (Report::reproduces; ADR-022 decision record, the decision behind this section). So a change that moves a number has to commit the report that shows it.

Then it holds the committed reports to the accuracy census accepted last (M2.4 milestone, ADR-084 decision record). Each number the harness’s report, the real flights’ and both OpenRocket reports hold hpr to is a row (the census). The check fails when:

  • a row moves by more than its slack, 0.1% of its scale, in either direction;
  • a row changes its standing, or comes or goes;
  • a group’s reference changes.

Regenerating the report is not enough to carry such a change in: it takes cargo xtask census --accept --reason "<why>", and the reason is committed with it. This is what holds a predicted-mode number, whose 3% is only a target, to where it was.

CI never runs RocketPy. So it shows that hpr still reproduces the committed report, not that the stored references are still what RocketPy produces: a change in RocketPy or in a generator shows up only when a person regenerates the references.

  • Locally: scripts/regenerate-references.sh, a bash script for macOS and Linux. It needs uv 0.12 or later and a one-time cargo xtask refs fetch python rocketpy. It runs rocket_mass.py, cargo xtask designs, recovery.py and flight.py, in that order, then cargo xtask validate --check, and rewrites the report only if that check fails. It overwrites files under validation/ in the working tree (git checkout validation undoes it) and lists what changed. It took about 40 s on the Mac it was measured on, after the fetch.
  • On GitHub: the Regenerate references workflow (regenerate-references.yml). Only someone with write access can start it, from the Actions tab or with gh workflow run regenerate-references.yml, and only from a branch that has the workflow (GitHub dispatches only workflows the default branch has). It runs the same script on an arm64 Mac, like the one the committed references came from, and uploads the diff as the references-diff artifact. Its token can read the repository and nothing more, so it cannot commit.

Another machine’s floating point can move the regenerated fixtures’ last digits, and a fixture that moved at all makes the report be rewritten too. The first run on GitHub’s Mac did that: the descents moved by at most 3.6e-11 of each value, the whole flights’ largest move was a landing height of 2e-8 m shifting by 3e-9 m, and no printed metric changed. So the script prints, for each fixture, how many values moved and the largest relative move; read that before the diff. Either way the result is a diff to read, not a new reference. Committing it is a decision a PR has to argue: Loft lesson L76, where a reference regenerated whenever a check failed ended up following the simulator it was meant to check.

Whole flights

This section covers the M2.1b2 whole-flight-comparison milestone.

The whole-flight cases (validation/cases/flight-*.toml) fly RocketPy’s examples from the pad to the ground in the same-drag mode: hpr flies the reference’s declared C_D0(M) through Simulation::with_drag_table, on the reference area the reference records. The metrics are measured as RocketPy defines them (L80 Loft lesson): at the center of dry mass, with the rail exit when the forward button reaches the top of the rail. The maxima depart from RocketPy’s on purpose: RocketPy takes them at its solution’s points, and hpr finds each peak between its solver’s steps as well (ADR-023 decision record, which sets how peaks are found in both modes), because a peak read only at the steps moves with the step sequence, which differs across platforms. That can only raise hpr’s reading; against its old step-end reading it rose by at most 6.3e-5 of itself. RocketPy’s own shortfall is not measured.

The first run found an input, not a model, difference: the transcribed designs corrected the thrust for ambient pressure with a sea-level stand-in, which RocketPy’s examples never do (reference_pressure=None). The designs now say None, the reference records the motor RocketPy flew, and the harness checks hpr’s against it. Heights are measured from the dry center of mass’s height at launch, since RocketPy’s starts at the ground, and the rail exit at RocketPy’s effective_1rl. Every scored metric agrees within 3%, and since M2.1d3 milestone that includes every drift but seven: those of the three rockets that leave the rail slowly for the wind they meet, Prometheus 2022, Juno III and Bella Lui, and NDRT 2020’s apogee drift (report) (ADR-026 decision record).

RocketPy’s equations as corrected upstream (M2.1d3 milestone). The whole-flight references fly RocketPy 1.13.0 with two corrections made or proposed upstream, applied by validation/oracles/rocketpy/corrections.py and recorded in each fixture’s corrections: PR #1188 (merged, unreleased), the nozzle’s jet-damping lever, and PR #1196 (open, for issue #1186), the sign of the center-of-mass and nozzle vectors in u_dot_generalized. As released, RocketPy took its moments during the burn about a point as far forward of the dry center of mass as the center of mass is behind it, which made it turn into the wind too far; that was most of issue #50. The corrected function is RocketPy’s own source with PR #1196’s three edits, each required to match once, so a RocketPy that has moved stops the generator. wind_response.py flies every case as released and corrected, then with hpr’s rail release, body lift and thin fins added, and lands within 1.3% of hpr’s drifts in wind (ADR-026 decision record; body lift Jorgensen’s since M1.8e6 milestone, ADR-037 decision record). Drop the corrections when RocketPy releases them.

That fix is worth stating, because it is what L75 Loft lesson means in practice. hpr’s default gravity is the full normal-gravity vector, which leans a few parts in 10⁶ toward the equator above the ellipsoid; RocketPy applies gravity to the vertical axis alone. The difference is 5.2e-4 m of northward drift over an 800 m descent, which is invisible in every metric that matters and swamps the one 20 µm number that does not. hpr ships GravityModel::VerticalTaylor as RocketPy’s own formula for like-for-like comparisons, so the suite flies that, and the metric comes to −1.8% (ADR-015 decision record, issue #27, docs/physics/recovery.md).

The committed report carries no timestamp, so a number that moves shows up in the diff. A --fast run writes latest-fast.{md,json} instead, which is not committed: a partial report never stands in for the whole suite’s record.

Predicted mode

This section covers the M2.1c2 predicted-mode milestone.

The predicted-* cases fly the same six examples with hpr’s own aerodynamics (mode = "predicted"), against validation/fixtures/flight/rocketpy-whole-flight-own-drag.json: RocketPy flying each example’s own drag, as RocketPy 1.13.0 flies the example (flight.py --own-drag). The curves stay in refs/; the reference records each one’s path and SHA-256 (ADR-009 decision record). Each mode refuses the other’s reference.

Each predicted metric keeps M2.1 milestone’s 3% as a target, not a gate: its verdict is within target or outside target, it sits in the report’s own Predicted mode section, and it never fails the run (ADR-023 decision record). Neither code’s drag is the truth, so a miss is a measurement to explain, and each case file explains its own. In short, 75 of 102 are within target; the apogees are −7.280% (Prometheus 2022), −0.609% (Calisto), +0.971% (Bella Lui), +2.097% (Juno III), +10.113% (Valetudo) and +10.302% (NDRT 2020) (report). The last two are where hpr’s drag is well below the example’s, which also moves their times and drifts; Juno III’s and Bella Lui’s drifts in wind differ as in same-drag mode (ADR-026 decision record). Prometheus 2022 flies through Mach 1 on hpr’s drag since M1.8b1 milestone (ADR-028 decision record), its peak Mach +1.069% from RocketPy’s, with 9 of its 17 metrics within target: hpr’s coasting drag rises to about 0.49 at Mach 0.8, where the example’s falls to 0.30, so it coasts lower. hpr’s drag is for the designs as transcribed, whose fin edges and finishes are placeholders where the examples record none. Predicted mode flies at rtol = atol = 1e-11, so its report reproduces across platforms (ADR-023 decision record).

Real flights

This section covers the M2.3b real-flights milestone (ADR-082 decision record).

cargo xtask real-flights [--check] flies seven of RocketPy’s documented rockets (Bella Lui, NDRT 2020, Prometheus, Juno III, Cavour, Genesis, Lince) with hpr’s own aerodynamics, on each example’s own thrust file, from its rail and site, in the ERA5 file and hour its notebook reads, and compares each with its team’s altitude log: the apogee, the RMS of the height over the ascent with both clocks aligned where each trace first reaches 30 m. hpr’s height is read as the log’s barometric altimeter reads the air: the standard atmosphere’s altitude of the ERA5 pressure, less the start’s. Each log is read up to its apogee, stopping before the recovery’s pressure transients. The logs, thrust files and weather files are read from the pinned refs/rocketpy checkout and never committed; the report, validation/reports/real-flights.{json,md}, commits only the numbers and each file’s SHA-256. Each flight is flown again on its example’s own drag as a diagnostic.

The mean absolute apogee error is reported against the 5% target of the principles above, not gated. A flight outside 5% must carry an explanation that is a checked claim (drag: the flight on the example’s drag is within the target and any on the recorded thrust file is not; thrust: where the example reshapes its thrust file, the flight on the file as recorded is within it and the one on the example’s drag is not), and one inside must not. CI has no refs/, so it holds the committed report to itself (hpr_validate::tests::real_flight_cases_report_apogee_and_trace_rms): the summary to the rows, each percentage to its meters, the words and each flight’s inputs to the code’s, the committed files read to their digests, the page to the data, and each explanation to its numbers; --check flies it again where the checkout is. Today: 6.04% over seven flights, outside the target, five outside 5% (NDRT 2020, Prometheus, Cavour and Genesis consistent with hpr’s drag; Juno III with its motor’s impulse). Four of the seven altimeters’ kinds are assumed barometric; with those four read as heights instead, the mean is 6.63%.

Reference simulators (oracles)

tooluselicensewherenotes
RocketPy 1.13.0 (PyPI, 2026-07-22)primary code-to-code oracle; headless PythonMIThttps://github.com/RocketPy-Team/RocketPyInstall in a uv venv under refs/. Whole flights fly it with upstream PRs #1188 and #1196 applied (validation/oracles/rocketpy/corrections.py, ADR-026 decision record). Acceptance tests to mirror: tests/acceptance/test_{bella_lui,ndrt_2020,prometheus}_rocket.py. Example apogees are in docs/examples/index.rst
OpenRocket 24.12 jarsecond oracle (run only, never read its source)GPL-3.0https://github.com/openrocket/openrocket/releases/download/release-24.12/OpenRocket-24.12.jarNeeds Java 17 exactly: it refuses 21 with “Supported version(s): 17”. brew install openjdk@17 is keg-only, so /usr/libexec/java_home will not list it; refs doctor scans the Homebrew kegs and takes it. How M2.2 milestone drives it is open: JPype directly, or the jar as a subprocess. One probe already runs it through JPype, headless, with empty motor and preset databases bound in place of the graphical ones: validation/oracles/openrocket/automatic_radius.py → validation/fixtures/ork/openrocket-automatic-radius.json, the radius OpenRocket gives an automatic body radius with nothing to take (0.025 m; ADR-054 decision record), held by hpr_io::ork::tests::a_radius_with_nothing_to_take_is_openrockets_default; validation/oracles/openrocket/mass.py → the structure mass, center of mass and inertias of every design OpenRocket opens, which cargo xtask ork holds hpr’s to (M2.2a milestone, ADR-060 decision record; the public record is openrocket-mass-loft-demo.json); and every body radius it settles on in the 17 jar examples and the parachute catalog, which cargo xtask ork holds hpr’s to (67 of 67 agree). The GPL-2.0 orhelper wrapper was dropped from the environment rather than imported. 17 example .ork files are in the jar under datafiles/examples/ (use them locally, don’t commit them; only numbers computed from them, such as the body radii in the ADR-054 decision record fixture, are committed)
RocketSerializer (66d8ca8, after 0.2.0).ork to RocketPy converter; a second reader of the same filesMIThttps://github.com/RocketPy-Team/RocketSerializerPinned, with the environment it runs in, by validation/oracles/rocketserializer/requirements.txt. validation/oracles/rocketserializer/geometry.py calls its extractors one by one on each design and asks OpenRocket 24.12 for the same numbers; cargo xtask ork holds hpr’s nose cone, transitions, fin sets, stations and body radius to both (M3.1d2 milestone, ADR-059 decision record): the current 2026-09-23 survey compares 1,171 numbers over 71 designs, none where hpr is apart from both, and every one of hpr’s also OpenRocket’s. It runs in its own environment, refs/venv-rs, installed with --no-deps so that its orhelper dependency (GPL-2.0) is never installed; the record for Loft’s public demo designs is validation/fixtures/ork/rocketserializer-loft-demo.json, checked in CI by xtask’s rocketserializer_agrees_on_the_loft_demos
RASAero II 1.0.2.0Windows-only freeware; no automationclosedhttps://www.rasaero.com/dl_software_ii.htmUse only the exports that ship with RocketPy data. M1.8a milestone also reads the full Calisto export of RocketPy’s first commit (C_D, C_Nα and CP to Mach 25; rocketpy-calisto-rasaero-2018 in the lock) for the normal force against Mach (ADR-027 decision record). Only Calisto’s (data/rockets/calisto/powerOffDragCurve.csv) is traceable to a RASAero II export; Juno III’s, Cavour’s and Valetudo’s are labelled RASAero but are 3-decimal tables with no input file, and Valetudo’s disagrees with its own OpenRocket export by 44%. M1.5b milestone compares hpr’s subsonic Cd with all four at Mach 0.3 (ADR-009 decision record), and M1.8b2 milestone every 0.05 from Mach 0.1 to 2.0, by band (ADR-029 decision record; results in docs/physics/aero.md)
JSBSimoptional generic 6-DOF cross-checkLGPL-2.1https://github.com/JSBSim-Team/jsbsimlow priority
CamPyRoSdormant; includes Martlet 4 RASAero dataGPL-3.0https://github.com/cuspaceflight/CamPyRoSRun-only if used at all
Missile DATCOMdo not use (ITAR)n/an/an/a

Primary physics sources (download to refs/papers/)

  • Barrowman 1967 thesis (NTRS 20010047838): https://ntrs.nasa.gov/api/citations/20010047838/downloads/20010047838.pdf
  • Barrowman 1966 report: https://www.apogeerockets.com/downloads/barrowman_report.pdf That copy lacks printed pp. 39–50, the worked examples. The complete scan, bound with Barrowman’s Centuri TIR-33 (1970), is https://www.nakka-rocketry.net/articles/Barrowman.NARAM-8.pdf. Its five worked examples (Testbed II, Aerobee 350, Javelin, Recruiter, Arcon-Hi) are level-2 references for CNα and CP: validation/fixtures/aero/barrowman-worked-examples.json, checked within 1% by hpr_aero::tests::barrowman_worked_examples (M1.5a milestone, docs/physics/aero.md). Four pass on hpr’s own model. The Recruiter’s six-fin slopes pass only with TIR-33’s own six-fin rule substituted: with hpr’s rule (MIL-HDBK-762, ADR-008 decision record) they are +3.4% (fins) and +2.9% (total).
  • Niskanen 2009 OpenRocket thesis (CC BY-NC-ND; read for methods only, don’t copy): https://github.com/openrocket/openrocket/releases/download/Development_of_an_Open_Source_model_rocket_simulation-thesis-v20090520/Development_of_an_Open_Source_model_rocket_simulation-thesis-v20090520.pdf
  • OpenRocket technical documentation v13.05 (CC BY-SA): https://github.com/openrocket/openrocket/releases/download/OpenRocket_technical_documentation-v13.05/OpenRocket_technical_documentation-v13.05.pdf
  • Karney’s geodesics (pinned as karney-2013-algorithms-for-geodesics and karney-geodtest): C. F. F. Karney, Algorithms for geodesics, arXiv:1109.4448v2, and his CC0 Test set for geodesics, doi:10.5281/zenodo.32156, 500,000 WGS 84 geodesics; every 500th line and the 21 mirror lines are committed, and validation/reports/geodesics.md holds the whole set’s errors.
  • GeoTIFF elevation (pinned as ogc-19-008r4-geotiff and usgs-3dep-1-n33w107): the OGC GeoTIFF Standard 1.1, and the USGS 3DEP 1-arc-second tile n33w107 (public domain), whose fixtures and whole-tile reading by rasterio 1.5.2 (GDAL 3.12.2), the outside reader, are in crates/hpr-io/tests/fixtures/geotiff/ (validation/oracles/geotiff/dem.py; ADR-128, the reader’s design).
  • ThrustCurve.org’s API (https://www.thrustcurve.org/info/api.html): two public-domain curve files, recorded 2026-10-01, and three stand-in makers’ searches in its shape (15 invented motors, and 282 records carrying only the motor finder’s CC BY 4.0 copy of its figures, with an invented id and file count), in crates/hpr-net/tests/fixtures/replay/; validation/reports/thrustcurve-join.md holds the in-stock motors’ match to the stand-ins (ADR-130, why the match is by name only; ADR-145, why the searches are stand-ins).
  • OpenRocket’s parts catalog (pinned as openrocket-database, Apache-2.0): its 16 .orc files, bundled in crates/hpr-io/data/openrocket-database/, and OpenRocket 24.12’s reading of every part (and of each file with its stated masses removed) and of 37 probe files, in crates/hpr-io/tests/fixtures/orc/openrocket-presets.json (validation/oracles/openrocket/orc_presets.py; ADR-132, the departures and their causes). What OpenRocket builds from each part (its mass, center of mass and the dimensions the file leaves unsaid) and from 6 probe parts is in crates/hpr/tests/fixtures/orc/openrocket-built.json (validation/oracles/openrocket/orc_built.py), which crates/hpr/tests/catalog_openrocket.rs holds the builder’s parts to (ADR-133).
  • US Standard Atmosphere 1976: https://ntrs.nasa.gov/api/citations/19770009539/downloads/19770009539.pdf. Python cross-checks: ambiance (Apache-2.0), pyatmos (MIT).
  • NASA sounding-rocket stability tests: NASA TN D-4013 (NTRS 19670020050, Mach 0.6–1.2) and TN D-4014 (NTRS 19670020031, Mach 1.5–4.63), on half-scale Arcas Robin models. Their plotted normal force, center of pressure and model dimensions are read into validation/fixtures/aero/arcas-robin-wind-tunnel.json with figure and page, the level-3 reference for the normal force through Mach 1, checked by hpr_aero::tests::normal_force_against_mach (M1.8a milestone, ADR-027 decision record). Their forebody axial force (drag without the base), fins on and off (TN D-4013 Figs. 11–12, TN D-4014 Figs. 5–6), read into the same file, is the level-3 reference for the drag through Mach 1, compared in validation/fixtures/aero/drag-vs-mach.json and checked by hpr_aero::tests::drag_against_mach (M1.8b1 milestone, ADR-028 decision record): 8 of 44 rows are within the 10% target set before measuring, hpr reading high (docs/physics/aero.md). Their roll effectiveness (TN D-4014 Fig. 14) is for M1.8c milestone.
  • Stoney, zero-lift drag of bodies of revolution: NASA TR R-100 (1961), NTRS 19630004995. Figure 12’s nose pressure-drag curves at fineness 3 are read into hpr_aero::nose_drag as the model’s own data (Niskanen’s appendix B uses them the same way), not as a reference; its 3:1 cone checks Niskanen’s closed-form cone (M1.8b1 milestone, ADR-028 decision record).
  • Galejs, “Wind instability”: https://www.argoshpr.ch/j3/articles/pdf/sentinel39-galejs.pdf
  • MIL-HDBK-762 (design of aerodynamically stabilized free rockets): https://archive.org/details/MILHDBK762DesignOfAerodynamicallyStabilizedFreeRockets. Its sample drag calculation (Table 5-4, pp. 5-58 to 5-66, for the rocket of Fig. 5-155) is transcribed into validation/fixtures/aero/mil-hdbk-762-sample-drag.json: a whole rocket’s drag, term by term, from Mach 0.5 to 3.2 with every input known. It is a calculation by the handbook’s methods, not a measurement, so it is a code-to-code reference, compared in validation/fixtures/aero/drag-vs-mach.json and checked by hpr_aero::tests::drag_against_mil_hdbk_762_sample (M1.8b2 milestone, ADR-029 decision record): 2 of 12 rows within 10%, hpr reading high (docs/physics/aero.md).
  • Fin flutter: D. J. Martin, NACA TN 4197 (1958), NTRS 19930085030. NTRS serves it with a 436-byte header before %PDF; the lock pins the bytes as served.
  • Parachute inflation: T. W. Knacke, Parachute Recovery Systems Design Manual, NWC TP 6575 (1991), DTIC ADA247666. DTIC refused automated downloads, so the lock uses archive.org’s mirror of the DTIC copy. It is a contractor report: DTIC stamps it for public release, but the title page limits distribution to US Government personnel, so cite it and never redistribute it.
  • Nose cone geometry: G. A. Crowell Sr., The Descriptive Geometry of Nose Cones (1996). Cited but not pinned: the only copy found is a plain-http mirror (servidor.demec.ufpr.br/CFD/bibliografia/aerodinamica/Crowell_1996.pdf), with no license stated. Its curves are checked by closed forms and by validation/oracles/design/shapes.py → validation/fixtures/design/shape-integrals.json (mpmath, 40 digits, 22 noses and transitions): every filled volume, centroid, moment and area agrees to 1e-12 relative. Walls: validation/oracles/design/walls.py → validation/fixtures/design/wall-integrals.json (20 walls, 25 digits): volume, centroid and moments agree to 1e-10 (M1.4a milestone, docs/physics/shapes.md).
  • Material densities: USDA Forest Products Laboratory, Wood Handbook FPL-GTR-190 (2010), pinned as fpl-gtr-190-wood-handbook; manufacturers’ data sheets and military specifications, cited per value with URLs in hpr_design::materials (M1.4a milestone, docs/physics/mass.md).
  • Index of further references: https://wiki.openrocket.info/Resources
  • Not available: there’s no legitimate free copy of Topics in Advanced Model Rocketry. Don’t use pirated copies.

Real flight data

Most of it lives in the RocketPy repo (MIT; its notebooks record each team’s permission), under data/rockets/.

flightfilesnotes
Bella Lui 2020 (EPFL)EPFL_Bella_Lui/bella_lui_flight_data_filtered.csvtime, z, v
NDRT 2020 (Notre Dame)NDRT_2020/ndrt_2020_flight_data.csvaccel in g, altitude in ft AGL. RocketPy sim 1296.77 m vs measured 1316.75 m (the log’s highest reading is 1320.4 m)
Prometheus 2022 (Western Engineering, SA Cup; not Cal Poly)prometheus/*TeleMetrum.csv, *TeleMega.csvraw AltOS CSV with GPS; also our AltOS importer test
Juno III 2023 (Projeto Jupiter, SA Cup)juno3/{cots_altimeter,cots_GNSS,srad_telemetry}.csvRRC3 log
Andromeda, Astra, Erebus (EuRoC 2022)andromeda/, astra/, erebus11/columns ts, filtered_altitude_AGL, filtered_acceleration
Halcyon (Aerospace Team Graz)astg/altimeter_halcyon.csv, astg/gnss_halcyon.csv
Genesis, Cavour (PoliTo), Camões, Lincegenesis/, polito/, camoes/, lince/
Hedy 2025 (TU Wien)hedy/cats_tust/*.csvCATS logs with IMU
Valkyrie 2025valkyrie/flightInfo_merged.csv
Valetudo, Defianceapogee only (no time series)Defiance sim 9238.01 m vs measured 9308.32 m

Matching environments: ERA5 netCDF files for NDRT, Bella Lui, Spaceport America 2018/2023 and EuRoC 2022/2023/2025 are in data/weather/*.nc, and a NASADEM tile is in data/sites/. They make excellent offline test fixtures for the weather-file readers.

Other sources:

  • Altus Metrum AltOS (GPL-2.0 software; format docs at https://altusmetrum.org/AltOS/doc/altusmetrum.html): .eeprom/.telem files and CSV export.
  • DOFPro archive (https://dofpro.org/RCK/fltdata/): PerfectFlite .pf2, Raven .FIPa, AIM .xtra and CSV. No license is stated, so fetch these and don’t commit them.
  • Eggtimer: telemetry spec PDF on eggtimerrocketry.com.
  • Featherweight and PerfectFlite: no official sample files found. Mark them “needs samples”.
  • nrdptel/loft-fixtures (private): 38 files from OpenRocket, RockSim, RASAero, RocketPy and SpaceCAD. The pin used by Loft is commit 37251476e5cfee330c88aa94cf0dd58f93370ccd, with a CHECKSUMS.sha256 manifest whose sha256 is d909aeae6e063b629a161ba36dcc0f8b51e45313997789c8ecbc71e342971510. Many .ork files carry stored OpenRocket simulation results, so they are a free code-to-code reference once the .ork importer exists. Never commit these files (rule 4 in CLAUDE.md).
  • nrdptel/debrief-fixtures (private): flight logs gathered for Debrief, 62 files in its manifest (commit 722f07cd5d9d8595b31c64e2964fb7fbd5e751e7), from about 30 flights of team, certification and sport rockets, recorded by altimeters of a dozen makes. None is a flight of a loft-fixtures design (ADR-083 decision record). Never commit these files (rule 4 in CLAUDE.md).
  • nrdptel/hpr-sim-fixtures (private): 468 real flights gathered for this project, each a design paired with what its source says of that flight; each flight’s notes list its gaps. In the 89 of tier A the source itself ties the design as flown to a numeric barometric log, with the exact motor, date, site and the day’s ERA5 weather. It is pinned at commit 31771d83051644da7ea545ec9dffbf14a175ea8d, with a CHECKSUMS.sha256 manifest of 9,556 files. cargo xtask fixture-flights flies its 83 tier-A .ork flights in hpr and OpenRocket; 55 of them are compared with their logged apogees, and only aggregates are published (report, ADR-184, how; ADR-151, the decision record that adds it). Its files may not be redistributed, so only aggregate statistics are published, crediting the sources collectively; a result derived from its ERA5 files also carries the Copernicus attribution and the ERA5 citation its LICENSING.md gives. Never commit these files (rule 4 in CLAUDE.md).
  • nrdptel/fusionspace-loft (MIT, Neer’s):
    • Eight demo .ork/.rkt/.CDX1 fixtures in fixtures/src/ (as XML).
    • A RocketPy cross-check (fixtures/rocketpy-cross-check.json, scripts/rocketpy/).
    • A candid limitations log (app/docs/limitations/page.tsx).
    • The known importer bugs listed in HANDOFF.md/BACKLOG.md: .CDX1 parts ignore <Location>, .ork auto radii are lost on round trip, and stage-boundary auto-radius resolution is wrong.

Motor data

sourcewhatlicensenotes
ThrustCurve.org API v1 (/api/v1/{metadata,search,download}.json)motor metadata and simfilesspec is ISC; data license per file (PD, free, other, or none; “free” can be GPL)Cache results and give attribution. search.json?maxResults=10000 returns every motor (1156 on 2026-09-17), out-of-production and hybrid ones included, so filter to solids; availability=all is ignored. Of 1712 solid-motor files, 554 are PD, and 196 of those match the stored statistics within 1%. M1.3 milestone bundles 32 (crates/hpr-motor/data/thrustcurve/, validation/oracles/thrustcurve/bundle.py, ADR-005 decision record; survey in docs/research/thrustcurve-data.md)
RASP .eng spechttps://www.thrustcurve.org/info/raspformat.html (thrustcurve-rasp-format)n/aimplicit (0,0) first point; ends at zero thrust. Reader and writer: docs/format/eng.md
RockSim .rse spechttps://www.thrustcurve.org/thirdparty/RockSim%20Engine%20File%20Format.pdfn/aXML; real files disagree with the guide on names and units. Reader and writer: docs/format/rse.md
ThrustCurve.org statistics codesimulate/analyze/analyze.js at commit 577afa6 (thrustcurve3-analyze)ISCvalidation/oracles/thrustcurve/analyze_stats.js runs it unchanged on the bundle → validation/fixtures/motor/thrustcurve-analyze-stats.json; hpr’s impulse, burn window and thrusts agree to 1.8e-15 (M1.3 milestone)
RocketPy SolidMotormass, centers and inertia vs time for BATES grainsMITvalidation/oracles/rocketpy/solid_motor.py → validation/fixtures/motor/rocketpy-solid-motor.json (three bundled curves). Total mass and both inertias agree within 7.9e-5 relative, the center of mass within 5.8e-6 of the motor length; propellant quantities within 1e-4 of their ignition values (M1.3 milestone; scales in docs/physics/motor.md)
RocketPy Rocket with a motortotal mass, center of mass and inertia vs time for nine example rockets (Calisto at two motor positions) and Prometheus’s GenericMotorMIT (notebooks and tests only; Valkyrie’s data-file inputs are left out)validation/oracles/rocketpy/rocket_mass.py → validation/fixtures/design/rocketpy-rocket-mass.json: each example’s own inputs, with the bundled public-domain curve nearest in impulse in place of its thrust file (ADR-007 decision record). hpr’s designs (validation/designs/) agree at RocketPy’s LSODA knots within 8e-10 (grain propellant mass 2.4e-9), and between knots within 1.3e-5 in mass, 3.6e-6 of the length in center and 2.6e-5 in inertia, RocketPy’s resampling; dry values to 2e-16. Four examples whose motors have no dry mass are not cases; Cavour is, for its drag curve, and Genesis and Lince for their logged flights (M2.3b milestone) (M1.4b milestone, M1.5b milestone, docs/physics/design.md)
broofa/thrustcurve-dbJSON snapshot including thrust samplesISC (code)handy offline seed; check the data terms per curve
openrocket/motor-databaseweekly SQLite mirrorGPL-3.0run-only reference; don’t bundle
motor.fusionspace.co API v1live US stock and prices (AeroTech, Cesaroni, Loki)CC BY 4.0, credit “Motor stock data from motor.fusionspace.co”https://motor.fusionspace.co/api/v1/{meta,motors,in-stock,vendors}.json, /motors/{mfr}/{designation}.json (/ becomes ~), /openapi.json. Refreshed hourly, CORS-open, no key. Prices are in integer cents. schema_version is 1. Docs: https://github.com/nrdptel/Hobby-Rocket-Motor-Finder/blob/main/docs/api.md
CertificationcertOrg field in ThrustCurven/ano machine-readable NAR/TRA/CAR lists

Recovery

sourcewhatlicensenotes
RocketPy Flight parachute phasedescent rate, descent time and drift for five example rockets (Calisto, Valetudo, NDRT 2020, Prometheus 2022, Juno III)MITvalidation/oracles/rocketpy/recovery.py → validation/fixtures/recovery/rocketpy-descent.json, replayed by hpr_sim::recovery::tests::descent_matches_rocketpy_examples. Both start from the same declared post-burnout state with the first device open, the same drag areas, triggers and declared wind, RocketPy’s noise zeroed, and (since issue #27) RocketPy’s own gravity model, compared as a vector rather than a magnitude. Agreement: descent time within 0.71%, impact descent rate within 0.03%, drift magnitude within 0.28% in the four cases with wind (Valetudo’s still-air 0.19 m, from the Earth’s rotation alone, −0.89%), the worst single drift component 2.86% (NDRT’s 49 m south of a 327 m drift, where RocketPy’s added mass is 15.9 kg against a 20.8 kg rocket), and the deployment heights of the later devices within 0.17% (RocketPy’s trigger sampling). The oracle runs at rtol = atol = 1e-8; at 1e-6 every compared metric moves by at most 3.5e-6 (its one larger entry, 2.1e-3, is on Valetudo’s 20 µm north drift component, which M1.7a milestone did not compare; M2.1a milestone measures it, and reading 28x high there is what found the gravity-model difference in ADR-015 decision record, issue #27). M1.7a milestone; docs/physics/recovery.md
RocketPy Flight from the padapogee and time to it, maximum velocity, Mach and acceleration, rail-exit velocity, burnout altitude and velocity, and the trajectory, for the same five example rockets and Bella LuiMITvalidation/oracles/rocketpy/flight.py → validation/fixtures/flight/rocketpy-whole-flight.json, the same-drag reference M2.1b2 milestone scores hpr against. The drag is declared by the generator as a constant C_D0 and handed to RocketPy’s power_off_drag and power_on_drag: RocketPy’s own exports carry their own terms and are never committed (ADR-009 decision record), and a Mach curve invented here would be an uncited drag model inside the reference (L18 Loft lesson). Same-drag mode scores the equations of motion, not the aerodynamics. Everything but each example’s rail comes from the mass fixture by way of recovery.py; Bella Lui, added in M2.1b2 milestone because Prometheus could not be scored until M1.8a milestone, declares its site and wind in flight.py itself (its example’s weather is an ERA5 file). Each case records the parachutes it flew, so the harness reads everything from the reference (L75 Loft lesson). Reproducible byte for byte; the loose run at rtol = atol = 1e-6 (RocketPy’s default rtol) moves every metric by at most 3.9e-3. max_time_step is bounded at 0.05 s: without it, thrust(0) = 0 and the generator’s 6000 s max_time let LSODA step over the whole burn and no case leaves the rail (issue #33). Recorded gap until M1.8a milestone: Prometheus peaks at Mach 1.013, which hpr refused until its normal force passed Mach 1 (ADR-027 decision record); since then it flies and is scored, its drifts reported as body lift. max_acceleration is the whole flight’s, which for NDRT and Prometheus is the parachute, so a power-on maximum is recorded beside it. M2.1b1 milestone; scored in M2.1b2 milestone (ADR-021 decision record): five cases pass every scored metric within 3%; Prometheus was a known gap until M1.8a milestone. Since M2.1d3 milestone RocketPy flies with the upstream corrections to its equations (corrections.py, ADR-026 decision record): the largest scored whole-flight difference is +1.783% in height, speed and acceleration and, since body lift took Jorgensen’s size (M1.8e6 milestone, ADR-037 decision record), −1.811% in a drift (Valetudo’s landing), and eight metrics are argued as not scored, five of them drifts in wind that hpr’s body lift and rail release account for (wind_response.py); M1.8a milestone adds Prometheus’s two drifts and its main opening, eleven in all. The trajectory is the series, 120 rows of time since ignition, height and speed of the center of dry mass, which M2.1d1 milestone compares (ADR-024 decision record): hpr’s height and speed at the same times, from the shared ignition clock with no fitted shift, until hpr lands, as a root mean square held to 3% of the reference’s apogee and max speed. All eighteen same-drag RMS pass (height 0.09 to 35.4 m, speed 0.02 to 1.55 m/s, since ADR-037 decision record; the largest are Prometheus’s)
Knacke’s canopy tablesdrag coefficients on the nominal area, canopy fill constants, drag-area growth exponents and opening-force coefficientsno clear terms: cited, never redistributedtranscribed into hpr_sim::recovery::CanopyType with the printed page at each accessor, and pinned by hpr_sim::recovery::tests::default_canopy_cd_carries_its_citation (which also fixes hpr’s default C_D0 as the middle of each printed range)

Streamers and tumble

This section covers the M1.7b streamer-and-tumble milestone.

sourcewhatlicensenotes
C. Kidwell, Streamer Duration Optimization, NAR R&D, NARAM-43 (2001)free-drop descent rates and per-streamer masses for sixteen 4 in × 40 in streamers over 20.1 mno terms stated: cited, never redistributedthe measurement both streamer models are checked against, recomputed in hpr_sim::recovery::tests::streamer_models_against_kidwells_drop_tests with his normalisation to a notional 5 g weight and his distance-over-time rates compared against the same average from the closed-form fall. His unpleated crêpe streamer descends at 2.80 m/s, a C_D of 0.155 on the planform area: Carruthers and Filippone’s correlation gives +9%, the OpenRocket technical documentation’s appendix C +88%. His pleated streamers descend slower than either model, and hpr models no pleats
J. Carruthers and A. Filippone, J. Aircraft 42(4), 2005wind-tunnel drag of cotton streamers clamped at the luff, AR 3.3 to 30, 6 to 18.9 m/spaywalled; the authors’ post-print states no terms: cited, never redistributedhpr’s default streamer model: all three of its fitted curves, eq. 1 (0.405 AR^−0.494 at 0.075 m²), eq. 2 (0.561 AR^−0.480 at 0.025 m²) and the trend line printed on Figure 3 (0.6514 AR^−0.6075 at 0.05 m², which the text does not repeat as an equation), pinned by streamer_models_reproduce_their_printed_equations. hpr interpolates between neighbours in ln S and holds the end curve outside; blending only the two equations reads 18% low at AR = 3.3
OpenRocket technical documentation v13.05appendix C’s streamer correlation, §3.5’s tumbling model with its fin efficiency table, and Table 3.3’s five drop-test modelsCC BY-SAall transcribed with their printed pages and pinned by tests. Replaying Table 3.3 through hpr’s reading of §3.5 (the_tumble_model_against_its_own_drop_tests) gives −5.8%, −5.4%, −7.2%, +19.0% and −10.0%, not the 3 to 14% the documentation claims for its own fit: the finless tube wants a body coefficient near 0.79 where the model prints 0.56, and the text pins neither area convention. The fit covers 6.8 to 160 g at 5 to 6.6 m/s, so a high-power body tumbling is an extrapolation (docs/physics/recovery.md)

Design formats

formatspec statusnotes
OpenRocket .orkzip containing rocket.ork XML (or gz, or raw XML). No XSD.Docs: https://openrocket.readthedocs.io/en/latest/dev_guide/file_specification.html and fileformat.txt. Schema 1.9 = OR 23.09; 1.10 = 24.12; 1.11 (26.xx, documented) adds embedded .rse, CSV lookup tables, gravity model and preview.png. Build the importer from the docs and sample files, not from OR’s Java
OpenRocket .orcparts DB XMLopenrocket/openrocket-database is Apache-2.0. It can be bundled with notices (Loft did this; 3,445 parts)
RockSim .rktXMLRockSim ships RockSim_Xml_Doc.txt. PWrInSpace/rkt_format (MIT) is a readable parser
RASAero .CDX1XML, no public specWork from sample files only (clean room)
RocketPy .rpyJSON tied to Python class signaturesrocketpy/utilities.py, _encoders.py (MIT)
Open Rocket Document (.ord)dead since 2016GPL; reference the idea only

Environment data

sourcenotes
Open-MeteoData CC BY 4.0 (attribution required). Free non-commercial tier: <10k calls/day. Pressure-level winds (for example wind_speed_850hPa) come from the forecast and historical-forecast APIs; the ERA5 archive API has no pressure levels. There’s an elevation API. The server can be self-hosted
NOAA GFS / RAPopen data; AWS noaa-gfs-bdp-pds, NOMADS grib filter, UCAR THREDDS (RocketPy uses these)
ERA5 pressure levelsCC-BY; needs a CDS account (a “Needs Neer” item if we want live access); reading user-provided .nc files offline is the priority
U. Wyoming soundingshttps://weather.uwyo.edu/wsgi/sounding?datetime=YYYY-MM-DD%20HH:00:00&id=<stn>&type=TEXT:CSV&src=FM35 (or src=BUFR); read by hpr_net::wyoming (ADR-120, how soundings are read). The site states no terms (checked 2026-09-30); only U.S. stations’ soundings, U.S. government works, are committed as fixtures. The legacy interface is retired
WMM2025public domain; valid to the end of 2029 (magnetic declination for headings)
Copernicus DEM GLO-30/90, NASADEMfree; COG on S3; for terrain and landing elevation
GravitySomigliana/WGS84 formula (what RocketPy uses); EGM2008 is optional later

Rust crates checked 2026-09-16 (use as a starting point; re-check before adding)

  • Mature and active:
    • Math and numerics: glam 0.33, nalgebra 0.35, faer 0.24, rayon 1.12.
    • Serialization and files: serde, schemars 1.2, quick-xml 0.42, roxmltree 0.21, zip 8.6, csv 1.4.
    • Bindings: pyo3 0.29 + maturin 1.15, wasm-bindgen 0.2.128 + wasm-pack 0.15, uniffi 0.32.
    • UI candidates: tauri 2.11, bevy 0.19, wgpu 30.
    • Testing: proptest 1.11, insta 1.48, criterion 0.8.
    • Networking and storage: reqwest 0.13, ureq 3.4, rusqlite 0.40.
    • Geo and data: geo 0.33, world_magnetic_model, polars 0.55, arrow/parquet 60.
  • ODE and optimization:
    • diffsol 0.16 and ode_solvers 0.6: cross-checks only.
    • argmin 0.11 and egobox 0.37.
    • cmaes 0.2.2 has low activity.
  • Weather files: grib 0.18 (GRIB2, pure Rust) is still a candidate. netCDF classic is read by hpr’s own reader from Unidata’s spec (ADR-081 decision record); netCDF-4 (HDF5) is converted, not read.
  • Avoid: serde_yaml (deprecated), serde_yml (unmaintained), hdf5 (abandoned; use hdf5-metno if needed), nav-types (stale), slint (GPL/commercial).
  • Gaps: no Rust crate exists for OpenRocket, ThrustCurve or model-rocket simulation, and ISA crates are tiny. Write USSA76 in-house.