Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 .ork files, 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 sim fetches a missing curve from ThrustCurve.org by the file’s manufacturer and designation, and caches it (motors from ThrustCurve.org). With those curves hpr sim flies 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 sim fly such powered separations, each device on the part its stage is in (M4.5g1, powered separation in hpr sim; M4.5g2, several separations); a dropped stage’s own flight is not validated. hpr sim flies 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.txt shipped with the program. There is no XSD: nothing machine-checkable describes a .ork, and every OpenRocket release has added tags.
  • Observed: 75 .ork files, all cached under refs/ 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 pinned OpenRocket-24.12.jar, 9 from the openrocket-database parts library, and 1 cached elsewhere. Generated files under refs/scratch/ are excluded from the default survey. By the files’ own creator attribute, 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 by cargo xtask ork unless 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):

containerfirst byteswhat it holds
zip50 4b 03 04 (PK␃␄)an entry called rocket.ork (the design as XML) beside anything else the design carries
gzip1f 8bthe design document, compressed on its own. What older OpenRocket versions wrote
XML<, after an optional byte-order mark and blank linesthe 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:

schemafileswritten by
1.44OpenRocket 13.05, 15.03
1.510OpenRocket 15.03
1.85OpenRocket 22.02
1.93OpenRocket 23.09
1.1053OpenRocket 24.12, and hand-written fixtures
1.111an 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, and cargo xtask ork finds 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 .ork tag 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.

tagautomatic
outerradius131
innerradius80
cd79
radius43
packedradius36
aftradius30
foreradius14

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.

newerolderelements with bothagree on textdiffer on frame
axialoffsetposition6426420
instancecountfincount1091090

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:

newerolderelements with bothagree on textdiffer on frame
angleoffsetrotation959595
angleoffsetradialdirection262626
radiusoffsetradialposition0n/an/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:

kindmeansraised for
Skippeda whole part was left outa 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
Droppeda value was ignoreda 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
Unusualread as it standsa 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 version attribute that is missing or is not major.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 found75
opened73
written and read back unchanged73
warnings raised reading the container and the document0 (building a rocket from them raises 39; see below)
refused, not well-formed XML2
deepest nesting17
elements holding text beside children48, 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:

sourcefilesreadlaid outerrors
the private design library (loft-fixtures)2727270
the OpenRocket 24.12 jar’s examples1717170
everything else under refs/3432312 (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:

partnumbers
the nose coneits shape, length and base radius, and, for a Haack series nose, its shape parameter (0 for a Von Kármán nose)
each transitionits length, and its radius at each end
each trapezoidal or elliptical fin setthe number of fins, root chord, tip chord, span, sweep, cant and cross-section (square, rounded or airfoil edges)
each of those partsits station: where its front sits, in meters aft of the nose tip
the rocketits 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 designseach file once
designs compared (files OpenRocket opens)7151
numbers compared1,171855
hpr agrees with RocketSerializer1,061772
RocketSerializer differs, and hpr’s number is OpenRocket’s11083
hpr differs from both00
hpr’s number is OpenRocket’s1,171855

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:

causecount
a fin set’s station: RocketSerializer adds the lengths of the parts before it in its parent, as in the worked example75
a transition’s radius: RocketSerializer looks transitions up in OpenRocket by name, and takes the first of that name15
no cause shown20

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 ogive of shape parameter 0. OpenRocket draws it as a cone, and so does hpr, while RocketSerializer passes on the word ogive.

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 ork checks 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 sayshow it is readwhy it is in doubt
no shapeclipped on a transitionclippedit is the shape that reaches both radii; only 1 of the 21 transitions in the corpus states it
an angle, which way it turnsthe same way hpr’s own frames turnhpr 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 finish2 µmthe 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 tagread asnotes
innertube, tubecoupler, engineblockInnerTubemay hold parts of its own
centeringringCenteringRingbore and outer radius may both be automatic
bulkheadCenteringRing with no bore
trapezoidfinset, ellipticalfinset, freeformfinsetFinSetwith 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)
tubefinsetTubeFinSetan 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, railbuttonLaunchLug, RailButtona 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)
masscomponentMassComponent
parachute, streamer, shockcordParachute, Streamer, ShockCordthe packed shape, not the deployment
podsetPodSetthe pod’s nose cones, body tubes and transitions, and everything on them (Pods)
parallelstagea Stage with its ParallelStage layoutinside 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 Skipped warning.
  • A packed part (a parachute, a mass component) inside a nose cone still takes no automatic radius. It is left out with a Skipped warning 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 + r around the airframe’s axis, where R is the body tube’s outer radius and r the tubes’ radius.
  • Neighbouring axes are 2π/N apart around that circle, for N tubes, so the straight line between them (the chord) is 2(R + r) sin(π/N) long.
  • Two tubes of radius r touch when their axes are 2r apart. Setting the chord to 2r gives r = 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 bodyradius
1 or 250 mm, the body’s
3323.2 mm
4120.7 mm
571.3 mm
650.0 mm, as wide as the body
831.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 Dropped warning, whether the radius is auto or 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 Dropped warning, 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: auto sets 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; six auto tubes with a wall thicker than their radius; and four auto tubes on a tube whose own radius is auto, taken from the nose cone ahead of it;
  • on a 20 mm tube: 1, 2 and 5 auto tubes. 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:

quantityhpr against OpenRocket, on all 19
the tubes’ radius and wallwithin 1e-15, relative, in the test (equal when the oracle script compares them in Python)
the number of tubesthe same
each part’s masswithin 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 airframewithin 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 itroughnesswhere that number comes fromin the corpus
roughRough500 µmthe author8
unfinishedUnfinished150 µmthe author2
normalRegular paint60 µmtechnical documentation §6 and the user guide’s dialog348
smoothSmooth paint20 µmthe author88
polishedPolished2 µmthe author; see the caveat below14

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):

patterntubesfigure
single1one tube
double, 3-row, 4-row2 to 4a row, one apart
3-ring, 4-ring, 5-ring3 to 5a triangle, a square, a pentagon, each of side one
6-ring6a hexagon of side one
3-star, 4-star, 5-star, 6-star4 to 7a center tube and a ring of radius one
9-grid9a 3 × 3 grid, 1.4 apart
9-star9a 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 methoddistancetube 50 mm in radius, pods 10 mm in radius, number 20 mm
relative (“surface of the parent component”)tube radius + pod radius + the number80 mm
surface (“… without offset”)tube radius + pod radius; the number is ignored60 mm
free (“center of the parent component”)the number, from the axis20 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:

quantityhow close to OpenRocket
each tube, fin set and lug in a pod: mass and center2 parts in 10¹⁵
a pod’s nose cone: mass, center8.3 parts in 10⁸, 4.0 parts in 10⁹
the whole rocket: mass1.2 parts in 10⁷
the whole rocket: center of mass1.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’s2 parts in 10⁹
pitch inertia about x_B, with no fins in the pods6.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 setscount
written9
read into PodSet9, holding 12 pods
left out0

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 ork lists 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 after the 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.

configurationapogee, 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:

rulein 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 linenone
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 touchesnone
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):

configurationOpenRocket’s apogee (m)hpr’s (m)difference
[A8-3]32.332.3−0.13%
[B6-4]90.590.7+0.18%
[C6-5]199.7200.7+0.48%
[C12-6]207.5207.7+0.10%
[D16-6]245.7246.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:
    • OpenRocket’s fin–fin interference factor, taken across the airframe’s fin sets (#325);
    • the freeform wings’ own center of pressure and normal force (#326).
  • 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.

designwhat the file saysOpenRocket 24.12hpr
a tubeauto25 mm25 mm
a tubeauto 0.0425 mm25 mm
two tubesauto, auto 0.0425, 25 mm25, 25 mm
a nose coneauto25 mm25 mm
a nose coneauto 0.0325 mm25 mm
a transitionauto 0.03 to auto 0.0225 to 25 mm25 to 25 mm
a nose cone, a tubeauto 0.03, auto 0.03−1 m, 25 mm25, 25 mm
a nose cone, a tube, a transition, a tubeauto 0.033, auto, auto to 22 mm, 22 mm−1 m, 25 mm, −1 m to 22 mm, 22 mm25, 25, 25 to 22, 22 mm
a nose cone, a tube (the control)auto, 30 mm30, 30 mm30, 30 mm
a nose cone, two tubes, a tubeauto, auto, auto, 30 mm30, 30, 30, 30 mm30, 30, 30, 30 mm
the same, a coupler of automatic radius in the third componentas above30, 30, 30, 30 mm (first read: 30, 25, 30, 30)30, 30, 30, 30 mm
a tube, two tubes30 mm, auto, auto30, 30, 30 mm30, 30, 30 mm
a tube, a transition, a tube30 mm, 30 mm to auto, auto30, 30 to −1 m, 25 mm30, 30 to 25, 25 mm
a nose cone, a transition, a tubeauto, auto to 20 mm, 20 mm−1 m, −1 m to 20, 20 mm25, 25 to 20, 20 mm
a nose cone, a tube | a tubeauto, auto | 30 mm30, 30 | 30 mm30, 30 | 30 mm

What the table shows:

  • The number cached after auto is 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):

filewhat it holdswhat happens
Debrief’s sample-design.orka <rocket> with a name, a comment and nothing else, plus a stored simulationholds no design; counted apart, not as a failure. Its stored simulation is read (stored simulations)
the openrocket-database parachute catalogfour tubes, every radius a bare auto, carrying the catalog’s parachuteslays out, four tubes at 25 mm, just as OpenRocket 24.12 opens it
Loft’s demo-quirks.orkthe worked example’s chain, and a parallel stage placed directly under the rocketlays 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 out72 of the 72 files that hold a design
documents that hold no design1, among the 73 readable files
automatic radii given OpenRocket’s default, 25 mm7, 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 file67 of 67 agree, over 18 of the 19 files it was run on
body components270
parts on and inside them752
by kind194 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 resolve320, plus the 7 above given the default: 327 in the files
parts left out, with a reason2; 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 nothing14, every one explained (below)
warnings raised16: 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 yetnone 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).

kindcountwhat raised it
Unusual1a part with no axial offset
Unusual7automatic radii with nothing along their chains to take, given OpenRocket’s default (above)
Unusual1a <rocket> holding nothing, so the document holds no design
Skipped1a 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: a configid, 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 as H148R, a digest (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:

  1. Inside the file. From schema 1.11 (the version on the file’s <openrocket> element), OpenRocket can save each motor’s curve in the archive as thrustcurves/<digest>.rse, named by the digest the <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.
  2. 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).
  3. 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 B4 is not a Quest B4.

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 saysit meanssource
<delay>6.0</delay>the ejection charge fires 6 s after burnoutOpenRocket’s technical documentation, p. 8
<delay>0.0</delay>the charge fires at burnoutthe same, p. 10: “zero-delay motors”
<delay>none</delay>plugged: no ejection chargethe 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 belowOpenRocket’s FAQ, “How do I create a staged rocket?”
launch, burnout, ejectioncharge, neverat launch; at the first burnout or ejection charge of the stage below; neverOpenRocket 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 eventthe 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 sayshpr lights the motor
launch plus dd seconds after launch
automatic plus d, in the bottom staged seconds after launch (an air start when d is more than 0)
automatic plus d, in a stage aboveas ejectioncharge
ejectioncharge plus dat 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 dd seconds after the stage below’s first burnout: the first of its motors to burn out, by each one’s ignition and curve
nevernever: the motor rides loaded, with no thrust
burnout or ejectioncharge, in the bottom stagenever, since it has no stage below
ejectioncharge or automatic, when the stage below’s motor is pluggednever, since a plugged motor fires no charge
automatic plus d, in a parallel stage hung on the last stage on the axisd 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 stageas written
any other event in a parallel stagerefused 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 burnout may 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 staging drops_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, and hpr sim notes 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 fromConfigurations that flyCounted by
The file and hpr’s bundled catalog alone4cargo xtask ork
Those, and ThrustCurve.org’s, as hpr sim fetches them136, and 9 more with --accept-design-errorscargo xtask ork-cli
Those, and OpenRocket’s own motor database145cargo 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 ork hands each solid motor’s curve to the reader for its digest, through design_with and SuppliedCurves. 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:

motorscount
read into their configurations208: 138 single-use, 65 reloads, 3 hybrids, and 2 not written, read like the rest
left out, in parts not read yet0; 2, both in parallel stages, before they were read (Parallel stages), and 6 before pods were read (Pods)
thrust curve from the file itself4
thrust curve from OpenRocket’s database, by digest178
thrust curve from the bundled catalog1
no curve25: 3 hybrids, and 22 with no curve in any of the three places
ejection delays132 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):

configurationscount
declared170
the rocket flies145, in 41 designs, and all 145 assemble
left out, by the first reason the reader finds24 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 166configurations
fly141
held back for another reason: stages hpr can’t separate as written1
still no curve: the motor records no digest, and the bundled catalog lacks it20
still no curve: a hybrid3
still no curve: a digest the database lacks1

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 wordOpenRocket’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 wordOpenRocket’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 altitude 30 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 .ork too, and hpr 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 the auto or 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 for auto: 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, ejection and launch fly; never, and ejection on 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 lowerstageseparation opens 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_recovery puts 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 in hpr sim).

Recovery in the reference library

cargo xtask ork, over the 72 designs, on 2026-10-05, after M4.5m read parallel stages:

quantitycount
parachutes and streamers read136: 134 parachutes, 2 streamers; 134 before parallel stages were read
left out, inside pod sets hpr did not read then2; since pods are read (Pods), none
drag coefficient79 auto, 57 stated
deploy event79 ejection, 32 apogee, 18 altitude, 6 lowerstageseparation, 1 never
devices a configuration changes11, with 25 changes in all
stages that state a separation20 of 92: 14 ejection, 4 upperignition, 2 burnout; 15 changes per configuration
separations left out, in parallel stages hpr does not read0; 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 as maxaltitude and optimumdelay.
  • The time series: a <databranch> per stage, whose types name its columns (Time, Altitude and 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, recoverydevicedeployment and 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 saysit meanshpr gives
<launchrodangle>5.0</launchrodangle>the rod tilts 5 degrees from verticalrod_angle_rad, 0.0873
<launchroddirection>45.0</launchroddirection>toward a compass bearing of 45 degrees, clockwise from northrod_direction_rad, 0.785
<winddirection>0.5</winddirection>the wind blows from a bearing of 0.5 radians, about 29 degreeswind_from_rad, 0.5
<windturbulence>0.1</windturbulence>turbulence intensity: the wind speed’s standard deviation over its meanwind_turbulence, 0.1
<atmosphere model="extendedisa"> with <basetemperature> and <basepressure>the standard atmosphere from a temperature (K) and pressure (Pa) at the launch siteAtmosphere::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.

wordOpenRocket 24.12 meansshown by the record
maxaltitudethe largest altitude above the launch site56 of 56
maxvelocitythe largest total speed (not the vertical speed)56 of 56
maxmachthe largest Mach number56 of 56
timetoapogeethe time of the highest stored step56 of 56; the apogee event’s time differs on 13
flighttimethe time of ground hit, the last step56 of 56
launchrodvelocitythe total speed at the first step past the rod’s length56 of 56; 0.06% to 7.50% above the speed at the rod’s end, placed by interpolating linearly in height
deploymentvelocitythe total speed at the last deployment, interpolated between the steps either side56 of 56; 53 deployments fall between steps, and on 17 flights the first deployment’s speed differs
groundhitvelocitythe total speed at ground hit, the last step56 of 56
maxaccelerationthe largest total acceleration before the first deployment56 of 56; over the whole flight it differs on 15, whose largest acceleration comes after a deployment
optimumdelaynot measured: withheldon 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 marginthe distance from the center of gravity (CG) aft to the center of pressure (CP), in calibres, at rod clearancethe 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:

deploymenttime, sspeed, m/s
first9.3018.305 at the 9.3 s step, 8.329 at 9.3025 s: 8.314 interpolated
last20.51414.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, while hpr sim prints 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 in hpr sim as 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 sizedflightswithin 5% after
    OpenRocket flies the flight again with its causes taken out (M2.2e4)64
    hpr flies the flight again on OpenRocket’s own drag (ADR-097)11

    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. At hpr 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 sim prints 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):

metricnamed causeflightsmedianrange
apogeenone36−0.18%−4.34% to +1.95%
apogeeOpenRocket’s parachute opened before apogee15+0.10%−1.24% to +13.80%
apogeehpr’s own drag, sized on OpenRocket’s1+6.95%+6.95%
apogeethe rocket turns over before apogee in both tools1−37.93%−37.93%
largest speednone51+0.46%−0.69% to +6.10%
largest speedhpr’s own drag, sized on OpenRocket’s1+6.40%+6.40%
largest speedthe rocket turns over before apogee in both tools1−15.76%−15.76%
margin at rod clearancenone53−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):

quantityflightsmedianrange
mass at launch53+0.008%−0.016% to +0.209%
mass as the rocket leaves the rod53+0.012%−0.030% to +3.257%
center of mass as the rocket leaves the rod53+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_it in xtask/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:

    motorbeforenownow, recovery in bothnow, 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:

designwhat it testsflightsapogeelargest speed
Two stage high power rocketa booster that separates, and a sustainer lit after it2−1.79% and −0.13%−0.54% and −0.04%
Three stage low power rocketa booster, then a middle stage, each dropping at its burnout as the stage ahead lights3−1.48% to −0.95%, two against the flight with nothing deployed+0.25% to +1.64%
Clustered motorsfour motors in a 4-ring, one in each tube5−0.79% to −0.35%, three against the flight with nothing deployed+0.20% to +0.92%
Airstart timinga 3-ring of I211W motors beside a K550W in its own mount, the ring lit at launch or 1, 2, 4 or 6 s later5+0.48% to +1.03%+0.46% to +0.81%
Parallel booster stagingtwo boosters beside the core, lit with it at launch and dropped at their charge at 2.44 s; or carried to apogee with no motor2+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 staging and later_stagings (MotorConfiguration::stagings lists them all, from the tail forward). A program turns them into the flight’s separations with hpr::ork::separations and 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_recovery gives each part the file’s own devices instead, and hpr sim flies a powered separation that way (Separation). The example ork_two_stage.rs does this for a made-up two-stage .ork; run it with cargo 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):

OpenRockethprΔ
separation, s0.860.860
height at the separation, m22.4422.18−1.17%
speed at the separation, m/s46.0846.06−0.04%
height at OpenRocket’s last row (1.81 s), m85.084.0−1.18%
speed there, m/s76.879.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):

rodapogee lost to the tilt, OpenRockethprwhere the rocket is at apogee, OpenRockethpr
5° toward north0.65%0.63%99.2 m north98.5 m north
10° toward east2.60%2.52%195.0 m east194.3 m east
10° toward south-west2.60%2.54%138.5 m west, 138.1 m south138.1 m west, 137.6 m south
20° toward east10.09%9.82%370.6 m east369.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 libraryOpenRocket’s exampleselsewhere under refs/
design files27, all distinct1731, 25 distinct
refused by OpenRocket004
configurations declared895624
with no motor OpenRocket loaded1016
aborted by OpenRocket010
flown to the end88, in all 27 designs55, in all 178, in 4 designs
with a margin at rod clearance88558
  • 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 .ork and 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 .CDX1 and 4 RockSim .rkt files. 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 .ork files 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):

metricflightsmedianfromto
apogee, no named cause28−0.31%−4.84%+1.17%
apogee, OpenRocket’s parachute open before apogee6−0.32%−3.09%+2.15%
apogee, hpr’s own drag coefficient1+13.60%+13.60%+13.60%
largest speed, no named cause34+0.27%−0.65%+2.28%
largest speed, hpr’s own drag coefficient1+7.98%+7.98%+7.98%
margin at rod clearance35+0.0308 cal−0.0166 cal+0.1108 cal
mass at launch35+0.000%−0.770%+0.070%
center of mass at rod clearance35−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 explain C07/1, which reads +0.68% with the parachute 0.55 s early, and C02/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 C03 and C09, 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 of C07, C08 and C11 read high, and so does C04/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%; and C12, 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. C03 and C09 come first, then C08 and C06/1. C03 reads +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. C09 reads 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).
  • C05 reads within 0.0007 calibres on four flights; on C05/2 it reads +0.0113, nearly all from its CG (−0.0111 calibres), with the mass there within 0.005%. Not traced.
  • C06/1 reads +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.
  • C01 reads −0.0166 and +0.0025 calibres. On C01/1 hpr 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 α), C12 reads +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 and C12’s three, an inner part is wider than its parent’s bore by more than the fit tolerance; on C06/1 a motor’s case can’t fit its mount; on C08/1 a fin is off its body tube. On all of C03’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, and C03’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 C06 the 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. On C06/1 the 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, in validation/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 in hpr-validate holds 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.
  • 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 onapogee Δ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:

quantitycount
stored simulations174, in 61 documents: 139 uptodate, 17 external, 11 outdated, 7 notsimulated
stored reference screen91 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 screenof 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 conditions173; 129 state the wind’s direction, and 135 launch into the wind
atmosphere172 isa, 1 not written
with a summary162
with a time series142, 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’s id, or the reference an angle or radius offset is measured from, which hpr does not read yet but for a pod set’s radiusoffset (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:

quantitycount
parts kept1, 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 kept87: 42 <photostudio>, 36 <docprefs>, 9 simulation <extension>s
tags kept1,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 kept3,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 path5,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 wrotehpr writes
a plugged delay as 0 snone, which reads back as plugged
ignition only for the whole mounteach configuration’s own ignition event and delay
no launch conditionsevery condition of every stored simulation
masses it had worked out, as overridesonly the overrides the design has, with their flags
a freeform fin as a trapezoid of the same areaevery point of the outline
a cluster without its scale and rotationthe cluster’s pattern, scale and rotation
each number to six decimalsthe 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:

quantitycount
designs written73
read back as the same design73
written again the same, byte for byte73
export warnings0
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:

wherefiles
the example designs that ship with OpenRocket17
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
all75

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.

quantitycount
designs75
opened as written71
opened as exported, of those71
designs with a configuration flown both ways48
configurations of the opened designs169
configurations flown neither way18
configurations flown both ways151
within 0.5% of the original’s apogee151
the same apogee to the last bit142
largest apogee difference0.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-fixtures row). 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.