Start here
hpr-sim is a flight simulator for hobby and high-power rockets, and it is not ready to rely on yet. This site explains what it models, where each model comes from, and how well each one has been checked. It is written for a rocketeer who knows some physics and some code, but not this project. This page covers what hpr-sim is, what works today, what doesn’t yet, how far to trust it, and how to read the other pages.
Every number hpr-sim produces is an estimate from a model, not a measurement, and never a go/no-go verdict. Your motor’s printed data and your range safety officer are authoritative.
What hpr-sim is
hpr-sim simulates a rocket’s flight from the launch rail to landing. It is a 6-DOF simulator: it follows all six degrees of freedom, the rocket’s position along three axes and its rotation about three. Meanwhile the motor burns, and the rocket’s mass, center of gravity and inertia (how hard it is to turn) change.
It is a Rust library first, meant to be built into other programs, as
RocketPy is. RocketPy is an open-source rocket flight simulator, written in
Python and used as a Python library. hpr-sim has a Python package too (Python), and a
graphical app is planned.
OpenRocket .ork design files are read today. The command-line tool, hpr, flies a .ork or a
rocket’s JSON, looks up and converts motor files, searches vendors’ motor stock and prices,
re-runs the validation, reads an altimeter’s flight log, and fetches a launch day’s weather
(The command line).
It is also built to be checked. Every model cites a published source, and tests pin every model. The simulator is compared against RocketPy, OpenRocket and the logs of real flights, with the results committed to the repository.
For now it covers only commercial off-the-shelf solid rocket motors (COTS motors), the kind you buy from a manufacturer. That stays true until the first app.
Nothing has been released yet. Release 0.1.0’s files are built and checked but not published; until they are, you build it from source, and once they are, they install as Install a release says. What a release holds, and how each of its files is checked, is in How a release is built; the changelog says what 0.1.0 does, how far to trust it, and its known gaps, the open issues that make a number look safer than it is among them. Releases come at product boundaries, and no dates are promised (Phase 8, releases); a dated estimate for each is in Plan and timeline:
- 0.1, the simulator: the library, the command line and the Python package, with Monte Carlo dispersion from the command line and Python. It waits on M2.3c1, the logged flights’ apogees and named fixes (release 0.1). It ships with its known limits listed, among them drag that reads high above Mach 0.8, so apogee there reads low.
- 0.2, the flight analyzer: the common altimeters’ logs read on their own, with no design file, then compared with its simulation and the rocket’s drag fitted from it.
- 0.3, accuracy and diagnosis: the simulator measured against logged flights and improved, and a flight’s likely faults ranked from its log.
- 0.4, the competition kit: rules presets for Student Launch, IREC and others, an optimizer, the numbers a submission asks for, ejection-charge sizing, and RASAero and RocketPy files.
- 0.5, an app preview: open a design or a log, fly it and replay it in 3D, with the simulated flight drawn as a “ghost” beside the real one, on the desktop and in a web browser.
- 1.0, the app, with its design editor.
After 1.0, new product lines come one at a time, in an order that may still change: designing your own solid motors, then parachutes, then a pad-day mode on your phone (the day’s forecast, the best launch hour, drift on the field, a flight card), then flight computers and trackers. Hybrids and liquids come last (the second-pass plan).
What works today
These parts are built and tested. Each page gives its sources, and most say what they leave out. The pages, the program’s messages and the library’s names use US spelling for the eight words the style guide checks. The saved forms that held a British-spelled key (a landing ellipse, a design-check finding, a laid-out rocket and a few more) still read the old key and write the new one, so an older build may not read what this one saves; design files never held one and are unchanged (M0.6e, US names).
| part | what it does | pages |
|---|---|---|
| Earth | Gravity from WGS 84 (the model of the Earth’s shape and gravity that GPS uses), varying with latitude and height; the Earth’s rotation; launch-site coordinates, the distance and bearing between two places, and a site’s elevation looked up online or read from a GeoTIFF file | Frames, Geodesy, Gravity, A launch site’s elevation |
| Air | The 1976 US Standard Atmosphere up to 86 km, with temperature offsets, humidity, and soundings (measured or forecast profiles of pressure, temperature and wind against height), including the weather of a real day from an ERA5 file, an Open-Meteo forecast, a weather balloon or NOAA’s GFS and RAP forecasts | Atmosphere, ERA5 weather files, Launch-day weather, Weather-balloon soundings, NOAA forecasts: GFS and RAP |
| Wind | Constant, layered, power-law and logarithmic wind profiles; turbulence (random gusts) from a random-number generator started from a seed, a number you choose: the same seed gives exactly the same gusts every time on the same platform (operating system and processor) | Wind, Turbulence |
| Motors | Reads .eng and .rse thrust curves; thrust, mass, center of gravity and inertia through the burn; 32 bundled motors; a motor the catalog lacks, fetched from ThrustCurve.org by name and cached, so it flies offline after (M4.5b, motors on demand); motors in stock and their prices, from motor.fusionspace.co | Solid motors, .eng files, .rse files, Motor stock and prices |
| Rocket | Nose cones, body tubes, transitions (tapered sections between tubes of different diameters), fins and other parts, their materials, the whole rocket’s mass properties, and design checks. Everyday fits warn and only what can’t be built is refused (M4.5c, design checks that match reality); a packed part, such as a payload or a parachute, drawn wider than its bore warns, as its width sets only its own inertia (M4.5l, a packed part wider than its bore); a part inside a nose cone or transition is checked against the room where it sits, and warns if it fits at the wide end but meets the wall where the cone narrows (M10.1a, the flight and design fixes) | Design tree, Shapes, Mass properties |
| Aerodynamics | The center of pressure (where the aerodynamic force acts; its distance behind the center of gravity is the stability margin), the normal force (the sideways force when the rocket flies at an angle to the airflow, its angle of attack) and drag. For small angles of attack: the normal force and the drag from Mach 0 to 5, both checked against a wind tunnel from 0.6 to 4.63 (the drag reads high at most speeds). A part’s or stage’s own drag coefficient, stated in a .ork file, flies in place of its shape’s drag as OpenRocket flies it, checked on 25 OpenRocket probe designs; OpenRocket’s Base drag hack example, which relies on it, flies within 4.52% of OpenRocket’s apogee on two motors and 7.80% high on the third, where hpr gives its very blunt nose a value read between two measured heads and OpenRocket more, so the 5% bar is not met there (M4.5h, a part’s drag override; M4.5o, a blunt nose’s measured drag; A part’s stated drag coefficient) | Aerodynamics |
| Flight | The launch rail, powered flight and coast to apogee, with an adaptive time step and events such as burnout and apogee | Rigid-body flight, Time integration |
| Recovery | Parachutes, streamers and tumbling, the drift they carry the rocket downwind, and a rocket that separates into bodies that each descend on their own. A .ork file’s parachutes and streamers fly as OpenRocket flies them, in hpr sim too, within 0.02% of OpenRocket’s landing speed on 50 of its 53 example flights and within 0.12% on all 53 (M4.5a, a .ork’s recovery flown). hpr sim flies a .ork’s powered separation, each part under its own devices; the dropped booster’s landing is rough (M4.5g1, powered separation in hpr sim). Several powered separations fly in turn: OpenRocket’s three-stage example flies as saved, all three configurations within 1.48% of OpenRocket’s apogee and 1.64% of its largest speed on OpenRocket’s own curves, and within 2.48% and 1.67% in hpr sim on the curves it fetches (M4.5g2, several separations). A payload dropped with nothing left to burn flies when its own parachute opens at the split: OpenRocket’s two payload examples fly, the payload’s apogee within 1.1% of OpenRocket’s (M4.5g3, a payload’s split) | Recovery |
| Design files | Opens an OpenRocket .ork file (zip, gzip or plain XML) and reads the whole design: the stages and body components, the tubes, rings, fins, lugs and recovery gear on and inside them, the motor configurations, when parachutes open, and the simulations OpenRocket stored. The airframe’s shape is cross-checked against a second reader and OpenRocket itself (positions against OpenRocket alone; mass and center of gravity in Mass properties). Pods are read, weighed and flown, checked against OpenRocket on five small probe designs and on OpenRocket’s Pods–airframes and winglets (Pods), and motors in more than one pod set fly, as do rail buttons’ screw heads, weighed: OpenRocket’s Pods–powered with recovery deployment flies, 0.36% below OpenRocket’s apogee with only the sustainer’s motor and 1.86% below with only the booster’s motors, and in the conditions of OpenRocket’s record its staged flight turns over before apogee in both programs: its site is at 28.61° N, where the Earth’s rotation tips hpr’s flight, and hpr’s angle of attack passes 90° at 2.25 s (M4.5i, Pods–powered flies); its first configuration, whose booster burns out first in its main tube and drops its pods still burning, flies too, checked against OpenRocket’s flight at two points up to where OpenRocket aborts it, not at an apogee: within 5% in height and speed at the separation and at OpenRocket’s last row, weaker evidence than an apogee (M4.5n, a stage’s first burnout), a parallel stage (boosters strapped beside the core) is read, weighed and flown on a rocket of one stage on the axis, and dropped at its own separation (OpenRocket’s Parallel booster staging flies both its configurations within 1.3% of OpenRocket’s apogee; M4.5m, parallel stages), fins on a nose cone or a transition are read with their root along its surface, as OpenRocket draws it (OpenRocket’s Pods–airframes and winglets flies, each apogee within 0.5% of OpenRocket’s, its stability margin 0.07 calibres above OpenRocket’s, the flattering side; M4.5g4, fins on a nose cone), a part hpr cannot shape honestly is left out with a warning, and a configuration flies only when every motor in it has a thrust curve and lights at a moment hpr can fly: hpr sim flies 136 of the 170 motor configurations in the reference library and OpenRocket’s examples as saved, 54 of OpenRocket’s 56 among them, and 9 more with --accept-design-errors; 25 are refused, mostly for a motor with no curve (How many configurations hpr sim flies; M4.5j, the corpus count). A design is written back out as a .ork that reads back as the same design (writing a .ork), and hpr convert or a Rust program can keep it as a .hpr, hpr’s own format, and write the .ork back from that. TypeScript and Python programs can read a .hpr with generated types (The hpr design format). OpenRocket’s parts catalog, the 16 .orc files it ships with 3,449 makers’ parts, is built in and read as OpenRocket reads it; a program can look a part up, and the builder makes a rocket of the parts, each weighing what OpenRocket weighs it at but for a few departures the builder’s page names (Parts from a catalog, .orc parts catalogs) | .ork design files, The hpr design format, .orc parts catalogs |
| Flight logs | Reads a PerfectFlite altimeter’s .pf2 log on its own, with no design file, and takes liftoff, apogee, the top speed, landing and the descent from it, each saying where it came from or why the log can’t support it | Reading a flight log, Flight-log readings, .pf2 files |
| Monte Carlo | One rocket flown many times with its mass, drag, motor, wind, rail and recovery delays scattered, seeded so a run repeats exactly; the spread of the apogee, the landing or any number a flight reports, with failed flights counted. From Rust or hpr mc, staged flights included; every flight exported as CSV, or, for a design without staging, returned to Python as NumPy arrays, the same numbers to the bit with both built in release mode | Monte Carlo dispersion, hpr mc |
| Optimization | Three optimizers search for the design that makes a result best. CMA-ES tunes a design’s numbers (a ballast mass, a body length) and picks from choices such as a motor or a catalog nose cone, within limits such as a minimum stability margin. NSGA-II weighs two goals against each other, such as apogee against stability. EGO, efficient global optimization, is for models so slow that only tens of evaluations can be afforded. Each is held to test functions whose answers are known, and CMA-ES and NSGA-II also to outside implementations. On a rocket, an answer is only as good as hpr’s flight models. From Rust only; EGO is checked on two, three and six variables, and competition rule files are not built yet | Optimization |
| Python | The hpr package: the builder’s rocket, motor, site and flight from Python, with the recording as NumPy arrays and a design read from a file. A Monte Carlo run, its flights as NumPy arrays. Built from source, not on PyPI; no staging; a design read from a file flies its stored parachutes with recovery=True, and most .ork configurations are refused as they don’t fly as written | Python |
What doesn’t work yet
These are the gaps you are most likely to meet. Each model page lists what its own model leaves out.
Speed and angle of attack
- Drag near and past Mach 1 is lightly checked. Since
M1.8b1 (drag through Mach 1), a flight on hpr’s own drag flies
from Mach 0 to 5, and one that reaches Mach 5, the top of the models, stops with an error.
- Near and above the speed of sound, the transonic and supersonic speeds, the drag is Niskanen’s 2009 method, which is semi-empirical: formulas fitted to measurements.
- Against NASA’s wind-tunnel tests of the Arcas Robin sounding rocket, from Mach 0.6 to 4.63, it reads high at most speeds, most of all with fins past Mach 1 (Aerodynamics).
- So for a rocket that goes past Mach 1, expect hpr’s apogee to come out low rather than high. Its base drag, on the flat aft end, hasn’t been checked faster than Mach 0.3 at all.
- It has been compared with RASAero II’s drag near Mach 1; the comparison is not uniformly within the 10% target. See Accuracy for the cases and errors. A drag table from another tool can replace hpr’s own drag (Aerodynamics).
- The normal force still has gaps near Mach 1 and on some supersonic bodies. The current body and Arcas Robin comparisons, including their measured error ranges, are in Aerodynamics and Accuracy.
- Small angles of attack only. Nothing models stall, the loss of lift at a large angle of attack, yet a flight uses the same models at every angle. So results near rail exit in a strong crosswind, and near apogee, are the least trustworthy.
Staging, two-stage rockets, clusters and air starts
- Staging is new, and checked against OpenRocket on its own examples. Each motor lights at its
own time, an air start included, and a sustainer
flies on after dropping its booster, which lands on its own
(Staging). Tests check the bookkeeping. OpenRocket’s two-stage and
three-stage examples, its cluster example and its air-start example, 15 flights in all, are
each within 5% of OpenRocket’s apogee and largest speed. Five apogees, three of the cluster’s
and two of the three-stage example’s, are compared with OpenRocket’s flight with no parachute,
since its parachute opened before apogee
(M1.9c, a two-stage and a cluster design against OpenRocket;
M4.5g2, several separations;
results).
- A
.orkfile’s ignition settings and every powered separation are read, from the tail forward (M4.5g2, several separations). A powered separation has a time known before the flight, and then a motor ahead of it is still burning or yet to light and none behind it is. One at its own stage’s first burnout may drop the stage’s own other motors still burning, if they lit strictly before the split, as OpenRocket does (M4.5n, a stage’s first burnout). The ignitions come with the rocket. Your program passes the separation to the flight, with a recovery device on each part, since hpr refuses the flight without them;hpr simdoes the same for you (Separation). The exampleork_two_stage.rsdoes both. A lone separation with nothing ahead of it left to burn, such as a payload’s, is read too;hpr simflies it when the payload’s own device opens at the split (M4.5g3, a payload’s split). One beside another separation, or at or after apogee beside another, isn’t flown (which configurations fly). - A cluster, several motors burning side by side, flies, whether they
share one mount of several tubes or each has its own: hpr adds up their thrust and mass, and a
motor off the rocket’s center line adds a turning moment. A motor that fails to light can be
set, and the rocket then turns as a hand calculation says (Clusters).
A
.orkfile’s cluster flies with a motor in every tube. - A rocket can separate into parts for recovery once the aft part’s motors have burnt out.
- A
Effects left out
- Some effects are left out of a flight, or approximated:
- tip-off (the rocket pitching as it leaves the rail), thrust misalignment (a motor pushing slightly off the rocket’s axis), and turbulence (the model exists, but a flight doesn’t use it), none of which a milestone plans yet (issue #39 tracks turbulence);
- the shock load when a parachute opens, and the canopy’s overshoot of its steady drag (Recovery);
- the internal momentum of the burning propellant, which is counted twice, as RocketPy counts it: a thrust curve measured on a test stand already includes its effect, and the equations of motion add it again. On Valetudo, the rocket that Getting started flies, it adds 21 N to the push at liftoff and changes the burnout speed by at most 0.05 m/s (Rigid-body flight).
Outputs and ways to use it
- Output files. Exporting a flight writes a recording as CSV, JSON or Parquet (M1.10c2), and the flight path with its landings as GeoJSON or KML for a map. A flight’s peaks, its stability margin from the rail exit to apogee (the weakest direction’s, for a rocket with a fin set of one or two fins; #329), the optimum ejection delay and its landing’s latitude and longitude are in Flight metrics, and its fins’ flutter speed and margin in Fin flutter.
- The command line flies powered separations only.
hpr simflies a.orkor hpr design file, exports its recording (The command line), and with--plotdraws its altitude, speed and acceleration against time, events marked and listed in a table, as an SVG in the FusionSpace chart style (Plotting the flight; M4.5e, plots; M0.6c, their style). hpr simreads by name. It leads with the static margin, apogee, rail exit speed, ejection delay and the descent speed under open parachutes, and calls configurations and parts by name, such as[C6-5]for a configuration the file leaves unnamed (M4.5d, readable output). It flies a.orkfile’s parachutes and streamers, and its powered separations, one or several (M4.5g1, powered separation inhpr sim; M4.5g2, several separations), and a payload dropped with nothing left to burn, when its own parachute opens at the split (M4.5g3, a payload’s split). A rocket’s.jsoncarries no parachute. A Rust program flies separations and parachutes: Getting started flies a first rocket, and The builder builds and flies one of your own in a few calls. A Python program flies the parachutes of a rocket it builds part by part, or a design file’s withrecovery=True, but no staging (Python)..orkfiles are read, but most don’t fly with their own motors. A.orkfile is read (.orkdesign files), but few of its motor configurations fly as written, as hpr has few motors’ curves. Its parachutes and streamers fly as OpenRocket flies them (Recovery).hpr sim --motorflies one with a motor you give (The command line).- Monte Carlo runs give the apogee’s spread and landing ellipses, from Rust, the command
line or Python. A Monte Carlo run flies a rocket many times with its mass,
drag, motor, wind and rail scattered, seeded and reproducible
(Monte Carlo dispersion), and draws the ellipse its landings fall in
(Landing ellipses).
hpr mcruns one on any designhpr simflies and exports every flight as CSV; for a design without staging,hpr.MonteCarloreturns the same flights to Python as NumPy arrays (Python), but not the ellipses. Morris screening and Sobol’ indices rank which inputs matter most (Sensitivity analysis). CMA-ES finds the values of a design’s numbers that hit a target, such as the ballast and body length for a 3,048 m apogee with a chosen margin. CMA-ES also chooses a motor and a nose cone within a competition’s limits, and NSGA-II weighs apogee against stability (Optimization). Their answers are only as good as hpr’s flight models. Competition rule files are not built yet. No app yet: it is on the roadmap. - The flight-log analyzer reads one logger so far.
hpr analyzereads a PerfectFlite altimeter’s.pf2log on its own, with no design file and no simulation, and prints liftoff, apogee, the top speed, landing and the descent, each saying where it came from, or withheld with the reason the log can’t support it (Reading a flight log). The other loggers come with M7.1: Altus Metrum (AltOS), Featherweight (Raven, Blue Raven and the GPS tracker), Missile Works RRC3, Eggtimer, Entacore AIM, Mercury/AltimeterCloud, CATS, and plain CSV with column mapping. The rest of the readings, such as burnout and each leg of the descent, come with M7.2, and comparing a flight with a simulation of it with M7.3.
How far to trust it
Accuracy gathers every result so far, gaps included. The accuracy work aims first at the flights most rocketeers make, the core band: up to Mach 2.5. It assumes angles of attack of 15° or less. The wider envelope runs to Mach 3.5, at any angle (the operating envelope). Whole flights are checked mostly below about Mach 1.15 so far. Faster flights still fly, and say so: a flight beyond the validated range, at high angle of attack, outside the core band or beyond the envelope carries a flag (M1.14a1, the envelope flags), and a flight that meets a known error in hpr’s drag or stability warns by its issue number (M1.14a2, the drag issue warnings; M1.14a3, the stability ones; M10.1d2, four more and a flight unstable under power). A flight whose static margin falls below zero while a motor burns says its apogee is not a prediction. In brief:
-
Whole flights match RocketPy’s in height, speed and time when both codes fly the same drag. Six of RocketPy’s example rockets agree within 3% on apogee, speeds, burnout and flight time (M2.1b2, the whole-flight comparison). So does where they go, except for rockets that leave the rail slowly in a wind. There hpr’s body lift, which RocketPy leaves out, its later release from the rail and, for one rocket, its simpler fin model put the apogee’s drift −4.333% to −38.158% from RocketPy’s, and the landing’s −21.686% to +36.678% (report; Accuracy). With hpr’s own drag, against RocketPy flying the drag its examples ship, hpr’s heights differ from RocketPy’s by −7.280% to +10.302% (report). The larger gaps are where the two drags differ most: hpr’s is well below the example’s for two rockets, and above it at high speed for Prometheus 2022, which flies through Mach 1 (Accuracy).
-
Against seven real flights, hpr’s apogees miss by 6.04% on average, outside the 5% target, and by −8.90% to +10.40% one by one. hpr’s height is read the way each log’s barometric altimeter reads the air (four of the seven assumed barometric); drift and speed are not compared yet (real flights, report).
-
Against 55 more real flights, from a private collection, hpr and OpenRocket both over-predict the logged apogee, hpr by 9.83% on average and OpenRocket by 9.00%, while agreeing with each other within 5% on 53 of them; neither meets the 5% target. The flights’ owners allow only aggregate numbers (private collection, report).
-
The descent under a parachute matches RocketPy’s. The comparison flies the descents of five of RocketPy’s example rockets in both codes:
- Each starts from the same state near apogee, with the first parachute opening at once.
- Both codes get the same drag areas and wind.
- RocketPy’s parachutes can add random noise, which would make each run differ; it is switched off.
- hpr uses RocketPy’s formula for gravity, and RocketPy’s way of interpolating the wind, that is, of working out the wind between the heights it is given.
Six numbers are compared for each rocket: the descent time, the mean descent rate, the descent rate at landing, and the drift in total, to the east and to the north. All 30 agree with RocketPy’s within 3%; the largest difference is +2.865%. That shows the two codes agree on the descent physics, not that either matches a real flight. The committed validation report has every number, and Recovery explains the comparison.
-
Each model is tested on its own: against exact answers, and where its source prints tables or worked examples, against those; several parts also against RocketPy. Each test states its tolerance. The largest known gaps:
- The drag was checked at Mach 0.3 against other programs’ curves (below), and from Mach 0.6 to 4.63 against NASA’s Arcas Robin wind tunnel. There it reads high at most speeds, most of all with fins past Mach 1: 2 of 44 measurements are within the 10% target set before measuring (Aerodynamics). The normal force and center of pressure were checked at Mach 0 against Barrowman’s worked examples and from Mach 0.6 to 4.63 against the same wind tunnel, where they miss between Mach 0.8 and 1.2, and past Mach 3, the current comparison and its measured range are reported in Aerodynamics.
- The drag was compared with drag curves that come with RocketPy’s example rockets, labelled as RASAero II’s (another rocket aerodynamics program). The curves don’t record the fins’ edges or the surface finish, so hpr’s copies of the designs follow a declared guess. hpr is within 10% in four of the seven cases.
- hpr’s drag is 18% low for Cavour, another of RocketPy’s examples, while its motor burns (power-on drag); the cause is not known yet.
- hpr’s drag is 47% to 50% below Valetudo’s example curve. Valetudo is the rocket that Getting started flies. Its references disagree with each other, though: at Mach 0.3 the example curve gives a drag coefficient of 1.05, 1.44 times the 0.728 in an OpenRocket file of the same rocket. Given that OpenRocket file’s own surface finish and launch lugs, hpr gives 0.714, 1.9% under the file’s 0.728 (Aerodynamics).
- The Recruiter is a six-fin model rocket that J. S. Barrowman, whose method hpr follows for the normal force, works through in his 1970 report Centuri TIR-33. hpr’s normal-force slope for it, how fast the sideways force grows with angle of attack, is 2.87% above his printed value, and 3.42% on the fins alone. Most of that comes from a different rule for six fins (Aerodynamics).
- Tumbling drag is −10 to +19% off its source’s own drop tests. A separated body’s parachute can open at a higher speed than it would for real, because the body falls with no drag until then (Recovery).
-
You can check it yourself. Getting started says how: run a program that shows how much the drag moves the apogee, trace any number with Checking a claim, or compare hpr’s apogee by hand with your own altimeter’s or another simulator’s.
Reading these pages
New here? Getting started builds hpr-sim and flies a first rocket, and How a flight is simulated follows a flight from the pad to the ground, linking the page for each model on the way.
Have an OpenRocket design? Three how-to guides take it through hpr on the command line:
Fly your .ork, Pick a motor and
Check stability for a certification flight.
Each model page opens with In short: what it models, its sources, how well it is validated and what it leaves out. Below that, it names the code that implements the model, the sources it follows and the tests that pin it.
Sources are cited by a short key in square brackets, such as [N09] for Niskanen’s 2009 thesis on model rocket simulation, with the full reference near the top of the page.
Equations are written in plain text, so they read the same here, on GitHub and in the code’s
documentation. For example, once its drag balances its weight, a rocket under a parachute falls
at the steady speed v_e = √(2 m g / (ρ C_D S)), where m is its mass, g gravity, ρ the air’s density and
C_D S the parachute’s drag area
(Recovery).
Terms are defined in the Glossary, and Checking a claim shows how to trace any number to its source, its test and its validation.
The code itself is documented in the API reference, which Rust’s documentation tool generates from the source. It lists every public type and function, and each crate’s front page links back to the pages here that explain its models.
Three kinds of label link to the project’s records on GitHub, which Decisions and the roadmap introduces:
- A milestone, such as M1.8, is a step of the roadmap, the ordered plan of work.
- A decision record, such as ADR-011, explains a significant choice and the alternatives that were considered. All of them are in the decision log.
- A Loft lesson, such as Loft lesson L15, is a mistake found in Loft, the project that came before hpr-sim. A test here guards against it, or will once its milestone ships. They are listed in Lessons from Loft.
Every page’s source is a Markdown file in the repository’s
docs/ folder. The pencil icon at the top of
a page opens its source in GitHub’s editor, where you can propose a fix.
Title block
This page ends with a title block, the box in the corner of an engineering drawing that says what
a document is, who issued it, when, and which sources it rests on. It describes the whole site and
this version of hpr-sim. FusionSpace, the family of tools hpr-sim belongs to, closes its pages
this way; FS · SW · TOOL 005 is hpr-sim’s number in that family’s register.
| Field | Entry |
|---|---|
| Owner | FusionSpace |
| Title | hpr-sim: a flight simulator for hobby and high-power rockets, and its documentation |
| Designation | FS · SW · TOOL 005 |
| Version | 0.1.0, not yet released |
| Date of issue | 2026-10-07 |
| Status | IN PREPARATION: no release yet; how far to trust each result is above |
| Units | SI (meters, kilograms, seconds); hpr sim’s summary adds feet and feet per second in parentheses after lengths and speeds |
| Data | Thrust curves: ThrustCurve.org’s public-domain files, catalog captured 2026-09-17. Atmosphere: U.S. Standard Atmosphere 1976, or a sounding or forecast you supply or fetch (weather). Magnetic field: WMM2025. Accuracy: the committed validation report. Every source and its terms: third-party notices. |
| Fonts | Archivo and Cascadia Mono, SIL Open Font License 1.1, served from this site; no page asks another server for anything |
Getting started
This page takes you from nothing to a first simulated flight: install Rust, build hpr-sim, and run a short program that flies a rocket from the launch rail to the ground and prints what happened. Then it walks through that program, so you can change it and fly your own variations. It needs some Rust, but no knowledge of this project.
The numbers this example prints are not validated. hpr’s whole flights match RocketPy’s in height, speed and time when both codes are given the same drag, and in where they go, except for rockets that leave the rail slowly in a wind (see How far to trust it). With its own drag, hpr flies this rocket about 10% higher than RocketPy does on the drag table RocketPy’s example ships (770 m against 700 m in the validation case); which drag is closer to the truth is open. This rocket left no flight log, so it can’t be checked against its real flight; seven other rockets have been, and hpr’s apogees missed theirs by 6.04% on average, outside the 5% target (real flights). Against 55 more, from a private collection, hpr’s apogees were +9.83% above the logs on average, and OpenRocket’s +9.00% (private collection). How far to trust it below says what that means for this one.
Install a release
Release 0.1.0 is built and checked, but not published yet. Until it is, build hpr-sim from the repository, as the rest of this page does. Once it is published, each part installs without a copy of the repository:
- The command line,
hpr. Download the archive for your system from the releases page, unpack it, and puthpron your path. There are archives for Linux on x86-64, Macs with Apple silicon, and Windows on x86-64. Elsewhere (an Intel Mac, Linux on ARM), or with Rust installed,cargo install hpr-cli --lockedbuilds it. The command line documents its commands. - The Python package.
pip install hpr-siminstalls it, and it imports ashpr. One wheel serves CPython 3.10 and later on each of those three systems; elsewhere pip builds it from source, which needs Rust (Python). - The library, for your own Rust program.
cargo add hpradds the front-door crate, which re-exports the others (The builder; the API reference).
Each archive and wheel holds hpr-sim’s two licenses and the license texts of everything compiled into it. How a release is built lists the files and the checks each passed; the changelog says what 0.1.0 does, how far to trust it, and its known gaps.
Two cautions for the downloaded archives:
- macOS may say it can’t check
hprfor malicious software, since the program isn’t notarized by Apple. To allow it, runxattr -d com.apple.quarantine hprin the folder you unpacked it into. - Linux files need the GNU C library (glibc) 2.35 or later, as in Ubuntu 22.04. On an older
system,
cargo install hpr-cli --lockedbuilds the command line and pip builds the Python package from source (issue #372).
What you need
- Rust, through rustup, Rust’s installer. The repository pins the version
it is built with (in
rust-toolchain.toml), and rustup installs that version the first time you build. - Your system’s C build tools, which Rust uses to link programs. You may have them already:
- On Windows, the Microsoft C++ build tools, from Visual Studio. The rustup installer checks for them and says how to get them.
- On macOS, Apple’s command-line developer tools. If
gitorccis missing, install them withxcode-select --install; they include Git. - On Linux, a C compiler such as
gcc, which most distributions have. On Debian and Ubuntu it comes with thebuild-essentialpackage.
- Git, to fetch the code.
- A network connection the first time, to download Rust and the libraries hpr-sim uses. After that, everything here works offline, on macOS, Windows and Linux.
Nothing else: no Python, Java or other simulator.
Build it and fly
git clone https://github.com/nrdptel/hpr-sim.git
cd hpr-sim
cargo run --example first_flight -p hpr-sim
The first run compiles hpr-sim and its libraries, which takes a few minutes; later runs start at
once. The last command runs the program
crates/hpr-sim/examples/first_flight.rs.
In that command, --example first_flight names the program, and -p hpr-sim (short for
--package) names the crate, one of the repository’s Rust packages, that
holds it: hpr-sim, the crate that flies the rocket. The program prints this:
Valetudo on a K400C: 9.67 kg at liftoff, a 3 m rail, 5 m/s of wind from 270°
Not yet validated: see the Accuracy page before trusting these numbers.
event time (s) CG height (m) speed (m/s)
liftoff 0.00 0.9 0.0
rail exit 0.37 3.9 16.2
burnout 3.26 216.0 111.0
apogee 13.83 778.7 6.4
drogue charge fires 13.83 778.7 6.4
drogue opens 14.33 777.6 7.7
main charge fires 39.23 150.0 27.0
main opens 40.23 123.5 27.0
landing 58.93 0.0 8.1
Apogee: 778.7 m (2555 ft) above the pad, 88.7 m from it at a bearing of 270°, at 13.83 s
Top speed: 112.3 m/s (Mach 0.34), at 3.0 s
Rail exit: 16.2 m/s
Landing: 90.2 m from the pad at a bearing of 90°, falling at 6.4 m/s, at 58.93 s
The heights are the center of gravity’s, above the launch site. For the peaks, the stability margin through the climb, the best ejection delay and the landing’s latitude and longitude, see Flight metrics; its example flies this rocket with one parachute.
You should see exactly these numbers. The project’s automated checks (CI, for continuous
integration) run the program on macOS, Windows and Linux on every change, and fail if it prints
anything but what is committed in
first_flight.output.txt,
the file quoted above.
What it printed
The rocket is Valetudo, which Projeto Jupiter, a student team at the University of São Paulo, flew in 2019. RocketPy, another open-source simulator, uses it as an example, and hpr’s copy of it is one of the project’s example rockets.
Its motor is not a real K400C. It keeps the size and mass of the motor in RocketPy’s example, and takes the thrust curve of a K400C, a commercial motor (motor designation) whose curve hpr bundles, in place of the original’s. Like RocketPy’s example, hpr flies the curve as it was measured: it adds no thrust for the thinner air at the site, 1,400 m above sea level. The rocket launches from a 3 m vertical rail, in a 5 m/s wind that blows from the west at every height, and comes down on a drogue and a main parachute.
The table lists the flight’s events in order:
| column | meaning |
|---|---|
| time | seconds since the motor ignited |
| CG height | the height of the rocket’s center of gravity above the launch pad. It starts at 0.9 m, not 0: the rocket stands on the rail with its aft end at the rail’s foot, and its center of gravity is 0.9 m above that |
| speed | the center of gravity’s speed over the ground, not through the air |
And the events:
| event | what happens |
|---|---|
| liftoff | the push up the rail first beats the weight and the rail’s friction (zero here): the rocket starts to move (liftoff) |
| rail exit | the last rail button, one of the small studs that slide in the rail’s slot, leaves the top of the rail, and the rocket flies free (rail exit) |
| burnout | the motor’s thrust curve ends (burnout) |
| apogee | the highest point, where the rocket stops climbing (apogee) |
| drogue charge fires | the drogue is set to fire at apogee |
| drogue opens | half a second later, the drogue’s lines are stretched and it takes effect (deployment) |
| main charge fires | the rocket falls past 150 m above the pad, the main’s setting |
| main opens | a second later, the main takes effect |
| landing | the center of gravity reaches the ground |
Liftoff comes 1.6 milliseconds after ignition, when the thrust is only 73 N against 95 N of weight. Most of the rest of the push, 21 N, comes from the propellant’s internal momentum: the propellant and gas moving inside the motor as it burns.
- A thrust curve measured on a test stand already includes that effect, and hpr’s equations of motion add it again, so it is counted twice.
- hpr does this on purpose, as RocketPy does, so that the two codes can be compared like for like. It is a known approximation, listed with the other gaps on Start here.
- It is small here: it changes the burnout speed by at most 0.05 m/s (Rigid-body flight).
The speed is over the ground, so it includes the drift. At apogee the rocket is still moving sideways at 6.4 m/s. At landing it falls at 6.4 m/s while the 5 m/s wind carries it east, 8.1 m/s in all (√(6.4² + 5²) ≈ 8.1).
Below the table:
- Apogee is the highest point, in meters and feet, and where it was: 88.7 m from the pad at a bearing of 270°. A bearing is a direction clockwise from north, so 270° is due west.
- Top speed is the fastest the rocket went, over the ground, with its Mach number (its speed through the air as a fraction of the speed of sound). It comes just before burnout, once the dwindling thrust no longer beats the drag and the weight.
- Rail exit is the speed as the rocket leaves the rail. The slower it is in a crosswind, the larger the angle of attack just after it, and that is where hpr’s models are least trustworthy (see what is left out). hpr sets no minimum rail-exit speed and doesn’t judge whether this one is enough; that call is your range safety officer’s.
- Landing is where the rocket came down, 90.2 m due east of the pad, and how fast it was falling. The rocket weathercocks: it turns into the wind as it climbs, so its apogee is west of the pad. It then drifts east under its parachutes, past the pad.
How far to trust it
-
Heights, speeds and times match RocketPy’s, given the same drag. Five of RocketPy’s example rockets, this airframe among them, flown from the pad to the ground by both codes with one declared drag coefficient, agree within 3% on how high, how fast and how long (M2.1b2, the whole-flight comparison). That checks the equations of motion, the motor and the air, not the drag.
-
Where it goes in wind is the least certain number. This airframe was compared with RocketPy only in still air, where its drifts agree within 3%. Here it leaves the rail at 16.2 m/s in a 5 m/s wind, at a steep angle to the airflow. At such angles hpr’s body lift, a sideways push on the body that RocketPy leaves out, and its later release from the rail put the drifts of two of RocketPy’s rockets 10 to 38% from RocketPy’s; how much body lift a body makes is itself uncertain (Accuracy). So this example’s apogee 86 m upwind and its landing point are the least trustworthy numbers it prints. Drift and landing have not been compared with any real flight yet (real flights compare heights).
-
The thrust is likely a little low for this site. hpr flies the curve as measured, to match RocketPy’s example. A motor fired on a test stand near sea level gives somewhat more thrust in the thinner air at 1,400 m. With hpr’s correction for that, which assumes a sea-level test, this flight reached 874 m. Motor files don’t say where the curve was measured, so neither number is certain; treat 779 m as a little low (Solid motors).
-
The drag is the largest doubt. hpr computes the drag coefficient from the rocket’s shape and surface. For this design it is 0.5566 at Mach 0.3, coasting with the motor burnt out (power-off drag), with a mirror-smooth surface finish (0 µm of roughness; a rougher surface adds skin-friction drag) and rail buttons.
- That is 23.5% under the 0.728 in the rocket’s own OpenRocket file.
- With that file’s rougher finish (60 µm) and its two launch lugs (short tubes on the outside of the body that ride along the rail, like rail buttons), hpr gives 0.714, 1.9% under it.
- The drag curve in RocketPy’s example says 1.05, which the comparison leaves unexplained (Aerodynamics). Flown on that curve, RocketPy’s flight peaks 10% lower than hpr’s on its own drag (Accuracy).
A second program,
drag_what_if.rs, flies the same rocket with each of the other two values in place of hpr’s drag, held at every Mach number. Run it the same way:cargo run --example drag_what_if -p hpr-simIt prints this, and CI checks that on every change, as it does for the first program:
Valetudo's apogee under three drag models, from a 3 m rail in 5 m/s of wind drag coefficient apogee (m) hpr's own, from the design 778.7 0.728, from the rocket's OpenRocket file 753.3 1.05, from RocketPy's example curve 711.5For this rocket, the three drag values on record move the apogee from 778.7 m to 711.5 m, 8.6% lower. That is a spread, not a bound:
- It shows how much this rocket’s apogee depends on its drag. It doesn’t say how far hpr’s apogee is from the truth.
- Three values don’t fence in the true drag: it could lie outside them.
- hpr’s drag has been checked against reference drag curves at Mach 0.3 only. It is within 10% in four of seven cases, 18% low for another rocket with its motor burning, and 47% to 50% low against this rocket’s own curve, the 1.05 above (Accuracy).
- Another rocket, or this one on another motor, has its own spread.
So 779 m is this design’s answer, and with more drag the same design would peak lower. The rocket that flew had a different motor, so none of these is a prediction of its flight.
-
The parachutes are simple. Each opens fully the moment its lines stretch, with no filling time and no drag overshoot (the canopy’s drag briefly rising above its steady value as it fills). So the opening load hpr reports is no safe bound either way. Each canopy’s drag coefficient is the middle of the range printed for its type in Knacke’s Parachute Recovery Systems Design Manual, a standard handbook (Recovery).
-
The descent is the best-checked part. From the same start near apogee, with the first parachute opening at once and RocketPy’s formula for gravity, hpr’s descent agrees with RocketPy’s within 3% for five rockets (Recovery). This example opens the drogue after half a second and uses hpr’s own gravity, and its landing point also depends on where the apogee is, which on each code’s own drag differs from RocketPy’s by 10% for this rocket.
Checking it yourself
You don’t have to take these numbers on trust. Four ways to test them:
- See how much the drag matters. Run
drag_what_if, as above. To try other drag coefficients, change the values in its list of them and run it again. - Trace a number to its source. Checking a claim follows any number on this site back to the published source of its model, the test that pins it, and any comparison with another program.
- Compare with a flight of your own, by hand. Build your rocket as
Your own rocket does, with the motor you flew
(Solid motors shows how to read its thrust-curve file). Set the
rail and the wind to match the day, and set hpr’s apogee beside your altimeter’s. hpr’s apogee is
the height of the rocket’s center of gravity above the pad. Most hobby altimeters measure air
pressure and turn it into height with the standard atmosphere, so on a day warmer or colder than
standard they read off by roughly 3 to 4% of the height for every 10 °C of difference. Give hpr
the day’s temperature too:
Ussa76::with_offsetshifts the standard atmosphere, andEnvironment::newtakes it. hpr reads a PerfectFlite altimeter’s log (Reading a flight log); other altimeters’ logs come with M7.1, and comparing one with a simulation with M7.3. - Compare with another simulator, by hand. Enter the same rocket, motor, rail and wind in OpenRocket or RocketPy, and compare the apogee. Give both the same surface finish and rail guides: for this rocket, the OpenRocket file’s finish and launch lugs take hpr’s drag coefficient from 0.5566 to 0.714, as above. hpr can’t import an OpenRocket design yet: that comes with M3.1, OpenRocket import.
The project’s own comparison of whole flights against RocketPy, with the drag given to both codes, is M2.1b2; with each code’s own drag it is M2.1c2.
The program, step by step
This is the whole program. It is also the file CI runs, line for line.
//! A first flight: a rocket with a drogue and a main parachute, flown from the launch rail to the
//! ground, with a summary of what happened.
//!
//! Run it from anywhere in the repository:
//!
//! ```text
//! cargo run --example first_flight -p hpr-sim
//! ```
//!
//! The documentation site's *Getting started* page (`docs/getting-started.md`) walks through it.
//! What it prints is kept next to it in `first_flight.output.txt`, and CI checks that the two
//! still agree (`cargo xtask examples --check`).
#![allow(
clippy::print_stdout,
reason = "the project's lints forbid printing in library code, and this program exists to print"
)]
use std::error::Error;
use hpr_atmos::ConstantWind;
use hpr_core::DVec3;
use hpr_core::geodesy::Geodetic;
use hpr_design::Rocket;
use hpr_sim::{
CanopyType, Device, DeviceDrag, Environment, EventKind, FlightSettings, FlightStep, Observer,
Rail, SimError, Simulation, Termination, Trigger,
};
fn main() -> Result<(), Box<dyn Error>> {
// The rocket: Valetudo, which Projeto Jupiter (University of São Paulo) flew in 2019 and
// RocketPy uses as an example. The design file describes its parts, materials and motor; the
// motor keeps the example's size and mass, with a bundled K400C thrust curve.
let rocket: Rocket = serde_json::from_str(include_str!(
"../../../validation/designs/rocketpy-valetudo.json"
))?;
// Where and in what weather: a site in New Mexico 1,400 m up, the 1976 US Standard
// Atmosphere, and a steady wind from the west (270°) at every height.
let site = Geodetic::from_degrees(32.99, -106.97, 1400.0)?;
let wind_speed_m_s = 5.0;
let wind_from_deg = 270.0_f64;
let environment = Environment::standard(site)?.with_wind(ConstantWind::new(
wind_speed_m_s,
wind_from_deg.to_radians(),
)?);
// A vertical launch rail.
let rail_length_m = 3.0;
let rail = Rail::vertical(rail_length_m);
// The flight: the design's configuration "example" (its K400C), the default settings, and
// dual deployment. The drogue opens half a second after apogee; the main opens a second after
// the rocket falls past 150 m above the ground, and the drogue stays attached.
let simulation = Simulation::new(
&rocket,
"example",
environment,
rail,
FlightSettings::default(),
)?
.with_recovery(vec![
Device::new(
"drogue",
DeviceDrag::canopy(CanopyType::FlatCircular, 0.6),
Trigger::Apogee,
)
.with_lag_s(0.5),
Device::new(
"main",
DeviceDrag::canopy(CanopyType::FlatCircular, 2.4),
Trigger::Altitude {
height_above_ground_m: 150.0,
},
)
.with_lag_s(1.0),
])?;
// Fly it. `TopSpeed` (below) watches every step of the flight for its fastest moment.
let mut top = TopSpeed::default();
let flight = simulation.run(&mut top)?;
if flight.termination != Termination::GroundHit {
return Err(format!("the flight ended with {:?}", flight.termination).into());
}
let liftoff = flight
.event(EventKind::Liftoff)
.ok_or("the rocket never lifted off")?
.sample;
println!(
"Valetudo on a K400C: {:.2} kg at liftoff, a {rail_length_m} m rail, {wind_speed_m_s} m/s \
of wind from {wind_from_deg}°",
liftoff.mass_kg
);
println!("Not yet validated: see the Accuracy page before trusting these numbers.");
println!();
println!("event time (s) CG height (m) speed (m/s)");
for event in &flight.events {
let name = match event.kind {
EventKind::Liftoff => "liftoff".to_owned(),
EventKind::RailExit => "rail exit".to_owned(),
EventKind::Burnout => "burnout".to_owned(),
EventKind::Apogee => "apogee".to_owned(),
EventKind::Trigger(i) => format!("{} charge fires", simulation.recovery()[i].name),
EventKind::Deployment(i) => format!("{} opens", simulation.recovery()[i].name),
EventKind::GroundHit => "landing".to_owned(),
other => format!("{other:?}"),
};
let sample = &event.sample;
// Heights are the center of gravity's above the pad. The flight ends where it reaches the
// ground, found to within a micrometer just below it: print that as 0 (`+ 0.0` turns a
// -0 into 0).
println!(
"{name:<20} {:>8.2} {:>15.1} {:>13.1}",
sample.time_s,
sample.height_above_ground_m.max(0.0) + 0.0,
sample.cg_velocity_enu_m_s.length(),
);
}
let apogee = flight
.event(EventKind::Apogee)
.ok_or("the flight has no apogee")?
.sample;
let rail_exit = flight
.event(EventKind::RailExit)
.ok_or("the flight never left the rail")?
.sample;
let landing = flight.final_sample;
let (apogee_distance_m, apogee_bearing_deg) = from_pad(apogee.cg_enu_m);
let (landing_distance_m, landing_bearing_deg) = from_pad(landing.cg_enu_m);
println!();
println!(
"Apogee: {:.1} m ({:.0} ft) above the pad, {apogee_distance_m:.1} m from it at a \
bearing of {apogee_bearing_deg:.0}°, at {:.2} s",
apogee.height_above_ground_m,
apogee.height_above_ground_m / METERS_PER_FOOT,
apogee.time_s,
);
println!(
"Top speed: {:.1} m/s (Mach {:.2}), at {:.1} s",
top.speed_m_s, top.mach, top.time_s,
);
println!(
"Rail exit: {:.1} m/s",
rail_exit.cg_velocity_enu_m_s.length()
);
println!(
"Landing: {landing_distance_m:.1} m from the pad at a bearing of \
{landing_bearing_deg:.0}°, falling at {:.1} m/s, at {:.2} s",
-landing.vertical_speed_m_s, landing.time_s,
);
Ok(())
}
/// The international foot.
const METERS_PER_FOOT: f64 = 0.3048;
/// Where `position` is from the pad: its distance over the ground, m, and its bearing, clockwise
/// from north, degrees. The launch frame's axes point east, north and up from the pad
/// (`docs/physics/frames.md`).
fn from_pad(position: DVec3) -> (f64, f64) {
let (east_m, north_m) = (position.x, position.y);
let bearing_deg = east_m.atan2(north_m).to_degrees().rem_euclid(360.0);
(east_m.hypot(north_m), bearing_deg)
}
/// The fastest moment of a flight, relative to the ground.
///
/// An [`Observer`] sees every accepted step of the integration as the flight runs. This one looks
/// at the end of each step. Looking at 200 points inside every step instead raises the top speed
/// by 0.005 m/s, and moves its time by 0.013 s, since the speed is flat near its peak. So the
/// speed can read low by one in its last printed digit, and the time is printed to 0.1 s.
#[derive(Debug, Default)]
struct TopSpeed {
speed_m_s: f64,
mach: f64,
time_s: f64,
}
impl Observer for TopSpeed {
fn step(&mut self, step: &dyn FlightStep) -> Result<(), SimError> {
let sample = step.sample(step.end_s())?;
let speed_m_s = sample.cg_velocity_enu_m_s.length();
if speed_m_s > self.speed_m_s {
*self = Self {
speed_m_s,
mach: sample.mach,
time_s: sample.time_s,
};
}
Ok(())
}
}
It has six steps.
- The rocket. The program reads Valetudo’s design from
validation/designs/rocketpy-valetudo.jsoninto aRocket: its parts, their shapes, materials and positions, and its motor. The format is hpr’s own and provisional: the open design format (M3.3) will replace it, and import from OpenRocket (M3.1) comes later. The designs folder holds the other example rockets, and its README says how each was built. - The surroundings.
Geodetic::from_degreesplaces the launch site by latitude, longitude and height.Environment::standardgives it the Earth’s WGS 84 gravity and rotation, the 1976 US Standard Atmosphere and no wind.with_windadds a constant wind:wind_speed_m_s, 5 m/s, fromwind_from_deg, 270°, clockwise from north (see wind direction). Angles in hpr are in radians, henceto_radians(). - The rail.
Rail::vertical(rail_length_m)is a rail 3 m long pointing straight up, with no friction. Its fields also set its heading, its angle above the horizon and its friction. - The simulation.
Simulation::newtakes the rocket, the name of the configuration to fly (a design can hold several, one per motor choice), the surroundings, the rail and the integration settings, which say how finely to step through time. It runs the design’s checks first, and refuses a rocket that can’t exist, such as a motor wider than its mount.FlightSettings::default()steps through time to a tight tolerance (Time integration). - The parachutes. Each
Devicehas a name, a drag, and a trigger that fires its charge.DeviceDrag::canopyis a parachute of the given type and nominal diameter in meters: 0.6 m for the drogue and 2.4 m for the main.CanopyType::FlatCircularis a flat circular canopy. It is one of thirteen canopy types, such as conical, hemispherical, cross and ringslot, each with drag data from Knacke’s parachute handbook. TheCanopyTypepage of the API reference lists them all, and Recovery explains the data.Trigger::Apogeefires at apogee;Trigger::Altitudefires when the rocket falls past a height above the pad.with_lag_sis the time from the charge to the lines stretching. With no filling rule given, one for how the canopy’s drag grows as it opens, each parachute opens fully at once.
- The flight.
runflies the rocket until it lands. It returns aFlightResult, the record of the finished flight: how it ended (termination), itsevents, each with aSampleof the flight at that moment, and the final sample.- The argument to
runis anObserver: any type that is shown every step of the flight as it runs.TopSpeed, at the bottom of the program, is one: it keeps the fastest moment. - For a whole trajectory, to plot or save, pass a
Recorderinstead. It keeps the quantities you choose, each aChannel, such as the height or the Mach number, as a table. Recording a trajectory shows one, lists the channels, and shows what it prints.
- The argument to
A Sample is a snapshot of the flight at one instant.
It holds what the program prints, and more: the time, the center of gravity’s position
(cg_enu_m, meters east, north and up of the pad) and velocity (cg_velocity_enu_m_s), the
height above the pad, the vertical speed, the airspeed, the Mach number, the angle of attack, the
thrust and the mass. Every quantity is in SI units, the metric units of
the International System, and its name ends in its unit:
| ending | unit | example |
|---|---|---|
_m | meters | height_above_ground_m |
_m_s | meters per second | vertical_speed_m_s |
_s | seconds | time_s |
_kg | kilograms | mass_kg |
_n | newtons | thrust_n |
_rad | radians | angle_of_attack_rad |
A number with no unit, such as the Mach number (mach), has none in its name.
Change it
Edit the program and run it again. The edits below are for you to try. Each says which way the results move; run it to see by how much. Nothing checks these edits or what they print, so this page gives no numbers for them. For example:
- More wind. Change
let wind_speed_m_s = 5.0;to10.0. The apogee drops a little and the landing moves farther east. - Angle the rail into the wind. Replace
Rail::vertical(rail_length_m)withRail { elevation_rad: 85_f64.to_radians(), azimuth_rad: 270_f64.to_radians(), ..Rail::vertical(rail_length_m) }, a rail tilted 5° toward the west. The rocket now lands west of the pad, upwind. - Open the main higher. Change
height_above_ground_m: 150.0to300.0. The rocket spends longer under its main and lands farther downwind. - Drop the drogue. Delete the first
Device, the drogue. The rocket now falls at over 100 m/s until the main opens, far faster than a real parachute survives. hpr reports the violent deceleration as it opens, but it has no model of a parachute or its harness failing, so the flight still ends in a gentle landing (Recovery).
Your edited program no longer prints what first_flight.output.txt says, which is expected.
If you propose a change to the program itself in a pull request, two more commands help.
cargo xtask runs the project’s own maintenance tasks, a Rust program in the repository’s
xtask folder:
cargo xtask examplesruns every example program and writes its new output next to it;cargo xtask sitebuilds this documentation site, and checks that the quotes on this page still match the program and its output.
Where next
- Recording a trajectory keeps the whole flight as a table, to plot or save.
- How a flight is simulated explains what happens between ignition and landing, and links the page for each model.
- Your own rocket builds a rocket of your own, with your dimensions and a
bundled motor, and shows its center of pressure, center of gravity and
stability margin. The builder does the same
in fewer lines, and
.orkdesign files reads a design from OpenRocket, though few of its motor configurations fly yet; its parachutes and streamers fly as OpenRocket flies them. - Accuracy gathers every validation result, and Checking a claim shows how to trace a number to its source and its test.
- The API reference documents every type used here, and
cargo doc --open -p hpr-simbuilds it on your machine.
Fly your .ork
This guide flies a rocket you designed in OpenRocket with hpr sim,
from the command line, and says how to read what it prints. It is for a flier who has a .ork
file and wants a second simulator’s view of it. Every flight here is a model’s estimate, not a
measurement: Accuracy says how close hpr comes to OpenRocket and to real flights,
and its summary sits at the top of the README.
The outputs on this page are made by running each command, and CI checks that they still match
what hpr prints.
The steps:
- Fly the file as it is.
- Choose the configuration and the launch.
- Fetch a motor hpr doesn’t have.
- Plot it, or save the numbers.
- When hpr refuses.
You need hpr installed: the README’s
Install and first flight takes a
few minutes. The examples fly the guides’ own rocket,
validation/fixtures/ork/guides/level-1.ork:
a 66 mm, 0.82 kg rocket with a 38 mm motor mount and one parachute on the motor’s ejection
charge, saved with two motors, an AeroTech H170M and an I175WS. OpenRocket 24.12 opens it. Run
the commands from a copy of the repository, or put your own file’s path in its place.
Fly the file as it is
hpr sim with the file’s name flies its default configuration, with the motors, delays and
parachutes the file holds. The guide rocket’s motors are among the 32 built into hpr, so it
flies with no network:
$ hpr sim validation/fixtures/ork/guides/level-1.ork
Level 1 guide rocket (level-1.ork)
configuration 1 of 2: [H170M-10]
help: --config flies the others, by number or name: 2 [I175WS-9]
static margin 1.70 calibres off the rail; least 1.70 calibres, at 0.12 s, before apogee
apogee 1065.1 m above the site at 11.62 s
rail exit speed 26.3 m/s
delay apogee 9.48 s after burnout; the motor's set delay: 10 s; its charge fires 0.52 s after apogee
descent 4.4 m/s at landing under `Parachute`
motor: 1 × H170M (the design's) in `Motor mount tube`, lit at launch
recovery: `Parachute` at the ejection charge, 0.520 m² of drag area, opened at 12.14 s
launched at 0° N, 0° E, 0 m above sea level, from a 1.5 m vertical rail, in calm air
help: see the Accuracy page before trusting these numbers: https://nrdptel.github.io/hpr-sim/accuracy.html
note: the file's 1 recovery device flies as OpenRocket flies it: each opens fully at its event, with the file's drag coefficient or OpenRocket's own, and once one opens the rocket descends as a point under the open devices' drag alone
warning: drag: issue #67: a cone-like nose's or shoulder's pressure drag reads high from Mach 0.8 (about twice a measured cone's at Mach 0.85, still +15% at 1.5), so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are; this flight reaches Mach 0.84 at 1.7 s (https://github.com/nrdptel/hpr-sim/issues/67)
warning: drag: issue #68: base drag reads high from Mach 0.8 to 1.2 (0.225 against a measured 0.156 at Mach 0.9), so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are; this flight reaches Mach 0.84 at 1.7 s (https://github.com/nrdptel/hpr-sim/issues/68)
warning: drag: issue #18: skin friction is taken as fully turbulent, but on a smooth surface the flow stays laminar near the nose, where friction is lower: hpr's reads high by 3.6% on RocketPy's Calisto at Mach 0.3; if this surface is that smooth, the drag reads high, so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are (https://github.com/nrdptel/hpr-sim/issues/18)
warning: stability: issue #172: the static margin read up to 0.1108 calibres higher than OpenRocket's on four private designs, for a reason not yet found, so this flight's margin may read high by as much (https://github.com/nrdptel/hpr-sim/issues/172)
event time height speed
liftoff 0.00 s 0.4 m 0.0 m/s
rail exit 0.12 s 1.9 m 26.3 m/s
burnout 2.14 s 406.4 m 240.4 m/s
apogee 11.62 s 1065.1 m 0.1 m/s
charge 12.14 s 1063.7 m 5.1 m/s
deployment 12.14 s 1063.7 m 5.1 m/s
ground hit 245.78 s 0.0 m 4.4 m/s
(heights are the center of gravity's above the site; speeds are over the ground)
top speed 285.6 m/s at 1.73 s
top Mach number 0.842
landing 0.7 m from the pad at 245.78 s, at 4.4 m/s
The five lines under the configuration are the ones to read first:
static margin: how far the center of pressure sits behind the center of gravity, in body diameters (calibres), as the rocket leaves the rail, and its least value before apogee. A rocket with a fin set of one or two fins has a different margin for each direction the air crosses it, and hpr prints the least (Flight metrics). Check stability for a certification flight says how to use it.apogee: the highest point of the center of gravity, above the launch site.rail exit speed: the speed as the rocket leaves the rail. The slower it is, the more a crosswind tips the rocket just after.delay: the coast from burnout to apogee, beside the delay the file sets. Here the H170M’s 10 s charge fires about half a second after apogee. Pick a motor says how to choose one.descent: how fast the rocket comes down under its open parachutes, without the wind’s drift.
The recovery line names each parachute, when it opened and its drag area; the events table
gives each event’s time, height and speed. The note says how the parachute is flown: it opens
fully at once, as OpenRocket’s does. The command line explains every
line, and any warning you may see.
Choose the configuration and the launch
--config picks another of the file’s configurations, by its number, its name or its motors.
The output’s second and third lines list them. A configuration the file leaves unnamed is called
by its motors and delays in brackets, so the guide rocket’s second is [I175WS-9], and
--config 2, --config I175WS-9 and --config i175ws-9 all fly it.
hpr sim does not read the launch conditions saved with the file’s simulations: give them as
options. Without them, the rocket flies from a 1.5 m vertical rail at sea level, at 0° N, 0° E,
in calm air. Set the site’s elevation for every real field: air thins with height, and a rocket
climbs higher from a high one. This flies the second configuration from a field 1,400 m up, off a 2.4
m rail tilted 5° toward the west, in a 4 m/s west wind:
$ hpr sim validation/fixtures/ork/guides/level-1.ork --config 2 --elevation 1400 --rail-length 2.4 --inclination 85 --heading 270 --wind 4 --wind-from 270
Level 1 guide rocket (level-1.ork)
configuration 2 of 2: [I175WS-9]
help: --config flies the others, by number or name: 1 [H170M-10]
static margin 1.69 calibres off the rail; least 1.69 calibres, at 0.17 s, before apogee
apogee 1174.3 m above the site at 11.51 s
rail exit speed 36.8 m/s
delay apogee 9.54 s after burnout; the motor's set delay: 9 s; its charge fires 0.54 s before apogee
descent 4.9 m/s at landing under `Parachute`
motor: 1 × I175WS (the design's) in `Motor mount tube`, lit at launch
recovery: `Parachute` at the ejection charge, 0.520 m² of drag area, opened at 10.97 s
launched at 0° N, 0° E, 1400 m above sea level, from a 2.4 m rail 85° above the horizon, leaning toward 270°, in a 4 m/s wind from 270°
help: see the Accuracy page before trusting these numbers: https://nrdptel.github.io/hpr-sim/accuracy.html
note: the file's 1 recovery device flies as OpenRocket flies it: each opens fully at its event, with the file's drag coefficient or OpenRocket's own, and once one opens the rocket descends as a point under the open devices' drag alone
warning: drag: issue #67: a cone-like nose's or shoulder's pressure drag reads high from Mach 0.8 (about twice a measured cone's at Mach 0.85, still +15% at 1.5), so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are; this flight reaches Mach 0.87 at 1.8 s (https://github.com/nrdptel/hpr-sim/issues/67)
warning: drag: issue #68: base drag reads high from Mach 0.8 to 1.2 (0.225 against a measured 0.156 at Mach 0.9), so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are; this flight reaches Mach 0.87 at 1.8 s (https://github.com/nrdptel/hpr-sim/issues/68)
warning: drag: issue #18: skin friction is taken as fully turbulent, but on a smooth surface the flow stays laminar near the nose, where friction is lower: hpr's reads high by 3.6% on RocketPy's Calisto at Mach 0.3; if this surface is that smooth, the drag reads high, so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are (https://github.com/nrdptel/hpr-sim/issues/18)
warning: stability: issue #172: the static margin read up to 0.1108 calibres higher than OpenRocket's on four private designs, for a reason not yet found, so this flight's margin may read high by as much (https://github.com/nrdptel/hpr-sim/issues/172)
event time height speed
liftoff 0.02 s 0.4 m 0.0 m/s
rail exit 0.17 s 2.8 m 36.8 m/s
burnout 1.97 s 389.9 m 271.2 m/s
charge 10.97 s 1172.0 m 15.4 m/s
deployment 10.97 s 1172.0 m 15.4 m/s
apogee 11.51 s 1174.3 m 1.0 m/s
ground hit 246.17 s 0.0 m 6.3 m/s
(heights are the center of gravity's above the site; speeds are over the ground)
top speed 290.9 m/s at 1.75 s
top Mach number 0.874
landing 755.8 m from the pad at 246.17 s, at 6.3 m/s
--inclination is the rail’s angle above the horizon, so 85 is 5° off vertical (OpenRocket
gives the same rail as 5°, from the vertical), and --heading 270 leans it toward the west, into the wind. The wind is the same at every
height. The launch lists every option and its default.
Fetch a motor hpr doesn’t have
A motor outside hpr’s 32 is fetched from ThrustCurve.org the
first time you fly it, then kept in hpr’s cache, so it flies offline after. A .ork names its
motor but rarely carries its thrust curve. With no network, and no
copy in the cache, hpr sim refuses and prints the command that fetches it. This test rocket
names an AeroTech H128W:
$ hpr sim validation/fixtures/ork/pod-flights/pods-none.ork --offline
error: configuration [H128W-0] can't be flown as the file has it: no thrust curve for H128W: no embedded curve, and no motor of that manufacturer and designation in the bundled catalog; AeroTech H128W is not in hpr's cache of ThrustCurve.org, and the run is offline
help: with a network connection, `hpr motors fetch --manufacturer AeroTech H128W` fetches it into the cache
help: give a motor with --motor
$ echo $?
1
Run that command once with a network connection, or fly the file once online:
hpr motors fetch --manufacturer AeroTech H128W
The curve is ThrustCurve’s, found by the file’s manufacturer and designation. For the motors of
OpenRocket’s own example designs, hpr knows which file holds the curve OpenRocket flies and takes
it; for any other motor, the file may not be the one OpenRocket flies. A note under the flight
names the curve file, who measured it, and which of the two it is.
Motors from ThrustCurve.org says how hpr picks one. If you
have the motor’s .eng or .rse file, fly it with --motor:
hpr sim my-rocket.ork --motor AeroTech_H128W.eng.
Plot it, or save the numbers
--plot draws the flight as a picture; --export saves every number.
hpr sim validation/fixtures/ork/guides/level-1.ork --plot level-1.svg --export level-1.csv
The plot is one fixed figure: altitude, speed and acceleration against time, each event marked.
An SVG opens in any web browser; Plotting the flight shows one. The
export holds every quantity hpr tracks, every 0.01 s; the file’s extension picks the format
(.csv, .json, .parquet, .geojson or .kml), and
Exporting a flight says what each column means. --json prints the
summary itself as data, for a script to read.
When hpr refuses
hpr sim refuses, with the reason, rather than fly something other than your design. The
common reasons, and what to do:
| it says | what to do |
|---|---|
| no thrust curve for the motor, and the run is offline | fetch it once online, as above, or give a motor file with --motor |
| the design’s checks found errors | each error names the parts, such as a motor wider than its mount. Fix the design; or, to fly it anyway, add --accept-design-errors, and the flight’s notes list the errors |
| a part hpr couldn’t read exactly as written | hpr flies only what it reads exactly. The message names the part; the .ork page lists what hpr reads |
| a stage that separates, a second motor lit in flight | powered separations fly without --motor, one or several in turn, as on a three-stage rocket (Separation); with --motor, drop the option to fly the file’s own motors. A payload dropped with nothing left to burn flies when its own parachute opens at the split; one that would coast with no drag first is refused: set one of the payload’s devices to open at “Lower stage separation” with no delay (M4.5g3, a payload’s split) |
| it can’t tell which configuration to fly | the file has several and no default: name one with --config |
Warnings are not refusals. A warning: line flags something the design’s checks found unusual
but buildable, such as a motor longer than its mount tube, and the flight goes on.
What it leaves out
- The launch conditions in your file. Give them as options, above.
- Real weather. One wind at every height, in the standard atmosphere.
hpr weatherfetches a launch day’s profile, buthpr simdoesn’t fly it yet (#265, flying a weather profile). - A fall with no parachute open. The landing is then marked “not a prediction” (the landing).
- A verdict. hpr prints estimates. Whether a rocket is fit to fly is for you, the motor’s printed data and your range safety officer to decide.
Pick a motor
This guide shortlists the motors that fit a rocket, flies the rocket on each, and picks an
ejection delay, all with hpr on the command line. It is for a flier choosing a motor for a
design they already have. hpr’s figures are estimates from a model:
Accuracy says how close they come, and the motor’s printed data and your range
safety officer have the last word. The outputs on this page are made by running each command, and
CI checks that they still match what hpr prints.
The steps:
- List the motors that fit.
- Fly the rocket on each.
- Compare them.
- Choose the delay.
- Put the choice in your design.
The examples use the guides’ own rocket,
level-1.ork,
with a 38 mm motor mount; Fly your .ork introduces it.
List the motors that fit
Start from the mount’s diameter: a motor’s case must match it. In OpenRocket, the motor mount
tube’s inner diameter is the size; a 38 mm mount takes 38 mm motors. hpr motors list filters
the 32 motors built into hpr by diameter, impulse class or maker:
$ hpr motors list --diameter 38
6 motors from ThrustCurve.org data files marked public domain, downloaded 2026-09-17; size, masses and delays from each file's header, the rest from its curve.
designation maker class dia mm len mm impulse N·s avg N burn s delays
G69N AeroTech G 38 106 136.3 72.1 1.89 1000
H170M AeroTech H 38 191 318.0 165.4 1.92 2,4,6,8,10,14
H125-CT Loki H 38 177.8 241.7 125.0 1.93 8-18
I175WS AeroTech I 38 214 333.2 177.5 1.88 5-9-13
411I175-14A Cesaroni I 38 245 411.4 174.1 2.36 6-8-9-11-12-13
I377-CT Loki I 38 292 525.8 377.9 1.39 8-18
The figures are worked out from each motor’s public-domain ThrustCurve.org curve file, the curve
hpr flies (The bundled motors). The delays column lists
the ejection delays as the file’s header writes them, in seconds after burnout, with - between
settings (5-9-13 is 5, 9 and 13 s; 8-18 is 8 and 18 s), which can be fewer than the maker
sells: check the motor’s instructions. P (or 1000) is a plugged motor with no
ejection charge. A
Level 1 certification flight uses an H or I motor (NAR and Tripoli’s Level 1 rules;
Check stability for a certification flight
quotes them).
Many more motors exist than the 32 built in. Name any motor ThrustCurve.org lists, such as
--motor H128W, and hpr fetches its thrust curve once online and keeps it
(Fetch a motor hpr doesn’t have).
hpr motors search lists what vendors have in stock and at what price
(Motors you can buy). hpr motors show gives one motor’s figures as
hpr reads its curve:
$ hpr motors show I175WS
I175WS (AeroTech), from the bundled catalog
impulse class I
total impulse 333.2 N·s
average thrust 177.5 N
peak thrust 252.6 N
burn time 1.88 s, from 0.023 s to 1.900 s (NFPA 1125, 5% of peak)
propellant 168.0 g of 348.0 g loaded
casing 38 mm across, 214 mm long
delays 5 s, 9 s, 13 s
curve file https://www.thrustcurve.org/simfiles/5f4294d20002e900000008bf/ (public domain)
Fly the rocket on each
--motor flies the design with another motor in its mount, and --delay sets that motor’s
ejection delay. Without --delay, the motor fires no charge, so a parachute set to open on the
charge, as the guide rocket’s is, never opens. Give one of the motor’s delays, in seconds, or P
for a plugged motor when your electronics fire the charges. This flies the guide rocket on a Loki
H125, with a 10 s delay:
$ hpr sim validation/fixtures/ork/guides/level-1.ork --motor H125-CT --delay 10
Level 1 guide rocket (level-1.ork)
configuration 1 of 2: [H170M-10] with --motor H125-CT --delay 10
help: --config flies the others, by number or name: 2 [I175WS-9]
static margin 1.59 calibres off the rail; least 1.59 calibres, at 0.17 s, before apogee
apogee 914.5 m above the site at 11.42 s
rail exit speed 25.9 m/s
delay apogee 9.42 s after burnout; the motor's set delay: 10 s; its charge fires 0.58 s after apogee
descent 4.7 m/s at landing under `Parachute`
motor: 1 × H125-CT (from the bundled catalog) in `Motor mount tube`, lit at launch
recovery: `Parachute` at the ejection charge, 0.520 m² of drag area, opened at 12.00 s
launched at 0° N, 0° E, 0 m above sea level, from a 1.5 m vertical rail, in calm air
help: see the Accuracy page before trusting these numbers: https://nrdptel.github.io/hpr-sim/accuracy.html
note: the file's 1 recovery device flies as OpenRocket flies it: each opens fully at its event, with the file's drag coefficient or OpenRocket's own, and once one opens the rocket descends as a point under the open devices' drag alone
warning: drag: issue #18: skin friction is taken as fully turbulent, but on a smooth surface the flow stays laminar near the nose, where friction is lower: hpr's reads high by 3.6% on RocketPy's Calisto at Mach 0.3; if this surface is that smooth, the drag reads high, so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are (https://github.com/nrdptel/hpr-sim/issues/18)
warning: stability: issue #172: the static margin read up to 0.1108 calibres higher than OpenRocket's on four private designs, for a reason not yet found, so this flight's margin may read high by as much (https://github.com/nrdptel/hpr-sim/issues/172)
event time height speed
liftoff 0.02 s 0.4 m 0.0 m/s
rail exit 0.17 s 1.9 m 25.9 m/s
burnout 2.00 s 303.8 m 201.2 m/s
apogee 11.42 s 914.5 m 0.1 m/s
charge 12.00 s 912.8 m 5.6 m/s
deployment 12.00 s 912.8 m 5.6 m/s
ground hit 202.71 s 0.0 m 4.7 m/s
(heights are the center of gravity's above the site; speeds are over the ground)
top speed 217.0 m/s at 1.56 s
top Mach number 0.639
landing 0.7 m from the pad at 202.71 s, at 4.7 m/s
And on the hardest-hitting motor in the list, a Loki I377, with the same delay:
$ hpr sim validation/fixtures/ork/guides/level-1.ork --motor I377-CT --delay 10
Level 1 guide rocket (level-1.ork)
configuration 1 of 2: [H170M-10] with --motor I377-CT --delay 10
help: --config flies the others, by number or name: 2 [I175WS-9]
static margin 1.18 calibres off the rail; least 1.18 calibres, at 0.08 s, before apogee
apogee 1320.4 m above the site at 12.02 s
rail exit speed 42.3 m/s
delay apogee 10.59 s after burnout; the motor's set delay: 10 s; its charge fires 0.59 s before apogee
descent 5.0 m/s at landing under `Parachute`
motor: 1 × I377-CT (from the bundled catalog) in `Motor mount tube`, lit at launch
recovery: `Parachute` at the ejection charge, 0.520 m² of drag area, opened at 11.43 s
launched at 0° N, 0° E, 0 m above sea level, from a 1.5 m vertical rail, in calm air
help: see the Accuracy page before trusting these numbers: https://nrdptel.github.io/hpr-sim/accuracy.html
note: the file's 1 recovery device flies as OpenRocket flies it: each opens fully at its event, with the file's drag coefficient or OpenRocket's own, and once one opens the rocket descends as a point under the open devices' drag alone
warning: design checks: the motor in `Motor mount tube` reaches 22.0 mm past the mount's forward end (configuration [H170M-10] with --motor I377-CT --delay 10)
warning: drag: issue #67: a cone-like nose's or shoulder's pressure drag reads high from Mach 0.8 (about twice a measured cone's at Mach 0.85, still +15% at 1.5), so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are; this flight reaches Mach 1.10 at 1.1 s (https://github.com/nrdptel/hpr-sim/issues/67)
warning: drag: issue #68: base drag reads high from Mach 0.8 to 1.2 (0.225 against a measured 0.156 at Mach 0.9), so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are; this flight reaches Mach 1.10 at 1.1 s (https://github.com/nrdptel/hpr-sim/issues/68)
warning: drag: issue #222: supersonic pressure drag on an ogive nose or airfoil fins is about twice OpenRocket's, and which is right is unresolved: if hpr's is high, so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are; this flight reaches Mach 1.10 at 1.1 s (https://github.com/nrdptel/hpr-sim/issues/222)
warning: drag: issue #18: skin friction is taken as fully turbulent, but on a smooth surface the flow stays laminar near the nose, where friction is lower: hpr's reads high by 3.6% on RocketPy's Calisto at Mach 0.3; if this surface is that smooth, the drag reads high, so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are (https://github.com/nrdptel/hpr-sim/issues/18)
warning: stability: issue #172: the static margin read up to 0.1108 calibres higher than OpenRocket's on four private designs, for a reason not yet found, so this flight's margin may read high by as much (https://github.com/nrdptel/hpr-sim/issues/172)
event time height speed
liftoff 0.00 s 0.3 m 0.0 m/s
rail exit 0.08 s 1.8 m 42.3 m/s
burnout 1.43 s 384.0 m 335.1 m/s
charge 11.43 s 1318.2 m 10.1 m/s
deployment 11.43 s 1318.2 m 10.1 m/s
apogee 12.02 s 1320.4 m 0.0 m/s
ground hit 269.86 s 0.0 m 5.0 m/s
(heights are the center of gravity's above the site; speeds are over the ground)
top speed 371.7 m/s at 1.08 s
top Mach number 1.095
landing 0.8 m from the pad at 269.86 s, at 5.0 m/s
The warning says the I377’s 292 mm case, which sits 10 mm out of the back of the rocket’s
260 mm mount tube, reaches 22.1 mm past the tube’s forward end. hpr flies it as drawn; whether it fits the real rocket is for you to check.
--motor replaces the motor of the configuration flown, so --config picks which of the file’s
configurations it goes in, and --mount which mount, if the design has several.
Compare them
Read the same five lines for each motor: margin, apogee, rail exit speed, delay and descent. Between the two flights above:
- Apogee. The I377 climbs higher. If your field has a ceiling, its waiver sets it, not hpr.
- Rail exit speed. The I377 leaves the rail much faster. The safety codes ask for a speed that ensures a stable flight but give no number; competitions set their own, such as 25 m/s for the Spaceport America Cup (Rail exit speed quotes them).
- Stability margin. It changes with the motor: a motor’s mass sits behind the center of gravity, so a heavier motor moves the center of gravity aft, toward the center of pressure, and shrinks the margin. Check it with every motor you might fly (Check stability for a certification flight).
- Top Mach number. The I377 takes the rocket past the speed of sound, Mach 1; the H125 stays well below it, and the H170M, at Mach 0.842, just reaches the range below. Between Mach 0.8 and 1.2, hpr’s center of pressure is up to 2.36 calibres behind NASA’s wind-tunnel data, so its margin there reads high (Known gaps), and published guidance asks for a larger margin (Compare with published guidance).
Flying a few more motors is quick: change --motor and --delay and run again. --json prints
each flight as data, for a script that tabulates many.
Choose the delay
The delay line’s first figure is the coast, from burnout to apogee: a delay near it fires the
charge near apogee, where the rocket moves slowest. The second figure is the delay you gave, and
the third says how far from apogee the charge then fires. In the events table, the charge row
gives the speed at that moment: the further the charge is from apogee, the faster the rocket
moves when the parachute comes out.
In the H125 flight above, the coast is 9.42 s, and the 10 s delay fires the charge 0.58 s after apogee. The H125’s delays are 8, 10, 12, 14 and 18 s, so 10 s is the nearest. hpr flies any delay you give; give one the maker sells, or one your motor’s delay can be adjusted to. A delay is a burning fuse, and real ones vary from motor to motor; hpr flies the stated time exactly. Monte Carlo dispersion, in the library, can scatter it.
The guide rocket’s own second configuration, [I175WS-9], is an example where the set delay
fires before apogee:
$ hpr sim validation/fixtures/ork/guides/level-1.ork --config 2
Level 1 guide rocket (level-1.ork)
configuration 2 of 2: [I175WS-9]
help: --config flies the others, by number or name: 1 [H170M-10]
static margin 1.67 calibres off the rail; least 1.67 calibres, at 0.14 s, before apogee
apogee 1100.7 m above the site at 11.48 s
rail exit speed 28.9 m/s
delay apogee 9.51 s after burnout; the motor's set delay: 9 s; its charge fires 0.51 s before apogee
descent 4.5 m/s at landing under `Parachute`
motor: 1 × I175WS (the design's) in `Motor mount tube`, lit at launch
recovery: `Parachute` at the ejection charge, 0.520 m² of drag area, opened at 10.97 s
launched at 0° N, 0° E, 0 m above sea level, from a 1.5 m vertical rail, in calm air
help: see the Accuracy page before trusting these numbers: https://nrdptel.github.io/hpr-sim/accuracy.html
note: the file's 1 recovery device flies as OpenRocket flies it: each opens fully at its event, with the file's drag coefficient or OpenRocket's own, and once one opens the rocket descends as a point under the open devices' drag alone
warning: drag: issue #67: a cone-like nose's or shoulder's pressure drag reads high from Mach 0.8 (about twice a measured cone's at Mach 0.85, still +15% at 1.5), so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are; this flight reaches Mach 0.83 at 1.8 s (https://github.com/nrdptel/hpr-sim/issues/67)
warning: drag: issue #68: base drag reads high from Mach 0.8 to 1.2 (0.225 against a measured 0.156 at Mach 0.9), so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are; this flight reaches Mach 0.83 at 1.8 s (https://github.com/nrdptel/hpr-sim/issues/68)
warning: drag: issue #18: skin friction is taken as fully turbulent, but on a smooth surface the flow stays laminar near the nose, where friction is lower: hpr's reads high by 3.6% on RocketPy's Calisto at Mach 0.3; if this surface is that smooth, the drag reads high, so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are (https://github.com/nrdptel/hpr-sim/issues/18)
warning: stability: issue #172: the static margin read up to 0.1108 calibres higher than OpenRocket's on four private designs, for a reason not yet found, so this flight's margin may read high by as much (https://github.com/nrdptel/hpr-sim/issues/172)
event time height speed
liftoff 0.02 s 0.4 m 0.0 m/s
rail exit 0.14 s 1.9 m 28.9 m/s
burnout 1.97 s 385.2 m 260.5 m/s
charge 10.97 s 1099.1 m 8.0 m/s
deployment 10.97 s 1099.1 m 8.0 m/s
apogee 11.48 s 1100.7 m 0.0 m/s
ground hit 247.48 s 0.0 m 4.5 m/s
(heights are the center of gravity's above the site; speeds are over the ground)
top speed 281.3 m/s at 1.75 s
top Mach number 0.830
landing 0.7 m from the pad at 247.48 s, at 4.5 m/s
Put the choice in your design
hpr doesn’t change your .ork file: set the motor and its delay in OpenRocket, save, and fly
the file again. Without --motor, hpr sim flies the motors and delays the file holds, as
Fly your .ork shows, and its delay line checks the delay you set.
What it leaves out
- Motor-to-motor differences. Each motor flies one thrust curve, ThrustCurve.org’s, and its stated delay exactly. Real motors differ a little from their published curves.
- Fit. hpr’s design checks flag a motor wider than its mount, and warn on one longer than its tube; they don’t know your motor retainer, thrust ring or closure.
- A verdict. hpr ranks nothing as safe or unsafe. The motor’s printed data, the safety codes and your range safety officer decide.
Check stability for a certification flight
This guide reads hpr’s stability numbers for a rocket you plan to fly for a certification, checks
them with every motor you might use, and says how far to trust them. It is for a flier preparing
a certification flight; the rules it quotes are Level 1’s. hpr gives no verdict: the safety codes
ask you to check stability, and the range safety officer (RSO) and your certifying member
decide. hpr’s margin at rod clearance agrees with OpenRocket 24.12’s to within 0.016
calibres on 41 of OpenRocket’s 53 example flights, but no margin
of hpr’s has been checked against a measured rocket. On OpenRocket’s two pod examples it reads
0.07 calibres above OpenRocket’s, and near the speed of sound its flight margin reads high
(how far to trust it). The outputs on this page
are made by running each command, and CI checks that they still match what hpr prints.
The steps:
- Know what the rules ask.
- Read the margin.
- Check every motor you might fly.
- Compare with published guidance.
- Check the rail exit speed.
- Fly the rocket you built.
What the rules ask
The safety codes require a stability check, but name no margin. The words below are the organisations’ own, read on 2026-10-04; check their current pages before your flight. This is not legal or regulatory advice.
- The NAR’s High Power Rocket Safety Code (revision of August 2012): “I will check the stability of my rocket before flight and will not fly it if it cannot be determined to be stable.”
- Tripoli’s Unified Safety Code (version 2, effective January 2026), 7-1.2: “Stability; the flier shall document the location of the center of pressure and be able to demonstrate the center of gravity.”
- A Level 1 flight uses an H or I motor. The NAR’s Level 1 procedures (revision of January 1, 2022): “at least one H or I impulse class motor”; Tripoli’s: “a single certified H or I motor”. Neither page states a margin.
The NAR’s procedures add that the candidate may be asked about “the model’s center of gravity and
center of pressure, methods used to determine model stability”. hpr’s output gives both points’
separation; --json and --export give more.
Read the margin
The static margin line is the distance from the center of gravity back to the center of
pressure, in body diameters (calibres), as the rocket leaves the rail; then its least value up to
apogee. A positive margin turns the nose back into the oncoming air when the rocket is disturbed
(stability margin). This flies the guides’ own rocket,
level-1.ork,
on its default motor, an AeroTech H170M:
$ hpr sim validation/fixtures/ork/guides/level-1.ork
Level 1 guide rocket (level-1.ork)
configuration 1 of 2: [H170M-10]
help: --config flies the others, by number or name: 2 [I175WS-9]
static margin 1.70 calibres off the rail; least 1.70 calibres, at 0.12 s, before apogee
apogee 1065.1 m above the site at 11.62 s
rail exit speed 26.3 m/s
delay apogee 9.48 s after burnout; the motor's set delay: 10 s; its charge fires 0.52 s after apogee
descent 4.4 m/s at landing under `Parachute`
motor: 1 × H170M (the design's) in `Motor mount tube`, lit at launch
recovery: `Parachute` at the ejection charge, 0.520 m² of drag area, opened at 12.14 s
launched at 0° N, 0° E, 0 m above sea level, from a 1.5 m vertical rail, in calm air
help: see the Accuracy page before trusting these numbers: https://nrdptel.github.io/hpr-sim/accuracy.html
note: the file's 1 recovery device flies as OpenRocket flies it: each opens fully at its event, with the file's drag coefficient or OpenRocket's own, and once one opens the rocket descends as a point under the open devices' drag alone
warning: drag: issue #67: a cone-like nose's or shoulder's pressure drag reads high from Mach 0.8 (about twice a measured cone's at Mach 0.85, still +15% at 1.5), so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are; this flight reaches Mach 0.84 at 1.7 s (https://github.com/nrdptel/hpr-sim/issues/67)
warning: drag: issue #68: base drag reads high from Mach 0.8 to 1.2 (0.225 against a measured 0.156 at Mach 0.9), so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are; this flight reaches Mach 0.84 at 1.7 s (https://github.com/nrdptel/hpr-sim/issues/68)
warning: drag: issue #18: skin friction is taken as fully turbulent, but on a smooth surface the flow stays laminar near the nose, where friction is lower: hpr's reads high by 3.6% on RocketPy's Calisto at Mach 0.3; if this surface is that smooth, the drag reads high, so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are (https://github.com/nrdptel/hpr-sim/issues/18)
warning: stability: issue #172: the static margin read up to 0.1108 calibres higher than OpenRocket's on four private designs, for a reason not yet found, so this flight's margin may read high by as much (https://github.com/nrdptel/hpr-sim/issues/172)
event time height speed
liftoff 0.00 s 0.4 m 0.0 m/s
rail exit 0.12 s 1.9 m 26.3 m/s
burnout 2.14 s 406.4 m 240.4 m/s
apogee 11.62 s 1065.1 m 0.1 m/s
charge 12.14 s 1063.7 m 5.1 m/s
deployment 12.14 s 1063.7 m 5.1 m/s
ground hit 245.78 s 0.0 m 4.4 m/s
(heights are the center of gravity's above the site; speeds are over the ground)
top speed 285.6 m/s at 1.73 s
top Mach number 0.842
landing 0.7 m from the pad at 245.78 s, at 4.4 m/s
A calibre here is the rocket’s widest body diameter, 66 mm, so a margin of 1.70 calibres puts the
center of pressure 1.70 × 66 mm ≈ 112 mm behind the center of gravity. hpr computes the static
margin at Mach 0, with the air along the rocket’s axis. The margin grows
as propellant burns and the center of gravity moves forward, so its least value is usually the
one at the rail. --json also gives the flight margin, at the flight’s own Mach number
(min_flight_margin_cal), and where each least value falls
(Flight metrics).
Check every motor you might fly
The margin depends on the motor, so check each one you might fly, with its delay. A motor’s mass sits behind the center of gravity, so a heavier motor moves the center of gravity aft and shrinks the margin. The guide rocket’s second configuration flies an I175WS:
$ hpr sim validation/fixtures/ork/guides/level-1.ork --config 2
Level 1 guide rocket (level-1.ork)
configuration 2 of 2: [I175WS-9]
help: --config flies the others, by number or name: 1 [H170M-10]
static margin 1.67 calibres off the rail; least 1.67 calibres, at 0.14 s, before apogee
apogee 1100.7 m above the site at 11.48 s
rail exit speed 28.9 m/s
delay apogee 9.51 s after burnout; the motor's set delay: 9 s; its charge fires 0.51 s before apogee
descent 4.5 m/s at landing under `Parachute`
motor: 1 × I175WS (the design's) in `Motor mount tube`, lit at launch
recovery: `Parachute` at the ejection charge, 0.520 m² of drag area, opened at 10.97 s
launched at 0° N, 0° E, 0 m above sea level, from a 1.5 m vertical rail, in calm air
help: see the Accuracy page before trusting these numbers: https://nrdptel.github.io/hpr-sim/accuracy.html
note: the file's 1 recovery device flies as OpenRocket flies it: each opens fully at its event, with the file's drag coefficient or OpenRocket's own, and once one opens the rocket descends as a point under the open devices' drag alone
warning: drag: issue #67: a cone-like nose's or shoulder's pressure drag reads high from Mach 0.8 (about twice a measured cone's at Mach 0.85, still +15% at 1.5), so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are; this flight reaches Mach 0.83 at 1.8 s (https://github.com/nrdptel/hpr-sim/issues/67)
warning: drag: issue #68: base drag reads high from Mach 0.8 to 1.2 (0.225 against a measured 0.156 at Mach 0.9), so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are; this flight reaches Mach 0.83 at 1.8 s (https://github.com/nrdptel/hpr-sim/issues/68)
warning: drag: issue #18: skin friction is taken as fully turbulent, but on a smooth surface the flow stays laminar near the nose, where friction is lower: hpr's reads high by 3.6% on RocketPy's Calisto at Mach 0.3; if this surface is that smooth, the drag reads high, so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are (https://github.com/nrdptel/hpr-sim/issues/18)
warning: stability: issue #172: the static margin read up to 0.1108 calibres higher than OpenRocket's on four private designs, for a reason not yet found, so this flight's margin may read high by as much (https://github.com/nrdptel/hpr-sim/issues/172)
event time height speed
liftoff 0.02 s 0.4 m 0.0 m/s
rail exit 0.14 s 1.9 m 28.9 m/s
burnout 1.97 s 385.2 m 260.5 m/s
charge 10.97 s 1099.1 m 8.0 m/s
deployment 10.97 s 1099.1 m 8.0 m/s
apogee 11.48 s 1100.7 m 0.0 m/s
ground hit 247.48 s 0.0 m 4.5 m/s
(heights are the center of gravity's above the site; speeds are over the ground)
top speed 281.3 m/s at 1.75 s
top Mach number 0.830
landing 0.7 m from the pad at 247.48 s, at 4.5 m/s
--motor flies any other motor in the mount, and --delay sets its ejection delay
(Pick a motor). There, the heaviest motor tried, a Loki
I377, leaves the guide rocket the smallest margin of the motors flown, and takes it past Mach 1,
where the guidance below asks for more.
Compare with published guidance
Published rules of thumb set a lower bound of one calibre, and more for fast flights; these are the sources’ guidance, not hpr’s judgement.
- James Barrowman, who devised the standard way to find the center of pressure, wrote: “A good rule of thumb is to have the static margin equal to the largest diameter of the rocket”, and “Remember that one maximum body diameter is the smallest safe static margin” (Stability of a Model Rocket in Flight, Centuri Technical Information Report TIR-30, 1970, as reprinted in this scanned compilation, PDF pages 73 and 76).
- OpenRocket’s user guide: “The recommended stability margin for subsonic flights is not less than 1.0 caliber, and not less than 2.0 calibers for transonic and supersonic flights” (Overrides and surface finish).
- NASA’s Student Launch handbook for 2027 asks for “a minimum static stability margin of 2.0 while sitting on the pad”.
- The Spaceport America Cup’s design guide (2026, version 1.1) states its limits as a share of the rocket’s length instead: at least 7.5% through a subsonic flight, at most 18% at launch.
A large margin isn’t free. A very stable rocket turns into a crosswind as it leaves the rail (weathercocking), and so flies off the vertical.
Rail exit speed
The rocket must leave the rail fast enough to fly straight, and the codes name no figure. The NAR’s code asks for “rigid guidance until the rocket has attained a speed that ensures a stable flight”; Tripoli’s, 7-3, for guidance “until it has reached the velocity necessary for stable flight”. Competitions set numbers: the Spaceport America Cup’s guide asks for at least 25 m/s (5.3.1), and NASA’s Student Launch handbook for 52 ft/s (15.8 m/s).
The reason is the wind. Just off the rail, a crosswind meets the rocket at an angle whose tangent
is the wind speed over the rocket’s speed. At the guide rocket’s 26.3 m/s in a 4 m/s crosswind,
that is about 8.6°, since 4 / 26.3 = 0.152 and the angle whose tangent is 0.152 is 8.6°. The
larger that angle of attack, the further the rocket turns into the
wind, and hpr’s aerodynamics hold only at small angles. Both of hpr’s margins are taken with the air
along the axis, at no angle at all. A longer rail raises the exit speed: --rail-length sets it,
and --wind the wind (Fly your .ork).
Fly the rocket you built
A design’s center of gravity is a calculation; your built rocket’s is a measurement, and the
codes ask you to demonstrate it. Weigh the finished rocket, ready to fly but without its motor,
and balance it to find its center of gravity. In OpenRocket, set that mass and center of gravity
as overrides on the stage, covering its parts, save, and fly the file
again in hpr, which reads those overrides (the .ork page). Leave the motor out
of the measurement: an override never covers a motor, in hpr or OpenRocket, and both add the
motor’s own mass, so a rocket weighed with its motor in would carry it twice. The margin then
rests on your rocket’s measured mass, not the drawing’s.
How far to trust the margin
hpr’s margin has been compared with OpenRocket’s, not with a measured rocket. Both programs start from Barrowman’s method, so agreement says hpr computes it as OpenRocket does, not that either is right (Accuracy).
- OpenRocket’s examples: within 0.016 calibres on 41 of 53 flights, with hpr’s margin taken with the air along the rocket’s axis, as OpenRocket’s is. The margin hpr prints is the weakest plane’s, which on a rocket with a fin set of one or two fins can be lower.
- The three-stage example: on its three flights hpr’s margin is 0.039 to 0.058 calibres below OpenRocket’s. hpr’s center of mass at rod clearance sits 0.043 to 0.062 calibres further aft than OpenRocket’s there, where OpenRocket’s recorded mass reads light (#185); that cause is not yet sized.
- The tube-fin example: a rocket with tube fins is 1.08 calibres below OpenRocket’s: hpr gives 0.79 calibres to OpenRocket’s 1.87. Neither program has been checked against a measured tube-fin rocket; check such a design in both and treat the smaller margin as the more cautious figure, not a bound.
- The pods-and-winglets example: on the five flights of Pods–airframes and winglets, hpr’s margin is 0.071 to 0.076 calibres above OpenRocket’s, the flattering side: hpr calls the rocket more stable than OpenRocket does. Two causes are open: how the two programs count fins that interfere across sets (#325), and the center of pressure of its kinked freeform wings (#326). A flight whose fin sets share a station, more than four fins between them, warns of #325, and a flight with a freeform fin set warns of #326. On a design with pods or freeform fins, check the margin in both and treat the smaller as the more cautious figure, not a bound. The three flights of Pods–powered with recovery deployment read 0.070 calibres above OpenRocket’s too, cause not yet traced.
- Private designs: on 35 flights of 11 designs, hpr’s margin runs from 0.0166 calibres below OpenRocket’s to 0.1108 above. Four designs read 0.0350 to 0.1108 calibres more stable in hpr than in OpenRocket, none with a measured cause yet (#172 and #186, two of the open causes; the private designs). A fifth, launched from a tilted rod, reads 0.0125 to 0.0366 above, most of it traced to the tilt. Reading more stable is the flattering side, so leave room for it near any limit.
- Near the speed of sound: between Mach 0.8 and 1.2, hpr’s center of pressure is up to 2.36 calibres behind NASA’s wind-tunnel data, and wherever it misses by more than half a calibre it sits behind, so the flight margin there reads high: the flattering side (Known gaps). The static margin is computed at Mach 0, so it says nothing about the rocket’s stability in that range; use the transonic guidance above.
Recording a trajectory
This page shows how to get a whole flight out of hpr-sim as a table of numbers over time, which a spreadsheet or a plotting tool reads. It flies the rocket of Getting started again, in the same weather, and keeps its height, speed and position every 5 seconds. It needs the first page’s setup, and a little Rust.
These numbers are not validated. They are the first flight’s, and How far to trust it on that page applies to them too.
Run it
cargo run --example trajectory -p hpr-sim > trajectory.csv
This runs
crates/hpr-sim/examples/trajectory.rs
and saves what it prints in trajectory.csv, a CSV (comma-separated values) file: a header line
naming each column, then one line per moment of the flight. It holds this:
time_s,height_above_ground_m,vertical_speed_m_s,airspeed_m_s,cg_east_m,cg_north_m,cg_up_m
0.000,0.9,0.0,5.0,0.0,0.0,0.9
0.002,0.9,0.0,5.0,0.0,0.0,0.9
0.371,3.9,16.2,17.0,0.0,0.0,3.9
3.259,216.0,110.6,111.4,-9.7,0.0,216.0
5.000,390.8,90.3,91.3,-24.1,0.0,390.8
10.000,707.8,37.5,39.4,-62.1,0.0,707.8
13.830,778.7,0.0,11.4,-88.7,0.0,778.7
14.330,777.6,-4.5,12.0,-91.9,0.0,777.6
15.000,772.6,-10.2,14.3,-95.6,0.0,772.6
20.000,665.5,-26.4,26.5,-98.4,0.1,665.5
25.000,531.1,-27.0,27.0,-78.6,0.1,531.1
30.000,396.4,-26.8,26.8,-54.4,0.1,396.4
35.000,262.6,-26.7,26.7,-29.5,0.1,262.6
39.234,150.0,-26.5,26.5,-8.4,0.1,150.0
40.000,129.7,-26.5,26.5,-4.5,0.0,129.7
40.234,123.5,-26.5,26.5,-3.3,0.0,123.5
45.000,89.0,-6.4,6.4,20.5,0.0,89.0
50.000,57.0,-6.4,6.4,45.5,0.0,57.0
55.000,25.1,-6.4,6.4,70.5,0.0,25.1
58.933,0.0,-6.4,6.4,90.2,0.0,0.0
Like the first flight’s, this output is committed in
trajectory.output.txt,
and CI runs the program on macOS, Windows and Linux and fails if it prints anything else.
This program rounds as it prints. To save a recording with every digit, as CSV or JSON, or the flight path as a map, see Exporting a flight.
Reading the table
| column | what it is | unit |
|---|---|---|
time_s | Time since the motor lit | s |
height_above_ground_m | The height of the center of gravity above the launch pad | m |
vertical_speed_m_s | How fast that height changes: positive going up, negative coming down | m/s |
airspeed_m_s | The rocket’s speed through the air, which the wind adds to or takes from | m/s |
cg_east_m, cg_north_m, cg_up_m | Where the center of gravity is: meters east, north and up of the pad, in the launch frame | m |
There is a row every 5 seconds, and one at every event, at the moment it happened:
| time (s) | event |
|---|---|
| 0.002 | liftoff: the push up the rail first beats the weight, and the rocket starts to move |
| 0.371 | the rocket leaves the 3 m rail |
| 3.259 | burnout |
| 13.830 | apogee; the drogue’s charge fires at the same moment, so it shares this row |
| 14.330 | the drogue opens, half a second later |
| 39.234 | the main’s charge fires, as the rocket falls past 150 m |
| 40.234 | the main opens, a second later |
| 58.933 | landing |
What the numbers show:
- On the pad the airspeed is 5.0 m/s with the rocket standing still: that is the wind.
- The rocket climbs into the wind. The wind blows from the west, and off the rail a stable
rocket turns its nose toward the wind it feels
(weathercocking), so it drifts west:
cg_east_mis −88.7 m at apogee. - Under the drogue alone it falls at about 27 m/s, and the wind carries it east.
- The main slows it from 26.5 to 6.4 m/s between the row where it opens (40.234) and the next (45.000), and it lands at 6.4 m/s, 90.2 m east of the pad.
cg_north_mshows 0.1 m for a while, with no wind from the south. The Earth’s rotation nudges a moving rocket sideways, to the right of its motion in the northern hemisphere (Coriolis acceleration). While the rocket moves west, that is north: it drifts up to 6.3 cm, which the table rounds to 0.0 or 0.1 m.cg_up_mandheight_above_ground_magree here, and don’t in general. The launch frame is a flat plane, and the Earth curves away below it, so far from the padcg_up_mreads low: on the ground 10 km from the pad, it is −7.8 m. At this landing, 94 m out, it is under a millimeter low. For heights, useheight_above_ground_m.
Record something else
The program below makes its recorder with
Recorder::new(vec![Channel::Time, Channel::HeightAboveGround, ...], Some(5.0)), and passes it to
run, which feeds it every step of the flight. Change either argument:
- The interval is
Some(5.0), a row every 5 s.Some(0.1)gives a smooth plot, about 600 rows for this flight.Nonekeeps a row at the end of every step the integrator takes, which is as fine as the flight was computed (Time integration). - The channels are the columns. Others include the Mach number, the
angle of attack, the dynamic pressure,
the thrust, the mass, the attitude and the rotation rates. The
Channelpage of the API reference lists every one with its unit, andChannel::ALLkeeps them all. - After the flight,
recorder.columns()gives the column names, units included, andrecorder.rows()gives one list of numbers per row, in the same order.
To save a file straight from Rust instead of printing, write the same lines to a
std::fs::File.
The program
This is the whole program, line for line the file CI runs. Everything above the recorder is the first flight’s setup, which Getting started explains step by step.
//! A trajectory to plot: the flight of `first_flight.rs`, recorded every 5 s and at every event,
//! printed as CSV (comma-separated values), which a spreadsheet or a plotting tool reads.
//!
//! Run it from anywhere in the repository, and save what it prints to a file:
//!
//! ```text
//! cargo run --example trajectory -p hpr-sim > trajectory.csv
//! ```
//!
//! The documentation site's *Recording a trajectory* page (`docs/recording-a-trajectory.md`)
//! walks through it. What it prints is kept next to it in `trajectory.output.txt`, and CI checks
//! that the two still agree (`cargo xtask examples --check`).
#![allow(
clippy::print_stdout,
reason = "the project's lints forbid printing in library code, and this program exists to print"
)]
use std::error::Error;
use hpr_atmos::ConstantWind;
use hpr_core::geodesy::Geodetic;
use hpr_design::Rocket;
use hpr_sim::{
CanopyType, Channel, Device, DeviceDrag, Environment, FlightSettings, Rail, Recorder,
Simulation, Termination, Trigger,
};
fn main() -> Result<(), Box<dyn Error>> {
// The first flight: Valetudo on a K400C, from a 3 m vertical rail in New Mexico, in 5 m/s of
// wind from the west, with a drogue at apogee and a main at 150 m.
let rocket: Rocket = serde_json::from_str(include_str!(
"../../../validation/designs/rocketpy-valetudo.json"
))?;
let site = Geodetic::from_degrees(32.99, -106.97, 1400.0)?;
let environment =
Environment::standard(site)?.with_wind(ConstantWind::new(5.0, 270_f64.to_radians())?);
let simulation = Simulation::new(
&rocket,
"example",
environment,
Rail::vertical(3.0),
FlightSettings::default(),
)?
.with_recovery(vec![
Device::new(
"drogue",
DeviceDrag::canopy(CanopyType::FlatCircular, 0.6),
Trigger::Apogee,
)
.with_lag_s(0.5),
Device::new(
"main",
DeviceDrag::canopy(CanopyType::FlatCircular, 2.4),
Trigger::Altitude {
height_above_ground_m: 150.0,
},
)
.with_lag_s(1.0),
])?;
// What to keep, and how often: the time, the height above the pad, the vertical speed, the
// airspeed and where the center of gravity is (meters east, north and up of the pad), every
// 5 s. A recorder also keeps a row at every event, such as burnout or a parachute opening.
// For a smooth plot, use `Some(0.1)`; `None` keeps a row at the end of every step.
let mut recorder = Recorder::new(
vec![
Channel::Time,
Channel::HeightAboveGround,
Channel::VerticalSpeed,
Channel::Airspeed,
Channel::CgPosition,
],
Some(5.0),
)?;
let flight = simulation.run(&mut recorder)?;
if flight.termination != Termination::GroundHit {
return Err(format!("the flight ended with {:?}", flight.termination).into());
}
// One header line with each column's name and unit, then one line per row: the time to
// 0.001 s, the rest to 0.1.
println!("{}", recorder.columns().join(","));
for row in recorder.rows() {
let fields: Vec<String> = row
.iter()
.enumerate()
.map(|(column, &value)| rounded(value, if column == 0 { 3 } else { 1 }))
.collect();
println!("{}", fields.join(","));
}
Ok(())
}
/// `value` to `decimals` places, without the minus sign of a value that rounds to zero. Some values
/// that are zero in principle come out a hair below it: the vertical speed at ignition and at
/// apogee, the landing height, which is found just below the ground, and the landing's `cg_up_m`,
/// under a millimeter below the pad's level because the ground curves away.
fn rounded(value: f64, decimals: usize) -> String {
let text = format!("{value:.decimals$}");
match text.strip_prefix('-') {
Some(unsigned) if unsigned.chars().all(|c| c == '0' || c == '.') => unsigned.to_owned(),
_ => text,
}
}
Exporting a flight
This page shows how to save a flight as files other programs read: a table of numbers over time for a spreadsheet or a plotting tool (CSV, JSON or Parquet), and a map of the flight path and the landing (GeoJSON or KML, which Google Earth, QGIS and most web maps open). It flies the rocket of Getting started again and writes all five files. It needs the first page’s setup, and a little Rust.
Without writing Rust, hpr sim --export writes the same five formats for a .ork or hpr design
file, every quantity hpr tracks in each (The command line).
Its maps mark the landing when a parachute or streamer opened, as a .ork file’s do, and no
landing otherwise (the landing).
The files are exact, the flight is not validated. Every number in a file reads back to exactly the value the simulator computed, and tests check that. The flight itself is the first flight’s, and How far to trust it on that page applies to it too.
Run it
cargo run --example export_flight -p hpr-sim --features parquet -- my-flight
This runs
crates/hpr-sim/examples/export_flight.rs,
which writes five files into the folder my-flight (without a folder, into hpr-sim-export in
the system’s temporary folder) and prints what it wrote. Parquet is an optional
feature of hpr-sim, named parquet,
so the command turns it on; without it, cargo says the example needs it. The feature adds no other
library; it is optional so that a program with no use for a binary file format leaves it out. In
your own program, turn it on in Cargo.toml with hpr-sim = { ..., features = ["parquet"] }.
wrote 67 rows of 5 columns, a path of 67 points
flight.csv
flight.json
flight.parquet, 3203 bytes
flight.geojson
flight.kml
landed at 32.99000° N, 106.96904° W, 90 m from the pad, at 58.9 s
CI runs it on macOS, Windows and Linux and fails if it prints anything else. The files hold every number to its last digit, which may differ in the last place between operating systems, so the program prints a rounded summary instead of the files.
The five files
| file | what it holds | opens in |
|---|---|---|
flight.csv | a header naming each column with its unit (time_s, cg_east_m, …), then one line per recorded moment, lines ending in CRLF as RFC 4180 says | a spreadsheet, pandas, any plotting tool |
flight.json | the same table as {"tool": {...}, "columns": [...], "rows": [[...], ...]} | any programming language |
flight.parquet | the same table in Apache Parquet, a binary format that stores it column by column: one column of 64-bit numbers per recorded column, with the CSV header’s names, and the program that wrote it in the file’s key-value metadata | pandas with pyarrow (Apache Arrow’s Python library), DuckDB |
flight.geojson | the flight path as a line, and a point for each landing: the rocket’s and any separated body’s; and a tool object naming the program that wrote it | QGIS, geojson.io, web maps |
flight.kml | the same path and landing | Google Earth |
Which program wrote a file
Each file but the CSV names the program that wrote it, its version and its designation in the FusionSpace product system, the way a drawing’s title block names the tool that made it:
- JSON and GeoJSON: a
toolobject at the top,{"name": "hpr-sim", "version": "0.1.0", "designation": "FS · SW · TOOL 005"}. In GeoJSON it is a foreign member, a key the standard doesn’t define, which RFC 7946 (section 6.1) allows and map programs pass over. - KML: a comment on the second line,
<!-- hpr-sim 0.1.0 · FS · SW · TOOL 005 -->. - Parquet: three entries of the file’s key-value metadata:
tool(hpr-sim),tool_versionanddesignation. pyarrow shows them withpyarrow.parquet.read_metadata(path).metadata.
The CSV holds only the header and the rows, so a spreadsheet opens it cleanly. hpr sim writes
its title block beside it instead: flight.meta.json for flight.csv, with the same tool
object, the CSV’s file name, the design and configuration flown, and the number of rows. The
library’s export::csv writes the CSV alone.
To look at the Parquet file from Python, install pandas and pyarrow (pip install pandas pyarrow;
pandas can’t read Parquet on its own), then:
python -c "import pandas; print(pandas.read_parquet('my-flight/flight.parquet'))"
or, with DuckDB’s command-line program, duckdb -c "SELECT * FROM 'my-flight/flight.parquet'".
The tables hold whatever channels the
recorder kept, one row per recorded moment. The maps need the recorder to keep the time and the
center of gravity’s position (Channel::Time and Channel::CgPosition); they place each recorded
position on the Earth, with the same conversion as the landing point in
Flight metrics.
A map line in KML looks like this, one longitude,latitude,height per recorded moment (the digits
here are cut short):
<LineString>
<altitudeMode>absolute</altitudeMode>
<coordinates>
-106.97,32.99,1400.93591...
-106.97,32.98999...,1403.85869...
Heights: two datums
A height needs something to count from, a datum. The two map formats use different ones, because their standards say so:
- GeoJSON heights are above the WGS 84 ellipsoid, the smooth shape GPS uses (ellipsoidal height; RFC 7946, section 4).
- KML heights with
altitudeModeabsoluteare above sea level, which KML takes from the EGM96 geoid (height above sea level; OGC KML 2.2, 07-147r2).
Sea level sits above or below the ellipsoid by the
geoid undulation N, up to about 100 m. hpr has no
model of it, so it uses the value the flight was given (Environment::with_geoid_undulation_m,
zero unless set), the same for every point of the flight. The geoid’s slope, about 5 cm per
kilometer and up to some 30 cm in mountains, moves it by centimeters to decimeters over a rocket’s
few kilometers. For example, at a site where sea level
is 25 m below the ellipsoid (N = −25 m), a point 1500 m above the ellipsoid is written as 1500 m
in GeoJSON and as 1525 m in KML. The first flight leaves N at zero, so its two files agree; at the real
site sea level is some tens of meters below the ellipsoid, so give N for heights you mean to
trust.
How the files are checked
- Numbers: each is written in the shortest form that reads back to the same number, and the
tests read every file back and compare it with the recording exactly, not within a tolerance. A
value that isn’t a finite number is refused with an error rather than written as
NaNornull, which different programs read differently. - GeoJSON: checked against the published GeoJSON schema (geojson.org/schema), with longitude before latitude as the standard requires. A test also shows the check rejects a broken file.
- KML: parsed by a strict XML parser, and checked for the KML 2.2 namespace, the height mode and every coordinate.
- Parquet: hpr-sim writes the file itself, from the format’s specification. The tests read it back with an independent reader, Apache’s own Parquet library for Rust, and compare every number bit for bit. One test uses a recording of every channel (30 columns today) long enough to fill at least three data pages per column, and another compares a small file with bytes worked out by hand from the specification. Once, by hand, when this was written, pyarrow 25.0.1 and DuckDB 1.5.5 each read the example’s file and a 30-column, 3005-row file and got the same numbers as the matching CSV. CI does not repeat that check.
The tests are in
crates/hpr-sim/src/export.rs
and
crates/hpr-sim/src/export/parquet.rs.
What it leaves out
- Parquet compression and statistics: the Parquet file is not compressed and carries no
per-page minimum and maximum. It takes 8 bytes per number, plus a header of about 20 bytes per
page of up to 1024 numbers and a short index of the columns at the end of the file: the
example’s is 3203 bytes, and its CSV about 5.5 kB. Without the minimums and maximums, a tool
such as DuckDB can’t skip parts of the file when filtering (say
WHERE time_s > 10) and reads it all, which for one flight takes no noticeable time. - A path over the antimeridian (±180° longitude) is not cut in two as RFC 7946 asks, so a map would draw it the long way round the globe.
- Landing heights: a landing is a point on the ground, without a height.
- A summary file: the flight’s peaks and margins (Flight metrics) are
not in these files. A
FlightSummaryconverts to JSON on its own withserde_json::to_string, but that path writes a value that isn’t finite asnullrather than refusing it.
The choices are recorded in ADR-079: exports as text built in the core and ADR-080: Parquet written in-house, read back by Apache’s library.
The program
This is the whole program, line for line the file CI runs. Everything above the recorder is the first flight’s setup, which Getting started explains step by step.
//! A flight written out for other programs: the flight of `first_flight.rs`, recorded every 1 s
//! and at every event, saved as CSV, JSON and Parquet tables and as a GeoJSON and a KML map of its
//! path and landing.
//!
//! Run it from anywhere in the repository, naming the folder to write the five files to. Parquet
//! is an optional feature of the library, so the command turns it on:
//!
//! ```text
//! cargo run --example export_flight -p hpr-sim --features parquet -- my-flight
//! ```
//!
//! Without a folder it writes them to `hpr-sim-export` in the system's temporary folder. The
//! documentation site's *Exporting a flight* page (`docs/exporting-a-flight.md`) walks through it.
//! What it prints is kept next to it in `export_flight.output.txt`, and CI checks that the two
//! still agree (`cargo xtask examples --check`).
#![allow(
clippy::print_stdout,
reason = "the project's lints forbid printing in library code, and this program exists to print"
)]
#![allow(
clippy::disallowed_methods,
reason = "the library builds each file's text without I/O; this program writes the files"
)]
use std::error::Error;
use std::path::PathBuf;
use hpr_atmos::ConstantWind;
use hpr_core::geodesy::Geodetic;
use hpr_design::Rocket;
use hpr_sim::{
CanopyType, Channel, Device, DeviceDrag, Environment, FlightMetrics, FlightSettings, Rail,
Recorder, Simulation, Termination, Trigger, export,
};
fn main() -> Result<(), Box<dyn Error>> {
// The first flight: Valetudo on a K400C, from a 3 m vertical rail in New Mexico, in 5 m/s of
// wind from the west, with a drogue at apogee and a main at 150 m.
let rocket: Rocket = serde_json::from_str(include_str!(
"../../../validation/designs/rocketpy-valetudo.json"
))?;
let site = Geodetic::from_degrees(32.99, -106.97, 1400.0)?;
let environment =
Environment::standard(site)?.with_wind(ConstantWind::new(5.0, 270_f64.to_radians())?);
let simulation = Simulation::new(
&rocket,
"example",
environment,
Rail::vertical(3.0),
FlightSettings::default(),
)?
.with_recovery(vec![
Device::new(
"drogue",
DeviceDrag::canopy(CanopyType::FlatCircular, 0.6),
Trigger::Apogee,
)
.with_lag_s(0.5),
Device::new(
"main",
DeviceDrag::canopy(CanopyType::FlatCircular, 2.4),
Trigger::Altitude {
height_above_ground_m: 150.0,
},
)
.with_lag_s(1.0),
])?;
// Two watchers on one flight: the metrics, for the landing, and a recorder of the time, the
// height above the pad and the center of gravity's position, which a map needs.
let recorder = Recorder::new(
vec![
Channel::Time,
Channel::HeightAboveGround,
Channel::CgPosition,
],
Some(1.0),
)?;
let mut watchers = (FlightMetrics::new(), recorder);
let flight = simulation.run(&mut watchers)?;
if flight.termination != Termination::GroundHit {
return Err(format!("the flight ended with {:?}", flight.termination).into());
}
let (metrics, recorder) = watchers;
let summary = metrics.summary(&flight, simulation.environment())?;
// The path on the Earth, then the five files. Each function returns the file's contents:
// text, or bytes for Parquet, which is a binary format.
let track = export::track(&recorder, simulation.environment())?;
let files = [
("flight.csv", export::csv(&recorder)?.into_bytes()),
("flight.json", export::json(&recorder)?.into_bytes()),
("flight.parquet", export::parquet(&recorder)?),
(
"flight.geojson",
export::geojson(&track, &summary)?.into_bytes(),
),
(
"flight.kml",
export::kml(&track, &summary, "Valetudo, K400C")?.into_bytes(),
),
];
let folder = match std::env::args_os().nth(1) {
Some(folder) => PathBuf::from(folder),
None => std::env::temp_dir().join("hpr-sim-export"),
};
std::fs::create_dir_all(&folder)?;
for (name, contents) in &files {
std::fs::write(folder.join(name), contents)?;
}
// What was written, and where it landed, to five decimal places of a degree (about a meter).
// The site is west of Greenwich, so the longitude is printed as degrees west.
println!(
"wrote {} rows of {} columns, a path of {} points",
recorder.rows().len(),
recorder.columns().len(),
track.len()
);
for (name, contents) in &files {
// The Parquet file's size is the same on every system: its numbers take 8 bytes each,
// however many digits they have.
if name.ends_with(".parquet") {
println!(" {name}, {} bytes", contents.len());
} else {
println!(" {name}");
}
}
if let Some(landing) = summary.landing {
println!(
"landed at {:.5}° N, {:.5}° W, {:.0} m from the pad, at {:.1} s",
landing.latitude_deg, -landing.longitude_deg, landing.distance_m, landing.time_s
);
}
Ok(())
}
The builder
This page shows the shortest way to fly a rocket of your own with hpr-sim. The hpr
crate has four types for it: Environment, Motor, Rocket and Flight.
You describe the rocket part by part from the nose back, put a motor in it, and fly it from a
rail. The page runs four example programs and walks through them. It needs the setup from
Getting started, and some Rust.
How far to trust it. The builder adds no physics. It writes the same design tree that Your own rocket writes by hand, and flies it with the same simulation. A test builds that page’s rocket with the builder and flies it: the mass properties and the whole flight come out the same, bit for bit (
the_builder_makes_the_rocket_own_rocket_builds_by_hand). So what that page says about trusting its numbers holds here. The masses come from each part’s shape and a published density, so glue, paint and hardware are missing until you weigh the parts. This rocket has never been flown for real, so no flight checks it. Where it lands in a wind is the least certain number of all: on two of RocketPy’s example rockets, hpr’s drift and RocketPy’s differ by 11 to 43% (Getting started explains why).
Run it
cargo run --example build_and_fly -p hpr
This builds the small rocket of Your own rocket. Its airframe is 54 mm inside and 56.3 mm outside, and it takes 29 mm motors. It weighs the rocket, and finds its center of gravity (CG), its center of pressure (CP) and its stability margin. Then it flies the rocket on a Cesaroni H54 from a rail leaning into a light wind, with a parachute that opens at the motor’s ejection delay. It prints this:
My 54 mm rocket on a 168H54-10A
Not yet validated: see the Accuracy page before trusting these numbers.
liftoff spent
mass (kg) 0.675 0.579
center of gravity (m from nose) 0.671 0.610
center of pressure (m from nose) 0.779 0.779
stability margin (calibres) 1.92 2.99
From a 1.8 m rail leaning 5° west, in 5 m/s of wind from the west:
Rail exit: 21.7 m/s
Apogee: 1106.6 m above the pad, at 13.69 s, 282 m west of it
Top speed: 186 m/s (Mach 0.56)
Landing: 877 m east of the pad, at 4.6 m/s, at 246.3 s
What the lines say:
- Liftoff and spent. The rocket loses 96 g of propellant as the motor burns. That moves the CG 6 cm forward, and the margin grows from 1.92 to 2.99 calibres.
- The CP is worked out by Barrowman’s method at Mach 0.3, a typical subsonic speed, with the air straight along the rocket. It depends on the shape alone, so it doesn’t move as the motor burns.
- The flight. The rail exit speed is how fast the rocket leaves the rail. The rail leans 5° west, into the wind, and the rocket turns further into the wind as it climbs, a rocket’s usual weathercocking. The lean and the turn together put its apogee 282 m west of the pad. Under the parachute the wind carries it back, to land 877 m east of the pad.
- Heights are the height of the rocket’s CG above the launch site. The CG starts above the ground, sitting on the rail, so the apogee includes that starting height.
The program
The program is
crates/hpr/examples/build_and_fly.rs.
It takes four steps: the motor, the rocket, the weighing and the flight.
The motor
Motor::from_catalog("H54") takes a motor from the catalog built into hpr-sim. It finds the motor
by its designation, such as 168H54-10A, or by its common name, the short form such as H54,
and ignores case, spaces and hyphens. Only the 32 motors that come with a thrust curve can be
found; the motor page lists them. A name that matches two motors, as I175
does, is refused with both listed rather than guessed. The delay is never read from the
designation: with_delay_s(10.0) sets it, and a parachute opened by the motor’s charge needs it.
Motor::from_eng reads the text of a RASP .eng file, such as one downloaded
from ThrustCurve.org, instead.
The rocket
Rocket::new(name, diameter_m) starts an empty rocket with an outer body diameter. Then each
add_ call adds one part and returns the rocket, so the calls chain. Body parts stack from the
nose tip in the order you add them. The parts that go inside or on the airframe attach to the last
body tube you added:
| part | what it is | where it goes |
|---|---|---|
Nose::hollow, Nose::solid | A nose cone of a shape and length, and its wall. with_shoulder adds the sleeve that fits inside the tube behind; with_capped_shoulder closes the sleeve’s aft end with a disc | First, at the tip. Its base takes the rocket’s diameter |
Tube::new | A body tube of a length and a wall. with_diameter_m gives it another diameter | Behind the last body part, at its diameter |
Transition::conical | A cone to a new diameter at its aft end: a boattail, or a step up or down in the airframe | Behind the last body part, starting at its diameter |
Fins::new | A set of identical fins of a shape (FinPlanform, which names each dimension), square-edged unless with_cross_section says otherwise | On the last tube, flush with its aft end unless at places them |
MotorTube::new | The tube the motor goes in: its length, bore and wall | In the last tube, flush with its aft end unless at places it |
Mass::new | Anything else inside: a recovery bay, an altimeter, ballast | In the last tube, where its position puts it |
A position (Position) is measured along the tube the part is on: Top places the part’s fore end
a distance aft of the tube’s, Bottom its aft end from the tube’s aft end, Middle its middle
from the tube’s middle, After its fore end behind the part before it, and Absolute its fore end
from the nose tip. So a Mass given a size with packed has its center half that length from the
end its position names, or at the point itself when placed by its Middle. In the example,
packing the recovery bay 15 cm long, its top 7 cm down the tube, moves the rocket’s CG at liftoff
22 mm aft of where a point mass at the top puts it.
Every part names its material, and every hollow part its wall. material("abs") finds one of the
built-in materials, each with the source of its density; the mass page explains
how the masses are found. The builder has no default material or wall, because each would be a
guess at your rocket’s mass. The defaults it does have change the drag, not the mass: fins have
square edges, and every outer surface has the design’s default finish, mass-production paint,
which sets the friction of the air on it (Drag). The builder can’t change the finish yet.
set_motor puts the motor in the motor tube, lit at launch. add_parachute adds a recovery device:
here a 90 cm flat parachute opened by the ejection charge of motor number 0, the first,
Trigger::MotorDelay { motor: 0 }. A device adds drag, not mass; the parachute’s mass is in the
200 g recovery bay. The recovery page explains the devices and what opens
them.
Weighing it
mass_properties(t) gives the mass, CG and inertia t seconds after the motor lights.
margin(t, mach) gives the CP and the margin, and static_margin_cal(t, mach) the margin alone.
The CG is in the body frame, whose z axis points to the nose, so a
point 0.671 m behind the nose tip is at z = −0.671. The CP is a station, meters aft of the tip,
so the example prints it as it comes and the CG with its sign turned. Weighing runs the same
checks on the design as a flight does, so a motor wider than its tube is refused by both.
Flying it
Environment::new(latitude, longitude, elevation) is the launch site, in degrees north, degrees
east (so the Americas are negative) and meters. It has the
standard atmosphere and no wind; with_constant_wind(5.0, 270.0)
adds 5 m/s blowing from 270°, the west. The elevation is taken both as the height above sea level,
where the air is read, and as the height above the ellipsoid.
Flight::builder(&rocket, &environment, 1.8) sets up a flight from a vertical 1.8 m rail.
inclination_deg is the rail’s angle above the horizon: 90 is vertical, and 85 leans 5° off it.
OpenRocket measures its launch rod angle from the vertical instead, so its 5° is 85 here.
heading_deg is the way the rail leans, clockwise from true north (add the
declination to a compass reading). A wind’s direction is where it
comes from, so a rail leaning into a west wind has both at 270. fly() flies the rocket to the
ground. The flight’s apogee_m, max_speed_m_s, rail_exit_speed_m_s and landing are the
numbers most asked for; summary() has every metric the metrics page
describes.
Which motor
The second example flies the same rocket on each 29 mm motor that comes with hpr-sim, from a vertical rail, with the parachute opening at apogee whatever the delay:
cargo run --example motor_choice -p hpr
My 54 mm rocket from a 1.8 m vertical rail, in 5 m/s of wind from the west
Not yet validated: see the Accuracy page before trusting these numbers.
motor liftoff margin rail exit apogee top speed best delay
(kg) (cal) (m/s) (m) (m/s) (s)
F15 0.569 3.00 10.0 206.9 55 4.6
F52C 0.548 3.32 18.5 438.6 106 8.0
168H54-10A 0.675 1.92 21.7 1134.6 186 10.5
set_motor swaps the motor in the tube, so one rocket flies on all three. The H54 reaches
1134.6 m here, from a vertical rail in the wind. The same rocket reaches 1144.5 m on
Your own rocket, from a vertical rail in calm air, and 1106.6 m at the top of
this page, from a leaning rail. The parachutes open at different times too: here at apogee, there
at the motor’s charge.
The best delay is the time from burnout, the end of the thrust curve, to apogee, so the charge fires at the top. It is 10.5 s on the H54, near the 10 s delay that motor’s designation names. The H54 burns out at 3.5 s, so on the leaning rail at the top of this page its 10 s delay fires at 13.5 s, 0.2 s before its apogee. The F52 wants 8 s. The F15 leaves the rail at only 10 m/s, the slowest of the three, and a slow rocket’s fins have the least air to steer with.
Sizing fins
The third example builds the same rocket with fins of five spans, the fin’s height from the body tube to its tip, and flies each from a vertical rail, in calm air and in 5 m/s of wind from the west. The parachute opens at apogee here, not at the motor’s charge, so the charge doesn’t cut the climb short:
cargo run --example fin_sizing -p hpr
My 54 mm rocket on an H54, fins of five spans, in calm air and a 5 m/s west wind
Not yet validated: see the Accuracy page before trusting these numbers.
fin span liftoff margin apogee (m) in the wind: drift (m)
(mm) (kg) (cal) calm wind apogee upwind landing downwind
25 0.666 -2.11 too little margin to fly
35 0.671 0.33 too little margin to fly
45 0.675 1.92 1145 1135 119 1068
55 0.680 2.97 1126 1110 156 1001
65 0.684 3.67 1107 1088 176 954
Because a rocket is a value built by a function, a design study is a loop. The example’s
rocket(span_m) builds the rocket with fins of that span, and the loop weighs and flies each.
- The margin, at liftoff and at Mach 0.3, grows fast with the span: from −2.11 calibres, a CP ahead of the CG, to 3.67. The usual rule of thumb asks for at least one calibre (stability margin), so the program doesn’t fly the two smallest; the example’s own fins are the 45 mm ones.
- Bigger fins cost height. In calm air the 65 mm fins reach 38 m less than the 45 mm ones: that is their drag and their extra mass, about 9 g. In the wind they lose 47 m, since they also turn the rocket further into it, as the apogee drift shows: 119 m upwind with the 45 mm fins, 176 m with the 65 mm.
- The landing is closer with bigger fins, 114 m closer from the 45 mm to the 65 mm: the parachute opens further upwind, and lower, so it drifts for less time.
Parts from a catalog
A rocket can also be built from a maker’s parts, as sold, from a parts catalog. hpr-sim bundles
the one OpenRocket ships: 3,449 parts from Estes, LOC Precision and a dozen more makers, read as
the .orc page explains. The fourth example finds LOC Precision’s 2.56 in
(65 mm) airframe parts in it by maker and part number, builds the rocket from them, and flies it on
an AeroTech H170:
cargo run --example catalog_rocket -p hpr
LOC 2.56 in from the catalog, with motor H170M
Not yet validated: see the Accuracy page before trusting these numbers.
part mass (g)
LOC Precision PNC-2.56 87.7
LOC Precision BT-2.56 120.0
LOC Precision BT-1.52, MMT-1.52 43.6
LOC Precision CR-2.56-38mm 4.2
LOC Precision CR-2.56-38mm 4.2
fins 99.7
LOC Precision LP-36-2022 70.7
structure 430.1
At liftoff: 0.760 kg, center of gravity 0.728 m from the nose,
stability margin 1.07 calibres at Mach 0.3.
Rail exit: 30.0 m/s
Apogee: 1119.3 m above the pad, at 11.77 s
Top speed: 302 m/s (Mach 0.91)
Landing: at 4.6 m/s, at 250.8 s
Each part is named by its maker and its part number, as the design keeps it. LOC’s numbers say
what the part is and its size in inches: PNC-2.56 is a plastic nose cone for the 2.56 in tube,
BT-2.56 the body tube, MMT-1.52 the 1.52 in (38 mm) motor tube, CR-2.56-38mm a centering
ring between the two, and LP-36-2022 a 36 in parachute. Structure is all the parts together,
without the motor.
How far to trust these numbers. Nearly every part weighs what OpenRocket 24.12 weighs when it builds the same catalog part: within 0.1%, with its center of mass within 0.1% of its length. The few exceptions are counted in “How far to trust it” below. One is deliberate, and this nose has it: its shoulder, the short tube that slides into the body, has the nose’s own plastic wall here and weighs nothing in OpenRocket. OpenRocket weighs the nose at 61.5 g; here it weighs 87.7 g, 26.1 g of it the shoulder. That is 6% of the structure. Nothing checks the flight itself; the note at the top of this page applies. The top speed, Mach 0.91, is close to the speed of sound, where drag rises steeply and is least certain (Drag through Mach 1). The stability margin, 1.07 calibres, is taken at Mach 0.3, about 100 m/s, more than three times the speed the rocket leaves the rail at.
The program is
catalog_rocket.rs.
Its steps:
- Find the part.
hpr::hpr_io::orc::bundled().find("LOC Precision", "PNC-2.56")returns the parts with that maker and number. Use the maker’s name as the file writes it (“LOC Precision”, where OpenRocket shows “LOC/Precision”). - Make the builder’s part from it.
Nose::from_catalog(part),Tube::from_catalog,Transition::from_catalogandMotorTube::from_catalog(a body tube used as the motor tube) take the catalog’s sizes, material and density. A catalog nose or transition states its own diameters, so the tube behind it takes the nose’s diameter, not the one given toRocket::new. - Cut a tube. Catalog tubes are sold long (LOC’s motor tube is 34 in).
with_length_mcuts one to length, and its mass follows. - Fittings. A fitting is a part that goes in or on the last body tube: a coupler, an engine
block, a centering ring, a bulkhead, a launch lug, or a packed parachute or streamer.
Fitting::from_catalog(part)makes one from a catalog part, andRocket::add_fittingadds it, flush with the tube’s aft end unlessatplaces it.Fitting::centering_ring,bulkhead,couplerandlaunch_lugmake one to your own sizes. - A parachute is two things. As a fitting it is its weight: canopy and shroud lines, at its place in the tube. Its drag is a recovery device, as in the first example. The example gives that device the catalog’s diameter.
- The fins are made by hand: the catalog has none.
229 of the 3,449 parts state their mass in the catalog, and each weighs that mass. The builder sets the part’s density so that the part, as the catalog sizes it, weighs the stated mass, and the material’s name gains “, density set by the part’s stated mass”. A part changed afterwards, such as a tube cut shorter, keeps that density, so its mass follows the change. OpenRocket gives a rigid part its stated mass the same way. It gives a parachute its stated mass as an override, which comes to the same for a parachute left as it is. It ignores a streamer’s.
What the catalog leaves unsaid
A catalog leaves some sizes out. The builder fills each one in the way OpenRocket 24.12 does when it builds the part, with one exception, a hollow part’s shoulder. The shapes are explained in Shapes:
| Left unsaid | The builder’s choice | OpenRocket’s |
|---|---|---|
| An ogive’s shape | tangent | the same |
A parabola’s parameter K′ | 1 | the same |
A Haack series’ C | 0, the von Kármán | the same |
| A power series’ exponent | ½ | the same |
| Whether a transition is clipped | yes for elliptical, Haack and power series | the same |
| A filled part’s shoulder | solid | the same |
| A parachute’s line material, when the file names none or one it doesn’t define | weightless lines | the same |
| A hollow part’s shoulder wall | the part’s own wall | zero: it weighs nothing |
A clipped transition is cut from a whole nose cone, rather than stretched from one (Shapes). A molded plastic nose cone’s shoulder is a tube of the same plastic, so a shoulder that weighs nothing would be wrong. The catalog’s stated masses agree: on the 74 hollow parts with a shoulder that state their mass, the file’s density weighs nearer the stated mass with the shoulder’s wall than without it on 46. The median stated mass is 0.97 of the mass with the wall, and 1.30 of the mass without it.
How far to trust it
tests/catalog_openrocket.rs
builds every part in the catalog with the builder. It holds each part’s mass and center of mass
to what OpenRocket builds from it, run as an oracle and recorded by
orc_built.py.
The tolerances were set before measuring. Every part is in one row:
| Parts | Count | Allowed | Largest difference found |
|---|---|---|---|
| Tubes, couplers, rings, bulkheads, lugs, parachutes, streamers | 2,230 | 1e-12 of the mass; center 1e-12 of the length | under 1e-14 of the mass, or 8.8e-10 where it is stated in ounces (below); center under 1e-13 of the length |
| Filled nose cones and transitions | 1,029 | 1e-3 of the mass; center 1e-3 of the length | 2.0e-4 of the mass, or 8.8e-10 in ounces; center 7.0e-5 of the length |
| Hollow nose cones and transitions, shoulders taken out | 181 | the same | 6.3e-4 of the mass, or 8.8e-10 in ounces; center 9.7e-4 of the length |
| Hollow elliptical nose cones whose walls differ | 4 | counted | up to 0.48% heavier here; center up to 1.7e-3 of the length |
| A streamer that states its mass | 1 | counted | hpr weighs the stated mass; OpenRocket ignores it |
| Refused by the builder | 4 | counted | OpenRocket weighs each as nothing |
| All | 3,449 |
Where the masses differ, the test checks each cause:
- OpenRocket’s volumes are close, not exact. Even a cone, whose volume has a formula, differs a little. The test holds hpr’s 85 filled cones to that formula, so the difference is OpenRocket’s.
- Hollow shoulders. A hollow part’s shoulders are taken out of hpr’s mass and center, each worked out on its own as a tube of the part’s wall. That leaves the body, which is what OpenRocket weighs.
- Two walls. hpr’s wall is every point within its thickness of the outer surface. OpenRocket’s
masses follow a wall measured across each station instead, whose inner
radius is
r − t √(1 + r′²)(rthe radius,tthe thickness,r′the slope). The test works out both on its own. All 185 hollow parts’ OpenRocket centers agree with the station-wise wall to 1.1e-4 of the length, and their masses to 2.5e-4, on the 111 that state no mass (a stated mass is OpenRocket’s whatever the wall). hpr’s 113 hollow conical, tangent-ogive and elliptical nose cones agree with integrals of its own wall, in volume and center, to 1e-9. The other 72 hollow parts (Haack and parabolic noses, and transitions) have no such integral: they are checked only against OpenRocket, within the tolerance. The two walls differ most on short, blunt nose cones. Four elliptical ones fall outside the tolerance: three up to 0.48% heavier here, their centers up to 1.7e-3 of their length apart, and one, which states its mass, by its center alone, 1.0e-3 of its length. - Masses stated in ounces, on 185 parts of every kind (a different 185 from the hollow parts above), differ by OpenRocket’s rounded ounce, 8.8e-10 of the mass.
- The 4 refused parts. One nose cone names a material its file doesn’t define. Three tubes or rings have a bore no narrower than their outside.
The test compares mass and center of mass, not the moments of inertia. What it checks is each part as the catalog describes it. A catalog’s sizes and densities are the makers’ or the database’s, and none was weighed here. Glue, paint and hardware are still missing: weigh the finished parts when you can.
Beyond the builder
The builder covers a single-stage rocket with one motor. Two ways lead further, both into the crates the builder is made of.
- Change the design. A rocket’s
design()is its design tree, which a design file holds. Clone it, add what the builder can’t with thehpr_designcrate (a cluster, pods, rail buttons, a stage), and make a rocket of it withRocket::from_design(tree, configuration), which names the configuration, the motors, to fly.from_designalso takes a design file, or an OpenRocket file read as the.orkpage shows. A second stage also needs a separation, which the flight builder’sseparationtakes, and a recovery device on each part it makes. For a.ork,hpr::ork::separated_recoveryputs the file’s own devices on their parts, ashpr simdoes (Separation); theork_two_stageexample flies one, building its simulation withSimulation::newrather than through the builder. - Change the flight. A flight builder’s
simulation()hands over the simulationfly()would run. Themotor_choiceexample passes it tohpr_sim::metrics::optimum_delays. The simulation’s methods add a separation, events of your own, or moving or released masses. A drag model of your own, or another program’s drag table, needs no detour: the flight builder’sdrag_modelanddrag_tabletake them (Models of your own).run(&mut ())flies it, with no observer watching. A flight flown that way returns the simulation’s own result, without the builder’sFlightmethods;hpr_sim::FlightMetricsgives the same metrics (Flight metrics).
What it refuses
Each part’s numbers are checked when the part is added: a negative length, a NaN, a zero diameter,
zero fins, a shape parameter out of range. The error names the number, such as
tube diameter, m is 0, outside its domain. So is a part in an order the tree can’t take: fins
before any tube, a second motor tube, a nose behind a tube. The launch site and wind are
checked when they are given, the rail when the flight is set up, and a design that comes in whole
through from_design before it flies. Tests try each of these and pin which check refuses it
(parts_out_of_order_are_refused, degenerate_designs_error_or_stay_finite).
One rocket in them has no fins at all: it flies, tumbling, to finite numbers. That test is
Loft lesson L95’s: a
Loft lesson is a mistake of the project before this one, with the test
that guards against it here.
A catalog part is refused when the builder can’t make it as the catalog describes it: a part
of another kind than the from_catalog it is given (a body tube given to Nose::from_catalog), a
material its file names but doesn’t define, a shape the builder doesn’t have, a nose cone or
transition the file neither fills nor gives a wall thickness, a tube or ring whose
bore is no narrower than its outside, or a part stating a mass with no volume to give it. The
error names the part by maker, number and file. Rocket::add_fitting also refuses a fitting before
any body tube, and a part that isn’t a fitting.
What it can’t do yet
- One motor, one stage. Clusters, staging and pods go through the design, as above; the staging page explains how they fly.
- Rail buttons go through the design. Without them the rocket leaves the rail when its aft end passes the rail’s top. A launch lug is a fitting (above), with its drag; without one, a lug’s drag is missing. With one, the rocket leaves the rail when the lug’s aft edge passes the rail’s top.
- The finish. Every surface is painted, as above.
Where next
- Models of your own flies a drag model and a wind of your own in hpr-sim’s place.
- The API reference’s
guidemodule is this page’s walk-through in five short chapters, each with code that CI runs. - The API reference documents every method, starting at the
hprcrate. - Your own rocket builds the same rocket without the builder, and shows every field a part has.
- Recording a trajectory keeps the flight’s path: pass a recorder to
fly_withinstead of callingfly.
Python
This page shows how to fly a rocket from Python with the hpr package. The package has the same
four types as the Rust builder: an Environment, a Motor, a Rocket and a
Flight. You describe the rocket part by part from the nose back, or read it from a design file,
then fly it from a rail, once or, as a MonteCarlo run, many times with its inputs scattered. A
flight’s metrics come back as numbers and dictionaries, and its recording and a run’s flights as
NumPy arrays. You need Python 3.10 or later and some
familiarity with it. Today you also build the package yourself, which needs the Rust toolchain
(Install it).
How far to trust it. The package adds no physics. Each call hands its numbers to the Rust builder and returns what that returns, so a flight made in Python runs the same code as one the Rust builder makes from the same numbers. A test builds The builder’s example rocket in Python and matches every digit that Rust example prints (
test_prints_what_the_rust_example_prints). Digits beyond those printed can differ between a release and a debug build. Because the code is the same, what that page and Accuracy say about the numbers holds here too. The rocket below has never been flown for real, so no flight checks it. Where it lands in a wind is the least certain number of all: on two of RocketPy’s example rockets, hpr’s drift and RocketPy’s differ by 10 to 38% (Getting started explains why). The package is new and covers less than the Rust library (What is not here yet). The gap that matters most if you bring a design file: most.orkconfigurations are refused (A design from a file). The package is built and tested on Linux, macOS and Windows, but not published on PyPI, Python’s package index.
Install it
Release 0.1.0 is built and checked, but not on PyPI yet. Once it is, pip install hpr-sim
installs it, with NumPy. One wheel serves CPython 3.10 and later on Linux on x86-64, Macs with
Apple silicon, and Windows on x86-64; elsewhere, and on Linux older than glibc 2.35, pip builds
the source package, which needs Rust
(Install a release).
Until then, you build it from this repository. You need the Rust toolchain from Getting started and maturin, the tool that builds Python packages from Rust. From the repository’s root, make a virtual environment and build into it:
python3 -m venv .venv
source .venv/bin/activate # on Windows: .venv\Scripts\activate
pip install maturin
maturin develop --release --manifest-path crates/hpr-py/Cargo.toml
maturin develop builds the package and installs it into the environment, with NumPy. It links
the environment to the build, so after a change to the Rust code, run it again.
To install elsewhere, build a wheel instead, the file pip install takes:
maturin build --release --manifest-path crates/hpr-py/Cargo.toml writes it to target/wheels/.
One wheel serves every CPython (the standard Python) from 3.10 on (tested on 3.10 and 3.13), on
the operating system and processor that built it. scripts/python-tests.sh builds one and runs the package’s tests on it,
which needs uv.
The package imports as hpr. Its distribution name, the one pip lists, is hpr-sim.
A first flight
This builds a simpler version of the rocket in Your own rocket and The builder: a 54 mm airframe with an ogive nose, three fins and a Cesaroni H54, whose recovery bay here is a point mass and whose nose has no shoulder. It flies from a 1.8 m rail leaning 5° into a 5 m/s west wind, with a parachute opened by the motor’s ejection charge at the end of its ejection delay.
import hpr
environment = hpr.Environment(32.99, -106.97, 1400.0, wind_speed_m_s=5.0, wind_from_deg=270.0)
rocket = hpr.Rocket("My 54 mm rocket", 0.0563)
rocket.add_nose("ogive", 0.22, "abs", wall_m=0.0015)
rocket.add_tube(0.9, 0.00115, "kraft_phenolic")
rocket.add_motor_tube(0.2, 0.029, 0.001, "kraft_phenolic", overhang_m=0.005)
rocket.add_fins(
3,
root_chord_m=0.1,
tip_chord_m=0.04,
span_m=0.045,
sweep_m=0.05,
thickness_m=0.003175,
material="birch_plywood",
cross_section="rounded",
)
rocket.add_mass(0.2, position="top", offset_m=0.07, name="recovery bay")
rocket.set_motor(hpr.Motor.from_catalog("H54", delay_s=10.0))
rocket.add_parachute("parachute", diameter_m=0.9, trigger="motor_delay")
print(f"Stability at liftoff: {rocket.static_margin_cal(0.0, 0.3):.2f} calibres")
flight = hpr.Flight(rocket, environment, 1.8, inclination_deg=85.0, heading_deg=270.0)
print(f"Apogee: {flight.apogee_m:.1f} m at {flight.apogee_time_s:.2f} s")
print(f"Top speed: {flight.max_speed_m_s:.0f} m/s (Mach {flight.max_mach:.2f})")
landing = flight.landing
print(f"Landing: {landing['distance_m']:.0f} m from the pad, descending at {landing['descent_rate_m_s']:.1f} m/s")
It prints:
Stability at liftoff: 2.12 calibres
Apogee: 1118.3 m at 13.67 s
Top speed: 190 m/s (Mach 0.57)
Landing: 905 m from the pad, descending at 4.6 m/s
Line by line:
Environmenttakes the launch site’s latitude and longitude in degrees (longitude positive east, so negative in the Americas) and its elevation in meters. The air is the US Standard Atmosphere 1976. The wind is the same at every height, and blows fromwind_from_deg, clockwise from north.Rockettakes a name and the airframe’s outside diameter, m.- The nose is a tangent ogive 0.22 m long, of ABS, hollow with a 1.5 mm wall. The tube is 0.9 m long with a 1.15 mm wall. The motor tube is 0.2 m long, with a 29 mm bore and a 1 mm wall.
- Parts go on from the nose back: the nose, then tubes. Fins, a motor tube and masses go on or in the tube before them.
- Each part names its material by an id from the built-in list,
hpr.materials(), and takes its mass from its shape and that material’s density. - The fins’ lengths are named, so two can’t swap unseen: the root chord, tip chord and span, and the sweep, how far aft of the root’s leading edge the tip’s is.
Motor.from_catalogfinds one of the 32 bundled motors by its name. It doesn’t take the delay from the designation (the H54’s full one is 168H54-10A), so give it asdelay_s: a parachute opened by the motor needs one.Motor.from_filereads a RASP.engor RockSim.rsefile.static_margin_cal(0.0, 0.3)is the stability margin at ignition (0 s) and Mach 0.3.Flightflies the rocket as soon as it is made. The rail is 1.8 m long and leansinclination_degabove the horizon, 90 by default, towardheading_deg, clockwise from north.- The landing’s
descent_rate_m_sis how fast it comes down. Itsground_hit_speed_m_sis faster, as it adds the wind’s drift.
The stability margin is the distance from the
center of gravity back to the
center of pressure, in body diameters
(calibres). It is 2.12 here, where The builder’s example says
1.92. That example packs its 200 g recovery bay into a 15 cm cylinder, which moves the bay’s
center, and so the rocket’s center of gravity, aft: about 0.4 calibres less. It also gives the
nose a capped shoulder, which adds mass at the front: about 0.2 calibres more. A test holds both
steps to these sizes
(test_the_margin_moves_as_the_python_page_says).
The recording
A flight is recorded at every step of the integrator, or every
interval_s seconds (at least 0.001 s) if you give Flight one, with a row at every event too.
flight.columns names the columns, each with its unit, and flight["name"] gives one as a NumPy
array. flight.series gives them all, as a dictionary of arrays. Continuing from above:
height_m = flight["height_above_ground_m"]
time_s = flight["time_s"]
print(len(flight.columns), "columns:", ", ".join(flight.columns[:3]), "...")
print(f"Highest recorded height: {height_m.max():.1f} m at {time_s[height_m.argmax()]:.2f} s")
for event in flight.events[:4]:
print(f"{event['kind']:>10} at {event['time_s']:6.3f} s")
30 columns: time_s, position_east_m, position_north_m ...
Highest recorded height: 1118.3 m at 13.67 s
liftoff at 0.001 s
rail_exit at 0.174 s
burnout at 3.500 s
trigger at 13.500 s
The columns are the ones Recording a trajectory lists: time, position
and velocity east, north and up of the pad, attitude, height, airspeed, Mach number, angle of
attack, thrust, mass and more. Heights are the center of gravity’s, above the pad; it stands on the
rail at the start, so the first height isn’t zero. The arrays are read-only copies, so a slip
can’t change the flight’s record. To plot one, hand the arrays to any plotting library, for
example matplotlib.pyplot.plot(time_s, height_m).
flight.events lists what happened, in the order it happened. Each event is a dictionary with its
kind, the index of the parachute or motor it is about, its time_s, and the flight’s state
then, sample. The kinds are "liftoff", "rail_exit", "burnout", "apogee", "trigger" (a
parachute’s charge fires), "deployment" (it is open) and "ground_hit". Here the parachute’s
charge fires at 13.5 s, the H54’s 3.5 s burn plus its 10 s delay, 0.17 s before apogee.
flight.landing is a dictionary of the landing: its time_s, latitude_deg, longitude_deg,
east_m and north_m of the pad, distance_m, ground_hit_speed_m_s, descent_rate_m_s, and
body, which separated body landed (None for a rocket that didn’t come apart).
flight.summary has every metric of the flight as a dictionary: the apogee, top speed, Mach
number and dynamic pressure, the stability margins and the landings, each explained in
Flight metrics. flight.envelope_flags lists where the flight went past what
hpr’s numbers have been checked for (the operating envelope),
and "unstable_under_power" for a static margin below zero while a motor burns, or
"unstable_without_margin" for a pitching moment that turns the rocket away while a motor burns
where hpr can give no margin
(Flight metrics: unstable under power): each a
dictionary with its flag, such as "beyond_validated_range", the value that raised it (a Mach
number, an angle of attack in radians, a margin in calibres, or a pitch-moment slope per radian),
its time_s and
height_above_ground_m, and a message. It is empty for a flight that raises none. flight.issue_warnings lists the known
errors in hpr’s drag or stability the flight meets (the list):
each a dictionary with its issue number, such as 67, its kind ("drag" or "stability"),
its url, the flight’s max_mach and its
time_s, the parts whose shape meets it, and a message saying which way its numbers lean.
flight.to_json() is the whole record as JSON text.
A design from a file
Rocket.from_file reads a design instead of building one: an hpr design (.hpr or .hprz,
The hpr design format), an OpenRocket .ork file (.ork design
files), or a rocket’s JSON. It flies the
motor configuration you name, or the file’s default one, or its only
one, as hpr sim does.
Most .ork files won’t fly yet. A configuration flies only if every motor lights at launch and
has a thrust curve, in the file or among hpr’s 32 bundled motors: that is 4 of the 170
configurations in the reference library. Otherwise from_file raises HprError and says why, and
Python can’t yet swap in a motor as hpr sim --motor does.
A design read from a file flies without the parachutes it stores unless you ask for them
with recovery=True: Rocket.from_file("validation/fixtures/ork/guides/level-1.ork", recovery=True) flies the file’s devices
as hpr sim does, each opening fully at its event with the file’s drag coefficient or
OpenRocket’s own (When parachutes open and stages separate).
Without it, add parachutes with add_parachute; until you do, the rocket falls to the ground
unslowed, and its landing numbers mean nothing. Parachutes added after recovery=True come after
the file’s, so the file’s first is parachute 0 for released_by. Stage separations are never
flown. rocket.notes lists what reading the file said: the .ork reader’s warnings, a design
written by an older version of the format, what the file holds that isn’t flown, and a file’s
parachute that never opens in the configuration flown. A file’s recovery hpr can’t fly, such as
a device opened by an event hpr doesn’t read, raises HprError with why.
This one is RocketPy’s example rocket, Calisto, from the file the validation suite uses, with its two parachutes as RocketPy’s example gives them. The path is the repository’s, so run it from the repository’s root:
calisto = hpr.Rocket.from_file("validation/designs/rocketpy-calisto-tests-motor-at-minus-1.373.json")
calisto.add_parachute("drogue", cd_s_m2=1.0, lag_s=1.5)
calisto.add_parachute("main", cd_s_m2=10.0, trigger="altitude", altitude_m=800.0, lag_s=1.5)
site = hpr.Environment(32.990254, -106.974998, 1400.0)
flight = hpr.Flight(calisto, site, 5.2, inclination_deg=85.0)
print(f"{calisto.configuration}: apogee {flight.apogee_m:.0f} m, landing at {flight.landing['descent_rate_m_s']:.1f} m/s")
example: apogee 2807 m, landing at 5.2 m/s
This flight uses hpr’s own drag, in calm air. The validation suite flies the same design with hpr’s own drag too, in a wind and in RocketPy’s atmosphere, and its apogee is 0.609% below RocketPy’s. That is the closest of the suite’s six rockets: on the others, hpr’s own drag puts the apogee from 7.280% below RocketPy’s to 10.302% above (Whole flights with each code’s own drag, same-drag and predicted mode). The next section flies Calisto from Python as that suite does, and compares it with RocketPy.
RocketPy’s example, flown as the suite flies it
This section flies Calisto again, this time the way the validation suite flies it against RocketPy. It is for checking hpr against RocketPy, or for moving a RocketPy script across. Both codes fly the same drag and the same inputs, so the comparison tests the rest of the physics: the equations of motion, the atmosphere, the rail and the parachutes. It is a comparison between two codes, not with a real flight.
Three options make it RocketPy’s flight:
- A drag table.
DragTableholds a zero-lift drag coefficientC_D0against Mach number, as RocketPy’spower_off_dragandpower_on_dragdo. Its power-on curve is flown while the motor burns, its power-off curve the rest of the time. Pass it to a flight asdrag_table=; it replaces hpr’s own drag, and nothing else.DragTable.from_csvreads RocketPy’s two-column drag files. - RocketPy’s gravity.
Environment(..., gravity="vertical_taylor")uses RocketPy’s formula for gravity instead of hpr’s default. Near the ground the two differ in size by about one part in 10⁸, but hpr’s default also turns with the rocket’s position over the ground. On this flight the switch moves the apogee by 2 parts in 10⁷ and the landing point by 0.3 m (Gravity). - One parachute at a time. RocketPy flies only the last parachute to open, and hpr adds
together every one that is open.
released_byis another parachute’s number, counted from 0 in the order they were added:add_parachute(..., released_by=1)cuts this one away once the second parachute is fully open (Recovery).
Here is an invented table with less drag while the motor burns. Calisto flies it in calm air, as in the section above, to 2650 m above the pad:
table = hpr.DragTable([(0.0, 0.5), (3.0, 0.5)], [(0.0, 0.45), (3.0, 0.45)])
print(f"C_D0 at Mach 0.6: {table.cd0(0.6):.2f} coasting, {table.cd0(0.6, thrusting=True):.2f} burning")
flight = hpr.Flight(calisto, site, 5.2, inclination_deg=85.0, drag_table=table)
print(f"apogee {flight.apogee_m:.0f} m")
C_D0 at Mach 0.6: 0.50 coasting, 0.45 burning
apogee 2650 m
The example
crates/hpr-py/examples/calisto.py
puts it all together. It is written as notebook cells (# %%), which editors such as VS Code run
one at a time. It reads the rocket, the wind, the drag and the parachutes from the repository’s
files, then flies them. Then it measures each metric as RocketPy defines it, and not always as
hpr’s own summary does:
- RocketPy follows the rocket’s dry center of mass, its center of mass without propellant, and starts that point at ground level. hpr stands the rocket on its rail, so the same point starts 1.250 m up (the first line printed below). The example subtracts 1.250 m from every height, and opens the main 1.250 m above RocketPy’s 800 m.
- RocketPy’s rail exit is when its forward rail button leaves the rail, after 3.745 m of travel
(RocketPy’s
effective_1rl). hpr’s is when its aft-most rail guide clears the top of the 5.2 m rail, after 4.45 m of travel, so the example reads RocketPy’s off the recording. - RocketPy’s flight ends when the dry center of mass is back at its starting height.
Run from the repository’s root, python crates/hpr-py/examples/calisto.py prints:
The dry center of mass starts 1.250 m above the ground.
metric hpr RocketPy difference
apogee_agl_m 2613.59 2612.22 +0.05%
apogee_time_s 22.88 22.85 +0.10%
max_speed_m_s 243.56 243.52 +0.01%
max_mach 0.7321 0.7329 -0.12%
max_acceleration_m_s2 112.35 112.24 +0.10%
max_acceleration_power_on_m_s2 112.35 112.24 +0.10%
rail_exit_speed_m_s 28.20 28.20 -0.01%
rail_exit_time_s 0.2945 0.2947 -0.07%
burnout_altitude_agl_m 684.23 684.10 +0.02%
burnout_speed_m_s 235.65 235.60 +0.02%
flight_time_s 260.86 260.53 +0.12%
apogee_drift_m 422.52 426.36 -0.90%
landing_drift_m 1277.36 1261.50 +1.26%
impact_speed_m_s 5.4549 5.4560 -0.02%
largest difference 1.26%, within 3%: True
Every metric is within 3% of RocketPy’s, the bound the validation suite holds this flight to
(M2.1). These are the suite’s numbers: a test holds each to within
0.001% of what the suite’s report records for the same flight
(test_calisto.py).
Because both codes fly the same drag, this says nothing about hpr’s own drag, the largest source
of difference: flying its own, hpr puts the suite’s six rockets’ apogees 7.280% below RocketPy’s to
10.302% above, as the section before says. The drifts differ most here. On two of RocketPy’s other
example rockets, Juno III and Bella Lui, flown in a wind, the drifts differ by 10 to 38%
(Getting started explains why).
Drag and wind of your own
A flight can fly a drag and a wind you write as Python functions, in place of hpr’s own drag and the environment’s wind: a drag curve from a wind tunnel or another program, say, or a wind profile from a weather balloon. hpr calls them as it flies, several times for each time step of its integrator, and keeps everything else its own: the rest of the aerodynamics, the thrust, the masses and the atmosphere. They are the Python side of the Rust library’s models of your own. How far to trust it: as far as your functions. hpr checks that each number is finite, and that a drag is not negative, but not that it is right.
| Given as | Called as | Returns |
|---|---|---|
Flight(..., drag=f) | f(mach, thrusting), with thrusting true while a motor burns | the whole rocket’s zero-lift drag coefficient C_D0, on its reference area (the largest body diameter’s circle) |
Environment(..., wind=f) | f(height_m), the height above sea level in meters, not above the launch site | the air’s velocity (east_m_s, north_m_s), as a tuple, list or array |
Three things about the drag number, as for a Rust drag model:
- It is not rescaled. A
DragTablecan carry the diameter it was measured on; a function’s number must already be on the rocket’s reference area. - At an angle of attack hpr scales it, as it scales its own.
- Under a parachute the drag is the parachute’s, not the function’s.
The wind’s two numbers are the way the air moves, so a wind from the west is (+speed, 0): the
opposite of wind_from_deg, which names where the wind comes from.
Here the drag rises through Mach 0.8 to 1.1 and drops 10% under power, and the wind strengthens with height:
def drag(mach, thrusting):
"""0.5 below Mach 0.8, rising to 0.8 by Mach 1.1; 10% less while the motor burns."""
rise = min(max((mach - 0.8) / 0.3, 0.0), 1.0)
cd0 = 0.5 + 0.3 * rise
return 0.9 * cd0 if thrusting else cd0
def wind(height_m):
"""From the west, 3 m/s at the 1400 m site, 1 m/s stronger every 100 m above it."""
above_m = max(height_m - 1400.0, 0.0)
return (3.0 + above_m / 100.0, 0.0)
windy = hpr.Environment(32.99, -106.97, 1400.0, wind=wind)
flight = hpr.Flight(rocket, windy, 1.8, drag=drag)
print(f"apogee {flight.apogee_m:.0f} m, landed {flight.landing['east_m']:.0f} m east")
table = hpr.DragTable([(0.0, 0.5), (3.0, 0.5)])
same = hpr.Flight(rocket, windy, 1.8, drag=lambda mach, thrusting: 0.5)
print(same.apogee_m == hpr.Flight(rocket, windy, 1.8, drag_table=table).apogee_m)
def refuses(mach, thrusting):
if mach > 0.3:
raise LookupError("no drag measured past Mach 0.3")
return 0.5
try:
hpr.Flight(rocket, windy, 1.8, drag=refuses)
except LookupError as error:
print(type(error).__name__, error)
apogee 1109 m, landed 1963 m east
True
LookupError no drag measured past Mach 0.3
A function that always returns the same number flies as a drag table of that number does. At 0.5 the two agree to the last digit; at some numbers, 0.3 or 0.45 say, they differ in the eleventh, because the table’s interpolation rounds its own constant.
A function that raises stops the flight, and Flight raises its exception unchanged, with its
type and traceback. That holds even for an exception at a trial step: the integrator tries a step,
and when a model refuses, it would normally retry with a shorter one. It asks at speeds and heights
a little past the flight’s own (the landing’s search, for one, asks the wind a few meters below the
ground), so a function that refuses past its data wants some margin.
A function can’t be given alongside its constant counterpart: drag with drag_table, or wind
with wind_speed_m_s or wind_from_deg, raises hpr.HprError, as does something that can’t be
called. So does a drag coefficient that is negative or not finite, or a wind that isn’t finite; an
answer of the wrong type, such as a string, raises Python’s TypeError.
Each call takes Python’s global interpreter lock, so a flight with a Python function runs slower than one without, and flights in other threads wait while it calls. The functions should give the same answer to the same question, or a flight can’t be repeated.
Monte Carlo
A Monte Carlo run flies the rocket many times, each time with its
uncertain inputs drawn afresh about their planned values, to show how far the apogee and the
landing spread (Monte Carlo dispersion explains the method and how to choose the
numbers). hpr.MonteCarlo takes the rocket, the site and the rail as Flight does (a vertical
rail by default), how many flights to fly (100 by default), a seed (0 by
default), and one standard deviation for each input to scatter:
a fraction for a share (impulse_sd_fraction=0.03 is 3%), degrees for an angle, meters or
seconds otherwise. An input left out isn’t scattered. Like Flight, it flies as soon as it is
made, its flights spread over the computer’s cores. The spread it shows is only as good as the
standard deviations you give and hpr’s flight models, which are not yet checked against repeated
real flights (Accuracy); where a rocket lands in a wind is the least certain number
of all.
The run reads as a dictionary of NumPy arrays, one value a flight: what each flight drew
(motor_1_impulse_scale, drag_scale and the rest) and what it came to (apogee_m,
landing_distance_m, max_mach and the rest). run.columns lists them, in the order of
hpr mc --export’s CSV columns. This flies a level 1 certification rocket’s
.ork design 200 times from a 1.5 m vertical rail, in its default motor configuration, with the
parachute stored in its file (recovery=True, A design from a file):
import numpy
level_1 = hpr.Rocket.from_file("validation/fixtures/ork/guides/level-1.ork", recovery=True)
windy = hpr.Environment(0.0, 0.0, 0.0, wind_speed_m_s=4.0, wind_from_deg=270.0)
run = hpr.MonteCarlo(
level_1, windy, 1.5, runs=200, seed=2026,
mass_sd_fraction=0.02, drag_sd_fraction=0.05, impulse_sd_fraction=0.03,
wind_sd_fraction=0.25, wind_from_sd_deg=15.0,
)
apogee = run["apogee_m"]
print(f"{run.flown} of {run.runs} flights flown, {run.failed} failed")
print(f"apogee: mean {numpy.nanmean(apogee):.0f} m, 5% to 95% "
f"{numpy.nanpercentile(apogee, 5):.0f} to {numpy.nanpercentile(apogee, 95):.0f} m")
print(f"mean landing: {numpy.nanmean(run['landing_east_m']):.0f} m east of the pad")
200 of 200 flights flown, 0 failed
apogee: mean 1069 m, 5% to 95% 1002 to 1134 m
mean landing: 842 m east of the pad
The command line flies the same run: hpr mc validation/fixtures/ork/guides/level-1.ork --runs 200 --seed 2026 --wind 4 --wind-from 270 --mass-sd 0.02 --drag-sd 0.05 --impulse-sd 0.03 --wind-sd 0.25 --wind-from-sd 15. run.to_csv() returns the text hpr mc --export writes.
A test holds the two equal
(test_a_run_is_hpr_mcs_bit_for_bit).
It flies 40 flights of this design with all eleven inputs scattered, and finds every number of
every column equal, bit for bit, and the two CSV texts equal byte for byte. That holds on one
platform, with both built in release mode (maturin develop --release, cargo build --release);
a debug build’s last digits can differ.
A flight that fails, such as one whose drawn mass is negative, is counted and kept, never
dropped. run.failed counts them, run.failures groups them by why (a dictionary each:
at, "inputs" or "flight"; reason; count; and first_index, the first one’s row), and
the outcome,
failed_at and reason columns say which they were. A failed flight’s figures are NaN, so use
NumPy’s nanmean, nanpercentile and the like, as above, or pick the flown ones with
run["outcome"] == "flown". run.nominal is the flight with nothing scattered, as
Flight.summary gives it, and run.dispersion the standard deviations given, by name.
The eleven inputs, each named for its unit:
| argument | one standard deviation of |
|---|---|
mass_sd_fraction | each stage’s mass without its motors, as a fraction of it |
cg_sd_m | each stage’s center of mass along the axis, m |
drag_sd_fraction | the rocket’s zero-lift drag coefficient, as a fraction of it: hpr’s own, a drag_table’s or a drag function’s |
impulse_sd_fraction | each motor’s total impulse, as a fraction of it; its propellant mass scales with it |
burn_time_sd_fraction | each motor’s burn time, as a fraction of it, at the same impulse |
delay_sd_s | each motor’s ejection delay, s |
wind_sd_fraction | the wind’s speed at every height, as a fraction of it |
wind_from_sd_deg | the wind’s direction, degrees |
inclination_sd_deg | the rail’s angle above the horizon, degrees |
heading_sd_deg | the rail’s heading, degrees |
deployment_lag_sd_s | each parachute’s lag after its trigger, s |
A drag or wind function of your own is called from every flight of the run, one call at a
time, and an exception it raises stops the run and is raised by MonteCarlo. The calls come in no
set order, so a run repeats bit for bit only if the function’s answer depends on its arguments
alone. A run takes 1 to
100,000 flights; a standard deviation that is negative or not finite raises hpr.HprError,
named by its argument.
When something is wrong
A value out of its range, a part out of order, or a motor or material that isn’t there raises
hpr.HprError, a kind of ValueError, with the library’s own message. So does an argument that
doesn’t apply, such as a canopy beside a cd_s_m2:
try:
hpr.Motor.from_catalog("Z9000")
except hpr.HprError as error:
print(error)
no motor with a bundled thrust curve matches `Z9000`
Names and units
Every value is in SI units, and every argument and attribute that carries one names it, as the Rust
API does: length_m, mass_kg, apogee_m, max_speed_m_s, wind_from_deg. Names that pick one
of several things are strings, in any case, with a space or hyphen allowed where the name has an
underscore:
| argument | the choices |
|---|---|
a nose’s or transition’s shape | "conical", "ogive", "elliptical", "power_series", "parabolic_series", "haack" (Shapes). parameter is an ogive’s radius ratio (1, a tangent ogive, by default), a Haack series’ C (0, the von Kármán, by default), and a power series’ exponent or a parabolic series’ K, which those two need |
fins’ cross_section | "square" (the default), "rounded", "airfoil" |
a part’s position | "top", "middle", "bottom" or "after" the tube it is in, then offset_m aft of that. A mass is at the top by default; fins and a motor tube sit flush with the tube’s aft end |
a parachute’s trigger | "apogee" (the default); "altitude" with altitude_m, on the way down past that height above the pad; "time" with time_s after launch; "motor_delay" with motor, the motor’s number (0, the first, by default) |
a parachute’s canopy | "flat_circular" (the default), "conical", "biconical", "triconical", "extended_skirt10_flat", "extended_skirt14_full", "hemispherical", "annular", "cross", "flat_ribbon", "conical_ribbon", "ringslot", "ringsail" (Recovery) |
A parachute is either a canopy diameter_m across, with its type’s drag coefficient or your own
drag_coefficient, or a drag area cd_s_m2, its drag coefficient
times its area in m², as RocketPy’s cd_s.
Every class and method
| call | what it does |
|---|---|
Environment(latitude_deg, longitude_deg, elevation_m, wind_speed_m_s=, wind_from_deg=, gravity=, wind=) | the launch site, a wind, and the gravity model: "ellipsoidal" (the default), "vertical" or "vertical_taylor" (RocketPy’s); wind= is a wind function, wind(height_m) -> (east_m_s, north_m_s) (Drag and wind of your own) |
DragTable(power_off, power_on=, reference_diameter_m=), DragTable.from_csv(power_off, power_on=, reference_diameter_m=) | a drag coefficient against Mach number, from (mach, cd) rows or CSV files; its cd0(mach, thrusting=), has_power_on and reference_diameter_m |
Motor.from_catalog(name, delay_s=), Motor.from_file(path, delay_s=), Motor.from_eng(text), Motor.from_rse(text) | a motor; its designation, diameter_m, length_m, delay_s, total_impulse_ns, propellant_mass_kg and thrust_curve(), two arrays |
Rocket(name, diameter_m), Rocket.from_file(path, configuration=, recovery=) | a rocket, built or read, with the file’s parachutes when recovery=True; its name, configuration and notes |
add_nose(shape, length_m, material, wall_m=, parameter=, shoulder_length_m=, shoulder_wall_m=, capped_shoulder=, name=) | the nose: hollow with wall_m, solid without; a shoulder, capped or not |
add_tube(length_m, wall_m, material, diameter_m=, name=) | a body tube |
add_transition(length_m, aft_diameter_m, wall_m, material, shape=, parameter=, solid=, name=) | a shoulder or boattail between tubes |
add_fins(count, root_chord_m=, tip_chord_m=, span_m=, sweep_m=, thickness_m=, material=, cross_section=, cant_deg=, position=, offset_m=, name=) | trapezoidal fins |
add_motor_tube(length_m, inner_diameter_m, wall_m, material, overhang_m=, position=, offset_m=, name=) | the motor tube |
add_mass(mass_kg, position=, offset_m=, packed_length_m=, packed_diameter_m=, name=) | a mass, at a point or packed in a cylinder |
set_motor(motor), add_parachute(name, ..., released_by=) | the motor, and a parachute, cut away once parachute released_by is fully open |
mass_properties(time_s=), static_margin_cal(time_s=, mach=), margin(time_s=, mach=), design_json() | the mass, center of gravity and inertia; the margin; the design as JSON |
Flight(rocket, environment, rail_length_m, inclination_deg=, heading_deg=, interval_s=, drag_table=, drag=) | the flight, drag= a drag function, drag(mach, thrusting) -> C_D0 (Drag and wind of your own); its apogee_m, apogee_time_s, max_speed_m_s, max_mach, rail_exit_speed_m_s, landing, envelope_flags, issue_warnings, summary, events, columns, series, flight[name] and to_json() |
MonteCarlo(rocket, environment, rail_length_m, runs=, seed=, inclination_deg=, heading_deg=, drag_table=, drag=, mass_sd_fraction=, ...) | a Monte Carlo run, the eleven standard deviations as Monte Carlo lists them; its runs, seed, flown, failed, failures, nominal, dispersion, columns, run[name] and to_csv() |
An argument written name= above can be left out, and is given by name: add_fins’ lengths
have no default, but are named too. Each class and method has a docstring that says what its
arguments mean: help(hpr.Rocket.add_fins), for example. The Rust reference for the package, hpr_py, says how it is
built.
What is not here yet
The package covers the Rust builder, not the whole library. Not yet:
- An atmosphere written in Python. Rust programs have one today (Models of your own).
- Staging and mass shifts. A configuration that drops a stage under power is refused. Any other
design of several stages flies as one stack: no stage drops away, a motor lit by another’s
burnout lights on the whole stack, one lit by a separation never does, and
rocket.notessays so. - Swapping a motor into a design read from a file, as
hpr sim --motordoes. - Separations stored in a design file, as above.
- Monte Carlo’s landing ellipses and spread summaries, which
hpr mcprints and Rust programs get fromhpr_analysis. From Python, NumPy finds them from the run’s arrays. - The library’s built-in winds that change with height (power law, log law, layers) and weather files fly only from Rust. From Python you can write any of them as a wind function (Drag and wind of your own).
- Parts from a parts catalog, which only Rust programs build with (Parts from a catalog).
- Type stubs, so an editor sees the docstrings but not the arguments’ types.
Models of your own
This page shows how to fly a model of your own in place of one of hpr-sim’s: a drag model, a wind, or an atmosphere. You might have a drag curve from a wind tunnel, from another program, or from your own flights, or a wind profile from a weather balloon that none of the built-in winds fits. The page runs two example programs and walks through them. It follows on from The builder, and needs some Rust. From Python, a drag and a wind can be plain functions (Python).
How far to trust it. As far as your model, and no further than hpr-sim’s other models, which still fly the rest of the rocket and are not yet validated against real flights (Accuracy).
- Refused: a drag coefficient that is negative or not a finite number, a wind velocity that is not finite, and air the flight can’t use: a density or pressure that is negative or not finite, or a temperature, speed of sound or viscosity that is zero, negative or not finite. Each error names the wind or the air’s field, and the height.
- Tested: a drag model that hands back hpr-sim’s own drag flies the same flight, bit for bit, as flying without one, and a model that gives the same drag at every speed flies exactly as a drag table of that value does (
a_drag_model_is_flown_in_place_of_hprs_drag).- Not checked: whether your model is right. hpr-sim can’t know.
- Invented: the drag curve and the wind on this page. They are not measurements of any rocket or any day.
Which models you can replace
Each is a Rust trait: a set of methods your own type provides. Write the type, give it the method, and hand it over.
| Model | What your type gives | How to fly it |
|---|---|---|
Drag, DragModel | The rocket’s zero-lift drag coefficient at a flow | Flight::builder(...).drag_model(model) |
Wind, Wind | The wind’s velocity at a height | Environment::with_wind(wind) |
Atmosphere, Atmosphere | The air’s pressure, temperature, density, speed of sound and viscosity at a height | Environment::with_atmosphere(air) |
The zero-lift drag coefficient is the drag with the rocket pointed straight into the air, divided by the dynamic pressure and the rocket’s reference area: by default the area of a circle of its largest body diameter. A drag model replaces that number and nothing else:
- At an angle of attack, hpr-sim scales your number up and down with the angle as it scales its own (Drag).
- The normal force, center of pressure, roll and damping stay hpr-sim’s. So the stability margin a rocket reports doesn’t change with the drag model.
- Recovery doesn’t use it: under a parachute the drag is the parachute’s.
- Staging: a drag model is the whole stack’s drag, so a flight with a powered separation refuses it at the separation (Staging).
A drag table read from another program’s export (the flight builder’s drag_table, or
with_drag_table on a simulation, which
Getting started uses) replaces the same number; a drag
model is the same idea with your code in place of the table. The last one set is the one flown.
One difference matters: a table can carry the diameter it was measured on, and hpr-sim rescales
it to the rocket’s reference area. A negative coefficient is refused from either, where the
flight meets it: a table can give one past its rows or between them if its curve extrapolates
linearly or bends. A model’s number is not rescaled. If your curve was measured
on another area, multiply it by your area over query.reference_area_m2() before returning it.
A drag model
cargo run --example custom_drag -p hpr
This flies the rocket of The builder three times on a Cesaroni H54, from a vertical rail in calm air: with hpr-sim’s own drag, with that drag made 10% higher, and with a drag curve of invented numbers. The parachute opens at apogee, so only the drag changes from one flight to the next. Then it tries a curve that stops short of the rocket’s speed. It prints this:
My 54 mm rocket on a 168H54-10A, from a 1.8 m vertical rail in calm air
Not yet validated: see the Accuracy page before trusting these numbers.
drag apogee apogee at top speed
(m) (s) (m/s)
hpr's own drag 1145.1 14.06 187
hpr's, 10% higher 1091.2 13.65 183
a curve (invented) 1146.5 13.99 188
A curve that ends at Mach 0.4 stops the flight: Mach number past the drag curve's last point, 0.40
What the lines say:
- 10% more drag costs 53.9 m of apogee, 4.7%, and reaches it 0.41 s sooner. That is a quick way to see how much a rougher finish, or an uncertain drag, could matter.
- The curve is invented. Its apogee lands within 2 m of hpr-sim’s because of the numbers chosen, which says nothing about whether either is right. A curve from your own data would put your numbers here.
- The short curve refuses to guess past its last point, so the flight stops there and says why, instead of flying on made-up drag.
The program
The program is
crates/hpr/examples/custom_drag.rs.
Each model is a type with one method, zero_lift_drag, which gets a DragQuery and returns the
coefficient:
query.mach()is the Mach number.query.conditions().thrustingsays whether a motor is burning. A burning motor fills the base of the rocket, so its drag is lower (power-on and power-off drag). The example’s curve has a column for each.query.conditions().reynolds_per_mis the Reynolds number per meter, for a model that depends on it.query.buildup()is hpr-sim’s own drag at that flow, what the flight would have used without the model. The 10% model multiplies itszero_lift_coefficientby 1.1.
The method returns a Result, so a model can refuse a question it can’t answer, as the curve does
past Mach 0.4. The flight stops with that error, wrapped to say it came from the drag model. The
integrator tries speeds a little past the flight’s own as it works out each step, so a curve that
refuses past its end wants some margin beyond the top speed. A model is asked several times every
step of the flight, so it should be quick, and give the same answer to the same question: a flight
is only as repeatable as its models. Whatever the drag model, a flight stops at Mach 5, where
hpr-sim’s normal force ends.
A wind model
cargo run --example custom_wind -p hpr
hpr-sim has steady, power-law, log-law and layered winds built in (Wind), and
they reach the flight through the same Wind trait. This example writes one of its own: a wind
that grows from 4 m/s at the ground to 10 m/s at 1,000 m, and veers, turning clockwise, from the
west (270°) to the north-west (315°) on the way up. Above 1,000 m it holds steady. It flies the
same rocket in that wind and in a steady 4 m/s west wind:
My 54 mm rocket on a 168H54-10A, from a 1.8 m vertical rail
Not yet validated: see the Accuracy page before trusting these numbers.
wind apogee landing east landing south descent
(m) (m) (m) (m/s)
steady, from the west 1138.0 853 0 4.6
veering 1135.7 1395 835 4.6
Both winds are the same at the ground, where a flier would measure them. Aloft, the veering wind is stronger and comes from further north, so the rocket drifts further under its parachute, and to the south-east rather than due east. The parachute’s descent rate is the same in both.
The program is
crates/hpr/examples/custom_wind.rs.
Its Veering type has one method, wind, which gets a height above mean sea level and returns a
WindSample: the air’s velocity as east, north and up components, and whether the model had to
extrapolate past its data (never, for this one). A wind is asked by height above sea level, not
above the ground, so the type keeps the ground’s elevation to measure from.
Environment::with_wind flies it. The wind direction is where the
wind blows from, so a west wind moves the air east.
hpr-sim refuses a wind velocity that isn’t finite wherever it reads the wind. A wind that
returns a NaN (not a number) or an infinity stops the flight with a SimError::Domain error. Its
message names the wind and gives the height above sea level, in meters, where the wind was asked.
For a test wind that turns to NaN above 1,700 m, the message reads “height above sea level, m, at
which the wind’s velocity is not finite is outside its domain: 1700.0…”, the height of the first
step to ask above 1,700 m, so its digits depend on the step. This holds
while climbing, under a canopy, and for bodies flown apart after a separation
(issue #237, the report that asked for it).
Tests pin each case: a_wind_that_is_not_finite_stops_the_flight and
a_wind_that_is_not_finite_under_a_canopy_stops_the_descent,
and
a_separated_body_refuses_a_wind_that_is_not_finite.
hpr-sim can’t tell whether a finite wind is right; that is up to the model.
An atmosphere
An atmosphere of your own works the same way, through Environment::with_atmosphere. hpr-sim’s
own are the standard atmosphere and a weather balloon’s
sounding (Atmosphere); there is no example
program for one of your own yet. Its one method, air, gets a height above mean sea level and
returns an AirSample: the air’s temperature, pressure, density, speed of sound and viscosity,
and whether the model had to extrapolate past its data.
hpr-sim refuses air it can’t use wherever it reads the air. Every field must be a finite number. The temperature, speed of sound and viscosity must also be above zero. The density and pressure may be zero, as in a vacuum, but not negative. hpr-sim’s standard atmosphere is the reason zero is allowed: far above its 86 km top, its pressure and density shrink to zero.
Air that breaks a rule stops the flight with a SimError::Domain error. Its message names the
field and gives the height above sea level, in meters, where the air was asked. For a test
atmosphere whose density turns to NaN above 1,700 m, the message reads “height above sea level,
m, at which the air’s density is negative or not finite is outside its domain:”, then the height
of the first step to ask above 1,700 m. If more than one field is bad, the first in the order
density, pressure, temperature, speed of sound, viscosity is the one named.
This holds while climbing, under a canopy, and for bodies flown apart after a separation
(issue #301, the report that asked for it).
Before, a density that was NaN or negative turned the drag off with no error. In the tests, a
rocket climbed about twice as high, and bodies flown apart landed at about 140 m/s. Tests pin
each case: air_the_flight_cannot_use_stops_the_climb and
air_the_flight_cannot_use_under_a_canopy_stops_the_descent,
and
a_separated_body_refuses_air_it_cannot_use.
Another test checks that hpr-sim’s own atmospheres pass, from 5 km below sea level to a million
kilometers up
(the_check_on_the_air_takes_every_stock_atmosphere).
hpr-sim can’t tell whether air that passes is right; that is up to the model.
Where next
- The API reference’s
guidemodule says the same in five short chapters, each with code that CI runs, andhpr_aero::customdocuments the drag trait. - The builder builds the rocket these examples fly.
- Accuracy says how well hpr-sim’s own models have been checked, which is what a model of your own replaces.
Monte Carlo dispersion
No two flights of one rocket are the same. The motor burns a little hotter or cooler than its
label, the rocket weighs a few grams more than the plan, the wind is not the forecast’s. A
Monte Carlo run flies the rocket hundreds or thousands of times. Each
time it draws the uncertain inputs afresh around their planned (nominal) values, and the run shows
how far the apogee and the landing spread, and draws the ellipse the landings fall in. This page
runs one, says what each dispersion does to a flight, and how to choose
the numbers. Its examples need some Rust and follow on from The builder;
hpr mc runs the same from the command line, with no code
(below), and hpr.MonteCarlo from Python (From Python).
How far to trust it. The sampling is tested; the spread it gives is only as good as the uncertainties you give it and hpr-sim’s flight models, which are not yet validated against real flights (Accuracy).
- Tested: the same seed gives the same run, bit for bit, however many flights it has and however many threads fly them; a run with no dispersion flies the nominal flight in every sample; each dispersion moves its input as the table below says; a dispersed motor keeps its specific impulse; failed flights are counted (
montecarlo.rs’s tests). The landing ellipses are tested against normal spreads with known answers.- Not checked: whether the spread matches the spread of real flights. No measured set of repeated flights has been compared yet.
- Left out: correlations between inputs, distributions other than the normal, and the inputs listed under What is not dispersed. A run flies what
Flight::buildersets up, a staged flight’s separations included; events of your own can’t be part of one yet.
A run of 200 flights
The example program
crates/hpr/examples/monte_carlo.rs
takes the 54 mm rocket of The builder on a Cesaroni H54
(motor designation 168H54-10A), at Spaceport America in a forecast
wind of 4 m/s from the west, off a rail leaned 5° into the wind. It disperses the rocket’s mass, its
center of mass, its drag, the motor’s impulse and burn time, the wind and the rail, and flies 200
flights. Run it from a copy of the repository with:
cargo run --example monte_carlo -p hpr
The heart of it:
let launch = Flight::builder(&rocket, &environment, 1.8)
.inclination_deg(85.0)
.heading_deg(270.0);
let dispersion = Dispersion {
dry_mass_sd_fraction: 0.02, // 2% of the mass without the motor
cg_sd_m: 0.005, // 5 mm
drag_sd_fraction: 0.05, // 5% of the drag coefficient
impulse_sd_fraction: 0.03, // 3% of the motor's total impulse
burn_time_sd_fraction: 0.02, // 2% of its burn time
wind_speed_sd_fraction: 0.25, // 25% of the wind's speed
wind_heading_sd_rad: 15_f64.to_radians(),
rail_elevation_sd_rad: 1_f64.to_radians(),
rail_azimuth_sd_rad: 2_f64.to_radians(),
..Dispersion::default()
};
let monte_carlo = MonteCarlo::new(launch.inputs()?, dispersion)?;
let run = monte_carlo.run(2026, 200); // seed 2026, 200 flights
let apogee = run.apogee()?; // the apogees' spread
let landing = run.landing()?; // where they landed
let ellipse = landing
.prediction_ellipse(0.95)? // where the next flight lands, 95 times in 100
.ok_or("too few landings")?;
It prints:
My 54 mm rocket on a 168H54-10A, from a 1.8 m rail at 85°, heading west into a 4 m/s west wind
200 flights, seed 2026: 0 failed
Not yet validated: see the Accuracy page before trusting these numbers.
nominal mean std dev 5% median 95%
apogee (m) 1113.3 1119.8 47.4 1040.5 1121.3 1198.8
landing distance (m) 662.4 660.3 208.9 329.3 670.5 996.5
landing east (m) 662.4 626.1 209.6 274.9 635.5 957.7
Landing ellipses, centered 626 m east and 23 m south of the pad:
semi-major semi-minor heading landings inside
50% 253 m 239 m 131° 48.0%
95% 525 m 497 m 131° 94.5%
95%, the next flight 532 m 503 m 131° 95.5%
Reached 1,100 m: 65.5% of the flights
Apogee with 5% less drag: +29 m; with 5% more: -27 m
How to read it:
- Nominal is the one flight with every input at its planned value.
- Landing distance is how far from the pad the rocket lands; landing east is the eastward, downwind, part of it. The nominal flight lands due east, so the two are equal. In the run the wind’s heading has a standard deviation of 15°, which pushes some flights north or south, so the mean landing east is smaller than the mean distance.
- Std dev is the standard deviation. If the spread is normal, about two values in three lie within one standard deviation of the mean. 5% and 95% are percentiles: one flight in twenty went lower than the 5% value, one in twenty higher than the 95% value. So nine flights in ten reached between about 1,040 and 1,200 m.
- The mean is not the nominal flight. The mean apogee is 6.5 m above the nominal one. With 200 flights the mean itself is uncertain by about 47.4 / √200 = 3.4 m (its standard error), so 6.5 m is 1.9 standard errors: chance can account for most of it. Part is that the apogee doesn’t respond evenly: 5% less drag gains 29 m and 5% more loses 27 m, so an even spread of drag lifts the mean apogee by about 1 m.
- The landing spreads far more than the apogee. The landing distance’s standard deviation is about a third of its mean (209 of 660 m); the apogee’s is 4% (47 of 1,120 m). Under the parachute the drift is the wind’s speed times the time in the air, and the wind’s speed is the most uncertain input here.
From the command line
hpr mc flies any design hpr sim flies, a staged .ork
included, with each dispersion an option in the units the command line uses elsewhere: degrees
for angles, a fraction for a percentage (--impulse-sd 0.03 is 3%). It prints the apogee’s and
the landing distance’s spreads, the landing ellipses below and the failed flights, and
--export runs.csv writes every flight’s draw and outcome, one row a flight. Its runs are the
library’s for the same seed, bit for bit: a test flies both and compares them
(mc_flies_a_public_ork_as_the_library_does).
hpr mc my-rocket.ork --runs 200 --seed 2026 --wind 4 --wind-from 270 \
--mass-sd 0.02 --drag-sd 0.05 --impulse-sd 0.03 --wind-sd 0.25 --wind-from-sd 15
From Python
hpr.MonteCarlo in the Python package flies the same run and returns it
as NumPy arrays. Each array is one column of the table hpr mc --export writes, with one value per
flight; failed flights are counted and their figures are NaN. It takes the same eleven
dispersions as hpr mc’s options, named for their units (impulse_sd_fraction,
wind_from_sd_deg). A test runs both on 40 flights with every input scattered and finds every
number equal, bit for bit, on one platform with both built in release mode
(test_a_run_is_hpr_mcs_bit_for_bit).
Landing ellipses and the summary tables aren’t in Python yet; NumPy finds the spreads from the
arrays. A configuration that drops a stage can’t be read from Python yet, so it runs only from
Rust and hpr mc.
Landing ellipses
A landing ellipse is the outline a range safety officer or a
competition asks for: an area on the ground that the rocket lands inside, say, 95 times in 100.
Comparing it with the field’s boundary says whether the field is big enough for this rocket in this
wind. For that check, use the next-flight ellipse below (Scatter::prediction_ellipse).
The ellipse assumes the landings follow a normal distribution, and it is only as good as the run’s landings, which carry the flight models’ errors and those of the dispersions you chose; neither has been compared with real flights yet. The section ends with how to tell when the landings aren’t normal.
hpr-sim draws the ellipse from the run’s landing points (Run::landing, then
Scatter::ellipse) in three steps. Its semi-major and semi-minor axes are its half-lengths
along its long and its short direction.
-
The center is the landings’ mean: here 626 m east and 23 m south of the pad.
-
The axes. The landings’ covariance gives the direction they spread most, the major axis, and the direction across it, the minor axis, with a standard deviation along each. Here those are about 214.6 m and 203.0 m. The spread is nearly round, because the wind’s uncertain heading (15°) spreads the landings sideways about as much as its uncertain speed (25%) spreads them downwind.
-
The size. If the landings follow a two-dimensional normal distribution, the ellipse reaching
kstandard deviations along each axis holds the sharep = 1 − e^(−k²/2)of them. So the ellipse of levelphask = √(−2 ln(1 − p)):Level p50% 90% 95% 99% Scale k1.177 2.146 2.448 3.035 The 95% ellipse’s semi-axes are 2.448 × 214.6 m = 525 m and 2.448 × 203.0 m = 497 m.
In two dimensions it takes more standard deviations to hold 95% than in one (2.448 against 1.960), because a landing can stray in two directions at once.
Heading is the major axis’s direction, clockwise from north, between 0° and 180°: here 131°, running from north-west to south-east. With axes this close to equal the heading means little: a few landings more or less could turn it a long way. A circle has no heading at all; hpr-sim then reports 90°, east.
The next flight. The mean and covariance of 200 flights are only estimates, so an ellipse
drawn from them holds a little less than its level of the flights still to come. For normal
landings Scatter::prediction_ellipse allows for that exactly. Its k comes from Hotelling’s
T² distribution, the one that accounts for the mean and the spread both being estimated from the
same flights: k² = ((n² − 1)/n)((1 − p)^(−2/(n − 2)) − 1) for n landings. With
200 landings its axes are 1.3% longer (532 m against 525 m); with 10 they would be 36% longer. Use
it to answer “will my next flight land in the field?”.
Landings inside counts the run’s landings each ellipse really holds
(Scatter::share_inside). Here they are 48.0%, 94.5% and 95.5%. Those are within the
standard error of a share p from a run of 200, √(p(1 − p)/200):
about 3.5 percentage points at 50%, 1.5 at 95%. So the landings are consistent with a normal
spread, though that doesn’t prove it. With few landings the shares run high, because the ellipse is fitted to the
same points: with three landings, even the 50% ellipse holds all three. If the share is far from
the level in a run of hundreds of flights, the landings aren’t normal: an uncertain wind
heading in a strong wind spreads them along an arc, and an ellipse is then the wrong shape. Look at
the points themselves (Scatter::points). A failed flight has no landing; it counts as outside
for the lower bound and inside for the upper (Failed flights are counted).
How far to trust it. The ellipse math is tested against normal spreads whose answers are known exactly (
ellipse.rs’s tests):
- The scale matches the NIST/SEMATECH handbook’s chi-square table and its closed form.
- The axes and heading of turned, stretched covariances come back to 1e-14.
- Integrating a normal density over its ellipse gives the level to 1e-12.
- 100,000 points drawn from a known normal spread give back its covariance, and land inside each ellipse at its level, within five standard errors.
- A new point lands inside the next-flight ellipse of 3, 5 or 20 others at its level, within five standard errors, and inside the plain ellipse visibly less often.
The tests can catch a wrong ellipse: the 95% ellipse of a spread 200 m long and 30 m wide (one standard deviation each way), turned 6° off its axes, holds 90.6%, and the test pins that.
What each dispersion does
Each dispersion is a standard deviation: zero, the default, leaves its input at the nominal value.
For each flight hpr-sim draws a standard normal number z (mean 0, standard deviation 1) for each
input and moves the input by σ z, with σ the standard deviation you gave. A rocket built of
more than one stage gets a draw for each stage, whether its stages fly together or separate; each
motor and each parachute gets its own draw too.
| Field | What each flight flies |
|---|---|
dry_mass_sd_fraction | Each stage’s mass without motors times 1 + σ z. Its moments of inertia scale with it. |
cg_sd_m | Each stage’s center of mass moved σ z meters towards the tail (towards the nose when negative). |
drag_sd_fraction | The rocket’s zero-lift drag coefficient times 1 + σ z, whether hpr-sim’s own, a drag table’s or a drag model’s. |
impulse_sd_fraction | Each motor’s thrust and propellant mass, both times 1 + σ z. Its total impulse changes and its specific impulse doesn’t, as for a motor that holds a little more or less of the same propellant. |
burn_time_sd_fraction | Each motor’s thrust curve stretched in time by 1 + σ z and its thrust divided by the same: a longer, softer burn of the same impulse. |
ejection_delay_sd_s | Each motor’s ejection delay plus σ z seconds, never below zero. |
wind_speed_sd_fraction | The wind at every height times 1 + σ z, never below calm. A calm forecast stays calm. |
wind_heading_sd_rad | The wind at every height turned σ z clockwise: the forecast’s direction, give or take. |
rail_elevation_sd_rad | The rail’s angle above the horizon plus σ z. Past vertical, it leans the other way. |
rail_azimuth_sd_rad | The rail’s heading plus σ z, clockwise. |
deployment_lag_sd_s | Each recovery device’s lag after its trigger plus σ z seconds, never below zero. A part’s tumble, which hpr-sim adds to a separated part with no device open, starts at the split and has no lag to scatter. |
Four cases need a word:
- A draw that makes an input impossible fails that flight. With a 60% mass spread, a draw of
zbelow −1.67 gives a negative mass; a rail drawn below the horizon is another. That flight is kept in the run as failed, with its reason, and counted (next section). The two delays and the wind’s speed are the exceptions: a charge can’t fire before its event and a wind can’t blow at less than calm, so a draw below zero is flown as zero. - A cluster is one draw. hpr-sim holds a cluster as one motor in a mount with several tubes, so all its motors get the same impulse and burn time.
- A vertical rail leans along one line. The elevation is dispersed in the plane of the rail’s
heading, so on a vertical rail with only its elevation dispersed every flight leans towards or
away from that heading, never sideways. RocketPy disperses its rail the same way, an inclination
and a heading (
rocketpy/stochastic/stochastic_flight.py:21-24, version 1.13.0). Disperse the heading too for leans in every direction. - A staged flight keeps its separations. Every flight comes apart where the nominal one does:
at the same event, or at the same time for a split the file times in seconds. A split timed by a
burnout follows the dispersed burn. One timed in seconds doesn’t, so a flight whose booster still
burns then stops with the flight’s error and counts as failed, unless the split may drop a
burning motor, as OpenRocket’s first burnout of a stage does
(decision record ADR-172).
--delay-sdmoves a booster’s ejection charge, and the parachute it fires, but not a split the file times in seconds. - A part dropped on the way up needs its device open. After a split with nothing left to burn, each part flies as a point with only its open devices’ drag. A part whose device fired by the split but waits out a dispersed lag would climb through the lag with no drag at all, so hpr refuses that flight, and it counts as failed.
Failed flights are counted
A run never drops a flight. Run::failed lists those that failed, saying whether the draw made
an impossible input or the flight refused it. A spread like Run::apogee counts every flight
tried: attempted() is the run’s size, count() the flights that gave a value, and missing()
the rest. Its mean and percentiles are over the flights that gave a value, so many failures can
bias them: check missing() first.
A share such as “reached 1,100 m” is given as two bounds, share_at_least(1100.0):
lowcounts a failed flight as not reaching it;highcounts it as reaching it.
With no failures the two are equal. With some, the truth lies between them, and a wide gap says the run can’t answer the question.
The same seed, the same run
Every number a flight draws comes from its own stream of random numbers, picked out by the run’s seed, the flight’s number in the run and the input it is for. So:
- flight 37 of a run is the same flight in a run of 100 or of 10,000;
- a run flown on one thread or on twelve is the same, bit for bit (with the
parallelfeature,run_parallel); - turning a dispersion on or off doesn’t change what the other inputs draw.
To fly on several threads, turn on the parallel feature where your program depends on hpr, as
Using it from your own program describes, and call
run_parallel instead of run:
[dependencies]
hpr = { git = "https://github.com/nrdptel/hpr-sim", rev = "<commit>", features = ["parallel"] }
On another platform (operating system and processor) a draw can differ in its last binary digit, because the normal numbers use the platform’s logarithm; a run then agrees to many digits, not to the bit. The decision record is ADR-134.
How long a run takes
On the development machine, an Apple M5 with 10 cores, 10,000 flights of a Level 2 rocket (one on a J, K or L impulse class motor) from ignition to the ground, under a drogue and a main, take:
| rocket | peak Mach | 10,000 flights, 10 threads | one flight, one thread |
|---|---|---|---|
| Valetudo, one of RocketPy’s examples: 9.7 kg on a K400C | 0.3 to 0.4 | 3.0 s | 1.9 ms |
| a 66 mm rocket with a 54 mm motor mount on a K940 | 1.6 to 2.0 | 9.4 s | 5.7 ms |
No flight failed. These are release builds, with optimisation on: add --release to
cargo run, or build your program with it. A debug build, what plain cargo run gives, is
many times slower. To time it on your own machine, from a copy of the repository:
cargo bench -p hpr --features parallel --bench ten_thousand
It takes about a minute. Its output, the before-and-after numbers and where the time goes are on Performance. Other machines haven’t been timed.
A rocket that passes Mach 1.2 flies on a supersonic table: its body’s lift and center of
pressure by the shock-expansion method, worked out at every 0.05 Mach once and then looked up
(The body faster than sound in a flight).
Building it takes about a fifth of a second for the rocket above. A dispersion changes the
rocket’s masses, its motor, the weather and a scale on its drag, never its shape, so the flights
of a run share the nominal flight’s table: the run builds it once. Each flight also reuses the
nominal rocket’s layout, its parts placed and weighed. Every flight flies exactly as it would
alone, bit for bit; a unit test holds a supersonic flight to that, on one, two and five threads
(samples_share_the_nominal_table_and_fly_as_alone).
Before this speed-up (M6.1d, October 2026), each flight built
its own table, and the supersonic run above took 264 s. To fly draws of your own, as a
sensitivity analysis does, use monte_carlo.fly(&draw), which shares the same
work. Flying a draw’s inputs yourself, monte_carlo.inputs(&draw)?.fly(), builds its own
table and layout. A two-stage rocket’s sustainer, built
when the stages separate, still builds its own table in every flight. Issue
#285 is about making each flight itself faster.
This times the run. It says nothing about how accurate a Mach 2 flight is: no validation flight goes past Mach 1.06 (see The body faster than sound in a flight).
Choosing the numbers
The spread a run gives is the spread you put in. Some places to start:
- Motor impulse. NFPA 1125, the code commercial motors are certified to, requires that the “standard deviation of the total impulse data shall be no greater than 6.7 percent of the mean” over a motor type’s certification firings (§8.1.7 and §8.2.7). Four certification reports of the National Association of Rocketry (NAR) measured 1.3% to 3.1%: an Estes C6 (2.0%), D12 (3.1%), E12 (1.28%) and an AeroTech G80 (2.3%), hosted on ThrustCurve.org. So 2% to 3% is a fair guess, and 6.7% the most the code allows.
- Ejection delay. The same code allows a measured delay to differ from the labelled one by “1.5 seconds or 20 percent (whichever is greater, but not to exceed 3 seconds)”. The E12 and G80 reports’ firings of one delay scatter by 0.2 to 0.9 s, and a delay’s average can sit more than a second from its label: the G80’s 7 s delay averaged 5.88 s. A dispersion is about the nominal value, so give the delay you expect, not only the one printed on the motor.
- Burn time. The code sets no limit of its own. The four reports disagree: the E12’s and G80’s burn times scatter by 1.5% and 1.8%, the older C6’s and D12’s by 17% and 18%. Why isn’t recorded; the older sheets may time the burn differently. Start from a few percent for a composite motor, and try a larger value to see whether it matters to your flight.
- Mass, center of mass and drag depend on how well you know your rocket. A rocket weighed ready to fly needs a smaller mass dispersion than one weighed on paper. Drag is usually the least certain of the three: Accuracy shows how far hpr-sim’s drag sits from other programs’ and from wind-tunnel data.
- Wind depends on the forecast, its age and the hour. A sounding of the day, or a forecast’s spread between models, is a better guide than a guess.
The NFPA wording is the 2019 edition’s, as quoted in the public first-draft documents of its next
revision
(public inputs 4 and 5),
whose first revisions FR-7 and FR-8 keep both sentences
(1125_A2021_PYR_AAA_FD_FRStatements.pdf).
The edition in force today hasn’t been checked.
What is not dispersed
- Inputs are independent: a heavier rocket isn’t also draggier.
- Every dispersion is normal; there are no uniform or skewed ones.
- Moving a stage’s center of mass keeps its inertia about the center.
- The drag dispersion scales the zero-lift drag only, not the normal force or the moments; a recovery device’s drag isn’t dispersed.
- A stretched thrust curve keeps its shape.
- The atmosphere’s temperature and pressure, a motor’s ignition time, a separation’s trigger or time, and events of your own.
What comes next
The run gives each flight’s whole FlightSummary,
so any number a flight reports can be spread with run.distribution(...), as the example does for
the landing. To find which input moves the apogee most, see
Sensitivity analysis.
The API reference is hpr_analysis::montecarlo,
hpr_analysis::statistics and
hpr_analysis::ellipse.
Sensitivity analysis
A Monte Carlo run shows how far a rocket’s apogee spreads. It doesn’t say which input causes the spread. Sensitivity analysis does: it ranks the uncertain inputs by how much each moves a result, so you know which to measure more carefully and which you can stop worrying about. This page shows hpr-sim’s two methods. They are Morris’s screening, which is cheap, and Sobol’ indices, which cost more and say more. It runs both on two test functions whose answers are known, then screens a rocket’s apogee and landing. It needs some Rust, and follows on from Monte Carlo dispersion.
How far to trust it. The two methods are tested against answers known in closed form. On a rocket, the ranking they give is only as good as the ranges you give and hpr-sim’s flight models, which are not yet validated against real flights (Accuracy).
- Tested: both methods match the known answers of two standard test functions within four of their own standard errors. Repeated with 500 to 1,000 different seeds, the error an analysis reports is the scatter the repeats really show: for every Sobol’ index of Ishigami’s function, and for the Sobol’ indices and Morris
μ*s of a four-factor g function. The same seed gives the same numbers, bit for bit (tests/sensitivity.rs).- Not checked: whether a rocket’s ranking matches what real flights would show.
- Left out: inputs that depend on each other, inputs that aren’t spread evenly over a range, and the Sobol’ indices of pairs of inputs. A Sobol’ analysis costs thousands of runs per input: 65,536 flights for this page’s rocket, against Morris’s 70, so the rocket example uses only the Morris screening.
Inputs as factors
Each uncertain input is a factor: a name and a range, with every value in the range equally
likely (Factor). The drag might be anywhere from 10% below its estimate to 10% above, say, or
the wind anywhere from calm to 8 m/s. Factors are independent: a heavier rocket isn’t also
draggier.
Both methods work the same way. They lay out the points to try (a design), you run your model at each point in order, and they analyze what came back. The model can be a whole flight, with the factors set into its inputs, or any function. So you can run the points however you like, on many threads or many machines. Each method also has a shortcut that takes a closure and runs the points itself.
Morris screening
Morris’s method nudges one factor at a time and watches the output move. It works on a grid: each
factor’s range is cut into p evenly spaced levels (4 is usual). A step moves one factor p/2
levels (two of four), which is a fraction Δ = p / (2(p − 1)) of its range: 2/3 for four
levels.
The change in the output over one step, divided by Δ, is an
elementary effect. It is the change the factor would cause across
its whole range, at the slope measured over the step. A path starts at a random grid point and
steps each factor once, in a random order, so k factors take k + 1 runs per path. Each step
gives one effect. After r paths, each factor has r effects, and the screening reports three
numbers for each factor (ElementaryEffects):
| Number | What it is | What it says |
|---|---|---|
μ* (mean_absolute) | the mean of the effects’ sizes | how much the factor matters: the ranking |
σ (standard_deviation) | how much the effects vary | a large σ means the factor’s effect bends, or depends on other factors |
μ (mean) | the mean of the effects, with their signs | effects of opposite sign cancel here, so use μ* to rank |
A factor with μ* near zero can be left at its planned value. The screening also gives μ*’s
standard error, which shrinks as 1/√r: ten paths are usually enough to rank, and the cost is
r (k + 1) runs.
For example, Ishigami’s test function (below) has a sin² x₂ as a term, with a = 7. On a
four-level grid from −π to π, a step of x₂ moves sin² by exactly 3/4, up or down, so every
effect of x₂ is ± (3/4)(7)/(2/3) = ± 7.875. The example’s screening finds μ* = 7.875 with no
error, and μ near zero.
M. D. Morris, “Factorial sampling plans for preliminary computational experiments”,
Technometrics 33(2), 161–174, 1991, defines the effects, the grid and the paths. F. Campolongo,
J. Cariboni and A. Saltelli, “An effective screening design for sensitivity analysis of large
models”, Environmental Modelling & Software 22, 1509–1518, 2007, add μ*. They found, by
experiment rather than proof, that it ranks factors in the same order as the total Sobol’ index.
That is usual, not certain. A step spans about half of each range, so it can miss a response
that repeats over about half the range. Ishigami’s sin² x₂ repeats every π, half of its range
from −π to π. At four levels, Morris puts x₂ first (μ* 7.875 against 7.704 for x₁), while
the total index puts x₁ first (0.558 against 0.442). At six levels or more, Morris puts x₂
last (2.687 at six levels), though it causes the largest share of the variance alone. A rocket’s
outputs rarely repeat like that, but treat factors whose μ*s are close as tied, and use Sobol’
indices to settle an order that matters.
hpr_analysis::sensitivity::morris gives the
equations and page numbers.
Sobol’ indices
Sobol’s method splits the output’s variance (the square of its
standard deviation) among the factors. It reports two shares for
each factor (SobolIndex):
- The first-order index
Sᵢis the share of the variance the factor causes alone. If you could pin the factor to its true value, the variance would drop by this share, on average. - The total index
S_Tᵢis the share it has any part in, alone or together with others. If you pinned every other factor, this share of the variance would remain, on average.
Sᵢ ≤ S_Tᵢ. The gap is how much the factor acts through others. A factor with a total index near
zero can be left at its planned value.
The analysis draws two independent tables, A and B, each of N rows with every factor spread
over its range. Then, for each factor i, it builds a third table: A with factor i’s column
taken from B. Comparing the outputs of the three tables row by row gives both indices. A.
Saltelli and others (“Variance based sensitivity analysis of model output”, Computer Physics
Communications 181, 259–270, 2010) give the estimates used here, the best of those they compared.
The cost is N (k + 2) runs, and the error shrinks as 1/√N: with N = 8,192, an index comes
out to about ±0.01.
Each index comes with its standard error, found by the
delta method from how much the rows scatter.
hpr_analysis::sensitivity::sobol gives the
formulas.
Checked against
Two test functions have Sobol’ indices known in closed form
(hpr_analysis::sensitivity::benchmark):
- Ishigami and Homma’s function,
y = sin x₁ + 7 sin² x₂ + 0.1 x₃⁴ sin x₁, eachxfrom −π to π.x₃does nothing alone (S₃ = 0), yet it is part of a quarter of the variance, throughx₁. The closed forms are I. M. Sobol’ and Y. L. Levitan’s (Computer Physics Communications 117, 1999, p. 57). - Sobol’s g function, a product of one term per factor, each with a weight
aᵢ: the factor matters most ataᵢ = 0and hardly at all at 99. The closed forms are Saltelli and others’ (2010, p. 268). The tests useaᵢ= 0, 1, 4.5, 9 and four 99s, spanning the four classes Marrel and others (2008) name, as the SFU library of test functions quotes them: very important at 0, relatively important at 1, non-important at 9 and non-significant at 99.
For Morris there is also an exact answer. A screening estimates the moments of a finite set of
effects: every step on the grid. Morris::population runs the model at every grid point and
computes them exactly: the example’s “whole grid” column. For both functions they also follow in
closed form, which the tests check.
The tests in
tests/sensitivity.rs
check four things:
- Every Sobol’ index of both functions, from 32,768 and 16,384 rows, lies within four standard errors of its closed form.
- Every Morris
μ*, from 1,000 paths, lies within four standard errors of the exact one. - The standard errors are honest. Take each estimate’s distance from the known answer, divided by
its standard error. Over
nseeds, these have a mean within4/√nof 0 and a standard deviation within4/√(2n)of 1. This holds for every Sobol’ index of Ishigami’s function (1,000 seeds) and of the g function’s first four factors (500 seeds), and for Morris’sμ*of those four (1,000 seeds). The same test checks that standard errors 15% too small would fail. A separate unit test computes each Sobol’ standard error a second way, from the raw row means. - On the g function, twenty Morris paths rank the four factors that matter in the order of their total indices.
An example
The example program
crates/hpr/examples/sensitivity.rs
runs both methods on the two test functions and prints them beside the known answers. Then it
screens the rocket of Monte Carlo dispersion over six inputs. Run it from a copy
of the repository with:
cargo run --example sensitivity -p hpr
Excerpts from it follow. The test functions take a closure:
let ishigami = Ishigami::STANDARD;
let sobol = Sobol::new(Ishigami::factors()?, 8192)?; // N = 8,192 rows: 40,960 runs
let indices = sobol.indices(seed, |x| ishigami.evaluate(x))?;
For the rocket, the screening lays out its points and the program flies each one. A Monte Carlo
set-up with no dispersion turns a draw (each input’s factor or offset) into the flight it flies
(MonteCarlo::inputs):
let monte_carlo = MonteCarlo::new(launch.inputs()?, Dispersion::default())?;
let nominal = monte_carlo.draw(seed, 0); // every input as planned
let factors = vec![
Factor::new("dry mass factor", 0.95, 1.05)?,
Factor::new("drag factor", 0.9, 1.1)?,
Factor::new("impulse factor", 0.94, 1.06)?,
Factor::new("wind speed (m/s)", 0.0, 8.0)?,
Factor::new("wind turn (°)", -30.0, 30.0)?,
Factor::new("rail angle (°)", 80.0, 90.0)?,
];
let morris = Morris::new(factors, 4, 10)?; // 4 levels, 10 paths: 70 flights
let design = morris.design(seed);
for x in design.points() {
let mut draw = nominal.clone();
draw.drag_scale = x[1]; // a factor on the drag
draw.wind_speed_scale = x[3] / 4.0; // a factor on the 4 m/s forecast
draw.rail_elevation_offset_rad = (x[5] - 85.0).to_radians(); // an offset from 85°
// ... and the dry mass, impulse and wind turn the same way
let flight = monte_carlo.fly(&draw)?;
apogees.push(flight.apogee.as_ref().ok_or("no apogee")?.height_above_ground_m);
}
let apogee = design.analyze(&apogees)?; // the same flights, any output
The six inputs, with ranges made up for the example:
| Input | Range | Planned value | What it is |
|---|---|---|---|
| dry mass factor | 0.95 to 1.05 | 1 | the rocket’s mass without its motor, ±5% |
| drag factor | 0.9 to 1.1 | 1 | the zero-lift drag, ±10% |
| impulse factor | 0.94 to 1.06 | 1 | the motor’s total impulse, ±6%; NFPA 1125 caps a motor type’s standard deviation at 6.7% |
| wind speed | 0 to 8 m/s | 4 m/s | the wind at every height |
| wind turn | −30° to 30° | 0° | the wind’s direction, turned clockwise from the forecast’s (a wind from the west) |
| rail angle | 80° to 90° | 85° | the rail’s angle above the horizon; 90° is vertical |
A Draw can also move a stage’s center of mass, the motor’s burn time and ejection delay, the
rail’s heading and a recovery device’s delay (What each dispersion does).
It prints:
Sobol' indices of Ishigami's function (a = 7, b = 0.1), 8192 rows, 40960 runs
first order: known estimate ± error total: known estimate ± error
x1 0.3139 0.3101 ± 0.0108 0.5576 0.5453 ± 0.0154
x2 0.4424 0.4549 ± 0.0098 0.4424 0.4494 ± 0.0074
x3 0.0000 -0.0071 ± 0.0099 0.2437 0.2404 ± 0.0050
Sobol' indices of the g function, a = 0, 1, 4.5, 9, 99 (four times), 8192 rows, 81920 runs
first order: known estimate ± error total: known estimate ± error
x1 0.7162 0.7162 ± 0.0124 0.7871 0.7844 ± 0.0127
x2 0.1790 0.1683 ± 0.0077 0.2422 0.2444 ± 0.0050
x3 0.0237 0.0246 ± 0.0029 0.0343 0.0353 ± 0.0008
x4 0.0072 0.0059 ± 0.0017 0.0105 0.0109 ± 0.0003
x5 0.0001 0.0001 ± 0.0002 0.0001 0.0001 ± 0.0000
(x6 to x8 are as x5)
Morris screening of Ishigami's function, 4 levels, 100 paths, 400 runs
μ*: whole grid estimate ± error σ: whole grid estimate
x1 7.704 7.079 ± 0.625 6.249 6.249
x2 7.875 7.875 ± 0.000 7.875 7.915
x3 6.249 6.124 ± 0.628 8.837 8.784
Morris screening of the Monte Carlo page's rocket, 4 levels, 10 paths, 70 flights
Not yet validated: see the Accuracy page before trusting these numbers.
Each effect is the change across the input's whole range, m.
apogee: μ* ± error σ landing distance: μ* ± error σ
dry mass factor 36.0 ± 1.8 5.6 70.0 ± 22.5 72.0
drag factor 112.9 ± 3.2 10.3 80.5 ± 18.0 57.0
impulse factor 118.3 ± 2.3 7.1 107.5 ± 23.7 75.1
wind speed (m/s) 50.3 ± 4.1 13.1 1417.5 ± 101.4 320.7
wind turn (°) 4.4 ± 1.0 5.4 53.4 ± 10.5 64.5
rail angle (°) 68.4 ± 3.1 9.9 350.2 ± 17.6 231.0
How to read it:
- The test functions land within 1.4 standard errors of the known answers, from the printed
digits.
S₃comes out slightly below zero: the estimate of a share that is really zero scatters around zero. The g function’sx5shows “± 0.0000” because its error is below 0.00005. - Ishigami’s Morris screening at 400 runs puts
x2,x1,x3in the whole grid’s order, but the gaps are only about one standard error, so at 100 paths they aren’t really ranked. Itsσs are as large as itsμ*s, the sign of a function that bends or whose factors act together. - The rocket’s apogee moves most with the motor’s impulse (118 m across ±6%) and the drag
(113 m across ±10%), then the rail’s angle (68 m across 80° to 90°), the wind’s speed (50 m)
and the dry mass (36 m across ±5%). The wind’s direction hardly matters (4 m). The
σs are small beside theμ*s: each input acts nearly in a straight line and alone. The exception is the wind’s direction, whoseσ(5.4 m) exceeds itsμ*(4.4 m): its effects differ in size or sign from path to path. The impulse and the drag are 1.4 combined standard errors apart: ten paths don’t settle which comes first. - The landing is the wind’s: 1,418 m across calm to 8 m/s, then the rail’s angle (350 m).
Here the
σs are large, so these effects bend or depend on the other inputs.
The ranges are made up for the example. With your own rocket, use the ranges your measurements support; a factor’s effect grows with its range.
Choosing the numbers
- Levels: four, an even number as the method needs. More levels let a path start from more places in each range, at the same cost per path, but the step stays about half the range, so more levels don’t help with a response that repeats (see Morris screening).
- Paths: ten to twenty to rank. Check
μ*’s standard error: two factors whoseμ*s differ by less than a couple of errors aren’t ranked yet. - Rows: a Sobol’ analysis needs about 8,000 rows, each
k + 2runs, to pin an index to about ±0.01 to ±0.015 (one standard error). Use Morris first to find the few factors that matter, then Sobol’ on those if you need the shares or the order of two close ones. - Normal inputs: give each one the same multiple of its standard deviation, such as ±2, so they are compared alike. A range spreads the input evenly, which weighs its ends more than a normal spread does.
Left out
- Factors are uniform over their ranges and independent. A normal input can be given as a range about its mean, such as ±2 standard deviations, which spreads it more evenly than it really is.
- Sobol’ rows are plain pseudo-random draws, not quasi-random ones (evenly spread sequences, such as Sobol’s own), which converge faster.
- No second-order Sobol’ indices (the share of each pair alone).
- No choice of the most spread-out Morris paths among many (Campolongo’s improvement).
- No command-line or Python front end yet.
The API reference is
hpr_analysis::sensitivity.
Optimization
Sometimes you know the result you want and need the design that gives it: that is optimization. That might be a rocket that reaches exactly 3,048 m (10,000 ft) for a competition, or the lightest fins that keep it stable. An optimizer searches for that design. It tries designs, flies each one, and uses what it learns to choose better ones, until it finds the best it can. This page shows hpr-sim’s optimizer, CMA-ES. It runs on test functions whose answers are known, then finds the nose ballast and body length that send a rocket to 3,048 m with a chosen stability margin. Then it chooses a motor and a catalog nose cone as well, within a competition’s limits on stability and speed off the rail. Then a second optimizer, NSGA-II, weighs two goals against each other: how much apogee each extra calibre of stability costs. A third, EGO, is for models so slow that only tens of evaluations can be afforded. It needs some Rust.
How far to trust it. All three optimizers are tested against answers known exactly; CMA-ES and NSGA-II also against outside implementations, EGO not yet. On a rocket, their answers are only as good as hpr-sim’s flight models, which are not yet validated against real flights (Accuracy).
- Tested: four standard test functions of ten variables are run from 20 seeds each. Every run reaches a value of 10⁻¹⁰ or less, which puts its best point within 10⁻⁵ of the known minimum (10⁻⁴ for Rosenbrock’s function). The exception is 3 of the 20 Rosenbrock runs, which end in that function’s known local minimum instead. An evaluation is one call of your model: here, one flight. Over the 20 seeds, the median number of evaluations is within 25% of pycma’s, the method’s author’s own Python implementation run from the same starts. That 25% is the test’s bound; measured, they are within 5%. Twelve generations of a three-variable run are also recomputed by a separate implementation of the published formulas, and agree to rounding (
tests/optimize.rs). A run is repeated bit for bit from its seed (a_seed_fixes_the_runinoptimize/cmaes.rs).- Checked by re-flying: the design each of the first two examples finds is flown again from scratch, and again with the flight’s numerical integration 100 times stricter (tolerances). Both reach apogee within 0.1 m of 3,048 m (the first example’s within 2 mm, measured).
- Limits (a minimum stability margin, say): held to three test problems whose answers on their limits are known exactly, from 20 seeds each, to 10⁻¹⁰ (
tests/constrained.rs). The second example’s winner is flown again and keeps its margin limits over the whole ascent and its rail-exit limit.- Choices (which motor, which catalog part): held to three test functions mixing continuous and whole-number variables, at two sizes, from 20 seeds each. Every run reaches the known minimum, with every whole number exactly right, and the median evaluations are within 25% of an outside implementation’s (the test’s bound; measured, within 5%;
tests/mixed.rs). The second example chooses a motor and a nose cone that hit 3,048 m, checked by flying them again (Choices). It finds a design that does; it doesn’t promise the best of several that would.- Trade-offs between goals (a Pareto front): NSGA-II is held to three test problems whose fronts are known exactly, from 20 seeds each, and to pymoo, an outside implementation run with the same settings. Every run’s front lies within twice pymoo’s worst distance from the true front (the test’s bound). At the median, hpr-sim’s fronts lie 1% to 8% closer to the true front than pymoo’s, and cover it as evenly, within 3% (the test allows a factor of 1.25 either way; Trade-offs). The third example’s front designs are flown again and give the same apogee and margin to the bit: that shows the result repeats, not that it is right. At 2.5 calibres the front is within 0.5% of the apogee CMA-ES finds alone (the example’s check).
- Few evaluations (EGO): from 20 seeds each, two standard test functions with several local minima, Branin’s (two variables) and Hartmann’s (three), end within 1% of their known minima in 50 evaluations (the test’s bound; measured, within 0.12%). Hartmann’s six-variable function needs its values transformed first, as the method’s authors did: then all 20 runs end within 1% in 250 evaluations (measured, within 0.29%), against 6 of 20 without. That check takes minutes in a release build, so the local gate runs it, not CI (Few evaluations).
- Left out, for now: optimizing a Monte Carlo run’s statistics, EGO beyond six variables, and a rule that stops NSGA-II when its front has settled (it runs the generations it is given). They are the next steps of M6.2, the optimization milestone. There is no command-line or Python front end yet.
What the optimizer does
The optimizer changes variables: numbers in the design, such as a ballast mass or a body length
(Variable). For each variable you give three things:
- a start, the value to begin from;
- a step, how far to try from the start at first, about a quarter to a third of the range you expect the answer in;
- optional bounds, the range the variable must stay in.
It minimizes one number your model returns. To maximize something, minimize its negative. To hit
a target, minimize the squared miss: (apogee − 3,048 m)² is smallest, zero, on the target.
How CMA-ES searches
CMA-ES is the covariance matrix adaptation evolution strategy, N. Hansen’s method (“The CMA Evolution Strategy: A Tutorial”, arXiv:1604.00772, 2023). It keeps a cloud of likely designs, shaped like a stretched, tilted ball (a normal distribution), and repeats four steps each generation:
- Sample. Draw a handful of designs from the cloud:
λ = 4 + ⌊3 ln n⌋of them fornvariables, so 6 for two variables and 10 for ten. - Rank. Fly each one and sort them by the number to minimize. Only the order matters, not the values.
- Move. Move the cloud’s center to a weighted average of the better half, the best weighted most.
- Learn. Stretch and tilt the cloud toward the directions the good designs lay in. Widen it when successive moves go the same way, which means the steps are too short, and shrink it when they cancel out, which means the steps overshoot.
So the cloud learns the problem’s shape. If the apogee depends far more on one variable than on another, or on a combination of them, the cloud stretches to match. No derivatives are needed, which suits flights: their outputs wobble slightly in the last digits as the integrator’s steps change, which would spoil a slope.
The formulas and default settings are the tutorial’s (Appendix A and Table 1). Table 1 also gives the worse half of each generation negative weights, which push the cloud away from them: the active variant. hpr-sim gives them zero weight, as the tutorial’s own code does.
The cloud is shaped in each variable divided by its step, so variables in meters and in
kilograms start on an equal footing, as the tutorial advises.
hpr_analysis::optimize::cmaes lists every equation.
Bounds
A sampled design outside a variable’s bounds is thrown away and drawn again, the tutorial’s simplest way of handling bounds. It works well while the answer lies inside the range. If it lies on a bound, the run slows down near it. If any one design of a generation is still outside after 1,000 tries, the run stops and says so. With many variables near their bounds this comes soon: each such variable halves the chance that a draw falls inside.
When a run stops
A run stops at the first of these (Stop):
| Stop | When |
|---|---|
Target | a value at or below the target you set: a miss small enough |
Evaluations | the number of evaluations you allowed is used up (10,000 by default) |
TolX | the cloud has shrunk below 10⁻¹² of each variable’s step: the run has converged |
TolFun | the values have stopped changing by more than 10⁻¹², in your model’s units |
Condition | the cloud is 10⁷ times longer than it is wide, too thin to keep accurate, or its numbers have overflowed |
Bounds | a design of a generation couldn’t be drawn inside the bounds (in the first generation, start returns an error instead) |
Both tolerances can be changed or turned off (with_tolerance_x, with_tolerance_value).
The result (Optimum) gives the best design found, its value, how many evaluations the run made,
and why it stopped. If every design so far failed (your model returned infinity for each), the
value is infinite: check it before using the design.
Checked against
Four test functions from CMA-ES’s authors (N. Hansen, S. D. Müller and P. Koumoutsakos,
Evolutionary Computation 11(1), 2003, Table 1), each with ten variables
(hpr_analysis::optimize::benchmark):
| Function | What it tests | Minimum |
|---|---|---|
sphere, Σ xᵢ² | the step size alone | 0 at x = 0 |
| ellipsoid | stretching the cloud: coefficients from 1 to 10⁶, so the cloud must grow 1,000 times longer one way than the other | 0 at x = 0 |
| rotated ellipsoid | the same, tilted so that no single variable lines up with it | 0 at x = 0 |
| Rosenbrock’s function | following a long, curved valley | 0 at x = 1 |
Each run starts with every variable at 1 (at 0 for Rosenbrock’s function) and steps of 0.5, and
goes until the value is 10⁻¹⁰ or less, the stopping value of the 2003 paper. The tests in
tests/optimize.rs
run each function from 20 seeds and check:
- Every sphere and ellipsoid run reaches the minimum, which puts its best point within 10⁻⁵ of
x = 0. - 17 of 20 Rosenbrock runs reach the minimum at
x = 1. The other 3 end in the function’s local minimum near(−1, 1, …, 1). The test requires at least 17. CMA-ES’s authors note that local minimum, and report 1 to 3 runs of 20 missing the global one at 4 to 16 variables (Kern, Hansen and Koumoutsakos, 2006). pycma ends in the local minimum in 1 of 20. Measured once over seeds 1 to 300, hpr-sim reaches the global minimum in 290 (97%), so 17 of 20 is at the low end of what chance gives. - The median number of evaluations is within 25% of pycma’s from the same starts (the test’s bound). Measured, they are within 5%:
| Function | hpr-sim | pycma 4.5.0 |
|---|---|---|
| sphere | 1,635 | 1,640 |
| ellipsoid | 5,920 | 5,910 |
| rotated ellipsoid | 6,010 | 5,930 |
| Rosenbrock | 6,445 | 6,195 |
- Tilting the ellipsoid changes the count by under 10%: the cloud learns the tilt.
- Every generation’s mean, cloud size and shape, over twelve generations with steps that differ 200-fold, match a second, separate implementation of the tutorial’s formulas to rounding. It takes the cloud’s inverse square root by a different method (the Denman–Beavers iteration), and passes through both branches of the step that pauses the shape’s learning when the cloud grows fast.
- The default settings that pycma also takes from the tutorial’s Table 1 match its values to rounding. pycma departs from the table in a few places by its author’s choice, such as how fast the cloud’s size adapts. That is why the counts differ a little.
The tests catch real faults. Without the step that learns the cloud’s shape from a generation’s better half (the rank-μ update, μ being the size of that half), the ellipsoid takes a third more evaluations (7,875) and the pycma comparison fails. A reviewer found three faults the pycma comparison misses: the shape’s learning never paused, the pause’s test off by one generation, and the mean’s step not scaled back to the variable’s units. Each fails the second implementation’s check. These were one-off checks, recorded in ADR-138 (the decision record for this work), not run in CI.
pycma is run by
validation/oracles/cmaes/pycma_runs.py,
whose results are committed beside the test.
An example
The example program
crates/hpr/examples/optimization.rs
runs CMA-ES on three of the test functions. Then it takes a 66 mm rocket on a J760, starting from
a 3 m rail at 85° into 5 m/s of wind. It finds the nose ballast and body tube length that give it
two things at once: an apogee of 3,048 m above the pad, and a static margin of 2.20
calibres at launch mass (at Mach 0.3). Run it from a copy of the
repository with:
cargo run --example optimization -p hpr
Why two goals? With one goal, the apogee, and two variables, there is a whole curve of answers: more ballast and a shorter body cancel out, and the run would stop at whichever hit it reached first. A second goal, the margin, leaves one design that meets both, so the answer is the rocket’s, not the run’s.
The heart of it, abridged from the example:
let variables = vec![
Variable::new("nose ballast (kg)", 0.3, 0.15)?.within(0.0, 1.0)?,
Variable::new("body tube (m)", 1.0, 0.1)?.within(0.6, 1.4)?,
];
let optimizer = Cmaes::new(variables)?
.with_target(1e-4)? // stop within a centimeter and a ten-thousandth of a calibre
.with_max_evaluations(2_000)?;
let optimum = optimizer.minimize(seed, |x| {
// (apogee miss in m)² + (margin miss in hundredths of a calibre)²;
// a design the simulator refuses ranks last
miss(x[0], x[1], FlightSettings::default()).unwrap_or(f64::INFINITY)
})?;
miss builds the rocket with that ballast and body length, flies it, and adds the two squared
misses, each in a unit that makes a miss of one about equally bad. A design that can’t fly returns
infinity, which ranks below every real flight. Then the example flies the winner twice more: once
from a fresh build, which must give the optimizer’s result to the last bit, and once with the
integrator’s tolerances 100 times tighter, which must still reach apogee within 0.1 m of
3,048 m.
The example prints whether each test function reached its minimum, not how many evaluations it
took. Operating systems round the last bit of ln and exp differently, and an optimizer’s path
amplifies that, so the counts move by a few per cent between macOS, Linux and Windows. The
medians above are the test’s, on the development machine. It prints:
CMA-ES on three test functions, 10 variables from 1.0, run to f ≤ 1e-10 (seed 2026)
function reached f ≤ 1e-10 best point within 1e-5 of x = 0
sphere yes yes
ellipsoid yes yes
rotated ellipsoid yes yes
A 66 mm rocket on a J760: find the nose ballast and body length for a 3,048 m apogee
and a static margin of 2.20 calibres at launch mass, at Mach 0.3
Not yet validated: see the Accuracy page before trusting these numbers.
Start: 0.30 kg of ballast, a 1.00 m body: apogee 3128.8 m, margin 1.79 calibres
Found: 0.34 kg of ballast, a 1.11 m body (stopped: Target)
Flown again: apogee 3048.0 m, margin 2.20 calibres
Flown again, tolerances 100 times tighter: apogee 3048.0 m
The rocket started 81 m too high, with a margin of 1.79 calibres. The optimizer found the design in 300 flights on the development machine, about 50 generations. Flown again with tighter tolerances, its apogee moves by 1.4 mm. Limits on other things, such as the rail-exit speed, are the subject of Limits on a design, and the second example uses them.
Evaluating designs your own way
minimize flies one design after another. To fly a generation’s designs on several threads or
machines, use the run’s steps yourself (Run): Cmaes::start draws the first generation,
candidates lists its designs, and tell takes their values, in the same order, and draws the
next. It returns the result once the run stops.
Choosing the numbers
- Steps: about a quarter to a third of the range the answer is likely in, in the variable’s own units. Steps of very different sizes are fine: the run works in each variable divided by its step.
- Target: for a target apogee, the squared miss you accept:
0.1 * 0.1for 0.1 m. Without a target, a run goes on until it converges, which can take many more flights than a hit needs. - Evaluations: a cap on flights, 10,000 by default. The first example’s two variables needed 300 flights, the second example’s four variables 360; the ten-variable test functions take 1,600 to 6,500 evaluations to converge.
- Population: leave it at the default unless the output has many local minima. Then a larger
population (
Cmaes::with_population) searches more widely, at more flights per generation. - Seed: a different seed gives a different run. Rerun with two or three seeds if the answer matters: if they agree, the answer is not luck.
- NSGA-II’s population and generations: the number of flights is the population times the generations. The defaults, 100 designs for 250 generations, are 25,000 flights, sized for 30 variables; the third example’s two variables needed 20 designs for 25 generations, 500 flights, to pass its 0.5% check. The population is also how many designs the front can hold. Compare the fronts from two seeds before trusting one.
Limits on a design
A design usually has limits as well as a goal: a stability margin of at least 1.5 calibres, say,
or at least 15 m/s off the rail. The optimizer takes them as constraints, each written as a
number g that must not be above zero. A margin of at least 1.5 calibres is g = 1.5 − margin.
Your model returns an Evaluation: the value, and the violation, the sum of every g that
is above zero. Evaluation::constrained(value, &[g1, g2]) adds them up for you.
Candidates are ranked by K. Deb’s feasibility rules (ADR-139, from Deb’s 2000 paper):
| Comparing | The better one is |
|---|---|
| one that keeps every limit, one that doesn’t | the one that keeps them |
| two that keep every limit | the smaller value |
| two that break some | the smaller violation |
No penalty weight is needed, because a value is never weighed against a violation. Do scale the
limits so that they count alike: a margin 0.1 calibre short and a rail speed 0.1 m/s short are
not equally bad, so divide each g by a size you care about (the 1.5 calibres, the 15 m/s).
Candidates that break a limit are kept and ranked, not redrawn, so a run can close in on an
answer that sits right on a limit, as most good designs do. Only a point that keeps every limit
counts as reaching a target; the Optimum reports its violation, zero when it keeps them all.
For a bound that the answer will sit on, leave the variable unbounded on that side and write the
bound as a limit; clamp the value you fly (no negative ballast), and work the limit out from the
unclamped one. A design that can’t be flown at all returns Evaluation::failed(), which ranks
behind every other, and a run that never keeps every limit says so: its violation is above
zero.
The tests hold this to three problems whose answers on their limits are known exactly:
| Problem | Limit | Answer |
|---|---|---|
| Σ xᵢ², 10 variables | x₀ ≥ 1 | 1, at x = (1, 0, …, 0) |
| Σ xᵢ², 10 variables (the “tangent” problem) | Σ xᵢ ≥ 10 | 10, at every xᵢ = 1 |
| g06 of the CEC 2006 benchmark, 2 variables | inside one circle, outside another | −6961.81388, where the circles cross |
From each of 20 seeds the run reaches the answer to 10⁻¹⁰ of its size and to within 10⁻⁴ of the point, also when it starts where the limit is broken. The next section’s example puts limits on a rocket: its stability margin and its speed off the rail.
Choices: a motor, a catalog part
Some variables are choices from a list rather than amounts: which motor, which nose cone from a
maker’s catalog. Make each one an integer variable (Variable::integer). It takes only the
whole numbers from its low bound to its high one, and your model uses the number as a place in
your list: 0 for the first motor, 1 for the second, and so on.
The optimizer still draws a real number for the variable, and gives your model the nearest whole
number. Left at that, the cloud would shrink in that variable until every draw rounded to the same
number, and the choice would freeze, even with a better one next to it. CMA-ES with margin
(R. Hamano, S. Saito, M. Nomura and S. Shirakawa, GECCO 2022,
arXiv:2205.13482) prevents that. After each generation it
keeps at least a small chance, the margin (nothing to do with a stability margin), that a draw
lands on another value. It does so by
moving the cloud’s center towards the edge between two values, or by widening the cloud in that
variable. The margin is 1/(n λ), for n variables and λ designs a generation: 1 in 32 for the
example below. So a choice is never final: its neighbours keep being tried.
Put the list in an order where neighbours are alike, such as motors by total impulse. The optimizer steps between neighbouring numbers, so in that order a step up means a little more motor. A list in no order still works, but the search has less to go on.
The example
The example program
crates/hpr/examples/motor_and_nose.rs
builds a 2.6 in rocket from Madcow Rocketry’s parts in the built-in
catalog: a 1.0 m fiberglass body tube, an 18 in motor tube and three
fiberglass fins. It flies it from a 3 m rail at 85° into 5 m/s of wind. The optimizer chooses four
things:
| Variable | Kind | Range |
|---|---|---|
| the motor | integer | the five 54 mm motors of the built-in catalog that fit, by total impulse |
| the nose cone | integer | four Madcow nose cones for 2.6 in, shortest first |
| the nose ballast | continuous | 0 to 1.5 kg |
| the fins’ span | continuous | 3 to 15 cm |
The goal is the squared miss from 3,048 m. The limits come from the International Rocket Engineering Competition’s rules (its Design, Test & Evaluation Guide, 2025), and hold over the whole ascent, from the rail exit to apogee (§10.3.1 says “from launch”; on the rail, the rail holds the rocket):
- a stability margin of at least 1.5 calibres in flight (§10.3.1 asks for a “dynamic” margin; the example takes the flight margin, the margin at the flight’s own Mach number, as the flight metrics define it);
- a static margin, the one at Mach 0, of at most 4 calibres, and a flight margin of at most 6, so the rocket isn’t over-stable (§10.4.1);
- at least 30 m/s off the rail (§10.2.1).
The margins change through the flight: the center of mass moves forward as the motor burns, so the margin grows, and the flight margin changes with speed as well. For the lower limit the example uses the least flight margin of the ascent, which the flight finds inside its steps, not only at their ends. For the upper limits it uses the largest static and flight margins seen at the end of each integration step. The static margin only grows during the burn and stays put after it, so its largest is exact. A peak of the flight margin between two step ends could be missed; the winner’s largest, 4.83 calibres, is well under its limit of 6.
The heart of it, abridged from the example. A choice is an integer variable: set its bounds with
within, then mark it integer, and use the whole number your model is given as a place in your
list. With limits, use the run’s steps yourself (Run) and give tell_constrained an
Evaluation per design:
let variables = vec![
Variable::new("motor", 2.0, 1.0)?.within(0.0, 4.0)?.integer()?, // a place in the motor list
Variable::new("nose", 1.0, 1.0)?.within(0.0, 3.0)?.integer()?, // a place in the nose list
Variable::new("nose ballast (kg)", 0.4, 0.15)?.within(0.0, 1.5)?,
Variable::new("fin span (m)", 0.07, 0.015)?.within(0.03, 0.15)?,
];
let mut run = Cmaes::new(variables)?.with_target(1e-4)?.with_max_evaluations(3_000)?.start(seed)?;
let optimum = loop {
let mut evaluations = Vec::new();
for x in run.candidates() {
// x[0] and x[1] are whole numbers within their bounds
let (motor, nose) = (x[0] as usize, x[1] as usize);
let design = Design { nose, ballast_kg: x[2], fin_span_m: x[3] };
evaluations.push(match flyer.fly(&motors[motor], &design) {
Ok(flown) => {
let miss = flown.apogee_m - 3048.0;
Evaluation::constrained(miss * miss, &flown.limits()) // each limit g ≤ 0
}
Err(_) => Evaluation::failed(), // a design that can't fly ranks last
});
}
if let Some(optimum) = run.tell_constrained(&evaluations)? {
break optimum;
}
};
Design, Flyer::fly and limits are the example’s own: they build the rocket, fly it, and
write each limit as a number g that must not be above zero, as Limits on a
design describes. A draw for an integer variable outside its bounds is
clamped to the nearest end, not drawn again. Run it from a copy of the repository with:
cargo run --example motor_and_nose -p hpr
It takes about half a minute in a debug build. It prints:
A 2.6 in rocket of Madcow Rocketry's parts: choose the motor, the nose cone, the nose
ballast and the fin span for a 3,048 m apogee, with margins over the ascent of at
least 1.5 calibres (flight), at most 4 (static) and 6 (flight), and at least 30 m/s off a 3 m rail
Not yet validated: see the Accuracy page before trusting these numbers.
motor total impulse (N·s) designs flown nearest apogee within the limits
J450DM 1061.6 3 2648 m
J300LR 1212.8 6 none
J760 1267.3 25 3057 m
K400C 1307.3 307 3048 m
K940 1636.1 19 3150 m
Found: the K400C with the PNC26K-W nose (3:1 ogive, plastic), 0.35 kg of ballast, fins 8.9 cm in span
(stopped: Target, after 360 flights)
Flown again: apogee 3048.0 m, top speed Mach 1.23, rail exit 33.8 m/s, liftoff mass 2.60 kg;
margins over the ascent: flight 3.04 to 4.83 calibres, static at most 3.88
Flown again, tolerances 100 times tighter: apogee 3048.0 m, rail exit 33.8 m/s
Both flights within 0.1 m of 3,048 m and within every limit: yes
The table counts the designs the run flew with each motor; the total impulse is from the motor’s thrust curve. The last column is the apogee nearest 3,048 m among the designs that kept every limit. The run tried every motor. Its few J450DM designs that kept the limits fell well short. None of its J300LR designs kept them all. It settled on the K400C and stopped once it was within a centimeter of the target. Flown again, with the integrator’s tolerances as set and then 100 times tighter, the winner is within 0.1 m of 3,048 m, and keeps every limit over the whole ascent.
Read the table as what this one run saw, not as what each motor can do: a few designs, or a few dozen, say little about a motor. The answer is not unique. A scan of every motor and nose over the ballast and the fin span, run once on the development machine and not kept, found that the J760, the K400C and the K940 can each hit 3,048 m within the limits, with any of the four noses. A hit with any of them scores the same, so another seed may well settle on another. To prefer one, say so in the goal. A small cost for liftoff mass is one way.
Unlike the first example’s counts, this run’s come out the same on all three operating systems:
CI checks the output on macOS, Linux and Windows, whose last bits of ln and exp differ. On
the development machine the run was also tried with every flight perturbed (the integrator’s
tolerance changed by up to 1%) and with the step size moved by a few bits each generation, and
neither changed a line of the output; those trials are not kept. Your own runs repeat bit for bit
on one machine from the same seed, but on another machine a run can take another path.
A goal that changes only when the choice changes, such as “the smallest motor that can do it”, gives the search nothing to follow between choices. For that question, run the optimizer once for each motor, the motor fixed, and compare. The continuous search then only has to find a design that keeps the limits.
The body tube’s length is fixed on purpose. Past Mach 1.2 a flight needs a table of the body’s
supersonic pressures, which takes a fifth of a second or more to build. Designs with the same
outside shape can share one table, so with the length fixed the example builds at most three,
one per fiberglass nose cone (the next paragraph says why the plastic one has none). With the length a variable, every design would build its own, and the run
would take minutes. The example shows how to share the table, in its Flyer::fly.
The winning plastic nose cone gets no table at all. The catalog lists it 0.05 mm narrower than the tube (2.638 in against 2.640 in), and the method behind the table doesn’t yet take a step in the body’s outline larger than a millionth of its area (issue #87). So for the short stretch of its flight past Mach 1.2 (its top speed is Mach 1.23) hpr-sim falls back on slender-body theory for the body’s normal force, its lift at an angle (Bodies faster than sound). How much that moves the apogee hasn’t been measured; the flight spends only a moment above Mach 1.2.
Checked against
Three test functions of Hamano and co-authors (§5.1 of their paper), each with half its variables
continuous and half whole numbers
(benchmark::mixed):
| Function | Whole-number variables | Minimum |
|---|---|---|
SphereInt, Σ xᵢ² | −10 to 10 | 0 at x = 0 |
| EllipsoidInt, the ellipsoid above | −10 to 10, with the largest coefficients | 0 at x = 0 |
SphereOneMax, Σ xᵢ² + the number of zeros | 0 or 1 | 0 at continuous 0, every whole number 1 |
Each runs with 10 and with 20 variables, from 20 seeds, to a value of 10⁻¹⁰. The starts and
settings are those of
validation/oracles/cmawm/cmawm_runs.py,
which runs the same functions with cmaes 0.13.1, an outside implementation (MIT) adapted from
the method’s authors’ code, with its active weights off as hpr-sim’s are. Every run of every case
reaches the minimum, with each whole number exactly right. The median evaluations are within the
test’s 25% of the outside implementation’s, and measured within 5%:
| Function | Variables | hpr-sim | cmaes 0.13.1 |
|---|---|---|---|
| SphereInt | 10 | 1,855 | 1,850 |
| SphereInt | 20 | 3,870 | 3,798 |
| EllipsoidInt | 10 | 3,460 | 3,625 |
| EllipsoidInt | 20 | 9,246 | 9,372 |
| SphereOneMax | 10 | 1,925 | 1,955 |
| SphereOneMax | 20 | 3,828 | 3,726 |
A unit test checks the margin itself after every generation of a mixed run, with each kind of
whole-number variable (integer_draws_keep_the_margin in
optimize/cmaes.rs).
Another sets up a run’s state by hand and checks one correction of each kind against the paper’s
equations worked out to 40 digits (margin_correction_matches_the_equations).
With the margin taken out, three of the six cases fail: some runs stall with a whole number
stuck on a wrong value. That was a one-off check, recorded in
ADR-140,
not run in CI.
Trade-offs: a Pareto front
Often two goals pull against each other. Bigger fins or more nose weight make a rocket more stable, and both cost apogee. There is no single best design then. Instead there is a set of designs, none of which can improve one goal without giving up some of the other: the Pareto front. A design dominates another when it is no worse in either goal and better in at least one; the front is the designs nothing dominates. Knowing the front shows what each extra calibre of stability costs, before you pick one design from it.
CMA-ES finds one design. For a front, hpr-sim has NSGA-II, a genetic algorithm by K. Deb and co-authors (2002). It keeps a population of designs and, each generation:
- breeds as many children as there are parents. Each pair of parents is chosen by two tournaments: of two designs picked at random, the one on a better front wins, or, on the same front, the one further from its neighbours (its crowding distance: the gaps between its two neighbours’ values of each goal, each as a share of that goal’s range in the front, added up). Every design plays two tournaments a generation. A pair’s children mix their parents’ values (crossover), then a few values are nudged at random (mutation);
- flies the children;
- sorts parents and children together into fronts: the designs nothing dominates, then those only the first front dominates, and so on;
- keeps the best half, front by front. The front that doesn’t fit whole keeps its designs with the most room around them, so the front stays spread out instead of bunching up.
Each variable needs a low and a high bound: the first generation is drawn evenly between them,
and the variable’s start and step are not used. The defaults are the paper’s: 100 designs, 250
generations, crossover of 90% of pairs and mutation of one variable in n, the number of
variables, on average (Nsga2). NSGA-II makes every goal as small as it can; to maximize one,
give its negative. It runs the generations it is given and has no test of when the front has
settled: run it again from a second seed, or for more generations, and compare the fronts.
Limits work as they do for CMA-ES: Goals::constrained takes each limit as a number g ≤ 0,
and a design that keeps every limit beats one that doesn’t. A design that can’t be flown is
Goals::failed, behind every other.
The example
The example program
crates/hpr/examples/pareto_front.rs
takes the 66 mm rocket on a J760 from An example, with its body fixed at 1.0 m. It
varies two things, the nose ballast (0 to 0.8 kg) and the fins’ span (4 to 10 cm), for two goals:
the highest apogee, and the largest static margin at launch mass, at Mach 0.3. Every design must
keep at least 1.5 calibres. The heart of it, abridged:
let variables = vec![
// A start and step are required by `Variable`; NSGA-II uses only the bounds.
Variable::new("nose ballast (kg)", 0.3, 0.15)?.within(0.0, 0.8)?,
Variable::new("fin span (m)", 0.06, 0.015)?.within(0.04, 0.10)?,
];
let optimizer = Nsga2::new(variables, 2)?.with_population(20)?.with_generations(25)?;
let front = optimizer.minimize_constrained(seed, |x| match flyer.design(x[0], x[1]) {
// Both goals as large as they can be: their negatives as small.
Ok((apogee, margin)) => Goals::constrained(vec![-apogee, -margin], &[1.5 - margin]),
Err(_) => Goals::failed(), // a design that can't fly ranks last
})?;
for member in &front.members {
// member.point: ballast and span; member.objectives: −apogee and −margin
}
flyer.design is the example’s own: it builds the rocket, works out its margin and flies it. Run
it from a copy of the repository with:
cargo run --example pareto_front -p hpr
It takes about a minute in a debug build. It prints:
A 66 mm rocket on a J760: how much apogee each calibre of static margin costs, over
its nose ballast (0 to 0.8 kg) and fin span (4 to 10 cm), with a margin of at least
1.5 calibres at launch mass, at Mach 0.3
Not yet validated: see the Accuracy page before trusting these numbers.
NSGA-II: 20 designs a generation, 25 generations, 500 flights (seed 2026)
The front: 20 designs, each flown again to the same apogee and margin
margin (cal) apogee on the front (m), interpolated between the two designs either side
2.0 3100
2.5 3050
3.0 2990
3.5 2940
At 2.5 calibres, CMA-ES alone (200 flights): apogee 3050 m; the front within 0.5%: yes
Between 2 and 3.5 calibres, each extra half calibre of margin costs this rocket 50 to 60 m of apogee. Each apogee in the table is interpolated in a straight line between the two front designs whose margins bracket it. Two checks back the front up:
- Every one of its 20 designs is flown again from a fresh build and gives the same apogee and margin to the last bit. That shows the result repeats; it doesn’t show it is right.
- At 2.5 calibres, CMA-ES alone, told to find the highest apogee with at least that margin, gets the same 3,050 m (to the nearest 10 m) in 200 flights: the front is within 0.5% of it, the example’s check.
How much that depends on the seed was measured once, on the development machine, and isn’t checked in CI. From each of the 26 seeds 2020 to 2045, the front came within the 0.5%, from 0.46% below CMA-ES’s apogee to 0.18% above (CMA-ES’s own answer in 200 flights varies by 0.24% over those seeds). An earlier version of the example missed the check: with 20 designs for 12 generations its front fell 0.66% short, and the generations were raised to 25. (Before that, 24 designs over wider ranges spread the front from 1.5 to 5.3 calibres, too thinly, and the ranges were narrowed.) The 0.5% was not changed (ADR-141).
Checked against
Three test problems of E. Zitzler, K. Deb and L. Thiele (2000), with 30 variables from 0 to 1
each, whose fronts are known exactly
(benchmark::zdt):
| Problem | The front | What it tests |
|---|---|---|
| ZDT1 | f₂ = 1 − √f₁, f₁ from 0 to 1 | a convex front |
| ZDT2 | f₂ = 1 − f₁² | a concave front |
| ZDT3 | f₂ = 1 − √f₁ − f₁ sin(10π f₁), in five separate pieces | a broken front |
Each is run from 20 seeds with the paper’s settings: 100 designs for 250 generations, 25,000 evaluations. Two numbers measure a run’s front against the true one:
- the generational distance (GD): the mean, over the front’s designs, of each one’s distance to the true front. Small when the front lies on the true one.
- the inverted generational distance (IGD): the mean, over points spread along the true
front, of each one’s distance to the nearest design. Small only if the front also covers all of
the true one, evenly. The points are at 1,000 evenly spaced
f₁; on ZDT3, the 265 of them that fall on its pieces.
hpr-sim measures GD to the true front’s curve itself, not to points along it. The same problems
are run with pymoo 0.6.2, an outside implementation (Apache-2.0) by J. Blank and K. Deb, set up
as the paper describes, from seeds 1 to 20
(pymoo_runs.py).
Both sets of columns below are scored by hpr-sim’s measures; the pymoo columns are pymoo’s
fronts. The rules were set from pymoo’s runs before hpr-sim’s were measured: every hpr-sim run
within twice pymoo’s worst, and hpr-sim’s median at most 25% above pymoo’s, the margin CMA-ES’s
tests allow against pycma. Review of the first results added a second rule: the median at most
20% below pymoo’s, a factor of 1.25 either way. On these problems every variable but the first
is best at its lower bound, and an optimizer that drifts toward its bounds would score better
than pymoo without being better. Measured:
| Problem | Measure | hpr-sim median | hpr-sim worst | pymoo median | pymoo worst | Bound on every run |
|---|---|---|---|---|---|---|
| ZDT1 | GD | 1.07e-3 | 1.36e-3 | 1.10e-3 | 1.46e-3 | 2.92e-3 |
| ZDT1 | IGD | 4.90e-3 | 5.66e-3 | 4.96e-3 | 5.48e-3 | 1.10e-2 |
| ZDT2 | GD | 9.78e-4 | 1.40e-3 | 9.85e-4 | 1.41e-3 | 2.82e-3 |
| ZDT2 | IGD | 5.01e-3 | 5.44e-3 | 5.08e-3 | 5.35e-3 | 1.07e-2 |
| ZDT3 | GD | 4.13e-4 | 7.02e-4 | 4.50e-4 | 6.53e-4 | 1.31e-3 |
| ZDT3 | IGD | 5.38e-3 | 3.40e-2 | 5.51e-3 | 3.39e-2 | 6.78e-2 |
At the median, hpr-sim’s fronts lie 1% to 8% closer to the true front than pymoo’s, and cover it
as evenly (IGD 1.3% to 2.3% smaller). Each problem’s worst hpr-sim run is 1.9 to 2.1 times inside
its bound. The
worst ZDT3 run of each has an IGD six times its median: it missed part of the front, a known
hazard on a front in pieces. The per-run bound on ZDT3’s IGD is loose for the same reason, set by
pymoo’s one such run; the median rule is what holds ZDT3’s coverage. These come from
cargo test -p hpr-analysis --test nsga2 -- --nocapture
(tests/nsga2.rs).
The same test checks hpr-sim’s generational_distance against pymoo’s own, on pymoo’s fronts
and 500 reference points, to 10⁻¹².
Deb and co-authors’ own runs (their Table II) report a mean distance of 0.033, 0.072 and 0.115 on these three problems, measured to 500 points of the true front: 18 to 87 times pymoo’s mean here, to the same 500 points (0.0013 to 0.0019). Why the paper’s are so much larger was not investigated.
Unit tests check the pieces by hand
(optimize/nsga2.rs):
- fronts and crowding distances of small sets worked out on paper;
- that crossover spreads children by the distribution its authors give, far from a bound and cut at one, and mutation steps likewise, cut at each side’s bound, each within five standard errors over 100,000 draws; and that crossover’s two children come out in either order;
- that children never leave a variable’s bounds, nor pile up on them;
- that a failed design, or one breaking a limit, ranks behind every design that keeps them.
ZDT3’s five pieces were solved to 40 digits and are checked against the equations that define
their ends (benchmark.rs).
They agree with pymoo’s to its ten digits, except where pymoo prints the second piece’s start as
0.182228780, a digit short of 0.1822287280.
Few evaluations: EGO
CMA-ES and NSGA-II spend hundreds of flights. That is fine when a flight takes a tenth of a second, but not when one evaluation is a long Monte Carlo run, or a slow outside program. EGO (efficient global optimization; D. R. Jones, M. Schonlau and W. J. Welch, 1998), a form of Bayesian optimization, is built for that case. It spends its effort thinking between evaluations, so it needs only tens of them.
It works like this:
- It evaluates an initial design: 10 points per variable, spread evenly over the box the variables’ bounds make. The design is a Latin hypercube: each variable’s range is cut into as many equal slices as there are points, and each slice gets one point.
- It fits a surrogate: a smooth guess at the model, drawn through every point evaluated so
far. The surrogate is kriging (a Gaussian process). Besides its guess
ŷat any point, it gives a standard errors: zero at the points already evaluated, and growing away from them. - It asks where an evaluation is most worth making. The expected improvement at a point is the average amount by which an evaluation there would beat the best value so far, outcomes that don’t beat it counting as zero. It is large where the guess is low, and where the guess is unsure.
- It evaluates the point of largest expected improvement, refits, and repeats.
Each variable needs a low and a high bound, as for NSGA-II; its start and step are not used.
By default a run stops after 20 evaluations per variable. Ego::with_max_evaluations sets
the budget, Ego::with_target stops at a good enough value, and
Ego::with_tolerance_improvement stops once no point is expected to gain more than a set
amount, in the model’s units (with a transform, below, in the transformed units); it is off by
default. Jones and co-authors stop at 1% of the best value’s size. That can end a run well short of 1% from the minimum: on Hartmann’s
three-variable function, 8 of 20 runs (seeds 1 to 20) stopped more than 1% away, the worst
4.7% (a reviewer’s measurement). Use a smaller amount (the paper suggests 0.1%) where it
matters. It never fires for a goal near zero (a miss): there, give an amount. A model that fails at a design returns +∞;
EGO fits it as the worst value so far and goes on.
A worked example, on Branin’s function of two variables: its least value is
5/(4π) ≈ 0.397887, at three points. The initial design is 20 points, and 30 more follow. The
1 is the seed; a variable’s start and step are required by
Variable::new but EGO doesn’t use them. This is the example on Ego’s API page, which
runs as a test from seed 1; tests/ego.rs runs seeds 1 to 20:
use hpr_analysis::optimize::Variable;
use hpr_analysis::optimize::benchmark::global::{BRANIN_MINIMUM, branin};
use hpr_analysis::optimize::ego::Ego;
let variables = vec![
Variable::new("x0", 2.5, 3.0)?.within(-5.0, 10.0)?,
Variable::new("x1", 7.5, 3.0)?.within(0.0, 15.0)?,
];
let optimum = Ego::new(variables)?.with_max_evaluations(50)?.minimize(1, branin)?;
assert_eq!(optimum.evaluations, 50);
assert!(optimum.value < 1.01 * BRANIN_MINIMUM);
Transforming the values
The surrogate assumes the model varies about as much everywhere. Some models don’t: Hartmann’s six-variable function is nearly flat at values near zero over most of its box, then drops into narrow wells, the deepest at −3.32237. Fitted to the raw values, the surrogate misjudges how deep an unexplored well could be, and EGO settles in the second-deepest, near −3.20.
Jones and co-authors meet this by fitting the surrogate to a transformation of the values,
chosen by checking how well the fit predicts points left out of it. They “typically try the log
transformation, ln(y), or the inverse transformation, −1/y” (§3, p. 468). On their test
functions this chose ln y for Goldstein and Price’s function and −ln(−y) for Hartmann’s
six-variable function (§4.2, p. 474). Ego::with_transform offers the two log transformations:
Transform::Log,z = ln y, for a model whose values are all positive;Transform::NegativeLog,z = −ln(−y), for one whose values are all negative.
Both rise with y, so the least z is at the least y. The surrogate, its guess and the
expected improvement are all on the z scale, as in the paper; the best point, a target and the
result stay in the model’s units. −ln(−y) turns −3.32 into −1.20 and −0.01 into 4.6: it pulls
the deep wells close together and pushes the near-zero plateau far out, so the wells no longer
look like narrow spikes against a flat floor. Jones and co-authors chose it after checking the fit (§4.2)
and give no deeper reason; neither does hpr-sim. A value outside the
transform’s range (zero or positive under −ln(−y)) is an error, not something to fit. hpr-sim
doesn’t choose a transform for you: it runs none of the paper’s checks of the fit.
A worked example: Hartmann’s six-variable function from seed 10, in 100 evaluations, the initial
design’s 60 included. Its second-lowest minimum is −3.20316, a well 3.6% above the deepest one.
With the transform, the run’s best value is −3.30740 on macOS, 0.45% above the minimum, so it
found the deepest well. Without it, the same seed ends at −3.20245, inside the shallower well.
How close the run gets inside the deepest well depends on the computer: the same run ended
1.01% above the minimum on Linux and 1.47% on Windows, as a seeded run magnifies the last-bit
differences between their maths libraries (most likely; Rust’s arithmetic is the same on all
three). So the test in tests/ego.rs checks only which well the run ends in, a value below
−3.2032, and accepts any run up to 3.59% above the minimum:
use hpr_analysis::optimize::benchmark::global::hartmann6;
use hpr_analysis::optimize::ego::{Ego, Transform};
// `variables`: six, each within [0, 1].
let optimum = Ego::new(variables)?
.with_max_evaluations(100)?
.with_transform(Transform::NegativeLog)
.minimize(10, hartmann6)?;
assert!(optimum.value < -3.2032); // below the second-lowest minimum
Checked against
tests/ego.rs
runs EGO from seeds 1 to 20 on two test functions: Branin’s, with three minima of one value,
and Hartmann’s, with local minima above its least. The rule is Jones and co-authors’ measure of
success: the best value within 1% of the minimum’s size. The budget was set from a probe on
seeds 1 to 10 at 40 evaluations, where Branin’s worst run missed by 1.03%; at 50 the 20 seeds
gave the gaps below (macOS debug build; print them with
cargo test -p hpr-analysis --test ego -- --nocapture).
| Function | Variables | Minimum | Budget | Worst of 20 runs |
|---|---|---|---|---|
| Branin | 2 | 0.397887 | 50 evaluations | 0.118% above |
| Hartmann 3 | 3 | −3.86278 | 50 evaluations | 0.124% above |
Hartmann 6, −ln(−y) | 6 | −3.32237 | 250 evaluations | 0.289% above |
Branin’s minimum is exact. Hartmann’s is the value printed to six figures, and the test checks
it by polishing with CMA-ES from the printed point. Unit tests in
optimize/ego.rs
check that the surrogate passes through its points, its fit, guess and standard error on three
points against the formulas worked by hand, the expected improvement’s limits, and that the
initial design fills every slice. Unlike CMA-ES and NSGA-II, EGO isn’t compared with an outside
implementation: the minima are known exactly, which is what the milestone asks for.
Hartmann’s six-variable function is held to the same rule with the −ln(−y) transform, from
seeds 1 to 20, in a program of its own,
benches/ego_hartmann6.rs
(cargo bench -p hpr-analysis --bench ego_hartmann6). Every run ended within 1%; the last to
get there did so at evaluation 205, so the budget of 250 was set from these same runs, with 45
to spare. Jones and co-authors needed 121 evaluations from one run, with an initial design of
65 points and an exact search for the largest expected improvement. Without the transform, 14
of the same 20 runs ended at the local minimum near −3.20 after 250 evaluations, 3.6% to 3.7%
above the minimum; the 6 that found the deepest well got within 1% by evaluation 78 in 5 of them
and at 241 in the sixth, so the transform is the more reliable route, not always the faster one.
The budget of 250 was set from these same 20 seeds, so treat the 20 of 20 as in-sample.
The 20 runs take about 3 minutes spread over 10 cores in a release build. CI’s tests use a debug
build, about 20 times slower, where they would take hours. So the local gate runs the program
whenever EGO’s code changes (scripts/gate.sh ego), and CI runs only the one-seed example
above, with and without the transform. The program’s numbers are from macOS; the debug and
release builds gave the same runs bit for bit there. Nobody has run the 20 on Linux or
Windows: a run there may end further from the minimum, or even in the shallower well, 3.6% away.
The one-seed example’s spread above (0.45% to 1.47%) shows that runs move; it doesn’t bound how
far.
EGO takes no limits, whole-number variables or noisy outputs yet
(ADR-142;
the transform: ADR-152).
Left out
- A choice is a whole number in a list you order. There is no separate handling for choices with no order, beyond putting alike ones next to each other.
- Limits are inequalities only. For an equality
h = 0, write|h| − ε ≤ 0with a smallε. - NSGA-II takes only continuous variables with two bounds: no choices from a list yet. It runs the generations it is given, with no test of when its front has settled.
- EGO is checked on two, three and six variables only. It doesn’t choose a transform of the
values for you (the paper checks the fit to choose one), and offers no
−1/y. It takes no limits, whole-number variables or noisy outputs. - No optimizing of a Monte Carlo run’s statistics, such as the chance of landing within a distance.
- No command-line or Python front end yet.
The API reference is
hpr_analysis::optimize.
The command line
hpr is hpr-sim’s command-line tool. This page is for anyone who wants to use it from a terminal
or a script. It says what each command does, shows its output, and lists the exit codes.
Today hpr does eight things:
- It flies a design, read from an OpenRocket file or an hpr design file, and prints how the flight went.
- It flies a design many times, its uncertain inputs scattered at random (Monte Carlo), and prints how far its apogee and landing spread.
- It looks up motors, from the catalog built into it or from a motor file of your own, and searches vendors’ stock and prices from motor.fusionspace.co.
- It converts motor files between the two common formats, and designs between OpenRocket’s
.orkand hpr’s own format (.hpr, and.hprzwith other files beside the design). - It re-runs hpr-sim’s validation against RocketPy and checks the results against the published ones.
- It reads a flight log from an altimeter and prints what it says about the flight, with no design file and no simulation.
- It fetches a launch day’s weather, or reads a weather file, and writes the air and wind over the site as a profile.
- It writes shell completion scripts.
Its other commands are registered but not available yet: each refuses and names the milestone, the step of the roadmap, that brings it (the table below).
How far to trust it.
hpr simprints what the library computes: a test flies the rocket of the example below throughhpr simand through the Rust library, and gets identical numbers, to the last bit. How close those are to a real flight is the Accuracy page’s subject.hpr simflies a.orkfile’s parachutes and streamers as OpenRocket flies them, and lands within 0.02% of OpenRocket’s landing speed on 50 of its 53 example flights, and within 0.12% on all 53 (Recovery). It flies a separation under power, or several in turn as on a three-stage rocket, but a dropped stage’s own flight is not validated. With no device open within 1 s of apogee, where and how fast its rocket comes down are not predictions (what it leaves out).hpr motors showworks out each figure from the motor’s thrust curve with the same code a flight uses. On all 32 bundled curves, that code matches ThrustCurve.org’s own statistics code to 1.8e-15, relative (Solid motors).hpr motors listgives the same figures for the bundled motors, worked out by the same code from the same ThrustCurve.org files.hpr convertkeeps the thrust curve, the size and the masses. On all 32 bundled motor files, converting to the other format and back gives each of them again as hpr reads the file, bit for bit, except two masses written with 17 digits, each flagged by a warning (test). OpenRocket 24.12 opens all 32 converted files and reads 29 as it reads the originals; the other three differ only in the motor’s type or a delay (other programs). Whether RockSim opens them is not checked.hprdoesn’t re-run the validation cases. To run the check the project’s automated tests make on every change, clone the repository and runcargo xtask validate --check(what it checks).hpr weatherruns the library’s readers and writes the profiles they build, to the last bit (how far to trust it). Its online fetch is not tested automatically.hpr motors searchgives back motor.fusionspace.co’s values unchanged (how far to trust it). Its online fetch is not tested automatically.- The tests in
crates/hpr-cli/tests/run every command as a user would, and check each--jsondocument against its published schema.
Running it
There is no ready-built download yet. hpr needs Rust and a copy of the repository, set up as
Getting started shows. From that copy, run it through
Cargo:
cargo run -p hpr-cli -- motors list
Or install it once, so that hpr works from any directory:
cargo install --path crates/hpr-cli --locked
hpr --help
hpr --version prints its version and its designation, FS · SW · TOOL 005: hpr-sim’s number
among FusionSpace’s tools, the family of programs whose design rules it follows
(ADR-164).
Colors and messages
The result, with its warning:, note: and help: lines, goes to standard output. A refusal
goes to standard error, or to standard output as a JSON document with --json. Each message
line starts with a word that says what it is, so it reads the same with or without color:
| prefix | meaning | color |
|---|---|---|
error: | what went wrong; the command stopped | bold red |
warning: | something to check; the command went on | bold yellow |
note: | something worth knowing about the result | bold |
help: | what to do next, such as the option to add; in a refusal, the last lines | bold blue |
The colors are your terminal’s own red, yellow and blue, so its theme decides how they look.
hpr decides for standard output and standard error separately, in this order:
--color alwaysor--color never, if given.NO_COLORset to anything but an empty value: no color.FORCE_COLORset to anything but an empty value, orCLICOLOR_FORCEset to anything but an empty value or0: color, even into a file or a pipe.- Otherwise (
--color auto, the default) a stream gets color only when it is a terminal andTERMisn’tdumb.
So hpr sim rocket.ork > flight.txt writes plain text to the file while an error on the
terminal stays red. --json output never has color, whatever the flag or the variables say. To
turn color off everywhere, set NO_COLOR=1; for one run, give --color never, which every
command takes.
The commands
This table is written from the tool’s own list of commands, so it names only what hpr has, and
only the files each command really reads. “Not yet” commands exit with
status 3.
| command | what it does | reads | prints | status |
|---|---|---|---|---|
hpr sim | Fly a .ork, an .hpr or .hprz design, or a rocket’s .json from a rail and print its flight; export its recording | .ork, .hpr or .hprz, a rocket’s .json, a motor from the bundled catalog, .eng or .rse, or fetched from ThrustCurve.org | text, JSON, a recording as .csv, .json, .parquet, .geojson or .kml | available (how to use it) |
hpr convert | Convert a motor file between .eng and .rse, or a catalog motor to either; or a design between .ork, .hpr and .hprz | .eng, .rse, the bundled catalog, a design as .ork, .hpr or .hprz | .eng or .rse, .ork, .hpr or .hprz, text, JSON | available (how to use it) |
hpr motors | Look up motors in the bundled catalog, read a .eng or .rse motor file, fetch a curve from ThrustCurve.org, or search vendors’ stock and prices | .eng, .rse, the bundled catalog, motor.fusionspace.co’s stock and prices, fetched or saved, ThrustCurve.org’s curves, fetched and cached | text, JSON | available (how to use it) |
hpr weather | Fetch a launch day’s weather, or read a weather file, as a profile of air and wind | Open-Meteo, a University of Wyoming sounding, GFS or RAP, fetched or saved, a whole GFS file, an ERA5 .nc | text, JSON, a profile as .json | available (how to use it) |
hpr mc | Fly a design many times, each flight’s inputs scattered at random, and print how its apogee and landing spread; export every flight | what hpr sim reads: a design and its motor | text, JSON, every flight’s draw and outcome as .csv | available (how to use it) |
hpr optimize | Search a design’s parameters for a goal | - | - | not yet: M6.3 |
hpr compare | Compare a flight log with its simulation | - | - | not yet: M7.3 |
hpr analyze | Read a flight log and print its readings, with no design file | a PerfectFlite .pf2 flight log | text, JSON | available (how to use it) |
hpr diagnose | Diagnose what went wrong in a flight from its log | - | - | not yet: M7.4 |
hpr completions | Print a shell completion script for hpr | - | a bash, elvish, fish, powershell or zsh script, JSON | available (how to use it) |
hpr sim
hpr sim flies a design from a launch rail to the ground, and prints what happened: its
stability margin as it leaves the rail, its
apogee, its speed off the rail, how its
ejection delay suits the climb, how fast it comes down, its
events, its top speed, and where it came down. It
reads an OpenRocket .ork file, a design in
the hpr design format (.hpr, or a .hprz with its attachments), or a rocket’s
JSON (.json, the tree Your own rocket describes). A .hpr or .hprz
flies exactly as the .ork it was converted from (converting a design),
though it doesn’t print the .ork reader’s warnings, which hpr convert printed.
It runs the same simulation code as the
Rust library, so a Rust program flying the same design gets the same numbers.
Flying a design
This flies one of the repository’s own test rockets, a small single-stage OpenRocket design. Its
file names an AeroTech H128W, which isn’t among the 32 motors built into hpr, so --motor H54
puts the catalog’s Cesaroni H54 in its motor mount instead:
$ hpr sim validation/fixtures/ork/pod-flights/pods-none.ork --motor H54
pods-none (pods-none.ork)
configuration 1 of 1: [H128W-0] with --motor H54
static margin 2.69 calibres off the rail; least 2.69 calibres, at 0.15 s, before apogee
apogee 846.1 m above the site at 11.21 s
rail exit speed 21.3 m/s
delay apogee 7.71 s after burnout
descent no recovery device opened, so the fall is not a prediction
motor: 1 × 168H54-10A (from the bundled catalog) in `Motor mount`, lit at launch
launched at 0° N, 0° E, 0 m above sea level, from a 1.5 m vertical rail, in calm air
help: see the Accuracy page before trusting these numbers: https://nrdptel.github.io/hpr-sim/accuracy.html
note: the file has no recovery device, so the rocket falls from apogee on its airframe alone, on aerodynamics that hold only at small angles of attack: its landing time, speed and place, and any peak it sets in the fall, are not a prediction
warning: drag: issue #18: skin friction is taken as fully turbulent, but on a smooth surface the flow stays laminar near the nose, where friction is lower: hpr's reads high by 3.6% on RocketPy's Calisto at Mach 0.3; if this surface is that smooth, the drag reads high, so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are (https://github.com/nrdptel/hpr-sim/issues/18)
warning: stability: issue #172: the static margin read up to 0.1108 calibres higher than OpenRocket's on four private designs, for a reason not yet found, so this flight's margin may read high by as much (https://github.com/nrdptel/hpr-sim/issues/172)
event time height speed
liftoff 0.00 s 0.3 m 0.0 m/s
rail exit 0.15 s 1.8 m 21.3 m/s
burnout 3.50 s 466.0 m 136.3 m/s
apogee 11.21 s 846.1 m 0.1 m/s
ground hit 29.71 s 0.0 m 65.9 m/s
(heights are the center of gravity's above the site; speeds are over the ground)
top speed 178.3 m/s at 2.31 s
top Mach number 0.525
landing 17.4 m from the pad at 29.71 s, at 65.9 m/s: with no recovery device opened soon after apogee, not a prediction
What each part says:
| lines | what they say |
|---|---|
| the first two | the rocket’s name and its file; the configuration flown, by its number in the file and its name (below). [H128W-0] is the file’s unnamed configuration, called by the motor it holds, and with --motor H54 says the H54 flew in its place. The file’s other configurations follow, each with why it doesn’t fly as read, if it doesn’t |
static margin | the static margin as the rocket leaves the rail, in calibres, at Mach 0, and its least value from there to apogee. Each is the weakest direction’s: a rocket with a fin set of one or two fins has a different margin for each direction the air crosses it, and hpr prints the least; the JSON’s roll_rad gives that direction (Flight metrics) |
apogee | the apogee: the height of the center of gravity above the site, and when |
rail exit speed | the speed at rail exit |
delay | the coast: the time from burnout to apogee. For one motor lit at launch, a delay no longer than that fires at or before apogee. Then the delay the design sets for its motor, if it sets one, and, for one motor lit at launch, how many seconds before or after apogee its charge fires. --motor sets no delay without --delay, so the example shows the coast alone; OpenRocket’s example A simple model rocket, with its C6-5, prints apogee 5.49 s after burnout; the motor's set delay: 5 s; its charge fires 0.49 s before apogee |
descent | the vertical speed under each set of open parachutes or streamers, when the next one opens and at landing, without the wind’s drift. It rests on the drag area the file gives each device (the landing, where an example shows it). With none open, it says so and gives no speed |
motor: ... | the motor, how many of it fly (a cluster’s tubes and a pod set’s pods each carry one), and the motor mount it sits in, by name |
recovery: ... | each parachute or streamer the file flies: its name, its event and delay, its drag area, and when it opened, such as recovery: `Drogue parachute` at apogee, 0.133 m² of drag area, opened at 9.12 s (recovery in the JSON, below); the example’s file has none |
launched at ... | the site, the rail and the wind, below |
note: | what the flight leaves out of the design, and which numbers that spoils |
warning: | what hpr’s .ork or motor-file reader accepted with a caveat, such as a part it left out, and what the design’s checks found unusual but buildable, each in a sentence naming the parts; the same words about several parts of one name are printed once, with a count such as (×2) |
warning: unstable: | the rocket is unstable while a motor burns, before apogee or the first deployment, and the apogee figure says it is not a prediction (Flight metrics: unstable under power). Either the static margin falls below zero, or, where hpr can give no margin, the pitching moment’s slope C_mα is above zero; a flight prints one of the two. A help: line links that page. In the JSON, flags lists the first as unstable_under_power, its peak the least margin under power in calibres, which the summary also has as min_powered_static_margin_cal; and the second as unstable_without_margin, its peak the largest C_mα per radian where the margin is undefined under power, which the summary also has as max_powered_moment_slope_per_rad |
warning: envelope: | a flag of the operating envelope: the flight went faster than any public flight hpr has been compared with a reference on (currently Mach 1.15; it rises as references are added), past the core band (Mach 2.5) or the envelope (Mach 3.5), or flew above 15° angle of attack while the air was strong enough to matter. Each says when, and a help: line links the page. A flag changes no number. In the JSON, flags lists them, each with its flag, the peak that raised it (its value, a Mach number or an angle in radians, with its time_s and height, as every peak has) and its message |
warning: drag:, warning: stability: | a known error in hpr’s drag (#18, #67, #68, #70, #72, #73 or #222) or stability margin (#64, #87, #120, #121, #172, #325 or #326) that the flight meets, by its issue number (the list, with each one’s condition). Each says which way its numbers lean. A flight on a drag table of your own prints no drag warning but keeps the stability ones; a normal-force table of its own prints none. In the JSON, issues lists them, each with its issue number, its kind (drag or stability), url, the flight’s max_mach peak, the parts that meet it (their ids; none for #18, #68, #121 or #172) and its message |
| the events | each event’s time, the height of the center of gravity above the launch site, and the speed over the ground; the height at liftoff isn’t zero, as the rocket stands on the rail |
| the last figures | the top speed and Mach number, and the landing |
The text names configurations and parts by their names, in backticks, not by the long ids an
OpenRocket file gives them; a part with no name is called by its id. --json prints the same
flight as data, with the ids beside the names: each configuration’s id, name and label, the
label being what the text calls it, and each motor’s mount and mount_name.
A missing motor is fetched, then kept: it flies offline from the cache. A .ork file names its motor but rarely
carries its thrust curve, and hpr’s catalog holds 32 motors. When the
file embeds no curve and the catalog lacks the motor, hpr sim finds it on
ThrustCurve.org by the file’s manufacturer and designation, keeps
it in hpr’s cache, and flies it; a note names the curve file, who measured it and its license
(Motors from ThrustCurve.org). For the motors of OpenRocket’s own
example designs, hpr takes the ThrustCurve file that holds OpenRocket’s curve; for any other motor
the curve may not be the one OpenRocket flies, and no such flight has been compared with
OpenRocket’s own curve yet. With --offline, or no network,
and no copy in the cache, hpr sim refuses and names the command that fetches it (abridged):
$ hpr sim validation/fixtures/ork/pod-flights/pods-none.ork --offline
error: configuration [H128W-0] can't be flown as the file has it: no thrust curve for H128W: ...; AeroTech H128W is not in hpr's cache of ThrustCurve.org, and the run is offline
help: with a network connection, `hpr motors fetch --manufacturer AeroTech H128W` fetches it into the cache
help: give a motor with --motor
Run that command once with a network connection, or fly the design once while online, and it
flies offline after. Or give a motor file you have: hpr sim my-rocket.ork --motor AeroTech_H128W.eng.
The landing
A .ork file’s parachutes and streamers fly, as OpenRocket flies them. So do those of a
.hpr or .hprz converted from a .ork. hpr sim flies them through
hpr::ork::recovery, so its numbers are the library’s. Each opens
fully at once, the file’s delay after its event, and the rocket then comes down under the open
devices’ drag alone. That lands within 0.02% of OpenRocket’s landing speed on 50 of its 53
example flights, and within 0.12% on all 53
(Recovery). Those flights are in calm air: they
check how fast and how long the rocket comes down, not where wind drifts it. When the first device
opens within 1 s of apogee, the landing is no longer marked “not a prediction”, and neither is a
peak set once a device is open. A later first opening, such as a main alone at 150 m, keeps the
mark on the landing and on any peak in the fall before it, and a note says how long the rocket fell
first: where and how fast the device opens come from that fall, on the airframe alone, as below. The JSON’s recovery list gives each device’s name, body (the
part that carries it, below), opens_at (apogee, altitude, ejection,
launch or separation), height_above_ground_m, delay_s,
drag_area_m2 and opened_s. A device the file sets to never, or to an ejection charge a
plugged motor doesn’t fire, opens nothing, and a note says so. So does one set to its stage’s
ejection charge in a stage where no motor lights, as in OpenRocket: the note says “Main never
opens: it opens at its stage’s ejection charge, and no motor of its stage lights”, and the other
devices fly. A device hpr can’t fly as written, such as one inside a part hpr doesn’t read, is
refused: the rocket then flies with none, and a note says why.
This flies the repository’s dual-deploy test rocket, a drogue at apogee and a main at 150 m,
with the catalog’s H54 in place of its own motor; the descent line gives the speed under the
drogue as the main opens, then at landing:
$ hpr sim validation/fixtures/ork/loft-demo/demo-dual-deploy.ork --motor H54
Loft Demo 54mm — dual deploy (demo-dual-deploy.ork)
configuration 1 of 1: [K550W-P] with --motor H54
static margin 6.25 calibres off the rail; least 6.25 calibres, at 0.27 s, before apogee
apogee 324.0 m above the site at 9.12 s
rail exit speed 11.2 m/s
delay apogee 5.62 s after burnout
descent 14.1 m/s at 150.0 m under `Drogue parachute`; 4.7 m/s at landing with `Main parachute` open too
motor: 1 × 168H54-10A (from the bundled catalog) in `Booster / motor bay`, lit at launch
recovery: `Main parachute` at 150 m on the way down, 1.052 m² of drag area, opened at 22.41 s
recovery: `Drogue parachute` at apogee, 0.133 m² of drag area, opened at 9.12 s
launched at 0° N, 0° E, 0 m above sea level, from a 1.5 m vertical rail, in calm air
help: see the Accuracy page before trusting these numbers: https://nrdptel.github.io/hpr-sim/accuracy.html
note: the file's 2 recovery devices fly as OpenRocket flies them: each opens fully at its event, with the file's drag coefficient or OpenRocket's own, and once one opens the rocket descends as a point under the open devices' drag alone
warning: drag: issue #18: skin friction is taken as fully turbulent, but on a smooth surface the flow stays laminar near the nose, where friction is lower: hpr's reads high by 3.6% on RocketPy's Calisto at Mach 0.3; if this surface is that smooth, the drag reads high, so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are (https://github.com/nrdptel/hpr-sim/issues/18)
warning: stability: issue #172: the static margin read up to 0.1108 calibres higher than OpenRocket's on four private designs, for a reason not yet found, so this flight's margin may read high by as much (https://github.com/nrdptel/hpr-sim/issues/172)
event time height speed
liftoff 0.00 s 0.6 m 0.0 m/s
rail exit 0.27 s 2.1 m 11.2 m/s
burnout 3.50 s 163.1 m 59.6 m/s
apogee 9.12 s 324.0 m 0.0 m/s
charge 9.12 s 324.0 m 0.0 m/s
deployment 9.12 s 324.0 m 0.0 m/s
charge 22.41 s 150.0 m 14.1 m/s
deployment 22.41 s 150.0 m 14.1 m/s
ground hit 53.94 s 0.0 m 4.7 m/s
(heights are the center of gravity's above the site; speeds are over the ground)
top speed 65.9 m/s at 2.73 s
top Mach number 0.194
landing 0.4 m from the pad at 53.94 s, at 4.7 m/s
With no device open soon after apogee, the landing is not a prediction. The rocket then
falls from apogee on its airframe alone, as the example’s does. hpr’s aerodynamics hold only at small
angles of attack, and a falling airframe turns far past them, so
where it lands, and how fast, are artifacts of the model; one test design glides tail-first far
from the pad in calm air (issue #241). So is a
top speed or Mach number set in the fall: a rocket that falls faster than it climbed shows its
top speed after apogee, and hpr sim marks it “in the fall: not a prediction” (after_apogee in
the JSON). The ascent, up to apogee, is what to read.
Separation
A .ork configuration whose booster drops away under power flies, as
does one that drops several stages in turn, but a dropped stage’s own flight is rough. hpr sim reads the staging from the file,
as hpr::ork::separations does, and flies it with no
--motor: a motor of your own can’t be told when the stages separate, so with --motor it is
refused. Each parachute and streamer rides the part its stage is in
(hpr::ork::separated_recovery). On both
configurations of OpenRocket’s Two stage high power rocket, the
sustainer’s landing speed is +0.00% off OpenRocket’s, and its flight
time +0.32% and +0.73%. On the first, its main opens 1.09 s before OpenRocket’s, mostly as its
apogee is 1.79% lower and OpenRocket opens 8.9 m below the set height (Staging).
After the split, each part flies as a point, with only its devices’ drag. So the booster
tumbles from the split until its first own device opens, and a
sustainer with no device tumbles from its apogee. A real booster flies nose-first for a while,
so its peak and landing are likely too low and too close to the pad (issue
#179); a note says so, and a map export draws
no pin for it. The sustainer, which keeps the nose, is part 0; the booster is part 1. Each device
line names its part (on part 1), and the booster’s landing is printed as landing, part 1,
marked rough. A tumble hpr adds counts as no recovery device for the landing’s caveat, so a
sustainer that only tumbles lands with “not a prediction”. In the JSON, a device’s body is
null for the sustainer and the part’s index otherwise, added is true for a tumble hpr added,
and a tumble opened at the split has opens_at separation. The
M4.5g1 milestone shipped this; the
decision record
says why each part needs a device.
Several separations fly too, one after another, from the tail forward. At each split the
stages behind it drop away, and the rest flies on as a new sustainer, which must still have a
motor to burn. The parts are numbered in the order they drop. On OpenRocket’s Three stage low
power rocket (one of the example designs OpenRocket ships), the booster is part 1 and
the middle stage part 2; the file names both of them Booster stage. On its first configuration,
an A8-5 above two B6-0s, the note reads: “the stack comes apart under power 2 times: at 0.857 s
part 1, Booster stage, drops away, at 1.714 s part 2, Booster stage, drops away; the
sustainer, part 0, flies on”. The output then prints landing, part 1 and landing, part 2, each
marked rough like a booster’s.
- Against OpenRocket, on its own thrust curves. All three configurations separate at OpenRocket’s times. Their apogees are within 1.48% and their largest speeds within 1.64% of OpenRocket’s (Staging).
- With the curves
hpr simfetches. The file holds no curves, sohpr simfetches them from ThrustCurve.org (Motors from ThrustCurve.org), taking the files that hold OpenRocket’s own curves. Its largest speeds are within 1.67% of OpenRocket’s; the first configuration reads 78.61 m/s against OpenRocket’s 78.37. Its apogees, with the file’s parachutes, are within 1.37% of OpenRocket’s own record, which opens them too, and within 2.48% of OpenRocket’s flight with nothing deployed. - Not validated: the dropped middle stage’s own flight, like the booster’s (issue #179).
The M4.5g2 milestone shipped this; its decision record (several powered separations) has the numbers.
Boosters strapped beside the core fly too, on a rocket of one stage on the axis: a
parallel stage, which burns with the core and drops at its own
separation. On OpenRocket’s Parallel booster staging, the boosters’ E12s light with the core’s
I115W at launch, and the note reads “the stack comes apart under power at 2.440 s: part 1,
Booster Set, drops away”. Boosters with no motor that lights stay on to the landing, as
OpenRocket flies them. Both configurations’ apogees are within 1.23% of OpenRocket’s
(Staging: Boosters beside the core); the dropped
boosters’ own flight is not validated, like a booster’s. The
M4.5m milestone shipped this; its
decision record
says how a parallel stage is read and flown.
A booster may drop with motors still burning, when the file separates it at its first
burnout. A stage with motors in more than one mount burns out when its first motor does, as
OpenRocket flies it, and drops the rest still burning. The dropped part flies with no thrust, so a
note names the motors and the impulse its flight leaves out. On the first configuration of
OpenRocket’s Pods–powered with recovery deployment, [C6-7; 2× A3-4, B6-0] (its motors by
stage, sustainer first, the stages separated by ;), the note begins “part 1 drops at 0.860 s
with A3, A3 still burning”. The impulse it gives, 0.509 N·s, is the one the report’s
Parts dropped still burning
section holds. This sustainer is unstable, with a least margin of −4.21 calibres. In the
conditions of OpenRocket’s record, a 0.15 m rod at 28.61° N with no wind, hpr’s sustainer turns
over at 2.04 s. From hpr sim’s defaults, a 1.5 m rail with no wind, it does not, so the apogee
printed assumes it stays upright, and why the two differ is not traced. hpr sim warns that the
rocket is unstable under power and marks that apogee as not a prediction
(Flight metrics: unstable under power;
#335). The
M4.5n milestone shipped this
(Staging: A booster dropped still burning).
A payload dropped with nothing left to burn flies when its own parachute opens at the split.
Some rockets drop a payload section at the booster’s ejection charge, after the motor has burned
out, sometimes before apogee. From the split, each part flies as a point with only its own
devices’ drag, as OpenRocket flies a descent. A payload with nothing open would coast as if in a
vacuum, so hpr sim flies the split only when the part that keeps the nose has a device of its
own open by then. A device set to lowerstageseparation opens at the split, its delay after.
Otherwise the configuration is refused with “the part that keeps the nose, would coast with no
drag from the separation at …”. To fly it, set one of the payload’s devices to open at the split:
in OpenRocket, its deployment event “Lower stage separation” with no delay.
The payload is part 0: its apogee is the flight’s, and its own events, such as its apogee and its
landing, follow the stack’s in the events table. A dropped part that climbs higher gets a note,
“part 1 peaks at … above the flight’s apogee”, as a waiver’s height is the highest any part
reaches. The note reads “the stack comes apart at 4.860 s
with nothing left to burn: part 1, Booster, drops away”. On OpenRocket’s Deployable payload
and ARC payload rocket, all six configurations fly. The payload’s apogee is within 1.1% of
OpenRocket’s, and every descent meets the targets: each deployment within 0.1 s of OpenRocket’s or
2% of its time, whichever is larger, and the landing speed and flight time within 5%
(Recovery). Two of the six separate before
apogee.
Each of the Deployable payload’s five configurations flies with two warnings: the payload and
the parachute are 25 mm across in a 21 mm bore, so they can’t fit as drawn. Their width sets only
their own inertia (a packed part wider than its bore).
- What the refusal avoids. Coasting from the split with no drag, the Deployable payload’s C6-3 payload would reach 268.8 m, 4.00% above OpenRocket’s 258.5 m for the same payload flying on its own airframe.
- Not validated: the booster’s flight after the split, as above (issue #179). Nor a real payload whose parachute opens late, which would land later and further away: OpenRocket’s record holds no such flight.
- The figure and the exports stop at the split.
--plotand--exportfollow the stack, which ends where it comes apart; after that, each part’s flight is in the events and landings only, and a note says so.
The M4.5g3 milestone shipped this; its decision record (a separation with nothing left to burn) has the numbers.
The launch
Every flight starts from a rail, at a site, in the
standard atmosphere. Without options, the site is at sea level
at 0° N, 0° E, the rail is vertical and 1.5 m long, and the air is calm. The rail has no
friction. hpr sim doesn’t read the launch conditions an OpenRocket file stores with its
simulations: give them with these options.
Set --elevation for any real field. The air thins with height, so the same rocket flies
higher from a high site: from 1,400 m, the rocket above climbs about 8% higher than from sea
level. Latitude changes gravity only a little: at 45° N its apogee moves by less than 0.2%. Set
--latitude and --longitude too before exporting a map, which is drawn where you say the pad
is.
| option | what it sets | default |
|---|---|---|
--latitude DEG | the site’s latitude, degrees north (south is negative) | 0 |
--longitude DEG | the site’s longitude, degrees east (west is negative) | 0 |
--elevation M | the site’s height above sea level, m | 0 |
--rail-length M | the rail’s length, from the rocket’s aft end to the rail’s top, m | 1.5 |
--inclination DEG | the rail’s angle above the horizon, degrees: 90 is vertical | 90 |
--heading DEG | the direction the rail leans toward, clockwise from true north, degrees (add the declination to a compass reading) | 0 |
--wind M_S | a wind of this speed at every height, m/s | calm |
--wind-from DEG | where the wind blows from, clockwise from north, degrees: 270 is a west wind | 0 |
OpenRocket measures its launch rod’s angle from the vertical instead, so its 5° is 85 here. This flies the same rocket from Spaceport America’s field, on a rail leaning 5° into a west wind:
$ hpr sim validation/fixtures/ork/pod-flights/pods-none.ork --motor H54 --latitude 32.99 --longitude -106.97 --elevation 1400 --rail-length 3 --inclination 85 --heading 270 --wind 5 --wind-from 270
pods-none (pods-none.ork)
configuration 1 of 1: [H128W-0] with --motor H54
static margin 2.71 calibres off the rail; least 2.71 calibres, at 0.21 s, before apogee
apogee 877.4 m above the site at 11.48 s
rail exit speed 30.2 m/s
delay apogee 7.98 s after burnout
descent no recovery device opened, so the fall is not a prediction
motor: 1 × 168H54-10A (from the bundled catalog) in `Motor mount`, lit at launch
launched at 32.99° N, 106.97° W, 1400 m above sea level, from a 3 m rail 85° above the horizon, leaning toward 270°, in a 5 m/s wind from 270°
help: see the Accuracy page before trusting these numbers: https://nrdptel.github.io/hpr-sim/accuracy.html
note: the file has no recovery device, so the rocket falls from apogee on its airframe alone, on aerodynamics that hold only at small angles of attack: its landing time, speed and place, and any peak it sets in the fall, are not a prediction
warning: drag: issue #18: skin friction is taken as fully turbulent, but on a smooth surface the flow stays laminar near the nose, where friction is lower: hpr's reads high by 3.6% on RocketPy's Calisto at Mach 0.3; if this surface is that smooth, the drag reads high, so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are (https://github.com/nrdptel/hpr-sim/issues/18)
warning: stability: issue #172: the static margin read up to 0.1108 calibres higher than OpenRocket's on four private designs, for a reason not yet found, so this flight's margin may read high by as much (https://github.com/nrdptel/hpr-sim/issues/172)
event time height speed
liftoff 0.00 s 0.3 m 0.0 m/s
rail exit 0.21 s 3.3 m 30.2 m/s
burnout 3.50 s 468.6 m 144.4 m/s
apogee 11.48 s 877.4 m 11.3 m/s
ground hit 28.71 s 0.0 m 70.3 m/s
(heights are the center of gravity's above the site; speeds are over the ground)
top speed 184.3 m/s at 2.35 s
top Mach number 0.556
landing 310.4 m from the pad at 28.71 s, at 70.3 m/s: with no recovery device opened soon after apogee, not a prediction
It climbs about 4% higher than the same rocket at sea level, in the first example, not the 8% the altitude alone gives: the rocket turns into the wind as it climbs, and the rail already leans that way, so its climb tips away from the vertical. The wind does most of it.
The motor and the configuration
--config NAMEflies the configuration of that number, name or id. The output’s first lines list the file’s configurations by number and name.- A configuration the file leaves unnamed, as OpenRocket files usually do (50 of the 56 in
OpenRocket’s own examples), is called by its motors and their
delays in brackets:
[C6-5],[C6-P]for a plugged motor,[C6-5; B6-0]for two motors. --configignores case and brackets, so--config c6-5takes[C6-5], as does its number, such as--config 4.- A name two configurations share is refused, with their numbers; so is a word that is one configuration’s number and another’s id or name, with their names and ids.
- Without
--config,hpr simflies the file’s default configuration, or its only one; if it can’t tell which, it refuses and lists them.
- A configuration the file leaves unnamed, as OpenRocket files usually do (50 of the 56 in
OpenRocket’s own examples), is called by its motors and their
delays in brackets:
--motor NAMEflies a motor of the built-in catalog, by its designation or common name (H54,168H54-10A);hpr motors listshows the catalog. A name the catalog lacks is fetched from ThrustCurve.org and cached, ashpr motors fetch NAMEdoes (Motors from ThrustCurve.org).--motor FILEflies the motor in a.engor.rsefile (RASP and RockSim files). Either way, the motor goes in the configuration’s motor mount, in place of the file’s motor, and lights at launch.--delay Sgives--motor’s motor an ejection delay ofSseconds after burnout, and--delay Pmakes it a plugged motor. Without it, the motor fires no ejection charge, so a parachute set to open on the charge stays shut. The first line then sayswith --motor H125-CT --delay 10, and thedelayline how far from apogee the charge fires (Pick a motor).--offlinetakes a fetched motor from the cache alone, never the network. Setting the environment variableHPR_OFFLINE=1does the same for every command.--mount NAMEsays which mount--motorgoes in, by its name or id, when the design has several and no configuration says, or to move the motor to another. A name two mounts share is refused, with their ids;--jsonlists each motor’smountid too.--accept-design-errorsflies a design whose checks find errors, such as a motor wider than its mount. The flight’s notes then list the errors: such a rocket can’t be built as drawn.
hpr sim refuses, with the reason, rather than fly something other than the design:
- with
--motor, a configuration with motors in more than one mount, as--motorflies one; - a rocket whose stages separate in a way
hpr simdoesn’t fly: unpowered before apogee, at or after apogee beside another separation, or before the separation behind it; a.orkconfiguration’s powered splits fly, one or several (Separation); - a motor lit by a stage’s separation, in a design that states no separation, such as a rocket’s
.json; - with
--motor, a.orkrocket of more than one stage, or a configuration that switches a stage off; - a
.orkrocket hpr couldn’t read exactly as written, such as one with a parallel stage on a rocket of several stages; - a design whose checks find errors, unless
--accept-design-errorsis given; - a hybrid motor in a
.rsefile, which says so: hpr flies solid motors only. A.engfile doesn’t say what kind of motor it holds, so a hybrid’s is flown as a solid; check the motor.
Exporting the recording
--export FILE writes the flight’s recording: every quantity hpr tracks, every 0.01 s (set it
with --interval, down to 0.001 s), and at every event. The file’s extension picks the format;
repeat --export for several files. hpr sim won’t write over a file it reads.
| extension | what it holds |
|---|---|
.csv | one row per sample, with a header naming each column and its unit |
.json | the same columns and rows |
.parquet | the same, as Apache Parquet, for data tools such as pandas |
.geojson | the rocket’s path over the Earth, for web maps |
.kml | the same path, for Google Earth |
The maps draw the whole path. They mark the landing point when a recovery device opened, and no landing point otherwise: with none open, the path after apogee and where it ends are not predictions.
Each file names the program that wrote it, its version and its designation. A CSV file can’t
without breaking a spreadsheet’s reading of it, so hpr sim writes a small file beside it,
flight.meta.json for flight.csv, which does, with the design and configuration flown and the
number of rows. Exporting a flight says where
each format keeps it, and what each column and field means.
Plotting the flight
--plot FILE.svg draws the flight as one picture, to look at rather than to analyze: the
altitude, the speed and the acceleration against time, in three panels over one time axis, with
each event marked. The figure is always the same set, so any two flights read
alike. An SVG opens in any web browser. The numbers behind it come from the same flight as the
text: for the data itself, or a plot of your own, use --export (above). The name must end in
.svg and its folder must exist.
- Altitude: the height of the center of gravity above the launch site, as the events table gives it.
- Speed: the center of gravity’s speed over the ground, whose peak is the
top speedline, and its vertical speed (the thin line, up positive), whose value under the parachutes is thedescentline’s rate. - Acceleration: the size of the nose tip’s acceleration, and its vertical part (the thin
line, up positive). The nose tip is the point hpr’s equations of motion track, and the peak
acceleration of the
--jsonsummary is taken there too; for a rocket that isn’t turning, every point of it accelerates alike. A recovery device opens fully at once (the landing), so its opening shows as a sharp spike. - Not what an accelerometer reads. The panel shows how the motion changes: zero on the pad, and in a coast about −10 m/s² vertically, gravity plus drag. An accelerometer reads about zero in that coast, and 9.8 m/s² on the pad.
- Events: each instant with events is a numbered balloon (a circle) above the panels, with a dotted line through them. The table under the figure gives each event a row: its balloon’s number, its time, the altitude then, and its name, with each parachute or streamer by name.
- Not a prediction: when no recovery device opens within 1 s of apogee, the fall from apogee to the first opening (or to the ground) is hatched and labeled, the rule the summary’s “not a prediction” marks follow, for the reason the landing gives. The example below has no hatching, as its drogue opens at apogee.
The figure is drawn in the chart style of the FusionSpace product system, the design rules shared by hpr and its sister tools (ADR-164, the decision to follow them). In that style, magenta marks a prediction, and every line here is one. There is no legend box; a caption under the title says once what the lines mean:
| Line | Means |
|---|---|
| Dashed magenta, thick | A quantity’s size, simulated by hpr |
| Dashed magenta, thin | Its vertical part, up positive, so below zero on the way down |
| Chain (long dash, dot) | The ground, in the altitude panel; the other panels’ zero lines are solid |
| Dotted | An event, under its numbered balloon |
| Hatched area | A fall that is not a prediction |
The caption’s first line is the figure’s summary: the apogee, its time and the top speed, the
summary’s own numbers. The SVG carries the same sentence as its description, which a screen
reader reads out. Numbers on the figure are written for reading, 1,000 and a real minus sign
−20; copy numbers from --export or --json, which are plain. The text is set in Cascadia
Mono where it is installed, else another fixed-width font, so the lines keep their widths.
The panels sample the flight every --interval (0.01 s unless set), at every event, and at the
start and end of every step the integrator takes, so a jump such as a parachute’s opening is drawn
at its instant and full height. Where a pixel column of the figure covers many samples, it keeps
the column’s first, least, greatest and last, so a long descent stays a small file and no sampled
peak is lost. A peak the summary prints is found between samples, so the plotted one can sit just
below it: in the example, a test holds the plotted top speed and the main’s opening peak within
1% of the summary’s
(ADR-157, the
plot’s design;
ADR-175,
its style).
This draws the dual-deploy rocket of the landing:
$ hpr sim validation/fixtures/ork/loft-demo/demo-dual-deploy.ork --motor H54 --plot sim-plot.svg
cargo xtask cli draws this figure with the command above, so it shows what the current hpr
draws.
What hpr sim doesn’t fly yet
- A rocket’s
.jsonhas no recovery devices. It holds no parachute, so the rocket falls from apogee on its airframe alone, and the fall is not a prediction; the output’s notes say so. A Rust program can fly one (Getting started). The devices of a.ork, or of a.hpror.hprzconverted from one, fly (the landing). One set to open at the lower stage’s separation (lowerstageseparation) flies only on a split with nothing left to burn (separation), and is refused otherwise. - Separation, beyond powered splits. A
.orkconfiguration whose booster drops away under power flies, and so does one that drops several stages under power in turn, such as OpenRocket’s Three stage low power rocket (separation). A design whose only separation can come only after apogee flies as one stack, with a note, as does a design with no separation stated. A payload dropped with nothing left to burn flies when its own device opens at the split. These are refused by name: such a payload whose device opens later, or that has none; a separation with nothing left to burn beside another; a separation at or after apogee beside another separation; and one that comes before the separation behind it. With your own--motor, a staged configuration is refused too, since that motor can’t be told when the stages separate. - Real weather. One wind at every height, in the standard atmosphere.
hpr weatherwrites a launch day’s profile, buthpr simdoesn’t fly it yet (issue #265).
hpr mc
hpr mc flies a design many times, each flight’s uncertain inputs drawn afresh about their
planned values, and prints how far the apogee and the landing spread, with
the ellipses the landings fall in. It reads whatever hpr sim reads, with the same
options for the design, the motor and the launch, and flies each flight as hpr sim does, a
staged .ork’s separations included. Monte Carlo dispersion explains the
method, what each dispersion does and how to choose the numbers.
How far to trust it. The run is the Rust library’s: a test flies the same design through
MonteCarlo::runand gets the same flights, bit for bit, on one platform. The spread is only as good as the standard deviations you give and hpr’s flight models, which are not yet validated against repeated real flights (Accuracy).
Scattering a flight
This flies the repository’s small test rocket on the catalog’s Cesaroni H54 200 times, in a 4 m/s wind from the west, off a rail leaned 5° into it, each flight’s mass, drag, motor, wind and rail drawn afresh:
$ hpr mc validation/fixtures/ork/pod-flights/pods-none.ork --motor H54 --runs 200 --seed 2026 --wind 4 --wind-from 270 --inclination 85 --heading 270 --mass-sd 0.02 --drag-sd 0.05 --impulse-sd 0.03 --burn-time-sd 0.02 --wind-sd 0.25 --wind-from-sd 15 --inclination-sd 1 --heading-sd 2
pods-none (pods-none.ork)
configuration 1 of 1: [H128W-0] with --motor H54
200 flights, seed 2026: 0 failed
nominal mean std dev 5% median 95%
apogee (m) 813.9 819.4 34.4 765.0 818.8 870.8
landing distance (m) 286.9 283.2 44.8 208.8 281.8 354.5
200 of 200 flights landed, centered 282 m west and 2 m north of the pad
landing ellipse semi-major semi-minor heading flights inside
50% 53 m 33 m 89° 48.5%
95% 110 m 68 m 89° 93.5%
95%, the next flight 111 m 69 m 89° 93.5%
motor: 1 × 168H54-10A (from the bundled catalog) in `Motor mount`, lit at launch
launched at 0° N, 0° E, 0 m above sea level, from a 1.5 m rail 85° above the horizon, leaning toward 270°, in a 4 m/s wind from 270°
help: see the Accuracy page before trusting these numbers: https://nrdptel.github.io/hpr-sim/accuracy.html
scattered, one standard deviation each: --mass-sd 0.02, --drag-sd 0.05, --impulse-sd 0.03, --burn-time-sd 0.02, --wind-sd 0.25, --wind-from-sd 15, --inclination-sd 1, --heading-sd 2
note: the file has no recovery device, so the rocket falls from apogee on its airframe alone, on aerodynamics that hold only at small angles of attack: its landing time, speed and place, and any peak it sets in the fall, are not a prediction
warning: drag, the nominal flight: issue #18: skin friction is taken as fully turbulent, but on a smooth surface the flow stays laminar near the nose, where friction is lower: hpr's reads high by 3.6% on RocketPy's Calisto at Mach 0.3; if this surface is that smooth, the drag reads high, so the apogee, the top speed and the drift read low, and the flutter margin and the largest dynamic pressure look better than they are (https://github.com/nrdptel/hpr-sim/issues/18)
warning: stability, the nominal flight: issue #172: the static margin read up to 0.1108 calibres higher than OpenRocket's on four private designs, for a reason not yet found, so this flight's margin may read high by as much (https://github.com/nrdptel/hpr-sim/issues/172)
The table gives the nominal flight’s figure (the flight hpr sim flies with the same options),
then the run’s mean, standard deviation, 5th percentile, median
and 95th percentile. A failed flight counts as tried with no value. The landing ellipses are the
Landing ellipses section’s: the 50% and 95% ellipses hold
that share of a normal scatter with the run’s mean and spread, and the last allows for the run’s
spread being only an estimate, so the next flight lands inside it 95% of the time. An ellipse’s
heading is the direction of its long axis, in degrees clockwise from north, and flights inside counts the run’s own landings inside each, a failed flight counted outside. This rocket
has no parachute, so its landings are not a prediction, and the note says so.
The options
Each dispersion is one standard deviation, zero by default, which leaves its input at its nominal value. With none given, every flight is the nominal one, and a note says so. Choosing the numbers gives values to start from, with their sources.
| option | one standard deviation of | the library’s field (in radians for an angle) |
|---|---|---|
--mass-sd FRACTION | each stage’s mass without motors, as a fraction of it (0.02 is 2%) | dry_mass_sd_fraction |
--cg-sd M | each stage’s center of mass along the axis, m | cg_sd_m |
--drag-sd FRACTION | the zero-lift drag coefficient | drag_sd_fraction |
--impulse-sd FRACTION | each motor’s total impulse, its propellant with it | impulse_sd_fraction |
--burn-time-sd FRACTION | each motor’s burn time, at the same impulse | burn_time_sd_fraction |
--delay-sd S | each motor’s ejection delay, s | ejection_delay_sd_s |
--wind-sd FRACTION | the wind’s speed at every height | wind_speed_sd_fraction |
--wind-from-sd DEG | the wind’s direction, about --wind-from | wind_heading_sd_rad |
--inclination-sd DEG | the rail’s angle above the horizon | rail_elevation_sd_rad |
--heading-sd DEG | the rail’s heading | rail_azimuth_sd_rad |
--deployment-lag-sd S | each recovery device’s lag after its trigger, s | deployment_lag_sd_s |
--runs N sets how many flights, 1 to 100,000 (100 by default). --seed N picks the random draws
(0 by default): the same seed, design and options give the same flights, bit for bit, on one
platform, whatever the machine’s number of cores. hpr mc refuses a negative standard deviation,
a run of no flights, and an export that isn’t a .csv, before it flies.
Exporting every flight
--export runs.csv writes one row a flight, in index order: what it drew, then what its flight
came to. Each number is written in the shortest form that reads back to the same bits, so a
spreadsheet or a script reading the file gets the run’s own numbers; a number a flight doesn’t
have is an empty cell. Beside it, runs.meta.json names the program that wrote it, the design,
the configuration, the seed, the number of flights and the dispersion. The columns:
| columns | what they hold |
|---|---|
index, outcome, failed_at, reason | the flight’s number from 0; flown or failed; for a failed flight, whether its draw made an impossible input (inputs) or the flight refused it (flight), and the error |
drag_scale, wind_speed_scale, wind_turn_rad, rail_elevation_offset_rad, rail_azimuth_offset_rad | the factors and offsets drawn for the drag, the wind and the rail (1 or 0 for an input not scattered) |
stage_N_dry_mass_scale, stage_N_cg_shift_m | each stage’s, counted from 1 at the nose |
motor_N_impulse_scale, motor_N_burn_time_scale, motor_N_ejection_delay_offset_s | each motor’s in the configuration flown |
device_N_deployment_lag_offset_s | each recovery device’s, a tumble’s always 0 |
termination, apogee_m, apogee_time_s | why the flight ended, and its apogee above the site |
rail_exit_speed_m_s, max_speed_m_s, max_mach, max_dynamic_pressure_pa, max_acceleration_m_s2, min_static_margin_cal, min_flight_margin_cal, max_angle_of_attack_rad | the flight’s peaks, as hpr sim --json’s summary has them |
landing_time_s, landing_east_m, landing_north_m, landing_distance_m, landing_latitude_deg, landing_longitude_deg, ground_hit_speed_m_s | where and when it landed; after a split with nothing left to burn, where the part that keeps the nose landed |
part_N_landing_time_s, part_N_landing_east_m, part_N_landing_north_m, part_N_ground_hit_speed_m_s | for a staged flight, each dropped part’s landing |
flags | the operating envelope’s flags the flight raised, and unstable_under_power or unstable_without_margin, separated by ; |
The same table is hpr_analysis::table::RunTable in the Rust library.
What the output warns of
Failed flights are counted, never dropped: each reason is printed once, with how many flights
failed so and the first one’s index, its row in the export. The
operating envelope’s flags, and the flag for a rocket
unstable under power, are counted over the flights, and
the known issues in hpr’s drag and stability are the nominal flight’s, as hpr sim prints them.
So are hpr sim’s notes on the flight’s own numbers, such as a fall from apogee before a device
opened, each beginning “the nominal flight:”.
A staged flight can fail where its nominal flight flies, and is counted with its reason:
- A separation timed in seconds isn’t scattered, so a flight whose drawn burn is longer still burns at it, which hpr refuses unless the split may drop a burning motor. A separation at a burnout follows the drawn burn.
- A part dropped on the way up with nothing left to burn, by a separation or an ejection charge,
flies as a point, with only its open devices’ drag. If its device fired by the split but waits
out a drawn lag, the part would climb through the lag with no drag at all, so the flight is
refused.
--jsonprints all of it as one document, its schemaschema/cli/mc.schema.json.
hpr motors
hpr motors looks motors up. The catalog built into hpr holds 32 motors, from class B to class O,
each with a public-domain thrust curve from ThrustCurve.org
(Solid motors says how they were chosen). hpr motors fetch
fetches any other motor’s curve from ThrustCurve.org into hpr’s cache, for hpr sim to fly
(Motors from ThrustCurve.org); or download its .eng or .rse
file (RASP and RockSim files) and show that.
hpr motors search lists the motors vendors have in stock, with their prices
(Motors you can buy).
Listing the catalog
hpr motors list lists the catalog. Three filters narrow it, and each one given must match:
--classtakes an impulse class, such asJ.--diametertakes a casing diameter in millimeters, such as54, and matches within 0.5 mm.--manufacturertakes a maker as themakercolumn spells it (AeroTech,Cesaroni,Loki,Estes,Quest,AMW) or its full name (Cesaroni Technology), in any case.
A class or diameter the catalog has no motor for lists none, and exits with 0. A class that doesn’t exist, a diameter that isn’t a positive number, or a maker the catalog doesn’t know is refused with status 1: a misspelt maker would otherwise look like a maker with no motors.
The figures are the catalog’s, each worked out from the motor’s public-domain ThrustCurve.org
curve file: size, masses and delays from its header, the rest from its curve
(The bundled motors). In the delays column, P (or
1000) means plugged: the motor has no ejection charge
(ejection delay). The delays are as the header writes them, which
can be fewer than the maker sells.
$ hpr motors list --class J
3 motors from ThrustCurve.org data files marked public domain, downloaded 2026-09-17; size, masses and delays from each file's header, the rest from its curve.
designation maker class dia mm len mm impulse N·s avg N burn s delays
J450DM AeroTech J 54 359 1061.6 465.6 2.28 14
1266J760-19A Cesaroni J 54 329 1267.3 758.2 1.67 8-10-12-14-16-18-19
J300LR Loki J 54 327 1212.8 297.0 4.08 P
A motor’s figures
hpr motors show takes a motor’s name or a file’s path. A name can be the full
designation (1266J760-19A) or the common name (J760), in any
case, with or without spaces and hyphens. When several motors share a name, it shows each of them.
An argument ending in .eng or .rse is read as a file; anything else is looked up by name.
It works each figure out from the thrust curve, the list of time and thrust points in the motor’s file:
- The total impulse is the area under the curve, with its points joined by straight lines. It sets the impulse class.
- The burn time is measured the NFPA 1125 way: from when the thrust first reaches 5% of its peak to when it last falls back to it.
- The average thrust is the total impulse divided by that burn time.
- The masses and the casing size are the catalog’s, or the file’s for a file.
For a catalog motor, the last line names the public-domain curve file the figures come from.
hpr motors list gives the same figures, as a
test checks:
ThrustCurve.org’s own published figures are not bundled, as it states no terms for them
(issue #295). When the 32 were chosen, their
total impulse, average thrust and burn time agreed with ThrustCurve.org’s within 1%, and hpr’s
peak, the curve file’s highest point, runs from 16.7% below ThrustCurve.org’s (Cesaroni 26E31-15A)
to 2.1% above (Loki M1378LR) (Solid motors).
$ hpr motors show J760
1266J760-19A (Cesaroni Technology), from the bundled catalog
impulse class J
total impulse 1267.3 N·s
average thrust 758.2 N
peak thrust 938.6 N
burn time 1.67 s, from 0.001 s to 1.672 s (NFPA 1125, 5% of peak)
propellant 576.0 g of 1076.8 g loaded
casing 54 mm across, 329 mm long
delays 8 s, 10 s, 12 s, 14 s, 16 s, 18 s, 19 s
curve file https://www.thrustcurve.org/simfiles/5f4294d20002e9000000074a/ (public domain)
A motor file shows every motor in it, as one .eng or .rse file can hold several. The
repository holds the 32 catalog motors’ files to try, such as:
hpr motors show crates/hpr-motor/data/thrustcurve/curves/5f4294d20002e90000000724.eng
Some motor data is ambiguous. A delay of 0 can mean an ejection charge at burnout, or a plugged
motor with no charge. hpr shows it as “0 (at burnout, or plugged)” and ends the output with a
warning; the Estes F15 in the catalog is one example. The warning’s “RASP spec” is the .eng
format’s description (RASP and RockSim files).
Motors you can buy
hpr motors search lists the motors that U.S. vendors carry, with who has each one in stock and
at what price. The list comes from motor.fusionspace.co, a free
site that reads a dozen vendors’ public listings every hour and covers AeroTech, Cesaroni and Loki
motors of class D and up (Motor stock and prices says what it publishes and how
hpr reads it). Five filters narrow the list, and each one given must match:
--in-stockkeeps the motors at least one vendor has in stock.--classtakes an impulse class, such asL.--diametertakes a diameter in millimeters, such as54, and matches within 0.5 mm.--manufacturertakesAeroTech,CesaroniorLoki, or the full name the site uses (Cesaroni Technology,Loki Research), in any case.--max-pricetakes U.S. dollars, such as150or149.99, and keeps the motors in stock whose price for one motor, at the cheapest vendor with it in stock, is at most that. A motor sold in a pack counts at the pack’s price divided by its size, as the site works it out, to the cent (What the site publishes). A motor out of stock has no such price, and nor does one whose cheapest vendor shows no price or prices it in another currency than U.S. dollars (every vendor’s price was in dollars on the recording):--max-pricenever keeps them.
The motors come cheapest first, by that price. The motors with no such price, including every
motor out of stock, come last, by maker and designation. A filter that can’t match anything real (a class that doesn’t exist, a maker the site
doesn’t cover, a price that isn’t dollars and cents) is refused with status 1, as hpr motors list
refuses one. A list with nothing in it exits with 0. When --max-price leaves nothing, the last
line names the cheapest motor in stock that the other filters keep, and its price, so you know
what one costs.
The list is fetched and saved as hpr weather’s answers are
(Online, offline, and saved answers): the first search
fetches it over HTTPS and keeps a copy in hpr’s cache folder, a search within the hour reads the
copy, and --offline reads the copy however old it is. --from FILE reads a list saved earlier,
the site’s motors.json or in-stock.json, and touches neither the network nor the cache. With
--in-stock or --max-price, which keep only motors in stock, hpr fetches the site’s list of
motors in stock (about 1 MB); otherwise, the whole list (about 1.6 MB). The two are saved
separately. Offline, a search of motors in stock reads the whole list’s copy when it has no copy
of the in-stock one; a search of every motor needs the whole list’s copy.
Every list, even an empty one, carries two credit lines, under its first two lines: the site’s,
“Motor stock data from motor.fusionspace.co”, which its data license (CC BY 4.0) asks for, with
its caution to check stock and price on the vendor’s own page before relying on them; and
ThrustCurve.org’s, since the motors’ figures (impulse, average thrust, burn time) are its published
values, which the site repeats (Credit and terms). The JSON
output carries both in attribution. Keep them wherever you show the list.
This reads the list of motors in stock that the tests use, recorded at 07:07 UTC on 1 October 2026, and keeps the L motors in stock at up to $300 each:
$ hpr motors search --in-stock --class L --max-price 300 --from crates/hpr-net/tests/fixtures/replay/motor-finder-in-stock.json
4 motors (in stock, class L, at most $300.00 each) in motor.fusionspace.co's list built 2026-10-01T07:07:29Z.
Read from motor-finder-in-stock.json
Motor stock data from motor.fusionspace.co, licensed CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/); aggregated from public vendor listings and ThrustCurve.org; provided as is, with no warranty: check stock and price on the vendor's own page before relying on them
Motor data and thrust curves courtesy of ThrustCurve.org, https://www.thrustcurve.org/
designation maker class dia mm impulse N·s avg N burn s each $ pack vendor
L1520T AeroTech L 75 3715.9 1567.8 2.36 260.99 1 Balsa Machining Service
L850W AeroTech L 75 3646.2 850.0 4.42 282.74 1 Sirius Rocketry
3419L645-P Cesaroni L 75 3419.8 644.8 5.30 286.36 1 Performance Hobbies
3683L851-P Cesaroni L 75 3683.2 849.1 4.34 290.39 1 Animal Motor Works
each $ is the price of one motor and pack how many come in the pack, both at the vendor named
last. A motor out of stock shows - for both and out of stock for the vendor; a vendor that
shows no price gives - under each $; a price in another currency names it, such as
282.74 CAD. The full product page is in the JSON output (cheapest_in_stock.url). At $150, the same
recording lists nothing:
$ hpr motors search --in-stock --class L --max-price 150 --from crates/hpr-net/tests/fixtures/replay/motor-finder-in-stock.json
0 motors (in stock, class L, at most $150.00 each) in motor.fusionspace.co's list built 2026-10-01T07:07:29Z.
Read from motor-finder-in-stock.json
Motor stock data from motor.fusionspace.co, licensed CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/); aggregated from public vendor listings and ThrustCurve.org; provided as is, with no warranty: check stock and price on the vendor's own page before relying on them
Motor data and thrust curves courtesy of ThrustCurve.org, https://www.thrustcurve.org/
None costs $150.00 or less: of the 20 motors in stock the other filters pass, the cheapest is AeroTech L1520T at $260.99.
How far to trust it.
hpr motors searchgives back the site’s values unchanged, and reads the list with the same checks the library makes on every answer from the site (What hpr refuses). The tests incrates/hpr-cli/tests/motors_search.rsrun it offline on the recorded lists, from the file and from the cache, and check every motor it lists, and their order, against the recording’s own JSON, filtered by the test itself, not by hpr’s code. With one L motor’s price edited to $149.99 in a copy, the--max-price 150search lists exactly that motor. Fetching online is not tested automatically. Whether a vendor really has a motor at that price is the vendor’s to say. A list fetched now, or read from a copy under an hour old, can be up to two hours behind the vendors’ pages. A list read with--offline, with--from, or after a failed fetch is as old as the “built” time on its first line (How far to trust it).
Motors from ThrustCurve.org
hpr motors fetch NAME finds a motor on ThrustCurve.org and keeps
its curve in hpr’s cache (Where the cache lives), so
hpr sim flies it with no network after. It is for a motor hpr’s 32-motor catalog lacks; hpr sim
fetches on its own too, so the command matters most before a trip with no signal. This is
ThrustCurve’s data, as its contributors uploaded it: hpr checks that a file makes a motor it can
fly, not that its curve is right.
- Before a trip. For a
.ork, run the commandhpr sim --offlineprints for it, which names the motor’s manufacturer as the file writes it (hpr motors fetch --manufacturer AeroTech H128W), or fly the design once with a network connection: either fills the cache the design reads offline.hpr motors fetch H128W, without--manufacturer, fills the cache forhpr sim --motor H128Winstead. - Which motor.
NAMEis a designation (F27R/L) or common name (F27), as ThrustCurve.org spells it;--manufactureradds the maker, by name or abbreviation. ThrustCurve matches a designation exactly, any case; if no motor has it, hpr asks for the common name. Exactly one motor must answer:J450names four makers’ motors, and the refusal lists them, so give the designation or--manufacturer. A hybrid is refused, as hpr flies solid motors only. - Which curve. A motor can have several curve files. hpr takes the first RASP (
.eng) file that makes a motor hpr flies, ranked by who measured it: a certification test, then the manufacturer, then a user, then a file that names no source; with none, a RockSim (.rse) file the same way.--file IDtakes the file of that ThrustCurve id first, when the motor has it and it reads; the id is the onehpr motors fetchprints on itscurve fileline, and an offline refusal’s command names it when the flight asks for a file. The output names the file, who measured it and its license, as ThrustCurve states them, and gives ThrustCurve’s credit. - OpenRocket’s curve for a
.orkmotor. ThrustCurve can hold several files of one motor with different curves, and OpenRocket may fly another than the first by that order: the Estes A8’s first certification file burns 0.536 s, the one OpenRocket’s examples fly 0.73 s. A.orkrecords which curve OpenRocket flies by a digest, a fingerprint of the curve, so for the motors of OpenRocket’s example designshpr simkeeps a table from digest to the ThrustCurve file holding that curve, matched by comparing the curves (cargo xtask ork-curves). It takes that file first, and the note on the motor says so. A motor outside the table flies the file by the order above, and the note says it may not be the curve OpenRocket flies. An offline refusal’s command then carries--file, so it fetches the file the flight takes. Offline with a cache that holds only some of the motor’s files, a file the cache lacks is given up for the usual order’s, and the note says the curve may not be OpenRocket’s. The table is as of 2026-10-04; if ThrustCurve’s files change,cargo xtask ork-curvesrewrites it. - Offline.
--offline, orHPR_OFFLINE=1, reads the cache alone, and a copy of any age flies; with no copy, the command fails with exit status 1. Online, a copy stays fresh for a day; after that hpr asks again, and keeps the old copy if the network fails.
A fetch with a network connection printed this on 2026-10-04 (pasted by hand, not run in CI; its
help: line as hpr writes it since M0.6b, which made each hint a
help: line):
$ hpr motors fetch F27R/L
F27R/L (AeroTech), from ThrustCurve.org: fetched now
curve file 5f4294d20002e90000000372, RASP (.eng), from a user, no license stated
case 29 × 83 mm
help: kept in the cache: `hpr sim --motor F27R/L` flies it, offline too
Motor data and thrust curves courtesy of ThrustCurve.org, https://www.thrustcurve.org/
Offline with nothing cached, it says what to run:
$ hpr motors fetch H128W --offline
error: H128W is not in hpr's cache of ThrustCurve.org, and the run is offline
help: with a network connection, `hpr motors fetch H128W` fetches it into the cache
$ echo $?
1
hpr bundles none of these curves: ThrustCurve.org grants no license for its motor records, and each file carries its contributor’s own license. The decision record on fetching motors, ADR-154, has the reasoning.
hpr convert
hpr convert writes a motor as a RASP .eng file or a RockSim .rse file, the two formats
ThrustCurve.org offers (RASP and RockSim files), and a
design as a .ork, .hpr or .hprz file (converting a design). Give it the
file to read and the file to write; the extensions pick the formats. It reads a motor file, or a
motor of the bundled catalog by its name:
hpr convert H170M H170M.eng
A catalog motor is written with the size and masses the catalog gives, which are the ones hpr
flies; where its curve file’s header says otherwise, a warning says so. Written as .rse, the
figures worked out from them (the mass fraction, the specific impulse, and the mass and center of
gravity at each point) are rescaled to match, and a warning says that too. The delays are the
curve file’s, which can differ from the ones hpr motors show lists. An existing file of the
output’s name is replaced, but never the file being read: to rewrite a .eng file in hpr’s
layout, convert it to a new .eng file name. Here the Estes F15’s .rse file, from the catalog’s
curves, becomes a .eng file:
$ hpr convert crates/hpr-motor/data/thrustcurve/curves/5f923edb1bca5800041716ab.rse F15.eng
read 5f923edb1bca5800041716ab.rse
wrote F15.eng: F15
warning: line 3: engine "F15": delay 0 in "0,4,6,8" is ambiguous: the RASP spec means an ejection charge with no delay, but files mostly mean plugged
warning: F15: a .eng maker is one word, so "Estes Industries, Inc." is written "Estes_Industries,_Inc."
warning: F15: dropped the comments' blank lines and the spaces that end their lines: a .eng comment is one line of text
warning: F15: dropped Type, auto-calc-mass, auto-calc-cg, avgThrust, peakThrust, throatDia, exitDia, Itot, burn-time, massFrac, Isp, m, cg: a .eng file has no place for them. hpr doesn't use them for a solid motor: it works the mass and center of gravity out from the curve and the masses
What a conversion keeps
Both formats give a motor’s name, maker, diameter and length, its loaded and propellant masses,
its delays, and its thrust curve. The curve, the size and the masses
are kept: converting a file and converting the result back gives each of them again as hpr reads
the file, bit for bit, but for a mass of 16 or 17 digits (below). The formats write some things differently, and hpr convert translates:
.eng | .rse | |
|---|---|---|
| masses | kilograms | grams |
| delays | 6-10-14 | 6,10,14 |
| a plugged motor’s delay | P | 1000 |
| the curve’s first point, zero thrust at ignition | left out | written |
| a name or maker of several words | joined by _ | as it is |
Masses move between kilograms and grams by moving the decimal point, not by multiplying, which
would round: 0.0041 kg times 1000 is 4.1000000000000005 in a computer’s arithmetic, while
moving the point gives 4.1. A mass written with 16 or 17 digits may still change in its last
digit; a warning says so. A flight turns a .rse file’s grams into kilograms by dividing by
1000, and the catalog’s by multiplying by 0.001, so the same motor flown from a .eng file and from its converted .rse can differ in a mass’s
last digit, a part in 10¹⁶.
Some things come back written differently, though they mean the same:
- Delays spelled
p,1000or with spaces come back in the table’s spelling. hpr reads them as the same delays. - A name or maker of several words comes back with
_between the words, in any.engfilehpr convertwrites. A.engheader is seven fields split by spaces, and OpenRocket 24.12 refuses one of eight (the script checks it); a warning says so. - A
.engmotor whose delays are-, which names none, is written to.rsewithout delays, so converting it back to.engneeds--delays. - Comments lose their blank lines and the spaces ending their lines, and comments after the last
motor of a
.engfile are dropped; a warning says so.
A .rse file also gives figures a .eng file has no place for: the motor’s type, its total
impulse, average and peak thrust, burn time, mass fraction,
specific impulse, nozzle throat and exit diameters, two flags
saying whether RockSim works the mass and center of gravity out itself, and the mass and center of
gravity at each point of the curve. Going to .eng, they are dropped, and a warning names them.
hpr doesn’t use them for a solid motor: it works the mass and center of gravity out from the curve
and the masses, whichever format it reads. Going to .rse, they are filled in the way
ThrustCurve.org’s .rse files are: the total impulse by adding up the curve, the remaining
propellant falling in step with the impulse delivered, the center of gravity at half the length,
both flags set, and the type unspecified. The rules, and the counts of real files behind them,
are in the .rse format notes.
hpr convert refuses to write a hybrid motor as .eng, which couldn’t mark it as a hybrid (hpr
models solid motors only). A .eng header must give delays, so a .rse motor without them is
refused until --delays gives them, such as --delays P for a plugged motor. --delays fills
only the motors that give none.
Other programs. OpenRocket 24.12 opens all 32 files hpr convert writes from the bundled
curves. A script compares what OpenRocket reads from each converted file with what it
reads from the original: the name, the maker as OpenRocket names it, the type, the diameter and
length, the launch and burnout masses, the curve’s points, and the delays. 29 of the 32 read the
same.
- Two are
.rsefiles that say the motor is reloadable or single-use. A.engfile can’t say it, so OpenRocket reads the converted files’ type as unknown. hpr flies both types alike. - The third is the Aerotech G69N’s
.engfile, which writes its plugged delay as1000. OpenRocket reads no delay from that original, but a plugged motor from the converted.rse. hpr reads both as plugged, so the difference is in OpenRocket’s reading of the original, not in whathpr convertwrote.
The project’s automated tests don’t run the script, as they have no OpenRocket. Whether RockSim opens the files is not checked.
Converting a design
Given design files, hpr convert takes a design between OpenRocket’s .ork and
the hpr design format: .hpr, one JSON document, or .hprz, a zip archive of the
document with other files beside it. The extensions pick the formats, any of the three to any.
Here one of the repository’s public designs becomes a .hpr:
$ hpr convert validation/fixtures/ork/loft-demo/demo-multi-config.ork demo.hpr
read demo-multi-config.ork
wrote demo.hpr: "Loft Demo 38mm — motor comparison", 2 motor configurations, hpr design format 0.2
The document holds everything hpr read from the .ork, including what hpr doesn’t model, so
hpr convert demo.hpr demo.ork writes the .ork hpr would write from the original, byte for byte.
hpr sim demo.hpr flies it to the same flight as the .ork, but without the .ork reader’s
warnings, which hpr prints only when it reads the .ork itself. A document of an older version of the format is migrated
as it is read, and the output says from which version. hpr sim may refuse --motor for a
version 0.1 document, which didn’t record whether the rocket was read exactly as written; converting
the .ork again fixes that (versions).
--attach adds a file to a .hprz, once for each file: a flight log, a photograph, anything, up
to 256 MiB for the whole container. Each goes at the top of the container, under its file name,
and a name the container can’t hold, such as CON.txt, is refused
(the container’s name rules). Converting a .hprz to a .hprz
keeps its attachments, and adds any --attach names. Converting one to a .hpr or a .ork leaves
its attachments out, and a warning names each.
With --json, the output
(convert.schema.json)
gives:
- the files read and written, with their formats;
- the rocket’s name, and how many motor configurations the design holds;
- the version of the format the input was migrated from, if it was;
- the attachments written into a
.hprz; - the warnings: the
.orkreader’s, the.orkwriter’s, and each attachment left out.
hpr analyze
hpr analyze reads the log an altimeter recorded during a flight and prints what it says: when
the rocket lifted off, how high it went, how fast it climbed, and when it landed. It needs only
the log. It takes no design file and runs no simulation, so it answers “what did my rocket do?”
for anyone who flew one, whatever they designed it in.
It reads PerfectFlite’s .pf2 logs so far (the format). The one real file read
is a Pnut’s; the StratoLogger and StratoLoggerCF are expected to write the same layout, but no
file of theirs has been tried. Other loggers’ files come with
M7.1, the milestone that reads the other formats. It has no
check yet for a barometer’s errors near the speed of sound. If the flight may have come near Mach
0.9, about 300 m/s (1,000 ft/s), treat the top speed and the heights near it with care: the
barometer’s error can pull the top speed down too.
hpr analyze flight.pf2
Each reading is either a value that says where it came from, or withheld with the reason the log
can’t support it. The text output gives the reason as a sentence; the JSON output adds its code,
listed on Flight-log readings. A log read
with readings withheld still exits with 0; a file hpr can’t read exits with 1
(Exit codes). A PerfectFlite has a barometer and no accelerometer, so the top acceleration is
always withheld: working it out from the altitude would turn the altitude’s one-foot steps into
spikes of many g. What the file states about itself, such as the altimeter’s own apogee, is
printed beside hpr’s readings, never in their place. Heights are meters above the altimeter’s
reading on the pad, with feet in brackets; times are seconds on the log’s clock.
This example log is invented, so every reading can be checked against the flight it was made from (Reading a flight log works through it). Its true apogee is 390.3 m (1280.5 ft), at 10.26 s:
$ hpr analyze validation/fixtures/logs/synthetic-pnut.pf2
synthetic-pnut.pf2: PerfectFlite Pnut, serial 0, flight 1
984 samples every 0.050 s, from 0.00 s to 49.15 s; heights from the altitude after a 0.30 s running median
the logger states: apogee 390.4 m (1281 ft); ground elevation 182.9 m (600 ft) above sea level
liftoff 0.55 s
apogee 390.1 m (1280 ft) at 10.28 s, 9.72 s after liftoff
highest sample 400.5 m (1314 ft) at 11.35 s, set aside by the median
top speed 79.9 m/s (262 ft/s) at 2.10 s, 64.0 m (210 ft) up: the logger's own, from its barometer
top acceleration withheld: a PerfectFlite logger has no accelerometer; hpr doesn't difference the altitude twice to make one, as its one-foot steps would read as spikes of many g
landing 45.85 s, 45.30 s after liftoff; 35.58 s from apogee, at 10.9 m/s (36 ft/s) on average
The highest sample in the file is 10.4 m above the apogee: the pressure pulse of the ejection charge that fired a second after it. The readings are taken from the altitude after a running median, which sets that pulse aside. Reading a flight log explains each reading, and how far to trust it.
hpr weather
hpr weather writes down the air and the wind over a launch site, level by level from the ground
up: at each height, the pressure, the temperature, the humidity, and the wind’s speed and the
direction it blows from. That list of levels is a profile (a
sounding, when a weather balloon measured it). It comes from one of five
sources:
| source | what it is | command |
|---|---|---|
| Open-Meteo | a free weather service’s forecast, or its archive of past forecasts (Launch-day weather) | hpr weather open-meteo --latitude 32.99 --longitude -106.97 --time 2026-10-02T18:00Z |
| University of Wyoming | a weather balloon’s measurements, from the station’s latest launch before yours (Weather-balloon soundings) | hpr weather wyoming --station 72364 --time 2025-06-21T15:30Z |
| GFS | NOAA’s global forecast model, on a 0.25° grid, fetched or read from a whole file you download (NOAA forecasts: GFS and RAP) | hpr weather gfs --latitude 32.99 --longitude -106.97 --cycle 2026-09-30T00Z --hour 18 |
| RAP | NOAA’s 13 km forecast model over the contiguous U.S. and nearby Canada and Mexico (NOAA forecasts: GFS and RAP) | hpr weather rap --latitude 32.99 --longitude -106.97 --cycle 2026-09-30T12Z --hour 6 |
| ERA5 | a netCDF file you download from Europe’s climate data service: the past weather, reconstructed (ERA5 weather files) | hpr weather era5 era5.nc --latitude 47.21 --longitude 9.00 --time 2020-02-22T13:00Z |
hpr sim doesn’t fly a profile yet (issue
#265). A Rust program can: each source’s page
flies Calisto, RocketPy’s example rocket, through the weather it fetched.
How far to trust it.
hpr weatherruns each source’s reader from the library and adds no physics of its own. Each reader is checked against the recorded answers on its source’s page. The tests incrates/hpr-cli/tests/weather.rsrun the four online sources throughhpr weatheroffline, both from a saved answer and from the cache, and ERA5 from its file. Each run writes the profile the library builds from the same bytes, to the last bit, and lists the same levels left out. Fetching online is not tested automatically: it was run once by hand, on 30 September 2026, for all four online sources. On a network that inspects encrypted traffic, every fetch fails (Fetching over HTTP). How good a forecast is at your launch is not measured.
Asking for the weather
- Times are UTC, written
2025-06-21T15:30Z: the date, aT, the hour and minutes, and aZfor UTC. Seconds are optional, and so are the minutes:2026-09-30T00Zis midnight. A time without itsZis refused, so local time can’t be mistaken for UTC. - Open-Meteo reads the forecast at
--time, between the two hours around it. Add--historicalfor a launch already gone: that asks Open-Meteo’s archive of past forecasts.--modelnames one of Open-Meteo’s weather models, by the name its documentation gives; by default it picks one (What hpr asks for). - Wyoming takes the station’s number, such as 72364 for Santa Teresa,
New Mexico (finding a station), and your launch
--time. It fetches that station’s latest sounding before the launch. Soundings are named for 00 and 12 UTC, and the balloon goes up about an hour before: the example’s 12 UTC sounding left at 11:02.--bufrasks for the detailed version, a row every second or two of the climb. - GFS and RAP take the forecast run and the hour:
--cycleis when the run started, and--houris how many hours after it the forecast is for. GFS runs every 6 hours and RAP every hour. What hpr asks for lists the hours each run covers, and how long NOAA keeps them. - ERA5 reads a file you downloaded, at your site and time (Getting a file).
Online, offline, and saved answers
The first time you ask, hpr weather fetches the answer over HTTPS and keeps a copy in hpr’s cache
folder (Where the cache lives; HPR_CACHE_DIR moves it).
Ask again and it reads the copy while that is fresh
(How long a copy stays fresh). If the network fails,
it falls back to an older copy, and the output says so.
--offlinenever goes online. It answers from the cache, fresh or not. With no copy it fails, with exit status 1, and names what it would have fetched.--from FILEreads an answer saved earlier and touches neither the network nor the cache: Open-Meteo’s JSON, the Wyoming archive’s CSV, or a GRIB2 file for GFS and RAP. For GFS and RAP, still give--latitudeand--longitude: the file covers an area, and the site picks the point in it. For Open-Meteo and Wyoming, the file says where and when it is for, so the options that choose what to fetch are refused beside it: Open-Meteo’s--latitude,--longitude,--historicaland--model, and Wyoming’s--station,--timeand--bufr. Open-Meteo’s--timestays, and must fall within the file’s hours. A GRIB2 file is checked as a fetched one is: it must be on its model’s grid, and when you give--cycleand--hour, it must be that run and hour. Check the first line of the output, which says where and when the profile is for.--output FILE(or-o) writes the profile as JSON, described below.
hpr reads the small GRIB2 files that NOAA’s download server, NOMADS, cuts out around a site, and
whole GFS files you download yourself, which are packed more tightly
(A whole GFS file). The decoder also reads fields compressed as JPEG
2000 images, which RAP’s whole files use, but no whole RAP file has been run through hpr weather
yet (Files in JPEG 2000).
An example
This reads a recorded answer from Open-Meteo’s archive, for Spaceport America in New Mexico at
15:30 UTC on 21 June 2025, and writes the profile to profile.json. (hpr names the folder it
wrote to in full; here that folder is shown as <the scratch folder>.)
$ hpr weather open-meteo --time 2025-06-21T15:30Z --from crates/hpr-net/tests/fixtures/replay/open-meteo-historical.json --output profile.json
Open-Meteo at 32.9965° N, 106.9695° W, for 2025-06-21T15:30:00Z
Read from open-meteo-historical.json
Weather data by Open-Meteo.com (CC BY 4.0)
height pressure temp humidity wind from
m MSL hPa °C % m/s °
1400.0 859.5 29.6 18 3.3 162
1482.0 850.0 28.2 16 2.9 189
2014.4 800.0 23.4 16 2.8 225
3160.6 700.0 14.5 28 6.8 236
4439.1 600.0 3.3 60 9.8 219
5894.6 500.0 -7.0 70 9.9 200
7604.0 400.0 -17.1 34 8.3 218
9712.0 300.0 -31.5 8 4.4 261
10981.7 250.0 -41.2 9 10.6 261
12465.1 200.0 -52.2 13 9.8 253
14276.4 150.0 -64.8 18 9.9 263
16716.8 100.0 -70.8 12 6.3 253
18863.9 70.0 -66.2 4 7.8 164
20940.5 50.0 -60.0 0 5.4 108
24212.5 30.0 -54.0 0 8.6 83
Left out: 5 levels
below the ground, 5 levels: 1000 hPa, 975 hPa, 950 hPa, 925 hPa, 900 hPa
Profile written to <the scratch folder>/profile.json
The first line says where and when the profile is for: for Open-Meteo, its model’s grid point nearest the site; for GFS and RAP, the site itself, blended from the four grid points around it; for Wyoming, where the balloon was released. The ground comes first: its height above sea level (MSL), its pressure in hectopascals (hPa; 1013 hPa is sea level’s standard), its temperature and humidity, and the wind 10 m above it. Each pressure level above the ground follows. The levels the model gives below the ground are listed as left out: here the ground is at about 1,400 m, and 1000 to 900 hPa lie beneath it. Where each value comes from, and why levels are left out, is on the source’s page.
The profile file
--output writes the profile in SI units, the way the library holds it. Its levels run from the
lowest up, each with:
| field | unit |
|---|---|
height_msl_m | meters above sea level |
temperature_k | kelvin |
pressure_pa | pascals (100 Pa = 1 hPa) |
relative_humidity | a fraction: 0.18 is 18%; null for ERA5, which hpr reads as dry air |
wind_speed_m_s | meters per second |
wind_direction_from_rad | radians clockwise from true north, where the wind comes from |
Beside them, latitude_rad is the latitude the profile was measured or forecast at, in radians,
and wind_interpolation says how the wind is blended between levels (speed_direction). The Rust library reads the file
back as a SoundingProfile, with the same
checks. The text output and --json give the same levels with the direction in degrees.
What the profile leaves out
- Time between forecast hours, for GFS and RAP: pick the run and hour nearest your launch. Open-Meteo and ERA5 blend the two hours around it.
- The site’s real ground. A forecast’s ground is its model’s smoothed terrain, not your pad’s height, and an ERA5 file has no ground at all: its levels start at 1000 hPa, which can lie below a high site.
- Humidity in ERA5. hpr doesn’t read it yet, so ERA5’s profile is dry air.
- Above the top level, the library carries the air on as the standard atmosphere and holds the top wind; each source’s page gives its top.
JSON output
With --json, a command prints exactly one JSON document on standard
output, and nothing on standard error. That holds when the command fails, too. A failure prints an
error document. Each document has a published JSON Schema, which
describes its fields and units:
| output | schema |
|---|---|
hpr sim | sim.schema.json |
hpr motors list | motors-list.schema.json |
hpr motors show | motors-show.schema.json |
hpr motors search | motors-search.schema.json |
hpr convert | convert.schema.json |
hpr analyze | analyze.schema.json |
hpr weather | weather.schema.json |
hpr completions | completions.schema.json |
| any failure | error.schema.json |
Every document opens with the same tool object, the program that wrote it:
{"name": "hpr-sim", "version": "0.1.0", "designation": "FS · SW · TOOL 005"}. An error
document’s help lists what to do about the failure, the lines the text output starts with
help:, and is empty when there is nothing to suggest. The sidecar of a CSV recording
(Exporting the recording) has its own schema,
sim-export-meta.schema.json.
Units are SI, and each field’s name says its unit: total_impulse_ns is in newton-seconds and
diameter_m in meters. The exceptions say so in their names too: hpr sim gives angles in
degrees (latitude_deg) and margins in calibres (margin_cal), hpr weather gives the wind’s
direction in degrees (wind_from_deg) and humidity as a fraction (relative_humidity, 0 to 1),
hpr motors list keeps the catalog’s millimeters (diameter_mm), and hpr motors search the
site’s millimeters and cents (diameter_mm, unit_price_cents). Numbers are not rounded, so a converted value can end in
digits such as 0.0036000000000000003; a motor’s figures carry no more precision than its curve
file.
hpr is pre-alpha, so the fields may still change. The schemas are published beside the code, and a change to a document changes its schema in the same commit.
$ hpr motors show B4 --json
{
"tool": {
"name": "hpr-sim",
"version": "0.1.0",
"designation": "FS · SW · TOOL 005"
},
"motors": [
{
"name": "B4",
"manufacturer": "Quest Aerospace",
"source": {
"kind": "catalog",
"curve_url": "https://www.thrustcurve.org/simfiles/5f4294d20002e9000000088e/",
"format": "eng"
},
"diameter_m": 0.018,
"length_m": 0.08,
"propellant_mass_kg": 0.0036000000000000003,
"loaded_mass_kg": 0.0198,
"total_impulse_ns": 4.892141999999999,
"impulse_class": "B",
"average_thrust_n": 4.403753380057823,
"peak_thrust_n": 7.2,
"burn_time_s": 1.110902808988764,
"burn_start_s": 0.01745,
"burn_end_s": 1.128352808988764,
"curve_end_s": 1.184,
"delays": [
{
"kind": "seconds",
"value": 4.0
},
{
"kind": "seconds",
"value": 6.0
}
]
}
],
"warnings": []
}
A failure prints an error document. Its kind matches the exit status, and milestone is set
only for a command not available yet. Here hpr sim --offline refuses the test rocket above
without --motor, as neither the file, the bundled catalog nor the cache holds a curve for its
motor:
$ hpr sim validation/fixtures/ork/pod-flights/pods-none.ork --offline --json
{
"tool": {
"name": "hpr-sim",
"version": "0.1.0",
"designation": "FS · SW · TOOL 005"
},
"error": {
"kind": "input",
"message": "configuration [H128W-0] can't be flown as the file has it: no thrust curve for H128W: no embedded curve, and no motor of that manufacturer and designation in the bundled catalog; AeroTech H128W is not in hpr's cache of ThrustCurve.org, and the run is offline",
"command": "sim",
"milestone": null,
"help": [
"with a network connection, `hpr motors fetch --manufacturer AeroTech H128W` fetches it into the cache",
"give a motor with --motor"
]
}
}
$ echo $?
1
Exit codes
| status | kind in JSON | meaning |
|---|---|---|
| 0 | - | The command did what was asked. |
| 1 | input | An input was missing, unreadable or refused, such as a motor the catalog doesn’t have. |
| 2 | usage | The command line was wrong: an unknown command or option, or a missing argument. |
| 3 | not_available | The command is registered, but the milestone that brings it hasn’t come yet. |
hpr --help and hpr --version print text and exit with 0, even with --json.
hpr completions
hpr completions writes a script that lets your shell complete hpr’s commands and options when
you press Tab. Save it where your shell looks for completions:
| shell | command |
|---|---|
| bash | hpr completions bash > ~/.local/share/bash-completion/completions/hpr |
| zsh | hpr completions zsh > ~/.zfunc/_hpr, with fpath+=~/.zfunc before compinit in ~/.zshrc |
| fish | hpr completions fish > ~/.config/fish/completions/hpr.fish |
| PowerShell | hpr completions powershell >> $PROFILE |
| elvish | hpr completions elvish >> ~/.config/elvish/rc.elv |
What it leaves out
- Unpowered stage separations, a dropped stage’s accuracy, and a rocket
.json‘s parachutes.hpr simflies powered separations, one or several in turn, but not an unpowered one before apogee. A dropped stage flies as a point under its devices’ drag, so its landing is rough. It flies a.orkfile’s parachutes and streamers, but a rocket’s.jsoncomes down with no recovery device (what it doesn’t fly yet). - One log format.
hpr analyzereads PerfectFlite’s.pf2so far; other loggers’ files, and readings such as the drogue and main descent rates and the Mach number, come with M7.1 and M7.2. - Only OpenRocket’s and hpr’s own design files.
hpr convertandhpr simread OpenRocket’s.orkand hpr’s.hprand.hprz, not RockSim’s.rktor RASAero’s.CDX1(how the formats compare). - No validation check. The validation cases and their reference results are files in the
repository, not part of the tool. To re-run them, clone the repository and run
cargo xtask validate --check(what it checks). - RockSim is unchecked. OpenRocket opens the files
hpr convertwrites; whether RockSim does is not checked. hpr simdoesn’t fly the weather.hpr weatherwrites a profile that only a program reads so far (issue #265).- Stock from one site.
hpr motors searchcovers what motor.fusionspace.co does: U.S. vendors, prices in dollars, and AeroTech, Cesaroni and Loki motors of class D and up (what it leaves out). It has no filter by total impulse or by reload case. - Only 32 motors are built in. Any other motor needs its
.engor.rsefile.hprnever goes online to fetch one:hpr motors searchlists motors you can buy, not their curves. - No ready-built program.
hpris built from source with Rust; downloads for macOS, Windows and Linux wait until the project publishes releases. - The text output is for people. Its layout may change between versions; scripts should read
--json, whose schemas are published.
Online data and the cache
This page covers hpr-net, the one crate that uses the network, and the cache that makes its
answers work offline. It is for anyone who will pull weather, elevation or motor data into a
flight. Today the crate holds the cache, the offline rule and an HTTP client
(M5.1, the online layer), and six data sources: Open-Meteo’s
weather (Launch-day weather), weather-balloon soundings from the University
of Wyoming (Weather-balloon soundings), NOAA’s GFS and RAP forecasts
(NOAA forecasts: GFS and RAP), Open-Meteo’s ground elevation
(A launch site’s elevation), motor.fusionspace.co’s motor stock and prices,
and ThrustCurve.org’s motor records and thrust curves (both on
Motor stock and prices; hpr sim and hpr motors fetch fetch a curve the
bundled catalog lacks, Motors from ThrustCurve.org). Its tests replay a small hand-written
sample response and answers recorded from each, from a folder and from a test web server on the
machine running the tests, never the live network. Those tests speak plain HTTP only: encrypted
HTTPS was checked by hand, against Open-Meteo and then through the command line’s
hpr weather against all four sources, not in CI.
What it promises
hpr’s simulation itself never touches the network (the architecture’s pure-core rule: no filesystem, network or clock). Anything fetched from the internet goes through a client, which puts a cache (a folder of saved responses) in front of a transport (the thing that actually fetches). The client runs in one of two modes:
- Online: if the cache holds a fresh copy, the client returns it without fetching. Otherwise it fetches, saves the response, and returns it. If the fetch fails and an old copy exists, it returns the old copy, marked stale, with the reason the fetch failed; with no old copy, the fetch’s error.
- Offline: it never calls the transport. It returns whatever the cache holds, marked stale if it is old, or an error saying the URL is not cached.
Each answer says how fresh it is (Fetched, Cached or Stale), when it was fetched, and the
data source’s attribution, the credit line that its terms ask you to show with the data.
A data source can also give the client a check, its own parser (Client::fetch_checked). Then an
answer the parser refuses, such as an error page sent as a success or a forecast whose hours are
still empty, is never saved, so it can’t take a good copy’s place. Online, a good copy that has
gone stale comes back instead, with the reason; with no copy, the fetch fails. A saved copy the
check refuses counts as missing online, and is the error offline.
How long a copy stays fresh
Each source has a time to live (TTL): how many seconds a saved copy counts as fresh. A copy
exactly one TTL old is no longer fresh: online it is fetched again, offline it comes back marked
Stale. A copy dated later than the time asked (saved while the clock ran fast) is never fresh.
The caller passes the time, in seconds since 1 January 1970 (Unix time); the crate reads no clock,
so the tests can use small numbers.
Here is a worked example with a one-hour TTL (3,600 s), from the tests. Each scenario starts with an empty cache and fills it at 1,000 s:
| scenario | time asked (s) | cache holds | answer | fetches made |
|---|---|---|---|---|
| A, online | 1,000 | nothing | Fetched, from 1,000 | 1 |
| A, online | 4,599 | copy from 1,000 (3,599 s old) | Cached, from 1,000 | 1 |
| A, online | 4,600 | copy from 1,000 (3,600 s old) | Fetched, from 4,600 | 2 |
| B, offline | 1,000, before filling | nothing | error: not cached | none |
| B, offline | 1,010 | copy from 1,000 | Cached, from 1,000 | none |
| B, offline | 2,593,000 (30 days after 1,000) | copy from 1,000 | Stale, from 1,000 | none |
| C, online, network down | 8,200 | copy from 1,000 | Stale, from 1,000, with the reason | one failed |
| C, online, network down | 1,000 | nothing, for another URL | error: the fetch’s | one failed |
Fetching over HTTP
The HTTP client is Http. It is behind hpr-net’s http cargo
feature, so a program that depends on hpr-net alone and needs only the cache and saved responses
builds no network code. The hpr library’s net feature turns HTTP on: with
hpr = { ..., features = ["net"] } in Cargo.toml it is hpr::hpr_net::Http. Its API page has
an example, compiled in CI, that fetches into the platform’s cache folder. Building with HTTP needs
a C compiler, since the encryption library compiles some C and assembly.
| behavior | what it does | checked by |
|---|---|---|
| encryption (TLS) | rustls, a TLS library written in Rust, with Mozilla’s list of trusted certificate authorities built in: no OpenSSL, and the computer’s own certificate store is not read | one manual fetch; not in CI |
| time allowed | 60 s for the whole request: finding the host, connecting, redirects and reading; at most 30 days | a test, at 0.3 s |
| largest answer | 64 MiB, counted after any unpacking; one byte more is refused | a test, at and one byte past a limit set to the sample’s size |
| compression | asks for gzip and unpacks it | a test |
| error status | anything but a 2xx success, such as 404 or 304, is a failed fetch | a test each for 404 and 304 |
| redirects | followed, up to ten; the cache files the answer under the address you asked for | a test with one redirect; the cap of ten is ureq’s default, not tested |
| proxy | the first set of ALL_PROXY, HTTPS_PROXY and HTTP_PROXY (either case), for both http and https addresses; hosts listed in NO_PROXY connect directly; an unreadable value is ignored. A SOCKS proxy is refused with an error, not bypassed, and under one no redirect is followed | tests that a SOCKS proxy is refused, that NO_PROXY exempts a host, and that the error doesn’t show the proxy’s password; the rest is ureq’s, not tested |
| identification | sends User-Agent: hpr-sim/<version> (+https://github.com/nrdptel/hpr-sim), so a data provider can see who is asking | a test |
A failed fetch (an error status, a timeout, a refused or dropped connection, a too-long answer) is handled as above: online, the client falls back to an old copy marked stale.
The time and size limits, and whether to use the environment’s proxy, are fields of
HttpConfig. Start from HttpConfig::default(), change a
field (for example config.timeout = Duration::from_secs(300)), and pass it to
Http::with_config(config).
Because the computer’s certificate store is not read, a network that inspects encrypted traffic with its own certificate (common on company and school networks) makes every HTTPS fetch fail, and there is no setting yet to add a certificate.
Where the cache lives
The cache folder is whatever you pass. Cache::platform_dir
gives the usual place for caches on each system, and the folder is created on the first save:
| system | folder |
|---|---|
| macOS | $HOME/Library/Caches/hpr-sim |
| Windows | %LOCALAPPDATA%\hpr-sim\cache |
| Linux and other Unix | $XDG_CACHE_HOME/hpr-sim if that variable holds a full path, otherwise $HOME/.cache/hpr-sim |
Setting the environment variable HPR_CACHE_DIR puts the cache in that folder instead, on every
system; a relative path there is taken from the current folder. The folders come from environment
variables alone: on Windows LOCALAPPDATA is read rather than asking the system, which gives the
same folder unless the variable was changed. If HPR_CACHE_DIR is not set and HOME (macOS,
Linux) or LOCALAPPDATA (Windows) is unset or empty, Cache::platform_dir returns nothing and you
must name a folder.
Setting HPR_OFFLINE to anything but empty or 0 makes every hpr command answer from the
cache alone, as each command’s --offline does: for a script, or a field laptop, that must not
reach the network.
How it is checked
The tests in crates/hpr-net/tests/offline.rs
replay a hand-written sample from a folder instead of using the network:
- The offline test gives the client a transport that fails the test if it is ever called, then asks for a URL before and after it is cached, fresh and thirty days stale.
- Offline, a corrupt entry is an error; online, it is fetched again and overwritten.
- The checked fetch is tested through Open-Meteo’s source (Launch-day weather): an answer with an hour’s values missing is not saved, and an earlier good copy comes back and stays saved. The soundings’ source is tested the same way (Weather-balloon soundings), and NOAA’s forecasts refuse, and don’t save, a file for another run or hour than the one asked for (NOAA forecasts: GFS and RAP). The elevation source refuses an answer with no heights, and a lookup made once works offline (A launch site’s elevation). The motor-stock source refuses, and doesn’t save, a motor’s page that holds another motor, and the ThrustCurve.org source a download of another motor’s file (Motor stock and prices).
The cache’s own tests check that a saved body reads back, and that another URL’s entry in the same
file reads as a miss. They also check that the hash that names each file (FNV-1a, a standard 64-bit
hash, so names stay the same on every platform) matches its published test values. The cache
folder rules are checked for each system with a made-up environment, HPR_CACHE_DIR included; the
XDG_CACHE_HOME case runs only on Unix test machines. The API reference for
hpr_net has a runnable example.
The HTTP tests in crates/hpr-net/tests/http.rs
start a small web server inside the test, on the computer’s own loopback address (127.0.0.1),
which serves the same sample. Nothing leaves the machine, and the tests turn off any proxy the
environment names. They check that:
- a fetch saves the sample in the cache, a second fetch inside the time to live sends no request, and offline mode answers from the cache without a request while the server is still running;
- a compressed answer arrives unpacked, and the request names hpr-sim and asks for gzip;
- a redirect is followed and saved under the address asked for;
- a 404 and a 304 are failed fetches and save nothing;
- an answer one byte over the limit is refused and one at the limit passes, and a megabyte of zeros that compresses to under 64 KiB is refused at a 64 KiB limit;
- a server that never answers is cut off at a 0.3 s limit (the test requires under 5 s; the
server stalls for 10 s), and a timeout of
Duration::MAX(the usual way to say “no limit”), which hpr treats as 30 days, does not crash; - when the server hangs up without answering, an old copy comes back marked stale, with the reason.
What it leaves out
- No retries: a failed fetch is tried once. Plain, unencrypted
http://addresses are accepted; nothing forces HTTPS. The 60 s time limit covers the whole transfer, so a large answer on a slow connection can time out; raise it inHttpConfig. - No test makes an encrypted (HTTPS) connection: the test server speaks plain HTTP, since a local encrypted server would need certificates made for the test. One manual run on a Mac fetched a real Open-Meteo forecast over HTTPS and refused a site with an expired certificate (ADR-118: M5.1b, HTTP over the cache), but that is not repeated in CI.
- Freshness comes only from the source’s TTL: a server’s cache headers are ignored, and the cache key is the URL alone.
- If saving a fetched body fails (a full disk, a read-only folder), the fetch fails too.
- Nothing prunes old entries, and nothing locks the cache. Each file is written whole under a temporary name and then renamed, so no file is ever half written. But two programs saving the same URL at once, or a crash between the two renames, can leave a body beside another fetch’s time. Temporary files a crash leaves behind are not cleaned up.
Launch-day weather
This page covers fetching the weather over a launch site at launch time from Open-Meteo, a free weather service, and flying a rocket through it. The weather arrives as a sounding: temperature, pressure, humidity and wind at a column of heights, from the ground up to about 24 km. It is for anyone who wants a flight in a day’s forecast instead of the standard atmosphere with one wind.
How far to trust it. hpr turns Open-Meteo’s answer into a profile that gives back the pressure, temperature, humidity and wind of every level it keeps, to rounding error. That is checked on two recorded answers, below. How good the forecast is depends on the weather model behind it, and nothing here measures that: no flight has been flown in Open-Meteo’s weather and compared with its log, and no forecast has been compared with a weather balloon. The wind on the launch rail is the model’s wind 10 m above the ground. The tests replay recorded answers; the live, encrypted (HTTPS) connection to Open-Meteo was checked once by hand, not in CI.
Code: hpr_net::open_meteo (API reference), written for
the first weather increment, M5.2a. It needs the net feature
of the hpr crate. Weather from a file you download is on
ERA5 weather files, the air a weather balloon measured is on
Weather-balloon soundings, and NOAA’s own GFS and RAP forecasts are on
NOAA forecasts: GFS and RAP. The choices are in
ADR-119: Open-Meteo’s pressure levels as a sounding.
What hpr asks for
Open-Meteo has two services with winds above the ground (its documentation). Both give the output of numerical weather models, hour by hour, on 19 pressure levels: heights named by the air pressure there, in hectopascals (hPa; sea-level pressure is about 1013 hPa). They run from 1000 hPa, about sea level, to 30 hPa, about 24 km up.
| service | covers | a saved answer stays fresh for |
|---|---|---|
| Forecast | about 16 days ahead and 3 months back | 1 hour |
| Historical forecast | the same forecasts, archived, for a past launch | 30 days |
hpr asks for the two whole hours around the launch time, in UTC (universal time, the time at Greenwich). On each level it asks for the temperature, relative humidity, wind speed and direction, and the level’s geopotential height. At the ground it asks for the pressure, the temperature and humidity 2 m up, and the wind 10 m up. The request names the units, and hpr refuses an answer in any other. Wind directions are where the wind blows from, clockwise from north, as weather services give them.
The answer goes through hpr-net’s cache (Online data and the cache), so a
second request within the “stays fresh” time above is answered from the disk, and offline mode
answers from the disk only. An answer hpr can’t read is never saved, so it can’t replace a good
copy. hpr writes the site’s latitude and longitude to 5 decimal places, about 1 m on the ground
and far finer than any model’s grid. So a site given to 8 decimals or fewer, which your program
keeps in radians and turns back into degrees (that can change its last digits), still finds its
saved answer. Open-Meteo’s data is licensed
CC BY 4.0: show the credit “Weather data by
Open-Meteo.com (CC BY 4.0)” wherever you show the weather. Every answer carries it. The free
service is for non-commercial use, under 10,000 calls a day
(Open-Meteo’s terms). A self-hosted Open-Meteo server can stand
in for Open-Meteo’s own: set the request’s endpoint.
How the answer becomes a sounding
- The ground is at the answer’s elevation, with the ground pressure, the 2 m temperature and humidity, and the 10 m wind. Putting the 10 m wind at the ground makes it the wind on the launch rail, rather than jumping to the lowest level’s wind above it (Loft lesson L6: a forecast profile that stepped at its lowest level).
- Levels below the ground are left out. The weather models report all 19 levels everywhere, inventing values beneath high ground. A level is kept only when its pressure is below the ground pressure and its height is above the ground. A level with a missing value at either hour is left out too, and so is one whose relative humidity is outside 0 to 100%. The profile lists every level it left out, and why.
- Heights. The models give a level’s height in geopotential meters, which hpr converts to heights above sea level at the site’s latitude with the World Meteorological Organization’s formula (WMO-No. 8 eq. 12.16, as the atmosphere page explains). Open-Meteo’s documentation calls the value an altitude above sea level, so hpr checks which it is on the two recorded answers. Air pressure falls with height at a rate set by the air’s temperature and humidity (the hypsometric equation), which fixes how thick each layer between two levels must be, in geopotential meters. From 500 to 30 hPa, the recorded layers match that to within 0.03% to 0.13% on average. Read as meters above sea level instead, they would be 0.58% to 0.68% too thin. So they are geopotential meters.
- Between the two hours every value is linear in time. The wind is interpolated by its east and north parts, so a wind that turns through the hour takes the shorter way round.
- Between levels the sounding works as for any other: the temperature and humidity are linear, the pressure is hydrostatic, and the wind is interpolated by its speed and direction (or by its east and north parts, as RocketPy does, if you ask). Above the top level the standard atmosphere continues, and the air is marked as extrapolated.
- Humidity is taken as relative to liquid water. A model that reports it relative to ice at cold levels changes the density there by very little: the two differ by at most 27 Pa of water vapour (near −12 °C), which moves the density by under 0.03% at 400 hPa. Higher up the air is colder and the gap smaller: about 6 Pa at −40 °C, 0.08% even at 30 hPa.
An example
crates/hpr/examples/open_meteo_weather.rs
asks for the weather over Spaceport America at 15:30 UTC (9:30 local) on 21 June 2025. It makes
five calls:
Client::new(transport, Cache::new(folder), Mode::Online)sets up the fetching. The transport is what fetches (Online data and the cache). A real program passeshpr_net::Http::new()and keeps its cache inCache::platform_dir(). This one never uses the network: a stand-in transport answers with the historical-forecast answer recorded for the tests, which holds 15:00 and 16:00.OpenMeteoRequest::new(latitude, longitude, time, OpenMeteoApi::HistoricalForecast)says what to ask for, andopen_meteo::fetch(&client, &request, now)fetches it and reads it at the launch time.profile.sounding(WindInterpolation::SpeedDirection)makes the sounding, andsounding.wind()its wind.hpr_sim::Environment::new(earth, sounding, wind)puts both in a flight’s environment.
The program prints the ground and the levels below 6 km, then the air at the pad and above it next
to the standard atmosphere. Last, it flies RocketPy’s Calisto (one of the
example rockets) in both, without its parachutes. Run it from a
copy of the repository with cargo run --example open_meteo_weather -p hpr --features net. It
prints:
Open-Meteo over 32.99° N, 106.97° W at 2025-06-21 15:30 UTC
Weather data by Open-Meteo.com (CC BY 4.0)
freshness: Fetched
level (hPa) height (m) temperature (°C) humidity (%) wind (m/s) from (°)
859.5 1400 29.6 18 3.3 162 the ground
850 1482 28.2 16 2.9 189
800 2014 23.4 16 2.8 225
700 3161 14.5 28 6.8 236
600 4439 3.3 60 9.8 219
500 5895 -7.0 70 9.9 200
Below the ground, left out: 1000, 975, 950, 925, 900 hPa
height above the pad (m) pressure (hPa) temperature (°C) density (kg/m³)
0 Open-Meteo 859.5 29.6 0.9858
0 standard 856.0 5.9 1.0687
1000 Open-Meteo 765.2 20.4 0.9058
1000 standard 756.3 -0.6 0.9667
3000 Open-Meteo 602.9 3.6 0.7565
3000 standard 585.2 -13.6 0.7854
Calisto to apogee apogee (m above the pad) east of the pad (m) north (m)
Open-Meteo 2880.9 15.7 -155.6
standard, calm 2821.3 -5.1 0.0
freshness: Fetched means the answer came from the transport, not the cache; a second call within
the hour would say Cached. The ground is at 1,400 m, so the five levels from 1000 to 900 hPa are
left out. That June morning was 24 °C warmer at the pad than the standard atmosphere, and the air
was 8% less dense there and 4% less dense 3 km up. Calisto climbs 2.1% higher in it. At apogee
it is 156 m south of the pad (and 16 m east), upwind: the wind blows from the south-southeast at
the ground, turning to the southwest by 600 m above the pad (2,014 m above sea level), and a
rocket just off the rail, still slow, turns into the wind
(weathercocking) and flies that way. The calm flight’s 5.1 m west
is Earth’s rotation: a climbing rocket is pushed west (the Coriolis effect), and with the rotation
turned off it drifts 0.006 m.
How it is checked
The tests in crates/hpr-net/tests/open_meteo.rs
use two answers recorded from Open-Meteo for Spaceport America: one from the historical-forecast
service (21 June 2025, 15:00 and 16:00 UTC) and one from the forecast service (2 October 2026,
18:00 and 19:00 UTC). No test uses the network.
- At each recorded hour, the sounding is sampled at every kept level’s height. It gives back the recorded pressure to a relative 1e-12, the temperature to 1e-9 K and the wind to 1e-9 m/s, and holds the recorded humidity. The expected values are read from the recording by the test itself, not by the code under test. The ground is checked the same way.
- Both answers leave out exactly the five levels below the 1,400 m ground, and keep 14. Each half of the rule (the pressure, the height) leaves a level out on its own, in an edited answer.
- At 10 and 30 minutes past the hour, every value is the weighted average of the two hours, and the wind the weighted average of its parts. A wind from 350° then 10° is from due north at half past, and a direction never reads 360°.
- A humidity of 100% at both hours stays 100% at any time between; above 100%, a level is left out and the ground refused.
- The request’s address is the one recorded, so a replayed answer fills the cache. A second
request is answered from the cache, and offline mode answers without the network.
tests/http.rsdoes the same over HTTP, from a test server on the machine’s own address. - An answer with an hour’s values missing is not saved: with no earlier copy the request fails, and with one, the earlier copy comes back, marked stale, and stays saved. A saved answer that can’t be read at a later launch time in the same hour is fetched again online, and is the error offline.
- Text that is not JSON, Open-Meteo’s error answer, a unit other than the one asked for, a missing field, hours outside 1970 to 9999, and a time outside the answer’s hours are refused.
- The heights are geopotential, by the hypsometric check above; the test holds each average to the ranges quoted there.
What it leaves out
- Nothing checks a forecast against the weather that came. Weather-balloon soundings, the measured air, can be fetched too (Weather-balloon soundings), but no forecast has been compared with one. NOAA’s Global Forecast System and Rapid Refresh (GFS and RAP) can be fetched by name, or read from a whole GFS file you download (NOAA forecasts: GFS and RAP).
- The 2 m temperature and humidity and the 10 m wind are placed at the ground itself, so the whole launch rail sees the 10 m wind. A real wind is weaker close to the ground; how much that changes a rocket’s turn into the wind off the rail is not measured.
- A relative humidity above 100% at the ground refuses the answer rather than trimming it.
- When Open-Meteo refuses a request (a place or date it doesn’t cover), the error names the HTTP status but not Open-Meteo’s reason, because the HTTP transport drops the body of a failed answer.
- Only one place and one launch time per request, and Open-Meteo’s own choice of weather model unless you name one.
- The command line fetches it with
hpr weather open-meteo, buthpr simdoesn’t fly it yet (issue #265), and the Python package doesn’t fetch weather.
Weather-balloon soundings
This page covers fetching a weather balloon’s measurements from the University of Wyoming’s archive and flying a rocket through them. The balloon carries a radiosonde, which measures the air on the way up, from the ground to 30 km or more. What it records is a sounding: temperature, pressure, humidity and wind at a column of heights. Hundreds of stations release one at 00 and 12 UTC (universal time, the time at Greenwich) every day. This page is for anyone who wants a flight in the air that was measured near a launch, instead of a forecast (Launch-day weather, NOAA forecasts: GFS and RAP) or the standard atmosphere.
How far to trust it.
- hpr turns the archive’s answer into a profile that gives back the pressure, temperature, humidity and wind of every level it keeps, to rounding error. That is checked on three recorded soundings, below.
- A sounding is a measurement, but only at its station and time. The nearest station can be 100 km or more from a launch site (Santa Teresa, below, is about 130 km from Spaceport America), and the balloon goes up hours before or after the flight. Nothing here measures how much that changes a flight.
- Each row’s height is checked against the row before it, which catches a gross error in a pressure or height (57 hPa recorded for 557). More than 10 bad rows in a row refuse the answer, and so does a ground that the rows after it agree is wrong. That can refuse a good ground too, when the first few rows after it are a little off. A wrong wind, humidity or temperature is not caught: on layers under about 100 m thick the check allows any temperature from −150 to 80 °C.
- The archive serves two versions of most soundings, and they can disagree near the ground. In the example below, Calisto is 402 m from the pad at apogee in one and 563 m in the other.
- The tests replay recorded answers; the live connection to the archive is not tested in CI.
Code: hpr_net::wyoming (API reference), written for the
second weather increment, M5.2b. It needs the net feature of
the hpr crate. The choices are in
ADR-120: University of Wyoming soundings.
What hpr asks for
A request names a station and the sounding’s hour. The station is its World Meteorological
Organization (WMO) number, such as 72364 for Santa Teresa, New Mexico; the archive’s page has a
map of them. WyomingRequest::latest_before(station, launch_time) picks the last 00 or 12 UTC
sounding at or before a launch, with times in seconds since 1 January 1970 UTC (Unix time). The
balloon goes up about an hour before that hour.
hpr asks for the archive’s comma-separated text, one row per level, and reads these columns. Pressures are in hectopascals (hPa, 100 Pa; sea-level pressure is about 1013 hPa).
| column | read as |
|---|---|
| time | when the balloon was released, from the first row |
| latitude, longitude | where it was released, from the first row |
| pressure, in hPa | the level’s pressure |
| geopotential height, in m | its geopotential height above sea level |
| temperature, in °C | its temperature |
| relative humidity, in % | its humidity, relative to liquid water (the file gives it relative to ice too) |
| wind direction, in degrees, and wind speed, in m/s | the wind, and where it blows from, clockwise from north |
The units are in the column names, and hpr refuses a file with any other. The archive serves two versions of most soundings:
| version | what it is | rows |
|---|---|---|
| Coded message, the default | the message stations have sent for decades (WMO’s TEMP code, FM 35) | about 200 |
| BUFR file | the station’s newer digital file (WMO’s Binary Universal Form for the Representation of meteorological data), a row every second of the climb | about 6,000 |
The coded message holds the standard pressure levels (850, 700, 500 hPa and so on) and the significant levels between them, where the temperature or wind changes its trend.
The answer goes through hpr-net’s cache (Online data and the cache), and
wyoming::fetch(&client, &request, now) takes the time now, in Unix seconds, to judge how fresh a
saved copy is. For a day after its hour the archive’s copy of a sounding can still fill in, as a
station’s later messages arrive:
| the sounding’s age | a saved copy stays fresh for |
|---|---|
| under a day | an hour |
| over a day | 30 days, if it was saved after the sounding’s first day; an earlier copy is fetched again |
Offline mode answers from the disk only, however old the copy. An answer hpr can’t read is never saved, so it can’t replace a good copy. A sounding the archive doesn’t have comes back as an HTTP error (404, or 400 for a BUFR file the station doesn’t send), which the request reports; no test covers that.
The archive states no terms of use. Show the credit “Sounding from the University of Wyoming’s radiosonde archive” wherever you show the sounding. Every answer carries it.
How the answer becomes a sounding
Heights. The file gives geopotential meters, which hpr converts to heights above sea level at the first row’s latitude with the World Meteorological Organization’s formula (WMO-No. 8, its Guide to Instruments and Methods of Observation, eqs. 12.15 and 12.16, as the atmosphere page explains). The balloon drifts as it climbs. Converting at the latitude it reached instead would move a height by about 0.8 m per degree of drift at 10 km, and 2.5 m at 30 km; the example’s balloon drifted 0.06°.
Why geopotential. Air pressure falls with height at a rate set by the air’s temperature and humidity. The hypsometric equation turns that into how thick each layer between two pressures must be, in geopotential meters:
thickness = (R_d T̄_v / g₀) ln(p_bottom / p_top)
Here p_bottom and p_top are the pressures at the layer’s bottom and top, R_d is dry air’s
gas constant (287.05 J/(kg·K)), g₀ standard gravity (9.80665 m/s²) and
T̄_v the mean of the two rows’ virtual temperatures: the
temperature, raised a little for the humidity (WMO-No. 8, eqs. 12.17 and 12.18). In the three
recorded soundings, across the 13 layers between the standard levels from 850 to 10 hPa, the
recorded thicknesses match that to 0.01% to 0.03% on average; single layers are off by 0.05% to
0.13% on average, either way. Read as meters above sea level instead, the layers would be 0.51% to
0.60% too thin on average. So they are geopotential meters, as the column says. The test holds the
average under 0.1% as geopotential meters, and more than 0.4% too thin as meters above sea
level.
Each row is checked against the row before it. A row with a pressure, height and temperature fits the row before it when its height above that row is the layer’s thickness, give or take an allowance: 5% of the thickness, plus what rounding the two pressures can move it, plus 30 m. The pressures are rounded to 1 hPa in the coded message at 100 hPa or more, and to 0.1 hPa elsewhere. For example, from the coded message’s row at 557 hPa to the next at 549 hPa:
| value | |
|---|---|
| virtual temperatures | 272.24 K and 271.42 K (−1.7 °C and −2.5 °C, 79% and 81% humidity) |
R_d T̄_v / g₀ | 7,956.8 m |
| thickness | 7,956.8 m × ln(557/549) = 115.1 m |
| recorded | 5,151 m − 5,035 m = 116 m, a miss of 0.9 m |
| allowance | 5.8 m (5%) + 14.4 m (rounding each pressure by 0.5 hPa) + 30 m = 50.1 m |
Rows are kept from a chain. hpr looks for the longest chain of rows from the ground in which each row fits the one before it in the chain. The chain may pass by up to 10 rows at a time, and the rows it passes by are left out. Of two chains equally long, it takes the one whose layers fit more closely: the smaller total of each layer’s miss as a share of its allowance (a miss of 25 m with an allowance of 50 m is a share of 0.5).
Why a chain: every row kept must also lie above the last row kept, so one bad row kept can hide the good ones. A 557 hPa row with a digit lost, 57 hPa at 5,035 m, would be kept, and every good row up to 57 hPa, about 20 km, would lie below it and be dropped. It would have to be 18.6 km above the row before it, at 570 hPa, not 183 m, so it doesn’t fit: the chain passes it by, from 570 hPa to 549 hPa, and it is left out. Two or three bad rows that fit each other are passed by the same way, since a chain through them would have to pass by more good rows. A row with no wind or humidity is left out of the profile but can still be in the chain, so the next row is checked across a thin layer, not a thick one. A row with no temperature can’t be checked: the chain steps over it without counting it, and it is left out as missing a value.
In the three recordings every row fits the one before it, missing by at most 1 m beyond rounding. So the 30 m and the 5% are margin, and the check is for gross errors, not small ones:
- The 30 m leaves room for the coded message’s heights from 500 hPa up, which are rounded to 10 m. A height 30 m off misplaces its level by about as much as a 0.33% to 0.55% pressure error (the air’s scale height, the climb over which pressure falls by a factor of e, is 5.5 to 9 km).
- The 5% is a judgment, not a measurement: no row in the recordings needs any of it. It leaves room for a layer whose inner rows have no temperature, where the mean of its two ends’ temperatures gives its thickness less well.
Then hpr keeps:
- The ground, the first row: the pressure, temperature, humidity and wind at the station when the balloon was released. A first row with a value missing or impossible refuses the answer.
- One row of each run of rows in the chain with the same pressure, the middle one. The BUFR file gives pressures to 0.1 hPa, and high up the balloon climbs tens of meters while the pressure falls that much, so runs of rows share one pressure. The rounded value is the pressure at about the middle of its run. In the example, 1,931 of the BUFR file’s 5,851 rows are the other rows of such runs. The coded message has none.
- Each such row that lies above the last row kept, higher and at a lower pressure. Rows that fall or stay at one height, a balloon coming down, fit but don’t lie above, and are left out however many.
It leaves out:
- A row the chain passes by, as above.
- A row with a value missing or impossible: the last row often has no wind, and a pressure below 0.1 hPa or above 1,200 hPa, a temperature outside −150 to 80 °C, a height outside −1 to 60 km, a wind speed below zero or above 300 m/s, a humidity below zero or a direction beyond 360° is dropped the same way.
The profile lists every row it left out, and why. Two things refuse the answer:
- A ground the rows after it disagree with. No row before the ground checks it, so hpr also looks for chains that start after it, at one of the next 11 rows with a pressure, height and temperature within the bounds. If one of them beats every chain from the ground (it is longer, or as long and fits more closely), the ground is taken as wrong. Bad rows right after a good ground can do the same: rows that miss the ground but fit the rows above them, within the allowance, beat it when the chain through them is longer (it passes by fewer good rows than it holds bad ones, less one) or as long and closer. That happens in narrow bands of error, just past the ground’s allowance: in the BUFR file, whose first layers are about 8 m thick, two rows 31 m high refuse the answer, but not 20 m high (they fit the ground and are kept) or 35 m (they fit nothing but each other, and are passed by); in the winter coded message, three rows 45 m high. So do 11 bad rows that fit each other, however far off. hpr can’t tell these from a bad ground, and refuses the answer. Rows grossly off (850 m, say) fit nothing above them and are passed by, up to 10.
- More than 10 rows after the chain’s end. The chain can pass by only 10 rows at a time, so 11 bad rows in a row end it. hpr takes that to mean the chain’s end is wrong, or all of them are (a block of heights 1 km off). A long run of rows with no temperature can also refuse the answer: the layer across the run is then too thick for its two ends’ temperatures to give. In a BUFR file 10 rows are about 10 s of the climb, some 50 m; in a coded message they can span kilometers.
A refused answer names the lines and is not cached; the other version, or the sounding 12 hours earlier, may be usable instead.
In the profile:
- Humidity above 100%, which radiosondes can report in cloud, is kept in the level as recorded and taken as 100%.
- Between levels the sounding works as for any other: the temperature and humidity are
linear, and the pressure is hydrostatic (as the atmosphere page
explains). The wind is interpolated by its speed and direction; pass
WindInterpolation::Componentsto interpolate its east and north parts instead, as RocketPy does. Above the top level the standard atmosphere continues, and the air is marked as extrapolated.
An example
crates/hpr/examples/wyoming_sounding.rs
fetches the Santa Teresa sounding a launch at 15:30 UTC on 21 June 2025 would have had, in both
versions, and flies Calisto (one of the example rockets) from the
station without its parachutes. It makes these calls:
Client::new(transport, Cache::new(folder), Mode::Online)sets up the fetching. A real program passeshpr_net::Http::new()as the transport (what fetches) and keeps its cache inCache::platform_dir(). This one never uses the network: a stand-in transport answers with the two versions recorded for the tests.WyomingRequest::latest_before("72364", launch_time)picks the 12 UTC sounding, andwyoming::fetch(&client, &request, now)fetches and reads it. Setting the request’sversiontoWyomingVersion::Bufrasks for the BUFR file instead.sounding.sounding(WindInterpolation::SpeedDirection)makes the profile, andprofile.wind()its wind.hpr_sim::Environment::new(earth, profile, wind)puts both in a flight’s environment.
It prints the ground and five standard levels, with heights in meters above sea level, and each
version’s second row. Then it prints the air at the pad and above it next to the standard
atmosphere, and where Calisto is at apogee: east and north of the pad, negative for west and
south. Each flight starts on its own sounding’s ground. The third flight keeps the BUFR file’s
ground row and its rows from 1,438 geopotential meters up (the height of the coded message’s
second row), dropping those between. Run it from a copy of the repository with
cargo run --example wyoming_sounding -p hpr --features net. It prints:
Santa Teresa, New Mexico (72364), 2025-06-21 12 UTC
Sounding from the University of Wyoming's radiosonde archive
freshness: Fetched
released 58 minutes before 12 UTC
coded message: 227 levels kept, 1 left out
BUFR file: 3920 levels kept, 1931 left out
level (hPa) height (m) temperature (°C) humidity (%) wind (m/s) from (°)
872 1254 28.4 31 5.7 265 the ground
850 1482 26.6 32 11.8 270
700 3165 14.6 36 2.6 215
500 5903 -5.3 63 8.2 180
250 11002 -40.1 7 13.9 260
100 16724 -73.1 13 13.9 255
second row above the ground (m) wind (m/s) from (°)
coded message 186 10.7 269
BUFR file 8 11.1 264
height above the pad (m) pressure (hPa) temperature (°C) density (kg/m³)
0 sounding 872.0 28.4 1.0022
0 standard 871.4 6.9 1.0842
1000 sounding 778.5 22.3 0.9151
1000 standard 770.3 0.4 0.9811
3000 sounding 613.5 4.9 0.7664
3000 standard 596.5 -12.6 0.7977
Calisto to apogee apogee (m above the pad) east of the pad (m) north (m)
coded message 2848.1 -401.4 -23.4
BUFR file 2822.5 -561.3 -48.0
BUFR above 1,438 m 2845.9 -419.1 -19.1
standard, calm 2811.0 -5.1 0.0
freshness: Fetched means the answer came from the transport, not the cache. That early
morning (6 a.m. local) was 21.5 °C warmer at the station than the standard atmosphere, and the
air was 7.6% less dense at the ground and 3.9% less dense 3 km up. Calisto climbs 1.3% higher in
it than in the standard atmosphere with no wind; the thinner air and the wind both play a part.
At apogee Calisto is about 400 m west of the pad, upwind: the wind blows from the west, and a rocket just off the rail, still slow, turns into the wind (weathercocking) and flies that way. The calm flight’s 5.1 m west is Earth’s rotation: a climbing rocket is pushed west (the Coriolis effect), as on the Launch-day weather page.
The two versions put Calisto’s apogee 26 m apart, and the BUFR flight 160 m further west. Most of that is the first 186 m of the climb. Both start from the station’s wind at the ground, 5.7 m/s. The coded message’s second row, 186 m up, is 10.7 m/s from the west, so its wind grows steadily over that climb. In the BUFR file the wind is already 11.1 m/s 8 m above the ground, where the rocket is slowest and turns into the wind the most. Without its rows below 1,438 geopotential meters the BUFR flight is 419 m west at apogee and peaks at 2,846 m, within 18 m and 2 m of the coded message’s. Which of the two is nearer the wind a rocket meets is not measured: a balloon’s first seconds of drift are not a steady wind either.
How it is checked
The tests in crates/hpr-net/tests/wyoming.rs
use three answers recorded from the archive: Santa Teresa on 21 June 2025 at 12 UTC in both
versions, and Salt Lake City on 15 January 2025 at 12 UTC, a winter sounding. No test uses the
network.
- The sounding is sampled at every kept row’s height. It gives back the recorded pressure to a relative 1e-12, the temperature to 1e-9 K, the humidity to 1e-15 and the wind to 1e-9 m/s. The test reads the expected values from the recording itself, not through the code under test, and applies the rules above on its own.
- The rows left out are exactly those the rules leave out: the last row of each coded message (no wind), and 1,931 rows of the BUFR file (the other rows of a run). The coded messages keep 227 and 241 rows, the BUFR file 3,920.
- Every BUFR row, kept or not, lies within 0.08 hPa of the profile’s pressure at its height; the worst is 0.071 hPa. Keeping the first row of each run instead would miss by 0.093 hPa.
- The ground’s wind, 5.7 m/s from 265°, is checked against its east and north parts worked out by hand: 5.678 m/s east and 0.497 m/s north.
- An edited file drops a row with a missing temperature; one with a humidity below zero, a temperature of −999 °C, a negative wind speed or pressure, or a direction of 361°; one that fits but lies below the last row kept (and not merely the row before); and a row at the ground’s pressure. The rows left out are listed in line order. A humidity of 103% is kept and taken as 100%, and a wind from 360° reads as from north.
- Every row with a pressure, height and temperature in the three recordings fits the one before it, missing by at most 1 m beyond rounding (0 m in the coded messages, 0.97 m in the BUFR file). The test works out the thickness on its own, with the file’s mixing ratio (grams of water vapour per kilogram of dry air) for the humidity. Unit tests pin the check, and the worked example above, to thicknesses and allowances worked out by hand, 0.01 m either side of the edge.
- Rows the chain passes by, with the rest of the answer kept:
- A pressure missing a digit (57 hPa at 5 km); the profile is the recording’s without the row.
- A height raised 850 m, and a height lowered to 4,000 m.
- Two bad rows in a row, and two blocks of six raised rows with a good row between them.
- A bad row after which the balloon bursts and falls back.
- A row whose vapour pressure would exceed the air’s pressure (16 hPa at 30 °C, saturated).
- A row that only just fits the row before it, alone: 854 hPa 50 m low, 101 hPa 95 m high, and a BUFR row at 112.8 hPa 35 m high. The next row misses it, and a chain through it is not longer and fits less closely. The rows before and after it are kept.
- A row that misses the good row before it but only just fits the one below that, alone: 808 hPa raised 50 m, and a BUFR row at 244.2 hPa raised 35 m. A chain could skip the good row to reach it, but the chain through the good row is longer, or as long and closer.
- A BUFR row at 150.6 hPa raised 50 m, whose pressure is rounded to 0.1 hPa. In the coded message, 549 hPa raised 45 m fits within its pressures’ 1 hPa rounding and is kept.
- Blocks of bad rows that fit each other but not the good rows beside them, which are kept: 820 and 808 hPa raised 50 m; 101, 100 and 99 hPa raised 100 m; BUFR rows at 112.8 and 112.7 hPa raised 50 m; and two BUFR rows at 13.9 hPa lowered 50 m, and two at 10.1 hPa lowered 60 m, each inside a run sharing one pressure.
- The two or three rows after the ground raised 850 m; the ground and the rest are kept.
- A row inserted at 870 hPa recorded 65 m above the ground, where the air’s thickness puts it about 20 m up. It misses the ground, but the next row fits the ground, and a chain starting at the inserted row fits less closely: the row is left out and the answer is not refused.
- Rows kept or left out as they fit:
- With no wind, or no humidity, from 250 to 55 hPa (73 rows), every row after the gap fits and is kept. Checked across the gap instead, the next row would not fit.
- Rows falling or staying at one height at the end are left out without refusing the answer. When a row that fits and lies above follows them, the rows are left out and the row is kept. Of rows crossing back and forth over the last row kept, the first above it is kept.
- A BUFR run of three rows at one pressure whose middle is raised 100 m keeps its first row.
- A row at 0.1 hPa and −150 °C, 55.6 km up, fits the row below it and is kept.
- Answers refused:
- A block of 10 rows raised 1 km is left out; 11 refuse the answer.
- In the coded message, a ground at 87.2 hPa (872 with its decimal point misplaced), at 125 m (1,252 m with a digit lost), 8 hPa off, or 52 m or 80 m high; and a BUFR ground 34 m high, whose first layer is 8 m thick. Also a ground at 1,200 hPa, at −1,000 m or at −150 °C: these are within the bounds, but no row above fits them. In the coded message a ground 40 m high, or at 80 °C, fits the first layer (186 m thick) and is kept with every row. With only one row after a bad ground, the ground is kept and the row left out. A row with no temperature right after a bad ground isn’t counted: the refusal names the row after it.
- Rows after a good ground that beat it: two raised 31 m and four lowered 35 m in the BUFR file; three raised 45 m in the winter coded message and four raised 50 m in the other; and 11 rows raised 1 km. Two BUFR rows raised 20 m fit the ground and are kept, leaving out the three good rows no higher than them. Two BUFR rows raised 35 m, and 10 coded-message rows raised 1 km, are passed by. A chain without the ground may start only at the 11 rows after it: after 11 rows that fit nothing, the answer is refused for the gap.
- A row raised within its allowance is kept, and leaves out the good rows just above it, which now lie below it. Raising one row, or a block of two or three, by 20, 35, 50 or 100 m, or lowering it by 50 or 100 m, refuses no answer. It loses at most 2 other levels of a coded message. In the BUFR file, sampled at every 150th row (to keep the test under a minute) and at the rows where a sweep of every row (run once, not in the tests) found the worst, it loses at most 8 other rows for one row and 10, about 50 m of the climb, for a block. Lowering one row, or changing its pressure by 3%, loses no other.
- Values just past the bounds above are left out. Unit tests keep values at each bound; at the top row, temperatures of −150 and 80 °C and a wind of 300 m/s are kept and make a profile.
- The request’s address is the one recorded, so a replayed answer fills the cache. A second request is answered from the cache, and offline mode answers without the network. A copy saved while the sounding was young is fetched again once it has settled.
- An answer that isn’t the archive’s text (such as an error page) is not saved: with no earlier copy the request fails, and with one, the earlier copy comes back, marked stale.
- Text with no header, a missing or doubled column, a unit other than the one required, a row with too few or too many fields, a field that isn’t a number, a first row with no date, and a first row with a value missing are refused. A header or a row of a million commas, and more than 100,000 rows, are refused without being read further.
- The heights are geopotential, by the hypsometric check above: the test holds each average under 0.1% as geopotential meters, and more than 0.4% too thin as meters above sea level.
What it leaves out
- How far a station’s sounding is from the air over a launch site is not measured, in distance or in time. A rocket flown hours from the balloon, 100 km away, flies other air.
- Which version is nearer the truth near the ground, where they differ most, is not known.
- A level with any value missing is left out whole, even when its other values are good.
- The check catches a gross error in a row’s pressure or height. It keeps:
- A wrong wind, or a wrong humidity: the check doesn’t use the wind, and humidity moves the thickness by only a few percent (1.6% for saturated air at 30 °C and sea-level pressure). Only a wind outside 0 to 300 m/s is caught.
- A wrong temperature that keeps the thickness within the allowance, and a height error within it: 50 m on the example’s layer, more on thicker ones.
- A row whose pressure and height are both wrong yet fit each other.
- A wrong ground that fits the row after it (in the coded message, 40 m high, or 80 °C instead of 28.4 °C), or has only one row after it.
- Bad rows that fit their neighbours: a block of them can be kept, and good rows beside them passed by instead, no more than the block holds, and fewer unless the block fits more closely. In the coded message, 683 and 673 hPa both raised 50 m are kept, and 664 hPa is left out. High in a BUFR file, where rounding the pressure to 0.1 hPa widens the allowance, four rows at 13.9 hPa lowered 50 m are kept, and the two good rows at 14.0 hPa before them are left out.
- A good ground followed by bad rows that fit the rows above them is refused, as if the ground were wrong, when the chain through them wins: two BUFR rows 31 m high do it.
- A row, or a short block, kept a little too high leaves out the good rows just above it: in the tests, at most 2 other levels of a coded message and 10 other rows of a BUFR file (8 for one row).
- Only the archive’s comma-separated text is read, not its other formats, and there is no list of stations to search by place.
- Its terms of use are not stated; only soundings from U.S. stations, which are U.S. government works, are committed as test data (ADR-120).
- The command line fetches them with
hpr weather wyoming, buthpr simdoesn’t fly them yet (issue #265), and the Python package doesn’t fetch soundings.
NOAA forecasts: GFS and RAP
This page covers fetching the United States weather service’s own forecasts over a launch site and flying a rocket through them. NOAA’s National Centers for Environmental Prediction (NCEP) run two weather models whose output hpr reads:
- the Global Forecast System (GFS), which covers the whole Earth on a grid a quarter of a degree apart (about 28 km), and
- the Rapid Refresh (RAP), which covers the contiguous United States and nearby parts of Canada and Mexico on a grid 13 km apart, and is started again every hour.
hpr asks NOAA’s download server, NOMADS (NOAA Operational Model Archive and Distribution System), for a small piece of one forecast around the site. The piece arrives as a GRIB2 file, the World Meteorological Organization’s binary format for weather on a grid, and hpr reads it with its own decoder. The result is a sounding: temperature, pressure, humidity and wind at a column of heights, from the ground up to about 31 km (GFS) or 16 km (RAP). You can also download a whole GFS file and read it offline, which goes up to about 79 km (A whole GFS file). This page is for anyone who wants a flight in a named NOAA model’s forecast, rather than Open-Meteo’s choice of model (Launch-day weather) or a weather balloon’s measurement (Weather-balloon soundings).
How far to trust it.
- hpr’s decoder gives every value in the two recorded files that ecCodes, the European weather center’s reference decoder, gives: all 6,123 values within 2.2e-16 of each other, relatively (one rounding in the last binary digit). The profile gives back those values, interpolated to the site, at every level it keeps. That is checked below.
- A whole GFS file you download yourself reads too (A whole GFS file): every one of the 746,770,303 values in one such file is within 4.4e-16 of ecCodes’, relatively, and its profile at Spaceport America is the recorded cut’s to 1.04e-7. That check was a script run once outside CI; CI checks eight of the file’s messages.
- How good a forecast is depends on the model, and nothing here measures that: no forecast has been compared with a weather balloon or a flight log.
- The pad sits on the model’s ground, which is smoothed: at Spaceport America it is 1,476 m in GFS and 1,429 m in RAP, where Open-Meteo gives 1,400 m (Launch-day weather’s example).
- Fields packed as JPEG 2000 images, as in RAP’s whole files, read too: four public RAP fields, 182,443 points, match ecCodes in CI (Files in JPEG 2000). No whole RAP file has been tried.
- The tests replay two recorded answers; the live connection to NOMADS is not tested in CI.
Code: hpr_net::nomads (API reference), with the decoder in
hpr_io::grib2 (API reference), written for the third weather
increment, M5.2c, and extended to whole GFS files in
M5.2d2 and to JPEG 2000 in
M5.2d3. It needs the net feature of the hpr crate. The
choices are in
ADR-121: GFS and RAP from NOMADS’ grib filter, read by an in-house GRIB2 decoder
and
ADR-123: Complex packing, and a whole GFS file,
and ADR-124: JPEG 2000 packing.
What hpr asks for
A forecast comes from a run: the model starts from the weather observed at one hour, its cycle, and steps forward. Each forecast hour is the forecast for that many hours after the cycle. Times are in UTC (universal time, the time at Greenwich) and given in seconds since 1 January 1970 UTC (Unix time). Pressures are in hectopascals (hPa, 100 Pa; sea-level pressure is about 1013 hPa). A pressure level is a height named by the pressure there.
| model | grid | runs start | forecast hours | pressure levels asked for |
|---|---|---|---|---|
| GFS | 0.25° of latitude and longitude, the whole Earth | every 6 hours (00, 06, 12, 18 UTC) | every hour to 120, then every third hour to 384 | 28, from 1000 to 10 hPa (about 31 km) |
| RAP | 13 km, on a Lambert conformal map of the contiguous United States and nearby parts of Canada and Mexico (not Alaska or Hawaii) | every hour | every hour to 21; to 51 only from the 03, 09, 15 and 21 UTC runs | 37, from 1000 to 100 hPa every 25 (about 16 km) |
NomadsRequest::new(latitude, longitude, model, cycle, forecast_hour) says what to ask for; it
can’t fail. The request’s url(), and so nomads::fetch, refuse a cycle the model doesn’t run
or a forecast hour its run doesn’t have (NomadsModel::has_forecast_hour(cycle_hour, hour) says
which it has). hpr picks neither for you: to fly at 18 UTC, ask for GFS’s 00 UTC run at hour 18,
say, or RAP’s 12 UTC run at hour 6. A site outside the model’s grid is refused once the answer arrives.
The request’s address is NOMADS’ grib filter, a web form that cuts chosen variables, levels and a box of latitude and longitude out of a run’s file. hpr asks for a box 0.3° each way around the site, which holds 9 GFS or about 25 RAP grid points, among them the four around the site: about 28 KB for GFS and 40 KB for RAP. The box’s edges are written to hundredths of a degree, worked out from the site rounded to 5 decimals (about 1 m). So a site given to 8 decimals or fewer, which your program keeps in radians and turns back into degrees (that can change its last digits), asks for the same box and finds its saved answer. It asks for:
| variable | where |
|---|---|
| geopotential height | the ground (the model’s terrain) and each pressure level |
| pressure | the ground |
| temperature and relative humidity | 2 m above the ground and each pressure level |
| wind, as two components | 10 m above the ground and each pressure level |
GRIB2 fixes each variable’s unit, so there are no units to check. The filter also sends the ground’s own (skin) temperature, since it asks for temperature at the surface; hpr decodes it but doesn’t use it. With it, a cut holds 147 GFS or 192 RAP messages, one variable on one level each. A cut of more than 1,000 is refused unread.
The answer goes through hpr-net’s cache (Online data and the cache), and
nomads::fetch(&client, &request, now) takes the time now, in Unix seconds, to judge how fresh a
saved copy is. A run’s files don’t change once NOAA writes them, so a saved copy stays fresh for
30 days. NOMADS keeps only recent runs; on 2026-09-30 its grib filters listed 10 days of GFS
runs and 2 of RAP, so a past launch needs a copy saved while NOMADS still had it. A run appears
on NOMADS some hours after its start time, and NOMADS limits how often one may ask, so keep to
that. Offline mode answers from the disk only. Only an
answer that decodes, makes a sounding, and is for the run and hour asked for is saved, so a bad
answer can’t replace a good copy.
NCEP’s forecasts are U.S. government works, free of copyright. Every answer carries the credit “Forecast data from NOAA/NCEP (GFS, RAP), via NOMADS”; show it wherever you show the forecast.
How the answer becomes a sounding
Between grid points. A model gives each value only at its grid points. hpr takes the four around the site and blends them bilinearly: each point’s weight grows as the site nears it, in the grid’s own rows and columns, and the four weights add up to 1. A cut whose grid steps and projection aren’t the model’s (GFS’s 0.25° steps, RAP’s 13,545 m cells about 265° E) is refused and not saved.
The ground is at the model’s terrain height at the site, with the surface pressure, the 2 m temperature and humidity, and the 10 m wind. Putting the 10 m wind at the ground makes it the wind on the launch rail, rather than jumping to the lowest level’s wind above it (Loft lesson L6: a forecast profile that stepped at its lowest level). This is what Open-Meteo’s page does too.
Levels below the ground are left out. The models give every level everywhere, inventing values beneath high ground. A level is kept only when its pressure is below the ground’s and its height is above the ground. A level with a value missing at any of the four grid points is left out too. At Spaceport America, about 1,400 m up, six levels are underground in each model: 1000 to 850 hPa in GFS and 1000 to 875 hPa in RAP. GFS keeps 22 of its 28 levels and RAP 31 of its 37. The profile lists every level it left out, and why.
Heights. GRIB2 gives heights in geopotential meters, which hpr converts to heights above sea level at the site’s latitude with the World Meteorological Organization’s formula (WMO-No. 8 eq. 12.16, as the atmosphere page explains). The model’s terrain height is also given in geopotential meters and converted the same way. At this latitude that adds about 2 m: GFS’s 1,474.4 gpm becomes 1,476.4 m. If the model’s terrain were really a height above sea level, as for Open-Meteo’s elevation, the ground would sit about 2 m too high.
RAP’s winds are turned to east and north. RAP’s map is a cone unrolled flat (a Lambert conformal projection), and it gives each wind as two components along its map’s rows and columns, not east and north. “Up” on that map points true north only along one line of longitude, 95° W. Elsewhere the lines of longitude lean towards the map’s center, so the map’s “up” is turned from true north by an angle that grows with the distance from 95° W:
θ = n (λ − λ₀), n = sin 25°, λ₀ = 265° (95° W)
Here λ is the site’s longitude, and 25° N is where RAP’s cone touches the Earth. At Spaceport
America, 106.97° W (253.03° E), θ = 0.4226 × (253.03° − 265°) = −5.06°: the map’s “up” points
5.06° west of true north. hpr turns the wind by that angle, with u and v the components along
the map’s rows and columns:
east = u cos θ + v sin θ
north = −u sin θ + v cos θ
Left unturned, a 20 m/s wind would be about 1.8 m/s off sideways (20 m/s × sin 5.06°). The
components are blended between grid points first and turned once, at the site; across one 13 km
cell θ changes by about 0.06°. GFS gives its winds east and north already.
Humidity is taken as relative to liquid water, as for Open-Meteo. Whether NCEP’s models give it relative to ice at cold levels is not settled here; if they do, the density there moves by under 0.1% (Launch-day weather explains the bound). A humidity above 100% is kept in the level as given and taken as 100% in the sounding.
Between levels the sounding works as for any other: the temperature and humidity are linear,
and the pressure is hydrostatic. The wind is interpolated by its speed and direction; pass
WindInterpolation::Components to interpolate its east and north parts instead, as
RocketPy does.
An example
crates/hpr/examples/nomads_forecast.rs
fetches both models’ forecasts for Spaceport America at 18 UTC (noon local) on 30 September 2026:
GFS’s 00 UTC run at hour 18 and RAP’s 12 UTC run at hour 6. It flies Calisto (one of the
example rockets) through each, without its parachutes. It makes
these calls:
Client::new(transport, Cache::new(folder), Mode::Online)sets up the fetching. A real program passeshpr_net::Http::new()as the transport (what fetches) and keeps its cache inCache::platform_dir(). This one never uses the network: a stand-in transport answers with the two files recorded for the tests.NomadsRequest::new(latitude, longitude, NomadsModel::Gfs, cycle, 18)says what to ask for (andNomadsModel::Rapfor RAP), andnomads::fetch(&client, &request, now)fetches it and reads it at the site. Here it reads the two recorded forecasts, as a real program would fetch them.profile.sounding(WindInterpolation::SpeedDirection)makes the sounding, andsounding.wind()its wind.hpr_sim::Environment::new(earth, sounding, wind)puts both in a flight’s environment.
It prints each model’s ground and the standard levels 850, 700, 500, 250 and 100 hPa that lie
above it, with heights in meters above sea level and the wind’s direction as where it blows from,
clockwise from north. Then it prints where Calisto is at apogee: east and north of the pad,
negative for west and south. Each flight starts on its own model’s ground; the standard-atmosphere
flight starts on GFS’s. Run it from a copy of the repository with
cargo run --example nomads_forecast -p hpr --features net. It prints:
Spaceport America, 2026-09-30 18 UTC
Forecast data from NOAA/NCEP (GFS, RAP), via NOMADS
freshness: Fetched
GFS: run of 00 UTC, hour 18: 22 levels kept, 6 left out
RAP: run of 12 UTC, hour 6: 31 levels kept, 6 left out
RAP's winds turned by -5.06° to east and north
GFS level (hPa) height (m) temperature (°C) humidity (%) wind (m/s) from (°)
848 1476 19.3 42 5.5 242 the ground
700 3077 2.8 90 7.3 262
500 5720 -12.9 55 17.0 246
250 10822 -32.7 1 45.8 230
100 16741 -70.6 17 17.7 209
RAP level (hPa) height (m) temperature (°C) humidity (%) wind (m/s) from (°)
854 1429 16.9 70 3.7 246 the ground
850 1463 16.1 67 4.4 249
700 3076 2.6 77 8.1 272
500 5722 -12.0 39 20.5 242
250 10822 -32.6 1 42.9 231
100 16739 -70.8 10 13.0 211
Calisto to apogee apogee (m above the pad) east of the pad (m) north (m)
GFS 2842.8 -253.0 -131.7
RAP 2841.3 -204.3 -83.9
standard, calm 2826.9 -5.1 0.0
freshness: Fetched means the answer came from the transport, not the cache; a second call within
30 days would say Cached. GFS has no 850 hPa row because its ground, at 848 hPa, is above that
level. The two models agree closely aloft: at 500 hPa their heights are 2 m apart and their
temperatures 0.9 °C. Near the ground they differ more: GFS’s ground is 47 m higher and 2.4 °C
warmer, and its 10 m wind 1.8 m/s stronger.
Calisto climbs 14 to 16 m higher in either forecast than in the standard atmosphere with no wind. At apogee it is west-southwest of the pad, upwind: the wind at the ground blows from the west-southwest (242° and 246°), and a rocket just off the rail, still slow, turns into the wind (weathercocking) and flies that way. GFS’s stronger wind near the ground turns it further into the wind; it ends 49 m further west and 48 m further south than in RAP’s forecast. How much of that comes from the ground’s wind and how much from the winds aloft is not separated. The calm flight’s 5.1 m west is Earth’s rotation: a climbing rocket is pushed west (the Coriolis effect), as on the Launch-day weather page. Which model is nearer the air Calisto would have met is not measured.
A whole GFS file
Instead of asking NOMADS for a cut, you can download a whole GFS file and read it offline, for example before driving out to a launch with no signal. NOMADS keeps the most recent runs, and NOAA’s open data bucket on Amazon’s cloud older ones. A file holds one run’s forecast for one hour over the whole Earth, about 550 MB:
https://nomads.ncep.noaa.gov/pub/data/nccf/com/gfs/prod/gfs.20260930/00/atmos/gfs.t00z.pgrb2.0p25.f018
https://noaa-gfs-bdp-pds.s3.amazonaws.com/gfs.20260930/00/atmos/gfs.t00z.pgrb2.0p25.f018
Here 20260930/00 and t00z are the run (00 UTC on 30 September 2026) and f018 the forecast
hour. Only the 0.25° files (pgrb2.0p25) read: the smaller 0.5° and 1° files are refused, since
hpr checks the file is on GFS’s 0.25° grid. The command line reads the file
as it reads a saved cut, with the site you want:
hpr weather gfs --latitude 32.99 --longitude -106.97 --cycle 2026-09-30T00Z --hour 18 --from gfs.t00z.pgrb2.0p25.f018 --output profile.json. It took about a third of a second on a Mac.
NCEP packs whole files more tightly than the grib filter’s cuts, with GRIB2’s complex packing: the values are split into groups, each with its own smallest value and number of bits, and most fields store the differences between neighbouring values rather than the values (spatial differencing). GRIB2 numbers these packings as templates 5.2 and 5.3; hpr’s decoder reads both. A whole file also holds 743 messages where a cut holds 147:
- totals and averages over a time span, such as the rain that fell in the last six hours, which hpr reads but the profile doesn’t use;
- values for a layer between two heights, such as the humidity from the ground to mid-air, which hpr skips because they don’t belong to one level;
- every pressure level up to 0.01 hPa, so the profile goes on above 10 hPa, where a cut stops. In the profile hpr writes from this file, the 0.01 hPa level is 79.2 km above sea level. The highest levels are near the top of the model, and nothing here checks how good its forecast is there.
The grid runs all the way round the Earth, so hpr joins its last column to its first: a site
between 359.75° and 0° of longitude still has four grid points around it (checked by
a_grid_round_the_earth_wraps_its_columns in crates/hpr-net/tests/nomads.rs).
For that run and hour at Spaceport America, the whole file’s profile has the cut’s 22 levels, within 1.04e-7 of each value, relatively, and 13 more above them. Why they differ at all: NOMADS packs its cuts again in fewer bits. Read with ecCodes, without hpr, the two files already differ by up to 2.4e-6, relatively, at the cut’s grid points.
Files in JPEG 2000
Some of NCEP’s files compress each field as a picture: the packed whole numbers become a
greyscale image, stored in the JPEG 2000 image format (GRIB2’s template 5.40). RAP’s whole
pressure-level files are packed this way. hpr decodes them with
hayro-jpeg2000, a JPEG 2000 decoder written in Rust,
then turns each whole number into a value as it does for the other packings.
A lossless image gives back exactly the numbers packed into it; a lossy one gives back approximations. hpr reads lossless images of up to 21 bits a value, coded the way NCEP codes them; the four RAP fields below have 6 to 16 bits. Anything else is refused, naming what it is: a lossy image, one with more bits, or one coded some other way. The limit of 21 bits comes from the decoder’s arithmetic: it works in 32-bit floats, which keep whole numbers exact only up to 16,777,216, and its intermediate sums run about 8 times larger than the values. A field with a bitmap is stored as one row of its values, and the decoder takes at most 60,000 in a row, so such a field of more values is refused too.
A picture has to be decoded whole, so reading one point decodes the whole field; read all the
values you need in one go
(Field::values_at), as hpr weather does.
A whole RAP file through hpr weather rap --from has not been tried: no whole RAP file was
downloaded, only four of its fields (How it is checked).
How it is checked
The tests in crates/hpr-net/tests/nomads.rs
use the two files the example reads, recorded unchanged from NOMADS’ grib filter on 30 September
2026 for Spaceport America (32.99° N, 106.97° W): GFS’s 00 UTC run at hour 18 (27,586 bytes) and
RAP’s 12 UTC run at hour 6 (40,026 bytes), both for 18 UTC. No test uses the network. The expected
values come from ecCodes 2.49.0, the European Centre
for Medium-Range Weather Forecasts’ decoder, run on the same files by
validation/oracles/grib2/eccodes_dump.py
and committed as crates/hpr-net/tests/fixtures/nomads-eccodes.json. The tests read that file
directly, not through the decoder under test.
Each line gives what was measured, then the bound the test holds it to.
All the measurements below were made on macOS; CI runs the same tests with the same bounds on Linux and Windows.
-
Every value. Each of the 147 GFS messages (9 grid points each) and 192 RAP messages (25 grid points each) names the same variable, level and times as ecCodes, and all 6,123 values are within 2.2e-16 of ecCodes’, relatively (bound 2.5e-16). That is one rounding: ecCodes multiplies by an inexact
10^−Dwhere hpr divides by an exact10^D. It uses only basic arithmetic, which is the same on every platform. -
Every grid point’s position is within 5.7e-14° of ecCodes’ (bound 1e-12°, since other platforms’ maths libraries can differ in the last digits). The four points’ weights add up to 1, and the blend of their positions is the site, to 1e-5°.
-
RAP’s wind turn is
sin 25° × (λ − 265°), −5.06° at the site. ecCodes’ own positions of two neighbouring grid points show the map’s rows bearing 90° + θ from north, to 2.2e-6° (bound 3e-6°), which pins the sign. GFS’s winds are not turned. -
The profile. The sounding is sampled at the ground and every kept level’s height. It gives back ecCodes’ values interpolated to the site: the pressure within a relative 1e-13, the temperature within 1e-11 K and the wind within 1e-11 m/s, and each level holds the humidity within 1e-14. The six underground levels of each model are the ones left out.
-
The heights are geopotential meters. From 500 hPa up, each layer’s thickness matches what the hypsometric equation gives it when the file’s heights are read as geopotential meters, on average to −0.002% (GFS, 15 layers to 10 hPa) and −0.08% (RAP, 16 layers to 100 hPa). Read as meters above sea level, the layers would be 0.63% and 0.51% too thin. The test holds each average to those figures as rounded.
-
The two models read alike for the same hour: their 2 m temperatures within 3 K, their 500 hPa heights within 30 m and their 500 hPa winds within 5 m/s. That is a guard against a misread file, not a check of either forecast.
-
The cache. The request’s address is the one recorded, so a replayed answer fills the cache. A second request is answered from the cache, and offline mode answers without the network, 40 days later, marked stale.
-
Wrong answers aren’t saved. A RAP request answered with a GFS cut, and a cut of another run or hour than the one asked for, are refused and not saved. A GFS cut whose steps are changed to 0.5° still reads, but is not GFS’s grid, so it would be refused too.
-
Humidity. A humidity over 100% is kept in the level and taken as 100% in the sounding (a unit test).
-
Missing and hostile data. A level whose field has no value at one of the four points around the site is left out as missing data; a cut of more than 1,000 fields is refused before it is read (a real one has 147 or 192).
-
A whole GFS file (the 00 UTC run of 30 September 2026 at hour 18, 550 MB, not committed):
check where it runs measured bound every one of its 746,770,303 values against ecCodes’, relatively a script, run once outside CI 4.4e-16 none its 24,642,017 grid points without a value are ecCodes’ the same script all the same all eight whole messages cut from it, one of each kind: which field each is, its points without a value, and every 997th value CI 4.4e-16 or less 4.5e-16 the same eight messages: two sums over all their values CI within the bound 1.35e-15 of the terms’ sizes hpr weatherwrites the cut’s profile from it, relativelya test skipped in CI, which doesn’t have the file 1.04e-7 1.1e-7 The script is
validation/oracles/grib2/whole_file.py; its record, with ecCodes’ survey of the file, isgfs-whole-file.json. The sums catch a value decoded wrong anywhere in a message: off by a single packing step, a value would move a sum at least 23,000 times more than the bound allows. -
Complex packing in CI. The recorded GFS cut, packed again by ecCodes with complex packing and both orders of differencing (
repack.py), goes through every check above: its 1,323 values match ecCodes’ to one rounding (bound 2.5e-16), and its profile matches at every level.hpr weatherwrites its profile, which is the cut’s within 6.9e-8, relatively (bound 7e-8). -
JPEG 2000 in CI. Four fields cut unchanged from RAP’s run of 00 UTC, 30 September 2026: 500 hPa temperature on RAP’s 13 km grid (151,987 points) and on a coarser one (10,152), and the heights of cloud base and cloud top, which have a value only where there is cloud (6,824 and 434 points of 10,152). Their 182,443 points match ecCodes’ reading: which points have no value, the plain sum of every value and a sum weighted by each point’s position (which catches a value in the wrong place), and every 13th value alone (
rap_messages_in_jpeg2000_decode_to_eccodes_valuesincrates/hpr-io/tests/grib2_gfs.rs; ecCodes’ reading is written byvalidation/oracles/grib2/whole_file.py cut-rap, ADR-124). A damaged image is refused, never a crash:damaged_jpeg2000_never_panicsincrates/hpr-io/src/grib2/tests.rschanges bytes of one at random, 256 times in CI, and another test cuts it short at every length. -
Refusals. A site outside the file’s grid, text that isn’t GRIB2, a cycle the model doesn’t run and a forecast hour its run doesn’t have are refused. The decoder’s own tests build small files by hand to pin the unpacking formula, bitmaps (masks of grid points with no value), both kinds of grid, complex packing’s groups, missing values and both orders of differencing, and refusals of every other kind of file by name.
What it leaves out
- Other JPEG 2000. A lossy image, one of more than 21 bits a value, one coded otherwise than NCEP codes, or a bitmap field of more than 60,000 values is refused by name (Files in JPEG 2000).
- Whole RAP files. Their packing is read, but a whole file has not been run through
hpr weather. - Coarser GFS files. GFS’s 0.5° and 1° files are refused: hpr checks for the 0.25° grid.
- Forecast accuracy. Nothing here checks a forecast against the weather that came.
- Time. One forecast hour per request, with no interpolation between hours: you pick the run and the hour closest to your launch.
- Above the top level. RAP stops at 100 hPa, about 16 km, and GFS as asked for here at 10 hPa, about 31 km (a whole GFS file goes on to 0.01 hPa, about 79 km, and the layer thickness check above stops at 10 hPa). Above that the sounding continues as the standard atmosphere, shifted to pass through the top level’s temperature and pressure, with the top level’s share of water vapour (capped where the air saturates). The wind holds the top level’s wind. Both are marked as extrapolated. A flight above 16 km in RAP’s forecast is flying on that guess.
- Underground grid points. A kept level is blended from the four grid points even where one of them has that level underground. In the RAP cut, 850 hPa takes 13% of its weight from a point whose ground is at 845.8 hPa; the effect here is about 0.02 K, and larger in steep terrain.
- The ground. The pad is placed on the model’s smoothed terrain, not the site’s real height; hpr doesn’t shift the profile to the real ground.
- Humidity over ice. Whether NCEP gives humidity over ice at cold levels is unsettled here; it moves the density by under 0.1%.
- The live connection to NOMADS is not tested in CI, and nothing tells you which runs NOMADS still holds before you ask.
- The command line fetches them with
hpr weather gfsandrap, buthpr simdoesn’t fly them yet (issue #265), and the Python package doesn’t fetch NOAA’s forecasts.
A launch site’s elevation
This page covers two ways to get a launch site’s elevation, its altitude above sea level (the
field elevation a club lists). The first looks it up from Open-Meteo, a
free weather and terrain service; the answer is saved, so the same lookup works later with no
network, on a field with no signal. The second reads it from an elevation file you have, a
GeoTIFF (below). It is for anyone who needs a site’s height
above sea level to start a flight at the right air pressure and density. There is no hpr
command for either yet: a Rust program calls the library.
How far to trust it, online. hpr gives back Open-Meteo’s number unchanged, and the saved copy gives it back offline. That is checked on two recorded answers, below. The number comes from a terrain model whose cells are about 90 m across, and it is a surface height: over trees or buildings it sits above the bare ground. The model’s makers state its accuracy as better than 4 m for 90% of points, averaged over the world outside Antarctica and Greenland; in about 1 area in 90 it is worse than 10 m. hpr hasn’t measured it. In the standard atmosphere, 10 m of height error changes the air’s density by about 0.1%: the example below shows it 12.8% thinner over 1,400 m. The recorded heights are whole meters; Open-Meteo doesn’t document its rounding.
The tests replay two saved answers and never contact Open-Meteo. The live service was contacted by hand, over an encrypted (HTTPS) connection, to record them. So a change in Open-Meteo’s answers would show only when a program runs, as a refused answer.
How far to trust it, from a file. At a given place, hpr reads the same stored value as GDAL, the library most mapping programs read terrain with. That is checked at 2,800 places in seven small files, in CI, and at 2,000 places in a whole tile from the US Geological Survey (USGS), on machines that have downloaded it, not in CI (how). Only files on a latitude and longitude grid are read. How accurate the height itself is depends on who made the file; hpr gives back what is stored.
Code:
- Online:
hpr_net::elevation(API reference), written for M5.3b, the second launch-site increment. It needs thenetfeature of thehprcrate. The choices are in ADR-126: Open-Meteo’s elevation through the cache. - From a file:
hpr_io::geotiff(API reference), which thehprcrate re-exports ashpr::hpr_io::geotiff, with no feature needed. It was written for M5.3c2, with its choices in ADR-128: a site’s height from a user’s GeoTIFF.
The weather over a site is on Launch-day weather, and the compass’s offset from true north on The magnetic field and declination.
What hpr asks for
Open-Meteo’s elevation service (its documentation) takes a list of places, each a latitude and longitude in degrees, and answers one height for each, in the same order. hpr asks for one place or up to 100 in one request, the service’s limit. This is a request recorded for the tests, for Spaceport America, the Dead Sea and a point in the open Atlantic:
| what | value |
|---|---|
| address | https://api.open-meteo.com/v1/elevation |
| the request’s places | ?latitude=32.99,31.5,0&longitude=-106.97,35.5,-30 |
| its answer | {"elevation":[1400.0, -427.0, 0.0]} |
| a saved answer stays fresh for | a year |
A request can name another server instead (ElevationRequest::endpoint), for a copy of
Open-Meteo you run yourself.
hpr writes each coordinate to 5 decimal places, about 1 m on the ground. So a place given to 8 decimals or fewer, which your program keeps in radians and turns back into degrees (that can change its last digits), still finds its saved answer.
A saved answer is found again only by the same request: the same places, in the same order. If you look up three club fields in one request at home, a later lookup of one of them alone is a new request, and offline it fails. To use a site offline, look it up alone, or repeat the exact list you used before.
The terrain model
The heights come from the Copernicus DEM GLO-90, a digital elevation model (DEM): a grid of heights over the whole Earth, made by the European Union’s Copernicus programme from radar satellites. Its points are 3 seconds of arc apart north to south, about 93 m. East to west they are 93 m apart at the equator and 60 m at 50° of latitude; further north and south the spacing widens in steps (product handbook, issue 5.0, Table 3, page 15). It is a digital surface model: its heights include buildings and vegetation (the dataset’s readme).
Its stated accuracy is under 4 m for 90% of points (handbook, Table 1, page 10). That is a mean over the world outside Antarctica and Greenland, and the makers warn that it varies from place to place: of the 16,363 tiles there, each about a degree across, 184 (1.1%) are worse than 10 m (Table 12, page 31, which prints 0.9%, a share of all tiles, Antarctica and Greenland included).
The ground doesn’t move, so a saved answer stays fresh for a year. Open-Meteo’s forecast for Spaceport America (Launch-day weather) gives the same ground height there, 1,400 m.
What hpr refuses
hpr refuses an answer with another number of heights than places asked for, a height that is not a number, or a height outside −1,000 m to 9,000 m. The lowest land, by the Dead Sea, lies a little over 400 m below sea level, and the highest, Everest’s summit, 8,849 m above it, so a height outside is a broken answer, such as the value −32,768 that some terrain files use for “no data”. An answer hpr refuses is never saved, as on Online data and the cache.
When Open-Meteo itself refuses a request (it answers with HTTP status 400 and a reason), the HTTP transport reports a failed fetch naming the status, without Open-Meteo’s reason. hpr checks each place’s latitude and longitude before asking, so that is rare.
What the height means
The height is above mean sea level. The model’s heights are measured from EGM2008, a worldwide model of the geoid: the shape the sea’s surface would take if it were still, extended under the land (product handbook, section 1.2.1, page 13). That is the height above sea level the atmosphere is looked up by (Atmosphere: the height datum). The sea has no tiles in the model and reads 0 m, so 0 m can also mean “no data”. The Dead Sea reads −427 m, its surface when the radar satellites measured it, between December 2010 and January 2015 (handbook, page 30); the lake has fallen since.
A flight’s launch site takes its height above the WGS 84 ellipsoid instead
(its ellipsoidal height). The two differ by the geoid
undulation N, the height of sea level above the ellipsoid there: between about −107 m and
+86 m around the world. hpr has no model of N; a geoid calculator gives it for a place, such as
GeographicLib’s GeoidEval, which gives
EGM2008’s −23.85 m at Spaceport America (32.99° N, 106.97° W). Then:
- If you know
N, place the site withGeodetic::from_degrees(latitude, longitude, H + N), whereHis the height from this page, and give the flight’s environmentNwithEnvironment::standard(site)?.with_geoid_undulation_m(N). At Spaceport America,H= 1,400 m andN= −23.85 m, so the site’s ellipsoidal height is 1,376.15 m. - If you don’t, place the site at
Hand leaveNat 0, as the example below does. The atmosphere still sees the right height above sea level, so the air, the drag and the flight are right. The site’s place in space is off byN, which moves gravity by about 3×10⁻⁴ m/s² per 100 m, 0.003% (normal gravity’s height term). A flight exported as KML keeps its heights above sea level, which stay right; the GeoJSON export’s heights are above the ellipsoid, and are off byN.
An example
crates/hpr/examples/site_elevation.rs
looks up the three places above in one request. It makes these calls:
Client::new(transport, Cache::new(folder), Mode::Online)sets up the fetching. The transport is what fetches (Online data and the cache). A real program passeshpr_net::Http::new(), and keeps its saved answers inCache::platform_dir(), the system’s usual folder for them. This one never uses the network: a stand-in transport answers with the answer recorded for the tests.ElevationRequest::new(places)says what to ask for, andelevation::fetch(&client, &request, now)fetches the heights and saves them;nowis the time, in seconds since 1 January 1970. Each comes back with its place, asheight_msl_m, the height above mean sea level.- The same
elevation::fetcha day later, through a client inMode::Offlinewhose transport has no network, answers from the saved copy. Ussa76::standard().air(height)gives the standard atmosphere (the 1976 US Standard Atmosphere) at each height, andEnvironment::standard(site)puts a flight’s start on the first place, Spaceport America.
Run it from a copy of the repository with
cargo run --example site_elevation -p hpr --features net. It prints:
Elevation data by Open-Meteo.com (CC BY 4.0), from the Copernicus DEM GLO-90: © DLR e.V. 2010-2014 and © Airbus Defence and Space GmbH 2014-2018 provided under COPERNICUS by the European Union and ESA; all rights reserved
first lookup: Fetched; offline a day later: Cached, the same heights: true
place lat (°) height (m) pressure (hPa) density / sea
Spaceport America, New Mexico 32.99 1400 856.02 0.872
the Dead Sea (its surface) 31.50 -427 1065.61 1.042
the open Atlantic 0.00 0 1013.25 1.000
A flight from Spaceport America, New Mexico starts 1400 m above sea level, at 856.02 hPa.
The first line is the credit that Open-Meteo’s license and the terrain model’s license ask for;
show it wherever the height is shown. The column density / sea is the air’s density as a
fraction of sea level’s. Fetched and Cached say where an answer came from
(how long a copy stays fresh). At Spaceport
America’s 1,400 m the standard air is about 13% thinner than at sea level, so at the same speed
and drag coefficient the drag there is about 13% lower.
How it is checked
The tests in crates/hpr-net/tests/elevation.rs
replay two answers recorded from the service on 1 October 2026 (UTC), one for Spaceport America
alone and one for the three places above. They read the expected heights from the recordings
themselves, not through the code being tested, and check that:
- a lookup gives each recorded height exactly, in order, each with the place asked for, and with the credit line;
- a second lookup online, inside the year, is answered from the saved copy without a fetch;
- offline, with a transport that fails the test if it is ever called, the same lookup gives the same heights, still fresh an hour later and marked stale a year later;
- offline, a place never looked up is an error naming its address;
- an answer with no heights is refused and not saved; a later good answer is saved, and when an answer that isn’t JSON arrives after the year, the good copy comes back, marked stale with the reason;
- a saved copy hpr would refuse (a −32,768 m height) is an error offline, naming the height, and online is fetched again and replaced;
- a place turned into radians and back, which changes its last digits, finds its saved answer offline.
The code’s own tests check the address hpr builds, for one place and three, and that it is the same after a trip through radians for every longitude in steps of 0.01°, and for 720,000 values given to 6 or 8 decimals, the 6-decimal ones all sitting on a rounding half step. They check that it refuses no places, 101 places, a latitude or longitude out of range or not a number (naming the place, counting from 0), and a bad server address. They check that the reader takes negative heights and the range’s edges, and refuses each kind of broken answer above.
What it leaves out
- No command. The command line has no elevation command yet, and
hpr simdoesn’t look a site up; a program calls the library and passes the height on. - No geoid model. The height is above sea level, and turning it into a height above the
ellipsoid needs the undulation
N, which you give (above). - No accuracy check. Nothing compares the model’s heights with surveyed ones; the 4 m is the makers’ statement.
- A surface, not the ground. Over a tree line or buildings the height can sit meters above the pad; for a pad cut out of forest, use a surveyed height.
- One number per place. The answer is the model’s height there, at about 90 m spacing; a pad on a hill or beside a cliff can sit meters off it.
- Your own terrain file is read by a separate reader, below.
From an elevation file of your own
A DEM (digital elevation model) is a grid of ground heights. Most are published as GeoTIFF files: an image whose pixels are heights, with tags saying where on Earth each pixel lies (the OGC GeoTIFF Standard 1.1). The United States Geological Survey (USGS) publishes the United States this way, free and in the public domain, through its 3D Elevation Program:
- Its 1-arc-second tiles (about 30 m) are what hpr is tested on. Its 1/3- and 1/9-arc-second tiles use the same latitude and longitude grid, but none has been tried.
- Its 1 m files are on a map projection (UTM) and are refused; GDAL’s free
gdalwarptool converts them first, as the table below says. - Copernicus and NASA publish the world as GeoTIFFs too; no file of theirs has been tried.
Reading your own file
- Bring the reader in:
use hpr::hpr_io::geotiff::ElevationRaster;. - Read the whole file into memory:
let bytes = std::fs::read(path)?;. ElevationRaster::parse(&bytes)reads its tags.info()then holds what they say: the size, the corner and pixel size in degrees, the CRS, the sample type, the scale and offset, the unit and whether the file states it (vertical_unit_stated), the vertical datum’s code (vertical_crs_epsg) and the nodata value.height_at(latitude_deg, longitude_deg), in degrees, givesResult<Option<f64>, GeoTiffError>:Ok(Some(height))in meters,Ok(None)where the file has no data, or an error from the table below.- For many places,
values_at(&[(lat, lon), …])reads them in one pass. It gives aVecwith oneResult<Option<f64>, GeoTiffError>per place, in order. Each is the raw stored value, not meters:info().meters(value)applies the scale, offset and unit.
The API reference shows the same steps as a short program that CI compiles and runs. The height is the value of the pixel the place falls in, as GDAL gives it. There is no smoothing between pixels, so on a 1-arc-second grid it is the ground within about 20 m of the site. A lookup decodes only the tile or strip of the file that holds the place, so it adds about one tile’s memory to the file’s.
| error | from | means |
|---|---|---|
Outside | a lookup | the place is off the file; the message names the file’s edges |
Location | a lookup | the latitude is past ±90° or a number is not finite |
Unsupported | parse | the file uses something hpr doesn’t read (the table below); the message names it and, where there is one, the GDAL command that converts it |
Missing, Malformed | parse; rarely a lookup | the file lacks a tag a GeoTIFF needs, or holds one the standard doesn’t allow |
TooLarge | parse, values | a tile is larger than 256 MiB decoded; for values, which reads every pixel, the whole raster is past 2²⁸ pixels |
Tiff | any | the image itself can’t be decoded |
What it reads
You rarely need this table: a USGS 1-arc-second tile reads as it comes. It is for checking an odd file.
| what | read | refused |
|---|---|---|
| the grid | latitude and longitude in degrees from Greenwich, on a datum within a few meters of WGS 84: WGS 84 itself, NAD83 and its updates, ETRS89, GDA94, GDA2020, NZGD2000, JGD2011, SIRGAS 2000 and CGCS2000 (the EPSG codes are NEAR_WGS84 in the API) | a map projection such as UTM, or an older datum such as NAD27, tens to hundreds of meters from WGS 84: the error names the code, and gdalwarp -t_srs EPSG:4326 in.tif out.tif converts the file |
| the pixels | one band of 8-, 16- or 32-bit integers, or 32- or 64-bit floats; tiles or strips; uncompressed, LZW, Deflate or PackBits; either byte order; BigTIFF | several bands; zstd, JPEG and other compression (gdal_translate -co COMPRESS=DEFLATE rewrites the file); a separate mask of missing pixels |
| where the pixels lie | one tiepoint (a pixel tied to a longitude and latitude) with a pixel size, or a matrix without rotation; pixel is area or pixel is point (whether the tiepoint names a pixel’s corner or its center); longitudes 0° to 360° as well as −180° to 180° | a rotated grid; several tiepoints (ground control points); a south-up pixel size, which GDAL and the standard read differently |
| the heights | a scale and offset as GDAL applies them (from the file’s pixel scale, or from GDAL’s own metadata tag); meters, feet or US survey feet as the file states (in its keys or in GDAL’s metadata tag), or as its vertical datum implies; meters if it says nothing, with vertical_unit_stated then false | a vertical datum outside a short list hpr knows (VERTICAL_CRS_UNITS in the API: EGM2008, EGM96, NAVD88 and a few more), since its unit could be feet; a height scale in the file’s pixel-scale tag when the file names a vertical datum in a way GDAL may not apply (hpr can’t tell what GDAL would do); vertical keys GDAL ignores, unit and all, or reads by rules of its own (a private code, keys beside WGS 84 3D, datum 6030 beside WGS 84 with a model type, or keys with no model type and no unit); a scale, offset or unit in GDAL’s metadata tag with a namespace, capitals in an attribute’s name, a sample that isn’t plain digits, a value that isn’t plain text, a blank written as a character reference, or the IMAGE_STRUCTURE domain, forms GDAL reads with quirks of its own; a unit other than these, or two that disagree |
| no data | the file’s nodata value, and NaN, read as None |
Which height it is. The height is above the file’s own vertical datum. The reader reports the
datum’s EPSG code (vertical_crs_epsg) when the file names one, and doesn’t change the height.
Check two things in info(), the first one first:
vertical_unit_statedisfalse: the file states no unit, and hpr took meters. A file in feet read as meters gives heights 3.28 times too high.vertical_crs_epsgisNone: the file names no datum. The USGS tile in the example below names none; the USGS’s own description of its data says its heights are above NAVD88, the North American vertical datum. Check your publisher’s description.
EGM2008 is a model of the geoid. NAVD88 was defined by levelling, and
the US National Geodetic Survey puts it about half a meter off the best geoid models, tilted by
about a meter from coast to coast (NGS, new datums): a meter or two at most. hpr takes a height above either as the H of
What the height means: the height above sea level the air is looked up
by. A meter’s difference changes the air’s density by about 0.01%. That section also shows how to
place a flight’s site from H and the geoid undulation N.
An example with a file
crates/hpr/examples/site_geotiff.rs
reads two small files cut from the USGS tile around Spaceport America, both with some pixels
blanked out as nodata: one in meters, one in whole US survey feet. It makes these calls:
ElevationRaster::parse(bytes)reads the tags;info()holds what they say.height_at(lat, lon)gives the height at three places: the runway, a place in a blanked-out hole (None) and Albuquerque, off the file (an error naming the file’s edges).value_atgives the feet file’s raw value, andheight_atthe same in meters.Ussa76::standard().air(height)gives the standard atmosphere at the runway’s height.
Run it from a copy of the repository with cargo run --example site_geotiff -p hpr. It prints:
70 by 50 pixels of 1.000" by 1.000", PixelIsArea, CRS EPSG:4269, values F32 in Meter (stated: false)
latitudes 32.98333 to 32.99722, longitudes -106.98500 to -106.96556
place lat (°) lon (°) height
Spaceport America, New Mexico 32.9900 -106.9700 1400.691 m
a hole in the data 32.9960 -106.9832 no data
Albuquerque, New Mexico 35.0844 -106.6504 refused: latitude 35.0844° and longitude -106.6504° are outside the raster, which spans latitudes 32.983333° to 32.997222° and longitudes -106.985000° to -106.965556°
in feet: 4595 UsSurveyFoot (vertical CRS EPSG:6360) = 1400.559 m, -0.132 m from the meters file
at 1400.7 m: 855.95 hPa, density 0.872 of sea level's
1.000"is one arc-second,PixelIsAreasays the tiepoint is a pixel’s corner, andEPSG:4269is NAD83.F32is 32-bit floating point, andMeter (stated: false)says the file states no unit, so hpr took meters.- In the feet file,
EPSG:6360is NAVD88 in US survey feet, which the file states.
The runway reads 1,400.691 m. Open-Meteo’s answer for the same place, above, is 1,400 m, from a different terrain model whose cells are 90 m across, above EGM2008. The feet file stores 4,595 US survey feet, which is 1,400.559 m, 0.132 m from the meters file: rounding to whole feet moves a height by up to 0.152 m.
How the file reader is checked
The oracle, the outside program hpr is held to, is GDAL 3.12.2, run through rasterio 1.5.2, its Python interface. A script,
validation/oracles/geotiff/dem.py,
cut 70 by 50 pixels around Spaceport America from the USGS’s 1-arc-second tile n33w107 and wrote
them in seven encodings:
| file | pixels | layout | georeferencing and heights |
|---|---|---|---|
usgs-f32-lzw-fp-tiles.tif | 32-bit floats, LZW, floating-point predictor | 16-pixel tiles | NAD83, pixel is area, nodata holes |
usgs-i16-deflate-strips-be.tif | 16-bit integers, Deflate, horizontal predictor | 7-row strips, big-endian | WGS 84 |
usgs-f64-raw-bigtiff-point.tif | 64-bit floats, uncompressed | BigTIFF | WGS 84, pixel is point |
usgs-u16-packbits-ftus-nodata.tif | 16-bit unsigned, PackBits | strips | NAD83 with NAVD88 in US survey feet, nodata holes |
usgs-u32-cm-scaled-egm2008.tif | 32-bit unsigned, Deflate | strips | WGS 84 with EGM2008; centimeters above 1,000 m by the pixel scale |
usgs-u8-metadata-scaled.tif | 8-bit unsigned, LZW | strips | WGS 84; quarter feet above 4,585 ft, scale, offset and unit all by GDAL’s metadata |
usgs-i32-lzw-tiles-lon360.tif | 32-bit integers, LZW, horizontal predictor | 32-pixel tiles | WGS 84, longitudes past 180° |
The script recorded what rasterio reads from each, and from the whole 3,612 by 3,612 tile:
- the corner, the pixel size, the CRS, the vertical datum and unit, the scale and offset, and the nodata value;
- the exactly rounded sum of every value, and of each value times its index (row × width + column), so a swapped pair of pixels would show;
- at 400 places in each small file and 2,000 in the tile (some off the edge), which pixel rasterio puts the place in, and its value.
The tests in
crates/hpr-io/tests/geotiff_rasterio.rs
hold hpr to all of it exactly:
| check | result |
|---|---|
| corner and pixel size | the same to the last bit, in all eight files |
| the two sums over every pixel | exact, 13 million pixels in the tile |
| places in the seven files | 2,800: the same pixel and value at all 2,368 on a file (9 of them nodata), and “outside” at the other 432 |
| places in the tile | 2,000: the same pixel and value at all 1,696 on it, and “outside” at the other 304 |
| unit | GDAL’s wherever GDAL reports one, and vertical_unit_stated false wherever it reports none |
| heights | at every 97th place, hpr’s height is GDAL’s scale and offset applied to the value, in the file’s unit |
CI checks the seven small files; the whole tile (45 MB, fetched by cargo xtask refs fetch) is
checked where it has been downloaded. The code’s own tests build files to check each refusal in
the table above, the half-pixel move of pixel is point, the units, the scale and offset, and
nodata. Property tests check, on random strip and tile layouts, that every place reads the pixel it
is placed in, and that a file with bytes changed is read or refused without a crash.
The places are random, so none sits exactly on a pixel’s edge. Rounding can put a place on either side of an edge, in hpr and in GDAL, if it is closer to the edge than about 10⁻¹³ of a pixel’s width. A coordinate typed to a few decimals never comes that close.
What the file reader leaves out
- No map projections. A UTM file, or one on an older datum, needs
gdalwarpfirst. - No smoothing. A place reads its pixel’s value; on a slope the next pixel can be meters higher.
- No datum changes. A datum within a few meters of WGS 84 is taken as WGS 84, and the vertical datum is reported, not converted. Near the rupture of a large earthquake since a datum was fixed (Chile’s in 2010 for SIRGAS 2000, for example) the ground has moved further, by several meters.
- The file’s accuracy is the file’s. The reader gives back what is stored; the USGS and other publishers state their own accuracy.
- No command. As for the online lookup, a program calls the library.
Motor stock and prices
This page covers where hpr gets motor stock and prices: motor.fusionspace.co
(the motor finder), a free site that reads a dozen U.S. vendors’ public listings every hour and publishes, for every
AeroTech, Cesaroni and Loki motor of impulse class D and up that they
carry, who has it in stock and at what price. hpr reads the site’s public data API (its
machine-readable files) and saves each answer, so the same list works later with no network. It
is for anyone choosing a motor they can actually buy: “which L motors are in stock, and what does
one cost?” hpr then matches each motor in stock to its record on
ThrustCurve.org, the public database of motor data, and can download
that record’s thrust curve, so a motor you can buy is a motor you can
fly (A motor from a file). At the command line,
hpr motors search --in-stock --class L --max-price 150 lists the motors in stock by class and
price (Motors you can buy); a Rust program calls the library, as
below.
How far to trust it. hpr gives back the site’s values unchanged, and the saved copy gives them back offline. That is checked on recorded answers, below: eight from motor.fusionspace.co and two curve files from ThrustCurve.org, beside three stand-in searches in ThrustCurve.org’s shape, since ThrustCurve.org grants no license for its motor records (its site reads “All rights under copyright reserved”). Whether a vendor really has a motor, at that price, is the vendor’s to say. The site’s data is up to about an hour old when it is built, and hpr counts its saved copy as fresh for another hour, so a fresh answer can be two hours behind the vendor’s page; a stale copy is as old as its date says. (ThrustCurve.org’s answers count as fresh for a day.) The site’s terms ask you to check stock and price on the vendor’s own page before relying on them. Prices are in U.S. dollars.
The match is by name. On ThrustCurve.org’s answers of 1 October 2026, all 282 motors in stock matched exactly one ThrustCurve.org record (below), and each matched record’s size, impulse, average thrust and burn time equal the motor finder’s. A curve is the file someone uploaded to ThrustCurve.org, read by the same readers as a motor file on your disk.
The tests replay saved answers, and CI never contacts either live site. Both were contacted by
hand, over an encrypted (HTTPS) connection, to record them. If a site changes its format, you will
find out when your program, or hpr motors search, gets a refused answer, not from a failing test.
Code: hpr_net::motor_finder (API reference), written for
M5.4a, the first motor-stock increment, and
hpr_net::thrustcurve (API reference), written for
M5.4b, the second. They need the net feature of the hpr
crate: hpr = { ..., features = ["net"] } in Cargo.toml, then hpr::hpr_net::motor_finder.
The choices are in
ADR-129: motor stock through the cache
and
ADR-130: ThrustCurve.org and the match.
hpr motors search was added by M5.4c, the third; its choices are in
ADR-131: the command.
How saved answers work is on Online data and the cache. The motors hpr carries
with it, with their thrust curves, are on Solid motors.
What the site publishes
The site documents its API, and its terms, in its own repository. The API is a handful of files, rebuilt about every hour. There is no key, no limit on how often you ask, and no way to ask for part of a file: a program fetches a whole file and picks what it needs.
| File | What it holds | On 2026-10-01 |
|---|---|---|
meta.json | when the files were built, and how many motors, motors in stock and vendors they hold | 760 bytes |
motors.json | every motor some vendor lists, D class and up | 598 motors, 1.6 MB |
in-stock.json | the same, only those in stock at one vendor or more | 282 motors, 0.96 MB |
vendors.json | the vendors read, with how many motors each lists and has in stock | 12 vendors, 1.1 kB |
motors/{maker}/{motor}.json | one motor, as in the lists | 1.4 to 8.1 kB for the four recorded |
Each motor carries its maker; its designation (L1150R,
3683L851-P), spelled as ThrustCurve.org spells it; its impulse
class, diameter, total impulse,
average thrust and burn time, taken from
ThrustCurve.org; its propellant; for a reload, the reusable case it needs (RMS-29/180); its
delays; whether it is discontinued (old stock only: two motors in
stock were, on the recorded morning); whether it ships as hazardous material, as the site labels it
from the propellant’s weight; and every vendor’s listing of it.
A listing is one product page: its status, its sticker price, its pack size, the price of one motor, units on hand when the vendor shows them (267 of the 3,685 listings did), and when the site last read it. The status is in stock, out of stock, special order (made or ordered for you, often with a lead time of weeks; not counted as in stock), or unknown. A vendor may list one motor several times, for different delays or pack sizes. Each motor in stock also names its cheapest offer, by the price of one motor.
Prices are whole cents. The price of one motor is the sticker price over the pack size, to the nearest cent: a $38.49 two-pack is $19.25 a motor (the 263 of the 3,685 listings that fall on a half cent all round up).
What hpr refuses
An answer is refused, and not saved, when:
- it isn’t JSON, or a field is missing or of the wrong type;
- its schema version (the number the API puts on its format) isn’t 1: the API puts a breaking change under a new address;
- its build time isn’t a UTC time;
- a list’s count disagrees with the list;
- a motor’s impulse class isn’t one capital letter, its diameter isn’t above zero, or its impulse, thrust or burn time is below zero;
- a motor’s listing count disagrees with its listings;
- a motor has a cheapest offer but isn’t in stock, or is in stock with none;
- a pack holds no motors, or a motor costs more than its pack;
in-stock.jsonholds a motor out of stock;- a motor’s own page holds another motor.
One broken value refuses the whole file: hpr keeps its last good copy rather than a list it can’t trust, and online it hands that copy back, marked stale, with the reason. Two things the API allows are read, not refused: a cheapest offer with no price (when no vendor with the motor in stock shows one), and a listing status the API adds later, which reads as unknown.
Asking for a motor the site doesn’t list gets the site’s “not found” page; hpr returns it as an
error naming the address, and doesn’t save it. The makers can be named in full or in short:
aerotech, cesaroni and loki, in any case.
Matching motors to ThrustCurve.org
The motor finder carries no thrust curves, and no ThrustCurve.org id. ThrustCurve.org has both: it
aims to hold a record for every certified motor, each with its own id (24 hexadecimal digits, such as
5f4294d2000231000000044f), and the simulator files people have uploaded for it. hpr asks its
public API two things:
| Request | What it gives | hpr’s call |
|---|---|---|
| a search | motor records: id, maker, designation, class, diameter, impulse, burn time, delays, how many data files | thrustcurve::fetch_search |
| a download | one motor’s data files in one format, RASP (.eng) or RockSim (.rse), each with who measured it and its license | thrustcurve::fetch_download |
The match. The finder spells each designation exactly as ThrustCurve.org does. So hpr matches a
motor in stock to the record whose maker (full name, such as Cesaroni Technology) and
designation are the same, character for character. If no record has that name, or more than one
does, the motor is a miss, and the match’s report (Join::report()) lists it with the reason.
hpr doesn’t guess: it doesn’t compare impulse or diameter, or try other spellings. A motor
ThrustCurve.org spells differently is reported as a miss, never matched by a guess. (The report
and the code call a matched motor mapped.) thrustcurve::fetch_finder_records
fetches the records the match needs, one search for each of the three makers the finder reads,
and thrustcurve::join matches them.
On ThrustCurve.org’s answers of 1 October 2026, every motor in stock matched (ADR-130: ThrustCurve.org and the match):
| Maker | Motors in stock | Matched | Missed | ThrustCurve.org records searched |
|---|---|---|---|---|
| AeroTech | 153 | 153 | 0 | 307 |
| Cesaroni Technology | 99 | 99 | 0 | 296 |
| Loki Research | 30 | 30 | 0 | 60 |
| All | 282 | 282 | 0 | 663 |
Every matched record listed at least one data file, in some format, and for every match the diameter, total impulse, average thrust and burn time were the same on both sides, so each match was the right motor by more than its name. The motor finder copies ThrustCurve.org’s names and figures, so a full match is expected; the match is there to catch drift.
ThrustCurve.org grants no license for its motor records; its site reads “All rights under copyright reserved”. So the repository no longer keeps those three answers, and the tests replay stand-ins in the same shape instead (ADR-145: the recordings’ licenses). They hold 297 records:
- 15 wholly invented, five per maker, with designations ending
-INVENTED; - 282, one for each motor in the finder’s in-stock list. Each carries only the values that list states, which are ThrustCurve.org’s published figures as the finder relays them under CC BY 4.0, plus an invented id and an invented count of data files. J450DM and F27R/L keep their real ids, those of the two public-domain downloads, so the match leads to a committed file. The fields only ThrustCurve.org states (length, weights, peak thrust, certifying body, dates, links) are left out.
On them, too, all 282 motors match. The tests write the match’s report into
validation/reports/thrustcurve-join.md,
and check it against the match on every run. So the committed tests show that the code keeps the
rule; the full match on ThrustCurve.org’s own records is the measurement of 1 October 2026 above,
which last ran at commit 5150e0d (the test was added by PR #277).
The milestone’s goal (M5.4b) was 95% of the motors in stock;
the test asserts that too.
The curve. A download’s file is read by the same readers as a motor file on your disk
(Solid motors): .eng by hpr_motor::eng, .rse by hpr_motor::rse.
DataFile::read() picks the reader by
the file’s format, and
Curve::thrust_curve() gives the
first motor’s curve. Each file says who measured it (cert, a certification test; mfr, the
maker; or user) and its license: PD for public domain, free or other, or none given.
ThrustCurve.org’s API doesn’t define free and other, and hpr doesn’t interpret them; only
PD files are recorded for the tests. An answer with no files gives an empty list, not an error;
that is how the API answered an unknown id when tried by hand on 1 October 2026.
What hpr refuses. A search or download is refused, and not saved, when:
- it carries the API’s error message, on the answer or on one of the search’s terms;
- a search returns more motors than it says match;
- an id isn’t 24 hexadecimal digits;
- a size, thrust, impulse, burn time or weight is below zero;
- a file isn’t base64 (the encoding the API sends files in);
- a download holds a file of another motor or format than asked;
- a search for the match was cut short: it says more motors match than it returned (it asks for up to 5,000), so a match never runs on part of a maker’s records;
- a search for the match holds another maker’s record.
Records may leave out any field but the id, maker and designation: the API sends only the fields that have values. A file that decodes but isn’t plain text (UTF-8) is not refused: reading that one file is an error, and the motor’s other files stay readable.
An example
crates/hpr/examples/motor_stock.rs
lists the L motors in stock, cheapest first, then matches the motors in stock to ThrustCurve.org
and reads one curve. It makes these calls:
Client::new(transport, Cache::new(folder), Mode::Online)sets up the fetching. The transport is what fetches (Online data and the cache). A real program passeshpr_net::Http::new(), and keeps its saved answers inCache::platform_dir(), the system’s usual folder for them. This one never uses the network: a stand-in transport answers with the files recorded for the tests.motor_finder::fetch_meta(&client, now)andmotor_finder::fetch_in_stock(&client, now)fetch the build time and the in-stock list, and save them;nowis the time, in seconds since 1 January 1970.- The same
fetch_in_stocktwo hours later, through a client inMode::Offlinewhose transport has no network, answers from the saved copy, marked stale: older than an hour. - It keeps the motors whose
impulse_classisL, and sorts them by theircheapest_in_stock.unit_price_cents, the price of one motor. thrustcurve::fetch_finder_records(&client, now)fetches ThrustCurve.org’s records of the three makers, andthrustcurve::join(&in_stock.motors, &records)matches the motors in stock.Download::new(id, Format::Rasp)andthrustcurve::fetch_downloadfetch AeroTech J450DM’s.engfile by its matched id, andread()?.thrust_curve()?reads it. It uses J450DM because the tests record only public-domain files, and this is one hpr already carries.
Run it from a copy of the repository with
cargo run --example motor_stock -p hpr --features net. It prints:
Motor stock data from motor.fusionspace.co, licensed CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/); aggregated from public vendor listings and ThrustCurve.org; provided as is, with no warranty: check stock and price on the vendor's own page before relying on them
built 2026-10-01T07:07:29+00:00: 598 motors listed, 282 in stock, from 12 vendors
first read: Fetched; offline two hours later: Stale, the same list: true
20 L motors in stock; the five cheapest by the price of one motor:
manufacturer motor dia (mm) impulse (N·s) one motor cheapest at vendors
AeroTech L1520T 75 3715.9 $260.99 Balsa Machining Service 1
AeroTech L850W 75 3646.2 $282.74 Sirius Rocketry 2
Cesaroni Technology 3419L645-P 75 3419.8 $286.36 Performance Hobbies 2
Cesaroni Technology 3683L851-P 75 3683.2 $290.39 Animal Motor Works 3
AeroTech L1150R 75 3517.0 $324.99 Animal Motor Works 1
Motor data and thrust curves courtesy of ThrustCurve.org, https://www.thrustcurve.org/
297 records of the three makers in stand-in searches: 15 invented, 282 with the motor finder's copy of ThrustCurve.org's figures; 282 of 282 motors in stock matched to one each, 0 missed
AeroTech J450DM: id 5f4294d2000231000000044f, a RASP file from source cert, license PD, 37 points from ignition
the file: total impulse 1061.6 N·s, burn time 2.28 s, average thrust 465.6 N
record: total impulse 1055 N·s, burn time 2.27 s, average thrust 465 N
CI checks that it still prints this (cargo xtask examples --check). The first line is the credit
the site asks for, with its caution; show it wherever a price or stock is shown. Fetched and
Stale say where an answer came from
(how long a copy stays fresh). vendors is how many
vendors had the motor in stock. On that morning no L motor in stock cost $150 or less: the
cheapest was $260.99.
The last lines come from ThrustCurve.org, under its own credit line, except the records: the 297
are the stand-in searches (above), so the count is theirs,
and the match runs on the finder’s own copy of ThrustCurve.org’s figures. J450DM’s file is a real
answer: the certification test’s curve (source cert), public domain, and the very file hpr
already carries for J450DM (Solid motors). Its curve starts at zero thrust at
ignition, then follows the file’s 36 points. hpr works its figures out from those points, so they
differ from the published figures on the record (here the motor finder’s copy of them):
| from the file | on the record | difference | |
|---|---|---|---|
| total impulse | 1,061.6 N·s | 1,055 N·s | +0.6% |
| burn time | 2.28 s | 2.27 s | +0.4% |
| average thrust | 465.6 N | 465 N | +0.1% |
The average thrust is the total impulse over the burn time.
Credit and terms
motor.fusionspace.co
The site licenses its API’s answers under Creative Commons Attribution 4.0 (CC BY 4.0), with the
credit “Motor stock data from motor.fusionspace.co”, and allows keeping recorded answers, such as
the test recordings below (its API page, “Data licence”). The
data is gathered from public vendor listings and from ThrustCurve.org, and comes as is, with no
warranty.
hpr puts its credit line, motor_finder::ATTRIBUTION, on every answer, fetched or saved; the
example above prints it first, and hpr motors search prints it at the top of every list, in
text and in JSON, with ThrustCurve.org’s below it. The site asks programs to use its files rather
than read the vendors’ pages themselves, and to keep a copy rather than fetch on every use: hpr’s saved copy
counts as fresh for an hour, as often as the site rebuilds.
ThrustCurve.org
ThrustCurve.org grants no license for its motor records; its site reads “All rights under copyright reserved”, and its API asks for no particular credit. hpr puts
thrustcurve::ATTRIBUTION, “Motor data and thrust curves courtesy of ThrustCurve.org”, on every
answer, as it credits the 32 curves it carries. Each data file has its own license, set by whoever
uploaded it: check it before passing a file on. hpr’s saved copy counts as fresh for a day, so a
program that asks again within the day doesn’t ask the site again, since motor records and
curves change seldom.
How it is checked
The tests in crates/hpr-net/tests/motor_finder.rs
replay eight answers recorded from one build of the site, on 1 October 2026 at 07:07 UTC: the four
lists, and the pages of four motors: one each from AeroTech, Cesaroni and Loki, plus AeroTech’s
F27R/L, whose name holds a / (its page is F27R~L.json). They read the expected values from
the recordings themselves, not through the code being tested, and check that:
- each answer, read and written back out, holds exactly the recording’s fields and values, and carries the credit line;
- the API’s words read with their meaning: H128W is a reload that ships as hazardous material, F27R/L a single-use motor whose shipping varies, D13W a reload that doesn’t, and the listings count 827 in stock, 2,363 out of stock and 495 on special order;
- a second read online, inside the hour, is answered from the saved copy without a fetch;
- offline, with a transport that fails the test if it is ever called, each read gives the same values, fresh for the hour and marked stale after it;
- each motor’s page is the same motor as in
motors.json, and a maker can be named in full or in short, in any case; - the files agree with each other:
in-stock.jsonismotors.json’s motors in stock, value for value, andmeta.jsoncounts both lists and the vendors; - each rule in the list above refuses an answer that breaks it, naming the field, and what the API allows (an offer with no price, a new status) is read;
- an answer that isn’t JSON is refused and not saved; when one arrives after a good copy has gone stale, the good copy comes back with the reason;
- a page holding another motor is refused and not saved;
- offline, a file never fetched is an error naming its address.
Some of the site’s rules hpr doesn’t enforce when it reads an answer. The tests confirm that the recording keeps them: each motor’s page is at the address it names, the price of one motor is the sticker price over the pack, the cheapest offer is the lowest-priced listing in stock, a motor is in stock exactly when one of its listings is, and the vendor counts count distinct vendors.
The tests in crates/hpr-net/tests/thrustcurve.rs
replay two answers recorded from ThrustCurve.org on 1 October 2026 at 08:22 UTC, AeroTech J450DM’s
.eng file and AeroTech F27R/L’s .rse file, both public domain, and the three stand-in searches
described above. They check that:
- each answer, read and written back out, holds exactly the recording’s fields and values, carries ThrustCurve.org’s credit, comes from the saved copy on a second read, and works offline, fresh for a day and stale after;
- the test builds its own name-to-id table straight from the stand-ins’ JSON, and the match agrees for all 282 motors; each match’s diameter, impulse, average thrust and burn time equal the finder’s; and the report is the committed one;
- a motor renamed, two records of one name, or a designation in another case is a miss with its reason, and the report lists it with its counts; the same record given twice counts once, and another maker’s record of the same designation doesn’t disturb the match;
- J450DM, a matched motor in stock, has its downloaded file read by
hpr_motorto the points in its lines, and the file is byte for byte the one hpr carries; F27R/L’s.rsefile reads to the points in its XML; - each refusal above refuses an answer changed to break it, naming the field, and a record with only an id, maker and designation reads;
- a download of another motor or format, a search cut short and a search holding another maker’s records are refused and not saved; a file that isn’t plain text is that file’s error alone.
hpr motors search is checked by
crates/hpr-cli/tests/motors_search.rs,
which runs the command on the recorded lists, from the file and offline from the cache
(how far to trust it).
What it leaves out
- No curves at the command line.
hpr motors searchlists stock and prices; it doesn’t match motors to ThrustCurve.org or download their curves. A program does, as in the example. - A match by name only. A motor ThrustCurve.org spells differently from the finder is a miss; hpr doesn’t fall back on impulse or size. None missed on the recording.
- The first motor of a file.
Curve::thrust_curve()reads a file’s first motor; the two recorded files hold one each. - No choice among files. A motor may have several files in one format (from a certification test, the maker or a user); hpr gives them all, in the API’s order, and leaves the choice to the program.
- Prices as listed. hpr shows a price as the site gives it, and doesn’t screen out a shop’s placeholder price.
- U.S. vendors and dollars only, and only the three makers the site reads.
- No history. Each answer is the stock of one hour; hpr keeps only the latest copy of each file.
Reading a flight log
This page is for anyone with an altimeter’s log who wants to know what their rocket did: how high it went, how fast it climbed, when it landed. hpr reads the log on its own. It needs no design file and runs no simulation, so it works whatever the rocket was designed in, or if it was never designed on a computer at all.
What works today: logs from PerfectFlite altimeters in their .pf2 format. The one real
file read so far is a Pnut’s; the StratoLogger and StratoLoggerCF are expected to write the same
layout, but no file of theirs has been tried (the format). Other loggers come
with M7.1, the milestone that reads the other formats that
Debrief, the project owner’s earlier flight-log analyzer, read.
How far to trust it: on an invented flight whose every number is known, the apogee comes within a quarter of a meter and one sample of the truth, and liftoff within a tenth of a second. The landing is read at the first sample within 2 m of the pad, so early by the time the last 2 m take: 0.33 s at 6 m/s, 0.5 s at 4 m/s. On one real flight, a public log that isn’t committed here and so isn’t checked in CI, hpr reads 1,010 ft where the altimeter states 1,009 ft. hpr has no check yet for a barometer’s errors near the speed of sound. If the flight may have come near Mach 0.9, about 300 m/s (1,000 ft/s), treat the top speed and the heights near it with care: the barometer’s error can pull the top speed down too, so a low reading doesn’t clear it. The rules behind each reading are on Flight-log readings, with what they were checked against. A reading the log can’t support is left out and says why, rather than printed as a number.
From the command line
hpr analyze flight.pf2
prints the readings as text, or as one JSON document with --json.
hpr analyze shows its output on an example log, and its
JSON schema lists
the fields.
From a program
The library is hpr_flightdata. It reads a log’s text into a record in SI units, then takes the
readings from that record. It doesn’t depend on the simulator, so a program that only reads logs
doesn’t build one. This program reads the invented log the tests use:
//! A flight log read on its own: the invented PerfectFlite log the tests use, its readings printed
//! with where each came from, or why it was withheld. No design file, no simulation.
//!
//! It uses the workspace crate `hpr-flightdata`, which doesn't pull in the simulator, and `serde`
//! and `serde_json` to print each code as the JSON output spells it.
//!
//! Run it from anywhere in the repository:
//!
//! ```text
//! cargo run --example read_a_log -p hpr-flightdata
//! ```
//!
//! The guide's page *Reading a flight log* (`docs/reading-a-flight-log.md`) quotes it and what it
//! prints, which is kept next to it in `read_a_log.output.txt`; CI checks that the two still agree
//! (`cargo xtask examples --check`).
#![allow(
clippy::print_stdout,
reason = "the project's lints forbid printing in library code, and this program exists to print"
)]
use std::error::Error;
use hpr_flightdata::perfectflite::{self, FOOT_M};
use hpr_flightdata::readings::{self, Reading};
/// The invented log: a Pnut's file of a flight made up for the tests.
const LOG: &str = include_str!("../../../validation/fixtures/logs/synthetic-pnut.pf2");
/// A code as the JSON output spells it, such as `no_accelerometer`.
fn code(value: impl serde::Serialize) -> String {
serde_json::to_value(value)
.ok()
.and_then(|json| json.as_str().map(str::to_owned))
.unwrap_or_default()
}
/// A reading's value as text, or why it was withheld.
fn show<T>(reading: &Reading<T>, value: impl Fn(&T) -> String) -> String {
match reading {
Reading::Read(read) => value(read),
Reading::Withheld(withheld) => {
format!("withheld ({}): {}", code(withheld.reason), withheld.detail)
}
}
}
fn main() -> Result<(), Box<dyn Error>> {
let log = perfectflite::read(LOG)?;
println!("{}: {} samples", log.logger, log.time_s.len());
if let Some(stated) = log.stated.apogee_m {
println!("it states an apogee of {:.0} ft", stated / FOOT_M);
}
let read = readings::read(&log);
println!();
println!(
"liftoff {}",
show(&read.liftoff, |liftoff| format!("{:.2} s", liftoff.time_s))
);
println!(
"apogee {}",
show(&read.apogee, |apogee| format!(
"{:.1} m ({:.0} ft) at {:.2} s, source: {}",
apogee.altitude_m,
apogee.altitude_m / FOOT_M,
apogee.time_s,
code(apogee.source)
))
);
println!(
"highest sample {}",
show(&read.apogee, |apogee| format!(
"{:.1} m at {:.2} s",
apogee.highest_sample.altitude_m, apogee.highest_sample.time_s
))
);
println!(
"top speed {}",
show(&read.max_speed, |speed| format!(
"{:.1} m/s at {:.2} s, source: {}",
speed.speed_m_s,
speed.time_s,
code(speed.source)
))
);
println!(
"top acceleration {}",
show(&read.max_acceleration, |top| format!(
"{:.1} m/s²",
top.acceleration_m_s2
))
);
println!(
"landing {}",
show(&read.landing, |landing| format!(
"{:.2} s; down from apogee at {:.1} m/s on average",
landing.time_s, landing.mean_descent_rate_m_s
))
);
Ok(())
}
It prints:
PerfectFlite Pnut: 984 samples
it states an apogee of 1281 ft
liftoff 0.55 s
apogee 390.1 m (1280 ft) at 10.28 s, source: barometer
highest sample 400.5 m at 11.35 s
top speed 79.9 m/s at 2.10 s, source: logger_speed_from_barometer
top acceleration withheld (no_accelerometer): a PerfectFlite logger has no accelerometer; hpr doesn't difference the altitude twice to make one, as its one-foot steps would read as spikes of many g
landing 45.85 s; down from apogee at 10.9 m/s on average
What the readings say
The log is of a flight invented for the tests (the flight): 80 m/s at burnout, 2.1 s after the start of the log, and a coast with no drag to 390.3 m (1,280.5 ft) at 10.26 s.
- Liftoff, 0.55 s: the last sample before the altitude shows the rocket moving. The rocket left the pad at 0.50 s, but its first 0.15 m rounds to 0 ft.
- Apogee, 390.1 m (1,280 ft) at 10.28 s, which is 10.275 s rounded: the top is flat over several samples, and hpr takes the middle. It is the top of the altitude after a 0.3 s running median. It is 0.17 m below the true apogee: the file rounds to whole feet.
- The highest sample, 400.5 m, a second after apogee, is the ejection charge’s pressure pulse, not the rocket. The median sets it aside.
- The top speed, 79.9 m/s at 2.10 s: the altimeter’s own speed column, which it works out from its barometer. The true speed at burnout is 80.0 m/s; the file rounds to whole feet per second.
- The top acceleration is withheld: a PerfectFlite has no accelerometer.
- Landing, 45.85 s: the first sample within 2 m of the pad. The rocket touches down 0.29 s later, at 6 m/s under its main.
- The mean descent rate, 10.9 m/s: the height lost from apogee to landing over the time taken, drogue and main together.
What the file states about itself, such as the altimeter’s own apogee of 1,281 ft, is kept in
log.stated, beside hpr’s readings and never in their place. The codes, such as
no_accelerometer and barometer, are the ones the JSON output of hpr analyze uses. The top
speed is max_speed in both.
What it doesn’t do yet
- Read other loggers’ files (M7.1).
- Split the descent into the drogue’s and the main’s rates, find the deployments, or give the Mach number and dynamic pressure (M7.2).
- Compare a flight with its simulation (M7.3).
- Check a barometric reading near the speed of sound. Debrief stops trusting one above Mach 0.9; hpr doesn’t check yet.
- Correct a barometric altitude for the day’s air. The altitude is the altimeter’s own conversion, which assumes a standard atmosphere (Barometric altimeter).
Your own rocket
This page builds a rocket of your own in Rust, part by part: your dimensions, your materials, and a motor from the catalog that comes with hpr-sim. It then finds the rocket’s center of gravity (CG), its center of pressure (CP) and its stability margin, flies it, and turns it into a design file and back. It needs the setup from Getting started, and some Rust.
How far to trust it. The CP comes from Barrowman’s method, which hpr checks against Barrowman’s own worked examples at Mach 0. hpr’s CP agrees with all five within 1%; for one of them, a six-fin rocket, hpr’s normal-force slope is 2.87% high (Aerodynamics). The mass and CG come from each part’s shape and a published density, so glue, paint and hardware are missing until you weigh the parts and enter the weights (What else a design can hold). This rocket’s flight is not validated: whole flights of other rockets have been compared with RocketPy’s, OpenRocket’s and seven real flights’ (Accuracy), but not this one’s (Getting started).
Run it
cargo run --example own_rocket -p hpr-sim
This runs
crates/hpr-sim/examples/own_rocket.rs,
which prints this:
My 54 mm rocket (Cesaroni 168H54-10A): 1.120 m long, 56.3 mm across
Not yet validated: see the Accuracy page before trusting these numbers.
liftoff burnout
mass (kg) 0.675 0.579
center of gravity (m from nose) 0.671 0.610
center of pressure (m from nose) 0.779 0.779
stability margin (calibres) 1.92 2.99
Normal-force slope (per radian) and center of pressure, at Mach 0.3:
nose 2.00 at 0.102 m
fins 4.82 at 1.060 m
rocket 6.82 at 0.779 m
From a 1.8 m vertical rail, with no wind:
Rail exit: 21.7 m/s
Apogee: 1144.5 m above the pad, at 13.92 s
Top speed: 187 m/s (Mach 0.56)
Ejection: at 13.50 s, at 5.5 m/s
The fin set, as the design file stores it:
{
"id": "fins",
"name": "",
"part": {
"fin_set": {
"count": 3,
"planform": {
"kind": "trapezoidal",
"root_chord_m": 0.1,
"tip_chord_m": 0.04,
"span_m": 0.045,
"sweep_m": 0.05
},
"thickness_m": 0.003175,
"cross_section": "rounded",
"tab": null,
"cant_rad": 0.0,
"base_angle_rad": 0.0,
"material": {
"name": "Birch plywood",
"density": {
"kind": "bulk",
"kg_m3": 680.0
}
}
}
},
"position": {
"from": "bottom",
"aft_offset_m": 0.0
}
}
Like the first flight’s in Getting started, this output is committed in
own_rocket.output.txt,
and CI (the project’s automated checks) runs the program on macOS, Windows and Linux and fails if
it prints anything else.
To fly your own rocket, change the numbers in the program and run it again. Or copy it to a new
file in the same folder, say my_rocket.rs, and run that with
cargo run --example my_rocket -p hpr-sim.
The rocket
The rocket has a 54 mm airframe (the tube’s inside diameter) and flies on a Cesaroni H54, a 29 mm reloadable motor (a propellant load for a reusable case). These are the program’s inputs:
| part | Rust type | in the program |
|---|---|---|
| nose cone | NoseCone | a tangent ogive (Shapes) 0.22 m long, of ABS with a 1.5 mm wall, and a 6 cm shoulder that slides into the tube |
| airframe | BodyTube | 0.9 m of kraft phenolic tube, outer radius 0.02815 m (56.3 mm across), 1.15 mm wall |
| motor mount | InnerTube | 0.2 m long, outer radius 0.0155 m, 1 mm wall, so a 29 mm bore; flush with the airframe’s aft end, with the nozzle 5 mm past it |
| fins | FinSet | three trapezoidal fins of 1/8 in (3.175 mm) birch plywood with rounded edges: root chord 0.1 m, tip chord 0.04 m, span 0.045 m, and the tip’s leading edge 0.05 m aft of the root’s |
| recovery bay | MassComponent | 200 g standing in for the parachute, shock cord and altimeter, packed as a cylinder 0.15 m long and 50 mm across (packing), 7 cm below the airframe’s top |
| motor | MountedMotor | the Cesaroni 168H54-10A from the bundled catalog, with a 10 s ejection delay |
Every size is in meters, and every round part takes a radius, not a diameter: halve the diameters on your drawings.
What it printed
Mass, center of gravity and center of pressure
The table has two columns. liftoff is the rocket on the pad, with the motor full. burnout is the rocket with the motor spent: its case and nozzle are still aboard, its propellant is gone.
- Mass: 0.675 kg on the pad and 0.579 kg at burnout. The 0.096 kg between them is the propellant; the catalog lists 96.6 g for this motor.
- Center of gravity: where the mass balances, as a station: meters aft of the nose tip. It moves forward, from 0.671 m to 0.610 m, as the propellant in the tail burns away.
- Center of pressure: where the air’s sideways push acts, 0.779 m aft of the tip. It depends on the rocket’s shape and speed, not its mass, so it is the same in both columns. Both use Mach 0.3, with the air straight along the rocket’s axis.
- Stability margin: how far the CP lies behind the CG, in calibres, that is, in body diameters. At liftoff it is (0.779 − 0.671) m ÷ 0.0563 m ≈ 1.9; the program works from the unrounded values and prints 1.92. At burnout the CG has moved forward, so the margin has grown to 2.99. This program works the margin out by hand; hpr also gives it from the rail exit to apogee or the first deployment, with an optimum ejection delay, as Flight metrics shows.
A positive margin means that when something tips the rocket, the air turns its nose back into the oncoming air. That oncoming air is the relative wind: the airflow the rocket feels, from its motion over the ground combined with the wind. In a crosswind, the same turn swings the rocket upwind (weathercocking).
hpr doesn’t judge whether a margin is enough; your club’s or range’s rules do.
Where the center of pressure comes from
Barrowman’s method works out each nose cone, transition and fin set on its own. Each gets a CP and a normal-force slope: how fast its sideways push grows with the angle of attack, per radian. A plain body tube’s slope is zero, so it has no line of its own. (A tube’s sideways push appears only at larger angles, so it adds nothing to the slope.) The rocket’s CP is the average of the parts’ CPs, each weighted by its slope:
| part | slope (per radian) | CP (m from the nose tip) | slope × CP |
|---|---|---|---|
| nose | 2.00 | 0.102 | 0.204 |
| fins | 4.82 | 1.060 | 5.109 |
| rocket | 6.82 | 5.313 |
So the CP is 5.313 ÷ 6.82 ≈ 0.779 m. The fins sit far aft and have more than twice the nose’s slope, so they pull the CP toward the tail. Moving the CP aft (bigger fins, or fins farther aft) or the CG forward (a heavier nose) raises the margin; run the program to see by how much.
How speed moves the CP. In hpr, only a fin set’s terms change with the Mach number: its slope grows as the rocket speeds up toward Mach 1, and from Mach 0.8 its own CP moves aft too. The slopes and CPs of nose cones, transitions and body tubes stay where they are (Aerodynamics).
- With the fins at the tail, as here, the growing fin slope pulls the rocket’s CP aft as it speeds up.
- A rocket with canards (a second fin set near the nose) is different. The canards’ slope grows too and pulls the CP forward, so which way the CP moves depends on both fin sets.
- hpr keeps each fin set’s CP a quarter of the way back along its mean aerodynamic chord, a kind of average chord, up to Mach 0.8, and moves it aft from there toward where supersonic linear theory puts it (Aerodynamics). Niskanen’s 2009 thesis, which hpr’s aerodynamics also draw on, starts moving it at Mach 0.5; NASA’s wind tunnel found an Arcas Robin rocket’s CP moving forward, not aft, between Mach 0.6 and 0.8, so hpr doesn’t. This rocket’s top speed, Mach 0.56, is well below either.
Flow::axial(0.0) in place of Flow::axial(0.3) gives the low-speed value that Barrowman’s method
gives by hand.
The flight
The rocket flies from a 1.8 m vertical rail, 1,400 m up in New Mexico, with no wind. It has one parachute, 0.9 m across (nominal diameter), which opens when the motor’s ejection charge fires.
- Rail exit: 21.7 m/s. The design has no rail buttons, so hpr takes the rocket as off the rail when its aft end passes the top (rail exit).
- Apogee: 1144.5 m above the pad, 13.92 s after ignition (apogee).
- Top speed: 187 m/s, Mach 0.56: the fastest airspeed at the end of any of the time steps the flight was computed in. With no wind, the airspeed is also the speed over the ground.
- Ejection: at 13.50 s, at 5.5 m/s. The charge fires the 10 s delay after burnout, which in hpr is the time of the thrust curve’s last point, 3.50 s for this motor. That is 0.42 s before apogee, while the rocket is still climbing slowly.
This motor has three times near the end of its burn, and they measure different things:
| time | what it is | where it comes from |
|---|---|---|
| 3.12 s | the burn time ThrustCurve.org publishes for the motor | the bundled catalog, which copies ThrustCurve.org’s values |
| 3.13 s | the burn time hpr works out from this motor’s thrust curve, by the same NFPA 1125 rule: from when the thrust first reaches 5% of its peak to when it last falls to 5% | Solid motors lists it |
| 3.50 s | burnout: the curve’s last point, where the thrust reaches zero | the thrust curve; the ejection delay counts from here |
- The first two differ by 0.01 s. hpr bundles a motor only if its computed burn time is within 1% of ThrustCurve.org’s (Solid motors).
- From 3.13 s to 3.50 s the motor still pushes, with under 5% of its peak thrust (the curve’s peak is 103 N, so under about 5 N). The burn time leaves that tail out; the flight doesn’t.
The fin set as a design file
The last lines are the fin set as a design file stores it, in JSON. As a design file, below, explains the form.
The program, step by step
This is the whole program, line for line the file CI runs.
//! Your own rocket: a 54 mm rocket built part by part in Rust, with a motor from the bundled
//! catalog. It prints the rocket's mass, center of gravity, center of pressure and stability
//! margin, then flies it.
//!
//! Run it from anywhere in the repository:
//!
//! ```text
//! cargo run --example own_rocket -p hpr-sim
//! ```
//!
//! The documentation site's *Your own rocket* page (`docs/your-own-rocket.md`) walks through it.
//! What it prints is kept next to it in `own_rocket.output.txt`, and CI checks that the two still
//! agree (`cargo xtask examples --check`).
#![allow(
clippy::print_stdout,
reason = "the project's lints forbid printing in library code, and this program exists to print"
)]
use std::error::Error;
use hpr_aero::{AeroModel, Flow};
use hpr_core::geodesy::Geodetic;
use hpr_design::{
AutoDimension, BodyTube, Component, Configuration, FinCrossSection, FinPlanform, FinSet,
Ignition, InnerTube, MassComponent, Material, MotorMount, MountedMotor, NoseCone, NoseShape,
Overrides, Packing, Part, Position, ReferenceDiameter, Rocket, Shoulder, Stage, Wall,
materials,
};
use hpr_motor::{Catalog, Delay};
use hpr_sim::{
CanopyType, Channel, Device, DeviceDrag, Environment, EventKind, FlightSettings, Rail,
Recorder, Simulation, Trigger,
};
fn main() -> Result<(), Box<dyn Error>> {
// The nose: a 22 cm tangent ogive of ABS with a 1.5 mm wall and a 6 cm shoulder. Its base
// radius and its shoulder's radius are automatic: they fit the tube behind it.
let mut nose = component(
"nose",
Part::NoseCone(NoseCone {
shape: NoseShape::Ogive { radius_ratio: 1.0 },
length_m: 0.22,
base_radius_m: 0.0,
wall: Wall::Shell {
thickness_m: 0.0015,
},
shoulder: Some(Shoulder {
length_m: 0.06,
outer_radius_m: 0.0,
thickness_m: 0.0015,
capped: true,
}),
material: material("abs")?,
}),
None,
);
nose.auto = vec![AutoDimension::BaseRadius, AutoDimension::ShoulderRadius];
// The airframe: 90 cm of kraft phenolic tube, 56.3 mm across, with a 1.15 mm wall.
let mut airframe = component(
"airframe",
Part::BodyTube(BodyTube {
length_m: 0.9,
outer_radius_m: 0.02815,
thickness_m: 0.00115,
material: material("kraft_phenolic")?,
}),
None,
);
// Inside it, flush with its aft end, a 20 cm motor mount tube with a 29 mm bore. The motor's
// nozzle will stick out 5 mm past it.
let mut mount = component(
"motor-mount",
Part::InnerTube(InnerTube {
length_m: 0.2,
outer_radius_m: 0.0155,
thickness_m: 0.001,
radial_offset_m: 0.0,
angle_rad: 0.0,
material: material("kraft_phenolic")?,
cluster_m: Vec::new(),
}),
Some(Position::Bottom { aft_offset_m: 0.0 }),
);
mount.motor_mount = Some(MotorMount { overhang_m: 0.005 });
// Three trapezoidal fins of 1/8 in birch plywood, flush with the aft end.
let fins = component(
"fins",
Part::FinSet(FinSet {
count: 3,
planform: FinPlanform::Trapezoidal {
root_chord_m: 0.1,
tip_chord_m: 0.04,
span_m: 0.045,
sweep_m: 0.05,
},
thickness_m: 0.003175,
cross_section: FinCrossSection::Rounded,
tab: None,
fillet: None,
cant_rad: 0.0,
base_angle_rad: 0.0,
material: material("birch_plywood")?,
}),
Some(Position::Bottom { aft_offset_m: 0.0 }),
);
// The parachute, shock cord and altimeter, as one 200 g mass 7 cm below the tube's top, clear
// of the nose's shoulder. Its packing is the cylinder the mass fills: 15 cm long, 5 cm across.
let packing = Packing {
length_m: 0.15,
radius_m: 0.025,
radial_offset_m: 0.0,
angle_rad: 0.0,
};
let bay = MassComponent {
mass_kg: 0.2,
packing,
};
let top = Position::Top { aft_offset_m: 0.07 };
let bay = component("recovery-bay", Part::MassComponent(bay), Some(top));
// The fin set as a design file stores it, to print at the end.
let fins_json = serde_json::to_string_pretty(&fins)?;
airframe.children = vec![mount, fins, bay];
// The motor: a Cesaroni H54 from the bundled catalog, found by its designation, with the
// catalog's size, masses and thrust curve, and the 10 s delay its designation names.
let catalog = Catalog::bundled()?;
let entry = catalog
.find("168H54-10A")
.next()
.ok_or("not in the catalog")?;
let motor = MountedMotor {
mount: "motor-mount".to_owned(),
designation: entry.designation.clone(),
diameter_m: entry.diameter_mm / 1000.0,
length_m: entry.length_mm / 1000.0,
motor: entry.bundled_motor()?,
delay: Some(Delay::Seconds(10.0)),
ignition: Ignition::Launch,
failed_tubes: Vec::new(),
};
// The rocket: one stage, and one configuration, "h54", with that motor in the mount.
let rocket = Rocket {
name: "My 54 mm rocket".to_owned(),
stages: vec![Stage {
id: "sustainer".to_owned(),
name: String::new(),
components: vec![nose, airframe],
overrides: Overrides::default(),
drag_override: None,
parallel: None,
}],
reference_diameter: ReferenceDiameter::Maximum {},
configurations: vec![Configuration {
id: "h54".to_owned(),
name: String::new(),
motors: vec![motor],
}],
};
// Place the parts and the motor, and find the mass properties with the motor full and spent.
// A point `s` meters aft of the nose tip is at z = -s in the body frame.
let assembly = rocket.assemble("h54")?;
let full = assembly.mass_properties(0.0);
let spent = assembly.dry_mass_properties();
let (cg_liftoff_m, cg_burnout_m) = (-full.cg_m.z, -spent.cg_m.z);
// The center of pressure by Barrowman's method, at Mach 0.3 with the air straight along the
// axis. Barrowman's slopes are the small-angle limit, so this is the CP at small angles.
let flow = Flow::axial(0.3);
let aero = AeroModel::new(&assembly.layout)?;
let total = aero.normal_force(&flow)?;
let cp_m = total.cp_station_m.ok_or("no normal force")?;
let calibres = |cg_m: f64| (cp_m - cg_m) / assembly.layout.reference_diameter_m;
println!(
"{} ({} {}): {:.3} m long, {:.1} mm across",
rocket.name,
entry.manufacturer_abbrev,
entry.designation,
assembly.layout.length_m,
assembly.layout.reference_diameter_m * 1000.0,
);
println!("Not yet validated: see the Accuracy page before trusting these numbers.");
println!();
println!(" liftoff burnout");
let (m0, m1) = (full.mass_kg, spent.mass_kg);
println!("mass (kg) {m0:>7.3} {m1:>9.3}");
println!("center of gravity (m from nose) {cg_liftoff_m:>7.3} {cg_burnout_m:>9.3}");
println!("center of pressure (m from nose) {cp_m:>7.3} {cp_m:>9.3}");
let (s0, s1) = (calibres(cg_liftoff_m), calibres(cg_burnout_m));
println!("stability margin (calibres) {s0:>7.2} {s1:>9.2}");
println!();
println!("Normal-force slope (per radian) and center of pressure, at Mach 0.3:");
for part in aero.components(&flow)? {
let force = part.normal_force;
if let Some(station_m) = force.cp_station_m {
let (id, slope) = (&part.id, force.slope_per_rad);
println!("{id:<10} {slope:>5.2} at {station_m:.3} m");
}
}
let slope = total.slope_per_rad;
println!("{:<10} {slope:>5.2} at {cp_m:.3} m", "rocket");
// Fly it from a 1.8 m vertical rail, 1,400 m up in New Mexico, with no wind. The parachute
// opens when the motor's ejection charge fires, 10 s after burnout.
let site = Geodetic::from_degrees(32.99, -106.97, 1400.0)?;
let parachute = Device::new(
"parachute",
DeviceDrag::canopy(CanopyType::FlatCircular, 0.9),
Trigger::MotorDelay { motor: 0 },
);
let simulation = Simulation::new(
&rocket,
"h54",
Environment::standard(site)?,
Rail::vertical(1.8),
FlightSettings::default(),
)?
.with_recovery(vec![parachute])?;
// A recorder keeps the airspeed and the Mach number at the end of every step.
let mut recorder = Recorder::new(vec![Channel::Airspeed, Channel::Mach], None)?;
let flight = simulation.run(&mut recorder)?;
let rows = recorder.rows();
let fastest = rows.iter().max_by(|a, b| a[0].total_cmp(&b[0]));
let fastest = fastest.ok_or("no steps")?;
let rail_exit = flight.event(EventKind::RailExit).ok_or("no rail exit")?;
let apogee = flight.event(EventKind::Apogee).ok_or("no apogee")?;
let ejection = flight.event(EventKind::Trigger(0)).ok_or("no ejection")?;
let (rail_exit, apogee, ejection) = (rail_exit.sample, apogee.sample, ejection.sample);
println!();
println!("From a 1.8 m vertical rail, with no wind:");
let speed_m_s = rail_exit.cg_velocity_enu_m_s.length();
println!("Rail exit: {speed_m_s:.1} m/s");
let (height_m, time_s) = (apogee.height_above_ground_m, apogee.time_s);
println!("Apogee: {height_m:.1} m above the pad, at {time_s:.2} s");
println!("Top speed: {:.0} m/s (Mach {:.2})", fastest[0], fastest[1]);
let (time_s, speed_m_s) = (ejection.time_s, ejection.cg_velocity_enu_m_s.length());
println!("Ejection: at {time_s:.2} s, at {speed_m_s:.1} m/s");
// The whole rocket as a design file's text, JSON, and read back from it.
let text = serde_json::to_string_pretty(&rocket)?;
let read_back: Rocket = serde_json::from_str(&text)?;
if read_back != rocket {
return Err("the design file doesn't read back as the same rocket".into());
}
println!();
println!("The fin set, as the design file stores it:");
println!("{fins_json}");
Ok(())
}
/// A built-in material by its id; `hpr_design::materials` lists them, each with its source.
fn material(id: &str) -> Result<Material, String> {
materials::find(id)
.map(|builtin| builtin.material())
.ok_or_else(|| format!("no built-in material `{id}`"))
}
/// A node of the design tree holding `part`, placed at `position` along its parent. Body
/// components (nose cones, body tubes and transitions) have no position: they stack from the nose.
fn component(id: &str, part: Part, position: Option<Position>) -> Component {
Component {
id: id.to_owned(),
name: String::new(),
part,
position,
auto: Vec::new(),
motor_mount: None,
finish: None,
overrides: Overrides::default(),
overrides_include_children: false,
drag_override: None,
children: Vec::new(),
}
}
It has eight steps.
- The parts. Each part is a Rust value from the
hpr_designcrate, wrapped in aComponent: a node of the design tree with an id, the part, and where it sits. Thecomponenthelper at the bottom of the program fills in the fields a part seldom needs: a display name, a surface finish, mass overrides and children.Partlists every kind of part. - Where each part sits. Nose cones, body tubes and transitions are body components: they
go in a stage’s list, nose first, and stack from the nose tip aft, so they have no position.
Every other part hangs from a body component, or from an inner tube, and has a
Position:Top,Middle,Bottom,AfterorAbsolute, with an offset in meters, positive aft (Positions). The mount and the fins are atBottomwith no offset, flush with the aft end.- Automatic dimensions. A dimension named in a component’s
autolist is taken from the parts around it, and the value stored for it (0 here) is ignored. The nose’s base radius and its shoulder’s radius follow the airframe (Automatic dimensions). Other parts can take theirs the same way: a tube fin set’souter_radius_m, for one, can be automatic. Then three tubes or more are just wide enough to touch the body and each other, closing the ring, and one or two take the body’s radius. - Motor mount. Setting
motor_mountmakes a body tube or inner tube a mount, and itsoverhang_mis how far the nozzle sits aft of the mount’s end. - Packing. A
MassComponentis a mass and itsPacking: the size of the solid cylinder hpr spreads the mass through. Here it is 0.15 m long withradius_m0.025, so 50 mm across, inside the airframe’s 54 mm bore.- The length places the mass. Its CG is the cylinder’s middle, 0.145 m below the airframe’s top, since the cylinder starts 7 cm down.
- Of the mass properties, the radius changes only the moments of inertia: how hard the mass is to turn.
- A cylinder a little wider than the tube’s bore, within the
fit tolerance, gets a warning; any wider is an
error.
AutoDimension::PackedRadiusin the component’sautolist fits it to the bore instead. radial_offset_mandangle_radmove it off the rocket’s axis. Parachutes, streamers and shock cords have a packing too.
- Automatic dimensions. A dimension named in a component’s
- Materials.
material("abs")looks up one of hpr’s 49 built-in materials by its id, each with the source of its density (Mass properties). Thematerialspage of the API reference lists them. For a material of your own,Material::bulk(name, kg_m3)takes a name and a density in kg/m³. - The motor.
Catalog::bundled()is the catalog of 32 ThrustCurve.org motors that comes with hpr.findlooks one up by its designation or common name, ignoring case, spaces and hyphens, so"h54"finds this one too.bundled_motor()builds the motor from its thrust curve and the catalog’s size and masses. TheMountedMotornames the mount by its id, and carries the ejection delay, the case’s diameter and length, and when the motor lights:Ignition::Launchhere, and a later time for an air start or a sustainer (Staging). The design checks compare the case with its mount: a case wider than the mount’s bore is an error, but a nominal 29 mm motor in a 1.140 in (28.956 mm) tube only warns, since the real case is narrower than its name (a nominal motor in its matching tube). While the motor burns, hpr also uses the case’s diameter for the base drag, the drag on the rocket’s flat aft end: the part of that end the burning case covers gets none. Solid motors lists the bundled motors, and shows how to use a motor file of your own, such as one from ThrustCurve.org, instead. - The rocket. A
Rocketholds its stages (one here), how its reference diameter is chosen, and its configurations.ReferenceDiameter::Maximum {}takes the widest body part, the 56.3 mm airframe, as the diameter that the margin and the aerodynamic coefficients are measured by (reference area). A configuration is one choice of motors, at most one per mount, under an id; this rocket has one,"h54". Add another to compare motors in the same rocket. - Mass, CG and CP.
assembleplaces every part and the configuration’s motor, and returns anAssembly.- Its
mass_properties(t)is the whole rockettseconds after ignition, anddry_mass_properties()is the rocket with every motor spent. - Each gives a mass, a CG and the moments of inertia (how hard the rocket is to turn), which
the flight needs. The CG,
cg_m, is in the body frame, whose origin is the nose tip and whosezaxis points forward, out through the nose. So a pointsmeters aft of the tip hasz = −s, and the CG’s station is−cg_m.z. AeroModel::newbuilds the aerodynamic model from the placed parts. Itsnormal_force, atFlow::axial(0.3)(Mach 0.3, with the air straight along the axis), returns the rocket’s slope and its CP,cp_station_m. Barrowman’s slopes are the small-angle limit, so this is the CP at small angles of attack.components, at the same flow, returns each part’s share.- The margin is the CP’s station less the CG’s, divided by the reference diameter,
assembly.layout.reference_diameter_m.
- The flight. This is as in Getting started,
with three differences.
Simulation::newnames the configuration to fly,"h54", and runs the design’s checks first (Checks).- The parachute’s trigger is
Trigger::MotorDelay { motor: 0 }: the ejection charge of the configuration’s first motor (Recovery). - A
Recorderkeeps the airspeed and the Mach number at the end of every step, and the program takes the row with the highest airspeed. Recording a trajectory explains recorders.
- The design file. The end of the program writes the rocket as JSON, reads it back, and prints the fin set’s part of it. As a design file, below, explains.
As a design file
A design file is a rocket written as text, to keep, share or edit outside Rust. Here it is the
rocket’s JSON, which is also the rocket key of a document of
the hpr design format, the form that adds a versioned header, the motor
configurations, recovery and what a .ork held:
serde_json::to_string_pretty(&rocket)gives the text, andstd::fs::writesaves it to a file.serde_json::from_str::<Rocket>(&text)reads it back. The program checks that the rocket it reads back is the one it wrote.
The JSON follows the Rust types, so the API reference documents every key. The fin set at the end of the output shows the rules:
| in Rust | in the JSON | in the output |
|---|---|---|
| a struct’s field | a key with the field’s name | "thickness_m": 0.003175 |
| the unit in the name | SI units: _m meters, _kg kilograms, _rad radians, kg_m3 kg/m³ | "span_m": 0.045 |
a Part | an object with one key, the kind of part | "part": { "fin_set": { … } } |
| a shape, planform, wall, density, finish or delay | an object whose "kind" names it | "planform": { "kind": "trapezoidal", … } |
a Position | an object whose "from" names it | "position": { "from": "bottom", "aft_offset_m": 0.0 } |
| a choice with no values | a string | "cross_section": "rounded" |
None | null | "tab": null |
- Some keys can be left out, and take a default: a component’s
name, or a fin set’stab,filletandcant_rad, for example.cant_radis the fins’ cant in radians, 0 by default; a cant spins the rocket (Roll: forcing and damping).filletgives the fins’ fillets, a radius and a material, and is not written when there are none, which is why the output above has nofilletkey. In Rust it is, for example,fillet: Some(FinFillet { radius_m: 0.005, material: material("epoxy")? }), withFinFilletadded to theuse hpr_design::{…}list. - A key hpr doesn’t know is refused, so a misspelt key is an error rather than silently ignored.
- A mounted motor is stored whole: its thrust curve, masses and size. Its
designationis only a label, so a design file doesn’t depend on the catalog.
For a complete file of a similar rocket, with centering rings, rail buttons, a parachute and a
shock cord as parts, on a 38 mm Cesaroni I175, see
synthetic-54mm-three-fin.json.
A program in the repository writes the files in that folder, so edit a copy rather than the file.
The format is provisional. It is hpr’s own, and the open design format (M3.3, a documented and versioned design file with a schema) will replace it and convert the repository’s own designs.
What else a design can hold
The example leaves out several kinds of part and setting that a design can have:
- More parts: transitions, centering rings (whose radii can be automatic), launch lugs, rail buttons, parachutes, streamers, shock cords, and elliptical or freeform fins (The design tree).
- Rail guides. With rail buttons or launch lugs, the rocket leaves the rail when its last guide passes the top, not its aft end.
- Weighed masses. An
Overridessets a part’s mass, CG or inertia to measured values, for the part alone or with everything attached to it (Overrides). - Checks.
hpr_design::checks::checklists a design’s problems (Checks). Errors, such as a motor wider than its mount, describe a rocket that can’t exist, and a simulation refuses them: put the 38 mmH170Min this program’s 29 mm mount and it stops withMotorWiderThanMount. Warnings, such as a step in the body’s radius, don’t stop a flight.
What it can’t do yet
- Built only in Rust, or in JSON. The builder builds the same rocket in
fewer lines, and
hpr simflies the JSON file from a terminal, but opens no parachute: a design file can carry a parachute’s weight as a mass part, but not the parachute itself or when it opens. Python (M4.3) is planned. - Import from one other program. OpenRocket
.orkfiles are read (.orkdesign files), though few of their motor configurations fly yet (their parachutes and streamers fly as OpenRocket flies them); RockSim.rktfiles (M3.4, RockSim import) can’t be read yet. - Drag near and past Mach 1 is lightly checked. Since M1.8b1 (drag through Mach 1), hpr’s own drag, like its normal force, carries a flight from Mach 0 to 5, and a flight that reaches Mach 5 stops with an error. Near and above the speed of sound the drag has been checked against one wind tunnel, which measured from Mach 0.6 to 4.63. hpr reads high there at most speeds, most of all with fins past Mach 1 (Aerodynamics). Against a worked example in a U.S. Army design handbook, the body alone reads a little low faster than sound. Against RASAero II’s drag for a rocket with a short, steep boattail, the whole rocket reads about a quarter low faster than sound, for reasons not yet pinned down (Aerodynamics). So if your rocket goes supersonic, its drag there may be off by a quarter or more either way: possibly low with a steep boattail, high with thin, sharp fins. Treat a supersonic flight’s apogee as rough until M1.8b3 (a boattail’s drag faster than sound) and the issues it leaves are done.
- Staging needs its settings. A motor lights at launch unless its
ignitionsays otherwise, so a two-stage design flies with both stages burning at once until you give the sustainer its ignition and the flight a separation (Staging). A.orkfile’s own ignitions and powered separations are read for you. The ignitions come with the rocket, but the separations don’t: turn the configuration’sstagings()into the flight’s separations withhpr::ork::separationsand pass them to the flight, with a recovery device on each part, since hpr refuses the flight without them. The exampleork_two_stage.rsdoes both. Staged, clustered and air-start flights of OpenRocket’s examples are within 5% of OpenRocket’s apogee and largest speed. Three cluster apogees are compared with OpenRocket’s flight with no parachute, since its parachute opened before apogee (M1.9c, a two-stage and a cluster design against OpenRocket; results). - Pieces that land on their own are yours to declare. A nose cone on a shock cord comes down
with its rocket, and that is what hpr flies unless you say otherwise. To fly a nose cone, a
section or a payload that leaves and lands on its own, give the flight an
Ejectionfor each, and a recovery device on each piece: a parachute, or its own tumble (Simulation::tumbling_piece). An ejection can push the pieces apart with the charge’s impulse (Ejection::with_impulse). A.orkfile’s recovery settings don’t make them for you. A stage whose mass is overridden can’t be parted inside: remove the override, or put it on the components instead. This is checked against exact answers only (Recovery: ejected pieces, with the exampleejected_pieces.rs). - Mass that moves in flight is yours to declare too. To slide ballast or a payload along the
airframe during the flight, give the flight a
MassShiftfor it withSimulation::with_shifts: the part’sid, how far it moves (positive toward the tail), how long it takes, and a trigger, the same kinds a parachute has. The center of mass, the inertia and the stability margin follow it, andSimulation::mass_propertiestells you what they were at any time. A shift must start after the rocket leaves the rail. A flight that separates or ejects pieces can’t have one yet. This is checked against exact answers only (Moving mass, with the examplemoving_ballast.rs). - Mass released in flight is yours to declare too. To let ballast or a payload go during the
flight, give it a
MassReleasewithSimulation::with_releases: the part’sid, a trigger, and the part’s own drag area once it is out. The rest flies on without it, and the part falls to the ground on its own (FlightResult::released). A release must come after the rocket leaves the rail, and a flight can’t combine one with a separation, ejected pieces or a mass shift yet. Check the stability margin after it: hpr doesn’t warn. This is checked against exact answers only (Released mass, with the examplereleased_ballast.rs). - Commercial solid motors only (COTS motors). With only catalog data, a motor’s own CG stays at its mid-length, full or spent (Solid motors).
- Tube fins fly as ring wings, below Mach 0.8, with three tubes or more. Tube fins are open tubes that run along the body, touching it, in place of flat fins. Each tube’s slope comes from a cited ring-wing formula. Its center of pressure comes from Fletcher’s wind-tunnel rings for short tubes and from hpr’s own derivation for tubes longer than 1.5 diameters, a judgement. The drag applies the flat fins’ rules. No tube fin rocket has been checked against a measurement. hpr’s tube-fin drag probably reads low, so treat an apogee as high. On OpenRocket’s example hpr’s margin is 0.79 calibres against OpenRocket’s 1.87; nothing measured says which is right. A flight that reaches Mach 0.8 stops with the tube-fin model’s error. Also refused: fewer than three tubes, solid tubes, tubes that overlap each other, a tube shorter than a third of its diameter, tube fins on a pod, and a tumbling airframe with tube fins (aerodynamics: Tube fins).
- Pods fly on Barrowman’s rules, without their interference with the body. A pod set gives each pod’s parts their own normal force and drag, once per pod, as if the airframe did not disturb the air around them. Nothing measured checks it yet, and a single pod’s off-axis moments are left out (aerodynamics: Pods). Canted fins on a pod are refused.
- Two nose shapes have no drag of hpr’s own, on a nose cone or on a transition that widens,
because no drag data covers them: a bulged secant ogive (
NoseShape::Ogivewith aradius_ratiobelow 1, which bulges wider than the body just ahead of its base) and a Haack shape whose parameterCis above 1/3, past the LV-Haack (Shapes). Since M1.8b1, the drag through Mach 1, the drag buildup refuses them, naming the part. The CP still works, and so does a flight on a drag table from another tool; a flight on hpr’s own drag stops with that error. - Fin sections and supersonic drag. Faster than sound, every fin section takes a blunt leading edge’s drag: the square section a flat face’s, the rounded and airfoil sections a rounded edge’s. The airfoil section differs from the rounded only in having no trailing-edge base drag. That reads far high for thin, sharp fins, and the one wind tunnel hpr has been measured against tested only double-wedge fins, so how well square or rounded edges fare is unmeasured (Drag limits).
Where next
- Getting started adds wind, tilts the rail and uses a drogue and a main parachute; the same code works with this rocket.
- Recording a trajectory keeps the whole flight as a table.
- The design tree, Mass properties and Aerodynamics explain the models behind these numbers.
How a flight is simulated
This page follows a flight from ignition to landing, and says in plain words what hpr computes at each stage and which model does it. Read it to learn what lies behind a number hpr prints, before the model pages it links. It describes the method, not how well it works. When both codes fly the same drag, whole flights match RocketPy’s in height, speed and time, and in where they go, except for rockets that leave the rail slowly in a wind (Accuracy). With hpr’s own drag, against RocketPy flying the drag its examples ship, heights differ by −7.280% to +10.302% (report; Accuracy). Accuracy keeps every result so far.
The drawing is the shape of the Getting started example’s flight, not to scale: its apogee is 779 m up and 86 m west of the pad, and it lands 94 m east of it.
What goes in
Building a simulation gathers five inputs, and works out once everything that doesn’t change during the flight.
| input | what hpr takes from it | pages |
|---|---|---|
| the rocket | A tree of parts, such as a nose cone, body tubes and a fin set, with their positions. Their shapes and materials give each part’s mass, center of gravity and inertia. The design’s checks run first, and a rocket that can’t exist, such as one with a motor wider than its mount, is refused | Design tree, Shapes, Mass properties |
| the motor | The thrust at every instant, from its thrust curve. Its mass, center of gravity and inertia as its propellant burns away, so the whole rocket gets lighter, and its center of gravity moves, during the burn | Solid motors |
| the aerodynamics | Built from the rocket’s shape and surface: the normal force on each body part and fin set, the center of pressure where it acts, and the drag. They depend on the Mach number, the angle of attack and the Reynolds number | Aerodynamics |
| the surroundings | The launch site on the WGS 84 model of the Earth’s shape, and the launch frame: east, north and up from the pad. Gravity that changes with latitude and height, and the Coriolis acceleration, a small sideways push that anything moving over the rotating Earth appears to feel. The air’s density, pressure, temperature and speed of sound, and the wind, at each height. A turbulence model exists, but no flight uses it yet | Geodesy, Frames, Gravity, Atmosphere, Wind, Turbulence |
| the rail, recovery and settings | The rail’s length, direction and friction; each parachute or streamer and when it fires; how finely to step through time | Rigid-body flight, Recovery, Time integration |
From the pad to the ground
The numbers match the drawing.
-
Ignition and liftoff. Every motor lights at time zero unless its design gives it a later ignition: an air start, or a sustainer lit after its booster (Staging). A two-stage design that says nothing lights both stages on the pad. The rocket stands on the rail, its aft end at the rail’s foot, and holds still until the push up the rail, mostly the thrust, beats the weight’s pull down it and the rail’s friction. That instant is liftoff. If the motors burn out first, the flight ends on the pad.
-
On the rail. The rocket slides along the rail without turning: one degree of freedom. Rail exit comes when its last rail guide, a rail button or a launch lug (a short tube on the body), leaves the top of the rail. A rocket that stops on the rail comes to rest there (Rigid-body flight).
-
Powered flight, to burnout. Off the rail, the rocket is a rigid body free to move and turn in every direction, the six degrees of freedom of a 6-DOF simulator. At every instant hpr adds up the forces and their turning effects:
- the thrust of each burning motor, along the rocket’s axis. So a cluster of motors that all light at once is flown, with their thrusts added, and a motor off the center line adds a turning effect, as when one motor of a cluster fails to light (Clusters). Tests check it; no other simulator has yet;
- the weight and the Coriolis force, at the center of gravity;
- the air’s forces, from the air’s velocity past the rocket, wind included: the drag along the axis, and each body part’s and fin set’s normal force at its own center of pressure;
- the burning propellant’s effects: the center of gravity moving inside the rocket, jet damping (the exhaust carrying away some of any turning motion), and the propellant’s internal momentum, which is counted twice (see What is left out).
From these it works out how the rocket speeds up and turns (Rigid-body flight). A crosswind meets the rocket partly from the side, and the fins’ normal force, behind the center of gravity, swings the nose into it, so the rocket climbs upwind (weathercocking). The top speed usually comes just before burnout, the end of the last motor’s thrust curve, once the thrust no longer beats the drag and the weight.
-
Coast, to apogee. The same equations with no thrust: drag and gravity slow the climb. Apogee is where the vertical speed falls through zero.
-
The drogue. Each recovery device fires its charge at its trigger: apogee, a height on the way down, a time, or a motor’s ejection delay. After its lag, a set time from the charge to its lines stretching, it deploys. From the first deployment the rocket is a single point with mass: its attitude (which way it points) freezes, and it falls under gravity and the open devices’ drag while the wind carries it along, which is its drift.
-
The main. A second device, typically the main set to a height above the ground, adds its drag to the drogue’s. A device can also cut another away as it opens, and a rocket can separate into parts that each come down on their own. Streamers and tumbling are recovery devices too (Recovery).
-
Landing. The flight ends when the center of gravity comes back down to the launch site’s height. The ground is flat, at the height of the pad: there is no terrain.
A flight can also end in other ways, and hpr reports each by name (Rigid-body flight):
| ending | what happened |
|---|---|
| on the pad | the motors burnt out before the rocket lifted off |
| stalled on the rail | it lifted off, then stopped on the rail after the motors burnt out |
| time cap | the flight reached its time limit, 3,600 s (an hour) after ignition by default, before landing |
| step limit | the integrator (below) used up its budget of steps, a million attempted steps by default, before landing |
| separated | the rocket split into parts, and each part’s own descent says where it landed |
The time cap and the step limit are safety stops, so that a flight which doesn’t land still ends.
A stiff stretch of flight, one that forces very short steps, can show
up as the step limit (Time integration). Both limits
are fields of FlightSettings (max_time_s and step_limit), and a program can change them.
Anything else that stops a flight is an error: reaching Mach 5, for example, the top of the speeds hpr’s normal force and drag cover.
How hpr steps through time
At any instant, the rocket’s state is 13 numbers: where it is (three), how fast it moves (three), which way it points (four, as a quaternion, a compact way to store a rotation) and how fast it turns (three). The equations above turn a state into its rate of change. An integrator builds the flight from them by stepping forward in time, one short step after another (Time integration).
- Steps that size themselves. hpr’s default method is Dormand–Prince 5(4). On each step it makes two estimates of the new state, one of fifth order and one of fourth; that is the “5(4)”. The higher a method’s order, the faster its error shrinks as the step gets shorter. hpr keeps the fifth-order estimate, and takes the difference between the two as the step’s error. It sizes the next step to keep that error within a tolerance. Steps are short where things change fast, at liftoff and burnout, and long in a steady descent (adaptive time step). At the default settings, the Getting started flight’s apogee is within about a micrometer of the answer at much tighter settings, and a whole flight takes about a millisecond of computing (Rigid-body flight).
- Stop times. Moments known in advance where a force changes abruptly, such as each point of the thrust curve and burnout, are stop times: a step always ends exactly there, so none straddles a jump.
- Events. Moments found during the flight, such as liftoff, rail exit, apogee, an altitude trigger and landing, are events. When the quantity that defines one changes sign within a step (the vertical speed, for apogee), hpr finds the instant it crossed zero inside that step.
- What you get back. Every event, with a snapshot of the flight at that instant: time,
position, velocity, height, airspeed, Mach number, angle of attack, thrust, mass and more. A
program can also watch every step as it happens, as
Getting started does to find the top speed, or
record chosen quantities at a fixed interval with a
Recorder(Recording a trajectory).
What is left out
Each model page lists what its model leaves out. These are the gaps that matter most for a whole flight:
- Near and past Mach 1, the drag is lightly checked. The normal force, center of pressure and drag all carry a flight from Mach 0 to 5. The normal force was checked against a wind tunnel to Mach 4.63 (Aerodynamics). The drag was checked at Mach 0.3 against other programs’ curves, and against the same wind tunnel from Mach 0.6 to 4.63, where it reads high at most speeds, most of all with fins past Mach 1 (Aerodynamics). Near and above the speed of sound it is Niskanen’s semi-empirical method (formulas fitted to measurements), not yet compared with RASAero II’s (M1.8b2, the drag against RASAero II).
- Large angles of attack. The aerodynamics are for small angles, with no stall, but a flight uses them at every angle: just off the rail in a strong crosswind, and near apogee.
- Staging, clusters and air starts are checked against OpenRocket on three of its examples. Each motor lights at its own time, and a sustainer flies on after a powered separation (Staging). OpenRocket’s two-stage, cluster and air-start examples, 12 flights, are each within 5% of OpenRocket’s apogee and largest speed. Three cluster apogees are compared with OpenRocket’s flight with no parachute, since its parachute opened before apogee (M1.9c, a two-stage and a cluster design against OpenRocket). A motor that fails to light in a cluster is checked by tests only (Clusters). A rocket that separates more than once is not compared yet, and neither is one that ejects a nose cone or a payload to land on its own: those pieces are checked against exact answers only (Recovery: ejected pieces).
- Moving and released mass are checked against exact answers only. Ballast or a payload that slides along the airframe in flight, or leaves it, is compared with hand calculations and conservation laws, not with another simulator or a real flight (Moving mass, Released mass).
- Tip-off, thrust misalignment (a motor pushing slightly off the rocket’s axis) and turbulence, which no milestone plans yet. Roll from canted fins and roll damping are modeled, and checked against measurements only from Mach 1.5 up (Roll: forcing and damping).
- One term counted twice. A thrust curve measured on a test stand already includes the propellant’s internal momentum, and the equations of motion add it again, as RocketPy’s do. hpr keeps it so that the two codes can be compared like for like. On the Getting started rocket it adds 21 N to the push at liftoff and changes the burnout speed by at most 0.05 m/s (Rigid-body flight).
- Under a parachute: the drag overshoot as a canopy fills, so the opening load hpr reports is no safe bound (by default a canopy opens at once); the air carried along with it (added mass); the airframe’s own drag; and the rocket swinging below the canopy (Recovery).
- Terrain. The ground is flat, at the pad’s height.
Accuracy
This page gathers every check hpr-sim has passed so far, and every known gap, in words and numbers. Start with the bottom line: when both codes fly the same drag (same-drag), hpr’s whole flights match RocketPy’s in height, speed and time, and in where they land without wind. In wind they agree for a rocket that leaves the rail fast. For one that leaves it slowly they differ, in large part because hpr includes a sideways force on the body that RocketPy leaves out. With each code’s own drag (predicted), hpr’s heights differ from RocketPy’s by −7.280% to +10.302% (report), the larger gaps where its drag differs most from the example’s. Against the logs of seven real flights, hpr’s apogees miss by 6.04% on average, outside the 5% target, and by −8.90% to +10.40% one by one (real flights, report); four of the five misses past 5% are consistent with hpr’s drag, and one with the motor’s impulse. Four of those seven altimeters’ kinds are assumed, and none is calibrated. Against 55 more flights from a private collection, hpr’s apogees are +9.83% above the logs on average and miss by 13.96% (mean absolute), also outside the target; OpenRocket’s are +9.00% above (private collection, report).
What has been checked so far:
- each model on its own, against exact answers, its published source and, in places, RocketPy, an open-source flight simulator;
- the descent under a parachute, against RocketPy, for five rockets;
- whole flights from the pad to the ground, against RocketPy, for six rockets flown with one declared drag coefficient, one of them past Mach 1: heights, speeds and times agree, and so does the path, except for rockets that leave the rail slowly in a wind;
- the same flights with each code’s own drag, reported against a target rather than gated, the one past Mach 1 included;
- seven real flights, flown in the weather of their day and compared with their altitude logs: the apogee and the climb to it (real flights);
- 55 more real flights of a private collection, flown by hpr and OpenRocket in the weather of their day and compared with their logged apogees as aggregates (private collection, report);
- the normal force and center of pressure from Mach 0.6 to 4.63, against NASA’s wind-tunnel tests of a sounding rocket, and against RASAero II, another code (fixture);
- drag from Mach 0.6 to 4.63 against the same wind-tunnel tests, the forebody only (Aerodynamics);
- a boattail’s own drag and the base pressure behind it, from Mach 0.3 to 3.24, against measured boattails in six NACA and NASA reports (drag fixture);
- the roll that canted fins give, from Mach 1.5 to 4.63, against NASA’s Arcas Robin wind-tunnel tests, and the roll damping from Mach 1.5 to 3 against the Basic Finner’s, a standard finned test body (roll fixture);
- drag from Mach 0.1 to 2.0 against the curves labelled RASAero II in RocketPy’s example rockets (Aerodynamics), and from Mach 0.5 to 3.2 against a worked example in MIL-HDBK-762, the U.S. Army’s handbook for designing unguided rockets (Aerodynamics);
- the readings
hpr analyzetakes from a flight log, against an invented log whose every reading is known, each within the bound its rounding and filter allow (Flight-log readings).
Every number here links to the page or file it comes from. Checking a claim shows how to follow one back to its source and its test, and how the site keeps the two in step.
The census
In short: the census counts the numbers the validation reports hold hpr to, once each, and says what each group was compared with, how, how many flights it holds and how fast they flew. It is also a check: CI fails when any of those numbers moves, better or worse, until the change is accepted with a written reason. A code-to-code line measures how closely hpr agrees with another simulator, not which of the two is right; only the real flights are measurements.
| compared with | kind | held to | flights (speed) | result |
|---|---|---|---|---|
| RocketPy 1.13.0: descents under a parachute | code-to-code, same inputs | gate: each metric’s tolerance, at most 3% | 5 descents | 30 of 30 gated metrics pass |
| RocketPy 1.13.0, patched: whole flights on the same drag | code-to-code, same inputs | gate: each metric’s tolerance, at most 3% | 9 flights (8 subsonic, 1 transonic) | 142 of 142 gated metrics pass; apogee +0.04% to +1.21%; 11 not scored, each for a written reason |
| RocketPy 1.13.0, patched: whole flights, each code on its own drag | code-to-code, each code’s own drag | target: 3% on each metric, reported, not enforced | 6 flights (5 subsonic, 1 transonic) | 75 of 102 metrics within target; apogee -7.28% to +10.30% |
| OpenRocket 24.12: calm flights of OpenRocket’s examples | code-to-code, each code’s own model | no target; an apogee more than 5% off needs a written cause | 54 flights (53 subsonic, 1 transonic); 3 more not flown | apogee -37.93% to +13.80%; apogee within 5% on 45 of 53, largest speed within 5% on 50 of 53, margin within 0.5 calibres on 52 of 53, launch mass within 1% on 54 of 54, mass at rod clearance within 1% on 50 of 54, center of mass at rod clearance within 0.5 calibres on 54 of 54; 3 values withheld by the report |
| OpenRocket 24.12: calm flights of the private designs | code-to-code, each code’s own model | no target; an apogee more than 5% off needs a written cause | 35 flights (28 subsonic, 6 transonic, 1 supersonic); 2 more not flown | apogee -4.84% to +13.60%; apogee within 5% on 34 of 35, largest speed within 5% on 34 of 35, margin within 0.5 calibres on 35 of 35, launch mass within 1% on 35 of 35, mass at rod clearance within 1% on 35 of 35, center of mass at rod clearance within 0.5 calibres on 35 of 35 |
| the teams’ altimeter logs: real flights | measured | target: mean absolute apogee error 5% | 7 flights (3 subsonic, 4 transonic) | mean absolute apogee error 6.04% (target 5%, missed); apogee -8.90% to +10.40%; apogee within 5% on 2 of 7, climb’s RMS height error within 3% of apogee on 2 of 7 |
Reading the table.
- Compared with names the reference and its version (census): RocketPy, “patched” where RocketPy 1.13.0 runs with two fixes from RocketPy’s own pull requests, which correct the point it takes the burn’s turning moments about (see whole flights against RocketPy), OpenRocket, or the teams’ altimeter logs.
- Kind says whether both programs flew the same inputs, each flew its own model, or hpr was compared with a measurement.
- Held to is what each report holds its numbers to (gate and target). A gate fails the run when a number falls outside it. A target is reported: missing it never fails the run. The OpenRocket comparisons have neither, only a line past which an apogee needs a written cause.
- Flights (speed) classes each flight by its largest Mach number: subsonic below 0.8, transonic from 0.8 to 1.2, supersonic above 1.2 (census). For the harness’s flights that is RocketPy’s largest Mach number, for OpenRocket’s flights OpenRocket’s, and for the logged flights hpr’s own, since a log has none. A configuration an OpenRocket report lists and hpr doesn’t fly yet is counted as “not flown”, with the report’s reason.
What it counts. Each of these is one row of the census (census):
- every metric of every case in the harness’s report, and each known gap;
- each real flight’s apogee and the RMS of its climb;
- each OpenRocket flight’s apogee, largest speed, stability margin, mass at launch and at rod clearance, and center of mass there, and each configuration not flown.
It leaves out the figures the reports give to explain a difference rather than to measure one, such as a real flight’s apogee on its team’s own drag.
The check. The census accepted last is committed, with a page of its own
(census). cargo xtask validate --check, which CI runs on macOS, Windows and Linux,
holds every row of the committed reports to it. To run it yourself, clone the repository and run
cargo xtask validate --check. It fails when:
- a row’s difference moves by more than its slack, in either direction;
- a row’s standing changes, for instance a miss that starts meeting its target;
- a row comes or goes, for instance a flight hpr used to refuse and now flies;
- a group’s reference changes, such as a new version of OpenRocket.
A row’s slack is 0.1% of its scale, and for a harness row never less than the harness’s reproduction bound, 2e-6 in the metric’s own unit or 1e-7 of the value, whichever is larger. The scale is the row’s own tolerance where it has one, and otherwise the bar of its kind: 3% of its reference for a harness metric not scored, 5% for an OpenRocket apogee or largest speed, 1% for an OpenRocket mass, 0.5 calibres for a stability margin or center of mass, 5% for a logged apogee, and 3% of the apogee for a logged climb’s RMS, the bound the RocketPy comparisons hold a whole flight’s height RMS to (census). For example, a 3% tolerance on a 1000 m apogee allows 30 m, and the census lets the difference move by 0.03 m before it fails.
So the census holds a number to where it was, not to its target. A flight on each code’s own drag, whose 3% is only a target, is held that way too (census): hpr’s own aerodynamics can’t drift unseen inside the target.
Accepting a change. cargo xtask census --accept --reason "<why>" writes the new census and
the table above. The reason and the list of what changed go into the census page, so a worse number
can still merge, but only in writing, in the change that brings it. An improvement has to be
accepted too; otherwise it could slip back later without anyone seeing.
How far to trust it. The census adds no evidence of its own: it is exactly as good as the reports it counts. CI flies the harness’s RocketPy comparisons again on every change. The OpenRocket and real-flight reports need files that CI doesn’t have, so CI holds their committed numbers, and a change to the code that would move them is caught only when someone runs them again and commits the result.
How to read the numbers
- Powers of ten. Very small and very large numbers are written the way programs print them.
The number after the
esays how many places the decimal point moves, to the left when it is negative. So 1e-12 is a millionth of a millionth, a 1 in the twelfth decimal place, and 1e6 is a million. Both appear in the two Frames rows of the results table below. - Absolute differences carry a unit: they say how far apart two values are, in that unit. Geodesy’s round trips return heights within 2e-8 m, twenty billionths of a meter.
- Relative differences are marked relative: the difference as a fraction of the value it is compared with. Gravity within 2e-14 relative means within two parts in a hundred million million. A percentage is a relative difference counted in hundredths.
- Signs. A signed difference is hpr’s value less the one it is compared with, over that one. A plus means hpr’s value is the larger in size, a minus the smaller.
Four kinds of evidence
A model can be checked in four ways, from the weakest to the strongest evidence that it matches reality. They are set out in the project’s validation plan.
| kind | what is compared | what agreement shows |
|---|---|---|
| Analytic | The code against exact answers: formulas solved by hand (closed forms), conservation laws, and round trips (converting a value and converting it back) | The code computes what its equations say |
| Published source | The code against a source’s printed tables and worked examples | The code implements the source correctly |
| Another code | hpr against another simulator, such as RocketPy, flying the same inputs | The two codes agree on the physics; not that either matches reality |
| Real flights | hpr against measured flights | The model matches reality, within the flight’s own uncertainty, which here is not measured |
The first three check the code. Only the fourth checks the physics against the world. Seven flights have been compared so far, each as a whole: the apogee and the climb to it, not each model on its own (real flights). Besides them, two recovery models are checked against published drop tests (Recovery), and the normal force against NASA’s wind-tunnel tests of the Arcas Robin sounding rocket (Aerodynamics): measurements, but not flights.
Where each model stands
A tick means the model has been checked that way; a “no” means it hasn’t yet. In the descents only means RocketPy’s air density and wind were compared with hpr’s at just the 23 heights its parachute descents sample, as part of that comparison, and nowhere else (Recovery).
| model | analytic | published source | another code | real flights |
|---|---|---|---|---|
| Frames | ✓ | no | ✓ RocketPy | no |
| Geodesy | ✓ | ✓ | no | no |
| Gravity | ✓ | ✓ | ✓ RocketPy | no |
| The magnetic field | no | ✓ | no | no |
| Atmosphere | ✓ | ✓ | ✓ RocketPy, in the descents only | partial: its pressure altitude against two altimeters’ readings of their own pressure, to 0.195 m and 1.321 m (report) |
| Wind | ✓ | no | ✓ RocketPy, in the descents only | no |
| Turbulence | ✓ | no | no | no |
| Design tree | ✓ | no | ✓ RocketPy, mass properties only | no |
| Shapes | ✓ | no | no | no |
| Mass properties | ✓ | no | ✓ OpenRocket, the structure without motors, and the parts of its parts catalog | no |
| Solid motors | ✓ | no | ✓ RocketPy, ThrustCurve.org; OpenRocket for a curve file’s own numbers | no |
| Aerodynamics | ✓ | ✓ Barrowman’s examples; MIL-HDBK-762’s drag example, fins left out: 6 of 12 within 10%, the body reading 6% to 10% low faster than sound | partial: drag and the normal force against RASAero II to Mach 2, the drag with the fins and finish guessed and 5% to 15% low faster than sound; and in whole flights, against a target | partial: in seven flights, four of the five apogees past 5% are consistent with its drag (real flights, report); wind tunnel ✓: normal force, drag and boattails; the Arcas Robin’s drag reads high at every speed |
| Rigid-body flight | ✓ | no | ✓ RocketPy, with the drag given; and on each code’s own drag, against a target; OpenRocket on 53 configurations of its examples (results) | partial: seven logged flights, as a whole: apogees 6.04% off on average, outside the 5% target (real flights, report) |
| Time integration | ✓ | no | no | no |
| Recovery | ✓ | ✓ | ✓ RocketPy | no (drop tests ✓) |
| Staging | ✓ ignition times, the mass step and momentum, against hand sums | no | ✓ OpenRocket: its two-stage, three-stage, cluster and air-start examples, every flight within 5% (three cluster apogees against OpenRocket’s flight with no parachute, because its parachute opened before apogee) (results) | no |
| Moving mass | ✓ the mass properties and the static margin against hand calculations, and both momenta kept in free flight | no | no | no |
| Released mass | ✓ the mass properties after a release against hand calculations, and mass and both momenta kept across it in free flight | no | no | no |
| Flight metrics | ✓ the boost’s peak, the margins of the page’s finless rocket with a boattail (including where a margin is withheld) and the landing placement against hand calculations; max q and top Mach against a 1 ms record; the least margins against 1 ms steps and a scan of every step; the flight margin and the optimum delay against hpr’s own models and a flight with no recovery | no | no | no |
| Fin flutter | ✓ the scaling in thickness, stiffness and pressure, and the margin at max q against a 1 ms record | ✓ Martin’s formula and both of his worked examples reproduced, with his verdicts against his figure 3 band; not compared with flutter data | no | no |
| Flight-log readings | ✓ each reading of an invented flight within the bound its rounding and filter allow | no | partial: one public altimeter log’s apogee, read at 1,010 ft against the 1,009 ft the altimeter’s software states from the same trace; checked only where the log is fetched, not in CI | no |
| Interpolation | ✓ | no | no | no |
| Quadrature | ✓ | no | no | no |
Results by model
The headline results, one check to a row. Each model page opens with In short, and its verification section lists every test and its tolerance, how far a result may be from its reference and still pass.
| model | compared with | how close |
|---|---|---|
| Frames | RocketPy’s starting attitude, worked out from the launch rail’s angles, for 8 rail setups | within 1e-12 rad |
| Frames | an exactly solvable spin whose axis sweeps round a cone (coning), over 1e6 integration steps | attitude within 1e-9 rad |
| Geodesy | the ellipsoid values printed in Table 3.5 of the WGS 84 standard | to their printed digits |
| Geodesy | Karney’s published test set of 500,000 WGS 84 geodesics, solved both ways | all 500,000 within Karney’s 15 nm (nanometers): distance within 11.18 nm, far point within 14.02 nm, heading there within 13.99 nm; the computed bearing and distance, flown forward from the first place, land within 11.26 nm. CI checks every 500th line and the 21 mirror lines; the whole set where downloaded, measured on macOS |
| A launch site’s elevation from a file | GDAL 3.12.2’s reading, through rasterio 1.5.2, of seven GeoTIFF files cut from a USGS tile and of the whole tile (the tests) | the same corner and pixel size to the last bit, the same sums over all 13 million pixels, and the same pixel and value at all 2,368 of 2,800 places on the seven files (9 of them nodata) and all 1,696 of 2,000 on the tile, the rest off the edge; the whole tile only where downloaded |
| Geodesy | round trips at random points from −10 km to +1000 km: latitude, longitude and height to Earth-centered x, y, z, and back | latitude within 1e-14 rad, height within 2e-8 m |
| Gravity | the WGS 84 formulas, worked to 40 digits by a separate script, at 11 points from the equator to both poles and up to 200 km high | gravity’s strength within 2e-14 relative |
| Gravity | RocketPy’s gravity formula, at 8 points | under 1e-12 relative |
| The magnetic field | NOAA’s test values for WMM2025: the report’s 12 test points and NCEI’s 100 high-precision points | the 12 to their last printed digit; the 100 to their last digit in declination, inclination, the east component and four rates. The north component differs at 97 points by up to 7.18e-4 nT, and the horizontal and total intensities by up to as much with it; the down component, moved by the same residue, by up to 2.2e-6 nT, and the rates of the north, horizontal and total components by up to 1.5e-6 nT a year. A test places the north component’s difference in NCEI’s file; the rates’ difference has no known cause |
| Atmosphere | the tables of the 1976 standard atmosphere, at 32 heights from −2 to 86 km | every value within 0.1% |
| Atmosphere | CIPM-2007 (Picard et al., 2008), a published reference formula for the density of humid air that treats air as a real gas, from 15 to 27 °C | humid-air density within 0.047% |
| Atmosphere | RocketPy’s air density, at the 23 heights its descents sample (Recovery) | within 3.7e-4 relative |
| Wind | RocketPy’s wind, at the same heights (Recovery) | each component within 1e-9 m/s |
| Wind | the drift of RocketPy’s four descents with wind (Recovery) | the distance drifted within 0.28% |
| Turbulence | the Dryden gust spectra, published formulas for how gust strength spreads over wavelength, over 2²⁰ random samples (about a million) | within 4 standard errors (the scatter expected by chance) in every octave band (a range of wavelengths spanning a factor of two): ±1–3% in the wide bands. Unvalidated for rockets |
| Design tree | a rocket worked by hand, loaded, burning and burnt out | mass within 1e-12 kg, center of mass within 1e-12 m, inertia within 1e-12 relative |
| Design tree | RocketPy, for eight cases of its example rockets, at the times its equation solver computed the burning grains (up to 60 per case) | mass, center of mass and inertia within 8.0e-10 relative (the center as a fraction of the rocket’s length) |
| Design tree | the same, at 103 even times through the burn and after it, where RocketPy interpolates between its solver’s times | mass within 1.3e-5 relative, inertia within 2.6e-5 relative |
| Design tree | the propellant mass left in the grains, at both sets of times | within 2.4e-9 of the initial propellant mass at the solver’s times, and 4.9e-5 between them |
| Shapes | exact formulas (closed forms) for filled noses and transitions | within 1e-10 relative |
| Shapes | separately computed high-precision integrals, for 22 noses and transitions | within 1e-12 relative |
| Shapes | the same kind of integrals, for 20 hollow shells of a given wall thickness | within 1e-10 relative |
| Mass properties | a cone, a tube, four fins and an off-axis payload, added up by hand | within 1e-11 relative |
| Mass properties | fin cross-sections, against exact numerical integration | within 1e-13 relative |
| Mass properties | material densities, converted from the units their sources print | the sources’ values, such as white ash at 678 kg/m³ |
| Mass properties | OpenRocket 24.12’s structure (every stage, no motor), on 71 compared designs in the current scratch-excluding survey | mass within 1% on 70 and center of mass within 1% of length on 70, the one file outside with a named cause: airfoil fins OpenRocket weighs by a factor, a cause named only when weighing them its way brings the design within both thresholds; pitch inertia within 1% on 58, the 13 outside with no named cause yet but the two copies of OpenRocket’s tube fin example (below); roll inertia a median 1.619% apart, which OpenRocket’s shortcut for fins accounts for, and on the cluster designs its stacking of their tubes on the axis (clusters): with the shortcut in hpr’s place the median is 0.001%, and 12 files (8 distinct designs) remain outside 1% with a named cause. A ring of tube fins departs in both inertias, kept on purpose: OpenRocket’s roll inertia for it is more than any mass inside the ring could have, and its pitch inertia leaves out how far the tubes sit from the axis, which accounts for the tube fin example’s pitch inertia being 1.98% below OpenRocket’s (tube fins). hpr’s airfoil fins are 19.4% lighter than OpenRocket’s, a departure kept on purpose (fins). What a file leaves unsaid (a wall of no thickness, no material), which override wins, held to OpenRocket’s on 32 probe designs, and each fin section and each kind of part alone on 50 more, packed parts among them (packed parts). Fin fillets, on 9 of those, agree to 1e-15 in the fillets’ mass and center of mass (the test holds 1e-12), the whole probe’s pitch inertia up to 0.64% apart (fillets). An automatic radius inside a nose cone, on 14 more (13 of which hpr flies), puts every part inside but one packed mass component (#186) at OpenRocket’s mass to 1e-14 and station to 1e-15, as the test holds them; the nose cones’ and the transition’s own walls keep their earlier gaps, up to 3.9e-5 of mass and 2.5e-6 m (the format guide). A tube fin set whose radius OpenRocket works out from the body, on 19 more, has OpenRocket’s radius and wall within 1e-15 and every part’s mass within 1e-14 but the one nose cone among those probes, within 5e-5 (the format guide). On those that ask what a file leaves unsaid: mass within 0.001%, center of mass within 0.001 mm, bar an elliptical fin’s 0.18%, and an attached tube that writes no thickness (−2.4% on the committed probe; measured in ADR-061, the .ork conventions decision). On the override probes: two rules kept as measured departures, and flags that disagree (4.7 mm) (probes) |
| Mass properties | OpenRocket 24.12’s mass and center of mass of the parts in its parts catalog that the builder makes (all 3,449 but four it refuses) (test, The builder) | tubes, rings, bulkheads, lugs, parachutes and streamers within 1e-14 of the mass; nose cones and transitions within 1e-3 of the mass, and their center of mass within 1e-3 of the part’s length (largest 6.3e-4 and 9.7e-4); but four blunt hollow nose cones, up to 4.8e-3 heavier here and 1.7e-3 of their length apart in center, where the two codes define a wall differently; masses stated in ounces, 8.8 parts in 10 billion apart (OpenRocket’s rounded ounce); and one streamer, whose stated mass OpenRocket ignores. A hollow part’s shoulder weighs nothing in OpenRocket and has the part’s wall here, and is taken out before comparing |
| Solid motors | ThrustCurve.org’s own statistics code (total impulse, burn time, average and peak thrust), on all 32 bundled curves | within 1.8e-15 relative |
| Solid motors | OpenRocket 24.12’s own reading of the same 32 bundled curve files (total impulse, peak thrust, the 5%-of-peak burn-time window and the curve’s duration) | every one bit for bit equal. Its average thrust divides the window’s own impulse by the window where hpr divides the whole curve’s, so hpr’s is +0.0107% to +0.3147% higher (median +0.0965%). Nothing else in the motor model is compared with OpenRocket |
| Solid motors | RocketPy’s solid-motor model, on three bundled motors, at 203 times each | total mass and inertias within 7.9e-5 relative; the propellant’s own mass and inertias within 1e-4 of their values at ignition |
| Aerodynamics | Barrowman’s five worked examples, at Mach 0 (low speed): each rocket’s normal-force slope and center of pressure | every center of pressure within 1%. Every slope within 1% too, except the six-fin Recruiter’s: +2.87% (+3.42% on its fins alone) |
| Aerodynamics | drag curves labelled RASAero in RocketPy’s examples, at Mach 0.3, with the fins and surface finish guessed because the curves don’t record them | within 10% in four of seven cases; −18.3% for Cavour power-on (motor burning), cause open |
| Aerodynamics | Valetudo’s drag table, which is 1.44 times the drag in the OpenRocket export for the same rocket | −47.0% power-off and −50.4% power-on. Against the OpenRocket export, hpr is 23.5% under as designed here, and 1.9% under with the export’s own surface finish and launch lugs |
| Aerodynamics | the same curves every 0.05 from Mach 0.1 to 2.0, as far as each reaches; Calisto’s, the one real RASAero II export, to Mach 2 | Calisto within 10% at 15 of 15 subsonic Mach numbers, 3 of 7 transonic and 8 of 17 supersonic, where hpr reads −14.9% to −5.1%, lowest at Mach 2 (−29.8% to −24.4% before the boattail’s supersonic wave drag). Other plausible fins put 14 to 17 of the 17 within 10%, though none puts every row within it, so most of what is left is within the unrecorded inputs; part of it is hpr’s body, which reads low faster than sound against MIL-HDBK-762 too |
| Aerodynamics | MIL-HDBK-762’s worked drag example, a rocket whose every term the handbook calculates, from Mach 0.5 to 3.2: a calculation with every input known, not a measurement. Its fins are sharp-edged wedges, which hpr can’t represent, so their pressure drag is left out on both sides | 6 of 12 within 10%. From Mach 0.9 to 1.2, +12.3% to +31.9%, mostly the nose and the base; from Mach 1.6, −6.0% to −9.6%, friction and the base |
| Rigid-body flight | the exact motion of a tumbling, spinning rocket in a vacuum, over 22 s | the center of mass within 1.7e-6 m of the exact parabola |
| Aerodynamics | NASA’s wind-tunnel tests of the half-scale Arcas Robin and a longer version, Mach 0.6 to 4.63: normal-force slope and center of pressure, 22 readings at 12 Mach numbers (fixture) | from Mach 1.5, every slope within target: +8.8% to −3.3% (short) and +9.4% to −2.3% (long), since the committed designs fly the shock-expansion method to their base; the center of pressure within 0.53 calibres, which misses the half-calibre target on the long model at Mach 1.8 and 2.3, where that model’s body alone reads 15% to 19% high (fins off, the short model’s reads as much as 38% high at Mach 1.5: the crossflow excess sized by M1.8e6); from Mach 0.8 to 1.2, 2 of 9 within 15% and half a calibre |
| Aerodynamics | RASAero II’s normal-force slope and center of pressure for Calisto, Mach 0.1 to 2.0 (fixture) | within 15% and half a calibre at 11 of 15 Mach numbers; hpr’s slope rises with Mach through subsonic flow where RASAero II’s stays flat (+21.9% at Mach 0.9), and from Mach 1.5, where its von Kármán nose flies the shock-expansion method behind a Newtonian cap, reads +8.3% to +12.9% |
| Aerodynamics | the body faster than sound, by a method no flight uses yet: the tables of NACA TN 3527, its source, for 144 cone- and ogive-cylinders at Mach 3 to 6.28, both its authors’ own values and their wind-tunnel measurements of the normal-force slope and center of pressure (fixture) | against the measurements, 117 of 120 slopes within ±0.2 per radian (−0.278 to +0.251) and 109 of 120 centers of pressure within 0.2 calibres (−0.540 to +0.328); against the authors’ values, 102 of 144 slopes within 0.05 (−0.134 to +0.146) and 125 of 144 centers of pressure within 0.1 calibres (−0.670 to +0.257), so the targets are not met, where a separate implementation of the same equations, an uncommitted script, agrees with hpr outside the 12 rows at the method’s limit (#81) |
| Aerodynamics | the same method on the Arcas Robin’s nose and cylinder, against its measured body alone, Mach 1.5 to 4.63, 11 readings; no target, since the measurement includes the boattail and crossflow (fixture) | the short model within 5% from Mach 1.8 to 2.96, +16.4% at 1.5, −15.0% and −18.7% at 3.96 and 4.63; the long model −13.7% to −26.4% |
| Aerodynamics | a blunt or vertical nose tip’s Newtonian cap ahead of the same method: NASA TN D-4865’s sphere-cone (a nose radius of 0.175 diameters on an 11.5° cone), its measured normal-force slope and center of pressure at Mach 1.50 to 4.63, 6 readings; and the Arcas Robin’s committed power-series nose on its cylinder and boattail, the lip left off, against its measured body alone, 11 readings; no target (fixture) | the sphere-cone like for like (at its plotted angles, body lift included) −1.2% to +32.1%, high from Mach 2.96, where the report’s own method reads −3.3% to +13.6% overall, and the center of pressure within 0.06 diameters; the Arcas Robin like for like −4.8% to +37.2%, below the fitted secant ogive’s +3.4% to +41.0% at every Mach number and nearer the tunnel at nine of its eleven rows. No measurement checks a tip that isn’t spherical, and a nose that is nearly a cone but for a vanishing tip carries an unmeasured bias (issue #101) |
| Aerodynamics | the lip at the Arcas Robin’s base, a reflexed flare behind its boattail, which hpr gives no normal force faster than sound: what the measured fins-off pitching moment says the lip’s share is, 11 readings at Mach 1.5 to 4.63; no target (fixture) | one share fitted to every row is +0.021 ± 0.019 per radian, so slender-body theory’s 0.178 sits 8.4 standard errors above it; fitted to each model alone it is −0.016 ± 0.022 (short, whose six rows disagree among themselves, χ² per degree of freedom 6.4) and +0.108 ± 0.034 (long, whose five agree, 0.9). The number blames the lip for every miss in the center of pressure, so it rejects slender-body theory’s 0.178 but cannot separate zero from Seiff’s embedded Newtonian bound (0.044 down to 0.014) |
| Aerodynamics | NASA TN D-4865’s model 2, the one flared body in the sources whose normal force and pitching moment are printed: a blunt 2.75° cone with an 18.5° flare, Mach 1.50 to 4.63, six readings, three of them with its boundary layer separated; no target (fixture) | like for like (at its plotted angles, body lift included) the normal-force slope −1.9% at Mach 1.90, +7.0% at 2.30, +13.4% at 2.96 and +51.5% and +50.4% at 3.95 and 4.63, with the center of pressure within 0.05 calibres through Mach 2.96 and 0.088 at 3.95. The report’s shadowgraphs show that flare’s boundary layer separated from Mach 2.96 up, though the cost only shows in the two fastest rows: the unflared model 1 reads +29.7% and +32.1% there against +12.5% at 2.96, so the flare itself adds 21.7 and 18.3 percentage points at 3.95 and 4.63 and between −1.9 and +0.9 below. Below about Mach 1.5289 there is no marched reading at all and a flared body falls back to slender-body theory, carried up over 0.3 Mach by the join: drawing the flare out to the steepest turn its shock holds lands past the steepest the march itself takes |
| Aerodynamics | the Arcas Robin’s body alone, fins off, against the 15% target the milestone set before the work began, 11 readings at Mach 1.5 to 4.63, the same bodies as the row above, with the lip in place, which moves them under a point; the boattail cap does not reach these, whose boattail is 15° (fixture) | not met on six rows: the short model +37.7% at Mach 1.5, +25.9% at 1.8, +16.9% at 2.3 and 2.96, and the long +19.4% at 1.8 and +15.5% at 2.3; at Mach 3.96 and 4.63 both are within 5%. Where the miss sits can only be told so far: the measurement’s own fit trades its slope at α → 0 against its curvature at a correlation of −0.96. At α → 0 hpr is within 1.5 standard errors on every row outside, and on five of the six most of the gap is in the curvature body lift adds (1.3 to 2.8 times the measured, and ×12.20 at Mach 1.8 where the tunnel’s curve barely bends); on the sixth, the short model at Mach 2.96, 77% of the gap is hpr’s own α → 0 slope |
| Aerodynamics | NASA’s wind-tunnel tests of the same two models, Mach 0.6 to 4.63: drag on the forebody (the models’ bases sat on a sting), fins on and off, 44 readings (Aerodynamics) | 2 of 44 within 10%, and every reading high. With the fins off, +13.5% to +24.1% from Mach 1.5 and +12.0% to +54.1% below, most of it the models’ 15° boattail, which hpr over-predicts in a thick boundary layer. With the fins on, from Mach 1.5, +39.4% to +154.0%, where hpr’s fins’ drag stays near 0.30 and the measured falls to 0.046. hpr’s base drag behind a plain cylinder is not measured by the tunnel and has been checked at no speed faster than Mach 0.3 |
| Aerodynamics | NASA’s wind-tunnel tests of the two Arcas Robin models, Mach 1.5 to 4.63: roll forcing, the rolling moment per degree of cant, 11 readings (roll fixture) | from Mach 2.3, all 8 within 5.3%; at Mach 1.5 and 1.8, +14.3% to +47.8% |
| Aerodynamics | The Basic Finner’s roll damping measured in a wind tunnel, Mach 1.5 to 3.0, and Barrowman’s computed value at Mach 0.07 (roll fixture) | −5.9% to −16.2% against the wind tunnel, lower as the Mach number grows; −2.0% against Barrowman’s computed value (a theory curve, not a measurement; it confirmed how hpr reads his method) |
| Aerodynamics | conical boattails measured in six NACA and NASA reports, jet off, Mach 0.3 to 3.24: the boattail’s own pressure drag and the base pressure behind it (drag fixture) | attached boattails of 3° to 10° from Mach 1.2: −21.9% to +28.3%, within 0.0123 (58 readings); from Mach 1.0 to 1.1, −18.2% to −5.4% (4), and −46.2% to +60.0% at points near Mach 1 their report calls questionable (27); through the rise from Mach 0.85 to 0.95, −77.5% to +7.6% (28); under Niskanen’s rule to Mach 0.8, −100% to −83.5% (58). 16° in a boundary layer a fifth of the diameter thick, +26.4% to +54.2% (9). Separated 30° and 45°, −2.8% to +6.6% (3, which set the separation angles). The base drag behind them within 0.0102 of the measured on the cylinder’s area, though behind small bases that is up to about 40% of the base’s own drag (8 of the 12 set the ratio below Mach 2.5) |
| Aerodynamics | OpenRocket 24.12’s normal-force slope and center of pressure for tube fins, on 14 probe designs and its Tube fin rocket, at Mach 0.05 to 0.75 (ADR-102, the decision that measured it); a code-to-code comparison. Tube fins as a whole are unvalidated: no measurement of a tube fin’s lift or center was found | not within the quarter calibre that lesson L19 asks: hpr’s center of pressure is 0.42 to 3.0 calibres forward of OpenRocket’s on every probe up to Mach 0.5, and 1.07 on the Tube fin rocket. From Mach 0.6, where OpenRocket moves the tubes’ center to their leading edge, 5 of 28 are within it. OpenRocket’s tubes lift 1.26 to 1.86 times hpr’s, and up to Mach 0.5 it puts their center at a quarter of their length, ahead of which hpr’s lies. All 70 probe gaps, and the Tube fin rocket’s, are pinned by a test |
| Rigid-body flight | RocketPy’s whole flights from the pad to the ground, for six rockets, one past Mach 1, both codes flying one declared drag coefficient | heights, speeds, times and accelerations within 3% (below), the largest +1.783% in the report; the path too, except the drifts of Juno III, Bella Lui and Prometheus 2022 in wind and NDRT 2020’s apogee drift, reported, not scored, as measured differences between the models (ADR-026) |
| Rigid-body flight | OpenRocket 24.12’s calm flights of the 54 configurations of its examples that hpr flies, each figure by OpenRocket’s own definition: apogee, largest speed, and the stability margin at rod clearance (report); a code-to-code comparison, with no target set. OpenRocket aborts one, so the figures are over the other 53, and that one is compared at two points up to the abort (flights OpenRocket aborted) | margin within 0.016 calibres on 41 of 53, taken with the air along the axis as OpenRocket’s is; the Tube fin rocket’s is 1.08 calibres below OpenRocket’s (tube fins), the three-stage example’s three are 0.039 to 0.058 below, cause not yet sized (#185), and the five of Pods–airframes and winglets are 0.071 to 0.076 above, the flattering side (#325 and #326, two open causes in the fins), and the three of Pods–powered with recovery deployment are 0.070 above, cause not yet traced. With no named cause, apogee −4.34% to +1.95% (36 flights), and largest speed −0.69% to +6.10% (51 flights). The two-stage, three-stage, cluster, air-start and parallel-booster examples are within 5% in apogee and largest speed on every flight, the bar the M1.9c milestone set before measuring; three cluster apogees and two three-stage ones are compared with OpenRocket’s flight with no parachute, since its parachute opened before apogee. One named cause moves the apogee more than 5%: OpenRocket’s parachute opening while the rocket still climbs (hpr’s flight compared here flies none), up to +13.80%. OpenRocket flying those six again without it brings four within 5% (M2.2e4, sizing the causes). The other two are Base drag hack (short-wide) on a D12-3 and an E12-4, whose part set to no drag now flies as OpenRocket flies it (M4.5h, a part’s drag override): against OpenRocket’s flight with nothing deployed they read +5.84% and +8.92%, and within 0.10% when hpr flies OpenRocket’s own drag, so their gap is the drag coefficient, by OpenRocket’s breakdown the very blunt nose’s. With recovery in both, the basis M4.5o judged, the D12-3 is within 5% (+4.52%) and only the E12-4 is over (+7.80%). hpr keeps a value for that nose read between Hoerner’s measured round heads, so the gap stays (ADR-173, #177). A seventh, the Tube fin rocket, reads +6.95%, and within 0.03% when hpr flies OpenRocket’s own drag, so the gap is hpr’s tube-fin drag, which probably reads low (#228). The flight OpenRocket aborts, Pods–powered with recovery deployment’s first configuration, is within 5% at both points (height −1.17% and −1.18%, speed −0.04% and +4.02%), weaker evidence than an apogee (M4.5n) |
| Rigid-body flight | OpenRocket 24.12’s calm flights of the 35 configurations of 11 private designs that hpr flies, published only as differences under anonymised ids (report); a code-to-code comparison, with no target set | covers 11 of the 12 private designs. Apogee −4.84% to +13.60%. One is more than 5% off: the one supersonic flight, C06/1, +13.60% in apogee and +7.98% in largest speed, whose cause is the drag coefficient: flown on OpenRocket’s own drag, hpr reads +1.11% and +1.04%; the supersonic pressure drag (mostly wave drag) and hpr’s unvalidated base drag under power are open (#222: hpr’s supersonic pressure drag is about twice OpenRocket’s). The rest: largest speed −0.65% to +2.28%. Margin −0.0166 to +0.1108 calibres. hpr calls four designs clearly more stable than OpenRocket does: C09 by 0.0350 to 0.0478 and C03 by 0.0564 to 0.0730 on every flight, cause not yet traced (#172); C08, a two-stage design, by 0.1108, a center-of-mass gap with a lead but no measured cause (#186); and C06/1 by +0.0604, most of it a center of mass 0.0447 calibres forward of OpenRocket’s, with a lead but no measured cause: its airfoil fins, which OpenRocket weighs by a factor of 0.85 (the mass survey’s fin-section cause, sized on the structure alone). A fifth, C12, launched from a tilted rod, reads 0.0125 to 0.0366, most of it OpenRocket’s rocket clearing the rod at an angle of attack: at that angle hpr reads 0.0033 to 0.0074 (a tilted launch rod) |
| Time integration | a separate line-by-line transcription of DOPRI5, the published Fortran integrator by Hairer and Wanner that hpr’s Dormand–Prince stepper follows, on the problem Hairer’s own example program for DOPRI5 solves: the Arenstorf orbit, the closed, looping path of a small body pulled by two large ones that circle each other | the same step counts |
| Time integration | a vertical flight with drag that has an exact solution | apogee, deployment and landing times within 1.5e-8 s |
| Recovery | RocketPy’s descents under a parachute, for five rockets | every descent metric within 3% (below) |
| Recovery | the same descents: the heights where the later parachutes fire, and the descent rate under the drogue | within 0.17% and 0.01% |
| Recovery | published drop tests of five small models falling with nothing deployed (tumbling) | descent rate −10 to +19% off |
| Recovery | Kidwell’s streamer drop tests (2001) | descent rate +9% fast for his one flat streamer, and +58% fast for one folded into pleats, which hpr doesn’t model |
| Interpolation | the exact formula of a smooth curve (a spline) through three points, y = 3x/2 − x³/2 | matched |
| Interpolation | property tests, which check a rule on many randomly generated tables | every table passes exactly through its own points |
| Quadrature | polynomials, whose integrals are known exactly | exact up to degree 22 |
| Quadrature | six test integrals with known answers, one of them infinite at an end | within 1e-11 relative |
The descent under a parachute, against RocketPy
This is one of the two comparisons the validation harness runs; the other is
whole flights. The harness is the program behind
cargo xtask validate, which flies every validation case and
writes the committed report.
Five of RocketPy’s example rockets are flown down in both codes, set up the same way:
- the same starting state near apogee, with the first parachute opening at once;
- the same drag areas, deployment triggers and wind;
- the random noise RocketPy can add to each parachute switched off, so its runs repeat exactly;
- RocketPy’s gravity formula, and its way of interpolating the wind (by its east and north components), in place of hpr’s own defaults, to compare like with like.
The committed validation report gives the six numbers below for each descent. All of them were scored and all are within tolerance; the largest difference is +2.865%.
Each metric must agree within 3% of RocketPy’s value, with no absolute floor (a fixed allowance, in meters or seconds, that would pass any smaller difference). Each case file argues why, for example NDRT’s. The metrics:
descent_time_s: the time from the shared start to landing.impact_speed_m_s: the vertical speed at landing; the wind adds to the speed over the ground.mean_descent_rate_m_s: the start’s height over the descent time, so it repeats the descent time in another form.drift_m,drift_east_manddrift_north_m: how far the rocket lands from where the descent started, not from the pad, and that distance’s east and north parts. A drift to the south or west is negative.
A difference is hpr’s value less RocketPy’s, over RocketPy’s. So a positive one means hpr’s value is larger in size, in the same direction: NDRT drifts south, and hpr carries it further south.
Every result of the report, as hpr’s difference from RocketPy:
| case | descent_time_s | impact_speed_m_s | mean_descent_rate_m_s | drift_m | drift_east_m | drift_north_m |
|---|---|---|---|---|---|---|
descent-calisto-tests-motor-at-minus-1.373 | +0.077% | −0.029% | −0.077% | +0.075% | +0.074% | +0.080% |
descent-valetudo | −0.019% | +0.004% | +0.019% | −0.889% | −0.889% | −1.766% |
descent-ndrt-2020-nose-to-tail | +0.705% | +0.012% | −0.700% | +0.276% | +0.214% | +2.865% |
descent-prometheus-2022-generic-motor | +0.083% | −0.030% | −0.083% | +0.083% | +0.086% | +0.082% |
descent-juno-iii | −0.018% | −0.008% | +0.018% | −0.018% | −0.018% | −0.018% |
Each case is named after the RocketPy example it flies. Three names carry more:
descent-calisto-tests-motor-at-minus-1.373is Calisto as RocketPy’s own tests build it, with the motor at −1.373 m in RocketPy’s coordinates, where its getting-started notebook puts it at −1.255 m (notes on RocketPy’s example rockets).descent-ndrt-2020-nose-to-tailis the NDRT 2020 rocket, which RocketPy’s example measures from the nose toward the tail (notes on RocketPy’s example rockets).descent-prometheus-2022-generic-motoris Prometheus 2022, whose motor RocketPy describes with its generic-motor model, which treats the propellant as a solid cylinder (notes on RocketPy’s example rockets, Recovery).
The Valetudo case flies RocketPy’s own Valetudo example, not the flight on Getting started. From the shared start, 800 m above the ground, it falls in still air under the example’s one drogue, with RocketPy’s drag area of 0.4537 m², and lands at 17.627 m/s. Getting started flies the same airframe from the pad, with its own drogue, a main parachute and a 5 m/s wind (case file, Recovery).
In still air, Valetudo’s drift, 0.19 m, comes only from the Earth’s rotation (the Coriolis acceleration), and its north part is 19 µm. So a small difference there is a large fraction (Recovery).
The largest gap, NDRT’s north drift, most likely comes from added mass: RocketPy counts the air a canopy drags along, 15.9 kg for NDRT’s main against the rocket’s 20.8 kg, and hpr has no such term, so the two respond differently as the canopy opens (Recovery). That explanation fits the size of the gap, but no test has isolated it yet.
What this shows: the two codes agree on the physics of a descent. It says nothing about whether either matches a real parachute on a real day.
Whole flights against RocketPy
Six of RocketPy’s example rockets are flown from the pad to the ground in both codes: Calisto, Valetudo, NDRT 2020, Juno III, Bella Lui and Prometheus 2022. They are set up the same way (ADR-021, the whole-flight comparison):
- one declared drag coefficient, a constant 0.5 (case file), on the same reference area;
- the example’s launch rail, site, parachutes and motor, with the thrust curve flown as measured and no correction for the thinner air at the site, as RocketPy’s examples fly it;
- RocketPy’s gravity formula, standard atmosphere and frictionless rail, and a declared wind;
- the random noise RocketPy can add to each parachute switched off.
This is same-drag mode. It checks the equations of motion, the motor and the air, not the drag. hpr’s own drag is compared below.
In short: how high, how fast and how long agree, and so does where the rocket goes, except for rockets that leave the rail slowly in a wind. The heights, speeds, times and accelerations of six flights, one of them past Mach 1, and of three of them again in calm air, agree within the 3% of each case’s gate (case file); the largest difference is +1.783% (report). Here still air is an example flown with no wind (Valetudo’s), and calm air a windy case flown again with its wind switched off. The apogee and landing points agree too, within 2.2%, in every flight without wind and for Calisto in wind (ADR-026). Juno III and Bella Lui leave the rail slowly in the wind, at a steep angle to the airflow. There hpr’s body lift, which RocketPy leaves out, its later release from the rail and, for Juno III, its simpler fin model put their drifts 10.195% to 38.158% from RocketPy’s. Prometheus 2022’s differ by −7.292% and +4.505%, from body lift and the rail release. NDRT 2020’s apogee drift differs by −4.333%, mostly from the rail release. These seven drifts are reported, not scored. So the landing offset that M2.1 asks for is met except where the two codes’ models differ.
Each of fifteen numbers per flight must agree within 3% of RocketPy’s, with no absolute floor, or say in its case file why it is not scored. Each case file argues why, for example Juno III’s. Two more numbers compare the whole trace; they are explained after the tables.
The numbers are measured as RocketPy defines them, with one exception. Each code’s solver advances the flight in time steps and keeps the state at each step’s end. RocketPy takes each maximum (top speed, top Mach, top acceleration) only at those step ends. hpr also searches between its own step ends for the true peak, so that its number does not depend on where its solver happened to step. That search can only raise a maximum. Against hpr’s old step-end readings it raised them by at most 6.3e-5 of themselves (NDRT 2020’s top speed). How much RocketPy’s own step ends miss was not measured. Either way it is far inside the 3% gate (ADR-023, the decision that also sets how peaks are found in both modes).
The definitions:
apogee_agl_mandapogee_time_s: the highest point, and when.flight_time_s: the time from ignition to landing.max_speed_m_sandmax_mach: the top speed over the ground, and the top Mach number.rail_exit_speed_m_sandrail_exit_time_s: when the forward rail button reaches the top of the rail, and the speed then. hpr’s own rail-exit event waits for the last button, so the comparison finds RocketPy’s instant instead.burnout_altitude_agl_mandburnout_speed_m_s: at the end of the thrust curve.impact_speed_m_s: the vertical speed at landing.max_acceleration_power_on_m_s2: the largest acceleration while the motor burns.max_acceleration_m_s2andmax_acceleration_time_s: the largest over the whole flight, and when. For NDRT 2020 that is its main parachute opening, not a flight load (case file).apogee_drift_mandlanding_drift_m: how far from the pad, along the ground, the apogee and the landing point are.
Speeds and accelerations are those of the rocket’s center of dry mass, the point RocketPy’s flight follows; hpr’s own output follows the center of mass of the loaded rocket. Heights are measured from where that point starts, as RocketPy’s are. A difference is hpr’s value less RocketPy’s, over RocketPy’s.
The validation report scores all but eleven of the numbers of the nine flights (the six, and Juno III, Calisto and Bella Lui again in calm air), and all of the scored ones are within tolerance. The eleven are measured and reported but not scored, each for a reason written in its case file (below). Every result of the report, as hpr’s difference from RocketPy:
| case | apogee_agl_m | apogee_time_s | flight_time_s | max_speed_m_s | max_mach |
|---|---|---|---|---|---|
flight-calisto-tests-motor-at-minus-1.373 | +0.052% | +0.103% | +0.123% | +0.015% | −0.120% |
flight-valetudo | +0.112% | +0.238% | +0.233% | +0.035% | −0.026% |
flight-ndrt-2020-nose-to-tail | +0.064% | +0.157% | +0.643% | +0.031% | +0.003% |
flight-prometheus-2022-generic-motor | +1.208% | +0.652% | +0.830% | −0.006% | −0.235% |
flight-juno-iii | +0.641% | +0.376% | +0.475% | +0.051% | −0.221% |
flight-bella-lui | +0.342% | +0.201% | +0.229% | +0.016% | −0.071% |
flight-juno-iii-calm | +0.085% | +0.073% | +0.066% | +0.015% | −0.123% |
flight-calisto-tests-motor-at-minus-1.373-calm | +0.045% | +0.099% | +0.118% | +0.022% | −0.105% |
flight-bella-lui-calm | +0.038% | +0.020% | −0.012% | +0.020% | −0.018% |
| case | rail_exit_speed_m_s | rail_exit_time_s | burnout_altitude_agl_m | burnout_speed_m_s | impact_speed_m_s |
|---|---|---|---|---|---|
flight-calisto-tests-motor-at-minus-1.373 | −0.010% | −0.072% | +0.019% | +0.019% | −0.020% |
flight-valetudo | −0.002% | −0.100% | +0.048% | +0.042% | +0.009% |
flight-ndrt-2020-nose-to-tail | −0.009% | −0.085% | −0.004% | +0.043% | +0.022% |
flight-prometheus-2022-generic-motor | −0.014% | −0.033% | +0.600% | −0.007% | −0.013% |
flight-juno-iii | −0.005% | −0.142% | +0.269% | +0.056% | −0.003% |
flight-bella-lui | −0.013% | −0.029% | +0.141% | +0.018% | +0.021% |
flight-juno-iii-calm | −0.002% | −0.137% | +0.042% | +0.020% | +0.001% |
flight-calisto-tests-motor-at-minus-1.373-calm | −0.001% | −0.074% | +0.023% | +0.025% | −0.003% |
flight-bella-lui-calm | −0.000% | −0.032% | +0.012% | +0.024% | +0.021% |
| case | max_acceleration_power_on_m_s2 | max_acceleration_m_s2 | max_acceleration_time_s | apogee_drift_m | landing_drift_m |
|---|---|---|---|---|---|
flight-calisto-tests-motor-at-minus-1.373 | +0.099% | +0.099% | −96.811% | −0.902% | +1.257% |
flight-valetudo | +0.248% | +0.248% | +0.006% | −0.931% | −1.811% |
flight-ndrt-2020-nose-to-tail | −0.020% | +83.059% | +0.197% | −4.333% | +1.627% |
flight-prometheus-2022-generic-motor | +0.003% | +19.062% | +1.094% | −7.292% | +4.505% |
flight-juno-iii | −0.197% | −0.197% | +0.001% | −38.158% | +36.678% |
flight-bella-lui | +1.783% | +1.783% | +0.001% | −10.195% | −21.686% |
flight-juno-iii-calm | −0.012% | −0.012% | +0.046% | −1.750% | −1.807% |
flight-calisto-tests-motor-at-minus-1.373-calm | +0.108% | +0.108% | −96.811% | −0.246% | −0.415% |
flight-bella-lui-calm | +1.778% | +1.778% | +0.001% | −1.155% | −1.791% |
The last two numbers compare the whole trace, not one point of it. The series height RMS
(series_height_rms_m) is the root mean square of hpr’s height less RocketPy’s: square each
difference, average the squares, and take the square root. The series speed RMS
(series_speed_rms_m_s) is the same for speed. Both follow the center of mass without propellant,
at RocketPy’s 120 series times, from ignition until hpr lands. Both codes’ clocks start at ignition
on the rail, so no time shift is fitted: a fitted shift would hide a real difference in the burn
or on the rail (case file). The center of mass without propellant is the point
RocketPy’s series records. Every time counts the same, so the long descent weighs most; a
difference during the burn shows in the burnout and top-speed numbers instead.
Exact agreement would give 0, so these two are given in meters and meters per second, not as a percentage. Each is held to 3% of RocketPy’s apogee (for height) or top speed (for speed). That is M2.1’s 3% for one number, applied to the whole trace (case file).
All nine flights pass, each well inside its bound. The largest height RMS is Prometheus 2022’s, 35.350931 m against its 110.3 m bound, about a third of it; its apogee is also the furthest off, +1.208%. Body lift accounts for that too: RocketPy flown with hpr’s body lift and rail release reaches 3723.8 m, against hpr’s 3723.6 (case file). Juno III’s is 14.550623 m against 78.4 m, about a fifth, and the other seven are at an eighth of theirs or less. The speed RMS runs from 0.022685 to 1.553509 m/s (report).
| case | series_height_rms_m | height bound, m | series_speed_rms_m_s | speed bound, m/s |
|---|---|---|---|---|
flight-calisto-tests-motor-at-minus-1.373 | +1.837754 | 78.3 | +0.058007 | 7.3 |
flight-valetudo | +2.115504 | 23.3 | +0.171627 | 3.3 |
flight-ndrt-2020-nose-to-tail | +2.425247 | 36.4 | +0.124657 | 5.4 |
flight-prometheus-2022-generic-motor | +35.350931 | 110.3 | +1.553509 | 10 |
flight-juno-iii | +14.550623 | 78.4 | +0.810392 | 6.7 |
flight-bella-lui | +1.657682 | 15.9 | +0.273966 | 2.9 |
flight-juno-iii-calm | +2.003892 | 78.6 | +0.065810 | 6.8 |
flight-calisto-tests-motor-at-minus-1.373-calm | +1.821296 | 78.4 | +0.051504 | 7.3 |
flight-bella-lui-calm | +0.089668 | 16.2 | +0.022685 | 2.9 |
What the two codes still do differently, and what it moves:
-
In wind: body lift, the rail release and Juno III’s fins. A rocket that leaves the rail slowly in a wind meets the airflow at a steep angle: Juno III leaves at 18 m/s in an 8.5 m/s wind, 26° off it (ADR-026). Three things differ there.
- hpr’s normal force includes body lift, which grows with the square of that angle; RocketPy’s does not. Much of it acts ahead of the rocket’s center of mass, the nose’s above all, so it moves the center of pressure forward and weakens the moment that turns the rocket into the wind: hpr turns into it less.
- hpr keeps the rocket guided until its last rail button leaves the rail; RocketPy frees it at the first.
- Juno III’s example gives its fins an airfoil lift curve, which RocketPy uses and hpr cannot model. RocketPy’s fin slope is 7.6% steeper than hpr’s flat-plate one (ADR-026).
Juno III’s apogee is 245.3 m from the pad in hpr and 396.6 m in RocketPy (−38.158%). Adding hpr’s choices to RocketPy one at a time moves RocketPy’s to 360.7 m with hpr’s rail release, 286.5 m with its body lift too, and 248.3 m with its fin slope as well (ADR-026, measured by
wind_response.py). Every windy drift lands within 1.3% of hpr’s the same way. Bella Lui’s drifts are −10.195% and −21.686%, Prometheus 2022’s −7.292% and +4.505% (within 0.1% of hpr’s once RocketPy has its body lift and rail release; case file), and NDRT 2020’s apogee drift −4.333%, mostly its rail release. These seven are reported but not scored, as measured differences between the models. Every other drift is scored and passes: Calisto’s in wind, Valetudo’s in still air, NDRT 2020’s landing, and all six in calm air (report, case file). -
RocketPy’s own equations, corrected. RocketPy 1.13.0 takes the turning moments during the burn about the wrong point: as far in front of the rocket’s center of dry mass as the real center of mass is behind it. That makes its rockets too stable while the motor burns, so they turn into the wind too far. The fix is proposed in a pull request to RocketPy, still open, built on one that is merged but not yet released. RocketPy 1.13.0 as installed still has the error; the comparison applies both fixes (ADR-026). Without them, Juno III’s apogee drift was 582.4 m, and hpr’s drifts in wind were up to −60.8% short of RocketPy’s at apogee and +151% beyond it at landing (the question of issue #50).
-
On the rail, hpr keeps the terms for the center of mass moving inside the body as the propellant burns, and RocketPy’s rail equation leaves them out. At a sharp ignition spike, with thrust and mass the same to five digits, hpr’s acceleration is 1.2 to 1.3 m/s² higher. That is Bella Lui’s +1.783%, whose peak is 7 ms after ignition (report, case file).
-
Calisto’s two peaks. Calisto’s acceleration peaks twice, 0.9% apart: on the rail at 0.05 s and at 1.568 s. The same rail terms make hpr’s first peak the higher, so
max_acceleration_time_smoves from one peak to the other (−96.811%); it is reported but not scored. The peak’s size is scored, but its +0.099% compares two instants; at 0.05 s hpr is 1.05% higher (report, case file). -
The main opening. RocketPy adds the air a canopy drags along (added mass), and hpr has none. So NDRT’s peak deceleration as its main opens is +83.059% in hpr, and Prometheus 2022’s +19.062%, reported but not scored. Their times are scored, since both codes put them where the main opens (report, case file).
Prometheus 2022 flies through Mach 1. RocketPy’s flight peaks at Mach 1.013 and hpr’s at 1.010371 (−0.235%). Until M1.8a hpr stopped any flight at Mach 1, and the case was a known gap; with the normal force carried past Mach 1 (Aerodynamics) it flies on its drag table to the ground. Its scored numbers agree within 1.208% (report, case file). This flight is a light test of the transonic normal force: no committed check measures its angle of attack there, but a local probe found it below 0.11 degrees from Mach 0.8 to 1.2 (case file).
What this shows: with the drag given, the two codes agree on how high, how fast and how long a rocket flies, and on where it goes, except for rockets that leave the rail slowly in a wind, where their models differ. M2.1’s landing offset is met everywhere else (ADR-026). Which code is nearer the truth for those is for real flights to say; the real flights so far compare heights, not drift. Nothing here says anything about hpr’s own drag, which the next section compares, or about a real flight. The comparison with OpenRocket is on its own page (hpr’s flights against OpenRocket’s), and the real flights are below.
Whole flights with each code’s own drag
The same six flights again, in predicted mode: hpr flies its own drag, from each design’s shape, where same-drag mode gives it the declared one; both modes use hpr’s own normal force, so only the drag differs. RocketPy flies the drag each of its examples ships, as RocketPy 1.13.0 flies the example: a drag curve for Calisto, Valetudo and Juno III, a function of Mach for Prometheus 2022 (case file), a constant for NDRT 2020 (0.44, case file) and Bella Lui (0.43, case file). Everything else is set up as in the same-drag flights above (ADR-023, the predicted-mode comparison).
In short: hpr’s heights are within 3% of RocketPy’s for Calisto (−0.609%), Bella Lui (+0.971%) and Juno III (+2.097%), well above for Valetudo (+10.113%) and NDRT 2020 (+10.302%), where its drag is well below the example’s, and below for Prometheus 2022 (−7.280%), where its drag is above the example’s through the coast (report). These are results, not a pass or fail. Neither code’s drag is the truth: each example’s drag came from RASAero, OpenRocket or its team’s own estimate. So each number is compared with the same 3% as the same-drag flights (M2.1, the validation milestone), but only as a target. A miss is reported and explained in its case file (Valetudo’s and NDRT 2020’s, for example), and does not fail the test suite. The report is committed, so any number that moves shows up in review.
Every predicted result of the report, as hpr’s difference from RocketPy:
| case | apogee_agl_m | apogee_time_s | flight_time_s | max_speed_m_s | max_mach |
|---|---|---|---|---|---|
predicted-calisto-tests-motor-at-minus-1.373 | −0.609% | −0.567% | −0.288% | +0.363% | +0.232% |
predicted-valetudo | +10.113% | +6.179% | +8.916% | +2.292% | +2.237% |
predicted-ndrt-2020-nose-to-tail | +10.302% | +6.534% | +6.870% | +1.378% | +1.351% |
predicted-prometheus-2022-generic-motor | −7.280% | −4.920% | −4.978% | +1.280% | +1.069% |
predicted-juno-iii | +2.097% | +1.046% | +1.686% | +1.006% | +0.737% |
predicted-bella-lui | +0.971% | +0.536% | +0.748% | +0.241% | +0.154% |
| case | rail_exit_speed_m_s | rail_exit_time_s | burnout_altitude_agl_m | burnout_speed_m_s | impact_speed_m_s |
|---|---|---|---|---|---|
predicted-calisto-tests-motor-at-minus-1.373 | −0.009% | −0.071% | +0.240% | +0.526% | −0.020% |
predicted-valetudo | +0.029% | −0.109% | +1.326% | +2.883% | +0.009% |
predicted-ndrt-2020-nose-to-tail | +0.000% | −0.087% | +0.723% | +1.532% | +0.022% |
predicted-prometheus-2022-generic-motor | +0.001% | −0.036% | +1.227% | +1.778% | −0.013% |
predicted-juno-iii | −0.001% | −0.143% | +0.772% | +1.129% | −0.003% |
predicted-bella-lui | −0.011% | −0.029% | +0.270% | +0.296% | +0.021% |
| case | max_acceleration_power_on_m_s2 | max_acceleration_m_s2 | max_acceleration_time_s | apogee_drift_m | landing_drift_m |
|---|---|---|---|---|---|
predicted-calisto-tests-motor-at-minus-1.373 | +0.123% | +0.123% | +0.002% | −2.233% | +0.901% |
predicted-valetudo | +0.250% | +0.250% | −0.030% | +12.215% | +10.437% |
predicted-ndrt-2020-nose-to-tail | +0.656% | +83.059% | +9.646% | +12.259% | +12.648% |
predicted-prometheus-2022-generic-motor | +0.848% | +19.062% | −6.903% | −17.642% | −6.167% |
predicted-juno-iii | +0.105% | +0.105% | +0.002% | −36.078% | +40.482% |
predicted-bella-lui | +1.797% | +1.797% | −0.029% | −9.390% | −20.805% |
The whole-trace numbers, in meters and meters per second, defined as for the same-drag flights above. Valetudo’s, NDRT 2020’s and Prometheus 2022’s height RMS are outside the target, and so is NDRT 2020’s speed RMS, for the same reason as their apogees: hpr’s own drag differs from those examples’ drag. Each bound is 3% of that case’s own RocketPy apogee or top speed, so it differs from the same-drag bound: Juno III’s 54.699661 m is inside its 83.9 m here (report).
| case | series_height_rms_m | height bound, m | series_speed_rms_m_s | speed bound, m/s |
|---|---|---|---|---|
predicted-calisto-tests-motor-at-minus-1.373 | +12.420364 | 84.6 | +0.331462 | 7.4 |
predicted-valetudo | +75.375007 | 20.9 | +2.790876 | 3.2 |
predicted-ndrt-2020-nose-to-tail | +116.136025 | 38.1 | +6.775468 | 5.5 |
predicted-prometheus-2022-generic-motor | +263.920106 | 128.8 | +7.202224 | 10.3 |
predicted-juno-iii | +54.699661 | 83.9 | +0.927234 | 6.8 |
predicted-bella-lui | +5.264438 | 16.2 | +0.279185 | 2.9 |
Why the misses, largest first:
- Valetudo and NDRT 2020 fly high: the drag. hpr’s drag coefficient at Mach 0.3 is −47.0% from Valetudo’s table, a hand-edited table 1.44 times the drag of the OpenRocket export for the same rocket (Aerodynamics). For NDRT 2020 it is 0.318 against the example’s constant 0.44 (case file). These drags are compared at Mach 0.3 only. Flown on the same drag, the apogees agree with RocketPy’s to +0.112% and +0.064% (report). Less drag also means a later apogee, a longer descent and further to drift, which moves their times and drifts too.
- hpr’s drag here is for the design as transcribed. Where RocketPy’s examples say nothing, the designs’ fin thickness and edges and their surface finish are placeholders, so these results compare hpr’s drag for those designs, not for the rockets as built. For Valetudo, the rocket’s own OpenRocket finish and launch lugs take hpr’s drag coefficient from 0.5566 to 0.714 (Aerodynamics). So a miss here is not a gap for hpr to close toward the example’s drag.
- The drifts of Juno III and Bella Lui in wind: hpr’s body lift and rail release, and Juno III’s fin slope, as in same-drag mode (ADR-026).
- Prometheus 2022 flies low: the drag again, the other way. It passes Mach 1 on hpr’s own drag since M1.8b1, the drag through Mach 1, peaking at Mach 1.060 against RocketPy’s 1.048. Its coasting drag rises to about 0.49 at Mach 0.8, where the example’s falls to 0.30, so hpr peaks −7.280% low and sooner, and its drifts and times follow. Flown on the same drag the apogees agree to +1.208% (report, case file).
- NDRT 2020’s and Prometheus 2022’s peak deceleration at their main openings, +83.059% and +19.062%, are the added-mass difference explained above. Their times move +9.646% and −6.903% with the apogee (report, case file).
What this shows: with its own drag, hpr’s heights differ from RocketPy’s by −7.280% to +10.302% (report), and the larger gaps are the two drags differing, not the flight. It does not say which drag is right; the real flights below start to, one rocket at a time.
Real flights
This section covers M2.3b, RocketPy’s logged flights (ADR-082, the decision). In short: hpr flew seven rockets whose teams logged their flights, in the weather of the day. Read the way each log’s altimeter reads the air, hpr’s apogees miss the logs’ by 6.04% on average, outside the 5% target; with its sign the mean is −0.25%, so these seven show no bias high or low. Five miss by more than 5%. Each has an explanation checked against the report’s numbers: four are consistent with hpr’s drag, and one with the motor’s impulse (report). Four of the seven altimeters’ kinds are assumed, none is calibrated, and the designs’ fin edges are guesses, so read the numbers as those seven flights’, not as a bound on every rocket.
What is flown. RocketPy’s documentation flies more than a dozen rockets against their teams’ logs. hpr flies seven of them the way a user would: the design built from the example’s masses and shapes, hpr’s own aerodynamics, and the example’s own thrust file, read as RocketPy reads it, to the same total impulse. It flies from the example’s launch rail and site, in the weather of the launch hour from the example’s ERA5 file, a reanalysis of the atmosphere (ERA5 weather files). Genesis and Lince were added to the public designs for this.
What is left out, and why. The sample is the documented flights hpr can fly today:
| rockets | why not yet |
|---|---|
| Astra, Andromeda | their weather file is in a newer format that needs converting first (ADR-082) |
| Camões, Erebus 11, Halcyon, Hedy | they fly their teams’ own motors, outside hpr’s scope of commercial motors (ADR-082) |
| Valetudo, Defiance | their logs give an apogee, not a climb (ADR-082) |
| Valkyrie | its inputs are only in a data file (ADR-082) |
Juno III flies its team’s own motor too, but its design was already public, and hpr flies the thrust file as it would any other: nothing of the motor is modeled (ADR-082).
The private designs add none: none of them is the rocket of a flight in the private collection of logs, so none has a log to compare with (ADR-083). A private flight collection added on 2026-10-04 pairs designs with their flights’ logs; its 55 compared flights are below.
Reading the logs. Every log here comes from a barometric altimeter, or is assumed to. Such an altimeter turns pressure into the standard atmosphere’s altitude. On a day warmer than the standard it reads less than the height climbed, 6.5% less on a day 20 K warmer (worked example), and more on a cold one. Two logs record their pressure, and their heights are that reading, to 0.195 m and 1.321 m (report). So hpr’s height is read the same way, from the ERA5 pressure at its center of mass. The table gives hpr’s apogee both ways. The reading moves it from −7.9% (Juno III, in June at Spaceport America) to +1.7% (NDRT 2020, in February) (report, ADR-082).
The kind of four altimeters is assumed rather than known; each row of the report gives its evidence. Reading all seven as heights would give a mean of 4.47%, but three logs are known to be barometric. Reading only the four assumed ones as heights gives 6.63%, so the target is missed either way. Read that way, Lince (+7.99%) would miss by more than 5% with no checked explanation, and Genesis would not (report, ADR-082).
Two logs also carry satellite (GNSS) heights, a geometric reference. Their barometric apogees are 0.943 and 0.935 of the satellite ones, and hpr’s conversion makes its own apogee 0.932 and 0.921 of its height (report). That is the same direction and nearly the same size, 1 to 2 points lower on both, on the side of both flights’ misses. Juno III’s log is cut up to 20 m below its apogee, which would put its own ratio as high as 0.941 (ADR-082).
Three logs are cut by hand just before a pressure transient at their apogee, where the reading jumps. Juno III’s rises 62 m in 0.3 s as it levels off, and the team’s reported apogee is that spike. At the cut its own velocity column still reads 17.5 m/s up, so its apogee may be 10 m to 20 m low (report).
What is compared.
- The apogee: the log’s highest reading, against hpr’s highest point read the same way. A plus means hpr flies higher.
- The climb: the root mean square (RMS) of hpr’s height less the log’s over the ascent, the report’s trace RMS. A log’s clock starts when its altimeter says so, not at ignition, so both clocks are set to zero where each first reaches 30 m. The RMS runs from there to the first of the two apogees (report). The descent is not compared: which parachute opened, and when, was the team’s.
The logs, thrust files and weather files are other people’s data, so they stay out of the
repository. cargo xtask real-flights reads them from a pinned copy of RocketPy (fetched with
cargo xtask refs fetch rocketpy) and commits only the numbers (report).
| flight | log apogee (m) | hpr apogee (m) | apogee error | hpr as a height (m) | climb RMS (m) | error on the team’s drag |
|---|---|---|---|---|---|---|
| Bella Lui, EPFL, 2020 | 459.0 | 462.9 | +0.86% | 463.6 | 3.2 | +0.24% |
| NDRT 2020, Notre Dame | 1320.4 | 1457.6 | +10.40% | 1432.8 | 89.9 | +0.17% |
| Prometheus, Western Engineering, 2022 ¹ | 3895.8 | 3549.3 | −8.90% | 3809.9 | 190.9 | +0.45% |
| Juno III, Projeto Jupiter, 2023 | 3151.5 | 2923.4 | −7.24% | 3174.5 | 228.8 | −9.05% |
| Cavour, Politecnico di Torino, 2023 | 2789.0 | 2946.0 | +5.63% | 3052.1 | 125.3 | −2.29% |
| Genesis, EuRoC 2023 | 2916.7 | 2746.0 | −5.85% | 2875.3 | 95.2 | −0.08% |
| Lince, EuRoC 2023 | 3587.7 | 3709.2 | +3.39% | 3874.3 | 63.9 | −12.20% |
¹ Flown in the weather of the same day a year later, 2023, as RocketPy’s example flies it: no file of the flight’s day is available (report).
The mean absolute apogee error is 6.04%, against the 5% target of the validation plan (gate and target). The largest climb RMS is Juno III’s, 7.26% of its apogee (report). No target is set on the climb. The census counts each climb against a bar of 3% of its apogee, the bound the RocketPy comparisons hold a height RMS to; Bella Lui and Lince are within it (census).
The last column is a diagnostic, not a prediction. It flies each flight again with hpr’s drag replaced by the drag the example’s RocketPy notebook specifies: the team’s estimate, a table, a curve from RASAero II or a CFD analysis, or a constant the notebook gives no source for. When the error falls inside 5%, the miss is consistent with hpr’s drag. Neither drag is the truth, and a team may have tuned its drag to this very flight. On the teams’ drag the mean absolute error is 3.50% (report). Where a notebook reshapes its thrust file to a stated burn time and total impulse, as Juno III’s does, a second diagnostic flies the file as recorded.
The five flights past 5%. Each explanation is a claim the report checks against its own
numbers. A change to hpr that makes one false fails cargo xtask real-flights --check, which runs
only where the pinned RocketPy copy is. CI checks each claim against the committed numbers
(ADR-082).
- NDRT 2020, +10.40%: consistent with hpr’s drag. On the team’s constant drag coefficient, 0.44, the apogee is +0.17% (report). hpr’s own drag is lower, as it was against RocketPy flying the same constant.
- Prometheus, −8.90%: consistent with hpr’s drag. On the team’s drag table the apogee is +0.45% (report). The barometric reading uses the temperatures of 24 June 2023, a year after the flight. Against the satellite heights, hpr’s conversion reads 1 to 2 points below the altimeter both here and on Juno III, which flew in its own day’s weather, so the wrong day’s share of the miss can’t be told apart.
- Juno III, −7.24%: consistent with the motor’s impulse. The notebook reshapes the team’s own motor curve to 8800 N s, 4.9% less than the file. On the file as recorded (9251.7 N s, its negative end read as zero) the apogee is +1.13%, while on the team’s drag it is −9.05% (report).
- Cavour, +5.63%: consistent with hpr’s drag. On the curves the team labels RASAero II the apogee is −2.29% (report). hpr’s drag is below those curves, 8.3% at Mach 0.3 with the motor off and 18.3% with it burning (Aerodynamics).
- Genesis, −5.85%: consistent with hpr’s drag. On the team’s curves the apogee is −0.08% (report).
These are consistent explanations, not proofs: the teams’ drags, and the impulse a notebook sets, are estimates too. Lince, inside the target on hpr’s drag, is −12.20% on its team’s; that is not investigated (report).
How far to trust it. The altimeters are not calibrated here: a barometer’s error, the filter of the four that are filtered, and the assumed kind of four of them all sit in the reference. Drift and landing are not compared, nor speeds. hpr’s designs of these rockets have placeholder fin edges and surface finish, which move its drag.
Real flights of the private collection
This covers M2.3c1, the logged apogees of a private collection (ADR-184, the decision). In short: 55 real flights, each a design paired with the log of the same flight, were flown by hpr and by OpenRocket in the weather of their day. Both over-predict: hpr’s apogees are +9.83% above the logs on average (mean absolute 13.96%), OpenRocket’s +9.00% (13.08%). hpr and OpenRocket agree with each other within 5% on 53 of the 55 (report). Neither meets the 5% target. hpr and OpenRocket agree with each other far better than with the logs, so the gap is not a difference between the two codes; what causes it is not shown here. The flights’ owners allow only aggregate statistics, so no flight is named here, and the logs’ readings were taken by hand from free text.
What is flown. The collection pairs each design as flown with its own flight’s log, the
motor, date, site and the ERA5 weather of the day
(ADR-151).
Of its 89 best-documented (tier A) flights, 83 have an OpenRocket .ork design
(report). hpr flies the whole ERA5 profile of the launch hour: temperature,
pressure and wind at every level. OpenRocket 24.12 can take only a temperature and pressure at
the pad, with the standard atmosphere above, so it gets those, and the same wind every 50 m. Both
fly OpenRocket’s curve for the motor the file names, from the same rail, with no parachute
opened, so each apogee is the climb’s. A third row flies hpr in OpenRocket’s air, which shows how
much of the difference between the codes is the atmosphere.
What each log says. Every log here is a barometric altimeter’s. Such an altimeter turns pressure into the standard atmosphere’s altitude and subtracts the pad’s, so on a warm day it reads less than the height climbed: 6.5% less on a day 20 K warmer than the standard (worked example). The comparison turns each reading back into a height through the day’s ERA5 air. Across these flights the heights came out 1.0361 times the readings on average, from 0.9601 to 1.0777 times (report). Where a log’s highest value was an ejection charge’s pressure spike, the reading before the charge was taken; 24 of the 83 readings differ from the collection’s own figure by more than 0.5%, each with its reason recorded (ADR-184).
| report | flights |
|---|---|
tier A, from a .ork | 83 |
| flown by hpr | 61 |
| flown by OpenRocket 24.12 | 79 |
| flown by both | 61 |
| left out: an airbrake or canard acted on the climb | 6 |
| compared with the log | 55 |
| over 55 flights | mean | median | mean absolute | within 5% | within 10% |
|---|---|---|---|---|---|
| hpr | +9.83% | +9.47% | 13.96% | 10 | 27 |
| OpenRocket 24.12 | +9.00% | +6.82% | 13.08% | 9 | 26 |
| hpr in OpenRocket’s air | +9.99% | +9.67% | 14.07% | 10 | 26 |
| hpr against OpenRocket | +0.69% | +0.55% | 1.72% | 53 | 55 |
Each error is the simulated apogee less the logged one, over the logged one, in percent, so a positive error is a prediction above the log. The mean absolute error averages the errors without their signs. The last row is hpr’s apogee less OpenRocket’s, over OpenRocket’s, so a positive value means hpr flies higher. “Within 5%” and “within 10%” count the flights whose error is strictly smaller than that, either way (report).
Early deployments. On 19 of the 55, a parachute charge fired or the rocket separated before the highest reading, while it was still climbing (report). The readers’ notes put each at under 1.5 s before the peak and a few meters of climb, so the apogee barely moves (ADR-184). Without them, over 36 flights, hpr is +5.88% (mean absolute 11.94%) and OpenRocket +5.29% (11.24%). The two sets differ in which rockets they hold more than in what the early charges cost.
What is not flown, and why. Of the 83, hpr flies 61, and each cause of the rest has an open
issue (report). Most are small reading problems in hpr’s .ork importer that
OpenRocket accepts: a fin tab a few tens of microns past its root (6 flights, #357), a
fin outline with a repeated point (3, #358), rail buttons spaced forward (1,
#359), unread surface
finishes (3, #360), two override flags read as one (1, #361), and a part in
an off-axis inner tube (2, #181). Three fly motors whose curve OpenRocket’s database
lacks (#362). In three more the collection itself falls short: two designs don’t hold
the flown motor (#363) and one flight’s day has no weather file (#364).
OpenRocket flies 79, missing the motor in one (#362).
How far to trust it. The readings were taken from free text and logs by hand, and checked for form, not cross-checked flight by flight by a second reader. Of the 83 flights, 26 have no known launch hour and fly at local noon (report); the altimeters are not calibrated; the designs are what their fliers drew, whose masses some fliers measured and some did not. Both codes over-predicting alike is common in hobby rocketry, where the real airframe carries rail buttons, paint and joints a design leaves out, but this comparison does not show which. The six tier-A flights in other formats wait for M3.4 to M3.6, and the climb traces for M2.3c2.
Sources. The flights are published by university rocketry teams, rocketry courses and hobbyists on GitHub, team websites and The Rocketry Forum. The weather is ERA5, generated using Copernicus Climate Change Service information 2026, and contains modified Copernicus Climate Change Service information 2026; neither the European Commission nor ECMWF is responsible for any use made of it. The report gives the full attribution and the ERA5 citations.
Known gaps
These are the largest known differences and missing pieces. Each model page’s In short lists the rest.
- hpr’s own drag in a whole flight. Its heights are +10.113% and +10.302% above RocketPy’s for Valetudo and NDRT 2020, where its drag is well below the examples’, and −7.280% below for Prometheus 2022, where it is above (report). Against real flights, four of the five apogees more than 5% from their logs are consistent with hpr’s drag: NDRT 2020 +10.40% and Cavour +5.63%, Prometheus −8.90% and Genesis −5.85% (real flights, real-flight report).
- Drag faster than sound reads high against NASA’s wind tunnel, above all with fins: with the fins on, +39.4% at Mach 1.5 to +154.0% at 4.63. The fins take a blunt leading edge’s formula, and nothing models a thin, sharp fin’s own wave drag. Niskanen’s cone, which ogives share, reads 45% to 105% above a measured cone through the rise near Mach 1 (Aerodynamics).
- A steep boattail’s drag reads high in a thick boundary layer: 16° boattails +26.4% to +54.2% from Mach 1.0 to 1.28, and the Arcas Robin’s 15° boattail puts its forebody, fins off, +13.5% to +24.1% high from Mach 1.5 and +20.3% to +50.8% from Mach 1.0 to 1.2. No cited correction exists in the sources used (issue #72; Aerodynamics).
- Drag past Mach 1.6 reads low against RASAero II’s Calisto, to −14.9% at Mach 2, with the fins and finish the Mach 0.3 check declares; other plausible fins bring most rows within 10%. Part of it is hpr’s body, which reads 6% to 10% low faster than sound against MIL-HDBK-762’s worked example as well (Aerodynamics).
- Roll: the forcing high near Mach 1.5, the damping low, nothing measured below Mach 1.5. The roll forcing from canted fins reads +47.8% at Mach 1.5 and +14.3% to +17.8% at 1.8 against NASA’s wind tunnel, where linear theory’s load climbs toward Mach 1 faster than the fins’ does; the roll damping reads 5.9% to 16.2% low against the Basic Finner’s. Nothing measured checks either below Mach 1.5 (Aerodynamics).
- A flared rocket’s supersonic normal force rests on one measured flare. A conical flare flies the shock-expansion method since M1.8e17, which moves the center of pressure of a test rocket with a 10° flare forward by 0.089 to 0.238 calibres against the model it had before, growing with Mach number. The one measurement beside it is NASA TN D-4865’s model 2, an 18.5° flare on a 2.75° cone, and it is not that rocket (Aerodynamics). No other flare angle has been checked, and the reading a flare steeper than its corner’s limit gets has been checked against nothing at all. A flare shallow enough to reduce its own element (a few thousandths of a degree on that rocket) is read by the older generalized method instead, which no measurement checks either, and which leaves a step where the corner’s pressure crosses its tangent cone’s (Aerodynamics). On a body with a short shoulder that crossing is not near-flat at all (0.7° to 4.6°) and the step reaches +4.3% of the body’s normal force and 0.19 calibres; its exact size for any conical flare is on that page.
- Some shapes fall back to slender-body theory faster than sound, and read more stable. Past Mach 1.2, a body with a step in its radius (#87), a flare behind a boattail too long for its wake (#120), a pointed tip steeper than 30° (#121) or a vertical tip steeper than its cap’s handover keeps slender-body theory for the whole body. The sizes are measured at Mach 3 and 4° on two test bodies: −8.65%, −27.5%, −7.7% and −7.0% in normal force, with the center of pressure 1.03, 0.29, 0.81 and 0.64 calibres aft, so the body reads more stable than it is. The flare’s is measured on a finless body, so its share is of a body’s normal force alone; the other three are on a straight rocket with four fins, so the two kinds of share don’t compare (Aerodynamics). No size inside the core band, below Mach 2.5, is pinned yet. They are first in M1.14d, supersonic accuracy, and in M1.14h above Mach 2.5.
- The normal force near and far past Mach 1. Against NASA’s wind tunnel, between Mach 0.8 and 1.2 hpr’s slope runs up to +27.1% high and its center of pressure up to 2.36 calibres off. From Mach 1.5 up its slope holds to within 9.4% (fixture, Aerodynamics) and its center of pressure to 0.53 calibres, which misses the half-calibre target on the long model at Mach 1.8 and 2.3, where the body alone reads 15% to 19% above the tunnel (Aerodynamics: body lift).
- Tube fins’ center of pressure is unmeasured, and OpenRocket’s differs. hpr’s is 0.42 to 3.0 calibres forward of OpenRocket 24.12’s on 14 probe designs up to Mach 0.5, and 1.07 on its Tube fin rocket, whose margin hpr gives as 0.79 calibres to OpenRocket’s 1.87. Neither code has been checked against a measured tube-fin rocket; hpr leaves out the body’s interference, which slender-body theory predicts (#234), and its tube-fin drag probably reads low (#228; tube fins). Check a tube-fin design in both programs and treat the smaller margin as the more cautious estimate, not a bound.
- Drag against the RASAero curves at Mach 0.3 is within 10% in four of seven cases, with the fins and surface finish guessed, because the curves don’t record them. Cavour power-on is −18.3%, cause open. Valetudo’s −47.0% and −50.4% are against a table 1.44 times the drag in the OpenRocket export for the same rocket (Aerodynamics).
- Six fins. The normal-force slope of Barrowman’s six-fin Recruiter is +2.87% above his printed value, and +3.42% on the fins alone, mostly because hpr uses a different six-fin rule (Aerodynamics).
- Tumbling is −10 to +19% off its source’s own drop tests, and is used far outside the fit behind it. That fit comes from small models falling at 5.0 to 6.6 m/s; if Valetudo came down tumbling, with nothing deployed, hpr would bring it down at 36 m/s. The default streamer model reads +58% fast on a pleated streamer (Recovery).
- Opening loads, the force on the rocket as a canopy opens, are no safe bound either way. With a filling time, hpr leaves out the brief rise of drag above its steady value while the canopy fills. Opening at once, it ignores how a light rocket slows while the canopy fills. The deployment speed can itself read high: a separated body falls with no drag until its device opens (Recovery).
- No added mass under a canopy, the likely cause of the 2.86% drift difference above (Recovery), and the cause of NDRT’s +83.059% and Prometheus 2022’s +19.062% peaks as their mains open in the whole flight (report, case file).
- Body lift in wind. A slow rocket leaves the rail at a steep angle to a crosswind, and there
hpr’s body lift, which RocketPy leaves out, is the largest reason its drift differs: Juno III’s
apogee drift is −38.158% against RocketPy’s (report). How much body lift a rocket body
makes is itself uncertain. hpr takes it from Jorgensen’s crossflow term
(Aerodynamics), whose factor is about 0.9 for these
rockets at low speed. Flown in RocketPy with hpr’s rail release and fins, that puts Juno III’s
apogee 248.3 m from the pad, against hpr’s own 245.3 m (case file). In the same
runs, Galejs’s constant
K, which hpr used before, gives 240.2 m at 1.0, 231.1 m at 1.1 and 194.1 m at 1.5, across its source’s range, and the drift would be 328.0 m with no body lift at all (ADR-026). Only real flights can say which is right, and the real flights so far compare heights, not drift. - Airfoil fins. hpr’s fins use the flat-plate lift slope. It cannot model an airfoil lift curve such as the one Juno III’s example gives its fins, which makes RocketPy’s fin slope 7.6% steeper (ADR-026).
- Turbulence is an aircraft model, unvalidated for rockets, and no flight uses it yet (Turbulence).
- Wall and fin mass may follow different conventions from OpenRocket’s, which its documentation doesn’t state; six are measured so far (Mass properties). Measuring a nose cone’s wall thickness straight out from the axis, rather than square to its surface, changes the wall’s volume by 1.4% on a cone three calibres long (Shapes).
Frames and sign conventions
In short
- What it models: the directions and signs the whole simulator shares: a frame fixed to the Earth at its center, the launch-site frame (east, north, up), the rocket’s body frame and its angle to the airflow, and its attitude (which way it points) and launch angles.
- Sources: the WGS 84 standard, NGA.STND.0036 (2014), as in Geodesy; J. Solà, Quaternion kinematics for the error-state Kalman filter (2017); RocketPy 1.13.0, for its launch-angle convention.
- How well it is validated: unit tests, and one check against another simulator: for 8 rail setups, launch angles give RocketPy’s starting attitude to 1e-12 rad. Over 1e6 integration steps, attitude stays within 1e-9 rad of the exact answer. No real-flight check.
- What it leaves out: heights are above the WGS 84 ellipsoid, not sea level; the two are up to about 100 m apart. The attitude equations leave out the Earth’s rotation rate, 7.3e-5 rad/s, which is tiny next to a rocket’s pitch rates (ADR-011, the rigid-body flight decision).
Code and sources
This is the single definition of the frames, and every crate follows it. Code:
hpr_core::{geodesy, frames, attitude}. ADR-003, the frames and gravity decision,
records why these choices were made.
Units and angles
- SI throughout: meters, seconds, kilograms, radians. Degrees exist only at I/O boundaries
(
Geodetic::from_degrees). - Every rotation is right-handed.
R_x(θ),R_y(θ)andR_z(θ)turn vectors by+θabout the named axis (active rotations).
Earth-centered Earth-fixed (ECEF)
The WGS 84 conventional terrestrial frame:
- origin at the Earth’s center of mass;
+Zalong the rotation axis toward the north pole;+Xthrough the prime meridian at the equator;+Ycompletes the right-handed set (90° E).
A geodetic position (φ, λ, h) gives the latitude φ (positive north, in [−π/2, π/2]),
the longitude λ (positive east), and the height h above the ellipsoid along its normal.
h is ellipsoidal height, not height above mean sea level. The two differ by the geoid
undulation N (h = H + N, with |N| up to about 100 m). Inputs quoted above sea level must be
converted before use. hpr has no geoid model, so a flight takes N at the launch site as an input;
Atmosphere shows where it is used.
Launch frame L (East-North-Up)
-
Origin: the launch site’s geodetic position
(φ₀, λ₀, h₀), at the pad on the ground. -
Axes:
x_Least,y_Lnorth,z_Lup along the ellipsoid normal at the origin. -
To ECEF:
r_ECEF = r₀ + R r_L. The columns ofRare, in ECEF components:ê = (−sin λ₀, cos λ₀, 0) n̂ = (−sin φ₀ cos λ₀, −sin φ₀ sin λ₀, cos φ₀) û = (cos φ₀ cos λ₀, cos φ₀ sin λ₀, sin φ₀) -
Earth-fixed, so non-inertial. It turns with the Earth at
Ω = ω (0, cos φ₀, sin φ₀)inLcomponents, withω = 7.292115e-5 rad/s. The translational equations inLadd the Coriolis term−2Ω × v. The centrifugal term is already inside normal gravity and must not be added again (Gravity). -
z_Lis not altitude.Lis a tangent plane, so a point atz_L = 0at horizontal distancedfrom the pad is aboutd²/(2R)above the ellipsoid: 7.8 m at 10 km. Height above the ellipsoid comes fromLaunchFrame::geodetic_from_enu. The flight engine detects apogee and ground contact with that height, not withz_L(Rigid-body flight, Loft lesson L35).
Body frame B
- Origin: the nose tip, on the axis (ADR-007, the design-tree decision). Positions in mass properties are measured from it.
- Axes:
z_Blies along the axis of symmetry, positive toward the nose.x_Bis the design’s zero radial direction, the angle from which fins, rail buttons and lugs are placed.y_B = z_B × x_B. - Thrust of a motor aligned with the axis acts along
+z_B. - Design stations measured aft from the nose tip, as design files state them, map to
z_B = z_ref − swithz_ref = 0, soz_B = −sand the whole rocket lies atz_B ≤ 0(Design tree).
Aerodynamic angles
- Angle of attack
α: the total angle between+z_Band the rocket’s velocity relative to the air, in[0, π]. - Flow roll
φ: the direction in which the air crosses the body, measured in thex_B–y_Bplane fromx_Btowardy_B. Finkof a set sits atΛ_k = θ₀ + 2πk/N − φfrom the lateral airflow, withθ₀the set’s base angle. - Force directions. With
ŵ = (cos φ, sin φ, 0)the lateral air direction inB, the normal and side forces areq A_ref (C_N ŵ + C_Y (z_B × ŵ)):C_Nis positive alongŵ, the way the crossing air pushes the body, andC_Yis across the flow’s plane (Aerodynamics). The axial force is−q A_ref C_A z_B:C_Ais positive when the flow meets the nose and drag pushes toward the tail, and negative pastα = 90°, when the rocket moves tail first. - Rolling moment.
q A_ref d C_labout+z_B, right-handed, withdthe reference diameter: positive turnsx_Btowardy_B. A fin set’s positive cant turns fin 0’s (the fin along+x_B) leading edge toward−y_B, so it turns the rocket about−z_B, clockwise seen from ahead of the nose, and the roll ratep = ω_zsettles negative (Roll: forcing and damping).
Attitude
The attitude is a unit Hamilton quaternion q (glam DQuat, stored x, y, z, w) that maps
body components to launch-frame components:
v_L = q ⊗ v_B ⊗ q* (glam: q.mul_vec3(v_B))
qand−qare the same attitude.- The body angular velocity
ω_Bis the rate ofBrelative toL, inBcomponents. - The kinematics are
q̇ = ½ q ⊗ (0, ω_B)(Solà 2017, eq. 200). The integrator renormalizesqafter every step (attitude::renormalize). Litself turns atΩ(above), soω_Bdiffers from the inertial rate byR(q)ᵀ Ω, at most 7.3e-5 rad/s. The flight engine’s rotational equations leave that term out (ADR-011, the rigid-body flight decision).
Launch angles
Launch-rail-style angles describe the attitude for input and output (frames::LaunchAngles):
- azimuth
A: the heading of the body axis, clockwise from true north (π/2is east), in[0, 2π); - elevation
E: the angle of the body axis above the horizon (π/2is vertical), in[−π/2, π/2]; - roll
φ: the turn aboutz_B, in(−π, π].
q = R_z(−A) ⊗ R_x(E − π/2) ⊗ R_z(φ)
z_B in L = (sin A cos E, cos A cos E, sin E)
- Zero roll.
x_Bis horizontal, to the right of the heading:(cos A, −sin A, 0)inL.y_Bis(sin E sin A, sin E cos A, −cos E). - Vertical singularity. At
E = ±π/2,Aandφturn about the same axis.LaunchAngles::from_quaternionthen reportsA = 0and puts the whole turn intoφ. This is for reporting only: the state is always the quaternion. - RocketPy correspondence. This is RocketPy’s 3-1-3 convention:
Ais the heading andEthe inclination.φis the rail-button angular position for atail_to_noserocket, and 2π minus it for anose_to_tailrocket.- RocketPy 1.13.0 sets precession
ψ = −heading, nutationθ = inclination − 90°and spinφ, then buildsq = q_z(ψ) q_x(θ) q_z(φ)(rocketpy/simulation/flight.py:1557-1579,rocketpy/tools.pyeuler313_to_quaternions). validation/oracles/rocketpy/attitude.pybuilds real RocketPy flights for 8 rail setups, with both orientations, and records the initiale0…e3.frames::tests::launch_angles_match_the_rocketpy_oraclechecks thatq(w, x, y, z) is the same attitude to 1e-12 rad.- RocketPy’s body
+zalso points toward the nose.
Tests that pin this
hpr_core::geodesy::tests:- geodetic → ECEF → geodetic, from −10 km to 1000 km;
- ECEF → geodetic → ECEF, from 6250 km to 46,000 km from the center;
- axis points, the sphere limit, and the ENU rotation being proper with
ûnormal to the ellipsoid.
hpr_core::frames::tests:- angles → quaternion → angles (off vertical), and quaternion → angles → quaternion (everywhere, exactly vertical included);
- ENU ↔ ECEF ↔ geodetic round trips for sites anywhere, within 2000 km and up to 1000 km high;
- named attitudes, and the tangent-plane rise
d²/(2N).
hpr_core::attitude::tests: the kinematic equation checked against a closed-form coning motion, and norm drift ≤ 1e-12 over 1e6 RK4 steps with attitude error < 1e-9 rad.
Geodesy: the ellipsoid, coordinates, distance and bearing
In short
- What it models: the Earth’s shape, as the WGS 84 ellipsoid (a sphere slightly flattened at the poles); conversions between latitude, longitude and height and Earth-centered x, y, z; the local east, north and up directions; and the distance and bearing between two places.
- Sources: the NGA’s WGS 84 standard, NGA.STND.0036 (2014); C. F. F. Karney, Geodesics on
an ellipsoid of revolution (2011), appendix B; C. F. F. Karney, Algorithms for geodesics
(2013), through the
geographiclib-rscrate. - How well it is validated: for the conversions, the derived ellipsoid values reproduce the standard’s Table 3.5 to its printed digits, and in unit tests random round trips return latitude within 1e-14 rad and height within 2e-8 m, from −10 km to +1000 km; they are not compared with another library, a simulator or a real flight. For distance and bearing, every one of the 500,000 lines of Karney’s published test set is matched within 15 nanometers (nm, billionths of a meter): distance within 11.18 nm, the far point within 14.02 nm, and the bearings within 15 nm of sideways miss (below). CI checks every 500th line and the 21 mirror lines; the whole set is checked where it has been downloaded, and has been measured on macOS.
- What it leaves out: height above sea level, which needs the geoid, up to about 100 m from
the ellipsoid (Frames). hpr has no geoid model; a
flight takes that difference at the site as an input. Nothing in a flight uses distance and
bearing yet: the landing distance
hprprints is measured on a flat map from the pad’s east and north offsets, and there is no command for geodesics.
Sources
Code: hpr_core::geodesy and hpr_core::geodesic. Conventions: Frames.
- [NGA] NGA.STND.0036_1.0.0_WGS84, Department of Defense World Geodetic System 1984, Its
Definition and Relationships with Local Geodetic Systems, 2014-07-08. Pinned as
wgs84-nga-stnd-0036. US government work. - [Karney] C. F. F. Karney, Geodesics on an ellipsoid of revolution, arXiv:1102.1215v1,
2011, appendix B. Pinned as
karney-2011-geodesics. - Karney2013 C. F. F. Karney, Algorithms for geodesics, J. Geodesy 87 (2013) 43–55,
arXiv:1109.4448v2. Pinned as
karney-2013-algorithms-for-geodesics. - GeodTest C. F. F. Karney, Test set for geodesics, doi:10.5281/zenodo.32156, CC0.
Pinned as
karney-geodtest.
WGS 84 ellipsoid
-
Defining parameters ([NGA] Table 3.1):
a = 6378137.0 mand1/f = 298.257223563. -
Derived values:
b = a(1 − f),e² = f(2 − f)andE = a e. -
Check: these reproduce [NGA] Table 3.5 to its printed digits (
wgs84_derived_geometry_matches_table_3_5):quantity value b6356752.3142 m e²6.694379990141e-3 E5.2185400842339e5 m
Geodetic to ECEF ([NGA] eqs. 4-14, 4-15)
N = a / √(1 − e² sin²φ)
X = (N + h) cos φ cos λ, Y = (N + h) cos φ sin λ, Z = ((b²/a²) N + h) sin φ
The code writes b²/a² as 1 − e².
ECEF to geodetic ([Karney] appendix B)
This is Vermeille’s closed form, which Karney extended to cover points near the Earth’s center.
Setup. Let R = √(X² + Y²), x = R/a and y = √(1 − e²) Z/a. The geodetic latitude
follows from the largest real root κ of
κ⁴ + 2e²κ³ − (x² + y² − e⁴)κ² − 2e²y²κ − e⁴y² = 0 (B1)
Solving for u.
r = (x² + y² − e⁴)/6, S = e⁴x²y²/4, d = S(S + 2r³)
d ≥ 0: T = (S + r³ ± √d)^(1/3), sign of √d = sign of S + r³, real cube root
u = r + T + r²/T (u = 0 if T = 0)
d < 0: ψ = ph(−S − r³ + i√(−d)), u = r(1 + 2 cos(ψ/3))
Solving for κ.
v = √(u² + e⁴y²)
v + u = e⁴y²/(v − u) when u < 0 (avoids cancellation)
w = ((v + u) − y²) e²/(2v)
κ = (v + u) / (√((v + u) + w²) + w) (B5)
Latitude, height and longitude.
φ = ph(R/(κ + e²) + iZ/κ) (B2)
h = (1 − (1 − e²)/κ) √(D² + Z²), D = κR/(κ + e²) (B3)
λ = ph(X + iY)
ph is the argument, computed with atan2; λ = 0 on the axis.
The closed form fails only in the equatorial plane within a e² (42.7 km) of the center. There
the paper needs its limiting forms (B6)–(B7); the code returns CoreError::Domain instead.
Measured accuracy (property tests, 256 cases each per run):
- geodetic → ECEF → geodetic: latitude within 1e-14 rad and height within 2e-8 m, from −10 km to +1000 km;
- ECEF → geodetic → ECEF: within 1e-7 m per 6400 km of radius, out to 46,000 km.
Local ENU axes
ecef_from_enu_rotation(φ, λ) has columns ê, n̂ and û (Frames). A test checks that û
equals the normalized gradient of x²/a² + y²/a² + z²/b² at the foot point, i.e. the ellipsoid
normal.
Distance and bearing: geodesics
How far is the landing from the pad, and in which direction? On a flat map that is Pythagoras;
on the Earth it is a geodesic, the shortest path over the
ellipsoid’s surface. Its length is the distance, and its direction where it leaves is the
bearing. Geodesists call a bearing an azimuth; on this page the two
words mean the same. hpr_core::geodesic solves the two classic problems
(API reference):
- Inverse (
Ellipsoid::geodesic_inverse): from two places, the distances₁₂and the azimuthsα₁(leaving the first) andα₂(arriving at the second). - Direct (
Ellipsoid::geodesic_direct): from a place, a bearingα₁and a distance, the latitude and longitude you reach and the azimuthα₂there.
Azimuths are clockwise from true north, in radians from −π to π, so due west is −π/2. For the 0°
to 360° a compass uses, take .to_degrees().rem_euclid(360.0). α₂ is the direction of travel
on arrival, so the bearing back to the start is α₂ ± π; over a long path it differs from
α₁ ± π because meridians converge. Heights are ignored: the path runs on the ellipsoid’s
surface, and the direct problem’s end has no height until you give it one.
Method. The code is Karney’s GeographicLib, as georust’s geographiclib-rs 0.2.7 (MIT) ports
it. hpr uses the crate rather than a port of its own, so the code is Karney’s line for line
(ADR-127 decision record). Karney2013 maps the ellipsoid onto an auxiliary sphere,
where a geodesic is a great circle (the sphere’s shortest path), and corrects distance and
longitude with series in the flattening to sixth order (§2). The inverse finds α₁ by Newton’s
method (§4), from a starting guess (§5). Karney states that round-off stays under 15 nm in both
problems on WGS 84 (§7, page 10), and that up to a flattening of 1/150 the series’ truncation is
smaller still (page 9). Past 1/150 the series lose accuracy, so hpr refuses such an ellipsoid;
WGS 84’s flattening is 1/298. The familiar haversine formula treats the Earth as a sphere, which
the Earth is not; Vincenty’s ellipsoidal method is less accurate than Karney’s, and its inverse
sometimes fails to converge (§7).
Worked example. A pad at 32.9904° N, 106.9750° W and a landing at 33.0000° N, 106.9680° W:
| quantity | value |
|---|---|
distance s₁₂ | 1,249.614 m |
bearing from the pad α₁ | 31.567° |
azimuth on arrival α₂ | 31.571° |
The other way round, 2 km from the same pad on a bearing of 60° reaches 32.999415° N,
106.956466° W, heading 60.010°. At this range a flat map gives the same distance, if it uses
the ellipsoid’s curvature at the middle latitude: 1,249.614 m again, to under a millimeter. The
geodesic matters over long paths, where a flat map’s error grows. A unit test,
the_guides_worked_example, holds these numbers.
Validation. GeodTest is Karney’s set of 500,000 WGS 84 geodesics. They were worked
separately from the code, with the series carried to thirtieth order and high-precision
arithmetic, each end known to 1e-18°, so they check this code’s method and its rounding. They come
in nine kinds of 50,000 or 100,000 lines each, listed in the
geodesics report:
random pairs, nearly antipodal pairs (almost opposite sides of the Earth), short paths, paths
near the poles, nearly along a meridian or the equator, and paths near a vertex, the point
where a geodesic runs due east or west. crates/hpr-core/tests/geodtest.rs solves every line both
ways and measures five errors, each held to Karney’s 15 nm on every line:
| error | what it is | largest |
|---|---|---|
| inverse distance | the computed s₁₂ against the set’s | 11.18 nm |
| inverse landing | solve the direct problem with the inverse’s own α₁ and s₁₂, and measure how far it lands from the second place | 11.26 nm |
inverse azimuths × m₁₂ | each azimuth’s error times the reduced length m₁₂, how far the far end moves sideways per radian the start’s bearing turns: the sideways miss the error stands for | 8.49 nm |
| direct end point | the computed end against the set’s, in Earth-centered coordinates | 14.02 nm |
direct heading × a | the angle between the computed and the set’s direction of travel at the end, as directions in space, times the Earth’s radius a | 13.99 nm |
The inverse’s azimuths can’t be checked on the 50,000 “between vertices” lines: there m₁₂ is
at most 1e-13 m, so any azimuth error reads as no miss. Their distance and landing are checked.
Mirror lines: two paths of the same length. When the second place’s latitude is exactly the
first’s negated (φ₂ = −φ₁) and the two azimuths differ (α₁ ≠ α₂), two geodesics of the same
length join the places, one the mirror of the other, and the second has α₁ and α₂ swapped
(GeographicLib’s GeodSolve manual, Multiple solutions). Either answer is right. Where
α₁ = α₂, as on the between-vertices lines, the geodesic is unique. The set has 21 mirror lines
once its numbers are read as f64 (the 64-bit floating-point numbers hpr computes in). They are
all nearly antipodal, with m₁₂ under a centimeter, so their azimuths are nearly undetermined.
On these lines the test scores hpr’s answer against whichever pair it is nearer to, and that
error is within 15 nm on all 21; on 4 the nearer pair is the swapped one.
CI checks every 500th line (1,000 of them) and the 21 mirror lines, which are committed. The
whole set is checked where cargo xtask refs fetch has downloaded it, against the committed
report. The whole set has been measured only on macOS, by the debug build that wrote the report’s
table; a release build there moves three cells by up to 1.83 nm, and on any other build the test
holds the 15 nm bound without comparing the table.
What it leaves out. Heights: two places at 3,000 m are as far apart as the same places at
sea level. Only WGS 84 is measured; on any other ellipsoid up to a flattening of 1/150, the
accuracy is Karney’s claim, not something hpr has measured. A distance of many trips round the
Earth carries its own rounding, one step of f64 in the distance (at least 15 nm past 67,109 km). Nothing
in a flight uses geodesics yet, and hpr has no command for them.
Gravity and Earth rotation
In short
- What it models: the pull a rocket feels: WGS 84 normal gravity, the gravity of a smooth, spinning model Earth, which changes with latitude and height; and the Coriolis effect, the sideways push on anything that moves over the spinning Earth.
- Sources: the NGA’s WGS 84 standard, NGA.STND.0036 (2014), chapter 4 and appendix B.
- How well it is validated: the derived constants reproduce the standard’s printed values to their last digit. At 11 points, both poles and heights up to 200 km included, gravity’s strength matches the published formulas, evaluated in 40-digit arithmetic, within 2e-14 relative. hpr’s copy of RocketPy’s formula matches RocketPy at 8 points, to under 1e-12 relative. No real-flight check.
- What it leaves out: the real Earth’s gravity anomalies, typically within ±1e-4 relative. RocketPy’s formula, an option for like-for-like comparisons, differs from the exact field by 1.4e-5 relative at 100 km.
Code and sources
Code: hpr_core::gravity (the normal gravity field) and hpr_core::earth (what the flight engine
uses). Source: [NGA] NGA.STND.0036_1.0.0_WGS84 (2014), chapter 4 and appendix B, pinned as
wgs84-nga-stnd-0036.
What normal gravity is
Normal gravity γ is the gravity of the level ellipsoid: the attraction of the WGS 84 ellipsoid
plus the centrifugal acceleration of the Earth’s rotation. It is what a body at rest on the
rotating Earth feels.
- In the Earth-fixed launch frame, the only extra inertial term is Coriolis,
−2Ω × v. Adding a centrifugal term as well would count it twice. - Normal gravity ignores the real Earth’s gravity anomalies, typically within ±1e-4 relative (±100 mGal). A geopotential model such as EGM2008 is a later option.
Defining parameters and derived constants
The four defining parameters ([NGA] Table 3.1) are:
a = 6378137.0 m1/f = 298.257223563GM = 3.986004418e14 m³/s²ω = 7.292115e-5 rad/s
Everything else is derived from them ([NGA] appendix B):
e′ = E/b (second eccentricity)
q₀ = ½[(1 + 3/e′²) atan e′ − 3/e′] (B-18)
q₀′ = 3(1 + 1/e′²)(1 − atan(e′)/e′) − 1 (B-19)
m = ω²a²b/GM (B-20)
γ_e = GM/(ab) (1 − m − m e′ q₀′/(6 q₀)) (B-24)
γ_p = GM/a² (1 + m e′ q₀′/(3 q₀)) (B-25)
k = (b γ_p − a γ_e)/(a γ_e) (B-26)
Cancellation. The closed forms for q and q′ cancel about five digits at ε = E/u ≤ 0.082.
A first version computed k with a relative error of 2.6e-11, so it no longer rounded to Table
3.6. Below ε = 0.5 the code therefore uses the series from atan ε = Σ (−1)ⁿ ε^(2n+1)/(2n+1):
q = Σ_{n≥1} (−1)^(n+1) 2n ε^(2n+1) / ((2n+1)(2n+3))
q′ = Σ_{n≥1} (−1)^(n+1) 6 ε^(2n) / ((2n+1)(2n+3))
Check. These reproduce the printed values to their last digit:
| quantity | value | printed in |
|---|---|---|
q₀ | 7.334625787083e-5 | eq. B-18 |
q₀′ | 2.688041300461e-3 | eq. B-19 |
γ_e | 9.7803253359 m/s² | Table 3.6 |
γ_p | 9.8321849379 m/s² | Table 3.6 |
k | 1.931852652458e-3 | Table 3.6 |
m | 3.449786506841e-3 | Table 3.6 |
Formulas
- On the ellipsoid (Somigliana, eq. 4-1):
γ = γ_e (1 + k sin²φ)/√(1 − e² sin²φ). - Taylor series in height (eq. 4-3):
γ_h = γ [1 − (2/a)(1 + f + m − 2f sin²φ) h + (3/a²) h²]. RocketPy uses this form. Its error against the exact field is 1e-8 relative at 1.4 km, 3e-7 at 30 km, 1.4e-5 at 100 km and 1.1e-4 at 200 km (fixture values below). - Exact field (eqs. 4-5 to 4-13): ellipsoidal-harmonic coordinates
(u, β)give the componentsγ_uandγ_β. The code rotates them into ECEF withR₁(eq. 4-18).- Eq. 4-8 is used in the equivalent form
u² = ½[s + √(s² + 4E²z²)], withs = x² + y² + z² − E², which never divides bys. βcomes fromatan2, so every quadrant works.- The field is undefined on the focal disc (
u = 0, the equatorial plane within 522 km of the center); the code returns an error there.
- Eq. 4-8 is used in the equivalent form
- Local components: in the ENU axes at the point,
−γ·ûis the exact normal componentγ_h(eq. 4-16),γ·n̂isγ_φ(eq. 4-23, positive north), and the length is|γ_total|(eq. 4-4).- Above the ellipsoid, the vector tilts slightly toward the equator:
γ_φ < 0in the northern hemisphere,−8.1e-4 m/s²at 45° N and 100 km. - That sign is confirmed independently. The reference script differentiates the normal
potential numerically, and its value on the ellipsoid matches Table 3.6’s
U₀.
- Above the ellipsoid, the vector tilts slightly toward the equator:
Gravity models for the flight engine (earth::GravityModel)
| model | vector in L | use |
|---|---|---|
constant { g_mps2 } | (0, 0, −g) | analytic tests; comparisons with tools that use 9.80665 |
vertical_taylor | (0, 0, −γ_h) from eq. 4-3 at (φ₀, h₀ + z) | like-for-like with RocketPy’s formula (see its flight quirks below) |
vertical | `(0, 0, − | γ |
ellipsoidal (default) | the full vector at the body’s position, rotated into L | everything else |
The ellipsoidal model follows the vertical as it turns downrange: by about d/(N + h) east-west
and d/(M + h) north-south, with M = a(1 − e²)/(1 − e² sin²φ)^(3/2) the meridian radius. A
test checks both at 20 km, to 1e-4 east and 1e-3 north (the curvature changes along a meridian). Earth rotation (earth::EarthRotation) is coriolis by default, −2Ω × v with
Ω = ω(0, cos φ₀, sin φ₀), or ignore.
STANDARD_GRAVITY_MPS2 = 9.80665 is the conventional g₀ (3rd CGPM, 1901; also used by the
U.S. Standard Atmosphere 1976). It is a unit convention, not a model of local gravity. Loft used it
as gravity, which put Loft 0.3% off RocketPy at the equator (Loft lesson L1).
RocketPy 1.13.0, for like-for-like cases
Recorded by validation/oracles/rocketpy/gravity.py into
validation/fixtures/earth/rocketpy-gravity.json, for the RocketPy comparison of the validation
milestone (M2.1).
- Formula: Somigliana (4-1) times the Taylor factor (4-3), with Table 3.6 constants
(
rocketpy/environment/environment.py,somigliana_gravity). It matchesNormalGravity::taylor_mps2to under 1e-12 relative. - Sampled, then held above 80 km: a flight samples the formula at 100 points between 0 and
max_expected_height(80 km by default) and holds the last value above that. At 45° and 100 km a flight uses 9.563982 m/s², where the formula gives 9.504874. - Height datum: it is fed height above sea level, not above the ellipsoid.
- Missing latitude: the default latitude of 0 gives equatorial gravity.
- Coriolis: included as
−2Ω×vwithΩ = 2π/86164.1 s, in the 6-DOF and parachute equations but not in the rail phase.
Tests that pin this
gravity::tests::somigliana_matches_published_values(Loft lesson L1, and the done when of M1.1, the core math, frames and Earth milestone):- Table 3.6 constants to their printed digits.
- At 11 latitude/longitude/height points, including the equator, both poles, launch sites and
heights up to 200 km:
- surface (4-1), Taylor (4-3),
|γ|(4-4) andγ_h(4-16) within 1e-6 relative, and in fact within 2e-14 relative; γ_φand the ECEF vector within 1e-12 m/s².
- surface (4-1), Taylor (4-3),
- The reference values come from
validation/oracles/wgs84/normal_gravity.py, which evaluates the published formulas in 40-digit arithmetic, checks the constants against the tables, and checks the vector against the gradient of the normal potential.
taylor_series_matches_the_rocketpy_oracle: 8 points against RocketPy’s formula.exact_field_reduces_to_somigliana_on_the_ellipsoid: 37 latitudes, zero horizontal component.q_functions_match_appendix_b: the printedq₀andq₀′, and continuity where the series hands over to the closed form.earth::tests: the models agree at the pad; the ellipsoidal model follows the vertical downrange; the Coriolis direction and magnitude.
The magnetic field and declination
In short
- What it models: the Earth’s main magnetic field at any place and time from 2025.0 to
2030.0, from the World Magnetic Model (WMM2025). Its most useful output is the
declination: the angle between true north and the north a compass shows. Nothing in hpr
applies it for you: a rail’s heading (
hpr sim --heading, the builder’sheading_deg) is a true bearing, so add the declination to a compass reading first (A worked example shows how). - Sources: A. Chulliat, W. Brown, M. Nair and others, The US/UK World Magnetic Model for
2025–2030: Technical Report, NOAA NCEI (2025), with NCEI’s coefficient file
WMM2025.COF, which NCEI places in the public domain. - How well it is validated: hpr reproduces all 12 of the report’s test points (its Table 6) to their last printed digit, declination to 0.005°. NCEI’s file of 100 points, printed to 1e-6 nT and angles to 0.01°, matches to its last digit in declination, inclination, the east component and four rates. Its north component differs at 97 points, by up to 7.18e-4 nT (2.11e-8 of the total field), and the horizontal and total intensities with it; an independent check in the tests that does not use hpr’s derivative formula places that difference in NCEI’s file, not in hpr (The test values). That shows the model is computed correctly. The model itself is only as good as the Earth allows: its own error estimate for declination is 0.29° at best and 0.35° to 0.56° at the example’s sites, more near the magnetic poles. Each result carries that estimate.
- What it leaves out: local magnetic rocks, magnetic storms and the steel near a compass. Dates outside 2025.0 to 2030.0 are refused, and so are heights below −1 km or above 850 km (Limits).
Sources
Code: hpr_core::magnetic. Conventions:
Frames and Geodesy. Each source below is pinned by its checksum in the
reference library (validation/refs.lock.toml),
under the name given; DLMF is a web reference.
- [WMM] A. Chulliat, W. Brown, M. Nair, N. Gomez Perez, L.-Y. Young, C. Watson, N. Boneh,
C. Beggan, B. Meyer and M. Paniccia, The US/UK World Magnetic Model for 2025–2030: Technical
Report, National Centers for Environmental Information, NOAA, 2025,
https://doi.org/10.25923/prbc-s316. Section 1.2 gives the equations, 1.4 the poles, 1.8 the
blackout zones, 3.4 the error model. Pinned as
wmm2025-report. - [COF] NCEI,
WMM2025COF.zip(2024-12-17): the coefficient fileWMM2025.COFand the test valuesWMM2025_TestValues.txt, committed unchanged incrates/hpr-core/data/wmm2025/, with the report’s Table 6 asWMM2025_TEST_VALUES.txt. Pinned aswmm2025-coefficients, and Table 6 aswmm2025-test-values. - [DLMF] NIST Digital Library of Mathematical Functions, equation 14.10.3: the recurrence used for the Legendre functions.
What declination is
A compass needle lines up with the horizontal part of the Earth’s magnetic field. That points to
magnetic north, which is not true north (the direction of the North Pole along the ground). The
angle from true north to magnetic north is the declination D. It is positive when magnetic
north is east of true north.
So a bearing read off a compass becomes a true bearing by adding the declination:
true bearing = magnetic bearing + D
At Spaceport America in mid-2026, D is +7.75°: a rail aimed at a compass’s north points 7.75°
east of true north. hpr’s flight engine takes headings as true bearings (clockwise from true north,
see Frames), so a heading measured with a compass needs this correction first.
The field also dips into the ground. The field’s inclination I (not the rail’s inclination)
is that dip, positive downward: about 60° across the southern United States, and 90° at a
magnetic pole, where a compass has nothing horizontal to follow.
Field strengths are in nanoteslas (nT). The Earth’s field is 20,000 to 70,000 nT at the surface.
The model
The WMM writes the field as a sum of spherical harmonics: patterns over the globe that get
finer as their degree n rises, from 1 (one north and one south pole, like a bar magnet) to
12, each split into orders m from 0 to n. That makes 90 terms. Each has two Gauss
coefficients, g and h in nT (h is zero where m = 0), which set how strong the pattern
is. Each coefficient changes linearly in time from its 2025.0 value, at the rate ġ or ḣ
([WMM] eq. 9):
g(t) = g(2025.0) + (t − 2025.0) ġ
where t is the decimal year (2026.5 is the middle of 2026). hpr computes the field the way [WMM] section 1.2 sets out:
-
To geocentric coordinates (eqs. 7, 8): the site’s latitude
φ, longitudeλand height above the WGS 84 ellipsoid become a radiusrand a geocentric latitudeφ′, the angle seen from the Earth’s center. -
The field’s three parts there (eqs. 10 to 12), north
X′, eastY′and downZ′, as sums over degreenand orderm, witha = 6,371,200 m:X′ = −Σ (a/r)^(n+2) Σ (g cos mλ + h sin mλ) dP̆(sin φ′)/dφ′ Y′ = Σ (a/r)^(n+2) Σ m (g sin mλ − h cos mλ) P̆(sin φ′) / cos φ′ Z′ = −Σ (n+1)(a/r)^(n+2) Σ (g cos mλ + h sin mλ) P̆(sin φ′)P̆are the Schmidt semi-normalized associated Legendre functions ([WMM] eq. 5). -
Turned into the local frame (eq. 17) by the angle
φ′ − φbetween the two latitudes, which givesX,Y,Zin the site’s north, east and down directions. -
The elements (eq. 19): horizontal intensity
H = √(X² + Y²), total intensityF = √(H² + Z²), inclinationI = atan2(Z, H)and declinationD = atan2(Y, X).
The same sums over the coefficients’ rates ġ and ḣ give how fast each part changes per year
(eqs. 13 to 15, 18 and 20), written with a dot: Ẋ, Ḋ and so on (dD in the example).
Two details differ from the report’s printed text, and NOAA’s test values settle both:
- The poles. Equation 11 and the derivative in equation 16 divide by
cos φ′, which is zero at a pole. hpr writes each function ascosᵐφ′times a polynomial insin φ′, so the division cancels exactly and the field at a pole is the limit along the meridian given ([WMM] section 1.4). A test checks the report’s printed field over each pole. - A sign in equation 15. The rate of
Z′is printed withġ cos mλ − ḣ sin mλ. The potential gives+, as in equation 12, and only+reproduces NOAA’s rates.
Grid variation
Near the geographic poles, declination swings with every step east or west, so polar navigators
use grid variation instead ([WMM] eq. 1): the angle from a map grid’s north, that of the polar
stereographic grid (Table 6’s note), to magnetic north. It is D − λ north of 55° N, and D + λ
south of 55° S. Elsewhere hpr gives none (None), as the test values print NaN.
How far a compass can be trusted
The report marks a blackout zone around each magnetic pole, where the horizontal intensity H
is under 2,000 nT and declination can be wrong by up to 180°, and a caution zone around it,
under 6,000 nT, where declination errors exceed 1° ([WMM] section 1.8).
MagneticField::compass_zone says which applies: Reliable, Caution or Blackout. The report
draws the zones on the ground; hpr uses the field at the height asked, which is a little weaker
higher up, so its zones there are a little wider.
The report’s error model ([WMM] eq. 43) gives the declination’s expected error, one standard deviation, in degrees:
δD = √(0.26² + (5417 / H)²) H in nT
That is 0.29° where the horizontal field is strongest and grows without bound toward a magnetic
pole. MagneticField::declination_uncertainty_rad returns it. It covers the coefficients’
errors, the drift of the forecast to 2030, local rocks the model leaves out, and magnetic storms;
it does not cover a steel rail, a car or a motor case beside the compass.
A worked example
The example program
crates/hpr/examples/declination.rs
asks for the field at four launch sites on 20 June 2026. It turns the date into a decimal year
with decimal_year (the start of that day, 2026 + 170/365), then calls WMM2025.field with each
site’s latitude, longitude and height above the ellipsoid. Run it from a copy of the repository
with cargo run --example declination -p hpr. It prints:
WMM-2025 on 2026-06-20 (decimal year 2026.4658)
site D (°) ± (°) dD (°/yr) I (°) F (nT) GV (°) zone
Spaceport America, New Mexico 7.75 0.35 -0.076 59.89 46981 - Reliable
Black Rock Desert, Nevada 12.85 0.36 -0.096 64.24 49646 - Reliable
Lucerne Dry Lake, California 11.16 0.35 -0.079 59.38 46407 - Reliable
Andøya, Norway 9.41 0.56 0.268 78.16 53674 -6.61 Reliable
At Spaceport America, New Mexico, a rail aimed at magnetic north points 7.75° east of true north.
± is the model’s own error estimate, dD the declination’s drift per year, GV the grid
variation (only at Andøya, north of 55°). The drift is small: Spaceport America’s declination
falls by about 0.08° a year, so it moves well under a degree over the model’s five years.
The test values
| check | points | what is compared | worst difference | held to |
|---|---|---|---|---|
| the coefficient table | 90 rows | every coefficient against WMM2025.COF | none | equal |
| the report’s Table 6 | 12 | X, Y, Z, H, F, their rates; I, D, grid variation, their rates | half the last digit | 0.05 nT, 0.005° |
| the report’s Table 3b | 1 | φ′, r, the coefficients, X′, Y′, Z′, X … Ḋ, printed to 10 decimals | 5.0e-11 nT on X′ | half the last digit, plus 8 units in the last place |
| the report’s poles (section 1.4) | 2 | X, Y, Z over each pole at r = a | inside 0.05 nT | 0.05 nT |
| NCEI’s high-precision file, printed to 1e-6 nT and angles to 0.01° | 100 | Y, D, I, and the rates of Y, Z, D, I | half the last digit | 5e-7 nT, 0.005° |
| the same file | 100 | X, H, F | 7.18e-4 nT | 7.2e-4 nT, the measured worst |
| the same file | 100 | Z; the rates of X, H, F | 2.2e-6 nT; 1.5e-6 nT/yr | the measured worst |
| the potential, by differences | 100 | hpr’s X′ and Ẋ′ | 1.3e-7 nT; Ẋ′ within the rounding allowance (under 1e-9 nT/yr) | 1e-6 nT; 1e-6 nT/yr |
Rows are counted from 0. The north component X in NCEI’s file differs from hpr’s at 97 of its
100 points, by up to 7.18e-4 nT (row 35: 2026.5, 12 km, 33° N, 145° W). That is at most 2.11e-8
of the total field, and 140 times smaller than the 0.1 nT the report allows for single precision
(the note under its Table 6).
The tests show where the difference comes from:
- It lies in one quantity, the geocentric
X′. If the file’sX′is off by an amounte, itsXmoves bye cos(φ′ − φ)and itsZbye sin(φ′ − φ). Takingefrom each point’sXand applying it toZbrings the file’sZto within 4.9e-7 nT of hpr’s, inside its printing. - hpr’s
X′is right. The tests take the potential’s derivative in latitude by differences, independently of hpr’s formula for it: hpr’sX′matches to 1.3e-7 nT at all 100 points. The report’s own ten-decimalX′(Table 3b) matches to 5e-11 nT. - A second program agrees with hpr. pygeomag 1.1.0 (MIT), a port of NOAA’s own
geomagprogram, run once by hand on two of the points (rows 3 and 35, not in CI), gives hpr’sXto 7e-7 nT, so it differs from the file by the same amount.
The rate of X differs too, by up to 9.5e-7 nT a year at 24 points: a second, smaller residue
whose cause is not known. hpr’s Ẋ′ passes the same derivative check. The rates of H and F
follow from X and its rate.
So the file’s X, H, F and Z, and the rates of X, H and F, are held to the measured
differences, not to the file’s printing. ADR-125 records the decision. For a flight
the difference is immaterial: 7.18e-4 nT turns the declination by about a millionth of a degree.
Every comparison with Table 6 and NCEI’s file first allows 64 units in the last place of the point’s total field, about 7e-10 nT, for the rounding in both programs’ sums: about 700 times below half the finest printed digit.
A property test also checks, at random places and times, that H, F, I and D agree with
X, Y, Z by their definitions, and that F lies between 20,000 and 70,000 nT. The report’s
Table 1 rounds the surface range to 23,000 to 67,000 nT, but the model itself falls to about
21,900 nT over South America by 2030, which a test pins at 26° S, 61° W.
Limits
- Five years only. WMM2025 covers 2025.0 to 2030.0. A flight log from 2024 needs WMM2020, which hpr does not bundle; such a date is refused, not extrapolated.
- Heights from −1 km to 850 km. The model is specified from 1 km below the WGS 84 ellipsoid to 850 km above it ([WMM] section 3); outside that, a height is refused.
- Height above the ellipsoid. Heights are above WGS 84, as
Geodeticholds them. A site’s height above sea level (from a map, a GPS or an altimeter) differs by the geoid, up to about 100 m; the report puts that effect at about 1 nT or less, far below the model’s error, so a height above sea level can be used as it is. - The main field only. The WMM leaves out the crust’s local fields, which can move a compass by degrees near iron ore or volcanic rock, and the fields of magnetic storms. The error estimate allows for them as a worldwide average; at any one site they can be larger.
- Nothing in a flight uses it yet. Converting a compass heading is the caller’s step, with
MagneticField::true_from_magnetic_rad.
Atmosphere
In short
- What it models: the air’s temperature, pressure, density, speed of sound and viscosity by height: the 1976 U.S. Standard Atmosphere, optionally shifted to field conditions, humid air, and weather-balloon soundings or forecasts; and the pressure altitude a barometric altimeter reads.
- Sources: the U.S. Standard Atmosphere, 1976; the WMO’s Guide to Instruments and Methods of Observation (WMO-No. 8, 2023); the CIPM-2007 moist-air density formula (Picard et al., 2008).
- How well it is validated: every value within 0.1% of the 1976 tables at 32 altitudes from −2 to 86 km; humid density within 0.047% of CIPM-2007 over 15–27 °C. Against RocketPy, its density agrees within 3.7e-4 over the 23 heights its parachute descents sample (Recovery). Its pressure altitude matches two flight logs’ own altimeter readings of their pressure, to 0.195 m and 1.321 m (report); the air itself has no real-flight check.
- What it leaves out: a real day’s changes aloft. A field-condition offset holds all the way up (+20 K at a 1400 m field puts density +30% off the standard’s at 30 km), so higher flights need a sounding. Viscosity ignores humidity, which lowers it 2.1% at 30 °C and saturation.
Code and sources
Code: hpr_atmos::ussa76 (the standard and its offsets), hpr_atmos::moist (humid air),
hpr_atmos::profile (soundings and forecasts), and the Atmosphere trait in hpr_atmos::air.
Sources:
- [USSA] U.S. Standard Atmosphere, 1976, NOAA-S/T 76-1562, part 1, pinned as
us-std-atmosphere-1976. Equation, table and page numbers below are its own. - [WMO] WMO-No. 8, Guide to Instruments and Methods of Observation, Vol. I (2023),
wmo-no8-vol1-2023. - [CIPM] A. Picard et al., “Revised formula for the density of moist air (CIPM-2007)”,
Metrologia 45 (2008) 149–155,
picard-2008-cipm-2007.
Height datum
Every atmosphere is queried with geometric height above mean sea level (height_msl_m). The
core’s heights are ellipsoidal (Frames), so the flight engine subtracts the geoid undulation
first: H_msl = h − N. Samples carry an extrapolated flag, set whenever a model answers outside
the range it is defined or tabulated over.
The 1976 standard, −5 km to 86 km
Seven layers in geopotential altitude H, each with a constant gradient of the molecular-scale
temperature T_M ([USSA] Table 4):
base H_b (km′) | 0 | 11 | 20 | 32 | 47 | 51 | 71 | 84.852 (top) |
|---|---|---|---|---|---|---|---|---|
L_M,b (K/km′) | −6.5 | 0 | +1.0 | +2.8 | 0 | −2.8 | −2.0 |
H = r₀ Z / (r₀ + Z) (18)
T_M = T_M,b + L_M,b (H − H_b) (23)
P = P_b [T_M,b / T_M]^(g₀′ M₀ / (R* L_M,b)) (33a) L_M,b ≠ 0
P = P_b exp[−g₀′ M₀ (H − H_b) / (R* T_M,b)] (33b) L_M,b = 0
ρ = P M₀ / (R* T_M) (42)
T = T_M M / M₀ (22)
a = (γ R* T_M / M₀)^½ (50)
μ = β T^(3/2) / (T + S) (51)
ν = μ / ρ (52)
- Constants:
R* = 8314.32 J/(kmol·K)(p. 3), not CODATA’s 8314.46. Table 2 misprints the exponent’s sign.M₀ = 28.9644 kg/kmol,g₀ = g₀′ = 9.80665,r₀ = 6 356 766 m.P₀ = 101325 Pa,T₀ = 288.15 K,γ = 1.40,β = 1.458e-6.S = 110.4 K(p. 19). Table 2 and p. 4 print 110 K, but the tables use 110.4 K: sea-level μ is 1.7894e-5 Pa·s with it and 1.7912e-5 with 110.
- 80 to 86 km:
M/M₀comes from Table 8, interpolated linearly inZ. The printed tables leave it out below 86 km and printT = T_M(p. 9). This model follows the equations, so its kinetic temperature there is up to 0.036%, and its viscosity up to 0.031%, below the print. - Outside the range: below −5 km the first layer continues, and above 86 km the atmosphere is isothermal at 186.87 K. Both are flagged. The real standard is also isothermal from 86 to 91 km, then warms, and its composition changes above 86 km. Pressure and density there are rough, but tiny.
- Loft got this wrong (Loft lessons L2 to L4):
- It fed geometric altitude to geopotential formulas: at 11 km it gave 216.65 K and 22 632 Pa, against 216.774 K and 22 699.96 Pa.
- It had only four layers, so at 70 km it gave 335 K against 219.6 K.
- Its Sutherland constants gave a sea-level viscosity 1.3% high.
Offsets and launch-site conditions
Ussa76::with_offset(ΔT, P₀) adds ΔT to T_M at every geopotential height and integrates eqs.
33a/33b from P₀, so the atmosphere stays hydrostatic. Ussa76::anchored(Z, T, P) picks ΔT and
P₀ so the profile passes through a measured temperature and pressure, such as field conditions.
This is not the aviation convention (ESDU 77022), which offsets temperature at equal pressure
altitude. Both are hydrostatic, but they differ aloft. At H = 3000 m′ with ΔT = +20 K, the
aviation convention gives 289.95 K where this gives 288.65 K, and densities 0.38% apart
(validation/oracles/atmosphere/conventions.py). This choice keeps the lapse rate attached to
height, like a sounding; see the atmosphere decision, ADR-004.
An anchor’s offset holds all the way up, which a real hot or cold day doesn’t. Anchoring
+20 K at a 1400 m field, at the standard’s pressure there, gives these densities against the
standard (conventions.py):
| height | 3 km | 20 km | 30 km |
|---|---|---|---|
| density | −5.7% | +14% | +30% |
Field conditions suit flights of a few kilometers; higher flights need a sounding.
Pressure altitude: what a barometric altimeter reads
A barometric altimeter measures pressure, not height. It
turns the pressure into the height at which the 1976 standard has that pressure, its pressure
altitude, and subtracts the pad’s. Ussa76::pressure_altitude_m(P) computes the same number,
so hpr can read its own flight the way a logged flight was read
(Accuracy: real flights).
It inverts eqs. 33a and 33b in the layer whose base pressures bracket P:
H = H_b + (T_M,b / L_M,b) [(P / P_b)^(−R* L_M,b / (g₀′ M₀)) − 1] L_M,b ≠ 0
H = H_b − (R* T_M,b / (g₀′ M₀)) ln(P / P_b) L_M,b = 0
In the standard’s troposphere this is the altimeter formula
H = 44330.8 m × [1 − (P / 101325 Pa)^0.190263]. The result is in
geopotential meters, m′, as an altimeter’s is.
A worked example. An altimeter on a pad at 86000 Pa reads 1361.8 m′ there. At 58000 Pa it reads
4464.4 m′, so it logs a climb of 3102.6 m′. On a day 20 K warmer than the standard all the way up,
with the same sea-level pressure, the same two pressures lie 3318.0 m′ apart: the rocket climbed
6.9% more than its altimeter says, and the altimeter reads 6.5% less than the climb. Warm air is
less dense, so pressure falls more slowly with height. In the troposphere, with the sea-level
pressure unchanged, the ratio is exactly (T₀ + ΔT) / T₀ = 308.15 / 288.15.
Where it is used. hpr’s flights don’t use it: they fly in the air of the day. The real-flight
comparison reads hpr’s height through it, from the ERA5 pressure at the center of mass, when the log
is barometric (hpr_validate::real_flight::Barometer). Two logs that record their pressure,
Prometheus’s and Juno III’s, are this reading less the first row’s, to 0.195 m and 1.321 m over the
rows compared
(report).
Moist air
An ideal mixture of dry air (M₀, γ = 1.4) and water vapour (M_v = 18.01528 kg/kmol,
[CIPM] §2.1):
e_w(t) = 6.112 exp(17.62 t / (243.12 + t)) hPa [WMO] Annex 4.B, eq. 4.B.1, t in °C
e = U e_w(T), x_v = e / p
ρ = p [(1 − x_v) M₀ + x_v M_v] / (R* T) = p / (R_d T_v) [WMO] eq. 12.18
C_p = (1 − x_v)(7/2) R* + x_v · 4 R*, γ = C_p / (C_p − R*), a = (γ R* T / M)^½
- Relative humidity is taken with respect to liquid water at every temperature, as
radiosondes report it ([WMO] §12.1.2). No enhancement factor is applied. Near the surface it
would raise
eby about 0.47%, which changes density by at most 0.013% (at 40 °C and saturation), and less in cooler or drier air. - Accuracy:
- Density agrees with CIPM-2007 (a real-gas equation) to 0.047% over its range: 15–27 °C, 600–1100 hPa, dry to saturated. Ignoring humidity entirely is 0.4% off at 20 °C and 50% RH.
- Taking water vapour’s
C_pas4R*instead of its real value near 300 K (about 1% higher) movesaby 0.009% at 30 °C and saturation. - Viscosity stays dry air’s Sutherland value. By Wilke’s rule with IAPWS R12-08 vapour viscosity, saturation at 30 °C lowers it 2.1%, which moves turbulent skin friction about 0.4%.
- All these numbers come from
validation/oracles/atmosphere/moist_air.py.
Sounding and forecast profiles
SoundingProfile takes the site’s latitude and levels of geometric height, temperature, optional
pressure, optional relative humidity, and optional wind speed and direction. Humidity and wind
are given on every level or on none. A given pressure must be below the level beneath it, which
catches hPa entered as Pa.
- Gravity: the profile works in WMO geopotential height
Z(z, φ)([WMO] eqs. 12.15–12.16), whose gravity is the normal gravity at the site’s latitude.- Surface gravity runs from 9.780 m/s² at the equator to 9.832 m/s² at the poles, ±0.27%
around the standard’s
g₀. - Over 2 km at 293 K the same sounding’s pressure falls 0.12% more at the pole than at the equator.
- A latitude-free geopotential would leave errors of that size.
- Surface gravity runs from 9.780 m/s² at the equator to 9.832 m/s² at the poles, ±0.27%
around the standard’s
- Between levels, interpolation runs in
Z:TandUare linear inZ.- Pressure uses
ln P = ln P_i + ln(P_{i+1}/P_i) · ln(T/T_i)/ln(T_{i+1}/T_i), which is exact for a dry hydrostatic layer withTlinear inZand passes through both levels’ pressures. - Levels sampled from the standard at its geopotential levels reproduce it between them to 1e-12, at any latitude.
- Missing pressures above the lowest level are filled hydrostatically, with virtual
temperature linear in
Z([WMO] eqs. 12.17–12.18).- A dry fill agrees with direct integration under WGS 84 normal gravity to 2e-8. The limit is WMO’s rounded surface gravity constants, 3–5e-8 low.
- A humid fill agrees to 1.1e-5, because vapour pressure is exponential in
Tand so not quite linear across the layer.
- Beyond the levels the profile continues as the standard atmosphere anchored at the end
level and evaluated at the same geopotential (so still with the local gravity), and flags the
sample:
- Below, it holds relative humidity.
- Above, it holds the vapour mole fraction, capped at saturation, so a humid top doesn’t carry water into the cold stratosphere.
- The continued pressure is the dry standard’s. It is hydrostatic for dry air, but in humid
air it falls up to
x_v(1 − M_v/M₀)faster (1.6% of the gradient at 30 °C and saturation).
- Geopotential heights. Soundings (Wyoming’s
HGHT) and forecasts (Open-Meteo’sgeopotential_height) report geopotential meters above sea level. Convert them withgeometric_from_wmo_geopotential_m. At 30 km that is 29.7785 km of geopotential at the equator and 29.932 km at 80° N. The standard’s latitude-freer₀formula is only for the standard itself. - From an ERA5 file:
hpr_io::era5builds this profile over a launch site at launch time from the day’s reanalysis; see ERA5 weather files. - From Open-Meteo:
hpr_net::open_meteobuilds it from a forecast, or an archived one, on pressure levels; see Launch-day weather. - From a weather balloon:
hpr_net::wyomingbuilds it from a radiosonde’s sounding in the University of Wyoming’s archive; see Weather-balloon soundings. - Loft lesson L5: “today’s conditions” kept the standard lapse from the field up, ignored humidity and never used sounding temperatures.
RocketPy 1.13.0, for the code-to-code comparison
These are findings from reading refs/rocketpy for the RocketPy comparison of the validation
milestone (M2.1), not yet pinned by fixtures:
- Standard atmosphere: ISO 2533 layers from −2 to 80 km, with
R = 287.05287(the same asR*/M₀). Temperature is linear in geometric height between converted layer boundaries. Pressure is sampled at 100 points and splined. - Custom profiles:
- Every quantity, pressure included, is linear in height above sea level, with constant extrapolation.
- Linear pressure is off hydrostatic by up to 0.06% between 1000 and 925 hPa, 1.15% between
700 and 500 hPa, and 3.4% between 50 and 30 hPa (
conventions.py). - M2.1 comparisons need a RocketPy-compatible option or levels dense enough that this doesn’t matter.
- Humidity is not used anywhere.
- Wyoming heights are converted from geopotential with a radius only (no latitude).
- The helper’s default radius, 63 781 370 m in
rocketpy/tools.py:972, is ten times the Earth’s. - Check which callers rely on that default before the M2.1 comparisons.
- The helper’s default radius, 63 781 370 m in
Tests that pin this
ussa76::tests::matches_the_1976_tables_at_32_altitudes(the done when of M1.2, the atmosphere and wind milestone):- Covers
T,T_M,H,P,ρ,a,μandνat 32 altitudes from −2 to 86 km. - Every value is within 0.1%, and within one count of its last printed digit.
- The exceptions are those above (80–85.5 km, where the printed
TequalsT_M) and the 84 km density. The latter prints 9.6940E-6 where the equations give 9.69387e-6; the row’s ownρ/ρ₀agrees with the equations. - The fixture was transcribed from the page images and cross-checked by
validation/oracles/ussa76/tables.pyagainst mpmath andambiance.
- Covers
- Loft lessons:
geometric_11_km_matches_the_1976_tables(Loft lesson L2)fifty_km_is_270_65_k_and_79_779_pa(Loft lesson L3)sea_level_viscosity_is_1_7894e_5(Loft lesson L4)profile::tests::sounding_temperature_overrides_standard_lapse(Loft lesson L5)
- Pressure altitude:
pressure_altitude_inverts_the_1976_tables: each printed pressure’s altitude is the printed geopotential altitude at all 32 rows, to what the prints resolve.pressure_altitude_is_the_altimeter_formula_in_the_troposphere, and its refusals.pressure_altitude_inverts_the_pressure: a property test over offsets and heights, the extrapolated layers included.
- Constants and structure:
constants_match_the_transcriptionchecks the constants and Tables 4 and 8.- Hydrostatic balance
dP/dZ = −ρgis checked in every layer, and as a property test over offsets. - Anchors reproduce their conditions, and layers join continuously.
moist::tests:- WMO 4.B.1 against the formula evaluated separately in mpmath (WMO prints no table of it).
- Dry air equals the standard.
- CIPM-2007 density to 0.05% at 36 points.
profile::tests:- Standard levels are reproduced at 0°, 45° and 80°.
- Dry fills at 0°, 33° and 90°, and a humid fill, match direct integration under WGS 84 normal gravity.
- Pressure falls faster at the pole.
- The continuation beyond the levels is hydrostatic when dry, with the humid shortfall pinned.
- WMO geopotential values.
- Errors (including rising pressures) and serde.
Wind
In short
- What it models: the steady wind’s speed and direction at each height: constant, growing with height from one measured wind (a power or log law), or from a table of levels, such as a forecast.
- Sources: the US military flying-qualities specification MIL-F-8785C (1980), NASA’s climatic criteria for aerospace vehicles NASA/TM-2008-215633 (2008), and the World Meteorological Organization’s observing guide WMO-No. 8 (2023).
- How well it is validated: tests with exact answers pin each model: with speed and direction interpolated separately (the default), halfway between 4 m/s from 350° and 12 m/s from 30° a table gives 8 m/s from 10°. In RocketPy’s parachute descents, hpr’s wind, interpolated by components as RocketPy does, matches RocketPy’s samples to 1e-9 m/s (in scientific notation: a thousand-millionth of a meter per second). That includes the wind of NDRT 2020, one of the example rockets, which changes with height. In the four descents with wind, the total drift agrees within 0.28%, and each east or north part within 2.9% (Recovery). Seven real flights have been flown in the ERA5 winds of their day, but only their heights are compared, not their drift (Accuracy: real flights).
- What it leaves out: wind varying in time or across the field, vertical wind, terrain, and gusts (no flight uses Turbulence). A flight takes one wind model, so a power or log law, which keeps growing with height, can’t hand over to winds aloft.
Code and sources
Code: hpr_atmos::wind in the API reference. Random gusts on
top of the steady wind are in Turbulence. The example program under
An example of each builds every model.
Sources. Each is pinned under the id given, in the reference lock file, which records where to download it and its SHA-256 hash (Checking a claim explains how to fetch and check it):
- [8785C] MIL-F-8785C, Flying Qualities of Piloted Airplanes (1980), §3.7.3.2, pinned as
mil-f-8785c. - [TM] NASA/TM-2008-215633, Terrestrial Environment (Climatic) Criteria Guidelines for Use
in Aerospace Vehicle Development (2008), §2.2.5.2 and Table 2-21,
nasa-tm-2008-215633. - [WMO] WMO-No. 8 (2023), Vol. I, chapter 5 and its annex,
wmo-no8-vol1-2023.
Conventions
- Velocity: a wind model returns the velocity of the air, in m/s, along the east, north and up axes of the launch frame. The flight engine’s airspeed is the rocket’s velocity minus this. Mean wind (the steady wind, without gusts) is horizontal, so its up part is zero.
- Direction: meteorological: the direction the wind blows
from, clockwise from true north, in radians. For a wind of speed
Vfrom directionθ, the east and north parts arev_E = −V sin θandv_N = −V cos θ. A wind from the west (3π/2, 270°) blows toward the east,+E. RocketPy’s heading is the direction the wind blows toward,θ + π. - Height: a flight asks every model for the wind at a height above mean sea level, as it asks the atmosphere. Some models are built from heights above the ground instead: Which height? says which.
- Flags: each answer a wind model gives carries a flag, which is set when the height is
outside what the model covers. That is below a table’s lowest level or above its highest, or
below the ground for a power or log law.
- The model still answers: a table holds its end level’s wind, and a law gives calm below the ground. The flag says that the answer was filled in, not taken from the model’s data.
- In code it is the answer’s
extrapolatedfield:Nonenormally, andSome(Side::Below)orSome(Side::Above)when flagged. - The power and log laws are not flagged high above the ground, although they describe only the air near it.
- A flight doesn’t read the flag yet (issue #45). A rocket that climbs above a table’s top level flies on the held wind, and nothing in its result says so. Ask the model yourself before flying a table, as the example does: it marks each flagged answer with a star.
Models
In the laws below, V is the wind speed at height z above the ground, and V_ref the speed
measured at a reference height z_ref above the ground, such as the 10 m mast of a weather
station. Each law blows from one direction at every height.
ConstantWind: one velocity at every height.PowerLawWind: the speed grows as a power of height,V = V_ref (z/z_ref)^αforz > 0, and is zero at and below the ground. A height below the ground is flagged.αis the exponent: the larger it is, the faster the wind grows with height.- [TM] eq. 2.1 gives the law for peak winds below 150 m, with
z_ref = 18.3 m. A peak wind is the strongest speed over a period, gusts included, not the steady mean wind hpr flies. - [TM] Table 2-1:
α = 0.2for 7–22 m/s and 0.14 above 22 m/s. Eq. 2.22 gives1/7withz_ref = 10 mfor strong 10 m winds. - Those exponents describe profiles of peak winds, and of strong 10 m winds. No single value fits mean winds everywhere, so treat any exponent as a starting point, not a measurement of your field.
- The example uses
α = 1/7(about 0.14) with a 10 m reference height, the pairing of [TM] eq. 2.22. Its power law column shows what that does to a 5.0 m/s wind at 10 m: 4.0 m/s at 2 m, 6.9 m/s at 100 m, and 10.7 m/s at 2,000 m, where it is still growing. - The law describes the surface layer: the air nearest the ground, where friction with the ground sets how fast the wind grows with height ([TM] fits it below 150 m). Above that layer the law keeps growing, so pair it with measured winds aloft.
LogLawWind: the speed grows with the logarithm of height,V = V_ref ln(z/z₀)/ln(z_ref/z₀)forz > z₀, and is zero from the ground up toz₀.z₀is the roughness length: the height at which the law’s wind falls to zero. It is set by how rough the ground is (see the values below).- This is the neutral surface-layer law
V = (u*/κ) ln(z/z₀)([WMO] ch. 5 annex; [TM] eq. 2.8 withΨ = 0), written through a reference wind. Thereu*is the friction velocity, a speed that measures how hard the wind drags on the ground, andκis the von Kármán constant, a fixed number of the theory. Writing the law through the wind measured atz_refcancels both, so hpr needs neither. Ψin [TM] eq. 2.8 corrects for the air’s stability: air warmed from below mixes more, and air cooled from below mixes less, which changes how the wind grows with height.Ψ = 0is neutral air, where neither happens. hpr has only the neutral law.- [8785C] §3.7.3.2 uses it with
z_ref = 20 ft. - Roughness lengths: 0.03 m for open flat terrain with grass ([WMO]; this is class 3 of the Davenport–Wieringa classification there, which gives a roughness length for each kind of terrain); 0.001–0.01 m for mown grass and 0.01–0.04 m for low grass or steppe ([TM] Table 2-21).
LayeredWind: speed and direction tabulated at heights, as from a sounding or a forecast. Between levels it interpolates in one of two ways, set byWindInterpolation.SpeedDirection(the default) interpolates speed linearly and turns the direction along the shorter arc. Between exactly opposite directions it turns clockwise with height. A turning wind keeps its speed.- With
SpeedDirection, a calm level (speed 0) takes the other level’s direction, because reports give calm as “0 from 0°”. Without that rule a wind growing out of calm would swing through a meaningless direction and create a crosswind neither level has. Componentsinterpolates the east and north parts linearly, as RocketPy does. Between levels 90° apart the speed dips by up to 29%.- Beyond the end levels the end wind is held and the sample is flagged.
- Put the surface wind (for example the 10 m observation) in as the lowest level, so the profile blends up from it.
WindModel: any one of the four, as a flight or a file takes it. In JSON itsmodelfield names which one (In JSON).
Loft lesson L6, a mistake found in Loft, the project before hpr-sim: design-file runs used one wind vector, and forecast profiles stepped at the lowest level instead of blending from the surface.
Which height?
A flight asks the wind model for the wind at the rocket’s
height above mean sea level. The laws are written in
height above the ground, so they take the ground’s height above sea level as well. Each input’s
name says which height it is: _msl_ is above mean sea level, and _agl_ is above the ground
(AGL).
| Model | Heights you give it | Measured from |
|---|---|---|
ConstantWind | none: the same wind at every height | n/a |
PowerLawWind | the reference height, reference_height_agl_m | the ground |
the ground’s height, ground_msl_m | sea level | |
LogLawWind | the reference height, reference_height_agl_m, and the roughness length, roughness_length_m | the ground |
the ground’s height, ground_msl_m | sea level | |
LayeredWind | each level’s height_msl_m | sea level |
A sounding’s wind (SoundingProfile) | each level’s height_msl_m | sea level |
So a table’s levels, from a forecast or a sounding, are above sea level. At a site 1,400 m above sea level, a forecast’s 10 m wind goes in at 1,410 m. Entered at 10 m instead, the whole profile sits 1,400 m too low. The example’s last column shows what the flight would then see: 11.6 m/s from 298° near the pad instead of 5.0 m/s from 270°, and the top level’s 12.0 m/s from 300° from 100 m up. Above that, every sample is flagged as beyond the table, which is the sign to look for.
What to enter for your field’s elevation. The launch site’s height, the third number in
Geodetic::from_degrees(latitude, longitude, height), is an
ellipsoidal height: height above the
WGS 84 ellipsoid, the smooth shape hpr gives the Earth. A field’s
elevation, as a map gives it, is height above sea level instead. The two differ by the geoid
undulation N, the height of sea level above the ellipsoid at that place, which can be up to
about 100 m. hpr has no map of N, so there are two ways to set up a site:
- The simple way, which the examples take. Enter your field’s elevation above sea level as
the site’s height, and leave
Nat 0, asEnvironment::standardsets it. The air and the wind are then looked up at the right heights above sea level. What is off is the height above the ellipsoid, by your field’sN. A flight uses that only to work out gravity, which changes by about 0.003% over 100 m of height. - The exact way, if you know
Nat your field. Enter the elevation plusNas the site’s height, and give the environmentNwithwith_geoid_undulation_m(Environment).
Either way, the ground is the site’s height_m less the environment’s geoid_undulation_m above
sea level, which is how the example works it out. A power or log law needs that number as its
ground_msl_m, and doesn’t take it from the site by itself. If its ground_msl_m is wrong, its
whole profile is shifted up or down by the error.
Three more things to watch:
- Geopotential heights. Soundings and forecasts often give heights in geopotential meters
above sea level, not geometric meters (the meters a tape measure would give).
- A geopotential meter measures height by the work done lifting a mass against gravity: it is the climb that takes as much work as one meter does where gravity is 9.80665 m/s².
- Gravity varies with height and latitude, so the two differ by an amount that depends on both. 30 km above sea level is 29.7785 km of geopotential at the equator and 29.932 km at 80° N.
- Convert them first with
geometric_from_wmo_geopotential_m(Atmosphere explains).
- A sounding’s wind is a
LayeredWind(SoundingProfile::wind). A flight flies it only if you also pass it towith_wind: the atmosphere and the wind are separate parts ofEnvironment. - The flags. A table flags a height below or above its levels, and a law a height below the ground (Conventions says how to read a flag). A flight doesn’t report flags yet (issue #45), so ask the model yourself before flying a table, as the example does.
An example of each
The example program
wind_profiles.rs
builds each model for the launch site of Getting started, 1,400 m above
sea level. It prints the wind each one gives at a few heights above the ground, and flies nothing.
Run it from anywhere in the repository:
cargo run --example wind_profiles -p hpr-sim
It prints this:
Wind at a site 1400 m above sea level: speed in m/s, from a direction in °
above ground (m) constant power law log law layered wrong datum
2 5.0 from 270 4.0 from 270 3.6 from 270 5.0 from 270* 11.6 from 298
10 5.0 from 270 5.0 from 270 5.0 from 270 5.0 from 270 11.6 from 298
50 5.0 from 270 6.3 from 270 6.4 from 270 5.2 from 271 11.8 from 299
100 5.0 from 270 6.9 from 270 7.0 from 270 5.6 from 272 12.0 from 300
500 5.0 from 270 8.7 from 270 8.4 from 270 8.0 from 280 12.0 from 300*
1000 5.0 from 270 9.7 from 270 9.0 from 270 10.0 from 290 12.0 from 300*
2000 5.0 from 270 10.7 from 270 9.6 from 270 12.0 from 300* 12.0 from 300*
* beyond the table's levels: the end level's wind, held, and the sample flagged
{
"model": "layered",
"levels": [
{
"height_msl_m": 1410.0,
"speed_m_s": 5.0,
"direction_from_rad": 4.71238898038469
},
{
"height_msl_m": 1900.0,
"speed_m_s": 8.0,
"direction_from_rad": 4.886921905584122
},
{
"height_msl_m": 2900.0,
"speed_m_s": 12.0,
"direction_from_rad": 5.235987755982989
}
],
"interpolation": "speed_direction"
}
Reading it:
- Each law passes through its reference wind, 5.0 m/s at 10 m, and blows from 270° at every height.
- The power and log laws agree near their reference height and part away from it: 4.0 against 3.6 m/s at 2 m, and 10.7 against 9.6 m/s at 2,000 m. Both keep growing far above the surface layer they describe, which is why winds aloft should come from a table.
- The layered wind blends from 5.0 m/s from 270° at 10 m to 8.0 m/s from 280° at 500 m, turning with height. At 2 m, below its lowest level, it holds the 10 m wind; at 2,000 m, above its top level, it holds 12.0 m/s from 300°. The star marks both as flagged.
- The wrong datum column is the same table with its heights entered above the ground by mistake (Which height?).
- The JSON at the end is the layered wind as a file holds it (In JSON).
To fly one of them, give it to the flight’s environment, as the program does for each column:
Environment::standard(site)?.with_wind(wind). Then build the Simulation from that environment,
as Getting started does with its constant wind.
The program, which CI compiles and runs on macOS, Windows and Linux:
//! Wind profiles: each kind of steady wind hpr can fly, built for a launch site 1,400 m above sea
//! level, with the wind each one gives at a few heights above the ground, and how a flight takes
//! one. Nothing is flown.
//!
//! Run it from anywhere in the repository:
//!
//! ```text
//! cargo run --example wind_profiles -p hpr-sim
//! ```
//!
//! The documentation site's *Wind* page (`docs/physics/wind.md`) walks through it. What it prints
//! is kept next to it in `wind_profiles.output.txt`, and CI checks that the two still agree
//! (`cargo xtask examples --check`).
#![allow(
clippy::print_stdout,
reason = "the project's lints forbid printing in library code, and this program exists to print"
)]
use std::error::Error;
use hpr_atmos::{
ConstantWind, LayeredWind, LogLawWind, PowerLawWind, WindInterpolation, WindLevel, WindModel,
};
use hpr_core::DVec3;
use hpr_core::geodesy::Geodetic;
use hpr_sim::Environment;
fn main() -> Result<(), Box<dyn Error>> {
// The launch site of Getting started: New Mexico, 1,400 m up.
let site = Geodetic::from_degrees(32.99, -106.97, 1400.0)?;
let calm = Environment::standard(site)?; // no wind until it is given one
// A flight asks for the wind at its height above mean sea level: its height above the WGS 84
// ellipsoid, which is how hpr takes the site's height too, less the geoid undulation N (0
// unless you set it). So the ground is this high above sea level:
let ground_msl_m = site.height_m - calm.geoid_undulation_m;
let from_west = 270_f64.to_radians();
// 1. The same wind at every height: 5 m/s from the west. It takes no height.
let constant = ConstantWind::new(5.0, from_west)?;
// 2. A power law through 5 m/s at 10 m above the ground, with exponent α = 1/7. The
// reference height is above the ground; the ground's height is above sea level.
let power_law = PowerLawWind::new(5.0, 10.0, 1.0 / 7.0, from_west, ground_msl_m)?;
// 3. A log law through the same 5 m/s at 10 m, over grass: roughness length z₀ = 0.03 m.
let log_law = LogLawWind::new(5.0, 10.0, 0.03, from_west, ground_msl_m)?;
// 4. A table of levels, as from a forecast. Its heights are above sea level, so a level given
// above the ground goes in at the ground's height plus its own. The 10 m wind is the
// lowest level, so the profile blends up from it.
let level = |height_agl_m: f64, speed_m_s: f64, from_deg: f64| WindLevel {
height_msl_m: ground_msl_m + height_agl_m,
speed_m_s,
direction_from_rad: from_deg.to_radians(),
};
let forecast = vec![
level(10.0, 5.0, 270.0),
level(500.0, 8.0, 280.0),
level(1500.0, 12.0, 300.0),
];
let layered = LayeredWind::new(forecast.clone(), WindInterpolation::SpeedDirection)?;
// The same table with its heights entered above the ground by mistake: 1,400 m too low.
let wrong_levels = forecast
.iter()
.map(|level| WindLevel {
height_msl_m: level.height_msl_m - ground_msl_m,
..*level
})
.collect();
let wrong_datum = LayeredWind::new(wrong_levels, WindInterpolation::SpeedDirection)?;
// Any of them goes to a flight the same way, through the flight's environment. Each
// environment here is the one a `Simulation` from this site would fly in.
let columns = [
("constant", WindModel::Constant(constant)),
("power law", WindModel::PowerLaw(power_law)),
("log law", WindModel::LogLaw(log_law)),
("layered", WindModel::Layered(layered.clone())),
("wrong datum", WindModel::Layered(wrong_datum)),
];
let mut environments = Vec::new();
for (_, wind) in &columns {
environments.push(Environment::standard(site)?.with_wind(wind.clone()));
}
println!(
"Wind at a site {ground_msl_m:.0} m above sea level: speed in m/s, from a direction in °"
);
println!();
let mut header = "above ground (m)".to_owned();
for (name, _) in &columns {
header += &format!("{name:>14} ");
}
println!("{}", header.trim_end());
for height_agl_m in [2.0, 10.0, 50.0, 100.0, 500.0, 1000.0, 2000.0_f64] {
let mut row = format!("{height_agl_m:>16.0}");
for environment in &environments {
// What the flight engine asks for: the wind at a height above sea level.
let sample = environment.wind.wind(ground_msl_m + height_agl_m)?;
let (speed_m_s, from_deg) = speed_and_direction_from(sample.velocity_enu_m_s);
let cell = format!("{speed_m_s:.1} from {from_deg:.0}");
// A star marks a height beyond a table's levels, where the model holds the end
// level's wind and flags the sample.
let flag = if sample.extrapolated.is_some() {
"*"
} else {
" "
};
row += &format!("{cell:>14}{flag}");
}
println!("{}", row.trim_end());
}
println!();
println!("* beyond the table's levels: the end level's wind, held, and the sample flagged");
println!();
// The table as JSON, tagged by `model`. Directions are in radians.
println!(
"{}",
serde_json::to_string_pretty(&WindModel::Layered(layered))?
);
Ok(())
}
/// The speed of a wind velocity (east, north, up), m/s, and the direction it blows from,
/// clockwise from north, rounded to a whole degree in `[0, 360)`.
fn speed_and_direction_from(velocity_enu_m_s: DVec3) -> (f64, f64) {
let (east, north) = (velocity_enu_m_s.x, velocity_enu_m_s.y);
let from_deg = (-east).atan2(-north).to_degrees().rem_euclid(360.0);
// `% 360.0` turns a 359.6° into 0°, and `+ 0.0` turns a -0 into 0.
(east.hypot(north), from_deg.round() % 360.0 + 0.0)
}
In JSON
WindModel reads and writes JSON as one object. Its model field names the kind of wind, and the
other fields are that model’s inputs:
model | Other fields |
|---|---|
constant | speed_m_s, direction_from_rad |
power_law | reference_speed_m_s, reference_height_agl_m, exponent, direction_from_rad, ground_msl_m |
log_law | reference_speed_m_s, reference_height_agl_m, roughness_length_m, direction_from_rad, ground_msl_m |
layered | levels, each with height_msl_m, speed_m_s and direction_from_rad; and interpolation, speed_direction (the default if left out) or components |
- Directions are in radians, the direction the wind blows from: 270° is 4.71238898038469, as the example’s JSON shows.
- Reading a file checks it as the model’s constructor does: a negative speed, or a field the model doesn’t have, is an error.
Not yet modeled
- Wind that changes with time, or across the field.
- Vertical mean wind.
- Terrain effects.
- A blend below the lowest tabulated level (the table holds the lowest level’s wind).
- Reporting during a flight that the wind was held beyond a table’s levels.
A flight takes one wind model, so it can’t join a power or log law near the ground to a table of levels aloft. The planned weather milestone (M5.2) decides how to.
Tests that pin this
wind::tests::layered_wind_interpolates_speed_and_heading(Loft lesson L6, the forecast profile that stepped at its lowest level):- Halfway between 4 m/s from 350° and 12 m/s from 30°, the wind is 8 m/s from 10°, turning through north.
- There is no step just above the surface level.
components_interpolation_averages_the_vectors,opposite_directions_turn_clockwiseandwind_grows_out_of_calm_without_turning.layered_wind_holds_and_flags_beyond_its_levels.meteorological_direction_convention, plus the power and log laws through their references, below ground, and atz₀.invalid_inputs_are_rejected, andwind_models_round_trip_through_json: each model written to JSON and read back gives the same wind.- Against RocketPy, in the recovery test
descent_matches_rocketpy_examples(Recovery):- At every height its parachute descents sample, hpr’s wind matches RocketPy’s within 1e-9 m/s in its east and north parts.
- Four of the five descents fly in wind: Calisto, NDRT 2020, Prometheus and Juno III. Their total drift agrees within 0.28%, and each east or north part within 2.9%. The largest gap is the north part of NDRT 2020’s drift, +2.865% (the validation report).
- The example above, which CI runs and compares with its committed output.
Turbulence
In short
- What it models: random gusts from the Dryden model: a gust pattern with a published spectrum (how gust strength spreads over wavelength), sized by height and the wind at 20 ft, and repeatable from a seed.
- Sources: MIL-F-8785C, the US military flying-qualities specification (1980), noting where MIL-HDBK-1797 (1997) differs; xoshiro256++ (Blackman and Vigna, 2021) and Marsaglia and Bray’s polar method (1964) for random numbers.
- How well it is validated: analytic and unit tests only: over 2²⁰ samples, each component’s spectrum is within 4 standard errors of theory in every octave band (±1–3% in the wide bands). Not compared with another simulator or a real flight.
- What it leaves out: Dryden is an aircraft model, unvalidated for rockets. Its frozen gust pattern needs airspeed well above the gusts, which fails on the rail and near apogee. No flight uses it, and none is planned (issue #39). Above 2000 ft the caller supplies the intensity.
Code and sources
Code: hpr_atmos::dryden, using the seeded generator in hpr_core::random.
Sources:
- [8785C] MIL-F-8785C, Military Specification: Flying Qualities of Piloted Airplanes
(5 November 1980), §3.7 and the definitions in §6.2.7, pinned as
mil-f-8785c. - [1797] MIL-HDBK-1797 (1997), Appendix A §4.9, for its differences only. It is not pinned: no stable public copy was found.
- [BV] D. Blackman and S. Vigna, “Scrambled linear pseudorandom number generators”, ACM TOMS 47(4) (2021): xoshiro256++ with SplitMix64 seeding.
- [MB] G. Marsaglia and T. A. Bray, “A convenient method for generating normal variables”, SIAM Review 6(3) (1964): the polar method.
Dryden spectra
Turbulence is a frozen random field that the vehicle moves through, so its spectra are functions
of spatial frequency Ω (rad/m, [8785C] §6.2.7). They are one-sided, with
∫₀^∞ Φ dΩ = σ² ([8785C] §3.7.1.2):
Φ_u(Ω) = σ_u² (2L_u/π) / (1 + (L_u Ω)²)
Φ_v(Ω) = σ_v² (L_v/π) (1 + 3(L_v Ω)²) / (1 + (L_v Ω)²)² (Φ_w likewise)
R_u(ξ) = σ_u² e^(−|ξ|/L_u)
R_v(ξ) = σ_v² e^(−|ξ|/L_v) (1 − |ξ|/(2L_v))
Φ = (2/π) ∫₀^∞ R(ξ) cos(Ωξ) dξ, so each spectrum and autocorrelation is a Fourier pair. A test
checks this numerically.
The factor-of-two trap. [1797] writes the transverse spectra with 2L_v and 12(L_vΩ)² over
(1 + 4(L_vΩ)²)², and halves the lengths (L_u = 2L_v). The spectra are identical, but mixing
one document’s lengths with the other’s formula puts the transverse scales off by two. This code
uses [8785C]’s form and lengths throughout.
Parameters
-
Low altitude ([8785C] §3.7.3.4, Figs. 10–11). With
hthe height above terrain in ft andu₂₀the mean wind at 20 ft:L_u = L_v = h / (0.177 + 0.000823 h)^1.2, L_w = h (ft) σ_w = 0.1 u₂₀, σ_u = σ_v = σ_w / (0.177 + 0.000823 h)^0.4- The formulas hold from 10 to 1000 ft. Below 10 ft this code uses the 10 ft values. From 1000
to 2000 ft the figures give
L = 1000 ftand equal intensities. - The model covers only up to about 2000 ft (§3.8.1), so above that the constructor returns an error instead of quietly holding values.
- Fig. 9 marks
u₂₀= 15, 30 and 45 kt for light, moderate and severe turbulence. - For aircraft at low altitude,
ulies along the horizontal relative mean wind andwis vertical ([8785C] p. 60).
- The formulas hold from 10 to 1000 ft. Below 10 ft this code uses the 10 ft values. From 1000
to 2000 ft the figures give
-
Medium/high altitude ([8785C] §3.7.2, above about 2000 ft): isotropic, with
L = 1750 ft.- The intensity against altitude and exceedance probability is only a graph (Fig. 7), so the caller supplies it.
- Neither military document says how to blend 1000–2000 ft. MATLAB’s documentation interpolates linearly, but that is its own choice.
-
Axes of the gust field:
uis the longitudinal component andv,wthe transverse ones.- The longitudinal spectrum belongs to the component along the path through the frozen
field. For a climbing rocket that is nearly vertical. The flight engine (M1.6)
doesn’t use turbulence, and no milestone plans it yet
(issue #39); whoever adds it must align
uwith the path. - Getting that wrong changes the statistics: a horizontal gust given the longitudinal spectrum has twice the transverse power at low frequency and 2/3 of it at high frequency.
- The axes’ signs don’t matter.
- The longitudinal spectrum belongs to the component along the path through the frozen
field. For a climbing rocket that is nearly vertical. The flight engine (M1.6)
doesn’t use turbulence, and no milestone plans it yet
(issue #39); whoever adds it must align
Generator
Exact discretization, so step length never changes the statistics.
u: a first-order Gauss–Markov state. Over a stepΔs, withρ = e^(−Δs/L):x ← ρx + √(1 − ρ²) n.vandw: two normalized states, driven asdx₁/ds = (−x₁ + η)/Landdx₂/ds = (x₁ − x₂)/L, with outputy = (σ/√2)(√3 x₁ + (1 − √3) x₂).-
The stationary covariance is
P = [[1, ½], [½, ½]]for everyL. -
The output’s autocorrelation is exactly
R_v. Algebra:c Φ(r) P cᵀ = 2e^(−r)(1 − r/2)withc = (√3, 1 − √3). -
Over
r = Δs/L, the transition isΦ(r) = e^(−r) [[1, 0], [r, 1]], plus Gaussian noise of covarianceQ = P − ΦPΦᵀ:Q = [[P(1,2r), ½P(2,2r)], [½P(2,2r), ½P(3,2r)]], P(n,x) = γ(n,x)/Γ(n) -
Written with the regularized incomplete gamma function,
Qkeeps full relative precision for tiny steps, where the closed form½ − e^(−2r)(r² + r + ½)cancels to nothing. The noise is drawn throughQ’s Cholesky factor.
-
- State is normalized, so intensities and lengths may change from one step to the next (with altitude, say) without breaking stationarity.
- Determinism: draws come in a fixed order (
u, two forv, two forw) from a seeded xoshiro256++, so the same seed and steps give bit-identical gusts on one platform.- The integer stream is identical everywhere.
- Normals and gusts go through the math library’s
lnandexp, which may differ in the last bit between platforms.
- Checkpointing: the generator serializes, so a run can be checkpointed and resumed.
GustField precomputes a realization at a fixed spacing and interpolates it linearly. That makes
the gust a pure function of the path coordinate, which an adaptive integrator can evaluate
repeatedly and on rejected steps.
- Spacing: linear interpolation smooths wavelengths near the spacing, so keep the spacing at or below a tenth of the smallest scale length.
- Size limit: a field holds at most 10⁷ samples.
Limits for rockets
- Scaled for aircraft. Dryden’s lengths and intensities describe aircraft flying roughly level. A rocket climbs through the low-altitude model’s height dependence in seconds. The frozen-field assumption holds when airspeed is well above the gust velocities. That is false on the rail and near apogee, where the gust field barely moves past the vehicle.
- Path coordinate: whoever adds turbulence to a flight decides what to key the field on: distance flown through the air, or altitude, and how to fade gusts in on the rail. This module does neither.
Tests that pin this
dryden::tests::dryden_spectrum_matches_theory(the done when of M1.2, the atmosphere and wind milestone):- Setup: 2²⁰ samples at 1 m, with
σ = (1.5, 1.2, 0.9)m/s andL = (40, 40, 20)m. - Estimate: 256 Hann-windowed segments of 4096 samples, averaged (Bartlett’s method).
- In every octave band from bin 1 to Nyquist, each component’s mean ratio to theory is within 4 standard errors.
- The variance of a band’s mean ratio is
[1 + 2ρ₁²(n−1)/n + 2ρ₂²(n−2)/n]/(nK)fornbins andKsegments, with the Hann window’s neighbouring-bin correlationsρ₁ = 2/3andρ₂ = 1/6. The bracket tends to 1.94. - The one- and two-bin bands at the bottom are loose (±25%, ±21%); the wide bands (±1–3%) pin the spectrum.
- Theory is the continuous spectrum sampled at 1 m, in closed form. Below a tenth of Nyquist it is checked against [8785C]’s formula to 1%.
- The variance of the record is within 5% of
σ².
- Setup: 2²⁰ samples at 1 m, with
- Exactness:
transition_preserves_the_stationary_covariance:ΦPΦᵀ + Q = Pto 4ε for steps from 10⁻¹² to 10³ scale lengths.two_steps_compose_into_one:Q(a+b) = Φ(b)Q(a)Φ(b)ᵀ + Q(b)to 1e-12 relative, down tor = 1e-9.transverse_output_has_the_dryden_autocorrelation.regularized_gamma_matches_independent_references.
- Stepping loop:
quarter_meter_steps_give_the_one_meter_correlation, to 2e-3.- The sampling error is about 4e-4, so this catches a scale length 10% off.
- It can’t tell an exact step from an Euler step (8e-5 apart), which is why the exactness tests above exist.
- Spectra:
spectra_integrate_to_the_variancespectra_are_the_cosine_transforms_of_the_autocorrelations
- Parameters:
low_altitude_parameters_follow_the_specification, at 100 ft moderate:L_u = 505.169 ftandσ_u/σ_w = 1.715849. It also checks the 10 ft floor, the 1000–2000 ft hold, and the refusal above 2000 ft. - Determinism: the same seed gives bit-identical fields, and a serialized generator resumes the stream.
hpr_core::random::tests:- Bit-identical to
rand_xoshiroover 10⁴ draws for 5 seeds. - The normal sampler’s moments and CDF at ±2σ, within 5 standard errors.
- Bit-identical to
The design tree, configurations and checks
In short
- What it models: how parts become a rocket: where each part sits, automatic radii (taken from the neighbouring parts), overrides (measured values that replace computed masses, centers and inertias), the reference diameter and the motor in its mount. It gives the rocket’s mass, center of mass and inertia (its resistance to turning) through the burn, and flags designs that can’t exist, such as a motor wider than its mount. Most of it is convention, not physics.
- Sources: RocketPy 1.13.0’s
Rocketcode, and Meriam and Kraige’s Engineering Mechanics: Dynamics for the parallel-axis theorem. - How well it is validated: by analytic tests and
code-to-code comparison, the first and third of four
kinds of evidence. A hand-worked rocket agrees to 1e-12 through the burn. For eight
cases of RocketPy’s example rockets,
a given structure with its motor placed agrees in mass, center and inertia within 8.0e-10
(relative) at the times RocketPy computed, and within 1.3e-5 in mass and 2.6e-5 in inertia
between them; the propellant grains’ mass within 2.4e-9 and 4.9e-5 of its initial value.
Placement, automatic radii and overrides are checked by hand; the whole structure against
OpenRocket on 71 compared designs, within 1% in mass on 70 and in center of mass on 70
(mass properties); and body radii against OpenRocket in
the
.orkimport (.orkdesign files). A cluster’s tubes sit where OpenRocket puts them, to 1e-15 m, and a motor out turns the rocket as the hand calculation says, to 3.7e-7 (below). A pod’s mass, center and inertia match the hand-worked parallel-axis sum to 1e-15 (Pods), and OpenRocket’s on eighteen probe designs (.ork: Pods); on whole flights, six probe designs, five with pods of bodies, fins and motors, are within 0.81% of OpenRocket’s apogee, and their launch masses within 0.0001% (aerodynamics: Pods). OpenRocket’s cluster example flies within 5% of OpenRocket’s apogee and largest speed. Three of its apogees are compared with OpenRocket’s flight with no parachute, since its parachute opened before apogee (M1.9c, a two-stage and a cluster design against OpenRocket; results). Not compared with a real flight. - What it leaves out: each motor lights at its own
ignition, at launch unless told otherwise, so a two-stage design whose file says nothing flies with every motor lit at once, and nothing warns; a staged flight gives the sustainer its ignition (Staging). A cluster’s motors light together, or not at all: no spread in ignition and no thrust misalignment. Fins on a nose cone or a transition sit there only as a freeform outline with no tab or fillet, its root along the surface. Pods fly with each pod’s parts’ own normal force and drag, without the flow between the pods and the body (aerodynamics: Pods). OpenRocket has its own conventions for positions, radii and overrides; the OpenRocket comparison (M2.2) is mapping them, and the mass conventions it has found are on the mass page.
Code and sources
Code: hpr_design::tree (the tree, placement, automatic
radii, overrides, reference diameter), hpr_design::config
(motor mounts, configurations, assembly) and hpr_design::checks.
Decisions:
ADR-007 (stations, placement, automatic radii, overrides, motors and checks). Part
geometry and mass are in Mass properties and Shapes.
Sources:
- [RP] RocketPy v1.13.0 (MIT),
rocketpy/rocket/rocket.py: how a rocket’s mass, center of mass and inertia combine with a placed motor.docs/research/rocketpy-rocket-mass.mdhas the formulas with line numbers. - [MK] Meriam and Kraige, Engineering Mechanics: Dynamics, appendix B: the parallel-axis theorem (Mass properties).
- [AT] AeroTech, RMS-18/20, RMS-24/40, RMS-29/40-120 and HP RMS-29/120 Motor Dimensional Drawing (2003–2004), archived by the Internet Archive in 2005: case outside diameters and the ±0.005 in drawing tolerance (a nominal motor in its matching tube). The 29 mm drawing; the others are in the same folder’s archive.
- [ISO 2768] ISO 2768-1:1989, General tolerances, part 1: tolerances for linear and angular dimensions without individual tolerance indications, table 1, class c: the fit tolerance (wider than its parent).
Most of this file defines conventions rather than physical models. OpenRocket has its own
conventions for positions, automatic radii and overrides. hpr never reads OpenRocket’s source
code, whose license (GPL) is incompatible with hpr’s (the clean-room rule). So the planned
OpenRocket import (M3.1, reading .ork files) and comparison (M2.2) will
map them by running OpenRocket itself.
Stations and the body origin
- A station
sis a distance aft of the nose tip, the way design files give positions. - The body frame’s origin is the nose tip, on the axis:
z_ref = 0in Frames. Itszaxis points along the rocket toward the nose, so stationsis bodyz = −s, and the rocket lies atz ≤ 0. - A part’s own frame has its origin at its forward end (Mass properties). A part placed at station
sis translated by(0, 0, −s). Radial offsets and roll angles stay as the part states them, always measured from the body axis.
The tree
A Rocket has stages: sections of the stack, forward
to aft. A separation splits the rocket at a boundary between two
stages, so a rocket that stays in one piece needs only one.
Each Stage lists body components, and each
Component holds a
Part and its children.
| role | parts | where |
|---|---|---|
| body | nose cone, body tube, transition | a stage’s list; they stack |
| external | fin set, tube fin set, launch lug, rail button, pod set | children of a body tube; they take its outer radius |
| a pod’s body | nose cone, body tube, transition | children of a pod set; they stack along the pod (Pods) |
| internal | inner tube, centering ring, mass component, parachute, streamer, shock cord | children of a body component or an inner tube |
- Stacking. Body components start at
s = 0and follow one another through every stage, forward to aft. Each one’s extent is its length without shoulders (the sleeves of a nose or transition that slide into the next tube). - Axial extent of an attached part:
- a fin set’s root chord;
- a row of lugs or buttons from the first one’s forward end to the last one’s aft end,
(n − 1)·spacing + length(for a button, its diameter); - a packed part’s packed length;
- a pod set’s pod, its body components’ lengths added;
- otherwise the part’s length.
- Refused trees (
DesignError::Tree): no stages, an empty stage, a part in the wrong role, an attached part without a position, a body component with one, children under anything but a body component, inner tube or pod set, anything but body components in a pod set, and motor mounts on anything but a body tube or inner tube.DesignError::DuplicateIdcovers an empty or repeated id. - A fin set may sit on a nose cone or a transition when its root follows the surface
(ADR-166).
The root is a freeform outline’s
root_mpoints and its ends, each within a micron of the surface (FinSet::ROOT_ON_SURFACE_M): withr(x)the body’s radiusxalong it andx_LEthe root’s leading edge, a point[x, h]is on it whenh = r(x_LE + x) − r(x_LE). The straight pieces between the points may cut off or add at most 0.1% of the fin’s area (FinSet::ROOT_SLIVER_SHARE). The set must stay on the body’s length and carry no tab or fillet. Its body radius isr(x_LE). Anything else there isDesignError::Geometry, and on a body tube a fin’s root must be level. Lugs, buttons and tube fins still attach to a body tube only. - Counts. A fin set has 1 to 64 fins and a tube fin set 1 to 64 tubes, as a pod set has 1 to
64 pods, and a row of launch lugs or rail buttons 1 to 64 copies; a count outside that is
refused (
DesignError::Domain, “fin count (1 to 64)”, or “instance count (1 to 64)” for lugs and buttons). Each fin, tube, pod, lug or button is weighed one by one, so the bound stops a file’s mistyped count from asking for billions of them. It is far above any real rocket: the most fins in one set among the.orkdesigns hpr’s checks read is 8 (a one-off count over 78 designs, not a committed survey; the.orkreader leaves out a part counted more than 64 times). The aerodynamics take 1 to 8 fins in a set (aerodynamics: fin count), so 9 to 64 fins are weighed but don’t fly.
Positions
An attached part of extent L in a parent spanning stations [p, p + P], with offset a
(positive aft):
from | forward end at |
|---|---|
top | p + a |
middle | p + (P − L)/2 + a |
bottom | p + P − L + a |
after | the previous sibling’s aft end + a, or p + a for the first child |
absolute | station_m |
Automatic dimensions
An auto list names dimensions that the tree resolves. The part’s stored value for them is ignored.
- Body radii follow neighbours through every stage, stage boundaries included:
- a nose cone’s base, and a transition’s aft radius, take the next component’s forward radius;
- a body tube, and a transition’s forward radius, take the previous component’s aft radius.
- Order. Sources are followed until nothing changes. A body tube still unresolved then takes
the next component’s forward radius instead: only the first such tube, and then the sweep
repeats. So a fixed radius forward of a tube wins over one aft of it. A radius with no fixed
radius to reach is refused.
Rocket::unresolvable_body_radiilists exactly those radii. The.orkimporter gives them OpenRocket’s own default of 25 mm before laying the design out (when an automatic radius has nothing to take). The tree itself never invents a radius. - Shoulders take the inner radius (
R − t, outer radius less wall thickness) of the adjoining body tube: behind a nose; ahead of a transition for its forward shoulder, behind it for its aft one. - Centering rings: the outer radius is the parent’s inner radius. The inner radius is the outer radius of the widest on-axis inner tube among its siblings that overlaps it along the axis (by a positive length). With none, it is zero: a bulkhead.
- Packed parts (mass components and recovery parts) take the parent tube’s inner radius, less the distance from the parent’s axis to the part’s axis. An offset outside the bore is refused.
- Tube fin sets: the outer radius
ris the one at which the tubes close the ring around the body tube of radiusRthey sit on, each touching the body and its two neighbours. ForN ≥ 3tubes,r = R sin(π/N) / (1 − sin(π/N)); one or two tubes take the body’s radius. Six tubes on a 50 mm body are 50 mm; four are 120.7 mm. A wall thicker thanris cut to it. This is the radius OpenRocket 24.12 gives, measured on probes (.orkdesign files).
Overrides
Overrides replace computed mass properties with
measured ones: the mass m, the center of mass c and the inertia tensor I (the 3×3 table of
moments and products of inertia; see Mass properties). Primes mark the new values.
They apply in this order:
- Mass
m′:I′ = I m′/m, same center. The body keeps its shape. A body withm = 0becomes a point massm′atc. A packed part (a mass component, parachute, streamer or shock cord) withm = 0instead becomes a solid cylinder ofm′filling its packing, as OpenRocket’s does (Packed parts). - Center
a(cg_aft_m):c′_z = −(s_fore + a), withs_forethe station of the component’s own forward end (a stage’s for a stage, and never a shoulder’s), whether or not the children are covered.cg_xy_msetsc′_xandc′_y; without it they are kept. The tensor about the center is unchanged. - Inertia: the tensor about the center is replaced.
InertiaOverridegives its six entries with the sign convention of Mass properties (I_xy = −∫ x y dm); the off-diagonal ones default to zero.
The result must pass MassProperties::validate, which also refuses inertia on a body with no mass.
Errors inside a stage or component name it (DesignError::InComponent).
- Scope. A component’s overrides cover the component alone. With
overrides_include_children, they cover the component and everything attached to it. A stage’s overrides cover the whole stage, measured from its forward end. Motors are never covered. - Drag. A component or stage can also state a drag coefficient (
drag_override, with its owninclude_children). It plays no part in mass or inertia; the aerodynamics read it (A part’s stated drag coefficient). - Precedence. Deeper overrides apply first. A child’s override is inside its parent’s subtree total, and the stage override applies last.
- Scaling the tensor with the mass keeps the radii of gyration (
√(I/m): how far out, on average, the mass sits). That is the natural reading of “this part weighs more than its geometry says”. - OpenRocket differs in two ways, which hpr keeps as measured departures: it scales only the overriding part’s own inertia, and when a mass override covers the parts inside and gives no center, it puts the center at the overriding part’s own. Which override wins, and where a center override is measured from, the two agree on (Loft lesson L51, measured on probe designs in M2.2b1; see Mass properties).
Reference diameter
maximum(the default): twice the largest outer radius of any body component in any stage, including a bulged ogive’s peak (Profile::max_radius_m). Internal parts, shoulders, fins, tube fins, lugs and rail buttons never count. In Loft, the project before hpr-sim, an internal part could set it (Loft lesson L47).nose_base: the first nose cone’s base diameter.custom: a given diameter.
The reference area is π d²/4.
Motors and configurations
- Mounts. A
MotorMounton a body tube or inner tube holds a motor. Itsoverhang_mis how far the nozzle exit sits aft of the mount’s aft end. A mount that is a cluster of tubes holds the motor in every tube (below). - Configurations. A
Configurationputs at most oneMountedMotorin each mount. A mounted motor is aSolidMotorwith its case diameter and length (for the checks), an optional ejection delay (the time from burnout to its ejection charge, which doesn’t delay ignition) and itsignition: at launch unless told otherwise (Staging). - Placement. The motor’s axis runs forward from the nozzle exit (Solid motors).
With
s_aftthe station of the mount’s aft end, the nozzle exit is at stations_aft + overhang, on the mount’s axis: for an inner tube offsetrfrom the body axis at angleθ(fromx_Btowardy_B), at(r cos θ, r sin θ); otherwise on the body axis. A motor element atz_malong the motor’s own axis is at bodyz = −(s_aft + overhang) + z_m. - Composition at time
t:Assembly::mass_properties(t)combines the structure with each motor’sSolidMotor::state(t).total([MK]), every motor lit att = 0.Assembly::mass_properties_littakes each motor’s own ignition time: a motor burns on its own clock,t − t_ignition, and one not yet lit is loaded. The dry assembly uses each motor’s dry element. - More than one motor. In a flight, each burning motor’s thrust points along the rocket’s axis
(
z_B) and acts at its own nozzle exit, and the thrusts and their moments are summed (Rigid-body flight):- A cluster is flown, whether its motors share one mount (below) or each has its own, and a motor off the body axis adds a turning moment.
- A two-stage design fires in sequence when its motors are given their ignitions, and drops its booster at a separation (Staging). Its design file alone lights every motor at launch, booster and sustainer together, unless it says otherwise.
- Against RocketPy [RP]:
total_mass(t)andcenter_of_mass(t)are the same combination. RocketPy names the moment of inertia in pitch and yawI_11and the one in rollI_33. ItsI_11(t)is taken about the center of dry mass (the rocket without propellant), so hpr’s tensor is moved there before comparing.I_33sums the axial moments (every element is on the axis).
Clusters
A cluster is several like motors side by side. In hpr it is one inner tube repeated: the tube’s
cluster_m lists each
tube’s axis, [x, y] in meters in body axes, measured from the point the tube’s
radial_offset_m and angle_rad set (the body’s axis when both are 0). An empty list is one tube.
Motors of different kinds need one mount per kind. The decision record is ADR-075.
In a JSON design, a mount of three tubes 25 mm from the axis, with the first tube’s motor out, is these two fields (the rest of the inner tube and the mounted motor as usual):
"cluster_m": [[0.025, 0.0], [-0.0125, 0.021650635], [-0.0125, -0.021650635]]
"failed_tubes": [0]
- Mass. The tube weighs all its copies, each with its own
parallel-axis term
m d². Whatever the tube holds (an engine block, a mass) is repeated in every tube the same way. A mass override on the cluster sets the whole cluster’s mass; one on a part inside it (its mass, center or inertia) sets each copy’s, as OpenRocket does for the mass. - Motors. The configuration names one motor for the mount, and placing it gives one motor per tube, one after another in the order of the tubes, each nozzle on its tube’s axis. Their thrusts, masses and moments add up like any other motors’.
- A motor out. A mounted motor’s
failed_tubesnames tubes whose motor never lights, counted from 0 in the order ofcluster_m. That motor stays loaded and pushes nothing, which is how a cluster most often fails. The lit motors then push off-center, and the rocket turns toward the motor that is out. - Motor numbers. Every tube’s motor counts as a motor, so a cluster of three before another mount moves that mount’s motor from index 1 to 3. An ignition on the cluster’s burnout takes its first motor that lights. A recovery trigger or separation on one motor’s burnout or delay (recovery) waits on that motor alone: point it at a tube that lights, or with that motor out your parachute never opens.
- From a
.orkfile. The reader turns OpenRocket’s named pattern into the list (.orkdesign files).
Worked example. The tests’ single-stage rocket
(synthetic-54mm-three-fin),
with its mount made a ring of three tubes A = 0.02 m from the axis, at 0°, 120° and 240°
(measured from the body’s x axis toward its y axis, as in frames), and a Cesaroni
411I175-14A in each. It is an equation check, not a buildable rocket: three 38 mm motors don’t fit
a 54 mm body, and the design checks say so. It is held at rest in a vacuum, so no air and no motion
add anything to the motors’ push. With the motor at 0° out, 1 s into the burn:
| quantity | value |
|---|---|
each lit motor’s thrust T, in a vacuum | 193.98 N |
| the loaded motor, and each lit one at 1 s | 0.4375 kg, 0.3332 kg |
the rocket’s mass m | 1.584 kg |
center of mass across the axis, c_x (the loaded motor pulls it toward itself) | 1.33 mm |
pitch moment M = T (A + 2 c_x) about the center of mass | 4.396 N m |
pitch inertia I_yy about the center of mass | 0.1052 kg m² |
pitch acceleration M / I_yy, at rest | 41.80 rad/s² |
The center of mass moves toward the loaded motor: (0.4375 × 20 − 2 × 0.3332 × 10) mm / 1.584
is 1.32 mm, and the structure’s own center, 0.10 mm off the axis from its rail buttons, adds the
rest. The two lit motors sit at x = −A/2 each, so about the center of mass their thrust has the
lever A/2 + c_x twice. The flight’s equations give the same angular acceleration to 3.7e-7 (the
full inertia tensor, not only I_yy, turns the moment into a turn); the difference is the
mass-flow terms (the center of mass moving as two motors burn and one doesn’t, and the jets). With
all three lit, the thrusts balance, and the rocket turns 225 times slower (0.186 rad/s²), from its
center of mass sitting 0.033 mm off the axis. The numbers are pinned by the test
cluster_motor_out_produces_pitch_moment in hpr-sim.
Pods
A pod is a body beside the airframe: a side pod, or an outboard motor pod. In hpr a
PodSet is attached to a body tube like a fin set,
with a position along it. Its children are the pod’s own body components (a nose cone, body tubes,
a transition), which stack aft from that position along the pod’s axis, and take their automatic
radii from one another as a stage’s do. Parts go on and inside them as on the airframe: fins on a
pod’s tube, a mass or a motor mount inside it. A design with pods flies: each pod’s parts add their
own normal force and drag, once per pod (aerodynamics: Pods,
M1.13c1). The decision records are ADR-089 for
the layout and weight, and ADR-092 for the aerodynamics.
-
Where the pods sit.
countpods, 1 to 64, spaced evenly around the body’s axis atradial_offset_mfrom it, the first atangle_radfrom the body’sxaxis toward itsyaxis (frames): podkis atr (cos φ_k, sin φ_k),φ_k = angle + 2π k / count. -
Each pod is the first one turned. The pod written in the tree is one pod on the body’s axis. Pod
kis that pod turned byφ_kabout the body’s axis, then moved out, as a fin set’s fins are. So whatever it holds keeps its place relative to the airframe: a fin that points away from the airframe on one pod points away on every pod, and a symmetric pod set keeps its center of mass on the axis. -
Mass. Each copy is weighed where it sits, with its own parallel-axis term, and everything the pod holds is repeated with it. The pod set itself weighs nothing.
-
A pod of no length. A pod’s body tube may have length 0, and then it weighs nothing. OpenRocket draws winglets this way: a pod of one such tube, with no radius, which it calls a “phantom body” (
.ork: Pods). Parts on the tube sit at its radius, as on any tube. That radius is usually 0, so the parts sit on the pod’s own axis: fin roots there, and a lug’s axis its own radius out from it.- A worked example: two pods 50 mm from the body’s axis, each holding a launch lug 4 mm in
radius turned 180°, inward. Each lug’s axis is 46 mm from the body’s. The test
a_pod_of_no_length_holds_its_parts_at_the_pod_s_axisworks the pair’s inertia out by hand. - Three or more fins whose roots meet on the axis overlap there. Each fin is weighed as a whole plate, so the overlap counts more than once, as it does in OpenRocket. The overlap is about a fin’s thickness across, so it grows with thickness over span. For three fins 3 mm thick and 20 mm tall it is a few per cent of their mass: an estimate, not a measurement.
- A body tube of no length is allowed only in a pod; a stage refuses one. A pod refuses a nose cone or a transition of no length. A pod mixing a tube of no length with other parts lays out, but no probe has checked it against OpenRocket.
- A worked example: two pods 50 mm from the body’s axis, each holding a launch lug 4 mm in
radius turned 180°, inward. Each lug’s axis is 46 mm from the body’s. The test
-
An empty pod set holds nothing, lays out, and weighs nothing.
-
Overrides. A mass override on the pod set is the total for all its pods, and must be set with
overrides_include_children(an override on the pod set alone is refused, since it has nothing of its own). An override on a part inside a pod is that part’s in each pod. A center overridecg_xy_minside a pod is measured in the pod as written, on the body’s axis, and turns with each pod; so does an inertia override’s tensor. The testwhat_a_pod_holds_turns_with_it_and_overrides_keep_their_scopepins all three. -
Motors. A motor mount inside a pod gives one motor per pod, each nozzle on its pod’s axis. The configuration names the mount by its id, the pod tube’s for a pod that is its own motor tube, and a mounted motor’s
failed_tubescounts the pods, in order from the first. -
Checks. A pod may run past the end of the tube it hangs from, or past the rocket’s end, without a warning: a pod is held by a pylon, and an outboard booster often extends past the tube it hangs from. A pod set that doesn’t touch its tube at all is still an error. A pod set in a pod and one of more than 64 pods are refused.
-
Flying. A pod’s parts add their normal force and drag once per pod, on the rocket’s axis at their stations (aerodynamics: Pods). Canted fins on a pod and a pod’s tube of no length with a radius are refused. The tumble model counts each pod’s tubes and fins as the airframe’s (recovery). An empty pod set adds no force and no drag area. Motor mounts may sit in up to four pod sets, and each set’s burning motors come off its own pods’ bases (aerodynamics: Pods); a rocket with motor mounts in more is refused. Until M4.5i, motor mounts in a second pod set were refused.
-
What it can’t do yet.
- Move or release a part inside several pods: a moving or released part must be one part, so it can be inside a pod only when there is one pod.
- Part at a pod: a pod’s body component is neither a joint nor an ejected payload; it stays with the tube its pod set hangs from.
- Check a pod’s geometry: nothing warns when pods overlap the airframe or each other, or when a pod’s radius steps (#206).
The refusals are pinned by tests in
hpr-aero(unsupported_inputs_are_refused) andhpr-sim(a_pod_s_parts_are_located_as_the_airframe_s_are,partings_the_design_cant_make_are_refused).
In a JSON design, this component goes in a body tube’s children list. It holds two pods 50 mm
from the axis (angle_rad is optional and 0 by default), each a 0.3 m tube, 0.1 m aft of the top
of the body tube:
{
"id": "pods",
"part": { "pod_set": { "count": 2, "radial_offset_m": 0.05, "angle_rad": 0.0 } },
"position": { "from": "top", "aft_offset_m": 0.1 },
"children": [
{
"id": "pod-tube",
"part": {
"body_tube": {
"length_m": 0.3,
"outer_radius_m": 0.012,
"thickness_m": 0.001,
"material": { "name": "cardboard", "density": { "kind": "bulk", "kg_m3": 790.0 } }
}
}
}
]
}
Worked example. The same two pods, at 0° and 180°, each a cardboard tube (790 kg/m³, radius 12 mm, wall 1 mm, 0.3 m long) with a 50 g mass added inside it (a solid cylinder 0.1 m long, radius 8 mm, its top 0.02 m below the pod’s). They hang on the tests’ 54 mm airframe (radius 27 mm), so each stands 11 mm clear of it, from a body tube whose top is 0.2 m aft of the nose tip. The pod starts 0.3 m aft of the nose tip (at station 0.3 m). Each pod, about its own axis and center:
| quantity | value |
|---|---|
the tube’s mass ρ π (r_o² − r_i²) L | 17.125 g |
one pod’s mass m (tube and mass) | 67.125 g |
| its center, from the tube’s at station 0.45 m and the mass’s at 0.37 m | station 0.39041 m |
its roll inertia, the two cylinders’ m (r_o² + r_i²)/2 and m r²/2 added | 3.869e-6 kg m² |
its pitch inertia, each cylinder’s m (3 (r_o² + r_i²) + L²)/12 and its own m Δz² | 2.53675e-4 kg m² |
Each pod’s axis is d = 0.05 m off the body’s. A pod’s parallel-axis term about a line through
the body’s axis is m times the square of the pod’s distance from that line. Both pods lie on the
x axis, so they add nothing about it, and m d² = 1.6781e-4 kg m² each about the y and z
axes:
| quantity (the two pods about their joint center, on the axis) | value |
|---|---|
| mass | 134.25 g |
I_xx: no m d² | 2 × 2.53675e-4 ≈ 5.0735e-4 kg m² |
I_yy | 2 × (2.53675e-4 + 1.6781e-4) ≈ 8.4298e-4 kg m² |
I_zz (roll) | 2 × (3.869e-6 + 1.6781e-4) ≈ 3.4336e-4 kg m² |
The pods’ roll inertia is 44 times what it would be with both on the axis: nearly all of it is the
parallel-axis term. The test a_pod_is_its_stack_repeated_with_its_parallel_axis_term in
hpr-design pins these to 1e-15. It also pins a single pod at 90° (y = d). About the nose tip,
where its center is at z = −0.39041 m, the pod’s
product of inertia is I_yz = −m y z = 0.067125 × 0.05 ×
0.39041 = 1.3103e-3 kg m².
Parallel stages
A parallel stage is a stage strapped beside the airframe instead of stacked behind it: a set of
boosters around a sustainer, burning beside it and dropping at a separation of their own. In hpr
it is a Stage whose parallel field is set (a
ParallelStage): the id of the body tube it
hangs on, in an axial stage before it, where along that tube, and its copies as a
pod set’s count, distance from the axis and angle. Its own components are each copy’s
body, a stack of nose cones, body tubes and transitions with parts on and inside them, as a pod’s
are. The decision record is ADR-171, and M4.5m
shipped it.
- Laid out as pods. The layout hangs the stage on its tube as a pod set, so everything in Pods holds: where the copies sit, each copy turned with its place, each weighed with its own parallel-axis term. A parallel stage doesn’t stack: the rocket’s length is the axial stages’.
- Its mass is its own stage’s. Its parts carry its stage’s index, its mass and ends count to
that stage and not to the tube’s, and
PlacedStage::hung_onnames the stage it hangs on. The rocket’s mass, center and inertia are the same as with the same pods hung on the tube. - Dropping it. A separation after stage k drops every stage after k, so the stages it
drops must hang together: each one’s carrier, its
hung_onstage or the axial stage before it, is among them, or it is the first dropped. A parallel stage hung on a stage the nose keeps can’t go with an axial booster behind it; drop the booster first, then the parallel stage (Staging: Boosters beside the core). - Refused. A parallel stage placed
aftera sibling (it sits along its tube), one that hangs on no body tube of an earlier axial stage, and, as for any pod set, one inside a pod. So is a mass override that covers what its tube holds, or the whole stage the tube is in, and a drag override on a parallel stage or covering the stage it hangs on: none says whether it covers the parallel stage, and no probe has measured how OpenRocket reads one. An override on the parallel stage itself is its total, every copy’s, as a pod set’s is; that reading is hpr’s, not measured against OpenRocket.
Worked example. The two pods of Pods’ worked example, made a parallel stage instead
(134.25 g, at 0° and 180°, 50 mm from the axis): the stage weighs 134.25 g, the airframe tube’s
mass with its children is 134.25 g less than with the pods hung on it, and the rocket’s mass,
center and inertia are the pod set’s to 1e-15. The test
a_parallel_stage_lays_out_as_a_pod_set_of_its_own_stage pins this for one, two and three
copies, and a_parallel_stage_hangs_on_a_body_tube_of_an_earlier_axial_stage pins the refusals.
In a JSON design, the stage goes in the rocket’s stages list after the stage it hangs on, and
is flown as any stage is: a motor in a mount inside it, and a separation after the stage it hangs
on (Staging: Using it today has a whole two-stage design to start
from). This one is the worked example’s, empty but for each copy’s tube, hung 0.1 m aft of the top of the
body tube airframe:
{
"id": "pods",
"components": [
{
"id": "pod-tube",
"part": {
"body_tube": {
"length_m": 0.3,
"outer_radius_m": 0.012,
"thickness_m": 0.001,
"material": { "name": "cardboard", "density": { "kind": "bulk", "kg_m3": 790.0 } }
}
}
}
],
"parallel": {
"on": "airframe",
"position": { "from": "top", "aft_offset_m": 0.1 },
"pods": { "count": 2, "radial_offset_m": 0.05, "angle_rad": 0.0 }
}
}
Checks
checks::check resolves a design and returns typed
Findings, each an error (impossible as described,
so a simulation would be wrong) or a warning (unusual, but it can be built and flown). Lengths compare with 1 nm of
slack (LENGTH_TOLERANCE_M), so round-off never raises one.
| finding | severity | when |
|---|---|---|
motor_wider_than_mount | error | case diameter > mount inner diameter, past the slack a nominal size has (Loft lesson L50) |
motor_outside_mount | error | the case doesn’t overlap its mount along the axis at all (an overhang typed in mm as m) |
attachment_off_body | error | an external part’s extent (a fin root) doesn’t overlap its body tube at all (Loft lesson L50) |
part_outside_rocket | error | an internal part lies wholly forward of the nose tip or aft of the rocket’s end, and touches none of the parts it hangs from |
internal_part_wider_than_parent | error | an internal part reaches farther from its parent’s axis than the parent’s bore (in a nose cone or transition, the most room along the part) by more than the fit tolerance for the bore’s size, and is no cap at its parent’s end, nor a packed part whose center is in the bore. A ring around its tube is measured in the part around it |
center_outside_rocket | error | a stage with an axial center-of-mass override (cg_aft_m, its own or a component’s) has its center off the rocket although its parts aren’t |
motor_tight_in_mount | warning | the motor’s nominal diameter is wider than the mount’s bore, but its real case fits: a 29 mm motor in a 1.140 in tube |
motor_past_mount_top | warning | the case’s forward end is forward of the mount’s |
attachment_past_body_end | warning | an external part runs past an end of its body tube |
internal_part_past_parent_end | warning | an internal part runs past an end of its parent |
internal_part_tight_in_parent | warning | an internal part other than a packed part reaches past its parent’s bore by no more than the fit tolerance: a fit to sand |
internal_part_wedged_in_parent | warning | an internal part other than a packed part fits a nose cone or transition where it is widest along the part, but runs into its wall where it narrows, past the fit tolerance |
packed_part_wider_than_parent | warning | a packed part (a mass component, parachute, streamer or shock cord) reaches past its parent’s bore, by any amount, with its center inside the bore |
ring_against_parent_end | warning | a centering ring or bulkhead too wide for its parent’s bore sits at the parent’s end face and fits what holds the parent: a cap glued against the end |
cluster_tubes_overlap | warning | two tubes of a cluster are closer than a tube’s diameter, so they cross and that mass counts twice |
ring_overlaps_inner_tube | warning | a centering ring crosses an inner tube beside it (a cluster’s off-axis tubes), counting that mass twice |
radius_step | warning | adjacent body components’ radii differ where they meet |
no_nose_cone | warning | the first body component isn’t a nose cone |
- Radial reach is measured about the parent’s own axis: the distance between the part’s axis and the parent’s, plus the part’s radius. A part inside a clustered tube is measured in that tube, and copied to each of the cluster’s places. A block centered in an off-axis pod fits; a part on the body axis inside that pod doesn’t. Centering rings have no offset, so they sit on the body axis.
- Parts wholly outside the rocket report only
part_outside_rocket, and a stage holding one is sparedcenter_outside_rocket. A single check covers each fault. - A retainer on a motor mount that sticks out past the airframe touches its mount, so it is on the
rocket, even flush against the mount’s end. Only the mount gets a warning. Touching an end face
counts as touching, within
LENGTH_TOLERANCE_M, for every part. - Fins may sweep past the rocket’s end, and a heavy part may run past its tube’s, so a stage’s
center can leave the rocket without an override, and neither a mass override nor a sideways
center override (
cg_xy_m) can move a center past its parts. It is an error only when an axial center override (cg_aft_m) is involved; no component-level center is checked.
Wider than its parent
A part drawn a little wider than the bore it sits in is a fit the builder sands. hpr flies it and
warns, internal_part_tight_in_parent, and the mass where the part crosses its parent’s wall
counts twice. How much wider is the fit tolerance
(fit_tolerance_m): the general tolerance
ISO 2768-1 sets for a dimension of the bore’s diameter in its coarse class, ±0.5 mm for 6 to
30 mm, ±0.8 mm for 30 to 120 mm and ±1.2 mm for 120 to 400 mm. A bore made at its upper limit
and a part made at its lower one close a radial overlap of that much. The standard is for
machined parts; no standard covers hobby airframes, so hpr borrows its coarse class
(ADR-155, the decision on which fits warn). Past the tolerance, the part can’t be where
it is drawn: an error, internal_part_wider_than_parent, unless it is a
packed part. Fix it by typing the part’s diameter to
fit the bore (an outside diameter typed for an inside one is the usual slip), or by choosing a
part that fits.
- In OpenRocket’s 3D printable nose cone and fins, a coupler 24.10 mm across sits 11.4 mm deep in a printed fin can with a 23.19 mm bore: its wall reaches 0.46 mm past the bore, within the 0.5 mm of a 23 mm bore, so it warns. The ring of PETG where they cross, 0.46 mm thick and 11.4 mm long, is 0.39 cm³, 0.48 g at the file’s 1250 kg/m³, counted twice.
- A cap is a centering ring or bulkhead on its parent’s axis that covers one of its parent’s
end faces, inside or outside the end, but not both. It can be glued against the end, so it needs room in what holds its parent instead:
that holder’s bore, with the same tolerance. It warns,
ring_against_parent_end. OpenRocket’s Two stage high power rocket draws its bulkheads this way: sized to the airframe’s 49.53 mm bore, on the ends of couplers whose bore is 48.44 mm. The same bulkhead away from the coupler’s ends would be an error, and so would a tube, which can’t be a cap, or a ring at the end of a clustered or off-axis tube, which sits beside the tube, not around it. - A ring around its tube is a centering ring written as a child of an inner tube, on the tube’s axis, whose bore is at least the tube’s outside diameter. It wraps the tube rather than sitting in its bore, as OpenRocket’s two pods examples draw the rings on their motor mounts. So it needs room in the part around the tube at the ring’s own station: out from the tube, the first part with room for parts whose length overlaps the ring’s. That part must hold the whole ring; the same tolerance applies, the findings name that part, and the ring is checked against the other tubes in it for overlap. Pods–airframes and winglets draws rings 32.54 mm across with an 18.75 mm bore on an 18.69 mm motor mount, inside an airframe with a 32.59 mm bore: they fit. Read as inside the mount, whose bore is 18.03 mm, they would be an error.
- A ring that isn’t around its tube is measured as a part in the tube’s bore, which for a ring this wide is an error: one whose bore is even 1 µm smaller than the tube’s outside, one on a tube that is off the axis or a cluster, and one with no part around it at its station that holds it whole, such as a ring on a motor tube where it sticks out past the airframe’s end (M4.5k, a ring around its tube).
In a nose cone or transition
A nose cone or transition narrows along its length, so the room a part has depends on where it sits. This is a design convention, checked by unit tests and by counting findings over real designs, not by comparison with a built rocket or with OpenRocket. hpr measures the room from the profile, the part’s outline (its radius against distance from the forward end): the outer radius less the wall (none when the part is filled), over the part’s own length. It takes two numbers from it, the least room along the part and the most (#313, a part measured against the cone’s widest radius). Before that fix, hpr used the largest outer radius anywhere on the part, so a bulkhead 20 mm in radius at the tip of a cone 27 mm in radius at its base passed, though the cone has no room for it there. Drawn there, a part sits forward of where it can, which moves the center of gravity forward and raises the stability margin.
- Too wide where it is widest: past the fit tolerance of the most
room along it, a part is
internal_part_wider_than_parent, an error, as in a tube. - Wedged: a part that fits where the profile is widest along it, but runs into the wall where
it narrows past the tolerance of the least room, is
internal_part_wedged_in_parent, a warning. It can’t slide that far in as drawn, and hpr flies it where it is drawn. The usual case is a coupler (a short inner tube that joins two sections) drawn reaching into a nose cone from its base. - Packed parts are measured against the least room, with the packed-part rule: a warning while the part’s center is in that room, an error once it is out. A mass on the axis at a cone’s tip warns, since the room closes to nothing there and the mass’s center is on the axis; off the axis it is an error.
- How the wall is measured. The wall is the thickness the design states, normal to the
surface, so the outer radius less the wall overstates the room on a slope
θbyt (1/cos θ − 1): 1% of the wall at 8°, 0.1 mm of a 3 mm wall at 15°. An automatic radius in a profile is already its room at the part’s narrower end (ADR-096, automatic radii in a profile), so it fits. - Why the ends decide. Every profile hpr draws is concave: its radius never dips between two stations, so the least room along a part is at one of its ends. The check samples 31 stations between the ends as well.
- Past the ends: a part running past the profile’s end is measured over the length inside it.
One wholly past an end, or touching it only, is measured against the largest outer radius, as
before;
internal_part_past_parent_endalready names it.
A worked example. A conical nose 200 mm long and 27 mm in radius at its base, with a 2 mm
wall, has 0.135 x − 0.002 m of room at x m from its tip. A tube 24.5 mm in radius over its
aft 100 mm has 25.0 mm of room at the base and 11.5 mm at its forward end, 13 mm too little:
wedged, a warning: far past the 0.5 mm tolerance of the 23 mm bore at its forward end. At
26.5 mm the same tube is 1.5 mm too wide even at the base, past the 0.8 mm tolerance of a 50 mm
bore (25.0 mm of room is 50 mm across): an error.
On real designs. cargo xtask design-checks, a developer command in this repository, counts
the findings over the reference library (ADR-185, the release 0.1 fixes, has the table
of counts). On its default set, the repository’s gitignored folder of reference designs and
OpenRocket’s examples (72 designs that open),
this rule adds no error: 3 more tight-fit warnings, 1 wedged and 8 more packed-part warnings. On
the private flight collection (560 files that open, many of them versions of one design), it adds
16 errors in 11 files newly refused, all tubes too wide even at the profile’s widest, and 14
tight-fit, 33 wedged and 240 packed-part warnings.
A packed part wider than its bore
A packed part drawn wider than its bore warns and flies as drawn. Its width changes only its own inertia; on the one OpenRocket example that has one, that moves the apogee by 12 µm at most.
A mass component, parachute, streamer or shock cord is a packed part
(Packed parts): hpr takes its mass m as stated, and its packed length
L and station say where that mass sits. Its packed radius r
enters the flight in one place, the part’s own moment of inertia, that of a solid cylinder:
m r²/2 about its axis and m (3r² + L²)/12 across it, about its center.
- The warning. A packed part that reaches past its parent’s bore is
packed_part_wider_than_parent, by any amount, while its center is inside the bore: its radial offset from the parent’s axis is no more than the bore’s radius. It can’t go in as drawn, and hpr flies it as drawn, as OpenRocket does. Packed into the room it has, radiusr_room, at the same mass, length and station, it would change the rocket’s moments of inertia bym (r² − r_room²)/2about its axis andm (r² − r_room²)/4across it, and nothing else. - No limit. The warning has no upper limit. A part typed far too wide, such as ballast 400 mm across in a 54 mm nose cone, adds inertia that packing it would not, and that changes how the rocket turns. Read the warning, and fix the part’s diameter if it is a slip.
- The error. A packed part whose center is past the bore is an error,
internal_part_wider_than_parent, however little it reaches past: its mass would sit where no part can be, moving the rocket’s center of gravity sideways. - No fit tolerance. A packed part’s mass doesn’t count twice where it crosses a wall, so it gets no fit tolerance (ADR-170, the decision on packed parts).
A worked example. OpenRocket’s Deployable payload packs its payload, 14.2 g, and its
parachute 25 mm across into a 21 mm bore: each reaches 12.5 mm from the axis, with 10.5 mm of
room. Packing the payload into its room would take 0.0142 × (0.0125² − 0.0105²) / 4 =
1.6e-7 kg m² off the rocket’s inertia across its axis; the parachute adds a term of its own.
Flown both ways, with both parts narrowed in a copy of the file, the five configurations’ apogees
differ by 12 µm at most, 7.5e-8 of the apogee, and their landing times agree to 1 ms
(ADR-170’s table).
A nominal motor in its matching tube
A motor’s diameter in hpr is its nominal size, as ThrustCurve.org and RASP files give it: 29 mm
for every 29 mm motor. The cases are not all that wide. AeroTech’s RMS dimensional drawings give
the case’s outside as 0.698 in for 18 mm, 0.938 in for 24 mm and 1.125 in for 29 mm, each to
±0.005 in, so a “29 mm” case is at most 1.130 in, 28.702 mm, across. A size whose case is
narrower than its name gets that difference as slack
(motor_fit_slack_m): 0.144 mm at 18 mm,
0.048 mm at 24 mm, 0.298 mm at 29 mm. Within it, the check warns (motor_tight_in_mount); past
it, the real case can’t go in and it is an error.
- LOC Precision’s 1.140 in motor tube has a 28.956 mm bore, 0.044 mm under 29 mm. The largest case leaves it 0.254 mm of clearance, so a 29 mm motor in it warns (OpenRocket’s Chute release; issue #280, the report that such a motor was refused).
- The Loft demo’s 28.0 mm bore is 1 mm under 29 mm, 0.70 mm under the largest case: an error.
- The same drawings give 38, 54, 75 and 98 mm cases as 1.500, 2.125, 2.965 and 3.870 in, so at the +0.005 in limit each is at least as wide as its name. Those sizes get no slack, so a 38 mm motor in a 37.9 mm bore is an error, as its 38.10 mm case would be. Fix it with a wider mount or a smaller motor. hpr doesn’t yet warn when a nominal size fits the bore but its wider case wouldn’t, such as 38 mm in a 38.1 mm bore (#312).
- These are one maker’s cases, drawn in 2003 and 2004 and archived from its site in 2005 ([AT], under Code and sources). No published standard gives a diameter tolerance: the NAR’s motor testing manual lists the sizes only. A Cesaroni or other maker’s case may differ, and a diameter that isn’t a nominal size gets no slack.
Errors mark designs that can’t exist as described. A simulation of one would be wrong, usually
on the flattering side: Loft flew a 54 mm motor in a 38 mm mount 69% high. The flight engine
refuses them with SimError::DesignChecks
unless the caller sets
FlightSettings::accept_design_errors.
Verification
- Placement and resolution (
tree::tests): each position rule at stations worked by hand; body components stacking through two stages; automatic radii across a stage boundary, the fallback, precedence and the unresolvable case; ring, shoulder and packed radii; every refused tree; a JSON round trip that refuses unknown fields. - Composition by hand:
tree_structure_matches_parts_placed_by_hand: the sample rocket’s structure equals its eight parts placed at hand-worked stations and combined, to 1e-13.config::tests: the placed motor’s nozzle station, and the rocket’s mass, center and inertia at loaded, burning and burnt out, by the parallel-axis theorem, to 1e-12; an off-axis mount’sI_yz = −m y z.
- Clusters by hand:
parts::tests::a_cluster_is_its_tubes_each_with_its_parallel_axis_term: four tubes’ mass, center and roll inertia about the body’s axis, to 1e-15.tree::tests::a_cluster_repeats_what_it_holds_in_every_tube: an engine block in every tube of a 3-ring, the structure gaining two tubes and two blocks, and the checks’ warnings.config::tests: a motor in a 3-ring is three motors at their tubes, the rocket’s roll inertia gaining each one’sm d²; a failed tube stays loaded and unlit, a tube the mount lacks is refused; a sustainer lit by a clustered booster’s burnout lights with one booster motor out.hpr_sim::staging::tests: three motors’ thrust and mass summed, and the motor out above (cluster_motor_out_produces_pitch_moment, Loft lesson L31); a clustered sustainer lit after a powered separation.
- Pods by hand (
tree::tests): two pods’ mass, center and inertia, and one pod off the axis with its product of inertia, against the textbook cylinders’ sum above, to 1e-15 (a_pod_is_its_stack_repeated_with_its_parallel_axis_term); a mass off a pod’s axis turning with its pod, a center override in the pod as written, and the two override scopes (what_a_pod_holds_turns_with_it_and_overrides_keep_their_scope); a pod’s nose taking its tube’s radius, its parts repeated in each of three pods, a motor per pod, no warning for pods past the rocket’s end, this page’s JSON, and each refused tree (pods_stack_hold_motors_and_refuse_the_wrong_trees); a pod’s nose never the reference nose (a_pod_s_nose_is_not_the_reference_nose). The aerodynamics, the tumble model, a mass shift and an ejection each refuse a pod, or a pod’s body component, by name (above). - Overrides (
overrides_rescale_move_and_replace,nested_overrides_apply_deepest_first): each step, the scopes, a stage override, deeper overrides first, the massless case, and refusal of non-finite and unphysical results. - Property (a proptest, which checks a rule on many random inputs): randomly placed masses sum to the structure’s mass and center, and sliding every part moves the center rigidly without changing the tensor.
- Checks (
checks::tests): each finding and its severity. A cluster pod’s block fits and an on-axis part in the pod doesn’t. Motors miss their mounts in both directions. Parts and a stage center lie off the rocket. A layout with a corrupt parent index is skipped, not a panic. A bulkhead 0.8 mm wider than a 49 mm bore warns and 0.1 µm more errs; at the coupler’s end it is a cap up to the airframe’s bore plus its tolerance, and an error inside the coupler or as a tube (a_part_wider_than_its_parent_warns_only_as_a_fit_or_a_cap); each band edge of the coarse tolerance is pinned (the_fit_tolerance_is_the_coarse_general_tolerance_of_the_bore). A 29 mm motor warns in a 28.956 mm bore and errs 1 µm past its slack, as do 24, 38 and 54 mm motors and a size that isn’t nominal (a_nominal_motor_in_its_matching_tube_only_warns). Mutation probes make these tests fail: the slack given to every diameter, the tolerance doubled, a cap allowed anywhere along its parent, off its parent’s axis, or over both ends. - Against RocketPy 1.13.0 (
config::tests::matches_rocketpy_example_rockets):-
Eight cases of the fixture
validation/fixtures/design/rocketpy-rocket-mass.json: seven example rockets (Calisto at two motor positions) and Prometheus’sGenericMotor(RocketPy’s motor described by its masses alone, with no grain geometry). Cavour (added for its drag curve in M1.5b, the drag milestone) has no motor dry mass; its design gives the motor 1e-15 kg, since hpr needs a positive one.docs/research/rocketpy-rocket-mass.mdgives the curve substitution and the examples left out. -
The test derives the stage override, nozzle station and motor inputs from the fixture itself, independently of the design generator.
-
The rockets are compared at 103 even times through the burn and after it, and at up to 60 of RocketPy’s LSODA knots: the times at which its ODE solver, LSODA, computed the grain geometry. Errors are relative: to the value itself, or to what a row names in brackets (the rocket’s length for a center).
-
Worst measured, with the test’s tolerance:
quantity worst tolerance dry mass, center, I_11,I_33; initial and column propellant mass2.4e-16 1e-12 products of inertia (of I_11)0 1e-15 at LSODA knots: total mass, center (of length), I_11,I_338.0e-10 1e-8 at LSODA knots: grain propellant mass (of initial) 2.4e-9 1e-8 even grid: total mass 1.3e-5 (Lince) 5e-5 even grid: center of mass (of the rocket’s length) 3.6e-6 (Cavour) 2e-5 even grid: I_11about the dry center and about the center of mass2.6e-5 1e-4 even grid: I_331.4e-5 1e-4 even grid: grain propellant mass (of initial) 4.9e-5 2.5e-4 -
At RocketPy’s knots, agreement is the ODE solver’s own accuracy (rtol 1e-11).
-
Between knots, the residual is RocketPy’s resampling: it interpolates grain volumes linearly between LSODA knots and samples
GenericMotorinertias at thrust knots. hpr’s values are exact for a piecewise-linear curve. -
The comparison sets mass, center and inertia together. So the override steps (rescaling the tensor with mass, moving the center) are checked by hand-worked tests, not against RocketPy.
-
- Public designs (
validation/designs/, written bycargo xtask designs, which a test keeps in sync): the eight RocketPy cases and two synthetic rockets resolve with no findings and assemble into valid bodies at ignition, mid-burn and burnout. - Lessons: Loft lesson L47
tests::reference_diameter_ignores_internal_components; Loft lesson L50checks::tests::motor_wider_than_mount_is_rejectedandchecks::tests::fin_root_must_touch_body.
Nose cones, transitions and solids of revolution
In short
- What it models: the outer shape of nose cones and transitions (conical, ogive, elliptical, power, parabolic and Haack series), and the volume, center of mass, inertia and surface areas of each, solid or as a shell of given wall thickness.
- Sources: G. A. Crowell Sr., The Descriptive Geometry of Nose Cones (1996), and appendix A of the published OpenRocket technical documentation v13.05 (2013).
- How well it is validated: by analytic tests only, the first of four kinds of evidence. Filled shapes match closed forms to 1e-10 (relative), and independent high-precision integrals to 1e-12 on 22 noses and transitions; 20 walls match to 1e-10. Not compared with OpenRocket, weighed parts or a real flight.
- What it leaves out: OpenRocket’s documentation doesn’t say how it measures wall thickness. Measuring it radially instead of square to the surface changes wall volume by 1.4% on a cone three calibres (base diameters) long. Where a steep end is cut square to the axis, rather than following the wall’s inner corner as hpr does, the part gains up to 2.24% of wall mass in this page’s examples. The OpenRocket comparison (M2.2) is to check both.
Code and sources
Code: hpr_design::shapes (profiles) and hpr_design::solids (volume, centroid, moments, areas).
Sources:
- [CR] G. A. Crowell Sr., The Descriptive Geometry of Nose Cones (1996), pp. 1–6 and 12–14. Cited, not pinned: the only copy found is on a plain-http university mirror, and the reference library (ADR-002) pins over https. [TD] gives the same curves.
- [TD] S. Niskanen, OpenRocket technical documentation v13.05 (2013), appendix A, pp. 102–106,
pinned as
openrocket-techdoc-13.05. This is the published document, not the program’s source.
Profiles
A profile gives the outer radius r(x) at distance x aft of its forward end, 0 ≤ x ≤ L. Each
shape is a normalized curve g(ξ) with g(0) = 0 at the tip and g(1) = 1 at the base:
| shape | g(ξ) or y(x) | parameter | source |
|---|---|---|---|
| conical | ξ | none | [CR] p. 1; [TD] A.1 |
| ogive | y = √(ρ² − (x − ρ cos α)²) + ρ sin α, α = atan(R/L) − acos(√(L² + R²)/2ρ) | ρ/ρ_t ≥ R/L | [CR] p. 4 |
| elliptical | √(1 − (1 − ξ)²) | none | [TD] A.6 |
| power series | ξⁿ | 0 < n ≤ 1 | [CR] p. 2; [TD] A.8 |
| parabolic series | (2ξ − K′ξ²)/(2 − K′) | 0 ≤ K′ ≤ 1 | [CR] p. 5; [TD] A.7 |
| Haack series | √((θ − sin 2θ/2 + C sin³θ)/π), θ = acos(1 − 2ξ) | 0 ≤ C ≤ 2/3 | [CR] p. 6; [TD] A.9–A.10 |
- Ogive. The shape is Crowell’s secant ogive: an arc of radius
ρthrough the tip and the base rim, withρgiven as a multiple of the tangent radiusρ_t = (R² + L²)/2R.1is the tangent ogive, whose slope is zero at the base.- Values above 1 meet the base at an angle.
- Values below 1 bulge past
Rbefore the base. - The arc reaches the tip only while its center is not above the axis, which needs
ρ ≥ (L² + R²)/2L. - The center is computed on the chord’s perpendicular bisector and the height as
y = x(2x_c − x)/(√(ρ² − (x − x_c)²) − y_c), which avoids the cancellation near the tip of a slender ogive.
- OpenRocket’s ogive parameter is not adopted. [TD] contradicts itself about it:
- A.3 defines it as
κ = ρ_t/ρ. - A.4–A.5 describe the first
Lof a tangent ogive of lengthL/κ, which atL = 4,R = 1,κ = ½hasρ = 24.8, notρ_t/κ = 17.0. - The planned
.orkimporter (M3.1) must settle the mapping by running the OpenRocket jar.
- A.3 defines it as
- Haack. Monotone for
C ≤ 2/3, becaused(g²)/dθ = sin²θ (2 + 3C cos θ)/π.C = 0is the von Kármán (LD-Haack) ogive andC = 1/3the LV-Haack. [TD] limitsCto1/3in the program. - Two shapes without drag data. A Haack nose or widening transition above
C = 1/3, and a bulged secant ogive (arc radius below the tangent ogive’s), have no drag data faster than sound, so hpr’s drag buildup refuses them since M1.8b1. Their shapes, mass properties and center of pressure are unaffected, and a drag table stands in for the drag (Aerodynamics). - Blunt tips. The elliptical, power-series (
n < 1) and Haack slopes are infinite at the tip.n = 0(a flat cylinder) is rejected; model it as a tube and a bulkhead. - Errors in [CR]. Its prose says “greater than twice the length” for the bulged secant ogive,
against its own formula. Its ogive and ellipsoid areas are wrong, and the author marked them so.
Its elliptical formula measures
xfrom the base, and its ellipse CP ratio should read2L/3. hpr uses none of those.
Transitions
A transition runs from fore radius R_f to aft radius R_a over L.
- Orientation. The shape’s tip lies at the smaller end. Growing aft,
r = R_f + (R_a − R_f) g(x/L). A boattail is the mirror image,r = R_a + (R_f − R_a) g(1 − x/L). [TD] doesn’t say how a shrinking transition is oriented; this choice keeps both ends’ radii exact and the profile monotone (Loft lesson L49). - Clipped ([TD] §A.7): cut a whole nose cone of base radius
max(R_f, R_a)where its radius ismin(R_f, R_a), with the nose length chosen so the piece isLlong.- Shapes other than the ogive invert
gby bisection. - The ogive’s curve depends on its fineness, so the nose length is found by bisection too.
- Conical and tangent-ogive transitions are the same clipped or not; the test checks this to 1e-12.
- A clipped ogive with
ρ/ρ_t < 1is rejected: its profile isn’t monotone, so the cut is ambiguous.
- Shapes other than the ogive invert
Solids of revolution
Per unit density, with inner radius r_i (zero when filled):
V = π ∫ (y² − r_i²) dx x̄ = π ∫ x (y² − r_i²) dx / V
J_a = (π/2) ∫ (y⁴ − r_i⁴) dx J_t = π ∫ [(y⁴ − r_i⁴)/4 + x² (y² − r_i²)] dx − V x̄²
S = 2π ∫ y √(1 + y′²) dx A_p = 2 ∫ y dx, x_p = ∫ x y dx / ∫ y dx
Each slice is an annulus: (π/2)(y⁴ − r_i⁴) dx about the axis and (π/4)(y⁴ − r_i⁴) dx about its
diameter, moved to the reference plane by the parallel-axis theorem. S excludes the end faces.
- Numerics. Each half of the profile is integrated from its own end in
u = s², whereuis the normalized distance from that end. The substitution removes theu^(−1/2)singularity of a blunt tip’s surface integrand, and measuring from the end keeps the tip exact. The integrals usehpr_core::quadrature(Quadrature) at a relative tolerance of 1e-12. - Walls (ADR-006, component geometry and mass properties).
- A wall of thickness
tis the part of the solid withintof the outer surface, sotis measured normal to the surface, which is how molded and laid-up shells are made. - Its inner radius is the lower envelope of circles of radius
ton the profile:r_i(x) = max(0, min_{s ∈ [0, L], |s−x| ≤ t} [y(s) − √(t² − (x − s)²)]). The surface is the profile over its own length, ends included, with no extension past a cut end. - The envelope is exact for any continuous profile. A point above the lower half of some
surface point’s circle has the profile crossing its height closer than
t, so it is in the wall anyway. - Cut ends. Where the surface meets the end plane at an obtuse angle inside the wall (the
small end of a transition, the base of a bulged ogive), the rim’s circle rounds the wall’s
inner corner.
- A square cut would add a sliver of
t² (tan φ − φ)/2of section per unit rim length, withφthe surface’s angle to the axis: 3.1e-4t²at 7°. On steep ends it matters: a square-cut part is heavier than this model by 1.26% of wall mass for a 27→49 mm transition over 15 mm (56°), and by 2.24% for 20→37.3 mm over 10 mm (60°), both witht = 2 mm. The OpenRocket comparison (M2.2) should check how real parts and OpenRocket treat such ends. - The sliver grows without bound only as the end turns vertical. There, a square cut (made by
extending the surface along its tangent) closes the end with a disc of thickness
t. - The first version did extend along finite end tangents only. Its wall mass jumped by 8.3% between shapes whose end slopes rounded to finite and to infinite.
- A square cut would add a sliver of
- The minimum comes from a 32-point scan, a golden-section search, and the two end points, which the search only approaches from inside.
- The hollow is integrated separately. It is split where
r_ireaches zero and where the nearest surface point moves between the lateral surface and a rim, since both are kinks. - [TD] doesn’t say how OpenRocket measures thickness, and [CR] measures it radially. The two
differ by a factor
√(1 + y′²)in wall volume, 1.4% for a cone three calibres long. The OpenRocket comparison (M2.2) will measure OpenRocket’s choice.
- A wall of thickness
Verification
- Closed forms (
shapes::tests::nose_volumes_match_closed_forms, 1e-10 relative):- Every shape’s filled volume and centroid.
- Cone and ogive:
V,x̄,SandA_pfrom arc integrals. - Half spheroid:
x̄ = 5L/8, andSfor prolate and oblate cases. - Power series:
V = πR²L/(2n+1),x̄ = L(2n+1)/(2n+2), and the paraboloid’sS. - Power series and parabolic series:
A_pandx_ptoo; parabolic series by polynomial integrals. - Haack:
V = πR²L(½ + 3C/16)andx̄ = L(11 + 3C)/(2(8 + 3C)), integrated inθ. - Loft’s tangent-ogive value,
R = 0.04 m,L = 0.25 mgives6.7509e-4 m³(Loft lesson L91).
- mpmath references (
solids::tests::filled_solids_match_the_mpmath_references):- 22 noses and transitions of every family, in both directions, clipped and not.
- All seven quantities, to 1e-12 relative (worst measured 2.4e-14).
- Reference:
validation/fixtures/design/shape-integrals.json, fromvalidation/oracles/design/shapes.py(40-digit tanh-sinh on the defining formulas, Haack inθ, with no code shared with Rust). - These references alone check the wetted areas of the power series (
n ≠ ½) and parabolic series, both Haack areas, and the moments of inertia of the filled shapes.
- Walls against mpmath (
solids::tests::walls_match_the_mpmath_references):- 20 walls: 11 noses of every family, and 9 transitions both ways, including unclipped blunt ends and clipped ones.
- Volume, centroid and both moments to 1e-10 relative (worst measured 5.9e-12).
- Reference:
validation/fixtures/design/wall-integrals.json, fromvalidation/oracles/design/walls.py, which shares no code with Rust.- It finds the envelope from the roots of its derivative, bracketed from the window edges and solved by bisection.
- It splits the integrals at the kinks it finds, and requires every hollow integral’s error estimate below 1e-18.
- Reviews found three faults this test now pins:
- Blunt transition ends failed to converge, or were 5e-6 low, until the end points became candidates.
- Tangent extensions made wall mass jump with the end slope.
- The oracle itself first missed minima next to the window edge and left kinks unsplit.
- Walls by hand:
- A conical wall is the cone minus the same cone moved aft by
t/sin β: volume, centroid and both moments by hand, to 1e-9. A cone so thick that its hollow is 3.8 mm long matches the same formula to 1e-12 (a_nearly_filled_cone_matches_the_offset_cone). - A conical transition’s wall is the square-cut frustum shell less the fore rim’s sliver, in
polar coordinates about the rim: mass and centroid to 1e-10, sliver section to 1e-10
(
mass::tests::hollow_transition_and_freeform_fin_cg_are_exact_centroids). - A tangent-ogive wall is bounded by the concentric arc of radius
ρ − t: volume by hand, to 1e-10. - A tube matches the hollow-cylinder formulas.
- A wall thicker than the body fills it, and a thin wall’s volume tends to
S t.
- A conical wall is the cone minus the same cone moved aft by
- Profiles: each ends at
0andR, slopes match central differences, and parameters out of range are errors (Loft lesson L48).- Haack tips use
θ = 2 asin √ξand a Taylor series forθ − sin 2θ/2belowθ = 0.1, so the tip slope is+∞, never NaN. - Ogive radius ratios up to 1e12 give the cone. A power series at the minimum exponent, 0.05,
matches its closed-form volume as a nose and as transitions both ways
(
extreme_parameters_stay_accurate_or_fail_loudly). - Unknown fields in a shape or wall are rejected.
- Transitions hit both radii and are monotone both ways, clipped or not (Loft lesson L49); bulged ogives are excluded because their profile is deliberately not monotone.
- Power-series exponents below 0.05 are rejected. Blunter profiles approach a flat face the integrals can’t resolve, and unclipped transitions below about 0.038 fail to converge.
- Haack tips use
Mass properties of components
In short
- What it models: the mass, center of mass and inertia of each part (tubes, rings, shoulders, fins and their fillets, rail buttons, lugs, mass components, recovery gear), every tube of a cluster, how they add up, and 49 built-in material densities.
- Sources: Meriam and Kraige’s Engineering Mechanics: Dynamics, the OpenRocket technical documentation v13.05, Abbott and von Doenhoff’s Theory of Wing Sections, Golub and Van Loan’s Matrix Computations, and data sheets, specifications and handbooks for densities.
- How well it is validated: by analytic tests, the first of four kinds of evidence: a cone, a tube, four fins and an off-axis payload agree with hand calculation to 1e-11, and fin cross-sections with exact numerical integration to 1e-13. Density unit conversions reproduce their sources, such as the Wood Handbook‘s white ash at 678 kg/m³. Against OpenRocket 24.12, on the structure (the rocket without motors) of 71 compared designs: the mass is within 1% on 70 and the center of mass within 1% of the rocket’s length on 70. The one file outside either has a named cause: airfoil fins, which OpenRocket weighs by a factor. Fin fillets agree with OpenRocket’s to 1e-15 in mass and center of mass on nine probe designs, with the probes’ pitch inertia up to 0.638% apart (below). The roll inertia is a median 1.619% apart, and that is explained: OpenRocket takes a shortcut for fins that hpr does not, and hpr’s figure is the exact one for the fin as drawn (below); on the four files with a cluster (two designs by content), OpenRocket also stacks the tubes on the cluster’s axis (below). A ring of tube fins departs twice: OpenRocket’s roll inertia for it is more than any mass inside the ring could have, and its pitch inertia leaves out how far the tubes sit from the axis. hpr keeps its own for both (below). The pitch inertia is within 1% on 56. Of the 15 outside, the two copies of OpenRocket’s tube fin example are the tube fin departure, and the rest have no named cause yet. Not compared with weighed parts or a real flight (checked against OpenRocket).
- What it leaves out: the sliver between a flat fin root and the round tube, and the step ring
at a nose shoulder. Fillets are weighed, but the aerodynamics leaves them out
(Drag limits). Parachutes weigh as flat circular canopies. Where a
.orkfile leaves something unsaid (a wall of no thickness, no material), hpr reads it as OpenRocket does, and two rules for overrides stay hpr’s own, each measured (below). Fin sections are hpr’s own too: an airfoil fin weighs 0.6851 of a square slab of its outline, where OpenRocket’s weighs 0.85, so hpr’s airfoil fins are 19.4% lighter, with no warning (below). Packed recovery gear and mass components are OpenRocket’s too, down to the size one takes when its file writes none (below). A cluster’s inertia differs from OpenRocket’s on purpose, since OpenRocket stacks the tubes on the axis and hpr weighs each where it sits (below); designs with parts hpr does not read, such as a parallel stage on a rocket of several stages, are retained as reduced designs (the format guide); none of the 71 compared is one. A parallel stage hpr reads is weighed as its own stage (Parallel stages).
Code and sources
Code: hpr_design::mass (MassProperties), hpr_design::parts, hpr_design::fins,
hpr_design::material, hpr_design::materials. The nose and transition solids are in
Shapes.
Sources:
- [MK] J. L. Meriam and L. G. Kraige, Engineering Mechanics: Dynamics, appendix B (moments of inertia of standard solids, the parallel-axis theorem and the inertia tensor).
- [GVL] G. H. Golub and C. F. Van Loan, Matrix Computations, 4th ed. (2013), §8.5 (the Jacobi eigenvalue method).
- [TD] S. Niskanen, OpenRocket technical documentation v13.05 (2013), §3.2.2, pp. 25–29 (fin
planforms), §3.4.4, pp. 49–50 (cross-sections), §4.2.3, p. 66 and Table 5.1, p. 75 (component
masses), pinned as
openrocket-techdoc-13.05. - [AvD] I. H. Abbott and A. E. von Doenhoff, Theory of Wing Sections, Dover (1959), eq. 6.2 (NACA four-digit thickness distribution).
Frames and conventions
These conventions were set in ADR-006, the decision on component geometry and mass properties.
- Body axes follow Frames:
zalong the axis toward the nose,xthe zero radial direction,y = z × x. Roll angles run fromxtowardy. - A component’s frame has body axes and its origin on the axis at the component’s forward end,
or at a nose cone’s tip, so the component lies at
z ≤ 0. The design tree places it by translating it to its station (Design tree). MassPropertiesholds the mass, the center of mass in body axes, and the full inertia tensor about the center of mass. The tensor is taken with the positive products-of-inertia convention:I = ∫ (|r|² E − r rᵀ) dm, soI_xy = −∫ x y dm.- Operations ([MK]):
- Parallel axis:
I_p = I_cg + m (|d|² E − d dᵀ), withd = cg − p. - Rotation:
cg′ = R cg,I′ = R I Rᵀ. - Combination: sum the masses, mass-weight the centers, and sum each tensor moved to the common center.
- Zero total mass gives the plain average of the centers, so placeholders stay finite.
- Parallel axis:
- Validity.
validaterequires a finite, non-negative mass, and a tensor that is symmetric (to 1e-9 of its largest entry) with non-negative principal moments obeyingI₁ + I₂ ≥ I₃. That condition is the same asJ = tr(I)/2 E − I = ∫ r rᵀ dmbeing positive semidefinite. Principal moments come from cyclic Jacobi ([GVL] algorithm 8.5.1), accurate for repeated eigenvalues where the closed-form trigonometric method loses√ε.
Standard solids ([MK])
- Hollow cylinder, radii
R ≥ r, lengthL:I_axis = m(R² + r²)/2andI_across = m((R² + r²)/4 + L²/12). This covers body tubes, inner tubes and couplers, centering rings, bulkheads (r = 0), launch lugs, tube fins, and shoulders.r = Ris allowed, and is a tube of no wall: massπ(R² − r²)L ρis exactly zero, and so is the tensor. That is a real thing for a design to say: an imported.orksays it of twelve parts (.orkdesign files), and refusing it would force a reader to invent a wall instead. A centering ring is the exception: a bore that reaches the rim leaves no ring at all, soCenteringRingrefusesr ≥ Rrather than weighing nothing in silence.Wall::Shellalso still refuses a zero thickness, because a solid of revolution says “filled” withWall::Filledand a zero there is a mistake, not a statement.- Loft used
mL²/12with no radial term, and no roll inertia at all (Loft lesson L44).
- Solid cylinder, radius
a, heighth:I_axis = m a²/2andI_across = m(3a² + h²)/12. This covers mass components, packed parachutes, streamers and shock cords ([TD] Table 5.1 treats recovery parts as cylinders too), and each disc of a rail button. - Rail button:
- Three coaxial discs stacked outward on a radial line: base (outer diameter), waist (inner diameter), flange (outer diameter).
- The waist height is the total height less the base and flange.
- The screw’s head, when there is one (
screw_height_m, 0 for none): half a solid ellipsoid of revolution on top of the flange, as wide as the button (a, half the outer diameter) and the screw’s heighthtall. Its volume is2/3 π a² h, its center of mass3h/8above the flange, its inertia2/5 m a²about its own axis andm(a²/5 + 19h²/320)across it through its center. The last is half the ellipsoid’sm(a² + h²)/5about its base’s diameter, moved by the parallel-axis theorem; witha = hthese are a solid hemisphere’s textbook3R/8and2/5 mR². It carries no drag, and OpenRocket’s comparison is below. - Buttons and lugs may repeat along the axis (
count,spacing_m).
- Shoulder: a hollow cylinder beyond the profile’s end. A capped shoulder adds a disc of its inner radius and wall thickness, flush with its far end. The step ring between a nose’s base radius and its shoulder is not modeled.
- Parachute:
m = ρ_s π D²/4 + n ℓ ρ_l, the nominal area of a flat circular canopy plus its shroud lines. A conical or hemispherical canopy has more cloth thanπD²/4; give its mass through an override (Design tree) or a matching nominal diameter. - Streamer:
ρ_s × length × width. Shock cord:ρ_l × length.
Fins
- Planforms ([TD] §3.2.2):
- Trapezoidal: root chord
c_r, tip chordc_tparallel to the body, spans, and sweepx_tfrom the root leading edge to the tip leading edge. - Elliptical:
c(h) = c_r √(1 − (h/s)²), centered on the root chord (implied by [TD] eq. 3.71). - Freeform: a simple polygon from the root leading edge
[0, 0]to the root trailing edge[c_r, h_r], closed along the root through its root points. On a body tube the root is level,h_r = 0with no root points. On a nose cone or a transition it follows the surface (ADR-166): the heights are measured from the radiusR_bat the root leading edge, and each root point lies within a micron of the surface. Crossing edges, points below the root leading edge, an outline that turns below its root, and outlines that don’t run from the origin aft are errors. On the cockpit of OpenRocket’s Pods–airframes and winglets, drawn through OpenRocket’s own root points, the area, mass and center of mass are OpenRocket’s to the six digits printed (fins::tests::a_root_along_a_nose_cone_is_openrockets).
- Trapezoidal: root chord
- Cross-sections. Each chord from
atobhas a thickness distributiont(x). [TD] uses the cross-section for drag only; hpr also counts the volume it removes.- Square:
t(x) = t. - Rounded: semicircular edges of radius
a_r = min(t, c)/2, so a chord shorter thantnear a pointed tip is a disc of diameterc. Its moments are closed forms ina_r:D₀ = a_r²(2 − π/2),D₁ = a_r D₀ − a_r³/3,D₂ = a_r² D₀ − π a_r⁴/8for the removed edge material, andE₀ = 8a_r⁴ − 3π a_r⁴/2for∫t³. A wide rounded chord loses(1 − π/4) t²of section area. - Airfoil:
t(x) = 10 t P(ξ)with the NACA four-digit polynomialP = 0.2969√ξ − 0.1260ξ − 0.3516ξ² + 0.2843ξ³ − 0.1015ξ⁴([AvD]).- Its maximum is
1.0003 tatξ = 0.2998. - Its moments
10∫ξᵏP = 0.685083, 0.288033, 0.158919hold term by term, and1000∫P³ = 0.4728895comes from mpmath. - An airfoiled fin weighs 68.5% of the square slab, with its centroid at 42% chord.
- A hand-sanded “airfoil” is between the two; [TD] doesn’t define the section.
- Its maximum is
- Square:
- Integrals. Per fin, over the span
hwithr = R_b + h, and chordwise momentsM_k = ∫ x^k t dxandT = ∫ t³/12 dx, taken per unit density:V = ∫M₀,∫r = ∫rM₀,∫r² = ∫r²M₀,∫x = ∫M₁,∫x² = ∫M₂,∫rx = ∫rM₁and∫τ² = ∫T. The span integration is split at every vertex height and runs adaptively. - Tabs are square slabs below the root,
−h_tab ≤ h ≤ 0, with closed-form integrals. A tab must lie along the root chord and reach no deeper than the body radius. Loft never read them (Loft lesson L46). - Root. The flat root is placed at radius
R_b; the sliver between it and the curved tube,t²/8R_bdeep, is ignored. Fillets are solids of their own (Clusters and fillets). - Cant
δturns each fin and its tab about the fin’s outward span axis through the root mid-chord, right-handed, so a positive cant turns fin 0’s leading edge toward−y_B(positive_cant_turns_the_leading_edge_toward_negative_y). [TD] doesn’t state the pivot. Mass and trace are unchanged; the products of inertia in the fin’s own frame grow assin 2δ. - Sets roll the fin to
φ_k = φ₀ + 2πk/Nand combine. Three or more fins are isotropic across the axis; one or two are not, and the full tensor keeps the difference.
Materials
Density is bulk (kg/m³), surface (kg/m²) or line (kg/m). A part asking for the wrong kind
is an error. A design stores the values, not a library key. The built-in values and their sources
are in hpr_design::materials and summarized below.
hpr_design::materials::BUILTIN holds 49 materials. Each carries its source (with table or page),
the URL it was read from, and a basis:
- published: the source states the value.
- derived: computed from the source’s numbers.
- maximum: a specification’s upper weight limit.
- vendor: a seller’s figure, used where no specification exists.
| group | values | sources | basis |
|---|---|---|---|
| hobby tubes | cardboard 790, kraft phenolic 950, Blue Tube 1250, Quantum 1090 kg/m³ | LOC Precision and Public Missiles weight tables (mass over wall volume); Always Ready Rocketry’s own material file | derived; published |
| composites | G10/FR-4 1800, filament-wound E-glass 1990, carbon/epoxy 1580 kg/m³ | Norplex-Micarta NP130, Comptec, Hexcel HexPly 8552 data sheets | published |
| metals | Al 6061 2700, Al 7075 2800, steel 7850, Ti-6Al-4V 4430, brass 8500 kg/m³ | Kaiser Aluminum, MIL-HDBK-5J, TIMET, Copper Development Association | published |
| woods | balsa 180; basswood, yellow birch, Sitka spruce, eastern white pine, sugar maple, northern red oak from G₁₂; birch plywood 680 kg/m³ | Wood Handbook FPL-GTR-190 (pinned as fpl-gtr-190-wood-handbook), p. 2-21 and Table 5-3a; Riga Wood Plywood Handbook | published; derived |
| plastics | PLA 1240, ABS 1040, PETG 1270, nylon 6/6 1140, PC 1200, PMMA 1190, acetal 1420, PS 1040, PVC 1400, HDPE 955, epoxy 1180, Depron 40, paper 755 kg/m³ | manufacturers’ data sheets (NatureWorks, SABIC, Eastman, Celanese, Covestro, Röhm, AmSty, Charlotte Pipe, Chevron Phillips, West System, Depron, HP) | published (paper derived) |
| fabrics | ripstop 1.1 and 1.6 oz/yd², Mylar and LDPE film at 1 mil, paper 80 g/m², Nomex cloth, silnylon | MIL-C-7020H, DuPont Teijin, Dow, HP, MIL-C-83429B; a seller for silnylon | maximum, derived, published, vendor |
| cords | nylon cord types I and III, tubular nylon ½“, 9/16“, 1“, ⅛“ and ¼“ Kevlar, ¼“ bungee, Tex 80 Kevlar thread | MIL-C-5040H, MIL-W-5625K, MIL-C-5651D, A-A-55220; Giant Leap Rocketry’s measurements for Kevlar | maximum, vendor, published |
- Wood at 12% moisture:
ρ = 1000 G₁₂ (1.12)(Wood Handbook eq. 4-12). The handbook’s own example, white ash atG₁₂ = 0.605, gives 678 kg/m³. - Specification maxima overstate typical cloth and webbing: Giant Leap’s measured 9/16“ tubular nylon is 12% under MIL-W-5625K’s limit.
- openrocket-database (Apache-2.0) was used only as a cross-check. Two problems turned up in it:
- Its ripstop weights use 31 g/m² per oz/yd² (the factor is 33.906), so they are 8.6% low.
- Its “Plywood, aircraft” at 337–361 kg/m³ is lite-ply, not birch.
Checked against OpenRocket
In short. hpr adds a design’s parts into its structure: every stage together, with no
motor. OpenRocket 24.12 computes the same thing. On 2026-09-21 the two were compared on every file
OpenRocket opens among hpr’s .ork test files: the reference library (designs gathered under
refs/, many of them private files other people shared) and the 17 example designs that ship
inside OpenRocket’s program file (its Java jar). The current default survey compares 71 designs.
Some hold the same design found in two places (several private files are copies of OpenRocket’s
examples), so there are 51 different files by content. Mass and center of mass agree closely on most,
and every file outside 1% has a named cause. The roll inertia is a median 1.619%
apart: OpenRocket’s shortcut for fins (below), and on the
cluster designs its stacking of their tubes on the axis (below).
This was M2.2a; ADR-060 records how it was decided.
The numbers below are from the current scratch-excluding rerun after
M1.9b, which weighs every tube of a cluster
(below); M2.2b4 settled two more of
its causes before that (next section).
What you can check yourself. The private files are not public, so only counts come from them,
and a fresh clone cannot reproduce the 71-file table. It can check the probe tube and Loft’s public
demo designs (cargo test -p xtask ork_mass), and it can run the script on .ork files of its own
to see OpenRocket’s numbers (Run it yourself, at the end of this section); comparing hpr’s with
them is not automated yet.
How. validation/oracles/openrocket/mass.py runs OpenRocket and asks it for each design’s
structure, after saving the design once so that every automatic dimension is the one OpenRocket
settles on. cargo xtask ork compares hpr’s with it:
- the mass, relative to OpenRocket’s;
- the center of mass’s station, as a share of the rocket’s length;
- the roll inertia (about the rocket’s axis) and the pitch inertia (about an axis across it), each about the program’s own center of mass and relative to OpenRocket’s. Pitch is taken as the mean of the two inertias across the axis, which does not depend on how either program turns its axes about the rocket’s length.
Which of OpenRocket’s numbers is roll was measured, not assumed. The script first reads a probe: one
tube, 1 m long, 50 mm in outer radius with a 2 mm wall, of a material at 1,000 kg/m³. Worked by
hand, it weighs 0.61575 kg, with a roll inertia of m (r_o² + r_i²)/2 = 0.0014790 kg·m² and a
pitch inertia about its middle of m ((r_o² + r_i²)/4 + L²/12) = 0.052052 kg·m². OpenRocket’s
numbers match to 15 digits, and so does hpr’s layout of the same file (the test
the_probe_tube_is_the_one_worked_by_hand).
The thresholds, 1% of the mass and 1% of the length, were set before any design was measured. A design outside either needs a written reason, not a pass.
The results, as cargo xtask ork printed them on 2026-10-05, after
M4.5m read parallel stages.
| within 0.1% | within 1% | median | |
|---|---|---|---|
| mass | 64 of 71 | 70 of 71 | 0.001% |
| center of mass (share of length) | 67 of 71 | 70 of 71 | 0.000% |
| pitch inertia | 41 of 71 | 58 of 71 | 0.065% |
| roll inertia | 10 of 71 | 31 of 71 | 1.619% |
| roll inertia, OpenRocket’s fin shortcut in hpr’s place | 59 of 71 | 59 of 71 | 0.001% |
Counting each file’s content once, 50 of 51 are within 1% in mass and 50 of 51 in center of mass.
On 2026-09-28, after M2.2e8 read tube fins sized from the
body, 60 and 68 of 71 were within 0.1% and 1% in mass, 65 and 68 in center of mass, 56 within 1%
in pitch inertia, and 57 in roll inertia with the shortcut, at a median 1.686% without it.
Before tube fins were read, 58 and 66 of 71 were within 0.1% and 1% in mass, and 63 and 66 in
center of mass. Before fillets were weighed, 65 of 71 were within 1% in mass, 55 in pitch inertia, and 56 in
roll inertia with the shortcut.
Before M1.9b read every tube of a
cluster, 58 and 59 of 71 were.
Before M2.2b1 (reading what a .ork leaves unsaid), 57 of
74 files were within 1% in mass and 58 in center of mass (median mass 0.020%). Those are the earlier
74-file measurement; the current default survey is the 71-file table above.
The 1 file outside a threshold is outside both, and has one cause. cargo xtask ork works the causes out, counts them by
content as below, and fails if a file outside has none:
| cause | what hpr does | what OpenRocket does | files by content |
|---|---|---|---|
| airfoil fin sections | integrates the airfoil’s section, 0.6851 of a square slab (below) | weighs the outline times the thickness times 0.85 | 1 |
The fin-section cause is sized, not only present. hpr gives no warning for it, since the section is
hpr’s own choice. So cargo xtask ork weighs the design again with its rounded and airfoil fins
weighed OpenRocket’s way: square, at 0.99 or 0.85 of their density. It names the cause only if the
design then comes within both thresholds. The one design it names is a private one: C06 in hpr’s
flights of the private designs. There hpr’s center of mass sits forward of OpenRocket’s, and the
same fins are a likely cause, but the survey’s thresholds are coarser than that flight’s gap, so
they are a lead for it, not its size
(the format guide).
Five causes are gone. A cluster read as one tube went when
M1.9b read every tube of a cluster: its two files by content
are now within both thresholds. Pods went when M1.13b read
them. Fin fillets, which hpr left out, went when M2.2e7
weighed them (below). Tube fins, which hpr left out when OpenRocket sizes
them from the body, went when M2.2e8 read them
(below): OpenRocket’s Tube fin rocket example, the one design with them, is now
within 5.0e-6 of OpenRocket’s mass and 2.4e-6 of its length in center of mass. Parallel stages,
which hpr kept unread, went when M4.5m read them
(Parallel stages): OpenRocket’s Parallel booster staging, in the
jar and in the library’s copy, is now within 3.3e-6 of OpenRocket’s mass and 1.3e-6 of its length
in center of mass. cargo xtask ork prints the file outside with the parts that differ most, by
id, or by name in an older file that writes no ids. A private design is named only by the start of its file’s hash.
A worked example, now settled. In the first comparison
(M2.2a) the OpenRocket jar’s Two stage high power rocket was
18.74% heavier in hpr: 1.956 kg in OpenRocket, 0.3666 kg more in hpr. All of it was the nose cone.
Its shoulder is written with a radius of 49.28 mm, a length of 50.8 mm and a wall thickness of 0.
hpr read that as solid: a cylinder of π × 0.04928² × 0.0508 = 3.875e-4 m³ of the file’s own
material, polypropylene at 946 kg/m³, weighs 0.3666 kg. OpenRocket gives the same shoulder no mass,
and since M2.2b1 so does hpr, so the file is within 1% in
both mass and center of mass. Two causes the first comparison counted, this shoulder of no wall (6
files by content) and a part written with no material (1), are gone the same way.
Two more conventions.
- Inertia under a mass override. A departure kept on purpose
(below). Loft’s public
stage-weighed.orkoverrides its stage to 1.234 kg on 0.614 kg of parts, a ratio of 2.009; hpr scales the stage’s inertia by it and OpenRocket does not, so hpr’s pitch inertia is +100.9% apart and its roll +108.6%: the largest inertia differences measured. - An airfoil fin section. hpr’s airfoil fin weighs less than OpenRocket’s: the CONTROL fins of the jar’s Simulation scripting example are 0.0378 kg in hpr and 0.0469 kg in OpenRocket, 19.4% lighter, with no warning. It is a departure kept on purpose (below), and the cause of the one private design above.
Roll and pitch inertia. On the six Loft demo designs OpenRocket opens, the roll inertia is 1.2%
to 3.8% apart, though their mass, center of mass and pitch inertia agree within 0.1% and every part
of each is within 0.3 g of OpenRocket’s. Across all 71 compared designs the median is 1.619%. It is the fins:
OpenRocket takes a shortcut for a fin set’s roll inertia, and hpr integrates the fin exactly
(below). With OpenRocket’s shortcut in hpr’s place, the
median is 0.001% and 59 files are within 1%. The shortcut takes OpenRocket’s own mass for each fin
set, paired by id, or in an older file by name, so the way OpenRocket weighs a section (below) is
set aside too. Five of the six Loft demos come within 0.0005%. The sixth, whose fins are
elliptical, is 0.093% apart in that row, and within 0.0002% once OpenRocket’s ellipse is drawn as
OpenRocket draws it, a 30-sided polygon (a test). For each of the 12 files still outside 1% (8 by
content), cargo xtask ork names a cause, and it fails if it can’t. Each has exactly one:
| cause | files by content |
|---|---|
| a mass override covering the parts inside (a departure, below) | 5 |
| tube fins, whose roll inertia is a departure (below) | 1 |
| a cluster’s tubes, which OpenRocket weighs stacked on the cluster’s axis (below) | 2 |
The cluster cause is sized, not only present: the survey names it only when hpr’s roll inertia, less the spread of the clusters’ own tubes, is within 1% of OpenRocket’s. The two are +1.01% and +2.08% apart, and +0.04% and +0.00% without the spread.
The pitch inertia is within 1% on 58 of 71. Two of the 13 outside are the two copies of OpenRocket’s Tube fin rocket, 1.98% below, which OpenRocket’s pitch rule for tube fins accounts for (below). The other 11 have no named cause yet, and no bound is known; on the fin probes below, pitch differs by up to 0.41% where the fins weigh the same.
What it leaves out. Motors: this is the structure alone, and a motor’s mass is M2.2c’s. Only the design’s selected configuration (the one OpenRocket opens it with) is weighed. The 4 files OpenRocket 24.12 does not open are not compared.
Run it yourself. CI does not run OpenRocket. It holds hpr to OpenRocket’s saved answers for
Loft’s seven public demo designs, validation/fixtures/ork/openrocket-mass-loft-demo.json, with
cargo test -p xtask ork_mass: OpenRocket opens six of the seven, and hpr is within 0.1% of it on
those six in mass, center of mass and pitch inertia, and every part of each within 0.3 g. With
Java 17 and the OpenRocket jar
(cargo xtask refs fetch), from the repository root:
refs/venv/bin/python validation/oracles/openrocket/mass.py \
validation/fixtures/ork/openrocket-mass-loft-demo.json validation/fixtures/ork/loft-demo
refs/venv/bin/python validation/oracles/openrocket/mass.py corpus-out/openrocket-mass.json refs --jar
cargo xtask ork
The script takes any directory of .ork files in place of refs; cargo xtask ork compares hpr
with the record of the reference library only.
What a .ork leaves unsaid, and overrides
In short. A design file does not say everything. What does a nose cone’s shoulder written with
a wall thickness of 0 weigh? What is a part that names no material made of? When a part and the
parts inside it both have an override (a mass or center of mass the
designer typed in), which wins? OpenRocket, which writes these files, has an answer to each, and
its answer is what the file means to the person who wrote it. So hpr asks it:
validation/oracles/openrocket/conventions.py writes 32 small probe designs, each a rocket of a
few parts built to ask one question, runs OpenRocket 24.12 on them and records its answers.
The test module hpr_validate::openrocket::tests reads the same designs with hpr and holds hpr to
them. Where hpr keeps a rule of its own, the test pins how far apart the two are. This was
M2.2b1; ADR-061 records the decisions.
How far to trust it: each probe asks about one kind of part at one size, so each reading is measured, not proven for every case. OpenRocket’s defaults were read with its preferences as a fresh install sets them; an OpenRocket whose preferences were changed may give others.
Read as OpenRocket reads it. On every probe of the readings in this table hpr’s mass is OpenRocket’s within 0.001%, part by part as well as whole (the worst, a transition, is 0.0003% apart), and its center of mass within 0.001 mm. Where no fin, rail button or recovery part is in the probe, the inertias agree within 0.001% too. One gap is pinned rather than hidden, and described below: an elliptical fin set weighs 0.18% more. None of these readings raises a warning, since nothing is assumed:
| the file says | what it weighs (OpenRocket 24.12, and now hpr) |
|---|---|
| a nose cone, transition or body tube with a wall of 0 | nothing: the part keeps its shape (hpr’s drag uses the shape, not the wall) but has no wall; a part meant to be solid is written filled |
| an inner tube, coupler or launch lug with a wall of 0 | nothing |
| a shoulder with a wall of 0, or none written | nothing, whether or not the file closes its end with a cap, on a hollow nose or a filled one |
| a filled nose cone with a walled shoulder | the solid cone plus the shoulder’s own wall |
| a nose cone, transition or body tube with no thickness written | a 2 mm wall, whatever its radius (measured on a nose cone and a tube at 50 mm and at 30 mm, and on a transition) |
| a part weighed by its volume (a nose, transition, tube, coupler, engine block, fin set, ring or lug) with no material | cardboard, 680 kg/m³ |
| a canopy or streamer with no material | ripstop nylon, 0.067 kg/m² |
| shroud lines or a shock cord with no material | a 2 mm elastic cord, 0.0018 kg/m |
| a rail button with no material | Delrin, 1,420 kg/m³ |
A worked example with the probe’s numbers: a conical nose cone 0.3 m long on a 50 mm base, with a
2 mm wall of a material at 1,000 kg/m³, weighs 0.091726 kg. With a shoulder 100 mm long, 48 mm in
radius and a 2 mm wall, it weighs 0.150787 kg. With the same shoulder written with a wall of 0 it
weighs 0.091726 kg again, in both programs (the probe a nose whose shoulder has no wall). Before
this step hpr read that shoulder as solid, and would have added π × 0.048² × 0.1 × 1000 =
0.724 kg.
Which override wins. An override is a number the designer typed in place of what the parts add up to, usually after weighing the real thing. The file can say that an override covers the parts inside: that the number is for the part together with everything attached to it. The probes find hpr and OpenRocket agree on which override wins, and on where a center is measured from:
- An override on a part that covers the parts inside it wins over any of theirs, and a stage’s wins over everything in the stage.
- A center-of-gravity override is measured from the part’s front, not from its shoulder’s, and the shoulder moves with the part.
- A center-of-gravity override alone, written to cover the parts inside, sets the whole assembly’s center. (The two place the parts inside differently, which shows only in the inertia: below.)
This settles Loft lesson L51, whose rule for this came from
OpenRocket’s source and was unsettled by up to 133 mm. The test is
override_precedence_matches_oracle.
Where hpr keeps its own rule. Each of these is a departure: hpr knowingly differs from OpenRocket, and a test pins by how much.
| when | hpr | OpenRocket | apart on the probe |
|---|---|---|---|
| a mass override covers the parts inside and states no center | keeps the center the parts lay out | puts it at the overriding part’s own, ignoring where the parts inside sit | hpr’s center 3.7 mm forward of OpenRocket’s, or 19.7 mm if the part inside has an override of its own |
| a mass override covers more than one part | scales the inertia of everything it covers by the override’s ratio | scales only the overriding part’s own inertia, and keeps the parts inside at theirs; a stage, having none of its own, scales nothing | roll inertia 6.9% to 37% lower in hpr under a tube’s; 2.5 to 5.0 times OpenRocket’s under a stage’s |
| a center override covers the parts inside | moves the whole assembly, so the inertia about the new center is the assembly’s own | moves the overriding part alone, and adds the parts inside where they were | the center agrees; pitch inertia 2.65% lower in hpr |
Under a tube’s covering override hpr’s roll inertia is the lower one, though hpr scales more of the parts. OpenRocket keeps the inertia of the parts inside while leaving their mass out of the total, so its assembly carries inertia for mass it does not count.
Why keep them: a builder who weighs a tube with its fins and motor mount inside has not moved their center, so the center the parts lay out is the better estimate. And scaling the inertia with the mass keeps it consistent with the mass: the extra weight sits where the parts’ weight does. Neither rule is right for every rocket (a heavy avionics bay at the center of mass adds little inertia). On a single part, with nothing inside, the two programs agree.
One more difference cannot be said in hpr’s design format: a part that overrides both its mass and its center, with one covering the parts inside and the other not. hpr scopes a part’s overrides once, takes the mass’s, and warns. On the probes the center is 4.7 mm apart when the center’s override is the covering one, and agrees when the mass’s is (with pitch inertia 8.2% lower in hpr).
Two gaps the probes found, both settled in M2.2b2 (next section): a rail button sat 5 mm further aft in hpr than in OpenRocket, and is now where OpenRocket puts it; and OpenRocket’s elliptical fin weighs 0.18% less than hpr’s exact ellipse, which matches, to 13 digits, a 30-sided polygon drawn inside the ellipse at equal angles. hpr keeps the ellipse.
What it leaves out. An inner tube, coupler or lug that writes no thickness at all is read as no wall, with a warning. OpenRocket gives it a wall of its own: on the probe, 0.5 mm for a 20 mm inner tube, 1 mm for a 5 mm lug, and none for a coupler. One size each does not say whether that wall follows the radius, and no file in the reference library has one, so hpr does not follow it yet; the test pins the difference, −2.4% in the mass of that probe’s structure.
Run it yourself. With Java 17 and the OpenRocket jar (cargo xtask refs fetch), from the
repository root:
refs/venv/bin/python validation/oracles/openrocket/conventions.py \
validation/fixtures/ork/openrocket-conventions.json
cargo test -p hpr-validate openrocket
Fins, rail buttons and roll inertia
In short. A rocket’s roll inertia is its resistance to spinning about its long axis. hpr’s was a median 2.351% from OpenRocket’s on the 71 compared designs, and nothing explained it. It is the fins. OpenRocket works out a fin set’s roll inertia with a shortcut; hpr integrates over the fin exactly. M2.2b2 measured the shortcut on 33 more probe designs, each a tube and one part. hpr keeps its own: a departure, a rule hpr keeps on purpose, measured and pinned by a test. The same probes settle how each fin section is weighed and where a rail button sits. ADR-062 records the decisions.
How far to trust it: the shortcut is inferred from OpenRocket’s output; its source is GPL, so the project does not read it. It matches every fin probe but two to 1e-12, and those two are explained below. Every tapered probe has a span half its root chord; Loft’s demos, whose spans are 0.39 to 0.50 of the root, hold to 0.0005% as well.
The other parts agree, bar a rail button. A bulkhead, centering ring, inner tube, mass component, parachute, shock cord and streamer each have OpenRocket’s mass, center of mass and both inertias on their probes, to 1e-15. A rail button’s inertias are apart by up to 0.05% of its probe’s, and a launch lug’s pitch inertia by 0.03% (below).
OpenRocket’s shortcut. For a set of two or more fins, OpenRocket spreads the set’s mass m
evenly along a thin rod that runs straight out from the body, at radius R, to R + hₑ, and
takes that rod’s roll inertia. hₑ is an effective span:
I_roll = m (R² + R hₑ + hₑ²/3), hₑ² = A h / c_r
Here A is one fin’s area, h its span and c_r its root chord. For a rectangular fin, hₑ is
the span, and the shortcut is exact except that it leaves out the fin’s thickness. hₑ is shorter
than the span when the fin narrows outward, and longer when it widens. The rule is inferred from
OpenRocket’s output, and every tapered probe narrows outward, so a fin that widens is the rule
carried past what was measured. A tab’s mass goes where the
fin’s does, and neither the section nor the thickness enters. A single fin gets the same rod about
its own middle, m hₑ²/12.
A worked example: the probe’s trapezoid. Three fins with a 100 mm root chord, a 50 mm tip
chord, a 50 mm span and 50 mm of sweep, 3 mm thick, of 1,000 kg/m³, on a tube 50 mm in radius.
Each fin has an area of 0.00375 m², so the set weighs 33.75 g. Then hₑ² = 0.00375 × 0.05 / 0.1 = 0.001875 m², so hₑ = 43.3 mm, and I_roll = 0.03375 × (0.0025 + 0.05 × 0.0433 + 0.000625) = 1.7854e-4 kg·m², OpenRocket’s figure. hpr’s exact integral is 1.8284e-4 kg·m², 2.4% more: the
fin’s outer part weighs more than the rod puts there.
The rod spreads the mass evenly, but a real fin’s mass follows its chord, so the sign depends on the outline: a triangle’s mass sits nearer the body than the rod’s. A tab lies inside the body tube, 40 to 50 mm from the axis on the probe, but the rod puts its mass out with the fin’s, so OpenRocket’s figure is the larger there.
| the probe’s fin set | hpr’s roll inertia (kg·m²) | OpenRocket’s | hpr against OpenRocket |
|---|---|---|---|
| rectangular, 100 mm by 50 mm | 2.6253e-4 | 2.6250e-4 | +0.013% (the thickness) |
| the trapezoid above | 1.8284e-4 | 1.7854e-4 | +2.41% |
| triangular, 100 mm root, 50 mm span | 1.0314e-4 | 1.0540e-4 | −2.14% |
| the trapezoid with a tab 50 mm by 10 mm | 1.9199e-4 | 2.0234e-4 | −5.12% |
The two fin probes the shortcut does not match to 1e-12 are an elliptical fin set (its polygon,
below) and a canted one (by 2.66e-5 of the probe’s roll inertia, not traced).
hpr_validate::openrocket::openrocket_fin_set_roll_kg_m2 states the shortcut, and the 74-file
comparison uses it for the second roll row of the table above. The
test each_part_alone_is_openrocket_s_or_pinned holds every probe of this section to OpenRocket’s,
or pins how far apart they are.
How each fin section is weighed. OpenRocket weighs a fin set as its outline times its thickness
times a factor for its section: 1 for square, 0.99 for rounded and 0.85 for an airfoil, whatever
the thickness (checked at 3 mm and 6 mm). hpr works the section out: a rounded edge is a
semicircle, 0.9914 of the square section on the probe, and an airfoil is NACA’s four-digit
section, 0.6851 (Abbott and von Doenhoff). A file that says airfoil does not say which airfoil,
and a builder who weighed the fins can give their mass. So hpr keeps its sections. The elliptical
fin stays the exact ellipse too; OpenRocket’s 30-sided polygon weighs 0.18% less.
Where a rail button sits. OpenRocket gives a rail button no length. It puts the button’s center
where a part of no length would sit, whichever end of the tube the file measures from, and a row’s
first button there, the rest following aft. hpr now reads a .ork button so (issue
#151). Before, hpr put the row’s forward edge, middle
or aft edge on the position. The move depends on which end the file measures from:
| measured from | how the row moves (r the button’s outer radius, s the spacing center to center, n buttons) | two buttons of 10 mm outer diameter, 100 mm center to center |
|---|---|---|
| the top, after a part, or absolute | forward r | 5 mm forward |
| the middle | aft (n − 1)s/2; one button does not move | 50 mm aft |
| the bottom | aft r + (n − 1)s | 105 mm aft |
A flight leaves the rail when its aft-most guide does. So a row placed from the top, after a part or absolutely now leaves it a radius earlier, and one placed from the middle or the bottom leaves it later: more rail to travel, so a little faster off the rail. The probes check the top, the middle and the bottom, with one button and with a row of two.
A rail button’s screw head is weighed. Since
M4.5i, hpr reads a .ork button’s <screwheight> and
weighs the head as half a solid ellipsoid on the flange (the model is above, under
Standard solids). Before, it left the head off with a warning, and a rocket
with one did not fly (ADR-168). A 10 mm button with a 2 mm screw, in Delrin at
1,420 kg/m³, gains a head of 2/3 × π × (5 mm)² × 2 mm = 104.7 mm³, or 0.149 g, its center
0.75 mm above the flange. OpenRocket 24.12, probed with screws of 0, 1, 2 and 5 mm, gives the
same volume to 2e-16 once its single-precision 2/3 is used; hpr’s double-precision 2/3 gives
1.1e-8 of the volume more at a 5 mm screw. Two things differ, both pinned by the test
a_rail_buttons_screw_head_is_half_an_ellipsoid:
- OpenRocket puts the head’s center
4h/3πabove the flange, a flat half-disc’s centroid, which is 0.049 h further out than the solid’s3h/8. hpr keeps the solid’s. - With a mass override on the button, OpenRocket still moves the button’s center by the screw, and so does hpr.
The screw head changes neither the drag, the center of pressure nor the normal force in OpenRocket, and hpr gives it no drag either.
What is left, each pinned by a test.
- Fin fillets’ pitch inertia: −0.008% to −0.638% on the eight probes with more than one fin, and +0.425% on a single fin (below). Their mass and center agree.
- Small and not traced: a canted fin set’s mass (−0.004%), fins’ pitch inertia (up to 0.41% where the fins weigh the same, on a single fin; up to 0.11% on the other probes), a launch lug’s pitch inertia (0.03%) and a rail button’s inertias (up to 0.05%).
- A rail button’s screw head: its center, 0.049 of the screw’s height nearer the body in hpr than in OpenRocket (above).
Fin fillets were left out, a measured departure (ADR-064, the decision to keep it visible), until M2.2e7 weighed them (below). A cluster’s departure is measured below. Two more gaps these probes found, both in packed parts, are settled below.
Run it yourself. As for the probes above: the same
script writes these, and cargo test -p hpr-validate openrocket checks them.
Tube fins
A tube fin set is a ring of short open tubes along the airframe, in place of flat fins. hpr
weighs each tube as a hollow cylinder with its axis at R + r from the airframe’s axis, where R
is the body tube’s outer radius and r the tube’s own. Since
M2.2e8 hpr also reads a .ork tube fin set whose radius
OpenRocket works out from the body
(the format guide has the rule).
ADR-098, the decision on that radius, records what follows.
Mass and center agree. On 19 OpenRocket probe designs, each tube fin set’s mass is within 1e-14 of OpenRocket’s and its center within 1e-15 m. OpenRocket’s Tube fin rocket example is within 5.0e-6 of OpenRocket’s mass and 2.4e-6 of its length in center of mass. Its two inertias are not: each is a departure, a difference hpr keeps on purpose, measured and pinned by a test.
Roll inertia, the first departure. Divide a part’s roll inertia by its mass and you get its
unit inertia, in m²: the mean of the squared distance of its mass from the axis. No mass inside
a ring of tubes is farther from the axis than R + 2r, the outer edge of the tubes. So no ring’s
unit inertia can exceed (R + 2r)². For two tubes or more, OpenRocket’s does:
| six tubes of 20 mm radius on a 50 mm body | unit roll inertia |
|---|---|
the most any ring can have, (0.05 + 2 × 0.02)² | 0.0081 m² |
| OpenRocket 24.12’s | 0.3047 m², about 38 times that |
hpr keeps its own figure, which is below that bound, as it must be. For a single tube the two
agree: OpenRocket puts its center at R + r, where hpr does, and its unit inertia about the
tube’s own axis is (r² + rᵢ²)/2, with rᵢ the tube’s inner radius, as hpr’s is.
Pitch inertia, the second departure. Pitch is turning end over end, about an axis across the
airframe through the set’s center. One tube’s own unit pitch inertia, about its own middle, is
(r² + rᵢ²)/4 + L²/12 for a tube of length L.
- hpr adds, for three tubes or more, the ring’s spread:
(R + r)²/2, the mean squared distance of the tube axes from the pitch axis, since they sit atR + rall around. - On all 19 probes, OpenRocket’s is
Ntimes one tube’s own, forNtubes, with no term for the tubes’ distance from the airframe’s axis.
OpenRocket’s figure is 1.3 to 2.6 times hpr’s on the probes, and 1.77 times on six tubes of 20 mm
radius on a 50 mm body. On the probes, whose tubes are 0.1 m long, it stays under the most any mass
inside the ring could have about that axis, (R + 2r)² + (L/2)². On the Tube fin rocket’s
longer tubes it is 18% over that bound (3.352e-3 m² against 2.834e-3 m² a unit of mass), so no
mass could have it. Either way it is not what tubes at R + r weigh, so hpr keeps its own. For a
single tube the two agree.
On the example. On OpenRocket’s Tube fin rocket, hpr’s roll inertia is 98.7% below
OpenRocket’s, and its pitch inertia 1.98% below. The pitch gap is sized: cargo xtask ork swaps
OpenRocket’s pitch rule into hpr’s structure for each tube fin set and prints the result, and the
gap goes from −1.9800% to +0.0023%, well inside the survey’s 0.1%. The survey
above names tube fins as the cause of the roll gap. OpenRocket’s fin
shortcut, which the survey swaps in for flat fins, is not given OpenRocket’s mass for tube fins,
so it cannot close this one.
The test a_tube_fin_sets_automatic_radius_reads_as_openrocket_does in hpr-validate holds both
departures both ways. OpenRocket’s roll is above the bound and hpr’s below it, on every probe with
two tubes or more. OpenRocket’s pitch is N times one tube’s own, to 1e-14. hpr’s roll and pitch
match their closed forms to 1e-13.
Clusters and fillets
A cluster is a motor mount with more than one motor tube. Since M1.9b hpr weighs every tube where it sits, each with its own parallel-axis term, and repeats what a tube holds in every tube (Clusters). OpenRocket 24.12, asked on 25 probes (ADR-075), agrees on the mass and the center of mass to 1e-12, but not on the inertia. It weighs a cluster’s tubes as if stacked on the cluster’s axis: a 3-ring at scale 1 and at scale 1.5 have the same inertias in OpenRocket. It does weigh an engine block inside each tube where it sits. hpr does not copy the stacking, since the tubes are not on the axis.
The fixed 3-ring probe is a tube and a 200 mm inner tube with a 20 mm outer radius and 1 mm wall,
three of them 23.09 mm from the axis. OpenRocket’s saved structure is 0.3813893481458014 kg, with
center 0.24036243822075784 m, roll 0.0007674901427941916 kg m² and pitch 0.007191233036548777
kg m². hpr’s mass and center are the same, and its roll inertia is +5.11% and its pitch +0.273%
apart: the spread of the tubes, 3 m d² (3.92e-5 kg m², each tube’s mass m at d = 23.09 mm
from the axis), and half of it. On every cluster probe on the body’s axis the difference is exactly
that, to 1e-12, whatever the pattern, scale, contents or overrides. Off the axis, three differences
are left and pinned as measured, with the spread taken out. A lone tube 10 mm off the axis is
+0.303% in roll and +0.016% in pitch: that is m d² (1 − m/M) in roll and half of it in pitch, for
the tube’s mass m, its offset d and the structure’s mass M, as if OpenRocket left the offset
out. The pitch of two clusters
off the axis is +0.015% and +0.021%, which hpr has not traced. Before
M1.9b, reading one tube left hpr 12.85% light, 5.95 mm forward,
2.43% low in roll and 3.68% low in pitch. each_part_alone_is_openrocket_s_or_pinned and
a_cluster_weighs_as_openrocket_s_but_for_its_tubes_spread check these.
Fin fillets
A fin fillet is the rounded glue joint along a fin’s root, where the fin meets the tube. Since M2.2e7 hpr weighs fillets as OpenRocket 24.12 does (ADR-096, the decision on fillets). Before, it left them out with a warning, and the 5 mm and 10 mm probes were 0.808% and 2.79% light (ADR-064).
The shape. A fillet fills the corner between the tube and the fin’s side. Its concave face is a
circle of the fillet’s radius that touches both. Its section is bounded by three edges: the tube’s
circle, the fin’s plane and that circle. As in OpenRocket, the fin is taken as having no thickness
there, so the section starts at the fin’s middle plane, not its face. Two fillets run along each
fin, one each side, as long as the root chord. So each is a prism of that section. It is made of the
file’s filletmaterial; a file that names none gets OpenRocket’s cardboard, 680 kg/m³. Most
fillets are epoxy, which is heavier (hpr’s epoxy is 1,180 kg/m³), so set filletmaterial in
OpenRocket, or the fillet’s material in hpr, to what you used.
The equation. Take the body radius R and the fillet radius r. Take x outward from the
body’s axis along the fin’s mid-plane, and y square to it. The fillet circle’s center is at
(c, r), with c = √(R² + 2Rr). That puts it √(c² + r²) = R + r from the axis, so the circle
just touches the tube. The section is the triangle (0, 0), (c, 0),
(c, r) less two circular sectors: the tube’s, up to the angle θ = atan(r/c), and the fillet
circle’s, whose angle is π/2 − θ:
A = c r/2 − R² θ/2 − r² (π/2 − θ)/2
On a flat body (R very large) this tends to r² (1 − π/4): a square less a quarter circle. hpr
works out the section’s first and second moments the same way, in closed form. The sectors’ second
moments are computed in a stable form, but the final subtraction still loses about log10(R/r)
digits: at most 2 for a fillet 1% of the body radius or larger. A test checks all four against
numerical integration to 1e-11 (fillet_section_is_its_region_by_quadrature in
hpr_design::fins). Far from real fillets the subtraction cancels away, so hpr weighs a fillet
under a millionth of the body radius as nothing, refuses one over a thousand times it
(FILLET_RATIO_MAX), and gives a body of no radius no fillet. Against 60-digit arithmetic, the
area keeps 10 significant digits at a millionth of the body radius and 12 at a thousand times it,
checked once while writing the code; the thousand is a cautious limit, not where the digits run
out (at ten thousand times it still keeps 10).
A worked example. An invented 5 mm fillet on a tube 30 mm in radius. Then
c = √(30² + 2 × 30 × 5) = 34.64 mm and θ = atan(5/34.64) = 0.1433 rad (8.21°).
| piece | formula | area |
|---|---|---|
| the triangle | c r/2 | 86.60 mm² |
| less the tube’s sector | R² θ/2 | 64.51 mm² |
| less the fillet circle’s sector | r² (π/2 − θ)/2 | 17.84 mm² |
| the section | A | 4.253 mm² |
That is 79% of the 5.365 mm² a flat body would give: the tube curves away from the fin, so the
corner holds less. Three fins with a 100 mm root chord have six fillets. Their volume is
6 × 4.253 mm² × 100 mm = 2,552 mm³ = 2.55 cm³. In cardboard they weigh
2.552 cm³ × 0.680 g/cm³ = 1.735 g. A unit test pins these numbers
(the_worked_fillet_example_is_the_docs in hpr_design::fins).
How well. Nine probe designs measure it: the 5 mm and 10 mm probes of M2.2b4, and seven new in M2.2e7:
- fillets of 30 mm;
- fillets in their own material;
- fillets naming no material;
- a single fin;
- four fins of rounded section;
- a freeform fin set;
- a wider tube.
On every one the fillets’ mass and center of mass are OpenRocket’s to 1e-15, and the test holds
them to 1e-12. On the rounded-section probe the whole fin set’s mass is 1.79e-4 apart, from the fin
section’s factor (below), not from the fillets. The whole
probe’s pitch inertia is apart by −0.0077% to −0.638% on the eight with more than one fin, growing
with the fillets’ mass. On the single fin it is +0.425%, near the +0.406% a single fin reads
without fillets. hpr’s figure is the exact prism’s; how OpenRocket works out a fin’s pitch inertia has not
been measured (ADR-062). The roll rows use OpenRocket’s fin shortcut, as every fin probe
does. each_part_alone_is_openrocket_s_or_pinned pins every row.
What it leaves out. The fillets’ drag and lift: the aerodynamics ignores them (Drag limits). A fillet that is not a circular arc, such as a hand-shaped bead of glue, is weighed as one.
Packed parts
A parachute, streamer, shock cord or mass component (an altimeter, a battery, ballast) is weighed as a solid cylinder: its packed length and packed radius, where it sits in the tube (above). Two cases need a rule of their own, and hpr now takes OpenRocket’s for both, measured on probe designs OpenRocket 24.12 reads (M2.2b3, ADR-063). Like every OpenRocket rule on this page, they are inferred from what OpenRocket prints, not read from its source.
A file that writes no packed size. OpenRocket packs the part 25 mm long and 12.5 mm in radius.
The radius is a fixed number, not the tube’s bore: it is the same in bores 48 and 98 mm in radius,
and OpenRocket does not shrink it to fit a bore of 8 mm. Neither does hpr. Each
number stands alone, so a file that writes only a length gets the 12.5 mm radius, and one that
writes only a radius gets the 25 mm length. hpr reads a .ork the same way, with no warning.
Before, it read a length of zero with no warning, and a radius of zero with one.
A mass override on a part that weighs nothing. A parachute with no canopy, or a mass component of 0 g, given a 30 g override: OpenRocket spreads the 30 g over the packing. So does hpr now. In a packing 50 mm long and 20 mm in radius that is:
| formula | 30 g in the probes’ packing | |
|---|---|---|
| roll inertia | m r²/2 | 6.0 × 10⁻⁶ kg·m² |
| pitch inertia, about its own center | m (3r² + l²)/12 | 9.25 × 10⁻⁶ kg·m² |
| center | halfway along the packing | 25 mm aft of its forward end |
Before, hpr put the 30 g at a point, which left the probe’s roll inertia 0.805% low. Any other part that weighs nothing still becomes a point mass under an override; both programs do that (above).
How well. Eleven probes ask these questions, a tube and one packed part each. hpr’s structure is OpenRocket’s to 1e-12 on every one, in mass, center of mass, roll and pitch. The earlier 74-file before-and-after measurement took the roll inertia, with OpenRocket’s fin shortcut in hpr’s place, from 49 to 53 files within 0.1%, and from 55 to 57 within 1%. The current survey, on 71 designs, has 59 within 0.1% and within 1% with that shortcut (above):
| (earlier 74-file measurement) | before | after |
|---|---|---|
| center of mass within 0.1% of length | 54 of 74 | 55 of 74 |
| pitch inertia within 0.1% (median) | 34 of 74 (0.110%) | 37 of 74 (0.086%) |
| roll inertia, fin shortcut in hpr’s place, within 1% | 55 of 74 | 57 of 74 |
| roll inertia, hpr’s own fins, within 1% (median) | 29 of 74 (2.112%) | 28 of 74 (2.354%) |
The last row moves the wrong way. hpr’s own fin roll departs from OpenRocket’s (above). In one file the point mass had left hpr’s roll inertia low, which happened to cancel part of that departure; the packing now adds it back.
What it leaves out. A packed size the file writes but hpr cannot read as a number is read as zero, with a warning, as any unreadable number is. Every probe places its part from the tube’s top, so how the 25 mm length moves a part placed from the middle, the bottom or after another part is worked out, not measured. The override rule was probed on a parachute, a mass component and a shock cord; a streamer takes it too, by the same packing, without a probe of its own. Mass is unchanged: the rules move mass, they add none.
Run it yourself. refs/venv/bin/python validation/oracles/openrocket/conventions.py validation/fixtures/ork/openrocket-conventions.json writes the probes (it needs OpenRocket 24.12
and Java 17), and cargo test -p hpr-validate openrocket checks them.
Verification
- By hand:
mass::tests: two boxes make one box; point masses give the products of inertia; rolling swaps and mixes axes asI′_xy = (I_xx − I_yy) sin θ cos θ; rotation keeps the principal moments; motor elements land on the axis.tests::composite_rocket_inertia_matches_hand_calculation(crate root) combines a filled cone, a tube, four fins and an off-axis payload. Each part’s moments come from its own formula and the six tensor terms are written out; the result agrees to 1e-11.parts::tests::a_nose_cone_with_a_capped_shoulder_adds_up_by_handchecks the cone, tube and cap to 1e-11.fins::tests:- A rectangular fin is a box, and four fins are the sum of rotated boxes.
- A swept fin’s
I_xzmatches quadrature of the planform. - A fin canted 90° is the box turned.
- Tube fins match the parallel-axis theorem.
- Closed forms against quadrature:
- Each cross-section’s
M₀, M₁, M₂, Tmatches exact quadrature oft(x)to 1e-13, on a wide chord and one narrower thant. - The airfoil constants match to 1e-14.
- Trapezoidal, elliptical and freeform areas and centroids match to 1e-12.
- Each cross-section’s
- Properties (
mass::tests, proptest): combining is associative and order-free, turning a body keeps its principal moments, and the inertia about any point exceeds that about the center. - Materials: ids are unique, sources present, and the unit conversions reproduce the sources (1.1 oz/yd² = 37.3 g/m², 225 ft/lb = 6.61 g/m, white ash 678 kg/m³).
- Loft lessons:
Solid motors
In short
- What it models: commercial solid motors: the thrust curve and its statistics (total impulse, burn time, average thrust, class), how the propellant burns away, the motor’s mass, center of mass and inertia as it burns, thrust at altitude, and ejection delays.
- Sources: NASA SP-8039 (1971), the National Association of Rocketry’s Standard Motor Codes, ThrustCurve.org’s glossary, statistics page and code, and RocketPy 1.13.0’s motor code.
- How well it is validated: by analytic tests (exact answers worked out by hand) and by another code (code-to-code), the first and third of the four kinds of evidence. Small numbers here are in scientific notation, and a relative difference is a fraction of the value compared. ThrustCurve.org’s own statistics code agrees to 1.8e-15, relative, on all 32 bundled curves. On three of them, RocketPy’s total mass and inertias agree within 7.9e-5 relative. The two centers of mass differ by under 5.8e-6 of the motor’s length, and the propellant’s own mass and inertias agree within 1e-4 of their values at ignition. OpenRocket 24.12, handed the same curve files, reads exactly the same total impulse, peak thrust, burn-time window and curve duration on all 32 bundled curves; its average thrust divides by the same window but counts only the impulse inside it, so hpr’s is +0.0107% to +0.3147% higher. Nothing else in this model is compared with OpenRocket (not the propellant’s burn-back, its mass and inertias, or the delays), and nothing here is compared with a real flight.
- What it leaves out: anything but commercial off-the-shelf solids. Only 32 curves are bundled, none in class A. Propellant burns in proportion to the impulse delivered, an approximation. With only catalog data, the center of mass stays at mid-length. Commercial motor files give no nozzle exit size, so thrust at altitude goes uncorrected unless one is supplied.
Using a motor
A flight needs a motor in the rocket’s motor mount. There are two ways to get one:
- take one of the 32 motors that come with hpr-sim, or
- read a thrust-curve file, such as one downloaded from ThrustCurve.org, by hand or through the library (Matching motors to ThrustCurve.org).
Either way, the motor then goes into one of the rocket design’s configurations. A short program,
crates/hpr-sim/examples/motors.rs,
shows all of this: it lists the built-in motors, reads a .eng file, and flies that motor in a
rocket. Run it from anywhere in the repository (the setup is on
Getting started):
cargo run --example motors -p hpr-sim
It prints this. The output is committed in
motors.output.txt,
and CI runs the program on macOS, Windows and Linux and fails if it prints anything else, so the
list below is always the list the code bundles.
32 motors come built in:
dia length loaded total average burn
designation maker type class mm mm mass g impulse N·s thrust N time s
B4 Quest single-use B 18 80 19.8 4.9 4.4 1.11
C5 Estes single-use C 18 70 23.8 7.8 3.9 1.98
D5 Quest single-use D 20 96 45.1 17.6 3.8 4.59
E26W AeroTech single-use E 24 88 44.0 27.6 23.5 1.18
26E31-15A Cesaroni reload E 24 69 52.0 26.1 30.7 0.85
F52C AeroTech single-use F 29 111 81.4 66.3 52.8 1.25
68F240-15A Cesaroni reload F 24 133 91.8 68.0 236.6 0.29
F15 Estes single-use F 29 114 103.0 49.6 14.5 3.42
G69N AeroTech reload G 38 106 201.0 136.3 72.1 1.89
131G84-10A Cesaroni reload G 24 228 172.0 131.3 84.2 1.56
H170M AeroTech reload H 38 191 330.0 318.0 165.4 1.92
168H54-10A Cesaroni reload H 29 187 209.0 168.2 53.8 3.13
H125-CT Loki reload H 38 177.8 342.0 241.7 125.0 1.93
I175WS AeroTech single-use I 38 214 348.0 333.2 177.5 1.88
411I175-14A Cesaroni reload I 38 245 437.5 411.4 174.1 2.36
I377-CT Loki reload I 38 292 560.0 525.8 377.9 1.39
J450DM AeroTech single-use J 54 359 1223.0 1061.6 465.6 2.28
1266J760-19A Cesaroni reload J 54 329 1076.8 1267.3 758.2 1.67
J300LR Loki reload J 54 327 1315.0 1212.8 297.0 4.08
K400C AeroTech single-use K 54 359 1194.0 1307.3 409.8 3.19
2245K1075-P AMW reload K 54 728 2638.8 2255.1 1072.9 2.10
1633K940-18A Cesaroni reload K 54 404 1366.5 1636.1 936.9 1.75
L2500ST AeroTech reload L 98 443 4989.5 4671.9 2501.7 1.87
3300L3200-P Cesaroni reload L 75 486 3263.7 3299.6 3206.8 1.03
L1040LR Loki reload L 54 736 2962.0 3722.5 1037.1 3.59
M1350W AeroTech single-use M 75 622 4808.0 5179.5 1350.1 3.84
8187M1545-P Cesaroni reload M 75 1025 7878.3 8181.8 1547.7 5.29
M1378LR Loki reload M 54 1108 4331.0 5362.3 1375.6 3.90
N3300R AeroTech reload N 98 1060 12269.0 14035.1 3189.9 4.40
13628N5600-P Cesaroni reload N 98 1010 11280.0 13633.5 5626.1 2.42
O6000W AeroTech single-use O 152 1120 31524.6 39687.2 5828.8 6.81
21062O3400-P Cesaroni reload O 98 1239 16842.0 21041.0 3424.3 6.14
From the .eng file: I377CT by Loki, 38 mm by 292 mm, 560 g loaded, 250 g of propellant
525.8 N·s (class I), 377.9 N average over 1.39 s, 657.0 N peak
effective exhaust velocity 2103 m/s
Valetudo on the I377CT: 8.82 kg at liftoff
leaves the 3 m rail at 17.7 m/s
apogee 151.7 m above the pad, at 6.09 s
Not yet validated: see the Accuracy page before trusting these numbers.
The whole program is at the end of this page, in The example program.
The bundled motors
License: each curve is a ThrustCurve.org file marked public domain, bundled unchanged. Every
number in the catalog comes from the file itself: the size, masses and delays from its header,
and the rest worked out from its curve. ThrustCurve.org states no terms for its own motor records,
so none of their figures is bundled (issue #295;
third-party notices).
cargo xtask motor-catalog writes the catalog from the files, and a test fails if the committed
one differs.
| column | what it is | where it comes from |
|---|---|---|
| designation | The motor’s full name (motor designation) | the motor’s name, as ThrustCurve.org lists the file |
| maker | The manufacturer, abbreviated | as ThrustCurve.org lists the file |
| type | single-use, or reload: a propellant load for a reusable case | as ThrustCurve.org lists the file |
| class | The impulse class letter | hpr, from the curve |
| dia, length | The case’s diameter and length, mm | the file’s header |
| loaded mass | The motor ready to fly, g. For a reload this includes the case | the file’s header |
| total impulse | Total impulse, N·s | hpr, from the curve |
| average thrust | Average thrust, N | hpr, from the curve |
| burn time | Burn time, s | hpr, from the curve |
How the 32 were chosen:
- They are public-domain ThrustCurve.org curves of motors in production.
- When they were chosen (2026-09-17), each curve’s total impulse, burn time and average thrust
were within 1% of the values ThrustCurve.org publishes for the motor. Those values are not
bundled; with ThrustCurve.org’s catalog saved outside the repository,
cargo xtask motor-catalogmeasures the gap again and fails over 1%. Total impulse runs from 0.83% below ThrustCurve.org’s to 0.72% above, average thrust from 0.81% below to 0.95% above, and burn time from 0.95% below to 0.99% above. Peak thrust is not held to it: it runs from 16.7% below (Cesaroni 26E31-15A) to 2.1% above (Loki M1378LR). - The files’ headers differ from ThrustCurve.org’s records in a few places, and the header is what hpr flies: 5 lengths differ (from 1.2% shorter, the B4, to 1.3% longer, the N3300R), 7 propellant masses (from 2.7% lighter, the C5, to 52% heavier, the 26E31-15A) and 6 loaded masses (from 4.0% lighter, the C5, to 2.3% heavier, the D5). For the 26E31-15A the header is the likelier: its 16.9 g of propellant gives 26.1 N·s at an effective exhaust velocity of 1,543 m/s, where the record’s 11.1 g would need 2,350 m/s, more than every other bundled motor but the K400C. The delays are the header’s too, which often lists fewer settings than the maker sells.
- There are up to three per impulse class, from different manufacturers. None is in class A.
- The selection, and the curves left out, are in the ThrustCurve.org data notes.
In code, Catalog::bundled() returns the list
(Catalog). catalog.find("I377") finds a motor
by its designation or common name, ignoring case, spaces and hyphens. It returns every match:
I175 finds both the AeroTech I175WS and the Cesaroni 411I175-14A. Then
entry.bundled_motor() builds the motor
(CatalogMotor), with the catalog’s size and
masses. The catalog doesn’t say where inside the motor its mass sits, so hpr uses a rough guess,
the envelope default: the propellant and the rest of the motor (case, nozzle and closures) are
each spread evenly along its length, so the center of mass stays at mid-length as it burns
(The whole motor gives the details).
A motor from a file
Motor files come in two formats, and ThrustCurve.org serves both:
- RASP
.eng, a plain-text format named after RASP, the rocket simulation program it comes from. It is the more common of the two: 889 of the 1712 solid-motor files ThrustCurve.org held when surveyed. - RockSim
.rse, an XML format from the RockSim simulator.
The example reads a .eng file in four steps:
- Read the text. For a file you downloaded, use
std::fs::read_to_string("my-motor.eng")?. hpr’s motor crate never opens files itself. The example builds one of the bundled files into the program withinclude_str!instead, so that it runs anywhere. - Parse it.
eng::parse(&text)?(eng::parse) returns the file’s entries, one per motor (a file can hold several), and a list of warnings. The reader is lenient: it accepts the oddities real files have and reports each one, with its line number (reader policy). This file reads with no warnings. - Take the curve.
entry.thrust_curve()?makes the thrust curve, starting it from zero thrust at ignition. - Build the motor.
SolidMotor::from_envelope(curve, diameter_m, length_m, propellant_kg, loaded_kg)(SolidMotor). A.engheader gives the size in millimeters and the masses in kilograms, so the example multiplies the size by 0.001 and passes the masses as they are. Check that step yourself: the motor can’t tell a size left in millimeters. The units check looks at the impulse and the propellant mass, not the size, and any positive size is accepted. The design checks catch part of it later, if the same size goes into the rocket (Putting it in a rocket): a diameter too wide for the mount stops the flight, but a length too long for it only draws a warning.
For a .rse file, use rse::parse (rse::parse) instead.
Its motors are in engines rather than entries, and its masses are in grams
(initial_mass_g, propellant_mass_g), so multiply those by 0.001 too. Forget it for the
propellant, and the units check catches it: grams read as kilograms make the exhaust velocity
1,000 times too small, and the motor is refused. The check sees only the propellant mass, so a
loaded mass left in grams, with the propellant converted, is accepted as a 1,000-times-heavier
motor.
The file here is Loki Research’s I377. The program prints what it read: 38 mm by 292 mm, 560 g loaded with 250 g of propellant. From the curve it works out 525.8 N·s, an I motor, averaging 377.9 N over 1.39 s. Its effective exhaust velocity, 2103 m/s, is well inside the check’s range.
Three things to know about a motor built from a file:
- The header can be wrong. Whoever made the file typed its size and masses. Of
ThrustCurve.org’s 554 public-domain files, 66 headers give a length more than 1 mm off the
catalog’s, and 5 a diameter more than 0.5 mm off
(data notes,
section 4). This file says 292 mm where ThrustCurve.org’s record says 292.1 mm. The 32 bundled
motors fly their headers’ values too;
cargo xtask motor-catalogrefuses a reload whose case names another diameter than its header (Loft lesson L43: one header said 75 mm for a 54 mm motor). For any other motor, check the header against the motor’s page on ThrustCurve.org. - Where its mass sits is a rough guess.
from_envelopespreads the propellant through the whole case, so the center of mass stays at mid-length as it burns (The whole motor). If you know the grain sizes,SolidMotor::newwithPropellant::Grainsdoes better (Where the propellant is). - Its nozzle size is unknown, so its thrust is not corrected for altitude (Thrust at altitude).
Putting it in a rocket
A rocket design keeps its motors in configurations: named sets of motors, at most one in each
motor mount (Motors and configurations). The example adds
one to Valetudo, the rocket of Getting started: a
Configuration with the id from-file,
holding one MountedMotor.
field of MountedMotor | what it holds |
|---|---|
mount | The id of the mount in the design, here motor-mount |
designation | A name, for display |
diameter_m, length_m | The case’s size, in meters. The design checks compare the diameter with the mount’s bore: a motor wider than its mount is an error, and the flight refuses to start, unless a nominal size’s real case fits (a nominal motor in its matching tube). A case that reaches forward past the mount’s top is only a warning |
motor | The motor built above |
delay | The ejection delay chosen, if any. A parachute can fire on it, with the MotorDelay trigger (Recovery) |
Valetudo’s mount is 43 mm inside, so this 38 mm motor fits. Simulation::new(&rocket, "from-file", ...) then flies the configuration by its id: straight up from a 3 m rail, in calm
air, with no parachutes. The rocket weighs 8.82 kg at liftoff, leaves the rail at 17.7 m/s, and
reaches 151.7 m above the pad at 6.09 s. These flight numbers are not validated; see
Accuracy before trusting them.
A design file holds configurations in the same form, as JSON, with the motor written out in full:
see the configurations list at the end of
the Valetudo design.
Every motor lights at launch, t = 0, unless its ignition says otherwise: at a time, after
another motor’s burnout, or after its stage separates (Staging). It then burns on its
own clock from that moment.
Code and sources
Code: hpr_motor (API reference), in its modules curve, class,
motor, grains, mass, delay and catalog. The file formats are described in
.eng files and .rse files.
Each source has a short key in square brackets, used below. A source that can be downloaded is pinned: its address and a checksum of its bytes are in the reference lock file, under the name given, so anyone can fetch the same copy (Checking a claim).
- SP NASA SP-8039, Solid Rocket Motor Performance Analysis and Prediction (1971),
free from NASA, pinned as
nasa-sp-8039. Page notes: nasa-sp-8039-motor-definitions.md. - NAR National Association of Rocketry, Standard Motor Codes, as
archived on 2014-02-05, pinned as
nar-standard-motor-codes. - TC-G ThrustCurve.org’s glossary, pinned as
thrustcurve-glossary. - TC-S ThrustCurve.org’s “Motor Statistics” page, pinned as
thrustcurve-motorstats. - TC-A ThrustCurve.org’s statistics code,
simulate/analyze/analyze.jsin the site’s source at commit577afa6, pinned asthrustcurve3-analyze. It is under the ISC license, a permissive open-source license like MIT, so hpr may read it and run it. - [RP] RocketPy 1.13.0 (MIT),
rocketpy/motors/motor.pyandsolid_motor.py, pinned asrocketpy. Notes: rocketpy-solid-motor.md.
Line numbers below, such as “lines 40–91”, link to those lines of the source.
Conventions
- Time
tis seconds from ignition. - Motor axis: positions are meters along the motor’s axis from the nozzle exit plane (where
the exhaust leaves) toward the forward closure (the motor’s front end), so
+zpoints toward the nose like the body frame (Frames). The design model (from M1.4, the design and mass-properties milestone) places the motor’s nozzle exit in the body frame. - Inertia: every part is symmetric about the axis.
I_ais about the axis andI_tabout a transverse axis, both through the part’s own center of mass (RocketPy’sI_33andI_11). - Motor files keep their own units (mm, kg or g). The model is SI: meters, kilograms and seconds.
Thrust curve
A thrust curve is the motor’s thrust against time, as its file lists it.
- Samples
(t_i, F_i)joined by straight lines, as [RP] reads them (interpolation_method"linear"). Before the first sample the curve starts from(0, 0)whent_0 > 0, as the RASP format specifies (.engfiles) and TC-A integrates. From the last sample onF = 0, that instant included: like any step, the end takes the later value, so at burnout the thrust, the mass flow and the propellant left are all zero together. - Equal consecutive times are a step: the later value holds from that time on. Real files use them for an abrupt burnout. Decreasing times and negative thrust are rejected, and so is a curve with no impulse or no burn time (defined below).
- A sample is delivered when it starts or ends a stretch of the curve that lasts some time
(
t_i < t_{i+1}ort_{i−1} < t_i). The thrust then really takes its value: at its own instant when it starts a stretch, or ever closer to it as time runs up to its instant when it ends one. A sample strictly inside a run of three or more equal times is never delivered: the step jumps straight past it. Nor is the first sample of a step at the curve’s start, or the last of a step at its end. Peak thrust is the largest delivered sample, which is exactly the most the curve’s thrust reaches, and the burn-time crossings below compare only delivered samples with the threshold. For example, times0, 1, 1, 1, 2s and thrusts10, 10, 1000, 10, 0N hold 15 N·s and never exceed 10 N: the 1000 N sample is skipped, the peak is 10 N, and the burn runs from 0 to 1.95 s. Counting the skipped sample would set the 5% threshold at 50 N, above anything the curve delivers, and refuse the curve (ADR-150 decision record, issue #10, the report of the refusal). A one-off run over the surveyed ThrustCurve.org files, with scripts that are not committed, found no file whose statistics change (the ADR-150 decision record has the counts). - Where TC-A differs. Before integrating, ThrustCurve’s code drops leading points below 500 µN and averages points closer than 50 µs (lines 40–91); hpr keeps leading zeros and treats equal times as steps. The results are identical on every bundled curve. ThrustCurve.org held 1712 solid-motor files when surveyed. On the 1710 of them that read, the two agree to 1e-9 except 17 with repeated times, where they differ by up to 1.1% in average thrust. hpr keeps the step because a vertical drop is what the file draws.
- Total impulse: the exact integral of the lines,
I = Σ ½ (F_i + F_{i+1}) (t_{i+1} − t_i)(SP glossary p. 96:I = ∫F dt).I(t)is the same sum up tot, with the partial interval. - Burn time, by the rule of NFPA 1125, the US National Fire Protection Association’s code for making model and high-power rocket motors, as ThrustCurve.org applies it: from the moment the thrust first reaches 5% of peak to the moment it last falls to 5% of peak, each crossing interpolated on its segment (TC-G “Burn Time”; TC-S; TC-A lines 165–203). The peak and the crossings use delivered samples only (above). The last sample’s time is not the burn time (Loft lesson L39: Loft took it as the burn time).
- Average thrust: total impulse over the NFPA 1125 burn time (TC-G “Average Thrust”; TC-A
line 231).
- TC-S says instead “the total impulse during the 5%-defined burn time”. The two differ by the impulse outside the window, a median 0.13% on ThrustCurve’s public-domain curves (data notes). hpr follows the glossary and the site’s code.
- Impulse class: each letter covers twice the total impulse of the one before.
Agoes up to 2.5 N·s,Bto 5,Cto 10, and so on:upper(k) = 1.25 · 2^k N·s, from1/8A(k = −2) toO(k = 15), and on toZby doubling. Upper limits are inclusive: NAR statesCas “5.01 to 10.0 N-sec”. Loft gaveBfor 2.5 N·s (Loft lesson L38).
Propellant consumption
The effective exhaust velocity c is the thrust
divided by the rate at which propellant mass leaves, c = F/ṁ (SP glossary p. 95). Held
constant through the burn, it makes the propellant burned proportional to the impulse delivered
so far: when half the impulse has been delivered, half the propellant is gone. With I the total
impulse, I(t) the impulse delivered by time t, and m_p0 the propellant at ignition:
c = I / m_p0, ṁ(t) = F(t) / c, m_p(t) = m_p0 (1 − I(t)/I)
- This is [RP]
SolidMotor(solid_motor.py:401-418;motor.py:483-524). ThrustCurve’s.rsefiles tabulate their mass column the same way (.rsefiles). - It is an approximation. SP defines
conly instantaneously. Measured specific impulse (I_sp, the same quantity divided by standard gravity) drifts during a burn, as the pressure inside the motor changes and the nozzle erodes (SP p. 14), so real consumption is not exactly proportional. No data published for commercial motors resolves the difference. - All propellant is gone at the last sample. The whole burned mass leaves as exhaust. Inert material that also leaves (bits of liner and igniter) is not modeled.
Where the propellant is
-
Column (
Propellant::Column): a hollow cylinder(R, r, L)(outer radius, inner radius, length) at a fixed center. It keeps its shape and loses density as it burns, soI_a = ½ m_p (R² + r²)andI_t = m_p ((R² + r²)/4 + L²/12). This is [RP]GenericMotor’s model (motor.py:1580-1648, a solid cylinder). -
BATES grains (
Propellant::Grains):Nidentical BATES grains(R, r₀, h₀)(outer radius, starting bore radius, starting height), spacedh₀ + sapart, with a gapsbetween them. Each burns on its bore, the hole along its axis, and on both ends unless the ends are inhibited: coated so that they can’t burn ([RP]solid_motor.py:487-632).- Every grain needs a bore (
r₀ > 0). A solid end burner, a grain with no hole that burns only on one face, shortens without widening, which this regression (the way the grain burns back) doesn’t describe, and neither does RocketPy’s. - Facing ends burn even with no gap, as in [RP].
- Every burning surface recedes by the same depth
x, the web burned so far (the web is the thickness of propellant between the burning surfaces):
V(x) = π (R² − (r₀ + x)²) (h₀ − 2x) ends burning, x ≤ min(R − r₀, h₀/2) V(x) = π (R² − (r₀ + x)²) h₀ ends inhibited, x ≤ R − r₀ N ρ V(x) = m_p(t)Here
Vis one grain’s volume andρthe propellant’s density, so the last line says theNgrains hold the propellant left.-
[RP] integrates
ṙ = −V̇/A_b,ḣ = −2ṙin time, withA_bthe burning area, using LSODA: a general-purpose solver for ordinary differential equations (ODEs), from the Python library SciPy. BecausedV/dx = −A_b, that ODE is the relation above. hpr solves it forxby safeguarded Newton iteration, exactly at any time. That is Newton’s method, which improves a guess using the slope, kept inside an interval known to hold the answer: a step that would leave the interval halves it instead. -
m_p0 = N ρ V(0)comes from the geometry, as in [RP]. -
About the stack’s center, which doesn’t move:
I_a = ½ m_p (R² + r²) I_t = m_p ((R² + r²)/4 + h²/12) + (m_p/N) (h₀ + s)² N (N² − 1)/12The last term sums
m_g d_k²over the grain offsets ([RP]solid_motor.py:724-740,784-789).
- Every grain needs a bore (
The whole motor
-
Dry mass (case, closures, liner, nozzle: everything but the propellant) is one element: mass, center and inertias about its own center.
with_added_dry_massjoins more hardware, such as a retainer (the cap or clip that holds the motor in its mount). It must be positive: at burnout it is the whole motor, and with no mass it has no center. -
The motor is the dry mass and the propellant combined about the instantaneous center with the parallel-axis theorem:
z = Σ m_k z_k / m,I_a = Σ I_a,k,I_t = Σ (I_t,k + m_k (z_k − z)²). This is [RP]Motor.I_11(motor.py:585-666). -
Envelope default (
SolidMotor::from_envelope), when only the motor’s size and masses are known, as from a catalog entry or a motor file’s header:- the propellant is a solid column of radius
D/2filling the length, centered atL/2; - the dry mass (loaded minus propellant) is a thin tube,
I_a = m r²andI_t = m (r²/2 + L²/12), also centered atL/2.
These are crude: the center of mass stays at
L/2throughout. Loft fixed the CG at the midpoint with no inertia of its own (Loft lesson L40); hpr gives the parts inertia, and moves the CG as soon as the dry and propellant centers differ.- ThrustCurve’s loaded mass includes the reusable case (TC-G “Total Weight”: “propellant and case”).
- [RP]
GenericMotor.load_from_engsets its chamber radius to the motor diameter (motor.py:1759-1761), which quadruples ther²inertia terms (and the default nozzle area it derives from that radius). hpr usesD/2.
- the propellant is a solid column of radius
-
Catalog envelope: diameter, length and masses are the curve file’s header values;
cargo xtask motor-catalogrefuses a reload whose case names another diameter than its header (Loft lesson L43: one header said 75 mm for a 54 mm motor). ThrustCurve.org’s own records differ in a few places (the bundled motors).
The effective exhaust velocity is a units check
Both constructors, SolidMotor::new and SolidMotor::from_envelope, refuse a motor whose curve
and propellant mass imply an effective exhaust velocity c = I/m_p outside 200 to 5,000 m/s.
It is a guard against a slip in the units of the propellant mass, not a filter on propellant:
- What it catches. A propellant mass in grams typed where kilograms belong, such as a
.rsefile’spropellant_mass_gpassed unconverted, which movescby a factor of 1,000. Nothing else in the API notices: the 411I175’s envelope typed in millimeters and grams,from_envelope(curve, 38.0, 245.0, 228.9, 437.5), has positive, finite dimensions and a propellant mass below the loaded mass, and an exhaust velocity of 1.8 m/s. It is the grams that give it away. - What it can’t catch. A size in the wrong unit.
cdoesn’t depend on the diameter or the length, and the motor accepts any positive size: the example’s I377 with its size left in millimeters,from_envelope(curve, 38.0, 292.0, 0.250, 0.560), is accepted, at the same 2103 m/s. Only the design checks see a size, and only the one given to theMountedMotor(Checks). A diameter wider than the mount’s bore is an error,motor_wider_than_mount, and the flight refuses to start; within a nominal size’s slack for its narrower case it only warns,motor_tight_in_mount. A case that runs forward past the mount’s top is only a warning,motor_past_mount_top, so a length slip alone still flies. Nor doescinvolve the loaded mass, so a loaded mass in grams passes too. - What it lets through. Every real motor checked. The 1,708 ThrustCurve.org files with a catalog propellant mass run from 236 to 3,031 m/s, with a median of 1,867 and 90% of them between 928 and 2,210. The 32 bundled motors, with their files’ own propellant masses, run from 708.59 m/s (a black-powder C) to 2,651.64 m/s (a K).
- Where it is tight. The lowest real value is 1.2x above the floor. Estes and Quest normally count the delay grain and the ejection charge as propellant, so their small black-powder motors read low; issue #11 records the fallback if one ever falls below 200 m/s.
I here is the curve’s own impulse, uncorrected for air pressure. The reasoning in full, the
table of percentiles, and two figures that were once got wrong are in
the exhaust-velocity note.
Thrust at altitude
A motor pushes harder in thinner air. Its thrust has two parts: the momentum of the exhaust, and a
pressure term, the exhaust’s pressure at the nozzle exit less the air’s, times the exit area:
F = ṁ u_e + (p_e − p_a) A_e (SP eq. 2, p. 3; SP-8039’s unit constant g_c is 1 in SI). Here
ṁ is the mass flow, u_e the exhaust speed at the exit, p_e the exit pressure, p_a the
ambient (air) pressure and A_e the exit area. With the flow unchanged, a curve measured at
reference pressure p_ref gives, at ambient p_a,
F(p_a) = F_curve + (p_ref − p_a) A_e, A_e = π r_e²
- SP doesn’t print this conversion; it follows from eq. 2. [RP]
Motor.pressure_thrust(motor.py:1173-1191) applies the same term. p_refis the air pressure at the static test site, where the motor was fired on a test stand to measure its curve. It is stored with the nozzle (Nozzle::reference_pressure_pa). Motor files and catalogs don’t record it, so a design gives it, or gives none (below). Standard sea-level pressure (101 325 Pa) suits a motor tested near sea level, but it is a guess: for a test at 1500 m elevation (84.6 kPa in the 1976 standard atmosphere), it makes the term 16.8 kPa ×A_etoo large at every altitude.- It holds while the exhaust fills the nozzle to its exit. A nozzle made for high altitude, tested at sea level, can have its flow come away from the nozzle wall (separate), and then it doesn’t (SP pp. 32–34).
- hpr applies it strictly inside the burn,
0 < t < t_end(t_endthe curve’s last time), as RocketPy’s flight does (simulation/flight.py:1936-1956), but only where the curve’s thrust is positive (RocketPy also adds it inside zero-thrust gaps, where nothing flows), and never lets thrust go negative. Without a known nozzle it returns the curve. Commercial motor files carry no exit diameter:.enghas no field for one, and the.rseformat’sexitDiaattribute is always 0 (.rsefiles). So a motor read from a file or the catalog gets no correction. - No reference pressure, no correction. A nozzle may leave
reference_pressure_paempty (None,nullin a design file). The curve is then flown as it is at every pressure, which is what RocketPy does by default: itsMotor(reference_pressure=None)makespressure_thrustzero (motor.py:1188-1189). The designs transcribed from RocketPy’s examples sayNonefor that reason (ADR-021). With the sea-level stand-in, the Valetudo of Getting started, at a 1,400 m site, carried about 23 N of thrust (15.7 kPa × 1.47e-3 m²) that RocketPy’s example never flies, and reached 874 m where it now reaches 779 m. A design must say which it means: the key is required,nullfor none, so leaving it out is an error rather than a silent choice. The unit testhpr_motor::motor::tests::pressure_correction_uses_the_exit_areapins both forms and the missing key. - Limits. The full-flow term steps in just after ignition and steps to zero at
t_end(the integrator should treat both as events). In the ignition transient (the first moments, while the pressure inside the motor builds) and the tail-off (the end of the burn, while it falls), the real exit pressure is far from its full-flow value, so the term misstates thrust there. In the tail-off, where the exit pressure falls with the chamber’s, it overstates it: on the 411I175 (a 9.5 mm exit) in vacuum it adds 3.9 N·s in the 0.14 s after the NFPA 1125 burn ends, which delivers 0.45 N·s itself; that is 0.95% of total impulse.
Delays
A motor’s ejection delay is the time from burnout to its ejection
charge, which deploys the recovery. TC-G lists every achievable delay, adjustable ones included.
A plugged motor has no ejection charge, and files mark it P. See
hpr_motor::delay and .eng files for
the markers files use. A 0 is read as its own “zero or plugged” setting, because the RASP spec
says it means ejection at burnout but most files mean plugged. hpr never turns it into an
ejection event by itself: the user has to decide.
Validation
- Catalog (
catalog::tests): every figure the catalog states is its curve file’s own, bit for bit, so whathpr motorslists is what flies, and all 32 are within 1% of the total impulse, average thrust and burn time ThrustCurve.org’s code works out from the same files (the next item). Against ThrustCurve.org’s published values, 1% was the bundling rule, so it held by construction;cargo xtask motor-catalogmeasures it again where ThrustCurve.org’s catalog is saved outside the repository (Loft lesson L42: in Loft, a mis-sourced curve flew about 26% high). Of 554 public-domain curves, 196 passed (data notes, which also list the four motors whose stored values are printed coarser than 1%). Each bundled file is public domain, and its bytes match the SHA-256 checksum (a fingerprint of a file’s exact bytes) recorded when it was downloaded (Loft lessons L41 to L43, on curve licenses, loose impulse checks and wrong headers). - ThrustCurve’s code (
catalog::tests::every_bundled_curve_matches_thrustcurve_statistics_code):validation/oracles/thrustcurve/analyze_stats.jsruns TC-A unchanged on every bundled curve, and writes its results to a fixture,validation/fixtures/motor/thrustcurve-analyze-stats.json. hpr’s impulse, burn window, burn time, average and peak thrust agree to 1.8e-15, so the definitions, not only the 1% rule, are checked. - OpenRocket (
catalog::tests::openrocket_s_total_impulse_matches_every_bundled_curve):validation/oracles/openrocket/motors.pyhands each bundled file to OpenRocket 24.12’s own motor loader and records what it makes of it invalidation/fixtures/motor/openrocket-curve-stats.json, tied to each file by its SHA-256. The two codes are compared on the same bytes, so what is measured is the arithmetic, not two catalogs’ data for one motor. Four numbers are identical, to the last bit of anf64, on all 32 curves: total impulse, peak thrust, the 5%-of-peak burn-time window (OpenRocket’sgetBurnTimeEstimate) and the curve’s whole duration (itsgetBurnTime, which is the last listed time rather than a window). The test holds the impulse to the 0.1% that M2.2c asks for (met with everything to spare) and asserts the bit-for-bit equality besides, so this paragraph cannot go stale while the suite stays green. Both codes integrate the listed points as straight lines, and both prepend(0, 0)to a file whose first point is after ignition (29 of the 32 start between 1 and 40 ms), so the point counts match as well and nothing rounds differently.-
One definition genuinely differs, and is recorded rather than held (ADR-066): the average thrust. Both codes divide by the same 5% window, but OpenRocket’s numerator is the impulse inside it while hpr’s, following TC-A, is the whole curve’s. The tails below 5% are the difference, so hpr’s average is the higher on every one of the 32, by +0.0107% to +0.3147% (median +0.0965%); the test prints all three. Carrying an average thrust between the two codes means saying which numerator it used.
-
Curves outside this repository are held to the same bound. They are not committed, so
cargo xtask orkchecks them on the machine that has them, and fails if one is outside 0.1% (M2.2c2, ADR-067):- The curves the reference library’s designs embed, which are other people’s data. hpr parses each file itself, so these are real checks. All 3 agree to the last bit.
- Every solid curve in the motor database OpenRocket 24.12 ships: 1,288 of its 1,452 motors, the rest hybrids. The survey supplies them to designs that name a curve by digest without carrying it. Both codes integrate the samples OpenRocket has already parsed, so agreement to the last bit proves the hand-off, not two independent readings.
The counts, and what the database motors’ masses leave out, are on the
.orkformat page.
-
- RocketPy (
motor::tests::matches_rocketpy_solid_motor_for_three_bundled_motors): three bundled curves with BATES loads cover grains that burn out radially (the bore reaches the outer wall first) and axially (the burning ends meet first), inhibited ends, and both of RocketPy’s axis directions (positions measured from the nozzle forward, or toward the nozzle).validation/oracles/rocketpy/solid_motor.pywritesvalidation/fixtures/motor/rocketpy-solid-motor.json, and regenerates it byte for byte.- On 203 times per motor, total mass,
I_tandI_aagree with RocketPy within 7.9e-5 of RocketPy’s own values, and the center of mass within 5.8e-6 of the motor length. - Quantities that go to zero are compared against a fixed scale: propellant mass and inertias against their ignition values, grain height against its initial height, centers against the motor length. On that scale they agree within 1e-4. Relative to their own tiny values in the last grams of propellant they differ by up to 39%. That comes from RocketPy: it interpolates between the points its ODE solver stored, and its solver stops slightly early.
- The test holds 0.1% on these scales.
- On 203 times per motor, total mass,
- Files: 1710 of ThrustCurve’s 1712 solid-motor files read, and write back out and read again
with every value identical to the last bit. The other two have times that go backwards
(
.rsefiles). - Unit and property tests (property tests check a rule on many random inputs): exact impulse
integration, burn windows, grain volume inversion, the parallel-axis theorem, and the
impulse-fraction flow integrating to
m_p0.
The example program
This is the whole of
crates/hpr-sim/examples/motors.rs,
line for line the file CI runs. Using a motor walks through what it does.
//! Motors: the thrust curves that come with hpr-sim, and a motor read from a RASP `.eng` file
//! like the ones ThrustCurve.org serves, put in a rocket's motor mount and flown.
//!
//! Run it from anywhere in the repository:
//!
//! ```text
//! cargo run --example motors -p hpr-sim
//! ```
//!
//! The documentation site's *Solid motors* page (`docs/physics/motor.md`, "Using a motor") walks
//! through it. What it prints is kept next to it in `motors.output.txt`, and CI checks that the
//! two still agree (`cargo xtask examples --check`).
#![allow(
clippy::print_stdout,
reason = "the project's lints forbid printing in library code, and this program exists to print"
)]
use std::error::Error;
use hpr_core::geodesy::Geodetic;
use hpr_design::{Configuration, Ignition, MountedMotor, Rocket};
use hpr_motor::catalog::MotorType;
use hpr_motor::{Catalog, ImpulseClass, SolidMotor, eng};
use hpr_sim::{Environment, EventKind, FlightSettings, Rail, Simulation, Termination};
fn main() -> Result<(), Box<dyn Error>> {
// 1. The motors that come built in. Size and loaded mass are each curve file's header values;
// impulse, class, average thrust and burn time are worked out here from each motor's curve.
let catalog = Catalog::bundled()?;
println!("{} motors come built in:", catalog.motors.len());
println!();
let heading = [
[
"", "", "", "", "dia", "length", "loaded", "total", "average", "burn",
],
[
"designation",
"maker",
"type",
"class",
"mm",
"mm",
"mass g",
"impulse N·s",
"thrust N",
"time s",
],
];
for [a, b, c, d, e, f, g, h, i, j] in heading {
println!("{a:<12} {b:<8} {c:<10} {d:<5} {e:>5} {f:>7} {g:>9} {h:>11} {i:>9} {j:>7}");
}
for entry in &catalog.motors {
let motor = entry.bundled_motor()?;
let curve = motor.curve();
let kind = match entry.motor_type {
MotorType::SingleUse => "single-use",
MotorType::Reload => "reload",
_ => "other",
};
let class = ImpulseClass::from_total_impulse(curve.total_impulse_ns())?;
// An entry the catalog gives no loaded mass for would show a dash; every bundled one has
// a loaded mass.
let loaded_g = entry
.total_mass_g
.map_or_else(|| "-".to_owned(), |mass_g| format!("{mass_g:.1}"));
println!(
"{:<12} {:<8} {kind:<10} {:<5} {:>5} {:>7} {loaded_g:>9} {:>11.1} {:>9.1} {:>7.2}",
entry.designation,
entry.manufacturer_abbrev,
class.label(),
entry.diameter_mm,
entry.length_mm,
curve.total_impulse_ns(),
curve.average_thrust_n(),
curve.burn_time_s(),
);
}
// 2. A motor from a file. This is one of the bundled files, built into the program so that it
// runs anywhere. For a file you downloaded, read its text instead with
// `let text = std::fs::read_to_string("path/to/your-motor.eng")?;`
let text = include_str!("../../hpr-motor/data/thrustcurve/curves/5f4294d20002e90000000863.eng");
let parsed = eng::parse(text)?;
for warning in &parsed.warnings {
println!("warning, line {}: {}", warning.line, warning.message);
}
let [entry] = &parsed.value.entries[..] else {
return Err("expected one motor in the file".into());
};
// The file gives the size in millimeters and the masses in kilograms; the motor takes meters
// and kilograms. Only the size needs converting, and the motor can't tell if it isn't: its
// units check looks at the impulse and the propellant mass, not the size.
let diameter_m = entry.diameter_mm * 1e-3;
let length_m = entry.length_mm * 1e-3;
let motor = SolidMotor::from_envelope(
entry.thrust_curve()?,
diameter_m,
length_m,
entry.propellant_mass_kg,
entry.total_mass_kg,
)?;
let curve = motor.curve();
println!();
println!(
"From the .eng file: {} by {}, {} mm by {} mm, {:.0} g loaded, {:.0} g of propellant",
entry.name,
entry.manufacturer,
entry.diameter_mm,
entry.length_mm,
entry.total_mass_kg * 1e3,
entry.propellant_mass_kg * 1e3,
);
println!(
" {:.1} N·s (class {}), {:.1} N average over {:.2} s, {:.1} N peak",
curve.total_impulse_ns(),
ImpulseClass::from_total_impulse(curve.total_impulse_ns())?,
curve.average_thrust_n(),
curve.burn_time_s(),
curve.peak_thrust_n(),
);
println!(
" effective exhaust velocity {:.0} m/s",
motor.exhaust_velocity_m_s()
);
// 3. Into a rocket: a new configuration that puts the motor in the design's motor mount. The
// rocket is Valetudo, the rocket of `first_flight.rs`. Its mount is 43 mm inside, so this
// 38 mm motor fits: the design checks compare the case diameter given here with the bore.
let mut rocket: Rocket = serde_json::from_str(include_str!(
"../../../validation/designs/rocketpy-valetudo.json"
))?;
rocket.configurations.push(Configuration {
id: "from-file".to_owned(),
name: format!("{} from its .eng file", entry.name),
motors: vec![MountedMotor {
mount: "motor-mount".to_owned(),
designation: entry.name.clone(),
diameter_m,
length_m,
motor,
// No ejection delay is chosen: this flight carries no parachutes.
delay: None,
ignition: Ignition::Launch,
failed_tubes: Vec::new(),
}],
});
// Fly it, straight up from a 3 m rail in calm air, with no parachutes.
let site = Geodetic::from_degrees(32.99, -106.97, 1400.0)?;
let simulation = Simulation::new(
&rocket,
"from-file",
Environment::standard(site)?,
Rail::vertical(3.0),
FlightSettings::default(),
)?;
let flight = simulation.run(&mut ())?;
if flight.termination != Termination::GroundHit {
return Err(format!("the flight ended with {:?}", flight.termination).into());
}
let sample = |kind| {
flight
.event(kind)
.map(|event| event.sample)
.ok_or(format!("the flight has no {kind:?}"))
};
let (liftoff, rail_exit, apogee) = (
sample(EventKind::Liftoff)?,
sample(EventKind::RailExit)?,
sample(EventKind::Apogee)?,
);
println!();
println!(
"Valetudo on the {}: {:.2} kg at liftoff",
entry.name, liftoff.mass_kg
);
println!(
" leaves the 3 m rail at {:.1} m/s",
rail_exit.cg_velocity_enu_m_s.length()
);
println!(
" apogee {:.1} m above the pad, at {:.2} s",
apogee.height_above_ground_m, apogee.time_s,
);
println!("Not yet validated: see the Accuracy page before trusting these numbers.");
Ok(())
}
Aerodynamics
In short
- What it models: the air’s forces on a rocket: the normal force (the sideways push when flying at an angle to the airflow) and the center of pressure (where it acts) from Mach 0 to 5, drag over the same range, and the rolling moment from canted fins and the roll rate. A flight can also take another program’s drag, or its normal force and center of pressure, in place of hpr’s own (The normal force from RASAero II).
- Sources: Barrowman’s 1966 report, 1967 thesis and Centuri TIR-33 (1970), the basis of Barrowman’s method; supersonic linear theory for fins past Mach 1; for drag, mainly Niskanen’s 2009 OpenRocket thesis, with Stoney’s 1961 NASA measurements of noses through Mach 1, and MIL-HDBK-762 (1990) for boattails faster than sound; for tube fins, Weissinger’s ring-wing formula and Fletcher’s 1957 NACA measurements.
- How well it is validated:
-
Faster than sound, drag is only partly validated, and it misses both ways. It reads high against a wind tunnel, most of all for thin, sharp fins; the body alone reads a little low against a handbook’s worked example; and a rocket with a short, steep boattail reads 5% to 15% low against RASAero II. Treat a supersonic flight’s drag, and its apogee, as rough. The bullets below give the numbers.
-
Against OpenRocket, faster than sound, hpr’s supersonic pressure drag (mostly wave drag) is about twice OpenRocket’s, on the one supersonic flight compared, read from OpenRocket’s per-component output but not kept as a record. Which is right is open (#222, a supersonic flight).
-
Base drag under power is unvalidated. hpr takes the burning motor’s area off the base (Niskanen); OpenRocket keeps the whole base. Neither rule has been checked against a measured flight. On one private supersonic flight the choice moves the apogee by about 24 percentage points (#222, Drag).
-
A boattail’s own drag faster than sound against 58 readings of 20 measured boattails of 3° to 10°, Mach 1.2 to 3.12: −21.9% to +28.3%, within 0.0123. Through Mach 1 it reads low, and under Niskanen’s subsonic rule long boattails get almost nothing. Steeper ones in a thick boundary layer read high: +26.4% to +54.2% for 16°. The drag of the base behind a boattail is within 0.0102 of 12 measured bases, which behind a small base can be 40% of its own drag (Boattails faster than sound).
-
Drag reads high against a wind tunnel. Against NASA’s wind-tunnel tests of the Arcas Robin sounding rocket, Mach 0.6 to 4.63, on the forebody only (the models’ bases sat on a sting), every reading is high and 2 of 44 are within 10%, both at Mach 1.0 with the fins on. With the fins on, from Mach 1.5 up, hpr reads +39.4% to +154.0% high: the fins take a blunt edge’s formula. With the fins off it reads +13.5% to +24.1% from Mach 1.5 and +12.0% to +54.1% below, most of it the models’ 15° boattail, which hpr over-predicts in a boundary layer thicker than the boattail is deep. hpr’s base drag behind a plain cylinder has been checked against no measurement faster than Mach 0.3 (Drag against the Arcas Robin wind tunnel).
-
Drag reads low against RASAero II past Mach 1.6, missing M1.8’s 10% there. The curves labelled RASAero in RocketPy’s example rockets don’t record their fins or surface finish, so hpr uses stated guesses for them. Against Calisto’s, the one real RASAero II export, hpr is within 10% at every Mach number up to 0.8, at 3 of the 7 between, and at 8 of 17 from 1.2 to 2.0, where it reads −14.9% to −5.1%, lowest at Mach 2. Other plausible fins bring 14 to 17 of the 17 within 10%, though none puts every row of every band within it; before hpr modeled the boattail’s wave drag it read −29.8% to −24.4% there (Drag against RASAero II through Mach 2).
-
Drag at Mach 0.3 against the same curves: four of seven cases within 10%. Cavour under power (motor burning) is −18.3%, cause open, and Valetudo’s table is 1.44 times its own OpenRocket export.
-
Against MIL-HDBK-762’s worked example, a rocket whose drag the U.S. Army’s handbook calculates term by term with every input known, the fins left out: 6 of 12 Mach numbers within 10%; hpr reads +12.3% to +31.9% from Mach 0.9 to 1.2 (the nose and the base) and −6.0% to −9.6% from Mach 1.6 (friction and the base) (Drag against MIL-HDBK-762’s sample calculation).
-
The normal force and center of pressure at Mach 0 against Barrowman’s worked examples (rockets he calculated by hand), where every center of pressure agrees within 1%, and so does every normal-force slope but his six-fin Recruiter’s: +2.87% high for the rocket and +3.42% for its fins, mostly from a different six-fin rule; and from Mach 0.6 to 4.63 against the same wind tunnel: from Mach 1.5 the slope within +9.4% to −3.3% and the center of pressure within 0.53 calibres (the long model misses the half-calibre target at Mach 1.8 and 2.3, where its body reads high), and between Mach 0.8 and 1.2 both miss (Normal force through Mach 1).
-
The body faster than sound, by the method a flight blends in over 0.3 in Mach from Mach 1.2 at the earliest, on the bodies it covers: against its report’s wind-tunnel measurements of 120 cone- and ogive-cylinders from Mach 3 to 6.28, 117 slopes within ±0.2 per radian and 109 centers of pressure within 0.2 calibres. Against the Arcas Robin wind tunnel’s body alone (its two lengths, the short model and the long), with a pointed nose fitted to its shape and the lip behind its boattail left off (Checking the shock-expansion method):
- Like for like, fitted at the tunnel’s angles with the body lift a flight adds, the body with its boattail reads +3.4% to +41.0%, within 15% from Mach 3.96 (before M1.8e6 sized body lift and the boattail’s share, 14.9% to 73.2% high). At its 62 angles from 5.5° to 21.7°, 48 are within 15%.
- The method alone at
α → 0against the tunnel’s line, which includes crossflow: the nose and cylinder of the short model within 5% from Mach 1.8 to 2.96, and −15.0% and −18.7% past Mach 3; the long model −13.7% to −26.4% throughout.
hpr’s committed Arcas Robin designs fly that method to their base since M1.8e8; like for like, the short model’s body reads 3.02 to 3.95 per radian from Mach 1.5, where the tunnel reads 2.19 to 4.15 (Normal force through Mach 1).
-
In whole flights in wind, body lift, which RocketPy leaves out, is the largest reason a slow rocket’s drift differs from RocketPy’s (ADR-026). Nothing against a real flight.
-
Roll: the spin that canted fins give, against NASA’s measured roll effectiveness (the rolling moment per degree of cant), is within 5.3% at all 8 readings from Mach 2.3 to 4.63, and 14.3% to 47.8% high at Mach 1.5 and 1.8; the roll damping reads 5.9% to 16.2% low against the one measured set, that of the Basic Finner, a standard finned test body, from Mach 1.5 to 3 (Roll against the Arcas Robin and the Basic Finner).
-
Another program’s normal force, read from RASAero II’s export: every row of Calisto’s export comes back from hpr’s table, and a flight on a table swings in pitch as the equations predict; no real export has flown faster than Mach 0.75 (The normal force from RASAero II).
-
Pods take each pod’s parts on Barrowman’s rules, once per pod. They are checked against hand-worked numbers, and against OpenRocket on six probe designs, within 0.81% of its apogee below Mach 0.81. No measured flight checks them. Both codes leave out the pods’ and the body’s effect on each other’s flow, and hpr a single pod’s off-axis moments (Pods).
-
Tube fins, each tube a ring wing below Mach 0.8: the slope is within 3% of five rings measured in a wind tunnel. No tube fin rocket has been checked against a measurement. On OpenRocket’s example, hpr’s apogee is 6.95% higher, a net gap in the drag, and OpenRocket’s tube-fin drag was refined against real flights, so hpr’s probably reads low. The center of pressure rests on a judgement that moves that example’s margin from 0.29 to 0.79 calibres, against OpenRocket’s 1.87. The gap to OpenRocket is measured on 14 probe designs and pinned by a test; nothing measured says which code is nearer (Tube fins, #228).
-
- What it leaves out: large angles and stall, though a flight uses these models at every angle. Faster than sound (transonic and supersonic), a steep boattail’s drag in a thick boundary layer reads high, and nothing corrects for it; the fins’ drag takes a blunt edge’s formula, which reads far high for thin, sharp fins, and nothing models a thin fin’s own wave drag or the drag where fins meet the body. Faster than sound a flight takes a pointed nose and its cylinder from the method that adds the cylinder’s lift, and a boattail behind them from a measured correlation of boattails of 4° to 9.5°, an extrapolation for steeper ones, which hpr stops reading past the angle where the flow separates (The body faster than sound in a flight). A nose with a vertical tip (power-series, Haack, elliptical) takes a Newtonian cap ahead of the method, checked on a sphere-cone only (Blunt tips), and a lip inside a boattail’s wake carries nothing (A lip in a boattail’s wake). A conical flare flush with the tube ahead of it flies the method too, and ends the run, while its corner’s shock stays attached, checked against one measured flare, where it reads −1.9% and +7.0% at Mach 1.9 and 2.3, +13.4% at 2.96, then +51.5% and +50.4% at 3.95 and 4.63 (What a marched flare is worth); a flare shallow enough to turn the flow almost not at all is read by an older, rougher method instead, which nothing measures (A near-flat flare). A rocket with any other widening shape, or any step, behind the nose (a motor retainer behind a step down counts: the step ends the run) keeps slender-body theory for its whole body at every speed, which reads low past Mach 3 (A step in radius: −8.65% and 1.03 calibres at the threshold). Body lift leaves out the fall in crossflow drag past the critical crossflow Reynolds number (Body lift), and it reads too large at the few degrees a slope is fitted over: the body alone misses the 15% target the milestone set on six of eleven wind-tunnel rows, by +37.7% at worst and within 5% at Mach 3.96 and 4.63 (The body alone, against the 15% target). There are no damping coefficients for pitch and yaw: a flight takes that damping from each part’s own local flow. The roll forcing near Mach 1.5 reads high, and nothing measured checks roll below it (Roll: forcing and damping).
Code and sources
Code: hpr_aero::body (bodies of revolution),
hpr_aero::crossflow (body lift),
hpr_aero::fins (fin sets),
hpr_aero::nose_drag (noses’ drag through Mach 1),
hpr_aero::afterbody (boattails faster than sound),
hpr_aero::shock_expansion (the body faster than
sound), hpr_aero::blunt_tip (a blunt tip’s cap and its
handover),
hpr_aero::table (tables from other programs),
hpr_aero::tube_fins (tube fins) and
hpr_aero::model (a whole rocket’s terms, built from its
Layout). Decisions: ADR-008 (normal force
and center of pressure) and ADR-009 (drag). The milestone M1.5a covers the
subsonic normal force and center of pressure, M1.5b the subsonic drag and override
tables; M1.8a the normal force through Mach 1
(ADR-027); M1.8b1 the drag through Mach 1
(ADR-028); M1.8b3 boattails faster than sound
(ADR-030); M1.8c roll (ADR-031);
M1.8d the normal force from RASAero II
(ADR-032); M1.8e1 the body faster than sound
(hpr_aero::shock_expansion, ADR-033),
flown from M1.8e2,
M1.8e4 the boattail’s share of it, and
M1.8e6 body lift’s size at every speed and the boattail’s
measured share (hpr_aero::supersonic_boattail,
ADR-037). The rest of transonic and supersonic flow arrives with the rest of
M1.8, the supersonic aerodynamics milestone.
A Loft lesson is something learned from Loft, the project that came before hpr-sim: usually a mistake it made, sometimes a check worth keeping. This page names the ones that concern aerodynamics, and the test here that covers each.
Sources:
- [B66] J. S. and J. A. Barrowman, The Theoretical Prediction of the Center of Pressure,
NARAM-8, 1966 (
barrowman-1966-naram8-nakka; the Apogee copy lacks pp. 39–50). - [B67] J. S. Barrowman, The Practical Calculation of the Aerodynamic Characteristics of Slender Finned Vehicles, MS thesis, 1967 (NASA/TM-2001-209983).
- [TIR] J. S. Barrowman, Calculating the Center of Pressure of a Model Rocket, Centuri TIR-33, 1970.
- [N09] S. Niskanen, Development of an Open Source model rocket simulation software, MSc thesis, 2009, chapter 3.
- [TD] OpenRocket technical documentation 13.05 (the thesis revised; a document, not code).
- [G] R. Galejs, Wind Instability: What Barrowman Left Out, Sentinel 39.
- [762] MIL-HDBK-762(MI), Design of Aerodynamically Stabilized Free Rockets, 1990.
- [TN2114] S. M. Harmon and I. Jeffreys, Theoretical Lift and Damping in Roll of Thin Wings with Arbitrary Sweep and Taper at Supersonic Speeds: Supersonic Leading and Trailing Edges, NACA TN 2114, 1950.
- [D4013] J. C. Ferris, Static Stability Investigation of a Single-Stage Sounding Rocket at Mach Numbers from 0.60 to 1.20, NASA TN D-4013, 1967.
- [D4014] C. D. Babb and D. E. Fuller, Static Stability Investigation of a Sounding-Rocket Vehicle at Mach Numbers from 1.50 to 4.63, NASA TN D-4014, 1967.
- [S61] W. E. Stoney, Collection of Zero-Lift Drag Data on Bodies of Revolution from
Free-Flight Investigations, NASA TR R-100, 1961 (
nasa-tr-r-100-stoney-1961). - [J53] J. R. Jack, Theoretical Pressure Distributions and Wave Drags for Conical Boattails, NACA TN 2972, 1953.
- [CS51] E. M. Cortright Jr. and A. H. Schroeder, Investigation at Mach Number 1.91 of Side and Base Pressure Distributions over Conical Boattails without and with Jet Flow Issuing from Base, NACA RM E51F26, 1951.
- [DN54] C. A. de Moraes and A. M. Nowitzky, Experimental Effects of Propulsive Jets and Afterbody Configurations on the Zero-Lift Drag of Bodies of Revolution at a Mach Number of 1.59, NACA RM L54C16, 1954.
- [MJ54] B. Moskowitz and J. R. Jack, Aerodynamics of Slender Bodies at Mach Number of 3.12 … V: Aerodynamic Load Distributions for a Series of Four Boattailed Bodies, NACA RM E54B11, 1954.
- [C57] J. M. Cubbage Jr., Jet Effects on the Drag of Conical Afterbodies for Mach Numbers of 0.6 to 1.28, NACA RM L57B21, 1957.
- [Love57] E. S. Love, Base Pressure at Supersonic Speeds on Two-Dimensional Airfoils and on Bodies of Revolution with and without Fins Having Turbulent Boundary Layers, NACA TN 3819, 1957.
- [C72] W. B. Compton III, Jet Effects on the Drag of Conical Afterbodies at Supersonic Speeds, NASA TN D-6789, 1972.
- [R1135] Ames Research Staff, Equations, Tables, and Charts for Compressible Flow, NACA Report 1135, 1953.
- [SD56] C. A. Syvertson and D. H. Dennis, A Second-Order Shock-Expansion Method Applicable
to Bodies of Revolution Near Zero Lift, NACA TN 3527, 1956 (also NACA Report 1328;
naca-tn-3527-syvertson-dennis-1956). - [S64] J. L. Sims, Tables for Supersonic Flow Around Right Circular Cones at Small Angle of
Attack, NASA SP-3007, 1964 (
nasa-sp-3007-sims-1964-cones-small-alpha). - [J77] L. H. Jorgensen, Prediction of Static Aerodynamic Characteristics for Slender Bodies
Alone and With Lifting Surfaces to Very High Angles of Attack, NASA TR R-474, 1977
(
nasa-tr-r-474-jorgensen-1977). - [WP68] W. D. Washington and W. Pettis Jr., Boattail Effects on Static Stability at Small
Angles of Attack, U.S. Army Missile Command report RD-TM-68-5, 1968
(
washington-pettis-1968-rd-tm-68-5). - [J68] C. M. Jackson Jr., W. C. Sawyer and R. S. Smith, A Method for Determining Surface
Pressures on Blunt Bodies of Revolution at Small Angles of Attack in Supersonic Flow, NASA TN
D-4865, 1968 (
nasa-tn-d-4865-jackson-1968). - [S62] A. Seiff, Secondary Flow Fields Embedded in Hypersonic Shock Layers, NASA TN D-1304,
1962 (
nasa-tn-d-1304-seiff-1962). - [R22] C. E. Rogers, RASAero II Comparisons with ARCAS Center of Pressure (CP) and Drag Coefficient (CD) Wind Tunnel Data, Rogers Aeroscience, 2022 (slides).
- [RAS] C. E. Rogers and D. Cooper, Rogers Aeroscience RASAero II Aerodynamic Analysis and Flight Simulation Program Users Manual, version 1.0.2.0, 2019.
- [F57] H. S. Fletcher, Experimental Investigation of Lift, Drag, and Pitching Moment of Five
Annular Airfoils, NACA TN 4117, 1957 (
naca-tn-4117-fletcher-1957-annular-airfoils). - [H65] S. F. Hoerner, Fluid-Dynamic Drag, 1965, p. 7-13, Ring Foil, and p. 3-12,
Fig. 20, the forebody pressure drag of blunt heads (
hoerner-1965-fluid-dynamic-drag). - [HB85] S. F. Hoerner and H. V. Borst, Fluid-Dynamic Lift, 1985, p. 19-16, Ducted Body
(
hoerner-borst-1985-fluid-dynamic-lift). - [W21] N. Wagner, Theoretical and Experimental Investigation into the Flight of an X-Zylo,
arXiv:2102.02647, 2021, eq. 15, quoting J. Weissinger, Zur Aerodynamik des Ringflügels, 1955
(
arxiv-2102.02647-x-zylo-ring-wing). - [PNK57] W. C. Pitts, J. N. Nielsen and G. E. Kaattari, Lift and Center of Pressure of
Wing-Body-Tail Combinations at Subsonic, Transonic, and Supersonic Speeds, NACA Report 1307,
1957 (
naca-report-1307-pitts-nielsen-kaattari-1957-wing-body-tail-lift).
Conventions
An aerodynamic coefficient is a force divided by the
dynamic pressure q (the pressure of the oncoming air,
½ ρ V², with ρ the air’s density and V the airspeed) and by the reference area. It has no
units, so the same number describes a small rocket and a large one of the same shape. These are
the symbols the whole page uses; each section defines its own as well.
| symbol | meaning | unit |
|---|---|---|
d_ref, A_ref | the reference diameter, and the reference area A_ref = π d_ref²/4 that every coefficient here is divided by | m, m² |
station, X | a station: a position along the rocket, in meters aft of the nose tip (Frames, Design tree) | m |
x_B, y_B, z_B | the axes of the body frame, fixed to the rocket: z_B along its axis toward the nose, x_B the direction around the body that fins are placed from, and y_B square to both | none |
M | the Mach number: airspeed divided by the speed of sound | none |
α | the total angle of attack: the angle between the nose direction +z_B and the rocket’s velocity relative to the air, from 0 to π (Frames). The oncoming air flows the opposite way, so at α = 0 it meets the nose head-on | rad |
φ | the flow roll: which way around the body the air crosses it, measured from x_B toward y_B (Frames) | rad |
C_N | the coefficient of the normal force: the sideways push, square to the axis, in the plane that holds the axis and the airflow | none |
C_Nα | the normal-force slope: how fast C_N grows with α. It is C_N/α for α > 0, and the derivative ∂C_N/∂α at α = 0 ([N09] eq. 3.8) | per rad |
C_Y | the side-force coefficient: a sideways push across that plane, along z_B × the direction the air crosses in. Only sets of one or two fins produce it | none |
| CP | the center of pressure: the station where the normal force acts | m |
The rocket’s CP is the average of its components’ CPs X_i, each weighted by that component’s
slope: X = Σ C_Nα,i X_i / Σ C_Nα,i ([B66] p. 38; [N09] eq. 3.29). The components i are the
nose cones, transitions, body tubes and fin sets; a body tube’s own slope is 0 (Bodies of
revolution, below). A rocket whose slopes add up to zero has no CP.
In code, these are the names the page uses:
| name | what it is |
|---|---|
Layout | a design resolved into placed parts, from Rocket::layout; it holds the reference diameter (reference_diameter_m) |
AeroModel | a rocket’s aerodynamic terms, built from its Layout |
Flow | the conditions a model is evaluated at: M, α and φ |
NormalForce | the result, for the whole rocket or one component: C_N (coefficient), C_Nα (slope_per_rad), the CP (cp_station_m, None when there is none) and C_Y (side_coefficient) |
NormalForce::moment_m | the normal force’s turning effect about the nose tip, divided by q and A_ref: Σ C_N,i X_i, in meters. At an angle of attack the CP is moment_m / C_N; unlike the CP, moment_m is defined even when the forces cancel |
Your rocket’s center of pressure
hpr works out the CP from the rocket’s shape alone, the way Barrowman’s method does by hand. Each nose, transition and fin set gets its own slope and CP (the next two sections), and the rocket’s CP is their weighted average, as above. Your own rocket prints the CP, the center of gravity and the stability margin of an example rocket.
To get it in code:
- Take the rocket’s
AeroModelfrom a simulation withSimulation::aero, or build one from a design withRocket::layoutandAeroModel::new. - Call
AeroModel::normal_forcewithFlow::axial(mach): the air straight along the axis, at a Mach number below 5. The result’scp_station_mis the CP, in meters aft of the nose tip, and itsslope_per_radis the rocket’sC_Nα. AeroModel::components, at the same flow, lists each component’s share, which shows what moves the CP.
What changes it:
-
Speed. Below Mach 1.2 only the fins’ terms change with Mach number; past it the bodies’ can too (The body faster than sound in a flight). Up to Mach 0.8 the fins’ slope grows through the Prandtl–Glauert factor, the classic correction for the air’s compressibility, whose effect grows as the speed nears that of sound (Prandtl–Glauert, under Fins). How much a fin set gains depends on its span, area and sweep. So as the rocket speeds up, the CP moves toward its fins:
- With fins only at the tail, it moves aft.
- With canards (a second fin set near the nose) as well, both sets gain, and the CP can move either way, depending on each set’s shape and place.
- Up to Mach 0.8 hpr keeps each fin set’s own CP a quarter of the way along its mean aerodynamic chord (MAC, a weighted average of its chords). From there it moves aft. The fins’ slope grows up to where supersonic theory starts, Mach 1.2 or later, and falls past it (Fins through Mach 1), so a fast rocket’s CP moves forward again.
Flow::axial(0.0)gives the low-speed CP that Barrowman’s method gives by hand. -
Angle.
Flow::axialgives the small-angle CP. At an angle of attack, body lift adds a force at each body’s side-view centroid, the center of its outline seen from the side (Bodies of revolution, below), and the CP moves with it. -
Stability. A rocket is statically stable when its CP is aft of its center of gravity (CG).
Assembly::mass_propertiesgives the rocket’s mass, CG and inertiatseconds after ignition. A simulation’s assembly comes fromSimulation::assembly.- Its
cg_mis the CG in body axes, so the CG’s station is−cg_m.z. - The CP’s station less the CG’s, divided by
d_ref, is the stability margin in calibres. - Flight metrics gives the margin from the rail exit to apogee or the first deployment, with the air along the axis, at Mach 0 (the static margin) and at the flight’s Mach number (the flight margin), and gives none where the slopes all but cancel.
Bodies of revolution
Nose cones, transitions and body tubes, from the outer profile. A shoulder (the sleeve of a nose or transition that slides into the next tube) is inside the body and adds nothing. For one component:
| symbol | meaning | unit |
|---|---|---|
l | its length | m |
A(x) | its cross-section area x aft of its fore end; A(0) at the fore end, A(l) at the aft end | m² |
V | its volume | m³ |
X_B | its CP, aft of its own fore end (not the body axis x_B) | m |
A_plan | its planform area: the area of its outline seen from the side. Body lift acts at its centroid, the center of that area | m² |
Body lift is the extra push of the air crossing the body at larger angles of attack. It grows with
sin² α, so it is zero at α = 0 and adds nothing to the slope there. Its size is in
Body lift, below.
| term | formula | source |
|---|---|---|
| slope | (C_Nα)_B = (2/A_ref)[A(l) − A(0)] · sin α/α | [B66] eq. 10, [B67] eq. 3-65, [N09] eq. 3.19 |
| CP, aft of the fore end | X_B = [l A(l) − V] / [A(l) − A(0)] | [B66] eq. 28, [B67] eq. 3-89, [N09] eq. 3.28 |
| moment slope | (2/A_ref)[l A(l) − V] · sin α/α | [N09] eq. 3.25 |
| body lift | C_N = η C_dn (A_plan/A_ref) sin² α, at the planform centroid (Body lift) | [J77] eq. 2.12 |
- A nose with a sharp tip has slope 2. A cylinder has 0 and no CP. A boattail has a negative slope, and the CP formula for a frustum (a cone with its tip cut off), [B66] eq. 44, still holds ([B66] p. 21).
- Radius steps (an extrapolation). A step where one body component meets the next adds
(2/A_ref)ΔAat the joint, the limit of a transition whose length goes to zero, so the body’s total slope is [B66] eq. 10 over the whole body. [B67] p. 18 assumes no discontinuities, so this goes beyond the source; leaving the step out would silently drop its slope (a 27 mm nose base on a 29 mm tube loses 13%). It is reported with the aft component (BodyAero::step_area_m2). A blunt front face gets no term, as eq. 10 gives. The design checks warn about steps (radius_step); the real flow separates there, and the drag buildup counts it as a zero-length shoulder or boattail (Steps in radius, under Drag). Vand the planform come from integrating the real profile (hpr_design::revolve), so ogive, power, parabolic and Haack transitions (Shapes) get their own CP (Loft lesson L9: Loft used the conical transition’s CP formula for every shape). [B66] puts a tangent ogive nose’s CP at 0.466 L instead, withLthe nose’s length: 0.2–0.9% different at fineness 2.8–5.- Slender-body theory’s slope has no Mach term: [B67] p. 18 leaves body compressibility out as a conservative choice, and [N09] p. 22 takes the body’s normal force as the same at all speeds. Faster than Mach 1.2 a flight can use another method (The body faster than sound in a flight).
- Body lift is zero at
α = 0, so Barrowman’s worked examples don’t test it.
Body lift
Changed in M1.8e6 (ADR-037): until then hpr
used Galejs’s constant, C_N = K (A_plan/A_ref) sin² α with K = 1.1 at every speed.
What this covers: the size of body lift, the sideways push of the air crossing a body at an angle of attack. How far to trust it: it is Jorgensen’s method ([J77]) with hpr’s own way of combining two of his figures (below). Against NASA’s Arcas Robin body alone at the 62 angles the wind tunnel plotted from 5.5° to 21.7°, from Mach 1.5 to 4.63, hpr’s normal force with it is within 15% at 48 (34 with Galejs’s constant); where the air crosses the body faster than sound, it reads 1% to 16% high. Below Mach 1 the only check is that body’s slope fitted from −4° to +4°, where body lift adds a little: at Mach 0.6 hpr’s body reads 25% high (41% with Galejs’s constant), against readings the tunnel determines poorly. No real flight checks it: the real flights so far compare heights, not drift, so whether it is better than Galejs’s constant for a slow rocket leaving the rail in wind, where it matters most, is open.
At an angle of attack α the air meets the body partly from the side, at V sin α. Behind a
long cylinder in a cross-wind the air separates, and the drag of that separated flow pushes the
body sideways. Jorgensen sizes body lift from that drag: C_dn, the drag coefficient of an
infinitely long circular cylinder in the crossflow, and η, the ratio of a finite cylinder’s to
an infinite one’s. Both depend on the crossflow Mach number M_n = M sin α, the Mach number of
the air crossing the body; η also on the body’s fineness f,
its length over its largest diameter.
| term | formula or value | source |
|---|---|---|
| body lift | C_N = η C_dn (A_plan/A_ref) sin² α, at each part’s planform centroid | [J77] eq. 2.12, p. 10 |
| crossflow Mach number | M_n = M sin α | [J77] eq. 2.3, p. 8 |
C_dn | 1.20 up to M_n 0.2, rising to 1.334 at 0.5 and 1.985 at 1.0, then falling to 1.266 by 4.8 | [J77] Fig. 1, p. 75 |
η at low M_n, η₄(f) | 0.577 at f = 2, 0.685 at 10, 0.753 at 20, 0.815 at 40 | [J77] Fig. 4, p. 77 |
η with M_n, η₆ | 0.69 at 0; 0.717, 0.804, 0.815, 0.845, 0.994, 0.979, 0.769, 0.910 and 0.937 at 0.4 to 1.2 in steps of 0.1; 0.985 at 1.4; 0.984 at 1.6 | [J77] Fig. 6, p. 78 |
η for fineness f | η = η₆ [η₄(f) + (1 − η₄(f)) r] / [0.69 + 0.31 r], r the most s = (η₆ − 0.69)/0.31 has reached up to that M_n | hpr’s, a judgement |
C_dnis Jorgensen’s value below the critical crossflow Reynolds number, where the air separates from a smooth cylinder early: “C_dn = 1.2” at low speed (p. 15). FromM_n0.6 to 1.2 hpr takes the points Jorgensen marks as extrapolated from NASA Ames wind-tunnel data, and from 1.4 his curve through the supersonic experiments.η. (Jorgensen’s; the shock-expansion method below usesηfor something else.) Fig. 4 givesηagainst length for cylinders measured only at low speed. Fig. 6 gives howηgrows toward 1 as the crossflow speeds up, but only for the two bodies (fineness 10 and 12) it was computed from: Jorgensen divided theη C_dnthose bodies’ measured normal force gives (his Fig. 5) by Fig. 1’sC_dn. For any other fineness hpr scales Fig. 6’sηby how much Fig. 4 changes it for the body’s length, and lets that scaling fade by the sharesFig. 6 has risen toward 1. The share it uses,r, never falls back: Fig. 6 dips atM_n= 1 only because Jorgensen divided by Fig. 1’s peak there, not because length counts again. BelowM_n0.8, where Fig. 6 only rises, this isη = η₄ + (1 − η₄) s; past 0.8 every fineness takes Fig. 5’sη C_dnto within 0.3%. That rule is hpr’s; it gives Fig. 6 back for a body of fineness about 10.6 and stays below 1.- Sampled, not smoothed. Near
M_n= 1 Fig. 1’sC_dnpeaks and Fig. 6’sηdips, each steeply. hpr reads both at the eleven crossflow Mach numbers Jorgensen computed Fig. 6 at and interpolates between them, so their product is his ownη C_dnthere: within 3% of his Fig. 5 at all ten of its points from 0.5 to 1.6 (testthe_product_follows_figure_5).
A worked example. NASA’s short Arcas Robin model without fins, fineness 18.18, has a planform
of 20.79 times its cross-section. At Mach 2.3 and α = 12.56°, M_n = 2.3 × sin 12.56° = 0.50.
Fig. 4 gives η₄ = 0.742; Fig. 6 gives η₆ = 0.804, so s = (0.804 − 0.69)/0.31 = 0.368 and
η = 0.742 + 0.258 × 0.368 = 0.837. With C_dn = 1.334, η C_dn = 1.117, and body lift is
1.117 × 20.79 × sin² 12.56° = 1.10. Adding the attached-flow part (the method’s slope at small
angles, without crossflow), hpr’s body gives C_N 1.632 there; the tunnel measured 1.630. This
point happens to agree closely; across the 62 points the spread is −22.3% to +31.7% (table below).
What changes in a flight. At the low crossflow speeds of most flights η C_dn is 1.2 η₄(f):
0.82 at fineness 10, 0.90 at 20, 0.95 at 30, against Galejs’s 1.1. A slow rocket leaving the rail
in wind feels it most (Validity and open questions). The tunnel
points where hpr reads high, the air crossing the body faster than sound, are at 12° to 21° from
Mach 2.3 up; a flight meets such angles that fast only if it is unstable or hit by a strong gust.
How it was checked. The Arcas Robin wind tunnel measured the body alone from −5° to 21° at six
Mach numbers (TN D-4014; the points above +4° were read for this milestone
into
arcas-robin-high-alpha.json).
arcas-robin-crossflow.json
compares hpr’s body at each point, with the pointed nose fitted to the tunnel’s and the lip left
off (Checking the shock-expansion method):
crossflow Mach number M_n | points | hpr’s C_N against the tunnel’s | with Galejs’s K = 1.1 |
|---|---|---|---|
| under 0.45 | 21 | −6.9% to +31.7% | +1.9% to +61.4% |
| 0.45 to 0.95 | 25 | −22.3% to +13.0% | −21.1% to +17.0% |
| 0.95 and over | 16 | +0.8% to +16.5% | −21.7% to −7.1% |
At the lowest crossflow speeds the readings can’t say how much of what is left is body lift and
how much the slope at α → 0 (Checking the shock-expansion method).
Where the air crosses faster than sound, hpr’s η C_dn (1.45 to 1.61) is above what the tunnel’s
points need (1.26 to 1.54). Jorgensen’s η C_dn was worked out from measured normal force less
his own attached-flow term, sin 2α cos(α/2); hpr pairs it with its own, sin α times its slope,
which is 8% larger at 20° and, faster than sound, carries the method’s slope for the nose and
cylinder, 2.55 to 3.40 against slender-body theory’s 2. Two cautions from Jorgensen: his η comes from cylinders measured “only at
very low subsonic Mach numbers” (p. 17), and the tunnel tripped its boundary layer, which can
move the flow past the critical crossflow Reynolds number, where C_dn falls to “between about
0.15 and 0.30” (p. 15).
What it leaves out. That fall past the critical crossflow Reynolds number (about 2 × 10⁵,
[J77] Fig. 2), which Jorgensen computes only for illustration; roughness and fins’ effect on the
body’s crossflow; and Jorgensen’s own attached-flow term, sin 2α cos(α/2) in place of hpr’s
sin α.
Galejs’s constant stays available for comparison
(BodyLift::Galejs, with
AeroModel::with_body_model):
[G] cites Hoerner’s 1.1 to 1.5 and fitted 1.0 to his own data.
Bodies faster than sound
Flown faster than sound for a pointed nose and its cylinder (see
The body faster than sound in a flight, below). This is
M1.8e1’s method, the second-order shock-expansion method as a
tested library model. Against its report’s wind-tunnel data, 117 of
120 slopes are within 0.2 per radian. On the Arcas Robin’s body it reads from 16% high at Mach 1.5
to 27% low past Mach 3 at α → 0, a comparison that leaves out the
body lift a flight adds; compared the way the tunnel measures, the
body reads high
(Checking the shock-expansion method, under
Verification). It needs a pointed tip; a blunt or vertical one (power-series, elliptical and
Haack noses) takes a Newtonian cap ahead of it (Blunt tips, below).
Slender-body theory, above, gives a pointed nose a slope of 2 and a cylinder none, at any speed. Faster than sound that is too little. The air speeds up around the shoulder where the nose meets the cylinder, and its pressure then recovers along the cylinder toward the free stream’s. At an angle of attack it recovers unevenly around the body, so the cylinder carries lift too, more the longer it is. NASA measured the Arcas Robin’s body alone at 3.9 per radian at Mach 3.96, where slender-body theory gives 2 for its nose.
hpr computes that lift by Syvertson and Dennis’s second-order shock-expansion method ([SD56]),
for a body with a pointed tip and supersonic flow everywhere on it, as the slope at small angles
of attack (α → 0). The older generalized shock-expansion method holds the pressure constant
along each straight piece of the profile; the second-order method also carries the pressure’s
rate of change across each corner, so the pressure can recover along a piece:
- The tangent body. The profile becomes straight elements, each tangent to it: the first at the tip, the rest at equal steps along a curved nose (ten per curved piece, the report’s own choice), one per cone or cylinder. Where two elements meet is a corner.
- The tip is a cone, so its flow is exactly a cone’s, found by integrating the Taylor–Maccoll equation (the exact equation of supersonic flow over a cone) from the shock to the surface ([R1135] eq. 177). The shock must be attached, touching the tip, which holds up to a half-angle that grows with the Mach number. For cones under 0.03°, where that equation can’t be integrated, hpr takes slender-cone linear theory, blended in up to 0.06°.
- Around each corner the flow turns by a Prandtl–Meyer expansion.
- Along each element the pressure relaxes from its value behind the corner toward the pressure on the element’s tangent cone: the cone, pointed into the oncoming flow, whose surface has the body’s local slope there. The lift per unit length relaxes the same way, toward that cone’s.
- The slope and CP follow by adding the lift over the body.
| step | formula | source |
|---|---|---|
| pressure along an element | p = p_c − (p_c − p₂) e^(−η), η = (∂p/∂s)₂ (x − x₂) / ((p_c − p₂) cos δ₂) | [SD56] eqs. 8, 9 |
| gradient behind a corner | (∂p/∂s)₂ = (B₂/r)(Ω₁/Ω₂ sin δ₁ − sin δ₂) + (B₂Ω₁/B₁Ω₂)(∂p/∂s)₁, B = γpM²/(2(M² − 1)) | [SD56] eqs. 4, 6 |
| gradient at an element’s end | (∂p/∂s)₃ = (p_c − p₃)/(p_c − p₂) · (∂p/∂s)₂ | [SD56] eq. 10 |
| lift per unit length | Λ = (1 − e^(−η)) tan δ · (dC_N/dα)_tc + (λ₂/λ₁) e^(−η) Λ₁, λ = 2γp / sin 2μ | [SD56] eqs. 5, 19 |
| slope and CP | C_Nα = (2π/A_ref) ∫ Λ r dx, x_cp = ∫ Λ r x dx / ∫ Λ r dx | [SD56] eqs. 14, 21 |
Here δ is an element’s angle to the axis, p the pressure over the free stream’s (the
undisturbed air ahead of the rocket), M the Mach number at the surface, r the radius at the
corner, Ω how much a thin tube of flowing air widens as its Mach number rises (its area over
its area at Mach 1, [SD56] eq. 7), μ the Mach angle asin(1/M), and (dC_N/dα)_tc the slope
of the tangent cone, digitised by hand from the report’s Fig. 2 into a table and continued past
that chart’s 24° by Sims’s own tables of the same theory, to 30° (ADR-042,
cone_normal_force_slope).
For a worked example with numbers, see
Checking the shock-expansion method.
- A cylinder’s tangent cone is the free stream, so its lift decays to nothing along it.
- A boattail has no tangent cone. The report’s footnote 8 takes the free stream’s pressure and a slope of 2, “reasonable results for bodies having moderate amounts of boattail”. The method does the same, but a measurement says it takes too little lift off: since M1.8e6 a flight gives a boattail a measured share instead (The body faster than sound in a flight).
- Where the method stops. The relaxation holds only where the gradient behind a corner points
toward the tangent cone’s pressure (
η ≥ 0, [SD56] p. 13). The report states that as a condition and doesn’t say how it went on where it fails, near a sharp tip at high Mach number. hpr’s own reading is to reduce such an element to the older generalized method, which the report says the equations become atη = 0: the pressure stays as it is along the element and no gradient passes to the next corner. On the report’s fineness-3 ogive at Mach 5.05 that departs from its values (issue #81: the method’s limit near a sharp tip). hpr reads an element that way wherever it has a tangent cone of its own, behind the nose as well as on it (A near-flat flare). A cylinder’s tangent cone is the free stream and a boattail’s is footnote 8’s, so neither is a solution of that element’s own flow: one of those that would need reducing is refused instead, and the whole body keeps slender-body theory (issue #123: a cylinder’s or a boattail’s reduced element). - Mach number over nose fineness. The report states the method for 0.4 to 2; hpr doesn’t enforce it (the report’s own Mach 6.28 rows are at 2.09, and the Arcas Robin at Mach 1.5 is at 0.36).
- Its range. The report states the method for Mach number over nose fineness from 0.4 to 2, within ±0.2 per radian and ±0.2 calibres of its measurements. Fig. 2 covers Mach 3 to 10; below Mach 3 hpr holds the Mach 3 curve, an assumption. The tip’s shock must be attached, and the profile continuous. Tangent cones run to 30°: to 24° from Fig. 2, and from there to 30° from NASA SP-3007’s tables of the same theory, which agree with the chart to 0.0021 per radian where both cover the same angle (ADR-042). A cone steeper than 30° is refused, and the whole body then keeps slender-body theory.
- What it leaves out: the crossflow lift that grows with
sin² α(Body lift, above), and anything viscous. It is the slope at small angles only.
The body faster than sound in a flight
What this covers: how a flight uses the method above, from Mach 1.2, and a boattail’s measured
share. How far to trust it: as far as the method’s own checks above, for a pointed nose and
cylinder. A boattail behind them takes Washington and Pettis’s measured increment ([WP68]), which
their data give within about 15% for conical boattails of 4° to 9.5°; a steeper, shorter, longer
or narrower one, like the Arcas Robin’s 15°, is an extrapolation, as is a transition that isn’t
conical (it takes the same correlation from its length and radii). A long boattail reads the
curve near zero argument, which comes from the report’s lowest supersonic runs. Past 16°, where the flow
separates, hpr stops reading the correlation any steeper and holds it there
(A steep boattail reads the correlation no steeper than 16°,
issue #90: how steep a boattail the correlation should cover);
nothing measures what such a boattail really carries, and the choice is worth 0.67 to 1.35
calibres of center of pressure at 30°, most at the lowest speeds. A tube behind the boattail takes the method’s decay of its expansion, which no
measurement here checks. A blunt or vertical nose tip takes a Newtonian cap ahead of the method
(Blunt tips), an extrapolation from spherical caps, and a lip inside a boattail’s
wake rides along carrying nothing (A lip in a boattail’s wake). A
conical flare flush with the part ahead of it flies the method while its corner’s shock is
attached, and is read drawn out where it is not; one measured flare has been put beside it
(What a marched flare is worth), and the drawn-out half still
has none. A rocket with any
other widening shape, or any step, behind the nose gets nothing from the method and keeps
slender-body theory, which on the Arcas Robin’s body reads 15% to 50% below the tunnel faster than
sound. The join between the two models is a
judgement, not a measurement, and no validation flight goes past Mach 1.06 (Prometheus, the
fastest; see its max_mach rows in the
validation report),
so no flight checks it yet.
Which model your rocket gets. Every body takes body lift at every speed. Past Mach 1.2, a rocket whose first body is a nose (pointed, or with a blunt or vertical tip that the cap covers), followed only by tubes of its radius and boattails (and tubes behind those), takes the method below for those parts, and each boattail its measured share. A lip wholly inside a boattail’s wake rides along, carrying nothing; one only partly in the wake gets the method in the wake’s own proportion, and slender-body theory for the rest (A lip in a boattail’s wake). A conical flare flush with the part ahead of it joins the run as well, and ends it (A flare through the method); a widening part behind a boattail is a lip, not a flare, and keeps the lip’s rule. Anything else (a step, a widening shape that is not a cone, a motor retainer behind a step down (the step itself ends the run) or a nose steeper than the cap’s handover all the way to its base) keeps slender-body theory for its whole body at every speed.
Where that choice still jumps. It is one choice for the whole body, so wherever it turns on a
threshold, a rocket either side of that threshold gets two different models, and the difference is
the whole body’s, not the part that changed. At Mach 3 and 4°, each remaining threshold is worth
this much. The first four rows are measured on two test bodies: the flare behind a boattail on a
finless body, so its share is of a body’s normal force alone, and the other three on the tests’
straight rocket, which keeps its four fins, so the two kinds of share don’t compare. The last
column says who owns it: open means an issue with no
milestone behind it, queued a milestone on the roadmap, and no longer a switch a threshold
since removed, kept here because its size is the measured cost of the model it replaced
(issue_87s_switches_are_this_big pins the first four,
a_lip_in_a_boattails_wake_carries_nothing the lip’s two, and
a_near_flat_flare_marches_every_row_and_the_fallback_is_still_measured the last):
| drawing this | normal force | center of pressure | who owns it |
|---|---|---|---|
| a step in radius, past a billionth of the local radius | −8.65% | 1.03 calibres | queued: M1.14d: supersonic accuracy, these switches first, and M1.14h above Mach 2.5; issue #87: a step has no model of its own |
| a flare behind a boattail and too long for its wake, however small | −27.5% | 0.29 calibres | queued: M1.14d: supersonic accuracy, these switches first, and M1.14h above Mach 2.5; issue #120: a lip longer than its wake |
| a pointed tip steeper than the cone tables’ 30° | −7.7% | 0.81 calibres | queued: M1.14d: supersonic accuracy, these switches first, and M1.14h above Mach 2.5; issue #121: a tip past the cone tables |
| a vertical tip steeper than the cap’s handover to its base | −7.0% | 0.64 calibres | queued: M1.14d: supersonic accuracy, these switches first, and M1.14h above Mach 2.5 |
| a lip leaving its boattail’s wake by rising or by sitting back | −29 to −34% | 0.93 to 1.97 calibres | no longer a switch: the rise is weighed, ADR-041 |
| a lip longer than its boattail’s drop in diameter, however little it rises | −33.0% | 1.77 calibres, forward | queued: M1.14d: supersonic accuracy, after the four switches above, since it reads less stable; issue #120: a lip longer than its wake |
| a near-flat flare, 0.03816° to 0.05882° at the table’s top rows, which the march used to refuse | −8.3% | 1.16 calibres | no longer a switch: the element is read by the generalized method, ADR-050 |
In the first four rows the center of pressure moves aft when the method is lost, so a rocket that trips one reads more stable than one that doesn’t. The two lip rows and the near-flat-flare row go the other way: on a body whose tail takes lift off (a boattail’s wake, or a flare the method reads lower than slender-body theory does), slender-body theory puts the center of pressure forward of the method’s, so losing the method there reads less stable. Which way it goes depends on the body; what is reliable is the size.
The lip row about length is a switch the wake’s grading does not cover: a lip is only sheltered if it is no longer than the boattail’s drop in diameter, which is the wake’s own scale, and that length is a threshold, not a ramp. It is the one this page’s measurements use to take a rocket off the method without changing a radius or an angle.
The first lip row is no longer a switch: it is spread over the band the wake grades, as above. The second still is one. Nor is the last: since M1.8e19 the march reads that flare’s element rather than refusing it, and what is left where it used to switch is the loading’s step at the corner’s crossing: +0.129% and 0.0051 calibres on this rocket, but larger on other bodies and on longer flares, in A near-flat flare. The two tips moved rather than went: a pointed tip’s edge is the cone tables’ 30° since M1.8e11, and a vertical tip’s is the handover’s cap, which stands at 24° for the reason in What the cap is worth.
The flare went (M1.8e17, ADR-047): a conical flare in the free stream now flies the method, and the boundary where its shock detaches has nothing jumping across it; see A flare through the method below. The second row is what is left of it, a flare behind a boattail, which is a lip in the boattail’s wake rather than a flare in the free stream and keeps its own rule. The last row is what that milestone turned up on the way, and M1.8e19 closed it: a band of near-flat flares, a third of a millimeter tall, whose one element the march reduces to the generalized method.
The step stayed, and M1.8e15 says why: the march needs a profile without a jump in it, so a step needs a model of its own rather than a decision about an existing one, and the obvious fix turned out to cost more than it saved. What that milestone did measure is in A step in radius below. Its threshold is finer than it sounds: a billionth of the radius, a few hundredths of a nanometer on a 54 mm body, so any step a person would draw is past it, and a rocket whose shape sits near one of these thresholds is worth checking on both sides.
What a flight takes. The method covers the nose, when it is the first body (a blunt or vertical
tip behind its Newtonian cap), the body tubes straight behind it at the same radius,
and boattails
(transitions that narrow toward the tail) and tubes behind those, with no step between them
(M1.8e4), and a conical flare, which ends the run
(M1.8e17). It flies only if nothing else behind them
changes the radius: no step, and no widening shape but that flare. Mixing the method’s nose and cylinder with slender-body theory’s
boattail would put the center of pressure further off than slender-body theory alone, so the
boattail takes a supersonic share too, measured (below). Each covered part gets its own share,
so the flight’s pitch damping still comes from each part’s own local flow. That flow is taken at
one station per part; the part’s force and its moment about the nose tip
are the method’s either way. A nose or cylinder takes that station at its share’s own center of
pressure. A boattail’s share is usually negative, and so is a tube’s behind it: the method carries the
boattail’s expansion down the tube, where it fades out over several calibers, so a long tube can
lose as much as the short boattail. On the finned rocket of the tests (measured by hand, not
pinned), a 5.7° boattail 0.05 m long takes 0.284 per radian off at Mach 2 (footnote 8 took 0.117)
and the 0.3 m tube behind it 0.210. Such a share could cross zero as the Mach number changes, and its center of
pressure would then run off to infinity. So these parts take their local flow where slender-body
theory does, on the part: a boattail at its slender-body center of pressure, a tube at its
body-lift station. Only the damping feels this: on the test rocket at Mach 2 the tube’s share acts
at 1.040 m but its flow is taken at about 1.15 m, so with the center of mass near 0.7 m that
part’s (negative) damping reads about 30% large. Body lift, the sin² α term, is unchanged.
The boattail, measured. Washington and Pettis ([WP68]) mounted the aft section of a wind-tunnel model on its own balance and measured it with a conical boattail and as a plain cylinder of the same length, Mach 1.75 to 4.5, and a whole model with and without one from Mach 0.8 to 1.5. The boattail’s increment in slope collapses onto one curve:
ΔC_Nα / [1 − (D_B/D)²] = F(√(M² − 1) / (L_B/D)), per degree on the cylinder’s area,
with D the diameter ahead of the boattail, D_B its base diameter and L_B its length
([WP68] Fig. 5, p. 8, read by hand into
WP_SLOPE_PER_DEG to about
±0.02 per radian). The increment acts about halfway along the boattail, from 43% of its length at
Mach 2 to 64% at 4.5 ([WP68] Fig. 6, p. 9). A flight gives a boattail the share the method gives
a cylinder of the boattail’s length and fore diameter in its place, plus that increment at that
center of pressure (test the_boattail_takes_washington_and_pettis_increment). Slender-body theory
gives the same boattail 2[(D_B/D)² − 1] at every speed, which is Fig. 5’s own subsonic line;
faster than sound the measured increment is less than half of it at the Arcas Robin’s speeds
(0.24 to 0.47), and footnote 8’s less again, though the curve is not always below that line:
across Fig. 5 it runs from 0.23 to 1.58 times it, passing it at an argument of 0.635, near Mach 1.
A steep boattail reads the correlation no steeper than 16°. Washington and Pettis measured
boattails of 4° to 9.5°, where the flow follows the surface. Past about 16° it doesn’t: the drag
buildup already treats a boattail as separating from there
(Boattails faster than sound, Cubbage’s steepest attached
boattail). Nothing measures what a separated boattail’s normal force then does, so hpr reads the
correlation at the steepest angle where the flow is still attached: a boattail past 16° takes the
increment of one of the same radii drawn out to 16°, and its center of pressure stays on the
real boattail (ADR-040,
issue #90: how steep a boattail the correlation should cover).
The Arcas Robin’s 15° boattail is untouched; Calisto’s 18.4° reads its correlation as a 16° one.
The increment is continuous in the angle, so a rocket doesn’t jump as its boattail is drawn
steeper (test a_separating_boattail_reads_the_correlation_at_its_steepest_measured_angle).
Why hold it rather than let it fade. The two honest limits for a separated boattail are the correlation held at 16°, and nothing at all, the body behaving as though the boattail were a cylinder, since a separated surface no longer turns the flow. hpr takes the first. The increment is negative, so it takes lift off the tail: holding it keeps the center of pressure forward, and letting it fade to zero would move the center of pressure aft and make a steep boattail look more stable than anything measured. On the tests’ rocket (an ogive nose, a tube, and a 30° boattail), the body’s center of pressure sits this much further aft if the increment fades away than if it is held:
| Mach | 1.5 | 2 | 3 | 4.63 |
|---|---|---|---|---|
| calibres between the two rules | 1.35 | 0.91 | 0.75 | 0.67 |
That is the size of the doubt, and it is largest where a hobby rocket spends its supersonic flight:
a boattail steeper than 16° is worth two thirds of a calibre at Mach 4.63 and a third of a calibre
more than one at Mach 1.5. hpr takes the forward end of that range (test
a_separating_boattail_reads_the_correlation_at_its_steepest_measured_angle pins both ends).
There is one more bound, on the holding rather than on the measurement. Reading a longer boattail
walks the correlation’s argument √(M² − 1)/(L_B/D) toward zero, where Fig. 5’s curve comes from
the report’s lowest supersonic runs and rises past Munk’s slender-body line, which the report
plots there for comparison at subsonic speeds (p. 3). hpr does not invent a length and then read
that branch, so the extra the holding takes off stops at potential flow’s
2 (A_aft − A_fore)/A_fore (holding_the_correlation_stops_at_potential_flow).
Be clear about what this does and does not do. A boattail’s read at its own length is never
clipped, wherever it sits; that is the correlation as published, and a genuinely long boattail
reads the same near-Mach-1 branch with no bound at all. A 4° boattail to 0.6 of the radius reads
1.29 times Munk’s line at Mach 1.5, and hpr flies it. Only the length the 16° hold invents is
capped. The bound bites when the aft radius is under about 1 − √(M² − 1)/1.11 of the fore
radius: two fifths at Mach 1.2, a quarter at 1.3, a twentieth at 1.45, nothing much above Mach
1.49, and only where the method’s table has started, which on such shapes it barely has.
How much that is worth is measured rather than argued, by sweeping boattails of 16° to 53.6°
narrowing to between a thousandth and three tenths of the fore radius, and reading their shares
back out of the table (what_the_potential_flow_bound_reaches). Two things come out.
- At the table’s rows a boattail never takes off more lift than potential flow, except in the sliver described below, where its own read already passes it and the bound never clips that.
- The bound moves a printed coefficient by about 0.060 per radian at most, at 53.5° narrowing to a thousandth of the radius. That is the printed number, after the join’s weight; the holdback on the boattail’s own cross-section is up to about six times larger near the join, where the join is barely open. 53.5° is the steepest boattail the sweep found the method willing to table at all; it refuses 53.6°, and refuses shallower angles than that where the boattail narrows less.
No committed design comes near: the steepest is Calisto’s 18.4°, whose aft radius is 0.685 of its fore radius, where the bound would need under 0.40 even at Mach 1.2.
Two caveats, both small and both real. The bound applies where the shares are computed, at the table’s rows; between rows the table interpolates, so a printed value beside a row of the next kind can sit past potential flow, by up to about 0.003 per radian on the shapes swept. And in a sliver just above the hold’s own angle (the sweep finds it at 16°, 16.5°, 17° and 17.25°), a boattail is deep enough that its own read already passes potential flow, and since the bound never clips a boattail’s own length, the hold does nothing there and the rocket flies an extrapolation nothing measured checks. That window runs a few hundredths of a Mach from where the table starts, so the join is barely open across it, and it closes as the angle or the speed rises rather than at a fixed angle.
How far to trust the 16°. It is Cubbage’s, measured at Mach 0.6 to 1.28 and on drag, and it is used here on the normal force from Mach 1.2 up. A shoulder turns the flow through a Prandtl–Meyer expansion faster than sound, where separation is less likely than transonically, so 16° is if anything early. No measurement of a steep boattail’s supersonic normal force exists to check it. Note too that the two models take opposite consequences from the same angle: separation lowers a boattail’s pressure drag toward the base value, and here it holds the lift the boattail takes off instead of letting it shrink. The argument for that is the stability one above, not a flow one. The angle is also read on each narrowing part’s own chord angle, while the drag merges adjacent narrowing parts into one cone before grading, so a boattail drawn in several parts can be graded differently by the two.
A worked example. The Arcas Robin’s boattail narrows from 2.25 in to 1.308 in over 1.757 in, so
L_B/D = 0.781 and 1 − (D_B/D)² = 0.662. At Mach 2.3, √(M² − 1) = 2.071 and Fig. 5’s
argument is 2.071/0.781 = 2.652, where the curve reads −0.01263 per degree, −0.7237 per radian;
times 0.662 that is −0.479 per radian on the body’s cross-section. Slender-body theory gives
−1.324, footnote 8 −0.103. Across the tunnel’s Mach numbers:
| Mach | Washington and Pettis | footnote 8 | slender-body theory |
|---|---|---|---|
| 1.5 | −0.622 | −0.177 | −1.324 |
| 1.8 | −0.554 | −0.144 | −1.324 |
| 2.3 | −0.479 | −0.103 | −1.324 |
| 2.96 | −0.409 | −0.068 | −1.324 |
| 3.96 | −0.340 | −0.038 | −1.324 |
| 4.63 | −0.314 | −0.026 | −1.324 |
Their models were conical boattails of 4° to 9.5°, 0.82 to 1.18 diameters long, narrowing to 0.72
to 0.86 of the diameter; the Arcas Robin’s is steeper (15°), shorter (0.78) and narrower (0.58),
so its row is an extrapolation. Their points scatter about the curve by up to about 15%. Past the
curve’s end (an argument of 6.1: a short boattail at a high Mach number) hpr holds its last value.
BodyModel keeps footnote 8 for comparison.
A table. One run of the method takes a few milliseconds, too slow for every step of a flight. So the first time a flow faster than Mach 1.2 needs it, hpr runs the method every 0.05 in Mach from Mach 5 down, to the lowest Mach at which it holds, and keeps the results. Between those Mach numbers it interpolates in a straight line. That took about 0.3 s once per rocket in a debug build on the development Mac (measured by hand, for a body the method takes from Mach 1.2); a body whose join starts higher adds about 48 runs for the bisection below. A rocket that never passes Mach 1.2 never pays it.
The join. Write SB for slender-body theory, SE for the shock-expansion method, and M_j for
where the join starts: Mach 1.2, or the lowest Mach at which the method holds if that is higher.
hpr narrows that Mach down between two rows of the table by halving the gap (bisection) until no
smaller step exists in the computer’s numbers. It then runs the method at that Mach and adds the
result as an extra row. The start must be that exact: there the method’s shares climb from zero
like the square root of the distance in Mach, so a start off by δ puts √δ-sized shares in
that row.
So the start moves smoothly with the nose’s shape instead of in 0.05 steps. A cone with a 20°
half-angle (the angle between its side and its axis) joins from Mach 1.341910; each 0.1° steeper,
up to 20.5°, moves the start about 0.0027 later, to 1.355500. From M_j to
M_j + 0.3, each covered part’s slope, moment and station move in a straight line from SB’s to
SE’s:
C_Nα = C_Nα,SB + w (C_Nα,SE − C_Nα,SB), w = (M − M_j)/0.3, clamped to 0 to 1.
Every piece is a straight line in Mach, so nothing jumps. The test
the_supersonic_join_has_no_jump looks at ±1e-9 in Mach on each side of the join’s ends, of
table rows, between rows and at Mach 4.999, and a_blunter_cone_joins_where_the_method_starts_to_hold
does the same for the 20° cone, whose join starts higher.
the_joins_start_moves_with_the_nose_not_in_steps pins that cone’s start and the 20.5° cone’s to
1e-6, checks the start is off the grid, that the cylinder’s share at the start is under 1e-5 per
radian (about 1e-7), and that the start moves by less than 1e-7 when the cone steepens by a
millionth of a degree (2.7e-8). a_boattailed_body_flies_the_method_without_a_jump does the same
±1e-9 probe for the finned rocket with its boattail and tail tube. Mach 1.2 to 1.5 is a judgement: below Mach 1.2 the flow over the nose is
transonic, which the method doesn’t cover, and Mach 1.5 is the
lowest Mach at which NASA measured the Arcas Robin
(ADR-034,
the decision behind it). Small changes in shape can still switch a body between the two models,
for example a nose so steep that the method never holds at any Mach up to 5, so the rocket
keeps slender-body theory throughout
(issue #87).
A worked example: the Arcas Robin’s body. NASA measured the Arcas Robin’s body without fins in a wind tunnel (TN D-4014). Flown through a flight’s own code, with the secant-ogive nose (a tangent ogive’s cousin, its arc larger) fitted to the report’s coordinates and the boattail left off, the nose and cylinder give the method’s own values: equal on the table’s rows (Mach 1.5, 1.8, 2.3), within 1e-4 between them.
With the boattail on (the lip behind it off, a flare the method doesn’t take), the boattail takes
its measured share, 0.31 to 0.62 per radian off, most at Mach 1.5 (footnote 8 took 0.03 to 0.18).
At α → 0 the short model then reads 12.0% to 26.2% below the measured line and the long model
29.7% to 33.3% below it. That line is fitted over the plotted angles, so it carries crossflow lift
and these columns leave body lift out: fitted the same way, with the body lift a flight adds, the
same body reads 3.4% to 41.0% high
(Checking the shock-expansion method).
hpr’s committed Arcas Robin design flies the method to its base since
Blunt tips and A lip in a boattail’s wake: the last
two columns are the method’s own values for its power-series nose, cylinder and boattail, with the
lip carrying nothing. Slopes are per radian on the body’s cross-section, at α → 0; the measured
slope is fitted over the plotted angles with the boattail and lip on, so it also carries some
crossflow lift and their share, which is why every column reads below it here.
| model | Mach | measured | method | flight, nose and cylinder | flight vs measured | flight, with boattail | with boattail vs measured | design as committed | committed vs measured |
|---|---|---|---|---|---|---|---|---|---|
| short | 1.5 | 2.192 | 2.552 | 2.552 | +16.4% | 1.930 | −12.0% | 1.852 | −15.5% |
| short | 1.8 | 2.613 | 2.724 | 2.724 | +4.3% | 2.171 | −16.9% | 2.143 | −18.0% |
| short | 2.3 | 3.078 | 2.931 | 2.931 | −4.8% | 2.452 | −20.3% | 2.394 | −22.2% |
| short | 2.96 | 3.284 | 3.124 | 3.124 | −4.9% | 2.716 | −17.3% | 2.612 | −20.5% |
| short | 3.96 | 3.884 | 3.300 | 3.300 | −15.0% | 2.963 | −23.7% | 2.735 | −29.6% |
| short | 4.63 | 4.149 | 3.371 | 3.371 | −18.7% | 3.063 | −26.2% | 2.718 | −34.5% |
| long | 1.8 | 3.159 | 2.724 | 2.724 | −13.7% | 2.171 | −31.3% | 2.143 | −32.1% |
| long | 2.3 | 3.525 | 2.932 | 2.932 | −16.8% | 2.453 | −30.4% | 2.394 | −32.1% |
| long | 2.96 | 3.868 | 3.127 | 3.127 | −19.2% | 2.718 | −29.7% | 2.614 | −32.4% |
| long | 3.96 | 4.455 | 3.313 | 3.313 | −25.6% | 2.974 | −33.3% | 2.740 | −38.5% |
| long | 4.63 | 4.615 | 3.395 | 3.395 | −26.4% | 3.082 | −33.2% | 2.724 | −41.0% |
The rows are in
validation/fixtures/aero/shock-expansion.json
(arcas_robin: nose_and_cylinder, in_flight, in_flight_with_boattail and as_designed), written by
cargo xtask aero; no test re-reads the flight columns, so they are regenerated by hand.
What it leaves out. Steps, and widening shapes that are not cones, as above; a conical flare has its own section (A flare through the method); a blunt or vertical tip takes the cap of Blunt tips, below. The method itself has no crossflow lift; a flight adds body lift on top.
A lip in a boattail’s wake
What this covers: a short flare at the very base, behind a boattail, like the reflex lip of NASA’s Arcas Robin models. How far to trust it: hpr gives such a lip no normal force faster than sound, which is what the measured pitching moment supports, but the moment bounds the lip rather than measuring it.
The rule. A lip that sits wholly in a boattail’s wake carries no potential-flow slope from the
Mach number where the method takes over; below the join it keeps slender-body theory’s
2 ΔA/A_ref, and the join blends the two, so nothing jumps. hpr takes the shelter’s share from its
drag model (ADR-030, which takes the same lip’s drag away): wholly in the wake up
to a rise of a quarter of the boattail’s drop in diameter, not at all from half of it, and the wake
fades over any tube between them. On top of that fraction the normal force asks one thing the drag
model doesn’t: the lip must be no longer than the boattail’s drop in diameter, the wake’s own
scale, or it grows out of the wake however little it rises. The decision record on the lip,
ADR-039, sets out the readings behind the share itself.
A lip part way out of the wake. Where the wake covers the lip only partly, the drag model has always graded it so. Since M1.8e10 the normal force reads the same number as a weight: the method’s share counts for the wake’s share of the lip, slender-body theory for the rest, exactly as they blend across the Mach join (ADR-041). It is the whole wake fraction, not the rise alone: the shelter fades with the lip’s rise, with any tube between it and the boattail, and with anything else in the way.
Be clear about what that buys. The jump is gone: a lip drawn a hair taller no longer switches the whole body between the two models, which it used to do by a third of the normal force and 1.77 calibres of center of pressure on the tests’ rocket at Mach 3. The sensitivity is not gone; it is spread over the band. On that rocket, at Mach 3 and 4°:
| drawn from | to | normal force | center of pressure |
|---|---|---|---|
| a lip rising a quarter of the boattail’s drop | rising a half (1.25 mm of radius) | −29.1% | 0.93 calibres |
| a lip flush behind the boattail | one a boattail’s drop in diameter behind it (10 mm) | −33.8% | 1.97 calibres |
That table is a design sensitivity: how the printed answer moves as you draw the lip differently. It is not the same as how far apart the two models are, which is what a lip in the band is actually uncertain by. At one fixed shape (a lip rising a quarter of the drop) the method and slender-body theory differ by 33.0% of the normal force and 1.77 calibres, and nothing measured says which is right for a part-sheltered lip. That number has not changed; what changed is that a rocket can no longer cross it in a ten-thousandth of its geometry.
Why nothing. Three readings point the same way.
- The tunnel itself. TN D-4014 ([D4014] p. 6) traces an odd chamber axial force at Mach 1.50 and 1.80, fins off, to the reflex lip, and says the effect is “masked” once separation runs over the boattail at higher Mach numbers or the fins thicken the boundary layer, and that the longer model shows none of it, “probably because of the thicker boundary layer at the model base”. So the lip does something at those two speeds, and those are the very rows whose moment implies a negative share below; what it does there isn’t a normal force this model can carry.
- Seiff’s own limits. His embedded Newtonian flare method holds for “thin shock layers when the flow is not extensively separated” ([S62] p. 13), and he notes that “a 90° ramp will invariably separate the flow” (p. 4). The Arcas lip’s face stands about 57° to the axis, behind a 15° expansion.
- The size, at most. Taking Seiff’s method anyway as an upper bound (eq. 9, p. 12, which for a
conical flare at one dynamic pressure is
2 (q₁/q∞) cos²θ ΔA/A_ref, withq₁the flow that has expanded through the boattail’s turn, andθtaken to the axis, 56.8°, where Seiff measures it from the local stream, the looser of the two) gives 0.044 per radian at Mach 1.5 falling to 0.014 at 4.63, against slender-body theory’s 0.178 at every speed.
What the moment says, and what it can’t. For each fins-off row, the share at the lip’s station that would put hpr’s center of pressure on the measured one runs from −0.256 ± 0.068 per radian (short model, Mach 1.5) to +0.229 ± 0.084 (long, Mach 3.96), changing sign with Mach number and with the model’s length. Fitting one share:
| rows | share, per radian | χ² per degree of freedom | from zero | from slender-body theory’s 0.178 |
|---|---|---|---|---|
| all eleven | +0.021 ± 0.019 | 4.5 | 1.1 σ | 8.4 σ |
| the short model’s six | −0.016 ± 0.022 | 6.4 | 0.7 σ | 8.7 σ |
| the long model’s five | +0.108 ± 0.034 | 0.9 | 3.2 σ | 2.1 σ |
A χ² per degree of freedom of 1 means rows agreeing within their own error bars. The short model’s 6.4 means its rows disagree among themselves; the long model’s 0.9 means its five agree, on a share of +0.108, five times Seiff’s bound at that speed and three standard errors above zero, yet still two below slender-body theory’s.
So the moment does not settle the lip, and the model doesn’t rest on it. The reason is in how the number is made: it blames the lip for every miss in the center of pressure, and hpr’s body alone reads 15% to 19% high on the long model at Mach 1.8 and 2.3, which shifts its center of pressure by far more than any lip. The short model’s own fit comes out negative, which no flare can produce. What the moment does say is that slender-body theory’s 0.178 at the base is too much: eight standard errors out on the short model, two on the long.
How it was checked. The committed designs, fins off, through a flight’s path, fitted at the
tunnel’s plotted angles as Checking the shock-expansion method
fits them. Slopes are per radian on the body’s cross-section; centers of pressure are calibres aft
of the nose tip. The last column is where hpr’s whole-body center of pressure would sit if the lip
carried slender-body theory’s share instead of nothing. hpr’s slopes here are fitted over the
tunnel’s angles, so they carry body lift; the same bodies’ slopes at α → 0, in
Checking the shock-expansion method’s table, are lower.
| model | Mach | measured | hpr, as committed | vs measured | measured CP | hpr’s | hpr’s CP with the lip at slender-body theory’s share |
|---|---|---|---|---|---|---|---|
| short | 1.5 | 2.192 | 3.017 | +37.7% | 1.00 | 2.46 | 3.33 |
| short | 1.8 | 2.613 | 3.290 | +25.9% | 2.36 | 3.06 | 3.84 |
| short | 2.3 | 3.078 | 3.598 | +16.9% | 3.56 | 3.69 | 4.37 |
| short | 2.96 | 3.284 | 3.838 | +16.9% | 3.21 | 3.75 | 4.43 |
| short | 3.96 | 3.884 | 3.946 | +1.6% | 4.88 | 4.57 | 5.16 |
| short | 4.63 | 4.149 | 3.950 | −4.8% | 5.05 | 4.72 | 5.30 |
| long | 1.8 | 3.159 | 3.770 | +19.4% | 4.61 | 4.31 | 5.19 |
| long | 2.3 | 3.525 | 4.071 | +15.5% | 5.04 | 4.89 | 5.68 |
| long | 2.96 | 3.868 | 4.400 | +13.7% | 5.33 | 4.64 | 5.47 |
| long | 3.96 | 4.455 | 4.428 | −0.6% | 6.19 | 5.20 | 5.98 |
| long | 4.63 | 4.615 | 4.425 | −4.1% | 6.40 | 5.96 | 6.65 |
The rows are in
validation/fixtures/aero/arcas-robin-lip.json,
written by cargo xtask aero, and a test holds this table to it cell by cell. With the lip left
off entirely the same bodies read within 0.05 percentage points of these rows at ten of the eleven,
and 0.5 at Mach 1.5 on the short model, so the lip changes little but which parts of the body the
method may cover.
What it means for a rocket. Most rockets have no lip, and nothing changes for them. For one that does, the rule decides whether the whole body flies the method or slender-body theory, so it is worth more than the lip itself: on the Arcas Robin’s short model at Mach 2.96 the body’s slope goes from 2.08 per radian to 3.84, and the whole rocket’s from −16.3% against the tunnel to +3.7% (Normal force through Mach 1). The extra lift sits on the body, ahead of the fins, so the center of pressure moves forward by 0.22 calibres there: a little less stability margin, and a good deal more restoring force.
What it leaves out. The lip still has drag, and its own wake rule there (ADR-030). Nothing here measures a lip’s lift directly: the tunnel gives forces for the whole body, and the moment bounds the share rather than measuring it. The shelter used to be a switch in shape, of the family issue #87 tracks, and a large one, because it decides whether the whole body flies the method. Since M1.8e10 the rise no longer switches it: on the test rocket at Mach 3 and 4°, lips rising 0.2499 and 0.2501 of the boattail’s drop now agree to a ten-thousandth. What is left is how far apart the models are at one shape: 33.0% of the normal force, and the center of pressure 1.77 calibres forward on slender-body theory, spread over the band the wake grades (above). A narrowing part behind the run is a boattail the method hasn’t covered, not a lip, and keeps slender-body theory’s share. A widening part behind a boattail that is too long for the wake stays a lip as well, and still takes the whole body off the method: the flow reaching its corner is the wake’s, which the march does not compute, so A flare through the method does not apply to it.
Where a flare’s march stops
In short: a flare is a transition that widens toward the tail, and the method will march one, but only up to a limit, and that limit is not where the flare’s shock detaches. It is where the corner’s turn would take the flow to Mach 1, which is a property of hpr’s method rather than of the air, and it depends on the whole body ahead of the flare: on the body measured below it falls short of a wedge’s detachment angle at Mach 1.5 and runs past it at Mach 2, and taking the tube away moves it past the wedge at both. From Mach 2.13 to Mach 5, the highest checked, the limit is neither: it is the 30° where the cone tables end. The flare’s own detachment angle is not known here: the wedge’s is a conservative stand-in for it. This section is the measurement, and it is why the attachment test a flight uses had to be chosen rather than read off the march’s refusal (ADR-045). What a flight does with a flare is the next section, A flare through the method, and how close that comes to a measured flare is the one after it, What a marched flare is worth. Neither this section’s edge nor the drawn-out reading past it is itself compared with a measurement.
Second-order shock-expansion turns every corner isentropically (no entropy rise, so no shock) with the Prandtl–Meyer angle ν ([SD56]) eq. 3. A flare is a compression corner, so the turn spends ν: the march goes on only while the flow reaching the flare has enough of it left to turn through the flare’s angle without dropping to Mach 1. Whether a shock instead stands attached to that corner is a separate question, and it is the one that decides whether the march is modeling the real flow at all.
The table below sweeps the flare’s angle on a fixed body (a pointed 2.75° cone, five calibres of
tube, and a conical flare) and bisects, until the two angles are adjacent double-precision
numbers, the steepest flare the march accepts. The two angles are NASA TN D-4865 model 2’s; the
layout is not. That model is blunt-nosed and has no tube at all, and the edge depends on what is
ahead of the flare, because that is what sets the flow reaching it. Angles are quoted to seven
decimals so the differences add up, and the detachment column is taken at the free-stream Mach
number (the flow reaching the flare is a little faster, which would move the wedge’s angle by about
0.02°). Both tests are in
shock_expansion.rs:
a_flare_marches_to_the_isentropic_turn_not_to_detachment and
past_mach_2_13_the_flare_stops_where_the_cone_tables_do.
| free stream | the march accepts a flare to | a wedge’s shock detaches at | so the march is |
|---|---|---|---|
| Mach 1.5 | 11.9312175° | 12.1126689° | 0.1814514° short of it |
| Mach 1.547787962528 | 13.346819° | 13.346819° | exactly on it |
| Mach 2 | 26.4714031° | 22.9735318° | 3.4978713° past it |
| Mach 2.5 | 30° (the tables) | 29.7974° | 0.20° past it |
| Mach 3 | 30° (the tables) | 34.0734° | 4.07° short of it |
How much of that is the tube. A great deal, and it is the point rather than a caveat: what the march has left to spend is ν of the flow arriving at the corner, and the body ahead sets that flow. Keeping the same cone and flare and changing only the tube’s length:
| tube | the march accepts a flare to, Mach 1.5 | at Mach 2 |
|---|---|---|
| none (the report’s own layout) | 14.194333° | 28.509856° |
| 1 calibre | 12.821811° | 27.500078° |
| 2.5 calibres | 12.144405° | 26.815015° |
| 5 calibres (the table above) | 11.9312175° | 26.4714031° |
So the first row of the first table (the march stopping short of a wedge’s detachment) is a property of that five-calibre body, not of the method: on the report’s own tube-less layout the Mach 1.5 edge is 14.19°, two degrees past the wedge’s limit. What does not depend on the body is the conclusion: the march’s edge is set by the corner’s isentropic turn, and it lands on both sides of a wedge’s detachment angle depending on nothing more than how long the tube is.
The detachment angles are a wedge’s largest deflection ([R1135], through
wedge_detachment_angle_rad, which
lives with the blunt-tip cap because that cap uses the same relation). A cone’s shock holds to
steeper angles than a wedge’s, and a conical flare on a cylinder sits between the two, so the
wedge’s column is a conservative stand-in, not the flare’s own boundary. Only the rows where the
march stops below the wedge’s angle prove anything about attachment; where the march runs past
it, the flare’s own limit may still be higher. Which boundary a flight uses is
the next section’s subject. What the table shows is that the march’s edge
lands on both sides of any such boundary: you cannot tell, from hpr returning an answer, that the
flow it modeled is the flow that would be there.
The last two rows of the first table are a different limit altogether. Each element’s tangent cone is looked up in NASA SP-3007 Table 2, whose slopes stop at 30° (the milestone that took them there from 24° is M1.8e11 (ADR-042)), so from Mach 2.129702032593 to Mach 5, the highest checked, every Mach number gives the same edge. The reference data runs out before the flow does, and the last column then says nothing about attachment.
An 18.5° flare, the report’s angle, on the 2.75°-cone body above (not the flared rocket of the next section, whose numbers are close but not these). The march accepts it from Mach 1.721760; a wedge’s shock reaches 18.5° only at Mach 1.767575. Between the two, hpr returns a number for a flare whose shock is, on that reckoning, detached: a bow shock standing ahead of the juncture with a pocket of subsonic flow behind it, which an isentropic corner turn does not describe. TN D-4865’s lowest run, Mach 1.50, is below both, and there the march refuses outright, as the report itself says it should. On a shorter body those two Mach numbers move, as the table above shows.
What it leaves out. These digits pin what this program does, not what air does: every one of them comes from bisecting hpr’s own refusal, and the 30° rows come from where a lookup table ends. A band of very shallow flares is not marched by the second-order law at all: the pressure behind such a corner moves away from its tangent cone’s rather than toward it, so its one element is reduced to the older generalized method, which since M1.8e19 is read rather than refused (A near-flat flare). Where that band sits is a property of the body ahead of the corner, and it moves by orders of magnitude: 0.773° to 0.823° at Mach 3 on this 2.5-calibre body, against 0.0066° to 0.0081° at the same Mach number on the flared rocket of A near-flat flare. The whole edge is inviscid, too. From Mach 2.96 up TN D-4865 records the boundary layer separating ahead of the flare and reattaching on it, which moves the pressure rise downstream of where a tangent body puts it; nothing here models that. And below Mach 1.5 nothing here was measured, although a flight uses the method from Mach 1.2.
A flare through the method
In short: since M1.8e17 a rocket with a flare flies the shock-expansion method rather than dropping to slender-body theory the moment one is drawn. The method marches through the flare’s corner while the shock there stays attached; a steeper flare is read as one of the same radii drawn out to the steepest attached angle, which is what keeps the answer from jumping as a shape or a speed crosses that boundary. How far to trust it: the attachment test is a standard relation, applied at a corner it was not derived for; the reading either side of the boundary it draws agrees to four parts in 1e11, though its slope kinks there; and what it is worth against the one measured flare in the sources is the section after this one: −1.9% and +7.0% at Mach 1.9 and 2.3, +13.4% at 2.96, and +51.5% and +50.4% at 3.95 and 4.63. Read the numbers below as what this program does, and that section as how close it lands. What qualifies is narrow: a conical flare, flush with the part ahead of it, not in a boattail’s wake, with nothing behind it that carries lift of its own. A boattail then a flare is a lip in a wake and keeps that rule; any other widening shape still ends the run. The decision record is ADR-047.
Why a test had to be picked. The march will return a number for a flare whose shock has long since detached (the section above measures exactly that), so “the method answered” is not evidence the flow it modeled is the flow that would be there. Something independent has to say where the corner’s shock detaches.
The test. A flare’s shock springs from a circular corner, not from a point apex. Where the
shock forms, the flow is two-dimensional: the body’s radius is the scale over which the
axisymmetric relief acts, and at the corner itself none of it has happened yet. So hpr uses
NACA Report 1135’s ([R1135]) largest deflection behind an attached plane oblique shock (eq. 168 into
eq. 138), the same relation, in the same function
(wedge_detachment_angle_rad),
that TN D-4865 ([J68]) p. 5 uses to hand a blunt tip’s cap over to this
method. One attachment rule, at both corners the program has.
Two details matter.
- It is read at the flow reaching the corner, not at the free stream. The body ahead has
already changed the air, and which way depends on what that body is: the ogive nose and tube of
the tests’ flared rocket leave it a little slower than the free stream, while the 2.75° cone
and tube of the section above leave it a little faster. The
march knows by how much
(
aft_flow). On the tests’ flared rocket a Mach 2.0 free stream reaches the corner at Mach 1.9998, so the limit is 22.9698° and not the free stream’s 22.9735°. The march is downstream-only, so the flare cannot change the flow arriving at its own corner, which is what lets the limit be worked out before the flare is drawn. - The cone tables cap it at 30°, because past that the march has no tangent cone to relax toward (tangent cone, ADR-042), but that bounds the flare’s surface angle, while the shock bounds the turn at its corner, so each caps its own quantity. On a flare behind a cylinder, where the surface ahead is at 0°, the two are the same number and the cap binds from Mach 2.5192034260 of the flow reaching the corner up; a faster flow buys nothing above that.
Because the shock’s bound is on the turn and not on the flare’s angle, a flare put straight onto an ogive nose (no tube between them) is read differently from the same flare behind a tube: the corner there turns the flow by the flare’s angle less the nose’s base slope, so the limit bites at a steeper flare. On the tests’ flared rocket (an ogive nose 0.25 m long on a 27 mm radius, then a 0.7 m tube of that radius) the surface ahead is at 0° and the turn and the angle are one number:
| free stream | the flow reaching the corner | the corner is read to |
|---|---|---|
| Mach 1.5 | Mach 1.4999688 | 12.1118502° |
| Mach 2.0 | Mach 1.9997809 | 22.9697612° |
| Mach 2.5 | Mach 2.4989548 | 29.7863106° |
| Mach 3.0 | Mach 2.9965267 | 30° (the tables) |
| Mach 4.95 | Mach 4.8922987 | 30° (the tables) |
A cone’s shock holds to steeper angles than a wedge’s, so if a conical flare on a cylinder does sit between the two, this errs one way only: it stops reading some flares whose shock is in fact still attached, and reads none whose shock is not. Nothing here measures where a flare’s own boundary actually is, so that “if” is an argument, not a result, and erring low is not the same as erring safely. A flare just past the limit is not left unread; it is read as a different flare, and how different nothing here measures either.
Past the limit, the flare is drawn out. A flare steeper than the limit is read as a flare of the same radii stretched to the limiting angle, longer and shallower, turning the same air through a corner the shock can hold, with its center of pressure put back on the real flare, at the same fraction along it. Because the radii are kept and the angle is not, every flare steeper than the limit reads the same force: held at the limit, in other words. There is no bound on how far that goes, and it under-reads badly at the extreme: at Mach 2 a 75° flare, an annular face a detached bow shock would stand in front of, reads 1.638 per radian against slender-body theory’s 2.010, where the truth is above both. It reads a steep flare as less stabilizing than it is, which is the safe direction for a stability margin and the wrong one for a load. This is the boattail rule turned around: a boattail past 16° reads the correlation of one of the same radii drawn out to 16° (ADR-040, A boattail faster than sound). The radii are what set how much air the flare turns, and they are never changed.
That is also what makes the answer continuous, by construction rather than by tuning: at the
limit the drawn-out flare is the real flare, so the two readings are the same body. The test
nothing_jumps_where_the_flares_shock_detaches probes either side of the boundary. At Mach 2.0
(a row of the table, so the reading is that row’s and not an interpolation) the boundary is a flare
of 22.969761173077°:
| probe, in the flare’s angle | the normal-force slope moves by, as a fraction of itself |
|---|---|
| ±1e-9° | 4.527e-11 |
| ±1e-7° | 4.527e-9 |
| ±1e-5° | 4.527e-7 |
A hundredfold smaller probe moves the answer a hundredfold less, so what the probe finds is a
slope and not a step: the reading is continuous across the boundary. Its slope is not, and
nothing here claims otherwise: a cap makes a kink, because below the boundary the flare’s angle
moves the body the march sees and above it only the radii do. Measured at the same place,
dC_Nα/dδ changes by −31.4% across the boundary on the whole rocket and by −141.6% on the flare’s
own share, where it changes sign. The marched branch is not smooth in the angle either: the same
probe at 20°, away from any boundary, finds +3.5% and +41.1%
(the_cap_makes_a_kink_in_the_slope_even_though_the_reading_holds). Probing the
Mach number instead, at the 18.5° of TN D-4865’s model 2, whose shock holds on the tests’
flared rocket from Mach 1.767666917849, gives 3.622e-10, 3.622e-8 and 3.622e-6 for the same
three probes. That half is taken on the table’s own rows rather than on a reading between them:
the crossing falls inside the Mach 1.75 to 1.80 interval, where the reading is a straight line
between rows and would look continuous whatever the two branches did.
What it is worth. The tests’ flared rocket is an ogive nose 0.25 m long on a 27 mm radius, a 0.7 m tube of that radius, a 10° conical flare 0.3 m long opening to a 79.9 mm radius, a 0.2 m tail tube and four fins. Its reference is the largest diameter, 0.1598 m, and the numbers are at a small angle of attack. The method reads less normal force than slender-body theory and puts the center of pressure forward of it, so this rocket now reads less stable rather than more:
| the method | slender-body theory | the method’s center of pressure | |
|---|---|---|---|
| Mach 2.0 | 3.5371 per rad, at 7.365 calibres | 3.7153 per rad | 0.089 calibres forward |
| Mach 3.0 | 2.8899 per rad, at 7.015 calibres | 3.0815 per rad | 0.169 calibres forward |
| Mach 4.95 | 2.4880 per rad, at 6.681 calibres | 2.6431 per rad | 0.238 calibres forward |
So for a flare of this size the stability margin drops by a tenth to a quarter of a calibre,
growing with Mach number. a_flared_body_flies_the_method pins every figure in the table.
The old behavior is still selectable, for comparing:
BodyModel::with_supersonic_flare(SupersonicFlare::SlenderBody) in Rust, or
{"supersonic_flare": "slender_body"} in the body model’s JSON, reproduces every number from
before this milestone. Which to fly is not settled here: neither column has been compared with
a measured flare, and the method is the default because it is the model the rest of the body
already uses faster than sound, not because it is known to be closer.
What it leaves out.
- The one measured flare is not this flare. What a marched flare is worth compares an 18.5° flare on a 2.75° cone, at six speeds, three of them with its boundary layer separated. The 10° flare in the table above is not that body, so the size of the difference it shows is still a change of model rather than a measured correction.
- The flare’s own detachment angle is still the wedge’s. A conical flare on a cylinder sits between a wedge and a cone, and nothing here measures where it actually is.
- Below about Mach 1.5552 the method has no reading for an 18.5° flare on the tests’ rocket at all. The corner’s isentropic turn runs out before its shock detaches (the section above), so the body’s table of the method’s shares starts there and the join carries the reading up from slender-body theory’s over 0.3 Mach. That is continuous (a ±1e-9 probe at the join’s start moves the slope 8.5e-10 of itself), but it means a flare’s march is not used at all at the low end of what a flight uses.
- A near-flat flare is read by the older, rougher method. Between about 0.0004° and 0.059° on the tests’ rocket, depending on the Mach number, a flare’s single element is reduced: the pressure behind its corner sits just past its tangent cone’s while the gradient the tube delivers still pushes it away, so hpr reads that element by the generalized shock-expansion method instead (issue #81: the method’s limit near a sharp tip is the same reading on a nose). Until M1.8e19: the near-flat flare the march refused such an element behind the nose and the whole body fell back to slender-body theory at every Mach number, which was a switch worth −8.3% and 1.16 calibres. A near-flat flare below solves for where the region is at each Mach number, says what the change was worth, and gives the one step that is left: +0.129% and 0.0051 calibres on this rocket, and up to +4.3% and 0.19 calibres on a body with a short shoulder.
- The march ends at the flare, and nothing behind it may carry lift. Anything behind the flare takes slender-body theory’s share, which for a tube is nothing, but a part that carries a share of its own, such as a small tail cone behind the flare, takes the whole rocket off the method again, at every Mach number, exactly as a second flare would. A flare followed by a plain tube and fins is the layout that flies. Where it does fly, the relaxation the method would give the tube behind the flare is left out, which reads a little less stable, not more.
- Everything here is inviscid. From Mach 2.96 up TN D-4865 records the boundary layer separating ahead of the juncture and reattaching on the flare, which moves the pressure rise downstream of where a tangent body puts it. Nothing here models that.
What a marched flare is worth
In short: A flare through the method, above, says what hpr does with a flare: it marches the shock-expansion method through the flare’s corner while the shock there stays attached. This section says how close that comes to a measured flare, which before M1.8e18, the milestone that did this work, nothing had.
There is one flared body in the sources whose normal force and pitching moment are printed: NASA TN D-4865’s model 2 ([J68]), a blunt 2.75° cone with an 18.5° flare, in the Langley Unitary Plan tunnel from Mach 1.50 to 4.63. Against it hpr reads the normal-force slope −1.9% at Mach 1.90, +7.0% at 2.30, +13.4% at 2.96, then +51.5% and +50.4% at 3.95 and 4.63, with the center of pressure within 0.05 calibres through Mach 2.96 and 0.088 at 3.95. The report’s own shadowgraphs show that flare’s boundary layer separated ahead of the juncture from Mach 2.96 up, but the cost only shows in the two fastest rows; why is not settled here. Below Mach 1.5289 there is no reading at all, and a flared body falls back to slender-body theory instead.
One body, one flare angle, six speeds, three of them separated: that is the whole of the evidence, and it is not enough to call the model right, only enough to say where it is not obviously wrong. The decision record is ADR-048.
The body. Two things about it bear on the comparison: its nose is blunter and more compound than model 1’s, so hpr had to learn to hand a blunt tip’s cap over on a later piece of a nose; and the report’s own drawing does not quite close, so which of its printed numbers to keep had to be chosen. Neither is worth much (the closure is worth 0.64 points at most, measured three ways), but both are choices, so here they are.
Fig. 3(b) (printed p. 91) draws model 2 in base diameters, d = 0.583 ft (0.178 m). Its nose is
not a sphere-cone: a 0.257 sphere from the tip, then a second arc of 0.429 whose
center sits 0.135 below the axis, then the 2.75° cone, then the 18.5° flare. Those three printed
radii fix everything else. The two arcs are internally tangent, so their centers are 0.429 − 0.257
= 0.172 apart, which with the 0.135 offset puts the second center 0.3635786 aft of the tip; its
tangent to the 2.75° cone then falls at 0.3429960 aft of the tip, matching the drawing’s
printed 0.343 to four figures, at diameter 0.5870, which misses its printed 0.586 by 0.001 of
a diameter. A fourth printed dimension checks the same derivation from the other end: 0.722 runs
from the arc’s center to the flare juncture, and 0.3635786 + 0.722 = 1.08558 against 0.343 + 0.743
= 1.08600, a residual of 0.0004. hpr draws that blend arc as a circular arc (the tangent
ogive’s shape with its radius ratio solved for a 0.429 arc), and
the profile it builds misses the drawn circle by 8.3e−17 of a diameter.
The printed dimensions do not quite close, and the gap is the flare’s. Nose, cone and flare come to 0.3429960 + 0.743 + 0.523 = 1.6090, the printed length exactly. The cone closes on its own numbers too: 0.586 + 2 × 0.743 tan 2.75° = 0.65737, against the printed 0.657. The flare does not: 0.657 + 2 × 0.523 tan 18.5° = 1.0074, against a base that is 1.000 by definition, and 1.0084 with the nose taken from its three radii rather than from its printed 0.586. So either the flare is shorter than 0.523, or it is shallower than 18.5°.
hpr keeps both half-angles, because they are the report’s text and not only its drawing: “a blunted cone with a 2.75° half-angle and a flare afterbody having an 18.500° half-angle” (printed p. 8), stated to three decimals, along with the nose, the length 1.609 and the base 1.000, which are the numbers the measured coefficients are divided by. What gives is the split of the length between the cone and the flare: a 0.7576199 cone and a 0.5083841 flare, which puts the juncture 0.0146 diameters aft of the printed one, at diameter 0.6598 against 0.657.
Two other closures are computed and published beside it, so the choice can be checked rather than trusted. Keeping the printed lengths and scaling the whole body to a 1.000 base moves the error by −0.12 to −0.17 points (points here and below are percentage points of error). Keeping the printed lengths and the base and giving up the flare’s stated angle instead (18.5° becomes 18.0864°), moves it by +0.06 to +0.64 points. Over all three the spread is 0.64 points or less in the slope and 0.0043 calibres or less in the center of pressure, and Mach 1.50 is refused in every one of them, so nothing below turns on which closure is flown.
What it is compared with. Fig. 8(b) (printed p. 102) plots normal force C_N, pitching moment
C_m and axial force C_A (all three as coefficients, C_m about the nose tip on the body’s
own length) against α at 0°, 4°, 8° and 12°, for each of six Mach numbers. The circles are the
report’s experiment: the surface pressures of its tables VII to XII integrated over the
forebody (printed p. 12), so no base pressure is in them. Every circle was read off the
page’s 300-ppi
scan by pixel analysis, the method written for model 1 in
M1.8e7; the α = 0 circles come out at −0.0039 to +0.0043
where they should read 0, which is what the plotting itself is worth. C_A is not read, because
hpr’s supersonic drag is a separate model this comparison does not touch. hpr’s slope and center
of pressure are fitted the way that section fits model 1: a straight line through hpr’s own C_N
at those same four angles, body lift included, so the two sides are the same
quantity.
| Mach | measured C_Nα, per radian | hpr | hpr’s error | measured CP, calibres aft of the tip | hpr | hpr − measured, calibres |
|---|---|---|---|---|---|---|
| 1.5 | 1.650 | none | none | 0.810 | none | none |
| 1.9 | 1.800 | 1.766 | −1.9% | 0.888 | 0.934 | 0.046 |
| 2.3 | 1.667 | 1.784 | +7.0% | 0.921 | 0.926 | 0.005 |
| 2.96 | 1.594 | 1.807 | +13.4% | 0.948 | 0.935 | −0.013 |
| 3.95 | 1.270 | 1.923 | +51.5% | 1.046 | 0.958 | −0.088 |
| 4.63 | 1.303 | 1.960 | +50.4% | 0.998 | 0.975 | −0.023 |
A positive number in the last column means hpr puts the center of pressure further aft than the tunnel did, which reads as more stable than the rocket is; a negative one reads as less. The half-calibre the rest of this page uses as a target is the scale to hold them against.
The flare is most of what is being compared. Its own share of the body’s slope is 52.1% at Mach 1.90 and 60.8% at 4.63, and never below 51.5% (at Mach 2.30) in between. That share acts 1.382 to 1.390 calibres aft of the tip, on the flare itself, which runs from 1.101 to 1.609. So this is a test of the flare and not of a body that happens to have one.
How much of the miss is the flare’s. The same report, the same tunnel, the same figure and the same reading and fit also give model 1, a sphere-cone with no flare, the body Blunt tips already checks. Putting the two side by side separates what the flare costs from what the rest of the body costs:
| Mach | hpr’s error, model 1 (no flare) | hpr’s error, model 2 (flared) | the flare adds | the report’s own method, model 1 | the report’s own method, model 2 |
|---|---|---|---|---|---|
| 1.5 | −1.2% | none | none | −3.3% | +28.6% |
| 1.9 | +0.0% | −1.9% | −1.9 points | +2.0% | +24.8% |
| 2.3 | +7.5% | +7.0% | −0.5 points | +8.5% | +31.6% |
| 2.96 | +12.5% | +13.4% | +0.9 points | +5.2% | +12.1% |
| 3.95 | +29.7% | +51.5% | +21.7 points | +11.3% | +28.8% |
| 4.63 | +32.1% | +50.4% | +18.3 points | +13.6% | +20.3% |
They are not one body with and without a flare: model 1 is an 11.5° cone on a 0.175-diameter nose radius, 1.755 diameters long, and model 2 a 2.75° cone on a 0.257-diameter one, 1.609 long, so the flare adds column bounds what the flare costs rather than measuring it. Read it as a signed difference and nothing more: it is not a verdict on which body hpr reads better. At Mach 1.90 the unflared model 1 is almost exact (+0.011%) and the flared one is 1.9% low, so in size of error the flare is the worse row, not the better one.
Taken that way the column still says something clear. Through Mach 2.96 the flare moves the
error by −1.9 to +0.9 points; that is, by less than the rest of the body already misses by. At
Mach 3.95 and 4.63 it moves it by 21.7 and 18.3 points, an order of magnitude more, and that is
where the measured C_Nα itself falls away (from 1.594 at Mach 2.96 to 1.270 at 3.95) while
both attached-flow methods on the figure, the report’s own and hpr’s, stay between 1.57 and 1.96.
The obvious explanation is the flow: from Mach 2.96 the report’s shadowgraphs show the laminar boundary layer separating ahead of the juncture and reattaching behind it (printed p. 10), and nothing in an attached-flow method describes that. How far the report backs it up is worth being exact about. It blames the separated flow for its own method’s disagreement with the measured pressures at the high Mach numbers (printed p. 10) and for its over-prediction of axial force (printed p. 12). It says nothing at all about what separation does to the normal force or the pitching moment. Nor does its method carry a separation signature on this body: it reads +24.8% at Mach 1.90 and +31.6% at 2.30, both attached rows, against +28.8% and +20.3% at the separated ones, uniformly high on model 2 and near the tunnel on model 1, which is a large offset this milestone has not explained. So read the separation as consistent with the two fast rows rather than measured by them. What nothing here settles is why it costs 0.9 points at Mach 2.96, where the report says it has already begun, and twenty times that at 3.95.
Where the method has no reading. At Mach 1.50 hpr refuses model 2 outright, and the refusal is worth following, because it is not the rule the section above describes. Two limits decide what the march does with a flare’s corner: the steepest surface angle whose shock stays attached there, which is the rule that section sets, and the steepest the march itself can turn the flow through. Both are in the table below as surface angles of the flare, so they can be read against its 18.5° and against each other:
| Mach | the flow reaching the corner | the steepest angle its shock holds | the steepest the march takes | the flare is read |
|---|---|---|---|---|
| 1.5 | Mach 1.5241 | 15.4885° | 15.3647° | drawn out, then refused |
| 1.9 | Mach 1.9354 | 24.5773° | 27.3345° | as drawn |
| 2.3 | Mach 2.3072 | 30.0000° | ≥ 30° (the tables) | as drawn |
| 2.96 | Mach 2.8966 | 30.0000° | ≥ 30° (the tables) | as drawn |
| 3.95 | Mach 3.6913 | 30.0000° | ≥ 30° (the tables) | as drawn |
| 4.63 | Mach 4.1636 | 30.0000° | ≥ 30° (the tables) | as drawn |
The turn the corner actually makes is each of those angles less the 2.75° of the cone ahead. From
Mach 2.3 up the third column is clipped at the cone tables’ 30° (past that an element has no
tangent cone to relax toward), and the search behind the fourth
stops at the same place, so neither is a measured edge there and the fourth says so rather than
printing the cap as though it were one. At Mach 1.50 the flare is steeper than the 15.4885° its
corner’s shock holds, so ADR-047’s rule draws it out to 15.4885°, and the march then
refuses that body too, because the steepest flare it can march there is 15.3647°, a tenth of a
degree shallower. The two limits are different things: one is where the corner’s shock detaches,
the other where the corner’s isentropic turn runs out (Where a flare’s march
stops, ADR-045), and which is the tighter one changes
with speed. Below where they cross, drawing a flare out to the shock’s limit lands past what the
march can do. Swept every 0.005 Mach from 1.05 to 4.63 the reading turns on exactly once, and
bisecting that one crossing to f64 resolution puts hpr’s first reading of model 2 at Mach
1.5288696; just below it the two limits agree to five parts in 1e14, both 16.2844275°, so the
reading begins exactly where they cross. (The fixture keeps the whole f64; seven figures is what
the three operating systems CI runs agree on, since each regenerates the bisection to within a
part in 1e12 of the others.) That is a different number from the Mach 1.5552
the section above quotes for the same 18.5° flare, and it should differ:
the body ahead of the corner is different, so the flow it delivers there is different, and the
crossing moves with that flow. Below that a flared body takes slender-body
theory instead, carried up over 0.3 Mach by the
join, which is continuous, but means the method is not used at the
low end of what a flight flies. The report says the same thing about its own method at that speed:
“at M∞ = 1.50, the shock wave produced by the flare is not theoretically attached”
(printed p. 10), and its method reads +28.6% there against +2.0% on model 1.
The rows above are in
validation/fixtures/aero/marched-flare.json,
written by cargo xtask aero; the readings in
tn-d-4865-flared-cone.json,
with how each circle was read. A test holds every table here to the fixture, cell by cell. The
reading is hpr’s own rule and not a second copy of it: hpr-design has no spherical-cap nose, so
model 2 cannot be flown through a Rocket, and the_flare_is_read_as_the_model_reads_it pins the
fixture’s reading against the model’s own shares: share by share, to a part in 1e12, on a flared
body the design route can express, at angles the corner’s shock holds and at angles it does not.
That pins the arithmetic, not the physics: an error in the rule itself would be in both and pass.
What it leaves out.
- Three of the six rows are a separated flare. From Mach 2.96 the report’s shadowgraphs show the boundary layer separating ahead of the juncture; nothing in hpr models that, and the +51.5% and +50.4% at Mach 3.95 and 4.63 are what those rows cost on this body, which the report’s own words make consistent with separation rather than caused by it, since it never says what separation does to the normal force. No source here says how a separated flare scales with the flare’s angle, its length or the boundary layer’s thickness, so those two numbers do not transfer to another flare.
- One body and one flare angle. 18.5°, on a 2.75° cone, at six speeds. Nothing here measures a shallow flare, a steep one, a flare behind a cylinder rather than a cone, or a flare on a pointed nose. A reader with a different flare has no measured error to apply: what this section supports is that hpr’s flare is not obviously wrong where the flow stays attached, not that it is good to 7% on some other body.
- The drawn-out reading past the limit is measured against nothing at all. The one row where it would have applied is the row the march then refused, so the rule above the corner’s limit is still a construction chosen for continuity.
- The measurement is the forebody only. Its
C_NandC_mare integrated surface pressures with no base term, which is what hpr’s body model computes too, but it also means the tunnel’s own balance never weighed this body, and a reading error of about 0.004 inC_Nsits under every circle. C_Ais not compared. The same figure plots axial force, and the report notes its own method reads it high where the flare separates. hpr’s supersonic drag is a different model with its own checks; this milestone did not touch it.- The drawing is 1% inconsistent and one closure had to be chosen. The spread over the three closures is small (0.64 points, 0.0043 calibres) and the flare’s own angle is inside it, but it is the drawing’s disagreement with itself, not an error bar on the measurement.
- No target was set, and none is met or missed here. M1.8e18 asked what a marched flare is worth, not that it reach a number. The tables above are the answer.
A near-flat flare
In short: a flare that opens by very little turns the flow so little that the second-order shock-expansion method’s own pressure curve has nothing left to describe, and the march falls back on the older generalized method for that one element. Until M1.8e19: the near-flat flare hpr refused to read such an element at all behind the nose, which took a rocket with a flare of about a third of a millimeter’s rise off the method entirely. It now reads it. This section says which flares those are (the two angles that bound them are solved from two equations about the corner’s own flow, rather than found by bisecting the model’s refusal), what the change was worth, and the one small step that is left.
How far to trust it. No wind tunnel has measured a flare this shallow, and the report does not say what it would have done here, so what follows is hpr’s own reading of the report’s own limit, chosen because it is continuous in the flare’s angle and smooth through the region, not because it is known to be nearer the air. What changed is which model runs, not how well either matches a measurement. And where the region sits depends entirely on the body ahead of the corner: a few thousandths of a degree on the rocket measured here, nearly a degree on the body of Where a flare’s march stops (a pointed 2.75° cone and five calibres of tube, whose radius is nearly four times as large).
What a flare that small does to the method. The march is the walk along the tangent body’s straight elements, from the nose tip aft, that the method makes. It fixes the pressure just behind each corner from the Prandtl–Meyer turn there, and then lets it relax along the element toward the pressure on that element’s tangent cone, as
p = p_c − (p_c − p₂) e^(−η), η = (∂p/∂s)₂ (x − x₂) ⁄ ((p_c − p₂) cos δ₂)
(NACA TN 3527 eqs. 8 and 9), where x − x₂ is the distance back from the corner. The same
exponent written as a rate per meter is the k of What a crossing is, and what it
costs. The symbols are the method’s own, listed under
Bodies faster than sound: δ an element’s angle to the axis, p the
pressure over the free stream’s, s distance along the surface, r the radius at the corner, Ω a
stream tube’s widening and B = γpM²/(2(M² − 1)); θ below is the flare’s own turn through its
corner, and subscript 1 is the state the body ahead delivers, 2 the state just behind the corner.
That is a curve that starts at p₂ and walks one way, toward p_c. It can only do that if the gradient just behind the corner points at p_c. The report keeps the form only for η ≥ 0 (p. 13) and says that at η = 0 “all equations reduce to those given by the generalized shock-expansion method”, whose pressure is simply constant along the element.
On a near-flat flare the two disagree, for a reason you can picture. The flow arrives at the flare along a long tube, where the pressure is still climbing back toward the free stream’s after the nose let it down: it is below the free stream and rising. The flare’s corner compresses it a little. Turn far enough and the pressure lands well above the tiny cone’s, and the gradient behind the corner turns downward with it, so everything agrees and the method runs. Turn less and the pressure stays below the cone’s, still rising toward it, so again everything agrees. In between there is a band where the compression has already carried the pressure just past its cone’s value while the tube’s own climb still pushes it further away. It has to rise, overshoot and come back, and one exponential cannot rise and fall. So η is negative there, and the element is reduced to the generalized method (issue #81: the method’s limit near a sharp tip is the same reading on a nose).
The region’s two edges are solved from the corner, not searched for. Each of the two quantities whose signs must agree is a smooth function of the flare’s turn, and on every corner state checked here each has a single zero, so the signs disagree on the open interval between those two zeros and nowhere else:
| the turn | what is zero there | what it means |
|---|---|---|
| the crossing | p_c − p₂ | the compression lands the pressure exactly on its tangent cone’s, and η has a pole |
| the balance | (∂p/∂s)₂ | the corner’s own compression exactly cancels the climb the tube delivers, and η is zero |
Single zero is not a promise, and a corner that breaks it exists: on a 25° cone with 20 mm of
tube behind it at Mach 7, the pressure meets its tangent cone’s three times, at about 0.91°,
7.3° and 24°, so a flare there is reduced from 0.91° to 3.88° and again from 7.3° to 24°.
flare_reduction_turns_rad sweeps the widening turns at a hundred stations before it brackets,
so it reports that corner rather than handing back whichever of the three roots it walked to
(a_corner_whose_gap_has_three_zeros_is_refused_rather_than_guessed_at). A pair of roots inside
one station would still slip through, and the two turns would then be reported as one band when
they are two.
Which of the two is the shallower is not fixed either: on this rocket the crossing is below the balance from Mach 1.5 up, and below that the order swaps.
Both are properties of the flow the body hands to the corner: its Mach number, its pressure, the
gradient it carries, the radius there, the angle ahead and the free stream it was read in.
flare_reduction_turns_rad takes exactly that and nothing else. The balance is TN 3527’s eq. 4
set to zero and rearranged, sin(δ₁ + θ) = (Ω₁/Ω₂(θ)) (sin δ₁ + r (∂p/∂s)₁ ⁄ B₁), which
iterates on itself. The crossing is p₂(θ) = p_c(δ₁ + θ): an isentropic turn on one side, a
cone solution on the other. It is bracketed over the turns a widening corner can make at all,
from a surface lying along the axis up to the isentropic turn running out or the cone tables’ 30°,
whichever comes first, and found by false position from the turn that would bring the pressure
back to the free stream’s.
On the tests’ flared rocket (an ogive nose 0.25 m long on a 27 mm radius, a 0.7 m tube and a 0.3 m conical flare) they are these:
| Mach | crossing | balance | a flare between them rises, over 0.3 m, by |
|---|---|---|---|
| 2.00 | 0.000403337° | 0.000454464° | 2.1 to 2.4 µm |
| 2.20 | 0.000901825° | 0.001041403° | 4.7 to 5.5 µm |
| 3.00 | 0.006619249° | 0.008121929° | 35 to 43 µm |
| 4.00 | 0.023088893° | 0.029499725° | 0.12 to 0.15 mm |
| 4.70 | 0.038161270° | 0.049811274° | 0.20 to 0.26 mm |
| 5.00 | 0.044649637° | 0.058820517° | 0.23 to 0.31 mm |
the_turns_a_reduced_element_lies_between_come_from_the_corners_own_state, in
shock_expansion.rs,
checks at eight Mach numbers that an angle a millionth either side of each edge falls on the right
side of the march’s own reduction, and that the midpoint between them is reduced. That is a spot
check at each edge, not an exhaustive sweep of the angles in between.
Those are the same numbers issue #117: the near-flat band the march used to refuse reported after bisecting the model’s own refusal (the band it quoted, 0.03816127° to 0.05882052°, is the crossing at Mach 4.70 and the balance at Mach 5, and the 0.00090182° it quoted is the crossing at Mach 2.20), but they are now read off two equations rather than found by trying the whole model against a sign test. Note what the table shows and that band hides: the region is not one interval in the angle. It moves with the Mach number, so a 0.04° flare is reduced at Mach 4.70 and marched at Mach 2.
How far to trust those digits. Not the search’s accuracy any more, but the tangent cone’s. Up
to a half-thousandth of a radian (0.029°) hpr’s cone flow is
slender-cone theory’s closed form, and the crossing closes to the last bits of an f64; the
residual it leaves in the pressure is under 2e-14 at Mach 2.00, 2.20 and 3.00. Above that angle
the cone flow is a Taylor–Maccoll integration, blended with the closed form up to 0.0573°, so the
two edges past Mach 4 are read off the blend; there the residual is that integration’s own, about
1e-10 of the free stream’s pressure. Divided by how fast the gap closes with the turn, that is
about 2e-10°, close to the 2.6e-10° the three operating systems CI runs were seen to spread the
band’s lower edge over, so that spread was the cone’s and not the bisection’s.
Neither residual is promised to be zero, and on a body whose cone flow is harder it is
larger: 4e-9 of the free stream’s pressure has been seen on a fatter body at Mach 2.6. So
flare_reduction_turns_rad returns both of them beside the turns, and a caller who needs the
digits should read them.
What hpr does now, and what changed. A reduced element is read by the generalized method wherever it has a tangent cone of its own: constant pressure and constant loading along it, which is what the report says the equations become. Before M1.8e19 that reading was allowed only on the nose, and anywhere behind it the march refused. Because hpr builds a body’s table of the method’s shares downward from Mach 5 and needs the whole 0.3 Mach of the join inside it, one refused row near the top took the table away and dropped the whole body to slender-body theory at every speed. Two switches came of that, and both are gone:
| drawing this | was worth | is worth |
|---|---|---|
| a flare of 0.05882052°, the band’s steep edge, at Mach 3 and 4° | −8.30% of the normal force and 1.16 calibres | nothing |
| a flare of 0.00090182°, which lifted the table’s start from Mach 1.2 to Mach 2.2, at Mach 2 and 4° | −4.62% and 0.75 calibres | nothing |
The sizes in the middle column are still measured, because they are the size of the fallback the
model used to drop to: a_near_flat_flare_marches_every_row_and_the_fallback_is_still_measured,
in model.rs, reads
the same rocket on SupersonicFlare::SlenderBody and finds them again. The table’s start is now
Mach 1.2 at fifteen flare angles from 0° to 1°, including 0.00024°, 0.00025° and 0.0003°: three
angles a hair apart that used to give three different answers.
What is left is the crossing, and here is how big it is. At the crossing itself η has a pole,
and that leaves a step, not in the pressure, which rides through because the gap it multiplies is
zero there, but in the loading. On the side the method still owns, η runs to +∞ as the turn
approaches the crossing, so the element sheds its corner’s loading onto its tangent cone’s within
its own length; on the reduced side it holds the corner’s. The two differ, so the reading steps.
On the whole rocket at 4°, measured
either side of that Mach number’s own crossing with a ±1e-9° probe by
a_near_flat_flare_reads_through_and_leaves_only_the_corners_crossing, in
model.rs
(Mach 4.95 rather than 5 because the body’s normal force stops at Mach 5):
| Mach | at a flare of | normal force | center of pressure |
|---|---|---|---|
| 2.00 | 0.000403337° | +0.00032% | −0.0000016 calibres |
| 3.00 | 0.006619249° | +0.011% | +0.00018 calibres |
| 4.00 | 0.023088893° | +0.055% | +0.0017 calibres |
| 4.95 | 0.043584193° | +0.129% | +0.0051 calibres |
A step, not a slope, except on the first row. Widening the probe a hundredfold, to ±1e-7°, leaves the figure where it is at Mach 3, 4 and 4.95, which is what says it is a step rather than the reading’s ordinary movement. At Mach 2 it does not: +0.00032% is already about what the reading itself moves over a ±1e-7° probe there, so that row is an upper bound on the step, not a measurement of one.
Those four numbers are one rocket, and they are not a bound. The step is the loading’s gap at
the pole, so it grows with the length of the element that holds it, and where the region sits
depends on the body ahead. Read on the body alone at Mach 5, either side of that body’s own
crossing (what_the_crossing_costs_is_the_loading_gap_times_the_element_that_holds_it, in
shock_expansion.rs):
| the body | its crossing | C_Nα steps by | its center of pressure by |
|---|---|---|---|
| the tests’ rocket’s body: ogive nose, 0.7 m tube, 0.3 m flare | 0.044650° | +0.40% | 0.064 calibres |
| the same, with a 2 m flare | 0.044650° | +2.61% | 0.761 calibres |
| the same, with the tube cut to 0.1 m | 1.387993° | +4.34% | 0.186 calibres |
| a 10° cone and a 0.3 m tube, 0.3 m flare | 0.695840° | +3.82% | 0.251 calibres |
Read the third and fourth rows twice: with a short shoulder the region is not near-flat at all: it sits between 0.7° and 4.6°, which is where real flares live.
But there is a bound, and it is exact: you can work out your own. At the crossing the two sides take the two constants the method relaxes between: the side it still owns sheds the corner’s loading onto its tangent cone’s at once, Λ_c = tan δ₂ (dC_N/dα)_tc, and the reduced side holds the corner’s, Λ₂ = (λ₂/λ₁) Λ₁. Along a conical flare both are constant, so eq. 19’s C_Nα = (2π/A_ref) ∫ Λ r dx integrates a constant and the whole step is
ΔC_Nα = (2π/A_ref) (Λ₂ − Λ_c) · ½(r_fore + r_aft) · L
for a flare of length L between those radii. flare_reduction_turns_rad returns Λ₂ − Λ_c beside
the turns, as crossing_loading_gap_per_rad; on the tests’ rocket’s body at Mach 5 it is
6.116195e-4 per radian, and the formula reproduces the measured step to a part in 1e5 at flare
lengths of 0.3, 1, 2 and 5 m. So the four rows above are an illustration of that formula, not the
claim: the claim is the formula, and it holds for any conical flare.
The worst of the whole-rocket rows is a 64th of the switch it replaced in the force and a 227th of it in the center of pressure; the step at Mach 4.95 against the switch measured at Mach 3, so that is a comparison of sizes, not of the same flight condition, and the body-alone numbers above are not that small.
And the same pole is crossed in the Mach number. At a fixed flare angle the corner’s crossing sweeps past it as the speed changes, and the table’s rows are 0.05 Mach apart, so a flight reads it as a step between two neighbouring rows. On the short-shouldered body above with a 1° flare, the rows from Mach 2.90 to 2.95 step −2.77% and 0.14 calibres, where the neighbouring rows move by a fifth of that or less. Before M1.8e19: the near-flat flare those lower rows were refused, so the table began above them and the join covered the pole; it is now inside the table. That is the milestone’s trade, and it is the honest description of it: two switches in shape removed, one pole exposed in both shape and speed. On a body whose reduced rows sit at the bottom of its table, what is given up is a continuous join; what is bought is that an arbitrarily small change of shape no longer moves the whole body between two models.
It is not a new question, though: it is the loading through a tangent-cone crossing, which is what a crossing costs inside a segment, and which is open as issue #108: the loading through a crossing. This part of it, at Mach 2.90 to 2.95, is inside the operating envelope’s extended band, so it is part of M1.14h: the extended band; only #108’s part above Mach 4 is deferred (ADR-143). The region’s other edge, the balance, has no step at all: η is zero there, so the exponential form and the generalized method are the same reading, and the two branches meet.
What it leaves out.
- A cylinder’s and a boattail’s reduced elements are still refused, and still take the whole body off the method. Not because a flare relaxes toward something and they do not; a reduced element does not relax at all, on a flare exactly as on a cylinder: it holds the pressure and the loading behind its corner for its whole length. What differs is what it is read against. A widening element’s p_c and Λ_c are a real cone’s at the flow’s own Mach number, so the two readings either side of the region are two readings of one picture and they meet at the balance. A cylinder’s Λ_c is identically zero and its p_c is the free stream’s, so there is no cone there to meet, and a boattail’s is footnote 8’s constant (a stand-in, not a solution of that element’s own flow). Nothing here measures what that would be worth, so the refusal stands: issue #123: a cylinder’s or a boattail’s reduced element. How near is it? An ogive nose on a tube (0.25 m on a 27 mm radius, with tubes of 0.7 m, 3 m and 6 m) marches every row from Mach 1.2 to Mach 5 with nothing reduced, so it is not something a plain rocket walks into. What does hit it is TN D-4865’s own Newtonian start on the Arcas Robin’s nose from Mach 3.96, which is a reason hpr does not use that start (The two starts).
- The generalized method is the older, rougher one. Reading an element with it is a real choice, not a formality, and TN 3527 does not say it is what it would have done. What is checked here is that the choice joins the second-order reading continuously at the balance and that it makes the reading smooth in the flare’s angle, not that it is closer to a measured flare. No measured flare in this band exists.
- Below about a millionth of a radian the flare is not drawn at all. Corners turning by less than that are merged into the element ahead of them, because two tangents that nearly coincide meet at an ill-conditioned point. That covers every flare shallower than about 0.00006°, which at the bottom of the table’s range swallows both turns: at Mach 1.2 the crossing is 0.00000023° and the balance 0.00000018°, so there is no corner there to reduce and nothing the reading could switch on.
- Only the flare’s own element was measured. The two turns are solved for a corner behind a body; where a body has several corners that could be reduced at once, nothing here says how their readings combine. Nor does anything here say whether the generalized reading or the slender-body fallback it replaced was the nearer of the two to the air: what is claimed is that one of them moves smoothly with the shape and the other jumps.
- There is no rule of thumb for “is my flare one of these?”. The region belongs to the corner,
so the only way to ask is to run the model:
ShockExpansionBody::aft_flowon the body ahead of the flare, at the Mach number you care about, thenflare_reduction_turns_radon what it returns.
A step in radius
In short: a step is a joint where one part’s radius does not match the next one’s, so the rocket’s outline jumps rather than bending: a 54 mm tube butted straight onto a 75 mm one, or a coupler left standing proud of the airframe. That stops hpr’s second-order shock-expansion method, the march: it walks a chain of straight elements, the tangent body, from the nose tip aft, and it needs an outline without a jump in it. What hpr does about that is drop the whole body to slender-body theory, at every speed: a rocket with a step reads as though the method did not exist. M1.8e15 measured what that costs and tried the obvious fix; the fix was worse, so the behavior is unchanged and the cost is published instead. The decision record is ADR-049; the work left over is issue #87: a step takes the whole body off the method, open and not on the roadmap. Nothing measures a stepped body faster than sound, so neither the present reading nor any replacement has a reference.
What it costs. On the tests’ straight rocket (a tangent-ogive nose 0.25 m and three tubes of
0.7, 0.05 and 0.3 m, all 27 mm in radius) at Mach 3 and 4°, with the reference diameter pinned at
54 mm so that every row is divided by the same area. With no step it reads C_N = 0.899592, its
center of pressure 16.9492 calibres aft of the nose tip. Each row
is the change from that. Down means the body narrows from that joint aft, up that it widens:
| the step, and which joint it is at | normal force | center of pressure |
|---|---|---|
| down 2.8e−11 m, the first size measured past the threshold, at any of the three joints | −8.65% | +1.0285 calibres |
| down 1 mm, at the nose’s joint | −10.62% | +1.1938 calibres |
| down 1 mm, at the last joint | −10.29% | +0.9949 calibres |
| down 2 mm, at the nose’s joint | −12.55% | +1.3593 calibres |
| down 2 mm, at the last joint | −11.89% | +0.9597 calibres |
| up 2.8e−11 m, at any of the three joints | −8.65% | +1.0285 calibres |
| up 1 mm, at the nose’s joint | −6.72% | +0.8674 calibres |
| up 1 mm, at the last joint | −7.05% | +1.0644 calibres |
| up 2 mm, at the nose’s joint | −4.75% | +0.7064 calibres |
| up 2 mm, at the last joint | −5.41% | +1.0987 calibres |
At the threshold the shape is flush to a part in a billion either way, so the whole difference there
is the method itself, the same wherever the step sits and whichever way it goes. Past that the
shape itself starts to matter, and the two directions part: a step down takes area off the body
and costs more, a step up adds area that carries slender-body normal force of its own and costs
less. The center of pressure moves aft in every row, so a rocket that trips this reads more
stable than the same rocket drawn flush. That extra margin is more likely optimistic than real: the
model it falls back to reads 15% to 50% below the wind tunnel on the Arcas Robin’s body faster than
sound (The body faster than sound in a flight). If you
can draw the joint as a short transition instead of a butt joint, the body keeps the method; hpr’s
own radius_step warning (the design model’s checks) is what tells you a design
has tripped this.
a_step_takes_the_whole_body_off_the_method pins every figure in this section and the flush
rocket’s own readings with it; Checking a claim says how to run a named
test.
Where the threshold is. It is a pair, not one number, and which of the two binds depends on the joint:
| the joint | what binds | on the tests’ bodies |
|---|---|---|
| radius changes, slope does not (tube to tube), either direction | the tangent body merges two elements whose radii agree to a billionth of the radius | 1e−9 × 27 mm = 2.7e−11 m |
| slope changes too (a step up at a boattail’s fore end) | the elements’ corners have to stay in order along the body | 1e−12 × 1.3 m × 0.1 = 1.3e−13 m |
Both are bisected. The second is 208× finer, and it depends on the body’s length and the change of slope rather than on its radius, so it is not a property of the step at all. It bites on the commonest high-power shape there is: on the tests’ finned rocket, a boattail whose fore radius is 27.0000000000002 mm rather than 27 mm loses the method for the whole body, worth −11.34% and 1.0951 calibres, larger than the tube-to-tube switch above.
Neither number is a judgement about steps. They are the widths of the rounding the tangent body can absorb, and every step anyone could build or draw is far past both, so in practice a step always takes the body off the method. (A third, much looser test, the run’s own coverage gate, which asks how much of the body the method can cover, at a millionth of the fore area or 13.5 nm of radius here, is what actually refuses every step bigger than that. It gives the same reading, and the test pins which of the two owns which range.)
What a fix has to handle. The obvious fix is to stop the march at the step and let the body ahead of it keep the method, the way the run already ends at a flare. That was built and measured, and it failed three ways. The numbers in this list were taken on that prototype, which was not kept: unlike the tables above, no committed test reproduces them, and ADR-049 records how each was measured and where the prototype lives.
- What is behind the step. ADR-034, the decision that the method covers a body or nothing, rejected mixing the two models on a measured case. A step’s remainder is supposed to be a plain tube, but “the run stopped at a step” does not make it one: with a boattail behind the step, the mixture’s center of pressure lands at 16.7209 calibres, forward of both pure models, the method’s 16.7286 and slender-body theory’s 17.8237. A reading outside the envelope of both models it is made of is the pathology ADR-034 measured.
- It does not close the band it was meant to close. The prototype removes the step’s switch at a tube-to-tube joint, but at a joint whose slope changes it is the corner ordering that refuses the body, at 1.3e−13 m, so a boattailed rocket still loses the method, worth −11.34% and 1.0951 calibres. The commonest shape it was supposed to help is the one it does not.
- Which shape stopped the run, not whether the joint was flush. The prototype keyed off the joint: any reason the run closed (a non-conical flare, a lip out of its wake) kept the forebody marched as soon as its fore radius was a picometer off, a new jump of +7.2% and 0.69 calibres where today the reading is continuous. That one is a property of how the prototype was built rather than of the idea, and a fix keyed off the shape would not have it; it is listed here because it is what a fix has to get right, not as evidence the idea cannot work.
What it leaves out.
- The step’s own force is slender-body theory’s, at any speed.
(2/A_ref)ΔAat the joint, the limit of a transition whose length goes to zero; [B67] p. 18 assumes no discontinuities, so this goes beyond its source, and there is no compressibility term. A forward-facing step at supersonic speed stands a detached shock with a separated pocket ahead of it; none of that is modeled. - No source gives a stepped body’s normal force faster than sound. MIL-HDBK-762 treats a rearward-facing step only as base drag, TN 3527 ([SD56]) needs a continuous profile, and nothing else pinned here covers one. So “take the whole body off the method” is not known to be right either; it is the reading that does not mix two models, which is the only argument for it.
- One body, one placement sweep. Three joints on one rocket at one Mach number and one angle, and the large steps up carry a moving reference diameter with them.
Blunt tips
What this covers: the body faster than sound when the nose’s tip is blunt or vertical, as on
power-series noses with n below 1, Haack series (the von Kármán and L-V Haack) and elliptical
noses, whose profile leaves the tip at 90°. How far to trust it: the cap comes from a NASA method
checked only on spherical caps. On that report’s own sphere-cone, compared as its tunnel measured
it (at the plotted angles, body lift included), hpr reads −1.2% to +32.1%: close through Mach 2.3,
high from Mach 2.96, where the report’s own method reads +5.2% to +13.6%. On the Arcas Robin’s
power-series nose the cap is an extrapolation. There, like for like, the body reads +37.2% at Mach
1.5 and +13.7% to +25.9% from Mach 1.8 to 2.96, and within 5% past Mach 3. Against the smooth
secant ogive fitted to the same nose it reads lower at every Mach number: closer to the tunnel at
nine of the eleven rows, by 0.7 to 5.8 points, and past Mach 4 it crosses into under-prediction and
lands 0.6 to 1.5 points further out. A nose that is
nearly a cone but for a vanishing tip carries a bias nothing here measures
(issue #101, below). No validation flight reaches
the speeds where any of this applies.
Why a cap. The shock-expansion method replaces the nose by straight elements, short cones and frustums each tangent to the profile, and starts at a pointed tip, where the air flows as it does over a cone. A vertical tip has no such cone: the shock stands off the nose, and the air just behind it is slower than sound. Jackson, Sawyer and Smith ([J68]) handled blunt noses by giving the tip’s cap Newtonian pressures and handing over to the method where the flow behind the cap is fast again, the handover. hpr does the same.
The cap. Newtonian theory takes the pressure from the angle δ between the surface and the
wind: C_p = C_p,max sin²δ ([J68] eq. 1, p. 5). C_p,max is the pressure coefficient at the
stagnation point, the tip, where the air comes to rest behind a normal shock: it follows from the
pitot pressure a probe would read there, the Rayleigh pitot formula ([R1135] eq. 100). At a small angle of
attack the windward side meets the wind a little more steeply, so the cap carries
C_p,max sin δ cos δ of loading in the method’s terms (a hemisphere then carries its Newtonian
drag turned into the body’s axes, C_p,max/2, as it must; test
a_hemisphere_carries_its_drag_turned).
The handover. The method takes over where the surface’s slope falls to the largest angle a wedge can turn the flow through with its shock attached: 12.1° at Mach 1.5, 22.97° at Mach 2 ([R1135] eqs. 138 and 168). The report chose this point “simply because it gave the best agreement with the available data in the low supersonic-speed range” ([J68] p. 5). hpr caps the handover at 24°, which the wedge’s angle passes at Mach 2.06. The cap is there because the method needs the normal-force slope of a cone tangent to the body, and TN 3527’s chart stopped at 24° ([SD56] Fig. 2). Those slopes now reach 30° (ADR-042) and the cap has not followed, because the method’s march does not carry it that far. What the cap is worth measures what moving it would buy and what it would cost.
How much of the nose the cap covers depends strongly on speed. Where it ends, as a share of the nose’s length and of its base radius:
| nose | Mach 1.25 | Mach 1.5 | Mach 2 | Mach 3 |
|---|---|---|---|---|
| arcas robin, the committed nose | 59.1% / 0.72 | 5.8% / 0.16 | 0.9% / 0.05 | 0.8% / 0.05 |
| von Karman, five calibres | 47.8% / 0.69 | 4.1% / 0.12 | 0.3% / 0.02 | 0.2% / 0.01 |
| power series n = 0.5, five calibres | 29.2% / 0.54 | 5.4% / 0.23 | 1.4% / 0.12 | 1.3% / 0.11 |
| elliptical, two calibres | 65.3% / 0.94 | 34.9% / 0.76 | 13.9% / 0.51 | 12.8% / 0.49 |
| TN D-4865’s sphere-cone (model 1) | past the sphere: the method doesn’t hold | 7.8% / 0.34 | 6.0% / 0.32 | 5.9% / 0.32 |
Near the join’s start a slender nose leans on Newtonian pressures over far more of itself than anything the report checked; by Mach 2 the cap is a percent or so of the nose, less than the report’s own. The join’s weight rises from 0 at its start to 1 a third of a Mach number later, which damps that, but read a vertical tip’s numbers between the join’s ends as the blend they are.
A power-series nose meets its base at a slope of n/(2f), with f its length over its diameter,
and the cap can’t end on the nose while that is steeper than the handover’s angle. So such a nose
takes the method no earlier than the Mach number where the handover’s angle passes its base’s,
and its join to slender-body theory starts there rather than at Mach 1.2: for n = 0.5, Mach 1.23 at
3 diameters long, 1.49 at 1.2 diameters; the Arcas Robin’s from 1.22. One shorter than n/0.89
diameters (0.56 for n = 0.5) never takes it, since the handover stops at 24°, and keeps
slender-body theory: hpr doesn’t warn, and
AeroModel::supersonic_body
returns None. Haack and elliptical noses end level, so the cap always ends on them.
A blunt nose can be more than one shape, and the cap may end on any of them. Since M1.8e18, a nose that starts with a sphere carries on through the curved, widening shapes behind that sphere, and hpr looks for the handover along all of them rather than in the sphere alone. TN D-4865’s own model 2 needs it: its nose is a sphere blended into a 2.75° cone by a second arc, and the sphere is still at 38.3° where the arc takes over, steeper than the 24° cap at any speed, so the handover always falls on the arc (What a marched flare is worth).
Everything else reads exactly as it did before, on purpose. A pointed nose is one shape however many curved shapes follow it, so a curved transition behind one is still the afterbody. And the search stops at the first shape that is straight or narrows: a cap that reached a cylinder or a boattail would hand the flow over at no angle at all, with none of the total pressure the tip took out of it, so a nose steeper than the handover all the way to one is still refused, which is the case in the paragraph above, and its numbers are unchanged.
Behind the handover. hpr starts the method there as it starts at a pointed tip, with the flow on the tangent cone, the cone that touches the body at the handover. The report starts it from the Newtonian pressure instead. What that choice is worth is set out in The two starts, after the checks below.
A worked example. The report’s sphere-cone at Mach 1.5: a nose radius of 0.175 base diameters
on an 11.5° cone. The pitot pressure is 3.413 times the free stream’s, so C_p,max = 2.413/(γM²/2) =
2.413/(0.7 × 1.5²) = 1.532. The wedge’s largest angle is 12.11°, reached on the sphere
0.175 (1 − sin 12.11°) = 0.138 diameters behind the tip. On a sphere, with θ the angle from the
tip, the loading C_p,max sin δ cos δ integrates over the cap to C_p,max sin⁴θ/2 on the
sphere’s own cross-section; to θ = 90° − 12.11° that is 0.700, and 0.086 on the base (times
0.35²). The cone behind it, marched from the flow on a 12.11° cone, carries the other 1.595 of
hpr’s 1.681.
How it was checked. Against the report’s own model 1, measured at Mach 1.50 to 4.63 ([J68]
Fig. 8(a), p. 101, read from the scan by pixel analysis to about ±0.003, the plotting itself
good to about ±0.01), compared as ADR-036 compares the Arcas Robin: hpr’s C_N at the
plotted 0° to 12°, the method’s slope with body lift (Jorgensen’s, for a body of
fineness 1.75, shorter than his Fig. 4 covers), fitted with a straight line just as the measured
C_N and C_m are, and the report’s own method fitted the same way; per radian on the base, the
center of pressure in base diameters from the tip:
| Mach | C_Nα measured, per rad | the report’s method | vs measured | hpr | vs measured | CP measured, diameters | hpr |
|---|---|---|---|---|---|---|---|
| 1.5 | 1.908 | 1.844 | −3.3% | 1.885 | −1.2% | 1.00 | 1.03 |
| 1.9 | 1.926 | 1.964 | +2.0% | 1.926 | +0.0% | 1.03 | 1.02 |
| 2.3 | 1.833 | 1.989 | +8.5% | 1.971 | +7.5% | 1.03 | 1.02 |
| 2.96 | 1.786 | 1.878 | +5.2% | 2.009 | +12.5% | 1.06 | 1.02 |
| 3.95 | 1.618 | 1.800 | +11.3% | 2.099 | +29.7% | 1.05 | 1.03 |
| 4.63 | 1.550 | 1.762 | +13.6% | 2.048 | +32.1% | 1.09 | 1.03 |
Past Mach 2.3 hpr reads high twice over. At Mach 3.95 and 4.63 its slope at α → 0 is 13% to 21%
above the measured one, and its body lift lifts its fitted slope 25% to 27% above that, where the
measured curve rises only 9% to 16% above its own. The slopes at α → 0, beside it and not
judged, with the measured one fitted with a curve two ways, as the decision record on comparing
with a wind tunnel, ADR-036, asks:
| Mach | measured, α|α| fit | measured, α³ fit | hpr | hpr from the report’s start |
|---|---|---|---|---|
| 1.5 | 1.741 | 1.810 | 1.681 | 3.392 |
| 1.9 | 1.979 | 1.952 | 1.711 | 1.960 |
| 2.3 | 1.657 | 1.717 | 1.713 | 1.792 |
| 2.96 | 1.646 | 1.684 | 1.702 | 1.700 |
| 3.95 | 1.433 | 1.484 | 1.678 | 1.625 |
| 4.63 | 1.339 | 1.419 | 1.613 | 1.484 |
And the Arcas Robin’s committed design, its power-series nose, cylinder and boattail with the lip left off, to show the cap’s own effect (the lip itself carries nothing: A lip in a boattail’s wake), through a flight’s path, fitted at the tunnel’s plotted angles as Checking the shock-expansion method fits them, beside the secant ogive fitted to the same nose:
| model | Mach | measured | committed nose | vs measured | fitted ogive | vs measured | cap to r/R |
|---|---|---|---|---|---|---|---|
| short | 1.5 | 2.192 | 3.007 | +37.2% | 3.090 | +41.0% | 0.163 |
| short | 1.8 | 2.613 | 3.289 | +25.9% | 3.312 | +26.8% | 0.070 |
| short | 2.3 | 3.078 | 3.597 | +16.9% | 3.651 | +18.6% | 0.045 |
| short | 2.96 | 3.284 | 3.837 | +16.8% | 3.936 | +19.8% | 0.045 |
| short | 3.96 | 3.884 | 3.944 | +1.5% | 4.168 | +7.3% | 0.045 |
| short | 4.63 | 4.149 | 3.948 | −4.8% | 4.288 | +3.4% | 0.045 |
| long | 1.8 | 3.159 | 3.769 | +19.3% | 3.792 | +20.0% | 0.070 |
| long | 2.3 | 3.525 | 4.070 | +15.4% | 4.124 | +17.0% | 0.045 |
| long | 2.96 | 3.868 | 4.398 | +13.7% | 4.497 | +16.3% | 0.045 |
| long | 3.96 | 4.455 | 4.426 | −0.7% | 4.655 | +4.5% | 0.045 |
| long | 4.63 | 4.615 | 4.424 | −4.1% | 4.777 | +3.5% | 0.045 |
The rows are in
validation/fixtures/aero/blunt-tips.json,
written by cargo xtask aero; the readings in
tn-d-4865-sphere-cone.json.
A test holds these tables to the fixture, cell by cell. The committed nose reads within 3.8
points of the fitted ogive up to Mach 2.96 and 5.1 to 8.2 points below it past Mach 3. Below it is
not always closer: past Mach 4 its error changes sign, so at Mach 4.63 it reads −4.8% where the
ogive reads +3.4%, 1.5 points further from the tunnel. Over the eleven rows it is nearer at nine. Below Mach 3 both read high, most at Mach 1.5, for
the reasons in Checking the shock-expansion method.
What the cap is worth
A nose with a blunt or vertical tip flies Newtonian pressures over the tip and hands the rest of the body to the shock-expansion method where the surface’s slope falls far enough, the handover (Blunt tips above). That handover is never steeper than a cap, 24°. This section measures what moving the cap to 30° would buy and what it would cost. Nothing here changes what a rocket flies: the cap is where it was, and no committed number moved.
The cap could move because the cone slopes the method reads now reach 30° (ADR-042),
where they once stopped at 24°. Moving it would follow the report’s own rule further: the wedge’s
largest deflection passes 24° at Mach 2.06 and 30° at Mach 2.52, so a 30° cap keeps TN D-4865’s
([J68]’s) rule over that whole band, where 24° cuts it short from Mach 2.06 up. Where a cap binds at
all, going all the way to 30° reads nearer the report’s own sphere-cone at every row, though not at
every step of the way. And on the Arcas Robin’s committed nose it breaks the march (the method
stepping element by element down the body from the handover) above Mach 4. hpr keeps 24° until that
is settled (ADR-043), and the cap is a parameter of the method rather than a constant to
argue over
(with_handover_cap_rad).
Each cap starts to bind at its own speed (Mach 2.06, 2.19, 2.34 and 2.52), and below that it
costs nothing at all. hpr’s error against TN D-4865’s sphere-cone, fitted as
How it was checked fits it (hpr’s C_N at the tunnel’s plotted 0° to 12°, body
lift included, fitted with a straight line), under four caps:
| Mach | error at 24°, as flown | at 26° | at 28° | at 30° |
|---|---|---|---|---|
| 1.5 | −1.2% | −1.2% | −1.2% | −1.2% |
| 1.9 | +0.0% | +0.0% | +0.0% | +0.0% |
| 2.3 | +7.5% | +7.3% | +7.1% | +7.1% |
| 2.96 | +12.5% | +12.2% | +11.9% | +11.5% |
| 3.95 | +29.7% | +28.9% | +28.3% | +28.0% |
| 4.63 | +32.1% | +31.2% | +30.9% | +31.3% |
Below Mach 2.06 no cap binds, which is why the first two rows are one reading four times; at Mach 2.3 only 24° and 26° bind, so the last two columns agree. Read the first two binding rows with care for another reason: the cone slopes are tabulated from Mach 3 up and held at that row below it (Bodies faster than sound), so at Mach 2.3 and 2.96 a steeper cap’s gain is read off a slope that is not itself a function of Mach there. Where a cap does bind, the whole step from 24° to 30° reads nearer the tunnel by 0.4 to 1.7 points, most at Mach 3.95. The one place a steeper cap reads further out is the last step at Mach 4.63, where 28° reads +30.9% and 30° +31.3%.
Now the cost, which two counts tell you about. A reduced element is one where the method’s
exponential law would run the wrong way: the pressure behind the corner heading away from the
tangent cone’s instead of toward it, η < 0 in the method’s own terms
([SD56] p. 13), so hpr holds the pressure along it instead,
issue #81’s open question. A crossing is the
rarer and worse thing: the marched surface pressure passing through its own tangent cone’s, either
way, within one part of the body. Where a nose meets a cylinder, a boattail or a flare the cone’s
own pressure steps, which is not the same thing and is not counted. Both counts are per march, not
per element.
What a crossing is, and what it costs says how they
differ and how far either one can be trusted
(tangent_cone_crossings).
Here are two of the four caps on the Arcas Robin’s committed power-series nose and the short
model’s cylinder, nothing aft, C_Nα per radian on its cross-section at α → 0, read with the
flown 10 elements per curve and with 160:
| Mach | 24°, 10 elements | 24°, 160 | 30°, 10 elements | 30°, 160 |
|---|---|---|---|---|
| 1.5 | 2.531 | 2.532 | 2.531 | 2.532 |
| 1.8 | 2.697 | 2.702 | 2.697 | 2.702 |
| 2.3 | 2.873 | 2.881 | 2.851 | 2.862 |
| 2.96 | 3.021 | 3.029 | 2.943 | 2.955 |
| 3.5 | 3.071 | 3.079 | 2.953 | 2.964 |
| 3.96 | 3.073 | 3.080 (1 of 160 reduced) | 2.919 | 2.927 (1 of 160 reduced) |
| 4.63 | 3.030 | 3.034 (2 of 160 reduced) | 3.047 (5 of 10 reduced, 2 crossings) | 3.260 (109 of 160 reduced, 2 crossings) |
| 5 | 2.980 | 2.984 (2 of 160 reduced) | 3.400 (9 of 10 reduced, 1 crossing) | 3.454 (145 of 160 reduced, 1 crossing) |
Through Mach 3.96 cutting the nose into sixteen times as many elements moves the answer by under 0.01 per radian under the flown cap and under 0.013 under the 30° one: the answer is the model’s, not the mesh’s. Above it the 30° cap’s march crosses its tangent cone twice, and reduces most of the nose along with it (5 of 10 elements at Mach 4.63 and 9 of 10 at Mach 5, which is what hpr would fly, and 109 and 145 of 160), and the answer follows the element count instead, and not even in order: 3.047 at 10 elements, 2.928 at the 40 the fixture also holds, and 3.260 at 160, a spread of 0.33 per radian, 11%, where the flown cap moves by 0.1%. The crossing is the cause and the reductions travel with it, which the next section takes apart.
What a crossing is, and what it costs
Throughout this section, p₂ is the surface pressure just behind a corner and p_c its tangent
cone’s, Λ the lift per unit length (the loading) and Λ_c the tangent cone’s, all as
the method’s equations define them.
The steeper cap starts the march from a steeper cone at a higher pressure, and from there the tangent cone’s own pressure falls away faster than the marched pressure does as the nose flattens. So the surface pressure catches its tangent cone’s and passes through it (at Mach 4.63 under the 30° cap, about a tenth of the way back) and stays above it until the nose flattens enough for the cone to catch up again. That is two crossings: one out, one back.
Why that hurts has nothing to do with η < 0. Write the exponent in e^(−η) as η = k (x − x₂),
so that k = (∂p/∂s)₂ / ((p_c − p₂) cos δ₂) is a relaxation rate per meter: the gradient just
behind the corner divided by how far the pressure has to go. A crossing closes that gap while the
gradient carries on, so k has a pole: it runs to infinity. The pressure itself doesn’t mind,
because k (p_c − p) cos δ₂ is only the gradient again, and that stays finite. The loading does
mind, because it relaxes toward Λ_c at the same k ([SD56] eq. 19) while its own gap is set by
something else entirely.
Follow that gap through the Mach 4.63 march under the 30° cap. Between the two crossings nearly
every element is reduced, so its loading is held where it was and never relaxes: by the second
crossing Λ stands about a quarter above Λ_c. The element at that second crossing is back
inside the method, and how much of that quarter it sheds in its own length is whatever
1 − e^(−η) happens to be for the step the mesh gave it. On the 40-element march it sheds 98% of
the gap in one step; on the 160-element march, 12%. That is the answer moving with the mesh, in one
number (test a_crossing_is_a_pole_in_the_rate_the_march_relaxes_at).
The counts say the same thing over the whole sweep. Of its thirty-two readings (four caps at eight Mach numbers on this one nose), twenty-seven never cross and five do:
| readings | most the answer moves over 10 → 160 elements | |
|---|---|---|
| no crossing | 27 | 0.012 per radian |
| a crossing | 5 | at least 0.035, up to 0.69 |
No overlap, and nearly three times (2.9×) between the two groups. The five are 30° and 28° at Mach
4.63 and 5, and 26° at Mach 5 (fixture
blunt-tips.json,
test over_the_sweeps_meshes_a_crossing_separates_the_readings_that_move).
A crossing is a flag, not a verdict, and it has to be read carefully. Three limits, all measured:
- A crossing does not prove an answer never settles. 28° at Mach 5 crosses at every mesh, and
its 0.69 spread is all in the coarse end: from 60 elements to 640 it holds to 0.005 per radian,
tighter than the worst reading in the sweep that never crosses. Compare 30° at Mach 4.63, which
moves by more than 0.2 per radian over that same range. What the sweep shows is that across its
three meshes (the flown ten elements per curve and refinements to forty and a hundred and sixty)
the crossings and only the crossings mark the readings that move (test
a_crossing_says_the_answer_moved_not_that_it_never_settles). - A count of zero does not prove one settled. Whether a crossing is seen depends on the mesh: 26° at Mach 5 and 28° at Mach 4.63 show none at the flown ten elements per curve and two at forty and a hundred and sixty, and both move. Read zero as “not proven”.
- hpr does not count crossings while it flies, and everything here is one nose. The count is a tool for studying a body, not a guard, and another blunt nose above Mach 4 could cross under the flown 24° cap without saying so.
Reduced elements, on their own, do not move an answer. TN 3527’s own fineness-3 ogive reduces 27 of
160 elements at Mach 5.05 and 50 of 160 at Mach 6.28, and its answer settles to 0.002 per radian
from 10 elements to 160. There η < 0 comes from the gradient changing sign, with the surface
pressure below its tangent cone’s the whole way down; the gap never closes, so there is no pole.
That ogive never crosses at either Mach number we can check it against the report at, which is why
the report could state its condition ([SD56] p. 13) and stop: it never had to say what a crossing
does (test a_reduced_element_settles_where_tn3527s_own_bodies_never_cross).
The two questions are tangled, though, and that is the state of play. The quarter-wide loading gap
the second crossing sheds was opened by the reduced stretch behind it, which is hpr’s η = 0
reading, not the report’s rule. So a different reading of η < 0 would change the size of the step
as well, and neither question can be judged without the other. What has changed is that the step
itself is taken by an element the method still owns, so a rule for η < 0 alone is not obviously
enough.
The disorder under the steeper cap is not rounding: nudge the Mach number by eight units in its
last place (about a part in 10^15) and the same elements reduce, for an answer that follows to a
part in a billion (test a_steeper_handover_moves_the_march_out_of_its_range).
The break is not at 30°, and it is not orderly. It sits between the flown cap and the next step, and 28° is the worst of the four: at Mach 5 its answer moves 0.69 per radian over the element count, twelve times the 30° cap’s 0.055. No cap above the flown one holds its answer to Mach 5:
| cap | Mach 4.63, 10 elements | 160 elements | Mach 5, 10 elements | 160 elements |
|---|---|---|---|---|
| 24°, as flown | 3.030 | 3.034 (2 of 160 reduced) | 2.980 | 2.984 (2 of 160 reduced) |
| 26° | 2.961 | 2.965 (2 of 160 reduced) | 2.900 | 2.946 (40 of 160 reduced, 2 crossings) |
| 28° | 2.892 | 2.926 (33 of 160 reduced, 2 crossings) | 2.923 (4 of 10 reduced, 2 crossings) | 3.612 (140 of 160 reduced, 1 crossing) |
| 30° | 3.047 (5 of 10 reduced, 2 crossings) | 3.260 (109 of 160 reduced, 2 crossings) | 3.400 (9 of 10 reduced, 1 crossing) | 3.454 (145 of 160 reduced, 1 crossing) |
Below Mach 4 the four agree to 0.013 per radian, so nothing here says a cap between the two ends is a middle ground. It says the flown cap is the last one whose answer is the model’s all the way to Mach 5.
Settled is not the same as right. Under the flown cap hpr still reads +29.7% and +32.1% against the sphere-cone at Mach 3.95 and 4.63, as the first table says. The cap chooses between an answer that is high and one that is high and moves with the mesh.
So the cap hpr flies is set by the march’s range rather than by a chart’s edge. It moves when the
method has a rule for what the loading does where the surface pressure crosses its tangent
cone’s: a rule whose answer stops changing as the nose is cut finer, judged together with the
reading of η < 0 that sets the gap it sheds. TN 3527 does not state either, because its own
bodies never cross, so this is a modeling decision rather than a measurement to look up (issue #108: a steeper handover crosses the tangent cone above Mach
4). The milestone that would then move the
cap, M1.8e16, the blunt tip’s handover past 24°, waits on
that (ADR-044, which records the measurement behind this section). Both are deferred
as beyond the operating envelope, since the crossing bites
above Mach 4; the vertical-tip switch’s error below that is part of
M1.14d: supersonic accuracy, and of
M1.14h: the extended band above Mach 2.5
(ADR-143).
All three tables are held to
blunt-tips.json
by a test, cell by cell; the second shows the two ends of the sweep, and the fixture holds 26° and
28° and a 40-element reading too. Its numbers are stored to six decimals, three more than the tables
quote: where most of the nose is reduced the march is not reproducible past about 1e-11 from one
machine’s maths library to another’s, so pinning more would only break the build
(ADR-043).
The two starts
hpr starts the march from the tangent cone at the handover, the report from the Newtonian pressure
there. Read at a small angle, the report’s start fails on
the Arcas Robin’s nose from Mach 3.96, where the march reduces the element at the nose’s end
(holding its pressure where the method’s exponential law would run the wrong way) on the
cylinder behind it, whose tangent cone is the free stream rather than a cone of its own, and
which hpr therefore refuses (issue #123: a cylinder’s or a boattail’s reduced
element); from Mach 2.96 its answer drifts as the
nose is cut into more elements, for the same reason
(issue #81, the method’s open question there). A flight’s table is built from Mach 5
down, so that failure would leave such a rocket no method at all. The tangent cone’s start holds
to Mach 5 and settles: the Arcas nose moves under 0.01 per radian from 10 elements to 40, a
five-calibre elliptical or von Kármán nose under 0.02 from 10 to 160. On the report’s sphere-cone, against the
measured slope at α → 0, the report’s start reads closer than hpr’s at Mach 1.9, 3.95 and
4.63, about the same at 2.96 and further at 2.3, and 95% high at Mach 1.5. That last is hpr’s
reading of the report’s start at α → 0, not the report’s method, which reads 1.844 there at
its own angles: the handover sits 0.6° above the cone, so its linear range is that small. hpr
takes the start that holds and settles everywhere over one that fits one body better where it
holds. Both are kept:
HandoverStart selects the report’s
for comparison (ADR-038). Slopes per radian on the body’s cross-section, at α → 0, for
the committed nose and the short model’s cylinder, nothing aft:
| Mach | the tangent cone’s start, 10 elements | 40 elements | the report’s start, 10 elements | 40 elements |
|---|---|---|---|---|
| 1.5 | 2.531 | 2.532 | 2.866 | 2.770 |
| 1.8 | 2.697 | 2.701 | 2.676 | 2.635 |
| 2.3 | 2.873 | 2.880 | 2.641 | 2.635 |
| 2.96 | 3.021 | 3.028 | 2.544 | 2.583 (6 of 40 reduced) |
| 3.5 | 3.071 | 3.078 | 3.361 (9 of 10 reduced) | 3.418 (39 of 40 reduced) |
| 3.96 | 3.073 | 3.080 | fails | fails |
| 4.63 | 3.030 | 3.031 (1 of 40 reduced) | fails | fails |
| 5 | 2.980 | 2.982 (1 of 40 reduced) | fails | fails |
What it means for a rocket. Mostly more force, barely any change of balance. On Calisto, whose von Kármán nose now takes the method past Mach 1.2, the whole rocket’s normal-force slope rises 17% to 31% from Mach 1.5 to 2 (Normal force through Mach 1), while its center of pressure moves by under 0.15 calibres (forward at Mach 1.5, aft at Mach 2). So the stability margin moves by under a sixth of a calibre, and the force that holds the rocket into the wind grows by about a quarter.
What it leaves out.
- No measurement checks a tip that isn’t spherical. The report tested spherical caps only. Newtonian theory on the cap and the tangent cone’s start are both approximations, and the method’s reduced elements (issue #81) still apply behind them.
- A cap that shrinks to nothing doesn’t reach the cone it sits on
(issue #101). The march carries its start
cone’s total pressure the whole way, as the method does from any vertex, and nothing makes that
fade as the cap shrinks. A power-series nose of
n= 0.99 is a 7.1° cone but for a tip 1e-55 calibres across, yet at Mach 4 its cylinder carries 1.21 per radian where the cone’s carries 1.37, 12% less, because the march runs on the 24° cone’s total pressure rather than the 7.1° cone’s. The shapes a rocket really uses have caps that are small but not vanishing (at Mach 1.5 the table above puts their ends at 0.12 to 0.76 of the base radius, where that nose’s is 1e-55), and how much of this bias they carry is unknown. - At
α → 0the handover is held where it sits on the body, as TN 3527 holds every other point. The report’s equivalent bodies turn the body about the sphere’s center, which slides the handover along the surface instead; hpr leaves that term out. How much it is worth is not measured here. The two starts in the tables above differ by more than it alone, since their pressure and total pressure differ too: 2.53 against 2.87 per radian on the Arcas nose at Mach 1.5, and 1.70 against 1.70 on the sphere-cone at Mach 2.96. - Two switches in shape, of the family issue #87
tracks: a vertical-tip nose steeper than the cap’s handover all the way to its base gets no
method at all, and a pointed tip steeper than the cone tables’ 30° is refused where a vertical
one flies. The pointed tip’s edge was Fig. 2’s 24° until
M1.8e11. The vertical tip’s edge is the handover’s cap,
which stands at 24° for the reason above. Moving the cap waits on
issue #108: a steeper handover puts the march into
η < 0above Mach 4, which is deferred; the switch’s error itself is part of M1.14d: supersonic accuracy (and of M1.14h above Mach 2.5). - Elements that merge, merge with Mach. Behind the cap, a tangency point whose tangent turns by under a microradian is folded into the element before it, because its corner can’t be placed in floating point. Which points merge changes with the handover, so the method’s answer takes a step of about a millionth of a per-radian slope as it does: far below anything measured here, but there.
- Drag is unchanged: the nose’s wave drag already covers blunt shapes (Drag through Mach 1).
Fins
A fin set is N identical fins spaced evenly around a body tube. For one fin of the set:
| symbol | meaning | unit |
|---|---|---|
s | span: the fin’s height from the body surface to its tip | m |
y | height above the root, from 0 to s | m |
c, c_r, c_t | chord: the fin’s length along the airflow at height y; at the root and at the tip | m |
x_LE, x_t | how far the leading edge sits aft of the root’s leading edge, at height y; at the tip (the sweep length) | m |
A_fin | one fin’s area, one side (written A inside the integrals) | m² |
Γ_c | the mid-chord sweep: the angle by which the line joining the chords’ midpoints leans aft from square to the body | rad |
β | the Prandtl–Glauert factor √(1 − M²), the classic correction for the air’s compressibility below Mach 1: 1 at rest, falling to 0 at Mach 1 | none |
c̄, y_MAC, x_MAC,LE | the mean aerodynamic chord (MAC), an average chord that weights long chords more; its height; and its leading edge, aft of the root’s | m |
X_f | the fin set’s CP, aft of the root leading edge | m |
Λ_k | the angle between fin k and the direction the air crosses in (set by the flow roll φ) | rad |
f_N | the fin-count factor, for five to eight fins | none |
r_t | the body tube’s radius at the fins | m |
K_T(B) | the interference factor: how much the body raises the fins’ normal force | none |
| term | formula | source |
|---|---|---|
| one fin | (C_Nα)₁ = 2π (s²/A_ref) / (1 + √(1 + (β s²/(A_fin cos Γ_c))²)), β = √(1 − M²) | [B67] eq. 3-4–3-6, [N09] eq. 3.38–3.40 |
| mean aerodynamic chord (MAC) | c̄ = (1/A)∫c² dy, y_MAC = (1/A)∫y c dy, x_MAC,LE = (1/A)∫x_LE c dy | [N09] eq. 3.30–3.32 |
| CP, aft of the root leading edge | X_f = x_MAC,LE + c̄/4 | [B66] eq. 76a, [N09] eq. 3.34 |
| N fins | (C_Nα)₁ Σ sin² Λ_k · f_N | [N09] eq. 3.51–3.53, [TD] eq. 3.54 |
| interference | K_T(B) = 1 + r_t/(s + r_t) | [B66] eq. 77, [N09] eq. 3.56 |
- Trapezoids.
tan Γ_c = (x_t + c_t/2 − c_r/2)/s, and the closed forms give [B66] eq. 57 and 76a exactly. - Ellipses on the root chord.
Γ_c = 0,c̄ = 8c_r/(3π),y_MAC = 4s/(3π)andX_f = (½ − 2/(3π)) c_r = 0.28779 c_r. Loft replaced the ellipse with an equal-area trapezoid, whose sweep made the slope 1.3% low (Loft lesson L10). - Freeform outlines.
c(y)runs from the leading edge to the trailing edge, so a jagged edge’s gap counts toward the CP but not towardA_fin([N09] pp. 27–28).Γ_cis the span average of the mid-chord angle ([N09] p. 29), which gives the natural angle for trapezoids and ellipses. The integrals are exact: between vertex heights the edges are straight, and a three-point Gauss rule (quadrature, a weighted sum of samples) per band is exact. Bands thinner than 1e-12 of the span (vertex heights a few rounding steps apart, as when a tip is converted from inches) are skipped. - A root along a nose cone or a transition (ADR-166, fins on a nose cone). There the
root (where the fin meets the body) follows the curved surface. Its points are part of the fin’s
outline, so
A_fin,c̄and the CP are those of the region between the outline and the surface, with heightsyfrom the surface at the root’s leading edge. The body radiusr_tin the interference factorK_T(B)above is the radius there. No measured fin on a nose cone checks the model; against OpenRocket, the one example is the cockpit of Pods–airframes and winglets:- Across the airflow at Mach 0.3, its normal-force slope is 0.2784 per radian in hpr and 0.2117 in OpenRocket, 31.5% more, with the CP 0.13 mm apart. Closed straight along its chord, hpr’s would be 0.2802, so the curved root isn’t the cause (#326).
- With the air crossing at 0° roll, the direction OpenRocket’s margin is taken in, the single fin lies in the airflow’s plane, and neither code counts it there. hpr’s margin is the weakest direction’s since #329 (Stability margin).
- Above Mach 1, the tip-cone correction still mirrors the flow at a level line through the root’s leading edge, an approximation on a root that rises.
- Prandtl–Glauert enters through
βin the fin slope only, up to Mach 0.8. The CP stays at the quarter chord, a quarter of the way along the MAC, through that range ([B67] p. 6). Past Mach 0.8 the fins follow Fins through Mach 1, below. - Fin count. A fin at angle
Λ_kto the lateral airflow adds(C_Nα)₁ sin² Λ_kin the plane of the flow. The sum isN/2for three or more evenly spaced fins, at any roll. For one or two fins it changes with the direction the air crosses, so such a rocket has a different margin in each direction; hpr reports the weakest (Stability margin).f_Nis 1 up to four fins, then 0.948, 0.913, 0.854 and 0.810 for five to eight ([TD] eq. 3.54). Those factors make six and eight fins 1.37 and 1.62 times four ([762] p. 5-24), and interpolate five and seven (Loft lesson L8). More than eight fins are refused: [TD]’s 0.750 has no data behind it. [N09]’s roll-dependent 15% and 6% reductions for three and four fins were dropped in [TD]. - Side force of one- and two-fin sets. Each fin sees
α sin Λ_k([N09] eq. 3.50) and pushes along its own normal. Eq. 3.51 keeps the in-plane sharesin² Λ_k; the share across the plane issin Λ_k cos Λ_k, which cancels for three or more fins but not for one or two. [N09] pp. 31–32 drops it, arguing that it cancels for two or more fins; for two fins the pushes add. hpr reports it asC_Yat the fins’ CP (derived here from eq. 3.50, not taken from a source). - Interference
K_T(B)is Barrowman’s straight-line fit to NACA TR-1307, justified forr_t/(s + r_t) < 0.4([B66] p. 36). - Not modeled.
- The body lift the fins induce,
K_B(T)([B66] p. 36 neglects it; [B67] eq. 3-98 has it). - The roll moment of a single fin’s normal force at an angle of attack: its force acts at
r_t + y_MACalong the fin’s normal. Two or more even fins cancel it; one fin doesn’t. Roll from cant and against the roll rate is modeled (Roll: forcing and damping). - Interference between fin sets at the same station.
- Damping coefficients for pitch and yaw. They would replace the local-flow damping below, not
add to it, or it would be counted twice; hpr keeps the local flow:
- In a flight, pitch and yaw damping come only from evaluating each component in its own local flow, which includes the speed the rocket’s rotation adds there (Rigid-body flight).
- Only components with a slope give it: nose cones, transitions and fin sets. A boattail’s slope is negative, so it takes some away.
- Body tubes give none at small angles. Their own slope is 0, and their body lift grows with
sin² α, so it adds nothing there.
- Tube fins, which have their own section (Tube fins). Any part kind the model doesn’t know is refused. Pods have their own section (Pods).
- Launch lugs and rail buttons add drag only.
- The body lift the fins induce,
Fins through Mach 1
Faster than sound, air can’t flow around a fin’s edges ahead of it. The fin’s lift comes from the pressure behind the shock and expansion waves at its surfaces, and a different theory applies. hpr uses Barrowman’s subsonic method to Mach 0.8, supersonic linear theory from where that theory holds, and a straight-line join between them. The body terms don’t change with Mach: slender-body theory’s slope and CP hold at any speed ([B67] p. 18). How well this agrees with a wind tunnel and with RASAero II is under Verification.
Supersonic linear theory. Past Mach 1, β is redefined as √(M² − 1), which grows from 0 as
the speed passes that of sound. A thin flat plate at a small angle α to a supersonic flow has
the pressure coefficient +2α/β on the side facing the flow and −2α/β on the other: the first
term of the pressure series Barrowman uses ([B67] appendix A, p. 82). So every part of the fin
carries the same load, 4α/β per unit area, and each strip (a narrow slice of the fin along the
airflow) carries it at its middle. MIL-HDBK-762 finds linearized theory accurate for the
supersonic stability of thin fins ([762] p. 5-15). Two things change the load near the tip:
- The tip’s Mach cone. The tip disturbs the flow only inside the
cone that spreads inboard from its leading edge at the Mach angle,
atan(1/β). Barrowman halves the load inside it ([B67] appendix A, p. 84). For a rectangular tip that is exactly linear theory’s loss. - The body as a mirror. At the root the body stands in for the fin’s mirror image. A cone that reaches the root continues into the mirror image, and the part of it there counts too, as the mirror fin’s cone crossing onto this one.
| term | formula | source |
|---|---|---|
| one fin | (C_Nα)₁ = (4/β)(A_fin − A_cone/2)/A_ref, β = √(M² − 1) | [B67] appendix A |
| CP, aft of the root leading edge | the centroid of that load: the fin’s area centroid, less half the cone’s | [B67] appendix A |
rectangle, AR = 2s/c | (4/β)(1 − 1/(2β·AR)) and X_f/c = (β·AR − 2/3)/(2β·AR − 1), exact linear theory | [TN2114], [N09] eq. 3.35 |
A_cone is the part of the fin (and of its mirror image) inside the tip’s Mach cone. The
fin-count factor, the sum over fins and K_T(B) apply as above.
Where it starts. Linear theory’s strips need four things, so it starts at
M_s = max(1.2, 1/cos Γ_L, 1/cos Γ_T, √(1 + 1/AR²), √(1 + (c_t/2s)²)), where Γ_L and Γ_T
are the leading- and trailing-edge sweeps, AR = 2s²/A_fin is the aspect ratio of the fin and
its mirror image, and c_t the tip chord:
- Mach 1.2, the bottom of the supersonic region ([N09] Table 3.1, p. 19);
- supersonic edges, each with its Mach number square to the edge,
M cos Γ, past 1, the case [TN2114] covers; β·AR ≥ 1, where linear theory’s tip loss holds; a rectangle’s slope peaks there, at2·AR;β ≥ c_t/(2s), so the mirror fin’s tip cone stays off this fin’s tip, which matters for a tip chord longer than the fin’s average.
Calisto’s and the Arcas Robin’s fins start at their leading edge’s 1.2806 and at 1.2.
The transonic join. From Mach 0.8 to M_s, the slope and the CP are each a straight line in
M between their values at the two ends. No source gives this region in closed form. MIL-HDBK-762
reads it from charts of transonic similarity (the way thickness and Mach number combine near
Mach 1, [762] pp. 5-104–5-105). The join keeps both continuous. For most fins the slope peaks at
M_s (a leading edge swept forward can make it fall across the join instead), and the
CP moves aft from the quarter chord toward the middle of the chord.
Worked example. Calisto’s 2018 fins: root chord 0.12 m, tip chord 0.04 m, span 0.10 m and
sweep length 0.08 m, on A_ref = 0.012668 m² (d_ref = 0.127 m). The leading edge is swept
38.66°, so M_s = 1/cos 38.66° = 1.2806. At Mach 2, β = 1.732. The tip cone is a triangle
0.04 m along the tip and 0.04/β = 0.0231 m down the unswept trailing edge: 0.000462 m², 5.8% of
the fin’s 0.008 m². So (C_Nα)₁ = (4/1.732)(0.008 − 0.000231)/0.012668 = 1.416 per rad. hpr
gives, per fin:
| Mach | 0 | 0.8 | 1.0 | 1.2806 | 1.5 | 2.0 | 3.0 |
|---|---|---|---|---|---|---|---|
(C_Nα)₁, per rad | 1.853 | 2.170 | 2.499 | 2.960 | 2.158 | 1.416 | 0.877 |
| CP aft of the root leading edge, m | 0.0550 | 0.0550 | 0.0632 | 0.0747 | 0.0753 | 0.0758 | 0.0761 |
What it leaves out.
- Thickness. Linear theory is for thin plates; a thick fin or a blunt leading edge detaches the bow shock near Mach 1.
- Exact linear theory for a tapered fin. The strip method’s constant load outside the tip cone
runs above it. Against [TN2114] eq. A7 (printed p. 18), a fin with a taper ratio of 0.5, an
unswept trailing edge and
βA = 3gets 4.5% more slope. - Subsonic leading and trailing edges, which the join covers without a method of its own. A
curved edge counts by its span-averaged sweep, so an elliptical fin, whose edge is swept 90° at
the tip, or a freeform fin with a raked outboard edge, keeps a subsonic stretch past
M_s. - Leading edges swept forward. The tip then sits ahead of the root and its Mach cone covers much
of the fin, where the half-load overstates the loss, so the slope rises past
M_sinstead of falling: by up to +7.7% for fins swept 40° to 50° forward, peaking up to 0.37 Mach later (issue #64). Such fins are rare on rockets. A property test holds every trapezoid whose leading edge is straight or swept aft (to 65°), tapered either way, to a slope that falls with Mach fromM_s. - The fins’ lift carried onto the body behind them,
K_B(T), as below Mach 1.
Other choices, and why not.
- Niskanen’s supersonic slope ([N09] eq. 3.48–3.49) multiplies the fin’s area by one strip’s
pressure coefficient,
K₁α + …withK₁ = 2/β: the pressure on one face. A plate is pushed by the difference between its faces, twice that. His thesis finds its simulatedC_Nαfor the Arcas Robin “notably lower than the experimental values”, with the cause unknown (p. 91, a comparison that runs to Mach 4). hpr counts both faces. - RocketPy 1.13.0 flies Diederich’s subsonic slope at every Mach number, with
βheld at 0.6 from Mach 0.8 to 1.1; past Mach 1 that tends to2π cos Γ_c/β, about π/2 times linear theory’s4/β. Its fin CP doesn’t move with Mach. - Tuning the join to the wind tunnel below would shrink its misses by fitting the model to its own check. The join’s ends come from the sources’ speed regions, set before measuring.
Roll: forcing and damping
Fins set at a small angle to the rocket’s axis, cant, each push sideways as a wing at that angle would. Each push acts off the axis, so together they twist the rocket and spin it up: the roll forcing. Once the rocket rolls, each fin also moves sideways through the air, meets it at an angle of its own, and pushes back against the spin: the roll damping. The two balance at a steady roll rate that grows with the airspeed. hpr takes both from Barrowman’s thesis ([B67] §3.13–3.14, appendix A), by strip theory: each narrow strip of a fin, running with the flow, lifts in proportion to the angle it meets the air at.
How far to trust it (Roll against the Arcas Robin and the Basic Finner):
- The forcing is within 5.3% of NASA’s measured roll effectiveness (the rolling moment per degree of cant) from Mach 2.3 to 4.63, and reads 14% to 48% high at Mach 1.5 and 1.8.
- The damping reads 6% to 16% low against the one measured set, from Mach 1.5 to 3.
- Below Mach 1.5, where most hobby flights stay, nothing measured checks either: only the flight’s agreement with the closed-form balance, and Barrowman’s own computed damping at Mach 0.07. Strips that each lift at the fin’s average slope ignore how the flow at one strip changes the next, which for short fins likely overstates the damping, so a subsonic spin may read low.
- The steady spin rate carries both errors, and both push it high: forcing that reads high and damping that reads low each raise it.
Other symbols are as in Fins: r_t the body’s radius at the fins, s the span, c_r
and c_t the root and tip chords, A_fin one fin’s area, y_MAC the mean aerodynamic chord’s
distance from the root.
| Symbol | Meaning | Unit |
|---|---|---|
δ | cant: the angle each fin is turned about its own span, positive turning fin 0’s (the fin along +x_B) leading edge toward −y_B (Mass properties) | rad |
p | roll rate about +z_B (Frames) | rad/s |
ξ | a strip’s distance from the rocket’s axis, r_t + y | m |
C_l | rolling moment about +z_B over q A_ref d, with d the reference diameter | none |
C_lδ | one fin’s rolling moment per radian of cant, in the sense its lift turns the rocket | per rad |
C_lp | one fin’s rolling moment per unit of p d/(2V): the damping, negative | none |
C_l0 | the whole rocket’s rolling moment from cant at no roll rate | none |
k_T(B), k_R(B) | the body’s effect on the forcing and on the damping | none |
The moment. A fin set of N fins adds C_l = −N C_lδ k_T(B) δ + N C_lp k_R(B) (p d/2V). The
minus sign is the cant’s direction: a positive cant turns fin 0’s leading edge toward −y_B, so
its lift pushes toward −y_B and turns the rocket about −z_B (a negative C_l), clockwise
seen from ahead of the nose, looking aft. Its first term, summed over the sets, is C_l0. In a
flight the cant’s forcing is scaled by cos α: the cant meets the air as it runs along the axis,
so there is none broadside and it reverses tail first, as the fins’ normal force follows sin α
(Rigid-body flight).
Bodies of revolution add nothing, and fin–fin interference is left out, as in [N09] eq. 3.66.
Below Mach 0.8. The cant is an angle of attack for each fin, so its lift is the fin’s own
normal force, acting at its mean aerodynamic chord: C_lδ = (C_Nα)₁ (r_t + y_MAC)/d ([B67] eq.
3-35, [N09] eq. 3.66). For the damping, a strip at ξ meets the air at −pξ/V, and lifts by the
fin’s slope per unit of its area, a = (C_Nα)₁ A_ref/A_fin: C_lp = −2a ∫ξ² dA/(A_ref d²) ([B67]
eq. 3-40–3-49, [N09] eq. 3.67–3.70). For a trapezoid ∫ξ² dA = (c_r + c_t) r_t² s/2 + (c_r + 2c_t) r_t s²/3 + (c_r + 3c_t) s³/12; for any outline hpr takes it from the polygon.
From M_s, where supersonic linear theory starts
(Fins through Mach 1), each strip carries the load 4α/β, halved inside
the tip’s Mach cone:
C_lδ = (4/β)(∫ξ dA − ½∫_cone ξ dA)/(A_ref d) and
C_lp = −(8/β)(∫ξ² dA − ½∫_cone ξ² dA)/(A_ref d²), with β = √(M² − 1) ([B67] appendix A, first
order). Between Mach 0.8 and M_s each is a straight line in M, as the fin’s slope is.
The body. The body reshapes the flow the fins meet. Barrowman’s factors from slender-body
theory ([B67] eq. 3-95 with 3-105, and 3-123 with 3-122), with τ = (s + r_t)/r_t, scale the
forcing by k_T(B), 0.940 at τ = 2, and the damping by k_R(B), 1.33 at τ = 2 for a
rectangular fin; both are 1 without a body. [N09] leaves both out. k_R(B) is for a chord that
falls linearly from root to tip; another outline takes it at its tip-to-root chord ratio, so an
elliptical fin is taken as a triangle, about 5.5% too much damping.
The steady roll rate is where the two cancel: p = −(C_l0/C_lp)(2V/d). Below Mach 0.8 the
fin’s slope cancels between them, and for one fin set
p = −δ V A_fin (r_t + y_MAC) k_T(B) / (k_R(B) ∫ξ² dA): it grows with the airspeed and the cant,
and not with the air’s density or the number of fins.
A worked example: Valetudo with its fins canted 1°. Valetudo, one of RocketPy’s example
rockets, has three fins 58 mm long at the root, 18 mm at the tip and 77 mm in span on a body of
radius 40.45 mm. One fin has A_fin = 2926 mm², y_MAC = 31.75 mm, ∫ξ² dA = 1.656 × 10⁻⁵ m⁴,
and τ = 2.904, so k_T(B) = 0.935 and k_R(B) = 1.228. At 100 m/s,
p = −0.01745 × 100 × 0.002926 × 0.0722 × 0.935/(1.228 × 1.656 × 10⁻⁵) = −16.95 rad/s, 2.7
turns a second. With no drag and no gravity hpr’s flight settles on it within 1e-6 (1e-11
measured), spinning up with a time constant of 0.48 s
(canted_fins_spin_to_the_analytic_balance, which also checks this example’s rate and time
constant).
Why these choices:
- The fin’s own slope in the damping. Barrowman’s text writes the airfoil’s slope
C_Nα0there (eq. 3-40,2π/β), as does [N09] eq. 3.69. His own computed curve for the Basic Finner reads −34.21 at Mach 0.07 (Fig. 5-7), which is the fin’s slope spread over its strips, −33.53 (the_basic_finner_damps_as_barrowman_computed); the airfoil’s gives about −81, 2.4 times as hard. His curve’s rise toward Mach 1, about 20% read from the figure, follows the fin’s slope too, where the airfoil’s would grow without bound. Stubby fins lift far less than an airfoil. - The body factors. Barrowman has them and [N09] doesn’t. For the Arcas Robin’s fins they lower
the forcing 6.5% and raise the damping 20%. His
k_R(B)is a ratio of forces (eq. 3-116, 3-120) applied to a moment (eq. 3-123); weighted by the moment it would be 3.4% to 4.2% smaller for the fins here. hpr keeps his, which his computed curve seems to use too.k_T(B)is reference 23’s factor for fins turned together; for cant, whose load turns the other way on the opposite fin, it isn’t derived. - Moments about the body’s axis. Faster than sound Barrowman’s appendix A takes each strip’s
moment about the fin’s root; hpr takes it about the axis,
ξ = r_t + y, as his subsonic eq. 3-27 and 3-35 do. About the root the Arcas Robin’s forcing would be about half: its load sits 25 mm from the root and 53 mm from the axis. - Limits on the input. hpr refuses a cant beyond 15°, where a fin stalls and the linear model means nothing, and a cant on a single fin, whose sideways push it doesn’t carry.
- Pitch and yaw keep the local-flow damping. A flight’s pitch and yaw damping come from each part’s own local flow (ADR-011); coefficients would count it twice.
- No target was set for the comparisons. The roadmap asked for the forcing to be compared with the measured roll effectiveness, and set no bar; the numbers are reported as they are (ADR-031).
What it leaves out:
- The roll forcing near Mach 1.5 reads high: linear theory’s load rises as
1/βtoward Mach 1, and the Arcas Robin’s measured forcing doesn’t. Barrowman found the same for the Tomahawk sounding rocket (“the theoretical value at M = 1.5 is no good”, [B67] p. 66). - The damping reads low for the Basic Finner’s thick wedge fins, 8% of the diameter thick: first-order theory has no term for thickness, the likely cause.
- Nothing measured checks roll below Mach 1.5.
- A fast spin at low airspeed meets the fins at angles past stall, where the linear damping no longer holds: Valetudo spinning at 17 rad/s at 5 m/s meets the air 23° off at its fin tips.
- The angle of attack: the measured roll effectiveness changes by up to 13% between 0° and ±4° (TN D-4014 Fig. 14); hpr’s is the same at every angle. A single fin’s roll from its normal force, the body’s own roll, and fins’ airfoil sections are not modeled.
Pods
A pod is a body beside the airframe: a side pod, or an outboard motor pod (the design page’s Pods). hpr gives each pod the forces its parts would have on the airframe, once per pod, and adds them to the airframe’s. No measured flight checks it. The formulas are checked against hand-worked numbers. They are also checked against OpenRocket 24.12 on six probe designs, small designs written only to test pods: five carry pods of bodies, fins, a tail cone, winglets or motors, and one is the same airframe without pods (M1.13c2, a pod design against OpenRocket). Below Mach 0.81, hpr’s apogee is within 0.81% of OpenRocket’s, and its stability margin within 0.0014 calibres. What the pods change agrees to 0.32 percentage points of apogee (table below). That shows the two codes compute the same thing, not that either is right: both leave out how the pods and the body disturb each other’s flow, whose size is given below. Faster than sound a pod keeps slender-body theory’s slope, which nothing checks.
The rule. A pod’s nose cones, transitions and tubes are
Barrowman’s bodies, as on the airframe ([B67] eq. 3-65, [N09]
eq. 3.19): a part whose cross-section grows from A_fore to A_aft has the slope
C_Nα = 2 (A_aft − A_fore)/A_ref at the center of pressure Bodies of revolution
gives it, on the rocket’s reference area A_ref. Each pod adds that once:
- Normal force.
Npods addNtimes one pod’s slope and moment, at the part’s station along the rocket. The pod’s first part steps up from nothing, as the airframe’s nose does, so a pod that starts with a tube gets no slope from its flat front. Faster than sound, a pod keeps slender-body theory’s slope, since the shock-expansion method (Bodies faster than sound) covers the airframe alone. - Body lift. Each pod’s body lift, Jorgensen’s crossflow term, is taken on the pod’s own planform and at the pod’s own fineness (its length over its largest diameter), not the airframe’s.
- Fins on a pod. A pod’s fin set is turned with its pod: pod
kat roll angleφ_kholds finjatθ_j + φ_k, and each fin takes its share of the flow as an airframe fin does (Fins). The fin–fin factor (how a set’s fins shade each other) counts one pod’s fins. The body’s interference with the fins,K_T(B), is the pod tube’s: 1 for the tube of no radius a pod of winglets hangs from. - Drag. Each pod adds its own drag buildup (Drag): friction on its surface, with the body form factor at its own fineness; its nose’s or shoulder’s pressure drag; its steps; its boattails; and its own base, the last part’s aft area. A pod’s fins, lugs and buttons drag once per pod too. The Reynolds number stays the rocket’s, as for every part ([N09] §3.4).
- Motors in pods. A burning motor’s cross-section comes off the base of the body it sits in:
the airframe’s motors off the airframe’s base, and a pod’s off its own pod’s
([N09] pp. 50–51 for the base). This is worked out per pod set: each pod set that holds motor
mounts has its own total of thrusting motor area
(
DragConditions::thrusting_pod_motor_areas_m2, in the order ofLayout::motor_pod_sets), and each of its pods takes an even share of that total. A base smaller than its motors has no base drag left, never less. Up toMOTOR_POD_SETS, 4, pod sets may hold motor mounts; a rocket with more is refused by name. Until M4.5i, motor mounts in two different pod sets were refused (ADR-168). Worked example: two pod sets of one pod each, both holding a burning 18 mm motor (2.54 cm² each), take 2.54 cm² off each pod’s own base; the airframe’s base keeps its own motors’ relief alone. - Roll. In hpr a pod adds no rolling moment of its own (but see A single pod’s moments
below); it damps a roll. Rolling at
p, a part at distanceρfrom the axis crosses the air atp ρ, so it meets it at the anglep ρ/V, and its normal force acts about the axis with the armρ. That gives, for each pod’s body part, a roll damping (Roll: forcing and damping) ofC_lp = −2 C_Nα ρ²/d², on the reference diameterd. A pod’s fins damp as the airframe’s fins do, with each strip’s distance taken from the rocket’s axis: a fin whose root isρ_0out along its span has its strips atρ_0 + y.
Where the forces act. The flight engine applies each pod part’s force on the rocket’s axis at
its station. For two pods or more, spaced evenly, the pods’ offsets add to zero, so the forces on
the axis turn the rocket in pitch and yaw as they would at the pods. What an offset adds, to first
order, is roll damping, which hpr adds as above, and a pitch damping from the pods’ drag: pitching
at q, a pod ρ off the pitch plane meets the air q ρ faster or slower on either side, so its
drag changes by as much and pitches back. That is C_mq ≈ −2 C_D,pod Σρ²/d² (the pitch moment per
unit pitch rate, C_D,pod one pod’s drag coefficient, the sum over the pods), about −0.15 on the
worked example below. hpr leaves it out: the airframe’s own pitch damping from its fins,
−2 C_Nα,fins ℓ²/d² with ℓ the fins’ distance from the center of gravity, is of the order of
−10³ on such a rocket.
Worked example. The tests’ rocket (the finned_rocket of hpr-aero’s tests: a 0.25 m ogive
nose, 0.7 m of tube 27 mm in radius, a 0.05 m boattail to a 22 mm tail tube 0.3 m long, and four
fins of 0.12 m root chord, 0.05 m tip chord, 0.06 m span and 0.07 m sweep) carries three pods
40 mm from its axis, starting 0.35 m aft of the nose tip. Each is a cone 0.05 m long on a tube
0.2 m long, both 10 mm in radius, so each pod’s fineness is 0.25/0.02 = 12.5. At Mach 0.3,
Reynolds number 5×10⁶ per meter:
| quantity | without pods | with three pods |
|---|---|---|
one cone’s slope, 2 (10/27)² per radian | 0.2743 | |
its center of pressure, 0.35 + ⅔ · 0.05 m | 0.3833 m | |
| the rocket’s normal-force slope, per radian | 12.374 | 13.197 (+3 × 0.2743) |
| the rocket’s center of pressure, m aft of the tip | 1.0662 | 1.0237 |
| zero-lift drag coefficient | 0.5051 | 0.6386 |
| the pods’ share: cones’ friction and pressure, tubes’ friction and base | 0.0074, 0.0127, 0.0592, 0.0542 | |
roll damping C_lp, the pods’ share −2 · 0.2743 · 3 · 0.04²/0.054² | −35.215 | −36.118 (−0.903) |
The pods move the center of pressure about 43 mm, 0.79 calibres,
forward, and add 26% to the drag. The test the_aero_page_s_pod_example holds this table to the
digits printed. The tests a_pod_adds_its_bodies_slopes_once_per_pod,
a_pod_s_body_lift_takes_its_own_fineness, a_pod_drags_once_per_pod,
a_pod_s_base_takes_its_own_motors_area, a_pod_s_bodies_damp_the_roll,
a_pod_s_fins_turn_with_their_pod and a_pod_s_fins_are_the_pod_s_turned_with_it hold the rules
to hand-worked values, the fins’ damping to Barrowman’s strips summed by hand over two pods with a
fin pointing each way. In hpr-sim, pods_damp_the_spin_of_canted_fins_by_their_cones flies
Valetudo with three pods and canted fins: it spins to the
balance the pods’ damping predicts, to 1e-6.
Against OpenRocket. The six probe designs are written by
pod_probes.py.
Each is one airframe, a 0.2 m conical nose on 0.6 m of tube 60 mm across, with three fins and an
AeroTech H128W, carrying one set of pods. The pods are about 0.3 m long and 20 mm to 32 mm across,
5 mm to 10 mm off the airframe; the winglets hang from pods of no length at its surface. Every
outside part is set to OpenRocket’s regular paint finish, 60 µm roughness. OpenRocket 24.12 flies
them straight up in calm air, and hpr flies the same files
(hpr’s flights against OpenRocket’s). In such
a flight the apogee and the largest speed test the drag and the mass; the normal force enters only
through the margin, which both codes take at the moment the rocket leaves the rod. Pods’ change
is the difference from pods-none, the same airframe with no pods, so it shows the pods’ own share
in each code:
| probe | its pods | apogee, OpenRocket | hpr | pods’ change, OpenRocket | hpr | margin, OpenRocket | hpr |
|---|---|---|---|---|---|---|---|
pods-none | none | 776.1 m | +0.81% | 2.757 cal | 2.759 cal | ||
pods-bodies-3 | three: a cone and a tube | 663.3 m | +0.48% | −14.53% | −14.81% | 2.360 cal | 2.362 cal |
pods-fins-2 | two: a cone, a tube, three fins | 613.7 m | +0.49% | −20.93% | −21.18% | 2.653 cal | 2.654 cal |
pods-fins-tail-4 | four: a cone, a tube, four fins, a tail cone | 533.3 m | +0.41% | −31.28% | −31.56% | 2.470 cal | 2.471 cal |
pods-winglets-2 | two of no length, two fins each | 699.8 m | +0.71% | −9.84% | −9.92% | 2.622 cal | 2.623 cal |
pods-motors-2 | two: a cone, a tube, three fins and an H128W each | 876.9 m | +0.53% | +12.98% | +12.66% | 2.990 cal | 2.991 cal |
pods-motors-2 flies an H128W in each of its two pods and none in the airframe, twice the impulse,
with 0.35 kg of nose ballast instead of 0.15 kg, so its change is not its pods’ alone. hpr’s
largest speed is 0.59% to 1.24% above OpenRocket’s, its margin 0.0011 to 0.0014 calibres larger,
and the pods’ change of margin agrees to 0.0004 calibres. The launch masses agree within 0.0001%.
The test pod_designs_are_within_5_percent_of_openrocket holds every probe within 5% of
OpenRocket’s apogee and largest speed, the bar every flight against OpenRocket is held to
(ADR-076). It also holds each margin within 0.005 calibres, and the pods’ change within
1 percentage point of apogee and 0.005 calibres. Those bounds were set after these numbers were
measured, tight enough to catch a broken pod rule: a pod fin’s body interference taken on the
airframe’s radius would move a margin by some 0.04 calibres. The whole table is in the
committed report.
The first probes stated no surface finish, and hpr flew them 6.0% to 7.3% high: OpenRocket reads a part with no finish as regular paint, 60 µm, and hpr’s reader gives it hpr’s own default, 20 µm (issue #216, which gives the run). The probes now state their finish.
One private design flies with pods too: C02, an anonymised design of the library
(hpr’s flights of the private designs).
Its pods hold only a launch lug each, on a tube of no length, so they add the lugs’ drag and
nothing else. Over its 5 configurations hpr’s apogee is −0.94% to +2.15% from OpenRocket’s; 3 of
them are compared with an OpenRocket flight whose parachute opened before apogee, and the +2.15% is
one of those. Of OpenRocket’s own pod examples, Pods–airframes and winglets flies, its cockpit fin
read on the nose cone
(fins on a nose cone), with every apogee
within 0.5% of OpenRocket’s and its margin 0.071 to 0.076 calibres above it, the flattering side
(#325,
#326). Pods–powered with recovery deployment
flies since M4.5i, as saved since
M4.5k, which weighs its rail buttons’ screw heads
and takes each of its two pod sets’ motors off that set’s own pods (ADR-168). With
only the sustainer’s motor it reads 0.36% below OpenRocket’s apogee, and with only the booster’s
motors 1.86% below. Its staged flight reads 37.93% low: its sustainer is unstable when the air
crosses it edge-on to its two-fin strake set, and in the conditions of OpenRocket’s record it
turns over before apogee in both codes. That record’s site is at 28.61° N, where the Earth’s
rotation tips hpr’s flight, and hpr’s angle of attack passes 90° at 2.25 s
(hpr’s flights against OpenRocket’s).
What neither code models. OpenRocket documents no pod model of its own. The probes show that,
on them, its answer is hpr’s: the pods’ change of margin agrees to 0.0004 calibres. Neither code
turns the flow around the body, then: on pods-fins-2 the upwash beside the body (below) would add
about a third to the pod fins’ force. So the limits below are both codes’ limits, and the agreement
above cannot size them. The probes do not test a single pod (each has two or more), a pod faster
than Mach 0.81, a rocket flying at an angle of attack (so not a pod’s body lift, which is zero
straight into the air), airframe and pod motors burning together, or a roll: none has canted fins,
and neither code’s roll is compared.
Left out, and how large it may be.
- The body’s flow around the pods, and theirs around it. The airframe turns the crossflow
around itself, so a pod beside it meets the air at a different angle. In potential flow past a
cylinder of radius
a, a point at distancer, at the angleθfrom the crossflow’s direction, meets it atα (1 − (a²/r²) cos 2θ)along the crossflow and−α (a²/r²) sin 2θacross it (atθ = 90°, NACA Report 1307’s upwashα (1 + a²/y²), [PNK57] p. 4 eq. 15). For three pods or more, spaced evenly, both average to zero, and the first-order change cancels. For one or two they do not: on the worked example’s pods,a²/r² = 0.46, so a pair’s cones lift up to 46% more or less, by roll angle, and push sideways by up to as much. hpr leaves this out; the probes above show OpenRocket leaves it out too. Interference drag is left out as well, as Barrowman leaves it out for fins ([B67] p. 62, “No interference drag effects are considered”) and Niskanen for the whole rocket ([N09] §3.4). - A single pod’s moments. One pod’s drag acts off the axis and pitches the rocket; its normal
force, and its fins’, act off the axis and roll it. hpr applies them on the axis, so it drops
those moments (issue #213). On the worked
example with one pod (drag coefficient 0.0445 at 40 mm) and a one-calibre margin, the pitch
moment would trim the rocket at about
0.0445 · 0.04/(12.65 · 0.054)= 0.0026 rad, 0.15°. - The interference on a pod’s fins’ roll damping. hpr takes the pod tube’s roll-damping
factor
k_R(B)on the whole sum. The part that comes from the pod’s offset is the pod moving sideways, whose factor is closer to the normal force’sK_T(B). For a fin whose span equals the tube’s radius (τ = 2),K_T(B)is 1.5 and a rectangular fin’sk_R(B)1.33 (Fins, Roll: forcing and damping), so that part may be some 10% too small; no test sizes it. On a pod of no radius, winglets’ usual pod, both are 1 and nothing is lost. - A pod inside the airframe. Nothing checks that a pod clears the airframe (#206); a pod sunk into it still gets its whole slope, friction and base.
- Canted fins on a pod are refused: their roll forcing about the rocket’s axis is not modeled. So is a pod’s tube of no length with a radius, a flat disc the drag buildup has no term for.
Tube fins
A tube fin set is a ring of short open tubes around the airframe, in place of flat fins. hpr flies each tube as an annular wing (a ring wing): a wing bent round into a tube, which lifts when the air meets it at an angle.
Only its parts are validated: no tube fin rocket has been checked against a wind tunnel or a measured flight. The ring wing’s slope is within 3% of five rings measured in a wind tunnel, which were thick and cambered, not paper tubes. What the tubes do to each other and to the body is not modeled: nothing measures it, and theory predicts more lift than hpr gives (#234).
The model came with M2.2e9 (ADR-099, tube fins flown as ring wings).
On OpenRocket’s Tube fin rocket hpr’s center of pressure is 1.07 calibres forward of OpenRocket’s. That misses the quarter calibre Loft lesson L19 asks for, and nothing measured says which code is nearer (below).
The normal force. A ring wing of diameter d and length L lifts about twice as much as a
flat wing of span d and chord L. A long, thin ring lifts twice what a solid body of its diameter
would, because it turns the air inside it as well as the air around it (Hoerner 1965, p. 7-13:
L = q d² π α for a ring of small aspect ratio). hpr takes Weissinger’s formula for a thin ring
(1955, as quoted by Wagner 2021, eq. 15), which runs from the short-ring limit to that long-ring
limit. With λ = L/d, its lift slope reads as below. At small angles that is its
normal-force slope, here on the area d L, not the
reference area:
C_Lα = π² / (1 + πλ/2 + λ arctan(1.2 λ)) per radian.
d is the tube’s mean diameter, its outer and inner radii added; for a paper tube it is within
a few per cent of the inner diameter the other choice would give. Fletcher’s rings run from
λ = 1/3 to 3. A longer tube rests on the formula alone, which the tests hold to its long-ring
limit. A set of N tubes takes N
times one tube’s slope, on the rocket’s reference area.
Faster than Mach 0, it takes Göthert’s rule, the same idea as the fins’ Prandtl–Glauert factor:
the slope is the one a ring 1/β times longer would have, over β = √(1 − M²). That leaves the
long-ring limit unchanged and scales the short-ring limit by 1/β.
The center of pressure. Fletcher measured where the lift of five rings acts (NACA TN 4117,
1957, Fig. 8, at Mach 0.13). hpr reads it against the ring’s aspect ratio A = d/L, interpolated
in straight lines:
A = d/L | 0 | 1/3 (thick ring) | 2/3 | 1 | 1.5 | 3 |
|---|---|---|---|---|---|---|
aerodynamic center, fraction of L aft of the leading edge | 0 (theory) | −0.11 (left out) | 0.143 | 0.203 | 0.253 | 0.355 |
The table ends at A = 3, Fletcher’s shortest ring; a shorter ring is refused. Below
A = 2/3 the line runs to the leading edge at A = 0. That end point comes from
slender-body theory, in which a long, thin ring’s lift all
appears at its front edge. Hoerner and Borst assume the same of the air turned inside an open
tube: that it turns “at or near the rim of the inlet” (Hoerner and Borst 1985, p. 19-16). They
write that they “do not have suitable experimental results at hand” on how an axial duct changes a
slender body’s lift and moment.
Fletcher’s A = 1/3 ring is left out, and that is a judgement. Its center sits ahead of its
leading edge. Fletcher puts that down to its low aspect ratio: such a ring behaves more like a
slender body of revolution than the others (p. 4). That this would not carry over to a paper
tube is hpr’s inference, untested. His rings had a Clark Y section 11.7% of the chord thick,
all of it outside a straight bore. At a chord of three bores that wall is 0.35 of the bore
thick, so about two thirds of the ring’s frontal disc is wall, against a few per cent for a
paper tube. His thinner rings may carry some of the same forward shift. The choice matters. On
the worked example below, the margin at rod clearance is:
| center rule | margin (calibres) |
|---|---|
Fletcher’s A = 1/3 point taken | 0.29 |
| the line to the leading edge (hpr’s) | 0.79 |
| OpenRocket | 1.87 |
The 0.29 holds Fletcher’s −0.11 below A = 1/3, as a first version of this model did; it comes
from a one-off run, not kept in the report.
The drag. Friction takes the inside and the outside of every tube, 2π L (r_o + r_i) per
tube,
at the rocket’s skin-friction coefficient (Drag). The wall’s front ring,
π (r_o² − r_i²) per tube, takes a square fin edge’s pressure drag: a blunt face at the front and
base drag behind (Drag, eqs. 3.90 and 3.92).
Refused. hpr refuses these cases rather than guess:
- Mach 0.8 and faster. No source covers tube fins near the speed of sound, where the flow through a tube can choke. A flight that reaches it stops with the tube-fin model’s error, even on an override table, which still takes the tubes’ stations and roll from the model.
- Fewer than three tubes. The body’s own flow around three or more evenly spaced tubes cancels in the sum, but around one or two it doesn’t.
- Solid tubes, with a wall as thick as the radius.
- Tube fins on a pod.
- A ring shorter than a third of its diameter (
A > 3), past Fletcher’s rings. - Tubes that overlap each other.
A tumbling airframe with tube fins stays refused too (Recovery).
Worked example. OpenRocket’s Tube fin rocket has six tubes 76.2 mm long, of radius
12.395 mm with a 0.330 mm wall, on a body of the same radius. Its tubes are just longer than
Fletcher’s longest ring, and their center falls on the line below his A = 2/3, the judgement
above.
| quantity | value |
|---|---|
mean diameter d | 24.46 mm |
λ = L/d | 3.115 |
Weissinger’s C_Lα on d L | 0.990 per radian, 98.1% of the long-ring limit π/λ |
| one tube’s slope, on the reference area 4.827 cm² | 3.82 per radian |
| the set’s, six tubes, at Mach 0 | 22.93 per radian |
| at Mach 0.35, its fastest | 22.96 per radian |
center, at A = 1/λ = 0.321 | 0.0689 L, 5.2 mm aft of the leading edge |
| friction area, six tubes inside and out | 145.6 times the reference area |
| wall area | 0.315 times the reference area |
| drag at Mach 0.2, friction and wall | 0.741 and 0.310, of the rocket’s 1.654 |
| OpenRocket 24.12’s slope for the set, at every Mach number | 37.85 per radian |
| OpenRocket’s center, to Mach 0.5 | 0.25 L, 19.05 mm aft of the leading edge |
Against OpenRocket. The Tube fin rocket is in the flight report. Flown on hpr’s own drag, its apogee is 6.95% above OpenRocket’s, 302.5 m against 282.8 m. On OpenRocket’s recorded drag, hpr’s apogee is within 0.03% of OpenRocket’s (ADR-097). So the net gap is the drag’s, though parts of it could cancel, and the rest of the flight agrees. OpenRocket’s per-component drag, read through its public API but not kept as a record, points to where. These are leads for #228, not measurements the repository reproduces:
- The nose, the body and the lug agree within 0.005.
- The tube fins take 1.18 in OpenRocket’s total and 1.05 in hpr’s.
- Keeping the whole base while the motor burns, as OpenRocket does, closes 2.3 of the 6.95 points. This one is in the report: hpr flew it. That rule is not specific to tube fins (#222).
hpr’s tube-fin drag probably reads low. OpenRocket refined its tube-fin drag against measured flights of two tube-fin rockets. A table in a comment on the change that last revised it (openrocket#2066) has seven flights in five motor cases: OpenRocket’s apogee is within −2% to +5.2% of the measured one, and high in four of the five. hpr has checked itself against no measured flight, and a code-to-code gap is not a measurement. Still, that is evidence OpenRocket’s drag is the nearer of the two here. What hpr’s leaves out is listed below, and #228 holds the search for a measured source.
The center of pressure against OpenRocket. The stability margin differs more. At rod clearance, the moment the rocket leaves the launch rod, hpr’s margin is 0.79 calibres and OpenRocket’s 1.87. Almost all of that gap is the center of pressure: hpr’s is 1.07 calibres forward of OpenRocket’s, and the two centers of mass differ by 0.002 calibres. L19 asks for a quarter calibre, and hpr does not meet it (ADR-102, tube fins’ center of pressure measured against OpenRocket).
OpenRocket 24.12’s answers for tube fins are kept as a
fixture,
openrocket-tube-fin-aero.json,
written by
tube_fin_aero.py.
It holds OpenRocket’s slope and center for the tubes and for the whole rocket, at five Mach
numbers. It covers the Tube fin rocket and 14 probe designs, each
changing one thing: the tubes’ length, count, radius or wall. The test
tube_fin_cp_against_the_oracle_measured_and_pinned
reads the same probes with hpr and pins all 70 gaps. The record shows:
- The two codes agree on the probes’ geometry and on the nose and body. Given OpenRocket’s slope and center for the tubes, hpr’s rocket has OpenRocket’s center of pressure within 0.01 calibres. So the whole gap is the tubes’.
- OpenRocket’s tubes lift 1.26 to 1.86 times what hpr’s ring wings do, at every Mach number.
- Its lift per tube is the same whatever the number of tubes, so it models no interference that changes with the count. Slender-body theory with the body included predicts one, and it falls as tubes are added. An unchecked estimate, taken 0.005 radii from the body and still rising as that gap closes, gives at least 1.96 times the lift of the same number of isolated rings for three tubes of 6 mm radius, 1.57 for six, and 1.13 for six touching each other (#234). OpenRocket’s lift on the 6 mm probes is 1.73 times the long-ring limit of isolated rings: below the estimate for three tubes. For six, the same estimate taken closer, and carried on to contact, is about 1.65, a little below OpenRocket’s. hpr leaves the interference out.
- OpenRocket puts the tubes’ center a quarter of their length aft of the leading edge up to Mach 0.5. Its maintainers describe that as the subsonic rule its flat fins and tube fins share (openrocket#3262).
On the probe built like the Tube fin rocket, six tubes 75 mm long touching the body and each other:
| Mach | 0.05 | 0.3 | 0.5 | 0.6 | 0.75 |
|---|---|---|---|---|---|
| hpr’s center of pressure less OpenRocket’s, calibres | −1.04 | −1.05 | −1.06 | −0.36 | −0.39 |
The Tube fin rocket itself, at rod clearance (Mach 0.056), gives −1.07: its tubes are 76.2 mm long and its nose and body differ a little from the probe’s.
From Mach 0.6 the gap shrinks only because OpenRocket 24.12 moves the tubes’ center to the leading edge. Its maintainers call that jump a bug and fixed it after 24.12 (openrocket#3235). The jump moves OpenRocket’s own center of pressure on the Tube fin rocket 0.73 calibres forward between Mach 0.5 and 0.6. It probably accounts for the five probe results, of 28 from Mach 0.6, that come within the lesson’s quarter calibre.
What each of OpenRocket’s two terms is worth: on that probe at Mach 0.05, giving hpr’s tubes OpenRocket’s center but keeping hpr’s slope moves hpr’s center of pressure 0.50 calibres aft. Giving them OpenRocket’s slope but keeping hpr’s center moves it 0.53 calibres aft. Up to Mach 0.5, every probe’s gap is between 0.42 and 3.0 calibres.
hpr keeps its model. Fletcher’s measured center moves forward, as a share of the ring’s length,
as a ring gets longer, and Hoerner and Borst assume an open tube’s inner flow turns at its inlet.
Nothing measured supports OpenRocket’s quarter length or its extra lift. hpr’s own center for
this rocket is unmeasured too: its tubes (A = 0.32) are longer than every ring Fletcher
measured. With his A = 1/3 ring left out, their center comes from hpr’s line from A = 2/3 to
the leading edge. Placed as his thick A = 1/3 ring’s instead, it would give a margin of 0.29.
Neither code has been checked against a measured tube-fin rocket, so which margin is nearer is
open (#228). Until then, check a tube-fin design in
both programs and treat the smaller of the two margins as the more cautious estimate. It is not
a bound: the thick-ring reading above gives a smaller one still.
What it leaves out:
- How the body and the tubes change each other’s flow. Slender-body theory predicts that they raise each other’s lift, but hpr applies no interference factor (#234).
- The gaps between tubes and body, and the drag where they meet.
- How the flow through a tube develops, or chokes.
- A thin tube’s measured center of pressure.
The rocket’s own crossflow, R²/s² around a tube s from its axis, turns with twice the roll
angle. It sums to zero over three or more tubes evenly spaced, which is why the model takes three
or more. That is a first-order derivation, not a measurement: it takes the body’s flow at each
tube’s center and leaves out the rest of their effect on each other.
The normal force from RASAero II
A flight can use another program’s normal force and center of pressure in place of hpr’s own. Today that program is RASAero II, read from the table it exports. Use it to fly two programs on the same aerodynamics, so that a difference between them comes from something else. Or use it to fly RASAero II’s numbers faster than sound, where hpr’s own normal force is less tested (Fins through Mach 1).
How far to trust it:
- The reading matches the file. On the export for Calisto, every one of its 4,999 rows at 2° and 4° comes back from hpr’s table to 2e-16. The 0° column, which hpr works out, agrees at 15 Mach numbers with the reading made for M1.8a.
- The flight uses the table as the equations say it should. A rocket flying on a table swings in pitch and yaw as the small-angle equations of motion predict for the table’s slope and center of pressure.
- Only up to Mach 0.75 on real data. Only one real export has been flown, Calisto’s. Faster than that, the table is checked by unit tests alone.
- Past the export’s last angle, and at 0° faster than Mach 1.3, hpr assumes. The assumptions fit RASAero II’s viscous part through Mach 1.3. Faster, that part grows much more slowly with the angle, so past 4° the table probably gives too much force at Mach 3 and above.
Three parts are hpr’s choices, not RASAero II’s:
- the damping, which stays hpr’s own;
- the normal force past the export’s largest angle of attack (4° in the one export tested);
- the slope at 0°, where the export’s normal force is zero.
In code, two calls take a file to a flight:
NormalForceTable::from_rasaero_csvreads the text.Simulation::with_normal_force_tableflies it. Its documentation has a worked program.
The decisions are in the record on normal-force overrides, ADR-032. The milestone is M1.8d.
What the export holds
RASAero II’s Aero Plots screen exports a table to CSV (File, Export, To CSV File; [RAS] p. 76).
There is one row for each Mach number and angle of attack
(Alpha, in degrees). The Calisto export has rows at 0°, 2° and 4°. hpr reads five of the
columns:
| column | what it is |
|---|---|
Mach, Alpha | the Mach number, and the angle of attack in degrees |
CN | the normal-force coefficient at that angle |
CN Potential | the part of CN from potential flow (the air treated as smooth and without friction), which grows in step with the angle |
CP | the center of pressure, in inches ([RAS] p. 13) measured from the nose tip (p. 114) |
CN also holds a viscous part, CN Viscous. It is the extra push, from the air’s friction, of
the air flowing sideways across the body. RASAero II takes it from Jorgensen’s method ([RAS]
p. 55), adds it from Mach 0.91 in Calisto’s export, and moves the center of pressure forward with
the angle. hpr’s own model has neither. From Mach 0.91 through Mach 1.3 the viscous part grows
exactly as sin² α: at 4° it is (sin 4°/sin 2°)² = 3.995 times its value at 2°. Faster, it grows
more slowly: 3.90 times at Mach 1.5, 3.16 at Mach 2, 1.73 at Mach 3 and 1.05 at Mach 4. The export’s CNalpha (0 to 4 deg) and CP (0 to 4 deg) columns
repeat its 4° values on every row; hpr doesn’t read them.
How hpr reads it
hpr builds one column for each angle in the export. Each column holds C_N/α (the normal force
over the angle, per radian) and the center of pressure, both against Mach number.
- At a positive angle,
C_N/αisCNover the angle in radians. - At 0°,
CNis zero, so it can’t be divided. hpr takesCN Potentialat the smallest positive angle, over that angle. The potential part grows in step with the angle: in Calisto’s export itsC_N/αis the same at 2° and 4° to 2e-15. Through Mach 1.3 the viscous part grows assin² α, so it adds no slope at 0°. Faster, the export doesn’t show how it starts from 0°, and leaving it out of the slope is an assumption. - The center of pressure is converted from inches to meters at 0.0254 m to the inch. It stays measured from the nose tip, as hpr’s stations are (station). So the design must start at the same nose tip as the RASAero II file. A table whose center of pressure, at one of its Mach numbers up to 5 (where a flight stops), lies outside the rocket, ahead of its nose or behind its tail, is refused: it is the sign of a length in the wrong unit.
- Reference area. RASAero II’s coefficients are on the body’s largest cross-section ([RAS] p. 72). hpr records that and rescales them to the rocket’s own reference area, when that is something else. For a rocket of several stages, export the whole stack (“Sustainer plus Booster” or “All Stages”): hpr flies the whole stack.
A flight looks up the table at its Mach number and angle of attack:
- Within one column, the values are linear in Mach number. Outside a column’s range the end values hold, and the lookup says so.
- Between two columns,
C_N/αand the center of pressure are linear in the angle. ThenC_N = (C_N/α)·αcomes back exactly at each column’s angle. Between them it is a part in step with the angle plus one in its square: RASAero II’s own shape through Mach 1.3 (α²is within 0.2% ofsin² αto 4°), and an assumption faster than that. - Past the largest angle
α_n, the normal force splits in two. The linear share is the slope at 0° timesα_n, at the 0° center of pressure; it grows assin α, as hpr’s own fins do (Aerodynamics in flight). The rest of the force, with the rest of the moment, grows assin² α, the form of the air crossing the body that hpr’s body lift also takes ([G] p. 1; [N09] eq. 3.26). The force and center of pressure are continuous atα_n, and the force is zero when the air comes from the tail. Two limits keep this sensible for any table. The rest’s center of pressure is held within the rocket. And a table whoseC_N/αfalls with the angle has no rest: its whole force grows assin α, so the force never turns round. Neither limit makes a jump as the Mach number changes. Either way this is an assumption, and the lookup reports it.
A worked example. Take an export with invented numbers. At Mach 1, CN Potential is 10 per
radian times the angle, CN Viscous is 0.03 at 2° and 0.12 at 4°, and the center of pressure is
50, 49 and 48 inches at 0°, 2° and 4°. These are the numbers in the CSV reader’s unit test,
reads_a_rasaero_export_by_angle_of_attack.
| angle | CN | C_N/α, per rad | center of pressure |
|---|---|---|---|
| 0° | 0 | 10.000 (CN Potential at 2° over 2°) | 1.2700 m |
| 2° | 0.37907 | 10.8594 | 1.2446 m |
| 3° (looked up) | 0.59110 | 11.2892, halfway between 2° and 4° | 1.2319 m |
| 4° | 0.81813 | 11.7189 | 1.2192 m |
| 10° (past the table) | 2.4815 | 14.2180 | 1.1662 m |
At 10°, sin 10°/sin 4° is 2.4893. The linear share, 10 × 0.069813 = 0.69813 at 1.2700 m,
grows to 1.7379. The rest, 0.12 at 0.9237 m (the station that gives the 4° moment), grows by
2.4893² to 0.7436. Together they make 2.4815 at 1.1662 m: the center of pressure moves forward
with the angle, as RASAero II’s does. The first draft scaled the whole 0.81813 by 2.4893 at
1.2192 m instead: 2.0366, 18% less force, with the center of pressure 5.3 cm further aft. That
reads the rocket as more stable than the viscous part makes it.
In a flight
The flight takes the table’s normal force at the center of mass’s airflow and applies it at the table’s center of pressure.
The export has no damping, so hpr keeps its own (Rigid-body flight). The table gives the force as if the rocket weren’t turning. When it turns, each part of the rocket meets the air at a slightly different angle, and hpr adds that difference: it is the damping. When the rocket isn’t turning, the difference is exactly zero.
The flight still stops at Mach 5, where hpr’s own parts, which give the damping, end. The table gives no side force: RASAero II’s rockets are symmetric.
How it was checked
| check | result | where the numbers are |
|---|---|---|
| Calisto’s export, every row at 2° and 4° read again apart from the library | 4,999 rows; CN within 2.2e-16 relative, CP exact; columns at 0°, 2° and 4° of 2,500, 2,500 and 2,499 Mach numbers, from Mach 0.01 to 25 (24.99 at 4°) | normal-force-override.json |
The 0° column at 15 Mach numbers against the reading of the export made for M1.8a, the normal force through Mach 1, which normal-force-vs-mach.json holds | the same to 1e-12 relative. That reading applies the same 0° rule, so this checks the reading, not the rule | both files |
| Valetudo at 100 m/s on a table of 1.5 times hpr’s slope with the center of pressure 5 cm further aft, against the small-angle equations of motion, in pitch and in yaw | period 1.104077 s against 1.104073 s, within the test’s 3e-5 (1.44965 s on hpr’s own); the decay within 0.03%, the test’s bound 1% | hpr_sim::tests::pitch_oscillation_follows_a_normal_force_table |
| Tables of hpr’s own normal force flown in a crosswind: every 0.5° and every Mach 0.01, and at 0°, 2° and 4° only, where the flight uses the continuation past 4° | apogee within 7.8 mm and 5.5 cm of hpr’s own flight, the test’s bounds 5 cm and 10 cm | hpr_sim::tests::a_table_of_hpr_s_own_normal_force_flies_as_hpr_does |
The continuation past the last angle: a table shaped as RASAero II’s (a part linear in the angle, one as sin² α), and random tables | the sin² α part continues to 1e-12; the force never turns round, the center of pressure stays within the rocket, and nothing jumps as the table’s values change with Mach number | hpr_aero::table::tests |
| Calisto from a 5.2 m rail at 85° in a 5 m/s crosswind, up to Mach 0.746: on the export, on hpr’s own normal force, and on hpr’s own as a table at the export’s angles | the export: apogee 2,793.09 m against 2,794.21 m, 12.4 m further into the wind. hpr’s own as a table moves it 0.02 m: the table’s method, apart from its numbers. Each flight spends 2.1 to 2.2 s past 4° before apogee | normal-force-override.json |
The Calisto flights show how much the change matters. They are not a check of accuracy: nothing
measured flew. cargo xtask aero writes the fixture from the export, which isn’t committed, and
checks it again wherever the export is present. CI has no copy of the export: there, a test checks
that the fixture’s 0° values agree with the ones committed for
M1.8a, and flies the 15-point table again.
What it leaves out:
- No real export has been flown through Mach 1. Calisto peaks at Mach 0.75. The reader’s unit tests cover the transonic columns.
- Past the export’s largest angle, the split continuation is hpr’s assumption. In a 5 m/s
crosswind Calisto flies past 4° for about 0.3 s just after leaving the rail (up to 7.9°) and for
the last 1.9 s before apogee, as it slows below 30 m/s. Its export has no viscous part below
Mach 0.91, so Calisto’s flights use only the linear share; the tables of hpr’s own normal force
are the flights that grow a rest as
sin² α. From Mach 3, where RASAero II’s viscous part hardly grows between 2° and 4°, thesin² αshare probably gives too much force. - The nose tip can’t be checked from the export beyond the refusal above. A design that starts somewhere else gets a shifted center of pressure, with no warning.
- Only RASAero II’s layout is read. A table from anywhere else can be built in code with
NormalForceTable::new.
Drag
Code: hpr_aero::drag (the terms),
AeroModel::drag and
AeroModel::buildup_components
(their sum over a rocket), hpr_aero::table (override tables),
hpr_design::Finish (roughness).
Decisions: ADR-009 (drag buildup, surface finishes and override tables). Extra sources:
- [B67] ch. 4 (pp. 43–62): the friction, roughness and leading-edge formulas Niskanen adopts, and Table 4-1 of roughness heights (p. 46, after Hoerner p. 5-3).
- [N09] §3.4 (pp. 41–53) and appendix B (pp. 106–110). [TD] reprints the same drag equations and tables unchanged.
On this page C_D0 is the zero-lift drag coefficient: the
drag with the air straight along the axis (no angle of attack), divided by q A_ref.
Recovery uses the same symbol for something else: a parachute canopy’s
drag coefficient on its nominal area.
Drag is built up term by term. Skin friction acts over the whole surface. Pressure drag acts on
noses and shoulders (here, a transition that widens toward the tail) and on boattails (one that
narrows). Base drag acts on the flat aft end, fin pressure drag on the fins’ edges, and parasitic
drag on launch lugs and rail buttons:
C_D0 = C_D,friction + Σ_T (A_T/A_ref)(C_D•)_T ([N09] eq. 3.75, 3.97), each pressure, base and
parasitic term on its own area. The axial coefficient is C_A = C_D0 f(α).
| symbol | meaning | unit |
|---|---|---|
C_D0 | the zero-lift drag coefficient: the drag with the air straight along the axis, divided by q A_ref | none |
C_D,friction | its skin-friction part | none |
A_T, (C_D•)_T | the area one pressure, base or parasitic term T acts on (a nose’s base, the fins’ front edges), and its coefficient on that area | m², none |
C_A, f(α) | the axial coefficient (the drag along the axis at an angle of attack), and the factor that turns C_D0 into it | none |
R, V, L, ν | the Reynolds number, the airspeed, the rocket’s length and the air’s kinematic viscosity | none, m/s, m, m²/s |
R_s, R_crit | the surface’s roughness height, set by its finish; the Reynolds number above which roughness, not R, sets the friction | m, none |
C_f, C_fc | the skin-friction coefficient, before and after its correction for Mach number | none |
f_B | the body’s fineness ratio: its length over its largest diameter | none |
A_body, A_fins | the areas friction acts on (see Friction on the axial projection) | m² |
t | a fin’s thickness | m |
φ (nose and shoulder row) | the joint angle between the surface and the axis where a nose or shoulder meets the next component: 0 for a smooth joint, π/2 for a step. Not the flow roll | rad |
γ, l, d₁, d₂ | a boattail’s length l over its drop in diameter, from d₁ at its fore end to d₂ at its aft end | none, m |
(C_D•)_base | the base drag coefficient (the base row) | none |
q_stag/q | the pressure rise where the air comes to rest on a blunt face, over q; 1 at low speed | none |
Γ_L | a fin’s leading-edge sweep | rad |
N t s | the fins’ frontal area: fin count × thickness × span | m² |
r_ext, r_int, l/d | a launch lug’s outer and inner radii, and its length over its outer diameter | m, none |
| square, rounded, airfoil | a fin’s cross-section: constant thickness with square edges; semicircular leading and trailing edges; or a NACA four-digit symmetric airfoil |
| term | formula | area | source |
|---|---|---|---|
| Reynolds number | R = V L/ν, L nose tip to aft end of the last body component | [N09] eq. 3.12, p. 42 | |
| skin friction | 1.48e-2 for R < 1e4; 1/(1.50 ln R − 5.6)² to R_crit; 0.032 (R_s/L)^0.2 from it | [N09] eq. 3.78–3.81, [B67] eq. 4-4–4-8 | |
| roughness limit | R_crit = 51 (R_s/L)^−1.039 | [N09] eq. 3.79, [B67] eq. 4-7 | |
| compressibility | C_f (1 − 0.1 M²) for M < 1; C_f/(1 + 0.15 M²)^0.58 turbulent and C_f/(1 + 0.18 M²) rough (not below turbulent) above | [N09] eq. 3.82–3.84 | |
| friction drag | C_fc [(1 + 1/(2 f_B)) A_body + (1 + 2t/c̄) A_fins]/A_ref | body: π A_plan; fins: both sides | [N09] eq. 3.85 |
| nose, shoulder | 0.8 sin² φ at rest, φ the joint angle at the aft end; through Mach 1 as under Drag through Mach 1 | base area; increase in area | [N09] eq. 3.86–3.87, appendix B |
| boattail | to Mach 0.8, (C_D•)_base × 1 (γ ≤ 1), (3 − γ)/2, 0 (γ ≥ 3); γ = l/(d₁ − d₂); faster, as under Boattails faster than sound | decrease in area | [N09] eq. 3.88; [762] Fig. 5-122 |
| base | 0.12 + 0.13 M² below Mach 1, 0.25/M above; behind a boattail, from Mach 0.8, times its base-pressure ratio (Boattails faster than sound) | aft base less thrusting motors | [N09] eq. 3.94, p. 50; [762] Fig. 5-141 |
| fin leading edge | square: 0.85 q_stag/q; rounded, airfoil: (1 − M²)^−0.417 − 1 (to 0.9), 1 − 1.785(M − 0.9) (to 1), 1.214 − 0.502/M² + 0.1095/M⁴; times cos² Γ_L | N t s | [N09] eq. 3.89–3.91, B.2 |
| fin trailing edge | square: base; rounded: half base; airfoil: 0 | N t s | [N09] eq. 3.92–3.93 |
| stagnation pressure | q_stag/q = 1 + M²/4 + M⁴/40 below Mach 1, 1.84 − 0.76/M² + 0.166/M⁴ + 0.035/M⁶ above | [N09] eq. B.1 | |
| launch lug | max{1.3 − 0.3 l/d, 1} · 0.85 q_stag/q | π r_ext² − π r_int² max{1 − l/d, 0} | [N09] eq. 3.95–3.96 |
| rail button | 0.85 q_stag/q (a rail pin) | side profile | [N09] p. 52 |
| angle of attack | f = 1 + 0.3(3t² − 2t³), t = α/17°; 1.3(1 − 3u² + 2u³), u = (α − 17°)/73°; −f(180° − α) past 90° | [N09] §3.4.7 (conditions only) |
- Roughness.
hpr_design::Finishnames the fifteen rows of [B67] Table 4-1 (0 to 1000 µm; [N09] Table 3.2 reprints ten) or takes a custom height. The default is “paint in aircraft mass production”, 20 µm. Each component has its own finish; the Reynolds number andR_s/Luse the whole rocket’s length, as [N09] does (Loft lesson L12). Loft cited none of its values: its 60 µm is OpenRocket’s “regular paint” ([N09] p. 83), its 2 µm isn’t in either table, and its1 + 60/f³ + 0.0025fis Raymer’s aircraft fuselage form factor, not [N09]’s. - Fully turbulent. The boundary layer (the thin layer of air the skin drags along) is taken as
turbulent everywhere, never laminar (smooth and layered). [N09] p. 43 found laminar runs changed
apogee by under 5% and dropped them.
Eq. 3.81’s
R < 1e4branch applies first, even on surfaces rough enough thatR_crit < 1e4. - Friction jumps where [N09] does. Eq. 3.79 is not where eq. 3.78 and 3.80 cross, so eq. 3.81
jumps at
R_crit: +9% for 60 µm on a 1 m rocket (0.00419 to 0.00458). The subsonic and supersonic corrections also differ at Mach 1 (0.900 against 0.922 turbulent). hpr keeps the published forms, and the tests pin both jumps (Loft lesson L90). - Friction on the axial projection (a departure). Wall shear (the air’s drag on the skin,
τper unit area) acts along the surface, so on an element of areadAat an angleθto the axis its axial share isτ cos θ dA, and the body’s friction area is2π ∫ r dx = π A_planrather than the slant surface in [N09] eq. 3.85. On slender noses the difference is small: a tangent ogive loses 1.1% of its own friction area at fineness 3 and 2.4% at fineness 2. On a short, steep shoulder it removes friction on what is nearly a flat face, so a shoulder’s drag tends to a bare step’s as its length goes to zero (Loft lesson L15); with the slant surface it would stay aboutC_fc ΔA/A_refabove it. The OpenRocket comparison (M2.2) will measure the difference. - Steps in radius. Where one body component meets the next with a different radius, a step up
is a zero-length shoulder, a flat face (
0.8 ΔAat rest, rising with Mach as under Drag through Mach 1), and a step down a zero-length boattail, the base drag of the uncovered area. A body with no nose cone gets the same flat face on its front. Each is the limit of the transition it replaces (Loft lesson L15), and it is reported with the aft component. A step up just behind a step down, such as a motor retainer behind the step to the motor tube, is sheltered by the step’s corner at every speed, by how far it rises and how much motor tube shows ahead of it, and not at all once that is as long as the step’s drop in diameter. This is unmeasured; for a 98 mm airframe with 12 mm of a 54 mm motor tube showing and a 62 mm retainer it lowers the rocket’sC_D012% to 23% from Mach 0.3 to 2.5 (Boattails faster than sound). - Boattails. [N09] eq. 3.88 writes
A_base/A_boattailwithout defining the areas, and p. 48 says a zero-length boattail drags like “the total base drag”. TakingA_baseas the aft base would count that base twice and leave out the uncovered ring (annulus), so hpr reads both as the boattail’s decrease in area (Calisto’s boattail: 0.052, against 0.046 the other way). The joint angle isatan(dr/dx)at the aft end,±π/2where a curved transition ends in a blunt tip. A lip (a short step up or flare) just behind a boattail or a step down is in its wake at every speed, and from Mach 0.8 a boattail’s drag rises to its supersonic wave drag (Boattails faster than sound). - Base drag under power subtracts the thrusting motors’ cross-section from the aft base, down
to zero ([N09] pp. 50–51: eq. 3.94 on p. 50, and on p. 51, “if the base is the same size as
the motor itself, no base drag”, which Niskanen takes from Fleeman’s Tactical Missile Design;
Loft lesson L13). Neither this rule nor OpenRocket’s below
has been checked against a measured flight. On one private design’s supersonic flight,
C06/1, the choice moves hpr’s apogee difference from OpenRocket’s from +13.60% to −10.71%, about 24 percentage points, more than any other known cause (#222, open on both this and the supersonic pressure drag).DragConditions::thrusting(reynolds_per_m, motor_area_m2)takes the cross-section of the burning motors from the flight engine (zero when unknown: no relief). The base belongs to the last body component.- OpenRocket’s rule, for comparisons. OpenRocket 24.12 does not take the motor off. While a
motor burns, its base drag is the whole base’s. A committed probe measures this:
validation/oracles/openrocket/base_drag.pyrecords OpenRocket’s base drag on its own example designs, the rows while a motor burns against the rows after, invalidation/fixtures/ork/openrocket-base-drag.json, and a test inhpr-validateholds it (ADR-097, the decision on sizing a drag cause). On all 42 of its flights of one data branch (nothing separating), the base drag while a motor burns is exactly the whole base’s, to 1e-12, where the motors cover 9% to 94% of the reference area, one flight’s motors in pods. On a rocket whose motor fills most of the base, the rule is a large difference under power. OnC06/1, switching hpr to OpenRocket’s base rule alone lowers its apogee from +13.60% to −10.71%, 24.3 percentage points. Switching the rest of the drag to OpenRocket’s then raises it to +1.11%, 11.8 percentage points. The second number is by subtraction, and the split depends on which change is made first (a supersonic flight).AeroModel::with_full_base_drag_under_power()andSimulation::with_full_base_drag_under_power()fly OpenRocket’s rule. A sustainer lit after a powered separation keeps it. They exist to size a difference from OpenRocket, not as a better model: hpr keeps Niskanen’s rule of taking the motor’s area off the base by default. - Supersonic pressure drag against OpenRocket. Faster than sound, the pressure on the nose, the fins’ edges and any step is mostly wave drag. On that flight hpr’s supersonic pressure drag is about twice OpenRocket’s: OpenRocket gives the nose almost none well above Mach 1, and the fins about a quarter of hpr’s. Which is right is open, since neither has been checked against a measurement on that shape (#222: hpr’s supersonic pressure drag is about twice OpenRocket’s).
- OpenRocket’s rule, for comparisons. OpenRocket 24.12 does not take the motor off. While a
motor burns, its base drag is the whole base’s. A committed probe measures this:
- Fins. Each fin set is its own term with its own thickness, chord and cross-section, so their
order doesn’t matter (Loft lesson L11).
c̄is the mean aerodynamic chord andΓ_Lthe leading-edge sweep:atan(x_t/s)for a trapezoid, the span average for freeform outlines ([N09] p. 50), and for an ellipseπ/2 − acos(k)/√(1 − k²),k = c_r/(2s)(the closed-form average, with itsacoshform fork > 1). The drag goes ascos² Γ, whose span average is 6% lower thancos²of the average angle for an ellipse ofk = 1. Fin–body interference drag and tip vortices are neglected ([N09] p. 41). - Launch lugs.
din eq. 3.95–3.96 is taken as the outer diameter: [N09] p. 52 treats a solid rail pin as a lug “with a length equal to its diameter”, which only reads that way (Loft lesson L14). A row ofcountlugs iscountlugs. Rail buttons follow [N09]’s rail-pin rule on their side profile (base and flange at the outer diameter, waist at the inner). - Angle of attack (derived coefficients). [N09] §3.4.7 describes, without an equation, a
two-part polynomial from 1 at 0° to 1.3 at 17° and 0 at 90°, with zero slope at each. hpr uses
the unique cubic on each part that meets those four conditions.
C_Ais positive toward the tail. Past 90° the flow meets the tail, and hpr mirrors with the sign reversed,−f(180° − α), an assumption that keeps drag opposing the motion. The planned OpenRocket comparison (M2.2) will check it against OpenRocket, whose polynomial may differ. - Refusals, not clamps (Loft lesson L16). Geometry the terms can’t use (a lug wall
thicker than its radius, a button’s base and flange taller than the button, a negative roughness,
which
Rocket::layoutalready refuses) is an error naming the component; a coasting condition (no motor burning) with a motor area and a non-finite result are errors; large coefficients are returned as they are. - Override tables (
DragTable) replace hpr’s ownC_D0with curves ofC_D0against Mach number from another tool, power-off and power-on, read from CSV text: two columns, optionally under a header (RocketPy’s curves;\r\n, a byte-order mark and01.05accepted), or a header naming the column, with rows at non-zeroAlphaskipped (RASAero II’s export:Mach, Alpha, CD, CD Power-Off, CD Power-On, …). An identical repeated row is skipped; a Mach number repeated with another value, or out of order, is refused with its line, not sorted. Tables interpolate linearly and hold their end values;Drag::tablereports any extrapolation.DragConditions::thrustingselects the power-on curve. A table’sreference_diameter_m, when set, rescales it to the rocket’s reference area. The angle-of-attack factor still applies, and an override accepts any Mach number.AeroModel::buildup_componentsalways reports the buildup, table or not. The normal force has a table of its own (The normal force from RASAero II).
A part’s stated drag coefficient
A part’s or stage’s stated drag coefficient flies as OpenRocket 24.12 flies it. The change it makes to the rocket’s drag is within 0.0059 of OpenRocket’s on 25 probe designs, but for lugs and buttons (up to 0.072, hpr’s own lug and button drag being higher) and, faster than Mach 0.6, for a nose or a boattail. This checks the rule, not the rocket’s whole drag, and the stated number itself is the designer’s.
OpenRocket lets a part say its own drag coefficient, in place of the drag its shape gives (the
Override tab’s coefficient of drag, saved as <overridecd>). Designers use it for a part whose
drag they measured, and for “hacks” such as OpenRocket’s Base drag hack example, which hangs a
weightless, dragless cone behind the rocket to make OpenRocket count a second
base. hpr reads the setting from a .ork into
DragOverride, a part’s or stage’s
drag_override. OpenRocket’s documentation names the checkbox but not what it replaces, so the
rule below was measured on 25 probe designs
(drag_override.py;
ADR-167, a part’s drag override as OpenRocket flies it).
- The coefficient is on the rocket’s reference area, the same at every Mach number, and counted once per instance: per fin of a fin set, per lug or button.
- It replaces all of the part’s own drag: friction, pressure drag, and the base drag of its aft face, both the rocket’s base and a step down to a narrower part behind it. A step up at its fore end is its own face too. A step down just ahead of it stays: that is the aft face of the part in front.
- With override for all subcomponents (the design’s
include_children), the parts attached to it have no drag of their own. - On a part inside the body (an inner tube, a ring, a mass) it does nothing; such a part has no drag anyway. On a stage it is added to the rocket’s drag, or, covering its children, it is all of the stage’s drag.
- hpr refuses by name what OpenRocket wasn’t measured on: a coefficient on a pod set, on a part in a pod, on a tube fin set, or covering a pod set.
A worked example. OpenRocket’s Base drag hack (short-wide) is a nose, a body tube 78.7 mm
across, and a 247 mm cone behind it that widens from a point back to the tube’s diameter. The
cone is stated at 0. hpr’s C_D0 at Mach 0.3, from the example file:
| term | without the setting | with it |
|---|---|---|
| the cone’s friction | 0.030 | 0 |
| the cone’s pressure drag, as it widens | 0.020 | 0 |
| the cone’s own base, a second base | 0.132 | 0 |
| the tube’s base, the step down to the cone’s point | 0.132 | 0.132 (the tube’s aft face, so it stays) |
| the nose’s and tube’s friction | 0.062 | 0.062 |
| fins and lug | 0.015 | 0.015 |
| total | 0.390 (rounded terms sum to 0.391) | 0.209 |
Checked against OpenRocket. On every probe, the change in the rocket’s drag from the same
airframe with nothing stated is held to OpenRocket’s
(a_stated_drag_coefficient_moves_the_drag_as_openrocket_s).
The stated number is the same in both, so what differs is each code’s own drag of the parts it
replaces.
| probes | held to | a wrong rule would move it by |
|---|---|---|
| bodies, at every Mach number | 0.0065 (largest 0.0059) | 0.043 or more: a step charged to the wrong part |
| a nose or a boattail stated | 0.0065, up to Mach 0.6 | the same |
| lugs and buttons stated | 0.075 | 0.5: one counted once, not per instance |
| a tube covering its children | 0.0065, less the lugs’ and buttons’ own gaps | 0.03 or more: a lug or button left uncovered |
| a stage covering its children | C_D0 exactly 0.5 in both | anything left uncovered |
| fins | 1e-4 | 1.0: three fins counted once |
| a stage alone, an inner part | 1e-9 | none |
Faster than Mach 0.6, the two codes’ own drag of a nose and a boattail differs by up to 0.21 and 0.061; that is the drag buildups’ difference, not the setting’s.
What it leaves out. When an aft part is stated, OpenRocket also raises the friction of the
parts left, by 6.8% of their friction on the Base drag hack example (about 1% of its C_D0),
as though the body were shorter. hpr keeps each part’s friction as its shape gives it: the parts
are all still there.
On the example’s flights. With recovery in both, Base drag hack reads +1.95% and +4.52% on OpenRocket’s apogee on a C11-5 and a D12-3 motor, and +7.80% on an E12-4. Flown on OpenRocket’s own drag curve, hpr comes within 0.1% of OpenRocket’s flight with nothing deployed, so what is left is the drag coefficient: by OpenRocket’s breakdown, the stubby ellipsoid nose (0.58 calibres long). OpenRocket gives it 0.064 at Mach 0.3. hpr gives it 0.0008, read off a straight line between Hoerner’s two measured round heads, a hemisphere at 0.01 and a longer head at −0.05 (blunt ellipsoids). A one-off probe, not committed, found that about 0.013 to 0.015 would bring the E12-4 within 5%, more than the hemisphere’s 0.01. The gap stays, and the 5% bar is not met. Nothing here measures which program’s nose drag is nearer the truth, but the measurement puts a head this long at or below the hemisphere’s (ADR-173, a blunt ellipsoid’s measured drag; #177).
Drag through Mach 1
Near the speed of sound a nose starts to push shock waves ahead of it, and the pressure on its
surface climbs: the transonic drag rise. Past Mach 1 this pressure drag, called
wave drag, settles
to a value set mostly by the nose’s shape and how slender it is. hpr follows Niskanen’s method
([N09] §3.4.3 and appendix B, pp. 47–48 and 106–110) for noses, shoulders and steps. The other
terms already had their faster-than-sound forms in the table above: friction’s Mach correction,
base drag’s 0.25/M, the fins’ leading and trailing edges, and the stagnation pressure on blunt
faces. The code is hpr_aero::nose_drag; the decision
record is ADR-028. How far it holds against a wind tunnel is under
Verification: it reads high except near Mach 1, most
of all with fins on past Mach 1.2.
A nose’s or shoulder’s pressure-drag coefficient, on the area it adds, has three parts:
- At rest, eq. 3.86’s
0.8 sin² φ, withφthe joint angle at the aft end (the table above). A step, or a bare front face, has no length, and takes the flat face’s0.85 q_stag/qat every Mach number instead (below). - From
M_L, the Mach number where the transonic formula takes over (0.8 or 1 by shape, in the table below; an ellipsoid has its own curve at every Mach number), a transonic and supersonic valueC_T(M)that depends on the shape and the fineness ratiof = l/(d_aft − d_fore): a nose’s length over its base diameter, and for a shoulder its length over its rise in diameter, so that a conical shoulder drags like the cone with the same surface angle. - Between rest and
M_L, eq. 3.87:a Mᵇ + 0.8 sin² φ, withaandbchosen so the curve meetsC_Tand its slope atM_L:b = C_T′(M_L) M_L/Δanda = Δ/M_Lᵇ, whereΔ = C_T(M_L) − 0.8 sin² φ. Niskanen asks for a curve that doesn’t fall and is flat at rest, which needsΔ > 0andb > 1; otherwise hpr uses a quadratic (below). Withbnear 10, as for slender cones, the curve stays close to its value at rest until about Mach 0.8.
| shape | C_T(M) | M_L | source |
|---|---|---|---|
| step up in radius, or a bare front face | a flat face: 0.85 q_stag/q, at every Mach number | n/a | [N09] eq. B.1–B.2 |
| cone | sin ε at Mach 1 with slope 4/(γ + 1)(1 − sin ε/2); 2.1 sin² ε + 0.5 sin ε/√(M² − 1) from Mach 1.3; a cubic between, meeting both ends’ values and slopes; tan ε = 1/(2f) | 1 | [N09] eq. B.3–B.6 |
| ogive | the cone of the same length and diameter, times 0.72 (κ − ½)² + 0.82, κ the tangent ogive’s arc radius over this one’s (0 for a cone, 1 for a tangent ogive) | 1 | [N09] eq. B.8 |
| power series, parabolic series, Haack series | Stoney’s measured curve at fineness 3, C₃(M), scaled to the nose’s fineness by C₀ (C₃/C₀)^log₄(f + 1), with C₀ the flat face’s 0.85 q_stag/q | 0.8 | [N09] eq. B.7, B.9; [S61] Fig. 12 |
| elliptical | below Mach 0.8, Hoerner’s low-speed forebody drag, interpolated in fineness; from Mach 1.2, Stoney’s ellipsoid scaled as above; a straight line between (blunt ellipsoids, below) | none: its own curve throughout | [H65] p. 3-12, Fig. 20; [N09] eq. B.9; [S61] Fig. 12; ADR-173 |
Here γ = 1.4 is the ratio of specific heats of air, q_stag/q the stagnation-pressure ratio of
the table above, and the fineness scaling is the curve a/(f + 1)ᵇ through a flat face at
fineness 0 and the measured nose at fineness 3 (eq. B.7). Stoney’s report suggested that fineness
and Mach number act separately ([N09] p. 108).
Stoney’s curves. Stoney’s 1961 NASA report collected the drag of about 200 bodies flown on
rockets at NASA Langley ([S61]). Its Figure 12 plots the pressure drag of noses of fineness 3
against Mach number: panel (a) from flight models, Mach 0.8 to 2.0, and panel (b) from a wind
tunnel (his ref. 30), to Mach 3.6. No table prints them, so hpr carries them as points read off a
600-dpi scan of the figure, each panel’s grid calibrated where the curve runs, to about ±0.0015.
hpr takes panel (a) for the seven shapes it has, and panel (b) for the x^¼ and the ellipsoid,
which only it has. Where (a) and (b) overlap, (b)’s von Kármán reads 0.004 to 0.011 higher from
Mach 1.2. Past a curve’s last point hpr holds its last value. Panel (b) checks two of those holds:
its von Kármán reads 0.079 to 0.086 from Mach 2.4 to 3.59, against panel (a)’s held 0.079, and its
x^¾ falls to 0.073 by Mach 3.2, 8% under the held 0.079; panel (a)’s x^½ is still rising at its
end. The x^¼ and the ellipsoid, which panel (b) starts at Mach 1.2, are joined by a straight line
to 0 at Mach 0.8, where every smooth 3:1 nose of panel (a) reads 0; so every measured shape starts
at Mach 0.8. An elliptical nose of another fineness draws its own line, after the scaling, from
its low-speed value, held to Mach 0.8 (blunt ellipsoids, below). The points and where each was read are in the code
(StoneyNose). A sample, on the nose’s base
area (panel (a)’s values at Mach 3.0 are its held end values):
| shape | Fig. 12 panel, Stoney’s model number | Mach 0.9 | Mach 1.0 | Mach 1.2 | Mach 1.5 | Mach 2.0 | Mach 3.0 |
|---|---|---|---|---|---|---|---|
| von Kármán | (a), 58 | 0.000 | 0.025 | 0.076 | 0.089 | 0.079 | 0.079 |
| L-V Haack | (a), 60 | 0.000 | 0.025 | 0.100 | 0.116 | 0.112 | 0.112 |
| parabola | (a), 59 | 0.000 | 0.037 | 0.116 | 0.108 | 0.107 | 0.107 |
| ¾ parabola | (a), 62 | 0.000 | 0.069 | 0.104 | 0.082 | 0.081 | 0.081 |
| ½ parabola | (a), 57 | 0.015 | 0.094 | 0.114 | 0.088 | 0.086 | 0.086 |
| x^¾ | (a), 61 | 0.013 | 0.085 | 0.109 | 0.093 | 0.079 | 0.079 |
| x^½ | (a), 63 | 0.000 | 0.046 | 0.080 | 0.086 | 0.090 | 0.090 |
| x^¼ | (b) | n/a | n/a | 0.141 | 0.181 | 0.216 | 0.246 |
| ellipsoid | (b) | n/a | n/a | 0.111 | 0.151 | 0.158 | 0.160 |
A shape between two measured ones interpolates between their curves in its parameter (the
exponent n, K′ or C of Shapes), before the fineness scaling ([N09]
p. 108): a power series xⁿ runs through a flat face (n = 0), x^¼, x^½,
x^¾ and the 3:1 cone (n = 1); a parabolic series through the 3:1 cone (K′ = 0) and the ½, ¾
and full parabolas; a Haack series between von Kármán (C = 0) and L-V Haack (C = ⅓).
Worked example. A 5:1 von Kármán nose at Mach 1.5. Stoney’s 3:1 von Kármán gives 0.0893. The
flat face gives 0.85 q_stag/q = 0.85 × 1.5381 = 1.3074, with
q_stag/q = 1.84 − 0.76/1.5² + 0.166/1.5⁴ + 0.035/1.5⁶. The exponent is log₄ 6 = 1.2925, so
the nose drags
1.3074 × (0.0893/1.3074)^1.2925 = 0.0407 on its base area. A 5:1 cone drags 0.0653 there, from
eq. B.4, so the von Kármán’s wave drag is 38% lower. The test
nose_drag::tests::the_guides_worked_example pins these numbers.
Where hpr departs from, or adds to, the source (ADR-028):
- Short cones and ogives. Below fineness 1 the cone formula runs past a flat face’s drag: as the cone flattens, eq. B.4 tends to 2.39 at Mach 2 against the flat face’s 1.41. So below fineness 1 hpr scales, at every Mach number, between a flat face at fineness 0 and the whole curve of the cone at fineness 1, as eq. B.9 scales the measured shapes. A shoulder then tends to a bare step as it shortens (Loft lesson L15), and above fineness 1 Niskanen’s cone is unchanged.
- Steps. A step up in radius, or a body with no nose cone, is a flat face: the blunt
cylinder’s
0.85 q_stag/qat every Mach number, 0.85 at rest, 0.9947 at Mach 0.8, 1.0888 at Mach 1 and 1.4118 at Mach 2. Eq. 3.86 “does not take into account the effect of extremely blunt nose cones (length less than half of the diameter)” ([N09] p. 47), and a step has no length. Before M1.8b1 it was eq. 3.86’s 0.8 at every speed. - Blunt ellipsoids. An elliptical nose has its own curve below Mach 1.2, from Hoerner’s measurement (blunt ellipsoids, below).
- Where eq. 3.87 has no solution. An x^½ nose meets its tube at a small angle, so it has some
drag at rest, but Stoney’s measured x^½ curve is still at 0 at Mach 0.8: no
a Mᵇcan rise from the first to the second. Eq. 3.87 needs the transonic value above the value at rest andb > 1; where either fails, hpr goes from the value at rest toC_T(M_L)along0.8 sin² φ + Δ (M/M_L)²instead: continuous, flat at rest, with a kink atM_L. Falling, as here, it follows Stoney’s measurement rather than Niskanen’s assumption that the curve doesn’t fall; the coefficients are below 0.01. Rising, it serves near-flat noses: a power series x^0.05 has almost nothing at rest by eq. 3.86, which leaves bluntness out, and rises along it to 0.80 at Mach 0.8. - Refused shapes. A bulged secant ogive (its arc radius below the tangent ogive’s) is outside
eq. B.8, and a Haack series past
C = ⅓outside Stoney’s data (Niskanen limits it the same way, p. 103). The drag buildup refuses both, naming the component, when asked for drag; the model still builds, so the normal force, the center of pressure and a drag table still work.
Blunt ellipsoids below Mach 0.8
This covers an elliptical nose’s pressure drag below Mach 1.2 (ADR-173, a blunt ellipsoid’s measured drag). Stoney’s ellipsoid curve is 0 at Mach 0.8, and eq. B.9 keeps it 0 at any fineness. So before, an ellipsoid nearly as blunt as a flat face got no drag below Mach 0.8, though a flat face gets 0.85. Hoerner measured the forebody pressure drag of round heads on a cylinder at low speed, from their pressure distributions ([H65] p. 3-12, Fig. 20), and hpr now uses it:
- A hemisphere (0.5 calibres long) reads 0.01, and a round head about one calibre long −0.05: the suction on its shoulder outweighs the stagnation pressure at its tip.
- Between them, a straight line, falling 0.12 per calibre past the hemisphere. It crosses 0 at 7/12 calibre, and hpr holds it there, since it never charges a negative pressure drag.
- Blunter than a hemisphere, eq. B.9’s form between the flat face (
0.85 q_stag/q, 0.85 at Mach 0) and the hemisphere’s 0.01. This is hpr’s interpolation: nothing in Fig. 20 measures an ellipsoid between the two. - From the hemisphere up, the value holds to Mach 0.8, and blunter heads follow the flat face’s rise with Mach number. That is hpr’s assumption: the measurement is at low speed, and the drag rise begins near the critical Mach number, about 0.65 to 0.7 for a hemisphere.
- From Mach 0.8, a straight line to Stoney’s curve, scaled to the nose’s fineness, at Mach 1.2, its first point. This is hpr’s join, not data. The scaling comes first, then the line; scaling the line instead, as before, made the drag rise with an infinite slope at Mach 0.8.
Worked example. A ¼-calibre ellipsoid at Mach 0: 0.85 × (0.01/0.85)^(ln 1.25/ln 1.5), where
1.25 is 1 + ¼ and 1.5 is 1 + ½ (the hemisphere), is 0.85 × 0.0867 = 0.074. Base drag hack’s
0.577-calibre nose lies on the line: 0.01 − 0.12 × (0.577 − 0.5) = 0.0008. The test
nose_drag::tests::a_blunt_ellipsoid_takes_hoerners_measured_forebody_drag pins both.
How far to trust it. The values come from one figure, which gives no Reynolds number and no length for the round head; drawn 1.4 calibres long instead of one, it would give this nose 0.005. No measurement covers such a head between Mach 0.3 and 1.2.
Cross-check against a measured cone. Stoney’s Figure 12(a) also has a 3:1 cone. Niskanen’s
closed form reads high through the whole rise: +87% at Mach 0.8 and +105% at 0.85, where eq. 3.87
carries its Mach 1 value down; +49% at Mach 1 and +48% at 1.1, where the cubic join is near its
peak, 0.234 against the measured 0.158; then +15% at Mach 1.5 and +4% at Mach 1.94, the curve’s end
(nose_drag::tests::niskanens_cone_against_stoneys_measured_cone). Ogives inherit this. So a
stubby cone or ogive gains the most drag at high subsonic speeds: Bella Lui’s 1.55:1 tangent ogive
takes its whole rocket’s C_D0 at Mach 0.9 38% above the model before
M1.8b1, which held the nose at its value at rest, where the
von Kármán noses of Calisto and Prometheus move it under 1% (ADR-028).
Boattails faster than sound
A boattail narrows the body toward the tail, usually to shrink the flat
base behind it. Below Mach 0.8 hpr keeps Niskanen’s boattail rule from the table above, a share of
the base drag. Faster than sound two more things happen. The air turns inward around the
boattail’s shoulder and expands, as in a Prandtl–Meyer expansion:
it speeds up, its pressure falls below the free stream’s, and it pulls back on the boattail. That
is a wave drag, and on a short, steep boattail it can be the largest
drag term on the rocket. Behind the boattail, the base’s pressure is higher than behind a plain
cylinder, which lowers the base drag. Code: hpr_aero::afterbody.
Decision: ADR-030.
How far to trust it. Against 58 readings of 20 measured boattails of 3° to 10° from Mach 1.2 to 3.12 it reads −21.9% to +28.3%, and within 0.0123 in drag coefficient: the largest percentages are the smallest drags. Theory that leaves out the air’s viscosity (inviscid theory) reads such boattails up to about 20% high ([CS51] p. 17). Through Mach 1 it reads low, and below Mach 0.8 the rule gives long, gentle boattails almost nothing. Steeper boattails in a thick boundary layer read 26% to 54% high, and the one full rocket measured with one, the Arcas Robin, reads high too. So M1.8b3’s targets were not met: none of the Arcas Robin’s 11 fins-off readings from Mach 1.5 is within 10%, and 8 of Calisto’s 17 supersonic rows against RASAero II are. No whole flight in the validation suite uses this model yet: none of its boattailed rockets passes Mach 0.8. The rules for a boattail drawn in parts, a lip in its wake and the gaps between (the last three rows of the table) apply at every speed, and are judgements that no measurement checks but the Arcas Robin’s lip.
In the table, a boattail runs from diameter d₁ to d₂ over its length l; a = (d₂/d₁)² is its
area ratio, θ = atan((d₁ − d₂)/(2l)) its half-angle (the cone through the same ends; curved
boattails are taken as that cone), and coefficients are on its fore area π d₁²/4, the
cross-section where it starts. C_p,PM is the pressure coefficient after the Prandtl–Meyer turn,
(C_D•)_base the base drag coefficient of the table above, a_b the base’s area over π d₁²/4,
and p_cyl/p_bt a cylinder’s base pressure over a boattail’s. “Jet off” means the measurements
were made with no motor exhaust.
| piece | what hpr does | source |
|---|---|---|
| wave drag, attached flow | MIL-HDBK-762’s chart for conical boattails, 4 C_D (l/d₁)² against x = √(M² − 1)/(2 l/d₁) for a from 0.25 to 0.80, read into the code (±(0.005 + 2%)) | [762] Fig. 5-122, p. 5-187 |
| its upper limit | never more than the pressure after a two-dimensional Prandtl–Meyer turn through θ over the whole annulus, −C_p,PM(M, θ)(1 − a); past the chart’s end at x = 1.4 the drag approaches that limit, the gap shrinking as 1/x | [R1135] eq. 44, 171c |
| separation | between 16° and 30°, a straight-line blend in θ from the attached value to the base drag on the annulus, (C_D•)_base(1 − a), where flow separates | [C57] pp. 6, 8 |
| through Mach 1 | the rule to Mach 0.8, where the buildup’s other transonic terms start; a straight line to Mach 1; from there the attached drag held at its Mach 1.2 value to Mach 1.2 | [N09] p. 47, [762] p. 5-47 |
| base behind a boattail | from Mach 2.5, p_cyl/p_bt = 0.442 + 0.558 a_b, with the cylinder’s pressure from Love’s correlation of measured bases, turned into the ratio of the two base-pressure coefficients, k = (1 − p_bt/p)/(1 − p_cyl/p), which multiplies hpr’s own base drag; below Mach 2.5, k at Mach 2.5; back to 1 between Mach 1 and 0.8; none for a separated boattail | [762] Figs. 5-139, 5-141, pp. 5-208, 5-210 |
| a lip in its wake | a lip behind a boattail or a step down (a boattail of no length), drawn as a shoulder, a step up or both, in one part or several, loses its pressure drag while its top rises up to a quarter of the boattail’s drop in diameter above the boattail’s end, keeps all of it from half, and a straight-line share between; a lip in parts takes at each part the smallest share any top so far leaves; the base behind it takes the same share of the relief, less the lip’s length’s fade. A lip’s rise is its top diameter less the boattail’s aft diameter. So a motor retainer behind the step down to its motor tube loses much of its step’s drag: behind a 98 mm airframe stepping down to a 54 mm motor tube, a 62 mm retainer rises 8 mm, 0.18 of the 44 mm drop, so its rise alone would shelter it wholly, and the 12 mm of motor tube ahead of it fades that by 12/44: its step keeps 27% of its drag (a_retainer_behind_a_step_down_is_in_its_wake), and the rocket’s C_D0 reads 12% to 23% lower from Mach 0.3 to 2.5 than with the retainer’s step in full (the physics review’s measurement; unmeasured in any tunnel); with the exposed motor tube as long as the 44 mm drop, none | [D4014] p. 6, [R22] slide 2; the quarter and half are a judgement |
| a boattail in parts | a narrowing part after another drags, as its share of the boattail it continues, as the cone from that boattail’s start through its aft end less the cone through its fore end (below 0 where extending the boattail lowers its drag); so parts of one straight cone add up to one cone. Its drag is that share for a turn of up to 3° between the parts, its own drag as a boattail from 10° (a corner), and a straight-line blend of the two between (the turn is the difference of the two parts’ half-angles); when either part is shallower than 1°, the merge is scaled by the smaller angle over the larger (the larger taken as at most 1°), so a part narrowing by almost nothing acts as a tube and a straight cone of any angle drawn in parts merges wholly. This holds at every speed, so below Mach 0.8 a curved boattail in parts drags as the cones through its ends, not part by part as eq. 3.88 would | a judgement |
| gaps and steps | whatever lies between a boattail and what follows weakens its effect in a straight line, gone once the gaps add up to one of the boattail’s drops in diameter; gaps add: the lengths of tubes, lips and parts, and the drops in diameter of steps down and narrowing parts. With several boattails ahead, the flow is shared among them: a narrowing part takes over the share it merges with, and takes what no boattail holds as its own; a step down counts as a boattail of no length. The base and each lip add the boattails’ shares. So a part narrowing by nothing drags as a tube, a part of no length as a step, and a small change in any radius or length changes the drag a little | a judgement |
Why each piece is there:
- The chart is second-order theory. MIL-HDBK-762 cites no source for it. Jack computed conical
boattails by Van Dyke’s second-order theory ([J53]), an inviscid method that keeps the next
term past linear theory’s small-disturbance approximation, from Mach 1.5 to 4.5, and the chart
agrees with his 83 points inside it within −10.4% to +8.0% for
aup to 0.6. First-order (linear) theory reads far higher at these angles, so the chart is not linear theory. - The 2D limit matters for short, steep boattails. The chart plots its drag against one
combined variable,
x, which holds only for small angles, and near Mach 1 it can ask for more suction than a flat (two-dimensional) turn gives, which a round boattail, whose pressure recovers aft of the shoulder, can’t exceed. Past the chart the same limit gives the curve its shape: against Jack’s 28 points beyond it, −2.7% to +8.0%. - Near Mach 1 no method exists for boattails; [762] p. 5-47 says so and advises holding the supersonic value to a peak between Mach 1.0 and 1.2. The straight line starts at Mach 0.8, where hpr’s other transonic terms start, and is half-way up at 0.9. The measured rise is later and steeper: half-way by about 0.89 for Compton’s 10° boattail ([C72]) and 0.92 to 0.96 for Cubbage’s ([C57]), with a peak at Mach 1.0 to 1.1 that holding the Mach 1.2 value doesn’t reach. An earlier draft started the line at Mach 0.9, a choice made after seeing the Arcas Robin, one of the targets; the validation audit caught it, and it was put back to 0.8.
- The base’s relief is the handbook’s correlation, measured at Mach 2.5 to 3.5. Used as a
ratio of pressures below Mach 2.5 it over-predicts the relief of the bases measured at Mach
1.59 and 1.91 ([DN54], [CS51]); held as a ratio of coefficients, it matches them on average.
It depends on the base’s area only, where the measured relief also grows with the boattail’s
angle: behind Cortright and Schroeder’s small bases (
a_b = 0.256) hpr keeps 0.352 of the cylinder’s base pressure coefficient at every angle, where they measured 0.60 at 5.6° and 0.26 at 9.3°. - The lip. NASA’s Arcas Robin models end in a lip 1.3 mm long that flares from the boattail’s end to the base. NASA found it lowering the force on the balance chamber inside the base at Mach 1.5 and 1.8 with the fins off, and its effect “masked” when the flow over the boattail separates or the boundary layer thickens ([D4014] p. 6); RASAero II’s own comparison with the tunnel left it out as “buried in the boattail boundary layer” ([R22]). hpr used to take it as a stubby cone in undisturbed air, 0.085 of drag. The quarter and half that bound the wake were chosen knowing this lip rises 0.17 of its boattail’s drop, and every one of the 44 Arcas Robin rows depends on that choice: with the lip counted as a shoulder in undisturbed air, each would read 0.065 to 0.086 higher.
A worked example: Calisto at Mach 1.5. Calisto’s boattail is 60 mm long from 127 mm to 87 mm:
a = 0.469, l/d₁ = 0.472, θ = 18.4°. The chart’s x is √1.25/(2 × 0.472) = 1.18, where it
gives C_D = 0.219. A Prandtl–Meyer turn of 18.4° from Mach 1.5 gives C_p = −0.398, so the
limit is 0.398 × 0.531 = 0.211, and the attached boattail drags 0.211. At 18.4° it is 17% of
the way from 16° to 30°, so the blend takes it 17% toward the base drag on the annulus,
0.167 × 0.531 = 0.088: 0.190, where the rule gave 0.066. For the base, a_b = 0.469: at Mach
2.5 Love’s cylinder gives −C_p = 0.1195, so p_cyl/p = 1 − 0.1195 × 0.7 × 2.5² = 0.477, and
p_bt/p = 0.477/(0.442 + 0.558 × 0.469) = 0.678, so k = 0.322/0.523 = 0.616; after the blend
0.683, so the base’s drag falls from 0.078 to 0.053. Calisto’s C_D0 at Mach 1.5 rises from
0.443 to 0.542. The module’s documentation test runs this example
(hpr_aero::afterbody).
Against measurements (drag-vs-mach.json, sections boattails,
base_pressures and second_order_theory, from the readings in
measured-boattails.json; tests::boattails_against_measurements). Every
source is a wind tunnel with a turbulent boundary layer and the jet off; each value was read from
the report’s figure, with its reading uncertainty in the file. The last column says whether the
rows helped build the model: those check it on its own data.
| boattails | Mach | rows | hpr against measured | helped build it |
|---|---|---|---|---|
| attached, 3° to 10°: [CS51], [DN54], [C72], [MJ54], [C57] | 1.2 to 3.12 | 58 | −21.9% to +28.3%, within 0.0123 | no |
| attached, 5.6° and 8° ([C57]) | 1.0 and 1.1 | 4 | −18.2% to −5.4% | partly: Cubbage’s peak was weighed in holding the Mach 1.2 value |
| attached, 3° to 10°, points near Mach 1 that Compton calls questionable (strut interference, reflected bow shock; [C72] p. 9) | 0.95 to 1.1 | 27 | −46.2% to +60.0% | no |
| attached, 3° to 10°, in the rise ([C72], [C57]) | 0.85 to 0.95 | 28 | −77.5% to +7.6% | no |
| attached, 3° to 10°, under the rule ([C72], [C57]) | 0.3 to 0.8 | 58 | −100% to −83.5% | no |
16°, attached, boundary layer 0.20 d₁ thick ([C57]) | 1.0 to 1.28 | 9 | +26.4% to +54.2% | no |
| the same, below Mach 1 | 0.6 to 0.9 | 6 | −30.2% to +60.4% | no |
| 30° and 45°, separated ([C57]) | 1.2 | 3 | −2.8% to +6.6% | yes: the separation angles |
| the base behind 5° to 10° boattails ([CS51], [DN54]) | 1.59, 1.91 | 8 | base drag within 0.0102 of the measured on the cylinder’s area; behind small bases up to about 40% of the base’s own drag | yes: the ratio held below Mach 2.5 |
| the base behind 2.5° to 15° boattails ([Love57]) | 3.24 | 4 | within 0.003 | no |
The 0.0102 and 0.0123 are pins on these readings, not tolerances. Below Mach 0.8 Niskanen’s rule, which gives nothing to a boattail longer than three times its drop in diameter, gives Compton’s and Cubbage’s long, gentle boattails 0 to 0.009 where they measure 0.011 to 0.075 (issue #73).
What it leaves out.
- Steep boattails in a thick boundary layer read high. hpr reads Cubbage’s 16° boattails, in a boundary layer a fifth of the diameter thick, 26% to 54% above the measurements, though the flow is still attached. The Arcas Robin’s 15° boattail sits in one thicker than its own drop in radius, and its forebody reads +13.5% to +24.1% from Mach 1.5 (below). No source here gives a correction, so none is applied (issue #72); MIL-HDBK-762 advises boattails under 8° to avoid separation ([762] p. 5-12).
- Through Mach 1 it reads low for gentle boattails: the straight line from Mach 0.8 misses the measured rise’s later, steeper climb and its peak.
- Separation’s 16° and 30° come from one report at Mach 0.6 to 1.28. Between 10° and 30° no attached boattail was measured faster than Mach 1.28, so a boattail of 12° to 20°, like Calisto’s 18.4°, rests on the least-validated part of the model, and likely reads high.
- For one length and area ratio, Jack found the cone’s wave drag the smallest of three shapes
([J53] p. 1), so a curved boattail likely drags more than hpr gives. The chart’s 0.70 and 0.80
curves read up to 32% above Jack past
x ≈ 1, 0.0084 at most. - A tube behind a boattail loses the base’s relief over one drop in diameter, and the 3°, 10°, quarter and half that shape the merge and the wake are judgements, with no measurement behind them but the Arcas Robin’s lip. Its own 1.3 mm length is a gap too: the base keeps 0.944 of its relief.
- A straight cone drawn in parts drags as one cone. A part’s share can be below 0, where the chart makes the longer cone drag less than the shorter: extending the boattail lowers its drag. The one exception: behind a boattail it only partly merges with (a turn of 3° to 10°), a cone’s parts may drag a little less than the whole: an 8° cone behind a 14° part reads the same in 2 or 4 parts, up to 0.45% lower in 8, and up to 2.2% lower drawn in hundreds, worst near Mach 1.
- The 1° below which a part merges only in part, and the fade of a step’s corner over one drop in diameter (flow behind a backward-facing step reattaches farther downstream, so this likely understates a lip’s shelter), are judgements too.
- Nothing models the jet. Fig. 5-141 is measured with the motor off, as is the base drag it scales, and under power hpr applies both to what the motors leave of the base.
Drag limits
- The buildup covers Mach 0 to 5 and refuses Mach 5 and faster, like the normal force
(
drag::BUILDUP_MACH_LIMIT); an override table takes any Mach number. - It reads high against the one wind tunnel it has been measured against, at every reading: with the fins off, 12% to 54%, most of it the model’s steep boattail; with the fins on, past Mach 1.2, far more, from the fins (Verification). The fins’ leading edge takes [N09]’s rounded-edge formula, a blunt edge’s, for the airfoil and rounded sections alike. Nothing models the wave drag of a thin, sharp fin, which is far smaller: the Arcas Robin’s four double-wedge fins measure 0.046 at Mach 4.63, against hpr’s 0.30.
- Against MIL-HDBK-762’s worked example, fins left out, the body reads high from Mach 0.9 to 1.2 (the nose and the base) and 6% to 10% low from Mach 1.6 (friction and the base) (Drag against MIL-HDBK-762’s sample calculation).
- It reads low against RASAero II’s Calisto past Mach 1.6, −14.9% at Mach 2 (Drag against RASAero II through Mach 2). Part of that is the body’s lean above; plausible fin inputs bring most rows within 10%.
- Through the transonic rise, from Mach 0.8 to 1.2, the measured shapes follow Stoney’s curves, and cones and ogives Niskanen’s closed form, which reads 45% to 105% above Stoney’s measured 3:1 cone there (the cross-check above).
- Shoulders and boattails past Mach 1. A shoulder takes the nose method, which [N09] calls “somewhat dubious at supersonic velocities” (p. 48), except a lip in a boattail’s wake, which loses a share of it. A boattail keeps eq. 3.88 to Mach 0.8, a rule “based primarily on subsonic data” (p. 49), which over-predicts the Arcas Robin’s 15° boattail and gives long boattails nothing (issue #73). Faster than sound its wave drag reads high for steep boattails in a thick boundary layer (Boattails faster than sound).
- Stoney’s curves end at Mach 1.94 to 1.99 (panel (a)) and 3.59 (panel (b)); past that hpr holds their last value, which panel (b) puts within 8% for two shapes (above).
- Stubby noses. Below fineness 1 a cone or ogive blends toward the flat face at every Mach number, so at rest it reads above eq. 3.86: a cone of fineness 0.5 gives 0.547 against eq. 3.86’s 0.400. And two routes to one shape disagree: a tangent ogive and an ellipse of fineness 0.5 are both hemispheres, but the ogive rises from rest by that blend, which no measurement backs, while the ellipse takes Hoerner’s measured 0.01 to Mach 0.8 and then rises in a straight line (blunt ellipsoids). Below Mach 0.8, trust the ellipse’s.
- Before M1.8b1 the buildup held nose and shoulder pressure drag at its value at rest and refused Mach 1.
- Nothing models laminar flow, fin-tip vortices, interference drag, fin tabs, fillets, canted fins or the flow a boattail guides into the base ([N09] p. 51).
Validity and open questions
- A flared body’s supersonic normal force rests on one measured flare. Since M1.8e17 a conical flare flies the shock-expansion method while its corner’s shock is attached, which moves a flared rocket’s center of pressure forward by a tenth to a quarter of a calibre against the model it had before. The one measurement beside it is TN D-4865’s model 2, an 18.5° flare on a 2.75° cone: −1.9% and +7.0% at Mach 1.9 and 2.3, +13.4% at 2.96, and +51.5% and +50.4% at 3.95 and 4.63, where that flare’s boundary layer is separated (What a marched flare is worth). No other flare angle, and no other body, has been checked. Two more things about that model are open: the attachment test is a wedge’s limit, not a flare’s own (A flare through the method), and a near-flat flare has its one element read by the older generalized method, which leaves a step at the corner’s crossing: +0.129% of the normal force and 0.0051 calibres on the tests’ rocket, and +4.3% and 0.19 calibres on a body whose shoulder is short, where the region sits at degrees rather than thousandths of one (A near-flat flare).
- A step in radius is unmodeled, and nothing measures one faster than sound. Any mismatch of radius at a joint (past 2.7e−11 m between two tubes, or past 1.3e−13 m stepping up at a boattail’s fore end, both far below any tolerance anyone builds to) takes the whole body off the shock-expansion method at every speed. On the tests’ straight rocket that is worth −8.65% of the normal force and 1.03 calibres of center of pressure at the threshold, and −12.55% and 1.36 calibres at a 2 mm step down; on the boattailed one, −11.34% and 1.10 calibres (A step in radius). No published source gives a stepped body’s normal force faster than sound, so the present behavior is not known to be right either; stopping the march at the step was built and rejected (ADR-049, issue #87: a step has no model of its own).
- The body alone misses the 15% target on six of eleven wind-tunnel rows, the one M1.8e set for the body faster than sound, by +37.7% at worst (the short Arcas Robin at Mach 1.5) and within 5% at Mach 3.96 and 4.63. The likeliest cause is Jorgensen’s crossflow term reading too large at the few degrees a slope is fitted over, but the measurement cannot split its own slope from its curvature cleanly, and one row points at the method instead. Closing it needs a cited rule for how the crossflow term grows from zero over the first few degrees, or measurements at finer angles than the reports plot; neither is in hand, so the gap is left visible (The body alone, against the 15% target).
- A center of pressure means little where the body’s normal force is near zero. A deep, steep transition can remove almost all the lift the nose and tube carry, and hpr still divides the moment by what is left: one test shape reports its body’s center of pressure 160 calibres ahead of its own nose tip at Mach 1.2 (issue #104: a near-zero normal force gives a meaningless center of pressure).
- A blunt tip’s handover is capped where the method’s march still settles, not where its
theory runs out. The cap is 24° and the cone slopes reach 30°; at 30° the committed Arcas
Robin nose reads nearer the report’s own sphere-cone but its answer starts to follow the element
count above Mach 4, so the cap stays. What that is worth is measured on both sides in
What the cap is worth, and the way out is
issue #108: a steeper handover puts the march into
η < 0above Mach 4. - A boattail steeper than 16° is worth 0.67 to 1.35 calibres of doubt, the most at the lowest supersonic speeds. Nothing measures a separated boattail’s supersonic normal force; hpr holds the measured correlation at 16° rather than letting it fade, which is the conservative end (A steep boattail reads the correlation no steeper than 16°).
- These are small-angle models.
αis accepted over[0, π], but fin slopes stay linear inαand nothing models stall. The flight engine uses them at every angle all the same (Rigid-body flight), so its results are least trustworthy where large angles occur: off the rail in a strong crosswind, and near apogee. - Body lift in wind (measured by flying both codes, not against a real flight;
ADR-026, ADR-037). A rocket that leaves the rail slowly in
a crosswind meets the air at a steep angle. Juno III, one of RocketPy’s example rockets, leaves
at 18 m/s in an 8.5 m/s wind, 26° off the airflow, and there body lift is nearly half its normal
force. Much of it acts ahead of the rocket’s center of mass, the nose’s above all, so it moves
the center of pressure forward and weakens the moment that
turns the rocket into the wind; hpr turns into it less than
RocketPy, whose normal force has no body term. Its sideways push alone is about a sixth of
body lift’s effect on the drift. Juno III’s apogee ends 245.3 m from the pad in hpr and 396.6 m in
RocketPy; body lift is about half of that difference, and hpr’s rail release and fin slope most
of the rest. Body lift’s size matters there. Flown in RocketPy with hpr’s model, Juno III’s
apogee drift is 248.3 m with Jorgensen’s crossflow (Body lift,
η C_dnabout 0.91 at that speed), and with a constantKacross [G]’s range from 194.1 m atK = 1.5to 240.2 m at 1.0 (231.1 m at 1.1, hpr’s before M1.8e6); it would be 328.0 m with no body lift. Calisto, off the rail at 28 m/s and 11°, changes its drift by under 0.5% across that range. Which is nearer a real flight is open: the real flights so far compare heights, not drift. - No airfoils. Fins use the flat-plate lift slope (2π per radian in two dimensions). An airfoil lift curve, such as the one Juno III’s example gives its fins, is not modeled; RocketPy uses it, and its fin slope there is 7.6% steeper (ADR-026).
- In one measured case, fins at
α = π/2giveC_N17.4 against a flat-plate estimate near 5, and atα = πthe fins still give 34.7 while every body term vanishes. That case is a 54 mm four-fin rocket at Mach 0.3. - Speed. The normal force and the drag buildup cover
0 ≤ M < 5; a drag table covers any Mach number.- The drag buildup’s transonic and supersonic terms are [N09]’s semi-empirical ones. [N09] expects them “to be reasonably accurate to at least Mach 1.5” (p. 94); against the one wind tunnel they read high from about Mach 1.2 (Drag limits). On the one supersonic flight compared with OpenRocket, hpr’s supersonic pressure drag is about twice OpenRocket’s (#222: which is right is open).
- The normal force between Mach 0.8 and linear theory’s start
M_sis the straight-line join of Fins through Mach 1, which the wind tunnel shows missing by up to +27.1% in slope and 2.36 calibres in CP. Past Mach 3 its body terms read low (Normal force through Mach 1). - [N09] eq. 3.35–3.36 would start moving a fin set’s CP aft at Mach 0.5, to about 0.30 of the way along its mean aerodynamic chord (MAC, defined under Fins) at Mach 0.8 for fins of aspect ratio 1.6 (a measure of how long the span is against the chord). hpr keeps 0.25 to Mach 0.8: the Arcas Robin’s measured CP moves forward, not aft, from Mach 0.6 to 0.8.
Verification
-
Barrowman’s worked examples (
hpr_aero::tests::barrowman_worked_examples). Inputs and printed results, with page numbers, are in the fixturevalidation/fixtures/aero/barrowman-worked-examples.json. Every printed component and total must agree within 1%. Measured:example C_Nα: hpr / printed CP: hpr / printed (in) Testbed II [B66] pp. 41–45 21.397 / 21.44 (−0.20%) 16.703 / 16.7 (+0.02%) Aerobee 350 [B66] pp. 47–50 21.449 / 21.5 (−0.24%) 390.48 / 391 (−0.13%) Javelin [TIR] pp. 21–22 35.927 / 35.9 (+0.07%) 11.286 / 11.3 (−0.13%) Recruiter [TIR] pp. 23–25, hpr’s model: outside 1% 36.416 / 35.4 (+2.87%) 15.665 / 15.6 (+0.42%) Recruiter with TIR-33’s six-fin rule substituted 35.415 / 35.4 (+0.04%) 15.627 / 15.6 (+0.17%) Arcon-Hi, two stages [TIR] pp. 27–29 96.163 / 96.2 (−0.04%) 20.803 / 20.8 (+0.02%) Arcon-Hi, sustainer alone 32.257 / 32.2 (+0.18%) 17.845 / 17.9 (−0.31%) - With hpr’s own model, every CP agrees within 1%, and every slope but the Recruiter’s. Its six-fin slopes are +3.42% (fins) and +2.87% (total). Those are the only 2 of the 38 printed values (19 slopes, 19 CPs) outside 1%, and the test pins that list.
- With TIR-33’s six-fin rule substituted for the Recruiter, the worst is the Testbed II nose CP, −0.77%: [B66]’s 0.466 L fit against the integrated tangent ogive.
- CPs are compared as stations from the nose tip. Measured from each part’s own front, two printed values miss 1%: the Testbed II boattail (0.655 in against 0.72 in, −9%, Barrowman’s diameter ratio slip) and the Javelin fins (0.653 in against 0.66 in, −1.1%, rounding).
- Recruiter’s six fins. TIR-33 scales six fins by
N/2withK = 1 + 0.5 R/(S + R)and no fin-count factor. With hpr’s own rule (0.913 and the fullK), the fins are +3.42% and the total +2.87% from the print. The difference between the two rules accounts for +3.22% and +2.83% of that. The test checks the TIR-33 rule within 1% (slopes and CP weighting), reports hpr’s own values, and checks that the rules differ by more than 2%. - The printed mid-chord lengths were measured or rounded. hpr computes them from the geometry (Aerobee: 39.7 in printed, 40.50 in geometric). The fixture’s notes list each slip in the printed arithmetic.
-
Loft lessons, each with what it concerns:
- L7, a fin slope and CP that never changed with Mach:
fins::tests::fin_cna_compressibility_reduces_to_barrowman_at_m0 - L8, no correction for five to eight fins:
fins::tests::six_fin_cna_applies_fin_count_factor - L9, the conical transition’s CP formula used for every shape:
body::tests::ogive_transition_cp_uses_volume_form - L10, elliptical fins given a trapezoid’s sweep:
fins::tests::elliptical_fin_cna_uses_zero_midchord_sweep - L89, Barrowman’s values worked by hand, a check kept from Loft’s tests:
tests::barrowman_hand_values. A cone’s slope is 2 with its CP at 2L/3 (Lits length); a conical transition from 20 to 40 mm radius over 0.1 m is 1.5 at 0.05556 m aft of its fore end; an elliptical fin’s CP is 0.28779c_raft of its root leading edge.
- L7, a fin slope and CP that never changed with Mach:
-
Limits and invariants (
body::tests,fins::tests,model::tests):- Cylinders and thin transitions; body lift at 0 and 90°.
- Eq. 57 and 76a closed forms against the same trapezoid as a polygon (1e-13).
- A 2000-gon ellipse, and a jagged fin.
- Prandtl–Glauert against [B67] eq. 3-6 written with the aspect ratio, and its
M → 1limit. - Roll sums against direct sums; a two-fin set along and across the flow.
- Mach changes only the fins.
- Supersonic linear theory on a rectangle, whose slope and CP have closed forms, including a tip cone that crosses the root; outlines of each planform; where linear theory starts.
- A proptest (a rule checked on many random inputs): scaling every length leaves slopes unchanged and scales the CP; the reference diameter scales slopes only.
- Refusals: nine fins, Mach 5 for the normal force and Mach 1 for the drag buildup, angles out of range.
Normal force through Mach 1
Two references, in the fixture
validation/fixtures/aero/normal-force-vs-mach.json,
which cargo xtask aero writes and tests::normal_force_against_mach recomputes and pins
(ADR-027). The targets, set before measuring: C_Nα within 15% and the CP within 0.5
calibres (a calibre is one reference diameter). 13 of the 37 rows miss, each for a measured
reason below.
- A wind tunnel. NASA tested half-scale models of the Arcas Robin sounding rocket from Mach 0.6
to 4.63 ([D4013], [D4014]): a nose 4.2 calibres long, a cylinder, a 15° boattail and four trapezoidal
fins swept 30°, 18.2 calibres long in all, and a longer version of 23.8. The reports print only
plots, so their points were read off the pages into
validation/fixtures/aero/arcas-robin-wind-tunnel.json, with every figure and page, eachC_Nto about ±0.01 to ±0.02. The designs model the reports’ nose, a table of coordinates rather than a named shape, as a power-series nose with the same volume, which sets its slender-body CP, and a planform within 2.4%. The slope is the straight line fitted through the plottedC_Nfrom about −4° to +4°, so hpr’sC_Nis fitted the same way at the same angles. Its CP is taken over −2° to 2°, the reports’ low angles. - RASAero II, another code, for Calisto from Mach 0.1 to 2.0: its potential-flow slope (the attached-flow part, without the crossflow lift its export adds from Mach 0.95) and CP from the export RocketPy’s first commit shipped, against hpr’s small-angle values.
The short model (the Arcas Robin itself, 18.2 calibres long), rows outside the targets in bold.
The last column is the body alone: the fins-off wind-tunnel reading, and hpr’s body terms fitted
the same way. The design’s lip sits in the boattail’s wake and carries nothing faster than sound
(A lip in a boattail’s wake), and its vertical tip flies behind a
Newtonian cap (Blunt tips), so the body flies the method to its base from Mach 1.2:
fins off it reads 3.02 to 3.95 per radian from Mach 1.5, where it read 1.90 to 2.09 on slender-body
theory. Below the join its body lift grows as sin² α and with the crossflow Mach number, so
their fitted slope moves a little with Mach and with the angles each plot happens to cover (1.90
to 2.09). From Mach 0.6 to 1.2 the fins-off readings, on a coarse grid (±0.02 per point), scatter
from 1.41 to 2.88 with no trend, so they don’t settle whether hpr’s 1.91 is high there.
| Mach | C_Nα measured, per rad | hpr | difference | CP measured, m | hpr | difference, calibres | body alone, measured / hpr |
|---|---|---|---|---|---|---|---|
| 0.6 | 11.05 | 10.54 | −4.6% | 0.7770 | 0.7837 | +0.12 | 1.53 / 1.91 |
| 0.8 | 9.92 | 10.85 | +9.5% | 0.7375 | 0.7896 | +0.91 | 1.41 / 1.90 |
| 0.9 | 10.22 | 12.77 | +25.0% | 0.7447 | 0.8215 | +1.34 | 2.44 / 1.91 |
| 0.95 | 11.88 | 13.74 | +15.6% | 0.7952 | 0.8342 | +0.68 | 2.88 / 1.92 |
| 1 | 15.79 | 14.69 | −6.9% | 0.8690 | 0.8455 | −0.41 | 1.58 / 1.92 |
| 1.2 | 15.42 | 18.54 | +20.2% | 0.8862 | 0.8798 | −0.11 | 2.43 / 1.92 |
| 1.5 | 13.43 | 14.61 | +8.8% | 0.8137 | 0.8282 | +0.25 | 2.19 / 3.02 |
| 1.8 | 11.99 | 12.46 | +3.9% | 0.7908 | 0.7883 | −0.04 | 2.61 / 3.29 |
| 2.3 | 9.89 | 10.50 | +6.2% | 0.7505 | 0.7369 | −0.24 | 3.08 / 3.60 |
| 2.96 | 8.77 | 9.10 | +3.7% | 0.6967 | 0.6858 | −0.19 | 3.28 / 3.84 |
| 3.96 | 7.73 | 7.85 | +1.6% | 0.6312 | 0.6330 | +0.03 | 3.88 / 3.95 |
| 4.63 | 7.55 | 7.30 | −3.3% | 0.5876 | 0.6073 | +0.34 | 4.15 / 3.95 |
| reference, Mach | C_Nα difference | CP difference, calibres | rows within both targets |
|---|---|---|---|
| Arcas, long, 0.6 and 0.8 | +0.7%, +8.9% | −0.36, +0.07 | 2 of 2 |
| Arcas, long, 0.9 to 1.2 | −8.8% to +27.1% | +0.67 to +2.36 | 0 of 3 |
| Arcas, long, 1.8 to 2.96 | +8.4% to +9.4% | −0.53 to −0.43 | 1 of 3 |
| Arcas, long, 3.96 and 4.63 | +2.8%, −2.3% | −0.13, +0.21 | 2 of 2 |
| Calisto against RASAero II, 0.1 to 0.7 | +0.1% to +10.1% | −0.08 to +0.43 | 4 of 4 |
| Calisto against RASAero II, 0.8 to 2.0 | −6.3% to +21.9% | −0.44 to +0.95 | 7 of 11 |
What the misses come from:
- Faster than sound, every slope now passes, and the long model’s CP at Mach 1.8 and 2.3 does not. Until M1.8e7 and M1.8e8 the committed designs’ vertical tip and lip kept the shock-expansion method off, so their bodies flew slender-body theory past Mach 1 and lifted 2.0 to 2.6 per rad where the tunnel’s body alone lifts 2.2 to 4.6: the short model read −16.3% at Mach 2.96 and −28.0% at 4.63. With the cap (Blunt tips) and the lip carrying nothing in the boattail’s wake (A lip in a boattail’s wake), the body grows with Mach, as the measurement does though not as steeply (3.02 to 3.95 per rad fins off on the short model, against the tunnel’s 2.19 to 4.15). The whole rocket’s rows from Mach 1.5 read +8.8% to −3.3% (short) and +9.4% to −2.3% (long). What is left is where the body reads high: fins off it is 15% to 19% above the tunnel at Mach 1.8 and 2.3 on the long model, which pulls the whole rocket’s CP 0.53 and 0.52 calibres forward of the measured one, just outside the half-calibre target. M1.8e6 sized that excess and left it (ADR-037); the body alone is judged against the 15% target in The body alone, against the 15% target, where it is outside on six of eleven rows. The fins’ share (the fins-on reading less the fins-off one) agrees with hpr’s fins within −1.4% to +7.0% at Mach 3.96 and 4.63, with about 5% of doubt of its own: over the boattail the models’ fin roots follow its 15° surface below the cylinder, and the design leaves that strip out, about 0.32 in² of each fin’s 5.8 in² (5.5%).
- Mach 0.6, within the targets by errors that cancel. Both models pass there, but hpr’s body is 25% and 19% above the fins-off readings, which are poorly determined at these speeds, and its fins’ share 9.3% and 3.9% below the measured one.
- Transonic, Mach 0.8 to 1.2. The fins’ measured share lifts less at Mach 0.8 and 0.9 than at
0.6, then jumps at Mach 1. hpr’s fins lift more, by Prandtl–Glauert and then along
the join to linear theory’s peak at
M_s(1.2 for these fins). The long model’s CP jumps forward at Mach 1, 2.36 calibres from hpr’s. No closed-form method covers this region, and the join is not fitted to it. - RASAero II keeps its slope and CP constant through subsonic flow, where hpr’s rise with
Prandtl–Glauert, so they part from Mach 0.8. Past Mach 1.2 Calisto’s von Kármán nose flies the
shock-expansion method behind a Newtonian cap, so its cylinder carries lift:
Mach 1.5 reads +12.9% and Mach 2 +8.3% (−3.1% and −16.8% on slender-body theory, before
M1.8e7). The wind tunnel sides with neither there. Below
that the agreement is partly by construction: the Calisto design has the 2018 fins because
they reproduce this export at low speed (ADR-009). Past Mach 1 the result rests on
the choice of RASAero’s columns: against its secant slope and CP to 4°, which include its
crossflow lift, 3 of the 11 rows from Mach 0.8 are within the targets (the tightest, Mach 1 by
0.00002 calibres), not 7, and Mach 2 is −9.6%. That comparison is a summary in the fixture’s
secant_comparison, not a second set of rows: the export stays inrefs/, and the fixture commits its values once (ADR-009, ADR-027). Calisto has no fins-off data, so its body and fins can’t be split as the wind tunnel’s can.
So, for fins like these, whose linear theory starts at M_s = 1.2: from Mach 1.5 up, trust hpr’s
slope to about 10% (the rows run +9.4% to −3.3%) and its CP to about half a calibre, which the long
model misses by 0.03 at Mach 1.8 and 0.02 at 2.3; between Mach 0.8 and M_s, in the join, neither. A fin set’s own M_s is
FinSetAero::fin.supersonic_mach,
from AeroModel::fin_sets. Fins
swept further back start later: a leading edge swept 48° starts at Mach 1.5, and until then it
is in the join. Nothing past Mach 4.63 has been checked, though the model runs to 5.
The body alone, against the 15% target
What this covers: how far hpr’s body alone is from NASA’s measurement of the same body, and where what is left of the gap sits. How far to trust it: at Mach 3.96 and 4.63 the two agree within 5%; below that hpr reads up to 38% high. On five of the six rows outside the target most of that excess is body lift; on the sixth it is the method itself.
The milestone M1.8e set a target before any of this was built: the Arcas Robin’s body alone within 15% at every Mach number from 1.5, and both configurations’ whole-rocket slope within 15% at Mach 3.96 and 4.63. The second half is met (+2.8% to −3.3%). The first is not, on six of eleven rows, and this is where they stand (ADR-040). Reading the table:
- Rows outside the target are in bold. Slopes are per radian on the body’s cross-section.
M/f_nis the Mach number over the nose’s fineness, the argument TN 3527 ([SD56]) states its method for from 0.4 to 2. One row, the short model at Mach 1.5, is below that at 0.36.- measured and hpr are the straight-line slopes fitted at the tunnel’s plotted angles, as ADR-036, which fixes how these comparisons are fitted judges them.
- at
α → 0is the slope at zero angle. hpr’s is the method alone, since body lift vanishes there; the measurement’s comes from fitting its points withC_N = a α + b α |α|, the form the tunnel’s own curves follow, andais quoted with its standard error. - curvature is the rest: the fitted slope less the slope at
α → 0. For hpr it is body lift.
| model | Mach | M/f_n | measured | hpr | difference | measured at α → 0 | hpr at α → 0 | at α → 0, hpr ÷ measured | curvature, hpr ÷ measured |
|---|---|---|---|---|---|---|---|---|---|
| short | 1.5 | 0.36 | 2.192 | 3.017 | +37.7% | 1.779 ± 0.32 | 1.852 | 1.04 | 2.82 |
| short | 1.8 | 0.43 | 2.613 | 3.290 | +25.9% | 2.519 ± 0.33 | 2.143 | 0.85 | 12.20 |
| short | 2.3 | 0.55 | 3.078 | 3.598 | +16.9% | 2.196 ± 0.32 | 2.394 | 1.09 | 1.37 |
| short | 2.96 | 0.71 | 3.284 | 3.838 | +16.9% | 2.184 ± 0.30 | 2.612 | 1.20 | 1.11 |
| short | 3.96 | 0.95 | 3.884 | 3.946 | +1.6% | 2.694 ± 0.32 | 2.735 | 1.02 | 1.02 |
| short | 4.63 | 1.11 | 4.149 | 3.950 | −4.8% | 2.758 ± 0.32 | 2.718 | 0.99 | 0.89 |
| long | 1.8 | 0.43 | 3.159 | 3.770 | +19.4% | 1.920 ± 0.42 | 2.143 | 1.12 | 1.31 |
| long | 2.3 | 0.55 | 3.525 | 4.071 | +15.5% | 2.245 ± 0.41 | 2.394 | 1.07 | 1.31 |
| long | 2.96 | 0.71 | 3.868 | 4.400 | +13.7% | 2.521 ± 0.36 | 2.614 | 1.04 | 1.33 |
| long | 3.96 | 0.95 | 4.455 | 4.428 | −0.6% | 3.129 ± 0.41 | 2.740 | 0.88 | 1.27 |
| long | 4.63 | 1.11 | 4.615 | 4.425 | −4.1% | 2.877 ± 0.41 | 2.724 | 0.95 | 0.98 |
What the rows say, and what they can’t. What is solid is the first three number columns: on six
rows hpr’s fitted slope is 15% to 38% above the tunnel’s, and at Mach 3.96 and 4.63 it is within
5%. The split into a slope at α → 0 and a curvature is softer, and it is worth saying why before
leaning on it. The tunnel plots seven points over about ±4.5°, and in a fit of
C_N = a α + b α |α| over so short a span the two terms trade off almost exactly: their
correlation is −0.96. A fit that reads a low must read b high. So the measurement’s own split
carries the standard errors in the table (±0.30 to ±0.42 per radian, the widest of them on a
slope of 1.920), and the curvature, being the same slope subtracted from another, carries at least
as much.
Where the gap most likely is. With that said: at α → 0 hpr is within 1.5 standard errors of
the measurement on every row outside the target (0.2 to 1.5 of one), so the readings cannot
convict the shock-expansion method, the Newtonian cap or the boattail’s measured share. On five of
those six rows most of the fitted gap sits in the curvature instead: what the rest of the plotted
angles add, which for hpr is body lift. The sixth is the short model at Mach 2.96, where 77% of the
gap is hpr’s slope at α → 0, 1.2 times the measured one: there the method itself, not body lift,
carries most of the miss. Hpr’s curvature is 1.3 to 2.8 times the measured one below Mach 2.96,
1.1 to 1.3 times it at Mach 2.96, and 0.89 to 1.27 times it at Mach 3.96 and 4.63. The ×12.20 on
the short model at Mach 1.8 is not a measurement of anything: the tunnel’s own curve barely bends
there (0.094 per radian, against an uncertainty three times its size), so the ratio’s denominator
is consistent with zero.
The likeliest single cause is Jorgensen’s crossflow term, which ADR-037 chose because
the tunnel’s high-angle points support its size: at the few degrees these slopes are fitted over it
reads too large. Two other explanations are open and the readings do not close them: the short
model at Mach 1.5, the worst row at +37.7%, sits at M/f_n 0.36, below the 0.4 that TN 3527 states
its method for, and the same model at Mach 2.96 points at the method rather than at body lift.
What would close it. A cited rule for how the crossflow term grows from zero over the first few
degrees, or measurements of this body at finer angles than the reports plot. Neither is in hand,
so the gap is left visible here rather than tuned away. The rows are in
validation/fixtures/aero/arcas-robin-body-gap.json,
written by cargo xtask aero, and a test pins which rows are outside.
Checking the shock-expansion method
The second-order shock-expansion method of Bodies faster than sound,
which a flight uses from Mach 1.2 on the bodies it covers, against two references in the fixture
validation/fixtures/aero/shock-expansion.json.
cargo xtask aero writes it, and shock_expansion::tests::against_tn3527_and_the_arcas_robin
recomputes every value and pins every miss (ADR-033).
To check a share by hand from outside the crate,
ShockExpansionBody::element_flows
reports each element’s flow (the state behind its corner, the tangent cone it relaxes toward, how
fast it does so, and the radius eq. 19 needs), which is what the library’s own hand integral of a
boattail and the tube behind it uses (footnote_eights_boattail_share_by_hand).
The tip cone’s flow (cone_flow_agrees_with_naca_1135_charts), against [R1135]’s cone
charts 5 to 7 at Mach 1.5 to 3 and cones of 10° and 20°: the shock angle within 0.3°, the
surface pressure coefficient within 0.004 and the surface Mach number within 0.015, twice the
charts’ reading error, since they are drawn for γ = 1.405 and hpr uses 1.4. At Mach 2 on a 10°
cone hpr gives a shock at 31.21° against the chart’s 31.25°.
The report’s own tables ([SD56] Tables I and II, transcribed from the page images into
tn3527-bodies.json).
They cover 144 bodies: cones and tangent ogives of fineness 3, 5 and 7, on cylinders of 0 to 10
calibres, at Mach 3, 4.24, 5.05 and 6.28. For each they give the method’s slope and CP, as its
authors computed them by hand in 1956, and, for all but the 8-calibre cylinders, NASA’s
wind-tunnel measurements. The targets were set before measuring: within 0.05 per radian and
0.1 calibres of the report’s values, and within the ±0.2 per radian and ±0.2 calibres the report
claims against its measurements. Each cell counts the rows within, with the range of hpr’s value
less the reference’s:
| nose | slope within 0.05 per radian of the report’s | CP within 0.1 calibre of the report’s | slope within 0.2 per radian of the measured | CP within 0.2 calibre of the measured |
|---|---|---|---|---|
| cone, fineness 3 | 24 of 24 (−0.006 to +0.024) | 24 of 24 (−0.003 to +0.040) | 20 of 20 (−0.128 to +0.145) | 19 of 20 (−0.194 to +0.250) |
| cone, fineness 5 | 23 of 24 (−0.003 to +0.066) | 24 of 24 (−0.003 to +0.072) | 20 of 20 (−0.075 to +0.176) | 19 of 20 (−0.165 to +0.272) |
| cone, fineness 7 | 11 of 24 (−0.001 to +0.146) | 16 of 24 (−0.003 to +0.257) | 18 of 20 (−0.061 to +0.251) | 16 of 20 (−0.153 to +0.328) |
| tangent ogive, fineness 3 | 12 of 24 (−0.115 to +0.014) | 19 of 24 (−0.670 to +0.062) | 19 of 20 (−0.278 to +0.008) | 17 of 20 (−0.540 to +0.104) |
| tangent ogive, fineness 5 | 15 of 24 (−0.134 to +0.009) | 20 of 24 (−0.181 to +0.090) | 20 of 20 (−0.111 to +0.138) | 19 of 20 (−0.143 to +0.217) |
| tangent ogive, fineness 7 | 17 of 24 (−0.072 to +0.013) | 22 of 24 (−0.107 to +0.128) | 20 of 20 (−0.107 to +0.142) | 19 of 20 (−0.206 to +0.147) |
The 12 cones with no cylinder only read Fig. 2 back, so against the report’s values they check the hand reading, not the method.
A worked example: a cone of fineness 5 on a cylinder 4 calibres long, at Mach 4.24. Slender-body theory gives 2 per radian at any length. The tip cone alone gives 1.868 (Fig. 2). With the cylinder hpr gives 2.922, the report 2.91, and the wind tunnel 2.84.
In all, against the measurements, 117 of 120 slopes and 109 of 120 centers of pressure are within the report’s ±0.2; against the report’s own values, 102 of 144 slopes and 125 of 144 centers of pressure are within 0.05 and 0.1. That is 75 of the 528 comparisons outside, so the targets are not met (ADR-033 records it):
- Against the report’s own values (61 misses), in two kinds.
- Where the march stays inside the method’s limit (49 misses), hpr follows the printed equations and the printed values depart from them. The largest are the fineness-7 cone on long cylinders (hpr high, up to +0.146 at Mach 6.28 over 10 calibres) and the ogives (hpr low, most at fineness 5 and Mach 3, down to −0.134). The evidence is a second implementation of the same equations, written from the paper during this work with its own cone solver. For the cone-cylinders it used the report’s closed form (its Appendix C), for the ogives its ten-element march. With hpr’s hand-read Fig. 2, it agrees with hpr within 0.0001 per radian on all 72 cone-cylinders and within 0.0006 per radian and 0.0003 calibres on the 60 ogive-cylinders that stay inside the limit. It is an uncommitted scratch script by the same author, so it can’t be rerun from the repository, and it can’t catch a misreading both share. Why the printed values differ is not known. The report took its cone pressures from charts (its Fig. 1), and a thin cone’s small pressure differences are sensitive to them; that is a guess, not a finding.
- Where the march reaches the method’s limit (12 misses), on the fineness-3 ogive at Mach 5.05 and 6.28, near the tip. There hpr’s CP at Mach 5.05 sits 0.19 to 0.67 calibres ahead of the report’s, and the report’s measurements agree with the report, so this is hpr’s gap, not the report’s. The report doesn’t say how it continued past its limit. Carrying the pressure gradient on through the reduced elements comes closer at Mach 5.05 but further at Mach 6.28, and it doesn’t settle as elements are added. This is open (issue #81).
- Against the measurements (14 misses), in four groups:
- The fineness-7 cone on long cylinders (6). The report is already 0.07 to 0.15 high there, and hpr, following the closed form, adds 0.06 to 0.19 more.
- Rows where the report is itself 0.20 to 0.22 off (3): the fineness-5 and fineness-3 cones’ CP at Mach 6.28 over 10 calibres, and the fineness-5 ogive’s at Mach 5.05 over 10.
- The fineness-3 ogive at Mach 5.05 (4): the limit above; the worst misses, −0.278 per radian and −0.540 calibres.
- The fineness-7 ogive’s CP at Mach 5.05 over 4 calibres (1): −0.206, just outside, where the report reads −0.13.
The Arcas Robin ([D4014], the wind tunnel of
Normal force through Mach 1). The method needed a pointed tip when
this comparison was made (the committed nose, behind the cap it now takes, is compared in
Blunt tips), so here the nose is the secant ogive (a circular arc meeting the body at an angle) through the tip
and base that best fits the report’s coordinate table, 4.17 calibres long, so the report’s
Mach-over-fineness range of 0.4 to 2 covers Mach 1.67 to 8.3: its arc radius is 1.744 times a
tangent ogive’s, it misses the table by 0.003 in
rms, and its tip half-angle is 10.76°. hpr’s committed design keeps its power-series nose, whose
tip is blunt. The measured slope is the fins-off reading fitted over the plotted angles, as
above. It includes the boattail, the lip behind it, and crossflow lift at those angles. The
method has neither the lip nor crossflow at α → 0, and takes the boattail only by the report’s
footnote 8, so the target is not applied here; it is applied to the body a flight flies, in
The body alone, against the 15% target. This table is the method’s own; the body a flight flies since
M1.8e6, with the boattail’s measured share, is compared
below.
| model | Mach | measured | nose and cylinder | error | with boattail (footnote 8) | error |
|---|---|---|---|---|---|---|
| short | 1.5 | 2.192 | 2.552 | +16.4% | 2.375 | +8.3% |
| short | 1.8 | 2.613 | 2.724 | +4.3% | 2.580 | −1.2% |
| short | 2.3 | 3.078 | 2.931 | −4.8% | 2.828 | −8.1% |
| short | 2.96 | 3.284 | 3.124 | −4.9% | 3.056 | −6.9% |
| short | 3.96 | 3.884 | 3.300 | −15.0% | 3.262 | −16.0% |
| short | 4.63 | 4.149 | 3.371 | −18.7% | 3.345 | −19.4% |
| long | 1.8 | 3.159 | 2.724 | −13.7% | 2.581 | −18.3% |
| long | 2.3 | 3.525 | 2.932 | −16.8% | 2.829 | −19.8% |
| long | 2.96 | 3.868 | 3.127 | −19.2% | 3.059 | −20.9% |
| long | 3.96 | 4.455 | 3.313 | −25.6% | 3.275 | −26.5% |
| long | 4.63 | 4.615 | 3.395 | −26.4% | 3.369 | −27.0% |
The method’s slope grows with Mach number, as the measurement does: 2.55 to 3.37 on the short model, where slender-body theory keeps its nose at 2. By the end of either cylinder the lift has died away, so the long model gets almost nothing more (3.313 against 3.300 at Mach 3.96), while its measurement is 0.57 higher. That difference goes with the longer body’s larger side area, the mark of crossflow lift. The boattail column here is the report’s footnote 8, the method’s own rule, which a flight used from M1.8e4; since M1.8e6 a flight takes Washington and Pettis’s measured share instead (the worked example under The body faster than sound in a flight).
The table above is the method alone, at α → 0. A flight also adds body lift, which grows as
sin² α; the table leaves it out, and the measured line includes it.
Most of that gap was the comparison, not missing lift
(M1.8e5 sized each cause of it on hpr’s body model before
M1.8e6, Galejs’s body lift and footnote 8’s boattail;
the research note has the tables). The measurement is a straight line through points
from about −5° to +4°, and crossflow lift, which grows as α |α|, steepens it. Fitted the same way
at the same angles, with the body lift a flight added, hpr’s body with its boattail read 14.9% to
73.2% high at every Mach number (fixture arcas-robin-gap.json, which
cargo xtask aero writes and aero_gap::tests::committed_fixture_is_current keeps current).
- Crossflow lift steepens that fitted line. In hpr’s body lift it adds 1.40 to 2.09 per radian;
in the tunnel’s own points (fitted with an
α |α|term, the curvature) it adds 0.09 to 1.74. From Mach 2.3 the tunnel’s curvature matches body lift with a factorK(Bodies of revolution) from 0.66 to 1.05, each ±0.18 to ±0.23 (one standard error), where hpr used 1.1. Jorgensen’s crossflow method ([J77] eq. 2.12, Fig. 4 and p. 15) gives about 0.9 for these bodies at small angles. - The fit’s slope at
α → 0and its curvature move together (their errors correlate at −0.95 to −0.96), so the readings can’t split hpr’s excess between body lift and its slope atα → 0. With body lift at Jorgensen’s size alone, hpr still reads 8.2% to 60.7% high. - The one sized cause that size is the boattail’s share: TN 3527’s footnote 8, which a flight used, gives −0.177 to −0.026, slender-body theory −1.324. The lip, which the method can’t take, adds +0.178 by slender-body theory; a blunt tip like the tunnel’s, scaled from a blunter one measured, loses 0.015 to 0.07 past Mach 3. Below Mach 3, where the tangent cones’ slopes are held at TN 3527’s Fig. 2 Mach 3 curve, Sims’s tables ([S64] Table 2, p. 20) move the nose and cylinder’s share by −0.031 to +0.056 at most. hpr’s reading of issue #81 (how it continues the method where TN 3527’s relaxation condition fails) changes nothing on this body.
From then on the Arcas Robin is judged like for like, at the tunnel’s angles (ADR-036).
Crossflow’s size and the boattail’s share, together. Since M1.8e6 body lift takes
Jorgensen’s crossflow (Body lift) and a boattail Washington and Pettis’s measured
share (The body faster than sound in a flight)
(ADR-037). Fitted like for like, as the tunnel’s line is, the same body reads +3.4% to
+41.0%. Each change takes about half of the old excess off; from Mach 3.96 both models are within
15%, and from Mach 1.5 to 2.96 hpr still reads 16% to 41% high. The columns are hpr’s body model
before M1.8e6, each change alone, and both; the last column is the current model’s slope at
α → 0, which leaves body lift out. Slopes per radian on the body’s cross-section; the measured
line’s standard error takes each reading’s accuracy as independent.
| model | Mach | measured, fitted | before | Jorgensen’s crossflow alone | measured boattail alone | both (current) | current at α → 0 |
|---|---|---|---|---|---|---|---|
| short | 1.5 | 2.19 ± 0.09 | 3.80 (+73.2%) | 3.53 (+61.2%) | 3.35 (+52.9%) | 3.09 (+41.0%) | 1.93 |
| short | 1.8 | 2.61 ± 0.09 | 3.98 (+52.2%) | 3.72 (+42.4%) | 3.57 (+36.6%) | 3.31 (+26.8%) | 2.17 |
| short | 2.3 | 3.08 ± 0.08 | 4.29 (+39.4%) | 4.03 (+30.8%) | 3.92 (+27.2%) | 3.65 (+18.6%) | 2.45 |
| short | 2.96 | 3.28 ± 0.08 | 4.54 (+38.1%) | 4.28 (+30.2%) | 4.20 (+27.8%) | 3.94 (+19.8%) | 2.72 |
| short | 3.96 | 3.88 ± 0.09 | 4.71 (+21.3%) | 4.47 (+15.0%) | 4.41 (+13.6%) | 4.17 (+7.3%) | 2.96 |
| short | 4.63 | 4.15 ± 0.09 | 4.79 (+15.5%) | 4.57 (+10.2%) | 4.51 (+8.7%) | 4.29 (+3.4%) | 3.06 |
| long | 1.8 | 3.16 ± 0.11 | 4.50 (+42.4%) | 4.20 (+33.0%) | 4.09 (+29.4%) | 3.79 (+20.0%) | 2.17 |
| long | 2.3 | 3.53 ± 0.11 | 4.80 (+36.1%) | 4.50 (+27.6%) | 4.42 (+25.5%) | 4.12 (+17.0%) | 2.45 |
| long | 2.96 | 3.87 ± 0.11 | 5.14 (+32.9%) | 4.84 (+25.1%) | 4.80 (+24.1%) | 4.50 (+16.3%) | 2.72 |
| long | 3.96 | 4.46 ± 0.11 | 5.23 (+17.3%) | 4.96 (+11.2%) | 4.92 (+10.5%) | 4.66 (+4.5%) | 2.97 |
| long | 4.63 | 4.62 ± 0.11 | 5.30 (+14.9%) | 5.06 (+9.7%) | 5.02 (+8.7%) | 4.78 (+3.5%) | 3.08 |
The rows are in
arcas-robin-crossflow.json,
which cargo xtask aero writes and aero_crossflow::tests::committed_fixture_is_current keeps
current; aero_crossflow::tests::the_guide_quotes_the_fixture checks this table against it cell
by cell. What is left at Mach 1.5 to 2.96 can’t be split between body lift and the slope at
α → 0 from these readings, as M1.8e5 found. On the short
model at Mach 1.5 and 1.8 the tunnel’s points from −5° to +4° barely curve (a factor of 0.32 ± 0.24
and 0.07 ± 0.25 on body lift, where hpr uses about 0.9), while hpr’s slope at α → 0 (1.93 and
2.17) lies within about one standard error of the tunnel’s (1.78 ± 0.32, and 2.52 ± 0.33, which
hpr is 1.1 below): there most of the excess is body lift, which at 6° is already about half of
hpr’s normal force, and the points at 6° need a factor of 0.47 and 0.58 on it. From Mach 2.3 the
curvature gives factors of 0.66 to 1.05, each within about one standard error (±0.18 to ±0.23) of
Jorgensen’s, five of the eight below it. The fixture also holds each of the 62 points above +4° and
the boattail’s share at α → 0 under each rule. One caution on the long model: its points from
−5° to +4° are M1.8a’s reading, which may carry a skew of the
page that puts its slope 3% to 5% high
(issue #97); its rows here depend on how that is
settled.
Where the body’s lift acts. A body model can match the slope with its lift in the wrong place,
so the body’s center of pressure is checked too, from the tunnel’s fins-off pitching moment (read
for this milestone into
arcas-robin-fins-off-moment.json,
±0.02 to ±0.025 in C_m). Both are taken the way the tunnel’s are: straight lines through the
pitching moment and the normal force at the plotted angles up to ±4.5°, calibres from the nose
tip; the measured ones carry about ±0.5 calibres from the readings. The measured boattail share,
which takes lift off at the tail, moves hpr’s center of pressure forward, toward the tunnel’s:
before M1.8e6 hpr put it 0.90 to 3.85 calibres aft of the tunnel’s on the short model and 0.59 to
1.94 on the long; now −0.19 to +1.59 and −0.88 to −0.18. Two rows still miss by more than the
readings’ half a calibre: the short model at Mach 1.5 and 1.8, where the slope misses most too.
| model | Mach | measured, calibres from the tip | before | current |
|---|---|---|---|---|
| short | 1.5 | 1.00 | 4.85 (+3.85) | 2.59 (+1.59) |
| short | 1.8 | 2.36 | 4.98 (+2.62) | 3.03 (+0.68) |
| short | 2.3 | 3.56 | 5.27 (+1.71) | 3.67 (+0.10) |
| short | 2.96 | 3.21 | 5.20 (+1.99) | 3.77 (+0.56) |
| short | 3.96 | 4.88 | 5.79 (+0.91) | 4.69 (−0.19) |
| short | 4.63 | 5.05 | 5.94 (+0.90) | 4.95 (−0.09) |
| long | 1.8 | 4.61 | 6.55 (+1.94) | 4.27 (−0.33) |
| long | 2.3 | 5.04 | 6.79 (+1.74) | 4.86 (−0.18) |
| long | 2.96 | 5.33 | 6.43 (+1.11) | 4.65 (−0.68) |
| long | 3.96 | 6.19 | 6.78 (+0.59) | 5.31 (−0.88) |
| long | 4.63 | 6.40 | 7.35 (+0.96) | 6.12 (−0.28) |
Drag verification
-
RocketPy’s drag curves at Mach 0.3 (
tests::rocketpy_drag_curves_at_mach_0_3, fixturevalidation/fixtures/aero/rocketpy-drag-curves.json, written bycargo xtask aerofromrefs/rocketpy). Every RocketPy example whose curve is labelled RASAero, at sea level in the 1976 standard atmosphere (USSA76; RASAero II computes its exports’ Reynolds numbers there); tolerance 10%. The fixture holds only derived numbers: each curve’s value at Mach 0.3, hpr’sC_D0and the error. The test recomputes hpr’sC_D0and the errors from the committed designs, andcargo test -p xtaskreruns the comparison whenrefs/rocketpyis present.case curve hpr C_D0error range over inputs Calisto, 2018 fins RASAero II export, power-off 0.3982 +4.4% −14.0% to +12.8% Calisto, getting-started fins (variant) the same 0.3537 −7.3% −12.9% to +19.3% Juno III labelled RASAero II, 3-decimal table 0.3525 −6.0% −10.5% to +24.1% Cavour, power-off labelled RASAero II, 3-decimal table 0.5034 −8.3% −22.3% to −0.4% Cavour, power-on (outside 10%) the same, power-on 0.4487 −18.3% −32.2% to −10.3% Valetudo, power-off (outside 10%) labelled RASAero, 3-decimal table 0.5566 −47.0% −59.4% to −42.5% Valetudo, power-on (outside 10%) the same, power-on 0.5189 −50.4% −62.8% to −45.9% - Inputs (ADR-009, the drag decision). The exports record none, so the designs
follow one declared rule:
- RASAero II’s default smooth finish.
- A NACA 00xx airfoil file in the example gives an airfoil section that thick at the mean aerodynamic chord (Calisto’s getting-started fins).
- A published section is used: Juno III’s team placed second for a technical award, cited for “análise de aletas com perfil de aerofólio truncado” (an analysis of truncated-airfoil fins); taken as rounded at the placeholder thickness, since the citation gives no thickness.
- Otherwise the placeholder, square 3 mm (Calisto’s 2018 fins, Cavour, Valetudo).
- Rail buttons are as RocketPy defines them (without them, Calisto is +1.8%).
- Sensitivity. The range is over square, rounded and airfoil fins (3 mm, or 12% for the airfoil), 0 or 20 µm, and with or without rail buttons. Before the published-section rule, square fins gave Juno III +14.6% and the getting-started Calisto +10.7%. The check places hpr near RASAero’s subsonic drag under a declared rule; without the inputs it can’t show agreement to 10%.
- Power-on. Separate power-on curves are compared (Cavour’s and Valetudo’s). Subtracting the motor’s area ([N09] pp. 50–51) removes 42% of Cavour’s base drag and 29% of Valetudo’s at Mach 0.3. Cavour’s power-on table is within its 0.001 rounding of power-off from Mach 0.16 up (0.0001 at 0.3) and 0.001 to 0.013 lower below; Valetudo’s is 0.004 lower, about a ninth of hpr’s relief. The designs’ motor diameter is the larger of the grain and nozzle exit diameters, since RocketPy gives no case, and Cavour’s result depends on it: −8.3% with no relief, −14.8% at 54 mm, −18.3% at the design’s 67 mm nozzle exit, −20.8% with the example’s 75 mm motor. The cause of that miss stays open: Niskanen’s rule of taking the motor’s area off the base, a RASAero run with little or no nozzle exit diameter, or tables sampled along a flight (their uneven Mach spacing suggests it; unconfirmed).
- Valetudo. Its table (1.05) is 1.44 times the OpenRocket export for the same rocket (0.728). With that file’s own inputs (60 µm, two 14 mm × 30 mm lugs, 3 mm square fins), hpr gives 0.714, 1.9% under the OpenRocket export and 32% under the table. As designed for this comparison, its 0.5566 is 23.5% under the export.
- Not compared.
- Calisto’s power-on curve, which equals its power-off curve (no nozzle exit diameter in RASAero).
- Juno III’s power-on drag, which RocketPy takes from the same file.
- Calisto’s power-on result would be −5.0% (its power-on file is its power-off file).
- The other examples, whose drag is a constant, CFD or of unknown origin.
- Inputs (ADR-009, the drag decision). The exports record none, so the designs
follow one declared rule:
-
Loft lessons, each with what it concerns:
- L11, drag that changed with the order of the fin sets:
drag::tests::drag_invariant_to_fin_set_order - L12, uncited form-factor, friction and roughness constants:
drag::tests::form_factor_and_roughness_match_cited_values - L13, no relief of base drag while a motor burns:
drag::tests::power_on_base_drag_subtracts_thrusting_motor_area - L14, uncited launch-lug drag:
drag::tests::launch_lug_drag_matches_cited_hollow_tube_formula - L15, shoulder drag that jumped as its length went to zero:
drag::tests::shoulder_drag_continuous_as_transition_length_tends_to_zero - L16, a silent cap on the drag coefficient that hid bad geometry:
drag::tests::malformed_geometry_is_an_error_not_a_clamped_cd - L90, drag invariants, a check kept from Loft’s tests (skin friction’s published
jumps, split fin sets, base drag at Mach 1):
drag::tests::skin_friction_follows_eq_3_81_and_drag_invariants_hold
- L11, drag that changed with the order of the fin sets:
-
Limits of every term (
drag::tests): friction below1e4, atR_critand to Mach 5; stagnation pressure against the isentropic series and its limits either side of Mach 1; base drag at rest, at Mach 1 and far above; the joint term from smooth to a step; the boattail factor’s three pieces and their joins; fin pressure drag by cross-section with the leading edge’s joins at Mach 0.9 and 1 and the sweep; the angle-of-attack factor’s stated values and zero slopes, monotonicity and its sign-reversed mirror (andC_A < 0at 135°); joint angles of cones and power-series, Haack and ogive noses; curved boattails ending in blunt tips; a tail closing to a point; leading-edge sweeps of trapezoids, kinked outlines and ellipses (against a quadrature to 1e-8); a whole rocket’s buildup written out by hand to 1e-12, its component sum, and override tables (rescaled to another reference diameter). -
Drag through Mach 1 (
nose_drag::tests,drag::tests): Loft lesson L17, Loft’s fin leading-edge drag frozen at its Mach 1 value and nose drag with no Mach term, isdrag::tests::leading_edge_and_cone_pressure_drag_have_supersonic_branches. A 3:1 cone by hand from rest through eq. B.4, with its joins smooth to 1e-5; the ogive factor at its ends and middle; eq. B.9 through both its anchors; eq. 3.87 meeting its lower bound and its fallback; a step from 0.8 to the flat face; short cones tending to the step and meeting the closed form at fineness 1 (Loft lesson L15); Stoney’s curves reproduced at fineness 3, held past their ends and interpolated between shapes; the guide’s worked example; the 3:1 cone against Stoney’s measured one; refused shapes; and a property test that every shape at any fineness, joint angle and Mach number to 5 gives a finite, non-negative coefficient with no jump atM_L. -
Boattails faster than sound (
afterbody::tests): the Prandtl–Meyer function against NACA Report 1135’s table and its limit of 130.45°, and its inverse; an expansion’s pressure by hand and at the vacuum limit; the chart giving back its readings, falling withxand the area ratio, and reaching 0 ata = 1; Calisto’s boattail by hand (the guide’s worked example) with the joins at Mach 0.9, 1 and 1.2; the drag past the chart continuous at its end and closing on the 2D limit; separation from 16° to 30°; the base-pressure ratio on Fig. 5-141’s line; and every boattail from 1° to 89° finite and non-negative to Mach 5. The comparison with measured boattails and Jack’s theory istests::boattails_against_measurements. -
Boattails in parts, wakes and gaps (
drag::tests): a boattail split in two drags as one (a_boattail_split_in_two_drags_as_one), and so does a straight cone of 0.5° to 7° in 2, 4 or 8 parts, where a part’s share is below 0 too (a_straight_cone_in_parts_is_one_cone); a corner keeps two parts apart and the merge is continuous in the turn (a_sharp_corner_keeps_its_boattails_apart); a pair drags between its two limits (soft_merges_stay_between_their_limits); a partial merge shares the flow (a_partial_merge_shares_the_flow); a change ofεin any radius or length behind 2° to 14° boattails moves the drag in proportion toε(a_part_narrowing_by_nothing_is_a_tube_and_one_of_no_length_a_step); a lip’s wake by its rise and its gaps (a_lip_in_a_boattails_wake_fades_with_its_rise,a_lip_drawn_as_a_step_up_is_a_lip,a_hairline_step_before_a_lip_changes_nothing); a retainer behind a step down (a_retainer_behind_a_step_down_is_in_its_wake); and a 40-part zigzag keeps its tails few (a_zigzag_boattail_keeps_its_tails_few). -
Tables (
table::tests): RocketPy’s quirks (\r\n,01.05, a repeated row), a byte-order mark, quoted fields and trailing commas, RASAero II’s header with rows at 2° and 4° skipped, and malformed text (a bad first row, repeated or unsorted Mach numbers,nan) by line.
Drag against the Arcas Robin wind tunnel
NASA measured the axial force on its half-scale Arcas Robin models, the same ones as the normal
force above, from Mach 0.6 to 4.63 ([D4013], [D4014]), fins at 0° and with the fins off. The
models sat on a sting, so their base pressure isn’t a free flight’s, and both reports take the base
apart: [D4013] plots the axial force “corrected for base axial force” (C_A,corr, Figs. 11–12),
and [D4014] the axial force and, separately, the force on the balance chamber inside the base
(C_A,c, Figs. 4–6). What compares, then, is the forebody: hpr’s C_D0 less its base drag
(friction, pressure and parasitic drag) against the measured axial force with the base at the free
stream’s pressure. For [D4014] that is C_A − 1.383 C_A,c, which takes the chamber’s pressure over
the whole base as [D4013]’s correction does: 1.383 is (1.470/1.250)², the base’s diameter in inches
over the 1.250-inch cavity drawn in [D4014] Fig. 1(a), squared. The report states no chamber
area, and taking it over the chamber alone moves the measured values by 0.002 to 0.015, which
changes no row’s verdict. The readings are in
arcas-robin-wind-tunnel.json, read off the reports’ plots with their figure,
page and reading uncertainty (±0.002 in C_A for most, against the reports’ own ±0.004).
hpr flies the committed designs (Normal force through Mach 1) at
both tunnels’ Reynolds number, 3.0 million per foot, with two inputs set for drag before measuring:
the double-wedge fins take hpr’s airfoil section, as Niskanen modeled them ([N09] p. 90), and the
machined steel models a polished finish, 0.5 µm, since the reports state none. The target, set
before measuring, was M1.8’s 10% for drag. cargo xtask aero writes
drag-vs-mach.json, each row with hpr’s drag by part, and
tests::drag_against_mach recomputes it from the designs and pins the 2 rows of 44 within target.
The two input choices matter, and moved hpr toward the tunnel: with square edges and the default
20 µm finish no row is within target, with the airfoil section alone none, with the polished
finish alone none, and with both 2 (tests::drag_against_mach_depends_on_the_fins_and_finish).
The airfoil section follows the drawings and Niskanen; the finish is a guess. Allowing each
reading its uncertainty and the reports’ ±0.004, neither of the 2 could fall the other side of
10%. Before
M1.8b3 modeled the boattail faster than sound and the lip
in its wake, 8 rows were within target, 6 of them because the lip’s 0.085 made up for the missing
wave drag. Forebody drag on the reference area, measured and hpr’s, and hpr’s error:
| Mach | fins | short: measured | hpr | error | long: measured | hpr | error |
|---|---|---|---|---|---|---|---|
| 0.6 | on | 0.2987 | 0.3371 | +12.9% | 0.3455 | 0.3874 | +12.1% |
| 0.6 | off | 0.2217 | 0.2523 | +13.8% | 0.2477 | 0.3041 | +22.8% |
| 0.8 | on | 0.3299 | 0.4200 | +27.3% | 0.3706 | 0.4688 | +26.5% |
| 0.8 | off | 0.2308 | 0.2585 | +12.0% | 0.2517 | 0.3088 | +22.7% |
| 0.9 | on | 0.4202 | 0.6346 | +51.0% | 0.4510 | 0.6826 | +51.3% |
| 0.9 | off | 0.2610 | 0.3623 | +38.8% | 0.2671 | 0.4116 | +54.1% |
| 0.95 | on | 0.5680 | 0.6722 | +18.3% | n/a | n/a | n/a |
| 0.95 | off | 0.2916 | 0.4214 | +44.5% | n/a | n/a | n/a |
| 1.0 | on | 0.6858 | 0.7344 | +7.1% | 0.7247 | 0.7825 | +8.0% |
| 1.0 | off | 0.4194 | 0.5044 | +20.3% | 0.3674 | 0.5540 | +50.8% |
| 1.2 | on | 0.5935 | 0.7706 | +29.8% | 0.6245 | 0.8172 | +30.9% |
| 1.2 | off | 0.4311 | 0.5187 | +20.3% | 0.4220 | 0.5667 | +34.3% |
| 1.5 | on | 0.4932 | 0.6873 | +39.4% | n/a | n/a | n/a |
| 1.5 | off | 0.3401 | 0.4148 | +22.0% | n/a | n/a | n/a |
| 1.8 | on | 0.4260 | 0.6412 | +50.5% | 0.4543 | 0.6827 | +50.3% |
| 1.8 | off | 0.3142 | 0.3570 | +13.6% | 0.3267 | 0.3997 | +22.3% |
| 2.3 | on | 0.3328 | 0.5879 | +76.7% | 0.3730 | 0.6251 | +67.6% |
| 2.3 | off | 0.2474 | 0.2940 | +18.8% | 0.2927 | 0.3323 | +13.5% |
| 2.96 | on | 0.2639 | 0.5409 | +105.0% | 0.3010 | 0.5729 | +90.3% |
| 2.96 | off | 0.2030 | 0.2423 | +19.4% | 0.2362 | 0.2753 | +16.6% |
| 3.96 | on | 0.2056 | 0.4928 | +139.7% | 0.2342 | 0.5186 | +121.4% |
| 3.96 | off | 0.1554 | 0.1929 | +24.1% | 0.1894 | 0.2195 | +15.9% |
| 4.63 | on | 0.1850 | 0.4700 | +154.0% | 0.2113 | 0.4926 | +133.1% |
| 4.63 | off | 0.1390 | 0.1704 | +22.6% | 0.1648 | 0.1937 | +17.5% |
Why it misses, from the drag by part in the fixture:
- The fins past Mach 1.2. hpr’s fins add about 0.30 from Mach 1.5 up, where the measured fins-on less fins-off falls from 0.153 at Mach 1.5 to 0.046 at 4.63: +78% at Mach 1.5 and +551% at 4.63 on the short model. The leading edge takes [N09]’s rounded-edge formula, whose value grows toward 1.2 on the fins’ frontal area, where a thin, sharp fin’s wave drag falls with Mach. Niskanen’s own comparison with this wind tunnel shows the same, his simulation about 80% high by Mach 3.96 ([N09] Fig. 6.6, p. 90). At Mach 0.6 hpr’s fins are +10% and −15% of the measured increment; from 0.8 to 0.9 they rise sooner than the measured fins do.
- The boattail faster than sound. hpr’s 15° boattail drags 0.285 from Mach 1.0 to 1.2, 0.196 at 1.5 and 0.037 at 4.63 (Boattails faster than sound). If the rest of hpr’s forebody were right, the tunnel’s boattail would drag about 0.12 at Mach 1.5 and 0.08 to 0.11 at 1.8, 40% to 94% under hpr, and next to nothing from Mach 3.96. Cubbage’s 16° boattails, in a boundary layer a fifth of the diameter thick, read high the same way, 26% to 54%; NASA reports the flow separating over this boattail at the higher Mach numbers ([D4014] p. 6). With the fins off hpr reads +13.5% to +24.1% from Mach 1.5 (issue #72).
- The lip. The models end in a lip 1.3 mm long that flares from the boattail’s 33.2 mm to the base’s 37.3 mm. It sits in the boattail’s wake, and hpr gives it no pressure drag. Before M1.8b3 hpr took it as a shoulder in the free stream, a stubby cone worth 0.065 at Mach 0.6 and 0.084 to 0.086 from Mach 1.2, which made up for the missing wave drag and put six rows within 10%. The short model also keeps the fins’ raised root fairings with its fins off, which hpr leaves out and TN D-4013 blames for its higher drag from Mach 0.975 to 1.2 (pp. 4–5).
- The boattail below Mach 1. [N09]’s boattail rule (eq. 3.88) gives the 15° boattail a pressure drag of 0.063 at Mach 0.6 (its 0.070 less its friction). The tunnel’s forebody holds that same pressure on the boattail’s surface, and on the short model the whole forebody with its fins off measures 0.22 there, against hpr’s friction alone of 0.19: little is left for the boattail’s pressure. The rule over-predicts this boattail, as Niskanen found against the same tunnel ([N09] p. 90). At Mach 0.6 and 0.8 hpr’s forebody with its fins off is +12.0% to +22.8% high, most of it the boattail rule.
- Through Mach 1, where drag rises steeply, the measured forebody with fins off jumps from 0.29 to 0.42 between Mach 0.95 and 1.0 on the short model. hpr’s boattail rises from 0.076 at Mach 0.8 to 0.285 at 1.0, sooner than the tunnel’s, and the forebody with its fins off reads +38.8% to +54.1% from Mach 0.9 to 0.95 and +20.3% to +50.8% from Mach 1.0 to 1.2.
What this shows: hpr’s drag reads high for this rocket at every Mach number, from about Mach 1.2 most of all by its thin, sharp fins, which hpr takes as blunt, and at every speed by its steep boattail. The body alone reads +12.0% to +54.1%; before hpr modeled the boattail faster than sound, with the lip in it and no wave drag, it read −9.2% to +71.1%. hpr’s base drag, which the tunnel can’t measure, is compared with a calculation (Drag against MIL-HDBK-762’s sample calculation) and, behind boattails, with measured bases (Boattails faster than sound).
Drag against RASAero II through Mach 2
M1.8 asks for drag within 10% of the curves labelled RASAero in RocketPy’s example rockets from Mach 0.1 to 2.0, with the errors by band. hpr doesn’t meet that. This section gives the errors, what the boattail’s wave drag changed, and how far the curves’ unrecorded inputs reach (ADR-029, the decision on this comparison, and ADR-030).
How it is compared. cargo xtask aero compares hpr’s zero-lift
drag coefficient, C_D0, with each curve every 0.05 from
Mach 0.1 to 2.0, wherever the curve reaches. Each point is at sea level in the 1976 standard
atmosphere, at the Reynolds number for its Mach number, as
RASAero II computes its exports. The designs and their inputs are those of the Mach 0.3 check
above. Bands are Niskanen’s (Table 3.1): subsonic to Mach 0.8, transonic below 1.2, supersonic from
1.2. The fixture, rocketpy-drag-curves.json, holds hpr’s value and the error
at every Mach number and each band’s summary. It doesn’t hold the curves, but the two numbers
give a curve’s value back at each Mach number.
drag::tests::supersonic_cd_against_rasaero_tables recomputes every row and pins the counts
(Loft lesson L18: Loft, the earlier simulator this project
learns from, used an invented transonic drag curve).
The curves don’t all reach Mach 2. Calisto’s, the one real RASAero II export, does. Juno III’s is hand-edited past Mach 0.92: it climbs a constant step per row to Mach 1.0 and then drops to 0.001, so the comparison stops at 0.92. Cavour’s stop below Mach 0.93, and Valetudo’s at 1.53.
Rows within 10%, and the range of the errors, by band:
| case | to Mach | subsonic, to 0.8 | transonic | supersonic, from 1.2 |
|---|---|---|---|---|
| Calisto, 2018 fins | 2 | 15 of 15: +3.9% to +8.9% | 3 of 7: −10.1% to +16.4% | 8 of 17: −14.9% to −5.1% |
| Calisto, getting-started fins (variant) | 2 | 12 of 15: −8.2% to +30.7% | 0 of 7: +13.5% to +65.4% | 0 of 17: +22.6% to +31.7% |
| Juno III | 0.9 | 15 of 15: −6.2% to +9.1% | 0 of 2: +19.3% to +32.0% | n/a |
| Cavour, power-off | 0.85 | 6 of 15: −12.6% to −2.2% | 0 of 1: −12.6% | n/a |
| Cavour, power-on | 0.9 | 1 of 15: −26.2% to −9.2% | 0 of 2: −27.0% to −26.7% | n/a |
| Valetudo, power-off | 1.5 | 0 of 15: −51.4% to −43.5% | 0 of 7: −53.3% to −50.9% | 0 of 7: −54.6% to −52.9% |
| Valetudo, power-on | 1.5 | 0 of 15: −55.6% to −46.6% | 0 of 7: −57.6% to −54.7% | 0 of 7: −57.8% to −56.2% |
The getting-started fins are a variant: RocketPy’s getting-started example gives Calisto larger fins with a thick NACA 0012 airfoil, but the export was made for the 2018 fins, whose normal force it matches. So the variant’s rows show how much the fins move drag, not a second agreement: its thick fins now read 23% to 32% high faster than sound. Calisto on its 2018 fins is within 10% up to Mach 0.8, at 0.95 and 1.0 and from 1.2 to 1.55; it reads +13.0% and +16.4% at Mach 0.85 and 0.9, where its boattail’s rise starts sooner than RASAero II’s, −10.1% at Mach 1.05, and falls below the curve from Mach 1.6, to −14.9% at 2.0. Valetudo’s curve is 1.44 times its own OpenRocket export, as the Mach 0.3 check found, and Cavour’s power-on miss is the same open question. Two misses are unexplained: Cavour’s power-off curve rises faster than hpr’s through subsonic flow, from −8.3% at Mach 0.3 to −12.6% at 0.85; and Juno III’s stays flat up to Mach 0.91, where hpr’s has begun its rise, its nose’s and its boattail’s, +19.3% at Mach 0.85 and +32.0% at 0.9.
What the boattail’s wave drag changed. Calisto ends in a short, steep conical boattail: 0.47 calibres long, narrowing to 69% of the diameter, a slope of 18.4°. Until M1.8b3 hpr gave it only a share of the base drag, 0.083 at Mach 1.2 and 0.050 at 2.0, and read −29.8% to −24.4% from Mach 1.2. Its supersonic wave drag (Boattails faster than sound) is 0.283 at Mach 1.2, 0.190 at 1.5 and 0.120 at 2.0, and its base drag falls by a third; together they close the gap from 0.204 to 0.035 at Mach 1.2 and from 0.128 to 0.077 at 2.0. RASAero II lists such drag as its own term, “other body wave” drag. Calisto’s 18.4° boattail is steeper than any attached boattail measured here, and 16° boattails read 26% to 54% high, so this agreement is not support for the model at that angle. What is left grows with Mach number, and part of it is hpr’s body, which reads 6% to 10% low faster than sound against a worked example with every input known (below).
The unrecorded inputs now span most of the rest. Calisto’s fins could be square, rounded or an
airfoil, 2 to 6.35 mm thick, smooth or painted (tests::calistos_rows_by_fin_and_finish). The
committed inputs, square, 3 mm and smooth by the rule of the Mach 0.3 check, have 15, 3 and 8
rows within 10% by band. Rounded fins 4.76 mm thick, smooth, have 15, 4 and 14; airfoil fins
6.35 mm thick, smooth, have 11, 4 and 17. No combination has every row within 10%. Before the
wave drag no combination had rows within 10% both below Mach 0.8 and from Mach 1.2. So most of
what is left is within what the unrecorded inputs span; hpr keeps the stated rule rather than
picking the inputs that fit.
Drag against MIL-HDBK-762’s sample calculation
RASAero II’s curves can’t show which way hpr leans, because their inputs are guessed. A reference with every input known can. MIL-HDBK-762, the U.S. Army’s handbook for designing unguided rockets, works one rocket’s drag through by its own methods, term by term, from Mach 0.5 to 3.2 ([762] Table 5-4, pp. 5-58 to 5-66). The rocket is 3.84 m long and 0.16 m across. It has a 3-calibre tangent ogive nose, a plain cylinder with no boattail, and four fins 0.32 m long, 51 mm tall and 6.4 mm thick, flush with the base (Fig. 5-155).
This is a calculation, not a measurement: it checks hpr’s methods against another set of
methods, whose base drag comes from measured bases. The table is transcribed with its pages in
mil-hdbk-762-sample-drag.json, and every row sums to its printed total.
The rocket is validation/designs/mil-hdbk-762-sample-rocket.json, with a smooth finish, as the
handbook’s friction is. hpr flies it at the table’s Reynolds numbers.
tests::drag_against_mil_hdbk_762_sample recomputes the comparison in
drag-vs-mach.json and pins the rows within 10%. The 10% target comes from
M1.8. hpr’s numbers for this rocket were seen before it was
chosen as a reference, so this is not a blind test.
The fins are left out. The handbook draws each fin as a single wedge, sharp at the leading edge and blunt at the trailing edge, and gives the fins a thin wedge’s wave drag and the base drag of their trailing edges. hpr has no such section. The design gives them square edges, whose leading edges hpr charges the pressure of air brought to a stop against them (0.100 at Mach 2, against the handbook’s 0.016 for the whole fin). So each side’s fin pressure drag is shown but left out of the totals compared; the fins’ friction stays in.
Each term, the handbook’s first and then hpr’s, on the reference area:
| Mach | handbook | hpr | error | nose (handbook, hpr) | base | friction | fins, left out (handbook, hpr) |
|---|---|---|---|---|---|---|---|
| 0.5 | 0.423 | 0.379 | −10.3% | 0.000, 0.000 | 0.170, 0.152 | 0.253, 0.227 | 0.023, 0.069 |
| 0.7 | 0.405 | 0.401 | −1.1% | 0.000, 0.006 | 0.163, 0.184 | 0.242, 0.211 | 0.023, 0.075 |
| 0.9 | 0.393 | 0.484 | +23.1% | 0.007, 0.062 | 0.156, 0.225 | 0.230, 0.197 | 0.023, 0.082 |
| 0.95 | 0.404 | 0.533 | +31.9% | 0.011, 0.102 | 0.163, 0.237 | 0.230, 0.194 | 0.026, 0.085 |
| 1.0 | 0.465 | 0.609 | +31.0% | 0.052, 0.164 | 0.183, 0.250 | 0.230, 0.195 | 0.043, 0.087 |
| 1.1 | 0.542 | 0.651 | +20.1% | 0.109, 0.234 | 0.215, 0.227 | 0.218, 0.189 | 0.043, 0.089 |
| 1.2 | 0.528 | 0.593 | +12.3% | 0.117, 0.200 | 0.194, 0.208 | 0.217, 0.184 | 0.036, 0.091 |
| 1.6 | 0.472 | 0.444 | −6.0% | 0.109, 0.123 | 0.168, 0.156 | 0.195, 0.165 | 0.022, 0.097 |
| 2.0 | 0.415 | 0.377 | −9.2% | 0.095, 0.104 | 0.147, 0.125 | 0.173, 0.147 | 0.016, 0.100 |
| 2.4 | 0.363 | 0.331 | −8.9% | 0.089, 0.094 | 0.124, 0.104 | 0.150, 0.132 | 0.013, 0.102 |
| 2.8 | 0.328 | 0.296 | −9.6% | 0.085, 0.088 | 0.106, 0.089 | 0.137, 0.119 | 0.011, 0.103 |
| 3.2 | 0.298 | 0.269 | −9.6% | 0.083, 0.084 | 0.089, 0.078 | 0.126, 0.107 | 0.009, 0.103 |
Six of twelve rows are within 10%. hpr reads +12.3% to +31.9% high from Mach 0.9 to 1.2, and −6.0% to −9.6% low from Mach 1.6:
- The nose through Mach 1. Niskanen’s ogive gives two to three times the handbook’s: 0.164 against 0.052 at Mach 1.0, and 0.234 against 0.109 at 1.1. Stoney’s measured 3:1 cone also sits under Niskanen’s closed form through the rise (Drag through Mach 1). From Mach 2 the two agree within 10%.
- The base. Niskanen’s base drag (eq. 3.94, after Fleeman’s missile design textbook) gives 0.250 at Mach 1.0 where the handbook reads 0.183 from measured bases. Faster than sound hpr’s is the lower: 0.125 against 0.147 at Mach 2.
- Friction reads 10.4% to 15.9% lower in hpr. The handbook takes a smooth flat plate’s friction and adds 15% on the body; hpr’s body factor ([N09] eq. 3.85) adds 2% for this slender body, which accounts for about 11 points. The rest is unexplained; the two methods correct friction for Mach number differently.
So hpr’s body reads high through Mach 1, from the nose and the base, and 6% to 10% low faster than sound, from friction and the base. That is the same sign as Calisto’s gap to RASAero II, a third of its size. This rocket has no boattail, so it says nothing about a boattail’s own drag.
Roll against the Arcas Robin and the Basic Finner
What is checked: hpr’s roll forcing against NASA’s measured roll effectiveness of the two Arcas
Robin models from Mach 1.5 to 4.63 (TN D-4014 Fig. 14, [D4014]), and its roll damping against the
Basic Finner’s measured from Mach 1.5 to 3.0 and Barrowman’s own computed value at Mach 0.07
([B67] Figs. 5-6 and 5-7). The readings are in the wind-tunnel fixture and
the Basic Finner’s; cargo xtask aero writes the comparison to
the roll fixture, and hpr_aero::tests::roll_against_mach recomputes every row.
The roadmap set no target.
The forcing. C_lδ per degree of cant, on the body’s cross-section and diameter, at an angle
of attack of 0 (the reports’ symbol is per degree; the models’ fins were canted 2°):
| Mach | model | measured C_lδ (the report’s) | hpr’s N C_lδ k_T(B) | error |
|---|---|---|---|---|
| 1.5 | short | 0.1684 | 0.2489 | +47.8% |
| 1.8 | short | 0.1722 | 0.1968 | +14.3% |
| 1.8 | long | 0.1670 | 0.1968 | +17.8% |
| 2.3 | short | 0.1449 | 0.1495 | +3.2% |
| 2.3 | long | 0.1460 | 0.1495 | +2.4% |
| 2.96 | short | 0.1152 | 0.1151 | −0.1% |
| 2.96 | long | 0.1140 | 0.1151 | +1.0% |
| 3.96 | short | 0.0874 | 0.0861 | −1.4% |
| 3.96 | long | 0.0870 | 0.0861 | −1.0% |
| 4.63 | short | 0.0721 | 0.0739 | +2.5% |
| 4.63 | long | 0.0780 | 0.0739 | −5.3% |
The two models differ only in the body’s length ahead of the fins, which hpr’s forcing doesn’t
see; the measured values differ by up to 0.006 per degree (8%, at Mach 4.63), and the report
calls the effectiveness “about the same for either vehicle” ([D4014] p. 6). The short model’s
readings were corrected when roll was added: the first reading had put each of its panels’ zeros 0.005 to
0.007 above the grid line it lies on (ADR-031). From Mach 2.3 all 8 are within 5.3%.
At Mach 1.5 and 1.8 hpr reads high, as
Barrowman found for another sounding rocket: linear theory’s load climbs toward Mach 1 faster than
the fins’ does. Without the body factor k_T(B) (0.935 here) every value would be 7% higher.
The damping. C_lp of the Basic Finner, four square fins one diameter in chord and span on a
body one diameter across, per unit of p d/(2V):
| Mach | reference | reference’s C_lp | hpr’s N C_lp k_R(B) | error |
|---|---|---|---|---|
| 0.07 | Barrowman’s computed curve (chose the method; not a validation) | −34.21 | −33.53 | −2.0% |
| 1.51 | wind tunnel | −33.60 | −31.62 | −5.9% |
| 1.82 | wind tunnel | −27.45 | −25.31 | −7.8% |
| 2.27 | wind tunnel | −23.48 | −20.12 | −14.3% |
| 2.60 | wind tunnel | −20.92 | −17.61 | −15.8% |
| 3.00 | wind tunnel | −18.32 | −15.36 | −16.2% |
hpr reads low faster than sound, more so as the Mach number grows. Barrowman’s own curve, from Busemann’s third-order expansion ([B67] eq. 3-7, a higher-order theory that counts the fins’ thickness), is 5.68% from the same points on average ([B67] p. 66); first-order theory, hpr’s, reads low partly for want of a term for the fins’ 8% thickness. At Mach 0.07 hpr gives his computed value within 2.0%, which is how hpr’s reading of his damping method, the fin’s own slope over the strips, was checked (above).
The flight (hpr_sim::tests::canted_fins_spin_to_the_analytic_balance): Valetudo with 1° of
cant at 100 m/s, with no drag and no gravity, settles on the closed-form steady roll rate of the
worked example, −16.948 rad/s, within 1e-6 (the test’s bound; 1e-11 measured), and one time
constant in is within 1e-5 of the exponential approach (2e-10 measured); no pitch or yaw
appears.
The pieces (hpr_aero::fins::tests): the polygon’s span moments against the trapezoid’s and the
ellipse’s integrals ([N09] eq. 3.70–3.71); the supersonic forcing and damping against a
20,000-strip sum of the same load on the Arcas Robin’s swept fin, within 1e-7, from Mach 1.5 to
4.63; the subsonic ones against Barrowman’s closed forms, and both continuous at Mach 0.8 and
M_s; k_R(B) against its integral (eq. 3-121) by Simpson’s rule within 1e-10.
Rigid-body flight
In short
- What it models: a rocket’s flight from the pad to the ground, rail included: a rigid body free to move and turn in every direction (six degrees of freedom) that gets lighter as its motor burns.
- Sources: the equations of motion in RocketPy’s technical documentation (RocketPy 1.13.0), and the RocketPy paper (Ceotto et al., 2021), which is cited but was not fetched.
- How well it is validated: by analytic and unit tests (a tumbling rocket’s center of mass stays on the exact parabola in a vacuum to 1.7e-6 m over 22 s), and against RocketPy. Five of its example rockets, flown from the pad to the ground by both codes with the same declared drag, agree on height, speed, time and acceleration within 3%; the largest scored difference is +1.783%, a peak acceleration on the rail (validation report, whole flights against RocketPy). So does the path, except for rockets that leave the rail slowly in a wind. There hpr’s body lift, which RocketPy’s normal force leaves out, its later release from the rail and, for Juno III, its simpler fin model put the apogee drift −4.333% to −38.158% from RocketPy’s (validation report; the causes in ADR-026). A sixth, Prometheus 2022, passes Mach 1 on its drag table and agrees as well, its drifts again apart from RocketPy’s by body lift. With hpr’s own drag, against RocketPy flying the drag its examples ship, hpr’s heights differ from RocketPy’s by −7.280% to +10.302% (validation report), the larger gaps where the two drags differ most: hpr’s is well below the example’s for two rockets, and above it at high speed for Prometheus 2022, which flies through Mach 1 on hpr’s drag since M1.8b1 (drag through Mach 1) (Accuracy). Seven flights have been compared with their teams’ altitude logs, apogee and climb only (Accuracy: real flights).
- What it leaves out: staging and delayed ignition, tip-off (the pivot as the rocket leaves the rail), turbulence and thrust misalignment. Its small-angle aerodynamics are used at every angle of attack (the angle between the rocket’s axis and its path through the air), with no stall. A flight that reaches Mach 5, the top of the aerodynamic models, stops with an error.
What the equations do
At each instant of a flight, hpr adds up every force on the rocket and every turning effect (a moment): the thrust, the weight, the air’s forces, and the effects of the propellant burning away. From those it works out how fast the rocket speeds up and how fast its turning changes. The integrator (Time integration) then carries the rocket forward, one short step at a time.
Burning propellant makes the rocket lighter, moves its center of mass, and lets the exhaust carry away some of the rocket’s turning (jet damping). The equations keep all three. They are RocketPy’s, from its technical documentation ([RP-EOM], under Code and sources), and are written out under Equations of motion.
Code and sources
Code: hpr_sim::{dynamics, flight, rail, recorder, state}. Decisions: ADR-011
(equations of motion, aerodynamic coupling, rail, phases and termination). The integrator and
events are in Time integration, and the frames in Frames.
Sources:
- [RP-EOM] RocketPy technical documentation, “Equations of Motion” v0 (Kane’s method with the
Reynolds transport theorem) and v1 (the form solved),
docs/technical/equations_of_motion.rstandequations_of_motion_v1.rstin RocketPy 1.13.0 (refs/rocketpy, MIT). - [C21] G. H. Ceotto, R. N. Schmitt, G. F. Alves, L. A. Pezente and B. S. Carmo, “RocketPy: Six Degree-of-Freedom Rocket Trajectory Simulator”, J. Aerosp. Eng. 34(6), 2021, doi:10.1061/(ASCE)AS.1943-5525.0001331. Cited, not fetched.
State
- Components. The state is the nose tip
O’s positionr_Oand velocityv_Oin the launch frameL, the attitude quaternionq(four numbers that give the rocket’s orientation, turning body axes intoL’s), and the body ratesω(how fast it turns about each body axis) relative toL, in body axes: 13 components. - Why the nose tip. It is fixed in the body and is the body origin (ADR-007, the
design tree). The center of mass
r(fromO, body axes) moves as propellant burns. - The quaternion. Its norm (its length, 1 for a pure rotation) drifts slightly between steps. Every use normalizes it, and it is never reset at an event (Time integration).
Equations of motion
[RP-EOM] v0 derives the motion of a variable-mass system (one whose mass changes as it flies) about a point fixed in the body. It uses two standard tools:
- Kane’s equations, a systematic way to write the equations of motion, for the rigid parts;
- the Reynolds transport theorem, which accounts for mass flowing across a boundary, for the propellant and the gas leaving the nozzle.
Its assumptions:
- the gas flow inside the motor is quasi-steady (it changes slowly enough to treat as steady at each instant), so the integrals over the space it flows through, the control volume, don’t change;
- the flow is axisymmetric: the same all round the rocket’s axis;
- the exhaust momentum is lumped into the thrust at the nozzle exit.
v1 solves the result for v̇ and ω̇, the rates of change of the velocity and of the body rates.
The lines below are that form, in body axes, with primes for time derivatives seen from the
rocket. T03, T04, T20 and T21 are labels carried over from [RP-EOM] v1, so each line can
be matched against it:
T03comes from the momentum of mass moving inside the rocket: gas flowing to the nozzles, and the center of mass shifting as propellant burns. The rotation turns that momentum into a force,ω×T03, as it does for anything moving inside a turning body (a Coriolis force).T04is the thrustTwith the propellant’s internal-momentum terms (Measured thrust curves and the internal-momentum terms, below).T20gathers every force:T04, the weightW, the air forceA, and two terms from the rotation,ω×T03and−ω×(ω×m r), the second because the center of mass is not atO.T21gathers every moment aboutO: from the rotation itself, from the jet and the changing inertia (Σ ṁ_k S_k − I_O′), and from the weight, the air and the thrust. Its last term,ω×h + h′, is for parts that move along the airframe, withhtheir angular momentum aboutOrelative to it: zero for a part on the axis, and zero when nothing moves (Moving mass).- The last three lines give the angular acceleration
ω̇, the nose tip’s accelerationa_O, and the rate at which the attitudeqchanges.
T03 = 2 Σ ṁ_k (n_k − r) − 2 m r′
T04 = T − m r″ − 2 ṁ r′ + Σ m̈_k (n_k − r)
T20 = −ω×(ω×m r) + ω×T03 + T04 + W + A
T21 = −ω×(I_O ω) + (Σ ṁ_k S_k − I_O′) ω + r×W + M_A + M_T − ω×h − h′
ω̇ = I_c⁻¹ (T21 − r × T20)
a_O = T20/m − ω̇ × r (then a_L = q a_O q*)
q̇ = ½ q ⊗ (0, ω)
- Mass terms.
m,ṁ = Σ ṁ_k ≤ 0andm̈_kare the mass, its rate and each motor’s mass acceleration.I_OandI_care the inertia aboutOand about the center of mass (I_O = I_c + m(|r|² 1 − r rᵀ)).n_kis motork’s nozzle exit.
- Forces and moments.
Tis the thrust alongz_B, the motors’thrust_at_pressure_n, with momentM_T = Σ n_k × T_kaboutO.Wis the weight: normal gravity (centrifugal term included) plus the Coriolis force−2m Ω×v_cg, both acting at the center of mass.AandM_Aare the aerodynamic force and its moment aboutO(below).
- Nozzle gyration tensor.
S_kdescribes how the exhaust leaving motork’s exit disc is spread aboutO; times the mass flowṁ_k, it gives the jet damping inT21.S_k = (r_e²/4) diag(1, 1, 2) + |n_k|² 1 − n_k n_kᵀ, integrated here from v0’s boxed rotational equation. The jet term there is∫ r×(ω×r) dṁover the exit disc of radiusr_e, with a uniform jet. For a disc atn_kon the axis, this gives(r_e²/4 + n_k², r_e²/4 + n_k², r_e²/2).- RocketPy 1.13.0’s code (
rocket.py:984-985,evaluate_nozzle_gyration_tensor) uses0.25·n²for the transverse distance term instead ofn². Itsnis measured from the center of dry mass, not from the nose tip, so the entries can’t be compared directly. The RocketPy code-to-code suite (M2.1) should compare the jet-damping coefficient about the center of mass. - A motor that gives no nozzle exit radius contributes
r_e = 0.
- RocketPy 1.13.0’s code (
- The classical limit. For an axisymmetric rocket turning slowly about a transverse axis, the
equations reduce exactly to the classical jet damping about the center of mass,
I_c ω̇ = [ṁ (r_e²/4 + l²) − İ_c] ω, withlfrom the center of mass to the nozzle exit. Theṁ l²part damps, and the falling inertia partly offsets it (dynamics::tests::jet_damping_matches_the_classical_form). - Time derivatives of the mass properties.
r′,r″,ṁandI_O′come from central differences ofAssembly::mass_propertieswith a half-width of 1e-4 s.- The stencil, the times each difference samples, is kept inside the current integration interval. Every thrust-curve knot and burnout is a stop time, so no difference straddles a change in the thrust’s slope. Near an interval’s end the derivative is taken at the nearest valid center and extended linearly.
m̈_kdifferences each motor’s mass flow the same way.- After burnout (an interval starting past every burnout) the rates are zero, and so are they over intervals shorter than 2e-5 s, where differences would be rounding noise.
- A part that moves along the airframe adds its own rates in closed form, not by differences (Moving mass).
- Motors at the ends of an interval. A motor that burns through an interval is evaluated at
its one-sided limit inside the burn,
tclamped to(0, t_end). The last stage of the step ending at burnout (or the first after ignition) then sees the burning motor, including the pressure correction that switches off att_end. Without this, fixed-step RK4 converged at first order (5.1 mm of apogee at 2 ms). - Measured thrust curves and the internal-momentum terms. A static test measures
T_exit − dP_int/dt, whereP_int = m r′ − ṁ(n − r)is the internal momentum of the burning propellant, so a.engcurve already contains it.T04subtracts−m r″ − 2ṁ r′ + m̈(n − r)again.- hpr keeps the terms as RocketPy does, for parity in the RocketPy comparison (M2.1). The form is exact for the [RP-EOM] model, not for a measured curve.
- The size of the double count on Valetudo: it lifts off at 1.56 ms with 73 N of thrust against
95 N of weight, 21 N coming from
m̈(n − r), and it changes the burnout speed by at most 0.05 m/s.
- Earth’s rotation. It enters only through the Coriolis force. The rotational equations use
ωrelative toL, which differs from the inertial rate by at most 7.3e-5 rad/s (Frames, and the rigid-body flight decision ADR-011).
Aerodynamics in flight
hpr-aero gives coefficients at a flow condition (Aerodynamics). The engine applies them as follows.
- Airspeed. The air velocity uses the wind at the center of mass’s height. Wind vectors are
taken in
L’s axes. - Axial force.
−q A C_A z_Bfrom the whole rocket’s drag, at the center of mass’s airspeed, Mach number, angle of attack and Reynolds number per meter (V/ν). The drag is power-on (DragConditions::thrusting, with the burning motors’ cross-section) while any motor burns in the interval.Simulation::with_full_base_drag_under_powerkeeps the whole base’s drag instead, as OpenRocket does, for comparisons with it (base drag under power). The force acts along the axis, so it has no moment aboutO. - Normal and side forces, component by component. Each body and fin set is evaluated at its
own local flow:
v_O − wind + ω × p_iat the stationp_ithe aerodynamics gives it (AeroModel::component_station_m). That station is the component’s small-angle center of pressure wherever one model carries the component. Faster than sound, where the shock-expansion method and slender-body theory share one, it is not: the station is joined between the two models while the force joins their slopes and moments, and the two agree only at the ends of the join. On a body that sits between the models at every speed (a lip riding half in its boattail’s wake), a tube’s station can sit two calibres from its own center of pressure (issue #106), which moves the lever arm the damping below uses by that much. ItsC_Nacts along the crossing airŵand itsC_Yalongz_B × ŵ(Frames), with moments−q A M_N (z_B × ŵ) + q A M_Y ŵaboutOfrom the component’s moment coefficients. - Fins at any angle of attack. A fin set’s normal force follows the crossflow
V sin α: its model’sC_N = C_Nα αis used asC_Nα sin α, the substitution Niskanen keeps for bodies (eq. 3.16–3.17). No source in hand gives a large-angle fin model, and stall is not modeled.- At small angles nothing changes (1.5% less at 0.3 rad).
- The force now vanishes for axial flow from the tail as well as from the nose.
- A force linear in
αstays large atα = πand flips with rounding noise in the lateral velocity. A calm vertical flight falling tail first after apogee then collapsed the step size and never landed (tests::a_calm_vertical_flight_falls_tail_first_and_lands).
- Damping. The rotation’s contribution to each local flow is the only aerodynamic pitch and
yaw damping. At a component’s station the rotation adds a velocity
ω × p_i, which changes the angle at which the air meets that component. The station is the lever arm, so where it is not the center of pressure (above) the damping is levered from the wrong point by the same distance.- Only components with a normal-force slope damp this way: nose cones, transitions and fin sets. A boattail’s slope is negative, so it takes some away.
- Body tubes give none at small angles: their own slope is 0, and their body lift grows with
sin² α. hpr-aerohas no pitch or yaw damping coefficients; they would have to replace the local-flow damping, not add to it.- A flight on another tool’s normal force keeps this damping. The table (The normal force from RASAero II) gives the normal force at the center of mass’s airflow, and each component adds only its force in its own local flow minus its force in the center of mass’s airflow: the part the rotation makes. Without rotation that part is exactly zero (ADR-032).
- Roll. The fins’ cant drives the roll and the roll rate damps it: a moment
q A d (C_l0 cos α + C_lp p d/2V)aboutz_B, withqthe dynamic pressure,Aanddthe reference area and diameter,C_l0the cant’s rolling moment,C_lpthe damping andp = ω_z, at the center of mass’s Mach number; the cant’s forcing follows the axial flow,cos α, so it vanishes broadside and reverses tail first (AeroModel::roll, Roll: forcing and damping). The damping is writtenρ V A d² C_lp p/4, so it fades smoothly as the airspeed does. The roll rate also changes through inertia coupling (turning about one axis driving turning about another) for a rocket whose mass isn’t symmetric about its axis, and, while a motor burns, through the jet and the falling inertia inT21. At constant speed a canted rocket spins up to the steady rate where cant and damping balance, as the closed form gives (tests::canted_fins_spin_to_the_analytic_balance). - Limits.
- The models are small-angle: no stall, and body lift and fins extended by
sin α. They overstate the forces at largeα. In normal flights largeαoccurs near apogee, where the dynamic pressure is small, and off the rail in strong crosswinds.Sample::angle_of_attack_radshows where it happens. - The normal force and the drag buildup cover Mach 0 to 5
(Fins through Mach 1,
Drag through Mach 1, and ADR-028, the drag’s
decision); a fin set’s CP moves with the Mach number, and each component takes its local
airflow at its CP for the center of mass’s Mach number. At Mach 5 or faster they refuse the
flow, and the flight stops with
SimError::Aero. A drag override table covers drag at any Mach, but the normal force still stops at Mach 5.
- The models are small-angle: no stall, and body lift and fins extended by
Phases
- Pad.
- The rocket starts with its aft end (its body’s aft end or its aft-most nozzle exit) at the rail’s foot, at rest, with the rail’s attitude.
- It stays until the force along the rail,
T20·z_Bwithω = 0, exceeds frictionμ |T20_⊥|(the liftoff event).T20_⊥is the rail’s reaction: the weight, the aerodynamic force and the mass terms across the axis.
- Rail.
- One degree of freedom:
a = (T20·z_B − μ |T20_⊥|)/malong the rail, with no rotation. - It ends when the aft edge of the aft-most rail button or launch lug passes the top of the rail, or the aft end with no guides (Rail geometry, below).
- If the speed along the rail falls to zero, the rocket returns to the pad phase, at rest where it stopped. It is held there, even if the force down the rail exceeds friction and a real rocket would slide back.
- One degree of freedom:
- Free. Six degrees of freedom until the ground.
- Rail geometry.
- A button’s axial extent is its outer diameter, and a lug’s is its length.
- The rocket leaves after travelling
L − (s_aft − s_guide). RocketPy ends its rail phase when the forward button reaches the top (flight.py:1716-1730,effective_1rl). hpr keeps the rocket guided to the last guide (Loft lesson L26). - The pivot about the last guide (“tip-off”) is not modeled, and the rocket leaves the rail with no angular velocity.
- Friction is Coulomb friction: a user coefficient times the net reaction
|ΣN|, the force pressing the guides onto the rail. With a moment across two buttons (a crosswind at low speed), the true frictionμ(|N₁| + |N₂|)is larger. No source gives a default coefficient, so the default is zero.
Events and termination
-
Stop times. Thrust-curve knots, burnouts and the time cap. A burnout that is reached records a
Burnoutevent. -
Liftoff and stall. Liftoff is the pad force margin rising through zero (checked also at each interval’s start, for a thrust that jumps at a knot). Stall is the speed along the rail falling through zero.
-
Rail exit. The travel along the rail reaching the exit travel.
-
Apogee. The center of mass’s ellipsoidal-height rate,
û(r_cg) · v_cg, falling through zero.ûis the ellipsoid normal at its position. A flight that has let a part go records one apogee (Released mass). -
Ground hit. The center of mass’s ellipsoidal height reaching the launch site’s, descending (Frames, Loft lesson L35).
-
User events. A function of the
Sample, in free flight. -
Mass shifts and releases. A part starting to move along the airframe (
Shift) or leaving it (MassRelease), on its trigger (Moving mass, Released mass). -
Heights. Atmosphere and wind heights are
h − N, the height above sea level:his the ellipsoidal height, and the geoid undulationN, the height of sea level above the ellipsoid, is given inEnvironment(no geoid model). -
Termination (Loft lesson L25). The flight ends in exactly one of these ways:
GroundHit;NoLiftoff(the last burnout passes on the pad);StalledOnRail(it lifted off, then stopped on the rail after burnout);TimeCap;StepLimit.
Any other failure is an error.
-
Not yet modeled. Turbulence and thrust misalignment. Recovery is modeled, and has its own page; so are motors lit at their own times and a powered separation, on Staging.
Integration settings
-
Defaults.
FlightSettings::default()uses Dormand–Prince 5(4) withrtol = atol = 1e-8and unit weights, a one-hour cap and 10⁶ steps. -
Accuracy against cost. Measured on Valetudo (K400C), with a 5 m/s wind from the west, compared with the apogee at 1e-11 (873.98272 m):
rtol = atolapogee error flight time (release) 1e-4 2.3e-2 m 0.36 ms 1e-6 6.8e-5 m 0.59 ms 1e-8 1.1e-6 m 1.13 ms 1e-10 1e-7 m 2.72 ms
Verification
Unit tests in hpr_sim::tests, hpr_sim::dynamics::tests and hpr_sim::rail::tests, at the
default settings. The numbers were measured on 2026-09-17.
- Vacuum ballistic. A tumbling, spinning Valetudo in a vacuum, from the nose tip at 500 m with
v = (30, −20, 80)m/s.- The center of mass stays on the closed-form parabola to 1.7e-6 m over 22 s.
- The angular momentum stays constant to 7.8e-7 (relative).
- Apogee and ground contact are within 4.7e-7 s and 1.1e-8 s of the parabola’s. Those times are located (root-found) on the exact ellipsoidal height.
- Terminal velocity. Falling nose down through uniform air with
C_D = 0.5, the speed followsv_t tanh(g t/v_t)to 5.5e-9v_t, and isv_tto 1e-5 after 150 s. - Torque-free precession (the wobble of a spinning body with no torque on it). Valetudo
(
I_a/I_t= 0.0019) spinning at 25 rad/s with a transverse rate turns in body axes atΩ = (I_a − I_t) ω_z/I_t= −24.95 rad/s. The rate matches to 4.7e-7 rad/s over 3 s, and the angular momentum inLholds to 1.5e-6. - Pitch oscillation against linear theory. At 100 m/s with no drag or gravity, the two-state
linear model (path turning, restoring moment
K₁, rotational dampingK₂) predicts a 1.44954 s period. The flight measures 1.44965 s (8e-5), and the decay per half period is within 0.2% of the model’s. - Jet damping. The flight equations give the classical jet damping to 1e-6, and the mass rate
equals the motor’s
−F/cto 1e-6, including stencils clipped by an interval’s end. - Powered climb. A vertical climb in vacuum from 0.5 s to 3.0 s ends at the speed of the axial equation, integrated independently from the motor and the assembly, to 4.3e-8 m/s.
- Rail friction. At 60°,
μ = 0.3removes exactlyμ g cos Efrom the acceleration along the rail. - Loft lesson L20, weathercocking. In a 5 m/s wind from the west, Valetudo’s unit axis has an east component of −0.12 at burnout. Its apogee is 96 m upwind, against 1.0 m (Earth rotation) in calm air.
- Calm vertical. With no wind and no Earth rotation the rocket falls tail first after apogee and still lands, in under 20,000 evaluations.
- Solvers agree. RK4 at 2, 1 and 0.5 ms gives the same apogee as Dormand–Prince to 1.2e-5 m and 2.1e-7 s.
- Burnout at an event. A user event on the thrust fires at burnout, and burnout is still recorded once.
StalledOnRail. A 2000 m rail gives it, with only liftoff and burnout recorded.- Errors and reuse. Observer errors end the flight with that error. Starts before ignition
or underground are refused. A cleared recorder records the same rows again.
Simulation,EnvironmentandRecorderareSend + Sync. - Loft lesson L24. Two runs of one
Simulation, and a secondSimulationbuilt the same way, record the same bits for every channel at every step. - Loft lesson L25. A normal flight hits the ground. A rail with
μ = 20at 60° givesNoLiftoffat rest. A 5 s cap givesTimeCapat exactly 5 s, and a 40-step limit givesStepLimit. - Loft lesson L26. On a 3 m rail tilted to 1.3 rad, the rail exit comes at the last
button’s travel to 1e-6 m. Across the rail the rocket stays within 1e-9 m, with no rotation.
Friction (
μ = 0.3) delays the exit and slows it. - A tilted rail against OpenRocket. From a 1 m rod tilted 5 to 20 degrees, hpr’s rocket is on
OpenRocket’s bearing at apogee to within 0.03 degrees and loses the same apogee to within 0.27
percentage points (
.ork: a tilted launch rod). - Events and recorder. Events come in order: liftoff, rail exit, burnout, apogee, ground hit. Apogee’s vertical speed is below 1e-6 m/s and ground contact’s height below 1e-6 m. Recorder rows fall on the interval or at events.
- Through Mach 1. The synthetic 54 mm rocket on an I175 passes Mach 1 and lands, both on hpr’s own drag and on a constant drag table.
- Cost. About 1.1 ms per Valetudo flight to the ground (
docs/perf.md).
Whole flights against RocketPy
cargo xtask validate flies six of RocketPy’s example rockets from the pad to the ground and
compares fifteen numbers of each with RocketPy 1.13.0’s own flight
(ADR-021, the whole-flight comparison). Both codes fly one declared drag coefficient,
a constant C_D0 of 0.5 on the same reference area. So what is compared is the equations of
motion, the motor and the air, not the drag. The rest is RocketPy’s where hpr has it: its gravity
formula, standard atmosphere, frictionless rail, the example’s rail angles, parachutes and motor,
and a declared wind.
| result | value |
|---|---|
| cases scored | 6, one of them (Prometheus 2022) past Mach 1, since M1.8a |
| height, speed, time, acceleration | all scored, all within 3% of RocketPy’s |
| largest of those | +1.783%, Bella Lui’s peak acceleration, on the rail |
| largest in apogee | +1.208%, Prometheus 2022; RocketPy flown with hpr’s body lift and rail release comes within 0.01% (case file) |
| path without wind (drift of apogee and landing) | all scored, within 1.9% (largest −1.811%, Valetudo’s landing, in still air) |
| path in wind | Calisto’s scored (largest +1.257%), and NDRT 2020’s landing; Juno III’s, Bella Lui’s and Prometheus 2022’s, and NDRT 2020’s apogee drift, differ by 4.3 to 38% and are reported, not scored: hpr’s body lift and rail release, and Juno III’s fin slope (ADR-026) |
What the two codes still do differently, and how much it moves:
- The wind. A rocket that leaves the rail slowly in a wind meets the air at a steep angle: Juno III at 18 m/s in an 8.5 m/s wind, 26° off the airflow. There hpr’s normal force includes body lift (Aerodynamics), which RocketPy’s leaves out. Much of it acts ahead of the center of mass, the nose’s above all, so it moves the center of pressure forward and weakens the turn into the wind, and hpr turns into it less: Juno III’s apogee is 245.3 m from the pad in hpr and 396.6 m in RocketPy. Given hpr’s body lift, its rail release and its flat-plate fin slope (it cannot model the airfoil lift curve Juno III’s example gives its fins), RocketPy puts it 248.3 m out, and every windy drift within 1.3% of hpr’s (ADR-026). hpr’s growth of drag with the angle of attack moves no drift by more than 0.1%.
- RocketPy’s equations, corrected. hpr’s equations of motion follow RocketPy’s technical documentation, which measures the center of mass from the center of dry mass. RocketPy 1.13.0’s code reads that vector the other way round, so during the burn it takes the turning moments about the wrong point and its rockets turn into the wind too far. The fix is proposed in a pull request to RocketPy, still open, built on one that is merged but not yet released; RocketPy 1.13.0 as installed still has the error, and the comparison applies both fixes. Without them, hpr’s drifts in wind were up to −60.8% short of RocketPy’s at apogee and +151% beyond it at landing (ADR-026, issue #50).
- The rail. hpr’s rail equation keeps the terms for the center of mass moving inside the
body as the propellant burns. RocketPy’s rail equation (
udot_rail1) leaves them out. At a sharp ignition spike, with thrust and mass the same to five digits, hpr’s acceleration is 1.2 to 1.3 m/s² higher. hpr also guides the rocket until its last rail button leaves, where RocketPy frees it at the first. - Calisto’s two peaks. Its acceleration peaks twice, 0.9% apart, and the rail terms make hpr’s maximum the other peak. So the time of the peak (−96.8%) is reported but not scored.
- The parachutes. RocketPy counts the air a canopy drags along (added mass); hpr has none. So the peak deceleration as NDRT 2020’s main opens is 83% higher in hpr, and that number is reported but not scored. The speeds at landing agree within 0.03%.
The metrics are measured as RocketPy defines them: speeds and accelerations at the center of dry
mass; heights from that point’s height at launch, since RocketPy’s starts at the ground; and the
rail exit when the rocket has travelled RocketPy’s effective_1rl, the forward button at the top
of the rail, where hpr’s own rail-exit event waits for the last one. Each case file argues its
tolerances; Accuracy gives every result.
Time integration and events
In short
- What it models: stepping a flight through time, by Dormand–Prince 5(4), which adapts its steps to an error tolerance, or fixed-step Runge–Kutta (RK4). It lands exactly on set times such as burnout, and finds events such as apogee.
- Sources: Dormand and Prince (1980); Hairer, Nørsett and Wanner’s Solving Ordinary
Differential Equations I (1993), whose
DOPRI5code the port follows; Brent (1973), for the root finding that locates events. - How well it is validated: by analytic and unit tests only, not yet on its own against another
simulator or a real flight. Its step counts match an independent transcription of
DOPRI5, and its error shrinks as theory predicts. In an exactly solvable flight with drag, apogee, deployment and landing times are right to 1.5e-8 s at default tolerances. - What it leaves out: it can’t detect a stiff problem, where very fast, heavily damped motion forces tiny steps; the run then stops with an error. Fixed 10 ms RK4 steps can diverge for a light body under a big canopy. An event that crosses zero and back within one step goes unseen.
Code and sources
Code: hpr_sim::integrator and hpr_sim::events. Decisions: ADR-010 (Dormand–Prince
with dense output, RK4, stop times and events).
Sources:
- [DP80] J. R. Dormand and P. J. Prince, “A family of embedded Runge-Kutta formulae”, J. Comput. Appl. Math. 6 (1980) 19–26, doi:10.1016/0771-050X(80)90013-3. Cited, not fetched (the publisher refuses scripted downloads).
- [HNW] E. Hairer, S. P. Nørsett and G. Wanner, Solving Ordinary Differential Equations I, 2nd ed., Springer, 1993: §II.1 (RK4, table 1.2), §II.2 (order conditions, table 2.2), §II.4 (error norm, step control, starting step), §II.5 (DOPRI5, table 5.2), §II.6 (dense output). The PI controller is §IV.2 of volume II. Cited, not fetched.
- [D5] E. Hairer and G. Wanner,
DOPRI5(Fortran, version of 2004), BSD-2-Clause, pinned ashairer-dopri5invalidation/refs.lock.toml. It is the book’s code, and the port follows it: coefficients (CDOPRI), the error norm and controller (DOPCOR), the starting step (HINIT) and the dense output (CONTD5). - [B73] R. P. Brent, Algorithms for Minimization without Derivatives, Prentice-Hall, 1973, ch. 4 (the zero finder). Cited, not fetched; implemented from the algorithm’s description.
Methods
- Dormand–Prince 5(4) ([DP80]; [HNW] table 5.2). Seven stages, first-same-as-last, so six new
evaluations per step. The step advances with the fifth-order solution (local extrapolation), and
the embedded fourth-order solution estimates the error:
err_i = h Σ (b_j − b̂_j) k_j. - Error norm ([HNW] eq. 4.11; [D5]):
‖err‖ = √(Σ (err_i/sc_i)²/N)withsc_i = w_i·atol + rtol·max(|y0_i|, |y1_i|). The weightsw_icome fromOdeSystem::absolute_tolerance_weights, so components in different units share oneatol. A step is accepted when‖err‖ ≤ 1. - Step control ([HNW] II, §IV.2; [D5] defaults). The PI controller
h_new = h / clamp(‖err‖^(0.2 − 0.75β) / ‖err_old‖^β / 0.9, 1/10, 5), withβ = 0.04. After a rejection,h_new = h / min(5, ‖err‖^0.17/0.9), and the step after a rejection can’t grow. - Departures from
DOPRI5.- A non-finite error estimate, or a derivative that fails in stages 2–7, counts as a rejection
that shrinks
hfive times. A long step’s stages can probe states off the trajectory (negative mass, a Mach number past a table’s end). The failure is reported only once the step can’t shrink further. A failure at the step’s first stage is reported at once, and so is any failure under RK4. - A final interval within the rounding of
t(0.1 h ≤ max(|t|, 1)·uround) counts as reached, instead of failing as too small. - After a step shortened to land on a stop time, the next call starts from the larger of the controller’s proposal and the step the controller wanted before shortening.
- The step size overflowing to infinity (a problem with zero error and no stop) is an error.
- There is no stiffness detection.
- A non-finite error estimate, or a derivative that fails in stages 2–7, counts as a rejection
that shrinks
- Starting step ([HNW] §II.4;
HINIT):h₀ = 0.01 ‖y₀‖/‖f₀‖, then one Euler step estimates the second derivative andh = min(100 h₀, (0.01/max(‖f₀‖, ‖y''‖))^(1/5)). - Dense output ([HNW] §II.6;
CONTD5). Withθ = (t − t₀)/handΔ = y₁ − y₀:y(θ) = y₀ + θ(Δ + (1−θ)(r₂ + θ(r₃ + (1−θ) r₄))), wherer₂ = h k₁ − Δ,r₃ = Δ − h k₇ − r₂andr₄ = h Σ d_j k_j. It is a continuous extension of order 4, so its error inside a step isO(h⁵). - RK4 ([HNW] table 1.2): weights 1/6, 2/6, 2/6, 1/6 at 0, ½, ½, 1, with a fixed step. Its
dense output is the cubic Hermite interpolant of
y₀, y₁, f₀, f₁in the same nested form withoutr₄(errorO(h⁴)inside a step).f₁is the next step’sk₁, so it costs nothing extra.
Stop times and discontinuities
Integrator::advance(system, t_stop)never steps pastt_stop. LikeDOPRI5, it stretches the last step up to 1% to land on it exactly (never pastmax_step_s), andtime_s()equalst_stopbit for bit.- The system is one
OdeSystem<N>: the derivative, optional tolerance weights, optional events (event_count,event_direction,event_value), andaccept_step.accept_stepsees every accepted step with its dense output and can stop the run (Advance::Stopped). So a flight phase can record from, and stop on, its own state. - A discontinuity in the right-hand side (burnout, a staging, a phase change) must be a stop time. An RK step across a jump drops to first order (Loft lesson L23).
- Which side a stop time belongs to. The last stage of the step ending at
t_stopis evaluated att_stopand belongs to the phase before it. The first stage of the next call belongs to the phase after it. A system that picks its phase fromtalone gets one of the two wrong, so the caller sets the phase between calls (integrator::tests::a_discontinuity_inside_a_step_...). - Each call evaluates
f(t, y)afresh, so the system can change between calls. The step-size estimate carries over.
Events
- An event is a scalar
g(t, y)with a direction: rising (g0 < 0 ≤ g1), falling (g0 > 0 ≥ g1) or either. The start value must be strictly on one side. - After each accepted step,
advanceevaluates everygat the step’s end. For each sign change it finds the zero ofg(t, y_dense(t))on the step with Brent’s method ([B73]) toEVENT_TIME_RESOLUTION_S = 1e-12 s(plus4ε|t|). The earliest zero sets the stop. - Where it stops. The integrator stops at the end of Brent’s final bracket on the far side of
the zero, with the dense output’s state there. It returns
Advance::Events, andfired_events()lists every event past its zero at that state, ascending. Coincident events, such as two devices set to deploy at apogee, fire together.gis already past zero (or exactly zero) for all of them when the next call starts, so none is reported twice. The system’saccept_stepsees the shortened step. - Accuracy. The event time is as accurate as the solution: the root finder adds at most about
2e-12 s. The state at the event is the dense output’s, which is fourth order for Dormand–Prince and third for RK4. - Limits.
- A function that crosses zero and returns inside one step goes unseen. Bound the step with
Adaptive::max_step_swhere that can happen. - A function that crosses and comes back to exactly zero at the step’s end is reported at the end.
- Events are checked only on accepted steps, never on stage values.
Integrator::resetbetween calls can move an event function back to its near side, by as little as renormalizing a quaternion. The event then fires again moments later. So normalize inside the system’s functions rather than resetting at an event.
- A function that crosses zero and returns inside one step goes unseen. Bound the step with
Defaults and limits
rtol = atol = 1e-8, no maximum step, and a limit of 10⁶ attempted steps.- The flight keeps these defaults with unit weights. On Valetudo’s flight on a K400C motor they take 1.13 ms, and put the apogee within 1.1e-6 m of a run at 1e-11 (Rigid-body flight).
- A stiff flight phase shows up as
StepTooSmallor the step limit. - A fixed step has to respect the drag’s own time scale. Quadratic drag
v̇ = −k|v|v, withk = ρ (C_D S)/2m, linearises toλ = 2k|v|, and RK4 is stable only forh ≲ 2.78/λ. A light body under a big canopy is the worst case in hpr: a 0.55 kg sustainer arriving at 168 m/s under 2 m² givesλ ≈ 740 1/s, so RK4 needsh ≲ 3.8 msand diverges at 10 ms (found in review; it surfaces as a geodesy error from an absurd position, not as a stability message). The adaptive method has no such limit. Recovery descents are the place this bites, because a separated body can be a tenth of the stack’s mass under a canopy sized for it. - Integration runs forward only.
t_stopmay be infinite, to run until an event or a stop fromaccept_step. After any error, the integrator stays at its last accepted step and can resume;set_step_limitraises a spent limit.
Verification
Unit tests in hpr_sim::integrator::tests and hpr_sim::events::tests. The numbers quoted were
measured on 2026-09-17.
- Tableau. Rows sum to
c. The fifth-order weights satisfy all 17 order conditions to order 5 ([HNW] table 2.2) to 1e-14. The embedded weights satisfy orders 1–4 and miss at least one fifth-order condition by more than 1e-4. The dense output’s weightsb(θ)satisfy the conditions to order 4 at θ = 0.1 to 0.9, which pinsD1–D7. - The port is
DOPRI5. On Hairer’s driver problem, the Arenstorf orbit, atrtol = atol= 1e-4, 1e-7 and 1e-10, the evaluations (494, 1442, 5060), attempted steps (82, 240, 843) and accepted steps (64, 216, 841) equal those of an independent line-by-line transcription ofDOPCORandHINIT. So do the end positions, to 1e-9. This pins the controller, the starting step and the error norm. - Step-halving convergence on the nonlinear, non-autonomous
y' = −2ty²,y = 1/(1 + t²)on[0, 2], driven throughadvance:- Dormand–Prince at a fixed step (first and largest step
h, tolerances too loose to reject), 10 to 160 steps: orders 5.88, 5.53, 5.31, 5.17, falling toward 5. At 320 steps the error, 6e-15, reaches rounding. The dense output atθ= ¼, ½, ¾ gives 4.81, 4.97, 4.99, 4.99. - RK4, 20 to 160 steps: orders 4.04, 4.02, 4.01. Its Hermite dense output gives 3.96, 3.99, 4.00.
- Dormand–Prince at a fixed step (first and largest step
- Tolerance proportionality. Over
[0, 10]withrtol = atolfrom 1e-4 to 1e-9, the error falls from 2.7e-5 to 2.2e-10, 0.22 to 0.28 of the tolerance throughout (asserted within 0.07 to 0.85). - Apogee under tolerance halving (Loft lesson L21). A vertical flight with quadratic
drag,
v' = −g − kv|v|, has a closed-form apogee (testing::closed_form_quadratic_drag). As the tolerance halves from 1e-5 to 2e-8, the apogee time error falls from 5.7e-4 s to 1.2e-8 s and the height error from 3.4e-4 m to 8.3e-8 m. The fall is not monotone step by step:v|v|has a kink at apogee. The log–log slope of the height error against the tolerance is 1.36, and the worst errors are 117·tol (time) and 91·tol (height). - Vacuum with constant thrust (Loft lesson L23). Burnout is a stop time. At burnout the state matches Tsiolkovsky’s equation with gravity loss to 1e-9 relative, for both methods. The coast apogee is within 6.3e-9 s and 5.2e-6 m of 48.8 km (Dormand–Prince, 20 steps) and 4e-11 s and 1.6e-8 m (RK4, 0.01 s). Integrated straight through the jump, RK4 misses the velocity by 0.56 m/s; with the stop time the miss is 1e-12 m/s.
- Events within 1e-6 s (Loft lesson L22).
- For the quadratic-drag flight, the apogee, a 300 m descending deploy and landing are all
located against the closed forms. Errors: 1.2e-8, 1.5e-8 and 1.4e-8 s at the default
tolerances, and 3e-12 to 1.3e-11 s with RK4 at 0.01 s.
gat the stop is below 1e-13. - For
x = cos t, falling zeros and extrema over 20 s are located to 3e-8 s (Dormand–Prince) and 1.6e-9 s (RK4). - Restarting on an event doesn’t report it again.
- Three coincident events fire together, once per crossing.
- Three crossings inside one RK4 step come out in time order, to 1e-12 s.
- Where the dense output is exact (
y' = 1), nonlinear event functions stop between 0 and 2.5e-12 s past their roots, on the far side.
- For the quadratic-drag flight, the apogee, a 300 m descending deploy and landing are all
located against the closed forms. Errors: 1.2e-8, 1.5e-8 and 1.4e-8 s at the default
tolerances, and 3e-12 to 1.3e-11 s with RK4 at 0.01 s.
- Brent. Checked on a smooth root to 1e-14, a triple root, a step discontinuity (under 60 evaluations, far side returned), exact zeros at the ends, a bracket that doesn’t bracket and a NaN.
- Failures. These are reported as errors, never as a quiet stop:
- a failing derivative (the adaptive method closes to within 1e-9 s of where it fails);
- a blow-up (
y' = y²stops neart = 1); - a step overflowing to infinity;
- the step limit (and resuming after raising it);
- bad settings, a backward stop and zero weights.
- Robustness. Stops 1 to 40 ulps ahead of
t= 0, 3, 100 and 1000 s are reached. Steps respectmax_step_s.accept_stepcan stop a run. Repeated runs are bit-identical. Unknown fields and the old tag names are refused in settings files.
Recovery
In short
- What it models: how a rocket comes down under a parachute, a streamer or tumbling: when each device fires, how a canopy fills, the descent in the wind, and a stack that splits into parts (separation), or lets out a nose cone or a payload (ejection), that each land.
- Sources: Knacke’s Parachute Recovery Systems Design Manual (1991) for parachutes; Carruthers and Filippone’s streamer tests (2005) and the OpenRocket technical documentation for streamers and tumbling.
- How well it is validated: against another simulator and against drop tests; no descent has
been compared with a real flight yet.
- The descent under a parachute matches RocketPy’s for five example rockets, started from the same state near apogee: all 30 metrics within 3%, the largest +2.865% (validation report). Drift is measured from that shared start, not from the pad.
- That comparison flies RocketPy’s gravity formula and RocketPy’s way of interpolating the wind, and hpr’s defaults differ from both (the full gravity vector, and a wind table interpolated by speed and direction). Under hpr’s own gravity, Calisto’s drift read a little closer, once, before the comparison switched (Against RocketPy, below); no test pins that. What hpr’s default wind interpolation does to drift has not been measured.
- Against measured drop tests, tumbling is −10 to +19% off, and the default streamer model predicts a descent +9% faster than Kidwell’s one flat streamer.
- A
.orkfile’s parachutes and streamers fly as OpenRocket flies them. The landing speed is within 0.02% of OpenRocket’s on 50 of its 53 example flights and within 0.12% on all 53. On the 51 with no named cause for an apogee gap, less the Base drag hack’s three, the flight time is −1.57% to +3.16% off (From an OpenRocket file). - Ejected pieces (a nose cone or payload that lands on its own, ejection) are checked against exact answers only: no simulator or flight has been compared with them yet.
- What it leaves out:
- Two effects on the opening load: the drag overshoot as a canopy fills, and, when the canopy opens at once (the default), the way a light rocket slows while it fills. So hpr’s peak opening load is no safe bound either way. Don’t size recovery hardware from it: use a dedicated opening-load method and the hardware’s own ratings.
- Airframe drag: a separated body falls with no drag until its device opens, so its deployment speed can read high, and the airframe’s own drag under a canopy is left out too. The same holds for an ejected piece.
- The push of a separation’s charge on the two stages (an ejection’s is modeled, a separation’s is not).
- Added mass (air carried along), the swing (the attitude freezes at deployment), and streamer pleats (+58% fast on a pleated one).
- Tumbling is used far outside its fit: 36 m/s for Valetudo, against 5.0 to 6.6 m/s, and a nose cone tumbling on its own is outside it too.
Code and sources
Code: hpr_sim::recovery in the API reference
(crates/hpr-sim/src/recovery.rs), and the descent branch of crates/hpr-sim/src/dynamics.rs.
Decisions: ADR-012 (parachutes and the descent), ADR-013 (streamers and
tumble), ADR-014 (separation), ADR-153 (a .ork file’s recovery, flown
by hpr::ork::recovery).
Sources:
- T. W. Knacke, Parachute Recovery Systems Design Manual, NWC TP 6575 (1991), for canopy drag
coefficients, filling times, drag-area growth and the equilibrium descent speed. Its title page
limits distribution, so it is cited, never redistributed (
docs/VALIDATION.md). - RocketPy 1.13.0 (MIT),
rocketpy/simulation/flight.py:2710-2790androcketpy/rocket/parachute.py, for the point-mass descent that the parachute milestone (M1.7a) is compared against. - J. Carruthers and A. Filippone, “Aerodynamic Drag of Streamers and Flags”, Journal of Aircraft
42(4), 2005, and the OpenRocket technical documentation v13.05 (CC BY-SA), Appendix C, for
streamers; the same documentation’s §3.5 for tumbling bodies, and its §4.2.5 for a
.orkfile’s parachutes and descent; and C. Kidwell’s drop tests (2001), a research report for NARAM-43, the National Association of Rocketry’s annual meet, as the measurement both streamer models are checked against.
Drag area
A device’s drag is set by its drag area C_D S, in m²: a drag
coefficient times the area that coefficient is measured on. hpr takes it in one of two ways:
- Given directly (
DeviceDrag::DragArea), which is RocketPy’scd_s. - From a canopy (
DeviceDrag::Canopy):C_D S = C_D0 · π D₀²/4, withD₀the nominal diameter andC_D0the drag coefficient on the nominal areaS₀ = π D₀²/4, Knacke’s convention (printed page 5-2;D₀ = √(4 S₀/π), soS₀includes the vent and every opening).
CanopyType carries Knacke’s printed data for thirteen canopy types:
- The
C_D0range (Table 5-1 for solid textile canopies, Table 5-2 for slotted). Knacke prints a range for every type and no single value; hpr’s default is the middle of the range (flat circular: 0.75 to 0.80, so 0.775). - The fill constant
n(Table 5-6), which sets how long the canopy takes to fill: the filling time isnnominal diameters divided by the speed at line stretch (Inflation). hpr takes it from the table’s unreefed column, for a canopy that opens freely. (Reefing is a line round a canopy’s edge that holds it partly closed at first, to cut the opening load.) - The drag-area growth exponent of Pflanz’s method (Figure 5-51). Pflanz’s method, in Knacke’s manual, works out the opening force with a drag area that grows as a power of the time since line stretch.
- The infinite-mass opening-force coefficient
C_x: the peak force as the canopy opens, over its steady drag at the same speed, for a load so heavy that it doesn’t slow while the canopy fills. hpr reports it; its equations don’t use it (Inflation).
Where Knacke prints “insufficient data” hpr has None, and the user has to supply the number.
RocketPy’s default parachute C_D of 1.4 is not a C_D0 in this sense: it is a hemispherical
canopy’s coefficient on the projected area, which it uses only to turn cd_s into a radius for its
added mass. Knacke’s hemispherical range on S₀ is 0.62 to 0.77.
Streamers
A streamer is a strip of fabric of length l and width w, so a
planform (one-side) area S = l w and an aspect ratio AR = l/w. StreamerModel picks the
correlation, the curve fitted to measurements that gives its drag coefficient:
-
Filippone(the default), onS, from wind-tunnel tests of cotton streamers clamped at the leading edge atAR3.3 to 30 and 6 to 18.9 m/s. The paper fits one power curve per planform area, and prints all three:planform area curve where 0.025 m² C_D = 0.561 AR^−0.480eq. 2 (Figure 2’s trend reads 0.561 AR^−0.4795)0.05 m² C_D = 0.6514 AR^−0.6075the trend line on Figure 3; the text does not repeat it 0.075 m² C_D = 0.405 AR^−0.494eq. 1 (Figure 4 reads 0.4046 AR^−0.494)hpr interpolates between neighbouring curves, linearly in the logarithm of the area (
ln S), and holds the end curve outside the fitted areas; that is hpr’s choice, not the paper’s. All three curves are needed, becauseC_Dis far from linear inln S. AtAR = 3.3, the middle area’sC_D(0.3154) sits 0.3% below the smallest area’s (0.3163), not partway down to the largest’s (0.2245); a straight line inln Swould put it 63% of the way there. Blending only the two end curves would read 18% low there.Three of the paper’s own findings bear on how to read it, and none is in its curves:
- a free leading edge gives more drag than the clamped mounting these curves come from;
- lighter, smoother fabric gives less (polyester at 64 g/m² against cotton at 177);
- at the largest area it finds a drag crisis, a sudden change in
the drag coefficient over a narrow range of Reynolds number,
near
Re = 7.2e5.
A rocket’s streamer is free and light, so the first two pull in opposite directions.
-
OpenRocket:C_Dm = 0.034 ((ρ_m + 25 g/m²)/(105 g/m²)) ((l + 1 m)/l)onS, withρ_mthe fabric’s mass per square meter (Appendix C, eq. C.6, printed page 117). It was fitted to model-rocket streamers (w0.01 to 0.09 m,l0.2 to 1.0 m, 10 to 80 g/m², 6 to 12 m/s), with a stated 12 to 27% error on an independent set. It is the only one of the two that uses the material.
Which model is right?
The two disagree by a factor that depends on the fabric: at AR = 10 and l = 1.016 m the ratio
of their drag areas is 5.8 at 10 g/m², 3.5 at 32 and 1.9 at 80.
C. Kidwell’s NARAM-43 drop tests (2001) settle it as far as one dataset can: sixteen 4 in × 40 in streamers, each with about 5 g at one corner, dropped 20.1 m.
Two details of his method decide how to compare, and both were got wrong here before review:
- His rates are distance over time, so they are averages over the drop rather than terminal speeds. A 20.1 m drop averages 0.97 of terminal at 2.8 m/s but 0.89 at 5.9 m/s.
- They are normalised to a notional 5.000 g weight: scaled to what that standard weight would give.
streamer_models_against_kidwells_drop_tests uses the notional mass and compares the same
average, from the closed-form fall:
| case | measured | C_D it implies, on S | Filippone | OpenRocket |
|---|---|---|---|---|
| crêpe paper, 32 g/m², unpleated | 2.80 m/s | 0.155 | 3.05 m/s (+9%) | 5.28 m/s (+88%) |
| Micafilm, 42 g/m², pleated | 2.04 m/s | 0.338 | 3.21 m/s (+58%) | 5.19 m/s (+154%) |
What the table shows:
- The flat streamer. Kidwell’s crêpe streamer is the one he left flat, and a flat correlation predicts it to 9%. Appendix C reads 88% fast.
- The pleated ones. His other materials were folded in ¾ in pleats, which more than doubles the drag again. hpr models no pleats, so it predicts a faster descent for a pleated streamer, which is the safe direction for a landing.
- Both are a little outside both fits: 0.1032 m² of planform against Filippone’s largest
0.075 m², and 0.1016 m wide by 1.016 m long against appendix C’s
w ≤ 0.09,l ≤ 1.0.
Streamers larger than the fits
Above 0.075 m², hpr’s clamp holds the largest area’s curve. Kidwell’s streamers are above it, so his drops test the clamp, and the evidence pulls two ways:
- The paper’s trend says the clamp reads high. Its
C_Dfalls as the area grows: the two end curves atAR = 10implyC_D ∝ S^−0.326, which extrapolated to Kidwell’s 0.1032 m² would giveC_D = 0.117where the clamp gives 0.130. The steeper inner pair,S^−0.53, would give 0.110. - The drop says it reads low. The drop itself implies 0.155, higher than any of them.
- What hpr does. It holds the end curve rather than extrapolating, because that is the closer of the two to the one free-drop measurement in hand. It says so here rather than claiming the trend.
- Two caveats. That rests on a single point, at one aspect ratio, with a fabric nothing like the paper’s cotton. And it holds partly because the correlation, measured with the leading edge clamped, is itself biased low, so two errors cancel.
- Nothing is measured anywhere near 0.225 m², the 1.5 m by 0.15 m streamer one of hpr’s tests flies on the clamp.
hpr therefore defaults to Filippone and keeps OpenRocket for comparing with OpenRocket
(ADR-013, streamer and tumble drag).
Tumble
A body with nothing deployed descends broadside, tumbling. DeviceDrag::tumbling(&assembly)
computes its drag area from the airframe by the OpenRocket technical documentation’s §3.5
(printed pages 53 to 55, eqs. 3.98 and 3.99):
C_D S = 1.42 A_f + 0.56 A_bt
A_btis the body’s side profile area, the outer diameter integrated along the axis,∫d dx. hpr takes a tube’s diameter times its length, and integrates each nose cone’s and transition’s own curve.A_fis, for each fin set, one fin’s planform area times an efficiency factor by fin count: 0.50, 1.00, 1.50, 1.41, 1.81, 1.73, 1.90, 1.85 for 1 to 8 fins (Table 3.4). It is a fit, not a model: four fins are 1.41 of one fin, not 2, and it is not monotonic. More than eight fins is refused.- Tube fins are refused: the model has no factor for them.
- A pod’s tubes and fins count as the airframe’s do, once per pod. The documentation’s model has no term for pods, nor for one part shading another from the air, so a rocket with pods tumbles at a drag area the fit was never checked on.
- The documentation notes 0.56 is half a circular cylinder’s 1.12 in crossflow (air flowing across
it, side-on), as expected of a cylinder falling at a random angle, and that 1.42 is “similar to
that of a flat plate 1.17 or an open hemispherical cup 1.42”. Those come from Hoerner’s
Fluid-Dynamic Drag (1965), which is copyrighted with no legal free copy; NASA TN D-540 and TR
R-474 carry the same numbers and are free (
docs/research/streamer-and-tumble-drag.md). - Until M1.11b hpr took each component’s end diameters, which under-counts a curved nose. For Valetudo’s tangent ogive that was 0.0111 m² against the true 0.0148 m², 25% low on the nose. Integrating the curve brought Valetudo’s tumbling speed from 36.77 to 36.38 m/s (ADR-086, ejection impulse and tumbling pieces).
How well it does. The documentation says its fit predicts its own five drop-test models within
3 to 14%. hpr does not reproduce that. Replaying Table 3.3 (printed page 54: five models, 22 m,
ρ = 1.31 kg/m³, the descent rate v₀ read from video to ±0.3 m/s) through hpr’s reading of the
model (the_tumble_model_against_its_own_drop_tests):
| model | fins | measured | hpr |
|---|---|---|---|
| #1 | 3 | 5.6 m/s | 5.27 (−5.8%) |
| #2 | 4 | 6.3 m/s | 5.96 (−5.4%) |
| #3 | 3 | 6.6 m/s | 6.13 (−7.2%) |
| #4 | none | 5.4 m/s | 6.43 (+19.0%) |
| #5 | fins only | 5.0 m/s | 4.50 (−10.0%) |
So the spread is −10 to +19%, and the finless tube is the outlier: it wants a body coefficient near 0.79 where the model prints 0.56.
The text pins neither the body-profile nor the fin-area convention (exactly which areas to measure). So either hpr reads the areas differently from whoever fitted the constants, or the claim is not reproducible. The table above is what hpr can demonstrate, so it is what hpr states.
Where it stops being true. The fit covers 44 to 103 mm bodies of 6.8 to 160 g descending at 5.0 to 6.6 m/s. A high-power booster is far outside it: Valetudo tumbling comes out at 36 m/s.
- A cylinder’s crossflow drag falls by roughly half above a Reynolds number near 2e5, its drag crisis. A 100 mm body reaches that at about 30 m/s, and Valetudo’s 80 mm at 36 m/s is right at it.
- So a real body that size would descend faster than hpr says.
- hpr does not model that fall, and nothing in the pinned sources covers it.
A piece on its own. DeviceDrag::tumbling(&assembly) covers the whole airframe, and
DeviceDrag::tumbling_stages(&assembly, (first, last)) a run of whole stages. For an ejected piece
that is part of a stage, Simulation::tumbling_piece(k) sums the same model over piece k’s own
body components and fins (Ejected pieces). A payload is refused: it has no
tube or fin of its own. A nose cone tumbling on its own is outside the fit, which was made on
whole rockets, so its speed is the model’s, not a measured one.
A tumbling body is a device like any other: give it a trigger, and it starts at that moment. hpr does not decide by itself when a rocket tumbles: nothing citable says when a stage becomes unstable enough (the same documentation declines to model the analogous streamer regime).
Triggers, lag and release
A device’s charge fires at its Trigger:
Apogee: when the center of mass is descending. hpr’s apogee event fires it the instant the height rate crosses zero.- A flight that starts in mid-air, already past its apogee, has no crossing to find, so the
charge fires at its first step. RocketPy’s own apogee trigger does the same: it fires whenever
the vertical velocity is negative (
parachute.py:368-376). - Such a flight starts from
Simulation::run_free, as the comparison with RocketPy below does, and as staging and flight-data replay will. - The charge fires once, at whichever of the two comes first.
- A flight that starts in mid-air, already past its apogee, has no crossing to find, so the
charge fires at its first step. RocketPy’s own apogee trigger does the same: it fires whenever
the vertical velocity is negative (
Altitude { height_above_ground_m }: the first time the center of mass is descending and at or below that height above the launch site. This is an altimeter’s main setting. A rocket whose apogee is already below the setting fires at apogee, because no crossing follows. That is RocketPy’s numeric trigger: vertical velocity negative and height below the setting (parachute.py:354-364). After the flight’s recorded apogee the rocket counts as descending even if its center of mass rises for a moment, as it can when a part is let go there (Released mass); without a release that is the same rule.Time { time_s }: a time after launch.MotorDelay { motor }: that motor’s ejection delay after its own burnout. The motor must have a delay in seconds; a plugged motor or one with no delay set is refused.Burnout { motor, delay_s }: a given delay after that motor’s burnout, whatever its ejection delay; a stage separation timed from the booster’s burnout, say (Staging). A motor that never lights fires neither this norMotorDelay.
Charges are only checked in free flight and during the descent, so a Time or MotorDelay
trigger whose time passes while the rocket is still on the pad or the rail fires at
rail exit.
lag_s seconds after the trigger the device deploys (line stretch, where the lines pull taut)
and starts to fill. The first deployment of a flight switches it to the descent phase.
A device can name another whose opening releases it (released_by), which is how a drogue is
cut away under a main:
- The release waits until the releasing device is fully open, not its line stretch. Cutting the drogue at line stretch would leave the rocket under an empty canopy: the drag area would collapse and the descent speed up (found in review; measured at 0.45 m² → 0.02 m² and 18.3 → 22.0 m/s before the fix).
- A released device contributes nothing from its release.
- One released before its own charge fires never deploys at all: its
Triggeris recorded and noDeploymentfollows.
Events, in the order a two-device flight records them: Apogee, Trigger(drogue),
Deployment(drogue), Trigger(main), Deployment(main), Release(drogue) (at the end of the
main’s filling), GroundHit.
Trigger times that are known before the flight (a time, or a motor delay) and every deployment and end of filling are stop times, so no integration step straddles a change in the drag area.
Inflation
A canopy doesn’t reach its full drag area at line stretch: it grows over a filling time. Each device that has deployed and is not released contributes
(C_D S)(t) = (C_D S)₀ · min(1, (t − t_d)/t_f)^j
with (C_D S)₀ its full drag area, t_d its deployment, t_f its filling time and j its growth
exponent. The open devices’ drag area at time t is the sum of these.
The exponent sets the shape of the growth:
j = 1is linear growth (Knacke’s ribbon and ringslot canopies);j = 2is the growth of solid cloth (Pflanz, Figure 5-51),(t/t_f)²: slow at first, fast at the end.
Inflation chooses t_f:
Instant(the default):t_f = 0, the full drag area at line stretch. This is RocketPy’s model. For a deployment well above the canopy’s terminal speed it gives hpr’s highest opening load; near terminal speed, as at apogee, a filling time can give a higher one, because the rocket speeds up while the canopy fills.FillingTime { time_s, exponent }: a filling time fixed in advance.FillConstant { constant, exponent }: Knacke’st_f = n D₀/v(printed page 5-43), withvthe airspeed at line stretch andnthe canopy fill constant, from Table 5-6’s unreefed column (printed page 5-44): 8 for a flat circular canopy. A deployment at rest has no filling time in this law (n D₀/vdiverges), so the canopy is taken as open at once and fills as the rocket picks up speed.
The fill constant at hobby speeds
Knacke states the linear form only “in the medium-velocity range of about 150 to 500 ft/s”
(45.7 to 152.4 m/s; Inflation::FILL_CONSTANT_RANGE_M_S). A hobby main is slower:
- A main opening at 20 to 30 m/s is below that range.
- There, his alternative for solid flat circular canopies is
t_f = n D₀/v^0.85withn = 4.0. It is a dimensional form (feet and ft/s) that cannot be used in SI as printed, so hpr does not. - So outside the range the filling time, and with it the peak load hpr reports, is an
extrapolation. At 25 m/s the linear form gives a 2.5 m main
t_f = 0.8 s, from a correlation fitted at three to six times that speed.
The opening load
hpr’s drag area rises to the steady value and stays there. Knacke writes the opening force as
F = (C_D S) q C_x X1 (printed page 5-50), with q the
dynamic pressure at line stretch. Its two factors fare
differently in hpr:
- The overshoot,
C_x, is left out. Knacke’s measured drag area overshoots the steady value by 10 to 80% near the end of filling (Figure 5-40, printed page 5-47). His infinite-mass opening-force coefficient isC_x = 1.7for a flat circular canopy; hpr’s is 1 in every mode. - The slowing,
X1, depends on the mode.X1allows for the rocket slowing while the canopy fills. It is 1 at infinite mass (a load too heavy to slow), and as low as 0.02 for a final-descent parachute with a low canopy loading (little weight for the canopy’s size).- With a filling time (
FillingTimeorFillConstant), hpr already includes the slowing: it integrates the rocket’s deceleration while the drag area grows, which is Pflanz’s method done step by step, without the overshoot. Don’t applyX1on top of hpr’s peak, or the slowing is counted twice and the load reads low. - Opening at once (
Instant, the default), there is no slowing: the peak is Knacke’s infinite-mass case withC_x = 1.
- With a filling time (
So the peak load hpr reports is no safe bound on the real one, in either direction:
- With a filling time, the missing overshoot alone can only raise the real peak, but the growth law and the filling time, extrapolated at hobby speeds, can move it either way.
- Opening at once, hpr applies the full drag area at line stretch: the infinite-mass case. A big main on a light rocket can therefore see far less than hpr’s instant peak.
- For scale, the 1.5 m flat circular canopy deployed at 60 m/s in
Verification peaks at 1.6 kN filling and 3.0 kN opening instantly. Knacke’s
infinite-mass
C_x = 1.7on the same dynamic pressure would be 5.1 kN.
Don’t size recovery hardware from hpr’s opening load. Use a dedicated opening-load method and
the hardware’s own ratings. Ludtke’s law and Pflanz’s X1 reduction factor are candidates for a
later milestone.
The descent
Once a device is open the rocket is a point mass: all of its mass at the center of mass, with no
rotation. In the launch frame, with m the mass, v_cg and
a_cg the center of mass’s velocity and acceleration, w the wind, ρ the density at its height,
g normal gravity, a_Coriolis the
Coriolis acceleration and T the thrust:
m a_cg = −½ ρ (C_D S)(t) |v_cg − w| (v_cg − w) + m (g + a_Coriolis) + T
- In code it is the free-flight equation. hpr uses the translational equation of
Rigid-body flight with the body’s rotation rate
ω = 0and the canopy drag in place of the airframe’s aerodynamics. Two things carry over:- The terms for a motor that is still burning: the center of mass moving inside the body as
propellant burns (
−m r″ − 2ṁ r′) and the exhaust jet’s terms. That page sums them, with the forces, intoT20. After burnout every one of them is zero. - The point the integrator follows is still the nose tip
O, not the center of mass. With no rotation and nothing burning the two move together, so the nose tip’s accelerationa_Oequalsa_cgand the equation is the one above.
- The terms for a motor that is still burning: the center of mass moving inside the body as
propellant burns (
- No moment. The drag acts at the center of mass along the air’s relative velocity, so it exerts no turning moment.
- The attitude freezes at deployment. The body rates are set to zero, and the nose tip keeps its
rigid offset from the center of mass,
r_cg(the center of mass’s position from the nose tip, in body axes). As the rates go, the nose tip’s velocity is shifted byω × r_cg(turned from body axes into the launch frame), so that the center of mass keeps the velocity it had. Whatever stops the rotation acts through the lines and the canopy, inside the system, so it can’t change the center of mass’s momentum. - The airframe’s own drag is left out, as RocketPy leaves it out. A rocket’s attitude under a canopy, and so the area it presents, is not modeled. For a drogue whose drag area is close to the airframe’s broadside area this is a real omission. It is the same omission RocketPy, the oracle this is compared with, makes. The tumble model (M1.7b, streamers and tumble) is where a body’s own drag belongs.
- The thrust
Tis kept, along the frozen axis, so a device that opens while a motor still burns (an off-nominal case) is not silently thrust-free. Its direction is wrong the moment the rocket would have swung under the canopy. - The rest is shared. Gravity, the Coriolis force, the atmosphere and the wind are the same models the rest of the flight uses (Rigid-body flight).
- Added mass is not modeled.
- Knacke gives no closed-form apparent mass (printed page 5-40 says only that it is the enclosed
volume times density times a form factor), and RocketPy’s
m_a = k_a ρ (2/3) π R² Hhas no citation in its code. - It carries no weight in RocketPy either, so it changes no equilibrium descent rate, only the transient right after an opening.
- It is not small: the fixture records 5.6 kg for Calisto’s main against a 16.2 kg rocket and 15.9 kg for NDRT’s against a 20.8 kg one (both at the start height; both grow with density as the rocket descends). The comparison below shows what leaving it out costs.
- Knacke gives no closed-form apparent mass (printed page 5-40 says only that it is the enclosed
volume times density times a form factor), and RocketPy’s
The equilibrium descent speed is Knacke’s (printed page 5-128), and recovery::terminal_speed_m_s
computes it:
v_e = √(2 m g / (ρ C_D S))
Separation
A Separation is a trigger and a stage boundary. At the trigger the stack comes apart into two
bodies:
- body 0 keeps the nose: the stages from stage 0, at the nose, through the one
after_stagenames; - body 1 is the stages aft of the split.
Each body flies on as a point mass under the devices that name it (Device::on_body). The ascent
ends there: its FlightResult has Termination::Separated, a Separation event, and one
BodyFlight per body in bodies. The exception is a powered separation, where body 0 still has a
motor to burn: it flies on as a sustainer, and only body 1 descends here
(Staging).
This section describes the unpowered case, and the booster’s descent after a powered one.
What happens at a separation:
- Each body is its own stages and their motors.
body_mass_propertiessums the stages’MassPropertiesand the motors mounted in them, so the bodies’ masses add to the whole rocket’s at that instant, which is a test. - Nothing pushes the bodies apart. Each body starts at its own center of mass, with the
velocity that point already had: the nose tip’s velocity plus the rotation’s share,
v_O + ω × r_cg, in the launch frame. So the bodies’ linear momenta add to the stack’s, which is a test. - Their spin is dropped. Each body’s center keeps moving round the stack’s as it was, but each body’s spin about its own center is lost, so angular momentum is not conserved. In the test, spinning at 0.6 rad/s, the lost spin is 31% of the angular momentum, and 0.02 J of energy.
- Only body 0’s devices act before the separation. A device meant for another body has a drag area computed for that body (a booster’s tumbling area, say), which is not a model of the whole stack, so it waits for its body. With no separation every device is body 0’s.
- Each body finds its own apogee, whatever its devices are triggered by. The ascent ends at the separation, so this is the only place a staged flight can record a peak, and without it a body separated while climbing would never fire an apogee charge (found in review, now a test).
- The bodies descend independently, each with the same point-mass equations as the descent
phase, less the thrust:
m a = −½ ρ (C_D S)(t) |v − w| (v − w) + m (g + a_Coriolis). They share the flight’s devices and their progress, so a canopy that opened before the separation stays open on whichever body carries it. - Bodies are not watched by the
Observer: their events and samples are in theirBodyFlight.
Limits:
- No push at a separation, and no tip-off. A separation’s charge or spring adds no impulse (an ejection’s can: see Ejected pieces), and the tumbling a tip-off would start is not modeled.
- A body must start above the ground, as a free flight must: the ground event is a falling crossing, so a body that started below the site would go on integrating underground until the flight’s time limit.
- Every body must carry a device, and it must open. The descent has no airframe drag (the descent-phase decision, ADR-012), so a body with nothing open would fall as if in a vacuum. A flight whose bodies are not all covered is refused when it is set up, and a body that reaches the ground without a single deployment (a timer set after that body lands, say) is a flight-time error rather than a landing at the speed of a fall with no drag (both found in review). A device on a body that nothing makes is refused when the flight starts, since an ejection given later could still make it. A device that opened on the stack before the separation counts as open.
- A body coasts with no drag at all until its first device opens, which is the same omission
as the descent phase’s and hurts more here: a 0.55 kg forward body that separates at 2 km and waits
for a 300 m main arrives at 168 m/s where an airframe would have held it near 60 to 70, so
its deployment speed, and any opening load taken from it, read high. Give a body a device that
opens at once (
DeviceDrag::tumbling_stagesover its own stages is the cited way) if the coast matters; after a powered separation hpr requires it (Staging). A spent booster’s device is usuallyDeviceDrag::tumbling_stages(&assembly, its stages), which is §3.5’s model over that body’s own components rather than the whole stack’s. - The aft body’s motors must have burned out, because a body’s mass is held constant through its descent. A trigger that fires while one burns is an error, before the flight when its time is known and in flight otherwise, not a silent approximation. A release across the separation is refused too: a line cuts a device on its own body. A forward body with a motor still to burn flies on as a sustainer (Staging).
Ejected pieces
An ejection lets a piece of the airframe go at a joint of your choosing, not only at a stage boundary: a nose cone pushed off its airframe, a body section, or a payload carried inside. Each piece then comes down on its own, under its own parachute or tumbling, and lands somewhere of its own. The charge can push the pieces apart. This is new with M1.11a and M1.11b, and is checked against exact answers only: no other simulator or real flight has been compared with it yet.
A few words first:
- A body component is one of the parts the airframe is made of, nose to tail: the nose cone,
a body tube, a transition. An internal component sits inside one: a parachute, an
altimeter bay, a mass. Each has an
idin the design file. - A joint is where two body components meet.
- A piece is a part of the airframe that can come away: the part between two joints that can part, or a payload.
- A body is whatever flies as one: the pieces still joined together.
How you give one. An Ejection is a trigger, the same kinds a parachute has (apogee, a
height, a time, a motor’s delay), and a place where the airframe parts:
Ejection::aft_of(trigger, "nose")parts the airframe at the joint just aft of the body component with that id. The side toward the nose keeps its body number. The new piece is the side aft of the joint, back to the next joint you have also given an ejection or a separation for, or to the tail. So ejecting a nose cone is written as parting the joint aft of it: the nose cone keeps body 0, and the airframe behind it becomes the new body.Ejection::payload(trigger, "payload")lets an internal component, and everything inside it, out of the piece that carries it. It must be inside the airframe, and not inside another payload.
Pieces tied together by a shock cord fly as one, so a nose cone on a shock cord is not an ejection at all: leave it out, and the rocket comes down in one piece.
Which body is which. A body takes the number of the piece nearest the nose among those still joined, and a payload flying alone takes its own:
| What makes the piece | Its body number |
|---|---|
| The nose’s side of the airframe | 0 |
| A separation, if the flight has one: the stages aft of it | 1 |
| Each ejection, in the order you give them | 1, 2, … in order; after 1 if the flight has a separation (2, 3, …) |
Each body needs a device that names it (Device::on_body). A flight whose bodies are not all
covered is refused when you give the ejections. A device on a body that nothing makes is refused
when the flight starts, since a separation given later could still make it: run returns an
error naming that body.
When devices act. Only body 0’s devices act before the airframe first comes apart. After that, a device acts only once its own body flies on its own. So a parachute on the nose cone (body 0) triggered at apogee fires and opens in the same step as the nose cone leaves. It is logged in the flight’s own events, as the example below shows, and its drag acts on the nose cone alone from then on. A device on a piece that hasn’t left yet waits for it, even if its trigger has come. Give a piece a trigger that follows its ejection. A drogue on the airframe, with the nose cone only ejected at 300 m, would leave the whole rocket falling from apogee to 300 m with nothing open.
What happens at each parting:
- When the airframe first comes apart, each body starts at its own center of mass with the
velocity that point had,
v_O + ω × r_cg(defined under Separation), as at a separation, plus the charge’s push below. The linear momenta add to the stack’s, which is a test. - When a body parts again on the way down, it is already a point mass with no attitude, so
there is nothing to place its pieces by. Both pieces start at the body’s position and velocity,
plus the push, and its mass steps down by the piece that left. Their true starting points could
be up to a rocket’s length apart, which is small against a descent of hundreds of meters. That
body’s event for the parting records its state just before (
BodyEvent::sample) and just after (BodyEvent::after): its lower mass, and its pushed velocity. - Each piece’s mass is its own components’. A stage that stays in one piece is counted by its own mass, overrides and all. A stage that parts is counted component by component. An override that doesn’t say how its mass divides is refused rather than guessed. That means a stage’s mass override, or a component’s override that covers what it holds. OpenRocket designs often override a stage’s mass, and such a stage can’t be parted. The pieces’ masses add to the rocket’s to 1e-12, which is a test.
- Every motor must have burned out by the time an ejection fires, because every body is a point mass of constant mass from then on. An ejection known to come earlier is refused when it is given, and one that fires early in flight is an error then.
The push of the charge. An ejection charge or spring pushes the two sides of the joint apart.
Give it as an impulse J: the push summed over its short duration, in newton-seconds, as a
motor’s total impulse is. For example,
Ejection::aft_of(Trigger::Apogee, "nose").with_impulse(1.0). Without one there is no push. hpr
doesn’t work J out from a charge’s size: as a rough guide, J ≈ m v for the speed v you
expect a piece of mass m to leave at, so 1 N·s puts 15.9 m/s on the example’s 63 g nose cone.
The push is equal and opposite:
-
The side forward of the joint gets
+Jtoward the nose, and the side aft of it−J. A payload leaves forward, out of its host, as it does when the nose cone comes off first. -
Each side’s velocity changes by
J/m, for its own massm, so the light side moves most. One newton-second is 4 m/s on a 250 g payload, and 1.8 m/s on the 556 g airframe it leaves. -
The momenta still add up exactly, which is a test.
-
Which way is “toward the nose”? While the airframe flies whole with nothing open, it is its axis at that instant. A device that opens at the same instant as the parting doesn’t count as open yet. Otherwise the body has no attitude to go by: a body flying on its own is a point mass, and a whole airframe that has hung from a device since earlier has only the attitude it had when the device opened. So hpr goes by the body’s velocity through the air:
- A body hanging from a device (any but a tumble) that was open just before that instant, deployed before it and not yet released, points its forward end against that velocity (upward, as it falls), toward the device. hpr assumes the device left through that end, as a main does once the nose cone is off: a payload let out under the airframe’s parachute is pushed up, toward it. For a drogue that left between the booster and the avionics bay, the airframe more likely hangs near level, and this direction is a guess.
- A body with nothing open, or only tumbling, points its nose along that velocity, as a stable rocket does.
- A body moving through the air at under 1 mm/s, at its own apogee in still air say, points up.
These are assumptions, not measurements (ADR-086). All the partings that fire on a body at one instant part it together, each final body taking the pushes of the joints on its sides, so the order you list them in doesn’t matter.
-
A separation has no push. A payload always leaves forward, so a push on a payload in the nose’s own section, which is closed at the nose, is refused when the flight starts. A pushed payload whose section’s forward joint hasn’t parted by the time it leaves is an error in flight.
In the example below, where every piece’s device opens as it leaves, 1 N·s at each parting moves the airframe’s landing by 0.0 m and the payload’s by 1.3 m: the drag takes the push away within seconds. A piece that coasts with nothing open keeps its push longer.
A piece that tumbles. A piece needs some drag of its own, or it falls as if in a vacuum. hpr
can treat a nose cone with no parachute as tumbling; a real one may instead fall point first, and
faster. Simulation::tumbling_piece(0) gives the drag area of the nose cone tumbling on its own,
by the tumble model over just its own parts. Use it as that body’s device,
Device::new("nose cone tumble", tumble, Trigger::Apogee).on_body(0), triggered with its
ejection, as the example below does.
- Piece
kis the piece nearest the nose in bodyk, numbered as in the table above. - It is that piece’s own area. A body that still carries another section, until that section’s own parting, tumbles with its first piece’s area only.
- The pieces of an airframe cut into sections have drag areas that add up to the whole airframe’s, which is a test.
- The model was fitted on whole model rockets, so for a lone nose cone the speed is the model’s answer, not a measured one.
A worked example. The example
ejected_pieces.rs
flies the 54 mm test rocket on an I175 with a 250 g payload in its airframe, in a 4 m/s wind from
the west. The nose cone leaves at apogee (16.23 s, 1,747 m) under a 0.45 m canopy. The airframe
comes down under a 0.9 m one, and lets the payload out at 300 m under a 0.6 m one. Each lands at
its own terminal speed, v_e above, for its own mass and canopy in the air at the ground:
| Body | Mass (kg) | Flies on its own from (s) | Lands (s) | At (m/s) | v_e (m/s) | East of the pad (m) |
|---|---|---|---|---|---|---|
| 0, the nose cone | 0.063 | 16.23 | 562.88 | 3.06 | 3.06 | 2,035.4 |
| 1, the airframe | 0.556 | 16.23 | 333.54 | 4.54 | 4.54 | 1,115.6 |
| 2, the payload | 0.250 | 268.05 | 333.14 | 4.57 | 4.57 | 1,114.0 |
The nose cone is light for its canopy, so it stays up 229 s longer and drifts 920 m further
downwind. The airframe’s body weighs 0.806 kg until the payload leaves, then 0.556 kg. The
flight’s own event list ends at the first parting. What happens to each body after that,
its canopy opening and the payload leaving the airframe, is in that body’s own list
(FlightResult::bodies).
The example then flies the same rocket again with a 1 N·s push at each parting, and with no canopy on the nose cone, which tumbles:
| Body | Mass (kg) | Lands (s) | At (m/s) | v_e (m/s) | East of the pad (m) | Moved from the first flight (m) |
|---|---|---|---|---|---|---|
| 0, the nose cone, tumbling | 0.063 | 130.93 | 14.82 | 14.82 | 269.6 | 1,765.7 |
| 1, the airframe | 0.556 | 333.42 | 4.54 | 4.54 | 1,115.6 | 0.0 |
| 2, the payload | 0.250 | 333.33 | 4.57 | 4.57 | 1,115.3 | 1.3 |
Tumbling, the nose cone comes down nearly five times as fast as under its canopy, and lands 1.8 km
nearer the pad. The push barely moves the other two. At 300 m it changes the payload’s velocity by
4.00 m/s, up toward the airframe’s parachute, and the airframe’s by 1.80 m/s, down: J/m for
each (the example prints the upward parts, +4.00 and −1.80 m/s). Within seconds the drag has
taken that away.
Limits:
- A piece coasts with no drag until its device opens, as every separated body does. Give it a parachute, or its tumble from the moment it leaves.
- The push’s direction is assumed whenever the airframe isn’t flying whole with nothing open: it goes by the velocity through the air, as above. A payload always leaves forward.
- No powered separation with ejections. A sustainer flies on a design cut at its stage boundary, whose components aren’t the pieces the ejections were given for, so it is refused, and so is an ejection ahead of a separation that would light a motor.
- A piece’s spin and attitude are not tracked, as for a separated body.
- A
.orkfile’s recovery settings don’t make ejections, and hpr has no names for pieces: you find the component ids in the design file.
The decision records on ejected pieces, ADR-085, and on the push and tumbling pieces, ADR-086, have the reasoning.
From an OpenRocket file
A .ork file’s parachutes and streamers fly as OpenRocket flies them, and land at OpenRocket’s
speed to within 0.02% on 50 of its 53 example flights, and within 0.12% on all 53. The function
hpr::ork::recovery turns each parachute and streamer of the
configuration flown into one of the devices above, and hpr sim flies a
.ork file’s devices through it. The model is OpenRocket’s, not Knacke’s, as the file’s author
chose it. This is a check against another program only: no descent from a .ork has been
compared with a real flight. The decision record on flying a .ork’s recovery,
ADR-153, has the reasoning and the targets.
How each device flies:
- Drag. A parachute’s drag area is
C_D · π D²/4, withDthe canopy diameter andC_Dthe file’s value, or 0.8 when the file saysauto. That default comes from OpenRocket’s technical documentation (v13.05, section 4.2.5), which cites Hoerner’s Fluid-Dynamic Drag (1965). A streamer’s is the statedC_Don its length times its width, or forautoOpenRocket’s own estimate (which streamer model is right). - Opening. Each device opens fully at once, the file’s delay after its event, with no filling time.
- Events.
apogeeopens at apogee.altitudeopens the first time, on the way down, that the center of mass is at or below the stated height above the ground.ejectionopens at the ejection delay of the first motor that lights in the device’s own stage.launchopens the delay after launch. - The descent. Once a device opens, the rocket is a point mass under the open devices’ drag alone, the airframe’s left out (The descent). That is OpenRocket’s own model: “the entire drag coefficient of the rocket is assumed to come from the deployed recovery devices” (section 4.2.5).
A worked example. A parachute 0.6 m across, with C_D set to auto, has a canopy of
π × 0.6²/4 = 0.283 m², and a drag area of 0.8 × 0.283 = 0.226 m². hpr sim prints it as
recovery: Main at apogee, 0.226 m² of drag area, then when it opened.
Some devices never open, and some are refused:
- No device. A device set to
never, or toejectionon a plugged motor or one with no delay given, gives no device.hpr simnotes “Mainnever opens”, with why. - No motor lights in its stage. A device set to
ejectionopens at its own stage’s charge (OpenRocket labels the setting “First ejection charge of this stage”). In a stage where no motor lights, it never opens, as in OpenRocket 24.12: probed, a charge from another stage never opens it, whether the stages are joined or apart, and neither does a stage with no motor, a motor that never lights, or a plugged one.hpr simnotes “Mainnever opens: it opens at its stage’s ejection charge, and no motor of its stage lights”, and flies the other devices. Until M4.5i, hpr refused the configuration’s whole recovery for it (ADR-168). - Refused by name. A device inside a part hpr doesn’t read, and one set to open by a word hpr
doesn’t know. One set
to open at
lowerstageseparationopens at the split right behind its stage when that split comes with nothing left to burn (Staging), and is refused otherwise. The flight then falls on its airframe alone, with the reason in its notes. - A height above the apogee. hpr opens such a device at apogee, as an altimeter below its
main’s height at apogee would fire. OpenRocket’s one probed run never opened it
(what the words mean).
hpr simnotes when this happens. - The height’s zero. hpr counts a height from the ground. OpenRocket counts it from where the center of mass starts on the pad, so hpr opens a device set to a height that much lower.
How close it is. cargo xtask ork-flights flies each configuration of OpenRocket’s examples
that hpr flies a second time, with its devices, and compares it with OpenRocket 24.12’s own flight
(the report’s descents). The targets were set before measuring: each deployment
within 0.1 s or 2% of OpenRocket’s time, whichever is larger, and the landing speed and the flight
time within 5%. All 53 descents are compared. On the 7 configurations that drop stages under
power, it is the sustainer’s descent, and on the 6 that drop a payload with nothing left to burn,
the payload’s: in each, the branch OpenRocket’s record keeps
(Separation).
- 48 flights: the report’s 51 with no named cause for an apogee gap, less the Base drag hack‘s three (below). The landing speed is +0.00% to +0.02% off OpenRocket’s, and the flight time −1.57% to +3.16% (0.68% on average, either way). Among them are two of Pods–powered with recovery deployment: on its booster’s flight, the two pods’ parachutes open at launch + 5.35 s in both programs, and the flight time is −1.02% off.
- 40 of those 48 meet every target as written. The other 8 miss only on a deployment’s time. All 8 reach an apogee 0.63% to 4.34% below OpenRocket’s, a climb gap the same report shows. And OpenRocket opens a device set to a height at the end of its time step: 0.06 to 12.64 m below that height over the 15 such deployments, 0.36 to 11.15 m on the 8 that miss. With those two taken out, every deployment is within its target; the largest left is 0.33 s. That is what “met once sized” means in the report: each deployment that misses is held instead by what is left of it. For a device opened at apogee this holds by construction, as it takes out the apogee’s own time; the evidence on the descent itself is the landing speed, and on the dual-deploy flights the drogue’s speed as the main opens, within 0.07% on 5 of 6.
- The Base drag hack’s three flights have no named cause either, but read high in apogee (a part set to no drag). They land 0.11% to 0.12% faster than OpenRocket, and their flight times are +1.84% to +7.23% off; the last misses its target.
- 2 flights with a named cause for an apogee gap. One is put down to hpr’s tube-fin drag, and
in the other,
[C6-7; B6-0]of Pods–powered with recovery deployment, the sustainer turns over before apogee in both programs. Both land at OpenRocket’s speed (+0.00%), but their flight times are −52.64% and +5.78% off. - The private designs. On 34 descents, published only as differences (report), the landing speed is −1.53% to +0.04% off OpenRocket’s and the flight time −4.98% to +3.72%. 31 of 34 meet every target as written, and all 34 once each deployment that misses is held by what is left of it; that sizing rests on numbers the private report does not publish, and the −1.53% landing speed is not explained.
A stage that drops away under power. Each device rides
the part its stage is in (hpr::ork::separated_recovery),
and each event is that part’s own: apogee is the part’s own apogee. After the split each part
is a point with no airframe drag (Separation), so a part needs a device from the
split. The booster tumbles from the split until its first own device in file order
opens, which releases the tumble (hpr::ork::tumbling). A
sustainer with no device of its own tumbles from its apogee. hpr sim flies it this way
(Separation). On the Two stage high power rocket’s two configurations,
the sustainer’s landing speed is +0.00% off OpenRocket’s and its flight time +0.32% and +0.73%.
On the first, the H148R-0 and H148R-0 configuration, the main opens 1.09 s before OpenRocket’s,
past its target. Its apogee is 1.79% lower and OpenRocket opens it 8.9 m below its set height;
with both taken out, as How close it is above does, 0.065 s is left
(ADR-159, powered separation in hpr sim).
What it leaves out. The booster’s own descent is not validated: OpenRocket flies it as a rigid
body, and its record keeps only the sustainer’s branch. A tumbling booster falls side-on from the
split, where a real one flies nose-first for a while, so its peak and landing are likely too low
and too close to the pad (#179). hpr sim
flies a configuration whose separation can only come after apogee as one stack, with a note.
Several separations under power fly, one after another
(M4.5g2, several separations). A payload dropped with
nothing left to burn flies when its own device opens at the split
(M4.5g3, a payload’s split).
Verification
crates/hpr-sim/src/recovery.rs’s tests, all analytic (checked against exact answers) unless they
name the oracle:
| What | Result |
|---|---|
Knacke’s v_e against a case from Loft, the project before hpr-sim (1.1 kg, 1 m flat canopy, C_D 0.8, ρ 1.225) | 5.294 m/s, as Loft printed |
| A descent from rest against the closed-form fall under quadratic drag (2 km, uniform air, constant gravity) | 2.1e-8 of the terminal speed v_t over the whole descent; the landing time within 1e-5 s of the closed form’s 204 s |
| Drift in a steady wind, entered drifting with the air, no Earth rotation | exactly the wind times the time of flight (1e-8); the fall itself within 1e-4 of the closed form |
| The Coriolis drift of a 3 km descent, Earth rotation on | 0.3666 m east against the steady prediction 2Ω cos φ · v_t²/g · T = 0.3685 m, 0.52% apart, and 29 µm north |
Knacke’s filling law, t_f = n D₀/v and (t/t_f)^j | the recorded drag area to 1e-9 of (C_D S)₀ |
| Inflation against instant opening (deployed at 60 m/s under a 1.5 m flat circular canopy) | peak load 1,615 N against 3,020 N instant (0.53 of it), between the closed-form 1,527 N without gravity and 1,670 N with it |
| An oversized canopy (5 m) opening at 100 m/s, 10 km of descent at 2.95 m/s | lands in 3,392 s in 6,914 accepted steps and 2 rejected (a mean step of 0.49 s, where Loft’s explicit RK4 needed a 2e-4 s floor) |
| A deployment with a 0.7 rad/s body rate, and another inside the burn in a crosswind | the center of mass keeps its velocity across the handover to 1e-12, and the nose tip’s moves by exactly ω × r_cg, turned into the launch frame |
| A whole flight: drogue at apogee with a lag, main at 300 m, drogue released | events in order; each stage settles within 2% of its own v_e |
| Two devices triggered at the same instant | both open in the same pass, and the descent settles at the v_e of the sum of their drag areas |
| A device released before its own charge fires | it is recorded as triggered and never deploys; the descent stays at the open device’s v_e |
| A drogue released by a main that fills over 2 s | the release waits for the end of filling, the drag area never falls below the drogue’s, and the descent never speeds up |
| An apogee charge on a flight that starts descending | it fires at the first step (there is no apogee event to find), and a climbing start still waits for the apogee |
| Two user events and an altitude device on one flight | the user events keep their numbers and fire during the descent, in height order |
| The same recovered flight flown twice | bit-identical rows, events, final sample and step counts (Loft lesson L24: a run does not mutate the simulation) |
| A separation at apogee of the two-stage test design, canopy on the sustainer and tumble on the booster | both bodies land: the 0.550 kg sustainer at 729.0 s and 2.11 m/s under its 1.8 m canopy, the 1.125 kg booster at 107.5 s and 16.74 m/s tumbling; the masses add to the 1.675 kg stack to 1e-12 and each lands within 0.1% of its own v_e |
| The linear momenta of the bodies at a separation with a 0.6 rad/s body rate | add to the stack’s to 1e-9, and each body starts at its own center of mass to 1e-12 (0.817 m apart on this design) |
| A separation while the booster’s motor burns | refused in flight, with the booster’s burnout time in the error |
| A separation while still climbing at 100 m/s | both bodies find their own apogee above 1,400 m, fire there, and land within 1% of their own v_e |
| A timed separation, and a height separation | fire at their own time to 1e-9 s and at their own height to 1e-6 m, rather than at the next boundary that happens to exist (found in review: one fired 186 s late, another never) |
| A body that runs out of time | says TimeCap in its own BodyFlight; FlightResult::bodies_landed is false and landings() is short |
| A body whose device never fires (a timer set after it lands) | refused in flight, naming the body, rather than landed at the speed of a fall with no drag |
| A body whose canopy opened on the stack just before the separation (an apogee separation with an apogee parachute) | lands: the open canopy counts, though its deployment is in the flight’s events, not the body’s (found with M1.9a) |
| A timed separation known to precede the booster’s burnout | refused when the separation is given; a height one that a climbing rocket passes early is refused in flight |
The ejected pieces’ tests are in crates/hpr-sim/src/pieces.rs, also analytic:
| What | Result |
|---|---|
| The nose cone at apogee and a 250 g payload at 300 m, from the pad in a 4 m/s wind, in uniform sea-level air (not the example’s atmosphere, so its numbers differ) | all three land, each under its own canopy; the payload leaves at 300 m to 1e-6 m; landings pinned (the nose cone 911.6 m further downwind than the airframe, 4 m/s times its 227.5 s longer descent to 1%) |
| The masses at each parting, with wind and a 0.6 rad/s body rate | the two bodies at the first parting, and the three pieces, add to the rocket’s mass to 1e-12; the nose cone and the payload are their own components’ masses to 1e-12 |
| The linear momenta at each parting | at the first, from the rigid stack, they add to the stack’s to 1e-9, and the nose cone starts at its own center of mass to 1e-12; at the second, on the way down, they add up by construction (one shared velocity), so only the masses test anything there |
| Each piece’s landing in uniform air | within 0.1% of its own v_e, the airframe’s at its mass after the payload left |
| An ejection with a separation (the two-stage test design, both at apogee) | the booster is body 1 and the airframe body 2; the booster’s mass is its stage’s and motor’s, and the three add to the stack’s, to 1e-12 |
| A payload whose trigger comes after the landing | never leaves: its airframe lands with both pieces, and the two bodies add to the rocket’s mass |
| Two partings firing in one pass on the way down (the two-stage design, three joints), given in either order | each piece counted once: the four bodies add to the stack’s mass to 1e-12, and the nose cone and the interstage are their own components (found in review: the interstage was counted twice, 1.7651 kg landed from 1.6752 kg) |
| An airframe whose own mass is overridden, but not what it holds | still divides: the payload is its own mass, and the pieces add to the rocket’s, to 1e-12 |
| A powered separation with ejections, an ejection ahead of a separation that lights a motor, an ejection timed from a motor with no ignition | each refused with its own reason |
| Partings the design can’t make: an unknown id, a joint aft of an internal part or of the tail, a body component or an external part as a payload, a payload inside another, two at one joint, one payload twice, an overridden stage or covering override | each refused with its own reason, naming the component |
| An ejection during the burn, a height below the ground, a body without a device, a device on a body nothing makes | refused, the last when the flight starts |
| A 1 N·s push at both partings, from a stack tilted 60° up toward 30° east of north, in wind, moving sideways and turning at 0.6 rad/s | each body’s velocity changes by J/m to 1e-9 against the same flight unpushed. At the first parting that is along the hand-computed rail axis, 15.9 m/s on the nose cone. At the second, the airframe under its canopy, it is 4 m/s on the 250 g payload, against its velocity through the air. The momenta add up to 1e-9 at both, and every piece still lands within 0.1% of its v_e |
| The same push at 300 m with the airframe’s canopy not yet open | the payload is pushed down its velocity through the air, 4 m/s, and the airframe the other way, to 1e-9 |
| A nose cone pushed off at 300 m from a stack that has hung from a drogue since apogee | pushed against its velocity through the air to 1e-9, not along the attitude frozen at apogee (found in review: it went along the frozen axis) |
| A push at a body’s own apogee, climbing straight up in still air with nothing open | straight up, 4 m/s on the payload, to 1e-9, where along its flight would point down |
| A separation and a pushed nose cone at apogee (the two-stage design) | the booster, the separation’s body, gets no push; the nose cone and the sustainer’s airframe get ±J/m along the axis to 1e-9; the momenta add up to 1e-9 |
| Two pushed partings at one instant on the way down (the two-stage design), given in either order | by hand, the nose cone +J/m, the airframe between the joints 0, the interstage −J/m, each to 1e-9; the momenta add up to 1e-9, and the landings agree between the orders to 1e-6 m (found in review: taken one at a time, the order moved the nose cone’s push from 15.85 to 17.66 m/s) |
| The airframe’s canopy opening at the same instant as the payload leaves | not yet hung from: the payload is pushed down its flight, 4 m/s, whether the canopy opens at once or fills over a second (found in review: the push flipped with the inflation law) |
| A pushed payload let out at apogee while its section’s forward joint waits for 300 m | an error in flight, at the ejection’s time |
| A stack with only a tumble since apogee, the nose cone pushed off at 300 m | along its velocity through the air, to 1e-9: a tumble is nothing to hang from |
| A drogue since apogee released by a main that opens at 300 m as the nose cone leaves | still hung from: against the velocity through the air, to 1e-9, whether the main opens at once or fills over a second (found in review: the push flipped with the law) |
| One charge pushing off the nose cone and letting the payload out, both at apogee | by hand, along the axis: nose cone +1 N·s, payload +1 N·s, airframe between them −2 N·s, each over its own mass to 1e-9 |
| A pushed payload in the booster, behind a separation, with the builders in either order | accepted both ways, and every piece lands; without the separation it is in the nose’s piece and refused at the start, and so is one in the sustainer’s airframe with it |
| A drogue since apogee cut away at 600 m by a tumble, the nose cone pushed off at 300 m | hung from nothing by then: along the velocity through the air, to 1e-9 |
| A parting on the way down with no push | the body after it has the same point and velocity, and its mass without the piece; only partings record a body after |
| A nose cone tumbling on its own after apogee, in uniform sea-level air | its drag area is 0.56 times its tangent ogive’s closed-form side area, to 1e-12; it lands at 13.849 m/s, its model’s v_e to 1e-6 (the example’s 14.82 m/s is in its own, thinner air at 1,400 m) |
| The tumbling drag areas of an airframe cut into a nose cone and the rest | add to the whole airframe’s to 1e-12; a payload or a piece not made is refused |
| A push that is negative, NaN or infinite | refused when the ejections are given; one on a payload in the nose’s own section when the flight starts |
Against RocketPy
hpr’s descent is compared with RocketPy’s for five of RocketPy’s example rockets, a code-to-code comparison. Every compared number agrees within 3%, and most within 0.1% (22 of the 30 in the validation report). The largest gaps, in order:
- NDRT 2020’s north drift: +2.86%. hpr carries it 50.8 m south, 2.86% further than RocketPy does. NDRT’s main has a drag area of 16 m², and RocketPy’s added mass, which hpr leaves out, is the likely cause; no test has isolated it yet.
- Valetudo’s north drift: −1.77%. That drift is 19 µm, from the Earth’s rotation alone, so a tiny difference is a large fraction (its own section below).
- Valetudo’s whole drift: −0.89%, of 0.19 m, also from the Earth’s rotation alone.
- NDRT 2020’s descent time: +0.71% longer in hpr, likely the same added mass.
How the comparison is run:
validation/oracles/rocketpy/recovery.pyflies RocketPy’s own parachute phase for the five rockets and writes what it computes tovalidation/fixtures/recovery/rocketpy-descent.json. The testdescent_matches_rocketpy_examplesreplays each case in hpr.- Both codes start from the same declared state after burnout, near apogee. The first device opens at once: its lag is overridden to zero, so no ballistic stretch, flown under each code’s own rocket aerodynamics, comes between them.
- Both get the same
C_D S, the same deployment settings and the same wind. RocketPy’s random noise on each parachute is set to zero, and hpr flies RocketPy’s formula for gravity (see Gravity below). - The test checks that the two environments agree first, then compares the descents.
- The oracle runs at
rtol = atol = 1e-8: its relative and absolute tolerances, how much error each step may make, both 1e-8 (scientific notation for 0.00000001). Run again at 1e-6, it moves every compared metric by at most 3.5e-6 relative (the fixture’ssolver.relative_change_from_loose), far below the gaps. The one larger entry, 2.1e-3, is Valetudo’s north drift of 19 µm, which has its own section below.
The results. Measured (hpr against RocketPy, 2026-09-17):
| case | descent time | descent rate under the drogue | impact descent rate | drift | worst drift component |
|---|---|---|---|---|---|
| Calisto (drogue 1.0 m², main 10 m² at 800 m, wind 5 E / 2 N) | +0.08% (257.27 s) | −0.01% (17.967 m/s) | −0.03% (5.454 m/s) | +0.08% (1,386.0 m) | +0.08% |
| Valetudo (drogue 0.4537 m², no wind) | −0.02% (45.76 s) | n/a | +0.00% (17.627 m/s) | −0.89% (0.19 m, Coriolis only) | −1.77% (north, 19 µm; +2704% under hpr’s own gravity, issue #27) |
| NDRT 2020 (drogue 0.438 m², main 16.05 m² at 167.6 m, sheared wind) | +0.71% (61.60 s) | +0.01% (28.156 m/s) | +0.01% (4.604 m/s) | +0.28% (327.9 m) | +2.86% (north, −50.8 m) |
| Prometheus 2022 (drogue 0.467 m², main 5.78 m² at 457.2 m) | +0.08% (153.50 s) | −0.01% (26.400 m/s) | −0.03% (7.323 m/s) | +0.08% (1,237.1 m) | +0.09% |
| Juno III (drogue 0.885 m²) | −0.02% (53.56 s) | n/a | −0.01% (22.431 m/s) | −0.02% (457.9 m) | −0.02% |
- Every metric is inside the 3% that the parachute milestone (M1.7a) set. The descent rate under the drogue, where a case has a main, agrees to 0.01%.
- The later devices’ trigger heights agree to −0.01%, −0.17% and −0.01%. RocketPy’s trigger sampling (below) accounts for them.
- Both simulators land within 1% of Knacke’s
v_efor the device that is open, computed from hpr’s own air and gravity at the site. - These numbers use RocketPy’s gravity and wind interpolation, not hpr’s defaults.
- Gravity: the comparison has flown RocketPy’s gravity model since issue #27. Under hpr’s own gravity the drifting cases read a little closer (Calisto +0.06% rather than +0.08%, measured once when the comparison switched and not pinned by a test), because the vertical’s turn downrange pushes the rocket back toward the pad and cancels part of a real difference. The like-for-like number is the honest one.
- Wind: here both codes interpolate the wind by its east and north components, as RocketPy does. For a table of wind levels, hpr’s default is to interpolate speed and direction instead (Wind). Only NDRT 2020’s wind changes with height; the other four cases have one wind at every height, where the two ways agree. What hpr’s default would do to NDRT’s drift has not been measured.
What still differs between the two codes:
- Added mass. hpr has none. RocketPy’s carries no weight, so it changes no steady descent rate,
only the response just after an opening. It is most likely the largest difference.
- RocketPy’s added mass for NDRT’s main is 15.9 kg, against the rocket’s 20.8 kg, so its response to the opening is slower.
- That most likely lengthens the descent (+0.71%) and, in a wind that shears with height, moves the smaller drift component by 2.86%.
- A cited apparent-mass model would show whether it closes that gap.
- When a trigger fires. RocketPy checks its triggers on a grid of
1/sampling_rate(100 or 105 Hz), anchored att = 0, and only over the span after its first accepted step. hpr has no sampling rate: its event finder locates the crossing.- So RocketPy’s first deployment is 2.5 ms late in the four 105 Hz cases, and 13 ms late in Prometheus’s.
- Its test, height below the setting (
h < setting), can fire only at or below the setting, by at most one sample of fall, the descent speed over the sampling rate (v_z/rate): 0.17 m for Calisto and 0.27 m for NDRT (about 0.01 s of descent). - The heights the fixture records at those triggers (800.07 m, 167.93 m, 457.26 m) come from the spline RocketPy fits through its stored samples for reporting, not from the continuous solution between steps that its trigger read, so they sit just above the setting instead.
- The table compares hpr’s trigger heights with those reported values, the closest the fixture can come. The difference is the same size either way.
- Release against replacement. hpr sums its open devices, and releases the drogue when the main
is full; RocketPy holds one
C_D Sand replaces it. For these cases, whose canopies open instantly, the two are the same. - Wind: no difference. Both codes interpolate the declared wind by its east and north components, and the test holds hpr’s to RocketPy’s samples within 1e-9 m/s in each, NDRT’s sheared profile included.
- Atmosphere. hpr evaluates the 1976 standard atmosphere; RocketPy interpolates a 100-point pressure table over 0 to 80 km. Over the fixture’s 23 samples they differ by at most 3.7e-4 in density, which the test gates at 5e-4.
- Gravity: the same size, a different direction. RocketPy’s “Somigliana” formula is
WGS 84 normal gravity, and hpr’s agrees
with the fixture’s samples to 1e-8 (the worst of 23 is 4.7e-9 relative). The two point it
differently, and for a long time this page compared only the size.
- RocketPy applies gravity to the vertical axis alone (
Flight.u_dot_parachute,flight.py:2777, where onlyazcarries a gravity term). - hpr’s default,
GravityModel::Ellipsoidal, uses the full normal-gravity vector. Above the ellipsoid it tilts slightly toward the equator (Gravity), in proportion to height above the ellipsoid: 4.0e-6 m/s² sideways at Valetudo’s site at ground level, and 8.7e-6 m/s² at 1,468 m, where Valetudo’s descent starts. - Over the fixture’s 23 gravity samples the tilt runs from +6.9e-6 m/s² (north) at Valetudo’s top sample, 1,168 m, to −3.3e-5 m/s² (south) at Calisto’s 4,400 m.
- hpr’s vector also turns with the local vertical downrange, by
g·d/R, withdthe distance drifted andRthe Earth’s radius: 2.1e-3 m/s² at Calisto’s 1.4 km of drift. Wherever a rocket drifts at all, that is much the larger of the two. - The parachute milestone’s test (M1.7a) used to compare
gravity by its size alone, so it could see neither. This comparison and the validation suite
now both fly
GravityModel::VerticalTaylor, which hpr ships as RocketPy’s own formula for like-for-like comparisons, and both check the gravity vector, not its length.
- RocketPy applies gravity to the vertical axis alone (
- Geometry. hpr flies over the curved ellipsoid and measures heights along its perpendicular
(ellipsoidal height); RocketPy’s height
zis measured in a flat frame. Over Calisto’s 1.4 km of drift the curvature is 0.15 m of height, 0.03 s of descent.
Valetudo’s north drift
This tiny number is worth its own section, because it is the one that found the gravity difference above.
- What to expect. In still air, Valetudo’s north drift comes from the
Coriolis acceleration alone. The falling rocket picks up a
small eastward velocity
v_eastfrom it, and the same acceleration acting on that eastward motion pushes it slightly north (Valetudo’s site is in the southern hemisphere). The horizontal velocity relaxes to a drag balance in aboutv_t/g≈ 1.8 s, withv_tthe terminal speed, sov_north ≈ −2 ω_z v_east · v_t/g, withω_zthe vertical part of the Earth’s rotation. That integrates to 2.0e-5 m over the descent. RocketPy gives 1.9653e-5 m. - What hpr gave at first. The parachute milestone (M1.7a)
didn’t compare this component; the validation harness
(M2.1a) does. When it first did, hpr read 28 times
RocketPy’s: 5.51e-4 m (0.55 mm), under hpr’s default gravity. The tilt of that gravity,
integrated down the 800 m of descent,
(1/g)∫₀^800 g_north dz= 5.2e-4 m, accounts for the difference to within a few percent (issue #27). - What it gives now. Flown against RocketPy’s own gravity formula, as the validation suite
does, hpr gives 1.93e-5 m, −1.8% (validation report). Both codes carry the same
Coriolis term (hpr in
dynamics.rs; RocketPy inflight.py:2779-2783), and on this evidence neither is wrong: they were being asked different questions.
Staging
In short
- What it models: motors that light at their own times, and a rocket that drops its booster under power, or several stages in turn. A motor can light at launch, at a time, a delay after another motor burns out, or a delay after its stage is freed, or never. When the stack comes apart (separation) with the forward part still to burn, that part (the sustainer) flies on with its own shape and mass, and the aft part (the booster) falls back to its own landing.
- Sources: no new physics. The flight is the same rigid-body flight (Rigid-body flight), the masses are the same sums (The design tree), and the booster’s descent is the same descent under a recovery device (Recovery). What is new is when each motor burns, and which rocket is flying.
- How well it is validated: against OpenRocket, not against a real flight. OpenRocket’s
two-stage, three-stage, cluster and air-start examples, read from their
.orkfiles, all fly within 5% of OpenRocket’s apogee and largest speed, flown on OpenRocket’s own curves. Five apogees are compared with OpenRocket’s flight with no parachute, since its parachute opened before apogee (Against OpenRocket).hpr sim, on the curves it fetches, flies the three-stage example within 5% too. A payload dropped with nothing left to burn flies under its own parachute from the split: on OpenRocket’s two payload examples, its apogee is within 1.1% of OpenRocket’s (A payload dropped with nothing left to burn). Boosters strapped beside the core burn with it and drop at their own separation: OpenRocket’s Parallel booster staging flies within 1.23% of its apogee (Boosters beside the core). Tests pin the bookkeeping: ignition times to 1e-12 s, the mass step at the split to 1e-12 of the mass, and, while the sustainer is not yet burning, the two parts’ momenta to 1e-9 of the stack’s. The flight of a dropped booster or middle stage after its split is not validated at all. - What it leaves out:
- The booster’s own airframe drag. After the split the booster is a point: a mass with only its recovery device’s drag. So hpr requires a device on it that opens at the separation, usually tumbling, and refuses the flight otherwise. hpr tumbles the booster side-on from the instant it separates, while a real finned booster flies nose-first for a while first, so it slows far faster than a real one would. Treat the booster’s peak and landing point as rough, likely too low and too close to the pad (#179, a model of the booster’s own drag).
- Any push from the separation (no charge or spring), the air flowing between the parts as they come apart, and the booster’s orientation as it falls.
- A drag or normal-force table from another program (
Simulation::with_drag_table), or a drag model of your own (Models of your own): it describes the whole stack, so a powered separation under one is refused. - With more than one separation, one that leaves nothing ahead of it to burn, such as a payload released after the stages (Several separations).
- The thrust a booster dropped still burning had left. A
.orkstage’s first burnout can drop the stage’s other motors while they burn, as OpenRocket does. The dropped part flies with no thrust, so its flight leaves out the impulse they had left, whichhpr simstates (A booster dropped still burning).
Using it today
Staging is set in a JSON design file or in code, or read from a .ork file. hpr sim flies a
.ork file’s powered separation, each parachute and streamer on its own part
(Separation; M4.5g1, powered
separation in hpr sim). It flies several powered separations in turn
(Several separations), and a payload dropped with nothing left to burn
when its own device opens at the split (A payload dropped with nothing left to
burn). It flies the stack whole when the
separation can only come after apogee. To fly a
.ork file’s staging from a program, start from the example
ork_two_stage.rs,
and see how such flights compare with OpenRocket’s in Against OpenRocket.
For a design of your own, start from the two-stage design
synthetic-two-stage-75mm-54mm.json
and the worked example below. The decision record is ADR-074.
When a motor lights
Each motor in a configuration has an ignition
(Ignition). The flight’s clock starts at launch (t = 0). Each motor burns on its own
clock from its ignition, so its thrust curve and its mass are its own curve shifted to that time.
ignition | lights at | example in a design file |
|---|---|---|
launch (the default) | t = 0 | nothing written |
time | a time after launch | "ignition": {"time": {"time_s": 4.0}} |
burnout | a delay after the motor in another mount burns out | "ignition": {"burnout": {"mount": "booster-motor-mount", "delay_s": 1.0}} |
separation | a delay after the separation that drops the stages behind this motor’s stage | "ignition": {"separation": {"delay_s": 0.5}} |
never | never: it rides loaded, as a motor that fails to light does | "ignition": "never" |
A design that says nothing lights every motor on the pad, the sustainer’s too, and hpr does not
warn. For a staged flight, give the sustainer’s motor its ignition and the flight a
Separation.
Before a motor lights it is loaded: its full propellant mass sits in the rocket, and it gives
no thrust. A motor that never lights, because it is set never, or the burnout or separation it
waits for never comes, is carried loaded to the ground. Every ignition time known before the flight, and every point of each shifted thrust curve,
is a stop time: the integrator ends a step there, so no step starts a
burn half way through (Time integration). An ignition that waits on a
separation becomes a stop time when the separation fires. The design refuses:
- a burnout of a mount with no motor, or a chain of burnouts that comes back to the motor itself;
- a separation ignition in the aft-most stage, which has nothing aft of it to separate;
- a time or delay that is negative or not a number.
Powered separation
A separation has a trigger and a stage boundary (Recovery). Besides
apogee, a height on the way down, a time, and a motor’s ejection
delay, it can now fire a delay after a motor’s burnout
(Trigger::Burnout).
When it fires, hpr looks at the forward part, body 0, which keeps the nose:
- If it still has a motor to burn, it is a sustainer. That covers a motor burning now, one due to light at a known time, and one lit by this separation. The sustainer flies on in six degrees of freedom, and the booster (body 1) descends under its own device.
- If it has nothing to burn, both parts descend under their devices, as before.
What hpr refuses:
- A booster still burning. Its motors must have burned out, unless the separation is told it
may drop one (A booster dropped still burning). If the
separation’s time is known before the flight, building the
Simulationreturns the error; otherwiserunreturns it when the separation fires, with no flight result. - A booster with nothing open at the split (see What it leaves out, above). A device on the
booster with
Trigger::Time { time_s: 0.0 }and no lag opens there, because a booster’s devices act only once it flies on its own. - A separation that could never fire, such as one timed from the burnout of the sustainer it lights.
Why the state carries straight across. hpr’s flight state is the motion of the nose tip, not of the center of mass (Rigid-body flight). The sustainer keeps the nose, so the nose tip’s position, velocity, attitude and rotation rate are all still right for it. What changes is the rocket they belong to:
- Mass. The mass drops by exactly the booster’s: its stages plus its spent motors. The center of mass and the inertia are the sustainer’s own stages and motors.
- Aerodynamics. hpr builds the sustainer’s aerodynamics from the design cut after the
stage boundary: the design with the booster’s stages removed. It is an ordinary rocket with a
nose and its own aft end, so its drag includes its own base. Its
reference area can be smaller than the stack’s (the widest body
may have been the booster’s), so a recorded coefficient such as
Sample::axial_coefficientsteps at the split even where the force doesn’t.
The integrator restarts at the separation, because the mass steps there.
Momentum. Nothing pushes the parts apart. Write m for a mass, v for the velocity of a
center of mass, and s, b for the sustainer and the booster. The sustainer keeps the nose tip’s
velocity v_O and rotation ω. The booster leaves with the velocity its own center of mass
already had, v_O + ω × r, where r runs from the nose tip to that center. So the two momenta add
up to the stack’s, m v = m_s v_s + m_b v_b. A test checks this to 1e-9 of the stack’s momentum
on a flight launched 10° off vertical, whose sustainer is not yet lit. With the sustainer burning,
m v is not simply additive, because the center of mass also moves inside the body as propellant
burns, so hpr makes no claim for that case.
Events. Along with the separation, the flight records an Ignition event for each motor lit
after launch. Burnout is recorded when no motor is burning or due to light at a known time. In
the example below that is 5.23 s only: the booster burned out at 1.73 s, but the sustainer was
already due. With a sustainer lit by its separation, whose time isn’t known in advance, Burnout
is recorded twice, at the booster’s burnout and at the sustainer’s.
What the result holds. The flight’s events and final sample are the sustainer’s, from the pad
to its landing. FlightResult::bodies holds the booster’s descent, and
FlightResult::landings gives both landings, the sustainer’s first.
A worked example
The program
crates/hpr-sim/examples/two_stage.rs
flies a synthetic two-stage design: a 54 mm sustainer on a 75 mm booster, with a J760 in the
booster and an I175 in the sustainer. The stages come apart 0.5 s after the booster’s burnout, and
the sustainer lights 1 s after it. The ignition names the booster by its mount’s id, as the
design file does. The separation’s trigger names it by its index among the placed motors, as
every trigger does, so the program looks that up:
sustainer_motor.ignition = Ignition::Burnout {
mount: "booster-motor-mount".to_owned(),
delay_s: 1.0,
};
let booster = simulation
.assembly()
.motors
.iter()
.position(|motor| motor.mount == "booster-motor-mount")
.ok_or("no booster motor")?;
Device::new("booster tumble", tumble, Trigger::Time { time_s: 0.0 }).on_body(1),
.with_separation(Separation::new(
Trigger::Burnout {
motor: booster,
delay_s: 0.5,
},
0,
))?;
The 0 is the stage boundary: the stages from the nose through stage 0 are the sustainer. Run it
with cargo run --example two_stage -p hpr-sim. It prints:
A 54 mm sustainer on a 75 mm booster, J760 then I175, calm air
Not yet validated: see the Accuracy page before trusting these numbers.
event time (s) CG height (m) speed (m/s) mass (kg)
liftoff 0.00 0.5 0.0 2.480
rail exit 0.20 6.3 61.0 2.410
separation 2.23 643.0 341.2 1.904
sustainer lights 2.73 797.4 280.7 0.779
sustainer burnout 5.23 1798.9 403.2 0.550
apogee 18.42 3103.2 0.1 0.550
parachute charge 18.42 3103.2 0.1 0.550
parachute opens 18.42 3103.2 0.1 0.550
sustainer lands 865.34 0.0 3.4 0.550
The booster (1.125 kg) leaves at 2.23 s and 642.7 m, tumbling. It peaks at 745.0 m at 5.11 s and lands at 47.21 s at 17.9 m/s.
How to read it:
- The split. At 2.23 s the stack weighs 1.904 kg. The booster (its stage and a spent J760) takes 1.125 kg of that, so the sustainer flies on at 1.904 − 1.125 = 0.779 kg, which is the mass shown when it lights. The booster starts at 642.7 m rather than 643.0 m because its own center of mass sits 0.3 m below the stack’s.
- The coast. Between the separation and the ignition the sustainer coasts for half a second and slows from 341.2 to 280.7 m/s. That is about 121 m/s², mostly drag, on a 0.78 kg rocket near Mach 1.
- The burn. The I175 burns for 2.5 s and the mass falls to 0.550 kg: the sustainer’s structure and a spent motor.
- Two landings. The sustainer lands under its parachute at 3.4 m/s. The booster tumbles from the split, climbs only 102.3 m more (to 745.0 m), and lands at 17.9 m/s. That short climb comes from tumbling side-on from near Mach 1 at once, and is likely too short (see What it leaves out).
These numbers are for a made-up rocket that no other program has flown, so nothing checks them beyond the tests below.
Several separations
A rocket of three stages or more flies, dropping its stages under power one after another, from the tail forward. At each split the stages behind it drop away and descend to their own landing, and the rest flies on as a new sustainer. hpr cuts that sustainer from the whole design at the new boundary, as it cut the first, and carries the nose tip’s state straight across, as in Powered separation. This is checked against OpenRocket’s three-stage example, not against a real flight, and the dropped stages’ own flights are not validated. The decision record is ADR-160 (several powered separations).
How it is set. Simulation::with_separations takes the separations in the order they fire.
with_separation is a list of one, and a flight with one separation flies as it did before, to the
bit.
- The order. Each separation’s stage boundary is forward of the one before. Separation
kmakes bodyk + 1: the stages from its boundary back to the boundary before it, or back to the tail. Body 0 keeps the nose. On a three-stage rocket the booster is body 1 and the middle stage body 2 (Separation::body_ofandSeparation::stages_of_bodygive the numbering). - Each split hands the flight on. With more than one separation, each must leave the body with the nose a motor still to burn.
- A motor lit at a split stays lit at the next one. A motor that an earlier separation lit counts as burning from that split when the next one fires. So a middle stage whose motor the first split lit, and which has burnt out by the second, leaves with that motor’s dry mass, not its loaded mass.
- Where else the list goes. The facade’s
FlightBuilder::separationstakes the same list, andhpr::ork::separationsmakes it from a.orkconfiguration’s stagings, from the tail forward.
What is refused.
- When the
Simulationis built: boundaries that don’t move forward; separation times known before the flight that come in the other order; and more than one separation together with ejections, since a sustainer cut from the design doesn’t track the ejected pieces. - In flight, by name: with more than one separation, one that leaves nothing ahead of it to burn, since once every body is a point mass no sustainer is left to part; and a separation that fires before the one listed ahead of it.
- Each dropped part must have a device open at its split, as a booster must.
- A
.orkconfiguration is left out, with its reason, when a separation at or after apogee sits beside another, or when one comes before the separation behind it.
A worked example: OpenRocket’s three-stage example. OpenRocket’s Three stage low power
rocket drops its booster at the booster’s burnout, as the middle stage lights. It drops the
middle stage at that stage’s own burnout, as the sustainer lights. Each configuration is written
sustainer first: [A8-5; B6-0; B6-0] is an A8-5 in the sustainer above B6-0s in the middle stage
and the booster. cargo xtask ork-flights flies each configuration on OpenRocket’s own thrust
curves, matched by their digests. Δ is hpr less OpenRocket, in per cent of OpenRocket’s:
| motors | apogee, OpenRocket (m) | Δ apogee | Δ largest speed | separations, OpenRocket / hpr (s) | notes |
|---|---|---|---|---|---|
| A8-5, B6-0, B6-0 | 277.1 | −1.37% | +0.25% | 0.857 / 0.857; 1.714 / 1.714 | nothing deployed |
| C6-5, B6-0, B6-0 | 505.4 | −0.95% | +1.51% | 0.857 / 0.857; 1.714 / 1.714 | nothing deployed |
| C6-7, C6-0, C6-0 | 689.5 | −1.48% | +1.64% | 1.860 / 1.860; 3.720 / 3.720 |
How to read it:
- The splits. Both come at OpenRocket’s times, to the 1 ms the report shows: at the booster’s burnout, then at the middle stage’s.
- The apogee. On the first two, OpenRocket’s parachute opens before apogee, 0.45 s and 1.57 s early, so the apogee is held to OpenRocket’s flight with nothing deployed, as for the clusters below. Against OpenRocket’s own record those two read −1.24% and +0.98%.
- The descent. Flown with the file’s own parachutes, the sustainer lands within 0.009% of OpenRocket’s landing speed on each, and its flight time is −1.13%, −0.56% and −1.41% off OpenRocket’s. All three meet every descent target (Recovery).
- The mass off the rod. OpenRocket’s recorded mass falls as if every motor of the booster’s type burned from launch (#185). From launch to rod clearance it drops 2.0000, 1.9998 and 2.9944 times as much as hpr’s: the count of such motors in each configuration. So hpr reads 1.80% to 3.26% heavier as the rocket leaves the rod, though the launch masses agree within 0.054%. At that moment hpr’s stability margin is 0.039 to 0.058 calibres shorter than OpenRocket’s, about as much as hpr’s center of mass sits further aft, 0.043 to 0.062 calibres. Whether OpenRocket’s light mass accounts for that is not sized.
- With the curves
hpr simfetches. The file holds no curves, sohpr simfetches them from ThrustCurve.org (Motors from ThrustCurve.org), taking the file that holds OpenRocket’s curve for each motor’s digest (ADR-161, OpenRocket’s curves by digest). The largest speeds read 0.31%, 1.55% and 1.67% above OpenRocket’s: 78.61, 121.55 and 132.02 m/s. The apogees, flown with the file’s parachutes, read 1.15%, 0.58% and 1.37% below OpenRocket’s own record, which opens them too; against its flight with nothing deployed, the table’s figures, the first two read 1.28% and 2.48% below. Before hpr kept a table of which ThrustCurve file holds each OpenRocket curve, the first configuration flew another A8 file, which burns out 0.536 s after lighting where OpenRocket’s burns 0.73 s, and its largest speed read 5.7% high.
What it leaves out.
- The dropped middle stage’s own flight, like the booster’s: a point under its devices’ drag, tumbling side-on from its split until a device of its own opens, so its peak and landing are rough (#179).
- An unpowered separation after a powered one, such as a payload released after the stages: it is refused. A lone unpowered one flies only as A payload dropped with nothing left to burn says.
- A motor lit by a separation can’t time a later separation, since its burnout has no time
before the flight. The
.orkreader never lights a motor that way.
A payload dropped with nothing left to burn
When the stack comes apart with nothing ahead of the split left to burn, every part flies on as
a point with only its own devices’ drag, so hpr sim flies it only when the part that keeps the
nose has a device of its own open by then. That is how a payload section usually leaves: the
booster’s ejection charge pushes it off after the motor has burned out, sometimes before apogee,
and its parachute opens at once. No new physics is involved. Each part is a point mass under its
open devices, the descent model of Recovery, from the instant it
separates, as OpenRocket flies a descent (its technical documentation v13.05, §4.2.5).
- The payload’s device. A
.orkdevice set tolowerstageseparationopens on the split’s own trigger, plus its delay. A payload whose device opens later, or that has none, would coast with no drag, sohpr::ork::tumblingrefuses it by name. It is not tumbled instead: a finless payload on its own is unstable, and nothing measured says how it flies. To fly such a file, set one of the payload’s devices to open at the split: in OpenRocket, its deployment event “Lower stage separation” with no delay. - A device that fired but waits out its lag. The flight itself refuses a part parted on the
way up, before apogee and rising, whose device fired by the split but opens after its lag: the
part would climb through the lag with no drag. A
.orknever flies one, since the check above refuses its delay first; a Monte Carlo run’s drawn deployment lag can make one, and that flight fails by name and is counted (Monte Carlo). A device set for the part’s own apogee leaves it to coast as before, which the check above refuses for the payload. A part parted by an ejection charge on the way up is refused the same way. A part already falling when it parts still falls with no drag through its device’s lag: a smaller error, not refused. - The booster tumbles side-on from the split until its own device opens, as after a powered split, and its flight is not validated (#179).
- Not measured: a real payload whose parachute opens late would land later and further away; OpenRocket’s record holds no such flight to compare with.
- The flight’s apogee is the payload’s own when the split comes before the stack’s apogee.
A dropped part can climb higher: the C6-3 example’s booster, tumbling, peaks 0.9 m above the
payload, and
hpr simsays so in a note, as a waiver’s height is the highest any part reaches.
A worked example. OpenRocket’s Deployable payload on a C6-3 drops its booster at the
charge, 4.86 s after launch. In hpr’s flight, from
hpr sim "Deployable payload.ork" --config "[C6-3]", the stack is then
230.4 m up and still rising at 27.6 m/s (OpenRocket’s record: 27.9 m/s). The payload’s 10 in
(0.254 m) parachute, set to lowerstageseparation in the file, opens at that instant, and the
payload climbs 3.3 m more, to 233.7 m above the site at 5.44 s. That height is the center of
mass’s, which starts 0.4 m above the site; from where it starts, as the table below compares, it
is 233.3 m. Coasting with no drag, it would have risen 27.6² / (2 × 9.81) = 38.8 m more
instead.
Against OpenRocket. All six configurations of the Deployable payload and the ARC payload rocket separate at OpenRocket’s times. The payload’s apogee is within 1.1% of OpenRocket’s record, measured from where each tool’s flight starts, and every descent meets the targets of Recovery. Two of the six separate before apogee:
Each Δ is taken from the report’s unrounded heights, so it can differ in the last digit from the rounded ones shown.
| motors | split (s) | payload’s apogee, OpenRocket / hpr (m) | Δ | coasting with no drag: OpenRocket’s free payload / hpr (m) | Δ |
|---|---|---|---|---|---|
| C6-3 | 4.86 | 233.2 / 233.3 | +0.04% | 258.5 / 268.8 | +4.00% |
| C6-5 | 6.86 | 265.3 / 264.4 | −0.32% | 266.6 / 265.4 | −0.43% |
The last two columns measure what the refusal avoids. OpenRocket’s flight of the same rocket with nothing deployed lets the payload fly on its own airframe. A coast with no drag puts the C6-3 payload 10.3 m above that, and 3.5 m above the whole stack’s own climb. The report’s table has all six. The M4.5g3 milestone shipped this; its decision record has the reasoning.
Boosters beside the core
Boosters strapped beside the core, a parallel stage, burn with it
and drop at a separation of their own; from a .ork, on a rocket of one stage on the axis. No
new physics is involved. The stack flies whole until
the split, and the split is a powered separation like any other
(Powered separation): the core flies on with its own shape and mass, and
the boosters fly as a point with only their devices’ drag, tumbling side-on from the split, as a
dropped booster does in hpr. Their own flight after the split is not validated: their peak and
landing are likely too low and too close to the pad
(#179).
- Which separation drops them. A separation after stage k drops every stage after k, so a parallel stage is dropped by the separation after the stage it hangs on, together with whatever hangs on it. A separation that would drop boosters hung on a stage the nose keeps together with an axial booster behind them is refused; drop the axial booster first.
- When they light. In a
.orkfile, a booster’s motor set to lightautomatically lights at launch when the boosters hang on the last stage on the axis, as OpenRocket lights them (.ork: Delays and ignition). - Boosters with no motor stay on. When no motor in the boosters lights, their separation at burnout or at the charge never comes, and they ride to apogee and land with the core, as OpenRocket flies them.
A worked example. OpenRocket’s Parallel booster staging, one of the example designs
OpenRocket ships, has an I115W in its core and an E12-0 in each of two boosters. The file holds
no thrust curves, so hpr sim fetches them from ThrustCurve.org once
(Motors from ThrustCurve.org). In configuration 1 both light at launch. From
hpr sim "Parallel booster staging.ork" --config 1, with its defaults (a 1.5 m rail at sea
level, calm air), the E12s burn out and fire their charges at 2.44 s, 290.1 m up at 207.6 m/s,
and the boosters drop away. The core burns on to 3.51 s and peaks at 1138.2 m at 13.01 s; the
boosters land at 33.0 s. In configuration 2 the boosters hold no motor and stay on: the stack
peaks at 858.2 m. These heights are a little above the table’s below, which flies each
configuration under OpenRocket’s stored conditions instead (a 1 m rod, at its launch site).
Against OpenRocket. Under OpenRocket’s own stored conditions, from the flights report, recovery as saved:
| motors | split (s), OpenRocket / hpr | apogee, OpenRocket / hpr (m) | Δ | largest speed Δ |
|---|---|---|---|---|
| I115W-10 and 2× E12-0 | 2.44 / 2.44 | 1123.6 / 1137.5 | +1.23% | +3.25% |
| I115W-10, boosters empty | none / none | 851.8 / 857.6 | +0.68% | +1.56% |
At rod clearance the masses agree to 0.7 g (0.06%) and the static margins to 0.003 calibre, and both descents meet the targets of Recovery. The 3.25% in largest speed is not traced. M4.5m shipped this; its decision record is ADR-171.
A booster dropped still burning
A powered separation may drop a booster whose motors still burn, when it is told to. The
dropped part then flies with no thrust, so its flight leaves out the impulse those motors had
left. This is how hpr flies a .ork stage with motors in more than one mount. OpenRocket 24.12
separates such a stage at its first motor’s burnout, measured by a committed probe
(Delays and ignition), and drops the stage’s other motors
still burning (M4.5n, a stage’s first burnout;
ADR-172).
- The opt-in.
hpr_sim::Separation::drops_burning, set bySeparation::dropping_burning(), is off by default, and then the booster’s motors must have burned out. On, a powered separation may drop a motor that has lit and still burns. A motor behind it still to light is refused either way, and so is any burning motor behind an unpowered separation.hpr::ork::separationsets it only for a staging the.orkreader markeddrops_burning. The reader marks only a split at its own stage’s firstburnout, and only when the split is powered: a motor ahead of it burns then, or lights then or later. A delay after that burnout counts too, though no probe has measured a delayed one. Such a split may drop only the stage’s own other motors, and only those lit strictly before the split. A motor behind it lit at the split itself, or one burning in another stage behind, is refused. Any other split while a motor behind it burns (a time, an ignition, a charge) is still refused, since leaving its thrust out could leave out most of a motor. A motor lit by a charge among several mounts is refused by name: which fires its charge first is not measured. - What the flight leaves out. A dropped part flies as a point with no thrust, as every
dropped part does, with its mass at the split, propellant left included.
BodyFlight::impulse_left_n_sgives the impulse its motors had left: each lit motor’s curve total, less what it had given by the split. That thrust could have changed the part’s speed by about that impulse over its mass, a little more as the propellant left burns away. It is 0 for every other part. - What
hpr simsays. It names the motors and gives the number. On the first configuration of OpenRocket’s Pods–powered with recovery deployment,[C6-7; 2× A3-4, B6-0], the booster drops at 0.860 s with both A3s still burning. Its flight as a point leaves out 0.509 N·s, as the report’s Parts dropped still burning section gives it.
The sustainer’s flight is not affected: it keeps no thrust from the dropped motors in either program. The dropped booster’s own flight is not validated (#179).
Against OpenRocket
In short. hpr reads when each motor lights and when the stages come apart from an OpenRocket
.ork file, and flies OpenRocket’s own two-stage, three-stage, cluster and air-start examples.
Every one of their 15 flights is within 5% of OpenRocket’s in apogee and in largest speed, a
tolerance written down before any of them was flown
(M1.9c milestone, decision record ADR-076; the
three-stage example since M4.5g2, several separations).
Five apogees, three of the cluster’s and two of the three-stage example’s, are compared with
OpenRocket’s flight with no parachute, since its parachute opened before apogee.
This is agreement with another program, not with a real flight.
What hpr reads. A .ork file says when each motor lights with a word and a delay
(Delays and ignition), and when each stage drops away with
another (When parachutes open and stages separate).
hpr turns them into its own:
| the file says | hpr flies |
|---|---|
a motor lit at launch, or automatic in the bottom stage, plus a delay d | lit at t = d: an air start when d is not 0 |
a motor lit at burnout of the stage below, plus d | lit d after the stage below’s first burnout: the first of its motors to burn out, by each one’s ignition and curve, in whichever mount (ADR-172) |
ejectioncharge, or automatic in a stage above | lit at the stage below’s ejection charge: its burnout plus its ejection delay, plus d |
never; burnout or ejectioncharge in the bottom stage; ejectioncharge or automatic over a plugged motor; or a motor lit by one of these | never lit: carried loaded, with no thrust |
a stage separating at launch plus a delay, at its motor’s ignition, burnout or ejection, or at the upperignition of the stage above | a separation at that instant, plus its delay, if at that moment a motor ahead of it is burning or has yet to light (one timed before the rocket leaves the rod fires as it leaves, #231) |
a stage separating at apogee, or at a height on the way down (altitudedescending) | no separation: it belongs to the descent, which hpr’s flights of a .ork don’t fly yet, so the configuration flies whole |
hpr flies such a separation when it is powered: at its time a motor ahead of it is burning or has
yet to light, and no motor behind it is. A powered separation at its own stage’s first burnout
may also drop the stage’s other motors still burning, as OpenRocket does
(A booster dropped still burning). A configuration with several flies them all, from the
tail forward (Several separations), as long as each is powered. An apogee or altitudedescending separation is left to
the descent only on the assumption that every motor is spent by apogee; cargo xtask ork-flights
checks that of every flight it reports. When flown, hpr refuses a powered separation that fires
after a recovery device on the part with the nose has opened. A configuration it can’t fly as
written is left out with its reason, never flown some other way:
- a separation at or after apogee beside another separation, and one that comes before the separation behind it (Several separations);
- a separation with no motor ahead of it burning or yet to light when it fires, beside another
separation. Alone, at a time known before the flight, it flies
(A payload dropped with nothing left to burn),
unless the part that keeps the nose would coast with no drag from it. A lone one at
apogeeor at a height on the way down can only come after apogee, so the climb is the whole stack’s, as in OpenRocket; - a separation while a motor behind it is still burning or yet to light, but for a powered one at
its own stage’s first
burnout, which may drop the stage’s own other motors still burning if they lit strictly before the split (the reader marks itdrops_burning); - a separation at a height on the way up (
altitudeascending), which hpr has no trigger for; - a negative delay on a separation hpr would fly;
- a motor lit at a word hpr does not know, by a stage below that holds no motor, by the charge of a stage below with motors in more than one mount (which fires its charge first is not measured), or by the charge of a motor below that states no delay;
- a separation at the ignition of a motor that never lights, or at launch, since hpr’s flight fires a separation only once the rocket is off the rod;
- a configuration in which no motor lights.
A motor set never, or lit by an event that never comes, flies unlit and loaded, as OpenRocket
flies it (M2.2e10, probed by
validation/oracles/openrocket/unlit_motors.py): one in the bottom stage lit at the stage below’s
burnout or charge, or one lit by a plugged motor’s charge. A stage that separates at the burnout or
charge of its own motor that never lights stays on. A stage below with no motor is still refused:
OpenRocket’s reading of it has not been probed.
A cluster flies with a motor in every tube (Clusters).
The tolerance. A design is within when every configuration of it that hpr flies has its apogee
and its largest speed within 5% of OpenRocket’s. Where OpenRocket’s parachute opened before its apogee,
which lowers it, the apogee is compared with OpenRocket’s flight of the same configuration with
nothing deployed. OpenRocket’s record is the set of flights OpenRocket 24.12 flew for this
comparison, committed as
openrocket-flights.json.
It holds the flight of the part that keeps the nose, so both numbers are the sustainer’s.
The result. Δ is hpr less OpenRocket, in per cent of OpenRocket’s.
| OpenRocket’s example | motors | apogee, OpenRocket (m) | Δ apogee | Δ largest speed | notes |
|---|---|---|---|---|---|
| Two stage high power rocket | H148R-0, then H148R-0 | 678.5 | −1.79% | −0.54% | separates at 1.535 s in both |
| Two stage high power rocket | I357T-14, then I59WN-P | 1,384.2 | −0.13% | −0.04% | separates at 1.515 s in both |
| Three stage low power rocket | A8-5, B6-0, B6-0 | 277.1 | −1.37% | +0.25% | nothing deployed; separates at 0.857 s and 1.714 s in both |
| Three stage low power rocket | C6-5, B6-0, B6-0 | 505.4 | −0.95% | +1.51% | nothing deployed; separates at 0.857 s and 1.714 s in both |
| Three stage low power rocket | C6-7, C6-0, C6-0 | 689.5 | −1.48% | +1.64% | separates at 1.860 s and 3.720 s in both |
| Clustered motors | 4× A8-3 | 58.2 | −0.35% | +0.20% | |
| Clustered motors | 4× B4-4 | 143.1 | −0.79% | +0.31% | nothing deployed |
| Clustered motors | 4× C6-3 | 308.6 | −0.72% | +0.92% | nothing deployed; +9.43% against its parachute opening 2.59 s early |
| Clustered motors | 4× C6-5 | 308.6 | −0.72% | +0.92% | nothing deployed |
| Clustered motors | 4× C6-7 | 308.6 | −0.72% | +0.92% | |
| Airstart timing | 3× I211W-P and a K550W-P, all at launch | 1,317.5 | +0.94% | +0.81% | |
| Airstart timing | the three I211W lit at 1 s | 1,292.5 | +1.03% | +0.76% | |
| Airstart timing | at 2 s | 1,296.0 | +0.84% | +0.72% | |
| Airstart timing | at 4 s | 1,303.7 | +0.59% | +0.51% | |
| Airstart timing | at 6 s | 1,274.2 | +0.48% | +0.46% |
The full report, with the stability margin, mass and center of mass at rod clearance, is
openrocket-flights.md.
The test a_two_stage_and_a_cluster_design_are_within_5_percent_of_openrocket in
xtask/src/ork_flights.rs holds it to the tolerance in CI.
A flight OpenRocket aborted is checked up to the abort. OpenRocket stops its own flight of
Pods–powered with recovery deployment’s first configuration at 1.81 s (“Stage began to tumble
under thrust”), so it has no apogee to compare. Instead, cargo xtask ork-flights compares hpr’s
height and speed with OpenRocket’s just after the separation and at OpenRocket’s last row. It
needs the splits at one time and each value within 5%. Both separate at 0.86 s, where hpr is
1.17% lower (22.18 m against 22.44 m) and 0.04% slower. At 1.81 s hpr is 1.18% lower (84.0 m
against 85.0 m) and 4.02% faster (79.9 m/s against 76.8 m/s), so it is met
(the report’s section;
.ork: Flights OpenRocket aborted). This is
weaker evidence than an apogee: two points on the way up, the second where OpenRocket’s flight is
breaking up. When OpenRocket aborts is its own call, too: its probe of the same configuration
with nothing deployed aborts at 2.40 s, not 1.81 s. The sustainer’s least static margin is −4.21
calibres at the split. In the record’s conditions, a 0.15 m rod at 28.61° N with no wind, hpr’s
sustainer turns over at 2.04 s. From hpr sim’s defaults, a 1.5 m rail with no wind, it does
not, and why the two differ is not traced. hpr sim warns that the sustainer is unstable under
power and that its apogee is not a prediction
(Flight metrics: unstable under power;
#335).
What is not settled.
- On the first two-stage flight, OpenRocket’s mass falls about twice as fast as hpr’s before the sustainer lights: by 0.0823 kg against 0.0412 kg from launch to the end of the rod, as if both H148R motors lost propellant from launch. On the second, with two different motors, the masses agree to about 1 part in a million. The three-stage example shows the same fault: OpenRocket’s drop to the end of the rod is 2.0000, 1.9998 and 2.9944 times hpr’s, the count of motors of the booster’s type (Several separations). What that does to the apogee has not been sized (#185).
- hpr’s apogee is its center of mass’s, which jumps forward at the split from the whole stack’s to the sustainer’s. Whether OpenRocket’s altitude jumps the same way is not measured (#187). The jump is under 1.32 m on the two-stage flights, less than 0.2% of either apogee.
- hpr’s descent of a separated part needs a recovery device on each part. So the climb’s
comparison tumbles the booster from the split and the sustainer from its apogee; neither acts on
the climb, which flies no parachute. A second flight, for the
descent, flies the file’s own devices, each on its part
(ADR-159, powered separation in
hpr sim). The sustainer’s landing speed is +0.00% off OpenRocket’s on both configurations, and its flight time +0.32% on the H148R-0 and H148R-0 configuration and +0.73% on the I357T-14, then I59WN-P one. On the three-stage example it lands within 0.009% of OpenRocket’s speed, and its flight time is −1.13%, −0.56% and −1.41% off. The descents of the booster and the middle stage are not compared: OpenRocket’s record keeps only the sustainer’s branch.
Running it. cargo xtask ork-flights flies them. It needs OpenRocket’s jar and its motor
database, which cargo xtask refs fetch and the oracle scripts put in place, so CI checks the
committed report rather than flying it.
Flying a .ork file’s staging yourself. The rocket a .ork configuration builds does not
carry its separations. The reader hands them over separately, as the configuration’s staging
and later_stagings (MotorConfiguration::stagings gives them all, from the tail forward).
hpr::ork::separations turns them into the flight’s separations, which the program passes to the
flight with Simulation::with_separations, or the facade’s
FlightBuilder::separations, along
with a recovery device on each part. For a configuration with one, hpr::ork::separation,
Simulation::with_separation and FlightBuilder::separation still take it alone. The example
ork_two_stage.rs
does this for a made-up two-stage .ork: it tumbles the booster from the split and the
sustainer from its apogee. Run it with cargo run --example ork_two_stage -p hpr. For the
file’s own parachutes and streamers instead,
hpr::ork::separated_recovery puts each on its part
and adds the tumbles a part needs; hpr sim flies a .ork that way
(Separation).
Tests
In crates/hpr-sim/src/staging.rs and crates/hpr-design/src/config.rs:
serial_plan_timing_and_mass_step(Loft lesson L93, a check carried over from hpr’s predecessor): the sustainer lights at the booster’s burnout plus its delay, to 1e-12 s, and the mass steps down by exactly the booster’s, to 1e-12 of the mass, and holds until the sustainer lights. It also checks a sustainer whose separation never comes: it never lights, and it lands loaded.a_sustainer_set_never_to_light_flies_as_a_motor_outanda_motor_set_never_to_light_stays_loaded: a motor setneverflies exactly as one whose only tube fails, carried loaded to the end, and a motor lit by that motor’s burnout never lights either.a_sustainer_keeps_a_motor_set_never_to_light_through_the_separation: a sustainer cut at a powered separation keeps it unlit.- In
crates/hpr-io/src/ork/tests.rs,openrocket_flies_a_motor_whose_ignition_never_comes_unlitholds OpenRocket’s probe record, anda_motor_waiting_on_one_that_never_lights_is_never_litthe reader’s chains and refusals. apogee_separation_fires_in_flight_and_booster_flies_to_landing(Loft lesson L30): an apogee separation fires at the flight’s own apogee, and a height separation where the flight crosses the height. Both bodies land, powered or not.staged_events_come_in_order: liftoff, rail exit, separation, ignition, burnout, apogee and landing, in that order, with the burnout recorded once at the sustainer’s, or twice with a sustainer lit by its separation.a_powered_separation_conserves_linear_momentum: the two bodies’ momenta add to the stack’s, to 1e-9 of it, launched 10° off vertical.an_air_start_lights_at_its_time_and_burns_on_its_own_clock: a motor lit at 3 s, loaded until then, burns out at 3 s plus its burn time.staging_refuses_what_it_cannot_fly: a drag table or a drag model past a powered separation, a separation timed while the booster burns, a delay that is negative or not a number, a booster with nothing open at the split, a separation that could never fire, and a powered separation after the sustainer’s canopy opened.two_powered_separations_drop_the_booster_then_the_middle: two splits at the booster’s and the middle stage’s burnouts, to 1e-9 s; at each the mass steps by exactly the dropped part’s, to 1e-12 of the mass, and the momenta add to the stack’s, to 1e-9 of it. The booster is body 1, the middle stage body 2, both land, and the flight repeats to the bit.a_motor_lit_by_the_first_separation_counts_from_it_at_the_second: a middle stage lit at the first split leaves at the second with its motor’s dry mass, and a second split timed while that motor burns is refused.several_separations_refuse_what_they_cannot_fly: the refusals in Several separations.ignition_times_follow_their_events,bad_ignitions_are_refused.strap_on_boosters_burn_beside_the_sustainer_and_drop: boosters lit at launch drop at their separation; the mass steps down by exactly their structure and spent motors, the core burns on, and it climbs higher than with the boosters carried to apogee.a_separation_drops_only_what_hangs_together: the refusal above, and the order that flies.a_powered_separation_drops_a_burning_booster_only_when_told: a powered separation setdropping_burningdrops a booster still burning, and the booster’simpulse_left_n_sis its curve’s total less what it had given by the split, to 1e-12 of it; not set, unpowered, or with a motor behind still to light, it is refused. Incrates/hpr-io/src/ork/tests.rs,a_stage_in_several_mounts_burns_out_first_where_its_motors_sayholds the reader’s first burnout and its refusals.
Moving mass
In short
- What it models: a part carried inside the airframe, such as ballast or a payload, sliding along the rocket’s axis during the flight on a trigger. The rocket’s center of mass and inertia follow the part, and so do the equations of motion. Use it to see what a payload that moves, or ballast moved on purpose, does to the stability margin and the flight. This is new with M1.12a (a mass that moves along the airframe).
- Sources: the parallel-axis theorem and the kinetics of a system of particles, from Meriam and Kraige’s Engineering Mechanics: Dynamics, applied to the equations of motion on Rigid-body flight; the cycloidal motion of cam design, from Norton’s Design of Machinery.
- How well it is validated: against exact answers only. No other simulator or real flight has been compared with it. The mass properties match a hand calculation to 1e-15, and the static margin to 1e-12 calibres. In free flight with nothing acting on the rocket, its angular momentum is kept to 6.9e-12 of itself and its center’s velocity to 2.6e-12 m/s.
- What it leaves out: a mass that leaves the rocket, which is Released mass; a shift in a flight that also separates, ejects pieces or releases a mass; a shift before the rocket leaves the rail; a part that moves across the axis or turns; and the shock of a part hitting a stop. The motion’s shape is a choice made here, not something a source gives for rockets.
Describing a shift
A MassShift names the part, how far it moves, how long it takes, and when it starts:
| field | meaning |
|---|---|
trigger | when it starts: the same triggers a parachute has (apogee, a height on the way down, a time after launch, a motor’s burnout or delay) |
component | the id of the internal component that moves; everything inside it moves too |
travel_m | how far along the axis, in meters: positive toward the tail, negative toward the nose |
duration_s | how long the move takes, in seconds: at least 0.01 s |
In code, for a part with the id ballast that slides 0.3 m toward the tail over 1 s, starting 5 s
after launch:
let simulation = simulation.with_shifts(vec![MassShift::new(
Trigger::Time { time_s: 5.0 },
"ballast",
0.3,
1.0,
)])?;
let flight = simulation.run(&mut ())?;
The part is any internal component of your design, such as a mass component. Each shift that
starts is recorded as an EventKind::Shift event. Simulation::mass_properties(flight, t) gives
the mass, center of mass and inertia the flight had at any time.
Shifts of one part add up. hpr can’t know before the flight which triggers will fire, so it adds all of a part’s moves toward the nose, and separately all of its moves toward the tail, and refuses either total if it would take the part out of the component that holds it. A part the design already places partly outside its holder, such as a weight in a nose cone’s shoulder, may go as far as it already reaches.
Some parts can’t move, and hpr says which rule a refused one breaks:
- a body component, or a part on the outside of the airframe;
- a part holding a motor, since the motor would not go with it;
- a part inside another part that moves;
- one tube of a cluster;
- a part covered by an override of mass: an override on its stage, or one on a component around it that covers what that component holds. The override doesn’t say how much of that mass is the part’s.
A flight with a separation or ejections can’t have shifts yet. A shift can’t start before the rocket leaves the rail either: the rail has no stop at its foot, so a part thrown toward the tail on the pad could push the rocket up the rail and leave it hanging there. hpr stops the flight with an error if one would. A shift timed close to the rail exit can therefore fly in one flight and stop another that leaves the rail later; time it from the burnout or the apogee instead.
The motion. A part doesn’t jump from one place to the next. It follows a cycloid, the curve a
cam designer calls cycloidal motion. It starts at rest, speeds up, and slows to rest again. Its
acceleration is zero at both ends, so neither its speed nor its acceleration jumps. With τ the
fraction of the move’s time gone, the part is s(τ) of the way along:
s(τ) = τ − sin(2πτ) / 2π, 0 ≤ τ ≤ 1
At τ = ½ it is exactly halfway, moving fastest, at twice the average speed. Its acceleration
peaks at 2π × travel / T² for a move taking T. A move shorter than 10 ms is refused: the peak
there is 63 km/s² for each meter of travel, which is closer to an impact than a motion.
The move’s time is cut into 16 equal parts by stop times, so the integrator takes at least 16 steps across it. That matters with fixed-step RK4: a 10 ms step would otherwise take a 10 ms move in one step.
What the rocket’s mass properties do
Moving a part inside the rocket doesn’t change the rocket’s mass. It moves the center of mass
the same way as the part, by the part’s share of the mass. With M the rocket’s mass, m the
part’s and Δ how far it has moved:
center of mass moves by m Δ / M
The inertia changes as well. hpr takes the part’s contribution out where it was and puts it back
where it is, both about the new center of mass, by the parallel-axis theorem. The part’s own
inertia about its own center goes with it unchanged. Write c₀ for the part’s center where the
design puts it, δ for the move, cg_new for the rocket’s new center, I_new for its inertia
about that center, and J(v) = |v|² E − v vᵀ (with E the unit matrix):
I_new = (the rocket's inertia with the part where it was, taken about cg_new)
+ m [ J(c₀ + δ − cg_new) − J(c₀ − cg_new) ]
As a check by hand, think of the rocket as two bodies: the part and everything else. About the
center of mass, the rocket’s inertia is the sum of the two bodies’ own inertias plus μ J(L). Here μ = m (M − m) / M is the
reduced mass and L is the line from the rest’s center to the part’s. Moving the part changes
only L. The tests check hpr’s inertia against this formula.
The stability margin. The static margin is the distance from the center of mass back to the
center of pressure, in calibres of the rocket’s diameter
d (Flight metrics: stability margins). It takes the center of
pressure with the air along the axis at Mach 0, which doesn’t depend on where the mass is. So a
part moving Δ toward the tail changes it by:
change in static margin = − m Δ / (M d)
The flight margin, at the flight’s own Mach number, moves by the same amount at any one instant, since only its center of pressure differs.
What the equations of motion add
The equations of motion (Rigid-body flight) are written about the nose tip, O, a
point fixed in the airframe, in body axes. ω is the airframe’s rate of turn and r the center
of mass seen from O. The equations already allow for a center of mass that moves through the
airframe and an inertia that changes, because burning propellant does both. Primes there, as
below, mean how fast something changes with time as seen from the airframe: r′ and r″ are the
center of mass’s velocity and acceleration through the airframe, and I′ how fast the inertia
about O changes. A part that moves adds to those terms, and adds one term more.
Sum Newton’s law over every particle of the rocket. Each particle’s acceleration is the nose
tip’s acceleration, plus the airframe’s rotation, plus its own motion inside the airframe. In the
force equation, the particles’ motion inside the airframe gives the r′ and r″ terms, with
nothing more. In the moment equation it gives I′ ω, and also this term:
ω × h + h′, h = Σ m ρ × ρ′
h is the moving parts’ angular momentum about the nose tip, relative to the airframe. ρ is a
part’s center and ρ′ its velocity inside the airframe. A part only slides, so every point of it
moves at the same ρ′. When the part is on the axis, ρ and ρ′ both lie along the axis and h
is zero. When it is off the axis, h is not zero, and hpr carries it. The sum is over the parts
that move, each with its own m, ρ and ρ′. On the Rigid-body flight page, T21 is the name
the equations (from RocketPy’s documentation) give the sum of the moments about the nose tip;
it subtracts ω × h + h′.
hpr works out how fast the center of mass and the inertia change exactly, from the cycloid. For a burning motor it still estimates them from points 0.1 ms apart. The two add, so a part can move while the motor burns:
r′ = r_a′ + Σ m (δ′/M − δ M′/M²)
r″ = r_a″ + Σ m (δ″/M − 2 δ′ M′/M² + δ (2 M′²/M³ − M″/M²))
I′ = I_a′ + Σ m (2 (c · δ′) E − δ′ cᵀ − c δ′ᵀ)
The sums are over the parts that move, each with its own m, δ and c. Here r_a and I_a
are the rocket’s with every part where the design puts it, M′ and M″
are how fast its mass changes and how fast that changes (both zero once the motor is out), δ′
and δ″ are the part’s velocity and acceleration along its move, and c = c₀ + δ is the
part’s center now.
A worked example
The example
moving_ballast.rs
flies the project’s 54 mm test design (validation/designs/synthetic-54mm-three-fin.json, a
body 56.3 mm across) on an I175 motor (motor designation),
with 200 g of ballast in its airframe. The ballast is a cylinder 50 mm long on the axis, its
center 0.375 m aft of the nose tip. At 5 s, well after the 2.5 s burn, it slides 0.3 m toward
the tail over one second. What the example prints (the transverse inertia is about an axis across
the rocket through its center of mass, the one it pitches about):
| time (s) | mass (kg) | center of mass (m aft of the nose tip) | transverse inertia (kg·m²) |
|---|---|---|---|
| 4.00 | 0.8188 | 0.6681 | 0.09556 |
| 5.25 | 0.8188 | 0.6748 | 0.09248 |
| 5.50 | 0.8188 | 0.7048 | 0.08138 |
| 5.75 | 0.8188 | 0.7347 | 0.07483 |
| 7.00 | 0.8188 | 0.7414 | 0.07399 |
By hand, the center of mass moves 0.2 × 0.3 / 0.8188 = 0.0733 m aft. At 5.5 s, halfway through
the move, it has gone exactly half of that. The transverse inertia falls because the ballast ends
up near the center of mass. The two-body check gives the change too:
μ = 0.2 × 0.6188 / 0.8188 = 0.1511kg.- The rest of the rocket, 0.6188 kg, has its center at
(0.8188 × 0.6681 − 0.2 × 0.375) / 0.6188 = 0.7628m aft of the tip. - The ballast’s center moves from 0.375 m to 0.675 m, so its distance from the rest’s center shrinks from 0.3878 m to 0.0878 m.
- The inertia changes by
0.1511 × (0.0878² − 0.3878²) = −0.0216kg·m², as the table shows.
The static margin falls from 4.297 calibres to 2.996, a change of −1.302 from the unrounded
values. By hand it is −0.0733 / 0.0563 = −1.302, with d = 0.0563 m. The apogee barely moves:
1749.8 m with the ballast moving, 1749.9 m with it held still.
How it is checked
The tests are in hpr_sim::shifts and hpr_design::mass. The last column is what each test
measured, where it records it, and in brackets the bound it holds the code to.
| test | what it shows | measured (bound) |
|---|---|---|
a_part_moved_inside_a_body_gives_the_hand_computed_whole | a point moved inside a cube matches a hand calculation, and the whole rebuilt with the point moved | exact (1e-15) |
mass_properties_before_during_and_after_a_shift_match_the_hand_calculation | the example’s rocket before, halfway and after, against the two-body formula | (1e-15 m and kg·m²) |
a_moving_mass_shifts_the_static_margin_by_the_hand_calculation | the static margin at every coast sample of the flight equals −m Δ s(τ)/(M d) from its start | (1e-12 calibres) |
a_part_moving_off_the_axis_keeps_both_momenta_in_free_space | no air or gravity, the rocket turning about all three axes, the ballast 1 cm off the axis: the angular momentum about the center and the center’s velocity stay constant | 6.9e-12 of the angular momentum (1e-10), 2.6e-12 m/s (1e-10) |
a_shift_s_rates_are_the_derivatives_of_its_mass_properties | the exact rates against differences of the mass properties, during the burn and after it | r′ 2e-11 m/s (1e-9), r″ 3e-8 of itself (1e-7) |
a_fixed_step_follows_a_short_shift_in_its_stops | the free-flight case with RK4 at 10 ms steps and a 10 ms move: 16 steps across it | 1.2e-4 m/s (3e-4) |
a_canopy_that_opens_while_the_ballast_moves_keeps_the_center_s_velocity | in a vacuum, a drogue opening halfway through a move keeps the center’s velocity, and from then on the center falls freely while the ballast finishes its move | (1e-12 m/s at the opening, 1e-9 m/s after) |
a_shift_the_flight_starts_gets_its_stops_too | a move started at apogee gets its 16 stop times when it starts | exactly 16 steps |
a_shift_starts_at_apogee_or_at_its_height_on_the_way_down | the triggers the flight watches for start the move at the apogee, and at 200 m on the way down | (1e-6 m) |
The free-flight test is the one that checks the new term. Without ω × h + h′, its error would be
the size of the ballast’s own angular momentum relative to the airframe, which peaks at 1.8% of
the rocket’s angular momentum. It measures 6.9e-12.
A first version took the part’s rates from differences 0.1 ms apart, as the motor’s are. In free flight it kept the center’s velocity only to 1e-8 m/s, the error of the difference itself, so the rates were made exact. A check with RK4 taking a 10 ms move in a single step found the center’s velocity off by 0.071 m/s; the 16 stop times across each move bring it to 1.2e-4.
Every refusal has a test that checks which rule fired, in shifts_that_cannot_be_made_are_refused,
refusals_that_need_a_design_of_their_own and
triggers_a_shift_cannot_have_are_refused_in_its_own_words.
What it leaves out
- Releasing a mass. Ballast or a payload that leaves the rocket while the rest flies on is Released mass, added with M1.12b.
- Shifts with a separation, ejections or a release. Their pieces and parts are fixed before the flight, with every part where the design puts it.
- Shifts on the pad or the rail, as above.
- Other motions. A part can only slide along the axis, at whatever distance from the axis the design puts it. It can’t move across the axis or turn.
- Stops. The cycloid brings the part to rest smoothly. A real mechanism may stop it with a shock, which a rigid airframe can’t model.
- Warnings. hpr doesn’t warn when a shift takes the margin below a safe value; read the margin from the flight’s metrics (Flight metrics).
The decision record is ADR-087.
References
- [MK] J. L. Meriam and L. G. Kraige, Engineering Mechanics: Dynamics: chapter 4 (kinetics of systems of particles: the force and moment equations summed over particles, and angular momentum about a moving point) and appendix B (the parallel-axis theorem and the inertia tensor).
- [N] R. L. Norton, Design of Machinery, the chapter on cam design (cycloidal displacement:
s = h [θ/β − sin(2πθ/β)/2π]). - The equations of motion this adds to: the RocketPy technical documentation, “Equations of Motion” v0 and v1 (Rigid-body flight).
Released mass
In short
- What it models: a part carried inside the airframe, such as ballast or a payload, let go during the flight on a trigger. The rest of the rocket flies on in six degrees of freedom, its mass, center of mass and inertia changing at once to those of what remains. The part falls to the ground on its own. Use it to see what dropping ballast or a payload does to the flight, and where the part comes down. This is new with M1.12b (a mass released in flight).
- Sources: the parallel-axis theorem and the kinetics of a system of particles, from Meriam and Kraige’s Engineering Mechanics: Dynamics. The part’s fall uses the equations a separated body already has (Recovery).
- How well it is validated: against exact answers only. No other simulator or real flight has been compared with it. The mass properties after a release match a hand calculation, and the design built without the part, within the tests’ bound of 1e-15 (kg, m, kg·m²). In free flight with nothing acting on the rocket, the rest and the part together keep the rocket’s momentum to a relative error of 1.5e-13, and its angular momentum to 7.3e-12.
- What it leaves out: a push that throws the part out; a release in a flight that also separates, ejects pieces or moves a mass; a release before the rocket leaves the rail; parachutes on the part; and the part’s own spin once it is out. Where and how fast the part lands depends on its drag area, which you give. hpr doesn’t warn when a release leaves the rocket unstable: dropping a part forward of the center of mass moves the center aft, and the stability margin falls by 1.683 calibres in the example below. Read it from the flight’s metrics (Flight metrics).
Describing a release
A MassRelease names the part, when it leaves, and its drag area once it is out:
| field | meaning |
|---|---|
trigger | when it leaves: the same triggers a parachute has (Recovery: triggers): apogee, a height on the way down, a time after launch, a motor’s burnout or its ejection delay |
component | the id of the component in the design that leaves; everything inside it leaves too |
drag_area_m2 | the part’s drag area C_D S once it is out, in m²: positive |
Give the list to a flight with Simulation::with_releases. The part must be one component carried
inside the airframe, such as a mass component or an inner tube
(Rocket design: the tree). These are refused, each with an error that names
the part and the rule:
- an
idthat names no component; - a body component (a nose cone, a tube, a transition), or a part outside the airframe (fins, rail buttons);
- one of several copies in a cluster of tubes;
- a part that holds a motor: the rocket’s motors stay with it;
- a part whose mass is set by an override, on its stage or on a component around it that includes it, because the override doesn’t say how much of the mass is the part’s;
- a part released twice, or inside another part that is released;
- a part with no mass, or releases that would leave the airframe, its motors aside, with none;
- a drag area that is zero, negative or not finite. A part with no drag would fall as if in a vacuum, which is a wrong number rather than a model;
- a trigger a parachute couldn’t have either: a time before launch, a height that isn’t positive, a motor that isn’t there, has no ejection delay, or is set to fail to light.
A release that would come on the pad or the rail makes Simulation::run return an error, with no
flight: the part has nowhere to go. A flight can’t combine a release with a separation, ejected
pieces or a moving mass yet.
What happens at the release
At the instant the part leaves, three things happen.
The rocket becomes the rest. With M the rocket’s mass and cg its center, m the part’s
mass, c its center and I_p its own inertia about c, the rest has
M' = M − m
cg' = (M cg − m c) / M'
I' = I_about_cg' − (I_p + m (|c − cg'|² E − (c − cg')(c − cg')ᵀ))
where I_about_cg' is the rocket’s inertia moved to the new center by the parallel-axis theorem,
E is the identity matrix, and the bracket is the part’s inertia about that center. It is the
sum that builds a rocket from its parts, run backwards. The aerodynamics don’t change: the part
was inside the airframe.
The rest flies on from the same state. The flight’s state is the nose tip’s position and
velocity, the attitude and the turning rates (Rigid-body flight). The rest
keeps the nose tip, so none of these changes. The mass properties step, and the integrator (the
numerical method that steps the flight forward in time) starts afresh at the same instant, as it
does when a sustainer flies on after a separation
(Staging). What the flight reports about the center of mass
steps too: its position moves by cg' − cg (9.5 cm aft in the example), and its height with it.
The part leaves with the velocity it had. Every point of a rigid body moves at v_O + ω × r,
with v_O the nose tip’s velocity, ω the rocket’s turning rate and r the point’s place in
the rocket. The part leaves from where it was, with its center’s velocity v_O + ω × c, and the
rest’s center goes on at v_O + ω × cg'. Nothing pushes the two apart, so every bit of the rocket
keeps its velocity: once the motor has burned out, the rest’s momentum plus the part’s is the
rocket’s just before, and the same holds for the angular momentum.
While the motor burns, the center of mass drifts along the airframe as propellant is used, and the
reported center-of-mass velocity includes that drift. A release steps the center by cg' − cg, so
the drift’s share of the velocity steps too. The reported momenta then differ by
Ṁ (cg' − cg), with Ṁ the propellant’s mass flow, turned into the
launch frame. No part of the rocket changes its velocity; only
the reported center does.
The flight has one apogee. The rest’s center is not where the rocket’s was, so on a flight
that turns, it can rise for a moment after the rocket’s apogee, or already be falling when a part
leaves just before it. The flight records one apogee: the rocket’s, or
the release itself when it leaves the rest already falling. A part let go at apogee leaves at
that one, even when another part leaving first sets the rest rising again, and whatever order
the parts are listed in. Past the recorded apogee the rocket counts as coming down, so a part or
a parachute set to a height above the apogee still goes there, as
Recovery: triggers describes. A flight started part way (Simulation::run_free) already
falling lets a part waiting for the apogee go at its start; it records an apogee only if the
rest later rises and falls again.
The rest can land at the release. A part let go just above the ground, forward of the center of mass of a rocket falling nose up, steps the rest’s center down, to the ground or below it. The rocket has then landed, at the release. A recorded trajectory’s last row is at that time, but of the rocket as it was before the part left. A release that steps the rest below the ground while it is still climbing is an error instead.
The part’s fall
Once out, the part is a point mass under its drag area, its weight and the Earth’s rotation, the equations a separated body falls by:
m a = −½ ρ (C_D S) |v − w| (v − w) + m (g + a_Coriolis)
with v and a the part’s velocity and acceleration, ρ the air’s density, w the wind, g
gravity and a_Coriolis the Coriolis acceleration. It
falls from its release to the ground or to the flight’s time cap (FlightSettings::max_time_s).
If it left climbing, its own apogee is recorded.
The drag area is yours to give. For a part tumbling at random, the tumble model’s body term gives
0.56 times its side profile, its diameter times its length
(Recovery: tumble). That is half the 1.12 of a cylinder broadside. It was
fitted to whole rockets 44 to 103 mm across falling at 5 to 6.6 m/s, and its one drop test without
fins wanted 0.79, which gives a speed 16% lower. So treat a tumbling part’s landing speed as
uncertain by at least that much. hpr doesn’t build the area for you, because its tumble model
needs body tubes and fins, and a released part has neither. If the part carries a parachute, give
the parachute’s drag area; it is taken as open from the instant the part leaves.
Reading a release back
EventKind::MassRelease(i)among the flight’s events marks releasei(its place in the list given towith_releases); its sample is the rocket just before the part left.FlightResult::releasedholds each part’s flight (ReleasedFlight): its start, its events, its landing. The separated bodies of a separation or an ejection are elsewhere, inFlightResult::bodies.Simulation::mass_properties(&flight, t)gives the rocket’s mass properties at timetas the flight flew it, without every part released at or beforet.
A worked example
The example
released_ballast.rs
flies the project’s 54 mm test design
(synthetic-54mm-three-fin.json,
a body 56.3 mm across) on an I175 motor
(motor designation), with 200 g of ballast in its airframe: a
cylinder 50 mm long and 30 mm across, on the axis, its center 0.375 m aft of the nose tip. It flies
in calm standard air from a site in New Mexico 1,400 m up, and a drogue with a drag area of 0.3 m²
opens at apogee. At 5 s, well after the 2.5 s burn, the ballast is let
go to tumble down under 0.56 × 0.05 × 0.03 = 0.00084 m², the tumble model’s body term.
In the table the transverse inertia is about an axis across the rocket through its center of mass, the one it pitches about. What the example prints:
| time (s) | mass (kg) | center of mass (m aft of the nose tip) | transverse inertia (kg·m²) |
|---|---|---|---|
| 4.00 | 0.8188 | 0.6681 | 0.09556 |
| 6.00 | 0.6188 | 0.7628 | 0.07277 |
By hand, the rest’s center is at (0.8188 × 0.6681 − 0.2 × 0.375) / 0.6188 = 0.7628 m aft of the
tip. It moves 0.09474 m aft, because the ballast sat forward of the center, and the static margin
falls by 0.09474 / 0.05630 = 1.683 calibres (the body’s diameter is 0.05630 m), from 4.297 to
2.615: still stable.
At the release the rocket carries 131.5839 kg·m/s of upward momentum. Every point of the airframe
moves at v_O + ω × r, so it divides between the rest, 99.4433, and the ballast, 32.1406; the
free-flight test below checks that the flight keeps it so.
| apogee (m) | lands at (s after launch) | landing speed (m/s) | |
|---|---|---|---|
| the rocket, ballast dropped | 1670.0 | 276.3 | 6.15 |
| the ballast | 1496.5 | 40.0 | 66.74 |
| the rocket, ballast kept | 1749.9 | 253.5 | 7.07 |
- The rocket climbs 80 m less without its ballast: the same drag slows a lighter rocket more.
- It comes down more slowly under the same drogue, and so lands later. A terminal speed goes
as the square root of the mass:
7.07 × √(0.6188 / 0.8188) = 6.15m/s. - The ballast peaks lower than the rocket, having more drag for its mass. Its terminal speed
at the ground is
√(2 × 0.2 × 9.79 / (1.069 × 0.00084)) = 66.0m/s, with the standard air’s density 1.069 kg/m³ at the site, 1,400 m up (Atmosphere), and gravity 9.79 m/s² there (Gravity). It lands a little faster, 66.74 m/s, because it is still slowing as the air thickens.
How it is checked
The tests are in hpr_sim::releases and hpr_design::mass. The last column gives the value each
test measured, where its comments record one, and in brackets the bound it holds the code to.
| test | what it shows | measured (bound) |
|---|---|---|
a_part_taken_out_of_a_body_leaves_the_hand_computed_rest | a box taken out of a cube leaves the cube’s mass, center and inertia, and putting it back gives the whole | (1e-15) |
mass_properties_after_a_release_match_the_hand_calculation | the example’s rocket, with the ballast 1 cm off the axis, before and after the release, against the two-body formula and the design built without the ballast; every step flies the rest’s mass and center; the part leaves from its place with v_O + ω × c | (1e-15 m, kg and kg·m²; 1e-12 m and m/s) |
a_release_conserves_mass_and_momentum_in_free_space | no air or gravity, the motor spent, the rocket turning about all three axes: the rest and the part keep the rocket’s mass, momentum, and angular momentum about their common center of mass | momentum 1.5e-13 (1e-12), angular momentum 7.3e-12 (1e-10) |
two_releases_leave_the_design_without_both_parts | two parts released at different times: between them only the first is gone, after both the rocket is the design built without either | (1e-15) |
a_payload_let_go_under_the_drogue_lands_slower_and_falls_at_its_own_speed | in uniform air, under a drogue, a release at 150 m on the way down: the rest lands at the lighter rocket’s terminal speed, the part at its own | (1e-6 m/s) |
a_release_comes_at_apogee_and_a_part_let_go_climbing_has_its_own | a release at apogee comes at the rocket’s apogee; a part let go climbing records its own apogee, then its landing | (1e-6 m/s at its apogee) |
a_release_at_apogee_on_a_tilted_rail_leaves_one_apogee | off a rail 5° from vertical, in wind, with and without a drogue, the flight records one apogee | exactly one |
a_part_let_go_just_before_apogee_can_make_the_apogee_there | a release that leaves the rest already falling makes the apogee, and fires the drogue, at the release; a part waiting for the apogee, listed before or after, leaves there too | exactly one, at the release |
parts_waiting_for_the_apogee_all_leave_at_it | two parts let go at apogee off the tilted rail, listed either way round, both leave at the flight’s one apogee | exactly one |
a_main_set_above_the_apogee_opens_there_whatever_leaves | off the tilted rail, in wind, a main set 1600 m up, above the 1533 m apogee, opens at the apogee with a part let go there, and a part set to 1600 m leaves there too, listed either way round | at the apogee; landing under 10 m/s |
a_release_that_puts_the_rest_on_the_ground_lands_it | under a drogue, a release 5 cm above the ground steps the rest’s center 9.5 cm down, below it: the rocket lands at the release; climbing, the release is refused | 1e-8 m, 1e-15 kg |
the_optimum_delay_holds_a_release_on_the_motor_s_charge | a release or a mass shift fired by the motor’s ejection charge is held with the charge when hpr works out the optimum delay (the delay that fires the charge at apogee), so the answer doesn’t depend on the delay flown | equal |
a_release_and_its_flight_read_back_as_written | a release, and a flight with a released part, write to JSON and read back unchanged | equal |
a_part_let_go_at_the_ground_has_landed | a part let go as the rocket hits the ground, already at or below it, has landed | n/a |
In the free-flight test the ballast leaves 0.146 m/s away from the rocket center’s velocity, because the rocket turns. Had it left at the nose tip’s velocity, the momentum would be off by 5.0e-3 of itself; at the rocket center’s, by 3.4e-3. The flight keeps it to 1.5e-13. The part’s own spin, which a point mass drops, is 1.5e-3 of the rocket’s angular momentum there.
Every refusal has a test that checks which rule fired, in releases_that_cannot_be_made_are_refused,
parts_with_no_mass_are_refused, triggers_a_release_cannot_have_are_refused_in_its_own_words and
a_release_is_refused_with_partings_or_shifts_in_either_order.
What it leaves out
- A push. Nothing throws the part out: a spring or a charge would add to its velocity and take from the rocket’s, as an ejection’s impulse does (Recovery: ejected pieces).
- Releases with a separation, ejections or mass shifts. hpr can’t combine these yet.
- Releases on the pad or the rail, as above.
- Parachutes on the part. It falls under one drag area from the instant it leaves; it has no devices of its own, and no opening time.
- The part’s spin. Its spin is dropped: 1.5e-3 of the rocket’s angular momentum in the free-flight test.
- Warnings. hpr doesn’t warn when a release leaves the rocket unstable, as above.
The decision record, ADR-088, sets out these choices: the rest flying on from the same state, the part leaving at its own velocity and falling under a drag area you give, and what is refused. A part that moves along the airframe without leaving it is Moving mass.
References
- [MK] J. L. Meriam and L. G. Kraige, Engineering Mechanics: Dynamics: chapter 4 (kinetics of systems of particles: the momentum and angular momentum of a system summed over its particles, and their conservation with no external force) and appendix B (the parallel-axis theorem and the inertia tensor).
- The equations of motion the rest flies on: the RocketPy technical documentation, “Equations of Motion” v0 and v1 (Rigid-body flight).
- The tumble model’s body term: the OpenRocket technical documentation, §3.5 (Recovery: tumble).
Flight metrics
In short
- What it models: the numbers a flight is judged by. These are the apogee, the top speed and
Mach number, the peak dynamic pressure (“max q”), the boost’s
peak acceleration and, kept apart, the opening shock. It also gives the
stability margin from the rail exit to apogee or the first
deployment, the largest
angle of attack that counts toward the envelope’s
high-angle flag (below), whether the rocket is unstable while a
motor burns (below), the
ejection delay that would fire the charge at apogee, and each
landing’s latitude and longitude. Anything that didn’t happen is
None, never a zero. - Sources: the margin is Barrowman’s center of pressure (to Mach 0.8; faster, as Aerodynamics describes) against the center of mass (the CG), defined as RocketPy defines its static margin and stability margin. The peak search is Kiefer’s golden-section search (References). Latitude and longitude come from hpr’s WGS 84 conversions (Geodesy).
- How well it is validated: each number is only as good as the flight it comes from. The flight is checked against RocketPy and OpenRocket (Accuracy), and the metrics add no physics of their own. Tests check each against a hand calculation or against hpr’s own models evaluated directly (Tests). The margin in the weakest plane is checked against hpr’s own scan of 3,600 planes in a test, and against one OpenRocket run that is not committed. None is validated against a real flight.
- What it leaves out:
- Fin flutter, which has its own page: Fin flutter. Its margin comes from the max q found here.
- File exports: a later part of the same milestone, M1.10c (CSV, JSON, KML and GeoJSON files).
- The descent of a separated body, such as a dropped booster. It gets a landing, but no peaks.
- A damping ratio: how fast a wobble dies out. Both margins here are static quantities.
- The margin at the flight’s angle of attack. Both margins take the air along the rocket’s axis, in the weakest direction it can cross (Stability margins). In hpr’s model a rocket meeting the air at an angle, as it does leaving a rail in wind, can have a smaller or a larger margin than these, depending on where its body’s lift acts (Stability margins says why they leave it out).
Why these rules
Loft, this project’s predecessor, got four things wrong that these metrics are built to avoid:
- It read peaks off a table of recorded rows and missed them, and it counted the opening shock as the boost’s peak acceleration (L34).
- It printed zeros for things that never happened, and never said which height an apogee was counted from (L35).
- It published margins of ±12 to 15 calibres for rockets where a margin has no meaning (L33).
- Its optimum ejection delay depended on the delay flown (L94).
Each is a numbered lesson with a test (Tests).
Getting the numbers
Fly a Simulation with a
FlightMetrics watching it. Then ask the
watcher for a FlightSummary:
let mut metrics = FlightMetrics::new();
let flight = simulation.run(&mut metrics)?;
let summary = metrics.summary(&flight, simulation.environment())?;
let best = hpr_sim::metrics::optimum_delays(&simulation)?;
A watcher keeps one flight. Call metrics.clear() before it watches another; summary refuses a
flight unless the watcher saw each of its steps once.
The example program
flight_metrics.rs
does this for Valetudo, the rocket from Getting started. It flies with a
5 m/s wind from the west and one 1.5 m parachute. The charge fires 6 s after
burnout (the end of the thrust curve), and the canopy is open 0.5 s
later. Run it with cargo run --example flight_metrics -p hpr-sim. It prints:
Valetudo on a K400C, a 1.5 m parachute fired 6 s after burnout, open 0.5 s later
Not yet validated: see the Accuracy page before trusting these numbers.
Heights are the center of mass's above the launch site; it starts 0.94 m up.
Apogee: 714.0 m above the site (713.0 m of climb) at 11.16 s
Top speed: 112.3 m/s at 3.00 s, 188 m up
Top Mach number: 0.337 at 3.02 s, 190 m up
Max q: 6667 Pa at 2.99 s, 186 m up
Boost acceleration: 47.2 m/s² at 0.03 s, 1 m up
Opening shock: 153.4 m/s² at 9.76 s, 698 m up
Rail exit: 16.2 m/s at 0.37 s, 17.2° off the oncoming air
At rail exit (0.37 s): static margin 3.09 cal, flight margin 3.09 cal at Mach 0.051
Least static margin: 3.09 cal at 0.37 s, 4 m up
Least flight margin: 3.09 cal at 0.37 s, 4 m up
Optimum delay: 10.6 s after burnout at 3.26 s (flown: 6.0 s), for an apogee of 778.7 m at 13.83 s
Landing: 32.990000° N, 106.967084° W: 272.6 m east and 0.0 m north of the site, at 11.6 m/s, at 78.67 s
The 6 s delay is too short:
- The charge fires at 9.26 s, and the canopy opens at 9.76 s: about 4 s (13.83 − 9.76) before the apogee it would have reached.
- The rocket still rises 1.4 s more, to an apogee of 714.0 m.
- With the charge held, it would have coasted to 778.7 m at 13.83 s: the apogee of the Getting started flight, whose drogue fires at apogee.
- So the optimum delay is 10.6 s. The opening shock, 153.4 m/s², is three times the boost’s 47.2 m/s², and it is not counted as the boost’s peak.
- Don’t size a shock cord from the 153.4 m/s²; it is no bound either way. The canopy reaches full drag at line stretch, 0.5 s after the charge, with no filling time (hpr’s default), so the rocket doesn’t slow while it fills: that reads high. hpr also leaves out a canopy’s drag overshoot near the end of filling: that reads low (The opening load).
Top speed comes at 3.00 s, before the 3.26 s burnout, because in the thrust curve’s last moments the motor pushes less than drag and gravity pull back.
Heights
Every height is of the center of mass: its
ellipsoidal height above the launch site’s, the same as
a flight’s height_above_ground_m. The center of mass starts above the site, because the rocket
stands on the rail: 0.94 m for Valetudo. So the summary gives both:
apogee.height_above_ground_m: 714.0 m, above the site.apogee.gain_m: 713.0 m, the climb from where the center of mass stood at launch. OpenRocket’s altitude counts this way. A flight started in the air (Simulation::run_free) has no launch height, and no climb:None.
A flight ends when its center of mass comes back down to the site’s height, so a landing is at height 0 by this measure.
Peaks
Each peak comes with its time and its height. The watcher looks at every step the integrator takes (Time integration and events), not at a table of recorded rows:
- It evaluates the equations of motion at each step’s start, middle and end.
- It fits a parabola through the three values. When the parabola bends down with its top inside the step, the peak may lie inside. A golden-section search then looks for it on the step’s dense output, the integrator’s smooth curve through the step, to a billionth of the time since launch (or of 1 s early in the flight).
- It keeps the largest of what the search finds and the three samples.
Thrust-curve knots, the times where a motor’s tabulated thrust changes slope, end steps. So a spike in the thrust curve is a step’s end, and is never averaged away. Loft took its peak acceleration from a finite difference of the recorded speed, and read it low. Valetudo in a vacuum shows how much: the watcher’s peak matches the hand value from the motor and the masses within the test’s 1e-6, and a finite difference of the speed recorded at 100 Hz reads it more than 1% low.
| Peak | What it is |
|---|---|
| Top speed | The center of mass’s speed relative to the ground, after liftoff |
| Top Mach number | The airspeed over the local speed of sound |
| Max q | The largest dynamic pressure, ½ρv² on the airspeed |
| Boost acceleration | The largest acceleration of the nose tip, from liftoff until a recovery device opens |
| Opening shock | The largest acceleration while a device is open, the opening load over the mass. It follows hpr’s inflation model (Recovery) |
The accelerations are of the nose tip, which is the body’s origin in hpr’s frames. It differs from the center of mass’s only by the rocket’s turning, which is small in a straight boost, and by the center of mass’s slow drift forward as propellant burns. The accelerations are relative to the launch frame and straight from the equations of motion, so they include gravity’s pull, as a trajectory’s acceleration does. An accelerometer reads something else: it does not feel gravity.
Nothing is kept while the rocket sits on the pad. A rocket that never lifts off has no peaks at
all: None, not zero.
Stability margins
The margin is how far the center of pressure lies behind
the center of mass, in calibres of the reference diameter d:
margin = (x_cp − x_cg) / d, x_cp = Σ C_Nα,i x_i / Σ C_Nα,i
Here x is a station measured aft of the nose tip, and C_Nα,i is component i’s
normal-force slope. hpr gives two margins at each instant:
-
Static margin: the center of pressure at zero angle of attack and Mach 0, against the center of mass of that instant. This is RocketPy’s
static_margin. It changes only as propellant burns. -
Flight margin: the center of pressure at the flight’s own Mach number, still with the air along the axis, against the center of mass of that instant. It is defined as RocketPy’s
stability_marginis. RocketPy’smin_stability_margintakes the least over its whole flight, on the rail and in the descent too, at its solver’s steps, so it can differ from hpr’s least below. No page compares hpr’s margins with RocketPy’s yet. Against OpenRocket, in calm air at rod clearance, where a rocket is still slow, hpr’s margin at the flight’s Mach number is within 0.016 calibres on 41 of the 53 flights of OpenRocket’s own examples. This comparison takes hpr’s margin with the air along the rocket’s axis (the 0° plane), as OpenRocket’s margin is, not in hpr’s weakest plane (below). On[C6-7; B6-0]of Pods–powered with recovery deployment, hpr’s margin at rod clearance in the report is +3.74 calibres along the axis, whilehpr simprints a least margin of −4.21 in the weakest plane. Where the two programs differ: on the Tube fin rocket hpr’s is 1.08 calibres below OpenRocket’s, tube fins; on the three-stage example’s three flights 0.039 to 0.058 below, cause not yet sized, #185; and on the five of Pods–airframes and winglets 0.071 to 0.076 above, the flattering side, #325 and #326; and on the three of Pods–powered with recovery deployment 0.070 above, also not yet traced. On 35 flights of private designs it is from 0.017 lower to 0.11 higher, cause not yet traced (Accuracy). Near Mach 1 hpr puts the Arcas Robin’s center of pressure up to 2.36 calibres behind the wind tunnel’s (Normal force through Mach 1), so there its flight margin reads high.The Mach number moves the center of pressure. On a rocket with fins only at the tail it generally moves aft up to where supersonic theory starts for the fins, Mach 1.2 or later: their slope grows, and from Mach 0.8 their own center moves aft too. Past that their slope falls and the body’s lift can grow, and the center of pressure mostly moves forward again (Your rocket’s center of pressure). Valetudo is slow: at its rail exit, at Mach 0.051, both margins read 3.09 calibres.
Both margins are the weakest direction’s. The air can cross a rocket from any direction around
its axis, and for most rockets the margin is the same in every one. A fin set of one or two fins
breaks that: it carries Σ sin²(φ − θ_k) of its force in the plane at roll angle φ, for fins at
angles θ_k (Niskanen 2009, eq. 3.51, as in Fins). So a rocket with one has a margin for
each direction, and hpr gives the least, the direction it was found in as roll_rad. Until
#329, hpr’s margins were the 0° direction’s
alone, which can be the strongest. A rocket whose fin sets each have three or more fins, or that
flies a normal-force table, keeps its one margin, bit for bit.
A worked case: the sustainer of OpenRocket’s Pods–powered with recovery deployment example has
a single fin at 0° and a two-fin strake set at 90°. At Mach 0.2, after the booster drops, its
margin is +1.86 calibres with the air crossing at 0°, where the single fin lies edge-on and the
strakes carry the force. At 45° it is +0.17. At 90° the strakes lie edge-on and only the single
fin works: −3.44, unstable. OpenRocket 24.12, asked for that sustainer at 90° in a scratch run
(not committed), puts its center of pressure at 0.2954 m with C_Nα 3.49; hpr’s are 0.2993 m and
3.47. For one of its staged configurations, [C6-7; B6-0], hpr sim‘s least margin was +1.56
calibres and is now −4.21, at 0.86 s. That is the sustainer’s margin at the split itself, the
moment the booster drops, with its own motor just lit and still full and the air at Mach 0.07;
the −3.44 is a later instant, at Mach 0.2. In the conditions of OpenRocket’s record, both programs’ flights of that
configuration turn over before apogee: its site is at 28.61° N, where the Earth’s rotation tips
hpr’s flight, and hpr’s angle of attack passes 90° at 2.25 s. At hpr sim’s default site, on the
equator, nothing tips hpr’s flight: it stays upright and reaches 251.3 m
(the format guide).
hpr finds the least in closed form, not by a scan
(weakest_margin). Each part’s slope varies as
a + b cos 2φ + c sin 2φ, so the net slope S(φ) and its moment M(φ) = Σ C_Nα,i x_i do too.
Three directions, 0°, 60° and 120°, give each sum’s a, b and c. The center of pressure M/S
is foremost where
P sin 2φ + Q cos 2φ + R = 0, P = m₀s₁ − m₁s₀, Q = m₂s₀ − m₀s₂, R = m₂s₁ − m₁s₂
with m and s the coefficients of M and S. The margin’s conditioning (below) is checked at
its own worst direction the same way. Each root and the 0° direction are evaluated through the
model, and a direction where no margin can be given wins over any number.
Both margins leave out the angle of attack. In hpr,
body lift grows with the angle and acts at each body’s side-view centroid, and
the center of pressure moves toward it. Where that centroid lies ahead of the zero-angle center of
pressure, the margin at an angle shrinks; on a long body with small fins it can lie behind, and the
margin grows. No test pins either direction. But the angle is not a steady property of
the rocket. Valetudo leaves its rail 17.2° off the oncoming air in the example’s 5 m/s crosswind,
and near apogee the angle swings toward 90° as the rocket slows and tips over. If the least margin
followed the angle, it would land wherever hpr chose to stop counting large angles, and hpr models
no fin stall that could say where that is. For the margin at a given
angle, call margin with
Flow::new(mach, angle_rad, roll_rad).
The watcher keeps both margins in FlightMetrics::stability(). The series starts at the rail exit
and ends at apogee or when a recovery device opens, whichever comes first. Before the rail exit the
rail holds the rocket, so its margin says nothing about how it flies.
The summary’s least margins cover the same span. Each is looked for inside every step, as a peak is (Peaks): wherever the margin is defined at a step’s start, middle and end and the parabola through them bends up with its bottom inside the step, a golden-section search finds the bottom. A later least replaces an earlier one only when lower by more than rounding, so a flat least keeps its first time. A margin with a sharp corner near a step’s end can still hide from the parabola.
- On four test flights, and in the example, both leasts come at the rail exit, where the rocket is heaviest and its center of mass furthest aft. Flown in steps of at most 1 ms, those flights give the same leasts to a millionth of a calibre.
- On a two-stage test flight the least flight margin comes inside a step, after the split. There it matches a scan of every step at 201 points to 1e-7 calibres, and is never above it.
After a powered separation the margins are the sustainer’s, in its own diameter. The split has two entries in the series: the stack’s, then the sustainer’s.
When there is no margin
As the net slope Σ C_Nα,i goes to zero, the air’s loads become a pure couple: a turning moment
with no line of action. The quotient x_cp then runs away. That is how Loft came to publish
margins of ±12 to 15 calibres.
hpr judges the net slope against the sum of its terms’ sizes:
κ = Σ |C_Nα,i| / Σ C_Nα,i
Each part the model adds up acts at a station x_i on the rocket, within its length L. The
center of pressure then lies within κ L of every station. So a fractional error ε in one
part’s slope moves the center of pressure by at most about ε κ² L (to first order). A part that
is a pure couple on its own (below) has no station, and κ doesn’t count it.
- A rocket whose parts all push the same way has
κ = 1, and a 1% error moves its center of pressure by at most 1% of its length. All 13 designs invalidation/designs/stay below 1.5 at Mach 0, 0.3, 0.8, 1.2 and 2 and at angles of attack of 0°, 5°, 10° and 20°. - At
κ = √10 = 3.16, a 1% error in one slope can move the center of pressure by a tenth of the rocket. Past that, or when the net slope is not positive, hpr gives no margin and no center of pressure:None. The limit is a chosen bound on that sensitivity, not a measurement.
The pitch-moment slope about the center of mass is always given, because it stays finite:
C_mα = −(Σ C_Nα,i x_i − x_cg Σ C_Nα,i) / d
A negative C_mα turns the nose back into the wind. With a margin defined, C_mα = −C_Nα · margin.
A worked case, with no fins, pinned by a test:
- The rocket is a conical nose 0.3 m long of radius
R= 0.05 m, a 0.5 m tube, and a conical boattail 0.4 m long, narrowing to a radiusr. Sod= 0.1 m. - Barrowman gives the nose a slope of 2 at 0.2 m. The boattail gets
2((r/R)² − 1), at0.8 + (0.4/3)(1 + 1/(1 + R/r))m. - So
κ = 2/ρ² − 1, withρ = r/R. The margin is given forρ ≥ 0.6932, an aft radius of 34.66 mm or more. - The center of mass is at 0.5 m.
| Boattail’s aft radius | Net slope | κ | Margin | C_mα |
|---|---|---|---|---|
| 40 mm | 1.28 | 2.13 | −7.46 cal | +9.55 |
| 35 mm | 0.98 | 3.08 | −11.2 cal | +10.98 |
| 34.5 mm | 0.952 | 3.20 | none (the quotient: −11.7 cal) | +11.11 |
| 21.5 mm | 0.370 | 9.82 | none (the quotient: −37.1 cal) | +13.72 |
| 5 mm | 0.020 | 199 | none (the quotient: −741 cal) | +14.82 |
With no fins, every one of these rockets is unstable: its center of pressure lies ahead of its
center of mass, and C_mα is positive. The cut is not about how large the margin is. The 35 mm and
34.5 mm rows have margins near −11 calibres; what differs is whether the quotient can be trusted.
A component can be a pure couple on its own. A step down in radius followed by a flare back up
cancels its own slope and still turns the rocket. C_mα keeps its moment, which a test also
checks.
The largest angle of attack
max_angle_of_attack_rad is the largest angle between the rocket’s axis and the air flowing past
it, in radians, over the span the envelope’s high-angle flag watches
(Validation plan: the operating envelope;
ADR-179):
- from more than 1 s after the rail exit, as the first second off the rail is the expected transient in a crosswind;
- to apogee or the first deployment, the span of the least margins above;
- only at instants where a 15° angle would give a
normal force of at least a fifth of the rocket’s weight:
q (π d²/4) |C_Nα| · 15° ≥ 0.2 m g, withqthe dynamic pressure,dthe reference diameter andC_Nαthe flight margin’s normal-force slope.
The angle is read at each step’s start, middle and end, without the search between them that the
peaks have, so an excursion inside one step can be missed. Above 15°, the flight’s
envelope_flags include high_angle_of_attack.
The floor is there because near apogee every flight whose path turns over passes 15°, while the air
is too weak to turn it: gravity does. Valetudo, one of RocketPy’s examples, launched off an 84° rail
in calm air, passes 15° about 0.9 s before apogee, at 16.7 m/s through the air, a little faster
than its 16.2 m/s rail exit. There a 15° angle would give a normal force of 1.0% to 1.5% of its
weight, so an error in the aerodynamics at that angle barely moves it. A wind layer met at speed is
different: Valetudo climbing into 30 m/s of wind 300 m up passes 15° where that force is 62% of its
weight, and the flag is raised. On the tests’ RocketPy rockets, turn-overs give 0.7% to 5.0% and
wind layers met at speed 62% to 141%; the fifth, chosen rather than measured, sits between. The
tests a_tilted_calm_climb_passes_15_degrees_only_with_too_little_force,
a_wind_layer_met_at_speed_raises_the_high_angle_flag and
a_wind_layer_met_in_a_fast_coast_raises_the_high_angle_flag (in hpr-sim’s metrics.rs) fly
these cases.
Unstable under power
A flight that is unstable while a motor burns raises one of two flags, and its apogee is not a prediction (#335). Unstable means the air turns the rocket away from its path instead of back. Where it ends up then depends on small disturbances, and on aerodynamics that hold only at small angles of attack. hpr still flies it, and the flag changes no number: it marks the flight’s path and apogee as not a prediction. A flight raises at most one of the two flags.
unstable_under_power: the static margin falls below zero while a motor burns. The center of pressure is then ahead of the center of mass.unstable_without_margin: where hpr can give no static margin while a motor burns (When there is no margin), the pitching moment’s slopeC_mαis above zero. It is raised only when the first flag isn’t.
min_powered_static_margin_cal is the margin the first flag reads. It is the least static margin,
in the weakest plane (Stability margins), from the rail exit to apogee or
the first deployment, over the moments a motor burns. It is found inside steps, as the other least
margins are. A step counts when the thrust at its middle is above zero. hpr ends a step at every
ignition, thrust-curve point and burnout, so a motor that burns at a step’s middle burns through
all of it, and the margin at the burnout itself counts. A margin of exactly zero raises nothing;
any margin below it does.
max_powered_moment_slope_per_rad is what the second flag reads. Where the parts’ normal-force
slopes nearly cancel, or their sum is not positive, hpr gives no margin, since the center of
pressure would be noise. The moment about the center of mass is still well defined there:
C_mα = −(Σ C_Nα,i x_i − C_Nα x_cg) / d
with C_Nα,i and x_i the normal-force slope and station of part i, C_Nα their sum, x_cg
the center of mass, every station aft of the nose tip, and d the reference diameter. A negative
C_mα turns
the rocket back toward its path; a positive one turns it away. hpr keeps the largest C_mα at
each powered step’s start, middle and end where the margin is undefined, without searching
between them. A C_mα of exactly zero raises nothing; any above it does. Without this flag, a
rocket so unstable that its margin is undefined would raise no warning at all, while a milder one
would.
Only the moments under power count. A rocket that turns then is driven along its new heading by
its own motor, so the path flown follows the turn. A rocket that is unstable only in the coast,
after its motors burn out, raises neither flag: it has no thrust to carry it off its path, and
the least static margin over the whole span, min_static_margin_cal, still prints its margin
there.
For example, OpenRocket’s example Pods–powered with recovery deployment, flown on [C6-7; B6-0]
or [C6-7; 2× A3-4, B6-0], separates at 0.86 s. The sustainer left has a static margin of −4.21
calibres with its C6 still burning. On [C6-7; B6-0]:
static margin 2.05 calibres off the rail; least -4.21 calibres, at 0.86 s, before apogee
apogee 251.3 m above the site at 5.74 s, not a prediction: unstable under power
warning: unstable: the static margin falls to -4.21 calibres at 0.86 s while a motor burns: the rocket is unstable under power, so the air turns it away from its path rather than back, and its apogee is not a prediction
For the second flag, take RocketPy’s Valetudo with its fins taken off and a conical tail that
narrows from the body’s 40.45 mm radius to 22 mm over 0.08 m. At the rail exit its net slope is
0.59 per radian against a sum of sizes of 3.41, too small for a margin, but C_mα is +43.9 per
radian:
apogee 399.1 m above the site at 8.46 s, not a prediction: unstable under power
warning: unstable: while a motor burns, the pitching moment turns the rocket away from its path (C_mα +43.9 per radian at 0.25 s) where hpr can give no static margin: the rocket is unstable under power, so its apogee is not a prediction
With the tail ending at 30 mm instead, the margin is defined, −34.37 calibres, and the first flag
fires in its place. These figures are from a run on 2026-10-06; the test
(sim_flags_a_rocket_unstable_where_it_has_no_margin) pins only the flag and a slope above 1.
hpr sim prints the warning: unstable: line and marks the apogee for either flag, and its JSON
lists the flag under flags; the library has it in FlightSummary::envelope_flags, and Python in
Flight.envelope_flags. When this was written (2026-10-06), none of the 25 public .ork files
and 12 validation designs that hpr sim flies offline raised either flag, on their default
configurations off an 85° rail in calm air and in 8 m/s of wind. No test or report pins that
count.
What it leaves out:
- A rocket unstable only after its motors burn out raises no flag, as above.
- It reads the static margin and
C_mαat Mach 0, not at the flight’s Mach number. C_mαis read only at each step’s start, middle and end, not searched between: a moment slope above zero for less than half a step, with no margin, can pass unseen.- A flight that tips over above 15° raises the high-angle flag (The largest angle of attack) as well; a stable rocket in a strong wind can raise that one alone.
The tests the_powered_margin_counts_only_while_a_motor_burns,
a_moment_slope_without_a_margin_counts_only_while_a_motor_burns and
a_rocket_without_fins_is_unstable_under_power (in hpr-sim’s metrics.rs), and
the_stability_flag_starts_just_below_a_zero_margin and
the_moment_flag_starts_just_above_zero_and_only_without_the_margin_flag (in envelope.rs) pin
it: a margin of −2⁻³⁰ calibres at a burnout raises the flag, +2⁻³⁰ doesn’t, nor does −1 calibre
in the coast that follows; with no margin, a C_mα of +2⁻³⁰ per radian under power raises the
second flag, −2⁻³⁰ doesn’t, nor does +2⁻³⁰ after the burnout. In hpr-cli,
sim_flags_a_rocket_unstable_where_it_has_no_margin flies the Valetudo example above.
Optimum ejection delay
The optimum delay is the time from a motor’s burnout to apogee: the delay that fires its charge at
the top. optimum_delays flies the rocket again
with every recovery charge held, so that it coasts to the apogee it would reach untouched. So the
answer belongs to the rocket, its motors and its air, not to the delay flown. A charge that fires
too early cuts the coast short. Loft’s optimum then came out too short as well, which advised an
even shorter delay. hpr gives the same optimum for delays of 1 s and 20 s.
- A separation with nothing ahead of it left to burn is part of the recovery, so it is held too. A two-stage rocket that separates some seconds after its last burnout gets the same optimum for a 3 s delay as for a 30 s one.
- A powered separation still happens. The motors in the booster it drops have no optimum: their charges fire in the booster, which never reaches the sustainer’s apogee.
- A motor that burns out after the apogee, or never lights, has none either. A flight that never
reaches an apogee (it never lifts off, or hits its time cap) gives
None.
Landing points
A landing is where the center of mass came back down to the site’s height. It is given as a WGS 84 latitude and longitude, as east and north meters from the site in its local frame, and as the speed at the ground hit. The flight’s own landing is the stack’s (the whole rocket before any separation), or after a powered separation the sustainer’s. Each separated body that lands has its own landing, with its body number: 0 keeps the nose, and 1 is the stages aft of the split.
A flight that did not land has no landing and no ground-hit speed: None. A test checks this for a
flight stopped by its time cap, and the JSON it writes, where the missing values are null.
Tests
In crates/hpr-sim/src/metrics.rs, unless named otherwise:
| Test | What it pins |
|---|---|
static_margin_undefined_when_cn_alpha_near_zero | The boattail table above, against Barrowman by hand, to 1e-9 relative, on both sides of the limit (L33) |
ordinary_rockets_keep_their_margin | All 13 validation designs keep their margin at Mach 0, 0.3, 0.8, 1.2 and 2 and at 0°, 5°, 10° and 20°, with κ below 1.5 |
a_pure_couple_keeps_its_moment | The step and flare above: C_mα against the hand value, to 1e-9 |
a_normal_force_table_gives_its_own_margin | A table’s margin and C_mα against its own numbers |
the_least_margin_is_the_weakest_plane_s | Six layouts of a single fin and a two-fin set, at Mach 0.1, 0.6 and 1.5 and three centers of mass: the least margin is never above any of 3,600 scanned planes’, and within 1e-5 calibres of the scan’s least, including where the weakest plane lies between the fins’ planes; fin sets of three or more fins keep the margin along the axis, bit for bit |
peak_acceleration_is_analytic_and_excludes_opening_shock | The boost’s peak in a vacuum against the hand value, to 1e-6; a 100 Hz finite difference reads it more than 1% low; an opening shock over three times the boost’s is kept apart (L34) |
unlanded_flight_has_no_ground_hit_speed_and_outputs_name_datum | None and null for an unlanded flight; the launch height against the rail’s geometry, to 1e-9 relative (L35) |
optimum_delay_independent_of_flown_delay | Delays of 1 s and 20 s give the same optimum, equal to a flight with no recovery (L94) |
peaks_are_refined_inside_steps | On three rockets, max q and top Mach are above every row of a 1 ms record, and the record’s best row is within 0.01% of them; on Valetudo max q comes before top speed, and top speed before top Mach |
landings_are_placed_on_the_ellipsoid | A landing more than 100 m downwind, against the radii of curvature at the site, to second order |
stability_is_kept_from_rail_exit_to_apogee | The series’ ends; the static margin at the rail exit and, in a crosswind, the flight margin at the flight’s Mach number, against the model and the masses directly, to 1e-12; the least flight margin is at or below every entry |
least_margins_do_not_depend_on_where_steps_end | Valetudo in calm air off a vertical and an 84° rail and in a crosswind, and Prometheus: both leasts are at the rail exit, and agree with steps of at most 1 ms to 1e-6 calibres |
a_least_margin_between_step_ends_is_found | A margin of 2 + (t − 0.37)² across a step: the search finds 2 at 0.37 s; a margin falling across the step, or undefined in its middle, is not searched |
a_summary_needs_the_flight_it_watched | A watcher refuses a flight when it missed some of its steps or also watched another flight, naming the steps it saw, and sums up one that ended without a step; a flight started in the air has no launch height, and one started on its way down keeps no stability |
the_powered_margin_counts_only_while_a_motor_burns | A margin falling 1 calibre a second across a burnout at 1 s: −2⁻³⁰ calibres at the burnout raises the flag, +2⁻³⁰ doesn’t, nor does −1 in the coast after; a least inside a powered step is found by the search; a NaN under power is kept, one in the coast isn’t; a margin of −10⁻¹³ replaces +10⁻¹³ though the two are closer than the tie |
a_moment_slope_without_a_margin_counts_only_while_a_motor_burns | With no margin, a C_mα of +2⁻³⁰ per radian under power raises unstable_without_margin, −2⁻³⁰ doesn’t, nor does +2⁻³⁰ after the burnout at 1 s; a NaN under power is kept; a slope where the margin is defined doesn’t count; a margin below zero as well raises only unstable_under_power |
a_rocket_without_fins_is_unstable_under_power | Valetudo with its fins taken off raises unstable_under_power at or before its burnout; stock, its least margin under power is above 1 calibre and it raises nothing |
envelope::tests::the_stability_flag_starts_just_below_a_zero_margin | The flag from the least negative f64 down; none at 0, −0 or above |
envelope::tests::the_moment_flag_starts_just_above_zero_and_only_without_the_margin_flag | unstable_without_margin from the least positive f64 up, and for a NaN; none at 0, −0 or below; never beside unstable_under_power |
staging::tests::metrics_follow_a_powered_separation | Both landings; the sustainer’s diameter after the split, with its own entry there; both leasts against a scan of every step at 201 points, to 1e-7 calibres, the flight margin’s inside a step; no optimum for the booster’s motor |
staging::tests::a_held_flight_holds_a_separation_after_the_last_burnout | A split after the last burnout is held: the same optimum for delays of 3 s and 30 s |
References
- J. Kiefer, “Sequential minimax search for a maximum”, Proceedings of the American Mathematical Society 4(3), 502–506, 1953. The golden-section search.
- J. S. Barrowman and J. A. Barrowman, “The theoretical prediction of the center of pressure”, NARAM-8 research and development report, 1966. The slopes and stations, as Aerodynamics uses them.
- S. Niskanen, Development of an Open Source model rocket simulation software, MSc thesis, Helsinki University of Technology, 2009, eq. 3.51. A fin’s share of its force in the plane the air crosses in, which makes a one- or two-fin set’s margin depend on that plane.
The decision behind these choices is ADR-077, from the M1.10a milestone.
Fin flutter
In short
- What it models: whether a fin may flutter, meaning its bending and twisting feed each other until the fin shakes itself apart. hpr gives two readings of one criterion. The first is Martin’s own chart check: is the fin on the flutter side of the line his flight data draws? The second is the ratio of the criterion’s flutter speed to the rocket’s airspeed at the flight’s peak dynamic pressure. The first decides; the second alone is not a safe line.
- Sources: D. J. Martin’s criterion in NACA TN 4197 (1958), eq. 18, and his figure 3, which separates missile and wind-tunnel wings that fluttered from those that didn’t (References). The fin’s stiffness enters as its shear modulus; hpr has one, with its source, for 14 of its built-in materials.
- How well it is validated: hpr reproduces Martin’s formula and both of his worked examples, including his verdicts on three metals and his titanium design (Tests). His safe/unsafe line is a band that hpr measured off his printed chart: 0.25 to 0.31 (The line in Martin’s data). His unfailed wings flew to at least Mach 1.3; nothing here is checked against a hobby rocket. Where the source allows two readings, hpr takes the one with the lower flutter speed. That doesn’t make the whole result conservative: nothing shows that it is.
- What it leaves out: sweep, and how the fin is mounted: Martin’s wings were clamped at the root, and a fin glued to a tube is less stiff there. Also left out are stall flutter at high angles of attack, Mach-number effects such as a dip near Mach 1, the rocket body’s own bending, and fins that aren’t trapezoids. G10/FR-4, the commonest fibreglass fin sheet, has no built-in modulus (Shear moduli).
What flutter is
Air pushing on a fin twists it a little. The twist changes the fin’s angle to the air, so the push changes, and the fin bends. Below a certain speed the fin’s stiffness damps this out. Above it, each cycle feeds the next, and a fin can fail within a second. The speed depends on the fin’s shape and thickness, how stiff its material is in shear, and the air it flies through.
The criterion
Martin starts from Theodorsen and Garrick’s flutter speed for a wing that bends and twists (his
eq. 1, from their NACA Report 685 of 1940). He reduces it to a few numbers from one fin’s outline.
For a trapezoidal fin with root chord c_r, tip chord c_t, span s and thickness t:
| Symbol | Meaning | For a trapezoid |
|---|---|---|
A | Panel aspect ratio: span over the chord halfway out | 2s / (c_r + c_t) |
λ | Taper ratio: tip chord over root chord, 0 to 1 | c_t / c_r |
t/c | Thickness ratio | t / c_r |
G_E | The fin’s effective shear modulus, Pa | the material’s (Shear moduli) |
p, a | The air’s static pressure and speed of sound | from the atmosphere |
ε | Where the section’s mass sits: this fraction of the chord behind the quarter chord | 0.25, at mid-chord |
γ | Air’s ratio of specific heats | 1.4 |
Eq. 18 gives a flutter speed V_f through a denominator D, in pascals:
(V_f / a)² = G_E / D, D = (24 ε γ / π) · p · K · (λ + 1)/2, K = A³ / ((t/c)³ (A + 2))
Martin prints the constant in pounds per square inch (psi) at sea-level pressure p₀:
24 · 0.25 · 1.4 / π · 14.696 psi = 39.29 psi, which he rounds to 39.3. His X (eq. 19) is
39.3 K psi, and D = X · (λ + 1)/2 · p/p₀. The constant is derived, not fitted. Stiffer or
thicker fins flutter faster: V_f grows as √G_E and as (t/c)^1.5. At a fixed speed of sound,
thinner air raises it as 1/√p.
A flutter dynamic pressure. The air enters only through ρ a² = γ p, and the dynamic pressure
is q = ½ ρ V². So the criterion fixes a dynamic pressure at V_f, the same at every height:
q_f = π G_E / (24 ε K (λ + 1))
A fin flying at dynamic pressure q is below V_f by the ratio V_f / V = √(q_f / q). That makes
the flight’s least ratio the one at its peak dynamic pressure, “max q”, which
Flight metrics already finds between the integrator’s steps.
The line in Martin’s data
Eq. 18’s V_f is not the speed at which a fin is known to flutter. Martin plots D against G_E
for missiles and wind-tunnel models (his figure 3). Wings that fluttered or failed lie above a
shaded band, and wings that flew to at least Mach 1.3 without known failure lie below it.
hpr measured the band on a 250 dots-per-inch scan of the figure. Both log axes were calibrated on
their tick marks, and the band’s edges were traced in 69 pixel columns from G_E = 0.05 to
10 × 10⁶ psi. The band runs at D / G_E = 0.25 to 0.31 all along that range, from wood to steel. In eq. 18’s terms that is
(V_f/a)² = 3.2 to 4.0, so Martin’s line sits where V_f is 1.8 to 2.0 times the speed of sound.
hpr calls this ratio D / G_E the figure 3 ratio:
Figure 3 ratio D / G_E | Martin’s data |
|---|---|
| above 0.31 | mostly wings that fluttered or failed (a few that didn’t lie there too) |
| 0.25 to 0.31 | the band: marginal |
| below 0.25 | wings that flew to at least Mach 1.3 without known failure |
Martin takes p where the wing flies. At the launch site’s pressure, the highest a flight sees, the
ratio is at its largest. Figure 3’s axis runs from 0.05 to 20 × 10⁶ psi (0.34 to 138 GPa) in G_E;
balsa’s modulus is left of it, so its ratio is an extrapolation.
Which reading decides. The figure 3 ratio does: it is Martin’s own check, and a fin must be
below the band. V_f / V alone is not a safe line, at 1 or at any fixed number. At the flight’s
max q the two are tied by D / G_E = 1 / (M · V_f/V)², with M the Mach number there, so the
band is at V_f / V from about 1.8/M to 2.0/M. A fin is below the band only if V_f / V is above
about 2.0/M: 1.5 at Mach 1.3, but 4.0 at Mach 0.5.
Martin’s data show nothing about a fin above the band on a rocket slower than Mach 1.3: treat it as
not shown to be safe.
Loft’s mistake. Loft, hpr’s predecessor, wrote the constant as 1.337 · (λ + 1)/2 per psi,
half of 39.3 / 14.696 = 2.674. So its flutter speeds were √2 too high, about 41%, on the unsafe
side (L32, a lesson from Loft).
Martin’s worked examples
Martin gives two examples (pp. 6–7). The first is a wing with A = 2, 4% thick, untapered and
ground-launched. He reads its X off his figure 4 as “about 1.25 × 10⁶ psi” and judges the wing by
material. The second picks titanium and holds figure 3’s ordinate to 0.8 × 10⁶ psi, well under
titanium’s modulus, “to allow a reasonable margin of safety”. It then asks how thick the wing must
be at each aspect ratio.
| Example | Martin | hpr, from eq. 19 | Difference |
|---|---|---|---|
X for A = 2, 4% thick | “about 1.25 × 10⁶” psi | 1.228 × 10⁶ psi | −1.8% |
Titanium at 0.8 × 10⁶ psi, A = 1 | 2.5% thick | 2.54% | +1.6% |
Same, A = 2 | 4.5% | 4.61% | +2.4% |
Same, A = 3 | “about 6.5”% | 6.43% | −1.1% |
Martin read these off a log-scale chart and printed them on grids of 0.05 × 10⁶ psi and half a percent (his thicknesses all end in .5). Each of hpr’s values rounds to his on that grid.
His verdicts on the first wing are the margin half of the example; the titanium row is his second example, at the ordinate he chose. hpr checks them with the moduli Martin marks on figure 3’s axis, each a small box read off the same scan:
| Material | Martin’s mark, 10⁶ psi | Figure 3 ratio | Martin says | Against the band |
|---|---|---|---|---|
| Magnesium | 2.40 to 2.63 | 0.47 to 0.51 | “in the flutter region” | above |
| Aluminium | 3.82 to 4.28 | 0.29 to 0.32 | “marginal” | on it |
| Steel | 8.92 to 11.3 | 0.11 to 0.14 | “probably safe” | below |
| Titanium, his second example at 0.8 × 10⁶ psi | 5.78 to 6.34 | 0.13 to 0.14 | his design, with “a reasonable margin of safety” | below |
A rocket’s fins
The example program fin_flutter (in crates/hpr/examples/) takes the repository’s synthetic
54 mm rocket. Its three fins are 3.2 mm thick, with a 150 mm root, a 60 mm tip and a 70 mm span.
It flies the rocket on an I175 motor from a site 200 m up, and prints:
Panel: aspect ratio 0.667, taper ratio 0.400, thickness ratio 0.0212
Max q: 72455 Pa at 2.09 s, 432 m above the pad
Top speed: 355 m/s at 2.10 s; top Mach number: 1.05 at 2.10 s
Martin's band (figure 3): D/G_E from 0.25 to 0.31
material G (GPa) q_f (kPa) V_f 0 m (m/s) V_f 3 km (m/s) V_f/V at max q D/G_E
aluminum_6061 26.200 836.3 1169 1356 3.40 0.083
carbon_fiber 4.826 154.1 502 582 1.46 0.450
birch_plywood 0.750 23.9 198 229 0.57 2.893
basswood 0.511 16.3 163 189 0.47 4.246
balsa 0.138 4.4 85 99 0.25 15.680
The columns V_f 0 m and V_f 3 km are eq. 18’s flutter speed in standard air at sea level and
3 km above it. D/G_E is the figure 3 ratio at the launch site’s pressure.
- Aluminium passes both readings: its figure 3 ratio is well below the band, and at max q the
rocket flies at under a third of
V_f. - Carbon fibre at 3.2 mm has
V_f / Vof 1.46, but its figure 3 ratio, 0.45, is above the band, where most of Martin’s wings fluttered or failed: not shown to be safe. Its modulus is a unidirectional ply’s of one aerospace prepreg; a ±45° layup of it would be stiffer, but wet-laid or woven hobby sheet may be softer. - Plywood, basswood and balsa fins of this size fail both: at max q the rocket flies at 1.75
times plywood’s
V_f.
CI checks that the program still prints exactly this.
In code, with a FlutterPanel (its API page
has a worked example that CI runs):
// `fin_set` is a design's `FinSet`; `summary` a flight's `FlightSummary` (Flight metrics).
let panel = FlutterPanel::of_fins(&fin_set)?;
let g = materials::shear_modulus_of(&fin_set.material).unwrap().shear_modulus_pa;
let chart = panel.figure_3_ratio(g, launch_pressure_pa)?; // against FIGURE_3_BAND
let margin = panel.margin(g, &summary)?; // V_f/V at max q; `None` if it never flew
FlutterPanel::new takes A, λ and t/c directly. Martin’s figure 4 covers A from 0.5 to 3
and t/c from 1% to 10%; the example’s 0.667 is inside, and outside that range the numbers are an
extrapolation.
Readings where the source leaves room
Most of these give the lower of the flutter speeds the source allows. Two can go the other way: an airfoiled fin’s modulus, and a booster’s max q.
- Thickness ratio at the root. Martin’s wings keep one thickness ratio from root to tip. A hobby fin keeps one thickness, so its ratio grows toward the tip. hpr takes the root’s, the smallest.
- A solid fin’s
G_Eis its material’sG. Martin says a solid wing of aluminium plots at aluminium’s modulus (p. 6), and hpr does the same. His definition,G_E = 6 J G / (c t³)(eq. 12), assumes a thin airfoil’s torsion constantJ ≈ c t³/6(eq. 10). A flat plate’s isc t³/3, which would doubleG_Eand raise the flutter speed by√2. hpr keeps the lower reading. The exception runs the other way: for a fin with an airfoil section, eq. 12 gives0.946 G, so hpr’sV_ffor such a fin is up to 2.7% high. - Martin’s taper factor. His derivation carries a factor
1/(f₁² f₂²). Heref₁corrects the twisting frequency for taper (his eq. 8) andf₂gives the chord three-quarters of the way out (his eq. 14). He replaces the factor with(λ + 1)/2. The two agree atλ = 1and are 3% apart atλ = 0. Between,(λ + 1)/2is up to 47% larger (atλ ≈ 0.31), which lowersV_fthere by up to 17.5%. hpr keeps his form, since his figure 3 was drawn with it. - Every fin set sees the whole flight’s max q. A booster’s fins leave at the separation, so the
flight’s max q can come after they are gone. Their true
V_f / Vis then at least the one given, as long as the booster’s own dynamic pressure after the separation stays below the flight’s peak. hpr doesn’t check that.
Shear moduli
The criterion needs the fin’s shear modulus in its own plane. hpr_design::materials::SHEAR_MODULI
gives it for 14 built-in materials, each with its source, page and the web address it was read
from, and shear_modulus_of finds a design’s copy of a built-in material by its name and density. Where a source gives a
range, the lower value is kept. Metals are given in ksi (1000 psi) and Msi (10⁶ psi); 1 Msi is
6.895 GPa.
| Material | G, GPa | Source | Basis |
|---|---|---|---|
| Aluminium 6061 | 26.2 | MIL-HDBK-5J, Table 3.6.2.0(b1): 3.8 × 10³ ksi | stated |
| Aluminium 7075 | 26.9 | MIL-HDBK-5J, Table 3.7.6.0(b1): 3.9 × 10³ ksi | stated |
| Steel (plain carbon) | 75.8 | MIL-HDBK-5J, Table 2.2.1.0(b): 11.0 × 10³ ksi | stated |
| Titanium Ti-6Al-4V | 42.7 | MIL-HDBK-5J, Table 5.4.1.0(b): 6.2 × 10³ ksi (TIMET: 6.2 and 6.66 Msi) | stated |
| Carbon fibre/epoxy | 4.83 | NCAMP (a composite-material qualification programme) report on Hexcel 8552 AS4 tape: in-plane G₁₂ 0.70 Msi, room temperature | stated |
| Birch plywood | 0.750 | Riga Wood Plywood Handbook, Table 4.11: panel shear | stated |
| Birch (yellow) | 1.04 | Wood Handbook ratio × bending modulus × 1.10 | derived |
| Oak (northern red) | 1.11 | same | derived |
| Maple (sugar) | 0.873 | same | derived |
| Spruce (Sitka) | 0.725 | same | derived |
| Basswood | 0.511 | same | derived |
| Balsa | 0.138 | same | derived |
| Acetal (Delrin) | 1.06 | data sheet E / (2(1 + ν)) | derived |
| Nylon 6/6 | 0.490 | data sheet E / (2(1 + ν)), conditioned | derived |
How the derived values are made:
- Woods:
G_LTis the shear modulus in the plane along the grain and along the growth rings. hpr takes the Wood Handbook’s ratioG_LT / E_L(Table 5-1) and multiplies it by the wood’s bending modulus at 12% moisture raised by 10%, which is how the table’s footnote says to estimate the stiffness along the grain,E_L.G_LTis the smaller of the two in-plane ratios for every wood listed. Eastern white pine is not in Table 5-1, so it has none. Martin marks solid wood at 0.070 to 0.120 × 10⁶ psi (0.48 to 0.83 GPa); the handbook’s birch and oak are above that. - Plastics: an unfilled plastic taken as isotropic (the same in every direction), from its data
sheet’s tensile modulus
Eand Poisson’s ratioν. Nylon soaks up water. Its conditioned value, measured after it has, is less than half the dry one. - Carbon fibre: a unidirectional ply’s in-plane
G₁₂. That is a 0/90 laminate’s in-plane shear modulus. Plies at ±45° raise it several times, so for such a laminate this reads low.
No source found states a shear modulus for G10/FR-4, the commonest fibreglass fin sheet: its data
sheets give flexural moduli only. There is none for PLA, ABS, PETG, polycarbonate or acrylic
either. For those, pass your own G_E from a measurement or your supplier. A woven glass/epoxy
laminate is not isotropic, so E / (2(1 + ν)) doesn’t apply to it.
Tests
In crates/hpr-sim/src/flutter.rs, unless named otherwise:
| Test | What it pins |
|---|---|
flutter_denominator_matches_tn_4197_eq_18 | The constant against Martin’s 39.3 psi, and D against eq. 18 on three panels at two pressures, to his rounding; twice Loft’s constant (L32) |
martins_worked_examples | Both of Martin’s examples on his grid, and his three verdicts and his titanium design against the band, through figure_3_ratio (the tables above) |
scaling_laws_in_thickness_shear_modulus_and_pressure | V_f as (t/c)^1.5, √G_E, 1/√p and a, to 1e-12; q_f the same in three atmospheres |
taper_factor_against_the_frequency_factors | (λ + 1)/2 against 1/(f₁² f₂²): 3% at λ = 0, at most 47% at λ = 0.308 |
the_margin_is_the_flights_least_at_its_peak_dynamic_pressure | On the flight of Valetudo (a RocketPy example rocket), the margin is at max q and at or below every row of a 1 ms record; V_f from each row’s pressure and speed of sound agrees with √(q_f/q), to 1e-12; no peak gives None, a peak that isn’t a number is refused |
a_trapezoidal_fin_set_gives_its_panel, out_of_range_inputs_are_refused | A fin set’s A, λ and t/c; elliptical and reverse-tapered fins, a taper outside 0 to 1 and inputs that aren’t positive are refused by name, from code and from JSON |
hpr_design::materials::tests::shear_moduli_reproduce_the_sources | Each built-in modulus against its source’s numbers |
References
- D. J. Martin, Summary of Flutter Experiences as a Guide to the Preliminary Design of Lifting Surfaces on Missiles, NACA TN 4197, 1958. Appendix, eqs. 1 to 19, pp. 11–15; examples, pp. 6–7; figures 3 and 4, p. 19.
- T. Theodorsen and I. E. Garrick, Mechanism of Flutter: A Theoretical and Experimental Investigation of the Flutter Problem, NACA Report 685, 1940. Martin’s ref. 6, the source of his eq. 1; not read for hpr.
- MIL-HDBK-5J, Metallic Materials and Elements for Aerospace Vehicle Structures, 2003.
- Forest Products Laboratory, Wood Handbook, FPL-GTR-190, 2010, Tables 5-1, 5-3a and 5-5a.
- NCAMP, Hexcel 8552 AS4 Unidirectional Prepreg Qualification Statistical Analysis Report, NCP-RP-2010-008 Rev D, 2011, Table 3-3.
- Riga Wood, Plywood Handbook, 2022, Table 4.11.
- Celanese, Zytel 101L NC010 data sheet, 2023; Delrin 100P NC010 data sheet.
The decision behind these choices is ADR-078, from the M1.10b milestone.
Flight-log readings
In short
- What it models: the readings taken from a flight log on its own, with no design file and no simulation: liftoff, apogee and the time to it, the top speed in the climb, landing, and the mean rate of descent. Each reading is a value that says where it came from, or is withheld with the reason the log can’t support it. It never guesses.
- Sources: the running median is the Hampel filter at threshold zero, as Pearson and colleagues define it (References). The thresholds (a 3 m climb, landing within 2 m, a 4,000 m/s ceiling, a 20% noise share) are those of Debrief, the project owner’s earlier flight-log analyzer, set on its collection of real logs rather than taken from a published source. The landing check uses a fall from rest in vacuum: drag only slows a fall, so no real descent is faster.
- How well it is validated: against an invented flight whose every reading is known exactly, each reading lands within the bound the log’s rounding and the filter allow (Checked against). On a real log, Debrief’s public Pnut file, hpr reads 1,010 ft where the altimeter’s own software states 1,009 ft: one flight, and two readings of one pressure trace rather than a measurement of the height. That log isn’t committed, so this check runs only where it has been fetched, not in CI. The private collection of logs Debrief was built on hasn’t been read yet: that is M7.1’s work.
- What it leaves out: the drogue and main descent rates each on its own, Mach number, dynamic pressure, burnout, and anything an accelerometer or GPS would give. It takes the logger’s altitude as the logger converted it, with no correction for the day’s air, and has no check for a barometer’s errors near the speed of sound (What it leaves out).
Why these rules
Code: hpr_flightdata::readings
(API reference), on the command line
hpr analyze. It came with M4.2d,
the milestone that added hpr analyze; the choices below are recorded in ADR-108, the
decision on how a log is read. The one log format read so far is
PerfectFlite’s .pf2.
A barometric altimeter’s altitude is a good record of a flight’s heights, with two flaws a reading must survive. It moves in steps of its resolution, one foot for a PerfectFlite, so the top of the climb sits flat for several samples. And when the ejection charge fires, the pressure inside the electronics bay jumps, and for a tenth of a second the altitude swings tens of feet away from the truth. Take the highest sample as the apogee and you read that swing.
So every height and time comes from the altitude after a running median, which removes the swing, and each reading has a rule that says when the log can’t support it.
The running median
The running median replaces each sample with the median of the samples within K places of it,
the window cut short at either end of the log. hpr sets K from the log’s own sample interval
Δt so that the window spans 0.3 s: at a PerfectFlite’s 20 samples a second, K = 3, seven
samples.
Pearson and colleagues define the Hampel filter by the window’s median m_k and its scale
S_k = 1.4826 × median |x_(k−j) − m_k|: a sample more than t·S_k from m_k is replaced by m_k.
At t = 0 every sample is replaced, and the filter is the running median (§2, eqs. 1 and 2).
Two properties make it the right tool here:
-
It removes any pulse up to
Ksamples wide. Such a pulse holds at mostKof the window’s2K + 1samples, so the median is never one of them. On the public log below, the ejection pulse’s high side is two samples wide. -
It reads a clean peak a little low, and never high. At 20 samples a second, the apogee of a noise-free trace reads at most 0.077 m low. The steps behind that number:
- At the highest sample,
K + 1of the window’s2K + 1samples lie within⌈K/2⌉places of it (⌈K/2⌉isK/2rounded up). So the median is no lower than the altitude that far away. - The true peak lies within half a sample of the highest sample, so that altitude is at most
(⌈K/2⌉ + ½) Δtfrom the true peak in time. - At apogee the vertical speed is zero, so drag has no vertical part and only gravity curves
the path. In that time the height falls at most
g ((⌈K/2⌉ + ½) Δt)² / 2. - With
K = 3andΔt = 0.05 s: 9.80665 × 0.125² / 2 = 0.077 m.
A property test checks this for every
Kfrom 1 to 6 and every place the peak can fall between samples. - At the highest sample,
Debrief uses the Hampel filter at t = 4 over the same window. hpr doesn’t, because of what
the public Pnut log shows. Around its ejection pulse the trace dips below itself just before the
pulse and stays lower after it. Those samples sit in the pulse’s own window and widen its scale
S_k, and the Hampel filter keeps the pulse: its highest value is the pulse’s 1,028 ft, against
the 1,009 ft the altimeter states. After the running median, the apogee reads 1,010 ft. The
invented flight below repeats that shape, so the tests show the difference where anyone can run
them.
Each reading
In the rules below, the climb begins at the first sample whose filtered altitude is 3 m above where the log starts. The pad is the median of the altitude before it first rises 1 m, so no one sample’s jitter sets it. hpr chose 1 m, a third of the 3 m climb. It is low so that a log starting just before liftoff counts few climbing samples as pad. A PerfectFlite zeroes its altitude on the pad, so a pad more than 3 m from zero means the log didn’t start there.
| reading | rule | where it comes from |
|---|---|---|
| liftoff | the last sample before the climb whose filtered altitude is within half the altitude’s resolution (half a foot) of the pad | the barometer |
| apogee | the filtered altitude’s highest value; its time the middle of the run of samples that hold it | the barometer |
| time to apogee | apogee’s time less liftoff’s | the barometer |
| top speed | the highest of the logger’s own vertical speed from liftoff to apogee | the logger’s speed column, which it works out from its barometer |
| top acceleration | withheld when the log has no accelerometer | none |
| landing | the first sample after apogee within 2 m of the pad that stays under 5 m for a second, and no sooner than a fall from rest in vacuum could lose that height | the barometer |
| mean descent rate | the filtered height lost from apogee to landing, over the time taken | the barometer |
Liftoff is the last sample before the altitude shows the rocket moving: the filtered altitude was within half the altitude’s resolution of the pad then, and passed it within the next sample.
Landing is the first sample within 2 m of the pad, so it comes before touchdown by the time the last 2 m took: a third of a second under a main at 6 m/s. A barometer drifts during a flight, so the ground can read a little above or below the pad. If it reads below, the landing is read while the trace is still falling; if it reads more than 2 m above, no landing is found. The mean descent rate covers the drogue and the main together. Splitting it into each leg’s own rate is M7.2’s work.
The top acceleration is withheld rather than worked out from the altitude. Differencing twice turns the altitude’s one-foot steps into spikes: at 20 samples a second, one step differenced twice is 0.3048 / 0.05² = 122 m/s², over 12 g.
When a reading is withheld
A withheld reading has a code and a sentence with the log’s own numbers. Only the readings that need the missing one go: a log that starts in the air loses its liftoff, and with it the time to apogee, the top speed, the landing and the mean descent rate, but keeps its apogee’s height and time.
| code | when | readings withheld |
|---|---|---|
too_short | the log has fewer than 3 samples | all |
no_climb | the filtered altitude never climbs 3 m above where the log starts | all |
starts_off_the_pad | the pad is more than 3 m from the logger’s zero | liftoff, and so the top speed and landing |
ends_before_landing | the log ends before the altitude comes within 2 m of the pad and stays under 5 m for a second | landing |
faster_than_free_fall | the altitude comes down to the landing sample sooner after apogee than a fall from rest in vacuum could lose that height, √(2h/g), allowing one step of the altitude’s rounding in h and a sample for the apogee’s time | landing |
no_speed_column | the log has no speed column | the top speed |
implausible_speed | the speed column peaks above 4,000 m/s, Debrief’s ceiling | the top speed |
noisy_speed | from liftoff to apogee the speed swings below zero by more than 20% of its top | the top speed |
speed_peak_at_liftoff | the speed peaks on the liftoff sample itself: a spike, not a climb | the top speed |
no_accelerometer | the log has no accelerometer | the top acceleration |
needs | the reading needs another that was withheld | as the sentence says |
bad_record | the record breaks what every reader guarantees: channels as long as the clock, and times that increase. No file reader produces one; only a record built by hand in code can | all |
sampled_too_fast | the samples come so often, more than about 6,700 a second, that the 0.3 s window would hold more than 1,000 either side, which would make the median slow | all |
“All” leaves out the top acceleration, which keeps its own reason, no_accelerometer, on a
PerfectFlite log.
An apogee within half a window of the log’s end is read, but marked is_floor: the log may have
stopped before the rocket did.
Checked against
An invented flight. The file
synthetic-pnut.pf2
holds a flight made up for the tests. It sits on the pad for half a second, then accelerates at
50 m/s² for 1.6 s and coasts with no drag. It falls from rest to 25 m/s, descends at 25 m/s to
150 m and at 6 m/s to the ground. It is written as a Pnut writes one, rounded to whole feet and
feet per second, with an ejection pulse of the public log’s shape a second after apogee. Every
reading is known in closed form. The
tests
hold each within the bound worked out beside it:
| reading | true | hpr reads | allowed |
|---|---|---|---|
| liftoff | 0.50 s | 0.55 s | from 0.50 s to 0.578 s, when the height first rounds to a foot |
| apogee | 390.31 m (1,280.5 ft) | 390.14 m (1,280 ft) | 0.23 m: half a foot of rounding and the median’s 0.077 m |
| apogee time | 10.258 s | 10.275 s | one sample, 0.05 s: the middle of the flat run at the top |
| time to apogee | 9.758 s | 9.725 s | follows from the two rows above; not tested on its own |
| top speed | 80.0 m/s at 2.10 s | 79.9 m/s (262 ft/s) at 2.10 s | half a foot per second |
| landing | 46.14 s, touchdown | 45.85 s | up to 0.38 s early: 2 m at 6 m/s is 0.33 s, and the test allows a sample more |
| mean descent rate | 10.92 m/s over hpr’s span, 10.275 s to 45.85 s | 10.92 m/s | the two heights’ bounds over the span |
The middle of the flat run at the top falls between two samples here, so the apogee time is
10.275 s. hpr analyze prints times to two decimals, as 10.28 s and
9.72 s after liftoff.
The first sample of the flat run at the top would read the apogee over 0.2 s early; a test shows that too, so the rule of taking the run’s middle is pinned.
The file’s highest sample is the pulse’s, 400.5 m. The Hampel filter keeps it, and the median sets it aside; a test holds both.
A real flight. Debrief ships a public PerfectFlite Pnut log, trimmed from a publicly shared
flight. Its terms upstream are unclear, so it isn’t committed here, and CI doesn’t have it. Where
it has been fetched (cargo xtask refs fetch), a
test reads it and
holds these numbers:
| the altimeter states | hpr reads | |
|---|---|---|
| apogee | 1,009 ft | 1,010 ft (307.8 m) |
| highest sample | 1,028 ft, the ejection pulse, set aside | |
| top speed | 257 ft/s (78.3 m/s) | |
| liftoff | 0.15 s |
The altimeter’s apogee comes from the same pressure trace, so the agreement shows that hpr reads the trace as the altimeter does, not that either is the true height.
A Featherweight Raven flew on the same flight, and Debrief reports it agreeing at about 1,009 ft. Reading the Raven’s file is M7.1’s work.
What it leaves out
- The day’s air. The altitude is the logger’s own conversion of its pressure, which assumes a standard atmosphere. On a warm day a barometric altimeter reads low, 6.5% on a day 20 K warmer than standard (Atmosphere). hpr prints what the logger recorded and doesn’t correct it.
- Fast flights. Debrief stops trusting a barometer’s altitude above Mach 0.9, about 300 m/s (1,000 ft/s) near the ground (the readings note). hpr has no such check yet. On a flight that may have come that close to the speed of sound, judged from its simulation or motor rather than from the log, the altitude near the top speed may be off, and so may the top speed, which the logger works out from that altitude and which can itself read low.
- Noise and wide pulses. On a noisy trace the highest of the medians can read above the true
peak, and a pulse wider than half the window passes the median. Neither is flagged. Noise of a
foot or two can also trip
faster_than_free_fallon a low flight that fell with little drag, such as one whose recovery didn’t deploy: its allowance covers the rounding, not noise. - Each leg’s descent rate, and deployment events. One mean rate covers drogue and main.
- Mach number, dynamic pressure and burnout. These need an atmosphere or an accelerometer (M7.2).
- Other loggers. Only PerfectFlite’s
.pf2is read (M7.1).
Tests
hpr_flightdata::synthetic: the invented flight, its readings against the truth, and the Hampel filter keeping the pulse the median removes.hpr_flightdata::readings: each withheld code on a log built to trigger it, and the top speed’s guards at their edges: for example, a 20% swing passes and 22.5% is refused. A vacuum fall rounded to feet lands at 10 to 100 samples a second, wherever its apogee falls within a foot and its liftoff between samples, and one at 1.3 g is refused. A clock too fine for the window is refused at its edge. A jitter in the first samples leaves the pad where it is, a log that starts just before liftoff keeps its pad, and a pad between two feet takes half a foot either way. A property test holds the median’s peak bound for everyKfrom 1 to 6 and every place the peak can fall between samples.hpr_flightdata::filter: a pulse removed, a ramp untouched, the Hampel filter at zero equal to the median, and property tests that every output lies within its window and that the sliding median equals each window’s own, bit for bit.crates/hpr-cli/tests/cli.rs:hpr analyzeon a log alone in a folder, its JSON checked against the published schema; the public Pnut log, where fetched.
References
- R. K. Pearson, Y. Neuvo, J. Astola and M. Gabbouj, The Class of Generalized Hampel Filters,
23rd European Signal Processing Conference (EUSIPCO), 2015, §2, eqs. 1 and 2. Pinned as
pearson-2015-generalized-hampelin the reference lock file. - Debrief,
lib/analyze/index.tsandlib/parsers/perfectflite.ts(MIT, the project owner’s own), for the thresholds and the format; notes in the readings note.
Interpolation tables
In short
- What it models: reading values between a table’s points, such as drag coefficient against Mach number, along straight lines or a smooth natural cubic spline. Each table sets what happens past its ends, and every lookup says if it went there.
- Sources: the slope form of the cubic spline in C. de Boor, A Practical Guide to Splines (Springer, 2001).
- How well it is validated: analytic and unit tests only. The spline through (0, 0), (1, 1)
and (2, 0) matches its closed form,
y = 3x/2 − x³/2on[0, 1], and property tests check that both kinds hit every point. Not compared with another simulator or a flight. - What it leaves out: tables of more than one input. A natural spline can overshoot where data turn sharply (a thrust spike, a transonic drag peak), and there is no overshoot-free (monotone) cubic yet, so use linear tables there.
Code and sources
Code: hpr_core::interp (Table1D). Today it carries the drag tables that override hpr’s own
drag, C_D(M) (Aerodynamics); thrust curves and soundings interpolate on their own
(Solid motors, Atmosphere). Every table states how it interpolates and
what happens outside its range, and every lookup reports whether it extrapolated.
Knots
A table has n ≥ 2 knots (x_i, y_i). The x_i strictly increase, and every value and every
secant slope δ_i = (y_{i+1} − y_i)/h_i, with h_i = x_{i+1} − x_i, is finite. Construction and
deserialization both enforce this.
Interpolation
Inside [x_0, x_{n−1}], with t = (x − x_i)/h_i on interval i:
-
Linear:
y = (1 − t) y_i + t y_{i+1}. This form returns the knot values exactly. -
Natural cubic spline: the cubic Hermite form with knot slopes
s_i:y = h00(t) y_i + h10(t) h_i s_i + h01(t) y_{i+1} + h11(t) h_i s_{i+1} h00 = (1 + 2t)(1 − t)², h10 = t(1 − t)², h01 = t²(3 − 2t), h11 = −t²(1 − t)The second derivative at the ends of interval
iisy''(x_i⁺) = (6δ_i − 4s_i − 2s_{i+1}) / h_i y''(x_{i+1}⁻) = (−6δ_i + 2s_i + 4s_{i+1}) / h_iEquating the two at each interior knot and multiplying by
h_{i−1} h_i / 2givesh_i s_{i−1} + 2(h_{i−1} + h_i) s_i + h_{i−1} s_{i+1} = 3(h_i δ_{i−1} + h_{i−1} δ_i)Setting
y''to zero at both ends (the “natural” or free-end condition) closes the system with2s_0 + s_1 = 3δ_0ands_{n−2} + 2s_{n−1} = 3δ_{n−2}. This is the slope form of cubic spline interpolation (C. de Boor, A Practical Guide to Splines, rev. ed., Springer, 2001, ch. IV). Every row is strictly diagonally dominant, so the Thomas algorithm needs no pivoting. With two knots the spline is the straight line.
A natural spline can overshoot where the data turn sharply (a thrust spike, a transonic drag peak). Use linear tables for such data; a monotone cubic can be added when a model needs one.
Extrapolation
| policy | below x_0 | above x_{n−1} |
|---|---|---|
clamp (default) | y_0 | y_{n−1} |
linear | y_0 + m_0 (x − x_0) | y_{n−1} + m_{n−1} (x − x_{n−1}) |
error | CoreError::OutOfRange | CoreError::OutOfRange |
m is the end interval’s secant for a linear table and the spline’s end slope s_0 or s_{n−1}
for a cubic one, so linear extrapolation of a natural spline stays twice differentiable. A lookup
at NaN is an error under every policy.
Tests that pin this (crates/hpr-core/src/interp.rs)
natural_spline_matches_the_three_knot_closed_form: knots(0,0), (1,1), (2,0)give slopes(3/2, 0, −3/2)andy = 3x/2 − x³/2on[0, 1], symmetric aboutx = 1.- Property tests: both kinds reproduce the knots exactly; linear values stay within each interval’s end values; the spline’s second derivative is continuous at interior knots and zero at the ends; the spline reproduces straight lines, extrapolation included; each policy behaves as in the table above; a JSON round trip rebuilds an identical table.
rejects_malformed_knots,deserializing_checks_the_knots: malformed input fails.
Adaptive quadrature
In short
- What it models: a definite integral (the area under a curve) to a requested accuracy, splitting the range where the error estimate is largest. It gives the volume, center of mass and inertia of parts such as nose cones and fins.
- Sources: QUADPACK (Piessens, de Doncker-Kapenga, Überhuber and Kahaner, Springer, 1983), public domain: its 15-point rule, which carries its own error estimate, and its adaptive scheme.
- How well it is validated: analytic tests only. The rule integrates polynomials up to degree 22 exactly, and six test integrals, one infinite at an end, reach 1e-11 relative. The integrals of 22 nose cones and transitions agree with 40-digit references to 1e-12 relative (Nose cones). Not compared with another simulator or a flight.
- What it leaves out: QUADPACK’s extrapolation for singular integrands. It relies on splitting, plus changes of variable at known singularities, and reports an error when it runs out of subintervals.
Code and sources
Code: hpr_core::quadrature.
Source: [QP] R. Piessens, E. de Doncker-Kapenga, C. Überhuber and D. Kahaner, QUADPACK: A
Subroutine Package for Automatic Integration, Springer, 1983. QUADPACK is public domain. Its
routine qk15 has the rule’s nodes and weights to 33 digits.
Method
- Rule. On
[a, b], the 15-point Gauss–Kronrod ruleKembeds the 7-point Gauss ruleG.Kis exact for polynomials of degree ≤ 22 andGfor degree ≤ 13. - Error estimate of a subinterval:
|K − G|for each component. This overestimates the error ofKwhen the integrand is smooth. - Global adaptivity ([QP]
QAG, §2.2 and §3.3):- Keep every subinterval with its estimates.
- While the summed error of some component exceeds
max(absolute, relative · |Σ K|), bisect the subinterval that contributes most to the worst component. - Stop with
QuadratureDidNotConvergeat the subinterval budget, or when a bisection point can no longer be represented. - QUADPACK’s
QAGSalso extrapolates with the ε-algorithm; hpr does not, and relies on bisection plus variable substitutions at known singularities (Shapes).
- Vector integrands share one set of subintervals, so the moments of a solid come from the same integrand evaluations. Integrands should be scaled to order one so that one absolute tolerance suits every component.
- Floors. The relative tolerance never goes below
50 ε. A NaN or infinite sample is an error (QuadratureNotFinite), never a silent zero.
Verification (quadrature::tests)
- The rule is the published one.
- The Gauss nodes are roots of
P₇to 1e-15. - Both weight sets sum to 2.
- One panel integrates
x^kexactly fork ≤ 22(Kronrod) andk ≤ 13(Gauss). At the next even degree it is not exact. - A digit typo in any node or weight fails these checks.
- The Gauss nodes are roots of
- Convergence:
eˣ,sin x,√x,x^(−1/2),x^0.3and|x − 0.3|all reach 1e-11 relative.- Four components converge together.
- Reversed limits change the sign.
- Failures:
1/xon(0, 1]runs out of subintervals.- A NaN integrand is reported.
The hpr design format (.hpr and .hprz)
An .hpr file is a rocket design written as one JSON document: hpr’s own format, open and
described by a published JSON Schema. It holds everything hpr reads from an
OpenRocket .ork: the rocket’s parts, every motor configuration,
when each parachute opens and each stage separates, the simulations the file stored, what the file
holds that hpr doesn’t model, and the other files inside it, such as an embedded thrust curve or a
decal image. It is plain text, so a design can be kept in git and a change to it reads as a small
diff. A .hprz is the same document in a zip archive, with other files beside it, such as the
design’s flight logs and photographs (the container).
What works today: hpr convert turns a .ork into a .hpr or .hprz and back, and hpr sim
flies a .hpr or .hprz as it flies the .ork (reading and writing one).
A document of the older version 0.1 is migrated when it is read. Rust programs can do all of this
through the hpr_format library, and TypeScript and Python programs through types generated from
the schema, each with a reader that checks a document (TypeScript and Python).
How far to trust it. Converting keeps everything hpr read from the .ork except the reader’s
warnings. hpr’s checks use 75 .ork files and read 73. Each of the 73 goes .ork → .hpr →
.ork and comes back as the same
design, bit for bit, and as the .ork hpr writes from the original, byte for byte. That .ork is
close to the original file but not the same: what the writer changes
says how. The 109 motor configurations that fly, spread over 30 of the designs, reach the same
apogee from the .ork, from the document, and from the .ork written from the document, bit for
bit. These counts come from a run on the developers’ machine that includes other people’s private
designs; the automatic checks repeat the checks, not the counts, on the 17 public ones
(checked on real designs).
Keep your .ork too. The format is version 0.2, a draft until hpr’s first release. A document
of version 0.1 still reads, but 0.1 didn’t record whether the rocket’s airframe was read exactly as
written. When the document can’t show it either, hpr sim refuses to fly another motor in it until
the .ork is converted again (versions).
What a document holds
A document is an object with nine keys, always written in this order:
| key | what it holds |
|---|---|
format | always "hpr-design", so a program can tell a design from any other JSON |
version | the format’s version, "0.2" |
provenance | the program that wrote it (tool), that program’s own version (tool_version, hpr’s version, not the format’s) and its designation in the FusionSpace product system (designation, "FS · SW · TOOL 005"; absent in a document written before it was added); the format and SHA-256 of the file the design came from; and, if the design’s airframe was not read from that file exactly as written, why not. Converting a document again names the program that converted it, and keeps the source as it was |
rocket | the stages and their parts, with every motor configuration that flies |
motors | every motor configuration, flown or not, with why one is not |
recovery | when each parachute and streamer opens, and when each stage separates |
simulations | the simulations the source file stored, with their conditions and results |
extensions | what the source file holds that hpr doesn’t model, kept for writing it back |
source_files | the source file’s other files, such as a .ork archive’s embedded thrust curves and decal images |
Every number is in SI units, and a key says its unit where it has one: length_m is meters,
kg_m3 kilograms per cubic meter. A SHA-256 is a
fingerprint of a file’s bytes: it names the source without giving its path or name, which can
name a person. Here is the start of the
stable trainer,
one of the public demonstration designs from Loft, the project
before hpr, as a document:
{
"format": "hpr-design",
"version": "0.2",
"provenance": {
"tool": "hpr-sim",
"tool_version": "0.1.0",
"designation": "FS · SW · TOOL 005",
"source": {
"format": "ork",
"sha256": "199f71f9fc774d5f7f29df70ecac94d8ad46e682a001ef7e67b144970934a234"
}
},
"rocket": {
"name": "Loft Demo 38mm — stable trainer",
"stages": [
{
"id": "10f70005-0000-4000-8000-000000000002",
"name": "Sustainer",
"components": [
{
"id": "10f70005-0000-4000-8000-000000000003",
"name": "Nose cone",
"part": {
"nose_cone": {
"shape": {
"kind": "ogive",
"radius_ratio": 1.0
},
"length_m": 0.25,
"base_radius_m": 0.019,
A stage strapped beside the airframe, such as a pair of boosters, is a parallel stage: it comes
in stages after the stage it hangs on, with an optional parallel key holding the id of its
body tube (on), its place along that tube (position) and its copies (pods), laid out as a
pod set’s. A stage on the axis leaves parallel out, so a 0.2 document written before it reads
the same (Parallel stages).
A motor that flies is written out in full, its thrust curve included, so a document flies with no motor database. That has a consequence for sharing. When a program supplies curves from a motor database, as hpr’s checks supply OpenRocket’s, the document holds those curves, and OpenRocket’s database publishes no terms for reusing them (where the curves come from). Check before sharing such a document.
The source file’s other files are under source_files, in the order the file held them. A text
file, such as an embedded thrust curve, is written as its text. Any other file, such as an image, is
written in base64, a standard way to spell
bytes with 64 printable characters. A .ork written from the document puts every one back.
Why the airframe’s reading is recorded. Some .ork rockets aren’t read exactly as written:
a part is left out, a value dropped, or a size assumed. hpr flies none of their motor
configurations, and hpr sim won’t fly another motor in them either. A .ork written back from the
document spells out every assumed value, so reading it again can’t tell that anything was assumed.
The document therefore records the reason, as provenance.source.airframe_not_as_written, and
hpr sim reads it from there.
What the document doesn’t keep: the .ork reader’s warnings. hpr convert prints them once,
when it reads the .ork; hpr sim prints them for a .ork but not for a .hpr or .hprz.
The container (.hprz)
A .hprz file is a zip archive holding a design and the files that go with it: flight logs,
results, photographs, anything. Its first entry, design.hpr, is the design exactly as a .hpr
file, so unzipping a container gives a .hpr any reader of the format takes. Every other entry is
an attachment, kept byte for byte, under its name, in its order.
- Names are relative paths, with
/between folders, such aslogs/flight-1.csv. No part of a name may be empty,.or.., and a name holds no\,:or control character, so no name can climb out of the folder a container is unpacked into. No attachment may be nameddesign.hpr, in any mix of capitals, nor sit in a top-level folder of that name (design.hpr/notes.txt). - Names that would unpack as one file are refused: two names the same but for capitals, a name
that is also another’s folder (
logsbesidelogs/a.csv), and, for Windows, a part ending in.or a space, or named as a device (CON,NUL,COM1and the like, with or without an extension). No part of a name is longer than 255 bytes, the most common file systems’ limit. Capitals are compared as Unicode maps them, which also takesßforss, sostraße.csvbesidestrasse.csvis refused though Windows would keep both. Two spellings of one accented letter are refused too, as macOS would merge them:écan be written as one character or asefollowed by a combining accent, and the names are compared with every accent written the second way (Unicode’s canonical decomposition, NFD). The refusal names both spellings, with the combining accent written out (cafe\u{301}.txt), since the two look alike on screen. - At most 256 MiB, unpacked. The design and its attachments together hold no more: hpr refuses to write a bigger container, and to read one, so a small, hostile archive can’t fill memory.
- The same design and files give the same bytes. Every entry is compressed with deflate, zip’s usual method, and dated 1 January 1980, zip’s zero date, never the clock. (This holds for one build of hpr; another deflate library could give other bytes that read back the same.)
- Reading is held to the same rules, so a container made by another program is refused with the reason rather than half read. The reader checks every name before it unpacks anything. It takes the design wherever the archive holds it, and passes over a folder’s own entry, which some zip tools write, as long as it is empty. It refuses a symbolic link, a name beyond plain ASCII that isn’t marked as UTF-8 (the mark zip has for it), and two entries of one name, which the zip library hpr uses would otherwise read as one, dropping the other without a word.
A .hpr has no place for attachments, and a .ork keeps only the source file’s own. So
hpr convert names each attachment it leaves out, as a warning.
Reading and writing one
hpr convert takes a design between .ork, .hpr and .hprz, choosing each format by the file’s
extension (hpr convert):
hpr convert my-rocket.ork my-rocket.hpr
hpr convert my-rocket.hpr my-rocket.hprz --attach flight-1.csv --attach pad.jpg
hpr sim my-rocket.hprz
--attach puts each file at the top of the container, under its file name; a Rust program can
use folders. In Rust, the hpr_format library does each step
(API reference, with worked
examples):
| function | what it does |
|---|---|
DesignFile::from_ork | reads a .ork into a document |
to_json | writes a document’s text |
from_json, read_json | read the text back, migrating an older version; read_json also says which version it was |
DesignFile::to_ork | writes the .ork |
container::write, container::read | write and read a .hprz |
Editing a document by hand. hpr flies what the document says, and the .ork written from it
carries an edit to the airframe, the recovery or the simulations. (hpr sim flies the
parachutes and streamers, as it does a .ork’s, and its powered separations
(Separation).) The motors are the exception: they are
held twice (what is not there yet). hpr sim flies the motors under
rocket.configurations, and the .ork writer takes its motors from motors.configurations, so
change a motor in both places, or convert the .ork again. Nothing warns when the two disagree.
Any program that checks JSON against a JSON Schema can check an edited document before hpr reads
it (the schema). No other design program reads .hpr or .hprz yet.
The text is canonical, meaning there is exactly one way to write a given design: two-space indents, keys in the order above, and a final newline. So the same design always gives the same bytes.
Before it returns, the writer reads its own text back and compares it with the design. A value JSON can’t carry, such as an infinite number, is refused with an error rather than written as something else.
Versions
A version is two numbers, major.minor. The reader checks format and version before anything
else, so a file of another kind or another version is refused with that reason, not with the
first key it doesn’t know.
- A reader takes its own version and migrates older ones. A migration rewrites an old document into the current version’s shape, one version at a time, and then reads it as a current document, so it is held to every current rule. A newer version is refused as “written by a newer program”. A key the reader doesn’t know is refused too, so a document from a later build of hpr, with a key added within the same version, is refused as an unknown key.
- While the major number is 0, the current version may change in place only in a way that
leaves every document already written readable, with the same meaning. Any other change takes a
new minor version, with a migration from the one before.
provenance.designationwas added to 0.2 this way: it is optional, so a document written without it reads as before. The old version’s schema stays committed, with a document its program wrote, which a test migrates. - From 1.0, a new minor version only adds, and a change that breaks old documents takes a new major version, with a migration.
| from | to | what the migration changes |
|---|---|---|
| 0.1 | 0.2 | attachments is renamed source_files, leaving “attachment” to mean a file in a .hprz. Whether the airframe was read exactly as written, which 0.1 didn’t record, is worked out from the motor configurations, or marked unknown |
What the 0.1 migration can’t recover. 0.1 recorded why a .ork‘s airframe wasn’t read exactly
as written only in a motor configuration left out for that reason. The .ork reader asks it after
the configuration’s motors and before its stages’ separation. So a configuration left out for the
airframe gives the reason, and one that flies, or is left out only for its separation, shows the
airframe was read as written. When every configuration was left out for an earlier reason, such as
a missing thrust curve, or there is none, nothing shows it: the migration marks it unknown, and
hpr sim refuses to fly another motor in the rocket, saying so. On the 73 designs of hpr’s checks,
the migration:
- recovers 5 of the 8 reasons, and marks the other 3 unknown;
- marks 28 of the 65 designs read as written unknown too, so 31 of 73 in all are unknown;
- gives no wrong answer.
Converting the .ork again gives a 0.2 document that knows. To see whether a 0.1 document was
marked unknown, convert it to a .hpr and look at provenance.source.airframe_not_as_written.
The reasons are in ADR-111, which set the extensions and the first version policy, and ADR-112, which added the container and the first migration.
Extensions and unknown keys
What a source file holds that hpr doesn’t model is kept under extensions, in a namespace: a key
that starts with x- and names the program the data belongs to. Version 0.2 has one, x-openrocket: the parts, sections, tags and attributes of
a .ork that hpr doesn’t read, each kept whole with where it was
(what hpr keeps). That is how a .ork written
from a document gets them back.
A key the version doesn’t define is refused, not dropped, anywhere in the document, an unknown namespace included. A document either reads whole or says why it doesn’t. Keeping another program’s namespace as it is waits for a later version.
The schema
schema/format/hpr-design-0.2.schema.json
is the document’s JSON Schema. A JSON Schema is a machine-readable description of a JSON
document’s keys and types, which many languages can check a file against; this one follows the
standard’s 2020-12 edition. It is generated from the Rust types by cargo xtask format (one of the
repository’s development commands), and a test fails if the committed file is stale. Its
descriptions are the types’ documentation. It refuses every key the version doesn’t define, as
the reader does. hpr’s reader refuses what the schema refuses, such as a
provenance.source.sha256 that isn’t 64 lowercase hexadecimal digits, and names the place; tests
hold it to the schema on thousands of altered documents (how far to trust the
readers). A few
rules the schema can’t express, so a document can pass the schema and still be refused by hpr: a
source file’s base64 must decode, no two source files share a name, a name can’t be rocket.ork
or a folder’s, and every embedded thrust curve must be among the source files.
Version 0.1’s schema stays beside it,
hpr-design-0.1.schema.json.
TypeScript and Python
Programs in TypeScript (or JavaScript) and Python can read a document with types generated from the schema. Each language gets one file, with a reader that checks a document against the schema. Three limits come first:
- The readers check what the schema checks, not everything hpr does (how far to trust them).
- They read version 0.2 only, which is a draft, so copy the file again when hpr’s format version changes.
- They read designs; they don’t fly them. To fly a design from Python, use the
hprpackage (Python).
| language | file | needs |
|---|---|---|
| TypeScript | schema/format/typescript/hpr-design.ts | Node.js 22.18 or later to run it as it is, or a TypeScript compiler with target ES2020 or later; tested with Node.js 24 and TypeScript 7.0.2 |
| Python | schema/format/python/hpr_design.py | Python 3.11 or later (macOS’s own python3 is 3.9: check python3 --version), nothing outside its standard library; tested with Python 3.12 and the type checker mypy 2.3.1 |
Using one in your project. Neither is a package on npm or PyPI yet: copy the one file into your
source tree. Both are MIT OR Apache-2.0, like the rest of hpr.
- TypeScript: the file is an ES module, so your
package.jsonneeds"type": "module". Where the compiler only checks types (noEmit, as with a bundler or with Node.js running TypeScript as it is), import./hpr-design.tswithallowImportingTsExtensions, as hpr’s tests do. Where it writes JavaScript, import./hpr-design.jsundermodule: nodenext; hpr’s tests don’t try that. - Python: put
hpr_design.pybeside your code or onPYTHONPATH. - A reader takes a
.hprfile’s text. For a.hprz, unzip it and read thedesign.hprinside (the container).
What the file holds. A type for every object in the schema, named as the schema names it, with
the schema’s descriptions as its comments: DesignFile for a whole document, Rocket, Component,
NoseCone and so on. In TypeScript they are interfaces and unions; in Python, TypedDicts (typed
dictionaries) and Literals. A document stays plain JSON data, objects and arrays (dictionaries
and lists in Python), so a program can write it back with its own JSON writer. hpr reads the
result as the same design, but numbers may be spelled differently (1850 for 1850.0).
hpr convert edited.hpr tidy.hpr writes it back in hpr’s own spelling; replace the original with
tidy.hpr so diffs in git stay small. To check a document your program built or changed, pass
its text through the reader: readDesign(JSON.stringify(design)) or
read_design(json.dumps(design)).
The reader, readDesign in TypeScript and read_design in Python, takes a document’s text and
checks it against the schema, which is copied into the file, before handing it back typed. It
refuses, with a DesignFormatError:
- a document of another version:
hpr convertrewrites an older one, and a newer one needs the file from a newer hpr; - a missing key, an unknown key, or a value of the wrong type, naming where, for example
$.rocket.stages[0].components[0].part.nose_cone: has no "length_m", which it needs; - a misspelt fixed word, listing the words allowed there, for example
…position.from: is "topp", not "top", "middle", "bottom" or "after".
This example reads each document it is given and counts what it holds:
// Reads each design document named on the command line with the generated types, and prints a
// line for each: what it holds, or why the reader refused it. Exits 1 if it refused any.
//
// node schema/format/typescript/read-design.ts design.hpr [more.hpr ...]
//
// Node.js 22.18 or later runs TypeScript as it is; an older one needs --experimental-strip-types.
import { readFileSync } from "node:fs";
import { DesignFormatError, readDesign, type Component } from "./hpr-design.ts";
/** How many parts `components` hold, counting the parts inside parts. */
function count(components: Component[]): number {
return components.reduce((sum, c) => sum + 1 + count(c.children ?? []), 0);
}
let refused = 0;
for (const path of process.argv.slice(2)) {
try {
const design = readDesign(readFileSync(path, "utf8"));
const stages = design.rocket.stages;
const parts = stages.reduce((sum, stage) => sum + count(stage.components), 0);
const configurations = design.motors.configurations.length;
console.log(
`read ${path}: stages ${stages.length}, parts ${parts}, motor configurations ${configurations}`,
);
} catch (error) {
if (!(error instanceof DesignFormatError)) {
throw error;
}
refused += 1;
console.log(`refused ${path}: ${error.message}`);
}
}
process.exitCode = refused === 0 ? 0 : 1;
The Python one,
read_design.py,
does the same. From a checkout of the repository, with hpr installed (the command
line), convert one of Loft’s public demonstration designs and read it:
hpr convert validation/fixtures/ork/loft-demo/demo-dual-deploy.ork demo-dual-deploy.hpr
node schema/format/typescript/read-design.ts demo-dual-deploy.hpr
python3 schema/format/python/read_design.py demo-dual-deploy.hpr
Each prints:
read demo-dual-deploy.hpr: stages 1, parts 7, motor configurations 1
How far to trust the readers
Both are checked on every change:
cargo xtask formatwrites both files from the schema, and a test fails when either is stale.- Both readers read the 17 public designs’ documents (checked on real designs), plus the committed version 0.1 document after hpr migrates it to 0.2. They print the same counts of stages, parts and motor configurations as a count of the same JSON in Rust.
- Each language’s own JSON writer (
JSON.stringify, Python’sjson.dumps) writes the 18 documents back out, and hpr reads each as the same design. - Tests make 4,910 altered copies of two of those documents: a key added or removed, an array
lengthened or shortened, or a value replaced by another (a different type, an out-of-range
number, an unknown word, or a string with a final newline). The schema refuses 4,124 of them. Each reader takes a copy exactly when a separate schema checker, the Rust
jsonschemalibrary, does. - The TypeScript compiler (at
targetES2020) and mypy, both at their strictest, accept the 18 documents written out as values of typeDesignFile, and the two examples, and refuse a document with a misspelt fixed word. - Like hpr, both refuse a number too large for a 64-bit float (
1e400), a lone UTF-16 surrogate ("\ud800"), and arrays nested 128 levels deep. Tests hold each to hpr’s own reader.
A reader checks what the schema says, so it shares the schema’s blind spots: a document it takes can still be refused by hpr for the few rules the schema can’t express. The two readers also differ from hpr on three details of JSON:
| in the text | hpr | TypeScript reader | Python reader |
|---|---|---|---|
| a key twice in one object | refused | takes the last | refused |
a whole number written with a point or an exponent (2.0, 3e0) | refused | taken as 2, 3 | refused |
| a stage number of 253 or more | read | refused, since JavaScript would round it | read |
Each row of the table, and each refusal in the list above, is tested against hpr’s own reader.
The other way round, hpr’s own reader refuses each of the altered copies the schema refuses, so a
document hpr reads, the readers read too. Until
#253 was fixed, hpr read four kinds the schema
refuses: a provenance.source.sha256 that isn’t 64 lowercase hexadecimal digits, an array where
the schema has an object, another key beside "model": "isa", and "value": null beside a
plugged delay. Tests now hold hpr to the schema on each.
Checked on real designs
The .ork reader and writer are checked on 75 .ork files, 73 of which hpr reads
(.ork design files). Some are other people’s private designs, so
only counts are published. cargo xtask ork takes each one through the format:
| check | designs |
|---|---|
| its document follows the committed schema | 73 of 73 |
| the document reads back as the same document | 73 of 73 |
the .ork written from it is the .ork hpr writes from the file itself, other files and all, byte for byte | 73 of 73 |
that .ork reads back as the design first read, bit for bit | 73 of 73 |
| the document, rewritten by the check in version 0.1’s shape, migrates back to the same document, but for whether the airframe was read as written, which 0.1 didn’t record | 73 of 73 |
| of the 8 whose airframe was not read exactly as written, the migration recovers the reason | 5 of 8; 3 marked unknown |
| of the 65 read as written, the migration marks unknown | 28 of 65 |
Between them, the documents carry the files’ 55 other files: 3 as text, the thrust curves the designs embed, and 52 as base64, their images.
Then each motor configuration that flies is flown three ways: the design first read, the design
read back from its document, and the design read back from the .ork written from the document.
Each flight is in calm air of the standard atmosphere at sea
level, off a 1.5 m vertical rail, with its stages separating if they do. Of the designs’ 170
configurations, 109 fly, and each reaches the same apogee all three ways, bit for bit. The
format’s first step, M3.3a, asked for 1 part in 10⁹.
These 109 fly because the check supplies OpenRocket’s own motor database for the curves the files
name but don’t carry. With only the files’ own curves and hpr’s bundled motors, 4 fly
(the reference library’s survey of motors). The other 61 have no
flight to compare: the .ork reader leaves them out of the rocket all three ways, by the first reason it finds: 24 for want of a
thrust curve, 19 for stages hpr can’t separate as written, and 18 for other reasons
(which configurations fly).
In CI, the automatic checks that run on every change don’t have the private designs. So
hpr-format’s tests take the 17 public .ork designs under validation/fixtures/ork/ through the
same checks. None carries a curve hpr can fly, so bundled motors
stand in: each motor gets the bundled motor of its designation, or the one nearest its diameter.
18 configurations fly, one per design and two for demo-multi-config. From the document they reach
the same apogee bit for bit. Through the written .ork the test holds them to 1 part in 10⁹: the
stand-in motors are not written into a .ork, so they are supplied again when it is read. A test
also takes each key out of two documents in turn, and checks that the schema and the reader agree on
whether the document is still valid.
The tests also cover the rest of this page:
- Migration: a document version 0.1’s program wrote, committed as
embedded-curve-0.1.hpr. 0.1’s schema takes it and 0.2’s doesn’t. It reads as 0.2, key for key the old document withattachmentsrenamed. It is the document the current reader makes from the same.ork, flies on the curve it embeds, and writes that.orkback. Other tests migrate edited 0.1 documents: each way the airframe can be recovered, found read as written, or marked unknown, and old documents that already hold a 0.2 key, which are refused. A last one migrates each of the 17 public designs from its 0.1 shape, plus two made from them: one with a parallel stage hpr leaves out, and one with an invented embedded curve. Each comes back as the document first written, but for the airframe’s answer, which is never wrong. With no motor catalog in these tests, no public design’s configuration flies, so all 17 come out unknown, and only the invented-curve design’s answer is recovered. - The container: a design with four attachments (a text file, an image, an empty file and a name in accents) reads back the same and writes the same bytes again. Each name rule above, two entries of one name, a symbolic link, a name not marked as UTF-8, a folder’s entry that holds bytes, a missing or damaged design, and a container past 256 MiB, written or read, are each refused with the reason.
- The command line:
hpr converttakes a public design.ork→.hpr→.hprz→.ork, each file the library’s own, byte for byte.hpr simflies a public design from its.hprand its.hprzto the same output as from its.ork. It refuses another motor in a rocket whose airframe wasn’t read as written, whichever of the three it reads.
How it compares with other design formats
Four other formats hold a hobby rocket design. hpr reads .ork and writes it back; it doesn’t read
the other three yet (OpenRocket’s flights of the private designs
lists the reference library’s .rkt and .CDX1 files it leaves out).
OpenRocket .ork | RockSim .rkt | RASAero II .CDX1 | RocketPy .rpy | hpr .hpr / .hprz | |
|---|---|---|---|---|---|
| encoding | XML, usually in a zip archive with other files | XML | XML | JSON | JSON, canonical text; a zip archive for .hprz |
| published description | a prose page, no schema | in the help of SMARTSim, Apogee’s companion program | none: “not documented”, its author says | none beyond the code | a JSON Schema, generated and tested |
| version in the file | yes, major.minor | yes | yes | RocketPy’s version | yes, major.minor, with migrations |
| units | mostly SI, not stated in the file | fixed default units, not stated | the program’s English units (inches), not stated | SI by RocketPy’s convention, not stated | SI, the unit in every key’s name |
| motor configurations, stages, recovery | all three | stages, motors and parachutes | a sustainer and up to two boosters; recovery at apogee and at a set altitude | one motor object per rocket (a ring cluster, several motors in a ring, is modeled as one), parachutes, no staging | all three, as .ork has them |
| thrust curves and other files | embedded curves, images | motors by name | motors by name | curves as data, no other files | embedded curves and images; any file in a .hprz |
| another program’s data | a simulation’s extension settings | none found | none found | none | extensions, one namespace per program |
| what it is | a design | a design | a design, as its outer shape for aerodynamics | a saved Flight: the rocket, its environment and the results | a design |
Sources: OpenRocket’s file format page
and hpr’s own reading of 73 files (.ork design files); Apogee’s
SMARTSim manual, which says
RockSim’s rocket files are XML with data “stored using prescribed default units”, and sends the
reader to SMARTSim’s own help for the elements; RASAero II’s
author on its file format
and its user’s manual, which gives every dimension
in inches; the four .rkt and four .CDX1 files in hpr’s reference library, of which only these
facts are published; and RocketPy’s code at the
version hpr’s checks pin, where .rpy came with version 1.10.0
(save_to_rpy and load_from_rpy).
Why hpr didn’t adopt one of them:
.ork: its page describes the structure and leaves the rest to OpenRocket’s source code, which is GPL-licensed and which this project doesn’t read. It has no schema, and the usual zip archive shows no useful difference in git. hpr keeps reading and writing it, as the format most designs are shared in..rkt: RockSim is a commercial program, and its file’s elements are described only in the help of another of its maker’s programs. Its units are implied, not stated..CDX1: undocumented, in English units, and it describes the rocket’s outer shape for aerodynamics, not a design to build..rpy: a saved state of one Python library’s objects, not a design format. It has no schema, holds one motor and no stages, and a function in it (a parachute’s trigger, say) is stored as a Python pickle, which runs code when the file is loaded. So a.rpyfrom someone else is only as safe as a program from them.
What is not there yet
- The generated types aren’t on npm or PyPI. Copy the file you need from the repository (TypeScript and Python).
- Reading
.rkt,.CDX1or.rpy: no milestone yet. - The motors are held twice: under
motors.configurations, every configuration in the file as the.orkholds it, and underrocket.configurations, the ones that fly, each motor ready to fly.hpr simuses the second and the.orkwriter the first. Editing one doesn’t change the other, and nothing checks that they agree yet. - An embedded curve is held twice: as the file’s text under
source_files, and as the motor built from it in the configuration. A flight uses the motor; a.orkwritten from the document uses the text. Editing one doesn’t change the other, and nothing checks that they agree yet. - Some type names come from
.ork, such asOrkMotor, because the document holds the design as hpr’s.orkreader models it. A later version can rename them, with a migration. - A container’s attachments aren’t read by anything yet.
hpr simflies the design and says how many it leaves unread; reading a flight log from a.hprzwaits for the commands that compare a flight with its simulation.
OpenRocket .ork design files
A .ork file is an OpenRocket design: the tree of parts a rocket is built from, the materials they
are made of, the motors flown in it, and the results of the simulations OpenRocket last ran. It is
the format hobby designs are most often shared in, so reading it is how a design gets into hpr
without being typed again.
What works today: hpr reads the three .ork container forms and turns the XML into a design tree.
It keeps what it doesn’t model, and writes a design back out as a .ork that reads back as the
same design (writing a .ork back out). OpenRocket 24.12 opens each
written file whose original it opens, and flies it to the original’s apogee within 0.5%
(checked in OpenRocket). Layout is normalized; comments, processing instructions
and XML namespaces are dropped.
It currently builds supported stages, body components, tubes, rings, fins, lugs, rail buttons,
recovery gear, motor configurations, recovery settings and stored simulations. Staged, clustered
and air-start configurations fly, with each stage separation under power, one or several in turn
(staged, clustered and air-start flights). So does a
payload dropped with nothing left to burn, when its own device opens at the split, and so do
boosters strapped beside the core on a rocket of one stage on the axis
(Parallel stages). Pods, unsupported shapes and recovery behavior are not
fully modeled; the trust limits below
are measured against OpenRocket 24.12 and the current reference corpus. A Rust program reads a
file with hpr_io::ork. From a terminal, hpr sim flies one, with its
parachutes and streamers. It refuses what it can’t fly as written, such as a payload that would
coast with no drag after its split.
How far to trust it.
- The shape is cross-checked. The airframe’s key geometry was compared with a second program
that reads
.orkfiles, RocketSerializer, and with OpenRocket itself. That geometry is the nose cone, the transitions, the fin sets, where each sits, and the body radius. Over 71 designs in the current scratch-excluding survey, hpr’s value is within the survey’s 1-in-10⁹ comparison tolerance of OpenRocket’s for all 1,171 numbers. The survey found 75 files, 73 readable and 72 with a design that lays out; four readable designs are not opened by OpenRocket. Where parts sit is checked against OpenRocket alone; mass and the center of gravity are checked in Mass properties (checked against RocketSerializer). - Few motor configurations fly with hpr alone. Motors are read, but a configuration flies only
when every motor in it has a thrust curve and lights at a moment hpr can fly, its stages come
apart in a way hpr flies, and the airframe was read without a warning. Most designs don’t carry
their curves. With the file’s own curves and hpr’s small bundled catalog, 4 of the 170 motor
configurations in the reference library’s 72 designs fly. When a caller also supplies
OpenRocket’s own motor database, as the validation survey does, 145 fly. hpr doesn’t ship that database
(motors in the reference library).
hpr simfetches a missing curve from ThrustCurve.org by the file’s manufacturer and designation, and caches it (motors from ThrustCurve.org). With those curveshpr simflies 136 of the 170 as saved, and 9 more with--accept-design-errors(the count). - Parachutes and streamers fly as OpenRocket flies them. hpr lands within 0.02% of
OpenRocket’s landing speed on 50 of its 53 example flights and within 0.12% on all 53. On the
51 with no named cause for an apogee gap, less the Base drag hack’s three, its flight time is
−1.57% to +3.16% off
(flown as OpenRocket flies them). A stage
separation is flown when a motor ahead of it is still burning or yet to light at that moment and
none behind it is, and a configuration’s several such separations are flown in turn, from the
tail forward. One at a stage’s first burnout may drop the stage’s other motors still burning,
as OpenRocket does
(when parachutes open and stages separate). A lone
separation with nothing left to burn, such as a payload’s, is flown when the part that keeps the
nose has a device of its own open by then
(ADR-165,
a payload’s split). A Rust
program and
hpr simfly such powered separations, each device on the part its stage is in (M4.5g1, powered separation inhpr sim; M4.5g2, several separations); a dropped stage’s own flight is not validated.hpr simflies a configuration whose separation can only come after apogee as one stack, with a note. - Whole flights are compared with OpenRocket’s on its own examples. On 41 of the 53 configurations of OpenRocket’s examples that both programs fly to the end, the stability margin off the rod agrees within 0.016 calibres, taken with the air along the rocket’s axis as OpenRocket’s is. On the Tube fin rocket it is 1.08 calibres below OpenRocket’s (tube fins), and on the three-stage example’s three configurations 0.039 to 0.058 calibres below. On the Pods–airframes and winglets example’s five it is 0.071 to 0.076 calibres above, the flattering side (#325, #326), and on the three of Pods–powered with recovery deployment 0.070 calibres above. Where nothing named explains a difference, hpr’s apogee is from 4.34% low to 2.06% high. Parachutes that open while the rocket still climbs move six apogees by more than 5%, and hpr’s tube-fin drag a seventh. The eighth, 37.93% low, is a pod example’s sustainer that turns over before apogee in both programs in the conditions of OpenRocket’s record. OpenRocket aborts its own flight of that example’s first configuration, so hpr’s is compared at two points up to the abort, weaker evidence than an apogee: at OpenRocket’s last row, 1.81 s, its height is 1.18% below OpenRocket’s and its speed 4.02% above (flights OpenRocket aborted). Two of the six, the Base drag hack example on a D12-3 and an E12-4, stay more than 5% high with the parachute held, which hpr’s drag coefficient explains (a part set to no drag). A two-stage design, a three-stage design, a cluster, an air start and boosters beside the core are each within 5% of OpenRocket’s apogee and largest speed. Five of their apogees are compared with OpenRocket’s flight with no parachute, since its parachute opened before apogee (hpr’s flights against OpenRocket’s).
- One private flight is far off, and it is the supersonic one. On the private designs, one
apogee is more than 5% from OpenRocket’s:
C06/1(a private design’s first motor configuration, under an anonymised id), the only supersonic flight, +13.60%. Flown on OpenRocket’s own drag it reads +1.11%, so its cause is in the drag. Which drag is right is open (#222: hpr’s supersonic pressure drag and base drag under power, a supersonic flight). - Pods are read, weighed and flown (Pods). Five small probe designs with pods of bodies, fins, a tail cone, winglets or motors are compared with OpenRocket, and so is OpenRocket’s Pods–airframes and winglets, whose stability margin reads 0.07 calibres above OpenRocket’s, the flattering side (below).
- Parallel stages are read and flown on a rocket of one stage on the axis (Parallel stages). OpenRocket’s Parallel booster staging flies both its configurations within 1.3% of OpenRocket’s apogee. A parallel stage hpr can’t read, such as one on a rocket of several stages, is kept whole and the design marked reduced (what hpr keeps).
- Some parts are left out. A part hpr cannot give an honest shape, such as a launch lug on a nose cone, is left out. Each one is named in a warning rather than guessed at (what is left out, and why). None in the reference library is.
- Fins on a nose cone or a transition are read with their root along its surface (Fins on a nose cone or a transition).
- Tube fins are read, weighed and flown, each tube as a ring wing (Tube fins sized from the body; the aerodynamics). OpenRocket’s example flies 6.95% higher in hpr than in OpenRocket, a net gap in the drag.
- Every one of the 72 designs in the current reference survey lays out, meaning every part gets a position and a radius. A radius the file leaves with nothing to be worked out from gets OpenRocket’s own default of 25 mm, with a warning (when an automatic radius has nothing to take).
- A written file flies in OpenRocket as its original does. Of 151 configurations OpenRocket flies both ways, all 151 reach the original’s apogee within 0.5%, and 142 exactly. This checks the file, not hpr’s physics (checked in OpenRocket).
Opening a file today
This is the doctest on hpr_io::ork, which CI runs. It stops at the document: the tree of
elements the file said, with nothing interpreted. To go one step further and get an
hpr_design::Rocket out of that tree, see
opening a design, in full.
let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
<openrocket version="1.10" creator="OpenRocket 24.12">
<rocket><name>Sounder</name></rocket>
</openrocket>"#;
let read = hpr_io::ork::read(xml)?;
assert_eq!(read.value.container, hpr_io::ork::Container::Xml);
assert_eq!(read.value.document.version.to_string(), "1.10");
let rocket = read.value.document.root.child("rocket").expect("a rocket");
assert_eq!(rocket.child("name").expect("a name").text(), "Sounder");
assert!(read.warnings.is_empty());
read takes bytes, not a path: hpr-io does no file I/O, so that it can also run in a browser.
Hand it the bytes of a real .ork and the three containers are all the same call. What comes back
is an OrkFile (the container, the document, and the archive’s other entries) beside the
warnings the read raised.
Code: hpr_io::ork (API reference), written for
M3.1a. Rules below are from the format documentation unless
marked Observed (seen in real files) or Policy (hpr’s own choice).
Sources
- [F] OpenRocket file format documentation,
https://openrocket.readthedocs.io/en/latest/dev_guide/file_specification.html, and the
fileformat.txtshipped with the program. There is no XSD: nothing machine-checkable describes a.ork, and every OpenRocket release has added tags. - Observed: 75
.orkfiles, all cached underrefs/and never committed: 27 from the private design corpus, 20 hand-authored fixtures from Loft and 1 from Debrief (hpr’s two predecessors), 17 example designs inside the pinnedOpenRocket-24.12.jar, 9 from theopenrocket-databaseparts library, and 1 cached elsewhere. Generated files underrefs/scratch/are excluded from the default survey. By the files’ owncreatorattribute, 55 of the 73 readable files were written by OpenRocket and 18 were hand-authored, so where a count says what a real OpenRocket writes it is given over those 55. Every count below is printed bycargo xtask orkunless it names another source. - OpenRocket’s own Java source is not consulted: it is GPL, and hpr is MIT OR Apache-2.0 (ADR-051): hpr is built from published documentation and real files, never from another program’s source, which is what “clean room” means here.
The three containers
The first bytes say which one a file is in ([F]; Loft lesson L56: Loft told them apart by their magic bytes, and a malformed file had to give an error rather than crash):
| container | first bytes | what it holds |
|---|---|---|
| zip | 50 4b 03 04 (PK␃␄) | an entry called rocket.ork (the design as XML) beside anything else the design carries |
| gzip | 1f 8b | the design document, compressed on its own. What older OpenRocket versions wrote |
| XML | <, after an optional byte-order mark and blank lines | the design document itself |
Observed: of the 73 files that open, 71 are zip and 2 are plain XML. No gzip .ork survives in
the current scratch-excluding survey, so that path is held by a test rather than by a real file.
Policy. Inside a zip, the design is the entry named rocket.ork. If there is none, the first
entry whose name ends in .ork or .xml is read as the design and a warning says so. Every other
entry is kept byte for byte as an attachment: thrustcurves/*.rse motor curves (schema 1.11),
preview.png, lookup tables. Observed: 38 .png, 11 .jpg, 3 .gif and 3 .rse
attachments across the corpus, but all three .rse curves come from the single schema 1.11 file,
so the 1.11 attachments M3.1c will read
(Loft lesson L57: Loft threw them away) are a sample of one.
Nothing reads them yet; they are kept so that
M3.1c can, and so an export can put them back.
The schema version
The root element carries it: <openrocket version="1.10" creator="OpenRocket 24.12">. Version 1.9
is OpenRocket 23.09, 1.10 is 24.12, and 1.11 (26.xx, documented but not yet released) adds embedded
.rse curves, CSV lookup tables, a gravity model and preview.png.
Observed, across the corpus:
| schema | files | written by |
|---|---|---|
| 1.4 | 4 | OpenRocket 13.05, 15.03 |
| 1.5 | 10 | OpenRocket 15.03 |
| 1.8 | 5 | OpenRocket 22.02 |
| 1.9 | 3 | OpenRocket 23.09 |
| 1.10 | 53 | OpenRocket 24.12, and hand-written fixtures |
| 1.11 | 1 | an OpenRocket 26.xx snapshot |
Policy. A version past 1.11 is read anyway, with a warning: a newer file is mostly an older
file with tags added, and refusing it outright would help nobody. A version that is not
major.minor is an error.
The document
The design document is read into a tree of elements and text and nothing is interpreted. That is deliberate: with no schema to check against, the only way to be sure a later step has not quietly dropped something is to keep the whole file and be able to write it back.
Policy, and what the tree keeps:
- Every element, in document order, with its attributes in the order they were written.
- The text of a leaf element exactly as written, including leading and trailing spaces:
<name> Sounder </name>is not the same as<name>Sounder</name>until something decides to trim it. - Text an element holds beside child elements. Observed: OpenRocket writes this, and only
here; a simulation’s
<warning>prints its own message after its fields, in 48 elements of 19 files, andcargo xtask orkfinds no other tag that does it. - The blank text that only lays the file out (the indentation between child elements) is not kept, so that writing the document out again is free to lay it out afresh.
- An XML comment or processing instruction is dropped, and counted as a warning. No
.orktag carries meaning in one. A comment, a processing instruction or a CDATA section splits a run of text in two as it is parsed; the pieces are joined back, because dropping the comment leaves the writer nowhere to put the split.
The one thing the tree does not keep is XML namespaces: a prefix and its declaration are
dropped, and two attributes differing only by prefix become one. No .ork OpenRocket writes uses
them, and a document that declares any says so in a warning.
Writing the document back out uses one fixed style (not W3C Canonical XML, which is a different
thing): two-space indentation, <tag/> for an empty element, and character references for the characters that would otherwise change when read again
(a carriage return in text; a tab, newline or return in an attribute). Reading a document,
writing it, and reading it again gives the same document. That is what “keeps everything” means
here. Invariant (tested):
parse(to_xml(parse(x))) == parse(x): by hpr_io::ork::tests::any_document_written_and_read_again_is_unchanged
over generated trees, by reading_writing_and_reading_again_gives_the_same_document and
awkward_text_and_attributes_survive_being_written on awkward text, and by cargo xtask ork over
every real file in the corpus.
The values inside the tags
A component’s numbers sit in leaf elements, and three things about them are not obvious. All three
are mistakes Loft made, and each is settled here by what the corpus shows rather than by a
specification, because .ork has none. Code: hpr_io::ork::value
(API reference), decided in ADR-052.
A dimension may be automatic. <aftradius>auto 0.025</aftradius> means “OpenRocket works this
out from the neighbouring components, and 0.025 m is what it last worked out” (so OpenRocket 24.12
does; older releases may differ, see below). A bare auto
(<outerradius>auto</outerradius>) is the same with nothing worked out yet, and is the commoner
form: 309 of the 413 automatic dimensions in the corpus cache no number, so a reader that resolves
them cannot treat the cached value as a shortcut. hpr keeps both halves: which it is, and the cached number.
Keeping only the number is
Loft lesson L58: it turned automatic dimensions into hand-typed
ones the next time the design was saved. Observed: 413 automatic dimensions across the corpus,
on seven tags.
| tag | automatic |
|---|---|
outerradius | 131 |
innerradius | 80 |
cd | 79 |
radius | 43 |
packedradius | 36 |
aftradius | 30 |
foreradius | 14 |
A tag may be written under two names. OpenRocket renamed several and writes both, so an older
reader still finds one. Two of those renames are that and nothing more, and hpr reads either name,
taking the newer. Observed: on every element that carries both, the two agree: on the text,
and on the type/method attribute that says what the number is measured from.
| newer | older | elements with both | agree on text | differ on frame |
|---|---|---|---|---|
axialoffset | position | 642 | 642 | 0 |
instancecount | fincount | 109 | 109 | 0 |
Two that disagree is not something OpenRocket writes, so it raises a warning, whether they disagree on the number or on where the number is measured from.
Three more pairs look the same and are not. The newer name of each carries a method
attribute (the frame the number is measured in) that the older name never carries:
| newer | older | elements with both | agree on text | differ on frame |
|---|---|---|---|---|
angleoffset | rotation | 95 | 95 | 95 |
angleoffset | radialdirection | 26 | 26 | 26 |
radiusoffset | radialposition | 0 | n/a | n/a |
For a long time hpr read neither name of any of them. The two angle pairs agree on the number
every time and differ on the frame every time; radiusoffset (on 106 elements) and
radialposition (on 542) are never written together at all, so nothing about them had been
measured. Reading one as the other would move a component without saying so.
M3.1b3 settled all three without ever having to say what the older name’s frame is (see how far off the axis, below).
A stated zero is a value. <overridecd>0.0</overridecd> means no drag at all, not “no
override”: reading it as missing is
Loft lesson L63, which charged a zero-drag part full drag. A
component declares its own mass, center of gravity and drag coefficient with three tags, and
whether each covers the components inside it with three more, all read independently. A part’s or
a stage’s drag coefficient goes into the design as its drag_override, with its own flag, and
flies as OpenRocket 24.12 flies it: on the reference area, once per fin, lug or button, in place
of all the part’s own drag (A part’s stated drag coefficient,
ADR-167).
One on the <rocket> element itself has no place in the design and is not applied; no file seen
has one.
Observed override tags: overridemass 108, overridesubcomponentsmass 95,
overridesubcomponents 10, overridecg 14, overridesubcomponentscg 9, overridecd 2,
overridesubcomponentscd 2. The third of those is the single flag that files before schema 1.9
use in place of the three per-quantity ones. Policy: it is read as OpenRocket 24.12 reads it:
as setting all three, in files of schema 1.4, 1.8 and 1.10 alike. Where a part writes both forms,
the one written later wins, quantity by quantity: <overridesubcomponents>true</overridesubcomponents>
then <overridesubcomponentsmass>false</overridesubcomponentsmass> covers the parts inside for the
center of gravity and the drag but not the mass. No element in the corpus carries both forms. Eleven
probe designs measured this (M2.2e6, the old override flag),
and the tests in hpr_validate::openrocket hold hpr to them. hpr no longer warns about the flag.
Not measured: a value other than true or false, which is dropped with a warning (so the design
is not flown), and a flag tag written twice on one part, which is read at its first copy, also with
a warning.
Warnings, not failures
A design written by an older OpenRocket, or by another program, should still open. Everything that
departs from [F] but leaves the file readable is a warning that travels with the result
(hpr_io::ork::Warning), not an error:
| kind | means | raised for |
|---|---|---|
Skipped | a whole part was left out | a component this reader cannot give an honest shape (below); an attachment entry that could not be decompressed, or one that would pass the unpacking limit; a damaged design entry is an error, not a warning |
Dropped | a value was ignored | a comment or processing instruction; an XML namespace; a tag whose text is not the number, count or flag it should be; two names for one value that disagree; a dimension the file does not give, read as zero |
Unusual | read as it stands | a schema version past 1.11; no creator attribute; a design entry not called rocket.ork; a surface finish or an axial-offset method this reader has no rule for; an automatic radius with nothing to take, given OpenRocket’s 25 mm default; a part inside an inner tube set off the body’s axis, placed from the body’s axis (below); a <rocket> holding nothing |
Observed: reading the corpus’s containers and documents raises no warnings at all: every file that opens is ordinary. Building a rocket from those documents raises 21 warnings over 73 readable files: 4 dropped, 8 skipped and 9 unusual (below). Every kind of warning the container and document readers can raise is therefore exercised by a test rather than by a file anyone shipped.
Only these stop a read:
- bytes that are none of the three containers;
- a damaged zip or gzip;
- a zip with no entry that could be the design;
- a design that is not UTF-8, or not well-formed XML;
- a root element that is not
<openrocket>; - a
versionattribute that is missing or is notmajor.minor; - a document that nests more than 64 elements deep;
- an archive that unpacks to more than 256 MiB.
Why there are two limits
A deflate stream can expand by about a thousand to one, so a .ork of a megabyte can ask for more
than a gigabyte of memory, and one of forty megabytes can ask for more than a machine has. Running
out is an abort, not an error. So a read decompresses at most 256 MiB out of one archive (the
largest in the corpus unpacks to 2,052,024 bytes, document and attachments together) and an entry
that would pass the limit is left out with a warning. If the entry left out was the design, the read fails rather than
returning half a file. hpr_io::ork::container::unpack_within takes another limit, for a caller
with less memory to spend.
Reading descends the tree, and so does the XML parser underneath. A file written to nest deeply
enough exhausts the stack, which is a crash rather than an error, where
Loft lesson L56 asks for an error. Feeding roxmltree documents
of increasing nesting in a debug test build, on a 2 MiB test-thread stack, it read 120 levels and
died on 130; the process aborts, so this one measurement cannot itself be a committed test, and
the figure moves with the stack a platform gives a thread. That is the argument for not relying on
it.
So hpr counts the nesting before the text reaches the parser, with a scan that skips comments,
CDATA and processing instructions and tracks quotes, and refuses anything past 64 levels. The scan
can only ever count more levels than a parser will descend (XML forbids a raw < in an
attribute value, so every element start it sees is a real one) and
hpr_io::ork::tests::the_depth_scan_never_undercounts holds it to that over generated documents.
Observed for the depth, over the 73 files that open: 53 nest 11 deep, and the distribution runs 4, 7, 9, 10, 11, 12, 13, 14 and 17. An ordinary single-stage design reaches 11; the deepest, at 17, is OpenRocket’s own parallel-booster example, where each nested stage or inner tube costs two levels and a component’s appearance three. So 64 leaves about three more levels of nesting than anything anyone has written.
Checked against real files
cargo xtask ork reads every .ork in the reference library and in the OpenRocket jar’s example
set, writes them back out, reads them again, and reports counts. The private corpus stays private:
the per-file detail goes to a gitignored corpus-out/, and only counts are published. On
2026-09-23, excluding generated refs/scratch/ files:
| files found | 75 |
| opened | 73 |
| written and read back unchanged | 73 |
| warnings raised reading the container and the document | 0 (building a rocket from them raises 39; see below) |
| refused, not well-formed XML | 2 |
| deepest nesting | 17 |
| elements holding text beside children | 48, in 19 files |
| largest unpacked (document and attachments) | 2,052,024 bytes |
The two that do not open are hand-written fixtures for Loft’s browser tests, and neither is XML: a
<databranch> is closed with </flightdata>. Python’s expat refuses both at the same lines hpr
does, so this is the files’ fault and not the reader’s. They are listed by name in the survey,
which fails if either one ever behaves differently.
Here the reference library is every .ork under refs/, fetched by cargo xtask refs fetch;
the 17 example designs inside the OpenRocket jar (also under refs/) are counted with it unless a
table lists them apart.
Every other file imports without an error. An import error is a file that does not read, or a
design that reads but does not lay out. Warnings are not errors, because a warning never stops an
import. The survey counts both kinds of error for each source. On 2026-09-23, excluding generated
scratch/ files from the default scan:
| source | files | read | laid out | errors |
|---|---|---|---|---|
the private design library (loft-fixtures) | 27 | 27 | 27 | 0 |
| the OpenRocket 24.12 jar’s examples | 17 | 17 | 17 | 0 |
everything else under refs/ | 34 | 32 | 31 | 2 (the two Loft fixtures that are not XML) |
One file in the last row reads but holds no design, so it has nothing to lay out.
What you can check yourself. The corpus is not public, so these counts are not reproducible on
a fresh clone: cargo xtask ork needs cargo xtask refs fetch first, and stops with
“no .ork files found” without it. What is reproducible anywhere is everything the tests cover:
cargo test -p hpr-io, and the same command on any .ork files you have, with
cargo xtask ork --dir <path>.
Snapshots of public designs
Seven small designs from Loft, the project owner’s earlier tool, are committed in
validation/fixtures/ork/loft-demo/ (MIT). The test
hpr_io::ork::tests::loft_demo_designs_read_as_snapshotted reads each with hpr_io::ork::design,
and a synthetic design whose motor flies from the bundled catalog besides, and compares a summary of
what it reads with a committed insta snapshot. A snapshot is a saved copy of
the output that the next run must match. Each summary records:
- the stages and body components;
- the structure’s mass and center of mass, to nine significant figures;
- the motor configurations, and which of them fly;
- the recovery settings and the stored simulations;
- what is kept in
x-openrocket, and every warning.
A snapshot shows that the reading has not changed, not that it is right. None of these numbers
is compared with another program here; the geometry is, in
the next section. For example, demo-stable.ork is a 38 mm trainer on
an AeroTech H128W. Its snapshot says the structure weighs 0.540 kg and that its one configuration
does not fly ("flown": []), because that motor has no thrust curve in the file or in hpr’s small
bundled catalog (motors). Each field’s meaning is in the section
of this page that reads it.
When a snapshot changes, the test fails and shows the old and new output. Run
cargo insta review (from cargo install cargo-insta) to see them side by side, and accept only a
change you can explain (or rerun with INSTA_UPDATE=always cargo test -p hpr-io and read the
diff in git); the new .snap file goes in the same pull request. The private reference
library is never snapshotted; cargo xtask ork reports it only as counts, as above.
Checked against RocketSerializer
In short. RocketSerializer is a second program that reads .ork files.
RocketPy’s team wrote it to turn an OpenRocket design into RocketPy’s inputs. This check compares
hpr’s reading of each design’s shape with RocketSerializer’s. Where the two differ, OpenRocket
itself, run on the same file, decides which is right.
On 2026-09-23 the current scratch-excluding check covered 71 designs. Of 1,171 numbers:
- hpr matched RocketSerializer on 1,061;
- on the other 110, OpenRocket gave hpr’s number, not RocketSerializer’s;
- hpr never differed from both.
Every one of hpr’s 1,171 numbers is also OpenRocket’s. That includes the 1,061 where it matched RocketSerializer. So no agreement here is a mistake the two readers happen to share.
This checks the reading: the dimensions in the file, and where each part sits. It does not check
mass, the center of gravity, or any physics. The ongoing M2.2
work compares those properties, motors, stored results and flights with OpenRocket; completed
mass-property results are in Mass properties.
The check was added by M3.1d2, the roadmap step that finished
reading .ork files, and ADR-059 records how it was decided.
What is compared. What RocketSerializer reports about the airframe’s shape:
| part | numbers |
|---|---|
| the nose cone | its shape, length and base radius, and, for a Haack series nose, its shape parameter (0 for a Von Kármán nose) |
| each transition | its length, and its radius at each end |
| each trapezoidal or elliptical fin set | the number of fins, root chord, tip chord, span, sweep, cant and cross-section (square, rounded or airfoil edges) |
| each of those parts | its station: where its front sits, in meters aft of the nose tip |
| the rocket | its body radius: the largest radius the file writes as a number |
RocketSerializer also reports each part’s name, and a fin set’s sweep angle when the file writes one (the files here write the sweep as a length); those are not compared.
How a difference is settled. A script, validation/oracles/rocketserializer/geometry.py, runs
RocketSerializer’s extractors, the functions it uses to pull each part out of a file, on every
design. It also asks OpenRocket 24.12, loaded on the same file, for each of the same numbers. It
writes what both programs read to a JSON file, the record. cargo xtask ork then compares hpr’s
reading with the record. Two numbers count as the same when they differ by less than 1 part in 10⁹:
- If hpr’s number is RocketSerializer’s, they agree.
- If it is not, but it is OpenRocket’s, then RocketSerializer differs.
- Otherwise hpr differs, and the survey fails.
Whatever RocketSerializer says, the survey also fails when one of hpr’s numbers is not OpenRocket’s. Otherwise a mistake hpr and RocketSerializer made together would pass as agreement. One such case is known. OpenRocket limits a fin’s cant to 15°, while hpr and RocketSerializer take a larger cant as written, so the survey would fail on such a file. Issue #148 tracks what hpr should do. No file here cants a fin past 15°.
A worked example. Loft’s public demo-dual-deploy.ork puts its fin set at the bottom of the
booster tube. The same tube also holds the drogue parachute, packed 0.08 m long, listed before the
fins in the file. The parachute sits inside the tube, so it adds nothing to where the fins are.
The file places the fins relative to the tube’s aft end, and OpenRocket and hpr both put the fins’
front 1.45 m aft of the nose tip. RocketSerializer adds
up the lengths of every part listed before the fins in the same tube, the parachute’s 0.08 m
included, so it puts them at 1.45 + 0.08 = 1.53 m. The record keeps that sum of earlier lengths
for every part, so this cause is checked, not assumed.
The results. From cargo xtask ork on 2026-09-23, over the 75 files it finds (the reference
library under refs/, excluding generated refs/scratch/, and the 17 examples inside the OpenRocket jar):
| all designs | each file once | |
|---|---|---|
| designs compared (files OpenRocket opens) | 71 | 51 |
| numbers compared | 1,171 | 855 |
| hpr agrees with RocketSerializer | 1,061 | 772 |
| RocketSerializer differs, and hpr’s number is OpenRocket’s | 110 | 83 |
| hpr differs from both | 0 | 0 |
| hpr’s number is OpenRocket’s | 1,171 | 855 |
Some files are in the library more than once: 13 of Loft’s private designs are copies of the jar’s examples, for one. The second column counts each file’s content once.
The 110 differences, by cause:
| cause | count |
|---|---|
| a fin set’s station: RocketSerializer adds the lengths of the parts before it in its parent, as in the worked example | 75 |
| a transition’s radius: RocketSerializer looks transitions up in OpenRocket by name, and takes the first of that name | 15 |
| no cause shown | 20 |
Stations rest on OpenRocket. RocketSerializer does not read a station, or a transition’s radius, from the file. It loads the design into OpenRocket and walks OpenRocket’s tree, adding up lengths as it goes, which is where the worked example’s extra 0.08 m comes from. So its stations are not an independent reading. It agrees with hpr on 8 of the 96 fin sets’ stations and 18 of the 22 transitions’; the rest are among the 110 above, where hpr’s station is OpenRocket’s. For stations this is really a check against OpenRocket alone. For lengths, chords, spans, counts and cant, which RocketSerializer reads from the file, it agrees with hpr every time.
For the 20 with no cause shown, the reason RocketSerializer’s number differs has not been traced. In each of them hpr’s number is OpenRocket’s, and the per-file record holds all three programs’ numbers:
- 17 are stations, 13 of fin sets and 4 of transitions.
- 1 is a nose cone’s base radius, and 1 is the body radius.
- 1 is a nose cone’s shape: a nose written as an
ogiveof shape parameter 0. OpenRocket draws it as a cone, and so does hpr, while RocketSerializer passes on the wordogive.
Four things OpenRocket does that the check allows for.
-
A design worked out again. OpenRocket’s first reading of an automatic radius can differ from the answer it settles on once it works the design out again (when an automatic radius has nothing to take). The script saves each design once, to a throwaway, before it reads anything, so every number in the record, RocketSerializer’s included, comes from the settled design.
-
A canted fin. OpenRocket turns a canted fin about the middle of its root chord. That moves the front of the root aft by half the chord times (1 − cos δ), where δ is the cant: 38 µm for a 0.495 m root at 1°, as in the OpenRocket jar’s Simulation extensions example. The file places the root before it is turned, and so does hpr. So the script reads OpenRocket’s station with the cant set to zero, then puts the cant back. It keeps the turned station too. For all 10 canted fin sets, the turned station is aft of the unturned one by exactly that amount, and
cargo xtask orkchecks it. -
A shape word. An ogive of shape parameter 0 is a cone (the spine says why). To tell which shape OpenRocket really draws, the script records OpenRocket’s radius a quarter, a half and three quarters of the way along the nose. hpr’s profile is compared with those three radii: all 72 noses RocketSerializer reports match. So a nose’s shape counts as OpenRocket’s when OpenRocket draws hpr’s profile, whatever word it uses.
-
A leading comment. OpenRocket 24.12 refuses a file that begins with a long enough comment. For 3 files, the script removes the comment from the copy OpenRocket reads, and the record says so.
What it leaves out.
- RocketSerializer reports no body tube, no inner part, no mass, no fin thickness, and no freeform or tube fins, so none of those is compared here.
- 6 parts inside pods or parallel stages are not compared. hpr reads pods since M1.13b and parallel stages inside a body tube since M4.5m, but the cross-check still leaves their parts out (issue #211).
- 3 values RocketSerializer gives nothing for are not compared: 1 body radius, in a file that
writes every radius as
auto, and 2 fin cross-sections, in a file that writes none. - 4 files are not compared, because OpenRocket 24.12 does not open them. One is Loft’s
demo-quirks.ork, whose parallel stage sits directly under the rocket. Two are the Loft fixtures above that are not XML. The fourth holds no design.
Run it yourself. CI does not run RocketSerializer or OpenRocket. It holds hpr to their saved
output for the seven public Loft designs, validation/fixtures/ork/rocketserializer-loft-demo.json,
with cargo test -p xtask rocketserializer. OpenRocket opens six of the seven. Of their 80
numbers, 74 agree, and the other 6 are fin stations that RocketSerializer adds earlier lengths to,
as in the worked example.
To make a record yourself you need Python 3.11, uv, Java 17 and
the OpenRocket 24.12 jar (cargo xtask refs fetch downloads the jar to refs/openrocket/). From
the repository root:
# Once: an environment of its own, with every package pinned.
uv venv -p 3.11 refs/venv-rs
uv pip install -p refs/venv-rs --no-deps -r validation/oracles/rocketserializer/requirements.txt
# The seven public designs, written over the committed record; nothing should change:
refs/venv-rs/bin/python validation/oracles/rocketserializer/geometry.py \
validation/fixtures/ork/rocketserializer-loft-demo.json validation/fixtures/ork/loft-demo
git diff validation/fixtures/ork/
# The whole reference library and, with --jar, the jar's example designs; then compare:
refs/venv-rs/bin/python validation/oracles/rocketserializer/geometry.py \
corpus-out/rocketserializer.json refs --jar
cargo xtask ork
For your own files, give the script their directory in place of refs: it writes what
RocketSerializer and OpenRocket read. Comparing hpr’s numbers with them is not automated yet;
cargo xtask ork compares hpr with the record of the reference library only.
The spine: stages and body components
The trunk of a design is its spine: the stages, and inside each of them the nose cones, body
tubes and transitions that stack end to end along the axis. hpr_io::ork::rocket turns a document’s
spine into an hpr_design::Rocket (shapes, lengths, radii, walls, materials and overrides)
and everything else (the tubes and rings inside the body, the fins and lugs on it, the recovery
gear) is counted and left for M3.1b3.
An automatic radius is marked, not filled in. Where the file says auto, the component carries
the dimension’s name and Rocket::layout() works the radius out from the neighbours, including
across a stage boundary, so a booster’s first component takes its radius from the stage ahead of
it. That is Loft lesson L59: Loft resolved within a stage only,
and a booster came out as whatever number happened to be cached. A stated wall is also kept when
the radius it sits in is automatic; judging the wall against a radius that is not known yet threw
it away, which is half of Loft lesson L61.
The shape parameter is not the same number. OpenRocket writes the ogive’s parameter as
κ = ρ_tangent/ρ (a tangent ogive’s radius of curvature over this one’s), so κ = 1 is a tangent
ogive and κ = 0 an infinite radius, which is a cone. hpr-design states the same shape the other
way up, as ρ/ρ_tangent, so the two are reciprocals: reading one as the other turns every secant
ogive into a bulged one. The power, parabolic and Haack parameters carry over unchanged. All four
are Niskanen’s appendix A, equations A.3 and A.7 to A.9.
Readings this page is not sure of, every one of them for the OpenRocket oracle (M2.2) to settle. The first is the spine’s; the rest come from the parts. Two readings this table held are settled: a shoulder, and a tube, of zero wall thickness weigh nothing, as in OpenRocket 24.12, measured in M2.2b1 (ADR-061).
| what the file says | how it is read | why it is in doubt |
|---|---|---|
no shapeclipped on a transition | clipped | it is the shape that reaches both radii; only 1 of the 21 transitions in the corpus states it |
| an angle, which way it turns | the same way hpr’s own frames turn | hpr measures a roll angle right-handed about an axis pointing at the nose; OpenRocket’s technical documentation puts its own x axis along the centerline pointing aft and leaves the rest unstated. If it means what that implies, every angle read here is mirrored; see below |
a polished finish | 2 µm | the number is the author’s, from 2013; a newer OpenRocket may have moved it (below) |
Measured on the reference library (cargo xtask ork, 73 readable files): 72 designs’ spines lay
out, over 92 stages and 274 body components: 178 body tubes, 73 nose cones, 23 transitions. Two
of the stages, and two nose cones and two body tubes, are parallel stages, read since
M4.5m (Parallel stages). The
survey marked 327 automatic dimensions, including 7 body radii that took OpenRocket’s 25 mm default.
The counts are what may be published; the per-file detail stays in the gitignored corpus-out/.
(When this was written, 3 designs did not lay out, and they were thought to be waiting on parts that were not read yet. Reading those parts, in M3.1b3, showed otherwise. One document holds no design at all, and two have radii with nothing to take, which M3.1b4 settled: when an automatic radius has nothing to take. All 72 designs lay out now.)
The parts on and inside the body
Everything that is not the spine hangs off it, and hpr reads it into the same
hpr_design::Rocket. Four families, with the words this page uses for them:
- Tubes inside a body component: an inner tube (usually the motor mount), a coupler (the short tube that joins two airframe sections), an engine block (the ring that stops a motor sliding forward). Each can hold parts of its own.
- Rings that center them: a centering ring, a disc with a hole (its bore) that holds a motor tube on the airframe’s axis, and a bulkhead, the same disc with no hole.
- What sits on the outside: fin sets, tube fins, launch lugs (the tubes a launch rod passes through) and rail buttons.
- What is packed in the bore: mass objects (an altimeter, a battery), parachutes, streamers and shock cords. hpr reads the cylinder each takes up when packed, not what it does when it opens.
Code: hpr_io::ork::attached, decided in
ADR-053: the parts on and inside a .ork body.
Opening a design, in full
rocket reads the airframe alone. To get the motors as well, call hpr_io::ork::design instead
(motors and their configurations).
This is the doctest on hpr_io::ork::rocket, which CI runs.
Hand it the bytes of a .ork, and what comes back is a design and the warnings reading it raised:
let read = hpr_io::ork::read(xml)?;
let design = hpr_io::ork::rocket(&read.value.document);
// Anything the reader could not take at face value travels with the result. Nothing here did.
assert!(design.warnings.is_empty(), "{:?}", design.warnings);
// The ring's outer radius says `auto`, so the design carries the dimension, not a number...
use hpr_design::AutoDimension;
let ring = &design.value.stages[0].components[1].children[0];
assert!(ring.auto.contains(&AutoDimension::OuterRadius));
// ...and the layout works it out: the bore of the tube the ring sits in, 0.05 - 0.002.
let layout = design.value.layout()?;
let (_, placed) = layout.find("ring").expect("the ring");
let hpr_design::Part::CenteringRing(ring) = &placed.part else { panic!("a ring") };
assert!((ring.outer_radius_m - 0.048).abs() < 1e-12);
assert!(placed.own.mass_kg > 0.0);
design.warnings is where a part that was left out is named. It is a Vec of
Warning, each carrying where in the file it happened,
how much was lost (Skipped, Dropped or Unusual) and a sentence saying what was read and how:
for example “a launch lug sits on a nose cone, and hpr attaches one only to a body tube; it was
left out”. Read them: a design that opens cleanly raises none, and the 73 readable files of the
reference library raise 10 between them, every one of them explained on this page
(what the 10 warnings are).
.ork tag | read as | notes |
|---|---|---|
innertube, tubecoupler, engineblock | InnerTube | may hold parts of its own |
centeringring | CenteringRing | bore and outer radius may both be automatic |
bulkhead | CenteringRing with no bore | |
trapezoidfinset, ellipticalfinset, freeformfinset | FinSet | with its tab, its cant (the angle the fins are turned to induce roll) and its section (the shape along the chord: square, rounded or airfoil) |
tubefinset | TubeFinSet | an auto radius closes the ring around the body, as OpenRocket works it out (Tube fins sized from the body); both sets in the corpus are written so |
launchlug, railbutton | LaunchLug, RailButton | a row of them is one part with a count and a spacing; a rail button’s <screwheight> is its screw head, weighed since M4.5i (mass) |
masscomponent | MassComponent | |
parachute, streamer, shockcord | Parachute, Streamer, ShockCord | the packed shape, not the deployment |
podset | PodSet | the pod’s nose cones, body tubes and transitions, and everything on them (Pods) |
parallelstage | a Stage with its ParallelStage layout | inside a body tube on a rocket of one stage on the axis: a stage of its own, laid out as a pod set on that tube (Parallel stages); anywhere else, kept whole (what hpr keeps) |
Angles in a .ork are degrees. Nothing in the file says so, and every length beside them is in
meters, so it is easy to read one as radians, which would make <angleoffset>180</angleoffset>
more than twenty-eight turns instead of half of one.
Observed: of the 993 angles the corpus writes, 188 are not zero, and 178 of those are
larger than 2π, more than a whole turn, which no component is written at. The values themselves
are 180, 90, 45, 30 and 120, carrying the float dust (119.99999999999999) of a conversion that
went through radians and came back. cargo xtask ork prints all three counts, and
hpr_io::ork::tests::angles_are_degrees_not_radians holds the reading.
A fin’s cant is the same degrees: the corpus’s two non-zero cants are 1.0 and −3.98, which as radians would be 57° and 228°, a fin turned past half a turn from the airflow.
Where a part sits is one tag: <axialoffset method="bottom"> on the newer name,
<position type="bottom"> on the older, with the same five words (top, middle, bottom,
after and absolute) which are hpr_design::Position unchanged, but for a rail
button. OpenRocket gives a button no length and puts its center at the position, a row’s first
button there and the rest aft, so hpr moves the offset to where the button’s forward edge must be
(issue #151; measured on probes from the top, the
middle and the bottom). Observed: one tube coupler in the corpus has neither tag; it is read
flush with its parent’s forward end, with a warning.
How far off the axis a part sits is radialposition on some tags and radiusoffset on others.
ADR-052 read neither, for want of a source saying what the older name measures from.
That question need not be answered, because the two never meet: radialposition (542 elements)
is written on the parts inside a body, radiusoffset (106) on the parts on it and on the pods,
and no element carries both. They are two tags on different components, not two names for one. So
each is read where it is the only name its part has, and neither is ever read as the other.
Which way round a part sits is the angle, and there the two names do meet. angleoffset is the
newer one; the older is rotation on a fin set (95 elements carry both) and radialdirection on
everything else (26). On all 121, the two agree on the number, so which one is read cannot
change an angle. They differ on the frame (relative to the parent against fixed in the
rocket), but that is the same angle for every parent hpr builds, all of which sit on the rocket’s
own axis. A pod set hangs from a body tube, which is on the axis too, and OpenRocket 24.12 puts its
pods in the same places whichever of its three frame words the file writes (relative, fixed,
or mirror_xy, mirrored) (Pods). Whether a part inside a pod measures its angle from
the pod or from the rocket has not been tested against OpenRocket.
Which direction the angle turns is assumed, and is not settled. hpr measures a roll angle
from x_B toward y_B, right-handed about +z_B, which points at the nose
(frames). OpenRocket’s technical documentation (§3.1.4) puts its
own x axis along the centerline pointing aft, and says nothing about the other two. A
right-handed angle about an aft-pointing axis is a left-handed one about hpr’s +z_B, so if that
is what OpenRocket means, every angle read here is mirrored: a mass object at 90° sits on the
other side of the airframe, and a canted fin set rolls the other way. Nothing in the reference
library can settle it, because a mirrored design is still a perfectly valid design; one
deliberately asymmetric design put through the OpenRocket oracle
will. Until then hpr takes the number unchanged, and this is on the list of
readings that are not settled.
An override that covers the parts inside a component is read from the mass flag. A .ork says
“this figure covers the components inside this one” once for the mass, once for the center of
gravity and once for the drag; hpr-design says it once for the whole component, so the two cannot
always agree. Mass is the quantity the flag is written for (95 of the 104 written in the corpus
are the mass flag), so the mass flag decides, and a center-of-gravity flag that disagrees with it
raises a warning. Nothing in the corpus disagrees. This matters from this milestone on and not
before: until a component had parts inside it, “covers the parts inside” covered nothing.
What a part takes from its parent is resolved by Rocket::layout(), never here. A coupler’s or
a ring’s automatic outer radius is its parent’s bore (inside a nose cone or a transition, the bore at
the part’s narrower end, below); a ring’s automatic bore is the widest motor
tube beside it that overlaps it along the axis; a packed part fills the room left in the bore.
Automatic outer radii resolve in a pass before any ring’s automatic bore, so a ring’s answer
cannot depend on whether the tube inside it was written first:
Loft lesson L60, where Loft resolved as it walked and a bulkhead
inside a coupler stayed NaN (not a number, meaning no numeric value was available).
Inside a nose cone or a transition
A coupler, an engine block or a ring can sit inside a hollow nose cone or transition, with its outer
radius written auto. A nose’s bore narrows toward the tip, so “the parent’s bore” needs a place
along the nose. Since M2.2e7 hpr takes it where OpenRocket
24.12 does (ADR-096, the decision on fillets and a nose’s bore):
- The radius is the parent’s outer radius at the part’s narrower end, less the parent’s wall. The nose’s shoulder is left out, even when the part reaches into it.
- If the wall is thicker than that radius, the radius is zero.
- A filled (solid) nose cone has no bore. The part is left out with a
Skippedwarning. - A packed part (a parachute, a mass component) inside a nose cone still takes no automatic radius.
It is left out with a
Skippedwarning too. - A coupler at the very tip, where the wall meets the axis, has no radius at all. OpenRocket weighs it as nothing; hpr refuses to lay out the design.
- A tube whose own wall is thicker than the automatic radius it gets is a solid rod. This holds in a body tube too, and is here because two of the probes below measure it, one in each.
For example, take a conical nose cone 200 mm long, 50 mm in radius at its base, with a 2 mm wall,
and a coupler 30 mm long whose aft end sits at the nose’s base. Its narrower end is its forward end,
30 mm ahead of the base, where the cone’s radius is 50 × 170/200 = 42.5 mm. So its outer radius
is 42.5 − 2 = 40.5 mm.
An inner tube written auto keeps 9.5 mm. OpenRocket 24.12 does not work out an automatic
radius for an innertube, the tag a motor mount is usually written with. It keeps the 9.5 mm it
starts with, in a nose cone or a body tube alike. hpr reads it the same way, with no warning, so
a design holding one flies as OpenRocket flies it. A tubecoupler
or an engineblock written auto fills its parent as above.
Measured. Fourteen probe designs in validation/oracles/openrocket/conventions.py ask
OpenRocket these questions. They hold a coupler of automatic radius at a nose’s bottom, past its
base, with a shoulder, long from its middle, at the tip, and with a wall thicker than its bore; an
ogive nose; a transition; an engine block; a centering ring and a bulkhead; a mass component inside;
and an inner tube written auto, in a nose and in a tube. hpr flies 13 of the 14. Every part
inside a nose cone, transition or tube but one weighs OpenRocket’s mass at OpenRocket’s station:
the test holds the mass to 1e-14 and the station to 1e-15. Three things are pinned apart:
- One mass component, packed with an automatic radius inside the coupler, sits 20.75 mm from OpenRocket’s. OpenRocket shortens its packed length to keep its volume, and hpr keeps the written length (#186: OpenRocket repacks a part that does not fit).
- The nose cones’ and the transition’s own walls keep the gaps they already had: up to 3.9e-5 of the mass and 2.5e-6 m in the center, since hpr measures a wall square to the surface.
- The coupler at the tip is the 14th probe. hpr refuses it, and OpenRocket weighs it as nothing.
The test an_automatic_radius_inside_a_nose_reads_as_openrocket_does in hpr-validate holds hpr
to these answers. Before, hpr had no bore to give such a part, so it left the part out with a
warning, and a design holding one did not fly.
Tube fins sized from the body
A tube fin set is a ring of short open tubes glued along the airframe in place of flat fins. In
OpenRocket, a tube fin set’s radius can be written auto. OpenRocket then sizes the tubes from
the body tube they sit on. Since M2.2e8 hpr works that
radius out as OpenRocket 24.12 does (ADR-098, the decision on a tube fin set’s
automatic radius). Before, hpr left such a set out with a Skipped warning
(#133). hpr reads and weighs tube fins now, and since
M2.2e9 a design that holds them flies
(below).
The rule. With three tubes or more, each tube touches the body and its two neighbours, so the ring closes:
- The tubes’ axes sit on a circle of radius
R + raround the airframe’s axis, whereRis the body tube’s outer radius andrthe tubes’ radius. - Neighbouring axes are
2π/Napart around that circle, forNtubes, so the straight line between them (the chord) is2(R + r) sin(π/N)long. - Two tubes of radius
rtouch when their axes are2rapart. Setting the chord to2rgivesr = R sin(π/N) / (1 − sin(π/N)).
One or two tubes can’t close a ring. For those, OpenRocket gives the tubes the body’s radius, and
so does hpr. The rule is
TubeFinSet::closing_radius_m.
hpr applies it when it lays the design out, the step that works out every automatic dimension
(Automatic dimensions).
For example, take four tubes on a body of 50 mm radius. sin(π/4) is 0.7071, so
r = 50 × 0.7071 / 0.2929 = 120.7 mm. The axes then sit 170.7 mm from the airframe’s axis, and
neighbours are 2 × 170.7 × 0.7071 = 241.4 mm apart, which is 2r: they touch. Fewer tubes
must be wider to close the ring. Three to five tubes come out wider than the body, six exactly as
wide, and more than six narrower:
| tubes on a 50 mm body | radius |
|---|---|
| 1 or 2 | 50 mm, the body’s |
| 3 | 323.2 mm |
| 4 | 120.7 mm |
| 5 | 71.3 mm |
| 6 | 50.0 mm, as wide as the body |
| 8 | 31.0 mm |
Three more things are read as OpenRocket reads them:
- A wall thicker than the radius is cut to the radius, so the tubes are solid rods. OpenRocket weighs them that way, as it does an inner tube’s (above).
- More than 8 tubes are read as 8, with a
Droppedwarning, whether the radius isautoor stated. OpenRocket 24.12 reads 9, 12, 20 and 100 tubes as 8. hpr caps the count before it checks its own limit of 64 parts in a row, so 100 tubes read as 8 rather than being refused. That cap is OpenRocket’s reading of the file, not a limit of the physics: hpr’s own design files take any count. - A radial offset (tubes standing off the body) changes none of OpenRocket’s numbers. hpr
reads such a set sitting on the body, with a
Droppedwarning, as it did before.
Measured. Nineteen probe designs, small designs made only to ask
OpenRocket one question each, are in validation/oracles/openrocket/conventions.py. Each carries
one tube fin set:
- on a 50 mm tube:
autosets of 1, 2, 3, 4, 5, 6, 8, 9, 12, 20 and 100 tubes; six tubes of a stated 20 mm radius, with and without a 10 mm radial offset; twelve tubes of a stated radius; sixautotubes with a wall thicker than their radius; and fourautotubes on a tube whose own radius isauto, taken from the nose cone ahead of it; - on a 20 mm tube: 1, 2 and 5
autotubes. One and two take the body’s 20 mm; five take the closed form’s 28.5 mm.
OpenRocket’s answers are in validation/fixtures/ork/openrocket-conventions.json. The test
a_tube_fin_sets_automatic_radius_reads_as_openrocket_does in hpr-validate holds hpr to them:
| quantity | hpr against OpenRocket, on all 19 |
|---|---|
| the tubes’ radius and wall | within 1e-15, relative, in the test (equal when the oracle script compares them in Python) |
| the number of tubes | the same |
| each part’s mass | within 1e-14, relative; the one nose cone, on the probe whose tube takes its radius from it, within 5e-5, the gap nose walls already had (above) |
| each part’s center of mass, along the airframe | within 1e-15 m; that nose cone within 5e-6 m |
OpenRocket’s Tube fin rocket example is the only design in the reference library with tube fins, in two copies. It now reads with its tube fins, and its mass is within 5.0e-6 of OpenRocket’s and its center of mass within 2.4e-6 of its length (Mass properties).
What differs: the roll and pitch inertia. hpr keeps its own figure for both, each a departure: a difference kept on purpose, measured and pinned by the same test.
- Roll (spin about the airframe’s axis). For two tubes or more, OpenRocket’s roll inertia for the set is larger than any mass inside the ring could have. For a single tube the two agree.
- Pitch (turning end over end). OpenRocket’s figure has no term for how far the tubes sit from the airframe’s axis: per kilogram, it is the number of tubes times one tube’s own figure about its own middle. On the probes it is 1.3 to 2.6 times hpr’s. Some mass inside the ring could have that much, but not tubes where both programs put them.
Mass properties has the numbers. On the Tube fin rocket, hpr’s roll inertia is 98.7% below OpenRocket’s and its pitch inertia 1.98% below, almost all of it OpenRocket’s pitch rule.
How they fly. Since M2.2e9, tube fin aerodynamics, a design with tube fins flies. hpr’s aerodynamics takes each tube as a ring wing: a cited slope, a center from Fletcher’s measurements and hpr’s own derivation, and drag from the fin rules (Tube fins). The Tube fin rocket’s apogee is 6.95% above OpenRocket’s. On OpenRocket’s own drag, hpr’s apogee is within 0.03%, so the net gap is the drag’s (ADR-099, tube fins flown as ring wings). OpenRocket’s tube-fin drag was refined against measured flights, so hpr’s probably reads low (#228).
The surface finish
A surface finish is a roughness height R_s, the size of the bumps a painted or bare surface
leaves, which is what sets skin friction (drag). OpenRocket writes one of
five words for it; what each is worth in micrometers is not in the file format documentation,
and these are the only numbers on this page that come from neither the documentation nor the
corpus:
<finish> | OpenRocket calls it | roughness | where that number comes from | in the corpus |
|---|---|---|---|---|
rough | Rough | 500 µm | the author | 8 |
unfinished | Unfinished | 150 µm | the author | 2 |
normal | Regular paint | 60 µm | technical documentation §6 and the user guide’s dialog | 348 |
smooth | Smooth paint | 20 µm | the author | 88 |
polished | Polished | 2 µm | the author; see the caveat below | 14 |
Only the default is officially documented, and it twice over: the technical documentation section 6 says that in its test design “the ‘regular paint’ finish was selected, which corresponds to an average surface roughness of 60 µm”, and the user guide’s body-tube dialog reads “Component finish: Regular paint (2.36 mil)”, which is 59.9 µm. The user guide names all five and their order: “Rough, Unfinished, Regular paint, Smooth paint, and Polished, each with a decreasing (CD) from rough to polished”, but gives no numbers. The other four come from OpenRocket’s author, in The Rocketry Forum thread “Open Rocket Finishes” (post #6, 22 August 2013): “Rough (500 µm) / Unfinished (150 µm) / Regular paint (60 µm) / Smooth paint (20 µm) / Polished (2 µm)”.
Each becomes a Finish::Custom height (a roughness given as a number) rather than
one of hpr’s named finishes, whose names (“raw wood”, “dip-galvanized metal”) mean other surfaces
that happen to share a height.
polished is the one to doubt. Its 2 µm rests on a 2013 forum post and on nothing else, and
the number sits oddly: in the table the OpenRocket technical documentation itself
reprints (Table 3.2, from Hoerner), 2 µm is aircraft-type sheet metal, while “finished
and polished surface” is 0.5 µm. So OpenRocket’s “Polished” is not the row its name points at,
and a later version could reasonably have moved it. Rocketry-forum posts from 2023 onward list nine
finishes rather than five, which suggests the list has indeed changed, though neither the 23.09 nor
the 24.12 release notes mention it.
What it would cost: skin friction in the fully-rough branch goes as R_s^0.2
(drag), so 0.5 µm instead of 2 µm is a 1.32× change in the skin-friction
coefficient of whatever part says polished: 14 components in the corpus. No file in the
corpus writes any word but the five, so nothing here can settle it; the
OpenRocket oracle can. A word hpr has no sourced roughness for
takes hpr’s default and says so in a warning.
A part with no <finish> gets hpr’s own default, 20 µm, but OpenRocket reads it as regular
paint, 60 µm (issue #216). On the pod probes that
raised hpr’s apogee 6.0% to 7.3% above OpenRocket’s. OpenRocket writes a finish on every outside
part it saves, so this touches hand-written files and other tools’ files. Until it is fixed, set a
finish on every outside part.
Clusters
An inner tube can be a cluster: several like tubes side by side, each
holding a motor. The file names a pattern in clusterconfiguration, spreads it with
clusterscale (1 unless written) and turns it with clusterrotation (degrees, as every .ork
angle). hpr reads every tube into the inner tube’s list of places,
cluster_m, and the
motor in it becomes one motor per tube (Clusters).
No published document gives the patterns, so OpenRocket 24.12 was asked, run as an outside program
through its public interface (validation/oracles/openrocket/clusters.py, ADR-075).
Each pattern is a figure of points pₖ, one per tube, in units of 2 R s (defined below):
| pattern | tubes | figure |
|---|---|---|
single | 1 | one tube |
double, 3-row, 4-row | 2 to 4 | a row, one apart |
3-ring, 4-ring, 5-ring | 3 to 5 | a triangle, a square, a pentagon, each of side one |
6-ring | 6 | a hexagon of side one |
3-star, 4-star, 5-star, 6-star | 4 to 7 | a center tube and a ring of radius one |
9-grid | 9 | a 3 × 3 grid, 1.4 apart |
9-star | 9 | a center tube and eight on a ring of radius 1.4 |
The unit is 2 R s, for the tube’s outer radius R and the scale s: at scale 1 neighbouring
tubes touch, except in 9-grid, whose rows and columns are 1.4 diameters apart, and 9-star,
whose ring is 1.4 diameters from its center tube. The pattern is
turned by the tube’s roll angle θ less the rotation ρ (Rot turns a point by that angle):
[x, y]ₖ = 2 R s · Rot(θ − ρ) · pₖ
measured from the tube’s own radial offset. For example, a 3-ring of 40 mm tubes at scale 1 puts
the three axes 23.09 mm from the center (40 mm / √3). OpenRocket’s (y, z) are read as hpr’s
(x, y), the same assumption as for every roll angle (above).
On 25 probes (every pattern, a scale, a rotation, a radial offset, and all three at once) hpr puts
every tube within 1e-15 m of where OpenRocket does (the test
every_tube_of_a_cluster_is_where_openrocket_puts_it in hpr-validate). A pattern name OpenRocket
doesn’t know (4-square), it reads as one tube; so does hpr, with a warning.
A cluster’s mass and center of mass agree with OpenRocket’s. Its inertia does not: OpenRocket weighs the tubes as if stacked on the cluster’s axis, and hpr weighs each where it sits (Mass properties). Two more readings differ from what a builder would expect:
- A centering ring with an automatic bore beside a cluster takes one tube’s radius as its bore, as
OpenRocket gives it, so the tubes run through the ring and that mass counts twice. For a 3-ring
of 40 mm tubes in a ring 98 mm across, the ring weighs about two thirds more than one with three
holes would. The design checks warn of it (
ring_overlaps_inner_tube). - A part inside an inner tube set off the body’s axis is read at its own offset from the body’s
axis, with no parent’s offset added. OpenRocket places it from the tube’s axis: an engine block
in a tube 10 mm off the axis sits on that tube’s axis in OpenRocket and on the body’s axis in
hpr. hpr warns of it (
Unusual), and no file in the survey has one (#181). A cluster on the axis, the common case, is not affected, and neither is a motor in a mount that sits in the body tube, whose nozzle takes its mount’s offset.
A configuration with a motor in a cluster flies with one motor in every tube. On OpenRocket’s Clustered motors example, four motors in a 4-ring, all five configurations are within 1% of OpenRocket’s apogee and largest speed, with the parachute-opening cause removed (hpr’s flights against OpenRocket’s; the M1.9c milestone, which set a 5% bar before measuring).
Pods
A pod set hangs pods beside a body tube: each pod is the stack of nose
cones, body tubes and transitions written inside the podset, with everything on and in them, and
instancecount of them are spaced evenly around the axis. hpr reads it into a
PodSet (M1.13b). The first pod’s roll angle is
angleoffset, read as every angle is (which way round). Pods fly from
M1.13c1, each pod’s parts with their own normal force and
drag (aerodynamics: Pods).
Five probe designs with pods of bodies, fins, a tail cone, winglets and motors, and the same airframe without pods, fly within 0.81% of OpenRocket’s apogee (M1.13c2, a pod design against OpenRocket). They are listed apart from the designs, at the end of the report.
The one private design with pods, C02, has only a launch lug on each. So its flights, within
2.2% of OpenRocket’s apogee
(hpr’s flights of the private designs), check the pods’
placement and weight, not their aerodynamics. Of OpenRocket’s own two pod examples, Pods–airframes
and winglets flies since M4.5g4, the milestone that reads
its cockpit fin on the nose cone. Pods–powered with recovery deployment flies since
M4.5i, the milestone that weighs its rail buttons’ screw
heads and flies motors in more than one pod set (ADR-168); its flights are
below.
How far from the axis depends on the method written on radiusoffset, and no document says
how. OpenRocket 24.12 was asked, as an outside oracle, on probe designs
(validation/oracles/openrocket/pods.py, eighteen of them). It puts each pod’s axis at this
distance from the body’s (OpenRocket’s own names for the methods in brackets):
radiusoffset method | distance | tube 50 mm in radius, pods 10 mm in radius, number 20 mm |
|---|---|---|
relative (“surface of the parent component”) | tube radius + pod radius + the number | 80 mm |
surface (“… without offset”) | tube radius + pod radius; the number is ignored | 60 mm |
free (“center of the parent component”) | the number, from the axis | 20 mm, inside the tube |
The pod radius is the widest of the pod’s own parts. One probe has a stated 10 mm nose, a
10 mm tube, a 15 mm tube and another 10 mm tube, and OpenRocket puts the pod at the 15 mm radius:
not the first part’s, the first tube’s or the last part’s. hpr’s test
(every_pod_is_where_openrocket_puts_it, in hpr-validate) holds every part in every pod to
OpenRocket’s place, to 10⁻¹⁵ m. An automatic radius inside a pod can only take a radius stated in
the pod, so hpr takes the widest stated one.
hpr fixes each pod’s distance when it reads the file, so it cannot work out an automatic tube
radius later. When the tube’s radius is automatic, hpr uses the number OpenRocket cached for it and
warns that it did. With nothing cached, the pod set is left out. A method hpr does not know is
read as relative, and a missing radiusoffset as touching the tube, each with a warning.
A pod of no length. OpenRocket’s own example Pods–airframes and winglets hangs its winglets from a pod whose only part is a tube of no length, no wall and, usually, no radius. OpenRocket names that tube “(phantom body)”. hpr reads such a pod as written:
- The tube weighs nothing.
- The pod’s radius is the tube’s, usually 0, and the distance table above applies with it: for
relative, the pod’s axis is the body tube’s radius plus the number out. - Fins and a launch lug on the tube sit on its surface, as on any tube. With a radius of 0 that is the pod’s own axis: the fins’ roots meet there, and a lug’s axis is the lug’s own radius out from it, at the lug’s angle. A lug 3 mm in radius on a pod 62 mm from the axis, turned to 180°, has its axis 59 mm from the body’s.
- Everything turns with its pod, as in any pod.
- A tube of no length has no room inside and no wall. A pod set whose tube of no length holds a part inside it is left out, with a warning. A fin tab deeper than the tube’s radius is dropped, also with a warning, and the fins are read without it.
OpenRocket 24.12 agrees on six probes:
- two fins at 90° on a tube of no radius;
- three fins on each of two pods, on a tube of no radius;
- two fins on a tube 10 mm in radius;
- two pods at 30° on a tube 10 mm in radius, three fins each at 20°, which shows each fin turned with its pod;
- a lug turned to 180°, and two at 0°.
Each fin’s root and each lug’s axis is where OpenRocket puts it, to 10⁻¹⁵ m (the test
every_pod_is_where_openrocket_puts_it, in hpr-validate).
A pod set that holds nothing is read too, and weighs nothing, in OpenRocket as in hpr. A mass override on one is dropped, with a warning. OpenRocket 24.12 puts that mass at the rocket’s tip, on its axis, where no design means it to be.
What it weighs. The test pods_weigh_as_openrocket_s, in hpr-validate, compares hpr with
OpenRocket on every probe:
| quantity | how close to OpenRocket |
|---|---|
| each tube, fin set and lug in a pod: mass and center | 2 parts in 10¹⁵ |
| a pod’s nose cone: mass, center | 8.3 parts in 10⁸, 4.0 parts in 10⁹ |
| the whole rocket: mass | 1.2 parts in 10⁷ |
| the whole rocket: center of mass | 1.10 parts in 10⁷ with pods that have a length, 1.13 parts in 10⁷ with pods of no length |
| roll inertia, with OpenRocket’s fin shortcut in place of hpr’s | 2 parts in 10⁹ |
pitch inertia about x_B, with no fins in the pods | 6.1 parts in 10⁷ |
OpenRocket weighs a nose cone a little differently from hpr. The probes’ own nose, on the airframe, is 5.1 parts in 10⁷ apart in mass, and that alone puts the bare airframe 1.17 parts in 10⁷ from OpenRocket’s. No probe with pods is further from OpenRocket than that.
hpr works out a fin’s roll inertia exactly, and OpenRocket takes a shortcut: the two differ by up to 5% on a fin set (mass: fins). The 2 parts in 10⁹ holds only once OpenRocket’s shortcut is swapped in for hpr’s (ADR-062: why fin roll inertia differs). OpenRocket’s rule for a fin’s pitch inertia is not known, so where the pods hold fins the pitch gap is only pinned: 2.6 parts in 10⁷ to 3.4 parts in 10⁵.
OpenRocket gives one pitch inertia for both pitch axes. With one or two pods the rocket is not the
same both ways, so hpr’s inertia about y_B differs from that one number, by 0.3% to 1.1% on the
probes of pods that have a length (M1.13b1). hpr’s is the
true inertia about that axis.
With three pods the two agree.
A flipped nose cone, anywhere in a design, is a tail cone, and hpr reads it as one: a transition from the nose’s base radius to a point. Its base is forward, so an automatic radius takes the part ahead of it. With one end a point, a clipped and an unclipped transition are the same whole nose shape (shapes). The only one in the corpus closes a pod.
Left out, with a reason. A pod set is left out with a Skipped warning when hpr cannot lay it
out, in any of these cases:
- a pod holds a nose cone or transition of no length, or a part of negative length;
- a pod’s tube of no length holds a part inside it;
- its pods’ radii are all automatic;
- the tube’s radius is automatic with nothing cached;
- the distance comes out negative;
- it hangs from anything but a body tube;
- it sits inside a pod.
The corpus has none of these. A part directly inside a pod set that is not a nose cone, body tube or transition is left out on its own. An override on a pod set is its pods’ total, since a pod set weighs nothing of its own.
cargo xtask ork counts them. On the corpus, 2026-09-27:
| pod sets | count |
|---|---|
| written | 9 |
read into PodSet | 9, holding 12 pods |
| left out | 0 |
cargo xtask ork on 2026-09-27 also weighs each design against OpenRocket 24.12:
- Pods–powered with recovery deployment (a public example): its mass is now within 1% of OpenRocket’s. It was 12.5% light with its pods left out (M1.13b1).
- Pods–airframes and winglets: with its winglet pod left out, its roll inertia was 1.78% below
OpenRocket’s, even with OpenRocket’s fin shortcut. With the pod read, it is no longer among the
designs
cargo xtask orklists outside 1% with that shortcut. Its freeform cockpit, on the nose cone, is read since M4.5g4 (Fins on a nose cone or a transition).
Parallel stages
A <parallelstage> inside a body tube, on a rocket of one stage on the axis, is read as a stage
of its own, laid out as a pod set on that tube. OpenRocket uses one for boosters strapped beside
the core. hpr reads it since M4.5m
(ADR-171: a parallel stage is a pod set that separates). Its copies sit as a pod set’s
pods do (Pods): instancecount of them, radiusoffset from the axis, the first at
angleoffset, at its axial position along the tube. Its parts weigh to its own stage, not to the
tube’s, and its own separationevent drops it, read as an axial stage’s is.
The design’s side is in Parallel stages, and the flight’s in Boosters beside the core. When its motors light, and why one with no motor that lights stays on, is in Delays and ignition. The writer puts a parallel stage it read back inside its tube.
Left out, with a Skipped warning naming the reason. hpr leaves out a parallel stage:
- on a rocket of more than one stage on the axis: OpenRocket numbers the parallel stages among the axial ones, and no example or library design has one to check a reading against;
- inside a pod or another parallel stage;
- with no nose cone, body tube or transition in it;
- placed
afterthe part before it, since a parallel stage sits along its tube; - in any case where a pod set would be left out (Pods).
One written directly under <rocket>, as in Loft’s demo-quirks.ork, is not read there, and
stays in the tally of tags hpr does not read. OpenRocket 24.12 refuses to open that file. Every
parallel stage left out is kept whole for writing the file back
(what hpr keeps).
Measured. OpenRocket’s Parallel booster staging example flies both its configurations as
saved. Under OpenRocket’s stored conditions, recovery as saved
(the report).
A configuration is named by its motors, stage by stage with the sustainer first, the stages
separated by ;: [I115W-10; 2× E12-0] is an I115W-10 in the core above two E12-0s in the
boosters, and None is a stage with no motor.
| configuration | apogee, OpenRocket / hpr (m) | Δ | largest speed Δ |
|---|---|---|---|
[I115W-10; 2× E12-0] | 1123.6 / 1137.5 | +1.23% | +3.25% |
[I115W-10; None] | 851.8 / 857.6 | +0.68% | +1.56% |
In the first, the boosters drop at 2.44 s, at the E12s’ charge, as in OpenRocket. In the second,
they hold no motor and stay on. The survey’s only other parallel stages are the library’s copy of
the same example and demo-quirks.ork’s.
What is left out, and why
A part hpr cannot give an honest shape is left out with a Skipped warning naming the part and
the reason, rather than guessed at. The design still opens and still lays out; what is missing is
named, never silent. No part in the whole corpus is left out now, under these rules:
| rule | in the corpus |
|---|---|
| an external part other than a fin set on anything but a body tube: a lug’s or a button’s footing on a nose cone is not a straight line | none |
| a fin set on a nose cone or a transition whose root can’t be read along the surface (below) | none; the 2 fin sets it caught, the cockpit of Pods–airframes and winglets in the jar’s copy and the corpus’s, are read since M4.5g4 |
| a freeform outline on a body tube that does not end on the root, which would have to be closed along a body it never touches | none |
| a part whose automatic radius needs a bore its parent has not got: a filled nose cone, or a packed part inside a nose cone (above) | none; the 1 coupler in a nose cone it caught is read since M2.2e7 |
Another rule left out the 2 tube fin sets whose radius OpenRocket sizes from the body, until M2.2e8 worked that radius out (Tube fins sized from the body).
Fins on a nose cone or a transition
This covers how hpr reads a fin set that sits on a nose cone or a transition, where the body’s surface is not level along the fin’s root (the edge where the fin meets the body). It is checked on one fin on one design, against OpenRocket only, and no measured flight checks it.
OpenRocket gives a fin’s outline as points (x, h): x aft of the root’s leading edge and h
out from the body’s surface there. The root runs along the surface. On a body tube the surface is
a straight line at h = 0; on a nose cone it curves. The cockpit of OpenRocket’s Pods–airframes
and winglets is a single fin on the last 50 mm of a
tangent ogive nose. Its outline is (0, 0), (9.35, 6.96) and
(50, 2.23) mm: the last point is 2.23 mm up because the ogive’s surface rises 2.23 mm over those
50 mm.
hpr reads such a fin as it is written (ADR-166, fins on a nose cone):
- The outline is kept as written. The root is drawn through 63 points on the parent’s profile
between its ends, 64 straight pieces in all. On the cockpit each piece sits at most 0.14 µm
inside the curve (
a_root_along_a_nose_cone_stays_within_its_documented_sag). - The fin’s heights are measured from the radius at its root leading edge, which is also the body radius for its mass and its fin–body interference.
- The design’s layout checks that every root point is within a micron of the surface, and that the straight pieces between them cut off or add no more than 0.1% of the fin’s area.
How well it matches. Through OpenRocket’s own 21 root points, hpr’s area, mass and center of
mass for the cockpit are OpenRocket’s (1.44954e-4 m², 0.172 g), to the six digits OpenRocket
printed (a_root_along_a_nose_cone_is_openrockets). hpr’s 64 pieces give 0.029% less area, nearer
the curve. Closed straight along its chord instead, the cockpit would have 12.8% more area than
the file’s. All five configurations of the example fly in hpr sim as saved. Its centering rings wrap its
18 mm motor tube and fit the airframe around it (a ring around its tube):
| configuration | OpenRocket’s apogee (m) | hpr’s (m) | difference |
|---|---|---|---|
| [A8-3] | 32.3 | 32.3 | −0.13% |
| [B6-4] | 90.5 | 90.7 | +0.18% |
| [C6-5] | 199.7 | 200.7 | +0.48% |
| [C12-6] | 207.5 | 207.7 | +0.10% |
| [D16-6] | 245.7 | 246.0 | +0.10% |
These come from the committed comparison (hpr’s flights against OpenRocket’s). The cockpit’s drag counts: without it the [C6-5] would reach 204.4 m.
What disagrees.
- Stability margin. As the rocket leaves the rod, hpr’s margin is 0.071 to 0.076 calibres above OpenRocket’s on all five configurations. That is the flattering side, and it does not come from the cockpit: at the margin’s wind direction the single fin lies in the airflow’s plane, and neither code counts it. Probes put it on two older departures:
- Cockpit normal force. Across the airflow, the cockpit’s own normal force is 31.5% above OpenRocket’s (0.2784 against 0.2117 per radian at Mach 0.3), with its center of pressure 0.13 mm from OpenRocket’s. The curved root isn’t the cause (#326). It acts forward of the center of mass, so it shortens hpr’s margin, the safe side.
A fin set there is left out, with its reason, when:
- it is trapezoidal or elliptical (hpr reads the root along the surface only from a freeform outline);
- it carries a tab or a fillet, whose models take a body tube;
- it is placed after a sibling or at an absolute station, where the reader can’t find the surface;
- the body’s radius is automatic, which the layout settles only after the reader draws the root;
- its root runs past the body’s ends;
- its outline ends off the surface;
- the body narrows along its root (a boattail).
A design built in code that puts such a fin on a nose cone gets the same checks from the layout,
as a DesignError::Geometry, where the reader leaves the fin out with a warning. On a body tube an
outline that ends off the root is still left out. Writing the design back, hpr writes the outline
alone, as OpenRocket does, so a root drawn through other points than the reader’s comes back
through the reader’s.
One more thing is read as the simpler part hpr models, with a warning so that what is missing from a mass is visible: a row of more than one ring read as one. It is a measured departure, not a silent compatibility claim (ADR-064). A rail button’s screw head (2) was another until M4.5i weighed it (Mass properties; ADR-168). A cluster of motor tubes was a third (4) until M1.9b read every tube (above). A fin’s fillets (5) were another until M2.2e7 weighed them as OpenRocket does (Mass properties).
A tube of no wall thickness carries no mass, among them couplers in two of OpenRocket’s own
example designs. Reading those as solid would invent the mass: a solid coupler filling a 50 mm
airframe for 180 mm is a few hundred grams the design never had. Since
M2.2b1 it is the rule for every part, and it is not warned
of: OpenRocket 24.12 gives an inner tube, coupler, lug, nose cone, transition, body tube or shoulder
of no wall no mass either, measured on probe designs (ADR-061), and a solid body
component is written <thickness>filled</thickness>. An inner tube, coupler or lug that writes no
thickness at all is still read as no wall, with a warning; OpenRocket gives it a wall of its own,
and no file in the library has one (Mass properties).
Checked against the answers OpenRocket cached
auto 0.0125 is not just a flag: the number is what OpenRocket itself last worked out (in
OpenRocket 24.12, at least; older releases may differ, as the note below explains). That makes
an oracle for the resolution rules that needs no OpenRocket, and cargo xtask ork runs it over the
corpus: on every automatic dimension that caches a number and sits on a component the file gives
an <id>. On 2026-09-27, with pods read, 71 of 75 agree to a part in 10⁹; the 4 added since
2026-09-23 are the pods’ nose cones, and all 4 agree. Of the 4 that disagree, the 2 body radii are settled in hpr’s favour
below; the 2 packed radii follow the bore of that tube, so they are settled
with it by the bore rule, not measured (the oracle reads body radii only).
What that number is, measured. OpenRocket 24.12 writes the radius it resolved, not the number
it read: a tube that says auto 0.04 and has nothing to take is saved again as auto 0.025. It
also ignores the number when it opens a file (below).
One older statement disagrees. The 2021 change that started writing the number,
OpenRocket PR #998, calls it the “manual
value”, the last one typed in. So a file saved by some release between then and 24.12 may cache a
hand-typed number rather than an answer. Which releases wrote which is not settled. That is one
more reason hpr never reads the number as a radius.
It reads the number in one place only: as the wall of a filled tube whose radius is automatic but
reachable, which has no other number to be solid to, and says so in a warning. No file in the
corpus has one. (A tube whose radius takes OpenRocket’s default is solid to that default instead,
below.)
What the oracle does not reach. A cached answer only exists where OpenRocket wrote one, and it
never writes one for two of the tags that matter most here: across the whole corpus, outerradius
caches a number 0 times out of 131 and innerradius 0 out of 80. So the 71 comparisons are
all aftradius, foreradius, radius and packedradius, and the two rules this milestone adds,
an inner tube’s automatic outer radius and a ring’s automatic bore, have no oracle coverage
at all. They rest on their unit tests and on the argument for them, until an OpenRocket oracle
reads those parts too: the one below reads body radii only, and
M2.2 is where the rest belongs. cargo xtask ork prints the
per-tag denominators and names the tags nothing reaches, so the gap is in the report rather than
only here.
Since M2.2e7, fourteen probe designs run in OpenRocket do
reach an inner tube’s automatic outer radius, a ring’s and a bulkhead’s, inside a nose cone, a
transition and a body tube (above); the library’s own
files still cache no such number.
The four cached numbers that disagree (not the four inside a pod) are one body tube and the parachute packed inside it (whose radius follows the tube’s bore), in OpenRocket’s own “Dual parachute deployment” example, which the corpus holds twice: once inside the jar and once cached beside it. Its spine is a nose cone and four body tubes; the third tube states a radius of 0.028321 m, every automatic radius caches 0.028321 m too, and hpr resolves them all to it, except that the first tube caches 0.025 m.
OpenRocket itself settles it, and agrees with hpr. Run on that file (below), OpenRocket 24.12 first reads the first tube as 0.025 m, its default. Once it works the design out again, as it does when saving, it reads 0.028321 m, and writes that. The first reading depends on an unrelated part, the coupler inside the second tube, whose own radius is automatic: the tenth and eleventh rows of the table below are the same small design without and with such a coupler, and only the one with it is first read at the default. So the 0.025 m in the file is most likely a first reading that an earlier save wrote out: the probe reads before and after a save, not twice without one, and nothing yet checks which radius OpenRocket’s own simulation uses after a plain open (M2.2 will). hpr is held to the answer OpenRocket settles on.
Held against OpenRocket itself. cargo xtask ork also compares every body radius hpr resolves
with the one OpenRocket 24.12 settles on for the same file, read from the oracle’s committed
results in validation/fixtures/ork/openrocket-automatic-radius.json, and prints the result on its
line “body radii against OpenRocket 24.12 run on the same file”. It covers 18 of the 19 files
OpenRocket was run on: the 17 examples in its jar, and the parachute catalog below (it refuses
the 19th, below). 67 of 67
agree: body radii this time, a different 67 from the cached numbers above. Unlike the cached
answers, this reaches every body radius, fixed or automatic, including the Dual parachute tube.
When an automatic radius has nothing to take
Sometimes a chain of automatic radii has no fixed radius anywhere along it, so the
neighbour rule has nothing to work from. For
example, a nose cone’s base follows the tube behind it, and that tube follows the nose cone. Two
designs in the reference library do this. hpr gives each such radius OpenRocket’s own default,
25 mm, as a fixed radius, and raises a warning at its tag naming the radius. A document whose
<rocket> holds nothing at all is not a design, and is reported that way.
If you see that warning, the design has a radius its author never set. OpenRocket 24.12 shows a tube there at 25 mm too, and a nose cone’s base or a transition’s end that looks at another automatic radius at −1 m, which no shape can have. Neither is likely to be the rocket that was built. Set the radius in the design (in OpenRocket, untick Automatic and type the diameter) and open it again.
Why 25 mm. OpenRocket’s user guide doesn’t say what happens here. Its issue tracker does:
- A maintainer: “OR returns the default radius” (#1988).
- An open issue: a tube left with nothing to take “reverts to default diameter” (#1992).
- A user reports a nose cone that “may be retaining the default 1.969 in. base diameter” (#871). That is 50.0 mm across, or 25 mm of radius, but it is a user’s guess, so the number rests on the measurement below.
Since 2021, OpenRocket’s dialogs grey out the checkbox where a radius would have nothing to take (PR #998), though #1988 shows a later version still leaving one ticked. So chains like these come mostly from older or hand-written files.
Measured. validation/oracles/openrocket/automatic_radius.py runs the OpenRocket 24.12 program
on fifteen small designs of its own and records the radius it gives each body component. It
records it twice: when the file is first opened, and once OpenRocket has worked the design out
again, as saving makes it do. The results are committed in
validation/fixtures/ork/openrocket-automatic-radius.json, and the test
hpr_io::ork::tests::a_radius_with_nothing_to_take_is_openrockets_default holds hpr to them.
In the table, radii are listed forward to aft. “30 to 20” is a transition’s forward and aft radius, and “|” is a stage boundary. OpenRocket’s column is the settled answer.
| design | what the file says | OpenRocket 24.12 | hpr |
|---|---|---|---|
| a tube | auto | 25 mm | 25 mm |
| a tube | auto 0.04 | 25 mm | 25 mm |
| two tubes | auto, auto 0.04 | 25, 25 mm | 25, 25 mm |
| a nose cone | auto | 25 mm | 25 mm |
| a nose cone | auto 0.03 | 25 mm | 25 mm |
| a transition | auto 0.03 to auto 0.02 | 25 to 25 mm | 25 to 25 mm |
| a nose cone, a tube | auto 0.03, auto 0.03 | −1 m, 25 mm | 25, 25 mm |
| a nose cone, a tube, a transition, a tube | auto 0.033, auto, auto to 22 mm, 22 mm | −1 m, 25 mm, −1 m to 22 mm, 22 mm | 25, 25, 25 to 22, 22 mm |
| a nose cone, a tube (the control) | auto, 30 mm | 30, 30 mm | 30, 30 mm |
| a nose cone, two tubes, a tube | auto, auto, auto, 30 mm | 30, 30, 30, 30 mm | 30, 30, 30, 30 mm |
| the same, a coupler of automatic radius in the third component | as above | 30, 30, 30, 30 mm (first read: 30, 25, 30, 30) | 30, 30, 30, 30 mm |
| a tube, two tubes | 30 mm, auto, auto | 30, 30, 30 mm | 30, 30, 30 mm |
| a tube, a transition, a tube | 30 mm, 30 mm to auto, auto | 30, 30 to −1 m, 25 mm | 30, 30 to 25, 25 mm |
| a nose cone, a transition, a tube | auto, auto to 20 mm, 20 mm | −1 m, −1 m to 20, 20 mm | 25, 25 to 20, 20 mm |
| a nose cone, a tube | a tube | auto, auto | 30 mm | 30, 30 | 30 mm | 30, 30 | 30 mm |
What the table shows:
- The number cached after
autois ignored. OpenRocket gives the tube that caches 0.04 m 25 mm, and hpr does the same. The cache is an answer OpenRocket once wrote, never an input. - A chain that reaches a fixed radius takes it, in every case probed (up to two automatic radii in between) and across a stage boundary too, in both programs. The default is only for a chain with nothing fixed on it.
- hpr departs from OpenRocket in one way, on purpose. Where a nose cone’s base or a transition’s end looks at another automatic radius, OpenRocket gives it −1 m. No shape can have a negative radius, so hpr gives it 25 mm too, and the chain is one radius end to end. That is 6 of the 39 radii in the table.
- OpenRocket’s first reading can differ from its answer. With a coupler of automatic radius in the third component, OpenRocket first reads the tube ahead of it at its default, then corrects it to 30 mm when it works the design out again. That is the Dual parachute example’s cached 25 mm (above).
Worked example. The eighth row is the chain in Loft’s quirks fixture, on a small copy
the oracle script automatic_radius.py writes itself: a nose cone whose base is automatic (it
caches 0.033 m), an automatic tube, and a transition whose forward end is automatic and whose aft
end is fixed at 22 mm, then a 22 mm tube. Each automatic radius follows another automatic radius. A
transition’s two ends never follow each other, so the fixed 22 mm stops at the transition and none
of the three ever reaches it. (The same is why, in the thirteenth row, a transition’s fixed forward
end doesn’t reach its automatic aft end.)
hpr gives all three 25 mm. The nose cone ends at 25 mm, the tube is 25 mm, and the transition narrows from 25 mm to 22 mm. The cached 0.033 m is not used, and three warnings name the tags.
A tube’s wall is judged against the default. A tube whose automatic radius takes the default
has its wall read again now there is a radius to read it against, by the rule a stated radius
gets: filled, or a wall at least as thick as 25 mm, is solid to 25 mm. The test
a_tube_given_the_default_has_its_wall_judged_against_it holds that.
The three files this settles (cargo xtask ork prints the counts):
| file | what it holds | what happens |
|---|---|---|
Debrief’s sample-design.ork | a <rocket> with a name, a comment and nothing else, plus a stored simulation | holds no design; counted apart, not as a failure. Its stored simulation is read (stored simulations) |
the openrocket-database parachute catalog | four tubes, every radius a bare auto, carrying the catalog’s parachutes | lays out, four tubes at 25 mm, just as OpenRocket 24.12 opens it |
Loft’s demo-quirks.ork | the worked example’s chain, and a parallel stage placed directly under the rocket | lays out as in the worked example. OpenRocket 24.12 will not open this file: it refuses a parallel stage there, so its answers for this chain come from the oracle script’s copy. hpr opens it and skips the parallel stage with a warning: hpr reads a parallel stage only inside a body tube (Parallel stages) |
How it was decided, and the sources quoted in full, are in ADR-054.
Measured on the reference library
cargo xtask ork, over the 73 readable files, on 2026-09-23, with the rows for parts left out
and warnings raised from 2026-09-28:
designs whose Rocket lays out | 72 of the 72 files that hold a design |
| documents that hold no design | 1, among the 73 readable files |
| automatic radii given OpenRocket’s default, 25 mm | 7, in 2 designs: 5 on body tubes, 1 on a nose cone, 1 on a transition |
| body radii against OpenRocket 24.12 run on the same file | 67 of 67 agree, over 18 of the 19 files it was run on |
| body components | 270 |
| parts on and inside them | 752 |
| by kind | 194 centering rings, 156 inner tubes, 132 parachutes, 104 fin sets, 81 mass components, 40 shock cords, 27 launch lugs, 16 rail buttons, 2 streamers |
| automatic dimensions marked for the layout to resolve | 320, plus the 7 above given the default: 327 in the files |
| parts left out, with a reason | 2; 4 before M2.2e8 read tube fins sized from the body, 5 before M2.2e7 read a coupler inside a nose cone |
| parts that lay out weighing nothing | 14, every one explained (below) |
| warnings raised | 16: 2 dropped, 5 skipped, 9 unusual (below); 18 before tube fins sized from the body were read (M2.2e8), 21 before fin fillets and the coupler inside a nose cone were read (M2.2e7), 31 before the old override flag was read as OpenRocket reads it (M2.2e6), 35, with 12 skipped, before pods were read (Pods) |
| tags no milestone reads yet | none since parallel stages are read (Parallel stages); 3 parallelstage before, and 9 podset too before pods were read (Pods) |
The 14 parts that weigh nothing are worth checking, because a structural part with no mass is
silent by nature: the design lays out, the report is written, and the mass is simply missing. All
14 are accounted for: 7 inner tubes (couplers among them) and 4 launch lugs whose wall the file
states as zero, which OpenRocket gives no mass too (ADR-061); 1 mass object the file
says weighs 0 kg; and 2 transitions the designs override to zero mass:
which is OpenRocket’s “base drag hack”, a massless,
dragless transition added only to change the base geometry. cargo xtask ork counts them by kind,
so a new one would show up. Before
M2.2b1 there were 21: the 7 more (2 body tubes, 2 fin sets,
2 inner tubes and a nose cone) name no material, and now take OpenRocket’s default.
What the 10 warnings are. Every one is a reading this page
explains, and none of them means a file is broken. Before
M4.5m read parallel stages there were 12. Before
M4.5i weighed a rail button’s screw head there were 14.
Before M4.5g4 read fins on a nose cone there were 16. Before
M2.2e8 read tube fins sized from the body there were 18.
Before M2.2e7 weighed fin fillets and read an
automatic radius inside a nose cone there were 21. Before M2.2e6 read the old override flag as
OpenRocket does there were 31, before M1.13b read pods 35, before
M1.9b read clusters 39, and
before M2.2b3 57: 5 more for a
packedradius the file does not give, read as zero, which hpr now reads as OpenRocket’s 12.5 mm
(packed parts).
| kind | count | what raised it |
|---|---|---|
Unusual | 1 | a part with no axial offset |
Unusual | 7 | automatic radii with nothing along their chains to take, given OpenRocket’s default (above) |
Unusual | 1 | a <rocket> holding nothing, so the document holds no design |
Skipped | 1 | a tally of the tags hpr does not read where they are written, kept in x-openrocket, one per design that has any: the parallel stage directly under the rocket in demo-quirks.ork (Parallel stages); 3 before parallel stages were read, 7 before pods were read |
Every design that holds a design lays out. The one readable document that doesn’t is Debrief’s demonstration file, which holds no design at all: it is a stored simulation with a rocket’s name on it. The two designs that needed a radius the file doesn’t give are above.
Motors and their configurations
In short. hpr reads every motor a design names, in every configuration, with when it lights and
its ejection delay. It finds the thrust curve in the file itself, in curves a caller supplies, or in
hpr’s bundled catalog. A configuration becomes one the rocket can fly only when every motor in it
has a curve and lights at a moment hpr can fly. Most designs in the reference library name motors the bundled
catalog doesn’t hold, so 4 of their 170 configurations fly with hpr alone, and 145 with
OpenRocket’s motor database supplied (cargo xtask ork counts it). The rest are read, kept, and say why not.
A configuration is one set of motors to fly the design with: OpenRocket calls it a flight configuration, and a design can have several, one per motor choice. The file keeps it in two places:
<rocket>declares each one: aconfigid, a name, whether it is the one OpenRocket opens with (default="true"), and which stages fly.- Each motor mount (a body tube or inner tube holding a motor) holds a
<motor configid="…">per configuration: the manufacturer, the designation such asH148R, adigest(OpenRocket’s fingerprint of the thrust curve’s data), the case diameter and length, and the ejection delay. The mount also says when its motor lights, and may say it differently for each configuration.
So hpr collects each mount’s motor into its configuration. A mount that names a configuration the rocket never declares still gets one, with a warning (L65).
Where the thrust curve comes from
A <motor> names a motor; it does not describe one. hpr looks for its thrust curve in three places,
in this order:
- Inside the file. From schema 1.11 (the
versionon the file’s<openrocket>element), OpenRocket can save each motor’s curve in the archive asthrustcurves/<digest>.rse, named by thedigestthe<motor>gives (L57). That is exactly the curve the design was saved with. OpenRocket 24.12 still writes 1.10, so few files carry one yet: one file in the reference library does. - Curves a caller supplies, each for one digest, through
design_with. A supplied curve is never matched by name, only by the digest. hpr ships none: the validation survey supplies OpenRocket’s own motor database (motors in the reference library). - hpr’s bundled catalog: 32 motors from ThrustCurve.org. The
manufacturer and the designation must both match, ignoring case, spaces and hyphens. Both are
needed: an Estes
B4is not a QuestB4.
OpenRocket’s own order is the other way round: its motor database first, then the curve in the file, because the database “may have more accurate or updated data” (its file specification). hpr’s catalog is far smaller than that database, and the curve in the file is the exact one, so hpr reads the file first. So on a file that carries curves, hpr and OpenRocket can fly different curves for the same motor.
hpr builds the motor from the curve file’s header (its case size and its loaded and propellant masses) as it does for any catalog motor, and does not use the mass or center-of-gravity column listed beside each thrust point. Where the design’s case size and the curve’s differ by more than a millimeter, a warning says so: the design’s size places the motor, and the curve’s gives its mass. A motor found in none of the three places is kept with its reason. Nothing is invented for it. A hybrid motor (solid fuel burned with a liquid or gas oxidiser) never gets a curve: hpr flies commercial solid motors only.
To fly a motor none of the three places has, build the configuration yourself: read its .eng or .rse file
with hpr_motor (Solid motors) and put it in an
hpr_design::Configuration.
Delays and ignition
| the file says | it means | source |
|---|---|---|
<delay>6.0</delay> | the ejection charge fires 6 s after burnout | OpenRocket’s technical documentation, p. 8 |
<delay>0.0</delay> | the charge fires at burnout | the same, p. 10: “zero-delay motors” |
<delay>none</delay> | plugged: no ejection charge | the same, p. 8 (“P … stands for plugged”); OpenRocket issue #2002 |
<ignitionevent>automatic</ignitionevent> | the bottom stage lights at launch; a stage above lights at the ejection charge of the stage below | OpenRocket’s FAQ, “How do I create a staged rocket?” |
launch, burnout, ejectioncharge, never | at launch; at the first burnout or ejection charge of the stage below; never | OpenRocket 24.12’s labels, with every word measured by a committed probe (below). A word hpr does not know is kept as written |
<ignitiondelay>1.5</ignitiondelay> | 1.5 s after that event | the technical documentation, section 4.2.6 |
A 0 means something different here than in a motor file. An .eng file has no word for plugged,
so hpr reads its 0 as “zero or plugged” (ejection delay). A
.ork has none for plugged, and OpenRocket flies a 0 as a charge at burnout: in its own
“Parallel booster staging” example, the E12-0’s burnout and ejection charge are stored at the same
2.44 s. But until OpenRocket 23.09 put plugged in its delay list
(issue #2090), few authors knew to type
none, and some used 0 to mean plugged, as OpenRocket’s own examples did
(issue #2111). So a 0 in an older design
may be meant as plugged; hpr reads it as the file says, as OpenRocket does. 23 motors in the
reference library have one.
A configuration’s own <ignitionconfiguration> replaces the mount’s event and delay one at a time:
whichever it leaves out, the mount’s own value stands.
hpr flies each motor’s ignition as the file says it. Here d is the ignition delay, and a missing
delay is 0. “The stage below” is the next stage aft on the axis, since a parallel stage hangs
beside its stage, not below it. Its burnout is its first motor’s: the first of its motors to burn
out, by each one’s ignition time plus its curve’s burn time, whichever mount holds it, and the
first in the file on a tie. The rules are also in the
hpr_io::ork::staging module’s documentation.
| the file says | hpr lights the motor |
|---|---|
launch plus d | d seconds after launch |
automatic plus d, in the bottom stage | d seconds after launch (an air start when d is more than 0) |
automatic plus d, in a stage above | as ejectioncharge |
ejectioncharge plus d | at the burnout of the stage below’s motor, plus that motor’s ejection delay, plus d; refused when the stage below has motors in more than one mount |
burnout plus d | d seconds after the stage below’s first burnout: the first of its motors to burn out, by each one’s ignition and curve |
never | never: the motor rides loaded, with no thrust |
burnout or ejectioncharge, in the bottom stage | never, since it has no stage below |
ejectioncharge or automatic, when the stage below’s motor is plugged | never, since a plugged motor fires no charge |
automatic plus d, in a parallel stage hung on the last stage on the axis | d seconds after launch, as OpenRocket lights the E12s of its Parallel booster staging with the I115W at 0 s |
launch plus d or never, in a parallel stage | as written |
| any other event in a parallel stage | refused by name: hpr has no reading for it |
A motor that never lights is flown as OpenRocket flies it: carried with all its propellant, giving
no thrust. So is a motor that waits on one that never lights: that follows from the probes, since
a motor that never lights has no burnout or charge, but no probe chains two. A stage that
separates at the burnout or ejection charge of its own motor, when that motor never lights, never
separates. A parallel stage with no motor that lights stays on too: its burnout or ejection
separation never comes, as in configuration 2 of Parallel booster staging, where OpenRocket
records no STAGE_SEPARATION event and the boosters land with the core
(ADR-171). upperignition as a parallel stage’s separation is refused by name.
These rules come from a committed probe, validation/oracles/openrocket/unlit_motors.py
(M2.2e10). It flies OpenRocket’s Two stage high power
rocket example, an I59WN over an I357T, five ways. In three, the booster never lights (set
never, or at burnout or ejectioncharge in the bottom stage) and the sustainer lights at
launch. In two, the booster is plugged and lights at launch, and the sustainer waits on its charge
(ejectioncharge, or automatic). Each time OpenRocket lights one motor and separates nothing;
the booster’s separation is set at its own motor’s burnout and ejection in two of them, which
measures that rule, and never in the other three.
By apogee the rocket has lost exactly that motor’s propellant: the sustainer’s 0.272 kg, or the
booster’s 0.1792 kg. The unlit one keeps all of its own. Two controls fly the example as written,
and with the booster not plugged, where its charge lights the sustainer 14 s after its burnout.
One exception is recorded apart: with an H148R-0 in both stages, OpenRocket’s mass column loses
nothing while a motor burns (#185). The test
openrocket_flies_a_motor_whose_ignition_never_comes_unlit in crates/hpr-io/src/ork/tests.rs
holds the record.
A configuration is still not flown when a motor names a word hpr does not know, or waits on a stage
below with no motor (not yet probed), or on the charge of a motor that states no delay. It is not
flown either when a motor waits on the charge of a stage below with motors in more than one mount
(ejectioncharge, or automatic in a stage above), since which fires its charge first is not
measured, or when no motor of it lights at all, since the rocket would not leave the pad. Its
reason is a motor hpr can’t light as written.
A stage’s burnout is its first motor’s. A second committed probe,
validation/oracles/openrocket/first_burnout.py, shows it
(M4.5n, a stage’s first burnout; ADR-172). It
flies the first configuration of OpenRocket’s Pods–powered with recovery deployment, whose
booster holds a motor in its main tube and one in each of two pods, with nothing deployed. As
saved, the main tube’s B6 burns out first, at 0.86 s, so it can’t tell the first burnout from the
main tube’s. With a C6 in the main tube instead, burning out at 1.86 s, OpenRocket separates the
booster and lights the sustainer at the pods’ burnout, 1.01 s. With C6s in the pods instead, it
does both at the B6’s 0.86 s. So the stage burns out with its first motor, whichever mount holds
it, not with the main tube’s or the last. The record is
validation/fixtures/ork/openrocket-first-burnout.json.
Which configurations the rocket flies
hpr reads each motor’s ignition (above) and each stage’s separation
(below) into the flight, since the
M1.9c milestone (decision record
ADR-076, a .ork file’s ignitions and one powered separation). A configuration becomes
one of the rocket’s only when all of these hold. Otherwise flying it would be wrong, for example
lighting a sustainer on the pad.
- Every motor has a thrust curve, and a case diameter and length.
- Every motor lights at a moment hpr can fly: at launch, at a time after launch, or after the stage below burns out or fires its charge. Or it never lights, as above.
- No motor sits in a part hpr doesn’t read yet, such as a parallel stage on a rocket of several stages or a pod set hpr could not lay out (M3.1c4). A motor in a parallel stage hpr reads flies (Parallel stages). A motor in a cluster of motor tubes flies with one motor in every tube (above).
- No stage is switched off in the configuration’s own stage list
(
<stage number="1" active="false"/>). OpenRocket leaves a switched-off stage out of the flight, and hpr flies every stage. - The rocket and its motor mounts were read without a single warning: nothing left out (a pod, a parallel stage hpr can’t read, a part hpr could not shape), nothing dropped or simplified (a ring written as a row read as one, a material that could not be read), and nothing assumed (a shape hpr does not know read as a cone). Otherwise hpr might fly a different rocket from the design, so no configuration of it is flown. One design in the library was held back only by its shoulders of no wall; since M2.2b1 reads them as OpenRocket does, it flies.
- No mount holds two motors for the configuration. Which one OpenRocket would fly is not known, so neither flies.
- Its stages come apart in a way hpr can fly. Each separation is flown, from the tail forward,
and each one’s time must be known before the flight: a time after launch, or after a motor lights, burns out or
fires its ejection charge. At that time a motor ahead of it must still be burning or yet to
light, and no motor behind it may be. That is a powered separation, which drops the stages
behind it and flies the rest on as a sustainer
(Several separations). One exception: a separation
at its own stage’s first
burnoutmay drop the stage’s own other motors still burning, as OpenRocket does. It drops only motors lit strictly before the split, and only when the split is powered: a motor ahead of it burns then, or lights then or later. The reader marks such a stagingdrops_burning, and hpr’s flight drops a burning motor only for a marked staging. A delay after that burnout counts too, though OpenRocket’s probe measured only a split with none. The dropped part flies with no thrust, andhpr simnotes the impulse it leaves out (A booster dropped still burning). A motor behind it still to light, or lit at the split itself, is refused, and so is any other separation while a motor behind it burns. Any other separation must come only at apogee or on the way down; the configuration then flies whole (below). A separation at the burnout or charge of the stage’s own motor that never lights never comes, and is left out (above). Otherwise its reason is stages hpr can’t separate as written: a separation at or after apogee beside another, one that comes before the separation behind it, a negative delay, a sustainer already burnt out at the split, a separation at launch, or one at the ignition of a motor that never lights, for example. A separation timed after launch but before the rocket leaves the rod fires only as it leaves: hpr’s flight fires none on the rod (#231).
Every other configuration is still read, whole, with the first reason it can’t be flown. The motors’ reasons are checked first, each across every motor, and the two about the whole rocket last.
This is the doctest on
hpr_io::ork::design, which CI runs. The Estes F15 has no
curve in the file, so it comes from the bundled catalog: 49.6 N·s over 3.45 s, as ThrustCurve.org
lists it. The rocket assembles with it. assemble takes the configuration’s configid; its name
is for display, and may be one of OpenRocket’s templates, such as [{motors}].
let read = hpr_io::ork::read(xml)?;
let design = hpr_io::ork::design(&read.value).value;
// The F15 has no curve in this file, so it comes from the bundled catalog...
let motor = &design.motors.configurations[0].motors[0];
assert!(matches!(motor.curve, hpr_io::ork::Curve::Catalog { .. }));
assert_eq!(motor.delay, Some(hpr_motor::Delay::Seconds(4.0)));
let impulse_ns = motor.curve.motor().expect("a curve").curve().total_impulse_ns();
assert!((impulse_ns - 49.61).abs() < 0.01, "{impulse_ns}");
// ...and it ignites at launch, so the configuration is one the rocket flies.
let assembly = design.rocket.assemble("c1")?;
assert_eq!(assembly.motors[0].mount, "body");
How many configurations hpr sim flies
In short. hpr sim flies 136 of the survey’s 170 motor configurations as saved, offline
after one fetch, and 9 more with --accept-design-errors; 25 are refused. These are counts, not
accuracy. A configuration counted as flying is one hpr sim accepts and flies to the ground. How
close each flight comes to OpenRocket’s is
hpr’s flights against OpenRocket’s.
The survey is every .ork that Checked against real files reads.
That is the design library under refs/, which is other people’s designs and not in the
repository, and the 17 examples inside OpenRocket 24.12’s jar. A motor configuration is one set
of motors to fly a design with (above). A file found in two
places is counted in each.
The three counts of the same 170 differ by where the thrust curves come from:
| Curves from | Configurations that fly | Counted by |
|---|---|---|
| The file and hpr’s bundled catalog alone | 4 | cargo xtask ork |
Those, and ThrustCurve.org’s, as hpr sim fetches them | 136, and 9 more with --accept-design-errors | cargo xtask ork-cli |
| Those, and OpenRocket’s own motor database | 145 | cargo xtask ork |
hpr doesn’t ship OpenRocket’s database, so 136 is what a user of hpr sim gets. The 145 is the
validation survey’s count (Motors in the reference library).
The 136 come from the committed report, which cargo xtask ork-cli writes
(the CLI’s corpus count, M4.5j). It runs each configuration
through the same function the hpr command runs, as hpr sim --json --offline --config ID FILE.
A first pass with the network caches every curve hpr sim fetches
(Where the thrust curve comes from), and the counts are
taken offline after it.
A configuration refused as saved is flown again with --accept-design-errors. The 9 that fly
then were refused only because the design’s own checks found an
error, such as a ring or tube wider than the tube it sits in. Their flights list those errors in their
notes (The command line).
Of OpenRocket’s 56 example configurations, 54 fly as saved, 0 more with
--accept-design-errors, and 2 are refused. Deployable payload’s five fly with warnings: its
payload and its parachute are drawn 25 mm across in a 21 mm bore, so they can’t fit as drawn, but
a packed part’s width sets only its own inertia
(a packed part wider than its bore,
M4.5l). Parallel booster staging’s two fly since
M4.5m read its boosters
(Parallel stages). Pods–powered with recovery deployment’s first flies
since M4.5n read its booster’s burnout as its first motor’s
(Delays and ignition). The 2 refused are Simulation extensions and
Simulation scripting, for a hybrid motor: hpr flies commercial solid motors only until release
1.0.
Most of the library’s refusals are motors with no curve whose file records no
digest, OpenRocket’s fingerprint of the curve. hpr sim
supplies a fetched curve only by its digest, so it fetches none for them. The report gives every
reason’s count.
Counting again needs the private library under refs/ and, once, the network. Run it after any
change to the reader or to hpr sim:
cargo xtask ork-cli --fetch # once, with the network: fills the motor cache
cargo xtask ork-cli # offline: writes validation/reports/ork-cli-flights.{json,md}
cargo xtask ork-cli --check # fails unless the committed report is still what hpr sim flies
Motors in the reference library
In short. Most designs in the reference library don’t carry their motors’ curves. They name each curve by its digest, OpenRocket’s fingerprint (a hash) of the curve’s data, and OpenRocket finds the curve in the motor database that ships inside its program. The validation survey supplies that database to hpr, so 145 of the 170 configurations fly instead of 4. Every curve involved matches OpenRocket’s total impulse, and in every configuration hpr flies with a supplied curve, OpenRocket places that curve too. What still differs is where the motor’s weight sits (below).
How it works:
- An oracle,
validation/oracles/openrocket/motor_database.py, records OpenRocket 24.12’s own database: 1,452 motors. cargo xtask orkhands each solid motor’s curve to the reader for its digest, throughdesign_withandSuppliedCurves. A curve the file embeds still comes first, and hpr’s bundled catalog last.- A supplied curve is never matched by name. In the database, 286 manufacturer-and-designation pairs have more than one curve.
- The database’s curves come from ThrustCurve.org by way of OpenRocket, and neither publishes terms for reusing them. So the record stays on the machine that ran it, and only counts are published here.
To repeat the survey you need the private design library under refs/, which most readers won’t
have, plus Java 17 and the OpenRocket jar (cargo xtask refs fetch), as for the
mass comparison. Then, from the repository root:
refs/venv/bin/python validation/oracles/openrocket/motor_database.py \
corpus-out/openrocket-motors.json refs --jar
cargo xtask ork
cargo xtask ork, over the 72 designs, with that record, on 2026-10-05, after
M4.5n read a stage’s first burnout. The counts are of motors, one per mount per configuration:
| motors | count |
|---|---|
| read into their configurations | 208: 138 single-use, 65 reloads, 3 hybrids, and 2 not written, read like the rest |
| left out, in parts not read yet | 0; 2, both in parallel stages, before they were read (Parallel stages), and 6 before pods were read (Pods) |
| thrust curve from the file itself | 4 |
| thrust curve from OpenRocket’s database, by digest | 178 |
| thrust curve from the bundled catalog | 1 |
| no curve | 25: 3 hybrids, and 22 with no curve in any of the three places |
| ejection delays | 132 in seconds, 21 at 0 s, 53 plugged (none), 2 not written |
And of configurations, over 80 motor mounts in 63 designs (none named only by a mount):
| configurations | count |
|---|---|
| declared | 170 |
| the rocket flies | 145, in 41 designs, and all 145 assemble |
| left out, by the first reason the reader finds | 24 a motor with no curve, 1 stages hpr can’t separate as written, and none now a motor hpr can’t light as written (1 before M4.5n read a stage’s first burnout); before parallel stages were read, 2 more for a motor in a part not read and 2 for an airframe not read exactly as written |
On 2026-09-28, after M2.2e8 read tube fins sized from the body, 109 flew in 30 designs, and 15 were held back as an airframe not read exactly as written and 19 as stages hpr can’t separate as written. On 2026-09-25, before pods, the old override flag, fillets, the bore inside a nose cone and tube fins were read, 93 flew in 23 designs, and 30 were held back as an airframe not read exactly as written.
What the database changed. With the bundled catalog alone, 166 configurations are held back for want of a curve, and 4 fly. The table follows those 166. A configuration that now has its curves can still be held back for another reason, so these counts differ from the table above:
| what became of the 166 | configurations |
|---|---|
| fly | 141 |
| held back for another reason: stages hpr can’t separate as written | 1 |
| still no curve: the motor records no digest, and the bundled catalog lacks it | 20 |
| still no curve: a hybrid | 3 |
| still no curve: a digest the database lacks | 1 |
How far to trust the curves.
- Total impulse is within 0.1% of OpenRocket’s on every curve, and the survey fails otherwise.
All of them agree to the last bit. The two checks differ in strength:
- The 3 curves the designs embed are real checks. hpr parses each file itself, as for the bundled curves.
- For the 1,288 solid database curves, both codes integrate the same samples, which OpenRocket has already parsed. That check proves the hand-off, not two independent readings.
- The curve OpenRocket flies. The oracle opens each design in OpenRocket with the database loaded, and records the digests of the motors it places in each configuration. The survey fails unless, in every configuration hpr flies with a supplied curve, OpenRocket places each supplied curve too. It does in all 142 such configurations (177 motors), and OpenRocket opens every design they are in, as the survey prints.
- What the survey doesn’t supply:
- the 164 hybrid motors;
- 6 digests that are each shared by two motors whose data differ (samples, case or masses), in OpenRocket 24.12’s database;
- one motor whose propellant mass gives an effective exhaust velocity of 10.1 km/s, which hpr refuses as impossible.
- Other masses are taken as OpenRocket holds them, including some that are physically unlikely.
- Where the weight sits differs. hpr builds every motor from its envelope. The center of mass sits at mid-case, and the dry case is a thin tube. OpenRocket gives each database motor a fixed center of mass of its own, and treats the motor as a solid cylinder for inertia. The 1,288 solid motors are 1,221 digests once the 54 repeats, the 12 motors sharing 6 digests and the refused one are set aside. For 163 of those 1,221, OpenRocket’s center of mass is more than 1 mm from mid-case. Among the 55 distinct supplied motors the designs fly, 6 are, by up to 6.5 mm. This matters to stability margin and roll, and M2.2d will meet it when it flies these designs against OpenRocket.
How this was decided, and the counts in full, is in ADR-067. The reading order for curves is in ADR-055.
When parachutes open and stages separate
In short. hpr reads when each parachute and streamer opens, with the drag coefficient it states,
and when each stage separates, in every configuration. Since the
M4.5a milestone (a .ork’s recovery flown), parachutes and
streamers fly as hpr’s own recovery devices, the way OpenRocket flies
them (flown as OpenRocket flies them). Since the M1.9c milestone
(.ork staging flown against OpenRocket), a configuration’s powered separation is flown: one
whose time is known before the flight, when a motor ahead of it is still burning or yet to light
and none behind it is. It drops the booster and flies the sustainer on. Since
M4.5n (a stage’s first burnout), one at its own stage’s
first burnout may also drop the stage’s other motors still burning, as OpenRocket does. Since the
M4.5g2 milestone (several separations), a configuration’s
several powered separations are flown in turn, from the tail forward. A separation at apogee or
on the way down is part of the descent: it is left out, and the configuration flies whole, even
when a sustainer sits ahead of it. That assumes every motor is spent by apogee, which the report’s
tool checks of every flight. Any other separation, such as one after a sustainer has already burnt
out, is not flown (which configurations the rocket flies). Every device and stage in the reference library is read. The separations of its 2 parallel
stages are read since M4.5m
(Parallel stages), and the 2 parachutes inside pods since Pods are.
A deployment is an event, a height for the event that needs one, and a delay after it. A parachute or streamer states its own, and a configuration may change any of the three:
<deployevent>ejection</deployevent>,<deployaltitude>200.0</deployaltitude>(meters) and<deploydelay>0.0</deploydelay>(seconds) are the device’s own.<deploymentconfiguration configid="…">changes them for one configuration. Whichever of the three it leaves out, hpr keeps the device’s own, as it does for a motor’s ignition. That is hpr’s reading: OpenRocket was not probed on a file that leaves one out.
A stage’s separation is written the same way, with <separationevent>,
<separationaltitude>, <separationdelay> and <separationconfiguration configid="…">. The stage
that states it is the one that drops away: OpenRocket’s labels (below) speak of the “current
stage” and the “upper stage”.
What the words mean
OpenRocket’s documentation lists none of these words. The committed probe
validation/oracles/openrocket/events.py runs OpenRocket 24.12, sets every value of each event
through the program’s public setters, saves the design, and records the word written and the label
OpenRocket shows. Its results are in validation/fixtures/ork/openrocket-events.json, and the test
hpr_io::ork::tests::every_event_word_openrocket_writes_is_read holds hpr’s reader to every word.
| deploy word | OpenRocket’s label |
|---|---|
launch | “Launch (plus NN seconds)” |
ejection | “First ejection charge of this stage” |
apogee | “Apogee” |
altitude | “Specific altitude during descent” |
lowerstageseparation | “Lower stage separation” |
never | “Never” |
| separation word | OpenRocket’s label |
|---|---|
launch | “Launch” |
ignition, burnout, ejection | “Current stage motor ignition”, “… burnout”, “Current stage ejection charge” |
upperignition | “Upper stage motor ignition” |
altitudeascending, apogee, altitudedescending | “Specific altitude during ascent”, “Apogee”, “Specific altitude during descent” |
never | “Never” |
The same probe measures three things the words do not say:
- A deploy height is above the ground, meaning the launch site: OpenRocket has no terrain. The
probe flies in calm air with a fixed seed, so it writes the same numbers every run. On a pad
1,000 m above sea level, a parachute set to
altitude30 m opened at 29.9 m above the ground, 1,029.9 m above the sea. A height read above the sea would never have been reached. - Set above apogee, it did not open. Set to 100 m, on a flight whose apogee was 51.7 m, the
parachute never opened, and the flight reached the ground. That is one run of one design, not a
rule OpenRocket states. hpr’s own altitude trigger opens at apogee instead
(Recovery), so the two differ here. hpr
keeps its own rule for a
.orktoo, andhpr sim’s notes say when it applies. <cd>auto</cd>is reported as 0.8 for a parachute, on the canopy’s area: OpenRocket’s technical documentation gives 0.8 as the default (section 4.2.5), and the probe reads it back. For a streamer it is worked out from the strip’s length and material, on the strip’s area (appendix C, equations C.4 and C.5): 0.089, 0.060 and 0.050 for strips 0.5, 1.0 and 1.5 m long, in a material of 67 g/m², which the three values imply by equation C.5. The documentation puts that estimate’s accuracy at about 20%, and one user’s report (issue #2031) finds a 2.5 by 44 in streamer’s 0.06 far too small. hpr keeps theautoor the stated number as the file wrote it.
Flown as OpenRocket flies them
A configuration’s parachutes and streamers fly as OpenRocket flies them, and land at
OpenRocket’s speed to within 0.02% on 50 of its 53 example flights, and within 0.12% on all 53.
hpr::ork::recovery turns each one into a flight device, and
hpr sim flies them through it, so its numbers are the library’s. This is
checked against OpenRocket alone, not against a real flight. Recovery
has the model and the full results; the decision record on flying a .ork’s recovery,
ADR-153, has the reasoning.
- How. The drag is the file’s
C_D, or OpenRocket’s own forauto: 0.8 on a parachute’s canopy, and appendix C’s estimate for a streamer. Each device opens fully at once, the file’s delay after its event, and the descent is a point mass under the open devices’ drag alone, as in OpenRocket.apogee,altitude,ejectionandlaunchfly;never, andejectionon a plugged motor, open nothing. - How close. On 48 of OpenRocket’s example flights (the report’s 51 with no named cause for an apogee gap, less the Base drag hack’s three, whose flight time runs up to +7.23%), the landing speed is +0.00% to +0.02% off OpenRocket’s and the flight time −1.57% to +3.16%. 40 meet every target set before measuring; the other 8 miss only on a deployment’s time, which the climb’s own apogee gap and OpenRocket’s time step explain (the report’s descents).
- The limits. A device inside a part hpr doesn’t read and an unknown event are refused, and
the flight falls on its airframe alone with the reason in its notes. A device set to open at
its stage’s ejection charge, in a stage where no motor lights, never opens, as in OpenRocket
(M4.5i; ADR-168). One set to open at
lowerstageseparationopens at a split with nothing left to burn, and is refused otherwise. A device set above the apogee opens at apogee, where OpenRocket’s one probed run never opened it. hpr counts a height from the ground, OpenRocket from where the center of mass starts on the pad. - A stage that separates under power.
hpr::ork::separated_recoveryputs each device on the part its stage is in. The booster tumbles from the split until its first own device opens, and a sustainer with no device tumbles from its apogee. The sustainer’s descent is held to the same targets: on the Two stage high power rocket’s two configurations, its landing speed is +0.00% off OpenRocket’s and its flight time +0.32% and +0.73%. The first meets them once sized, as its main opens 1.09 s early on an apogee 1.79% low. OpenRocket’s record keeps only the sustainer’s branch, so the booster’s descent is not compared (ADR-159, powered separation inhpr sim).
Recovery in the reference library
cargo xtask ork, over the 72 designs, on 2026-10-05, after
M4.5m read parallel stages:
| quantity | count |
|---|---|
| parachutes and streamers read | 136: 134 parachutes, 2 streamers; 134 before parallel stages were read |
| left out, inside pod sets hpr did not read then | 2; since pods are read (Pods), none |
| drag coefficient | 79 auto, 57 stated |
| deploy event | 79 ejection, 32 apogee, 18 altitude, 6 lowerstageseparation, 1 never |
| devices a configuration changes | 11, with 25 changes in all |
| stages that state a separation | 20 of 92: 14 ejection, 4 upperignition, 2 burnout; 15 changes per configuration |
| separations left out, in parallel stages hpr does not read | 0; 2 before parallel stages were read (Parallel stages) |
How this was decided is in ADR-056.
What OpenRocket last did: stored simulations
In short. A .ork keeps the simulations OpenRocket last ran on the design, and hpr reads them
back: the launch conditions each was flown in, the ten summary figures, and each stage’s time
series with its events. Reading a result is not the same as validating it. M2.2b5 applies a
reference gate (a screen deciding whether stored data may be used as a reference datum): it
checks current status, two known OpenRocket provenance markers, plausible stored values, and
internally consistent series data. It does not re-run OpenRocket or compare the stored flight with
a fresh hpr or OpenRocket flight; an eligible stored run may still disagree with that later
comparison. Every parseable result stays visible, including results that fail the screen.
The reference screen and hpr’s reproduction screen answer different questions. The first asks whether stored data is suitable as a reference datum. The second asks whether hpr can reproduce the named design and motor configuration. A complete OpenRocket result can pass the first and fail the second when its motor curve is absent or its airframe is reduced. A later flight-comparison gate must report both screens rather than treating either one as a physics validation.
A stored simulation has three parts:
<conditions>: the launch rod, the wind, the launch site, the atmosphere and the time step.- The summary: ten figures on
<flightdata>, such asmaxaltitudeandoptimumdelay. - The time series: a
<databranch>per stage, whosetypesname its columns (Time,Altitudeand the rest: 58 in the files OpenRocket 24.12 writes, other versions differ) and whose<datapoint>rows give a value for each, beside the flight’s<event>s (launch,apogee,recoverydevicedeploymentand so on) and any<warning>OpenRocket stored.
What the numbers mean
The file-format page gives units only for the multilevel wind (meters, m/s and radians), and its
own example writes a launch rod’s direction as 90.0 and a wind’s as 1.5707963267948966. In this
section, SI means the International System of Units; lengths are meters, times seconds, speeds
meters per second, pressures pascals and angles radians unless the table says otherwise. A denominator
is the count of runs used to calculate a reference statistic. The
committed probe validation/oracles/openrocket/conditions.py runs OpenRocket 24.12, sets the
conditions through its public setters, saves, loads them again and flies them. Its results are in
validation/fixtures/ork/openrocket-conditions.json, and the test
hpr_io::ork::tests::wind_direction_is_not_rod_direction holds hpr’s reader to them.
| the file says | it means | hpr gives |
|---|---|---|
<launchrodangle>5.0</launchrodangle> | the rod tilts 5 degrees from vertical | rod_angle_rad, 0.0873 |
<launchroddirection>45.0</launchroddirection> | toward a compass bearing of 45 degrees, clockwise from north | rod_direction_rad, 0.785 |
<winddirection>0.5</winddirection> | the wind blows from a bearing of 0.5 radians, about 29 degrees | wind_from_rad, 0.5 |
<windturbulence>0.1</windturbulence> | turbulence intensity: the wind speed’s standard deviation over its mean | wind_turbulence, 0.1 |
<atmosphere model="extendedisa"> with <basetemperature> and <basepressure> | the standard atmosphere from a temperature (K) and pressure (Pa) at the launch site | Atmosphere::Extended |
Here ISA means International Standard Atmosphere, the reference atmosphere; K means kelvin,
Pa pascal and rad radian. The probe also flies the example, in three ways that settle what the
directions mean:
- The rod’s direction is a compass bearing. In calm air, a rod tilted 10 degrees toward bearing 0 (north) lands the rocket 21.3 m north, and toward 90 (east), 21.3 m east, while the example’s own wind setting stays at 90 degrees. OpenRocket’s preferences page still calls the direction relative to the wind; the program does not treat it so.
- The wind’s direction is where it blows from. From a vertical rod, in a steady 5 m/s wind from bearing 90 (east), the rocket lands 48.4 m west; from bearing 0, 48.4 m south.
<launchintowind>rewrites the rod. With it true, OpenRocket overwrites the rod’s direction with the wind’s bearing, in degrees: a wind from 0.5 radians is written as a rod direction of 28.648.
All of this is measured on OpenRocket 24.12. A file written by a much older version may have meant the rod’s direction otherwise, which is not measured.
The rod’s direction and the wind’s are different numbers in different units.
Loft read the wind’s direction from launchroddirection
(L64). hpr reads each from its own tag.
The probe saves a flight and compares one stored row with the same quantities as OpenRocket held
in eight columns: time, altitude, vertical velocity, two angles, latitude, air temperature and air
pressure. These are SI (the International System of Units): lengths in meters, times in seconds,
pressures in pascals and speeds in meters per second; angles are in radians and latitude in degrees.
Values are rounded when stored: to three decimal places (287.857 K, 0.218 rad), so a small quantity
keeps few digits, and a large one to four significant figures (100,796.6 Pa is stored as 100,800).
NaN means “not a number”: OpenRocket did not
compute that quantity at that step; hpr stores it as Rust’s None value, meaning no numeric
value is available. A design with stored results therefore survives being saved as JSON and read back.
This is from the test stored_results_are_read_back: a stored run reads back through design.simulations, and a column comes out by its name.
hpr_io::ork::StoredSimulation::reference_exclusion then classifies the stored run without
altering it. An explicitly uptodate run must name both RK4Simulator and BarrowmanCalculator,
the simulator and aerodynamic calculator markers shown in the public file specification’s
Simulation Data example. These strings identify recorded provenance, not the complete OpenRocket
version, settings or design. The run must have finite, non-negative values, positive altitude, speed
and time to apogee, and no contradiction between its summary and time series. A missing summary or
flightdata, a backwards time series, or a time-series altitude materially different from the stored
apogee is excluded with a stable reason. The comparison allows max(0.1% of the stored apogee, 1 mm) for stored-value rounding; it is a data-integrity policy, not an accuracy claim about the
flight model. For example, with a 100 m stored apogee, 100.05 m passes this screen and 100.2 m fails
it; the comparison test
pins the two-sided allowance and its boundary. For this screen, hpr treats the first branch with an apogee event in file order as the summary
branch; the selection test
exercises that implementation policy. The rule has not been independently validated against
multi-stage OpenRocket output. A single
altitude-bearing branch is checked even when its apogee event is missing. Multiple branches without
an apogee event are uninspectable because their summary branch cannot be identified. Times in rows
and events must be finite, non-negative and ordered. When the summary is present, no time may exceed
the stored flight time beyond the 1 ms allowance for stored precision; the first apogee event must
also agree with timetoapogee within 1 ms. Fatal events
(such as SIM_ABORT, with case, underscore and hyphen variants normalized) are excluded. There is
no arbitrary minimum apogee: a small but self-consistent flight is not rejected merely for being
small.
Design::reproduction_exclusion is a second, stricter screen. It excludes reduced designs, missing
or unknown configurations, configurations hpr cannot assemble, and configurations left out while
reading the design. These reasons describe hpr’s current ability to reproduce the design, not the
stored result’s provenance or physical correctness.
What the summary words mean
This section records what OpenRocket 24.12 means by each of the ten summary figures on
<flightdata> (the summary words, such as maxvelocity), measured on flights OpenRocket ran
itself. hpr’s own flights of the same configurations are compared with them in the next
section, hpr’s flights against OpenRocket’s.
A word names a quantity, but not how it was taken. deploymentvelocity could be the speed at the
first parachute or at the last. timetoapogee could be the apogee event’s time or the time of the
highest stored step. Another tool, or another OpenRocket version, can use the same word for a
different quantity (Loft lesson L80).
So hpr will compare a stored value only through a written definition for the version that wrote
it. Only OpenRocket 24.12’s are measured. A file’s writer is its root creator attribute
(OpenRocket 24.12). For any other version, every summary value is withheld, not compared.
How they were measured. The oracle script
flights.py
runs the OpenRocket 24.12 program file (its jar). It flies every motor configuration of the public
designs: the 17 examples inside the jar and the seven Loft demos. Each flight uses the design’s
first stored launch conditions, in calm air (no wind, no turbulence). Of the demos, only one has a
motor OpenRocket finds, and demo-quirks.ork does not open. That leaves 18 designs with 57 motor
configurations between them, so 57 runs. OpenRocket aborted one of them, Pods–powered with
recovery deployment [C6-7; 2× A3-4, B6-0] (its motors by stage, sustainer first, the stages
separated by ;: a C6-7 in the sustainer, two A3-4s and a B6-0 in the booster), at 1.81 s and
85 m up (“Stage began to tumble under thrust”). Its figures are where the run stopped, not a
flight’s, so it is not a reference, which leaves 56 complete flights. The record,
validation/fixtures/ork/openrocket-flights.json, keeps each word beside the quantities of
OpenRocket’s own time series (its stored steps) that the word could mean. The test
stored_metric_definitions_are_per_tool_and_version
holds each defined word to its quantity on all 56, to 1 part in 10⁹. Nine of the ten words are
defined; the tenth, optimumdelay, is withheld, with the flights that rule out its two obvious
readings.
| word | OpenRocket 24.12 means | shown by the record |
|---|---|---|
maxaltitude | the largest altitude above the launch site | 56 of 56 |
maxvelocity | the largest total speed (not the vertical speed) | 56 of 56 |
maxmach | the largest Mach number | 56 of 56 |
timetoapogee | the time of the highest stored step | 56 of 56; the apogee event’s time differs on 13 |
flighttime | the time of ground hit, the last step | 56 of 56 |
launchrodvelocity | the total speed at the first step past the rod’s length | 56 of 56; 0.06% to 7.50% above the speed at the rod’s end, placed by interpolating linearly in height |
deploymentvelocity | the total speed at the last deployment, interpolated between the steps either side | 56 of 56; 53 deployments fall between steps, and on 17 flights the first deployment’s speed differs |
groundhitvelocity | the total speed at ground hit, the last step | 56 of 56 |
maxacceleration | the largest total acceleration before the first deployment | 56 of 56; over the whole flight it differs on 15, whose largest acceleration comes after a deployment |
optimumdelay | not measured: withheld | on 15 flights it is not the time of apogee less the time of the last burnout, nor that of the same flight flown again with nothing deployed |
| (no word) stability margin | the distance from the center of gravity (CG) aft to the center of pressure (CP), in calibres, at rod clearance | the stability column, 56 of 56 |
The stability margin follows Niskanen’s definition (N09 p. 12): the
CP’s distance behind the CG, divided by the reference length
and so counted in calibres. OpenRocket’s reference length is by
default the largest body diameter, and that is the setting on all 57 runs. The optimum delay is the
best ejection delay. The 15 flights it misses are exactly those on
which a recovery device deploys before apogee; five others fire an ejection charge before apogee
but deploy only after it, and agree. Flown again with nothing deployed, every one of the 56 gives an
optimumdelay equal to its apogee less its last burnout. So an early deployment is what changes the
figure, but how is not known, and the figure is not used.
OpenRocket does not say which point on the rocket its speeds belong to. RocketPy’s are the center of dry mass’s, and RocketPy’s rail exit is where the rocket’s travel equals the rail, found between steps (ADR-021). So RocketPy’s and OpenRocket’s largest speed and rail-exit speed are kept as different definitions.
A worked example. The Chute release example with a G40W-7 fires its ejection charge after apogee and opens its main parachute lower down:
| deployment | time, s | speed, m/s |
|---|---|---|
| first | 9.301 | 8.305 at the 9.3 s step, 8.329 at 9.3025 s: 8.314 interpolated |
| last | 20.514 | 14.231, on a step |
OpenRocket’s deploymentvelocity is 14.231 m/s: the last deployment. A comparison using the first
deployment would have set hpr’s value against 8.31 m/s instead. The interpolation matters too. For
the 3D printable nose cone and fins example with a B6-4, the one deployment is at 4.861 s,
between steps at 4.86 s (0.6575 m/s) and 4.8625 s (0.6330 m/s). OpenRocket reports 0.6477 m/s, the
value interpolated at 4.861 s, not either step’s.
An event that never happened has no value. The record also holds a 58th flight, outside the
57: the A simple model rocket example edited so its parachute never opens. It flies to the ground, and OpenRocket writes
NaN for deploymentvelocity. On every complete flight, a speed taken at an event is NaN exactly
when the event is missing. Loft scored such values as 0
(L81). hpr’s
compare
never scores them. When neither flight had the event, the metric is withheld. When only one did,
the flights disagree about what happened, and the comparison fails. Every figure of an aborted run
is withheld. The test
metric_for_missing_event_is_withheld_not_scored
checks all three.
hpr’s flights against OpenRocket’s
This section compares hpr’s whole flights with OpenRocket 24.12’s on the 54 configurations of OpenRocket’s example designs that hpr can fly, in 15 of the examples. OpenRocket aborts its own flight of one of them, so the figures below are over the other 53, and that one is checked up to the abort (below). It is a code-to-code comparison: agreeing with OpenRocket is not the same as agreeing with real flights. Use it to judge how closely hpr’s flight matches OpenRocket’s on ordinary hobby rockets.
-
The stability margin as the rocket leaves the rod agrees within 0.016 calibres on 41 of the 53. hpr’s margin here is taken with the air along the rocket’s axis (the 0° plane), as OpenRocket’s is, not in hpr’s weakest plane: on
[C6-7; B6-0]of Pods–powered with recovery deployment it is +3.74 calibres along the axis, whilehpr simprints a least margin of −4.21 calibres in the weakest plane. The Tube fin rocket‘s is 1.08 calibres short of OpenRocket’s: the two codes’ tube fins differ (Tube fins). The three-stage example’s three are 0.039 to 0.058 calibres short, about as much as hpr’s center of mass sits further aft there. The Pods–airframes and winglets example’s five are 0.071 to 0.076 calibres long, the flattering side: hpr’s center of pressure sits 2.6 mm aft of OpenRocket’s there (#325, fin–fin interference; #326, a kinked fin’s center of pressure). The Pods–powered with recovery deployment example’s three are 0.070 calibres long, the same side: hpr’s center of pressure sits 2.4 mm aft of OpenRocket’s there, a gap not yet traced. -
Pods–airframes and winglets flies since M4.5g4, which reads its cockpit fin on the nose cone (Fins on a nose cone or a transition). Its five apogees are within 0.5% of OpenRocket’s, −0.13% to +0.48%, and its margin 0.07 calibres high.
-
Pods–powered with recovery deployment flies since M4.5i, which weighs its rail buttons’ screw heads and flies its motors in two pod sets (ADR-168). Its first configuration,
[C6-7; 2× A3-4, B6-0], flies since M4.5n (a stage’s first burnout), checked up to OpenRocket’s abort (below). Its sustainer alone,[C6-5; None], reads 0.36% low, and its booster alone with its two pod motors,[None; 2× A10-3, B6-4], 1.86% low. The two pod parachutes of that flight open at launch + 5.35 s in both programs; its landing speed is +0.00% off OpenRocket’s and its flight time −1.02%, and both descents meet their targets (ADR-153). Its staged flight,[C6-7; B6-0], is the eighth apogee more than 5% off (below). The file flies inhpr simas saved: the centering rings on the booster’s 18 mm motor tube wrap it (a ring around its tube). One fits the booster’s airframe; the other, in a coupler, reaches 0.76 mm past the coupler’s bore and warns as a fit to sand. -
The mass at launch agrees within 0.21% on all 53, and the center of mass (CG) as the rocket leaves the rod within 0.016 calibres on all but the three-stage example’s three, which are 0.043 to 0.062 calibres further aft (M2.2e1, mass and center of mass). That is where OpenRocket’s recorded mass falls as if every motor of the booster’s type burned from launch (#185); whether that accounts for the gap is not sized.
-
On the 36 flights with no named cause, hpr’s apogee is from 4.34% low to 2.06% high. Only 14 read high: the five Airstart timing flights, the ARC payload rocket, the Deployable payload on an A8-3, four of the five Pods–airframes and winglets flights, both Parallel booster staging flights (+1.23% and +0.68%, Parallel stages), and Base drag hack (short-wide), the highest.
-
On the six Dual parachute deployment flights, hpr’s apogee is 0.63% to 4.34% low. The cause is not traced.
-
A two-stage design, a three-stage design, a cluster, an air start and boosters beside the core fly, and every flight of each is within 5% of OpenRocket’s apogee and largest speed, the bar the M1.9c milestone set before measuring. Five of their apogees are compared with OpenRocket’s flight with no parachute, since its parachute opened early (staged, clustered and air-start flights, below).
-
Six probe designs, five with pods and one without, written for M1.13c2 (a pod design against OpenRocket) and not counted among the 53, fly within 0.81% of OpenRocket’s apogee and 1.24% of its largest speed, their margins within 0.0014 calibres (aerodynamics: Pods).
-
The bar, set for the whole corpus (M2.2, OpenRocket comparisons), is that every apogee more than 5% off has a written cause. Eight apogees are more than 5% off, and each has a named cause. Seven are sized in one of two ways:
how the cause is sized flights within 5% after OpenRocket flies the flight again with its causes taken out (M2.2e4) 6 4 hpr flies the flight again on OpenRocket’s own drag (ADR-097) 1 1 The two of the six still over 5% are the Base drag hack on a D12-3 and an E12-4. With nothing deployed in OpenRocket either, hpr reads 5.84% and 8.92% high. Flown on OpenRocket’s own drag along that flight, hpr comes within 0.10%, so what is left is the drag coefficient: by OpenRocket’s part-by-part breakdown, the nose (#177, a very blunt nose’s drag; see below).
The seventh is the Tube fin rocket, 6.95% high. On OpenRocket’s drag hpr comes within 0.03%, so its cause is hpr’s own drag: the net gap is the drag’s, though parts of it could cancel. Its written breakdown is in ADR-099.
The eighth is Pods–powered with recovery deployment on
[C6-7; B6-0], 37.93% low, its largest speed 15.76% low. Its sustainer is unstable when the air crosses it edge-on to its two-fin strake set, and in the conditions of OpenRocket’s record it turns over before apogee in both programs, which is the cause the report names. OpenRocket’s own record tumbles at 2.84 s: its calm, vertical flight holds the angle of attack at zero until then. Seeded with a 0.5° rod tilt, OpenRocket aborts the flight at 1.71 s and 51 m up (a scratch run, not committed). hpr’s sustainer turns over as the Earth’s rotation at the record’s site, 28.61° N, tips it: its angle of attack passes 90° at 2.25 s. Athpr sim’s default site, on the equator, nothing tips it and it stays upright (Stability margin). Its descent lands at OpenRocket’s speed (+0.00%), but its flight time is 52.64% short. Since #329, hpr’s printed margin is the weakest direction’s:hpr simprints this configuration’s least margin as −4.21 calibres, at the separation (0.86 s), where it printed +1.56 before (Stability margin).
How they were flown. cargo xtask ork-flights flies every configuration of the record in the
section above that hpr can fly. It uses the conditions OpenRocket flew: the launch rod as recorded
(its length, its angle from the vertical and the compass bearing it leans toward, as
below), the recorded site, the
standard atmosphere and no wind. Each figure is taken the
way OpenRocket takes it, by the definitions above:
- The apogee is the highest point above where the rocket started. OpenRocket counts altitude from 0 at launch, so hpr counts its center of mass’s height from where it stood on the pad.
- The largest speed is the peak speed on the way up, taken over every sample from launch to apogee. OpenRocket’s rockets come down under parachutes, but hpr’s flight compared here flies none, so its unbraked fall is left out. Until M2.2e7, hpr ended the search at the first sample whose vertical speed was no longer above zero. A rocket held on the pad can read a few µm/s up or down (#223: a vertical-speed drift on the pad), and on one private flight that stopped the search before liftoff.
- The stability margin at rod clearance is the distance from the center of mass aft to the center of pressure, in body diameters. It is taken at the step where OpenRocket’s rocket had just travelled past the rod’s length, at that step’s time and Mach number.
The design checks are hpr’s tests of whether the parts fit together. Where they only warn, hpr flies the design as OpenRocket does; the comparison flies even a design with errors, and the report lists any errors. They find none on OpenRocket’s examples. The Deployable payload‘s payload mass component and parachute are 25 mm across in a 21 mm bore, so they can’t go in as written, but as packed parts they warn (a packed part wider than its bore). The two pods examples write each centering ring as a child of the 18 mm motor mount it wraps. A ring around its tube is checked in the part around it at its own station (a ring around its tube, M4.5k). Pods–airframes and winglets’ rings fit its airframe. Of Pods–powered’s, one fits the booster’s airframe and one, in a coupler, reaches 0.76 mm past the coupler’s 31.45 mm bore: a fit to sand, so it warns. Elsewhere the checks give only warnings (ADR-155, the decision on which fits warn): a coupler whose wall reaches 0.46 mm past the bore it sits in, a fit within the tolerance for that size; bulkheads sized to the airframe on the ends of couplers, which are caps; and a 29 mm motor in a 28.956 mm mount, which its real case fits.
The record holds 57 powered configurations. The two payload designs, whose payload drops away with nothing left to burn, fly since M4.5g3 (a payload’s split), the three-stage example since M4.5g2 (several separations), Pods–airframes and winglets since M4.5g4 (fins on a nose cone), and three of the four of Pods–powered with recovery deployment since M4.5i (screw heads and motors in several pod sets), its first, which OpenRocket aborts, since M4.5n (a stage’s first burnout), and both of Parallel booster staging since M4.5m (parallel stages). The other 3 are listed in the report with the reason hpr doesn’t fly them yet: a motor with no thrust curve, among them the one powered Loft demo.
Of the other six Loft demos, five have no motor OpenRocket finds, and OpenRocket does not open
demo-quirks.ork.
Where to find it. The committed report has every flight’s figures, and its JSON twin the same with more digits. Flying them needs the pinned OpenRocket jar and the record of OpenRocket’s motor database, which only a local checkout holds. CI does not fly them again. It checks that every figure of OpenRocket’s in the report is the record’s, and that every outcome, summary and table follows from hpr’s figures.
Results (hpr less OpenRocket):
| metric | named cause | flights | median | range |
|---|---|---|---|---|
| apogee | none | 36 | −0.18% | −4.34% to +1.95% |
| apogee | OpenRocket’s parachute opened before apogee | 15 | +0.10% | −1.24% to +13.80% |
| apogee | hpr’s own drag, sized on OpenRocket’s | 1 | +6.95% | +6.95% |
| apogee | the rocket turns over before apogee in both tools | 1 | −37.93% | −37.93% |
| largest speed | none | 51 | +0.46% | −0.69% to +6.10% |
| largest speed | hpr’s own drag, sized on OpenRocket’s | 1 | +6.40% | +6.40% |
| largest speed | the rocket turns over before apogee in both tools | 1 | −15.76% | −15.76% |
| margin at rod clearance | none | 53 | −0.0003 cal | −1.0768 to +0.0764 cal |
The margin’s −1.0768 cal is the Tube fin rocket‘s, whose tube fins’ center of pressure the two programs place differently (aerodynamics: Tube fins). The +0.0764 cal is the Pods–airframes and winglets example’s on an A8-3: its five flights read +0.0706 to +0.0764 cal, hpr’s margin above OpenRocket’s, the flattering side (#325, #326). The three Pods–powered with recovery deployment flights read +0.0695 to +0.0702 cal, on the same side. The other 44 flights’ margins are within −0.0580 to +0.0045 cal. Below −0.0151 cal are only the three-stage example’s three, −0.0387 to −0.0580 cal.
The rocket’s mass and center of mass, over the same 53 flights (hpr less OpenRocket):
| quantity | flights | median | range |
|---|---|---|---|
| mass at launch | 53 | +0.008% | −0.016% to +0.209% |
| mass as the rocket leaves the rod | 53 | +0.012% | −0.030% to +3.257% |
| center of mass as the rocket leaves the rod | 53 | +0.0007 cal | −0.0028 to +0.0624 cal |
A positive center-of-mass difference means hpr’s is further aft than OpenRocket’s. That shortens the margin by the same number of calibres, so the flight at +0.0624 cal, the three-stage example on an A8-5, is also the one whose margin is 0.0580 cal short.
- Mass doesn’t explain the apogee misses with no named cause. On 35 of those 36 flights hpr’s mass is within 0.05% of OpenRocket’s at launch. As the rocket leaves the rod it is within 0.04% too, on all but four: the two-stage example on two H148R-0 motors, where hpr is 1.65% heavier, the three-stage example on C6 motors, 3.26% heavier (see below), and the two Parallel booster staging flights, 0.041% and 0.063% heavier. The 36th, the Base drag hack on a C11-5, is 0.21% heavier, which would lower its apogee, yet it reads 2.06% high. On the largest miss, Dual parachute deployment with a J570W (−4.34%), hpr is 0.01% lighter as it leaves the rod, which would raise its apogee, not lower it.
- The largest launch-mass differences are on the Base drag hack. Its three flights are 0.18% to 0.21% heavier at launch, which would lower their apogees, yet all three read high. As the rocket leaves the rod, only the two-stage flight on two H148R-0 motors (+1.65%) and the three three-stage flights (+1.80% to +3.26%) differ more.
- The largest center-of-mass differences are on two designs. The three-stage example’s three flights are +0.043 to +0.062 cal, as the rocket leaves the rod at the moment OpenRocket’s recorded mass reads light (#185, above). The six Dual parachute deployment flights are +0.010 to +0.016 cal, and they account for almost all of those flights’ margin difference. That design overrides a part’s mass, covering everything inside it, which hpr applies as OpenRocket does: so its launch masses match exactly, and the matched mass says nothing about hpr’s own mass model there. Every other flight is within 0.0054 cal, the most the Pods–airframes and winglets example on a D16-6.
An early parachute can move only the apogee, which is why more flights count toward the largest speed than toward the apogee. The early-parachute group’s median means little: on a rocket whose parachute opens only a little early, the parachute’s cost and hpr’s own miss can cancel. The largest speed’s +6.10% is the Base drag hack on an E12-4, the same drag coefficient that leaves its apogee high (below).
A named cause, and its size. A named cause is only a guess until it is sized (M2.2e4, sizing the causes; decision ADR-073). So for every flight with a named cause, the oracle flies OpenRocket’s same configuration again with the cause taken out, and the report compares hpr’s apogee with that flight too. Nothing else changes between the two OpenRocket flights, so the difference between them is what the cause costs in OpenRocket, and what is left against hpr is what the cause does not explain. A cause counts as sized to the bar when every such flight, with all the flight’s causes taken out, is within the same 5% of hpr’s.
-
hpr’s flight compared here flies no parachute. Its descent is compared on a second flight, with the devices (flown as OpenRocket flies them). With a short motor delay, OpenRocket’s parachute opens while the rocket is still climbing, and stops it lower. On the A simple model rocket example with a C6-3, the parachute opens 2.99 s before the apogee the same flight reaches with nothing deployed. OpenRocket’s apogee is 280.2 m, and hpr’s, with no parachute, is 318.9 m: +13.80%. Flown again with nothing deployed, OpenRocket climbs to 322.4 m, and against that hpr reads −1.07%. On the 3D printable nose cone and fins example the same pair reads +12.19% and −0.16%, and on the Clustered motors example +9.43% and −0.72%.
-
The check both ways. A test (
a_parachute_moves_openrockets_apogee_only_when_it_opens_before_itinxtask/src/ork_flights.rs) runs over all 56 flights of OpenRocket’s record that finished. On the 41 whose parachute opens at or after apogee, the flight with nothing deployed reaches exactly the same apogee, to the last bit: nothing else differs between the two runs. On 14 of the 15 whose parachute opens before apogee, the flight with nothing deployed climbs higher. The 15th opens only 0.10 s early, and its two apogees differ by 1 mm. OpenRocket records that flight every 0.05 s near the top, so its highest recorded point can move by up to 3 mm with timing alone, and 1 mm is within that. -
A part set to no drag flies as OpenRocket flies it, since M4.5h (a part’s drag override; ADR-167, the decision). The Base drag hack (short-wide) example is a short, wide rocket with four fins. Behind it sits a weightless cone, flaring from a point to the body’s full width, that OpenRocket is told has no drag: a modelers’ trick for stubby rockets, which moves the center of pressure aft, since the cone still gives lift. Before then, hpr read the setting but charged the cone the drag of its shape (#165, the drag override not applied). Now it flies the cone with no drag, as OpenRocket does (A part’s stated drag coefficient). hpr’s apogee against OpenRocket’s:
motor before now now, recovery in both now, against OpenRocket’s flight with nothing deployed C11-5 −16.77% +1.95% +1.95% +1.95% D12-3 −15.47% +12.96% +4.52% +5.84% E12-4 −19.13% +10.79% +7.80% +8.92% The “recovery in both” column is the apogee of each flight’s descent in the report’s JSON (
descent.apogee_m), also in ADR-167’s table. The C11-5’s parachute opens after apogee, so it changes nothing there. On the D12-3 and E12-4 OpenRocket’s parachute opens 1.80 s and 1.24 s before apogee, while hpr’s flight in the “now” column flies none: that early parachute is the named cause of +12.96% and +10.79%. With it held in OpenRocket too, the D12-3 and E12-4 still read 5.84% and 8.92% high, more than 5% off. Flown on OpenRocket’s own drag along that flight, hpr comes within 0.10% of it, so what is left is the drag coefficient, not the setting.
What the causes leave. On the three C6-3 flights, what is left is −0.16%, −1.07% and −0.72%, like the flights with no named cause. The Deployable payload‘s C6-3, +13.75% against OpenRocket’s flight, is left at +2.63%, but that pair is not like for like. In OpenRocket’s flight with nothing deployed the payload still drops its booster and climbs on alone, while hpr’s flight compared here climbs as the whole stack. The Base drag hack reads high on all three motors, +1.95% to +8.92% with nothing deployed in either program, more so on bigger motors. That is more than any other flight with no named cause reads high: 20 of those 32 read low, and the other 11 that read high, the five Airstart timing flights, the two payload examples’ ARC payload rocket and Deployable payload on an A8-3, and four Pods–airframes and winglets flights, are at most +1.05%.
A part-by-part comparison of the two programs’ drag at the same speeds, a one-off check that is not part of the report, suggested two candidates, both on the side of hpr flying higher. The report now sizes one of them, and nothing here measures which program is right.
- The nose. OpenRocket charges this very blunt, rounded nose, whose length is only 0.58 of its diameter, some pressure drag even at low speed. Its coefficient, on the body’s cross-section of 48.7 cm², goes from 0.0117 at Mach 0.1 to 0.0482 at Mach 0.25, about the fastest these flights go. hpr charges it 0.0008, read off a straight line between two round heads Hoerner measured: a hemisphere at 0.01 and a head one diameter long at −0.05 (blunt ellipsoids; ADR-173, a blunt ellipsoid’s measured drag). A one-off probe, not committed, found that bringing the E12-4 within 5% would take about 0.013 to 0.015, more than the hemisphere’s. hpr keeps the measurement, and the gap stays. By OpenRocket’s breakdown, this is where the drag coefficient differs.
- The base while the motor burns. hpr takes the motor’s area off the base drag while it burns, and OpenRocket 24.12 doesn’t. The report flies hpr again with OpenRocket’s base drag under power: on the D12-3 and E12-4 it reaches the same apogee as on its own drag, so this candidate moves nothing on this rocket.
#177 (a very blunt nose’s drag below Mach 0.8) holds the numbers; ADR-173 the measurement and the decision.
The margin, part by part. The report also lists the parts of each margin at rod clearance: the mass, the center of mass, the center of pressure and the reference diameter. The reference diameters agree exactly, and the centers of pressure within 0.25 mm on every flight but two examples’. The Tube fin rocket’s tube fins put hpr’s 26.6 mm forward of OpenRocket’s, and on Pods–airframes and winglets hpr’s is 2.6 mm aft. Apart from those two and the three-stage example’s (above), the largest margin gaps are on the Dual parachute deployment example, −0.0097 to −0.0151 calibres: hpr’s center of mass is 0.6 to 0.9 mm aft of OpenRocket’s there. Mass times that gap, from the report’s JSON figures, stays between 1.1 and 1.35 g·m on all six motors, while the rocket’s mass runs from 1.49 to 2.18 kg. So the gap is probably in the airframe, not the motors, but it is not traced yet.
What it leaves out. Every flight is calm and vertical, with no wind and no recovery. One
flight, the Dual parachute deployment example on a J570W, is briefly faster than sound
(OpenRocket’s largest Mach number is 1.147). The other 52 stay below Mach 0.72, so this barely
tests hpr faster than sound. That flight is also the largest gap with no named cause: −4.34% in
apogee, with its largest speed −0.69%. The Earth is not the same in both programs. hpr’s is the
WGS 84 ellipsoid, with its gravity and rotation. OpenRocket’s runs record
a flat Earth for 23 of the 53 flights and a spherical one for the other 30 (the record’s
geodetic field). Each program keeps its own
model, and the effect of the difference is not measured. The decision is ADR-069.
Staged, clustered and air-start flights
Since the M1.9c milestone, hpr flies three kinds of flight it used to leave out: a rocket that drops a booster, a cluster of motors, and an air start. The bar was written down before any of them was measured (decision record ADR-076): every flight of a design within 5% of OpenRocket’s apogee and of its largest speed. Where OpenRocket’s parachute opened before apogee, the apogee is held to OpenRocket’s flight of the same configuration with nothing deployed. All three designs meet it. So does a fourth, the three-stage example, flown since the M4.5g2 milestone (several separations). A fifth staged design, Pods–powered with recovery deployment, flown since M4.5i, does not: in the conditions of OpenRocket’s record its sustainer turns over before apogee in both programs (above). Its first configuration, which OpenRocket aborts, is checked up to the abort instead (below). A sixth, Parallel booster staging, flown since M4.5m (Parallel stages), meets it. The figures are hpr less OpenRocket:
| design | what it tests | flights | apogee | largest speed |
|---|---|---|---|---|
| Two stage high power rocket | a booster that separates, and a sustainer lit after it | 2 | −1.79% and −0.13% | −0.54% and −0.04% |
| Three stage low power rocket | a booster, then a middle stage, each dropping at its burnout as the stage ahead lights | 3 | −1.48% to −0.95%, two against the flight with nothing deployed | +0.25% to +1.64% |
| Clustered motors | four motors in a 4-ring, one in each tube | 5 | −0.79% to −0.35%, three against the flight with nothing deployed | +0.20% to +0.92% |
| Airstart timing | a 3-ring of I211W motors beside a K550W in its own mount, the ring lit at launch or 1, 2, 4 or 6 s later | 5 | +0.48% to +1.03% | +0.46% to +0.81% |
| Parallel booster staging | two boosters beside the core, lit with it at launch and dropped at their charge at 2.44 s; or carried to apogee with no motor | 2 | +1.23% and +0.68% | +3.25% and +1.56% |
- The two-stage flights separate when OpenRocket’s do, at 1.535 s and 1.515 s after launch (ADR-076). OpenRocket’s record keeps the part with the nose, so both programs’ apogee and largest speed are the sustainer’s. For its descent, hpr gives the booster a recovery device that opens at the split and the sustainer one that opens at its apogee. Neither acts on the climb, which is what is compared. How hpr flies a separation is in Staging.
- The three-stage flights separate twice when OpenRocket’s do, at 0.857 s and 1.714 s on the B6-0 boosters and at 1.860 s and 3.720 s on the C6-0s: at the booster’s burnout, then at the middle stage’s. hpr flies each split in turn, from the tail forward (Several separations). The sustainer lands within 0.009% of OpenRocket’s landing speed on each.
- The rocket a configuration builds doesn’t carry the separations. The reader hands them
over separately, as the configuration’s
stagingandlater_stagings(MotorConfiguration::stagingslists them all, from the tail forward). A program turns them into the flight’s separations withhpr::ork::separationsand passes those to the flight. hpr refuses that flight unless each part has a recovery device, so the example gives both parts one.hpr::ork::separated_recoverygives each part the file’s own devices instead, andhpr simflies a powered separation that way (Separation). The exampleork_two_stage.rsdoes this for a made-up two-stage.ork; run it withcargo run --example ork_two_stage -p hpr. - On two H148R-0 motors, OpenRocket’s rocket gets lighter twice as fast before the sustainer lights. From launch to rod clearance it loses 0.0823 kg, and hpr’s 0.0412 kg: 1.9992 times as much. The launch masses agree, so hpr is 1.65% heavier as it leaves the rod. On the other configuration, with two different motors, the two programs lose the same mass. OpenRocket’s recorded mass falls as if both H148Rs lost propellant from launch. The three-stage example shows the same: OpenRocket’s drop is 2.0000, 1.9998 and 2.9944 times hpr’s, the count of motors of the booster’s type in each configuration, so hpr is 1.80% to 3.26% heavier as it leaves the rod. What that does to the apogee is not sized (#185).
- Three of the cluster flights have a parachute that opens early in OpenRocket, 0.37 s, 2.59 s and 0.59 s before apogee. Against its own record, the C6-3 reads +9.43%; against its flight with nothing deployed, −0.72%.
Flights OpenRocket aborted
hpr’s flight of Pods–powered with recovery deployment’s first configuration,
[C6-7; 2× A3-4, B6-0], is within 5% of OpenRocket’s at two points up to where OpenRocket
stops. That is a check of the climb’s first 1.81 s, not of an apogee. OpenRocket aborts the
flight at 1.81 s: “Stage began to tumble under thrust.” Its record then has no apogee, largest
speed or margin, so the first table withholds them. Up to the abort the record is a flight like
any other, so cargo xtask ork-flights compares hpr’s height above its start and its speed at
two points. The first is OpenRocket’s separation: its row just after the split, against hpr’s
sample at its separation event, the whole stack’s center of mass. The second is OpenRocket’s last
row. It is met when both separate at the same time and each value is within 5% of OpenRocket’s,
the band an apogee gets before it needs a written cause (ADR-172):
| OpenRocket | hpr | Δ | |
|---|---|---|---|
| separation, s | 0.86 | 0.86 | 0 |
| height at the separation, m | 22.44 | 22.18 | −1.17% |
| speed at the separation, m/s | 46.08 | 46.06 | −0.04% |
| height at OpenRocket’s last row (1.81 s), m | 85.0 | 84.0 | −1.18% |
| speed there, m/s | 76.8 | 79.9 | +4.02% |
This is weaker evidence than an apogee. It is two points on the way up, and the second is where OpenRocket’s flight is breaking up. When OpenRocket aborts is its own call, too: the committed probe of this configuration with nothing deployed (Delays and ignition) aborts at 2.40 s, not 1.81 s.
The booster’s B6 burns out first, so the booster drops at 0.86 s with both pods’ A3s still
burning, as in OpenRocket (Delays and ignition). Its least static margin,
in the weakest plane, is −4.21 calibres at the split: the same unstable sustainer as
[C6-7; B6-0] (above). The +4.02% in speed comes just as both rockets start to turn over:
OpenRocket’s speed peaked at 77.5 m/s and is falling at its last row, while hpr’s still climbs.
The report’s
Flights OpenRocket aborted
section has the row.
Whether hpr’s sustainer turns over depends on how it is launched. In the record’s conditions,
a 0.15 m rod at 28.61° N with no wind, hpr’s sustainer turns over, its angle of attack past 90°,
at 2.04 s. From hpr sim’s defaults, a 1.5 m rail with no wind, it does not, so the apogee
hpr sim prints assumes the unstable sustainer stays upright. Why the two launches differ is not
traced. hpr sim prints the least margin of −4.21 calibres with a warning that the rocket is
unstable under power and its apogee is not a prediction
(Flight metrics: unstable under power;
#335).
A tilted launch rod
hpr flies a tilted launch rod the way OpenRocket records it (M2.2e5, issue #173). This is checked only against OpenRocket, on one small probe airframe and one private design, from a 1 m rod in calm air. A tilted rod in wind (or with OpenRocket’s launch into wind setting), a longer rod and the rocket’s roll on the rod are not tested.
OpenRocket stores the rod’s angle from the vertical and the compass bearing it leans toward, as
the conditions probe found. The flight comparisons here give hpr’s rail
the same bearing and an elevation of 90 degrees less the tilt, so a rod 10 degrees from the
vertical is a rail at 80 degrees. Like the vertical rod before it, the rail has no friction. The
conversion is in the comparison tool (cargo xtask ork-flights), which takes the conditions from
OpenRocket’s record. The .ork reader stores the file’s degrees as radians, as the table above
says, but loading a design does not set up a rail: a program that flies it gives its own.
How it was checked. Four small probe designs fly the airframe of the
pod probes (a 0.2 m cone, a 0.6 m tube 60 mm across, three fins and an
AeroTech H128W) in both programs: from a rod tilted 5° toward north, 10° toward east, 10° toward
south-west and 20° toward east. The same airframe from OpenRocket’s default vertical rod
(pods-none) is the control. From the
public report’s table
(2026-09-27):
| rod | apogee lost to the tilt, OpenRocket | hpr | where the rocket is at apogee, OpenRocket | hpr |
|---|---|---|---|---|
| 5° toward north | 0.65% | 0.63% | 99.2 m north | 98.5 m north |
| 10° toward east | 2.60% | 2.52% | 195.0 m east | 194.3 m east |
| 10° toward south-west | 2.60% | 2.54% | 138.5 m west, 138.1 m south | 138.1 m west, 137.6 m south |
| 20° toward east | 10.09% | 9.82% | 370.6 m east | 369.6 m east |
At apogee both rockets are on the same bearing from the pad to within 0.03 degrees, and hpr’s is
at most 0.64% nearer. In calm air, the direction of the tilt changes only where the rocket goes,
not how high: the two 10° rods lose the same apogee. The test
tilted_rods_fly_as_openrocket_flies_them (in xtask/src/ork_flights.rs) holds each probe within
0.1 degrees of OpenRocket’s bearing, 1% of its distance and 0.5 percentage points of the apogee it
loses (ADR-094). The small differences left are partly OpenRocket’s position being
read from its highest recorded step rather than the apogee itself, and partly sideways motion
across the tilt: hpr’s fits Earth’s rotation in sign and size, and OpenRocket’s, the other way,
has no measured cause.
What differs: the margin. OpenRocket’s rocket reaches its first step past the rod’s end (rail exit) at a small angle of attack that grows with the tilt: none from the vertical rod, 0.116 degrees at 10 and 0.228 at 20. Its center of pressure moves forward with the angle, so its stability margin falls, by 0.016 calibres at 20 degrees. hpr takes the margin at no angle of attack, so there the margins part by 0.017 calibres. Taken at OpenRocket’s angle, hpr’s margin falls by 0.012 calibres, to 0.005 from OpenRocket’s. So most of the gap is the angle at which each program looks. The rest grows with the tilt too, because hpr’s center of pressure moves about a fifth less than OpenRocket’s for the same angle.
Vertical rods too. OpenRocket records a direction for a vertical rod as well (90 degrees by default). On a vertical rail it only sets which way the fins face on the pad. Taking it as recorded moved the public flights’ apogees by under 0.001%, enough to change one rounded apogee in the report by 0.1 m.
OpenRocket’s flights of the private designs
This section counts OpenRocket 24.12’s flights of the private design library, the corpus: 27
.ork files, which stay out of this repository
(M2.2e2, OpenRocket flies the corpus). They are flown the
same way as the public designs: every motor
configuration, in calm air, from the launch conditions of the
design’s first stored simulation. hpr’s flights of
them are compared below.
The flights are written to the gitignored corpus-out/, because a flight’s numbers can identify
someone’s design. Only counts are published. cargo xtask ork-flights --corpus prints them from that
record, and never prints a file name.
Counts on 2026-09-25, by where the design was found. Distinct means distinct in content (by SHA-256 hash). Flown to the end means the simulation ran to its end, not stopped by OpenRocket (aborted). The last row shows each such flight recorded the stability margin the next comparison needs.
| the private library | OpenRocket’s examples | elsewhere under refs/ | |
|---|---|---|---|
| design files | 27, all distinct | 17 | 31, 25 distinct |
| refused by OpenRocket | 0 | 0 | 4 |
| configurations declared | 89 | 56 | 24 |
| with no motor OpenRocket loaded | 1 | 0 | 16 |
| aborted by OpenRocket | 0 | 1 | 0 |
| flown to the end | 88, in all 27 designs | 55, in all 17 | 8, in 4 designs |
| with a margin at rod clearance | 88 | 55 | 8 |
- OpenRocket flew every configuration of the private library it loaded a motor into. None was refused and none aborted.
- The one configuration without a motor names one in its file, with its thrust curve’s digest, but OpenRocket loaded no motor for it, so it had nothing to fly.
- The record doesn’t keep OpenRocket’s warnings on loading a file, or the digest of the thrust curve each flight used. So a flight on a curve other than the one saved in the file would not show here. hpr’s comparison checks each curve against the motor record before it compares (below).
- 15 of the 27 files are public designs: 13 are the very files of OpenRocket’s examples, and two are edited copies of examples. So the library holds 12 private designs, other people’s.
- All 27 files have a stored simulation, so every flight took its launch conditions from the design’s first one. As those simulations were set, OpenRocket flew 64 of the 88 on a spherical Earth and 24 on a flat one. hpr uses the WGS 84 ellipsoid, and the effect of that difference is not measured yet.
- The examples column is a new run of the 17 examples inside OpenRocket’s jar from M2.2d1 (OpenRocket’s flights of the public designs), with the same 56 configurations and one abort. That earlier run’s 57 also counted a Loft demo, which is in elsewhere here.
- Elsewhere is mostly the test and demo designs of Loft and
Debrief, the two projects hpr took over, plus one private design log’s
.orkand nine from OpenRocket’s database repository. OpenRocket refuses four of them. Most of their configurations have no motor. - Left out: the library’s 4 RASAero
.CDX1and 4 RockSim.rktfiles. hpr can’t read either format yet, so there is nothing to compare them with, and the flight script fails on three of them (#168). The decision is ADR-071.
To repeat it, you need the private library under refs/, which most readers won’t have, plus Java
17 and the OpenRocket jar (cargo xtask refs fetch), as for the
mass comparison. Then, from the repository root:
# --jar also flies the 17 examples inside OpenRocket's jar
refs/venv/bin/python validation/oracles/openrocket/flights.py \
corpus-out/openrocket-flights.json refs --jar
cargo xtask ork-flights --corpus
hpr’s flights of the private designs
This section compares hpr’s flights of the 12 private designs with OpenRocket’s
(M2.2e3, hpr’s flights of the corpus), as
the public comparison does for OpenRocket’s examples, by the
same definitions. It is a code-to-code comparison with
no target, and hpr flies 11 of the 12 designs. On four of them, C03, C06, C08 and C09,
hpr’s stability margin is clearly larger than OpenRocket’s, so it calls those rockets more stable
than OpenRocket does. C03 and C09 are open
(#172); C08 has a lead
(#186); and C06’s is mostly its center of mass.
A likely cause is its airfoil fins, which OpenRocket weighs at 0.85 of a slab and hpr at 0.6851
(the mass survey’s fin-section cause); that
cause is sized on the structure alone, not on this flight. A fifth, C12,
reads larger too, but mostly because it leaves a tilted rod at an angle of attack
(below). One flight, the only supersonic one, climbs 13.60% higher than
OpenRocket’s, and the cause is the drag coefficient
(below). Nobody without the
private library can fly them again: CI checks only that the report adds up and names nothing of a
design.
How far it gets. M2.2, the OpenRocket comparison, asks for
at least 20 designs compared in five ways: apogee, largest speed, stability margin, mass and center
of mass. With the public report’s 15, these make 26, so that count is met. Its mass conventions
(M2.2b) are rolled up too, and two checks carried over from
Loft end it (below): one passes, and one, the tube-fin center of pressure, is
missed, with the gap measured and pinned. Staging and clusters
(M1.9) added one of these private designs and three public
ones, a tilted launch rod (M2.2e5) one private design, and
reading the old override flag as OpenRocket does (M2.2e6)
another. Weighing fin fillets and reading an automatic radius inside a nose cone
(M2.2e7,
#174: airframes read simpler than written) added
two more, C01 and C06, and flying tube fins
(M2.2e9) OpenRocket’s Tube fin rocket. The twentieth,
C04, flies since M2.2e10: one of its motors is set to
light at an event that never comes, and hpr now flies that motor unlit, as OpenRocket does
(above). hpr’s apogee is 0.88% above OpenRocket’s (the report’s row
C04/1). It is a staged flight, the kind in which OpenRocket’s recorded mass has gone wrong
before (#185). A check run locally on the private
file, not committed, found OpenRocket’s mass falling by each burning motor’s propellant along this
flight, so that fault does not show there; no committed check holds it.
Two checks carried over from Loft, the project before hpr-sim, end the comparison (M2.2f). L82 passes; L19 does not, and its gap is measured instead:
- L19: a tube fin set’s center of pressure should be within a quarter calibre of OpenRocket’s. It is not. On the Tube fin rocket hpr’s is 1.07 calibres forward, which is nearly all of the 1.08 calibres between the two margins, hpr’s 0.79 and OpenRocket’s 1.87. A test measures and pins the gap on 14 probe designs and on this rocket, and nothing measured says which code is nearer (tube fins).
- L82: a case let off a pass-or-fail bar still counts in the
error statistics. A test holds the accuracy census to it. Every
number the reports compare is counted, including the misses that have a written cause or
explanation. A rocket with two references counts against both: RocketPy’s flight of Juno III,
for one, and its team’s altimeter log. The results stored inside
.orkfiles never enter the census, so this does not cover them.
Still not flown:
- stages hpr can’t separate yet: the second of the two private designs the old override flag was blamed for. Its flag reads the same either way; what holds it is a separation that comes while a motor behind it still burns. The two public payload designs that waited here fly since M4.5g3 (a payload’s split);
- none held back by what hpr leaves out any more. The library’s copy of Parallel booster staging flies since M4.5m read parallel stages. The pods themselves fly since M1.13c1, the cockpit on Pods–airframes and winglets’ nose since M4.5g4, and Pods–powered with recovery deployment, once held back by its rail buttons’ screw heads, since M4.5i.
What is published. The designs are other people’s, so the
report
holds only differences: hpr’s number less OpenRocket’s, in per cent for apogee, largest speed and
mass, and in calibres (OpenRocket’s reference diameters) for the
stability margin, the center of mass (CG) and the center of
pressure (CP). A design’s own values are never written: next to its difference, hpr’s value would
let anyone work out OpenRocket’s, and so the design’s. Each design is an id, C01 to C12, in the
order of its file’s SHA-256 hash, and each flight is the design’s id and the configuration’s place
in the file: C09/2 is the second configuration of design C09. A CI test checks that the report
holds only these ids, reasons and differences from fixed lists, rounded so that no rounding residue
gives a value back, and that its summary matches its rows.
Which flights are compared. hpr flies a configuration only when it can show both programs used the same thrust curves. Each curve must be the one saved in the design file, or one from OpenRocket’s motor database matched by its digest (OpenRocket’s fingerprint of a curve), and the record of OpenRocket’s run must show it loading exactly those curves. One configuration is left out because hpr’s curve came from its own catalog, found by the motor’s name. A tilted rod is flown as recorded, as in A tilted launch rod.
The 35 flights (from the committed report, 2026-09-28):
| metric | flights | median | from | to |
|---|---|---|---|---|
| apogee, no named cause | 28 | −0.31% | −4.84% | +1.17% |
| apogee, OpenRocket’s parachute open before apogee | 6 | −0.32% | −3.09% | +2.15% |
| apogee, hpr’s own drag coefficient | 1 | +13.60% | +13.60% | +13.60% |
| largest speed, no named cause | 34 | +0.27% | −0.65% | +2.28% |
| largest speed, hpr’s own drag coefficient | 1 | +7.98% | +7.98% | +7.98% |
| margin at rod clearance | 35 | +0.0308 cal | −0.0166 cal | +0.1108 cal |
| mass at launch | 35 | +0.000% | −0.770% | +0.070% |
| center of mass at rod clearance | 35 | −0.0004 cal | −0.1102 cal | +0.0198 cal |
For example, C09/9 reads −4.84% in apogee: hpr’s rocket peaks 4.84% lower than OpenRocket’s on
the same design and motor. Its largest speed is +0.26%, so the two agree on the climb under thrust
and part on the coast, where drag matters most.
- One apogee is more than 5% from OpenRocket’s:
C06/1, +13.60%. Its cause is sized below. No flight has a part with a drag override. - An early parachute lowers OpenRocket’s apogee, so it can make hpr read high but not low. It
cannot explain the four that read low (
C03/3,C03/4,C02/4,C02/5). It could explainC07/1, which reads +0.68% with the parachute 0.55 s early, andC02/2, which reads +2.15% with the parachute 1.45 s early, the longest of the six; how much of either it explains is not measured. - The 14 flights of
C03andC09, launched above sea level, all read low in apogee. The 15th above sea level,C06/1, reads high for its drag (below). Of the 20 at sea level, the 4 ofC07,C08andC11read high, and so doesC04/1, flown since a motor that never lights flies;C01, flown since fillets are weighed, once each way;C02, flown since pods fly, reads high once and low four times;C05, flown since the old override flag is read as OpenRocket reads it, high four times and low once, all within 0.26%; andC12, flown since a tilted rod flies, high once and low twice. Most of the public report’s flights with no named cause read low as well (16 of 21), and every public flight launches at sea level (its record gives a launch altitude of 0 m throughout). So altitude does not yet explain the sign. M2.2e4 sized only the apogees more than 5% off, so this pattern is still untested. - hpr’s margin is larger than OpenRocket’s on four designs: hpr calls them more stable, the
direction to worry about.
C03andC09come first, thenC08andC06/1.C03reads +0.056 to +0.073 calibres: on every flight hpr’s CP sits 0.061 calibres further aft than OpenRocket’s, and the CG accounts for the rest.C09reads about +0.04, half from its CP (+0.020) and half from its CG, which hpr puts forward of OpenRocket’s by 0.0145 to 0.0273 calibres. The reference diameters agree on all 35 flights, so the calibres are the same. On the public designs the margin gap is at most 0.0151 calibres, and the largest (−0.0151) has hpr calling the rocket less stable. No milestone covers it yet: it is #172. A third design,C08, a two-stage rocket flown since the M1.9c milestone, reads +0.1108 calibres because hpr puts its CG 0.1102 calibres forward of OpenRocket’s at rod clearance, with the mass there within about 0.001%. That gap is older than the staging: hpr’s structure alone puts the CG forward by less than the mass survey’s 1% of length. A stage whose mass is set at several times its parts’ own weight magnifies where those parts sit, and two of them differ: an airfoil fin set, a departure hpr keeps on purpose (ADR-062), and packed parachutes whose automatic radius OpenRocket may meet by stretching the packed length (#186). C05reads within 0.0007 calibres on four flights; onC05/2it reads +0.0113, nearly all from its CG (−0.0111 calibres), with the mass there within 0.005%. Not traced.C06/1reads +0.0604 calibres, +0.0540 at OpenRocket’s angle of attack. Most of it is its CG, 0.0447 calibres forward of OpenRocket’s, with hpr’s mass at launch 0.770% below OpenRocket’s. The mass survey names a sized cause for this design: its airfoil fins. OpenRocket weighs an airfoil fin at 0.85 of a square slab of its outline, and hpr at 0.6851, so hpr’s fins are lighter. Weighed OpenRocket’s way, the design’s structure comes within the survey’s 1% thresholds (the fin-section cause). Those thresholds are coarser than this flight’s 0.0447 calibres, and no flight has been flown with OpenRocket’s fin weighting, so the fins are a lead for this gap, not its size.C01reads −0.0166 and +0.0025 calibres. OnC01/1hpr calls the rocket less stable, from a CG 0.0198 calibres further aft.C12, launched from a tilted rod, reads +0.0125 to +0.0366 calibres, nearly all from its CP. OpenRocket’s rocket clears the rod at an angle of attack, as on the probes. With hpr’s CP taken at that angle (the report’s at OR’s α),C12reads +0.0033 to +0.0074, so most of its gap is the angle each program looks at.- hpr’s design checks find errors on 8 of the flights. On
C07/1,C11’s two andC12’s three, an inner part is wider than its parent’s bore by more than the fit tolerance; onC06/1a motor’s case can’t fit its mount; onC08/1a fin is off its body tube. On all ofC03’s flights the inner part is within the tolerance, so it only warns (ADR-155, the decision on which fits warn). Such a part puts mass in a slightly different place, not lift, so it cannot move the CP, andC03’s CG agrees within 0.013 calibres. - The one design hpr does not fly waits on stages hpr can’t separate as written: a separation that comes while a motor behind it still burns. The report lists each configuration with its coarse reason; the breakdown is in ADR-072 (hpr’s flights of the private library) and ADR-095 (the old override flag).
To repeat it, you need the private library, OpenRocket’s flights of it and the motor record, as above, and OpenRocket’s drag curves. Then:
refs/venv/bin/python validation/oracles/openrocket/drag_curves.py \
corpus-out/openrocket-drag-curves.json refs
cargo xtask ork-flights --library # writes the report
cargo xtask ork-flights --library --check # compares with the committed one
A supersonic flight, and a cause in the drag
C06/1 is the first supersonic flight in either OpenRocket report. hpr’s apogee is 13.60% above
OpenRocket’s, and its largest speed 7.98% above. The mass at launch is within 0.77%, and the
difference builds during the burn. No parachute opens early, and no part states its own drag
coefficient. M2.2e4 (causes for apogees
more than 5% off) asks for a written cause with a size, not just a candidate.
Part by part, with the rocket pointing into the airflow, OpenRocket’s drag differs from hpr’s in two ways, and they pull opposite ways:
- Base drag while a motor burns. hpr takes the burning motor’s cross-section off the aft base,
following Niskanen (who cites Fleeman for it): a base the size of the motor has no base drag.
OpenRocket 24.12 keeps the whole base’s drag while the motor burns. On
C06the motor fills most of the base, so hpr has less drag under power and climbs higher (Aerodynamics). What OpenRocket does is measured from its own output. Which rule is right is not: neither has been checked against a measured flight. OnC06/1the choice moves hpr’s apogee difference from +13.60% to −10.71%, about 24 percentage points, more than any other known cause (#222, open on both drag questions).- The measurement: a committed probe,
validation/oracles/openrocket/base_drag.py, records OpenRocket’s base drag on its own example designs, the rows while a motor burns against the rows after, invalidation/fixtures/ork/openrocket-base-drag.json. On all 42 of its flights of one data branch (nothing separating), OpenRocket’s base drag while a motor burns is exactly what the whole base gives, to 1e-12. It does not subtract the motor, even where the motor covers 94% of the reference area, and the drag it flies is the sum that includes that base drag. A test inhpr-validateholds it (ADR-097, the decision on sizing a drag cause). - Motors in pods are among them: the one such flight of OpenRocket’s powered-pods example that burns pod motors keeps its base whole too, and hpr’s opt-in treats a pod’s motors the same way.
- The measurement: a committed probe,
- Supersonic pressure drag. This is the drag from the pressure on the nose, the fins’ edges and any step faster than sound, much of it wave drag. hpr’s is about twice OpenRocket’s, read from OpenRocket’s per-component output but not kept as a record. OpenRocket gives the nose almost none well above Mach 1, and the fins about a quarter of hpr’s. That gives hpr more drag (#222: which supersonic pressure drag is right).
How the cause is sized. hpr flies the flight again on OpenRocket’s own drag coefficient. An
oracle, validation/oracles/openrocket/drag_curves.py, flies each configuration as flights.py
does, with nothing deployed. From launch to apogee it records OpenRocket’s drag coefficient as two
curves in Mach number: power on, the rows while a motor burns, and power off, the rows after the
last one that burns. cargo xtask ork-flights --library flies hpr on them as a
drag table, for any apogee more than 5% off with no other named cause
and no stage separation. Where hpr’s apogee then comes within 5% of OpenRocket’s, the apogee’s
cause is hpr’s own drag coefficient, and the same goes for the largest speed, judged on its own
number. That says the difference is in the drag, not which drag is right, so each such flight
also gets a written breakdown, held by a test (ADR-097, the decision on sizing a drag cause).
The report also flies hpr’s own drag with OpenRocket’s base rule alone, through
Simulation::with_full_base_drag_under_power (Aerodynamics):
C06/1, flown by hpr on | apogee Δ | largest speed Δ |
|---|---|---|
| its own drag | +13.60% | +7.98% |
| OpenRocket’s drag coefficient | +1.11% | +1.04% |
| its own drag with OpenRocket’s base rule | −10.71% | −3.43% |
On OpenRocket’s drag the flight is within the 5% bar, so the drag is the cause. Switching hpr to OpenRocket’s base rule alone lowers its apogee from +13.60% to −10.71%, 24.3 percentage points. Switching the rest of the drag to OpenRocket’s then raises it to +1.11%, 11.8 percentage points. A look at OpenRocket’s per-component columns, not kept as a record, points that rest to the supersonic pressure drag. The second number is by subtraction, and the split depends on which change is made first. Which supersonic pressure drag is right is open: neither code has been checked against a measurement on this shape, and the public report has no supersonic flight.
The drag curves are OpenRocket’s numbers for private designs, so they stay in the gitignored
corpus-out/ with the other records.
Stored simulations in the reference library
cargo xtask ork, over the 73 readable files, on 2026-09-25:
| quantity | count |
|---|---|
| stored simulations | 174, in 61 documents: 139 uptodate, 17 external, 11 outdated, 7 notsimulated |
| stored reference screen | 91 eligible, 83 excluded: 47 inconsistent, 17 external, 11 outdated, 7 not-simulated and 1 missing simulator; only the 91 may enter a stored-data reference denominator (the count of runs used to calculate a reference statistic) |
| hpr reproduction screen | of those 91, 40 can be reproduced when OpenRocket’s database is supplied (1 without it); 51 can’t: 40 because their configuration can’t be flown, 11 because the design is reduced |
| with launch conditions | 173; 129 state the wind’s direction, and 135 launch into the wind |
| atmosphere | 172 isa, 1 not written |
| with a summary | 162 |
| with a time series | 142, over 176 stage branches and 97,541 rows |
The screen counts are separate from the unconditional census below: conditions, summaries, time series, branches, rows and events are counted across all 174 stored simulations, including runs that fail either screen. The counts are an as-of snapshot of this private corpus, not a universal property of every OpenRocket file.
How the stored-result policy was decided is in ADR-065; the underlying read-back policy is in ADR-057.
What hpr keeps for writing the file back
In short. Some of what a .ork holds, hpr’s design does not model: a parallel stage it
can’t read, OpenRocket’s 3D-view settings, a simulation’s plug-ins, a part’s color, a material’s group. hpr keeps
each whole, beside
the design, in an extension (a named slot for data another program wrote) called x-openrocket,
at a path that leads back to where it was, so that writing the file back out puts it back
(writing a .ork back out). A design whose rocket is missing parts
this way says it is reduced; the flag is on the design, not on its rocket, so check it before
using the rocket on its own.
Four kinds of thing are kept: parts, sections, tags and attributes. A tag or attribute is kept for one of two reasons: no reader asked for it, or the reader that asked dropped what it says.
- Parts hpr does not read: a parallel stage outside a body tube, or on a rocket of several stages (Parallel stages, L66), a pod set it cannot lay out (Pods), a part hpr cannot give an honest shape (above), or a tag it has never seen.
- Sections of the document hpr does not read:
<photostudio>(the 3D view),<docprefs>(the design’s own materials), anything else beside<rocket>and<simulations>, and the parts of a stored simulation beyond its conditions and results, such as an<extension>. - Tags no reader asks for, inside a part, stage or stored simulation hpr does read, or inside
a tag it does read: a part’s color (
<appearance>), a catalog preset, a comment, a wind’s standard deviation, a drag coefficient stated on the<rocket>element itself, or a tag hpr has never seen. hpr records every tag its readers ask for while it reads a file, so a tag is kept when nothing asked for it. - Tags a reader asks for and then drops are kept too, because the design does not hold what they say. Examples: an override on a pod set that holds nothing, a ring written as a row of three, a tube fin set of more than eight tubes, a fin section or finish hpr has no reading for, or the older of two names for one value when the two disagree. A value hpr reads exactly, such as a fin set’s count, is not kept.
- Attributes no reader asks for, on an element hpr does read: a material’s
group, an event’sid, or the reference an angle or radius offset is measured from, which hpr does not read yet but for a pod set’sradiusoffset(Pods; issue #145). An attribute whose value a reader drops is kept too, such as a material’s declared kind where the part needs another.
A kept value goes back as it was. When the design is written out as a .ork again, each kept
tag goes back in place of anything the writer would have written under that name from the design.
Take a ring written as a row of three. hpr reads one ring and warns, and the file it writes still
says <instancecount>3</instancecount>. So OpenRocket reads the rings the original had. Reading the written file
gives the same warning. As with the original, that warning means hpr doesn’t fly the rocket’s
configurations, since only a rocket read without such a warning flies
(ADR-055).
A rail button’s screw height was this page’s example until
M4.5i: the design holds it now
(RailButton::screw_height_m), and the writer
writes <screwheight> from the design.
Each is kept with its path, such as openrocket/rocket/stage[0]/bodytube[1]/podset[0]: the
podset that is the first part inside the second part of the first stage. A part counts among all
the parts beside it, since their order is where they stack. A tag’s step starts with @ and
counts only among the tags of its own name: openrocket/rocket/stage[0]/nosecone[0]/@appearance[0]
is that nose cone’s first <appearance>, wherever it stood, and a tag inside it adds another,
such as …/@wind[0]/@gusts[0]. A section counts the same way. A tag that belongs to one
configuration, by its configid, names it and counts only among that configuration’s tags of its
name: …/@deploymentconfiguration(b)[0]. OpenRocket reads no meaning into
the order of tags, and counting by name lets an export put each tag back without knowing where
it stood (ADR-109). An attribute is kept with
the path of the element it was on, its name and its value. The function hpr_io::ork::element_at
follows a path back to the element.
The extension is written under its namespace when the design is saved as JSON, and reads back
unchanged. The test hpr_io::ork::tests::unknown_content_round_trips_through_x_openrocket checks
both, and that each kept element is the one at its path in the file:
let json = serde_json::to_string(&design.extensions).expect("JSON");
assert!(json.starts_with(r#"{"x-openrocket":"#), "{json}");
let back: Extensions = serde_json::from_str(&json).expect("read back");
assert_eq!(back, design.extensions);
What is not kept. The text of a second copy of a tag a reader takes once by name. A reader asks
for a tag by name and uses the first copy, so the second’s own value is taken as read, though its
attributes and anything unread inside it are kept. 13 fin tabs in the library carry a second
<tabposition> this way. That text, and everything else, is still in the document itself, which
hpr keeps whole when it opens a file (ADR-051).
Kept in the reference library
cargo xtask ork, over the 73 readable files, on 2026-10-05, after
M4.5m read parallel stages:
| quantity | count |
|---|---|
| parts kept | 1, in 1 reduced design: the parallel stage directly under the rocket in demo-quirks.ork (Parallel stages). There were 3, in 3, before parallel stages were read. On 2026-09-23 there were 17, in 10; pods (Pods), tube fins (Tube fins sized from the body), a tube coupler and 2 freeform fin sets on a nose cone (Fins on a nose cone or a transition) are read since |
| sections kept | 87: 42 <photostudio>, 36 <docprefs>, 9 simulation <extension>s |
| tags kept | 1,857, most often a part’s <appearance> (309), <radialdirection> (170), <instanceseparation> (159), a wind’s <standarddeviation> (129) and <preset> (127). None of them is a value a reader drops; 16 were, rail buttons’ <screwheight>, before M4.5i read it into the design. Parts’ drag overrides, 2 with their 2 flags before M4.5h (a part’s drag override), are read into the design since |
| attributes kept | 3,209, most often an event’s id (1,623), a material’s group (594), an active stage’s number (201) and a stored branch’s optimum altitude and its time (168 each) |
| kept elements and attributes found again at their path | 5,154 of 5,154 (the survey fails if one is not) |
How this was decided is in ADR-058.
Writing a .ork back out
In short. hpr_io::ork::export writes a design as a .ork file for OpenRocket 24.12: schema
1.10 (the file-format version OpenRocket 24.12 writes), zipped with the design as rocket.ork,
as OpenRocket packs one. It writes from hpr’s design alone, not from a copy of the file it came
from, so a design built in hpr is written the same way. One exception: a value kept from the
original file is written as the file had it, even after the design is edited. Read back, the
written file gives the same design, bit for bit, for each of the 73 designs hpr reads among the
.ork files the checks use (which files). Some are other people’s
private designs, so the counts are published but those files are not. OpenRocket 24.12 opens
every written file whose original it opens, and flies each configuration it can fly (151) to
the original’s apogee within 0.5%: the largest difference is 0.000162%.
The written file is not a copy of the original. It is laid out afresh, and it states some values
the original left to OpenRocket’s defaults, such as each part’s roll angle. Its root’s creator
attribute names the program that wrote it, its version and its designation in the FusionSpace
product system, as every file hpr writes does: creator="hpr-sim 0.1.0 · FS · SW · TOOL 005".
The measurements above were made before the attribute took this form; opening such a file in
OpenRocket has not been checked since.
How it is written. Each part of the design is written as the tags hpr reads it from. What hpr
keeps but doesn’t model goes back where it was
(what hpr keeps): a parallel stage hpr can’t read
in its place among the parts, a part’s color in the part, a simulation’s plug-in in the simulation. A value
hpr read and dropped goes back as the file wrote it. A ring written as a row keeps its
<instancecount>, even though hpr reads one ring. A parallel stage hpr read goes back inside its
body tube (Parallel stages). Two functions do the work:
export::document(&design)gives the document, with a warning for anything kept that has no place any more.export::write(&design, &attachments)gives the file’s bytes. The attachments are the original’s other zip entries (OrkFile::attachments), such as a preview image.
The API reference has a worked example
(hpr_io::ork::export).
Seven mistakes of Loft’s exporter (Loft is the project before
hpr-sim) are pinned by two tests, round_trip_keeps_delays_ignition_conditions_and_override_flags
(L67) and fin_points_clusters_and_floats_round_trip_exactly
(L68), in hpr_io::ork::export::tests. They run on invented
designs, as does a third test holding a small design’s written document to one worked out by
hand:
| Loft wrote | hpr writes |
|---|---|
| a plugged delay as 0 s | none, which reads back as plugged |
| ignition only for the whole mount | each configuration’s own ignition event and delay |
| no launch conditions | every condition of every stored simulation |
| masses it had worked out, as overrides | only the overrides the design has, with their flags |
| a freeform fin as a trapezoid of the same area | every point of the outline |
| a cluster without its scale and rotation | the cluster’s pattern, scale and rotation |
| each number to six decimals | the shortest number that reads back to the same bits |
Numbers to the last bit. A length of 0.1 + 0.2 meters is written 0.30000000000000004,
because 0.3 would read back as a different number. Angles are stored in radians and written in
degrees. The writer picks the shortest number of degrees that converts back to exactly the stored
radians: a fin set at 45° is written 45.
What OpenRocket insists on. These were found by opening written files in OpenRocket 24.12, and flying them (checked in OpenRocket). Reading a file back in hpr is not enough: before the last of these was found, every design read back the same, yet one of OpenRocket’s own examples flew 0.6% to 0.9% low and a private design 10% to 11% low.
- An id that isn’t a UUID (a 36-character code such as
0f0e0d0c-0b0a-4900-8800-070605040302) makes OpenRocket refuse the whole file. So such an id is left out, and OpenRocket gives the part one of its own. A part the file gave no id reads back with the same id hpr made up for it. - OpenRocket reads an inner tube’s roll angle, and a parachute’s or a mass’s, only under the older
tag
radialdirection. So they are written under that tag. - OpenRocket applies a part’s catalog preset (
<preset>, a maker’s part number) at the moment it reads that tag, over the sizes and material it has read so far. hpr keeps the tag without reading it, and writes it first, after the part’s name and id, as OpenRocket does. Written after the part’s own sizes, it had OpenRocket fly the catalog’s part instead: one design flew 11% low.
What is still lost. None of these happens in the reference library:
- A tag the file leaves out, where hpr assumes a value (a radius with nothing to take, a missing wall or density). The written file states the assumption, so reading it again no longer warns.
- A second motor in one mount for one configuration.
- A configuration with no id, or one declared twice.
- Simulation rows and events hpr left out on reading.
- The text of a second copy of a tag (above).
After an edit to the design, a kept tag’s text still wins over the edit. Change a ring written as
a row in hpr and its original <instancecount> is written as it was. A rail button’s screw height is the
design’s since M4.5i, so an edit to it is written.
Checked on the reference library. cargo xtask ork writes every design out, reads it back,
compares the two, and writes the design read back once more. It fails on any difference. On
2026-09-29:
| quantity | count |
|---|---|
| designs written | 73 |
| read back as the same design | 73 |
| written again the same, byte for byte | 73 |
| export warnings | 0 |
cargo xtask ork # counts, and fails if a design comes back different
cargo xtask ork --export corpus-out/written # also saves each written file, by the original's path
How this was decided is in ADR-109.
Checked in OpenRocket
In short. OpenRocket 24.12 flew each design twice: as the original file, and as hpr’s export of it. Every export opened where its original did, and every configuration that flew both ways reached the same apogee within 0.5%. Both flights are OpenRocket’s, so this checks the file hpr writes, not hpr’s physics; how hpr’s own flights compare is in hpr’s flights against OpenRocket’s. hpr itself flies a written file exactly as it flies the original, since it reads back the same design.
The files. The check uses every .ork file the survey (cargo xtask ork) finds:
| where | files |
|---|---|
| the example designs that ship with OpenRocket | 17 |
the private design library (the corpus; the report’s refs/loft-fixtures) | 27 |
other .ork files under refs/, mostly Loft’s and Debrief’s test designs (listed above; the report’s elsewhere under refs/) | 31 |
| all | 75 |
hpr reads 73 of them; the other two are malformed XML, which OpenRocket refuses too. OpenRocket’s examples and most of the other files are public.
How they are flown. Each flight takes its launch conditions from the file’s
first stored simulation, or OpenRocket’s defaults
when it has none, in calm air, with random seed 1. So a difference between the two flights comes
from the file alone. A configuration passes when the export’s apogee is within 0.5% of the
original’s. The 0.5% is the roadmap’s bar
(M3.2, writing .ork files), set before measuring. The
flights agree about 3,000 times more closely, so the report also gives the largest difference.
| quantity | count |
|---|---|
| designs | 75 |
| opened as written | 71 |
| opened as exported, of those | 71 |
| designs with a configuration flown both ways | 48 |
| configurations of the opened designs | 169 |
| configurations flown neither way | 18 |
| configurations flown both ways | 151 |
| within 0.5% of the original’s apogee | 151 |
| the same apogee to the last bit | 142 |
| largest apogee difference | 0.000162% |
- Four designs don’t open as written. hpr can’t read two of them, so it writes nothing. OpenRocket refuses the exports of the other two with the same error as their originals.
- 23 opened designs have nothing to fly. They have no configuration, or none with a motor.
- Eighteen configurations fly neither way. Seventeen have no motor. OpenRocket aborts the other one both times, for the same cause.
- Nine configurations differ, but by very little. All nine are in the private design library
(the report’s
refs/loft-fixturesrow). The largest difference is 0.000162%. Their cause has not been traced.
The committed report,
openrocket-export-flights.md,
gives these counts by where the designs came from. The flights themselves stay in the gitignored
corpus-out/, since they could identify private designs
(OpenRocket’s flights of the private designs).
Running it again. It needs Java 17, the OpenRocket jar fetched by cargo xtask refs fetch,
and a Python environment at refs/venv with JPype, which lets the oracle
scripts drive OpenRocket, as for OpenRocket’s mass.
Without the private library, the commands fly the public files alone.
The first command flies the originals, and is needed only when refs/ or the flight scripts
change. The second writes each design back out; the third flies those files; the last compares:
refs/venv/bin/python validation/oracles/openrocket/flights.py corpus-out/openrocket-flights.json refs --jar
cargo xtask ork --export corpus-out/ork-export
refs/venv/bin/python validation/oracles/openrocket/flights.py corpus-out/openrocket-export-flights.json corpus-out/ork-export
cargo xtask ork-export-flights
The last command stops with an error when the flights are stale: when hpr would now write
different files, when an original has changed since it was flown, or when the two sets of
flights used a different OpenRocket or different scripts. With --check, it compares its result
with the committed report instead of writing it.
What is not read yet
A parallel stage on a rocket of several stages on the axis, or anywhere but inside a body tube, is kept, not modeled (what hpr keeps). One inside a body tube on a rocket of one stage on the axis is read and flown (Parallel stages), and so are pods (Pods).
What keeping the whole document buys you is this: when hpr meets a part it does not model, it can
say so and carry the part’s own XML along untouched in x-openrocket, rather than dropping it
silently the way Loft did with pods and parallel stages. The writer puts that XML back where it
was (writing a .ork back out). ADR-064
confirms that the reduced flag and this preserved content are the current rule for b4, not a hidden
mass estimate.
OpenRocket .orc parts catalogs
A .orc file is a parts catalog for OpenRocket, which calls its parts component presets.
It lists nose cones, body tubes, couplers,
centering rings, bulkheads, transitions, launch lugs, parachutes and streamers, as their makers
sell them, each under its maker and part number, with its sizes and its material. OpenRocket 24.12
ships 16 of them, from the openrocket-database project: 3,449 parts from Estes, LOC Precision,
Giant Leap, Madcow, SEMROC and others.
hpr has those 16 files built in. A program can look a part up by maker and part number, or search for one. This page is the reference for that reader: what a file holds, how each value is read, and where hpr’s reading differs from OpenRocket’s.
How far to trust it. OpenRocket’s own reader was run on the same files as an oracle, a program whose answers hpr is checked against. hpr reads every part OpenRocket reads, in the same order. Of the 18,306 sizes, masses and densities compared, 17,911 come out equal to the last bit. The other 395 (185 masses in ounces, 207 densities and 3 undefined materials) are counted, and so are the 252 parts whose maker OpenRocket names otherwise; each has a known cause (Where hpr and OpenRocket differ). A part is only as right as its file, though. The database’s README warns that its data may be wrong for your rocket and that you should weigh your real parts.
Building with it. The builder makes a rocket’s parts from catalog parts, and weighs each one as OpenRocket does, with the few differences it names (Parts from a catalog, written for M5.5b). A part that states its mass weighs that mass: its density is scaled to give it.
Code: hpr_io::orc (API reference), written for
M5.5a, the parts reader. The decisions are in
ADR-132: OpenRocket’s parts catalogs.
From a program
hpr_io::orc::bundled() is the built-in catalog. find takes a maker and a whole part number;
search finds every part whose number or description holds some text. This program looks up a
LOC Precision nose cone and a Giant Leap parachute, then searches Estes’ parts:
//! Parts from the catalog OpenRocket ships: a nose cone and a parachute found by maker and part
//! number, a search by a piece of a part number, and what the catalog holds.
//!
//! Run it from anywhere in the repository:
//!
//! ```text
//! cargo run --example parts_catalog -p hpr-io
//! ```
//!
//! The guide's page *OpenRocket `.orc` parts catalogs* (`docs/format/orc.md`) quotes it and what
//! it prints, which is kept next to it in `parts_catalog.output.txt`; CI checks that the two still
//! agree (`cargo xtask examples --check`).
#![allow(
clippy::print_stdout,
reason = "the project's lints forbid printing in library code, and this program exists to print"
)]
use hpr_io::orc::{PartKind, bundled};
fn main() {
let catalog = bundled();
println!(
"{} parts from {} makers",
catalog.parts.len(),
catalog.manufacturers().len()
);
// One part, by maker and its whole part number. Its sizes are in meters; print millimeters.
for part in catalog.find("LOC Precision", "PNC-3.00") {
println!(
"\n{} {}: {}",
part.manufacturer, part.part_number, part.description
);
if let PartKind::NoseCone(nose) = &part.kind {
println!(
" {:?}, {:.1} mm long, {:.1} mm across",
nose.shape,
nose.length_m * 1e3,
nose.outer_diameter_m * 1e3,
);
// A nose is hollow with a wall, or filled. The file may give `Filled`, a wall or both;
// the ten built-in parts that give both say `Filled` is false.
match (nose.filled, nose.thickness_m) {
(Some(true), _) => println!(" filled"),
(_, Some(wall_m)) => println!(" wall {:.2} mm", wall_m * 1e3),
_ => println!(" wall not given"),
}
println!(
" shoulder {:.1} mm long, {:.1} mm across",
nose.shoulder_length_m * 1e3,
nose.shoulder_diameter_m * 1e3,
);
match nose.material.density {
Some(density) => println!(" {}, {density} kg/m³", nose.material.name),
None => println!(" {}, density not given", nose.material.name),
}
}
}
// A parachute that states its own mass.
for part in catalog.find("Giant Leap", "TAC-24") {
println!(
"\n{} {}: {}",
part.manufacturer, part.part_number, part.description
);
if let PartKind::Parachute(chute) = &part.kind {
println!(
" {:.0} mm across, {} sides, {} lines of {:.0} mm",
chute.diameter_m * 1e3,
chute.sides,
chute.line_count,
chute.line_length_m * 1e3,
);
}
if let Some(mass_kg) = part.mass_kg {
println!(" stated mass {:.1} g", mass_kg * 1e3);
}
}
// Estes writes two numbers in one, `BT-20, 30316`: a search finds it by either.
println!("\nEstes parts numbered with 30316:");
for part in catalog.search(Some("Estes"), "30316") {
println!(" {}: {}", part.part_number, part.description);
}
}
It prints:
3449 parts from 16 makers
LOC Precision PNC-3.00: Nose cone, polypropylene, PNC-3.00, ogive, 12.5"
Ogive, 285.8 mm long, 78.7 mm across
wall 1.96 mm
shoulder 95.2 mm long, 75.9 mm across
Polypropylene, bulk, 946 kg/m³
Giant Leap TAC-24: Parachute, TAC-1 type, 24 in., 4 shrouds
610 mm across, 4 sides, 4 lines of 914 mm
stated mass 153.1 g
Estes parts numbered with 30316:
BT-20, 30316: Body tube, BT-20, 18 in.
Every size comes back in meters, every mass in kilograms, and every density in kg/m³ (solids),
kg/m² (fabric) or kg/m (cord), all SI units. A file of your own reads
with hpr_io::orc::read(text, name): text is the file’s contents, and name is the file’s name,
which each part keeps so you can tell where it came from. The program does the reading from disk;
the reader itself does no I/O.
Two things to know when looking parts up:
- A part number is matched whole. Many makers write several numbers in one. Estes’ BT-20 is
BT-20, 30316, sofind("Estes", "BT-20")finds nothing;searchfinds it by either piece. The maker matches in any case. - A few numbers name two parts. In the built-in files, 21 part numbers name two parts of the
same kind; 3 of those pairs are identical.
findreturns all of them, in file order.
What a file holds
A .orc file is XML with three parts:
<Version>:0.1in 15 of the built-in files,1.0in one.<Materials>: each material’s name, its density, the density’s units, and its kind:BULK(per volume, for solid parts),SURFACE(per area, for canopy and streamer fabric) orLINE(per length, for shroud lines).<Components>: the parts. Each names its maker, part number and description, and its material by name and kind. That material must be defined in the same file.
Each size carries its own Unit attribute, such as <Length Unit="in">10</Length>. There is no
published schema. The fields and units below are the ones the database project documents in its
docs/TechnicalInfo.md, and their meaning is what OpenRocket does with them.
| part | sizes it gives | in the built-in files |
|---|---|---|
BodyTube, TubeCoupler, EngineBlock, CenteringRing, LaunchLug | inside and outside diameter, length | 1,089, 237, 38, 499 and 59 |
BulkHead | outside diameter, length | 115 |
NoseCone | shape, length, base diameter, shoulder diameter and length, filled or a wall thickness | 855 |
Transition | shape, length, each end’s diameter, shoulder diameter and shoulder length, filled or a wall thickness | 360 |
Parachute | diameter, number of sides, number and length of shroud lines, the lines’ material | 151 |
Streamer | length, width, thickness | 46 |
Any part may also state its Mass; 229 of the built-in parts do: 207 solid parts, and 22
parachutes and streamers. Unlike a solid part’s (see
Where hpr and OpenRocket differ), a parachute’s or streamer’s
stated mass leaves its fabric’s density as written, in hpr’s reading and in OpenRocket’s. When
the part goes into a rocket, OpenRocket gives a parachute its stated mass as an override and
ignores a streamer’s; the builder scales either one’s density to give it.
A centering ring’s length is its thickness. A coupler with an inside diameter of zero is a solid nose block.
The units read are the ones OpenRocket 24.12 reads:
| quantity | units |
|---|---|
| length | m, cm, mm, in, ft |
| mass | kg, g, oz, lb |
| bulk density | kg/m3, g/cm3, kg/dm3, lb/ft3 |
| surface density | kg/m2, g/m2, g/cm2, oz/in2, oz/ft2, lb/ft2 |
| line density | kg/m, g/m, g/cm, oz/ft |
A value with no unit is in SI, as OpenRocket reads it. Each unit is its exact definition: the inch is 0.0254 m, the foot 0.3048 m, the pound 0.45359237 kg and the ounce a sixteenth of that, 0.028349523125 kg (NIST Handbook 44, Appendix C).
What a file leaves unsaid
- A shape’s parameter. A nose cone’s or transition’s shape is one of
CONICAL,OGIVE,ELLIPSOID,PARABOLIC,HAACKandPOWER(OGIVEis a tangent ogive). The last four have a parameter (an ogive’s radius, a Haack series’ C), but no.orcfield can give it. OpenRocket uses its own default when the part goes into a design. - A shoulder’s wall. A shoulder has a diameter and a length but no wall thickness, and no end cap. For a hollow part, OpenRocket gives it no wall, so it weighs nothing, as the database’s own usage notes say and the builder’s test confirms from OpenRocket’s own output. The builder gives the shoulder the part’s wall instead (What the catalog leaves unsaid). For a filled part, OpenRocket weighs the shoulder as a solid cylinder: the check of stated masses below uses that volume, and it agrees.
- A parachute’s drag. No field gives a drag coefficient.
- A filled part’s walls.
Filledsays a nose cone or transition is solid. Where it is absent, aThicknessgives the wall instead. Eight nose cones and two transitions give both, and all ten sayFilledis false, so they agree: a hollow part with that wall. A file that said filled with a wall would leave which one counts open; hpr keeps both, as OpenRocket’s reading does.
Where hpr and OpenRocket differ
The test crates/hpr-io/tests/orc_openrocket.rs reads every built-in part and compares it with
OpenRocket 24.12’s reading, value by value. The two agree everywhere except in these places. Each
is counted, and each count is checked:
| what | parts | hpr | OpenRocket |
|---|---|---|---|
| a mass stated in ounces | 185 | the exact ounce, 0.028349523125 kg | 0.0283495231 kg, so 8.8 parts in 10 billion lighter |
| two makers’ names | 252 | as the file writes them | “LOC/Precision” and “Public Missiles, Ltd.” for “LOC Precision” and “Public Missiles” |
| a solid part that states its mass | 207 | the material keeps the file’s density; the part keeps its stated mass | replaces the density with the one that gives the part that mass |
| a material the file names but doesn’t define | 3 | no density | a density of zero |
The third row is the one that matters for a rocket’s weight. For example, an Estes balsa nose cone that states its mass gets, in OpenRocket, a balsa density that makes the cone weigh exactly that. hpr keeps both numbers.
The test shows that a stated mass is the cause on all 207. The oracle also reads each file with
every <Mass> taken out, and then OpenRocket’s density equals hpr’s on every part, these 207
included. On the 54 of them that are simple solids (7 body tubes, 4 bulkheads, and 43 filled
conical parts: 34 nose cones and 9 transitions), the test also checks that OpenRocket’s replaced density times the
part’s volume gives the stated mass, to 1 part in 10¹⁵.
The builder (Parts from a catalog) makes a part that
states its mass weigh that mass by scaling its density, as OpenRocket does for rigid parts.
OpenRocket also rounds a few other imperial factors: pounds per cubic foot, ounces per square inch or foot, pounds per square foot, and ounces per foot. No built-in file uses them.
Warnings, not failures
Only three things make hpr refuse a whole file: text that isn’t XML, a top element that isn’t
<OpenRocketComponent>, and elements nested more than 16 deep, which no catalog needs and which
is refused before the XML is read. Anything else that can’t be read is left out with a warning,
and reading goes on:
- A part with a missing, unreadable or negative size, a unit or shape the format doesn’t have,
a material of the wrong kind, a value with an element inside it, or an element that isn’t a kind
of part. OpenRocket 24.12 refuses the whole file for three of these: a missing size, an unknown
unit and an unknown shape. It reads an unreadable size as zero, a material of the wrong kind
with a density of zero, and a part number written
BT<b>-</b>20as20, and it skips an element that isn’t a part. What it does with a negative size is not probed. - A material with an unknown unit or kind, no density, a density below zero or too large for a 64-bit number, or an element inside its name. Parts that name it read with no density.
- A field a part’s kind doesn’t have is ignored, with a warning. The 37 built-in nose cones that state an inside diameter read this way.
- A field stated twice keeps the last, as OpenRocket does. Three built-in parts state two descriptions.
- A material defined twice with two densities: parts take the first, as in OpenRocket.
- A list stated twice: the last
<Materials>and the last<Components>are read, as OpenRocket reads them. Anything else beside the lists is ignored. - A value that is read as written but looks wrong:
- a tube whose inside diameter is not less than its outside diameter;
- a solid lighter than air (under 1 kg/m³);
- a fabric lighter than 1 g/m². The lightest correctly labelled fabric in the built-in files, a
polyethylene film, is 7.05 g/m², and their ripstop nylons are tens of g/m², so this is almost
certainly a kg/m² value labelled
g/m2, 1,000 times too light.
A file with more than 1,000 warnings lists the first 1,000 and then says how many more there were.
in/64 (sixty-fourths of an inch), which the project’s notes list as a length unit, is refused,
because OpenRocket reads it as whole inches: 3 in in/64 would come out as 3 inches.
On the built-in files there are 55 warnings, all of kinds listed above:
| warning | count |
|---|---|
| a nose cone’s inside diameter, ignored | 37 |
| a description stated twice | 3 |
a material not defined (Carpet Thread twice in mpc.orc, and a balsa in semroc.orc) | 3 |
| a tube-like part no narrower inside than out (one Quest, two SEMROC) | 3 |
a solid lighter than air (Paper, bulk at 0.0011 kg/m³ in BMS.ORC and ROCKETARIUM.ORC, an elastic in generic_materials.orc) | 3 |
a fabric under 1 g/m² (five in generic_materials.orc, one in giantleaprocketry.orc) | 6 |
Nothing is changed on account of them: each such part reads as written, as OpenRocket reads it.
The 18 parts made of that paper (centering rings and engine blocks) would weigh about a millionth
of a real one: paper is roughly 1,000 kg/m³. The six Giant Leap parachutes on the light fabric all state their mass, so use
that. bundled() drops these warnings; read each of BUNDLED_FILES to see them.
Checked against OpenRocket
validation/oracles/openrocket/orc_presets.py runs OpenRocket 24.12’s preset loader, its public
API (run, never read: its source is GPL), on each of the 16 files. It first checks that the jar’s
copy of each file is byte for byte the one built into hpr. It records every value of every part to
crates/hpr-io/tests/fixtures/orc/openrocket-presets.json,
one part to a line, and each part’s densities as OpenRocket reads the file with its masses taken
out.
It also has OpenRocket read 37 small .orc probe files, each asking
one question: every unit above, values with no units, a part or a file OpenRocket can’t read, and
what it does with a material or a list stated twice. The probe test in the same file reads each
probe:
| probes | result |
|---|---|
| 23 | read as OpenRocket reads them, to the bit |
| 6 | in a unit whose factor OpenRocket rounds: within 2 parts in 10⁹, by hpr’s exact factor |
| 4 | OpenRocket refuses the file; hpr leaves the part out (for oz/in, ounces per inch, a cord density neither reads, hpr leaves the material out and reads the part without the cord’s density) |
| 4 | OpenRocket reads the part (in/64 as inches, a length of ten as zero, a material of the wrong kind with a density of zero, BT<b>-</b>20 as 20); hpr leaves it out |
As a check that the tests can fail, moving hpr’s inch to the next number a 64-bit float can hold above 0.0254 was tried by hand: both the catalog comparison and the probe test failed.
Sources and license
- The files:
openrocket/openrocket-database, https://github.com/openrocket/openrocket-database, at commit1512874a(2025-07-27), pinned asopenrocket-databasein the reference lock file. Apache License 2.0. Created by Dave Cook and maintained by the OpenRocket team. They are built into hpr unchanged, incrates/hpr-io/data/openrocket-database/with the project’sLICENSE. - The format: the project’s
docs/TechnicalInfo.mdanddocs/Usage.mdat that commit. - The units: NIST Handbook 44 (2024), Appendix C, General Tables of Units of Measurement.
RASP .eng motor files
A .eng file is a motor’s thrust curve in plain text: a one-line
header with the motor’s name, size, delays and masses, then one time and thrust per line. The
format is named after RASP, the rocket simulation program it comes from. It is the more common of
the two formats ThrustCurve.org serves (the other is
RockSim .rse).
To fly a motor from a .eng file, see
A motor from a file on the Solid motors page. It reads a
file, builds the motor and puts it in a rocket, with a program that CI runs.
This page is the reference for hpr’s reader and writer: what the published spec says, what real files do, and what hpr does with each. All 889 RASP files ThrustCurve.org held on 2026-09-17 read, and write back with every value unchanged (Checked against real files).
Code: hpr_motor::eng (API reference), written for the
solid-motor milestone (M1.3). The rules below are from the spec unless marked
Observed (seen in real files) or Policy (hpr’s own choice).
Sources
- [R] ThrustCurve.org, “RASP File Format”, https://www.thrustcurve.org/info/raspformat.html,
captured 2026-09-17 and pinned as
thrustcurve-rasp-format(sha25669573e9f…) in the reference lock file, which records each source’s address and checksum. Sections are cited as [R Header], [R Data], [R Problems]. The page names the RASP C source as “the ultimate authority”; its license is not stated, so it was not consulted. - Observed: 734 entries in the ThrustCurve manufacturer file sets (12
.engsets) and 13 single-motor API downloads, fetched 2026-09-17 into a local cache,refs/samples/formats/, that is never committed. Counts below are over those entries.
Grammar
Read := as “is made of”, * as “any number of”, + as “one or more”, and | as “or”.
file := entry+
entry := (blank | comment)* header point+ (blank | comment)*
comment := ';' text whole line; [R Header], [R Problems]
header := name dia len delays prop total mfg seven fields "separated by spaces" [R Header]
point := time thrust "usually preceded by a few spaces" [R Data]
- Blank and
;lines are ignored before the header. After the last point an entry “may end or contain comments and blank lines, but nothing else” [R Header], [R Data]. - “All seven must be present for the entry to be read successfully” [R Header].
- Points start “immediately after the header line” [R Data].
- A file may hold several entries; “make sure that each entry is separated by at least one comment
line”. A lone
;after the data is the customary separator [R Data], [R Problems].
Header fields [R Header]
| # | Field | Unit | Meaning |
|---|---|---|---|
| 1 | name | n/a | Common name: “just the impulse class and average thrust” (F32) |
| 2 | diameter | mm | Casing diameter |
| 3 | length | mm | Casing length |
| 4 | delays | s | Available delays “separated by dashes”; 0 = ejection charge, no delay; P = plugged, no ejection charge |
| 5 | propellant mass | kg | “Weight of all consumables” (the propellant, for a solid) |
| 6 | total mass | kg | Motor “loaded and ready for flight” |
| 7 | manufacturer | n/a | Abbreviation, per the combined motor list of the NAR (National Association of Rocketry) |
Thrust curve [R Data], [R Problems]
- Each point is a time (s) and a thrust (N), as “floating-point numbers”.
- An implicit first point at (0, 0) “is assumed and should not be specified explicitly”. An explicit (0, 0) is called “a common mistake”.
- “The final point must have a thrust of zero and it indicates the motor’s burn time.” A zero thrust anywhere else is rejected by ThrustCurve. (ThrustCurve’s metadata uses the burn time of the NFPA 1125 5% rule instead; see Loft lesson L39, where Loft took the last point as the burn time.)
- Points “must be in order of time”. ThrustCurve rejects a point “before the previous point” and a first point at negative time. Equal times are not addressed.
- RASP allowed at most 32 points, including the final zero; modern tools don’t enforce this.
Where the spec is silent
Number syntax (exponents, sign; its own example uses .0377). Whether tabs or several spaces
separate fields. Line endings, text encoding, and a BOM (byte-order mark: an invisible character
some editors put at the start of a file). Delay lists that mix numbers with P, or use other
separators. Equal consecutive times. Inline comments. An entry with no comment separator before the
next header.
Observed in real files
- Whitespace: tabs separate data fields in 4 of 12 sets. 52 headers use several spaces or
column alignment; one header is indented. Trailing spaces are common. 8 of 25 files use CRLF
(Windows line endings, a carriage return then a line feed; other systems use LF alone);
13 of 13 single downloads lack a final newline. There is no BOM, no non-ASCII byte, no inline
;, no blank or comment line inside the data, and no indented comment. - Entries: always separated by a comment; there are 470 lone
;lines. A reader that stops at the first header loses the rest of the file (Loft lesson L36). - Header: never more than 7 fields; spaces in a manufacturer name become
_(Contrail_Rockets). Manufacturer spellings vary (AT,A,Aerotech,AERO,AT-RMS,AT/RCS;CTI,Ces,CSR,Pro38). The name is often the full designation, not class plus thrust (1266-J760-WT-19A,I216-CL(I),O25,000-VM-P,1/2A3T,C6-0). - Numbers: leading dot (
.0377, 21 headers), trailing dot (2415.), leading zeros (068.8604), padded decimals (0.000), binary float noise (0.060700000000000004, 72 headers), up to 15 significant digits. Diameters and lengths can be fractional (114.3). No exponents or signs were seen. - Delays: 574 dash lists, 143
P, 4 comma lists (5,8,11), 8 lists ending inP(6-10-14-P), 1 lowercasep, and 4 malformed (4-7-10,,-,1-3--4-6-7-9-10). 58 lists are descending (14-12-10-8-6). 27 contain100or1000. Checked against ThrustCurve search metadata,100/1000mean plugged in 14 of 14 cases, and0means plugged in 120 of 149, against the spec’s “no delay” (Loft lesson L37: Loft read100and1000as seconds). - Curve: 32 entries have an explicit first point at t = 0 with nonzero thrust. Loft’s bundle also had an explicit (0, 0). 4 entries don’t end at zero thrust. 4 have equal consecutive times (a vertical drop to zero, or rounded times). 69 have more than 32 points. None have decreasing time, negative thrust or an interior zero.
Reader policy (lenient, with diagnostics)
- The reader takes text and strips a UTF-8 BOM. Decoding bytes into text is the caller’s job:
today, read the file into a string yourself, as with
std::fs::read_to_string, which expects UTF-8. That job, and any fallback to Windows-1252 (an older Windows character set), is planned forhpr-io, the crate for other programs’ file formats, which doesn’t do it yet. Lines split on LF, CRLF or a bare CR and are trimmed. - A line whose first non-blank character is
;is a comment. Fields split on runs of spaces and tabs. - Each entry is read in stages (a state machine). Before the header, skip blanks and collect comments. The header is the first other line. In the data, a 2-field numeric line is a point; skip blank lines; a comment ends the data, and an entry that ends with no points is an error. A line with 7 or more fields starts a new entry, with a warning about the missing separator. Anything else is an error, with its line number.
- The header needs 7 or more fields. With more than 7, join fields 7 onward with single spaces
as the manufacturer, and warn. Dia and len must be finite and > 0, and the masses finite and
≥ 0; warn if prop ≥ total. Keep file units (mm, kg) in the file model, in fields named for them
(
diameter_mm). Convert to SI only when building the physical motor: mm → m → mm is not bit-exact (502.1 / 1000 * 1000 != 502.1; with* 1e-3and* 1e3, 30 of 290 distinct header lengths fail). - Numbers: Rust
str::parse::<f64>accepts.5,5.,068.8,+1and1e3, but alsoinf,infinityandNaN. Reject every non-finite value. - Points are kept verbatim; no implicit origin is inserted into the file model. Errors: no points, negative time, time decreasing. Warnings, on the header’s line: last thrust ≠ 0, negative thrust, and delay pieces the delay reader drops or flags. Equal consecutive times are accepted (a step in the physical curve). The RASP 32-point limit is not enforced.
- Delays: keep the raw token verbatim. The derived view splits on
-or,, drops empty pieces (with a warning), mapsP/p→ plugged and100/1000→ plugged, reads0as its own “zero or plugged” setting with a warning (spec: no delay; files: usually plugged), and reads other pieces as seconds. A0never becomes an ejection at burnout without a decision. Physics should prefer catalog metadata for delays. - An entry with an error is skipped, with the error as a warning, when other entries in the file read; the reader resumes at the next comment line. With no entry read, the first error returns. Every warning carries a kind: skipped (an entry lost), dropped (a value ignored) or unusual (read as it stands).
- Comments: store the text after
;verbatim, but drop comments that are empty after trimming. Comments between two entries belong to the next entry; comments after the last entry are file trailer comments. Policy: a comment that names hpr as the program that wrote the file (hpr-sim, a version, thenFS · SW · TOOL 005; see the writer policy) is dropped when reading, because it says who wrote the file, not anything about the motor. Like any comment, it still ends the entry before it.
Conversion to a physical curve (EngEntry::thrust_curve, not part of the file model): prepend
(0, 0) when the first time is > 0. A leading (0, F) stays a step at ignition. Negative thrust is
an error there.
Writer policy (strict, round-trip stable)
Round-trip stable: a file hpr writes reads back to exactly the values it was written from.
- Policy: the first line is a comment naming the program that wrote the file, its version and
its designation in the FusionSpace product system, as every file hpr writes does:
; hpr-sim 0.1.0 · FS · SW · TOOL 005. The·is a middle dot, the file’s only non-ASCII character unless its comments hold others; the file is UTF-8. A comment carried over from the source that is itself such a line, from any version, is left out, so a file converted again holds one such line, not two. Whether other programs (RockSim, OpenRocket, a flight computer’s tools) accept the middle dot in a comment has not been checked. - Each entry: its comments as
;text, then the header with single spaces, then onet Fline per point, then a lone;. Finally the trailer comments. LF endings, a final newline, UTF-8. - The name must be one token that doesn’t start with
;(the line would read as a comment), and the delays one token. The manufacturer may hold spaces if its words are separated by single spaces, since the reader joins extra fields that way. Comment text must be non-empty, on one line, and without trailing whitespace (the reader would trim it). Otherwise return an error, never a silent substitution. - Numbers use Rust
{}(Display). It prints the shortest digits that parse back to the same bits, never uses an exponent (older readers may not accept one), and writes-0.0as-0. - Points are written as stored: no origin is added or removed. The writer rejects negative or decreasing times, the same rules as the reader.
- Invariant (test it): whenever
write(parse(x))succeeds,parse(write(parse(x))) == parse(x), with f64 compared by bits. Dropping empty comments is what keeps the writer’s;separator stable. A lenient read the writer cannot represent (a manufacturer containing spaces) is a write error, not a changed value. - Leading and trailing whitespace inside comment text: the reader trims only trailing whitespace,
and the writer never adds any, so
; textround-trips astext. - Converting to and from
.rse(hpr_motor::convert,hpr convert, since M4.2c, the command-line conversion) is described with that format’s writer policy. From.rse, a code or maker of several words joins them with_, as no real file has more than seven header fields and OpenRocket 24.12 refuses eight (motor_files.pychecks it), andhpr convertjoins a maker read from a.engfile the same way. A leading(0, 0)point is dropped when the next is after ignition, and the comment text becomes one comment per non-blank line.
Checked against real files
On 2026-09-17, all 889 RASP files in ThrustCurve.org’s solid-motor survey
(data notes) and
the 721 entries of its manufacturer sets read, and
write-parse-write reproduces every value bit for bit. The files are cached under refs/samples/
and never committed; the committed test covers the bundled curves.
RockSim .rse motor files
A .rse file is a motor database in XML, a text format of nested, named elements, from the RockSim
flight simulator. It holds one or more motors (“engines”). Each has its size, masses and delays
as attributes, and a table of thrust, mass and center of gravity
(CG) over time. ThrustCurve.org serves it beside
RASP .eng.
To fly a motor from a .rse file, see
A motor from a file on the Solid motors page. It reads a
.eng file, and says what changes for a .rse one.
This page is the reference for hpr’s reader and writer. The spec is thin and disagrees with every real file on element and attribute names, so a reader must follow the observed structure. Of the 823 RockSim files ThrustCurve.org held on 2026-09-17, 821 read and write back with every value unchanged; the other two have a time that goes backwards (Checked against real files).
Code: hpr_motor::rse (API reference), written for the
solid-motor milestone (M1.3). The rules below are from the spec unless marked
Observed (seen in real files) or Policy (hpr’s own choice).
Sources
- [P] “RockSim & EngEdit – Engine File (.rse) Format Guide”, 3 pp., PDF hosted by ThrustCurve.org,
pinned as
rocksim-rse-spec(sha256c47a04f4…) in the reference lock file, which records each source’s address and checksum, and fetched torefs/papers/rocksim-rse-spec.pdf, a local folder that is never committed. Cited as [P p.N]. - [S] ThrustCurve.org, “Flight Simulators”, RockSim section,
https://www.thrustcurve.org/info/simulators.html, captured 2026-09-17 and pinned as
thrustcurve-simulators(sha2565bb2bad7…). - [X] W3C, XML 1.0 5th ed., https://www.w3.org/TR/xml/, §2.11 (end of line), §3.3.3 (attributes).
- Observed: 715 engines and 15,149 points in the ThrustCurve manufacturer file sets (9
.rsesets) and 10 single-motor API downloads, fetched 2026-09-17 into a local cache,refs/samples/formats/, that is never committed.
Structure
Spec: the minimal example is a bare <engine …> holding <data> with <point t="…" f="…"/>
children [P p.2]. Observed in 715 of 715 engines:
<engine-database>
<engine-list>
<engine ATTRIBUTES> repeated; one list holds every engine in the file
<comments>free text</comments> optional (415 of 715), before <data>
<data>
<eng-data t="…" f="…" m="…" cg="…"/> one per sample, in file order
</data>
</engine>
</engine-list>
</engine-database> no XML declaration, DTD or namespace
<engine> attributes
“Req” is the spec’s Required column [P p.1]. Units are from [P p.1, p.3] unless noted.
| Attribute | Req | Unit | Meaning |
|---|---|---|---|
mfg | yes | n/a | Manufacturer name |
code | yes | n/a | “Unique engine identifier”, the “primary lookup key” |
Type | yes | n/a | Engine type [P p.1 spells it type; files always use Type]. Seen: reloadable, single-use, hybrid [S], unspecified, Single-Use |
dia | yes | mm | Diameter |
len | yes | mm | Length |
initWt | yes | g (spec: “kg or g”) | Initial (loaded) motor mass |
propWt | yes | g (spec: “kg or g”) | Propellant mass |
delays | yes | s | “Comma-separated delay values” |
Itot | yes | N·s | Total impulse |
avgThrust | yes | N | Average thrust |
peakThrust | yes | N | Peak thrust |
burn-time | yes | s | Burn duration |
auto-calc-mass | no | flag | 1: RockSim computes mass as m(t) = initMass − (propMass / burnTime)·t [P p.2] |
auto-calc-cg | no | flag | 1: “CG is fixed at engine center” [P p.2] |
massFrac | n/a | % | Not in spec. Observed: 100·m₀/initWt (703 of 710), where m₀ is the first point’s m |
Isp | n/a | s | Specific impulse. Not in spec. Observed: Itot / (m₀[kg]·9.80665), within ±0.006 in 696 of 710 |
throatDia, exitDia | n/a | mm? | Nozzle throat and exit diameters. Not in spec. Always 0. |
tDiv tStep tFix FDiv FStep FFix mDiv mStep mFix cgDiv cgStep cgFix | no | n/a | “Rendering attributes … control how graphs are drawn” [P p.2]. Always Div 10, Step -1., Fix 1 |
The spec contradicts itself: its summary names initMass and propMass [P p.2], while its
table and example use initWt and propWt [P p.1–2]. Only initWt/propWt occur in real files.
Mass unit (verified). Of 557 .rse engines whose code also appears in the .eng sets,
497 have initWt within 1% of 1000 × the .eng total mass. None match the kg value; the other 60
are different data (another diameter or mass). massFrac and Isp are consistent with grams.
The smallest initWt is a 1.2 g micro motor.
<eng-data> point attributes
| Attr | Req | Unit | Meaning |
|---|---|---|---|
t | yes | s | Time since ignition [P p.1] |
f | yes | N | Thrust [P p.1] |
m | no | g | “Mass … over time”, needed if auto-calc is off [P p.1]. Observed: propellant mass remaining. m₀ = propWt in 696 of 715; last m = 0 in 715 of 715. In 698 of the 700 set engines with m₀ > 0, m(t) = m₀·(1 − I(t)/I_total), with I the trapezoidal impulse, to 1e-3 relative (median 1.3e-6). That is not the spec’s linear auto-calc formula. |
cg | no | mm | Motor CG over time [P p.1], [S]. Observed: constant = len/2 in 685 of 715. The datum (forward or aft end) is stated nowhere: unverified. |
With every observed file setting both auto-calc flags to 1, RockSim may ignore m and cg
[P p.2]. Policy: hpr-sim preserves them for round trips but derives mass and CG itself.
Thrust curve
- The spec example lists an explicit
t="0.0" f="0.0"first point and ends withf="0.0"atburn-time[P p.2]. Unlike.eng, the origin is written out. The spec is silent on ordering, duplicates and a nonzero last point. - Observed: the first point is (0, 0) in 715 of 715. The last
fis 0 in 713; 2 put a nonzero point after the zero at the samet. 70 engines have equal consecutivet, and 1 hastdecreasing.burn-timediffers from the lasttin 281 (rounded).Itotmatches the trapezoidal integral within 0.1% in 712, andavgThrust=Itot/(lastt) within 0.1% in 712.peakThrust≠ maxfin 9.
Observed in real files (other)
- Delays: missing in 5. Always comma integers; never
P. Descending lists are common (15,12,10,8,6). Checked against ThrustCurve search metadata,1000means plugged in 126 of 128 single-token cases and0in 24 of 25. Lists can end in1000(10,14,1000). - Strings:
mfghas 20+ spellings, including leading or trailing spaces (Aerotech), commas (Estes Industries, Inc.) and underscores.codecan hold spaces (Micro Maxx II), commas and parentheses. - Numbers: 6 significant digits, like C
%g, with a trailing.on integral values (35.,0.,-0.). One exponent (1.51982e-05); some padded (0.00). - XML text: start tags wrap over several lines; two spaces after the element name. Attribute
order is either RockSim’s (490) or alphabetical (220); XML gives order no meaning. 6 of 19 files
use CRLF, and many lack a final newline.
<comments>spans lines in 197 engines; 2 use CDATA, one holding a raw&. There are no entity references, and everything is ASCII. - Bad values:
propWt10×m₀(a typo), andcg0.1 × len/2. Five engines (four hybrids) have everymandcg= 0, so m₀ = 0 cannot mean “no propellant”.
Reader policy (lenient, with diagnostics)
- Use a conforming XML parser (
roxmltree): it handles CDATA (raw-text sections), entities (escapes such as&) and end-of-line normalization [X §2.11]. Refuse DTDs and external entities, declarations that can make a parser fetch other files (core crates do no I/O). The reader takes text and strips a BOM. Decoding bytes is the caller’s job, as for.engfiles: planned forhpr-io, which doesn’t do it yet. - Take every
engineelement at any depth, so a bare<engine>root [P p.2] also works, but not one nested inside another engine. An engine with an error is skipped, with the error as a warning, when others in the file read. With repeated<data>or<comments>, the last is read, with a warning.<comments>keeps only text: XML comments and processing instructions inside are not part of it, and nested markup contributes its text. Every warning carries a kind: skipped (an engine lost), dropped (a value ignored) or unusual (read as it stands). Points areeng-datachildren ofdata; also acceptpoint[P p.2]. - Match attribute names exactly, then ASCII case-insensitively (
Type/type); acceptinitMass/propMassas aliases [P p.2]. Ignore the rendering attributes silently; warn on and ignore other unknown attributes and elements. - Required:
code,dia,len,initWt,propWt, and at least 2 points withtandf. A missingmfgreads as empty, with a warning. Everything else isOption; a missingdelaysmeans no delay information. If only some points carrym(orcg), treat it as absent for all of them, and warn.auto-calc-*flags read1or0; anything else is ignored with a warning. - Numbers: trim XML whitespace, then parse with Rust’s f64 parser. Reject non-finite values:
Rust accepts
inf,infinityandNaN. Warn ifpropWt≥initWt. Keep file units (g, mm) in the file model, in fields named for them (initial_mass_g). Convert to SI only when building the physical motor: g → kg → g is not bit-exact (4030.comes back as4030.0000000000005; 14 of 1330 distinct mass and length values in the sets fail). - Keep strings verbatim, including the spaces in
mfg; normalize names in the catalog layer. Delays: keep the raw string; interpret it as in.engfiles (1000/100→ plugged,0ambiguous). - Point checks match
.eng: error on negative or decreasingt; warn on a lastf≠ 0, on negative thrust, and on anItotorpeakThrustthat disagrees with the curve by more than 1%.burn-timeis not checked: files round it from the last time.
Writer policy (strict, round-trip stable)
Round-trip stable: a file hpr writes reads back to exactly the values it was written from.
-
Emit
<engine-database>,<engine-list>and one<engine>per motor. Use 2-space indent, LF endings, a final newline, UTF-8, and no XML declaration (none was observed; whether RockSim accepts one is unverified). -
Policy: the first line is an XML comment naming the program that wrote the file, its version and its designation in the FusionSpace product system, as every file hpr writes does:
<!-- hpr-sim 0.1.0 · FS · SW · TOOL 005 -->. XML readers skip comments, hpr’s too, so the file reads back to the same values and writing it again gives the same bytes. Whether RockSim and OpenRocket accept a comment before<engine-database>has not been checked. -
Write attributes in RockSim’s order:
mfg code Type dia len initWt propWt delays auto-calc-mass auto-calc-cg avgThrust peakThrust throatDia exitDia Itot burn-time massFrac Isp tDiv tStep tFix FDiv FStep FFix mDiv mStep mFix cgDiv cgStep cgFix. Write only the ones present in the model. Points are written ast f m cg, verbatim, with no origin added or removed. -
Numbers use Rust
{}(Display): the shortest digits that round-trip exactly, no exponent, and-0for negative zero. -
Escape attribute values as
& < > ", and tab, LF and CR as	 . A literal one would read back as a space [X §3.3.3]. Escape<comments>text as& < >with CR as , without CDATA, and write it verbatim (no trimming). -
Flags are written
1or0. The rendering attributes are not kept, so they are not written. -
Apart from that comment, the writer adds nothing by itself. Converting from
.engis a separate step,hpr_motor::convertorhpr convert(since M4.2c, the command-line conversion), and it fills what.englacks the way the observed files do. The counts are out of the files that give each attribute: 715 engines, of which 710 givemassFracandIsp, and 700 have a propellant mass above zero form’s shape:attribute filled as observed in a (0, 0)first pointadded when the curve’s first point is after ignition 715 of 715 Itotthe trapezoidal integral of the curve within 0.1% in 712 peakThrustthe largest fall but 9 burn-timethe last tthe last t, or in 281 of 715 a rounding of itavgThrustItot/burn-timewithin 0.1% in 712 mm₀ (1 − I(t)/ Itot), m₀ =propWtm₀ = propWtin 696 of 715; the shape in 698 of 700cglen/2685 of 715 auto-calc-mass,auto-calc-cg1every file massFrac100 m₀/ initWt703 of 710 IspItot/(m₀ in kg × 9.80665)696 of 710 Typeunspecified, as the guide requires one [P p.1]one of the values seen Delays trade
-for,andPfor1000. Masses move from kg to g by moving the decimal point in their shortest digits, not by multiplying. Converting to.engdrops the attributes above, plusthroatDiaandexitDia, each named in a warning. It refuses a hybrid, and a motor with no delays hpr can read until--delaysgives them. OpenRocket 24.12 opens every file converted from the bundled curves (motor_files.py); whether RockSim does is unverified. -
Invariant (test it): whenever
write(parse(x))succeeds,parse(write(parse(x))) == parse(x), with f64 compared by bits and strings compared exactly.
Checked against real files
On 2026-09-17, 821 of the 823 RockSim files in ThrustCurve.org’s solid-motor survey read, and
write-parse-write reproduces every value bit for bit. The other two have a time that goes
backwards and are rejected with its line. In the manufacturer sets, 704 engines read; one engine
with a backwards time is skipped with a warning. The files are cached under refs/samples/ and
never committed.
ERA5 weather files
ERA5 is a record of the weather over the whole Earth, every hour since 1940, from the European Centre for Medium-Range Weather Forecasts (ECMWF). It is a reanalysis: a weather model run over the past and held to the observations of the time. hpr reads ERA5’s pressure-level files to fly a rocket in the weather of a real day: the temperature, pressure and wind over the launch site, from the ground up to a few kilometers or more.
How far to trust it. hpr reads these files the way RocketPy does, to 12 digits, on two real launch days (below). What it does with the numbers differs from RocketPy in four places. Two are measured here: the heights of the levels, and a launch between two of the file’s hours. Two are not measured yet: the pressure between levels, and the air above the file’s top level. Seven real flights have been flown in ERA5 weather and compared with their logs (real flights, milestone M2.3b): their apogees miss by 6.04% on average, outside the 5% target. Humidity is not read yet, so the air is taken as dry.
Code: hpr_io::era5 and hpr_io::netcdf
(API reference), written for the ERA5-weather milestone
(M2.3a). The choices are in
ADR-081: ERA5 weather read from netCDF classic.
Getting a file
ERA5 comes from the Copernicus Climate Data Store. An account is free. The data’s license is CC BY 4.0: you may use and share it if you credit it (“Contains modified Copernicus Climate Change Service information”), and you accept it once on the dataset’s page before the first download.
- Open the dataset ERA5 hourly data on pressure levels from 1940 to present and its Download tab. For the product type choose Reanalysis.
- Choose these variables: Geopotential, Temperature, U-component of wind and V-component of wind.
- Choose the pressure levels that cover the flight. Air pressure halves about every 5.5 km, so 1000 hPa down to 500 hPa covers the lowest 5.5 km. Include a level below the pad too (ERA5 continues its levels beneath high ground), so the pad lies between two levels. Below the lowest level hpr continues the standard atmosphere and marks the air as extrapolated.
- Choose the day and every hour around the launch, in UTC, not every third or sixth hour. hpr blends the two file times on either side of the launch however far apart they are, so a file with gaps blends weather hours apart.
- Choose a small area around the site, at least a quarter of a degree beyond it on each side. ERA5’s grid points are a quarter of a degree apart, and hpr needs the four around the site. hpr doesn’t join a whole-Earth grid’s last longitude to its first, so a site between them is refused: on a grid from 0° to 359.75° east, the last quarter degree west of 0°.
- Choose the NetCDF format, then convert the file as below. hpr does not read GRIB, the other format offered (the weather services’ own binary format).
Converting a current file
Files from today’s Data Store are netCDF-4. Inside, that is HDF5, a general container format
that hpr doesn’t read. Three lines of Python, with the xarray and netCDF4 packages
(pip install xarray netCDF4), rewrite a file in the older netCDF classic format:
import xarray
xarray.open_dataset("era5.nc").drop_vars(["number", "expver"], errors="ignore").to_netcdf(
"era5-classic.nc", format="NETCDF3_64BIT")
The two dropped variables are labels the classic format can’t hold: a 64-bit whole number and a text value. Older files, like most of the ones RocketPy ships, are classic already. If hpr can’t read a file, its error says which kind the file is and gives this conversion.
This conversion is checked. The test takes NDRT 2020’s launch day, downloaded from the Data Store in 2021 as a classic file and in 2024 as netCDF-4 and converted as above. NDRT 2020 is the University of Notre Dame’s rocket for NASA’s 2020 Student Launch. The two files agree at every level to 0.23 m²/s² of geopotential (about 2 cm of height), 0.35 thousandths of a kelvin and 0.11 mm/s of wind. The older file stores its values as 16-bit whole numbers and the newer one carries the rounding of ECMWF’s own archive, so the gaps are consistent with each file’s rounding. The test pins these largest gaps.
Reading it
The example program
era5_weather
reads a small cut of the file RocketPy ships for Bella Lui’s flight. The Swiss student team EPFL
Rocket Team flew Bella Lui at Kaltbrunn on 22 February 2020, from a pad 407 m above sea level,
and the file covers the pad at 13:00 UTC. The program prints the levels, then the air at the pad
and 500 m above it next to the standard atmosphere. Last, it flies the rocket in both, without its
parachute. Run it from a copy of the repository with cargo run --example era5_weather -p hpr.
On the command line, hpr weather era5 writes the same levels as a
profile file. The program prints:
ERA5 over 47.213476° N, 9.003336° E at 2020-02-22 13:00 UTC
level (hPa) height (m) temperature (°C) wind (m/s) from (°)
1000 240 14.6 1.3 212
975 453 13.2 1.3 213
950 670 12.1 1.1 213
925 892 10.3 1.7 207
900 1119 8.5 3.2 217
875 1351 7.6 5.6 232
850 1590 6.6 7.2 243
825 1834 6.0 8.7 253
800 2085 4.8 10.1 258
775 2344 3.5 10.7 261
height above the pad (m) pressure (hPa) temperature (°C) density (kg/m³)
0 ERA5 980.4 13.5 1.1916
0 standard 965.3 12.4 1.1778
500 ERA5 923.4 10.2 1.1354
500 standard 908.9 9.1 1.1218
Bella Lui to apogee apogee (m above the pad) drift at apogee (m)
ERA5 553.4 40.7
standard, calm 555.2 0.4
This design flies a stand-in motor; Bella Lui's real flight, on its own K828FJ, is
compared on the accuracy page.
The 1000 hPa level lies at 240 m, below the pad: ERA5 continues its levels beneath the ground, so the pad sits between two of them. That day was warm for February, and the pressure was high: the air at the pad was 1.2% denser than the standard’s. The wind was light at the pad and grew to 10 m/s by 2 km.
Reading your own file. hpr reads ERA5 from Rust only for now; there is no command-line tool
yet. Copy the example and change three things: read your file from disk
(let bytes = std::fs::read("my-file.nc")?; then NetCdf::parse(&bytes)) instead of the bundled
one, and give your site’s latitude, longitude and height and your launch time in UTC (the printed
heading has the date written in, so change it too). The copy still flies the example’s rocket,
Bella Lui; Your own rocket shows how to describe yours. The steps are the example’s own:
NetCdf::parse on the file’s bytes, Era5Profile::read with the site and time, then
Era5Profile::sounding gives the atmosphere and the wind for the flight’s Environment.
How hpr reads it
At each pressure level the file gives the geopotential, the
temperature and the wind’s east and north parts (u and v) at every grid point and hour.
- At the site, each value is interpolated between the four grid points around it, first along one side and then the other (“bilinear” interpolation, in degrees). This is how RocketPy 1.13 does it, and hpr follows its formula (RocketPy is MIT-licensed).
- At the launch time, hpr weighs the two hours on either side by how close each is. At 13:12 it takes 80% of 13:00 and 20% of 14:00; on the hour it takes that hour alone.
- Heights. Geopotential divided by
g₀ = 9.80665 m/s²is geopotential height. hpr turns that into height above sea level with the World Meteorological Organization’s formula for the site’s latitude, as for any sounding. ERA5 itself stores no height in meters; the next section compares the two ways to get one. - Between and above the levels, the atmosphere is the
sounding profile:
- Pressure falls with height as the weight of the air above requires (“hydrostatic”). RocketPy draws a straight line in height between the levels’ pressures instead.
- Above the top level, the 1976 standard atmosphere continues from that level.
- The wind’s east and north parts are interpolated in height, as RocketPy does. With
WindInterpolation::SpeedDirectionhpr interpolates speed and direction instead.
Against RocketPy
The tests read RocketPy’s two ERA5 files for Bella Lui (47.2° N) and NDRT 2020 (41.8° N), plus NDRT’s day from today’s Data Store. RocketPy 1.13 reads the same files.
| hpr | RocketPy 1.13 | measured difference | |
|---|---|---|---|
| Temperature, wind and geopotential at each level, on the hour | bilinear | bilinear | the same to 12 digits (5 readings, 14 or 37 levels each) |
| Height of a level | WMO’s formula, with gravity at the site’s latitude | ECMWF’s formula, with g₀ at every latitude | −0.0158% at Bella Lui, 47.21° N (−0.69 m at 4.4 km); +0.0343% at NDRT, 41.78° N (+1.45 m at 4.2 km) |
| Launch between two of the file’s hours | both hours, weighted by time | the nearer hour | hpr’s value is the weighted mean of RocketPy’s two readings, to 12 digits |
| Pressure between levels | hydrostatic | straight line in height | not measured yet |
| Above the top level | the standard atmosphere, continued | the top level’s values, held | not measured yet; Bella Lui’s file stops at 4.4 km |
Both height readings are approximations, and neither is exact. Geopotential measures the work of
lifting air against gravity, so turning it into meters needs gravity on the way up. ECMWF’s
knowledge base gives h = R·Z/(R − Z), with R the Earth’s radius and Z the geopotential
height, and says it neglects gravity’s change across the Earth. RocketPy uses it. hpr uses the
World Meteorological Organization’s formula, which takes gravity at the site’s latitude: at sea
level it is 9.780 m/s² at the equator and 9.832 m/s² at the poles.
How far off each is depends on how ERA5’s model builds the geopotential of its own ground, which
ECMWF doesn’t say. Taking it as g₀ times the ground’s height, hpr’s reading is off by a fixed
amount at every height and ECMWF’s by an amount that grows with height above the ground. With
RocketPy’s Earth radius in ECMWF’s formula (the WGS 84 ellipsoid’s distance from the Earth’s
center at the site), neither is always the smaller:
| model ground | hpr’s error | ECMWF’s error, at the ground | ECMWF’s, 3 km above it |
|---|---|---|---|
| 407 m at 47.2° N | −0.04 m | +0.03 m | +0.49 m |
| 1400 m at 33° N (like Spaceport America’s) | +1.88 m | +0.31 m | −3.07 m |
At the second site ECMWF’s reading is the closer one up to 1.95 km above the ground, and hpr’s
above that. hpr keeps WMO’s formula because it is the one it uses for every sounding. The
derivation is in the hpr_io::era5 module’s documentation
(API reference), and a test pins these numbers.
The netCDF reader
netCDF is a file format for gridded data, widely used for weather and climate. hpr reads the two
classic kinds from Unidata’s published specification: the original format and the 64-bit offset
format, whose files begin with the bytes CDF and then 1 or 2.
- Numbers. It reads every value exactly as stored, all six types, and the tests check each one against Unidata’s own library on files that library wrote.
- Packed values. Many files store small whole numbers plus a scale and an offset. hpr unpacks them as the netCDF Users Guide says.
- Missing values. A value can mean “missing”: the fill value, or a listed missing value. Where the Guide and the netCDF4 Python library disagree about which values are missing, hpr follows the Guide:
| stored value | the Guide, and hpr | netCDF4-python 1.7.4 |
|---|---|---|
| beyond the fill value, on the side away from zero (for example −32768 when the fill is −32767) | missing | a value |
| −127 in bytes with no fill value given | a value | missing |
The first row matters for ERA5. Many ERA5 files store each value as a 16-bit whole number with a
fill of −32767, and a few values sit at −32768. Two of RocketPy’s other ERA5 files (both
netCDF-4) have them: 102 of 5,241,600 geopotential values and 4 temperatures in one, 298 of
5,184,000 geopotential values and 2 temperatures in the other. hpr reads those as missing, where netCDF4-python returns a
number, a temperature of 198.66 K for example, among neighbours near 301 K. Such values look
like damaged data, so reading them as missing is the safer choice. Era5Profile::read then
refuses the whole reading, naming the level, rather than print a wrong number. None of the files
the tests read has such values.
netCDF-4 files, and the rarer 64-bit data format (files beginning CDF and 5), are refused with
the conversion above.
What it leaves out
- Humidity. hpr doesn’t read the humidity yet, so it flies in dry air. At 20 °C and 50% relative humidity, dry air is about 0.4% denser than the real air.
- Surface files. ERA5’s single-level files, with the 10 m wind and 2 m temperature, are not read.
- GRIB and netCDF-4. Convert them first, as above.
- The longitude seam. On a whole-Earth grid, a site between the grid’s last longitude and its first is refused (on a 0° to 359.75° grid, the last quarter degree west of 0°). Download a regional area around the site instead.
- Geoid. ERA5’s heights are above sea level. hpr has no geoid model, so a flight takes them as heights above the WGS 84 ellipsoid unless you give the site’s geoid height, the height of sea level above the ellipsoid, which is up to about 100 m (Geodesy).
Sources
- [U] Unidata, NetCDF File Format Specifications, “The Classic Format” and “The 64-bit Offset
Format”, and the netCDF Users Guide’s “Attribute Conventions”, netCDF-C documentation, captured
2026-09-26 and pinned as
unidata-netcdf-file-formatandunidata-netcdf-attribute-conventionsin the reference lock file. - [H] H. Hersbach et al., “The ERA5 global reanalysis”, Quarterly Journal of the Royal Meteorological Society 146 (2020), 1999–2049.
- [ECMWF] ECMWF Knowledge Base, “ERA5: compute pressure and geopotential on model levels,
geopotential height and geometric height”, captured 2026-09-26 and pinned as
ecmwf-era5-geometric-height. - [WMO] WMO-No. 8, Guide to Instruments and Methods of Observation, Vol. I (2023), eqs.
12.15–12.16,
wmo-no8-vol1-2023. - RocketPy 1.13.0 (MIT): its reading of the same files is the reference in the tests.
Tests that pin this
hpr_io::netcdf::tests::every_file_reads_as_the_unidata_library_reads_it: every type, record variables, the padding cases and the packing conventions, on 12 files the Unidata library wrote (validation/oracles/netcdf/write_cases.py), with each difference from netCDF4-python listed.hpr_io::netcdf::tests::each_break_of_the_grammar_is_refused_for_its_reason,variables_that_claim_more_bytes_than_the_file_holds_are_refusedanda_slab_too_large_to_pad_is_refused, with two fuzz tests: damaged or hostile files are refused, never a crash.hpr_io::era5::tests::on_the_hour_it_reads_the_levels_rocketpy_reads, and the height difference’s cause, level by level (the_two_height_readings_differ_as_the_guide_says).hpr_io::era5::tests::each_height_reading_errs_as_the_module_documentation_says: the table of height errors above.hpr_io::era5::tests::between_hours_it_weights_the_two_hours_in_time.hpr_io::era5::tests::the_current_data_store_file_converted_as_the_guide_says_reads_like_the_older_file.- The extracts and RocketPy’s reading come from
validation/oracles/netcdf/era5.py.
PerfectFlite .pf2 flight logs
A .pf2 file is the flight log a PerfectFlite altimeter’s software saves. The one read so far is a
Pnut’s; the StratoLogger and StratoLoggerCF are expected to write the same layout, as the reader in
Debrief (the project owner’s earlier flight-log analyzer) assumes, but no file of theirs has been
read. It is plain text: a few lines about the altimeter and the flight, then one row per sample,
about 20 a second, with the time, altitude, speed, temperature and battery voltage. A PerfectFlite
has a barometer and no accelerometer, so every height and speed in it comes from air pressure.
To read a flight from one, run hpr analyze on it, or see
Flight-log readings for what is read and how.
This page is the reference for hpr’s reader. PerfectFlite publishes no specification of the format, so everything here was learned from exported files, by Debrief, the project owner’s earlier flight-log analyzer, whose reader this one follows. One real file has been read: Debrief’s public Pnut log. How far to trust it: the layout below is what that file and Debrief’s reader agree on. A variant it doesn’t cover is refused or noted, not guessed at.
Code: hpr_flightdata::perfectflite
(API reference), written for
M4.2d, the milestone that added hpr analyze.
The layout
PerfectFlite Pnut
Firmware: 1.0
Software: 1.1
Serial Number: 0
Apogee: 1281' AGL
Ground Elevation: 600' MSL
NumSamps: 984
Flight Number: 1
Comments: invented for hpr-sim's tests; no real flight's data
Data: (Time, Altitude, Velocity, Temperature (F), Voltage)
0.00, 0, 0, 70.00, 4.20
0.05, 0, 0, 70.00, 4.20
0.10, 0, 0, 70.00, 4.20
0.15, 0, 0
That is the head of the invented log the tests and the command-line guide read.
| part | what hpr reads |
|---|---|
| first line | the altimeter’s name. It must contain PerfectFlite, or the file is refused |
Apogee: | the apogee the altimeter worked out, in feet above the pad, marked ', ft or feet. A value that isn’t a plain number of feet, such as PWRLOSS (the power failed in flight) or 1,009', is noted, not read |
Ground Elevation: | the pad’s height above sea level, in feet, marked the same way |
NumSamps: | the sample count; if the rows differ, a note says so |
Serial Number:, Firmware:, Flight Number: | kept as the file states them |
Data: | the columns, in order |
| the rows | numbers separated by commas, one row per sample |
Other Key: value lines, such as Software: and Comments:, are skipped.
The columns and their units:
| column | unit in the file | hpr’s unit |
|---|---|---|
Time | seconds from the altimeter’s start | s |
Altitude | feet above the altimeter’s reading on the pad | m |
Velocity | feet per second, up; the altimeter works it out from its own altitude | m/s |
Temperature (F) | degrees Fahrenheit, inside the electronics bay | K |
Voltage | volts, the battery | V |
A row may stop after the speed: the temperature and voltage are logged less often. A missing
cell is a gap, stored as NaN. A foot is 0.3048 m exactly. Lines may end in \r\n, \n or a
lone \r, and a leading byte-order mark is skipped. hpr analyze
reads bytes that aren’t UTF-8, such as a Latin-1 degree sign in a comment, as replacement
characters, so the numbers still read.
What is refused, and what is noted
A file is refused, with its line number, when:
- a row holds something that isn’t a finite number, or more values than there are columns;
- a time doesn’t come after the one before it;
- a row stops before its time or its altitude;
- the
Data:line names no time or no altitude column, or names one twice; - a stated height is marked with another unit, such as
m: hpr knows only feet, and won’t read meters as feet; - a line after the rows began isn’t a row.
These are noted and read around. hpr analyze prints each note as a
note: line, and its JSON output lists them in log.notes. A file with many unreadable lines
keeps the first 20 notes about them and counts the rest in one more:
- no
Data:line: the columns are taken to be the five above, in that order, as Debrief takes them; - a column the reader doesn’t know: left out;
- a line in the header that isn’t
Key: value: skipped; - a sample count that differs from
NumSamps:; - a stated apogee or ground elevation that can’t be read as feet, such as
PWRLOSS,1,009', or1009 AGLwith no unit.
Where this comes from
Debrief’s lib/parsers/perfectflite.ts (MIT, the project owner’s own) reads the same layout. It
was written from exported files and cites no document. hpr’s reader differs in these ways:
- Debrief assumes the column order; hpr takes it from the
Data:line when there is one. - Debrief skips any line that isn’t a row; hpr refuses one after the rows have begun.
- Debrief reads a stated apogee only when it is marked
'; hpr also acceptsftandfeet, and refuses a height marked with another unit.
The public Pnut log marks its apogee with ', as 1009' AGL, and its rows agree with that
figure (Flight-log readings).
Checking a claim
Every number hpr-sim computes, and every number on this site, should lead back to three things: the published source of the model behind it, a test that pins it, and, where one exists, a comparison with another program or a real flight. This page shows how to follow that trail, with two worked examples. It is for anyone who would rather check a claim than trust it. If a trail breaks, that is a bug, and the last section says how to report it.
The trail
- The page. Each model has a page on this site. Its In short says what the model is, where it comes from, how well it is validated and what it leaves out. Below that, each section gives the equations in words and symbols, and cites its source by a short key in square brackets, such as [NGA].
- The source. Near the top of the page, under Code and sources (or Sources), each key’s
full reference is listed. Sources that can be downloaded are pinned in the
reference lock file by address and SHA-256 hash, a fingerprint of the file’s exact bytes
(ADR-002, the reference library decision), so everyone checks the same file.
cargo xtask refs fetchdownloads them into the repository’srefs/folder, which is never committed, andcargo xtask refs verifychecks their hashes again. A few sources could not be fetched, such as Dormand and Prince’s 1980 paper, and the page says so: “cited, not fetched”. - The test. The page’s Verification or Tests that pin this section names the tests that
hold the code to the source, and the tolerance of each: how far a result
may be from its reference and still pass.
cargo testruns all of them, on every change, on macOS, Windows and Linux. - The comparison. Where a model is compared with another program or a high-precision
calculation:
- The reference values are a fixture, a file under
validation/fixtures/. - A program under
validation/oracles/, an oracle, writes each fixture, with a record of how. - The model page’s verification section names the fixture and the test that reads it.
- Some comparisons, today the descents under a parachute, are also run by the validation
harness: the program behind
cargo xtask validate, which flies them again and rewrites the committed validation report, one row per number. To fly them again and check the report without rewriting it, clone the repository and runcargo xtask validate --check(what it checks). - Each of those has a case file under
validation/cases/. It says what is flown and how close the two must agree, and argues for that tolerance. - The rules behind this are in ADR-015, the validation harness decision.
- The reference values are a fixture, a file under
The Accuracy page gathers every result from steps 3 and 4 in one place.
Example: gravity at the equator
The claim. hpr’s gravity on the equator, on the WGS 84 ellipsoid, is 9.7803253359 m/s². That is at height 0 above the ellipsoid, which is not quite sea level (see ellipsoidal height).
-
The page. Gravity and Earth rotation models normal gravity, the pull of an idealised Earth plus the effect of its spin. The table under Defining parameters and derived constants gives
γ_e, the value at the equator, as 9.7803253359 m/s², and cites Table 3.6 of [NGA]. -
The source. [NGA] is the US National Geospatial-Intelligence Agency’s WGS 84 standard, NGA.STND.0036 (2014), a US government work. The lock file pins it as
wgs84-nga-stnd-0036, andcargo xtask refs fetch wgs84-nga-stnd-0036downloads it. Table 3.6 prints the same value. -
The test. Tests that pin this names
gravity::tests::somigliana_matches_published_valuesincrates/hpr-core/src/gravity.rs. It checks the constant against Table 3.6 to its printed digits. It also checks gravity at 11 latitudes and heights against values computed from the standard’s formulas at 40 digits, by a separate script,normal_gravity.py. Run it with:cargo test -p hpr-core somigliana_matches_published_values -
The comparison. None is needed here: the standard itself is the reference. This is the second of the four kinds of evidence that Accuracy describes: a model checked against its published source.
Example: how far a rocket drifts under its parachute
The claim. For the NDRT 2020 rocket, one of RocketPy’s example rockets, hpr’s north drift under the parachute differs from RocketPy’s by +2.865%, inside the 3% tolerance its case allows.
- The report. The validation report has a row for the case
descent-ndrt-2020-nose-to-tailand the metricdrift_north_m: hpr −50.83 m, RocketPy −49.42 m, a difference of +2.865%, a tolerance of 3.000%, and the verdict pass. - The case.
descent-ndrt-2020-nose-to-tail.tomlnames the rocket’s design and the reference file, and holds each metric to 3%. Its opening comment argues why there is no absolute floor, a fixed allowance in meters that would pass any smaller difference: a floor big enough to excuse a drift component near zero would hide a real error in a large one. - The reference. The values come from
rocketpy-descent.json, which the report names with its hash. The scriptrecovery.pywrote it by flying RocketPy 1.13.0’s own parachute phase; the lock file pins that RocketPy by commit, the id of one exact version of its code. The reference file’s first lines say what RocketPy computes, and what the comparison overrides (both codes start from the same state, with the first parachute opening at once). - The model. Recovery explains hpr’s descent, and its section Against RocketPy explains the differences that remain. The likely cause is added mass: RocketPy treats the air a canopy drags along as extra mass, 15.9 kg for NDRT’s main against the rocket’s 20.8 kg, and hpr has no such term. It slows the response to the opening, which most likely moves this small drift component by 2.86%. That fits the size of the gap, but no test has isolated it yet.
- The test.
recovery::tests::descent_matches_rocketpy_examplesincrates/hpr-sim/src/recovery.rsreplays all five cases on every change, andcargo xtask validatewrites them into the report.
What this shows, and what it doesn’t: the two codes agree on the physics of a descent. It says nothing about whether either matches a real parachute on a real day. The real flights compared so far stop at apogee, so no descent has been checked against one yet.
Rules that keep the trail honest
- A reference is never changed to make a comparison pass. It moves only when its generator runs again, which is a deliberate step recorded in the history. This guards against Loft lesson L76: Loft, the project before this one, said to regenerate a reference when its drift check failed, so the reference moved with Loft’s own drag.
- Checked on every pull request. CI reruns every case on macOS, Windows and Linux. It fails if a metric is outside its tolerance, or if the committed report no longer matches, apart from last-digit rounding. The workflow that regenerates the references runs only when a person starts it, and it cannot commit: its output is a diff for a person to review (how CI and regeneration work).
- A tolerance lives in its case file, with its reason. Loosening one to turn a failure into a pass is not allowed. A target that can’t be met gets a decision record that shows the measurement, and the gap stays in the report.
- The site checks its own numbers. The site’s build fails if any of these breaks:
- Every number on Accuracy outside code (anything with two or more digits, a decimal point, an exponent or a percent sign) must appear in a file that its paragraph, list item or table row links to, such as a model page outside its In short, the validation report or a case file. A guide page such as this one doesn’t count, so no page can vouch for itself.
- It must appear there written the same way: the same digits, and the same sign and percent sign where Accuracy writes them. So 2.8 doesn’t match 2.865, and −2.865% doesn’t match +2.865%.
- Accuracy’s table of the descent results must match the validation report cell by cell, and hold every result in it.
- Every number in a model page’s In short must appear in the rest of that page, or in a file its item links to.
- What that check can’t see. When a number changes at its source, the build fails until the page that quotes it follows, unless the old number still appears somewhere else in that file. And the check can’t tell whether a number is quoted in the right context, such as for the right rocket. The reviews check that, and the model pages say what each number means.
When the trail breaks
If a number on this site or from the code has no source, no test, or a test that doesn’t check what the page says, please open an issue with the page and the number. The pencil icon at the top of each page opens its source on GitHub, where you can propose a fix.
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:
- Analytic and unit tests.
- Component-level references (tables and worked examples).
- Code-to-code (RocketPy, OpenRocket).
- 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 bycargo 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:
| term | Mach number | what it means |
|---|---|---|
| Core band | 0 to 2.5 | Accuracy work goes here first. Nearly every flight on commercial motors is in it. |
| Extended band | 2.5 to 3.5 | Record flights on the largest commercial motors. hpr flies it; its accuracy is checked less than the core band’s. |
| The envelope | 0 to 3.5 | Every flight up to Mach 3.5, at any angle of attack. |
| Beyond the envelope | past 3.5 | Deferred, 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:
| issue | what reads wrong | when it warns |
|---|---|---|
| #67 | the 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.5 | such a nose, or such a shoulder (a transition widening aft), past Mach 0.8 |
| #68 | base 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 unmeasured | every rocket, past Mach 0.8 |
| #70 | a sharp fin’s drag, high faster than sound; hpr has no sharp section | airfoil fins, past Mach 1 |
| #72 | a 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 |
| #222 | supersonic pressure drag, about twice OpenRocket’s, a comparison between two codes with no measurement to say which is right | an ogive nose or airfoil fins, past Mach 1 |
| #73 | a 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° one | a 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 |
| #18 | skin 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 once | every 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):
| issue | what reads wrong | when it warns |
|---|---|---|
| #87 | a 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 |
| #120 | the 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 |
| #121 | the same, for a pointed tip steeper than the cone tables’ 30° (−7.7% and 0.81 calibres) | such a tip, past Mach 1.2 |
| #172 | the 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 found | every flight with a static margin |
| #64 | a 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 aft | such a fin (a trapezoid’s tip ahead of its root, or a freeform point ahead of the root’s leading edge), past Mach 1 |
| #325 | fin 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 example | fin 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 |
| #326 | a 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 unprobed | every 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
--fastmay 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 needsuv0.12 or later and a one-timecargo xtask refs fetch python rocketpy. It runsrocket_mass.py,cargo xtask designs,recovery.pyandflight.py, in that order, thencargo xtask validate --check, and rewrites the report only if that check fails. It overwrites files undervalidation/in the working tree (git checkout validationundoes 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 withgh 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 thereferences-diffartifact. 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)
| tool | use | license | where | notes |
|---|---|---|---|---|
| RocketPy 1.13.0 (PyPI, 2026-07-22) | primary code-to-code oracle; headless Python | MIT | https://github.com/RocketPy-Team/RocketPy | Install 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 jar | second oracle (run only, never read its source) | GPL-3.0 | https://github.com/openrocket/openrocket/releases/download/release-24.12/OpenRocket-24.12.jar | Needs 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 files | MIT | https://github.com/RocketPy-Team/RocketSerializer | Pinned, 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.0 | Windows-only freeware; no automation | closed | https://www.rasaero.com/dl_software_ii.htm | Use 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) |
| JSBSim | optional generic 6-DOF cross-check | LGPL-2.1 | https://github.com/JSBSim-Team/jsbsim | low priority |
| CamPyRoS | dormant; includes Martlet 4 RASAero data | GPL-3.0 | https://github.com/cuspaceflight/CamPyRoS | Run-only if used at all |
| Missile DATCOM | do not use (ITAR) | n/a | n/a | n/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% byhpr_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-geodesicsandkarney-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, andvalidation/reports/geodesics.mdholds the whole set’s errors. - GeoTIFF elevation (pinned as
ogc-19-008r4-geotiffandusgs-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 incrates/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.mdholds 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.orcfiles, bundled incrates/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, incrates/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 incrates/hpr/tests/fixtures/orc/openrocket-built.json(validation/oracles/openrocket/orc_built.py), whichcrates/hpr/tests/catalog_openrocket.rsholds 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.jsonwith figure and page, the level-3 reference for the normal force through Mach 1, checked byhpr_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 invalidation/fixtures/aero/drag-vs-mach.jsonand checked byhpr_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_dragas 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 invalidation/fixtures/aero/drag-vs-mach.jsonand checked byhpr_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 byvalidation/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 inhpr_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/.
| flight | files | notes |
|---|---|---|
| Bella Lui 2020 (EPFL) | EPFL_Bella_Lui/bella_lui_flight_data_filtered.csv | time, z, v |
| NDRT 2020 (Notre Dame) | NDRT_2020/ndrt_2020_flight_data.csv | accel 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.csv | raw AltOS CSV with GPS; also our AltOS importer test |
| Juno III 2023 (Projeto Jupiter, SA Cup) | juno3/{cots_altimeter,cots_GNSS,srad_telemetry}.csv | RRC3 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, Lince | genesis/, polito/, camoes/, lince/ | |
| Hedy 2025 (TU Wien) | hedy/cats_tust/*.csv | CATS logs with IMU |
| Valkyrie 2025 | valkyrie/flightInfo_merged.csv | |
| Valetudo, Defiance | apogee 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/.telemfiles and CSV export. - DOFPro archive (https://dofpro.org/RCK/fltdata/): PerfectFlite
.pf2, Raven.FIPa, AIM.xtraand 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 commit37251476e5cfee330c88aa94cf0dd58f93370ccd, with aCHECKSUMS.sha256manifest whose sha256 isd909aeae6e063b629a161ba36dcc0f8b51e45313997789c8ecbc71e342971510. Many.orkfiles carry stored OpenRocket simulation results, so they are a free code-to-code reference once the.orkimporter exists. Never commit these files (rule 4 inCLAUDE.md).nrdptel/debrief-fixtures(private): flight logs gathered for Debrief, 62 files in its manifest (commit722f07cd5d9d8595b31c64e2964fb7fbd5e751e7), from about 30 flights of team, certification and sport rockets, recorded by altimeters of a dozen makes. None is a flight of aloft-fixturesdesign (ADR-083 decision record). Never commit these files (rule 4 inCLAUDE.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 commit31771d83051644da7ea545ec9dffbf14a175ea8d, with aCHECKSUMS.sha256manifest of 9,556 files.cargo xtask fixture-flightsflies its 83 tier-A.orkflights 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 itsLICENSING.mdgives. Never commit these files (rule 4 inCLAUDE.md).nrdptel/fusionspace-loft(MIT, Neer’s):- Eight demo
.ork/.rkt/.CDX1fixtures infixtures/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:.CDX1parts ignore<Location>,.orkautoradii are lost on round trip, and stage-boundary auto-radius resolution is wrong.
- Eight demo
Motor data
| source | what | license | notes |
|---|---|---|---|
ThrustCurve.org API v1 (/api/v1/{metadata,search,download}.json) | motor metadata and simfiles | spec 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 spec | https://www.thrustcurve.org/info/raspformat.html (thrustcurve-rasp-format) | n/a | implicit (0,0) first point; ends at zero thrust. Reader and writer: docs/format/eng.md |
RockSim .rse spec | https://www.thrustcurve.org/thirdparty/RockSim%20Engine%20File%20Format.pdf | n/a | XML; real files disagree with the guide on names and units. Reader and writer: docs/format/rse.md |
| ThrustCurve.org statistics code | simulate/analyze/analyze.js at commit 577afa6 (thrustcurve3-analyze) | ISC | validation/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 SolidMotor | mass, centers and inertia vs time for BATES grains | MIT | validation/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 motor | total mass, center of mass and inertia vs time for nine example rockets (Calisto at two motor positions) and Prometheus’s GenericMotor | MIT (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-db | JSON snapshot including thrust samples | ISC (code) | handy offline seed; check the data terms per curve |
| openrocket/motor-database | weekly SQLite mirror | GPL-3.0 | run-only reference; don’t bundle |
| motor.fusionspace.co API v1 | live 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 |
| Certification | certOrg field in ThrustCurve | n/a | no machine-readable NAR/TRA/CAR lists |
Recovery
| source | what | license | notes |
|---|---|---|---|
RocketPy Flight parachute phase | descent rate, descent time and drift for five example rockets (Calisto, Valetudo, NDRT 2020, Prometheus 2022, Juno III) | MIT | validation/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 pad | apogee 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 Lui | MIT | validation/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 tables | drag coefficients on the nominal area, canopy fill constants, drag-area growth exponents and opening-force coefficients | no clear terms: cited, never redistributed | transcribed 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.
| source | what | license | notes |
|---|---|---|---|
| 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 m | no terms stated: cited, never redistributed | the 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), 2005 | wind-tunnel drag of cotton streamers clamped at the luff, AR 3.3 to 30, 6 to 18.9 m/s | paywalled; the authors’ post-print states no terms: cited, never redistributed | hpr’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.05 | appendix C’s streamer correlation, §3.5’s tumbling model with its fin efficiency table, and Table 3.3’s five drop-test models | CC BY-SA | all 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
| format | spec status | notes |
|---|---|---|
OpenRocket .ork | zip 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 .orc | parts DB XML | openrocket/openrocket-database is Apache-2.0. It can be bundled with notices (Loft did this; 3,445 parts) |
RockSim .rkt | XML | RockSim ships RockSim_Xml_Doc.txt. PWrInSpace/rkt_format (MIT) is a readable parser |
RASAero .CDX1 | XML, no public spec | Work from sample files only (clean room) |
RocketPy .rpy | JSON tied to Python class signatures | rocketpy/utilities.py, _encoders.py (MIT) |
Open Rocket Document (.ord) | dead since 2016 | GPL; reference the idea only |
Environment data
| source | notes |
|---|---|
| Open-Meteo | Data 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 / RAP | open data; AWS noaa-gfs-bdp-pds, NOMADS grib filter, UCAR THREDDS (RocketPy uses these) |
| ERA5 pressure levels | CC-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 soundings | https://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 |
| WMM2025 | public domain; valid to the end of 2029 (magnetic declination for headings) |
| Copernicus DEM GLO-30/90, NASADEM | free; COG on S3; for terrain and landing elevation |
| Gravity | Somigliana/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.
cmaes0.2.2 has low activity.
- Weather files:
grib0.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.
The API reference
The API reference documents hpr-sim’s code: every public type, function and constant, generated
from the source by rustdoc, Rust’s documentation tool. Use it when you write a program with
hpr-sim, as Getting started does. This page says which crate holds what, and
links each crate’s reference. The library is pre-alpha: none of its interface is stable yet, any of
it can change, and the simplest way in is the hpr crate’s builder (The builder).
Where to read it. On the site, the crate names below open the reference. On GitHub they lead nowhere, because the reference is built rather than stored in the repository. Build it on your own machine instead:
cargo doc --workspace --no-deps --open
That opens one crate’s front page; the others are in the list of crates on the left. cargo xtask site builds this whole site, with the reference beside the guide.
The reference is built from the same source that CI, the project’s automated checks, compiles on every change, so it describes the code as it is. Every link in it is checked in CI, apart from a few inside the vector types’ documentation, which rustdoc copies from glam, the vector library hpr-sim uses. Physics items give their equation and cite their source, as the model pages here do. Each crate’s front page links back to the pages here that explain its models.
The crates
hpr-sim is split into crates, Rust’s packages, so that a program takes only what it needs, and so that the models, which don’t read or write files or use the network, also build for the web. Eleven crates hold most of the code today:
| crate | what it holds | not yet | the guide’s pages |
|---|---|---|---|
hpr | One crate to depend on: the builder for environments, motors, rockets and flights, its parts made from catalog parts too (parts from a catalog), the other crates re-exported by name, hpr::ork::separation, which turns a .ork file’s staging into a flight’s separation (example), a drag model of your own in hpr’s place, and a guide in the reference, hpr::guide | One motor per built rocket | The builder, Models of your own |
hpr_core | Vectors and quaternions (a compact way to store a rotation), frames, the Earth’s shape, distance and bearing between places, gravity and magnetic field, interpolation tables and numerical integration of functions | Frames, Geodesy, Gravity, The magnetic field, Interpolation tables, Adaptive quadrature | |
hpr_atmos | The standard atmosphere, humidity, soundings, wind profiles and turbulence | A flight doesn’t use the turbulence yet | Atmosphere, Wind, Turbulence |
hpr_motor | Solid motors: thrust curves, mass and inertia through the burn, .eng and .rse files, and the 32 bundled curves | Hybrid and liquid motors, which are out of scope | Solid motors, .eng files, .rse files |
hpr_design | The rocket: its tree of parts, their shapes and materials, mass properties and design checks | The design tree, Shapes, Mass properties | |
hpr_format | hpr’s own design format: a design as one JSON document (.hpr) with its JSON Schema, read from and written to .ork, in a zip container with other files (.hprz), older versions migrated | Generated TypeScript and Python types (M3.3c) | The hpr design format |
hpr_aero | Aerodynamics: normal force, center of pressure and drag to Mach 5, tables from other programs for the drag and the normal force, and drag models of your own (Models of your own) | Faster than sound a flight takes a pointed nose, its cylinder and a boattail behind them from the shock-expansion method, the boattail’s share unvalidated; a blunt or vertical tip flies the method behind a Newtonian cap, checked on a sphere-cone only; a conical flare flush with the part ahead of it flies the method too and ends the run, checked against one measured flare; any other widening shape, or any step, behind the nose keeps slender-body theory, which reads low past Mach 3 | Aerodynamics |
hpr_sim | The flight: the launch rail, the equations of motion, time integration, events and recovery | Staging and air starts fly, checked by tests and against OpenRocket’s two-stage, cluster and air-start examples, each flight within 5% in apogee and largest speed (M1.9c, a two-stage and a cluster design against OpenRocket). Three cluster apogees are compared with OpenRocket’s flight with no parachute, since its parachute opened before apogee. A cluster flies, one mount of several tubes or one mount per motor, and so does a motor out, checked by tests against a hand calculation. A nose cone, a section or a payload can leave the airframe, optionally pushed by its charge, and land on its own under its own parachute or tumbling (ejection, M1.11a, M1.11b), checked against exact answers only. | How a flight is simulated, Rigid-body flight, Time integration, Recovery, Staging |
hpr_flightdata | Reading a flight log on its own, with no design and no simulation: PerfectFlite’s .pf2 so far, and liftoff, apogee, the top speed, landing and the descent, each saying where it came from or why it was withheld. Depend on this crate directly rather than on hpr, which pulls in the simulator | Other loggers’ files (M7.1); the descent’s legs, Mach number and the rest of the readings (M7.2) | Reading a flight log, Flight-log readings, .pf2 files |
hpr_validate | The validation harness: cases, reference data, metrics and reports | Whole flights against RocketPy (M2.1b2) | Accuracy, Checking a claim |
hpr_py | The hpr Python package: the builder’s environment, motor, rocket and flight from Python, designs read from files, and a flight’s recording as NumPy arrays. Built by maturin into one wheel per operating system; not on PyPI | A drag table and RocketPy’s example flight (M4.3b); a drag and a wind written in Python (M4.3c) | Python |
The other six crates are for planned work. Each has a front page that says what it will hold.
One already holds some code, hpr_io, as its row says:
| crate | what it will hold | planned in |
|---|---|---|
hpr_io | Import and export of OpenRocket, RockSim and RASAero designs, export to RocketPy, and OpenRocket’s parts catalog: its 16 .orc files are bundled, every part found by maker and part number (the format page). A .ork file’s container, design document and components are read into a design (the format page); so are its motors, when each lights, and its recovery settings. Each powered separation comes out as a Staging, from the tail forward (MotorConfiguration::stagings): its time is known before the flight, and then a motor ahead of it is still burning or yet to light and none behind it is. Any other separation that could come before apogee is refused, a sustainer already burnt out at the split included. The configuration’s rocket doesn’t carry the Staging: hpr::ork::separation turns one into the flight’s separation, and hpr::ork::separations a configuration’s list, which you pass to the flight yourself, with a recovery device on each part, since hpr refuses the flight without them (example); hpr::ork::recovery turns a configuration’s parachutes and streamers into the flight’s recovery devices, as OpenRocket flies them (Recovery), and hpr::ork::separated_recovery puts each on its part when the configuration separates, as hpr sim flies it. A design is written back out as a .ork with hpr_io::ork::export (writing a .ork). An ERA5 weather file (netCDF classic) gives the atmosphere over a launch site at launch time (ERA5 weather files), and a GeoTIFF elevation file a site’s height (A launch site’s elevation) | M3.1a, M3.2a, M2.3a and M5.5a done; M3.1b to M3.6 next |
hpr_net | Optional online data, cached for offline use: weather, soundings, elevation and motor data. The cache, the offline mode and HTTP work today, HTTPS checked by hand but not in CI (Online data and the cache); five sources, Open-Meteo’s weather (Launch-day weather), weather-balloon soundings (Weather-balloon soundings), NOAA’s GFS and RAP forecasts (NOAA forecasts: GFS and RAP), Open-Meteo’s ground elevation (A launch site’s elevation) and motor.fusionspace.co’s motor stock and prices (Motor stock and prices) | M5.1 done; M5.2a, M5.2b and M5.2c done; M5.2d1, the command line’s hpr weather, done; M5.2d2 and M5.2d3 done; M5.3b, elevation, done; M5.4a, motor stock, done |
hpr_analysis | Monte Carlo dispersion (flying many copies of a flight with randomly scattered inputs), the spread of its results, and landing ellipses (Monte Carlo dispersion); sensitivity analysis, Morris screening and Sobol’ indices (Sensitivity analysis); optimization (Optimization): CMA-ES over numbers and choices such as a motor, within limits such as a minimum margin; NSGA-II for two goals at once; EGO (efficient global optimization) for models too slow for more than tens of evaluations; on a rocket, answers only as good as hpr’s flight models; planned: competition rule files (M6.3) | M6.1 done; M6.2 done but for EGO on six variables (M6.2d2) and robust designs (M6.2e); M6.3 not yet |
hpr_forensics | A flown flight against a simulation of it: what will differ, what that says about drag, mass, impulse and wind, and what went wrong | M7.3 to M7.4 |
hpr_ffi | A C interface, for other languages | M4.4 |
hpr_wasm | WebAssembly bindings, for the browser | M4.4 |
The command-line tool, hpr-cli, is a program rather than a library, so it has no reference here:
The command line documents its commands, output and exit codes.
Using it from your own program
Release 0.1.0 is built and checked, but not on crates.io, Rust’s public package registry, yet.
Once it is, cargo add hpr adds the front door, hpr, which re-exports the crates under it; its
features net, parquet and parallel add the online data sources, Parquet export and Monte
Carlo on worker threads (Install a release). Until 1.0, a
new minor version, such as 0.2, may change the API.
Until then, a program outside this repository can depend on a crate straight from GitHub, pinned
to a commit, since anything can change between commits. Take the commit’s hash from
the history of main, and change it only on
purpose:
[dependencies]
hpr-sim = { git = "https://github.com/nrdptel/hpr-sim", rev = "<commit>" }
Reading it
- Names. Code names a crate with underscores (
use hpr_sim::Simulation;), andCargo.tomlnames its package with hyphens (hpr-sim). They are the same crate. - Search. The search box at the top of every reference page, or the S key, finds a type or function in any crate.
- Source. Each item’s Source link shows the code it documents.
- Links to the guide on the crates’ front pages go to the published site,
https://nrdptel.github.io/hpr-sim/. Until it is live, read the same pages in the repository’s
docs/folder.
How a release is built
Nothing has been released yet: when a release comes, one workflow builds every file of it from
one commit, tests each as you would get it, and publishes nothing until Neer Patel, who owns the
project, approves. This page says what a release holds, which license files come with each file,
how each is checked, and how to check one yourself. The building and checking run when the workflow
is started by hand, and on every pull request that changes it or its scripts (scripts/release/).
The publishing steps have never run.
What a release holds
A release has one version for everything in it. Release 0.1 is the simulator: the library, the
hpr command line and the Python package
(the release plan).
The changelog has each version’s
entry: what it holds, what works, how far to trust it, and its known gaps.
Install a release says how to install each file.
| File | What it is | Where it goes |
|---|---|---|
hpr-<version>-x86_64-unknown-linux-gnu.tar.gz | the hpr command line for Linux on x86-64 | the GitHub release |
hpr-<version>-aarch64-apple-darwin.tar.gz | the command line for Macs with Apple silicon | the GitHub release |
hpr-<version>-x86_64-pc-windows-msvc.zip | the command line for Windows on x86-64 | the GitHub release |
hpr_sim-<version>-cp310-abi3-<platform>.whl | the Python package, one wheel per operating system, for CPython 3.10 and later | PyPI, as hpr-sim |
hpr_sim-<version>.tar.gz | the Python package’s source; installing it compiles it, so it needs Rust | PyPI |
| 14 crates | hpr, the library’s front door, the 12 crates under it, and hpr-cli, the command line | crates.io |
Each file is built on the CPU of the machine CI builds it on, so there is no archive or wheel yet
for Intel Macs or for Linux on ARM; there, pip install builds the Python package from its source
package, which needs Rust. The Linux files need the GNU C library (glibc) of
a recent distribution: 2.35 for the wheel, as built on 2026-10-06.
Four crates and the repository’s own tool stay unpublished. hpr-py reaches Python users as the
wheels. hpr-ffi and hpr-wasm wait for their milestone. hpr-validate, the validation harness,
needs a copy of the repository, its cases and their references; to re-run its check, clone the
repository and run cargo xtask validate --check (Accuracy).
The license files
Each archive, wheel and source package carries four license files: LICENSE-MIT and
LICENSE-APACHE, hpr-sim’s two licenses, either at your option; THIRD-PARTY-NOTICES.md, every
outside source the project uses, such as bundled data; and THIRD-PARTY-LICENSES.txt, the license
texts of the Rust crates compiled in. An archive has them beside hpr. A wheel has them in its
metadata folder, hpr_sim-<version>.dist-info/licenses/, where pip show -f hpr-sim lists them.
THIRD-PARTY-LICENSES.txt is written for each build by
cargo-about from the crates’ own license files. It
lists each license once, with its text and the crates that use it. Built on 2026-10-06, the
command line’s file listed 114 crates under 7 licenses, 100 of them from outside hpr-sim; the
Python package’s listed 90 under 5, 77 from outside. A crate may appear under several texts, as
ring does under 18 ISC notices. The licenses it accepts are the ones the project’s dependency check
allows, all permissive, so a crate under any other license stops the build.
How each file is checked
Each file is unpacked or installed in an empty folder and run, as a user would get it, from a copy of the repository that provides the example design:
- An archive must hold
hpr, the README and the four license files, with the texts namingclap, a crate the command line is built on;hpr --versionmust name the release’s version;hpr motors show H54must read the bundled motor catalog; andhpr sim validation/fixtures/ork/pod-flights/pods-none.ork --motor H54must reach 846.1 m above the site, within a meter, the apogee the command line’s page prints for that example. - A wheel or the source package is installed into fresh environments on Python 3.10 and 3.13.
It must carry the four license files, the texts naming
pyo3, the crate that binds Rust to Python, and the release’s version, and the first example on the Python page must reach 1118.3 m, within a meter, as that page prints. - The crates must each package, and build from their packaged files alone
(
cargo publish --workspace --dry-run).
These smoke tests show a file installs and runs as built. They don’t check the physics: the test suite and the validation cases do that, on every change (Accuracy).
Publishing
Publishing needs Neer twice: he starts the workflow with publish ticked, and then approves
its release environment,
a GitHub setting whose required reviewer is him. Until he has made that setting, the workflow is
never started with publish ticked. The crates then go to crates.io, the wheels and
source package to PyPI, and the archives into a draft GitHub release; publishing the draft makes
the version’s tag.
Checking a file yourself
From a copy of the repository at the release’s commit, with uv installed:
scripts/release/smoke-cli.sh hpr-0.1.0-aarch64-apple-darwin.tar.gz
scripts/release/smoke-python.sh hpr_sim-0.1.0-cp310-abi3-macosx_11_0_arm64.whl
Each prints what it checked and ends with passed, or stops at the first check that fails. To
build the files yourself, scripts/release/archive.sh dist and scripts/release/python.sh wheel dist need cargo-about 0.9.2 (cargo install cargo-about --locked --version 0.9.2 --features cli).
How the workflow was chosen is in its
decision record.
Glossary
This page defines the terms the other pages use, each in a sentence or two, with a link to the page that models the term or uses it most. Terms are listed alphabetically, and each has its own heading, so any page can link straight to one. Where hpr uses a term in a particular way (a sign, a frame, a reference point), the entry says so. The definitions follow the model pages; if this page and a model page ever disagree, the model page is the authority.
6-DOF (six degrees of freedom)
A simulation that follows all six ways a rigid body can move: its position along three axes and its rotation about three. hpr flies a rocket in 6-DOF from rail exit on. On the rail it has one degree of freedom, along the rail, and under an open parachute it is a point mass whose attitude is frozen. See Rigid-body flight.
Accuracy census
The count of the numbers the validation reports hold hpr to, one row each, kept as it was last accepted. CI fails when a row moves by more than its slack (a small share of the bound it is judged by), changes its standing (pass, within target, not flown and so on), or comes or goes, until the change is accepted with a written reason. It holds a number to where it was, not to the truth. See Accuracy: the census.
Adaptive time step
A time step the integrator (the part that steps the equations forward in time) chooses for itself.
It estimates each step’s error, and shrinks or grows the next step to keep that error within a
tolerance. hpr’s default method, Dormand–Prince 5(4) at rtol = atol = 1e-8, puts
Valetudo’s apogee (one of RocketPy’s example rockets) within 1.1e-6 m of a far tighter run. See
Time integration and events.
Added mass
The air a canopy has to push along with it while it speeds up or slows down relative to the air, counted as extra mass that carries no weight; in steady descent it changes nothing. RocketPy includes it in its parachute descent and hpr doesn’t. For NDRT 2020’s main, one of the example rockets, RocketPy’s added mass is 15.9 kg against the rocket’s 20.8 kg, which makes it the likely cause of the largest difference between the two codes’ descents. See Recovery.
AGL (above ground level)
Height above the launch site. A recovery device’s altitude trigger is a height above the launch site, and a flight ends when the center of mass comes back down to the site’s height. See Recovery and Rigid-body flight.
Air start
A motor lit in flight, some time after launch, rather than on the pad. It can be in the same stage as a motor lit at launch: OpenRocket’s Airstart timing example lights a 3-ring of I211W motors 1, 2, 4 or 6 s after launch (and, in one configuration, at launch), beside a K550W lit at launch. In hpr each motor has an ignition: at launch, at a time, a delay after another motor’s burnout, or a delay after its stage separates. A .ork file’s ignitions are read into these (.ork design files), and each flight of OpenRocket’s air-start example is within 5% of OpenRocket’s apogee and largest speed (M1.9c, a two-stage and a cluster design against OpenRocket). See Staging.
Angle of attack
The angle between the rocket’s axis and the airflow it meets. In hpr it is the total angle α
between the body axis, pointing to the nose, and the rocket’s velocity relative to the air, from 0
to π (180°). hpr’s aerodynamics are small-angle models, so results at large angles, off the rail in
a strong crosswind and near apogee, are the least trustworthy. See
Frames and
Aerodynamics.
API reference
The documentation of hpr-sim’s code: every public type, function and constant, generated from the source by rustdoc, Rust’s documentation tool. It is part of this site, and on GitHub it has to be built; see The API reference.
Apogee
The highest point of a flight, where the rocket stops climbing. hpr finds it as the moment the center of mass’s rate of climb above the WGS 84 ellipsoid falls through zero. It does not use the launch frame’s up axis, whose flat plane rises above the curved Earth with distance (7.8 m at 10 km). See Rigid-body flight.
Average thrust
A motor’s total impulse divided by its burn time, in newtons (N), as ThrustCurve.org defines it. It is the number in a motor designation. See Solid motors.
Barometric altimeter
A flight computer that finds its height from the air pressure it measures. It converts the pressure to an altitude with the standard atmosphere and subtracts the pad’s. The air of the day is rarely standard: on a hot day a column of air is taller, pressure falls more slowly with height, and the altimeter reads less than the height climbed, 6.5% on a day 20 K warmer than the standard (worked example). On a cold day it reads more. hpr reads its own flight the same way when it compares with a log; see Accuracy: real flights.
Barrowman’s method
J. S. Barrowman’s 1966–67 method for the normal-force slope and center of pressure of a slender finned rocket: each nose, transition and fin set is worked out on its own, and the results are summed. hpr follows it, with extensions from Niskanen’s 2009 thesis. Four of Barrowman’s five printed examples agree within 1%, and the six-fin Recruiter’s slope is 2.87% high. See Aerodynamics.
Base drag
The drag on a rocket’s flat aft end, its base. hpr works it out on the base’s area, with a coefficient of 0.12 + 0.13 M² below Mach 1 and 0.25/M above it (M the Mach number). While a motor burns, hpr subtracts the burning motors’ cross-section from that area (power-on drag), following Niskanen, who takes from Fleeman that a base the size of the motor has no base drag. OpenRocket keeps the whole base’s drag, and hpr can fly that rule for a comparison (Aerodynamics). Neither rule has been checked against a measured flight. On one private design’s supersonic flight, C06/1, the choice moves hpr’s apogee difference from OpenRocket’s by about 24 percentage points, more than any other known cause (#222). Behind a boattail, faster than sound, the base’s pressure is higher and hpr lowers the base drag to match (Boattails faster than sound). See Aerodynamics.
BATES grain
A cylindrical propellant grain with a hole along its axis (the bore), which burns on the bore and, unless inhibited, on both ends. hpr can place a motor’s propellant as a stack of identical BATES grains, which sets how its center of mass and inertia change as it burns. See Solid motors.
Bearing
A direction on the ground, measured clockwise from true north: 0° is north, 90° east, 180° south and 270° west. Getting started gives the landing point as a distance and a bearing from the pad. A wind direction is a bearing too: the one the wind blows from. A compass gives a magnetic bearing; add the declination to make it true.
Boattail
A transition at the tail that narrows toward the aft end. Its normal-force slope is negative, so it moves the center of pressure forward. Below Mach 0.8 hpr counts its pressure drag as a share of the base drag on the area it removes: all of it for a short, steep boattail and none for a long, gentle one. Faster than sound a boattail has its own wave drag, which hpr takes from a handbook chart that matches measured boattails of 3° to 10° within about a quarter, and reads high for steeper ones. See Boattails faster than sound.
Body frame
The axes fixed to the rocket. The origin is the nose tip, on the axis. z_B points along the axis
toward the nose, x_B is the design’s zero direction around the body (the one fins and rail
buttons are placed from), and y_B completes a right-handed set, so every part lies at z_B ≤ 0.
See Frames.
Body lift
A sideways force on the body itself, not the fins, when the rocket flies at an angle to the airflow. It grows with the square of the sine of the angle of attack, so it is nothing at small angles and large at steep ones, such as a slow rocket leaving the rail into a crosswind. hpr sizes it by Jorgensen’s crossflow drag, which depends on the body’s length over its diameter and on how fast the air crosses it; before M1.8e6 it used Galejs’s constant. RocketPy leaves it out. See Aerodynamics.
Booster
The aft part of a staged rocket: the stages behind the separation, whose motor lights first and
which are dropped when the stack separates. After a powered separation hpr flies it as a point
mass under its own recovery devices, one of which must be open from the separation: hpr sim
tumbles it from the split until its own parachute opens. Its motors must have burned out by then. See Staging.
Boosters can also be strapped beside the core instead of behind it, a
parallel stage.
Boundary layer
The thin layer of air next to the rocket’s skin, slowed by friction with it. It grows thicker toward the tail: on a model rocket a meter long it can be a centimeter or more thick at the base. Skin friction comes from it, and where it is thick it softens what the outer flow does, such as the expansion around a boattail. hpr takes it as turbulent everywhere. See Aerodynamics.
Burn time
How long a motor burns, by the NFPA 1125 rule that ThrustCurve.org uses: from the moment the thrust first reaches 5% of its peak to the moment it last falls to 5% of its peak, in seconds. It is not the time of the thrust curve’s last point, which is burnout. See Solid motors.
Burnout
The moment a motor stops producing thrust. In hpr it is the time of the thrust curve’s last point: from then on the thrust, the propellant flow and the propellant left are all zero. A flight records a burnout event and steps exactly to it, because the equations change there. See Solid motors.
Calibre (caliber)
A length measured in body diameters. A stability margin is usually quoted in calibres, and a nose cone three calibres long is three times as long as its base diameter. See Shapes.
Canard
A fin set near the nose, ahead of the main fins. Its normal force acts well forward, so it moves the center of pressure forward, and as speed rises its slope grows too. See Aerodynamics.
Cant
The small angle at which fins are set to the rocket’s axis, turning each about its own span so that
the air pushes it sideways and the rocket spins. hpr measures it in radians, positive turning fin
0’s leading edge (fin 0 is the one along +x_B) toward −y_B (Body frame); a
positive cant spins the rocket clockwise seen from ahead of the nose, looking aft. See
Roll: forcing and damping.
Center of dry mass
The center of mass of the rocket with its motor’s propellant gone: the empty rocket, motor casing included. It does not move during the flight, so RocketPy follows this point, and the comparisons with RocketPy measure speeds and drifts there. The center of gravity of the loaded rocket lies aft of it while the motor burns. See Accuracy.
Center of gravity (CG)
The point where the rocket’s mass balances. The model pages call it the center of mass, which in uniform gravity is the same point. It moves as the propellant burns, and hpr recomputes it at every instant from the parts and the motors. See The design tree and Mass properties.
Center of pressure (CP)
The point along the rocket where the normal force acts, given as a station aft of the nose tip. hpr finds it by averaging each component’s own center of pressure, weighted by its normal-force slope, at small angles of attack. A rocket is statically stable when its CP is behind its center of gravity. See Aerodynamics.
CIPM-2007
The formula for the density of moist air, from its temperature, pressure and humidity, that the International Committee for Weights and Measures (CIPM) adopted in 2007. hpr’s humid-air density is checked against it, within 0.047% over 15 to 27 °C. See Atmosphere.
Closed form
An answer written as an exact formula, such as the parabola a body follows in a vacuum, rather than one computed step by step. hpr’s analytic tests compare the code with closed forms, the first of the four kinds of evidence on Accuracy.
Cluster
Several motors in one rocket, burning side by side: in one mount of several like tubes (an OpenRocket 3-ring, say), or each in its own mount. hpr lights each motor at its own ignition (at launch unless told otherwise). It sums their thrusts and masses, and adds the turning moment of any motor set off the axis, as when one motor of a cluster fails to light (a “motor out”). Tests check this against hand calculations, and OpenRocket’s cluster example, read from its .ork file with a motor in every tube, flies within 5% of OpenRocket’s apogee and largest speed. Three of its apogees are compared with OpenRocket’s flight with no parachute, since its parachute opened before apogee (M1.9c, a two-stage and a cluster design against OpenRocket). See Clusters.
CMA-ES
The covariance matrix adaptation evolution strategy, N. Hansen’s optimization method. It samples designs from a cloud (a normal distribution), moves the cloud toward the better ones, and learns from them which way and how far to step next. It needs only the order of the results, not their slopes. See Optimization.
Code-to-code comparison
Flying the same rocket, or the same part of a flight, in hpr and in another simulator from the same inputs, and comparing the numbers. It is the third of four kinds of evidence, and it shows that two codes agree, not that either matches a real flight. hpr’s parachute descents match RocketPy’s within 3% on all 30 numbers compared; whole flights come next. See Recovery.
Configuration
One choice of motors for a rocket design, at most one in each motor mount, named by an id such as h54. A design can hold several, such as the same rocket on different motors, and a flight names the one it flies. Each motor lights at launch unless its ignition says otherwise (Staging). See Your own rocket and The design tree.
Coriolis acceleration
The sideways acceleration that anything moving over the rotating Earth appears to have, −2Ω × v,
with Ω the Earth’s rotation (7.292115e-5 rad/s) and v the velocity in the Earth-fixed launch
frame. It is the only rotating-Earth term hpr adds, since the centrifugal part is already inside
normal gravity, and it is on by default. It is small: in one test it moves the
landing point of a 3 km parachute descent 0.37 m east. See
Gravity.
COTS motor
A commercial off-the-shelf motor: a solid rocket motor bought from a manufacturer, single-use or as a reload for a reusable case. hpr-sim covers only these for now, and bundles 32 of their thrust curves, listed under The bundled motors. See Solid motors and Start here.
Covariance
How two quantities vary together: positive when both tend to be high together, negative when one tends to be high while the other is low, zero when neither says anything about the other. For distances on the ground it is in square meters (m²). A spread of points on the ground has three numbers: the variance east (the square of the standard deviation), the variance north, and their covariance. Together they give the shape and the turn of a landing ellipse. See Monte Carlo dispersion.
Crate
A Rust package: the unit that Rust code is built, versioned and shared in. hpr-sim is split into
crates, such as hpr-core for the maths and the Earth and hpr-sim for the flight, so a program
takes only the ones it needs. See The API reference.
CRS (coordinate reference system)
The definition that turns a file’s coordinates into places on Earth: an ellipsoid and its datum (where the ellipsoid sits), and either latitude and longitude or a map projection such as UTM. Most have an EPSG code. hpr’s elevation-file reader takes latitude and longitude on a datum within a few meters of WGS 84. See A launch site’s elevation.
Decision record (ADR)
A short record of a significant choice, the alternatives considered and why one was picked, labelled like ADR-011 (the rigid-body flight decision). All of them are in the decision log. See Decisions and the roadmap.
Declination (magnetic)
The angle from true north to the north a compass shows, positive when magnetic north lies east of true north. A compass bearing becomes a true one by adding it: at Spaceport America in 2026 it is about +7.75°. hpr computes it from the World Magnetic Model. See The magnetic field.
DEM (digital elevation model)
A grid of ground heights, one per cell, such as the US Geological Survey’s (USGS) 1-arc-second map of the United States, whose cells are about 30 m across. Most are published as GeoTIFF files. See A launch site’s elevation.
Deployment
The moment a recovery device, such as a parachute, comes out to its full line length (line stretch) and starts to fill. In hpr it follows the device’s trigger (apogee, a height above the ground, a time, or a motor’s ejection delay) after a set lag. The flight’s first deployment switches the rocket to the descent, as a point mass. See Recovery.
Descent rate
How fast a rocket falls under its recovery device, in m/s. Once drag balances weight it settles at
the equilibrium descent speed v_e = √(2 m g / (ρ C_D S)), with m the mass, g gravity, ρ the
air density and C_D S the drag area. A 1.1 kg rocket under a 1 m flat canopy with
C_D 0.8, in air of 1.225 kg/m³, falls at 5.294 m/s. See
Recovery.
Design file
A rocket design saved as text, so it can be kept, shared and read back. hpr’s own is the .hpr design format: one JSON document with a versioned header, the rocket, its motor configurations, recovery and stored simulations. A .hprz is the same document in a zip archive, with other files beside it, such as flight logs. hpr sim flies both, and hpr convert writes them from a .ork. The rocket inside it is the JSON of hpr’s Rocket type, which hpr sim also reads on its own: each key is a Rust field’s name, with its unit in the name (length_m), and a mounted motor is written out in full, thrust curve included. The format is version 0.2, a draft until hpr’s first release; an older document is migrated when it is read, but keep the source file too. See Your own rocket.
Digest
OpenRocket’s fingerprint (a hash) of a motor’s thrust-curve data. A .ork file records it for each
motor, and OpenRocket uses it to find that curve in the motor database it ships. hpr uses it to
find a curve the file embeds, or one a caller supplies. Two different motors can, rarely, share a
digest. See the .ork format.
Dispersion
The uncertainty given to one input of a Monte Carlo run, as a standard deviation: how far the rocket’s mass, its motor’s impulse or the wind’s speed may differ from the plan from one flight to the next. See Monte Carlo dispersion.
Dormand–Prince and RK4
Two ways of stepping the equations of motion through time. Dormand–Prince 5(4), also called
DOPRI5, makes two estimates on each step and shrinks or grows the step to keep their difference
under a tolerance; it is hpr’s default. RK4, the classic fourth-order Runge–Kutta
method, takes steps of a fixed size. See Time integration.
Drag area
A recovery device’s drag coefficient times the area that coefficient is measured on, C_D S, in m².
The drag force is the dynamic pressure times it. hpr takes it directly
(RocketPy’s cd_s) or from a canopy’s diameter and its coefficient on the
nominal area, and adds up the drag areas of every open device. See
Recovery.
Drag coefficient
Drag divided by dynamic pressure and an area: a number without units that says
how draggy a shape is. For the rocket, C_D0 is the coefficient at zero
angle of attack on the reference area, built up from skin
friction, pressure, base and fin terms, or read from a table instead. For a parachute, Knacke’s
C_D0 is on the canopy’s nominal area, a different convention. See
Aerodynamics and Recovery.
Drag crisis
A sudden fall in a blunt body’s drag coefficient over a narrow range of Reynolds number, as the flow along its surface turns turbulent and stays attached further round. For a cylinder lying across the flow it comes at a Reynolds number of a few hundred thousand. For streamers, Carruthers and Filippone report a sudden drop near 7.2e5, which they put down to a change in how the streamer oscillates. hpr’s recovery models include none. See Recovery.
Drift
How far the wind carries a rocket sideways while it descends, in meters. In the comparison with RocketPy it is the horizontal distance from where the descent starts to the landing point, with an east and a north part. In still air a small drift remains from the Coriolis acceleration: 0.19 m for Valetudo’s descent. See Recovery.
Drogue and main
The two parachutes of a dual-deployment recovery. The small drogue opens at or near apogee, so the rocket falls fast but steadily; the large main opens lower down for a slow landing, and in hpr it can release (cut away) the drogue once it is fully open. RocketPy’s Calisto example flies a drogue of 1.0 m² drag area and a 10 m² main that opens at 800 m. See Recovery.
Dynamic pressure
The pressure of the oncoming air due to its motion, q = ½ ρ V², with ρ the air density and V
the airspeed, in pascals (Pa). Every aerodynamic force is q times a reference area times a
coefficient, so forces grow with the square of airspeed. Near apogee, where the rocket is slow, q
is small. See Frames and
Rigid-body flight.
ECEF (Earth-centered, Earth-fixed)
The x, y, z frame that turns with the Earth. Its origin is the Earth’s center of mass, +Z points
along the rotation axis to the north pole, +X through the prime meridian at the equator, and +Y
to 90° E. hpr converts between it and latitude, longitude and height on the WGS 84
ellipsoid. See Frames and
Geodesy.
Effective exhaust velocity
A motor’s thrust divided by its propellant mass flow, c = F/ṁ, in m/s. hpr holds it constant
through the burn, c = I/m_p (total impulse over propellant mass), so propellant burns in
proportion to the impulse delivered. It also refuses a motor whose c falls outside 200 to
5,000 m/s, which catches a propellant mass given in the wrong unit, such as grams for
kilograms; ThrustCurve.org’s catalog has a median of 1,867 m/s. See
Solid motors.
Ejection
A piece of the airframe leaving the rest on a trigger, at a joint you choose rather than only at a stage boundary: a nose cone pushed off its airframe, a body section, or a payload carried inside. Each piece then comes down on its own under its own recovery device. Pieces tied together by a shock cord fly as one, so they are not an ejection. Compare separation, which parts the rocket at a stage boundary. In hobby use “ejection” often means the charge firing; in hpr that is a recovery device’s trigger (see ejection delay), and the charge’s push on the pieces can be given to the ejection as an impulse in newton-seconds. See Recovery: ejected pieces.
Ejection delay
The time from a motor’s burnout to its ejection charge, in seconds. Motor files list
the delays available; P means plugged, with no ejection charge, and hpr reads a 0 as “zero or
plugged” rather than as ejection at burnout, because most files mean plugged. A .ork design says
none for plugged, so its 0 is a charge at burnout (.ork files). A recovery device can
use a motor’s delay as its trigger. See Solid motors and
Recovery.
Elementary effect
In Morris’s screening, the change in a result when one input steps part of its range with the others held, scaled to the input’s whole range. The mean of their sizes, μ*, ranks the inputs. See Sensitivity analysis.
Ellipsoidal height
Height above the WGS 84 ellipsoid, measured along the ellipsoid’s normal, in meters. It is hpr’s internal height, and it is not height above sea level: the two differ by up to about 100 m. See Frames.
EPSG code
A number naming a CRS, a datum or a unit in the EPSG Geodetic Parameter Dataset, which most mapping software shares: 4326 is latitude and longitude on WGS 84, 4269 on NAD83, 5703 heights above NAVD88 in meters, 9001 the meter. See A launch site’s elevation.
ERA5
The European Centre for Medium-Range Weather Forecasts’ reanalysis of the whole Earth’s weather, every hour since 1940 on a grid a quarter of a degree apart, from the Copernicus Climate Data Store. Its pressure-level files give the temperature, wind and geopotential height at each pressure level. See ERA5 weather files.
Event
A moment the simulator locates exactly and records, such as liftoff, rail exit, burnout, apogee and ground hit, and in recovery a trigger, a deployment, a release or a separation. The integrator finds each as the zero of a function of the state, adding at most about 2e-12 s of error to the solution’s own, and stops there so the flight can change phase. See Time integration and events and Rigid-body flight.
Example rockets
The rockets hpr is compared on. Calisto, Valetudo, NDRT 2020, Prometheus, Juno III, Cavour and Bella
Lui come from RocketPy’s own examples; their designs, as hpr reads them, are in the repository’s
validation/designs/ folder. The Recruiter and Barrowman’s other rockets are worked
examples from his papers, with their printed values in
barrowman-worked-examples.json. See Accuracy.
Fillet
A fin fillet: the rounded glue joint along a fin’s root, filling the corner between the fin and the body tube on each side. Its face is a circle of the fillet’s radius. hpr weighs fillets as OpenRocket does, as a prism of that corner’s section along the root chord, in their own material. The aerodynamics leaves them out. See Mass properties.
Fineness ratio
A nose cone’s length divided by its base diameter. A 3:1 tangent ogive has a fineness ratio of 3, which the pages also write as “fineness 3”. For a shoulder, a transition that widens toward the tail, the drag pages use its length over its rise in diameter, so a conical shoulder has the fineness of the cone with the same surface angle. See Aerodynamics.
Flare
A transition that widens toward the aft end, the opposite of a boattail: a conical skirt at the tail, or the step up onto a wider aft section. Its normal-force slope is positive, so it moves the center of pressure aft, which is why one is sometimes added for stability. Faster than sound, hpr marches a conical flare through the shock-expansion method while the shock at its corner stays attached, and reads a steeper one as a flare of the same radii drawn out to that angle. What that is worth against the one measured flare in the sources is What a marched flare is worth; how the model works is A flare through the method. Where the march itself stops, which is not where that shock detaches, is in Where a flare’s march stops. A flare shallow enough that its element is reduced is read by the older generalized method instead: A near-flat flare.
Flow separation
Air leaving a surface it can’t follow, such as the aft end of a steep boattail, and leaving a slow, recirculating region behind it. There the pressure is about the base’s, not what the attached flow would give. hpr blends a boattail’s drag toward that value between 16° and 30°, where measured boattails separate. Not the same as a separation of stages or recovery bodies. See Boattails faster than sound.
Flight log
The record an altimeter or flight computer writes during a flight: a
row per sample, each with its time and what the logger measured then, such as its altitude. Its
format is the logger maker’s. hpr analyze reads one and prints what it says, with no design file
(Reading a flight log).
Flutter
A fin shaking itself apart: above a certain speed, the air’s push twists the fin, the twist changes the push, and the fin’s bending and twisting feed each other instead of dying out. How fast that happens depends on the fin’s outline and thickness, its shear modulus and the air. hpr screens for it with NACA TN 4197’s criterion. See Fin flutter.
Forebody
Everything of a rocket but its flat aft end, the base: the nose, the body tube, the fins and any boattail. A wind-tunnel model sits on a sting that disturbs the air behind its base, so tunnel reports often give the forebody’s drag alone, as NASA’s Arcas Robin reports do, and hpr is compared with them on its drag without the base drag. See Aerodynamics.
Forecast run (cycle)
One start of a weather model: it begins from the weather observed at one hour, the run’s cycle, and steps forward. Each forecast hour is the forecast for that many hours after the cycle. NOAA’s GFS starts a run every 6 hours and its RAP every hour. See NOAA forecasts: GFS and RAP.
Gate and target
Two ways a validation result is held to a bound. A gate fails the test suite when a number falls outside it, so a change that breaks agreement cannot merge. A target is the same kind of bound, but a miss is only reported, with its explanation in the case file, and never fails the suite: it is used where neither side of the comparison is known to be right, as for hpr’s own drag against the drag RocketPy’s examples ship. Either way the report is committed, so any number that moves shows up in review. See Accuracy. Separately from both, the accuracy census fails CI when any number it counts moves, whether or not it is inside its gate or target.
Geodesic
The shortest path between two places on the WGS 84 ellipsoid: the true distance between them over ground that curves. Its length is the distance, and its direction where it leaves is the bearing to the second place. hpr’s flight output still measures the landing on a flat map round the pad. See Geodesy.
Geodetic latitude
Latitude as maps and GPS give it: the angle between the equator’s plane and the WGS 84 ellipsoid’s normal through the point, positive north. Gravity depends on it: at the surface it runs from 9.780 m/s² at the equator to 9.832 m/s² at the poles. See Frames and Gravity.
Geopotential height
A height measured by the work done lifting a unit of mass against gravity, divided by standard
gravity g₀ = 9.80665 m/s², in geopotential meters. Weather data give heights this way. Where
gravity is stronger than g₀, a geopotential meter is a little shorter than a meter; hpr turns it
into height above sea level with the site’s own gravity. See
Atmosphere and
ERA5 weather files.
GeoTIFF
A TIFF image whose tags say where on Earth its pixels lie (the OGC GeoTIFF Standard 1.1). In a DEM each pixel is a height. See A launch site’s elevation.
GRIB2
GRIdded Binary, edition 2: the World Meteorological Organization’s binary format for weather on a grid (WMO-No. 306, code FM 92). A file is a run of messages, each holding one variable on one level over the grid, with the values packed as whole numbers and a formula to turn them back. See NOAA forecasts: GFS and RAP.
Grid point
A place where a weather model gives its values. Between grid points hpr blends the four around a site, weighting each more as the site nears it (bilinear interpolation). See NOAA forecasts: GFS and RAP.
Height above sea level (MSL)
Height above mean sea level, as field elevations and soundings give it. hpr queries the atmosphere
and the wind with it, and gets it from ellipsoidal height by subtracting the
geoid undulation N, the height of sea level above the ellipsoid (up to about 100 m). hpr has no
geoid model, so a flight takes N at the site as an input. See
Atmosphere and
Frames.
Hypsometric equation
How thick a layer of air is between two pressures: the geopotential
thickness is (R_d T̄_v / g₀) ln(p_bottom / p_top), with p_bottom and p_top the pressures
at its bottom and top, R_d dry air’s gas constant (287.05 J/(kg·K)), g₀ standard gravity and
T̄_v the layer’s mean virtual temperature (WMO-No. 8, the Guide to
Instruments and Methods of Observation, eqs. 12.17 and 12.18). Warm air makes a thicker layer.
hpr checks each row of a weather-balloon sounding against it; see
Weather-balloon soundings.
Impulse class
The letter that ranks a motor by its total impulse, also called its motor class. Class C covers 5.01 to 10.0 N·s, each letter after it doubles the top of the range, and the upper limits are inclusive. See Solid motors.
Inflation and filling time
How a parachute opens: from deployment, its drag area grows over the filling time
t_f to its full value. hpr can open it at once (RocketPy’s model), over a fixed time, or over
Knacke’s t_f = n D₀/v, with n the canopy’s fill constant, D₀ its nominal diameter and v the
airspeed. hpr leaves out the drag’s overshoot as a canopy fills, and opening at once it ignores how
a light rocket slows while the canopy fills, so the opening load it reports is no safe bound either
way. See Recovery.
Internal momentum
The momentum of the propellant and gas moving inside a burning motor. A thrust curve measured on a test stand already includes its effect. hpr’s equations of motion, like RocketPy’s, add it again, so it is counted twice; hpr keeps it that way so the two codes can be compared like for like. On Valetudo it adds 21 N to the push at liftoff and changes the burnout speed by at most 0.05 m/s. See Rigid-body flight.
Jet damping
The damping of a rocket’s turning by its own exhaust: gas leaving the nozzle carries away some of the rocket’s rotation. hpr includes it through RocketPy’s equations of motion, so it acts only while a motor burns. See Rigid-body flight.
Lambert conformal projection
A map made by wrapping a cone around the Earth, touching it along one latitude, and unrolling it flat. It keeps shapes true locally, so weather models over the mid-latitudes use it for their grids. Its “up” points true north along one line of longitude only, so winds given along the map must be turned to east and north. See NOAA forecasts: GFS and RAP.
Landing ellipse
An ellipse on the ground drawn around the landings of a Monte Carlo run so that it holds a chosen share of them, its level: a 95% ellipse holds about 19 landings in 20. It is centered on their mean, its axes lie along the directions the landings spread most and least, and its size comes from the normal distribution. It is used to judge how likely a rocket is to come down inside a field. See Monte Carlo dispersion.
Launch frame (ENU)
The frame fixed at the launch pad, which the flight’s position and velocity are kept in: x_L
east, y_L north and z_L up along the ellipsoid’s normal at the pad, hence East-North-Up (ENU).
It turns with the Earth, so the equations add the
Coriolis acceleration. It is a flat plane, so z_L is not altitude: 10 km
from the pad the plane is 7.8 m above the ellipsoid. See
Frames.
Liftoff
The moment the rocket starts to move up the rail: the push up the rail, mostly the thrust, first beats the weight’s pull down it and the rail’s friction. The motor ignites a little earlier, at time zero. If the motors burn out first, the flight ends on the pad. See Rigid-body flight.
Loft lesson
A mistake found in Loft, the project that came before hpr-sim, such as Loft lesson L15 (a shoulder’s drag as its length goes to zero). A test here guards against each one, or will once its milestone ships. See Start here and Lessons from Loft.
Lip
A short flare at the very base of a rocket, rising from the end of a boattail or a step down: the Arcas Robin wind-tunnel models end in one, and a motor retainer ring is another. Sitting in the wake of what narrows ahead of it, a lip sees slow, turned air, so hpr takes its drag away there and gives it no normal force faster than sound. See Aerodynamics.
Mach cone
Faster than sound, a disturbance, such as a fin’s tip, can only affect the air downstream of it
inside a cone that opens backward at the Mach angle, atan(1/√(M² − 1)): 30° at Mach 2. hpr
halves the fin’s lift inside the cone from each tip. See
Aerodynamics.
Mach number
Airspeed divided by the local speed of sound, which the atmosphere gives from the air’s temperature. hpr’s normal force and drag both cover Mach 0 to 5, and a flight that reaches Mach 5 stops with an error. Both were checked against a wind tunnel from Mach 0.6 to 4.63, where the drag reads high at most speeds; the drag was also checked at Mach 0.3 against other programs’ curves. See Aerodynamics and Aerodynamics.
Mean aerodynamic chord (MAC)
An average of a fin’s chords (its lengths along the airflow, root to tip), weighted so that the long chords count more: c̄ = (1/A)∫c² dy over the span, A being one fin’s area. hpr puts each fin set’s center of pressure a quarter of the way back along it up to Mach 0.8, and moves it aft from there to the center of the load supersonic linear theory gives. See Aerodynamics.
Metric
One number a validation case compares between hpr and its reference, such as the descent time or the drift to the north. Each metric has its own tolerance, also called its gate. See Accuracy.
MIL-HDBK-762
Design of Aerodynamically Stabilized Free Rockets, a 1990 U.S. Army handbook for designing unguided rockets, and a U.S. Government work. hpr takes its fin-count factor from it, and compares its drag with the handbook’s worked example, a rocket whose drag it calculates term by term from Mach 0.5 to 3.2. That is a calculation by the handbook’s methods, not a measurement. See Aerodynamics.
Milestone
A step of the roadmap, the ordered plan of work, labelled like M1.8 (transonic and supersonic aerodynamics). The pages link a milestone where they say what it will add. See Decisions and the roadmap.
Monte Carlo
Flying the same rocket many times, each time with its uncertain inputs drawn at random, to see how far the results spread: the apogee, the landing. Named after the casino, for the random draws. See Monte Carlo dispersion.
Motor designation
A motor’s name, such as F32 or L1150R: the impulse class letter, then the
average thrust in newtons. Makers add their own codes around it, such as a
propellant letter (the R of L1150R), the total impulse in N·s in front (411I175) or a delay
after a dash. A RASP .eng file’s name field is meant to hold only the class and average thrust,
but often holds the full designation. See RASP .eng files.
netCDF
A file format for gridded data, such as weather over a map at several heights and times, from Unidata. hpr reads its two classic kinds; the newer netCDF-4 kind is HDF5 inside and is converted first. See ERA5 weather files.
Newtonian theory
A simple rule for the pressure on the front of a body in fast flow: the air hits the surface and
loses the speed it had toward it, so the pressure rises with the square of the sine of the angle
between the surface and the wind, C_p = C_p,max sin²δ. C_p,max is the pressure coefficient
at the nose’s stagnation point, where the air comes to rest behind a normal shock. It suits the steep, blunt front of a body
better than its shallow sides. hpr uses it on the cap of a blunt or vertical nose tip faster than
sound, ahead of the shock-expansion method. See
Aerodynamics.
NFPA 1125
The US National Fire Protection Association’s code for making model and high-power rocket motors. ThrustCurve.org measures a motor’s burn time by its rule: from the moment the thrust first reaches 5% of its peak to the moment it last falls to 5%. hpr does the same. See Solid motors.
Nominal area
A parachute canopy’s reference area, S₀ = π D₀²/4, from its nominal diameter D₀; it includes
the vent and every other opening. Knacke’s canopy drag coefficients, which hpr uses, are on this
area: 0.75 to 0.80 for a flat circular canopy, whose middle, 0.775, is hpr’s default. RocketPy’s
default parachute coefficient of 1.4 is on a different area, so the two can’t be compared directly.
See Recovery.
Normal distribution
The bell-shaped spread of a quantity that is the sum of many small, independent effects, described by its mean and its standard deviation. A standard normal number has mean 0 and standard deviation 1; hpr’s Monte Carlo runs scatter each uncertain input by a standard deviation times such a number. Not to be confused with the normal force. See Monte Carlo dispersion.
Normal force
The sideways aerodynamic force on a rocket flying at an angle of attack, square
to its axis and in the plane of the airflow. It acts at the
center of pressure, and its coefficient C_N is positive in the
direction the crossing air pushes the body. It is what makes a stable rocket
weathercock. See Aerodynamics and
Frames.
Normal gravity
The gravity of a smooth, spinning model Earth: the pull of the WGS 84 ellipsoid plus the centrifugal effect of the Earth’s rotation, which changes with latitude and height. By default hpr applies the full normal-gravity vector at the rocket’s position, leaving out the real Earth’s local anomalies, typically within ±1e-4 of it. The standard gravity 9.80665 m/s² is a unit convention, not a model of local gravity. See Gravity.
Normal-force slope
How fast the normal force coefficient grows with
angle of attack at small angles, C_Nα, per radian. Each nose, transition and
fin set has its own, and the rocket’s is their sum; a pointed nose cone’s is 2. The
center of pressure is the components’ positions averaged with these
slopes as weights. See Aerodynamics.
NSGA-II
The non-dominated sorting genetic algorithm II, K. Deb and co-authors’ optimization method for several goals at once. It breeds a population of designs, sorts parents and children into fronts by which designs beat which, and keeps the best half, spread out along the front. It finds a Pareto front rather than one design. See Trade-offs.
Octave band
A range of frequencies, or of wavelengths, whose top is twice its bottom. The turbulence test splits the gust spectrum into octave bands and checks each against the Dryden formula. See Turbulence.
Opening load
The peak force a parachute puts on the rocket as it opens. Knacke writes it as F = (C_D S) q C_x X1: the steady drag at the dynamic pressure q at line stretch, times C_x for the canopy’s overshoot when the load doesn’t slow (1.7 for a flat circular canopy), times X1 for how much the rocket slows while the canopy fills. hpr leaves out the overshoot. With a filling time it already includes the slowing, so X1 must not be applied on top; a canopy that opens at once has none. So the opening load it reports is no safe bound either way: don’t size recovery hardware from it. See Recovery.
OpenRocket
A widely used open-source rocket design and simulation program. hpr may run it as an external program to compare results, but never reads or copies its source code, whose license (GPL) is incompatible with hpr’s. The comparison with it is M2.2, the OpenRocket milestone: its mass comparison is done (M2.2a), and flights come in M2.2d.
Optimization
Searching for the design that makes a chosen number as small (or as large) as it can be: the squared miss from a target apogee, say. hpr’s optimizer is CMA-ES. See Optimization.
Oracle
An independent program run to produce reference values for hpr’s
tests. Usually it is another simulator: RocketPy 1.13.0, and OpenRocket 24.12, so far for mass,
flights, and the reading of .ork design files and .orc parts catalogs. Scripts that
evaluate a published formula in high precision, ThrustCurve.org’s own statistics code, and GDAL
(through rasterio) for elevation files, serve as oracles too; all of them live under validation/oracles/. See
Recovery and the list of simulator oracles.
Override
A measured mass, center of mass or inertia that replaces the value hpr computes, for one part, a part with everything attached to it, or a whole stage. Motors are never covered by one. See The design tree.
A part or a stage can also state its own drag coefficient, in place of the drag its shape gives, as OpenRocket’s Override tab does. See A part’s stated drag coefficient.
Aerodynamic override tables are separate: another program’s drag, or its normal force and center of pressure, flown in place of hpr’s own. See Aerodynamics.
Parallel stage
A stage strapped beside the rocket instead of stacked behind it, such as a set of boosters around a sustainer: copies placed around a body tube, like pods, that burn with the core and drop at a separation of their own. OpenRocket calls it a parallel stage or booster set. See The design tree: Parallel stages and Staging: Boosters beside the core.
Parallel-axis theorem
The rule for moving a moment of inertia from an axis through a part’s own center of mass to a parallel axis: add the part’s mass times the square of the perpendicular distance between the two axes. A part on the rocket’s center line adds nothing to the roll inertia this way, only to pitch and yaw. hpr uses its tensor form to add up the inertias of a rocket’s parts. See Mass properties.
Pareto front
The designs that trade two or more goals off against each other: no design on the front can improve one goal without giving up some of another. A design dominates another when it is no worse in any goal and better in at least one; the front is the designs nothing dominates. Higher apogee against a larger stability margin is one such trade-off. See Trade-offs.
Parquet data page
A Parquet file stores a table column by column, and each column in pieces called data pages, each with a small header saying how many values it holds and how they are stored. A reader decodes a page at a time. hpr-sim’s pages hold up to 1024 numbers of 8 bytes each: 8 KiB, the page size the format’s specification recommends. See Exporting a flight.
Percentile
The value below which a given share of a sample falls: the 5th percentile of 200 apogees is the height that about 10 of them didn’t reach. The 50th is the median. hpr computes them by linear interpolation between the sorted values (Hyndman and Fan’s definition 7, as R and NumPy do). See Monte Carlo dispersion.
Pod
A body mounted beside the airframe rather than on its axis: a side pod, or an outboard pod that holds a motor. hpr repeats one pod, and everything in it, evenly around the axis, turning each copy with its pod as a fin set’s fins turn, and weighs each copy where it sits, so the pods’ inertia is mostly the parallel-axis term. Each pod’s parts fly with their own normal force and drag, once per pod, without the pods’ and the body’s effect on each other’s flow (aerodynamics: Pods). See Pods.
Power-on and power-off drag
Drag while a motor burns, and while the rocket coasts. Under power, hpr subtracts the burning
motors’ cross-section from the base area, as Niskanen does (a rule no measurement has checked; see
base drag), and a drag table can carry separate power-on and power-off curves. A flight uses
power-on drag while any motor burns. OpenRocket 24.12 keeps the whole base under power;
with_full_base_drag_under_power flies its rule, for comparisons with OpenRocket. See Aerodynamics.
Prandtl–Meyer expansion
What happens to a flow faster than sound when its path turns away from itself, as around the shoulder of a boattail: it speeds up and its pressure falls, by an amount that depends only on the Mach number and the angle turned (the Prandtl–Meyer function). hpr uses the pressure after such a turn as the upper limit of a boattail’s wave drag. See Boattails faster than sound.
Probe design
A small design made only to ask an oracle one question, such as “what radius does
OpenRocket give four tubes written auto?”. It is usually a single body tube carrying the one part
being asked about. A probe can be a small file of another kind too: the
parts catalog probes are small .orc files, most
with one part. hpr’s probe designs for OpenRocket are written by scripts under
validation/oracles/openrocket/, and OpenRocket’s answers are committed under
validation/fixtures/ork/, where tests hold hpr to them. See
.ork design files for one set of them.
Product of inertia
An off-diagonal term of the inertia tensor, such as I_yz = −∫ y z dm. It is zero when the mass is balanced about the axes, as in a rocket that is symmetric about its center line, and not zero when mass sits off the axis on one side, like a single side pod. Its sign follows the positive convention in Mass properties. A rocket with products of inertia turns a little about one axis when pushed about another.
Property test
A test that checks a rule on many randomly generated inputs rather than a few chosen ones, such as that every interpolation table passes exactly through its own points. hpr writes them with the proptest library. See Interpolation tables.
QUADPACK
A library of routines for computing integrals numerically, published by Piessens and others in 1983 and in the public domain. hpr’s adaptive quadrature uses its 15-point Gauss–Kronrod rule, without its extrapolation. See Adaptive quadrature.
Rail exit and rail-exit velocity
Rail exit is the moment the rocket leaves the launch rail and starts to fly free; the rail-exit velocity is its speed then. hpr keeps the rocket guided until the aft edge of its aft-most rail button or launch lug passes the top of the rail (RocketPy stops at the forward button), and it leaves with no rotation, since tip-off is not modeled. The slower the exit in a crosswind, the larger the angle of attack just after it, where hpr’s models are least trustworthy. See Rigid-body flight.
Radiosonde
The instrument package a weather balloon carries: it measures pressure, temperature and humidity on the way up, and its drift, tracked by GPS, gives the wind. About 800 stations release one at 00 and 12 UTC each day. What it records is a sounding. See Weather-balloon soundings.
RASAero II
A rocket aerodynamics and flight program. Several of RocketPy’s example rockets carry drag curves labelled as RASAero’s, though only Calisto’s traces to an export. hpr’s drag is compared with those curves from Mach 0.1 to 2.0, with the fin shapes and surface finish guessed, because the curves don’t record them. See Aerodynamics. hpr can also fly the normal force and center of pressure from RASAero II’s exported table (The normal force from RASAero II).
RASP and RockSim files
The two thrust-curve file formats ThrustCurve.org serves. A RASP .eng file is plain text, named after RASP, the rocket simulation program it comes from; a RockSim .rse file is XML, from the RockSim simulator. hpr reads and writes both, and converts one to the other with hpr convert. See Solid motors.
Reanalysis
A record of past weather made by running a weather model over the past, held at each step to the observations of the time (weather balloons, aircraft, satellites, ground stations). It gives the weather everywhere, not just where it was measured. ERA5 is one. See ERA5 weather files.
Reference area
The area every aerodynamic coefficient of the rocket is divided by, A_ref = π d²/4. By default
d is the largest body diameter; it can be set to the nose’s base diameter or to a given value,
and a drag table with its own reference diameter is rescaled to the rocket’s. A drag model of your
own is not (Models of your own). Coefficients from two programs compare only on
the same reference area. See
The design tree.
Reference value and fixture
A reference value is a number hpr’s result is checked against: a value printed in a source, a
worked example, a closed-form answer or an oracle’s output. A fixture is a committed
file under validation/fixtures/ holding reference values and where they came from, written by a
generator under validation/oracles/ or transcribed from print. Tests read fixtures and never
write them: a reference changes only when its generator runs again. See
the validation harness.
Relative and absolute difference
Two ways to say how far a result is from its reference. An absolute difference carries a unit, such as 2e-8 m. A relative difference is the gap as a fraction of the reference value, so 2e-14 relative means two parts in a hundred million million, and a percentage is a relative difference in hundredths. hpr’s pages mark relative differences with the word relative or a percent sign. See Accuracy.
Reynolds number
The ratio of the air’s inertia to its viscosity over a length, R = V L/ν (often written Re),
without units, with V the airspeed, L a length and ν the air’s kinematic viscosity. For skin
friction hpr takes L as the whole rocket, nose tip to the aft end of the last body component, and
treats the flow as fully turbulent. See Aerodynamics.
RocketPy
An open-source (MIT) rocket flight simulator written in Python, and hpr’s main partner for code-to-code comparisons. hpr runs RocketPy 1.13.0, pinned to one commit, to produce its reference values. See Checking a claim.
Roll damping and roll forcing
Roll forcing is the twist that canted fins put on a rocket about its own axis: each fin pushes sideways off the axis. Roll damping is the air’s resistance to the spin: a rolling fin moves sideways through the air and is pushed back. They balance at a steady roll rate that grows with the airspeed. See Roll: forcing and damping.
Roughness length
The height above the ground at which the logarithmic wind law’s wind falls to zero, written z₀. Rougher ground has a larger one: 0.03 m for open flat terrain with grass, and 0.001–0.01 m for mown grass. hpr’s LogLawWind takes it as roughness_length_m. See Wind.
Running median
A filter that replaces each sample of a series with the median of the samples around it: the middle value once they are sorted. A short spike, fewer samples wide than half the window, is outvoted by its neighbours and disappears, while a steady climb or fall passes through unchanged. hpr reads a flight log’s altitude after a 0.3 s running median, which removes the pressure pulse of an ejection charge (Flight-log readings).
Same-drag and predicted mode
Two ways of comparing hpr’s whole flights with another simulator’s. In same-drag mode both codes fly one drag coefficient that the comparison declares, so a difference comes from the equations of motion, the motor or the air, not the drag. In predicted mode hpr works out its own aerodynamics from the rocket’s shape, and the other code flies the drag its example ships (from RASAero, OpenRocket or the team). A difference is then mostly the two drags. Neither drag is known to be right, so those results are held to a target, not a gate. See Accuracy and Accuracy.
Scientific notation
Writing a very small or very large number as a power of ten, the way programs print it. The number after the e says how many places the decimal point moves, to the left when it is negative: 1e-12 is a millionth of a millionth, and 1e6 is a million. 2²⁰ is 2 multiplied by itself 20 times, about a million. See Accuracy.
Seed
A number that starts a random-number generator. The same seed gives the same sequence of numbers, so a run that uses random numbers, such as turbulence or a Monte Carlo run, repeats exactly on the same platform (operating system and processor). The program that runs it chooses the seed. On another platform each random draw can differ in its last binary digit, and over a whole flight such differences can grow. See Turbulence.
Sensitivity analysis
Finding which uncertain inputs move a result most, so you know which to measure carefully and which don’t matter. hpr has Morris’s screening, which ranks inputs from a few runs each through their elementary effects, and Sobol’ indices, which share out the result’s variance. See Sensitivity analysis.
Separation
A stack coming apart for recovery. At its trigger hpr splits the rocket into bodies (body 0 keeps the nose), and each descends on its own under its own recovery devices, which it must have. It adds no impulse, and the aft part’s motors must have burned out. When the forward part still has a motor to burn, it flies on as a sustainer and only the aft part descends. A part leaving at any other joint is an ejection. See Recovery and Staging.
Shear modulus
How hard a material resists being twisted or sheared, G, in pascals (Pa): the shear stress over
the shear strain it causes. Aluminium’s is about 26 GPa, birch plywood’s 0.75 GPa. A fin twists in
its own plane, so its flutter speed needs the shear modulus in that plane. See
Fin flutter.
Shoulder
The sleeve at the end of a nose cone or transition that slides into the next body tube. It counts toward the rocket’s mass, but it is inside the body, so it adds no aerodynamic force. The drag buildup also uses the word for a transition that widens toward the tail, whose pressure drag is counted like a nose’s. See Aerodynamics.
SI units
The International System of Units: meters, kilograms and seconds, and units built from them, such as newtons for force and pascals for pressure. hpr works in SI throughout, with angles in radians. Degrees appear only where values come in or go out, as in Geodetic::from_degrees. In the code, a quantity’s name ends in its unit, such as mass_kg or vertical_speed_m_s. See Frames.
Slender-body theory
A way to work out the air’s forces on a long, thin body from how fast its cross-section grows
along its length. It gives a nose as wide as the reference a normal-force slope of 2 per radian whatever its shape, a
transition 2ΔA/A_ref, and a plain tube nothing, at any Mach number. Barrowman’s method uses it
for every body part. hpr uses it below Mach 1.2, and at every speed for bodies the shock-expansion
method can’t take (steps; a widening shape that isn’t a cone, isn’t flush with the part ahead of
it, or rides in a boattail’s wake; a nose too blunt for its cap); NASA’s wind tunnel shows a real
body lifting more past Mach 3. See
Aerodynamics.
Sobol’ index
The share of a result’s variance that one uncertain input causes: alone (its first-order index), or alone and together with the others (its total index). Named after I. M. Sobol’, who defined them. See Sensitivity analysis.
Sounding
A measured or forecast profile of the air against height: pressure, temperature and wind, and sometimes humidity, as from a weather balloon (a radiosonde) or a forecast service. hpr can fly one in place of the standard atmosphere, which carries on above its top level. Flights above a few kilometers need one, because an offset to the standard for field conditions holds all the way up. See Atmosphere.
Specific impulse
Impulse per unit weight of propellant, I_sp = I/(m_p g₀), in seconds, with g₀ = 9.80665 m/s²:
the effective exhaust velocity divided by g₀. RockSim .rse files
carry it as Isp; hpr works with the exhaust velocity instead. See
Solid motors and
RockSim .rse files.
Stability margin
How far the center of pressure lies behind the center of gravity, usually in calibres. A positive margin turns the rocket’s nose back into the oncoming air when it is disturbed, which in a crosswind is not quite its flight path (weathercocking). It changes through a flight as propellant burns and speed changes. hpr gives it from the rail exit to apogee or the first deployment, with the air along the axis, at Mach 0 (the static margin) and at the flight’s Mach number (the flight margin), and gives none where the parts’ normal forces all but cancel. A rocket with a fin set of one or two fins has a different margin for each direction the air crosses it, and hpr gives the least. See Flight metrics.
Stage
A section of a rocket’s stack in the design tree, listed forward to aft. A separation splits the rocket at the boundary between two stages, so a rocket that stays in one piece needs only one stage. Each motor lights at its own time, so stages can fire in sequence. See The design tree and Staging.
Stall
The pages use the word in two ways. In aerodynamics it is the loss of lift at a large
angle of attack; hpr models none, so it overstates forces at large angles. In
the flight engine it is the rocket’s speed along the rail falling to zero, which ends a flight as
StalledOnRail when it happens after burnout. See
Aerodynamics and
Rigid-body flight.
Standard atmosphere
An agreed model of the air’s temperature, pressure and density against height. hpr uses the 1976 U.S. Standard Atmosphere from −5 to 86 km (288.15 K and 101,325 Pa at sea level). It can be offset to match conditions at the field, or replaced by a sounding. See Atmosphere.
Standard deviation
How widely a set of numbers spreads about its mean: the square root of the mean squared difference from the mean (dividing by one less than the count, for a sample). For a normal distribution about two values in three lie within one standard deviation of the mean, and 95% within two. See Monte Carlo dispersion.
Standard error
The scatter expected by chance in an average taken from a random sample; it shrinks as the sample grows. hpr’s turbulence test requires each band’s average to lie within 4 standard errors of the Dryden formula. See Turbulence.
Station
A position along the rocket, in meters aft of the nose tip, the way design files give positions.
Station s is z_B = −s in the body frame. See
The design tree.
Sting
The rod that holds a wind-tunnel model from behind, entering its base. It changes the air pressure on the base, so a tunnel can’t measure a free-flying rocket’s base drag, only the forebody’s. See Aerodynamics.
Stiff problem
A problem in which some motion is so fast, and so strongly damped, that a method such as Dormand–Prince has to take tiny steps to stay stable. hpr doesn’t detect stiffness: the run stops with an error when its steps get too small or too many. See Time integration.
Stop time
A time the integrator steps to exactly rather than across, because the equations change there: thrust-curve points, burnouts, deployments and the end of a canopy’s filling. A Runge–Kutta step across such a jump loses most of its accuracy. See Time integration and events.
Streamer
A long strip of fabric used instead of a parachute to slow a rocket’s fall. Its drag is taken on its one-side area, length times width. hpr’s default model reads 9% fast on the one flat streamer in Kidwell’s drop tests, and models no pleats, so it predicts a faster descent for a pleated one. See Recovery.
Supersonic linear theory
The small-angle theory of thin surfaces faster than sound: a flat plate at an angle α feels a
pressure proportional to α/√(M² − 1), the same all along its chord. hpr uses it for fins from
the speed where it holds (about Mach 1.2 or later, set by each fin’s sweep and shape). See
Aerodynamics.
Surface layer
The air nearest the ground, where friction with the ground sets how fast the wind grows with height. hpr’s power-law and log-law winds describe it, but keep growing above it, so winds aloft should come from a table of levels. See Wind.
Sustainer
The forward part of a staged rocket, with the nose, that flies on after the booster is dropped and lights its own motor. In hpr a separation whose forward part still has a motor to burn makes it a sustainer, flown in six degrees of freedom with its own shape and mass. See Staging.
Tangent cone
The cone that touches a body along one short element of it: the same half-angle, with its apex on the axis. hpr’s supersonic body method works element by element, and each element’s pressure relaxes toward the pressure on its own tangent cone, looked up in tables that stop at 30°. A cylinder’s tangent cone is the free stream. See Bodies faster than sound.
Tangent ogive
A nose cone whose sides are a circular arc that meets the body tube without a kink: its slope is zero at the base. A 3:1 tangent ogive is three times as long as its base is wide, a fineness ratio of 3. hpr models it as the secant ogive whose arc radius equals the tangent radius. See Shapes.
Thrust curve
A motor’s thrust against time since ignition: (time, thrust) points joined by straight lines, read
from a RASP .eng or RockSim .rse file. hpr starts it from zero thrust at ignition, takes the
thrust as zero from the last point on, and treats two points at the same time as a sudden step. See
Solid motors.
ThrustCurve.org
A public database of motor thrust curves and data. hpr bundles 32 of its curves, and runs its
statistics code to check its own. A file downloaded from it can be read and flown
(A motor from a file); the library can search it and
download a motor’s files itself
(Matching motors to ThrustCurve.org), and
hpr sim and hpr motors fetch fetch a motor the bundled catalog lacks into hpr’s cache
(Motors from ThrustCurve.org). See
Solid motors.
Tip-off
The unwanted turn a rocket picks up as it is let go. Leaving a launch rail, it pivots about its last guide; the recovery page uses the word for a separation too. hpr models neither: a rocket leaves the rail with no rotation, a separated body starts with no spin of its own, and no milestone plans either yet. See Rigid-body flight and Recovery.
Tolerance
How far a result may be from its reference and still pass, such as the 3% every parachute-descent
number is held to. Each test and each validation case states its own, next to
the reason for its size. The integrator’s rtol and atol are tolerances in a second sense: how
much error each adaptive time step may make. See the
validation report and Time integration and events.
Total impulse
A motor’s total push, the area under its thrust curve, I = ∫ F dt, in
newton-seconds (N·s). It sets the impulse class. hpr integrates the straight-line
curve exactly. See Solid motors.
Transonic and supersonic
Flight near the speed of sound (transonic) and above it (supersonic), where shock waves change the drag and the lift. Since M1.8a hpr carries the normal force and center of pressure through both, to Mach 5: supersonic linear theory for the fins, joined to the subsonic method between Mach 0.8 and the speed where that theory holds. Since M1.8b1 (drag through Mach 1) its drag goes to Mach 5 too: Niskanen’s semi-empirical method, with the wave drag of noses and shoulders from closed forms and from Stoney’s 1961 NASA measurements. See Aerodynamics and Aerodynamics.
Tube fin
A short open tube glued along the airframe in place of a flat fin. A tube fin set is a ring of
them around the body. hpr weighs each tube as a hollow cylinder beside the body. A .ork file can
leave the tubes’ radius to OpenRocket. With three tubes or more, OpenRocket makes them just wide
enough to touch the body and each other; one or two take the body’s radius. hpr reads that radius
as OpenRocket does. The aerodynamics flies each tube as a ring wing, below Mach 0.8
(M2.2e9). See
.ork design files,
Mass properties and Aerodynamics.
Tumble recovery
Recovery with nothing deployed: the body falls broadside, tumbling, and its own drag slows it. hpr takes its drag area from the OpenRocket technical documentation’s fit to the fin and side-profile areas, which comes out −10% to +19% off that source’s own drop tests. hpr doesn’t decide by itself when a body tumbles: you give the tumble a trigger. See Recovery.
Turbulence (Dryden)
Random gusts on top of the steady wind. hpr models them with the Dryden spectra of MIL-F-8785C, a military aircraft specification, from a seeded generator that repeats bit for bit for the same seed on one platform. No flight uses turbulence yet, none is planned (issue #39), and Dryden is unvalidated for rockets. See Turbulence.
Validation case
One comparison the validation suite runs: what to fly, which numbers (metrics) to compare, each
with its own tolerance, and against which
reference values. cargo xtask validate runs every case and writes
the committed validation report, with every number and its verdict. See
the validation harness.
Verification and validation
Verification checks that the code does what its model says, against closed-form answers, printed tables and worked examples. Validation checks that the model matches the real world. hpr ranks its evidence in four kinds, named as on Accuracy: analytic (exact answers), published source (printed tables and worked examples), another code (code-to-code comparison), and real flights. See Accuracy.
Variance
The square of the standard deviation: the mean squared distance of a quantity from its mean. Unlike standard deviations, the variances that independent causes contribute add up, which is why Sobol’ indices share out the variance. See Sensitivity analysis.
Vertical datum
The surface heights are measured from. NAVD88, the North American Vertical Datum of 1988, and EGM2008, a worldwide model of the geoid, are both within a meter or two of the geoid (NAVD88, set by levelling, is about half a meter off and tilted about a meter coast to coast: NGS), so hpr takes heights above them as heights above sea level; an ellipsoidal height is measured from the ellipsoid instead. See A launch site’s elevation.
Virtual temperature
The temperature dry air would need to have the density of a humid parcel at the same pressure:
T_v = T / (1 − (e/p)(1 − M_v/M₀)), with e the water vapour’s pressure and M_v/M₀ ≈ 0.622
the ratio of the molar masses of water and dry air (WMO-No. 8, eq. 12.18). Water vapour is
lighter than air, so T_v is a little above T: about 1.6% at 30 °C, saturation and sea-level
pressure. See
Atmosphere.
Wave drag
The drag from the shock waves that form on a rocket at and above the speed of sound. Air meeting a nose, a shoulder that widens or a fin’s leading edge passes through a shock, which raises the pressure pushing back on the surface. Behind a boattail the opposite happens: the air expands around it, its pressure falls, and that pulls back on the boattail. It is much of the steep rise in drag near Mach 1. hpr has no separate term for it: it is part of the pressure drag of each nose, shoulder and step, which Niskanen’s 2009 method carries through Mach 1, and of each boattail (Boattails faster than sound). For fins, hpr uses a blunt leading edge’s formula, which reads far high for thin, sharp fins (Drag limits). See Aerodynamics.
Weathercocking
A stable rocket turning into the wind it feels. Off the rail in a crosswind, the airflow meets the rocket partly from the side, and the normal force, acting behind the center of gravity, swings the nose toward it, so the rocket climbs upwind. In hpr’s test, a 5 m/s wind from the west puts Valetudo’s apogee 86 m upwind. See Rigid-body flight.
WGS 84
The World Geodetic System 1984: the model of the Earth’s shape and gravity that GPS uses. Its
ellipsoid has an equatorial radius a = 6378137.0 m and a flattening 1/f = 298.257223563. hpr’s
latitudes, heights and normal gravity are all on it. See
Geodesy.
Wind direction
hpr gives the wind’s direction the meteorological way: where it blows from, clockwise from true north, so a wind from the west (3π/2 rad, 270°) blows toward the east. RocketPy’s wind heading is where it blows toward, 180° from this. The wind itself is the air’s velocity in the launch frame’s east and north axes. See Wind.
World Magnetic Model (WMM)
The model of the Earth’s main magnetic field that NOAA and the British Geological Survey publish every five years, used by GPS receivers and phones to give compass headings. hpr bundles WMM2025, valid from 2025.0 to 2030.0, and gives the field’s strength, dip and declination anywhere in that time. See The magnetic field.
Writing these pages
This page is the house style for hpr-sim’s documentation. It is for anyone who writes or edits a page of this site, a model page, or a doc comment. It applies to every new page and to every page a change touches. Older pages move to it as they are edited, not all at once.
The aim is simple: a hobby rocketeer who knows some physics and some code, but not this project, reads a page once and knows what it says and how far to trust it.
The rules
- Bottom line first. The first sentence of a page, a section or a paragraph says what it concludes. The detail and the reasoning come after.
- One idea a sentence. Aim for about 20 words a sentence on average. A sentence over 25 words is flagged for a second look; it is not forbidden.
- Short paragraphs. Five sentences at most.
- Numbers carry their meaning. Every number has a unit, and says what it is compared with: “apogee 1.2% above RocketPy’s”, not “1.2% error”.
- One fact, one precision. The same fact is written with the same precision everywhere it appears. If one page gives the real-flight apogee error as 6.04%, no other page rounds it to 6%.
- Labels are links. An internal label, such as a milestone or a decision record, is a link with a few words of meaning, never bare. The site’s checks enforce this one (the documentation site decision record).
- Words before equations. Say in words what an equation does, give the equation, then work an example with real numbers.
- Honesty comes first. These standing rules still apply: a page opens by saying how far to trust it; anything unvalidated says so in its first paragraph; example code runs in CI; and an accuracy number comes from the committed validation report (Checking a claim).
The FusionSpace rules
hpr-sim is a FusionSpace project (FS · SW · TOOL 005), and its pages also follow the product
system’s writing rules
and number rules
(the product system decision record).
They don’t conflict with the rules above; they add these:
-
US English: color, center, meter, catalog, license, analyze, gray. A product or command name keeps the spelling it shipped with, and a quotation or a source’s title keeps its own.
-
No em dashes. Use a colon, a comma or a period. An en dash only in ranges: 5,100–5,500 ft.
-
Both are checked.
cargo xtask spellingflags the UK spelling of eight words, whose US forms are center, meter, catalog, license, color, analyze, behavior and modeling, and names the US word to write; it flags every em dash, in the pages and their code blocks, the crates,xtaskand the schema alike (since M0.6d3). Other UK spellings (labelled, neighbours, maths, grey) are not checked yet. It reads the pages, the README, the scripts, the crates,xtaskand the schema: a page’s prose, each code block as its language reads (arustblock’s comments and strings, atextblock whole), and a source file’s comments and strings. It skips addresses, the frozen decision records and its own word list. -
Names are checked too (since M0.6e): every name in the code, a string that is one bare key such as
"center_x", and each name in the writing (in backticks, joined by_, in a format string). It reportscentre_of_pressure_mas “call itcenter_of_pressure_m”. A quotation, a source’s title, a name that keeps its spelling, or a key that files saved before the rename or another program’s format hold goes on the check’s short list of exceptions, which it counts on every run.--fixreplaces words but not names: a renamed name that is saved to a file keeps its old key as a serde alias, which hpr reads and never writes. An em dash needs its sentence rewritten. CI runs the check in the site step. -
How far to trust it, in a fixed shape, on every result someone might fly on: what kind of figure it is (estimate, measurement, copied value); what it was checked against, with numbers; what to rely on instead when it matters. Never a go/no-go verdict.
-
Signal words are those of ANSI Z535, the US safety-sign standard, and no others: DANGER, WARNING, CAUTION, NOTICE, NOTE, each as a panel above the text with the hazard, the consequence and how to avoid it.
-
Numbers and dates:
rule write a space before the unit, except degrees and percent 21.5 °C,45°,7.0%commas in groups of three in prose; none in terminal output or anything copied 5,104 ft;1280 ftprecision that matches what is known 5,104 ftfrom a barometer, not5,103.87 ftdates in prose; in tables and files October 4, 2026; 2026-10-04 -
FusionSpace is one word.
Older pages move to these rules as they are edited. M0.6, the product system milestone, adds a check for the first two.
Four kinds of page
Pages follow Diátaxis, a common way to sort documentation by what the reader is doing. Each page is one kind; a page that mixes them is split when it is next touched.
| kind | the reader wants to | example on this site |
|---|---|---|
| Tutorial | learn by doing, start to finish | Getting started |
| How-to guide | get one task done | Launch-day weather |
| Reference | look a fact up | The command line |
| Explanation | understand why | How a flight is simulated |
Measured, not gated
Readability will be measured, not used to fail a build. A planned cargo xtask report will
print, for each page, the average sentence length, the share of sentences over 25 words, and how many
internal labels it holds per 100 words. The report will show where to look; a person decides
what to change.
One hard check is planned, and may land in the bookkeeping clean-up (M0.5 milestone). A model page’s In short box, the bulleted summary that opens it (what it models, its sources, how well it is validated, what it leaves out), will hold at most about 150 words and five bullets.
The targets for user pages, measured at Neer’s next check-in: at most 20 words a sentence on average, and fewer than 10% of sentences over 25 words.
Where this comes from
The rules follow the UK Government Digital Service’s style guide, and the sentence-length checks that Datadog and GitLab run with the Vale linter. The page kinds are Daniele Procida’s Diátaxis framework. The decision to adopt them is in the check-in decision record.
Decisions and the roadmap
This page is the way into the project’s three records: the decision log, the roadmap and the
lessons from Loft. They are files in the repository, not pages of this site, because they are
written as the work happens. Every label you meet on the other pages, such as a decision record
(ADR- and a number), a milestone (M and a number) or a Loft lesson, links into one of them. For
how hpr behaves today, trust the model pages; the records say why it behaves that way and what
comes next.
Decision records
A decision record, or ADR (architecture decision record), explains one significant choice: the
problem, the options that were considered, what was chosen and why, and what it costs. Records
are numbered in order and never renumbered. A record is not rewritten when a choice changes; a
new record replaces it and points back. Each record is a file of its own; the decision log
is their index, one row per record with its summary and status. The table below gives each one in
a line and is kept by a command (cargo xtask records), so a new record appears here as soon as
it is written. From the 126th record on, a row shows the record’s own summary, which is more technical,
until someone rewrites it in plainer words.
| record | what it decides | where it shows |
|---|---|---|
| ADR-000: Kickoff decisions | A Rust library first; physics and validation before interfaces; the license; a clean room (no GPL source is read); commercial solid motors only | Start here |
| ADR-001: License and workspace layout | MIT OR Apache-2.0, and how the code is split into crates | Start here |
| ADR-002: The reference library | How published sources and reference programs are pinned by hash, fetched and checked | Checking a claim |
| ADR-003: Frames, attitude, geodesy and gravity | The axes and sign conventions, WGS 84 gravity, and the Earth’s rotation | Frames, Gravity |
| ADR-004: Atmosphere, wind and turbulence | The standard atmosphere, wind by height above sea level, and seeded turbulence | Atmosphere, Wind, Turbulence |
| ADR-005: Solid motors | How a thrust curve becomes impulse, mass and inertia over the burn, and which curves are bundled | Solid motors |
| ADR-006: Component geometry and mass properties | How each part’s shape, walls, fins and material give its mass, center of gravity and inertia | Mass properties, Shapes |
| ADR-007: The design tree | Where parts sit, automatic sizes, overrides, motor mounts and design checks | Design tree |
| ADR-008: Subsonic normal force and centre of pressure | The body and fin lift models behind the center of pressure | Aerodynamics |
| ADR-009: Subsonic drag | The drag buildup, surface finishes and drag tables that override it | Aerodynamics |
| ADR-010: Time integration | The adaptive integrator, and how events such as burnout and apogee are found | Time integration |
| ADR-011: Rigid-body flight | The equations of motion, the launch rail, the phases of a flight and when it stops | Rigid-body flight |
| ADR-012: Recovery | Parachute drag areas, when devices open, how they fill, and the descent | Recovery |
| ADR-013: Streamer and tumble drag | Which published data sets give a streamer’s and a tumbling body’s drag | Recovery |
| ADR-014: Separation | How a rocket splits into bodies that each descend on their own | Recovery |
| ADR-015: The validation harness | How comparisons with other programs are run, gated and reported | Accuracy, Checking a claim |
| ADR-016: The documentation site | This site: how it is built, and the checks every page passes | Start here |
| ADR-017: In short, traced numbers and this page | How every model page opens, how Accuracy’s numbers are checked against their sources, and why these records are files rather than pages | Accuracy |
| ADR-018: Examples and quotes | Every example program runs in CI and must print its committed output, and a page’s quote of a file must match it line for line | Getting started |
| ADR-019: Publishing the site | How this site and the API reference are built together, link each other, and are published from main | The API reference |
| ADR-020: The reader test | How the site was tested on a new reader, and why every milestone and lesson label links a row of plain words on this page | Decisions and the roadmap |
| ADR-021: Whole flights against RocketPy | What a whole-flight comparison measures and how, why a sixth rocket was added, and how a case declares a limit of hpr’s as a known gap | Accuracy |
| ADR-022: Validation in CI, and regenerating references only by hand | How every pull request reruns the validation cases on three operating systems, and why the references change only when a person regenerates them and reviews the diff | Checking a claim |
| ADR-023: Predicted mode | How hpr’s own aerodynamics are compared with RocketPy flying each example’s own drag, and why those results are reported against a target rather than gated | Accuracy |
| ADR-024: The time-series RMS | How each whole flight’s height and speed over time are compared with RocketPy’s, on what clock, and why each is held to 3% of its apogee or max speed | Accuracy |
| ADR-025: The calm-air cases | Juno III, Calisto and Bella Lui flown with no wind, and why Juno III’s drifts were at first reported but not scored: the two codes free the rocket from the rail at different points | Accuracy |
| ADR-026: The path in wind | Why hpr turned into the wind less than RocketPy: RocketPy’s equations took the turning moments about the wrong point during the burn (corrected upstream, and in the comparison), and hpr’s body lift, which RocketPy leaves out, pushes a slow rocket downwind | Accuracy |
| ADR-027: The normal force through Mach 1 | How the fins’ normal force and center of pressure carry through Mach 1: Barrowman’s subsonic method to Mach 0.8, supersonic linear theory once it holds, a straight-line join between, and how they compare with a wind tunnel and with RASAero II | Aerodynamics |
| ADR-028: Drag through Mach 1 | How noses, shoulders and steps drag through Mach 1: Niskanen’s method with Stoney’s measured nose curves, and how the drag compares with NASA’s wind tunnel | Aerodynamics |
| ADR-029: Drag against RASAero II through Mach 2 | How hpr’s drag compares with RASAero II’s curves by speed band, why it misses faster than sound, and a second reference with every input known: MIL-HDBK-762’s worked example | Aerodynamics |
| ADR-030: The afterbody faster than sound | A boattail’s supersonic wave drag from MIL-HDBK-762’s chart held to the Prandtl–Meyer limit, separation on steep boattails, the base pressure behind them, and a lip in a boattail’s wake; checked against measured boattails, and why its targets aren’t met | Aerodynamics |
| ADR-031: Roll from canted fins | Roll forcing from canted fins and roll damping by Barrowman’s strip theory with his body factors, the fin’s own slope in the damping, and the comparison with NASA’s measured roll effectiveness and the Basic Finner’s roll damping | Aerodynamics |
| ADR-032: Normal-force overrides | Flying RASAero II’s normal force and center of pressure: how its export is read, the slope at 0°, the angles past its last, and why the damping stays hpr’s | Aerodynamics |
| ADR-033: The body faster than sound | The lift a body’s cylinder carries behind its nose faster than sound, by the second-order shock-expansion method: the report’s tangent body, its limit, and the cone slopes read by hand | Aerodynamics |
| ADR-034: The body’s supersonic normal force in flight | How a flight uses that method: a table of each part’s share every 0.05 in Mach, joined in a straight line from slender-body theory over Mach 1.2 to 1.5 | Aerodynamics |
| ADR-035: Drop the orhelper dependency | The GPL-2.0 wrapper for the OpenRocket jar is removed from the oracle environment, unused; how the OpenRocket oracle milestone drives the jar is decided when it starts | How correctness is proven |
| ADR-036: The Arcas Robin’s supersonic body gap | How hpr’s body faster than sound is compared with NASA’s wind tunnel from now on (as the tunnel measures, at its angles), and why M1.8e6 takes the size of crossflow lift and the boattail’s share together | Aerodynamics |
| ADR-037: Body lift by Jorgensen’s crossflow | Body lift’s size at every speed from Jorgensen’s crossflow drag, how his two η figures are combined, a boattail’s share faster than sound from Washington and Pettis’s measurements, and why blunt tips moved to M1.8e7 | Aerodynamics |
| ADR-038: Blunt tips by a Newtonian cap | How a nose with a blunt or vertical tip (power-series, Haack, elliptical) flies the shock-expansion method faster than sound: NASA TN D-4865’s Newtonian cap and handover, the method started behind it from the tangent cone rather than the report’s own start, and how both were checked | Aerodynamics |
| ADR-039: A lip in a boattail’s wake carries nothing | Why a short flare at the very base, behind a boattail, gets no normal force faster than sound, what the tunnel’s pitching moment says about that, and how far the shelter reaches | Aerodynamics |
| ADR-040: A steep boattail’s correlation, and the 15% target judged | How steep a boattail hpr still reads its measured share for, how footnote 8 and the tube behind it were integrated by hand, and where the Arcas Robin’s body alone still misses its 15% target | Aerodynamics |
| ADR-041: A lip’s shelter weighed, not switched | Why a lip rising out of its boattail’s wake now moves a rocket between the two supersonic body models smoothly, and how big the switches that remain are | Aerodynamics |
| ADR-042: Cone slopes past Fig. 2’s edge, from Sims | Where the slopes for tangent cones of 24° to 30° come from, how they were checked against the chart they extend, and what a cone steeper than 30° still costs | Aerodynamics |
| ADR-043: The blunt tip’s handover cap | What moving a blunt tip’s handover from 24° to the cone tables’ 30° would be worth on the report’s own sphere-cone, what it does to the march on a nose that flattens fast, and why the cap stays where it is | Aerodynamics |
| ADR-044: What the answer follows when it follows the mesh | That it is the surface pressure crossing its tangent cone’s, not the method being reduced, that marks a reading whose answer moves with the element count: how far that goes, and what it re-aims issue #108 at | Aerodynamics |
| ADR-045: Where a flare’s march stops | That what stops the march is the corner’s isentropic turn, not the flare’s shock detaching; that the two limits cross near Mach 1.55, so neither bounds the other; and that above Mach 2.13 neither binds: the cone tables’ 30° does | Aerodynamics |
| ADR-046: Debrief folded in, and analysis that stands on its own | That a universal flight log analyzer becomes part of this project, and that reading a log never needs the simulator: hpr-flightdata may not depend on hpr-sim, a check enforces it, and comparing a flight with a simulation lives in hpr-forensics | Start here What the analyzer is being built from is written up in the log formats, the readings and what may be ported. |
| ADR-047: A flare flies the method where its corner’s shock is attached | That a flared body now flies the shock-expansion method; that the test for the corner’s shock is NACA 1135’s wedge limit read at the flow reaching it, the same one a blunt tip’s cap already uses; that a steeper flare is read as one of the same radii drawn out to that limit, so nothing jumps across the boundary; and what still switches: a band of flares a third of a millimeter tall the march refuses (read through since ADR-050) | Aerodynamics |
| ADR-048: What a marched flare is worth | That TN D-4865 model 2’s readings are committed and compared: an 18.5° flare read −1.9% and +7.0% against the wind tunnel at Mach 1.9 and 2.3, +13.4% at 2.96 and about +51% at 3.95 and 4.63, whose flare the report’s shadowgraphs show separated; with no reading at all below Mach 1.5289, where drawing the flare out to its shock’s limit lands past the march’s own | Aerodynamics |
| ADR-049: What a step in radius costs | That a step’s cost is measured and published rather than modeled: it takes the whole body off the shock-expansion method, worth −8.65% and 1.03 calibres at its threshold and −12.55% and 1.36 calibres at a 2 mm step down, and −11.34% and 1.10 calibres on a boattailed body, whose threshold is 1.3e−13 m rather than 2.7e−11 m; and that stopping the march at the step instead was built, measured and rejected, because the mixed reading lands outside both pure models and misses the boattail’s band | Aerodynamics |
| ADR-050: A reduced element read by the generalized method | That where the second-order method’s exponential form cannot hold (the pressure behind a corner on the far side of its tangent cone’s from where its own gradient points), the element is read by the generalized method wherever it has a tangent cone of its own, so a near-flat flare no longer takes the whole body off the method; that the region’s two edges are solved from the corner’s own state rather than bisected, reproducing all three published angles; and that what is left is the loading’s step at the crossing, +0.129% and 0.0051 calibres on the tests’ rocket but not bounded by it: +4.3% and 0.19 calibres on a body with a short shoulder, and −2.8% between two adjacent Mach rows | Aerodynamics |
ADR-051: M3.1 split, and a .ork document kept whole | That reading an OpenRocket file is split into four increments, the container and the document first; that the document is read into a tree and interpreted by nobody, because with no schema for .ork keeping the whole file is the only way to be sure nothing was dropped; that reading it, writing it and reading it again gives the same document, checked over generated trees and over all 76 corpus files that open; that nesting is counted before the text is parsed, since the XML parser underneath overflows the stack past 120 levels; and that the corpus survey names the two files that are not XML rather than skipping them quietly | OpenRocket .ork design files |
ADR-052: What a .ork value means | That an automatic dimension keeps both its flag and the number OpenRocket last worked out, rather than becoming a hand-typed one; that either name may be read of the two renames that are only renames, because where OpenRocket writes both it agrees with itself on the number and on the frame every time (642 and 109 elements), while two more pairs that look the same are left unread, their newer name carrying a frame the older never does; that a stated zero is a value, which is what an override to no drag at all needs; and that the single flag the three subcomponent-override flags replaced is read as setting all three, out loud, since no file carries both forms (the warning since dropped by ADR-095, which measured OpenRocket’s reading) | .ork design files |
ADR-053: The parts on and inside a .ork body | That .ork angles are degrees, which nothing in the file says and 178 of its 188 non-zero ones show by exceeding a whole turn; that radialposition and radiusoffset are each read on the parts that carry them, which no element carries both of, so the question ADR-052 left open need not be answered to read them; that a part this reader cannot shape honestly is left out with its reason rather than guessed at (5 in the corpus); that a tube of no wall carries no mass, which is OpenRocket’s own geometry, where a body component’s ambiguous zero is read as solid; that an inner tube’s automatic outer radius is its parent’s bore, resolved in a pass before any ring’s bore so no answer depends on sibling order; and that the five surface-finish words take the roughness heights OpenRocket’s author published, with polished flagged as possibly moved in a newer OpenRocket (the zero-wall reading was replaced by ADR-061: every part of no wall weighs nothing) | .ork design files |
| ADR-054: An automatic radius with nothing to take | That an automatic body radius with no fixed radius anywhere along its chain takes OpenRocket’s default radius, 25 mm, which OpenRocket’s maintainers call “the default radius” and OpenRocket 24.12 settles on in a committed oracle run, never the number cached after auto, which OpenRocket itself ignores; that hpr departs from OpenRocket only where OpenRocket answers −1 m, a radius no shape can have; that hpr is held to OpenRocket’s settled answer rather than its first reading, which settles the Dual parachute example’s odd cache in hpr’s favour (67 of 67 body radii agree over 18 designs); and that a <rocket> holding nothing is a document with no design rather than a design that fails | .ork design files |
ADR-055: M3.1c split, and the motors a .ork flies | That reading a .ork file’s motors, recovery, stored results and pods is split into four increments. That a motor’s thrust curve comes from the file first and hpr’s bundled catalog second, matched on manufacturer and designation both. That none is a plugged motor and 0 a charge at burnout. And that the rocket flies a configuration only when every motor in it has a curve and lights at launch, on a one-stage rocket read without a warning, because hpr lights every motor at launch and flies one body until staging arrives: 2 of the library’s 170 configurations do | .ork design files |
ADR-056: A .ork design’s recovery and separation | That when each parachute and streamer opens, and when each stage separates, is read as the file wrote it and not yet flown. That the words are OpenRocket’s own, measured by a committed probe. That a deploy height is above the ground. And that two choices are left open, in plain view, for the step that flies a .ork: a deploy height the rocket never reaches, which OpenRocket never opens, and an automatic drag coefficient | .ork design files |
ADR-057: A .ork design’s stored simulations | That the simulations OpenRocket last ran on a design are read back as it wrote them: the launch conditions, the summary, and each stage’s time series and events. That their units are measured by a committed probe: the rod’s angle and direction in degrees, a compass bearing; the wind’s direction in radians, where it blows from. And that they are OpenRocket’s answers to compare against, not flights hpr makes | .ork design files |
ADR-058: What a .ork holds that hpr does not model | That the parts and sections of a .ork hpr does not read (pods, parallel stages, OpenRocket’s 3D-view settings, a simulation’s plug-ins) are kept whole beside the design, in an extension called x-openrocket, at a path that leads back to where each was, so that writing the file back can put them back; that a design missing parts this way says it is reduced; and that the same goes for every tag and attribute no reader asks for, recorded as hpr reads | .ork design files |
| ADR-059: The RocketSerializer cross-check | That hpr’s reading of a design’s key geometry (the nose cone, transitions, fin sets, where each sits, and the body radius) is held to RocketSerializer’s, a second program that reads .ork files, with OpenRocket itself run on the same file to settle any difference; that “agrees” means no number of hpr’s is apart from both; that a cause is named only where the record proves it; and that an import error is a file that does not read or a design that does not lay out | .ork design files |
| ADR-060: M2.2 split, and the structure’s mass held to OpenRocket’s | The OpenRocket comparison goes mass first, then OpenRocket’s mass conventions, the motors OpenRocket flies, flights on public designs, and the corpus. Each design’s structure is held to OpenRocket’s within 1% in mass and 1% of length in center of mass, thresholds set before measuring, and every design outside is given its cause. Which of OpenRocket’s inertias is roll is measured on a tube worked out by hand | Mass properties |
ADR-061: What a .ork leaves unsaid, read as OpenRocket reads it | Probe designs measure what OpenRocket makes of a wall or shoulder of no thickness (nothing), a part with no thickness written (a 2 mm wall) or no material (its default by kind), and which override wins. hpr reads each the same way. Two override rules stay hpr’s own, each pinned by a test: the center under a mass override that covers the parts inside, and inertia under an override | Mass properties |
| ADR-062: Fins and rail buttons against OpenRocket; roll inertia explained | Probes of one tube and one part find the fins behind the roll inertia’s gap: OpenRocket’s shortcut for a fin set, inferred from its output, accounts for the corpus’s 2.1%, and hpr keeps its exact integral. hpr keeps its rounded and airfoil sections and its exact ellipse, each pinned against OpenRocket’s; a .ork rail button now sits where OpenRocket puts it (issue #151). M2.2b2, the fins and rail buttons, split their remaining clusters, fillets and unread parts into M2.2b4 | Mass properties |
| ADR-063: Packed parts read and weighed as OpenRocket packs them | A parachute, streamer, shock cord or mass component whose file writes no packed size is 25 mm long and 12.5 mm in radius, as OpenRocket packs it, and a mass override on one that weighs nothing is spread over its packing, not put at a point; both measured on probe designs | Mass properties |
| ADR-064: Clusters, fillets and unread parts remain visible departures | A 3-ring cluster is read as one tube and pinned as a measured departure (superseded for clusters by ADR-075); 5 mm and 10 mm fin fillets are omitted and pinned (superseded for fillets by ADR-096); unread parts remain in x-openrocket and mark the design reduced. The 2026-09-23 scratch-excluding corpus rerun gives 58 of 71 within 1% in mass, 59 in center, 50 in pitch inertia and 56 in roll with OpenRocket’s fin rule | Mass properties |
| ADR-065: Stored results are references only when current and structurally plausible | Stored results remain readable, but only current runs with RK4Simulator and BarrowmanCalculator provenance markers and complete finite, internally consistent ascent summaries pass the stored-reference screen; the 2026-09-25 survey found 91 of 174 eligible and 83 excluded (47 inconsistent, 17 external, 11 outdated, 7 not-simulated and 1 missing simulator). Hpr reproduction is separate: 1 of those 91 is reproducible and 90 are not (40 reproducible once ADR-067 supplies OpenRocket’s motor database). | .ork stored simulations |
| ADR-066: Every curve hpr flies is integrated as OpenRocket integrates it | The oracle hands OpenRocket the same bytes hpr reads, so the comparison is of two integrations of one file: hpr’s total impulse, peak thrust, 5% burn-time window and curve duration are all bit for bit OpenRocket 24.12’s on all 32 bundled curves. The one real difference is the average thrust, hpr’s being +0.0107% to +0.3147% higher because its numerator is the whole curve’s impulse where OpenRocket’s is the window’s: a written departure, not held. The reference library’s own curves stay private, counted in M2.2c2. | Solid motors: validation |
| ADR-067: Curves come from OpenRocket’s own database by digest, each held to its impulse | Most designs name their motor’s curve by its digest and do not carry it. An oracle records the motor database OpenRocket 24.12 ships, and cargo xtask ork supplies each solid curve for its digest, never by name. Every curve’s total impulse is within 0.1% of OpenRocket’s, and bit for bit: the 3 the library embeds are two independent readings, and the 1,288 database curves check the hand-off. In all 67 configurations hpr flies with a supplied curve, OpenRocket places each supplied curve too. Of the 162 configurations the bundled catalog left without a curve, 66 now fly, 72 are held back for another named reason and 24 still have none, each with its reason. Where each motor’s weight sits still differs from OpenRocket’s. | The .ork format: motors |
| ADR-068: OpenRocket’s flights of the public designs, and what its metric words mean | M2.2d, flights against OpenRocket, splits in two. OpenRocket 24.12 flies the public designs in calm air: 56 complete flights and one aborted run, which is no reference. A record keeps each summary word beside the quantities of its own time series: nine of ten words are defined, among them the deployment speed as the last deployment’s; the optimum delay is not measured and is withheld, as are other versions’ words (L80). A metric whose event is missing is never scored: withheld when neither flight had the event, failed when only one did (L81). | .ork: what the summary words mean |
| ADR-069: hpr’s flights of the public designs against OpenRocket’s | M2.2d2: cargo xtask ork-flights flies the 21 configurations of OpenRocket’s examples that hpr can fly, in OpenRocket’s recorded conditions, and a committed report sets each against OpenRocket’s apogee, largest speed and margin at rod clearance, by ADR-068’s definitions. The margins are within 0.016 calibres. With no named cause, hpr’s apogees are 0.06% to 4.34% low. Each apogee more than 5% off has a named cause: a parachute OpenRocket opened before apogee (hpr flies none from a .ork yet), or a part set to no drag, which hpr ignores (#165). | .ork: hpr’s flights against OpenRocket’s |
| ADR-070: M2.2e split: mass and centre of mass first, then the corpus | M2.2e is cut into four: M2.2e1 adds the spread of hpr’s mass and center of mass against OpenRocket’s to the flight report; then OpenRocket flies the private designs (the corpus), hpr flies them, and each apogee more than 5% off gets a written cause. On the 21 public flights the launch mass is within 0.21% and the center of mass within 0.016 calibres. | .ork: hpr’s flights against OpenRocket’s |
ADR-071: The corpus OpenRocket flies is its .ork files | M2.2e2 flies the private library’s 27 .ork files: 88 of 89 configurations to the end. Its 4 RASAero and 4 RockSim files wait until hpr reads those formats. | .ork: OpenRocket’s flights of the private designs |
| ADR-072: hpr’s flights of the private library, under anonymised ids | M2.2e3 tries the 12 private designs that are not copies of public ones and publishes only differences, under ids like C09/2. hpr flies 17 configurations of 4 of them, so the two reports hold 9 designs, not the 20 that M2.2 (the OpenRocket comparison) asks for. That bar is now M2.2e9 (ADR-094 split the tilted rod off as M2.2e5, which added one design, and ADR-095 the old override flag as M2.2e6, another). The rest can come from the staging and cluster designs (M1.9), the airframe readings (#174, two designs left), or the four public designs held by pods and parallel stages (OpenRocket’s pod examples, unflown for reasons outside the pods though M1.13 is done) or tube fins (#133). | .ork: hpr’s flights of the private designs |
| ADR-073: Each named cause sized by OpenRocket’s own flight without it | M2.2e4 sizes the two causes named for the five apogees more than 5% from OpenRocket’s: OpenRocket flies each flight again with nothing deployed and, where a part is told it has no drag, with that setting cleared (matching what hpr flies, since hpr reads the setting but can’t apply it yet) or the part removed. Four of the five come within 5% of every such flight. The fifth stays 7.80% high like for like (the no-drag part removed from both programs), and what is left on its design has a lead, not an explanation (#177, a very blunt nose’s drag). | .ork: the two named causes, and their size |
| ADR-074: Ignition times and powered staging: the sustainer flies on as a rigid body | M1.9a lets each motor light at its own time (at launch, at a time, after another motor’s burnout, or after its stage’s separation). A separation with the forward part still to burn is powered: that part, the sustainer, flies on with its own shape and mass while the booster falls to its own landing. The sustainer keeps the nose, so its aerodynamics are an ordinary rocket’s; nothing needs a model of a rocket without a nose. Checked by tests, not yet against another simulator. | Staging |
| ADR-075: A cluster is one tube repeated, and a motor in it one motor per tube | M1.9b makes a cluster a list of tube places on one inner tube: the tube and what it holds are repeated in each, and its motor is one motor per tube, so thrust, mass and moments add up as for any motors. A tube can be marked as a motor out. A .ork file’s pattern is read as OpenRocket places it, measured on 25 probes; OpenRocket weighs the tubes stacked on the cluster’s axis, which hpr does not copy. | Clusters |
ADR-076: A .ork file’s ignitions and one powered separation flown against OpenRocket | M1.9c reads each motor’s ignition in a .ork file into hpr’s (at launch, at a time, or after the stage below burns out or fires its charge) and flies one powered separation, a cluster with a motor in every tube, and an air start. The tolerance, set before measuring: every flight of a design within 5% of OpenRocket’s apogee and largest speed. OpenRocket’s two-stage, cluster and air-start examples all are (three cluster apogees against OpenRocket’s flight with no parachute, because its parachute opened before apogee) | Staging |
| ADR-077: Flight metrics: peaks on the dense output, margins only where they mean something | M1.10a finds each peak (speed, Mach number, dynamic pressure, the boost’s acceleration and, apart, the opening shock) between the integrator’s steps rather than in a recorded table. It keeps the static margin and the margin at the flight’s Mach number from the rail exit to apogee, and gives none where the parts’ normal-force slopes nearly cancel, since the margin is then noise. The optimum delay comes from a flight with the charges held, so it doesn’t depend on the delay flown. Anything that didn’t happen is None. Tests pin each against a hand calculation; nothing is checked against a real flight. | Flight metrics |
| ADR-078: Fin flutter by NACA TN 4197, the lower reading wherever the source leaves room | M1.10b gives a fin’s flutter speed and margin by Martin’s 1958 criterion, which reproduces both of his worked examples at the precision he printed them. It fixes a flutter dynamic pressure, so a flight’s least margin is at its max q. Martin’s own line between safe and failed wings, a band measured off his figure 3, puts the flutter speed at 1.8 to 2 times the speed of sound, so a flutter speed just above the flight’s speed is not shown to be safe. Where the source allows two readings (the thickness ratio, a flat fin’s stiffness), hpr takes the one with the lower flutter speed. 14 built-in materials carry a shear modulus from a cited source; G10/FR-4 has none. Nothing is checked against a hobby rocket’s flight. | Fin flutter |
| ADR-079: Exports as text built in the core, heights on each format’s own datum | M1.10c1 writes a recording as CSV or JSON, and the flight path and landings as GeoJSON or KML. Each function returns text and writes no file. Numbers are written so they read back exactly; a value that isn’t finite is refused. GeoJSON heights are above the WGS 84 ellipsoid and KML heights above sea level, as their standards say. GeoJSON is checked against the published schema and KML by parsing. Parquet is split off to M1.10c2. | Exporting a flight |
| ADR-080: Parquet written in-house, read back by Apache’s library | M1.10c2 writes a recording as an Apache Parquet file, behind the parquet cargo feature. hpr-sim writes it from the format’s specification, with no added library, and the tests read it back with Apache’s own Parquet library, every number bit for bit. The file is uncompressed and has no per-page statistics; pyarrow and DuckDB read it once, by hand, not in CI. | Exporting a flight |
ADR-081: ERA5 weather read from netCDF classic in hpr-io; M2.3 split a to c | M2.3a reads the weather over a launch site from an ERA5 file. hpr reads netCDF classic files itself, from Unidata’s specification; the newer netCDF-4 files are converted first with three lines of Python. Values are interpolated between the four grid points around the site as RocketPy does, and between the two hours around the launch, where RocketPy takes the nearer hour. Heights use the World Meteorological Organization’s formula at the site’s latitude, where RocketPy uses ECMWF’s; both are approximations, and the page compares them. Real flights in that weather are M2.3b, and the private designs with flight logs M2.3c. | ERA5 weather files |
| ADR-082: Real flights read from refs, compared over the ascent, with checked explanations | M2.3b flies seven of RocketPy’s documented rockets in the weather of their day, with hpr’s own aerodynamics and each example’s own motor file, and compares each with its team’s altitude log: the apogee, and the climb to it. hpr’s height is read the way the log’s barometric altimeter reads the air, which on a hot day is several per cent below the height climbed. The logs and motor files are other people’s, so they are read from a pinned copy and only the numbers are committed. The mean absolute apogee error is reported against a 5% target; each flight outside it carries an explanation that the report checks against its own numbers: a second flight on the team’s own drag, or on the thrust file as recorded. | Accuracy: real flights |
| ADR-083: M2.3c blocked: no private design is the rocket of a logged flight | M2.3c was to fly the private designs that have a flight log. None does: the private logs match no design in the private library, and the only designs there with a logged rocket are copies of RocketPy’s public examples, whose logs are RocketPy’s and were already flown in M2.3b. One design shares a source with some logs, but nothing ties it to any of those flights, and a comparison against a guessed pair would measure nothing. So it waits for a design and its flight’s log to be added to the private data, and work moves on to M2.4. | Accuracy: real flights |
| ADR-084: The accuracy census: the reports’ numbers held to the ones accepted | M2.4 counts the numbers the validation reports hold hpr to, once each, naming for each group what it was compared with, the kind of comparison, how many flights and how fast they flew. The census accepted last is committed, and CI holds each of those numbers to it: one that moves by more than 0.1% of its bar, better or worse, fails until the change is accepted with a written reason. That catches what a regenerated report could otherwise carry in, such as hpr’s own drag getting worse inside a target. | Accuracy: the census |
| ADR-085: Ejected pieces: an airframe that parts at any joint | M1.11a lets the airframe part anywhere: at the joint aft of any body component, as a nose cone pushed off does, or around a payload carried inside. Each piece flies on to its own landing under its own parachute. The pieces are fixed before the flight, so a body’s number never depends on which trigger fires first, and the nose’s body is always body 0. Each parting adds no impulse, so masses and momenta add up. An override that doesn’t say how its mass divides between pieces is refused rather than guessed. The push of an ejection charge, and a tumbling piece, are M1.11b (ADR-086). | Recovery: ejected pieces |
| ADR-086: Ejection impulse and tumbling pieces | M1.11b lets an ejection push its two sides apart with an impulse, equal and opposite, so each changes velocity by the impulse over its own mass and the momentum is unchanged. While the airframe flies whole with nothing open the push is along its axis. A body with no attitude to go by is assumed to point against its velocity through the air when it hangs from an open parachute, and along it when nothing is open. A piece can tumble by the tumble model over its own parts, and that model now integrates a curved nose’s side area rather than taking its end diameters. | Recovery: ejected pieces |
| ADR-087: Mass that moves along the airframe | M1.12a lets ballast or a payload slide along the airframe during the flight, on the triggers a parachute has, along a smooth curve that starts and ends at rest. The rocket’s center of mass and inertia follow it exactly. The equations of motion gain one term, the moving part’s angular momentum relative to the airframe, which is zero for a part on the axis; its rates are exact rather than differenced. A mass released in flight is M1.12b. | Moving mass |
| ADR-088: Mass released in flight | M1.12b lets ballast or a payload go during the flight, on the triggers a parachute has. The rest of the rocket flies on in six degrees of freedom from the same state with its mass, center of mass and inertia stepped to the rest’s; the part leaves with the velocity it had in the airframe, so mass and momentum are kept, and falls to the ground as a point mass under a drag area the user gives. | Released mass |
| ADR-089: A pod is a stack of body components repeated around the axis (superseded for aerodynamics and tumbling by ADR-092) | M1.13a adds pods: a pod set on a body tube holds the pod’s own body components, which stack along the pod, and hpr repeats that one pod, with everything in it, evenly around the axis, each copy turned with its pod as a fin set’s fins are. Each copy is weighed where it sits, with its parallel-axis term, and a motor in a pod is one motor per pod. The aerodynamics and the tumble model refuse pods until a cited method for their normal force and drag is in (M1.13c). | Pods |
ADR-090: .ork pods placed as OpenRocket places them; pods of no length left out | M1.13b1 reads a pod set’s pods and puts them where OpenRocket 24.12 does, measured on probes: a relative offset is from the tube’s surface to the pod’s widest part. A flipped nose cone is read as the tail cone it is. Pods of no length, drawn to hang fins off the axis, were left out with a warning until M1.13b2 read them (ADR-091). | .ork: Pods |
| ADR-091: A pod of no length weighs nothing and holds its parts on its axis | M1.13b2 reads the pods OpenRocket draws to hang winglets or a lug off the axis: a pod whose only part is a tube of no length, no wall and usually no radius. That tube weighs nothing, and fins or a lug on it sit on the pod’s own axis, as OpenRocket 24.12 places them on probes. A pod set that holds nothing is read, and weighs nothing. | .ork: Pods, Pods |
| ADR-092: A pod’s parts are Barrowman’s, once per pod, on the axis | M1.13c1 flies pods: each pod’s nose cones, transitions and tubes get the normal force and drag they would have on the airframe, at the pod’s own fineness, and each pod’s fins are its own, turned with it; every pod adds its share. The forces act on the rocket’s axis, exact for two pods or more to first order, and the roll damping the pods’ offsets give is added. How the pods and the body disturb each other’s flow is left out, with its size stated, and a single pod’s off-axis moments are left to issue #213. | Pods |
| ADR-093: Pod probes flown as the public designs are, and listed apart | M1.13c2 checks pods against OpenRocket on six probe designs, one airframe carrying pods of bodies, fins, tail cones, winglets or motors, or none, flown as the public designs are and listed apart from them. Each is within 5% of OpenRocket’s apogee and largest speed, and what the pods change is held too, since a straight-up flight tests drag more than normal force. The close agreement shows the two codes apply the same rules; it cannot size the interference both leave out. | Pods |
| ADR-094: A tilted launch rod flown as OpenRocket records it | M2.2e5 flies a tilted rod as OpenRocket records it: hpr’s rail takes the same compass bearing and an elevation of 90 degrees minus the tilt. Four public probes check it: the bearing at apogee agrees within 0.03 degrees and the apogee the tilt takes off within 0.27 percentage points. OpenRocket’s rocket clears a tilted rod at a small angle of attack, which on the probes and on the one private design explains most of a margin gap that grows with the tilt. | The .ork format: a tilted launch rod |
| ADR-095: The single pre-1.9 override flag read as OpenRocket reads it | Older .ork files use one flag to say a part’s overrides cover the parts inside it. M2.2e6 reads it as OpenRocket 24.12 does: as all three per-quantity flags, the later tag winning where a file writes both. Eleven probe designs measured it, and a test holds hpr to them. One private design now flies, within 0.26% of OpenRocket’s apogee. The other the flag was blamed for does not: it also has a stage separation hpr can’t fly yet (#184), so that part of the bar is recorded as not met. | The .ork format: the values inside the tags |
| ADR-096: Fin fillets and an automatic radius inside a nose cone read as OpenRocket reads them | M2.2e7 weighs a fin fillet as a prism of its section along the root chord, one each side of every fin, in its own material or OpenRocket’s cardboard. An automatic outer radius inside a hollow nose cone or transition is the bore at the part’s narrower end; an innertube written auto keeps OpenRocket’s 9.5 mm, as OpenRocket does, with no warning. 21 new probes measure it. On 9 fillet probes (7 new), the fillets’ mass and center of mass are OpenRocket’s to 1e-15, the whole probe’s pitch inertia up to 0.64% apart. On 14 bore probes, 13 of which hpr flies, every part inside but one packed mass component (#186) is at OpenRocket’s mass to 1e-14 and station to 1e-15; a coupler at a nose’s tip is refused. Two private designs now fly, 18 designs compared across both reports, of the 20 M2.2e9 needs | Mass properties: fin fillets, the .ork format: inside a nose cone |
| ADR-097: A cause in the drag sized by hpr flying OpenRocket’s drag | The first supersonic flight in the OpenRocket reports climbs 13.60% higher in hpr. An oracle records OpenRocket’s drag coefficient along its flight, and hpr flies it: +1.11%, so the cause is the drag. OpenRocket keeps the whole base’s drag under power, measured on its own examples, which hpr can now fly for comparisons; hpr’s supersonic pressure drag is about twice OpenRocket’s. Which of each is right is open (#222: supersonic pressure drag and base drag under power) | The .ork format: a supersonic flight, Aerodynamics: drag |
| ADR-098: A tube fin set’s automatic radius read as OpenRocket reads it | M2.2e8 reads a .ork tube fin set whose radius OpenRocket works out from the body. For three tubes or more, the radius is the one at which each tube touches the body and its neighbours; for one or two, it is the body’s radius. OpenRocket 24.12 gives the same radius, within 1e-15, on 19 probe designs. More than 8 tubes are read as 8, as OpenRocket reads them. Both inertias are departures, kept on purpose: OpenRocket’s roll inertia is larger than any mass inside the ring could have, and its pitch inertia leaves out how far the tubes sit from the axis; hpr keeps its own. A design with tube fins is weighed, and its configurations are left out of flight. The tube fin example still does not fly: that waits on an aerodynamic method for tube fins, now M2.2e9 | |
| ADR-099: Tube fins flown as ring wings | M2.2e9 flies each tube of a tube fin set as a ring wing: Weissinger’s slope, within 3% of five rings NACA measured, Fletcher’s measured aerodynamic center, joined to the leading edge by hpr’s own slender-body derivation (his longest ring, A = 1/3, left out, a judgement); and friction inside and out with a square edge’s drag on the wall, below Mach 0.8 and for three tubes or more. OpenRocket’s tube fin example flies: its apogee is 6.95% above OpenRocket’s, and within 0.03% on OpenRocket’s own drag, so the net gap is hpr’s drag. OpenRocket’s tube-fin drag was refined against measured flights, so hpr’s probably reads low (#228). Its margin is 1.08 calibres below OpenRocket’s; a first look, since kept as a record by ADR-102, put OpenRocket’s tube-fin slope at 1.62 times the long-ring limit of six isolated rings (1.65 times hpr’s) | |
| ADR-100: A motor whose ignition never comes flown unlit | M2.2e10 flies a .ork motor set never, or lit at an event that never comes (burnout or ejectioncharge in the bottom stage, or a plugged motor’s charge), unlit and loaded, as OpenRocket does on five probes of its two-stage example; a separation at such a motor’s burnout or charge never comes. One more private design flies, 0.88% above OpenRocket in apogee, which makes the 20 designs M2.2 asks for. It corrects ADR-076’s reading of one private record | |
| ADR-101: OpenRocket’s mass conventions rolled up | M2.2b closes: each mass convention OpenRocket was found to follow is hpr’s rule or a departure pinned by a test. The corpus survey, run again on 71 files (51 distinct by content), has mass and center of mass within 1% on 68, pitch inertia on 56 and roll inertia on 57 with OpenRocket’s fin shortcut; each file outside in mass, center or roll has a named cause (mass page). The comparison as a whole stays open for two Loft lessons, L19 and L82, moved to M2.2f | |
| ADR-102: Tube fins’ centre of pressure measured against OpenRocket | M2.2f keeps OpenRocket 24.12’s answers for tube fins as a record: 14 probe designs and its Tube fin rocket, at five Mach numbers. Its tubes lift 1.26 to 1.86 times what hpr’s ring wings do, the same per tube whatever their number, and act a quarter of their length aft of the leading edge up to Mach 0.5, its flat fins’ rule. No measurement supports either, so hpr keeps its model; slender-body theory adds a body interference that hpr leaves out and OpenRocket does not vary with the count (#234). L19’s quarter-calibre bar is not met: up to Mach 0.5 hpr’s center of pressure is 0.42 to 3.0 calibres forward of OpenRocket’s, 1.07 on the Tube fin rocket, and a test pins each gap (tube fins) | |
| ADR-103: The builder API wraps the crates’ own types, with no default materials | M4.1 is split: the builder first (M4.1a), then models of your own and a guide in the API reference (M4.1b). The builder’s Environment, Motor, Rocket and Flight make the crates’ own design tree and simulation and hand them over, so no physics is written twice. Every part names its material and wall, since a default would be a guess at the rocket’s mass. Degenerate numbers are refused where they are given, each by name (L95; The builder) | |
| ADR-104: A drag model replaces the zero-lift drag only, as a drag table does | M4.1b closes M4.1: a drag model of your own, a Rust trait, gives the rocket’s zero-lift drag coefficient at each flow, with hpr’s own drag at hand to adjust. The flight scales it for the angle of attack as it scales a table’s; the normal force, center of pressure and damping stay hpr’s. A model and a table replace each other, and a powered separation refuses either. A model handing back hpr’s drag flies the same flight bit for bit. Three examples join the builder’s, and hpr::guide is the guide in the API reference (Models of your own) | |
| ADR-105: The command line’s surface: every command registered, JSON by schema, a generated table | M4.2 is split in four: the command surface and hpr motors first (M4.2a), then flying a design (M4.2b), the validation cases and file conversion (M4.2c), and reading a flight log (M4.2d). Every command is registered from the start, and one not yet available refuses with exit status 3 and the milestone that brings it. With --json a command prints one JSON document, even when it fails, and each document has a published schema. The README’s command table is written from the tool’s own list of commands, so it can’t claim one that doesn’t exist (The command line) | |
ADR-106: hpr sim flies the library’s flight, the stack whole, from a stated launch | M4.2b: hpr sim flies a .ork or a rocket’s JSON with the library’s own code, so its numbers are a program’s to the last bit. Without options it launches at 0° N, 0° E, sea level, from a vertical 1.5 m rail in calm air, and prints what it used. It flies no parachute or separation yet, and says so in its notes; a rocket whose stages come apart under power is refused (The command line) | |
ADR-107: hpr validate shares the project’s own validation check; hpr convert translates .eng and .rse by the format notes | M4.2c: hpr validate re-runs the RocketPy validation cases and makes the check the project’s automated tests make, with the same code, so the two fail together; it needs a copy of the repository (since replaced by cargo xtask validate --check, ADR-187). hpr convert writes a motor as .eng or .rse: the thrust curve, size and masses come back bit for bit, what .rse adds is filled the way ThrustCurve.org’s .rse files are, and what .eng can’t hold is dropped with a warning (The command line) | |
ADR-108: A flight log read alone: PerfectFlite’s .pf2 first, heights after a running median, an invented log in CI | M4.2d: hpr analyze reads a PerfectFlite altimeter’s log, with no design file, and prints liftoff, apogee, the top speed and landing, each saying where it came from or why the log can’t support it. Heights come from the altitude after a running median, not the Hampel filter Debrief used, because on a real log the Hampel filter kept an ejection charge’s pressure pulse as the apogee. The tests read an invented log whose every reading is known; the one public real log, whose terms are unclear, is read only where it has been fetched (Flight-log readings) | |
ADR-109: M3.2 split, and a .ork written from the design | M3.2 is split: the writer first (M3.2a), then OpenRocket loading and flying what it writes (M3.2b). hpr writes a .ork from its own design, each value as the reader reads it, and puts back everything it keeps but doesn’t model where it was. A value the reader drops, such as a rail button’s screw height, is kept and written back, so OpenRocket still reads it. Each number is written as the shortest decimal that reads back to the same bits. Ids that aren’t UUIDs are left out, since OpenRocket refuses a file holding one (Writing a .ork) | |
| ADR-110: M3.2b: OpenRocket flies the export; only counts are published | OpenRocket 24.12 tries to fly each of the 75 .ork files hpr’s checks use twice, as written and as hpr writes it back out, and only counts are committed. Every design it opens as written must open as exported, a design it refuses must be refused for the same reason, and every configuration flown both ways must reach the original’s apogee within 0.5% (Checked in OpenRocket) | |
| ADR-111: M3.3: the hpr design format, its extensions, versions and crate | M3.3 is split: the JSON document first (M3.3a), then the zip container, migrations and the comparison (M3.3b), then generated types (M3.3c). A design is a .hpr file, the container a .hprz. A document names its format and a major.minor version, checked first; a key its version doesn’t define is refused, not dropped. The document holds the design as the .ork reader models it, with the source file’s other files, so hpr-format builds on hpr-io (The hpr design format) | |
ADR-112: M3.3b: the .hprz container and migrations | Version 0.2 renames the source file’s other files source_files, so “attachment” means a file in a .hprz, and records why a .ork’s airframe wasn’t read as written, which the .ork written from a document can’t say. A change that stops old documents reading takes a new version and a migration, tested on a committed older document. A .hprz is a zip whose first entry is the .hpr; attachment names are relative paths, refused otherwise. hpr convert and hpr sim take all three design formats (The hpr design format) | |
| ADR-113: M3.3c: TypeScript and Python types | xtask generates the types and a reader for each language from the schema, not a third-party generator. A reader checks a document against the schema embedded in its file, and tests hold both readers to a separate schema checker on 4,892 altered documents. Not on npm or PyPI (TypeScript and Python) | |
| ADR-114: M4.3: the Python package | M4.3, the Python bindings, is split in three: the package, RocketPy’s example flown from Python, and models written in Python. The package wraps the Rust builder and adds no physics; it keeps RocketPy’s shape (a flight flies when it is made, its recording comes back as arrays) but the Rust API’s unit-named arguments. One wheel per operating system serves CPython 3.10 on; nothing is published to PyPI | Python |
| ADR-115: M4.3b: a drag table, and Calisto from Python | The flight builder takes another program’s drag table, and the Python package gains it with RocketPy’s gravity formula and a parachute cut away when another opens. RocketPy’s ways of measuring a flight (from the dry center of mass, its rail exit at the forward button) stay in the Calisto example rather than the library; a test holds the example to 3% of RocketPy and to the validation suite’s own numbers | Python |
| ADR-116: M4.3c: drag and wind as Python functions | A flight takes a drag, and an environment a wind, written as Python functions. hpr calls them as it flies; an exception one raises stops the flight and reaches the caller unchanged, not as hpr’s own error. A constant drag function flies exactly as a table of the same number | Python |
| ADR-117: M5.1 split; the cache and offline mode | The online layer ships in two parts: first the cache and the offline rule over a transport you plug in, then HTTP. Offline never calls the transport; online, a copy younger than the source’s time to live is served without fetching, and a failed fetch falls back to an old copy marked stale | Online data and the cache |
| ADR-118: M5.1b, HTTP over the cache | The HTTP client is ureq with rustls, behind a cargo feature, so no OpenSSL. Its size limit counts the unpacked answer, since a small compressed one can unpack to gigabytes. The platform’s cache folder is found by hand, because the usual crate for it pulls in a weak-copyleft (MPL-2.0) dependency | Online data and the cache |
| ADR-119: M5.2 split; Open-Meteo’s pressure levels as a sounding | The weather milestone ships in four parts: Open-Meteo first, then weather-balloon soundings, then NOAA’s model files, then files you download and the hpr weather command. Open-Meteo’s answer becomes a sounding whose lowest level is the ground, with the 10 m wind as the wind on the rail. Levels the models report below the ground are left out, and the heights are read as geopotential, which a check on the recorded answers bears out | Launch-day weather |
| ADR-120: University of Wyoming soundings | A weather balloon’s sounding from the University of Wyoming’s archive is read from its comma-separated text, whose column names carry the units. Rows are kept from the longest chain from the ground in which each row fits the thickness the hypsometric equation gives the layer from the row before it; of each run of rows with the same rounded pressure the middle one is kept; a row kept must lie above the last one kept. The first row is the ground; the answer is refused when a chain without the ground beats every chain with it. A saved copy is fetched again once the sounding has settled. Only soundings from U.S. stations, U.S. government works, are committed as test data | Weather-balloon soundings |
| ADR-121: GFS and RAP from NOMADS’ grib filter, read by an in-house GRIB2 decoder | NOAA’s GFS and RAP forecasts are fetched as a small GRIB2 cut from NOMADS’ grib filter, 0.3° each way around the site, and blended bilinearly between the four grid points around it. The ground and the levels above it follow ADR-119 (the Open-Meteo sounding), and RAP’s winds, given along its map’s grid, are turned to east and north at the site. hpr reads GRIB2 with its own decoder (latitude/longitude and Lambert grids, simple packing) rather than the grib crate, which gives single-precision values and binds C libraries; every value matches ecCodes’ to one rounding. Whole NCEP files, with complex packing or JPEG 2000, are refused and left for files you download | NOAA forecasts: GFS and RAP |
ADR-122: hpr weather, and M5.2d split | The command line fetches a launch day’s weather from any of the sources, one subcommand each, through the same cache as the library. --offline answers from the cache alone, and --from reads an answer saved earlier, held to the same checks as a fetched one. The profile is written as the library’s own sounding type, which reads it back with its checks. NOAA’s whole files, with their other compressions, follow in two more steps | The command line |
| ADR-123: Complex packing, and a whole GFS file | hpr’s GRIB2 decoder reads the tighter packing of NOAA’s whole GFS files, and totals over a time span (such as accumulated rain; read, not used in a profile). Every value of one whole file was checked against ecCodes by a script, run once outside CI; CI checks eight of its messages and a recorded cut ecCodes packed the same way. A whole file’s profile is its NOMADS cut’s to 1.04e-7, with the levels above 10 hPa as well | A whole GFS file |
ADR-124: JPEG 2000 packing, through hayro-jpeg2000 | hpr’s GRIB2 decoder reads fields stored as JPEG 2000 images, as NOAA’s whole RAP files are, using a JPEG 2000 decoder written in Rust rather than one of its own. Only lossless images of up to 21 bits a value, coded as NCEP codes them, are read, which its 32-bit floats keep exact; the rest are refused by name. CI checks four public RAP fields against ecCodes | Files in JPEG 2000 |
ADR-125: Site data split, and WMM2025 in hpr-core | M5.3 split a to c; WMM2025 in hpr_core::magnetic, finite at the poles, refused outside 2025 to 2030 and −1 to 850 km; NOAA’s 112 test values, NCEI’s file’s X to its measured 7.18e-4 nT residue, shown to lie in the file’s X′ | |
| ADR-126: M5.3b, Open-Meteo’s elevation through the cache | M5.3b: Open-Meteo’s elevation in hpr_net::elevation, up to 100 places a request, coordinates to 5 decimals in the URL, a year’s TTL, heights refused outside −1,000 to 9,000 m, a surface model above the EGM2008 geoid with N left to the caller; no command yet | |
| ADR-127: M5.3c1, geodesics through GeographicLib, held to Karney’s test set | M5.3c split c1, c2; geodesics in hpr_core::geodesic through geographiclib-rs, flattening past 1/150 refused; Karney’s 500,000-line test set within his 15 nm on five measures, either azimuth pair on its 21 mirror lines | |
| ADR-128: M5.3c2, a site’s height from a user’s GeoTIFF, held to rasterio’s reading | M5.3c2: a user’s GeoTIFF in hpr_io::geotiff over the tiff crate; geographic CRSs within a few meters of WGS 84 only, projections and far datums refused; the containing pixel placed as GDAL places it; GDAL’s scale and offset, units the file states, else meters; held to rasterio 1.5.2 on seven fixtures and a whole USGS tile | |
| ADR-129: M5.4 split, and M5.4a, the motor finder’s API through the cache | M5.4 split a to c; M5.4a: motor.fusionspace.co’s five files in hpr_net::motor_finder through the cache, an hour’s TTL, its structural rules refused and its derived ones pinned on the recording; eight answers of one build committed as fixtures; the credit with the site’s caution on every answer | |
| ADR-130: M5.4b, ThrustCurve searches and curves through the cache, and the in-stock join | M5.4b: ThrustCurve.org’s search and download in hpr_net::thrustcurve through the cache, a day’s TTL; the join by exact maker and designation, misses reported not guessed; three makers’ searches and two public-domain files committed as fixtures; the join’s report in validation/reports/thrustcurve-join.md | |
ADR-131: M5.4c, hpr motors search, stock and prices at the command line | M5.4c: hpr motors search, the finder’s list from the network, the cache or a saved file; five filters, --max-price in exact cents on the cheapest in-stock offer; cheapest first; both credits on every list; the example tested on an edited copy, as the recording lists nothing at $150 | |
ADR-132: M5.5a, OpenRocket’s .orc parts catalogues, held to OpenRocket’s reading | M5.5 split a and b; M5.5a: hpr_io::orc reads OpenRocket’s .orc parts catalogs; the 16 files OpenRocket 24.12 ships bundled unchanged (Apache-2.0); held part by part to OpenRocket’s preset loader, run as an oracle; exact unit definitions, the file’s makers’ names and densities kept, a stated mass kept beside them; unreadable parts left out with a warning, not the file | |
| ADR-133: M5.5b, catalogue parts in the builder, weighed as OpenRocket builds them | M5.5b: catalog parts in the builder (from_catalog on Nose, Tube, Transition, MotorTube, and the new Fitting); what the file leaves unsaid as OpenRocket 24.12 builds it, but a hollow part’s shoulder takes its wall; a stated mass scales the part’s density; a part with an undefined material refused; every part held to OpenRocket’s built mass and center, the two codes’ hollow walls each checked | |
| ADR-134: Monte Carlo dispersion: independent normals, one stream per sample and input | M6.1a: Monte Carlo dispersion in hpr_analysis::montecarlo; each input an independent normal about its nominal value; one random stream per sample, input and copy, keyed by SeededRng::for_stream; impulse dispersed with the propellant mass; a drag scale in the aero model; failed samples kept; M6.1 split a to d | |
| ADR-135: Landing ellipses: the normal ellipse, the next flight’s, and a count of what each holds | M6.1b: landing ellipses in hpr_analysis::ellipse; the normal ellipse of the sample’s mean and covariance, scaled by k² = −2 ln(1 − p); a prediction ellipse for the next flight by Hotelling’s T²; the landings each ellipse really holds counted, failures as bounds; tested against normal spreads with known answers | |
| ADR-136: Sensitivity analysis: Morris paths and Saltelli’s Sobol’ estimates, with their standard errors | M6.1c: sensitivity analysis in hpr_analysis::sensitivity; uniform independent factors; Morris’s paths with μ, μ*, σ and μ*’s standard error, and the exact grid moments by Morris::population; Saltelli 2010’s first-order and Jansen’s total estimates on pseudo-random rows, with delta-method standard errors; held to Ishigami and Sobol’s g in closed form and calibrated over seeds | |
| ADR-137: Ten thousand flights: a run’s flights share the nominal’s layout and supersonic table | M6.1d: a Monte Carlo run’s flights share the nominal design’s supersonic table (AeroModel::share_supersonic_table, when the covered segments and reference area are equal) and its layout (Rocket::lay_out, LaidOut::relay, Simulation::from_laid_out); evaluations borrowed, not copied; 10,000 flights timed on Valetudo (the flight benchmarks’ Level 2 rocket) and on a supersonic K940 rocket, by a bench target; per-evaluation speed-ups left to #285 | |
| ADR-138: Optimization: CMA-ES first, held to test functions and to pycma | M6.2 split a to e; M6.2a: CMA-ES (Hansen’s tutorial, Table 1, positive weights only) over continuous variables, worked in each variable divided by its step, with optional bounds by resampling, ask-and-tell (Run::tell), Jacobi eigen-decomposition each generation; held to four test functions’ minima, to pycma 4.5.0’s evaluation counts (medians within 25%) and to a dense recomputation of every generation; the 3,048 m problem re-flown from a fresh build and at 100× tighter tolerances | |
| ADR-139: Optimization constraints by Deb’s feasibility rules | M6.2b split b1, b2; M6.2b1: constraints for CMA-ES by Deb’s (2000) feasibility rules (Evaluation, Run::tell_constrained), no penalty weight; infeasible candidates ranked, not redrawn; a target and TolFun count only feasible points; held to the sphere with x₀ ≥ 1, the tangent problem and CEC 2006 g06 from 20 seeds | |
| ADR-140: Discrete choices by CMA-ES with margin | M6.2b2: integer variables (Variable::integer) by CMA-ES with margin (Hamano et al., GECCO 2022), α = 1/(nλ); Φ by libm::erfc, Φ⁻¹ by AS 241; held to SphereInt, EllipsoidInt and SphereOneMax at 10 and 20 variables and to cmaes 0.13.1’s CMAwM; the rocket example hits 3,048 m with the motor and nose free, its body length fixed so designs share a supersonic table | |
| ADR-141: Several goals by NSGA-II | M6.2c: several goals by NSGA-II (Deb et al. 2002) with bounded SBX and polynomial mutation, constrained domination; held to ZDT1 to ZDT3’s exact fronts and to pymoo 0.6.2 (every run’s GD and IGD within twice pymoo’s worst, medians within a factor of 1.25 either way), tournaments paired as pymoo pairs them; a rocket’s apogee against static margin, its front flown again and matched by CMA-ES at 2.5 calibres | |
| ADR-142: Few evaluations by EGO | M6.2d: EGO (Jones, Schonlau and Welch 1998) with a kriging surrogate fitted by likelihood and the expected improvement searched by CMA-ES; M6.2d split d1 (Branin, Hartmann 3: within 1% of the minimum in 50 evaluations from 20 seeds, worst 0.12%) and d2 (Hartmann 6, which d1’s version leaves at a local minimum in 7 runs of 10) | |
| ADR-143: The operating envelope and a stop rule for accuracy work | The bands are set by Mach number alone. Accuracy work goes to the core band first, Mach 0 to 2.5, where nearly all commercial-motor flights are. Mach 2.5 to 3.5 is the extended band, which hpr flies with its accuracy checked less; the envelope is Mach 0 to 3.5 at any angle of attack. The angle is a separate condition: accuracy work assumes 15° or less. Faster flights still fly, and M1.14a will flag any flight beyond the validated range, at high angle of attack, outside the core band or beyond the envelope. An accuracy step counts as progress when it shrinks a measured error against an independent reference, or adds a reference that will; two steps in a row that do neither end the milestone, gaps written down. M1.8 closes with its drag target missed, and that target moves to M1.14; only the work above Mach 4 is deferred | Validation plan: the operating envelope |
| ADR-144: The 2026-10-03 check-in | What runs next (M0.5, leaner bookkeeping, then M6.2d2, M4.5, M1.14 and M6.2e), CAD interop added as M8.3, how issues are labelled and ordered, the writing standard for these pages, guardrails on safety-relevant numbers, and licensing questions settled | Writing these pages |
| ADR-145: Recorded answers and their licences: stand-in ThrustCurve searches, CC BY 4.0 for the motor finder | Recorded answers and their licenses: ThrustCurve.org’s three searches replaced by stand-ins (five invented motors per maker, and the in-stock motors’ records carrying the motor finder’s CC BY 4.0 values); motor.fusionspace.co’s answers under CC BY 4.0, credited “Motor stock data from motor.fusionspace.co” | |
| ADR-146: Leaner bookkeeping split, and one file per decision record | M0.5 in nine increments, one per item of its plan; each decision record in its own file, with this site’s links and the generated index pointing at it, and every older link still landing on the record’s row | Decisions and the roadmap |
| ADR-147: A roadmap of open work, an archive, and a queue | The roadmap now holds only open work, in an order set by a queue at its top that the checks follow; done milestones move, word for word, to an archive file, by a command rather than by hand | Decisions and the roadmap |
| ADR-148: M0.5c, a short STATUS with a shape the guard checks, and its notes moved to on-demand files | The status file is now short and checked: 12,000 bytes, 110-character lines, a short note for Neer first, a 15-line handoff and 5 done entries; its long working notes and caveats moved word for word to two research notes read on demand | |
| ADR-149: M0.5e to g, reviews while CI runs, a faster gate and CI, and a lighter session start | Reviews start once the PR is open and CI runs, with explicit models and effort, a shared bar for blocking findings, and cheap scoped re-reviews; the gate runs tests in parallel and adds the private-data checks when physics changes; CI’s slowest job is split; a session starts from the status file and its roadmap entry | |
| ADR-150: Peak thrust and the burn-time crossings come from delivered samples | A thrust-curve sample strictly inside a run of three or more equal times is never delivered, so it no longer sets the peak thrust or a 5%-of-peak burn-time crossing; no surveyed ThrustCurve.org file changes | |
| ADR-151: hpr-sim-fixtures joins the reference library as private data | Neer’s collection of designs paired with the logs of the same flights is pinned in the reference library and kept private under refs/; only aggregate statistics are published; it stays out of the default .ork survey until an increment brings it in | |
| ADR-152: Hartmann 6 by EGO’s log transform | M6.2d2: EGO fits its surrogate to −ln(−y) (or ln y) of the values when the caller asks, as Jones, Schonlau and Welch did on Hartmann 6; all 20 runs (seeds 1 to 20) end within 1% of the minimum in 250 evaluations, worst 0.29%, against 6 of 20 without; the 20-run check is a release-build program the local gate runs, as it takes hours in CI’s debug build | |
ADR-153: A .ork’s recovery flown as OpenRocket flies it, held to its descents | M4.5a: a .ork configuration’s parachutes and streamers become flight devices: the stated drag coefficient, or OpenRocket’s own (0.8 on the canopy for a parachute, appendix C for a streamer), opening at once; apogee, altitude, ejection and launch triggers with the file’s delay. Targets, set before any hpr descent was measured: each deployment within 0.1 s or 2% of OpenRocket’s time, landing speed and flight time within 5% | |
| ADR-154: Motors fetched from ThrustCurve.org by name, cached, and named when offline | M4.5b: hpr sim fetches a motor the bundled catalog lacks from ThrustCurve.org, by name for --motor and by manufacturer and designation for a .ork motor with no curve, through hpr_net’s cache; hpr motors fetch pre-loads; offline and uncached, the refusal names the exact command. One rule picks the file; nothing is bundled | |
| ADR-155: Design checks that match reality: a part through the skin, a case that can’t fit | M4.5c: an internal part wider than its parent’s bore but inside the airframe warns; one that reaches past the airframe’s outside by more than 0.005 in is an error. A nominal motor size wider than its mount’s bore warns while AeroTech’s drawn case fits it, and is an error past that. OpenRocket’s examples raise no design error; the private library’s errors fall from 13 flights to 5 | |
ADR-156: hpr sim reads by name: configurations by their motors, a lead summary | M4.5d: hpr sim‘s text names configurations, mounts and the design checks’ parts by name, never by UUID; an unnamed configuration is called by its motors and delays in brackets, such as [C6-5]; --config takes a configuration’s number, name or id, and --mount a mount’s name or id; the checks print as sentences; the summary leads with margin, apogee, rail exit speed, delay and descent. --json keeps the ids | |
ADR-157: hpr sim --plot draws one fixed SVG figure | M4.5e: hpr sim --plot FILE.svg writes one SVG: altitude, speed and acceleration against time in three panels over one time axis, each instant with events a numbered marker listed underneath, and the fall on the airframe alone shaded “not a prediction”. It is drawn by hand in hpr-cli, with no plotting dependency, from its own sampling of the flight, which adds every step’s start and end to the recorder’s times so a parachute’s opening is drawn at full height | |
ADR-158: Three how-to guides run like the command-line guide, on a rocket of their own, with --delay for --motor | M4.5f: the guides “Fly your .ork”, “Pick a motor” and “Check stability for a certification flight”, and the README’s “Install and first flight”, show hpr’s output made by running each command, as docs/cli.md does, so CI fails when one goes stale. They fly a hand-written public design, validation/fixtures/ork/guides/level-1.ork, whose two motors are in hpr’s catalog, so they run offline. hpr sim --delay sets --motor’s ejection delay. Guidance numbers are quoted from their sources, dated, never stated as hpr’s | |
ADR-159: A .ork’s powered separation flown in hpr sim, each part under its own devices, a part with none tumbling | M4.5g1: hpr sim flies a .ork configuration whose booster drops away under power. hpr::ork::separated_recovery puts each parachute and streamer on the part its stage is in, and adds the tumbles the flight needs: the booster tumbles from the split until its first own device opens and releases the tumble, and a sustainer with no device tumbles from its apogee. FlightBuilder::separation takes the separation, so the command flies the library’s flight, bit for bit. Each part’s landing is printed. The sustainer’s descent is held to OpenRocket’s with ADR-153’s targets; the booster’s is not validated | |
| ADR-160: Several powered separations, each handing the flight on to a sustainer cut at its boundary | M4.5g2: the flight takes a list of separations, in the order they fire, each at a stage boundary further forward than the one before (Simulation::with_separations). With more than one, each must leave the nose’s body a motor to burn: at each, the stages behind drop away as the next body and the flight flies on as a sustainer cut from the whole design at that boundary. The .ork reader returns every powered separation, from the tail forward. OpenRocket’s Three stage low power rocket flies in hpr sim and in cargo xtask ork-flights. In the report, on OpenRocket’s own curves, its three configurations are within 1.48% of OpenRocket’s apogee and 1.64% of its largest speed, both separations at OpenRocket’s times; in hpr sim, on the curves it fetches (ADR-161, OpenRocket’s curves by digest), within 2.48% and 1.67% | |
ADR-161: A .ork motor fetched from ThrustCurve takes the file holding OpenRocket’s curve for its digest | amends ADR-154 §2 and §4: a fetch may name a ThrustCurve data file to take ahead of the rank’s order (Wanted::prefer, hpr motors fetch --file). hpr sim names one for a .ork motor whose digest a committed table holds: the file whose curve is the one OpenRocket flies. cargo xtask ork-curves writes the table for the motors of OpenRocket’s examples by comparing curves; it holds ids alone. With it, OpenRocket’s three-stage example’s first configuration reads +0.31% of OpenRocket’s largest speed in hpr sim, not +5.7%, and every configuration is within 2.48% of OpenRocket’s apogee | |
| ADR-162: The suite and its releases: what ships when, and the order of the new product lines | Neer’s 2026-10-04 ideas: hpr-sim becomes a suite of six products built in one workspace and joined by shared formats (simulator, flight analyzer, app, motor designer, recovery designer, avionics). Releases come at product boundaries: 0.1 the simulator once M4.5 and M1.14a are met, 0.2 accuracy, 0.3 the flight analyzer, 0.4 competitions, 1.0 the first app. The UI phase no longer waits for every library phase. COTS solids stay the only scope until 1.0. After it, the new lines come one at a time: experimental solid motors, then custom parachutes, then avionics, then hybrids and liquids. Each line gets its own guardrails | |
| ADR-163: The second pass: six products, the analyzer before the accuracy campaign, and what each release must hold | amends ADR-162 §1 to §4 after a review of the whole project, its 95 open issues, its throughput, its users and its competitors. The six products are defined by who uses them; the competition kit is a product and avionics starts as a test bench for any flight computer. Release 0.1 adds Monte Carlo from the CLI and Python, an apogee check on the logged flights, and named issue fixes. 0.2 becomes the flight analyzer (logs, a flight against its simulation, drag fitted from logs), ahead of the accuracy campaign it feeds; 0.3 is accuracy and fault diagnosis; 0.4 the competition kit, with RASAero and RocketPy files and ejection-charge sizing; 0.5 an app preview whose center is the 3D ghost replay; 1.0 the app with the editor. After 1.0: the motor designer, the recovery designer, pad day on a phone, then avionics | |
ADR-164: hpr-sim follows the FusionSpace product system, as FS · SW · TOOL 005 | hpr-sim adopts the FusionSpace product system (nrdptel/fusionspace-design, product/, Rev A, pinned at 32770a4) for its command line, documentation site, plots, exports, writing and every later app and device. It takes the designation FS · SW · TOOL 005. Apache-2.0 files from the system are copied in with their license lines; brand files only as README banners, outside the repository’s license. Milestone M0.6 makes the existing surfaces comply, after M4.5 and before release 0.1; later milestones and every release checklist take the system’s rules and checklists. The CLI keeps SI first with US units in brackets, and carries no brand banner | |
ADR-165: A .ork’s separation with nothing left to burn flies in hpr sim when each part has drag from the split | M4.5g3: a .ork configuration whose only separation comes with nothing ahead of it left to burn, such as a payload dropped at its booster’s ejection charge, is read and flown. From the split every part flies as a point with only its own devices’ drag, ADR-014’s model unchanged, so hpr::ork::tumbling flies it only when the part that keeps the nose has a device of its own open by the split, and refuses it by name otherwise. lowerstageseparation opens a device on the split’s own trigger. OpenRocket’s Deployable payload and ARC payload rocket fly: the payload’s apogee within 1.05% of OpenRocket’s record and every descent within ADR-153’s targets. The coast with no drag that the rule refuses would put one payload 4.00% above OpenRocket’s own free flight of it | |
| ADR-166: A fin set on a nose cone or a transition, its root along the surface | M4.5g4: a freeform fin outline’s last point may stand above its first, and the planform gains root_m, the root’s points between its trailing and leading edges. On a body tube the root stays level. A fin set may now sit on a nose cone or a transition: the tree checks that each of its root’s points lies within a micron of the surface and that it has no tab or fillet. Its body radius is the surface’s at the root leading edge, from which its heights are measured. The .ork reader draws the root through 63 points on the parent’s profile. OpenRocket’s Pods–airframes and winglets flies all five configurations, each apogee within 0.5% of OpenRocket’s record; its cockpit’s area, mass and center of mass match OpenRocket’s on OpenRocket’s own root points | |
| ADR-167: A part’s drag override, as OpenRocket flies it | M4.5h: a .ork part’s or stage’s stated drag coefficient flies in hpr as OpenRocket 24.12 flies it, measured on 25 probes; a step down belongs to the part ahead of it; Base drag hack (short-wide) within 2.1% on a C11-5, but 13% and 11% high on a D12-3 and an E12-4 for its nose (#177), not the setting: not met for those two, recorded. | |
| ADR-168: Pods–powered flies, and the margin is the weakest plane’s | M4.5i: a rail button’s screw head weighed as half an ellipsoid; the thrusting motors’ area told apart per pod set; a device at the charge of a stage where no motor lights never opens, as in OpenRocket; the margin reported in the rocket’s weakest plane (#329); OpenRocket’s Pods–powered with recovery deployment flies its three complete configurations, two within 1.9% of OpenRocket’s apogee, the third turning over before apogee in both tools | |
| ADR-169: A ring around its tube needs room in the part around it | M4.5k: a centering ring on its tube’s axis whose bore takes the tube’s outside wraps the tube, and is measured in the part around it at its own station that holds it whole; amends how two earlier decisions read the pods examples’ rings, which now fly as saved | |
| ADR-170: A packed part wider than its bore warns | M4.5l: a packed part (a mass component, parachute, streamer or shock cord) drawn wider than the room in its parent warns, by any amount, while its center is in that room, and flies as drawn; its width sets only its own inertia. A ring, tube or packed part centered past the room stays an error. OpenRocket’s Deployable payload flies its five configurations as saved | |
| ADR-171: A parallel stage is a pod set that separates | M4.5m: hpr’s design gains a parallel stage, a stage laid out as a pod set beside a body tube of an axial stage before it, weighed to its own stage and dropped by its own separation as a part that tumbles. The .ork reader reads OpenRocket’s <parallelstage> on a rocket of one axial stage; a motor in it lights as one in that stage does, and a parallel stage with no motor that lights stays on. OpenRocket’s Parallel booster staging flies both configurations as saved, each apogee within 1.3% of OpenRocket’s | |
| ADR-172: A stage’s first burnout, and a booster dropped still burning | M4.5n: a .ork stage whose motors sit in several mounts burns out when its first motor does, by the motors’ ignitions and curves, as OpenRocket 24.12 flies it (measured by a committed probe). The stage above lights there, and the stage separates there, when the file says so. A split at that first burnout may drop the stage’s other motors still burning. The flight then leaves their thrust out of the dropped part’s flight as a point, and says how much impulse that is. OpenRocket aborts its own flight of Pods–powered with recovery deployment’s first configuration, so hpr’s flight of it is checked against OpenRocket’s up to the abort: at the separation and at OpenRocket’s last row, within 5% | |
| ADR-173: A blunt ellipsoid’s subsonic pressure drag, from Hoerner’s measurement | M4.5o: an elliptical nose or shoulder takes Hoerner’s measured forebody pressure drag below Mach 0.8 (a hemisphere 0.01, a round head one diameter long −0.05, a flat face the step’s), held at 0 from 7/12 calibre on, and rises in a straight line from Mach 0.8 to Stoney’s scaled curve at Mach 1.2. Base drag hack (short-wide)’s 0.577-calibre nose gets 0.0008, so its E12-4 stays 7.80% above OpenRocket’s apogee with recovery in both: the 5% bar would need about 0.015, which no measurement supports. Not met, recorded; M4.5 is closed with that gap. | |
ADR-174: The command line’s colors, help: lines and title block | M0.6b: hpr colors its streams by the product system’s cli.md (the --color flag, then NO_COLOR, then FORCE_COLOR or CLICOLOR_FORCE, then auto, each stream on its own), in the system’s fs_style.rs roles, copied in unchanged; never in --json output or a file. Every hint is a help: line after the fact it is about, and an error document’s help array; clap’s tip: and “For more information” lines are rewritten to match. hpr --version, every --json document (a tool object first), and every file hpr writes name hpr-sim, its version and FS · SW · TOOL 005, each in the place its format keeps for it; a CSV’s goes in a .meta.json sidecar. A converted design file names this program, its source kept as read, amending ADR-112 §6. | |
| ADR-175: The plot in the product system’s chart style | M0.6c: hpr sim --plot draws in the light theme’s color roles of the product system’s foundations (Rev A) and no other color; every series is simulated, so dashed in the predicted color, a quantity’s size thick and its vertical part thin; events are dotted lines under numbered balloons with a table of every event’s number, time, altitude and name; the ground is a chain line; axis titles are QUANTITY · unit; no legend box, the line types said once in a caption whose first line, also the SVG’s <desc>, gives the summary’s apogee, its time and top speed. The fall that is not a prediction is hatched, its label outside the hatching. Amends ADR-157 §2, §3 and §5. | |
| ADR-176: The spelling check: what it reads and what it leaves alone | M0.6d is split in two: M0.6d1 adds cargo xtask spelling and clears the pages, the README and the scripts; M0.6d2 widens its scope to the crates, xtask, the schema and the pages’ fenced code blocks. The check reads a file’s writing (a page’s prose, a source file’s comments and strings) and skips code identifiers, string literals that are one bare word, link targets and URLs, HTML tags, links quoting a decision record’s title, and the frozen records, which now include docs/DECISIONS.md. Quotations, source titles and names that keep their spelling sit on a short list in the check, each with the phrase that holds it; the check counts them on every run and fails on an entry that matches nothing. | |
| ADR-177: The spelling check in the crates: words first, em dashes next | M0.6d2 is re-split in two before it shipped: M0.6d2 widens cargo xtask spelling to crates xtask schema and the pages’ fenced code blocks, read by each block’s language, and clears every UK spelling there; M0.6d3 rewrites the 266 em dashes left in them, which the check counts and prints until then. A name in a format string and a key quoted inside a string are identifiers; the check’s own file isn’t read; probe names an oracle’s recording keys by are allowed names. | |
| ADR-178: US names, and the spelling check reads them | M0.6e renames the 63 names that held a word on M0.6d’s list (center_of_pressure_m, MorrisDesign::analyze, VerticalUnit::Meter and the rest, as they are now called) to their US forms, and cargo xtask spelling now reads names as well as writing, so a new one fails CI. Seven serialized types keep each renamed key’s old spelling as a serde alias, read and never written, with tests that read the old form back; the design format’s keys never held a word on the list. GDAL’s own names for a unit are another program’s data and stay. | |
| ADR-179: The envelope flags, and when an angle of attack counts | M1.14a splits in two: a1 the four envelope flags of ADR-143 §1, a2 the warnings for ADR-163 §4’s issues. The flags come from a flight’s summary: three by its top Mach number against 1.1467 (the fastest public reference, pinned to the committed reports by a test), 2.5 and 3.5, and one by a new peak, the largest angle of attack more than 1 s after the rail exit and before apogee or the first deployment, counting only instants whose dynamic pressure is at least a tenth of the largest so far. Each edge is exclusive. The library, hpr sim (text and JSON) and Python all report them. | |
| ADR-180: The drag issue warnings, and M1.14a2’s split | M1.14a2 splits in two: a2 the warnings for the five drag issues on ADR-163 §4’s list (#67, #68, #70, #72, #222), a3 the four stability ones (#87, #120, #121, #172). A drag warning comes from the flight’s top Mach number and the design’s parts: a cone or ogive nose past Mach 0.8 (#67); every rocket past Mach 0.8, saying low from 1.2 (#68); airfoil fins past Mach 1 (#70); a boattail steeper than 10° past Mach 1 (#72); an ogive nose or airfoil fins past Mach 1 (#222). Each Mach edge is exclusive; a NaN top Mach number raises every warning the design’s shape allows. The library, hpr sim (text and JSON) and Python report them. | |
| ADR-181: The stability issue warnings | M1.14a3’s four stability warnings. #87, #120 and #121 warn past Mach 1.2 when hpr_aero’s supersonic body run, which is otherwise silent about why it stops, names that switch as the one that stopped it: a new public SupersonicFallback on AeroModel. #172 states no condition, and its cause is unmeasured, so it warns on every flight on hpr’s own aerodynamics that reports a smallest static margin, quoting the largest measured gap (0.073 calibres high) and no threshold. | |
ADR-182: hpr mc, Monte Carlo from the command line | M4.6 is split: M4.6a hpr mc, M4.6b Python. A Monte Carlo run’s FlightInputs now carry the flight’s separations, so a staged .ork is scattered as hpr sim flies it, and a tumble hpr adds to a separated part is not given a dispersed lag. hpr mc takes one option a dispersion, in the command line’s units, flies its samples on every core, prints the nominal flight beside the apogee’s and landing distance’s spreads and three landing ellipses, counts its failed flights by reason, and exports every flight as a CSV table (hpr_analysis::table::RunTable) whose numbers read back to the run’s bits. | |
| ADR-183: Monte Carlo in Python | hpr.MonteCarlo flies the library’s run on every core and returns its RunTable as NumPy arrays: numbers as float64 with NaN for a missing value, index as uint64, words as string arrays. Its eleven arguments are hpr mc’s options under the names its JSON uses. Rocket.from_file(..., recovery=True) flies a file’s recovery devices as hpr sim does, so a Python run and hpr mc can fly the same design, and a test holds them equal bit for bit with both release builds. | |
| ADR-184: Logged apogees of the private collection | M2.3c1 flies the 83 tier-A flights of hpr-sim-fixtures whose design is a .ork, in hpr and in OpenRocket 24.12, each in its day’s ERA5 weather with nothing deployed, and compares each apogee with the flight’s logged one, turned into the height climbed in that day’s air. A hand-read manifest under the gitignored corpus/ holds what each flight’s free-text row says; only aggregates are published, with the collection’s credit and the Copernicus attribution. | |
| ADR-185: Release 0.1 split, and its first fixes | M10.1 is split in four: a, the flight and design fixes; b, the data, network and Python fixes; c, the release workflow; d, the release notes and ADR-162’s checklist. M10.1a refuses a separation or ejection timed before the rail exit instead of firing it late, records one apogee, counts only errors in SimError::DesignChecks’s message, makes DragTable::with_reference_diameter_m check its diameter (#79’s decision), and measures a part in a nose cone or transition against the room where it sits: an error past the fit tolerance of the widest room along it, a new warning when it fits there but runs into the wall where the profile narrows. | |
| ADR-186: The data, network and Python fixes of release 0.1 | M10.1b fixes five issues. The bundled motor catalog is written from its 32 public-domain curve files alone by cargo xtask motor-catalog, every figure the file’s own (#295). .hprz attachment names are compared after canonical decomposition as well as case folding (#252). Open-Meteo and NOMADS write a site’s coordinates to 5 decimals so a radians round trip keeps the cache key (#272). Three pages’ accuracy figures are the report’s, and the site’s number trace now checks them (#298). A pytest pins Python’s recovery=True flight to hpr sim’s bit for bit (#308). | |
ADR-187: The release workflow, its license texts, and hpr validate left to xtask | M10.1c: a Release workflow builds and smoke-tests the command line’s archives for three operating systems, the Python package’s abi3 wheels and its source package, each carrying both licenses, THIRD-PARTY-NOTICES.md and its dependencies’ license texts written by cargo-about; it publishes only from a dispatch that asks to, after approval in the release environment. Every library crate and hpr-cli become publishable. hpr validate leaves the command line, because a published crate can’t depend on the unpublished harness; cargo xtask validate --check makes the same check. | |
| ADR-188: Release 0.1’s notes, and the flattering-side issues without a warning | Release 0.1’s last increment splits in two. M10.1d1 writes CHANGELOG.md and the install pages, and lists by number every open env-core issue on the flattering side or of unknown sign; M10.1d2 adds a warning for each flattering-side issue that has none (#18, #64, #73, #179, #325, #326, #354, #367), settles or warns each unknown sign, fixes #335, and then shows the whole checklist at a commit on main. The changelog links pages by the site’s address and is held to the site’s link and label checks. | |
| ADR-189: The unstable-under-power flag and four shape warnings; M10.1d2 split in three | M10.1d2 splits in three: d2 the flag for a flight unstable under power (#335) and the warnings read from the design’s shape (#64, #73, #325, #367) with #172’s bound raised to 0.1108 calibres; d3 the warnings for separated parts, laminar friction and kinked fins (#18, #179, #326, #354) and the seven unknown signs; d4 the checklist and its Release dispatch. Each new warning’s condition takes in the unmeasured range next to its measured one. | |
| ADR-190: The friction and freeform fin warnings; M10.1d3 split in three | M10.1d3 splits in three: d3 the warnings read from the design and the drag alone (#18 on every flight on hpr’s drag, #326 on every freeform fin set, both at any speed); d5 the separated parts’ warnings (#179, #354); d6 the seven unknown signs. Each takes in its issue’s unmeasured range. | |
| ADR-191: Neer’s 2026-10-06 ideas: the four web tools, watches, the repo layout and the hardware line | the four FusionSpace web tools (Motor Finder, Charge, Window, Muster) are rebuilt as library code and app screens in M9.6, after mobile; Motor Finder’s scraper stays a hosted service whose API hpr keeps reading. hpr stays one repository with several workspaces, split only by named triggers. Watches wait until after mobile, with the phone as hub. The hardware line is ordered by regulation, and active-control firmware or hardware waits for Neer’s export-control answer. | |
| ADR-192: Neer’s 2026-10-06 follow-up: the best product over the first, no legacy contracts, and three export tiers | Neer’s answers to ADR-191’s three asks. The old web tools get no more work and constrain nothing: Motor Finder’s API is not a contract, and hpr’s stock service is designed fresh even if release 0.1’s reader breaks. The best product outranks the first release. Export control sorts the work into three tiers (public, held, never): the simulator and its control models stay public; firmware or hardware that moves a control surface is held off GitHub and out of cloud AI tools until a ruling; target guidance, thrust vector control and lifted GPS caps are never built. | |
| ADR-193: Neer’s 2026-10-07 control scope: drag-only airbrakes, roll-only canards and fin tabs | hpr’s active control, simulated or built, is limited to airbrakes that only add drag and to canards and tail-fin tabs that only roll the rocket, stable with every surface inert and with any one surface stuck. The controller has two commands, brake deployment and roll, through a fixed mixer, so nothing can command pitch or yaw. Active pitch and yaw control joins the never tier, and the backlog’s thrust vector control and active fins leave it. Roll tabs come before roll canards, because a tail spanning as much as the canards removed most of their roll in NASA’s supersonic tests. M6.5 becomes roll control (queued before the flight computer logic); new M13.6 holds the control hardware for a ruling. Hardware stays in the held tier. | |
| ADR-194: Neer’s 2026-10-07 field tools: checklists, equipment and ground tests | Neer’s launch-field ideas land in three places. Release 0.4 gains M6.8, a field kit in the library and the CLI: a launch checklist and guide built from the rocket’s configuration, a check of each altimeter’s settings against the simulated flight, and a ground-test log that M6.7’s charge sizing reads. M6.7 is tightened by the one measured source found (Fetter, 2025): a packed bay needed several times the calculated charge to separate, so a packed bay is sized to his measured outcomes, never from the empty-bay pressure; the bulkhead load takes the empty-bay peak’s high side; the charge grows with altitude, and above 20,000 ft no black-powder size is printed. After the mobile apps, M9.7 adds an equipment register and a cross-vendor frequency board. M13.7 is a ground-test bench (a pressure logger and a wired firing box), in the public tier. The roadmap’s budget rises to 36,000 bytes. | |
| ADR-195: Neer’s 2026-10-07 guides: rocketry explained, illustrated and live | Neer’s guides to all of high-power rocketry become M0.7, Rocketry explained, a section of the existing docs site in four increments. M0.7a, right after release 0.1, sets the format and writes four guides on the largest gaps that hpr already models: stability, fin flutter, drag through Mach 1, and how far to trust a simulation. M0.7b, before release 0.4, adds recovery, wind and rail exit, and motors. M0.7c, after release 0.5, makes the figures live, run by hpr itself in the browser. M0.7d, after 1.0, covers the rest of the hobby, mostly by linking out. Every number a guide shows comes from hpr or a cited source, every figure is checked for clipping, and nothing is copied from copyrighted or share-alike sources. The roadmap’s budget rises to 38,500 bytes. | |
| ADR-196: Neer’s 2026-10-07 product guides: each product, illustrated | Each of the suite’s products gets a guide on the docs site: a landing page and an illustrated tour that ends on a public real flight, in M0.7’s format and checks. Every picture is made by a command CI reruns, never taken by hand: CLI output as terminal figures, plots from hpr, app screens from end-to-end tests, with phone and watch captures checked against the device’s safe area. M0.8a writes the simulator’s guide after M0.7a; from release 0.2 on, the release checklist requires that release’s guide, read by Neer; each line after 1.0 ships its guide with its first milestone. The roadmap’s budget rises to 40,500 bytes. | |
| ADR-197: Neer’s 2026-10-07 parts catalog: commercial parts, and a catalog of hpr’s own | OpenRocket’s catalog, bundled since M5.5, stays as the base. M5.6 adds a catalog of hpr’s own in four increments. M5.6a adds the format, where every value carries its source and date, plus hpr parts search and a parts list to buy from. M5.6b adds parachutes and recovery hardware, each Cd stored with its reference area and a descent rate given as a range, the flattering side pinned. M5.6c adds motor hardware and rail buttons, never counting a mass twice. M5.6d adds more airframe makers and electronics, masses carried as ranges. Only facts go in, each cited; no vendor text or photos are copied. M9.1’s design editor gets a part picker and the parts list. M5.6a comes after release 0.5, ahead of M8.2. The rest comes before 1.0, with M5.6b ahead of M8.1’s parachute sizing. The roadmap’s budget rises to 43,000 bytes. | |
| ADR-198: Neer’s 2026-10-07 project name: Eridanus, its stars, and the design system’s catch-up | the project is Eridanus, FS-ERI, and the repository becomes nrdptel/fusionspace-eridanus when Neer renames it. Each product takes one of Eridanus’s stars as its internal name: the simulator is Achernar (FS-ACHERNAR), the app Zaurak, the flight analyzer Angetenar, avionics Sceptrum, the competition kit Acamar, the motor designer Rana and the recovery designer Cursa. External names don’t change: the program is still hpr-sim, the command hpr. The designation FS · SW · TOOL 005, which the design system lists as retired, becomes FS-ACHERNAR · SW · TOOL 001, and files stamped with the old one still read. GitHub doesn’t redirect a project site after a rename, so the rename comes before release 0.1 publishes anything. M10.1d7 and M10.1d8 do the code side, and catch up with the 21 design-system commits since the pin. | |
| ADR-199: Neer’s 2026-10-07 public name: FusionSpace HPR, on every product and package | the suite’s public name is FusionSpace HPR, and each product is “FusionSpace HPR ·” and its role: Sim, Analyzer, Competition Kit, Motor Designer, Recovery Designer, Avionics, with the app and its store listing called FusionSpace HPR. Packages carry the brand: PyPI fusionspace-hpr imported as fusionspace.hpr, crates fusionspace-hpr and fusionspace-hpr-*, npm @fusionspace/hpr. The command stays hpr, and its help, version and banner open with FusionSpace HPR. Stamps read FusionSpace HPR 0.1.0 · FS-ACHERNAR · SW · TOOL 001, and files stamped by hpr-sim still read. The docs move to hpr.fusionspace.co. M10.1d9 renames everything before release 0.1 publishes, because a published name is permanent. Star names stay internal codes. |
The roadmap
The roadmap is the ordered plan of work. It is split into phases, and each phase into
milestones. A milestone’s id is M, a topic number, a dot and its place in that topic. The topic
is not the phase: 0 is foundations, 1 physics, 2 validation, 3 file formats, 4 library interfaces,
5 online data, 6 Monte Carlo and optimization, 7 flight logs, 8 design help, 9 the app, 10 releases,
11 motor design, 12 parachute design and 13 avionics. Phases
mix topics, so M2.3, the real-flights milestone, sits in Phase 1 beside
M1.8, the second aerodynamics milestone. A milestone too big to ship at once is split
into increments with a letter, and sometimes a digit after it, such as M2.1b2. Each one
ends with done when conditions, and it is checked off only when all of them hold. Every
milestone has a row in the table below.
The roadmap file holds open work only. A queue at its top sets the order of work, and the
project’s automated checks fail if the running progress notes name any other milestone as current.
When a milestone is finished, cargo xtask records moves its entry, word for word, to
the archive of finished work, which opens with an index of one line per milestone.
The work runs in this order: the physics and its validation first, then the ways to use it and the analysis tools, with a release at each product boundary, then the app as release 1.0, then the suite’s new product lines one at a time (ADR-162, the suite and its releases).
- Phase 0: Foundations: the workspace, the reference library, the lessons from Loft and this site.
- Phase 1: Physics core, with validation interleaved: the models on this site, then transonic and supersonic aerodynamics, staging, and flight outputs such as the stability margin, with comparisons against RocketPy, OpenRocket and real flights along the way.
- Phase 2: Library surfaces and interop: a simpler interface, a command-line tool, Python bindings, and design files in and out.
- Phase 3: Uncertainty, optimization, challenges: Monte Carlo runs and design optimization for competitions.
- Phase 4: More formats and embeddings: RockSim, RASAero and RocketPy files, and use from C and the web.
- Phase 5: Flight data and forensics: reading flight logs and taking a flight’s readings off them (planned to work on its own, with no design file and no simulation) and then comparing a real flight with its simulation.
- Phase 6: Design experience: a design assistant for apps to build on.
- Phase 7: UI, 3D, web, mobile: a desktop app, 3D flight replay, a web app and mobile, started once release 0.4 is out.
- Phase 8: Releases: 0.1 the simulator (the library, the command line and Python), 0.2 the flight analyzer, 0.3 accuracy and diagnosis, 0.4 the competition kit, 0.5 an app preview with a 3D replay that draws the simulated flight as a “ghost” beside the real one, and 1.0 the app with its editor.
- Phase 9: The suite’s new lines: after 1.0, one at a time: designing experimental solid motors, then custom parachutes, then avionics (a test bench for flight computers, a ground station, a GPS tracker), then hybrids and liquids. Pad day on a phone (M9.4, mobile) comes after the parachute designer and before avionics.
What is done so far, and what is not, is on Start here. Ideas that are not on the roadmap yet, each with a tier, are in the ideas backlog.
Every milestone
Each milestone and increment has a row here, so a milestone label on any page leads to a line of
plain words. The label in the first column opens its phase in the roadmap, or in the archive once
it is done, where its full plan and its done when conditions are. The rows, their statuses and
those links are written by cargo xtask records; only the plain words are written by hand, and
the site check fails if a row is missing or its status disagrees.
| milestone | what it covers | status |
|---|---|---|
| M0.1 | The code workspace, the automated checks (CI) and the licenses | done |
| M0.2 | The library of published sources and reference programs, each pinned so results can be reproduced | done |
| M0.3 | The lessons from Loft, the project that came before this one | done |
| M0.4 | This documentation site | done |
| M0.4a | The site itself, and the checks on its links and labels | done |
| M0.4b | The model pages’ In short, Accuracy, the Glossary and Checking a claim | done |
| M0.4c | Getting started, and How a flight is simulated | done |
| M0.4d | Publishing the site and the API reference on the web | done |
| M0.4e | A new reader answers ten questions from the site alone, and what they find unclear is fixed | done |
| M0.5 | Leaner bookkeeping: one file per decision record, a roadmap of open work in queue order, shorter status notes, these tables generated rather than hand-edited, and reviews while CI runs (ADR-144) | done |
| M0.5a | One file per decision record, with a generated index | done |
| M0.5b | A roadmap of open work only, with an explicit queue order, and an archive of what is done | done |
| M0.5c | A short status file, with budgets the checks enforce | done |
| M0.5d | Indexes and tables like this one generated, not hand-edited | done |
| M0.5e | Reviews that run while the automated checks (CI) run | done |
| M0.5f | A lighter start to each working session | done |
| M0.5g | The local checks that need the private reference data run on their own when physics code changes | done |
| M0.5h | The checks on the planning files rewritten to enforce the new rules | done |
| M0.5i | The first 20 working sessions after this milestone, measured against those before it | done |
| M0.6 | The FusionSpace product system: the docs site, the command line, its exports and its plot made to the design rules of FusionSpace, the family of tools hpr-sim belongs to, and the pages in US spelling (ADR-164) | done |
| M0.6a | The docs site | done |
| M0.6b | The CLI and exports | done |
| M0.6c | The plot | done |
| M0.6d | US spelling, no em dashes | done |
| M0.6d1 | The check, and the pages | done |
| M0.6d2 | The crates | done |
| M0.6d3 | The crates’ em dashes | done |
| M0.6e | US names | done |
| M0.7 | Rocketry explained: guides to high-power rocketry on the docs site, illustrated, then live (ADR-195) | not yet done |
| M0.7a | The format and four guides | not yet done |
| M0.7b | Recovery, wind and motors | not yet done |
| M0.7c | Live figures | not yet done |
| M0.7d | The rest of the hobby | not yet done |
| M0.8 | Product guides: a landing page and an illustrated tour for each product, every picture generated (ADR-196) | not yet done |
| M0.8a | The simulator’s guide | not yet done |
| M1.1 | Vectors and rotations, frames, the Earth’s shape and gravity | done |
| M1.2 | The atmosphere and wind | done |
| M1.3 | Solid motors: thrust curves, motor files, and mass through the burn | done |
| M1.4 | The rocket’s design and its mass properties | done |
| M1.4a | Part shapes and materials, and each part’s mass, center of gravity and inertia | done |
| M1.4b | The design tree: where parts sit, motor configurations and design checks | done |
| M1.5 | Aerodynamics below the speed of sound | done |
| M1.5a | Normal force and center of pressure | done |
| M1.5b | Drag, and tables that override it | done |
| M1.6 | The six-degree-of-freedom flight | done |
| M1.6a | The time integrator, and how events such as burnout and apogee are found | done |
| M1.6b | The rigid-body flight: the rail, the equations of motion and the phases of a flight | done |
| M1.7 | Recovery | done |
| M1.7a | Parachutes, and the descent under them | done |
| M1.7b | Streamers and tumble recovery | done |
| M1.7c | Separated bodies, each flown to its own landing | done |
| M1.8 | Transonic and supersonic aerodynamics, damping, and overriding the aerodynamics; closed with its drag target missed, which moves to M1.14 (ADR-143) | done |
| M1.8a | The normal force and center of pressure through Mach 1 | done |
| M1.8b | Drag through Mach 1: the transonic rise and supersonic wave drag | done |
| M1.8b1 | The drag of noses, shoulders and steps through Mach 1, against NASA’s Arcas Robin wind tunnel | done |
| M1.8b2 | Drag against RASAero II’s from Mach 0.1 to 2 | done |
| M1.8b3 | The drag of a boattail faster than sound, and the base behind it | done |
| M1.8c | Roll from canted fins, roll damping, and pitch and yaw damping (kept as each part’s local-flow damping) | done |
| M1.8d | Tables that override the normal force and center of pressure, read from RASAero II | done |
| M1.8e | The body’s normal force faster than sound, which slender-body theory underestimates past Mach 3; closed with its 15% target missed for the body alone (ADR-040, ADR-143) | done |
| M1.8e1 | The second-order shock-expansion method: the lift a body’s cylinder carries behind its nose faster than sound, checked against its report’s tables | done |
| M1.8e2 | The body’s supersonic normal force in a flight: the nose and its cylinder, joined to the subsonic model | done |
| M1.8e3 | Faster than sound: the blend into the shock-expansion method now starts at the exact Mach where the method starts to hold, not rounded to a 0.05 step | done |
| M1.8e4 | Faster than sound: a boattail, and a tube behind it, take their share of the body’s normal force from the shock-expansion method | done |
| M1.8e5 | Faster than sound: how much of the Arcas Robin’s remaining gap each missing effect (crossflow, the blunt tip, and others) explains, measured before modeling | done |
| M1.8e6 | The size of crossflow lift at every speed (Jorgensen) and a boattail’s measured share faster than sound (Washington and Pettis), the causes that measurement ranked first, in flight | done |
| M1.8e7 | Faster than sound: blunt and vertical nose tips (power-series, Haack and elliptical noses) by a Newtonian cap ahead of the shock-expansion method | done |
| M1.8e8 | Faster than sound: the lip behind a boattail, so that the committed Arcas Robin designs fly the shock-expansion method | done |
| M1.8e9 | Faster than sound: a bound on the boattail angle, footnote 8’s size pinned by hand, and the Arcas Robin wind tunnel’s 15% target judged | done |
| M1.8e10 | A lip’s shelter weighed as the drag buildup weighs it, instead of switching the whole body’s model at a threshold, and the size of every switch that remains | done |
| M1.8e11 | Cone slopes from 24° to 30°, from NASA SP-3007, where TN 3527’s own chart stops | done |
| M1.8e12 | What a blunt tip’s handover cap is worth once the cone slopes reach 30°, and what stops it moving there | done |
| M1.8e13 | What puts a marched answer at the mercy of the mesh: the surface pressure crossing its tangent cone’s, counted and told apart from a reduced element | done |
| M1.8e14 | Where the method stops marching a flare, bisected over Mach, and what the edge is made of | done |
| M1.8e15 | What a step in radius still switches, how big it is, and what a model of one would need | done |
| M1.8e16 | A blunt tip’s handover moved past 24°, once the march has a rule for the loading through a crossing (the rest of what M1.8e13 used to be, renumbered so the flare and the step keep their ids). Blocked, and deferred above Mach 4 with issue #108 (ADR-143); the vertical tip’s error goes to M1.14d below Mach 2.5 and M1.14h above | blocked |
| M1.8e17 | A flared body flown through the method where its shock is attached, with nothing jumping across that boundary as the model changes (the second of the three the old M1.8e14 splits into) | done |
| M1.8e18 | NASA TN D-4865 model 2’s readings committed, and what a marched flare is worth (the third of the three the old M1.8e14 splits into) | done |
| M1.8e19 | The band of near-flat flares the march used to refuse, which took the whole body off the method as a shape crossed it (found by M1.8e17); now read by the generalized method | done |
| M1.9 | Staging, clusters and air starts, for COTS motors | done |
| M1.9a | Motors lit at their own times, and a sustainer that flies on after a powered separation while the booster lands on its own, with tests for the ignition times, the mass step, the order of events and a trigger that never fires (ADR-074, Staging) | done |
| M1.9b | Several motors in one mount, their thrusts and masses summed, a motor out turning the rocket as a hand calculation predicts, and a .ork file’s clusters read with every tube where OpenRocket puts it (ADR-075, Clusters) | done |
| M1.9c | A two-stage and a cluster design from .ork files, each within the per-case tolerance of OpenRocket’s flight: every flight of each within 5% in apogee and largest speed (ADR-076, Staging) | done |
| M1.10 | Flight outputs: the stability margin over the flight, the best ejection delay, the peak dynamic pressure, fin flutter and the landing point | done |
| M1.10a | A flight’s peaks, its apogee with the height it counts from, its static and in-flight stability margins from the rail exit to apogee, each motor’s optimum ejection delay and each landing’s latitude and longitude, with None for what didn’t happen (ADR-077, Flight metrics) | done |
| M1.10b | Fin flutter speed and margin from a cited primary source, matching its worked example (ADR-078, Fin flutter) | done |
| M1.10c | Flights written as CSV, JSON, KML and GeoJSON files (Parquet optional), checked by schema and by parsing | done |
| M1.10c1 | A recording written as CSV or JSON, and the flight path with its landings as GeoJSON (checked against the published schema) or KML (checked by parsing) (ADR-079, Exporting a flight) | done |
| M1.10c2 | A recording written as Parquet, behind a cargo feature, and read back the same by Apache’s own Parquet library (ADR-080, Exporting a flight) | done |
| M1.11 | Ejected nose cones, body sections and payloads, each flown to its own landing | done |
| M1.11a | The airframe parting at any joint or around a payload, every piece landed under its own parachute (ADR-085, Recovery: ejected pieces) | done |
| M1.11b | The push of an ejection charge on the pieces, and a tumbling piece (ADR-086, Recovery: ejected pieces) | done |
| M1.12 | Payload mass that moves, or is released, during the flight | done |
| M1.12a | Ballast or a payload that slides along the airframe in flight, with the mass properties, the margin and the equations of motion following it (ADR-087, Moving mass) | done |
| M1.12b | Ballast or a payload released in flight, the rest flying on and the part falling on its own (ADR-088, Released mass) | done |
| M1.13 | Pods: bodies mounted beside the airframe, with or without motors | done |
| M1.13a | A pod’s mass: one pod repeated around the axis, each copy with its parallel-axis term, a motor in a pod one per pod (ADR-089, Pods) | done |
| M1.13b | Pods read from a .ork file | done |
| M1.13b1 | Pods of body components read from a .ork file, placed as OpenRocket 24.12 places them (ADR-090, .ork: Pods) | done |
| M1.13b2 | Pods of no length, which hang fins or a lug off the axis, and an empty pod set, read from a .ork file (ADR-091, .ork: Pods) | done |
| M1.13c | Pod aerodynamics from a cited source, and a pod design against OpenRocket | done |
| M1.13c1 | Pods fly: each pod’s parts with Barrowman’s normal force and their own drag, once per pod (ADR-092, aerodynamics: Pods) | done |
| M1.13c2 | A pod design with bodies and fins against OpenRocket, the limits of both codes’ pod models stated (ADR-093, aerodynamics: Pods) | done |
| M1.14 | Accuracy inside the operating envelope: the core band (Mach 0 to 2.5) first, then the extended band (Mach 2.5 to 3.5) in M1.14h, at angles of attack up to 15° (higher angles are M1.14e’s); real flights within the 5% apogee target, and at least as accurate as OpenRocket on the same flights (ADR-143) | not yet done |
| M1.14a | Four warning flags, each tested at its edge. Beyond the validated range: faster than the fastest public whole-flight reference, any committed comparison of a public flight against an independent reference (OpenRocket’s example at Mach 1.147 today; private flights never set it). At high angle of attack: above 15° more than 1 s after leaving the rail, at any Mach. Outside the core band: past Mach 2.5. Beyond the envelope: past Mach 3.5. And the aerodynamics page split by Mach band | done |
| M1.14a1 | Envelope flags | done |
| M1.14a2 | Drag issue warnings: issues #67, #68, #70, #72 and #222 warn by number when a flight meets their Mach and shape conditions (ADR-180) | done |
| M1.14a3 | Stability issue warnings: issues #87, #120, #121 and #172, where stability reads high, warn by number | done |
| M1.14b | New references from Mach 1.5 to 2.5: a public database of flights with simulator predictions, free-flight and measured supersonic coefficients, NASA’s six-degree-of-freedom check cases, and the validation uncertainty of each real flight | not yet done |
| M1.14c | The normal force and drag near Mach 1, from 0.8 to 1.2 | not yet done |
| M1.14d | Faster than sound, Mach 1.2 to 2.5. First the four shapes that fall back to slender-body theory and so read more stable than they are: a step in a body’s radius (issue #87), a flare behind a boattail too long for its wake (issue #120), a pointed tip steeper than 30° (issue #121) and a vertical tip steeper than its cap. Then drag, where hpr reads 5.1% to 14.9% below RASAero II on RocketPy’s Calisto rocket, and the body-alone misses M1.8e left from Mach 1.5 to 2.3 | not yet done |
| M1.14e | Large angles of attack: what the legal 20 mph wind costs, and a high-angle model if it costs more than 1%; how long the first moments off the rail stay steep, which tests the 1 s the high-angle flag allows; and the issues about high angles | not yet done |
| M1.14f | Missing physics, ranked: misalignments, airframe drag under a parachute, base drag with the motor burning, gusts, parachute opening loads and more | not yet done |
| M1.14g | Generated figures on every accuracy and aerodynamics page, each with its reference and the error shown | not yet done |
| M1.14h | The extended band, Mach 2.5 to 3.5, after the core band: each fixed or re-measured in a decision record. The four switches’ errors as measured at Mach 3, where the center of pressure moves aft and the rocket reads more stable; the near-flat flare’s step at Mach 2.90 to 2.95 (issue #108), which way it errs not yet measured; and M1.8e’s body-alone row at Mach 2.96, 16.9% high in its normal-force slope, which tends toward the conservative side on a finned rocket, whose center of pressure then reads forward | not yet done |
| M2.1 | The validation harness, and comparisons with RocketPy | done |
| M2.1a | The harness itself: cases, reference data, tolerances and reports | done |
| M2.1b | Whole flights against RocketPy, with both codes given the same drag | done |
| M2.1b1 | The script that flies RocketPy’s example rockets from pad to landing, as the reference | done |
| M2.1b2 | hpr’s whole flights compared with that reference | done |
| M2.1c | The same cases flown with each code’s own drag, a CI job, and regenerating references | done |
| M2.1c1 | A CI job that checks every case against its stored reference, and a workflow, run only by hand, that regenerates the references | done |
| M2.1c2 | The same cases flown with each code’s own drag | done |
| M2.1d | The time-series comparison, and where a rocket goes in wind (issue #50, why hpr turned into the wind less) | done |
| M2.1d1 | Each whole flight’s height and speed, compared over time with RocketPy’s | done |
| M2.1d2 | Juno III, Calisto and Bella Lui in still air, as committed cases to measure the wind against (issue #50, why hpr turned into the wind less) | done |
| M2.1d3 | Why hpr turned into the wind less than RocketPy (issue #50): RocketPy’s equations, corrected, and hpr’s body lift | done |
| M2.2 | OpenRocket as a reference program, and a corpus of designs to compare; closed with M2.2f, L19’s tube-fin bar missed and pinned | done |
| M2.2a | Each design’s structure (every stage, no motor): mass, center of mass and inertia against OpenRocket’s | done |
| M2.2b | OpenRocket’s mass conventions and roll inertia, split into M2.2b1 to M2.2b5 below; the stored results moved from the third to the fourth when ADR-062 split the fins off, and to the fifth when ADR-063 split the packed parts off; rolled up in ADR-101 | done |
| M2.2b1 | What a .ork leaves unsaid (a wall or shoulder of no thickness, no thickness written, no material) read as OpenRocket reads it, and which override wins | done |
| M2.2b2 | Fins and rail buttons against OpenRocket: where the roll inertia’s gap comes from, how each fin section is weighed, and where a rail button sits | done |
| M2.2b3 | Packed parts (parachutes, streamers, shock cords, mass components) against OpenRocket: the size one takes when its file writes none, and a mass override on one that weighs nothing | done |
| M2.2b4 | Motor clusters, fin fillets and the parts kept unread | done |
| M2.2b5 | Stored results in a .ork used as a reference only when they are current and plausible | done |
| M2.2c | The motors OpenRocket flies, for the configurations held back for want of a thrust curve, split into M2.2c1 and M2.2c2 below by ADR-066 | done |
| M2.2c1 | Every curve hpr flies integrated as OpenRocket 24.12 integrates it, to the milestone’s 0.1% in total impulse | done |
| M2.2c2 | The curves the reference library embeds, and the configurations held back for want of one, supplied from OpenRocket’s own motor database by ADR-067 | done |
| M2.2d | Flights to apogee on the public designs, against OpenRocket, split into M2.2d1 and M2.2d2 below by ADR-068 | done |
| M2.2d1 | OpenRocket 24.12 flies the public designs; what each of its summary words measures is written down for that version, and other versions’ are withheld; a metric for an event that never happened is never scored | done |
| M2.2d2 | hpr flies the configurations of that record it can fly, in a report against OpenRocket’s apogee, largest speed and stability margin (ADR-069, results) | done |
| M2.2e | The corpus, with a hypothesis for every apogee miss over 5%, split into M2.2e1 to M2.2e10 below by ADR-070, ADR-072, ADR-094, ADR-095, ADR-098 and ADR-100 | done |
| M2.2e1 | The flight report adds how far hpr’s mass and center of mass are from OpenRocket’s, at launch and as the rocket leaves the rod (ADR-070, results) | done |
| M2.2e2 | OpenRocket flies the private designs, kept out of the repository; only counts are published (ADR-071, results) | done |
| M2.2e3 | hpr flies the private designs, in a report of anonymised ids that publishes differences, never a design’s values: 17 flights of 4 of 12 designs, so 9 designs with the public report’s 5 (ADR-072, results) | done |
| M2.2e4 | A written cause for every apogee more than 5% from OpenRocket’s, sized by OpenRocket’s own flight without it: four of the five within 5% after, the fifth +7.80% with the no-drag part removed from both programs (ADR-073, results) | done |
| M2.2e5 | A tilted launch rod flown as OpenRocket records it: its angle from the vertical and the compass bearing it leans toward, checked on four public probe designs (bearing at apogee within 0.03 degrees, the tilt’s apogee loss within 0.27 percentage points) and adding one private design, 15 in all (ADR-094, results) | done |
| M2.2e6 | The single override flag older OpenRocket files use for a part and everything inside it, read as OpenRocket 24.12 reads it, measured on eleven probe designs and held by a test; one more private design flies, 16 in all. Not met for the second design the flag held: it also waits on a stage separation hpr can’t fly (#184) (ADR-095, results) | done |
| M2.2e7 | Fin fillets and an inner tube whose automatic radius has nothing to take, read as OpenRocket reads them, so that the two private designs they hold back fly (#174). Measured on 21 new probes: the fillets’ mass and center of mass to 1e-15, and every part inside a nose cone but one packed mass component (#186) at OpenRocket’s mass to 1e-14 and station to 1e-15. Both designs fly, 18 designs compared across both reports, of the 20 M2.2e10 needs. The one supersonic flight climbs 13.60% higher than OpenRocket’s; on OpenRocket’s own drag it is +1.11%, so its cause is in the drag (#222) (ADR-096, ADR-097, results) | done |
| M2.2e8 | Tube fins whose radius OpenRocket works out from the body, resolved as OpenRocket 24.12 does (#133). Measured on 19 new probes: the radius within 1e-15, the tube fins’ mass to 1e-14; OpenRocket’s tube fin example now reads, its mass within 5e-6 of OpenRocket’s. Its roll and pitch inertia are departures: OpenRocket’s roll is larger than any mass inside the ring could have, and its pitch leaves out how far the tubes sit from the axis. The example’s flight moved to M2.2e9 (ADR-098, results) | done |
| M2.2e9 | Tube fin aerodynamics: a cited method for the normal force and drag of tube fins, pinned by tests, so that OpenRocket’s tube fin example flies within M2.2e4’s bar: an apogee more than 5% off has a sized cause. Each tube flies as a ring wing below Mach 0.8. The example’s apogee is 6.95% above OpenRocket’s, sized as hpr’s drag by flying OpenRocket’s; 19 designs compared across both reports (ADR-099, Tube fins) | done |
| M2.2e10 | At least 20 designs across the two reports with all five spreads (apogee, largest speed, margin, mass and center of mass), the bar M2.2e3 had, unchanged. Met by flying a motor whose ignition never comes unlit and loaded, as OpenRocket does, measured on five probes of its two-stage example: one more private design flies, 0.88% above OpenRocket’s in apogee, 20 in all (ADR-100, the rules) | done |
| M2.2f | The two lessons that kept the comparison open. Cases let off a pass-or-fail bar still count in the error statistics, against both references where a rocket has two (L82): tested. A tube fin set’s center of pressure within a quarter calibre of OpenRocket’s (L19): not met, measured and pinned by a test instead, as L18’s is. hpr’s center is 1.07 calibres forward of OpenRocket’s on its Tube fin rocket, and no measurement says which is nearer (ADR-102) | done |
| M2.3 | Comparisons with real flights | not yet done |
| M2.3a | The weather over a launch site read from an ERA5 file, as RocketPy reads it (ADR-081, ERA5 weather files) | done |
| M2.3b | RocketPy’s logged flights flown in their ERA5 weather and compared with the logs (ADR-082, Accuracy: real flights) | done |
| M2.3c | The private designs that have a flight log, flown in their day’s weather, published as statistics. Blocked until 2026-10-04, when none of the private designs was the rocket of any logged flight (ADR-083); the private flight collection added that day has such pairs (ADR-151) | not yet done |
| M2.3c1 | Logged apogees: the private collection’s logged flights against hpr’s and OpenRocket’s predictions | done |
| M2.3c2 | Logged traces: the same flights’ altitude traces, once their logs are read | not yet done |
| M2.4 | A summary of accuracy for the README, and CI that fails on any regression (ADR-084, Accuracy: the census) | done |
| M3.1 | Reading OpenRocket .ork design files | done |
| M3.1a | The container a .ork arrives in, and its design document read whole | done |
| M3.1b | The component tree: parts, shapes, materials, finishes and overrides into a design | done |
| M3.1b1 | What a .ork value means: dimensions OpenRocket works out for itself, tags written under two names, and overrides | done |
| M3.1b2 | The spine: the stages and the body components stacked in them, with their automatic radii marked for the layout to resolve | done |
| M3.1b3 | The parts on and inside the body, with their positions and the dimensions they take from their parents | done |
| M3.1b4 | The three designs of the 76 whose rocket still did not lay out: a radius with nothing to take, and a document with no design | done |
| M3.1c | Motors, recovery, stages, and what OpenRocket last simulated | done |
| M3.1c1 | A design’s motor configurations, the motors in them, and their thrust curves | done |
| M3.1c2 | When each parachute and streamer opens, and when each stage separates | done |
| M3.1c3 | The launch conditions and results of the simulations OpenRocket stored | done |
| M3.1c4 | Pods, parallel stages, and every part, section, tag and attribute hpr does not read, kept for writing the file back | done |
| M3.1d | The corpus and the cross-check against RocketSerializer | done |
| M3.1d1 | Snapshots of what hpr reads from public .ork designs | done |
| M3.1d2 | Every .ork imports without an error, and the cross-check against RocketSerializer | done |
| M3.2 | Writing OpenRocket .ork files | done |
| M3.2a | The writer: a design written back out as a .ork, reading back as the same design (ADR-109, Writing a .ork) | done |
| M3.2b | OpenRocket 24.12 loading and flying the written files, within 0.5% of the original’s apogee (ADR-110, Checked in OpenRocket) | done |
| M3.3 | hpr’s own open design file format | done |
| M3.3a | The document: a design as canonical JSON with its schema, every corpus design through it and back to .ork flying to the same apogee (ADR-111, The hpr design format) | done |
| M3.3b | The zip container (.hprz), the first migration (0.1 to 0.2), the comparison with other formats, and hpr convert and hpr sim taking .hpr and .hprz (ADR-112, The hpr design format) | done |
| M3.3c | TypeScript and Python types generated from the schema, each with a reader that checks a document (ADR-113, TypeScript and Python) | done |
| M4.1 | A simpler interface, with a builder for environments, motors, rockets and flights | done |
| M4.1a | The builder: environments, motors, rockets part by part, and flights, over the crates’ own types (ADR-103, The builder) | done |
| M4.1b | Models of your own: a drag model the flight calls in place of hpr’s, through a trait, with a guide in the API reference (ADR-104, Models of your own) | done |
| M4.2 | A command-line tool | done |
| M4.2a | The command line’s commands, its JSON output and exit codes, shell completions, and looking up motors (ADR-105, The command line) | done |
| M4.2b | Flying a design from the command line (hpr sim), with its recording exported (ADR-106, The command line) | done |
| M4.2c | Checking the validation cases (hpr validate) and converting motor files (hpr convert) from the command line (ADR-107, hpr convert; hpr validate has since left the published hpr for cargo xtask validate --check, ADR-187, what it checks) | done |
| M4.2d | Reading a flight log from the command line (hpr analyze), with no design file: PerfectFlite’s .pf2 first, and liftoff, apogee, the top speed and landing (ADR-108, Reading a flight log) | done |
| M4.3 | Python bindings | done |
| M4.3a | The hpr Python package: the builder’s environment, motor, rocket and flight, designs read from files, recordings as NumPy arrays, and wheels built and tested on three operating systems (ADR-114, Python) | done |
| M4.3b | A drag table on a flight, and RocketPy’s example rocket, Calisto, flown from Python within 3% of RocketPy (ADR-115, Python) | done |
| M4.3c | Drag and wind models written as Python functions, their exceptions raised in Python (ADR-116, Python) | done |
| M4.5 | Fly my .ork: OpenRocket’s example designs fly as saved in hpr sim, with their recovery, offline after one motor download (ADR-144); Base drag hack’s E12-4 not met (M4.5o) | done |
| M4.5a | Parachutes and streamers from a .ork flown, with their triggers (issue #240) | done |
| M4.5b | A missing motor fetched from ThrustCurve and kept, and the exact command to fetch it printed when offline | done |
| M4.5c | Design checks that warn, with a cited tolerance, on fits builders make every day, and refuse only impossible ones | done |
| M4.5d | Output a person reads: configurations by name, results in sentences, the summary led by margin, apogee and rail exit speed | done |
| M4.5e | hpr sim --plot: a flight’s altitude, speed and acceleration drawn as an SVG figure | done |
| M4.5f | How-to guides: fly your .ork, pick a motor, check stability for a certification flight | done |
| M4.5g | Then unpowered separations, freeform fins and several separations, in order of how many designs each holds back | done |
| M4.5g1 | A powered separation in hpr sim | done |
| M4.5g2 | Several separations | done |
| M4.5g3 | An unpowered separation before apogee | done |
| M4.5g4 | Freeform fins | done |
| M4.5h | A part’s drag override | done |
| M4.5i | A powered pods example | done |
| M4.5j | The CLI’s corpus count | done |
| M4.5k | A ring around its tube | done |
| M4.5l | A packed part wider than its bore | done |
| M4.5m | Parallel booster staging | done |
| M4.5n | Pods–powered’s first configuration | done |
| M4.5o | Base drag hack’s E12-4: an elliptical nose’s subsonic drag taken from Hoerner’s measurement and pinned by a test; the E12-4 stays 7.80% above OpenRocket’s apogee, so the 5% bar is not met (ADR-173) | done |
| M4.6 | Monte Carlo from the CLI and Python: apogee and landing spreads without writing Rust | done |
| M4.6a | hpr mc | done |
| M4.6b | Monte Carlo in Python | done |
| M5.1 | The online layer, with an on-disk cache for working offline | done |
| M5.1a | The cache, its freshness rule and an offline mode that never fetches, tested with a hand-written sample response (ADR-117, Online data and the cache) | done |
| M5.1b | The HTTP transport, with rustls, behind a cargo feature, and the platform’s cache folder, tested against a server on the loopback address (ADR-118, Online data and the cache) | done |
| M5.2 | Weather forecasts, turned into atmosphere and wind profiles | done |
| M5.2a | A launch site’s weather from Open-Meteo, forecast or archived, as a sounding: the ground and the pressure levels above it (ADR-119, Launch-day weather) | done |
| M5.2b | Weather-balloon soundings from the University of Wyoming’s archive, as a sounding: the ground and every level above it (ADR-120, Weather-balloon soundings) | done |
| M5.2c | NOAA’s GFS and RAP forecasts from NOMADS, as a sounding: a small GRIB2 cut around the site, read by hpr’s own decoder and checked value for value against ecCodes (ADR-121, NOAA forecasts: GFS and RAP) | done |
| M5.2d | Weather files you download, and the hpr weather command: the command done in M5.2d1; NOAA’s whole files in M5.2d2, complex packing and M5.2d3, JPEG 2000 | done |
| M5.2d1 | hpr weather: a launch site’s profile from Open-Meteo, a Wyoming sounding, GFS, RAP or an ERA5 file, fetched, from the cache, or from a saved answer (ADR-122, The command line) | done |
| M5.2d2 | NOAA’s whole GRIB2 files: complex packing, checked against ecCodes on every value of a whole GFS file (ADR-123, A whole GFS file) | done |
| M5.2d3 | NOAA’s whole GRIB2 files: JPEG 2000, checked against ecCodes on four public RAP fields (ADR-124, Files in JPEG 2000) | done |
| M5.3 | Launch-site data: magnetic declination in M5.3a, ground elevation in M5.3b, distances between places and a user’s elevation file in M5.3c | done |
| M5.3a | Magnetic declination from the World Magnetic Model, WMM2025, checked against NOAA’s test values (ADR-125, The magnetic field) | done |
| M5.3b | A launch site’s ground elevation from Open-Meteo, cached so it works offline after the first lookup (ADR-126, A launch site’s elevation) | done |
| M5.3c | Distance and bearing between two places on the WGS 84 ellipsoid, and a site’s elevation from a GeoTIFF file the user gives; split in two (ADR-127) | done |
| M5.3c1 | Distance and bearing between two places on WGS 84, checked against Karney’s published set of 500,000 geodesics (ADR-127, Geodesy) | done |
| M5.3c2 | A launch site’s elevation from a GeoTIFF file the user gives, checked against GDAL’s reading through rasterio on seven files and a whole USGS tile (ADR-128, A launch site’s elevation) | done |
| M5.4 | Motor stock and prices from motor.fusionspace.co, joined with ThrustCurve.org’s motors; split in three (ADR-129) | done |
| M5.4a | The motor finder’s files (its build, every motor, those in stock, the vendors, one motor’s page) read through the cache, so they work offline, each carrying the credit the site asks for (ADR-129; its CC BY 4.0 credit, ADR-145; Motor stock and prices) | done |
| M5.4b | ThrustCurve.org’s search and a motor’s thrust curve through the cache, and the motors in stock matched to ThrustCurve.org’s by name, with a report of the misses: 282 of 282 matched (ADR-130; the tests’ stand-in searches, ADR-145; Motor stock and prices) | done |
| M5.4c | hpr motors search: motors in stock by class and price, from the network, a saved list or offline, with both sources’ credit on every list (ADR-131, Motors you can buy) | done |
| M5.5 | A catalog of parts: OpenRocket’s parts files, looked up by maker and part number, and parts from them in a design; split in two (ADR-132) | done |
| M5.5a | The 16 .orc parts files OpenRocket ships, bundled and read, every part held to OpenRocket’s own reading: 3,449 parts, 17,911 values to the bit, the rest counted with their causes (ADR-132, OpenRocket .orc parts catalogs) | done |
| M5.5b | Catalog parts in the builder, and a rocket of them flown: all 3,449 parts built and held to what OpenRocket builds, the departures counted with their causes (ADR-133, parts from a catalog) | done |
| M5.6 | A catalog of hpr’s own | not yet done |
| M5.6a | Format, search and a parts list | not yet done |
| M5.6b | Parachutes and recovery hardware | not yet done |
| M5.6c | Motor hardware and rail buttons | not yet done |
| M5.6d | More makers and electronics | not yet done |
| M6.1 | Monte Carlo runs and sensitivity; split in four (ADR-134) | done |
| M6.1a | Seeded dispersion of mass, center of mass, drag, motor, wind, rail and recovery delays; each flight the same whatever the run’s size or thread count; failed flights counted; the apogee’s spread (ADR-134, Monte Carlo dispersion) | done |
| M6.1b | Landing ellipses holding a chosen share of the landings, checked against normal spreads with known answers; the next flight’s ellipse; the landings each holds counted (ADR-135, Landing ellipses) | done |
| M6.1c | Sensitivity analysis, Morris screening and Sobol’ indices, checked against functions with known answers within their own standard errors (ADR-136, Sensitivity analysis) | done |
| M6.1d | 10,000 flights of a Level 2 rocket (one on a J, K or L motor) in 10 s or less, timed and recorded: 3.0 s for Valetudo, 9.4 s for a rocket past Mach 1.6, a run’s flights sharing one layout and one supersonic table (ADR-137, How long a run takes) | done |
| M6.2 | Design optimization; split in five (ADR-138) | not yet done |
| M6.2a | CMA-ES over continuous design variables, held to four test functions’ known minima and to pycma, its author’s implementation; a rocket’s ballast and body length found for a 3,048 m apogee and a 2.2-calibre margin, and checked by flying it again (ADR-138, Optimization) | done |
| M6.2b | Discrete choices (a motor, a catalog part) and limits such as a minimum stability margin; split in two (ADR-139) | done |
| M6.2b1 | Limits on a design, such as a minimum stability margin: candidates that break one rank below those that don’t (Deb’s rules); held to three test problems whose limited minima are known | done |
| M6.2b2 | Discrete choices (a motor, a catalog part) as whole-number variables, by CMA-ES with margin; held to three mixed test functions’ known minima and to an outside implementation; a motor and a Madcow nose cone chosen for a 3,048 m apogee under margin and rail-exit limits, and checked by flying the winner again (ADR-140, Optimization) | done |
| M6.2c | Several goals at once: NSGA-II finds a Pareto front; held to the known fronts of three standard test problems (ZDT1 to ZDT3) and to an outside implementation (pymoo), and a rocket’s trade-off between apogee and static margin, flown again and checked against CMA-ES (ADR-141, Optimization) | done |
| M6.2d | Bayesian optimization (EGO) for costly flights, split d1, d2 (ADR-142) | done |
| M6.2d1 | EGO: a surrogate (kriging) fitted to the designs tried, and the next design where it expects the most improvement; Branin’s and Hartmann 3’s minima within 1% in 50 evaluations from 20 seeds (ADR-142, Optimization) | done |
| M6.2d2 | EGO on Hartmann 6, a six-variable test function whose local minimum traps M6.2d1’s EGO in 7 runs of 10; one attempt (ADR-142, ADR-144, Optimization) | done |
| M6.2e | Robust designs: a Monte Carlo run inside the optimizer | not yet done |
| M6.3 | Competition rules as files, with scoring, limits and presets | not yet done |
| M6.4 | Airbrakes, with a controller that aims for a target apogee | not yet done |
| M6.5 | Roll control, roll only: tail-fin tabs first, then canards | not yet done |
| M6.6 | Submission packs: the numbers Student Launch and IREC ask for, from one design | not yet done |
| M6.7 | Ejection charges: black-powder size for a bay, always “ground test first” | not yet done |
| M6.8 | Field kit: checklists, the settings check and the ground-test log | not yet done |
| M3.4 | RockSim .rkt files in and out | not yet done |
| M3.5 | RASAero .CDX1 files in and out | not yet done |
| M3.6 | RocketPy scripts and files in and out | not yet done |
| M4.4 | Use from C, and in the browser through WebAssembly | not yet done |
| M7.1 | Reading flight logs from altimeters and trackers, with no design file and no simulation | not yet done |
| M7.2 | The readings a flight gives, each with where it came from, and reconstructing the flight from its log | not yet done |
| M7.3 | A flight against its simulation: residuals, and fitting drag, mass, impulse and wind to the log | not yet done |
| M7.4 | Diagnosing what went wrong in a flight | not yet done |
| M8.1 | A design assistant | not yet done |
| M8.2 | An editing model for apps: commands, undo and stable ids | not yet done |
| M8.3 | CAD interop: parts and designs out to meshes, drawings, FreeCAD and STEP, and meshes or STEP solids in as custom parts, through files only (ADR-144) | not yet done |
| M8.3a | Any part or the whole design written as a watertight mesh in millimeters: STL, 3MF or OBJ | not yet done |
| M8.3b | Dimensioned drawings (SVG, DXF, PDF): fins with bevels, rings, nose profiles and tube cut lists | not yet done |
| M8.3c | A generated FreeCAD script that rebuilds the design as a parametric feature tree, driven by a spreadsheet of its dimensions | not yet done |
| M8.3d | STEP export as solids, through a permissively licensed geometry kernel | not yet done |
| M8.3e | A mesh read in as a custom part: its mass properties computed exactly from the closed mesh and a material, its aerodynamics recognised as a body profile, a fin or a protuberance, or else marked as not modeled | not yet done |
| M8.3f | STEP solids read in as custom parts, once a reader with a suitable license is found | not yet done |
| M8.3g | Warnings for parts printed on a 3D printer: heat limits, layer direction, unvalidated flutter | not yet done |
| M9.0 | Choosing how the app is built, with trial builds | not yet done |
| M9.1 | A desktop app | not yet done |
| M9.2 | 3D flight replay, the real flight beside the simulated one | not yet done |
| M9.3 | A web app that works offline | not yet done |
| M9.4 | Mobile apps | not yet done |
| M9.5 | Optional accounts that save designs and flights and sync them across devices | not yet done |
| M9.6 | Neer’s four web tools (Motor Finder, Charge, Window, Muster) rebuilt as library code and app screens | not yet done |
| M9.7 | Field equipment and frequencies | not yet done |
| M10.1 | Release 0.1: the simulator | not yet done |
| M10.1a | The flight and design fixes | done |
| M10.1b | The data, network and Python fixes | done |
| M10.1c | The release workflow | done |
| M10.1d1 | The release notes and the install pages | done |
| M10.1d2 | The missing warnings and the checklist | done |
| M10.1d3 | The friction and freeform fin warnings | done |
| M10.1d4 | The checklist | not yet done |
| M10.1d5 | The separated parts’ warnings | not yet done |
| M10.1d6 | The unknown signs | not yet done |
| M10.1d7 | The designation and the design pin | not yet done |
| M10.1d8 | The new name’s URLs | not yet done |
| M10.1d9 | FusionSpace HPR | not yet done |
| M10.2 | Release 0.2: the flight analyzer | not yet done |
| M10.3 | Release 0.3: accuracy and diagnosis | not yet done |
| M10.4 | Release 0.4: the competition kit | not yet done |
| M10.5 | Release 0.5: the app preview | not yet done |
| M10.6 | Release 1.0: the app | not yet done |
| M11.1 | A motor of your own | not yet done |
| M11.2 | Experimental solids | not yet done |
| M11.3 | Certified hybrids | not yet done |
| M11.4 | Nitrous blowdown | not yet done |
| M11.5 | Hybrid and liquid design | not yet done |
| M12.1 | Gores | not yet done |
| M12.2 | Opening loads | not yet done |
| M13.1 | Ground station | not yet done |
| M13.2 | GPS tracker logic | not yet done |
| M13.3 | Flight computer logic | not yet done |
| M13.4 | Hardware | not yet done |
| M13.5 | More tracker and flight-computer boards on one firmware | not yet done |
| M13.6 | Control hardware: drag-only airbrakes and roll-only tabs, held for a ruling | not yet done |
| M13.7 | Ground-test bench | not yet done |
Lessons from Loft
Loft was the project that came before hpr-sim. Its mistakes are listed as numbered lessons, such as Loft lesson L18, its made-up transonic drag curve, each with the test that guards against it here, or the milestone that will add one. The list of lessons says what went wrong, where in Loft, and how hpr avoids it.
The lessons these pages name
Each lesson that a page of this site names has a row here. Its label opens its section of the list of lessons, which gives Loft’s evidence and the name of the test that guards it; the last column is the milestone that added or will add that test.
| lesson | what went wrong in Loft, or what the lesson records | guarded by |
|---|---|---|
| L1 | Loft used one constant gravity on a flat Earth, so it read low against RocketPy | M1.1 |
| L2 | Loft treated geometric altitude as geopotential, so its temperature and pressure were off at 11 km | M1.2 |
| L3 | Loft’s atmosphere had four layers, and its last temperature gradient ran on forever: 335 K at 70 km, against 219.6 K | M1.2 |
| L4 | Loft’s air viscosity constants weren’t the 1976 standard’s, 1.3% high at sea level | M1.2 |
| L5 | Loft’s “today’s conditions” kept the standard temperature gradient above the field, with dry air and no sounding temperatures | M1.2 |
| L6 | Loft flew one wind vector; forecast profiles stepped at the lowest level; no gusts | M1.2 |
| L7 | Loft’s fin lift had no compressibility factor, so its slope and center of pressure never changed with Mach | M1.8a |
| L8 | Loft’s fin lift grew in proportion to the fin count, with no correction for 5 to 8 fins | M1.5a |
| L9 | Loft used the conical transition’s center-of-pressure formula for every transition shape | M1.5a |
| L10 | Loft took elliptical fins’ lift from an equal-area trapezoid with the wrong sweep | M1.5a |
| L11 | Loft merged fin sets into one for drag, so the result depended on their order | M1.5b |
| L12 | Loft’s drag used uncited constants, such as a body form factor of 1.95 where Niskanen gives 1.13 | M1.5b |
| L13 | Loft had no power-on base drag relief, which Niskanen’s drag method includes | M1.5b |
| L14 | Loft’s launch-lug drag was uncited, and rail buttons dragged as lugs | M1.5b |
| L15 | Loft gave a bare step in diameter no drag, and shoulder drag jumped to zero as a transition shrank | M1.5b |
| L16 | Loft silently capped the drag coefficient at 10, which hid malformed designs | M1.5b |
| L17 | Loft froze the fins’ leading-edge drag at its Mach 1 value, and gave nose and shoulder pressure drag no Mach term | M1.8 |
| L18 | Loft’s transonic wave drag was an invented curve, never checked against RASAero | M1.8 |
| L19 | Loft put a tube fin set’s center of pressure about 0.9 calibres forward of OpenRocket’s, and left out ring tails (a ring joining the fins’ tips). hpr’s is 1.07 calibres forward, measured and pinned, not within the lesson’s quarter calibre (ADR-102) | M2.2f |
| L20 | Loft flew in 3-DOF: no angle of attack, body lift, damping or roll, and no weathercocking | M1.6b |
| L21 | Loft stepped with RK4 without error control, and never tested that the apogee converges | M1.6a |
| L22 | Loft didn’t locate events exactly: apogee fell on a step, altitude deployments overshot, landings were below ground | M1.6a |
| L23 | Loft let discontinuities such as burnout fall inside steps, and checked its vacuum case to only ±2% | M1.6a |
| L24 | Loft’s simulate() changed its inputs, so a second run of the same flight differed from the first | M1.6b |
| L25 | Loft labelled any early stop “step budget”, even a rocket that never lifted off | M1.6b |
| L26 | Loft’s launch rail had no friction and no button geometry | M1.6b |
| L30 | Loft fixed the staging before the flight, so an apogee or height separation fell back to the burnout, and it never flew the booster | M1.9a |
| L31 | Loft’s clusters sat on the axis only, so a motor out turned nothing, and it sent a mixed cluster to its oracle as copies of the first motor | M1.9b |
| L32 | Loft’s flutter constant was half of NACA TN 4197’s, so its flutter speeds were √2 too high, on the unsafe side, and 7 of its shear moduli had no source | M1.10b |
| L33 | Loft published static margins of ±12 to 15 calibres where the normal-force slope had all but cancelled and the margin meant nothing | M1.10a |
| L34 | Loft counted the opening shock as the peak acceleration, and read thrust spikes low from a finite difference of the speed | M1.10a |
| L35 | Loft used zeros for “never happened”, and never said which height apogee was measured from | M1.10a |
| L36 | Loft’s .eng reader read only the first header, and appended a second motor’s points to the first curve | M1.3 |
| L37 | Loft’s delay parsing lost P (plugged) and lists of delays, and read marker values as seconds | M1.3 |
| L38 | Loft’s impulse class letter was off by one at the top of each band | M1.3 |
| L39 | Loft didn’t check that a curve’s times increase, and took the last point as burnout instead of NFPA 1125’s rule | M1.3 |
| L40 | Loft fixed the motor’s center of gravity at the casing’s middle, with no inertia of its own | M1.3 |
| L41 | Loft bundled thrust curves under mixed or unknown licenses | M1.3 |
| L42 | Loft’s impulse checks were loose (±8%); a mis-sourced curve flew about 26% high until caught | M1.3 |
| L43 | ThrustCurve’s data must override a .eng header’s size: one said 75 mm for a 54 mm motor | M1.3 |
| L56 | Loft told a design file’s container apart by its first bytes, and a malformed one had to give an error rather than crash | M3.1a |
| L57 | Loft threw away the thrust curves stored inside a .ork archive | M3.1a, M3.1c1 |
| L59 | Loft resolved an automatic radius within a stage only, so a booster’s first component took a stale number | M3.1b2 |
| L60 | Loft resolved automatic dimensions as it walked the tree, so a ring’s bore depended on sibling order and a bulkhead inside a coupler stayed NaN | M3.1b3 |
| L61 | Loft weighed a ring with an automatic bore at 0 g, and dropped a stated wall when the outer radius was automatic | M3.1b2, M3.1b3 |
| L58 | Loft kept an automatic dimension’s number but lost the flag, so saving turned it into a hand-typed one | M3.1b1 |
| L62 | Loft met a tag written under two names, and read a stated 0 as a missing value | M3.1b1 |
| L63 | Loft read neither the drag override nor the subcomponent flags, so a part set to no drag was still charged drag | M3.1b1 |
| L64 | Loft read the wind’s direction from the launch rod’s, and dropped it from the stored conditions | M3.1c3 |
| L65 | Loft read a configuration from only one of the two places a .ork keeps it, and fired a motor whose mount it could not find from the top stage | M3.1c1 |
| L66 | Loft dropped pods, parallel stages and booster sets, and its export then lost the note that the rocket was reduced | M3.1c4 |
| L67 | Loft exported a plugged delay as 0 s, ignition only for the whole mount, no launch conditions, and masses it had worked out as overrides | M3.2a |
| L68 | Loft exported a freeform fin as a trapezoid of the same area (42% too big), lost a cluster’s scale and rotation, and rounded every number to six decimals | M3.2a |
| L44 | Loft’s inertia was pitch only, with simplified formulas, and zero for rings and masses | M1.4a |
| L45 | Loft put a hollow transition’s center of gravity at the solid’s centroid | M1.4a |
| L46 | Loft never read fin tabs (100 to 120 g lost on two designs), and rail buttons weighed nothing | M1.4a |
| L47 | Loft’s reference diameter was the widest part, even an internal one | M1.4b |
| L48 | Loft had tangent ogives only, a silent default nose shape, and swapped Haack names | M1.4a |
| L49 | Loft’s transitions used a kinked profile, never checked against what OpenRocket means | M1.4a, M3.1 |
| L50 | Loft let a motor wider than its mount fly (+69% apogee), and fins could sit off the airframe | M1.4b |
| L51 | Loft’s rule for which center-of-gravity override wins was unsettled (up to 133 mm) and came from OpenRocket’s source | M2.2b1 |
| L75 | Loft’s RocketPy check wasn’t like for like: a different atmosphere, unstated latitude and gravity, and Loft’s own drag and mass fed to RocketPy | M2.1b, M2.1b2 |
| L76 | Loft’s advice to regenerate a reference when a check failed let the reference follow Loft’s own drag | M2.1a |
| L77 | Loft shipped hand-written “stored” results, one set inconsistent with itself | M2.1a |
| L78 | Loft’s test suites skipped themselves when their data was missing, and still reported a pass | M2.1a |
| L79 | Only 2 of Loft’s 12 metrics had a tolerance per case; a deployment speed 204% off passed as “ungated” | M2.1a |
| L80 | Same word, different quantity: Loft found the deployment and ground-hit speeds and the optimum delay differ by tool and OpenRocket version; hpr has measured OpenRocket 24.12 only | M2.2d1 |
| L81 | Metrics for events that never happened, such as a deployment speed with no deployment, were scored as 0 | M2.2d1 |
| L82 | References 60% apart were excused as “no single target”, and known issues excused the two largest misses. Tested: every compared case counts, whatever explains a miss, against both references where a rocket has two | M2.2f |
| L84 | Loft wrote its counts by hand, for populations it didn’t name, and counted one disagreement 15 times | M2.4 |
| L85 | Loft’s check that a known gap had closed used half the tolerance, so it missed gaps that closed in between | M2.1b2, M2.4 |
| L86 | Loft published a headline accuracy figure without saying what it was compared with, over which rockets, or at what speeds | M2.4 |
| L88 | Loft’s only independent check held drag equal, so its own aerodynamics were never held to another code | M2.4 |
| L89 | Barrowman’s hand-worked values for a cone, a conical transition and an elliptical fin, which hpr’s tests check | M1.5a |
| L90 | Properties any drag model must keep, such as split fin sets dragging like one set, which hpr’s tests check | M1.5b |
| L91 | Exact volumes of nose cones (cone, tangent ogive, Haack), which hpr’s tests check | M1.4a |
| L93 | Staging: the sustainer lights at the booster’s burnout plus its delay, the mass steps at the separation, and a trigger never reached lights nothing | M1.9a |
| L94 | The optimum ejection delay must come out the same whether the delay flown opens the parachute early or late | M1.10a |
| L95 | A degenerate design, with a zero radius, a NaN, no fins or a negative mass, must be refused or fly to finite numbers, never to a NaN | M4.1a |
Plan and timeline
This page is an estimate, not a promise. It says how likely each of hpr-sim’s goals is to work out, how much work each still needs, and when each release could be ready. It was made on October 6, 2026, from the roadmap and the project’s pace up to that day. It goes out of date as work ships or the roadmap changes, so read its dates as a forecast from that day.
How far to trust it.
- What kind of figure: an estimate, from a count of the work left and the pace so far.
- Based on: the first 20 days of the project (September 17 to October 6, 2026), in which 169 changes that each finish a milestone increment were merged (of 222 merged in all), about 8.5 a day. The open-ended milestones in that time took 3 to 6 times the increments first planned for them. The same history sets the pace, so it is not an independent check.
- What to rely on instead: the roadmap for what comes next and in what order; a published release for what is done.
In short
The dates are when each release’s code could be ready. A release is out once Neer, the project’s owner, publishes it.
- Release 0.1, the simulator: code ready around October 9, 2026; by October 11 if the work grows.
- Release 0.2, the flight analyzer: around October 14; by October 19.
- Release 0.3, accuracy and fault diagnosis: around October 23; by November 2.
- Release 0.4, the competition kit: around October 29; by November 12.
- Release 0.5, the app preview: code ready around November 3; by November 20. It is out once Neer has used it and any changes that turns up are made.
- Release 1.0, the app: code ready around November 13; by December 6. It is out, again, once Neer has used it.
- Everything else on the roadmap that is software: around December 5, 2026; by mid-January 2027 if the work grows.
- The one goal that is not only software, an avionics board built and flown, has no date. It follows Neer’s build and the launch calendar, so spring 2027 at the earliest.
How the estimate is made
Each milestone is counted in increments, then divided by the pace. An increment is one change with its own done when, the test the roadmap sets it: reviewed, tested on macOS, Windows and Linux, and merged. The roadmap lists the increments planned for each milestone. Each count below adds the growth that similar milestones showed once work began: little for well-defined work such as a file format, two to three times for open-ended work such as accuracy or a user interface.
The pace assumed is about 9 increments a day for release 0.1, 8 a day through release 0.4, and 7.5 a day after that, as the code grows and each check takes longer. Those rates already allow for time spent on bug reports. The first 20 days averaged 8.5 a day.
Each date comes in two forms. The likely date is the count divided by the pace. The slow case stretches every duration by 1.6, on top of the growth already in the counts. The 1.6 is a judgment from that history, not a statistical bound.
The releases
| release | what it adds | increments left | likely | slow case | also waits on |
|---|---|---|---|---|---|
| 0.1 the simulator | Monte Carlo from the command line and Python (M4.6; Monte Carlo flies many copies of a flight with its inputs scattered); logged apogees from real flights (M2.3c1); release builds and named fixes (M10.1) | about 25 | 2026-10-09 | 2026-10-11 | Neer publishing it |
| 0.2 the flight analyzer | flight-log importers (M7.1); readings and smoothing (M7.2); a flight against its simulation (M7.3) | about 45 | 2026-10-14 | 2026-10-19 | Neer publishing it |
| 0.3 accuracy and diagnosis | the accuracy campaign from Mach 0 to 2.5 (M1.14b to M1.14g); logged traces (M2.3c2); fault diagnosis (M7.4) | about 70 | 2026-10-23 | 2026-11-02 | Neer publishing it |
| 0.4 the competition kit | optimizing a Monte Carlo result (M6.2e); challenge presets (M6.3); airbrakes (M6.4); submission packs (M6.6); ejection charges (M6.7); RASAero and RocketPy files (M3.5, M3.6) | about 50 | 2026-10-29 | 2026-11-12 | Neer publishing it |
| 0.5 the app preview | C and WebAssembly bindings (M4.4); the user-interface choice (M9.0); 3D replay of a real flight beside its simulated ghost (M9.2) | about 35 | 2026-11-03 | 2026-11-20 | Neer using it, then publishing it |
| 1.0 the app | the desktop editor (M9.1); the web app that works offline (M9.3); the design assistant (M8.1); undo and edits (M8.2); RockSim files (M3.4) | about 75 | 2026-11-13 | 2026-12-06 | Neer using it, then publishing it |
| after 1.0 | canards, CAD files, phone apps, accounts, the extended Mach band, motor, parachute and avionics design | about 165 | 2026-12-05 | 2027-01-10 | see below |
The dates are for the code. A release is out when Neer publishes it, which takes minutes. For 0.5 and 1.0 it also takes his own use of the app, and any changes that use turns up. Each column is its own scenario: the slow case for 1.0 can fall after the likely date for the work after 1.0.
How feasible each goal is
| goal | outlook | why |
|---|---|---|
| A simulation library on par with RocketPy | high; mostly done | The physics, staging, pods, Monte Carlo, optimization and Python bindings have shipped. |
| Accuracy: 5% mean apogee error on real flights, at least as good as OpenRocket | medium | Against seven real flights, hpr’s apogees miss by 6.04% on average (Accuracy). Against 55 more from a private collection, hpr’s apogees are +9.83% above the logs on average and OpenRocket’s +9.00%: neither meets 5%, and hpr is close to OpenRocket (Accuracy). On real flights, the inputs limit accuracy too: motors vary in impulse, logged masses are off, the weather is uncertain. Matching OpenRocket is likely; the 5% mean may not hold everywhere. If the campaign stalls, its gaps are written down rather than hidden (the stopping rule). |
| Accuracy above Mach 1 | medium | Few real flights go supersonic. None of the seven public flights does. In the project’s private collection of flight logs (not published), 5 of the 42 flights that state a top speed reach about Mach 1 or more, and more may show up when the rest of its logs are read. Flights near Mach 2 are rare. |
| File interop: OpenRocket, RockSim, RASAero, RocketPy | high | OpenRocket’s files fly today. The others follow known formats and earlier work. |
| Competition design and submission packs | high | Mostly well-defined calculations from cited models. Testing that each safety number errs the safe way adds work, not risk. |
| Flight forensics: logs, fits, diagnosis | high; medium for diagnosis | Importers and fits are well defined. Diagnosis must name the right fault first for at least 80% of 200 simulated faulty flights; real anomalous flights depend on finding logs of them. |
| The app: editor, 3D replay, web app | medium | The code can be built steadily. Whether it is good to use is judged by people, starting with Neer, and that review sets the pace. |
| Phone apps and accounts | medium | They need an app-store account and a hosting provider, both Neer’s choices. Neither is part of 1.0. |
| CAD files | high for meshes and drawings; medium-low for STEP | Few permissively licensed libraries read or write STEP’s exact solids. STEP import may need a licensing decision. |
| Motor design | high for solid motors; medium for hybrids and liquids | Hybrid and liquid design waits first on a decision about US export rules for rocket propulsion. |
| Parachute design and avionics software | high | Each is checked against closed forms, published tables or exact round trips. |
| An avionics board | depends on Neer | It needs a board built, bench-tested and flown beside a commercial flight computer. |
What only Neer can do
- Publish each release.
- Use the 0.5 and 1.0 apps before they count as done.
- Choose an app-store account and a hosting provider, when phone apps and accounts come up.
- Decide how US export rules apply, before hybrid and liquid design.
- Build and fly the avionics board.
None of these blocks the work before release 0.5.
What could move the dates
- New goals. Each round of new goals so far has added weeks of work. The dates cover the roadmap as it stood on October 6, 2026.
- Bug reports. 96 issues were open that day, 29 of them high priority. A run of critical ones would delay milestone work.
- Hard milestones growing more than expected. Accuracy, diagnosis and the app are the most open-ended; the slow case allows for that.
- Slower checks as the code grows. The later pace already allows for some of this.
What 1.0 includes, and what comes after
Release 1.0 is the simulator, the flight analyzer, the competition kit and the desktop and web app. Phone apps, accounts, canards, CAD files and the Mach 2.5–3.5 band come after 1.0. Motor, parachute and avionics design come after that, one at a time (the suite decision record). Until 1.0, hpr flies only commercial solid motors.