Recovery
In short
- What it models: how a rocket comes down under a parachute, a streamer or tumbling: when each device fires, how a canopy fills, the descent in the wind, and a stack that splits into parts (separation), or lets out a nose cone or a payload (ejection), that each land.
- Sources: Knacke’s Parachute Recovery Systems Design Manual (1991) for parachutes; Carruthers and Filippone’s streamer tests (2005) and the OpenRocket technical documentation for streamers and tumbling.
- How well it is validated: against another simulator and against drop tests; no descent has
been compared with a real flight yet.
- The descent under a parachute matches RocketPy’s for five example rockets, started from the same state near apogee: all 30 metrics within 3%, the largest +2.865% (validation report). Drift is measured from that shared start, not from the pad.
- That comparison flies RocketPy’s gravity formula and RocketPy’s way of interpolating the wind, and hpr’s defaults differ from both (the full gravity vector, and a wind table interpolated by speed and direction). Under hpr’s own gravity, Calisto’s drift read a little closer, once, before the comparison switched (Against RocketPy, below); no test pins that. What hpr’s default wind interpolation does to drift has not been measured.
- Against measured drop tests, tumbling is −10 to +19% off, and the default streamer model predicts a descent +9% faster than Kidwell’s one flat streamer.
- A
.orkfile’s parachutes and streamers fly as OpenRocket flies them. The landing speed is within 0.02% of OpenRocket’s on 50 of its 53 example flights and within 0.12% on all 53. On the 51 with no named cause for an apogee gap, less the Base drag hack’s three, the flight time is −1.57% to +3.16% off (From an OpenRocket file). - Ejected pieces (a nose cone or payload that lands on its own, ejection) are checked against exact answers only: no simulator or flight has been compared with them yet.
- What it leaves out:
- Two effects on the opening load: the drag overshoot as a canopy fills, and, when the canopy opens at once (the default), the way a light rocket slows while it fills. So hpr’s peak opening load is no safe bound either way. Don’t size recovery hardware from it: use a dedicated opening-load method and the hardware’s own ratings.
- Airframe drag: a separated body falls with no drag until its device opens, so its deployment speed can read high, and the airframe’s own drag under a canopy is left out too. The same holds for an ejected piece.
- The push of a separation’s charge on the two stages (an ejection’s is modeled, a separation’s is not).
- Added mass (air carried along), the swing (the attitude freezes at deployment), and streamer pleats (+58% fast on a pleated one).
- Tumbling is used far outside its fit: 36 m/s for Valetudo, against 5.0 to 6.6 m/s, and a nose cone tumbling on its own is outside it too.
Code and sources
Code: hpr_sim::recovery in the API reference
(crates/hpr-sim/src/recovery.rs), and the descent branch of crates/hpr-sim/src/dynamics.rs.
Decisions: ADR-012 (parachutes and the descent), ADR-013 (streamers and
tumble), ADR-014 (separation), ADR-153 (a .ork file’s recovery, flown
by hpr::ork::recovery).
Sources:
- T. W. Knacke, Parachute Recovery Systems Design Manual, NWC TP 6575 (1991), for canopy drag
coefficients, filling times, drag-area growth and the equilibrium descent speed. Its title page
limits distribution, so it is cited, never redistributed (
docs/VALIDATION.md). - RocketPy 1.13.0 (MIT),
rocketpy/simulation/flight.py:2710-2790androcketpy/rocket/parachute.py, for the point-mass descent that the parachute milestone (M1.7a) is compared against. - J. Carruthers and A. Filippone, “Aerodynamic Drag of Streamers and Flags”, Journal of Aircraft
42(4), 2005, and the OpenRocket technical documentation v13.05 (CC BY-SA), Appendix C, for
streamers; the same documentation’s §3.5 for tumbling bodies, and its §4.2.5 for a
.orkfile’s parachutes and descent; and C. Kidwell’s drop tests (2001), a research report for NARAM-43, the National Association of Rocketry’s annual meet, as the measurement both streamer models are checked against.
Drag area
A device’s drag is set by its drag area C_D S, in m²: a drag
coefficient times the area that coefficient is measured on. hpr takes it in one of two ways:
- Given directly (
DeviceDrag::DragArea), which is RocketPy’scd_s. - From a canopy (
DeviceDrag::Canopy):C_D S = C_D0 · π D₀²/4, withD₀the nominal diameter andC_D0the drag coefficient on the nominal areaS₀ = π D₀²/4, Knacke’s convention (printed page 5-2;D₀ = √(4 S₀/π), soS₀includes the vent and every opening).
CanopyType carries Knacke’s printed data for thirteen canopy types:
- The
C_D0range (Table 5-1 for solid textile canopies, Table 5-2 for slotted). Knacke prints a range for every type and no single value; hpr’s default is the middle of the range (flat circular: 0.75 to 0.80, so 0.775). - The fill constant
n(Table 5-6), which sets how long the canopy takes to fill: the filling time isnnominal diameters divided by the speed at line stretch (Inflation). hpr takes it from the table’s unreefed column, for a canopy that opens freely. (Reefing is a line round a canopy’s edge that holds it partly closed at first, to cut the opening load.) - The drag-area growth exponent of Pflanz’s method (Figure 5-51). Pflanz’s method, in Knacke’s manual, works out the opening force with a drag area that grows as a power of the time since line stretch.
- The infinite-mass opening-force coefficient
C_x: the peak force as the canopy opens, over its steady drag at the same speed, for a load so heavy that it doesn’t slow while the canopy fills. hpr reports it; its equations don’t use it (Inflation).
Where Knacke prints “insufficient data” hpr has None, and the user has to supply the number.
RocketPy’s default parachute C_D of 1.4 is not a C_D0 in this sense: it is a hemispherical
canopy’s coefficient on the projected area, which it uses only to turn cd_s into a radius for its
added mass. Knacke’s hemispherical range on S₀ is 0.62 to 0.77.
Streamers
A streamer is a strip of fabric of length l and width w, so a
planform (one-side) area S = l w and an aspect ratio AR = l/w. StreamerModel picks the
correlation, the curve fitted to measurements that gives its drag coefficient:
-
Filippone(the default), onS, from wind-tunnel tests of cotton streamers clamped at the leading edge atAR3.3 to 30 and 6 to 18.9 m/s. The paper fits one power curve per planform area, and prints all three:planform area curve where 0.025 m² C_D = 0.561 AR^−0.480eq. 2 (Figure 2’s trend reads 0.561 AR^−0.4795)0.05 m² C_D = 0.6514 AR^−0.6075the trend line on Figure 3; the text does not repeat it 0.075 m² C_D = 0.405 AR^−0.494eq. 1 (Figure 4 reads 0.4046 AR^−0.494)hpr interpolates between neighbouring curves, linearly in the logarithm of the area (
ln S), and holds the end curve outside the fitted areas; that is hpr’s choice, not the paper’s. All three curves are needed, becauseC_Dis far from linear inln S. AtAR = 3.3, the middle area’sC_D(0.3154) sits 0.3% below the smallest area’s (0.3163), not partway down to the largest’s (0.2245); a straight line inln Swould put it 63% of the way there. Blending only the two end curves would read 18% low there.Three of the paper’s own findings bear on how to read it, and none is in its curves:
- a free leading edge gives more drag than the clamped mounting these curves come from;
- lighter, smoother fabric gives less (polyester at 64 g/m² against cotton at 177);
- at the largest area it finds a drag crisis, a sudden change in
the drag coefficient over a narrow range of Reynolds number,
near
Re = 7.2e5.
A rocket’s streamer is free and light, so the first two pull in opposite directions.
-
OpenRocket:C_Dm = 0.034 ((ρ_m + 25 g/m²)/(105 g/m²)) ((l + 1 m)/l)onS, withρ_mthe fabric’s mass per square meter (Appendix C, eq. C.6, printed page 117). It was fitted to model-rocket streamers (w0.01 to 0.09 m,l0.2 to 1.0 m, 10 to 80 g/m², 6 to 12 m/s), with a stated 12 to 27% error on an independent set. It is the only one of the two that uses the material.
Which model is right?
The two disagree by a factor that depends on the fabric: at AR = 10 and l = 1.016 m the ratio
of their drag areas is 5.8 at 10 g/m², 3.5 at 32 and 1.9 at 80.
C. Kidwell’s NARAM-43 drop tests (2001) settle it as far as one dataset can: sixteen 4 in × 40 in streamers, each with about 5 g at one corner, dropped 20.1 m.
Two details of his method decide how to compare, and both were got wrong here before review:
- His rates are distance over time, so they are averages over the drop rather than terminal speeds. A 20.1 m drop averages 0.97 of terminal at 2.8 m/s but 0.89 at 5.9 m/s.
- They are normalised to a notional 5.000 g weight: scaled to what that standard weight would give.
streamer_models_against_kidwells_drop_tests uses the notional mass and compares the same
average, from the closed-form fall:
| case | measured | C_D it implies, on S | Filippone | OpenRocket |
|---|---|---|---|---|
| crêpe paper, 32 g/m², unpleated | 2.80 m/s | 0.155 | 3.05 m/s (+9%) | 5.28 m/s (+88%) |
| Micafilm, 42 g/m², pleated | 2.04 m/s | 0.338 | 3.21 m/s (+58%) | 5.19 m/s (+154%) |
What the table shows:
- The flat streamer. Kidwell’s crêpe streamer is the one he left flat, and a flat correlation predicts it to 9%. Appendix C reads 88% fast.
- The pleated ones. His other materials were folded in ¾ in pleats, which more than doubles the drag again. hpr models no pleats, so it predicts a faster descent for a pleated streamer, which is the safe direction for a landing.
- Both are a little outside both fits: 0.1032 m² of planform against Filippone’s largest
0.075 m², and 0.1016 m wide by 1.016 m long against appendix C’s
w ≤ 0.09,l ≤ 1.0.
Streamers larger than the fits
Above 0.075 m², hpr’s clamp holds the largest area’s curve. Kidwell’s streamers are above it, so his drops test the clamp, and the evidence pulls two ways:
- The paper’s trend says the clamp reads high. Its
C_Dfalls as the area grows: the two end curves atAR = 10implyC_D ∝ S^−0.326, which extrapolated to Kidwell’s 0.1032 m² would giveC_D = 0.117where the clamp gives 0.130. The steeper inner pair,S^−0.53, would give 0.110. - The drop says it reads low. The drop itself implies 0.155, higher than any of them.
- What hpr does. It holds the end curve rather than extrapolating, because that is the closer of the two to the one free-drop measurement in hand. It says so here rather than claiming the trend.
- Two caveats. That rests on a single point, at one aspect ratio, with a fabric nothing like the paper’s cotton. And it holds partly because the correlation, measured with the leading edge clamped, is itself biased low, so two errors cancel.
- Nothing is measured anywhere near 0.225 m², the 1.5 m by 0.15 m streamer one of hpr’s tests flies on the clamp.
hpr therefore defaults to Filippone and keeps OpenRocket for comparing with OpenRocket
(ADR-013, streamer and tumble drag).
Tumble
A body with nothing deployed descends broadside, tumbling. DeviceDrag::tumbling(&assembly)
computes its drag area from the airframe by the OpenRocket technical documentation’s §3.5
(printed pages 53 to 55, eqs. 3.98 and 3.99):
C_D S = 1.42 A_f + 0.56 A_bt
A_btis the body’s side profile area, the outer diameter integrated along the axis,∫d dx. hpr takes a tube’s diameter times its length, and integrates each nose cone’s and transition’s own curve.A_fis, for each fin set, one fin’s planform area times an efficiency factor by fin count: 0.50, 1.00, 1.50, 1.41, 1.81, 1.73, 1.90, 1.85 for 1 to 8 fins (Table 3.4). It is a fit, not a model: four fins are 1.41 of one fin, not 2, and it is not monotonic. More than eight fins is refused.- Tube fins are refused: the model has no factor for them.
- A pod’s tubes and fins count as the airframe’s do, once per pod. The documentation’s model has no term for pods, nor for one part shading another from the air, so a rocket with pods tumbles at a drag area the fit was never checked on.
- The documentation notes 0.56 is half a circular cylinder’s 1.12 in crossflow (air flowing across
it, side-on), as expected of a cylinder falling at a random angle, and that 1.42 is “similar to
that of a flat plate 1.17 or an open hemispherical cup 1.42”. Those come from Hoerner’s
Fluid-Dynamic Drag (1965), which is copyrighted with no legal free copy; NASA TN D-540 and TR
R-474 carry the same numbers and are free (
docs/research/streamer-and-tumble-drag.md). - Until M1.11b hpr took each component’s end diameters, which under-counts a curved nose. For Valetudo’s tangent ogive that was 0.0111 m² against the true 0.0148 m², 25% low on the nose. Integrating the curve brought Valetudo’s tumbling speed from 36.77 to 36.38 m/s (ADR-086, ejection impulse and tumbling pieces).
How well it does. The documentation says its fit predicts its own five drop-test models within
3 to 14%. hpr does not reproduce that. Replaying Table 3.3 (printed page 54: five models, 22 m,
ρ = 1.31 kg/m³, the descent rate v₀ read from video to ±0.3 m/s) through hpr’s reading of the
model (the_tumble_model_against_its_own_drop_tests):
| model | fins | measured | hpr |
|---|---|---|---|
| #1 | 3 | 5.6 m/s | 5.27 (−5.8%) |
| #2 | 4 | 6.3 m/s | 5.96 (−5.4%) |
| #3 | 3 | 6.6 m/s | 6.13 (−7.2%) |
| #4 | none | 5.4 m/s | 6.43 (+19.0%) |
| #5 | fins only | 5.0 m/s | 4.50 (−10.0%) |
So the spread is −10 to +19%, and the finless tube is the outlier: it wants a body coefficient near 0.79 where the model prints 0.56.
The text pins neither the body-profile nor the fin-area convention (exactly which areas to measure). So either hpr reads the areas differently from whoever fitted the constants, or the claim is not reproducible. The table above is what hpr can demonstrate, so it is what hpr states.
Where it stops being true. The fit covers 44 to 103 mm bodies of 6.8 to 160 g descending at 5.0 to 6.6 m/s. A high-power booster is far outside it: Valetudo tumbling comes out at 36 m/s.
- A cylinder’s crossflow drag falls by roughly half above a Reynolds number near 2e5, its drag crisis. A 100 mm body reaches that at about 30 m/s, and Valetudo’s 80 mm at 36 m/s is right at it.
- So a real body that size would descend faster than hpr says.
- hpr does not model that fall, and nothing in the pinned sources covers it.
A piece on its own. DeviceDrag::tumbling(&assembly) covers the whole airframe, and
DeviceDrag::tumbling_stages(&assembly, (first, last)) a run of whole stages. For an ejected piece
that is part of a stage, Simulation::tumbling_piece(k) sums the same model over piece k’s own
body components and fins (Ejected pieces). A payload is refused: it has no
tube or fin of its own. A nose cone tumbling on its own is outside the fit, which was made on
whole rockets, so its speed is the model’s, not a measured one.
A tumbling body is a device like any other: give it a trigger, and it starts at that moment. hpr does not decide by itself when a rocket tumbles: nothing citable says when a stage becomes unstable enough (the same documentation declines to model the analogous streamer regime).
Triggers, lag and release
A device’s charge fires at its Trigger:
Apogee: when the center of mass is descending. hpr’s apogee event fires it the instant the height rate crosses zero.- A flight that starts in mid-air, already past its apogee, has no crossing to find, so the
charge fires at its first step. RocketPy’s own apogee trigger does the same: it fires whenever
the vertical velocity is negative (
parachute.py:368-376). - Such a flight starts from
Simulation::run_free, as the comparison with RocketPy below does, and as staging and flight-data replay will. - The charge fires once, at whichever of the two comes first.
- A flight that starts in mid-air, already past its apogee, has no crossing to find, so the
charge fires at its first step. RocketPy’s own apogee trigger does the same: it fires whenever
the vertical velocity is negative (
Altitude { height_above_ground_m }: the first time the center of mass is descending and at or below that height above the launch site. This is an altimeter’s main setting. A rocket whose apogee is already below the setting fires at apogee, because no crossing follows. That is RocketPy’s numeric trigger: vertical velocity negative and height below the setting (parachute.py:354-364). After the flight’s recorded apogee the rocket counts as descending even if its center of mass rises for a moment, as it can when a part is let go there (Released mass); without a release that is the same rule.Time { time_s }: a time after launch.MotorDelay { motor }: that motor’s ejection delay after its own burnout. The motor must have a delay in seconds; a plugged motor or one with no delay set is refused.Burnout { motor, delay_s }: a given delay after that motor’s burnout, whatever its ejection delay; a stage separation timed from the booster’s burnout, say (Staging). A motor that never lights fires neither this norMotorDelay.
Charges are only checked in free flight and during the descent, so a Time or MotorDelay
trigger whose time passes while the rocket is still on the pad or the rail fires at
rail exit.
lag_s seconds after the trigger the device deploys (line stretch, where the lines pull taut)
and starts to fill. The first deployment of a flight switches it to the descent phase.
A device can name another whose opening releases it (released_by), which is how a drogue is
cut away under a main:
- The release waits until the releasing device is fully open, not its line stretch. Cutting the drogue at line stretch would leave the rocket under an empty canopy: the drag area would collapse and the descent speed up (found in review; measured at 0.45 m² → 0.02 m² and 18.3 → 22.0 m/s before the fix).
- A released device contributes nothing from its release.
- One released before its own charge fires never deploys at all: its
Triggeris recorded and noDeploymentfollows.
Events, in the order a two-device flight records them: Apogee, Trigger(drogue),
Deployment(drogue), Trigger(main), Deployment(main), Release(drogue) (at the end of the
main’s filling), GroundHit.
Trigger times that are known before the flight (a time, or a motor delay) and every deployment and end of filling are stop times, so no integration step straddles a change in the drag area.
Inflation
A canopy doesn’t reach its full drag area at line stretch: it grows over a filling time. Each device that has deployed and is not released contributes
(C_D S)(t) = (C_D S)₀ · min(1, (t − t_d)/t_f)^j
with (C_D S)₀ its full drag area, t_d its deployment, t_f its filling time and j its growth
exponent. The open devices’ drag area at time t is the sum of these.
The exponent sets the shape of the growth:
j = 1is linear growth (Knacke’s ribbon and ringslot canopies);j = 2is the growth of solid cloth (Pflanz, Figure 5-51),(t/t_f)²: slow at first, fast at the end.
Inflation chooses t_f:
Instant(the default):t_f = 0, the full drag area at line stretch. This is RocketPy’s model. For a deployment well above the canopy’s terminal speed it gives hpr’s highest opening load; near terminal speed, as at apogee, a filling time can give a higher one, because the rocket speeds up while the canopy fills.FillingTime { time_s, exponent }: a filling time fixed in advance.FillConstant { constant, exponent }: Knacke’st_f = n D₀/v(printed page 5-43), withvthe airspeed at line stretch andnthe canopy fill constant, from Table 5-6’s unreefed column (printed page 5-44): 8 for a flat circular canopy. A deployment at rest has no filling time in this law (n D₀/vdiverges), so the canopy is taken as open at once and fills as the rocket picks up speed.
The fill constant at hobby speeds
Knacke states the linear form only “in the medium-velocity range of about 150 to 500 ft/s”
(45.7 to 152.4 m/s; Inflation::FILL_CONSTANT_RANGE_M_S). A hobby main is slower:
- A main opening at 20 to 30 m/s is below that range.
- There, his alternative for solid flat circular canopies is
t_f = n D₀/v^0.85withn = 4.0. It is a dimensional form (feet and ft/s) that cannot be used in SI as printed, so hpr does not. - So outside the range the filling time, and with it the peak load hpr reports, is an
extrapolation. At 25 m/s the linear form gives a 2.5 m main
t_f = 0.8 s, from a correlation fitted at three to six times that speed.
The opening load
hpr’s drag area rises to the steady value and stays there. Knacke writes the opening force as
F = (C_D S) q C_x X1 (printed page 5-50), with q the
dynamic pressure at line stretch. Its two factors fare
differently in hpr:
- The overshoot,
C_x, is left out. Knacke’s measured drag area overshoots the steady value by 10 to 80% near the end of filling (Figure 5-40, printed page 5-47). His infinite-mass opening-force coefficient isC_x = 1.7for a flat circular canopy; hpr’s is 1 in every mode. - The slowing,
X1, depends on the mode.X1allows for the rocket slowing while the canopy fills. It is 1 at infinite mass (a load too heavy to slow), and as low as 0.02 for a final-descent parachute with a low canopy loading (little weight for the canopy’s size).- With a filling time (
FillingTimeorFillConstant), hpr already includes the slowing: it integrates the rocket’s deceleration while the drag area grows, which is Pflanz’s method done step by step, without the overshoot. Don’t applyX1on top of hpr’s peak, or the slowing is counted twice and the load reads low. - Opening at once (
Instant, the default), there is no slowing: the peak is Knacke’s infinite-mass case withC_x = 1.
- With a filling time (
So the peak load hpr reports is no safe bound on the real one, in either direction:
- With a filling time, the missing overshoot alone can only raise the real peak, but the growth law and the filling time, extrapolated at hobby speeds, can move it either way.
- Opening at once, hpr applies the full drag area at line stretch: the infinite-mass case. A big main on a light rocket can therefore see far less than hpr’s instant peak.
- For scale, the 1.5 m flat circular canopy deployed at 60 m/s in
Verification peaks at 1.6 kN filling and 3.0 kN opening instantly. Knacke’s
infinite-mass
C_x = 1.7on the same dynamic pressure would be 5.1 kN.
Don’t size recovery hardware from hpr’s opening load. Use a dedicated opening-load method and
the hardware’s own ratings. Ludtke’s law and Pflanz’s X1 reduction factor are candidates for a
later milestone.
The descent
Once a device is open the rocket is a point mass: all of its mass at the center of mass, with no
rotation. In the launch frame, with m the mass, v_cg and
a_cg the center of mass’s velocity and acceleration, w the wind, ρ the density at its height,
g normal gravity, a_Coriolis the
Coriolis acceleration and T the thrust:
m a_cg = −½ ρ (C_D S)(t) |v_cg − w| (v_cg − w) + m (g + a_Coriolis) + T
- In code it is the free-flight equation. hpr uses the translational equation of
Rigid-body flight with the body’s rotation rate
ω = 0and the canopy drag in place of the airframe’s aerodynamics. Two things carry over:- The terms for a motor that is still burning: the center of mass moving inside the body as
propellant burns (
−m r″ − 2ṁ r′) and the exhaust jet’s terms. That page sums them, with the forces, intoT20. After burnout every one of them is zero. - The point the integrator follows is still the nose tip
O, not the center of mass. With no rotation and nothing burning the two move together, so the nose tip’s accelerationa_Oequalsa_cgand the equation is the one above.
- The terms for a motor that is still burning: the center of mass moving inside the body as
propellant burns (
- No moment. The drag acts at the center of mass along the air’s relative velocity, so it exerts no turning moment.
- The attitude freezes at deployment. The body rates are set to zero, and the nose tip keeps its
rigid offset from the center of mass,
r_cg(the center of mass’s position from the nose tip, in body axes). As the rates go, the nose tip’s velocity is shifted byω × r_cg(turned from body axes into the launch frame), so that the center of mass keeps the velocity it had. Whatever stops the rotation acts through the lines and the canopy, inside the system, so it can’t change the center of mass’s momentum. - The airframe’s own drag is left out, as RocketPy leaves it out. A rocket’s attitude under a canopy, and so the area it presents, is not modeled. For a drogue whose drag area is close to the airframe’s broadside area this is a real omission. It is the same omission RocketPy, the oracle this is compared with, makes. The tumble model (M1.7b, streamers and tumble) is where a body’s own drag belongs.
- The thrust
Tis kept, along the frozen axis, so a device that opens while a motor still burns (an off-nominal case) is not silently thrust-free. Its direction is wrong the moment the rocket would have swung under the canopy. - The rest is shared. Gravity, the Coriolis force, the atmosphere and the wind are the same models the rest of the flight uses (Rigid-body flight).
- Added mass is not modeled.
- Knacke gives no closed-form apparent mass (printed page 5-40 says only that it is the enclosed
volume times density times a form factor), and RocketPy’s
m_a = k_a ρ (2/3) π R² Hhas no citation in its code. - It carries no weight in RocketPy either, so it changes no equilibrium descent rate, only the transient right after an opening.
- It is not small: the fixture records 5.6 kg for Calisto’s main against a 16.2 kg rocket and 15.9 kg for NDRT’s against a 20.8 kg one (both at the start height; both grow with density as the rocket descends). The comparison below shows what leaving it out costs.
- Knacke gives no closed-form apparent mass (printed page 5-40 says only that it is the enclosed
volume times density times a form factor), and RocketPy’s
The equilibrium descent speed is Knacke’s (printed page 5-128), and recovery::terminal_speed_m_s
computes it:
v_e = √(2 m g / (ρ C_D S))
Separation
A Separation is a trigger and a stage boundary. At the trigger the stack comes apart into two
bodies:
- body 0 keeps the nose: the stages from stage 0, at the nose, through the one
after_stagenames; - body 1 is the stages aft of the split.
Each body flies on as a point mass under the devices that name it (Device::on_body). The ascent
ends there: its FlightResult has Termination::Separated, a Separation event, and one
BodyFlight per body in bodies. The exception is a powered separation, where body 0 still has a
motor to burn: it flies on as a sustainer, and only body 1 descends here
(Staging).
This section describes the unpowered case, and the booster’s descent after a powered one.
What happens at a separation:
- Each body is its own stages and their motors.
body_mass_propertiessums the stages’MassPropertiesand the motors mounted in them, so the bodies’ masses add to the whole rocket’s at that instant, which is a test. - Nothing pushes the bodies apart. Each body starts at its own center of mass, with the
velocity that point already had: the nose tip’s velocity plus the rotation’s share,
v_O + ω × r_cg, in the launch frame. So the bodies’ linear momenta add to the stack’s, which is a test. - Their spin is dropped. Each body’s center keeps moving round the stack’s as it was, but each body’s spin about its own center is lost, so angular momentum is not conserved. In the test, spinning at 0.6 rad/s, the lost spin is 31% of the angular momentum, and 0.02 J of energy.
- Only body 0’s devices act before the separation. A device meant for another body has a drag area computed for that body (a booster’s tumbling area, say), which is not a model of the whole stack, so it waits for its body. With no separation every device is body 0’s.
- Each body finds its own apogee, whatever its devices are triggered by. The ascent ends at the separation, so this is the only place a staged flight can record a peak, and without it a body separated while climbing would never fire an apogee charge (found in review, now a test).
- The bodies descend independently, each with the same point-mass equations as the descent
phase, less the thrust:
m a = −½ ρ (C_D S)(t) |v − w| (v − w) + m (g + a_Coriolis). They share the flight’s devices and their progress, so a canopy that opened before the separation stays open on whichever body carries it. - Bodies are not watched by the
Observer: their events and samples are in theirBodyFlight.
Limits:
- No push at a separation, and no tip-off. A separation’s charge or spring adds no impulse (an ejection’s can: see Ejected pieces), and the tumbling a tip-off would start is not modeled.
- A body must start above the ground, as a free flight must: the ground event is a falling crossing, so a body that started below the site would go on integrating underground until the flight’s time limit.
- Every body must carry a device, and it must open. The descent has no airframe drag (the descent-phase decision, ADR-012), so a body with nothing open would fall as if in a vacuum. A flight whose bodies are not all covered is refused when it is set up, and a body that reaches the ground without a single deployment (a timer set after that body lands, say) is a flight-time error rather than a landing at the speed of a fall with no drag (both found in review). A device on a body that nothing makes is refused when the flight starts, since an ejection given later could still make it. A device that opened on the stack before the separation counts as open.
- A body coasts with no drag at all until its first device opens, which is the same omission
as the descent phase’s and hurts more here: a 0.55 kg forward body that separates at 2 km and waits
for a 300 m main arrives at 168 m/s where an airframe would have held it near 60 to 70, so
its deployment speed, and any opening load taken from it, read high. Give a body a device that
opens at once (
DeviceDrag::tumbling_stagesover its own stages is the cited way) if the coast matters; after a powered separation hpr requires it (Staging). A spent booster’s device is usuallyDeviceDrag::tumbling_stages(&assembly, its stages), which is §3.5’s model over that body’s own components rather than the whole stack’s. - The aft body’s motors must have burned out, because a body’s mass is held constant through its descent. A trigger that fires while one burns is an error, before the flight when its time is known and in flight otherwise, not a silent approximation. A release across the separation is refused too: a line cuts a device on its own body. A forward body with a motor still to burn flies on as a sustainer (Staging).
Ejected pieces
An ejection lets a piece of the airframe go at a joint of your choosing, not only at a stage boundary: a nose cone pushed off its airframe, a body section, or a payload carried inside. Each piece then comes down on its own, under its own parachute or tumbling, and lands somewhere of its own. The charge can push the pieces apart. This is new with M1.11a and M1.11b, and is checked against exact answers only: no other simulator or real flight has been compared with it yet.
A few words first:
- A body component is one of the parts the airframe is made of, nose to tail: the nose cone,
a body tube, a transition. An internal component sits inside one: a parachute, an
altimeter bay, a mass. Each has an
idin the design file. - A joint is where two body components meet.
- A piece is a part of the airframe that can come away: the part between two joints that can part, or a payload.
- A body is whatever flies as one: the pieces still joined together.
How you give one. An Ejection is a trigger, the same kinds a parachute has (apogee, a
height, a time, a motor’s delay), and a place where the airframe parts:
Ejection::aft_of(trigger, "nose")parts the airframe at the joint just aft of the body component with that id. The side toward the nose keeps its body number. The new piece is the side aft of the joint, back to the next joint you have also given an ejection or a separation for, or to the tail. So ejecting a nose cone is written as parting the joint aft of it: the nose cone keeps body 0, and the airframe behind it becomes the new body.Ejection::payload(trigger, "payload")lets an internal component, and everything inside it, out of the piece that carries it. It must be inside the airframe, and not inside another payload.
Pieces tied together by a shock cord fly as one, so a nose cone on a shock cord is not an ejection at all: leave it out, and the rocket comes down in one piece.
Which body is which. A body takes the number of the piece nearest the nose among those still joined, and a payload flying alone takes its own:
| What makes the piece | Its body number |
|---|---|
| The nose’s side of the airframe | 0 |
| A separation, if the flight has one: the stages aft of it | 1 |
| Each ejection, in the order you give them | 1, 2, … in order; after 1 if the flight has a separation (2, 3, …) |
Each body needs a device that names it (Device::on_body). A flight whose bodies are not all
covered is refused when you give the ejections. A device on a body that nothing makes is refused
when the flight starts, since a separation given later could still make it: run returns an
error naming that body.
When devices act. Only body 0’s devices act before the airframe first comes apart. After that, a device acts only once its own body flies on its own. So a parachute on the nose cone (body 0) triggered at apogee fires and opens in the same step as the nose cone leaves. It is logged in the flight’s own events, as the example below shows, and its drag acts on the nose cone alone from then on. A device on a piece that hasn’t left yet waits for it, even if its trigger has come. Give a piece a trigger that follows its ejection. A drogue on the airframe, with the nose cone only ejected at 300 m, would leave the whole rocket falling from apogee to 300 m with nothing open.
What happens at each parting:
- When the airframe first comes apart, each body starts at its own center of mass with the
velocity that point had,
v_O + ω × r_cg(defined under Separation), as at a separation, plus the charge’s push below. The linear momenta add to the stack’s, which is a test. - When a body parts again on the way down, it is already a point mass with no attitude, so
there is nothing to place its pieces by. Both pieces start at the body’s position and velocity,
plus the push, and its mass steps down by the piece that left. Their true starting points could
be up to a rocket’s length apart, which is small against a descent of hundreds of meters. That
body’s event for the parting records its state just before (
BodyEvent::sample) and just after (BodyEvent::after): its lower mass, and its pushed velocity. - Each piece’s mass is its own components’. A stage that stays in one piece is counted by its own mass, overrides and all. A stage that parts is counted component by component. An override that doesn’t say how its mass divides is refused rather than guessed. That means a stage’s mass override, or a component’s override that covers what it holds. OpenRocket designs often override a stage’s mass, and such a stage can’t be parted. The pieces’ masses add to the rocket’s to 1e-12, which is a test.
- Every motor must have burned out by the time an ejection fires, because every body is a point mass of constant mass from then on. An ejection known to come earlier is refused when it is given, and one that fires early in flight is an error then.
The push of the charge. An ejection charge or spring pushes the two sides of the joint apart.
Give it as an impulse J: the push summed over its short duration, in newton-seconds, as a
motor’s total impulse is. For example,
Ejection::aft_of(Trigger::Apogee, "nose").with_impulse(1.0). Without one there is no push. hpr
doesn’t work J out from a charge’s size: as a rough guide, J ≈ m v for the speed v you
expect a piece of mass m to leave at, so 1 N·s puts 15.9 m/s on the example’s 63 g nose cone.
The push is equal and opposite:
-
The side forward of the joint gets
+Jtoward the nose, and the side aft of it−J. A payload leaves forward, out of its host, as it does when the nose cone comes off first. -
Each side’s velocity changes by
J/m, for its own massm, so the light side moves most. One newton-second is 4 m/s on a 250 g payload, and 1.8 m/s on the 556 g airframe it leaves. -
The momenta still add up exactly, which is a test.
-
Which way is “toward the nose”? While the airframe flies whole with nothing open, it is its axis at that instant. A device that opens at the same instant as the parting doesn’t count as open yet. Otherwise the body has no attitude to go by: a body flying on its own is a point mass, and a whole airframe that has hung from a device since earlier has only the attitude it had when the device opened. So hpr goes by the body’s velocity through the air:
- A body hanging from a device (any but a tumble) that was open just before that instant, deployed before it and not yet released, points its forward end against that velocity (upward, as it falls), toward the device. hpr assumes the device left through that end, as a main does once the nose cone is off: a payload let out under the airframe’s parachute is pushed up, toward it. For a drogue that left between the booster and the avionics bay, the airframe more likely hangs near level, and this direction is a guess.
- A body with nothing open, or only tumbling, points its nose along that velocity, as a stable rocket does.
- A body moving through the air at under 1 mm/s, at its own apogee in still air say, points up.
These are assumptions, not measurements (ADR-086). All the partings that fire on a body at one instant part it together, each final body taking the pushes of the joints on its sides, so the order you list them in doesn’t matter.
-
A separation has no push. A payload always leaves forward, so a push on a payload in the nose’s own section, which is closed at the nose, is refused when the flight starts. A pushed payload whose section’s forward joint hasn’t parted by the time it leaves is an error in flight.
In the example below, where every piece’s device opens as it leaves, 1 N·s at each parting moves the airframe’s landing by 0.0 m and the payload’s by 1.3 m: the drag takes the push away within seconds. A piece that coasts with nothing open keeps its push longer.
A piece that tumbles. A piece needs some drag of its own, or it falls as if in a vacuum. hpr
can treat a nose cone with no parachute as tumbling; a real one may instead fall point first, and
faster. Simulation::tumbling_piece(0) gives the drag area of the nose cone tumbling on its own,
by the tumble model over just its own parts. Use it as that body’s device,
Device::new("nose cone tumble", tumble, Trigger::Apogee).on_body(0), triggered with its
ejection, as the example below does.
- Piece
kis the piece nearest the nose in bodyk, numbered as in the table above. - It is that piece’s own area. A body that still carries another section, until that section’s own parting, tumbles with its first piece’s area only.
- The pieces of an airframe cut into sections have drag areas that add up to the whole airframe’s, which is a test.
- The model was fitted on whole model rockets, so for a lone nose cone the speed is the model’s answer, not a measured one.
A worked example. The example
ejected_pieces.rs
flies the 54 mm test rocket on an I175 with a 250 g payload in its airframe, in a 4 m/s wind from
the west. The nose cone leaves at apogee (16.23 s, 1,747 m) under a 0.45 m canopy. The airframe
comes down under a 0.9 m one, and lets the payload out at 300 m under a 0.6 m one. Each lands at
its own terminal speed, v_e above, for its own mass and canopy in the air at the ground:
| Body | Mass (kg) | Flies on its own from (s) | Lands (s) | At (m/s) | v_e (m/s) | East of the pad (m) |
|---|---|---|---|---|---|---|
| 0, the nose cone | 0.063 | 16.23 | 562.88 | 3.06 | 3.06 | 2,035.4 |
| 1, the airframe | 0.556 | 16.23 | 333.54 | 4.54 | 4.54 | 1,115.6 |
| 2, the payload | 0.250 | 268.05 | 333.14 | 4.57 | 4.57 | 1,114.0 |
The nose cone is light for its canopy, so it stays up 229 s longer and drifts 920 m further
downwind. The airframe’s body weighs 0.806 kg until the payload leaves, then 0.556 kg. The
flight’s own event list ends at the first parting. What happens to each body after that,
its canopy opening and the payload leaving the airframe, is in that body’s own list
(FlightResult::bodies).
The example then flies the same rocket again with a 1 N·s push at each parting, and with no canopy on the nose cone, which tumbles:
| Body | Mass (kg) | Lands (s) | At (m/s) | v_e (m/s) | East of the pad (m) | Moved from the first flight (m) |
|---|---|---|---|---|---|---|
| 0, the nose cone, tumbling | 0.063 | 130.93 | 14.82 | 14.82 | 269.6 | 1,765.7 |
| 1, the airframe | 0.556 | 333.42 | 4.54 | 4.54 | 1,115.6 | 0.0 |
| 2, the payload | 0.250 | 333.33 | 4.57 | 4.57 | 1,115.3 | 1.3 |
Tumbling, the nose cone comes down nearly five times as fast as under its canopy, and lands 1.8 km
nearer the pad. The push barely moves the other two. At 300 m it changes the payload’s velocity by
4.00 m/s, up toward the airframe’s parachute, and the airframe’s by 1.80 m/s, down: J/m for
each (the example prints the upward parts, +4.00 and −1.80 m/s). Within seconds the drag has
taken that away.
Limits:
- A piece coasts with no drag until its device opens, as every separated body does. Give it a parachute, or its tumble from the moment it leaves.
- The push’s direction is assumed whenever the airframe isn’t flying whole with nothing open: it goes by the velocity through the air, as above. A payload always leaves forward.
- No powered separation with ejections. A sustainer flies on a design cut at its stage boundary, whose components aren’t the pieces the ejections were given for, so it is refused, and so is an ejection ahead of a separation that would light a motor.
- A piece’s spin and attitude are not tracked, as for a separated body.
- A
.orkfile’s recovery settings don’t make ejections, and hpr has no names for pieces: you find the component ids in the design file.
The decision records on ejected pieces, ADR-085, and on the push and tumbling pieces, ADR-086, have the reasoning.
From an OpenRocket file
A .ork file’s parachutes and streamers fly as OpenRocket flies them, and land at OpenRocket’s
speed to within 0.02% on 50 of its 53 example flights, and within 0.12% on all 53. The function
hpr::ork::recovery turns each parachute and streamer of the
configuration flown into one of the devices above, and hpr sim flies a
.ork file’s devices through it. The model is OpenRocket’s, not Knacke’s, as the file’s author
chose it. This is a check against another program only: no descent from a .ork has been
compared with a real flight. The decision record on flying a .ork’s recovery,
ADR-153, has the reasoning and the targets.
How each device flies:
- Drag. A parachute’s drag area is
C_D · π D²/4, withDthe canopy diameter andC_Dthe file’s value, or 0.8 when the file saysauto. That default comes from OpenRocket’s technical documentation (v13.05, section 4.2.5), which cites Hoerner’s Fluid-Dynamic Drag (1965). A streamer’s is the statedC_Don its length times its width, or forautoOpenRocket’s own estimate (which streamer model is right). - Opening. Each device opens fully at once, the file’s delay after its event, with no filling time.
- Events.
apogeeopens at apogee.altitudeopens the first time, on the way down, that the center of mass is at or below the stated height above the ground.ejectionopens at the ejection delay of the first motor that lights in the device’s own stage.launchopens the delay after launch. - The descent. Once a device opens, the rocket is a point mass under the open devices’ drag alone, the airframe’s left out (The descent). That is OpenRocket’s own model: “the entire drag coefficient of the rocket is assumed to come from the deployed recovery devices” (section 4.2.5).
A worked example. A parachute 0.6 m across, with C_D set to auto, has a canopy of
π × 0.6²/4 = 0.283 m², and a drag area of 0.8 × 0.283 = 0.226 m². hpr sim prints it as
recovery: Main at apogee, 0.226 m² of drag area, then when it opened.
Some devices never open, and some are refused:
- No device. A device set to
never, or toejectionon a plugged motor or one with no delay given, gives no device.hpr simnotes “Mainnever opens”, with why. - No motor lights in its stage. A device set to
ejectionopens at its own stage’s charge (OpenRocket labels the setting “First ejection charge of this stage”). In a stage where no motor lights, it never opens, as in OpenRocket 24.12: probed, a charge from another stage never opens it, whether the stages are joined or apart, and neither does a stage with no motor, a motor that never lights, or a plugged one.hpr simnotes “Mainnever opens: it opens at its stage’s ejection charge, and no motor of its stage lights”, and flies the other devices. Until M4.5i, hpr refused the configuration’s whole recovery for it (ADR-168). - Refused by name. A device inside a part hpr doesn’t read, and one set to open by a word hpr
doesn’t know. One set
to open at
lowerstageseparationopens at the split right behind its stage when that split comes with nothing left to burn (Staging), and is refused otherwise. The flight then falls on its airframe alone, with the reason in its notes. - A height above the apogee. hpr opens such a device at apogee, as an altimeter below its
main’s height at apogee would fire. OpenRocket’s one probed run never opened it
(what the words mean).
hpr simnotes when this happens. - The height’s zero. hpr counts a height from the ground. OpenRocket counts it from where the center of mass starts on the pad, so hpr opens a device set to a height that much lower.
How close it is. cargo xtask ork-flights flies each configuration of OpenRocket’s examples
that hpr flies a second time, with its devices, and compares it with OpenRocket 24.12’s own flight
(the report’s descents). The targets were set before measuring: each deployment
within 0.1 s or 2% of OpenRocket’s time, whichever is larger, and the landing speed and the flight
time within 5%. All 53 descents are compared. On the 7 configurations that drop stages under
power, it is the sustainer’s descent, and on the 6 that drop a payload with nothing left to burn,
the payload’s: in each, the branch OpenRocket’s record keeps
(Separation).
- 48 flights: the report’s 51 with no named cause for an apogee gap, less the Base drag hack‘s three (below). The landing speed is +0.00% to +0.02% off OpenRocket’s, and the flight time −1.57% to +3.16% (0.68% on average, either way). Among them are two of Pods–powered with recovery deployment: on its booster’s flight, the two pods’ parachutes open at launch + 5.35 s in both programs, and the flight time is −1.02% off.
- 40 of those 48 meet every target as written. The other 8 miss only on a deployment’s time. All 8 reach an apogee 0.63% to 4.34% below OpenRocket’s, a climb gap the same report shows. And OpenRocket opens a device set to a height at the end of its time step: 0.06 to 12.64 m below that height over the 15 such deployments, 0.36 to 11.15 m on the 8 that miss. With those two taken out, every deployment is within its target; the largest left is 0.33 s. That is what “met once sized” means in the report: each deployment that misses is held instead by what is left of it. For a device opened at apogee this holds by construction, as it takes out the apogee’s own time; the evidence on the descent itself is the landing speed, and on the dual-deploy flights the drogue’s speed as the main opens, within 0.07% on 5 of 6.
- The Base drag hack’s three flights have no named cause either, but read high in apogee (a part set to no drag). They land 0.11% to 0.12% faster than OpenRocket, and their flight times are +1.84% to +7.23% off; the last misses its target.
- 2 flights with a named cause for an apogee gap. One is put down to hpr’s tube-fin drag, and
in the other,
[C6-7; B6-0]of Pods–powered with recovery deployment, the sustainer turns over before apogee in both programs. Both land at OpenRocket’s speed (+0.00%), but their flight times are −52.64% and +5.78% off. - The private designs. On 34 descents, published only as differences (report), the landing speed is −1.53% to +0.04% off OpenRocket’s and the flight time −4.98% to +3.72%. 31 of 34 meet every target as written, and all 34 once each deployment that misses is held by what is left of it; that sizing rests on numbers the private report does not publish, and the −1.53% landing speed is not explained.
A stage that drops away under power. Each device rides
the part its stage is in (hpr::ork::separated_recovery),
and each event is that part’s own: apogee is the part’s own apogee. After the split each part
is a point with no airframe drag (Separation), so a part needs a device from the
split. The booster tumbles from the split until its first own device in file order
opens, which releases the tumble (hpr::ork::tumbling). A
sustainer with no device of its own tumbles from its apogee. hpr sim flies it this way
(Separation). On the Two stage high power rocket’s two configurations,
the sustainer’s landing speed is +0.00% off OpenRocket’s and its flight time +0.32% and +0.73%.
On the first, the H148R-0 and H148R-0 configuration, the main opens 1.09 s before OpenRocket’s,
past its target. Its apogee is 1.79% lower and OpenRocket opens it 8.9 m below its set height;
with both taken out, as How close it is above does, 0.065 s is left
(ADR-159, powered separation in hpr sim).
What it leaves out. The booster’s own descent is not validated: OpenRocket flies it as a rigid
body, and its record keeps only the sustainer’s branch. A tumbling booster falls side-on from the
split, where a real one flies nose-first for a while, so its peak and landing are likely too low
and too close to the pad (#179). hpr sim
flies a configuration whose separation can only come after apogee as one stack, with a note.
Several separations under power fly, one after another
(M4.5g2, several separations). A payload dropped with
nothing left to burn flies when its own device opens at the split
(M4.5g3, a payload’s split).
Verification
crates/hpr-sim/src/recovery.rs’s tests, all analytic (checked against exact answers) unless they
name the oracle:
| What | Result |
|---|---|
Knacke’s v_e against a case from Loft, the project before hpr-sim (1.1 kg, 1 m flat canopy, C_D 0.8, ρ 1.225) | 5.294 m/s, as Loft printed |
| A descent from rest against the closed-form fall under quadratic drag (2 km, uniform air, constant gravity) | 2.1e-8 of the terminal speed v_t over the whole descent; the landing time within 1e-5 s of the closed form’s 204 s |
| Drift in a steady wind, entered drifting with the air, no Earth rotation | exactly the wind times the time of flight (1e-8); the fall itself within 1e-4 of the closed form |
| The Coriolis drift of a 3 km descent, Earth rotation on | 0.3666 m east against the steady prediction 2Ω cos φ · v_t²/g · T = 0.3685 m, 0.52% apart, and 29 µm north |
Knacke’s filling law, t_f = n D₀/v and (t/t_f)^j | the recorded drag area to 1e-9 of (C_D S)₀ |
| Inflation against instant opening (deployed at 60 m/s under a 1.5 m flat circular canopy) | peak load 1,615 N against 3,020 N instant (0.53 of it), between the closed-form 1,527 N without gravity and 1,670 N with it |
| An oversized canopy (5 m) opening at 100 m/s, 10 km of descent at 2.95 m/s | lands in 3,392 s in 6,914 accepted steps and 2 rejected (a mean step of 0.49 s, where Loft’s explicit RK4 needed a 2e-4 s floor) |
| A deployment with a 0.7 rad/s body rate, and another inside the burn in a crosswind | the center of mass keeps its velocity across the handover to 1e-12, and the nose tip’s moves by exactly ω × r_cg, turned into the launch frame |
| A whole flight: drogue at apogee with a lag, main at 300 m, drogue released | events in order; each stage settles within 2% of its own v_e |
| Two devices triggered at the same instant | both open in the same pass, and the descent settles at the v_e of the sum of their drag areas |
| A device released before its own charge fires | it is recorded as triggered and never deploys; the descent stays at the open device’s v_e |
| A drogue released by a main that fills over 2 s | the release waits for the end of filling, the drag area never falls below the drogue’s, and the descent never speeds up |
| An apogee charge on a flight that starts descending | it fires at the first step (there is no apogee event to find), and a climbing start still waits for the apogee |
| Two user events and an altitude device on one flight | the user events keep their numbers and fire during the descent, in height order |
| The same recovered flight flown twice | bit-identical rows, events, final sample and step counts (Loft lesson L24: a run does not mutate the simulation) |
| A separation at apogee of the two-stage test design, canopy on the sustainer and tumble on the booster | both bodies land: the 0.550 kg sustainer at 729.0 s and 2.11 m/s under its 1.8 m canopy, the 1.125 kg booster at 107.5 s and 16.74 m/s tumbling; the masses add to the 1.675 kg stack to 1e-12 and each lands within 0.1% of its own v_e |
| The linear momenta of the bodies at a separation with a 0.6 rad/s body rate | add to the stack’s to 1e-9, and each body starts at its own center of mass to 1e-12 (0.817 m apart on this design) |
| A separation while the booster’s motor burns | refused in flight, with the booster’s burnout time in the error |
| A separation while still climbing at 100 m/s | both bodies find their own apogee above 1,400 m, fire there, and land within 1% of their own v_e |
| A timed separation, and a height separation | fire at their own time to 1e-9 s and at their own height to 1e-6 m, rather than at the next boundary that happens to exist (found in review: one fired 186 s late, another never) |
| A body that runs out of time | says TimeCap in its own BodyFlight; FlightResult::bodies_landed is false and landings() is short |
| A body whose device never fires (a timer set after it lands) | refused in flight, naming the body, rather than landed at the speed of a fall with no drag |
| A body whose canopy opened on the stack just before the separation (an apogee separation with an apogee parachute) | lands: the open canopy counts, though its deployment is in the flight’s events, not the body’s (found with M1.9a) |
| A timed separation known to precede the booster’s burnout | refused when the separation is given; a height one that a climbing rocket passes early is refused in flight |
The ejected pieces’ tests are in crates/hpr-sim/src/pieces.rs, also analytic:
| What | Result |
|---|---|
| The nose cone at apogee and a 250 g payload at 300 m, from the pad in a 4 m/s wind, in uniform sea-level air (not the example’s atmosphere, so its numbers differ) | all three land, each under its own canopy; the payload leaves at 300 m to 1e-6 m; landings pinned (the nose cone 911.6 m further downwind than the airframe, 4 m/s times its 227.5 s longer descent to 1%) |
| The masses at each parting, with wind and a 0.6 rad/s body rate | the two bodies at the first parting, and the three pieces, add to the rocket’s mass to 1e-12; the nose cone and the payload are their own components’ masses to 1e-12 |
| The linear momenta at each parting | at the first, from the rigid stack, they add to the stack’s to 1e-9, and the nose cone starts at its own center of mass to 1e-12; at the second, on the way down, they add up by construction (one shared velocity), so only the masses test anything there |
| Each piece’s landing in uniform air | within 0.1% of its own v_e, the airframe’s at its mass after the payload left |
| An ejection with a separation (the two-stage test design, both at apogee) | the booster is body 1 and the airframe body 2; the booster’s mass is its stage’s and motor’s, and the three add to the stack’s, to 1e-12 |
| A payload whose trigger comes after the landing | never leaves: its airframe lands with both pieces, and the two bodies add to the rocket’s mass |
| Two partings firing in one pass on the way down (the two-stage design, three joints), given in either order | each piece counted once: the four bodies add to the stack’s mass to 1e-12, and the nose cone and the interstage are their own components (found in review: the interstage was counted twice, 1.7651 kg landed from 1.6752 kg) |
| An airframe whose own mass is overridden, but not what it holds | still divides: the payload is its own mass, and the pieces add to the rocket’s, to 1e-12 |
| A powered separation with ejections, an ejection ahead of a separation that lights a motor, an ejection timed from a motor with no ignition | each refused with its own reason |
| Partings the design can’t make: an unknown id, a joint aft of an internal part or of the tail, a body component or an external part as a payload, a payload inside another, two at one joint, one payload twice, an overridden stage or covering override | each refused with its own reason, naming the component |
| An ejection during the burn, a height below the ground, a body without a device, a device on a body nothing makes | refused, the last when the flight starts |
| A 1 N·s push at both partings, from a stack tilted 60° up toward 30° east of north, in wind, moving sideways and turning at 0.6 rad/s | each body’s velocity changes by J/m to 1e-9 against the same flight unpushed. At the first parting that is along the hand-computed rail axis, 15.9 m/s on the nose cone. At the second, the airframe under its canopy, it is 4 m/s on the 250 g payload, against its velocity through the air. The momenta add up to 1e-9 at both, and every piece still lands within 0.1% of its v_e |
| The same push at 300 m with the airframe’s canopy not yet open | the payload is pushed down its velocity through the air, 4 m/s, and the airframe the other way, to 1e-9 |
| A nose cone pushed off at 300 m from a stack that has hung from a drogue since apogee | pushed against its velocity through the air to 1e-9, not along the attitude frozen at apogee (found in review: it went along the frozen axis) |
| A push at a body’s own apogee, climbing straight up in still air with nothing open | straight up, 4 m/s on the payload, to 1e-9, where along its flight would point down |
| A separation and a pushed nose cone at apogee (the two-stage design) | the booster, the separation’s body, gets no push; the nose cone and the sustainer’s airframe get ±J/m along the axis to 1e-9; the momenta add up to 1e-9 |
| Two pushed partings at one instant on the way down (the two-stage design), given in either order | by hand, the nose cone +J/m, the airframe between the joints 0, the interstage −J/m, each to 1e-9; the momenta add up to 1e-9, and the landings agree between the orders to 1e-6 m (found in review: taken one at a time, the order moved the nose cone’s push from 15.85 to 17.66 m/s) |
| The airframe’s canopy opening at the same instant as the payload leaves | not yet hung from: the payload is pushed down its flight, 4 m/s, whether the canopy opens at once or fills over a second (found in review: the push flipped with the inflation law) |
| A pushed payload let out at apogee while its section’s forward joint waits for 300 m | an error in flight, at the ejection’s time |
| A stack with only a tumble since apogee, the nose cone pushed off at 300 m | along its velocity through the air, to 1e-9: a tumble is nothing to hang from |
| A drogue since apogee released by a main that opens at 300 m as the nose cone leaves | still hung from: against the velocity through the air, to 1e-9, whether the main opens at once or fills over a second (found in review: the push flipped with the law) |
| One charge pushing off the nose cone and letting the payload out, both at apogee | by hand, along the axis: nose cone +1 N·s, payload +1 N·s, airframe between them −2 N·s, each over its own mass to 1e-9 |
| A pushed payload in the booster, behind a separation, with the builders in either order | accepted both ways, and every piece lands; without the separation it is in the nose’s piece and refused at the start, and so is one in the sustainer’s airframe with it |
| A drogue since apogee cut away at 600 m by a tumble, the nose cone pushed off at 300 m | hung from nothing by then: along the velocity through the air, to 1e-9 |
| A parting on the way down with no push | the body after it has the same point and velocity, and its mass without the piece; only partings record a body after |
| A nose cone tumbling on its own after apogee, in uniform sea-level air | its drag area is 0.56 times its tangent ogive’s closed-form side area, to 1e-12; it lands at 13.849 m/s, its model’s v_e to 1e-6 (the example’s 14.82 m/s is in its own, thinner air at 1,400 m) |
| The tumbling drag areas of an airframe cut into a nose cone and the rest | add to the whole airframe’s to 1e-12; a payload or a piece not made is refused |
| A push that is negative, NaN or infinite | refused when the ejections are given; one on a payload in the nose’s own section when the flight starts |
Against RocketPy
hpr’s descent is compared with RocketPy’s for five of RocketPy’s example rockets, a code-to-code comparison. Every compared number agrees within 3%, and most within 0.1% (22 of the 30 in the validation report). The largest gaps, in order:
- NDRT 2020’s north drift: +2.86%. hpr carries it 50.8 m south, 2.86% further than RocketPy does. NDRT’s main has a drag area of 16 m², and RocketPy’s added mass, which hpr leaves out, is the likely cause; no test has isolated it yet.
- Valetudo’s north drift: −1.77%. That drift is 19 µm, from the Earth’s rotation alone, so a tiny difference is a large fraction (its own section below).
- Valetudo’s whole drift: −0.89%, of 0.19 m, also from the Earth’s rotation alone.
- NDRT 2020’s descent time: +0.71% longer in hpr, likely the same added mass.
How the comparison is run:
validation/oracles/rocketpy/recovery.pyflies RocketPy’s own parachute phase for the five rockets and writes what it computes tovalidation/fixtures/recovery/rocketpy-descent.json. The testdescent_matches_rocketpy_examplesreplays each case in hpr.- Both codes start from the same declared state after burnout, near apogee. The first device opens at once: its lag is overridden to zero, so no ballistic stretch, flown under each code’s own rocket aerodynamics, comes between them.
- Both get the same
C_D S, the same deployment settings and the same wind. RocketPy’s random noise on each parachute is set to zero, and hpr flies RocketPy’s formula for gravity (see Gravity below). - The test checks that the two environments agree first, then compares the descents.
- The oracle runs at
rtol = atol = 1e-8: its relative and absolute tolerances, how much error each step may make, both 1e-8 (scientific notation for 0.00000001). Run again at 1e-6, it moves every compared metric by at most 3.5e-6 relative (the fixture’ssolver.relative_change_from_loose), far below the gaps. The one larger entry, 2.1e-3, is Valetudo’s north drift of 19 µm, which has its own section below.
The results. Measured (hpr against RocketPy, 2026-09-17):
| case | descent time | descent rate under the drogue | impact descent rate | drift | worst drift component |
|---|---|---|---|---|---|
| Calisto (drogue 1.0 m², main 10 m² at 800 m, wind 5 E / 2 N) | +0.08% (257.27 s) | −0.01% (17.967 m/s) | −0.03% (5.454 m/s) | +0.08% (1,386.0 m) | +0.08% |
| Valetudo (drogue 0.4537 m², no wind) | −0.02% (45.76 s) | n/a | +0.00% (17.627 m/s) | −0.89% (0.19 m, Coriolis only) | −1.77% (north, 19 µm; +2704% under hpr’s own gravity, issue #27) |
| NDRT 2020 (drogue 0.438 m², main 16.05 m² at 167.6 m, sheared wind) | +0.71% (61.60 s) | +0.01% (28.156 m/s) | +0.01% (4.604 m/s) | +0.28% (327.9 m) | +2.86% (north, −50.8 m) |
| Prometheus 2022 (drogue 0.467 m², main 5.78 m² at 457.2 m) | +0.08% (153.50 s) | −0.01% (26.400 m/s) | −0.03% (7.323 m/s) | +0.08% (1,237.1 m) | +0.09% |
| Juno III (drogue 0.885 m²) | −0.02% (53.56 s) | n/a | −0.01% (22.431 m/s) | −0.02% (457.9 m) | −0.02% |
- Every metric is inside the 3% that the parachute milestone (M1.7a) set. The descent rate under the drogue, where a case has a main, agrees to 0.01%.
- The later devices’ trigger heights agree to −0.01%, −0.17% and −0.01%. RocketPy’s trigger sampling (below) accounts for them.
- Both simulators land within 1% of Knacke’s
v_efor the device that is open, computed from hpr’s own air and gravity at the site. - These numbers use RocketPy’s gravity and wind interpolation, not hpr’s defaults.
- Gravity: the comparison has flown RocketPy’s gravity model since issue #27. Under hpr’s own gravity the drifting cases read a little closer (Calisto +0.06% rather than +0.08%, measured once when the comparison switched and not pinned by a test), because the vertical’s turn downrange pushes the rocket back toward the pad and cancels part of a real difference. The like-for-like number is the honest one.
- Wind: here both codes interpolate the wind by its east and north components, as RocketPy does. For a table of wind levels, hpr’s default is to interpolate speed and direction instead (Wind). Only NDRT 2020’s wind changes with height; the other four cases have one wind at every height, where the two ways agree. What hpr’s default would do to NDRT’s drift has not been measured.
What still differs between the two codes:
- Added mass. hpr has none. RocketPy’s carries no weight, so it changes no steady descent rate,
only the response just after an opening. It is most likely the largest difference.
- RocketPy’s added mass for NDRT’s main is 15.9 kg, against the rocket’s 20.8 kg, so its response to the opening is slower.
- That most likely lengthens the descent (+0.71%) and, in a wind that shears with height, moves the smaller drift component by 2.86%.
- A cited apparent-mass model would show whether it closes that gap.
- When a trigger fires. RocketPy checks its triggers on a grid of
1/sampling_rate(100 or 105 Hz), anchored att = 0, and only over the span after its first accepted step. hpr has no sampling rate: its event finder locates the crossing.- So RocketPy’s first deployment is 2.5 ms late in the four 105 Hz cases, and 13 ms late in Prometheus’s.
- Its test, height below the setting (
h < setting), can fire only at or below the setting, by at most one sample of fall, the descent speed over the sampling rate (v_z/rate): 0.17 m for Calisto and 0.27 m for NDRT (about 0.01 s of descent). - The heights the fixture records at those triggers (800.07 m, 167.93 m, 457.26 m) come from the spline RocketPy fits through its stored samples for reporting, not from the continuous solution between steps that its trigger read, so they sit just above the setting instead.
- The table compares hpr’s trigger heights with those reported values, the closest the fixture can come. The difference is the same size either way.
- Release against replacement. hpr sums its open devices, and releases the drogue when the main
is full; RocketPy holds one
C_D Sand replaces it. For these cases, whose canopies open instantly, the two are the same. - Wind: no difference. Both codes interpolate the declared wind by its east and north components, and the test holds hpr’s to RocketPy’s samples within 1e-9 m/s in each, NDRT’s sheared profile included.
- Atmosphere. hpr evaluates the 1976 standard atmosphere; RocketPy interpolates a 100-point pressure table over 0 to 80 km. Over the fixture’s 23 samples they differ by at most 3.7e-4 in density, which the test gates at 5e-4.
- Gravity: the same size, a different direction. RocketPy’s “Somigliana” formula is
WGS 84 normal gravity, and hpr’s agrees
with the fixture’s samples to 1e-8 (the worst of 23 is 4.7e-9 relative). The two point it
differently, and for a long time this page compared only the size.
- RocketPy applies gravity to the vertical axis alone (
Flight.u_dot_parachute,flight.py:2777, where onlyazcarries a gravity term). - hpr’s default,
GravityModel::Ellipsoidal, uses the full normal-gravity vector. Above the ellipsoid it tilts slightly toward the equator (Gravity), in proportion to height above the ellipsoid: 4.0e-6 m/s² sideways at Valetudo’s site at ground level, and 8.7e-6 m/s² at 1,468 m, where Valetudo’s descent starts. - Over the fixture’s 23 gravity samples the tilt runs from +6.9e-6 m/s² (north) at Valetudo’s top sample, 1,168 m, to −3.3e-5 m/s² (south) at Calisto’s 4,400 m.
- hpr’s vector also turns with the local vertical downrange, by
g·d/R, withdthe distance drifted andRthe Earth’s radius: 2.1e-3 m/s² at Calisto’s 1.4 km of drift. Wherever a rocket drifts at all, that is much the larger of the two. - The parachute milestone’s test (M1.7a) used to compare
gravity by its size alone, so it could see neither. This comparison and the validation suite
now both fly
GravityModel::VerticalTaylor, which hpr ships as RocketPy’s own formula for like-for-like comparisons, and both check the gravity vector, not its length.
- RocketPy applies gravity to the vertical axis alone (
- Geometry. hpr flies over the curved ellipsoid and measures heights along its perpendicular
(ellipsoidal height); RocketPy’s height
zis measured in a flat frame. Over Calisto’s 1.4 km of drift the curvature is 0.15 m of height, 0.03 s of descent.
Valetudo’s north drift
This tiny number is worth its own section, because it is the one that found the gravity difference above.
- What to expect. In still air, Valetudo’s north drift comes from the
Coriolis acceleration alone. The falling rocket picks up a
small eastward velocity
v_eastfrom it, and the same acceleration acting on that eastward motion pushes it slightly north (Valetudo’s site is in the southern hemisphere). The horizontal velocity relaxes to a drag balance in aboutv_t/g≈ 1.8 s, withv_tthe terminal speed, sov_north ≈ −2 ω_z v_east · v_t/g, withω_zthe vertical part of the Earth’s rotation. That integrates to 2.0e-5 m over the descent. RocketPy gives 1.9653e-5 m. - What hpr gave at first. The parachute milestone (M1.7a)
didn’t compare this component; the validation harness
(M2.1a) does. When it first did, hpr read 28 times
RocketPy’s: 5.51e-4 m (0.55 mm), under hpr’s default gravity. The tilt of that gravity,
integrated down the 800 m of descent,
(1/g)∫₀^800 g_north dz= 5.2e-4 m, accounts for the difference to within a few percent (issue #27). - What it gives now. Flown against RocketPy’s own gravity formula, as the validation suite
does, hpr gives 1.93e-5 m, −1.8% (validation report). Both codes carry the same
Coriolis term (hpr in
dynamics.rs; RocketPy inflight.py:2779-2783), and on this evidence neither is wrong: they were being asked different questions.