The design tree, configurations and checks
In short
- What it models: how parts become a rocket: where each part sits, automatic radii (taken from the neighbouring parts), overrides (measured values that replace computed masses, centers and inertias), the reference diameter and the motor in its mount. It gives the rocket’s mass, center of mass and inertia (its resistance to turning) through the burn, and flags designs that can’t exist, such as a motor wider than its mount. Most of it is convention, not physics.
- Sources: RocketPy 1.13.0’s
Rocketcode, and Meriam and Kraige’s Engineering Mechanics: Dynamics for the parallel-axis theorem. - How well it is validated: by analytic tests and
code-to-code comparison, the first and third of four
kinds of evidence. A hand-worked rocket agrees to 1e-12 through the burn. For eight
cases of RocketPy’s example rockets,
a given structure with its motor placed agrees in mass, center and inertia within 8.0e-10
(relative) at the times RocketPy computed, and within 1.3e-5 in mass and 2.6e-5 in inertia
between them; the propellant grains’ mass within 2.4e-9 and 4.9e-5 of its initial value.
Placement, automatic radii and overrides are checked by hand; the whole structure against
OpenRocket on 71 compared designs, within 1% in mass on 70 and in center of mass on 70
(mass properties); and body radii against OpenRocket in
the
.orkimport (.orkdesign files). A cluster’s tubes sit where OpenRocket puts them, to 1e-15 m, and a motor out turns the rocket as the hand calculation says, to 3.7e-7 (below). A pod’s mass, center and inertia match the hand-worked parallel-axis sum to 1e-15 (Pods), and OpenRocket’s on eighteen probe designs (.ork: Pods); on whole flights, six probe designs, five with pods of bodies, fins and motors, are within 0.81% of OpenRocket’s apogee, and their launch masses within 0.0001% (aerodynamics: Pods). OpenRocket’s cluster example flies within 5% of OpenRocket’s apogee and largest speed. Three of its apogees are compared with OpenRocket’s flight with no parachute, since its parachute opened before apogee (M1.9c, a two-stage and a cluster design against OpenRocket; results). Not compared with a real flight. - What it leaves out: each motor lights at its own
ignition, at launch unless told otherwise, so a two-stage design whose file says nothing flies with every motor lit at once, and nothing warns; a staged flight gives the sustainer its ignition (Staging). A cluster’s motors light together, or not at all: no spread in ignition and no thrust misalignment. Fins on a nose cone or a transition sit there only as a freeform outline with no tab or fillet, its root along the surface. Pods fly with each pod’s parts’ own normal force and drag, without the flow between the pods and the body (aerodynamics: Pods). OpenRocket has its own conventions for positions, radii and overrides; the OpenRocket comparison (M2.2) is mapping them, and the mass conventions it has found are on the mass page.
Code and sources
Code: hpr_design::tree (the tree, placement, automatic
radii, overrides, reference diameter), hpr_design::config
(motor mounts, configurations, assembly) and hpr_design::checks.
Decisions:
ADR-007 (stations, placement, automatic radii, overrides, motors and checks). Part
geometry and mass are in Mass properties and Shapes.
Sources:
- [RP] RocketPy v1.13.0 (MIT),
rocketpy/rocket/rocket.py: how a rocket’s mass, center of mass and inertia combine with a placed motor.docs/research/rocketpy-rocket-mass.mdhas the formulas with line numbers. - [MK] Meriam and Kraige, Engineering Mechanics: Dynamics, appendix B: the parallel-axis theorem (Mass properties).
- [AT] AeroTech, RMS-18/20, RMS-24/40, RMS-29/40-120 and HP RMS-29/120 Motor Dimensional Drawing (2003–2004), archived by the Internet Archive in 2005: case outside diameters and the ±0.005 in drawing tolerance (a nominal motor in its matching tube). The 29 mm drawing; the others are in the same folder’s archive.
- [ISO 2768] ISO 2768-1:1989, General tolerances, part 1: tolerances for linear and angular dimensions without individual tolerance indications, table 1, class c: the fit tolerance (wider than its parent).
Most of this file defines conventions rather than physical models. OpenRocket has its own
conventions for positions, automatic radii and overrides. hpr never reads OpenRocket’s source
code, whose license (GPL) is incompatible with hpr’s (the clean-room rule). So the planned
OpenRocket import (M3.1, reading .ork files) and comparison (M2.2) will
map them by running OpenRocket itself.
Stations and the body origin
- A station
sis a distance aft of the nose tip, the way design files give positions. - The body frame’s origin is the nose tip, on the axis:
z_ref = 0in Frames. Itszaxis points along the rocket toward the nose, so stationsis bodyz = −s, and the rocket lies atz ≤ 0. - A part’s own frame has its origin at its forward end (Mass properties). A part placed at station
sis translated by(0, 0, −s). Radial offsets and roll angles stay as the part states them, always measured from the body axis.
The tree
A Rocket has stages: sections of the stack, forward
to aft. A separation splits the rocket at a boundary between two
stages, so a rocket that stays in one piece needs only one.
Each Stage lists body components, and each
Component holds a
Part and its children.
| role | parts | where |
|---|---|---|
| body | nose cone, body tube, transition | a stage’s list; they stack |
| external | fin set, tube fin set, launch lug, rail button, pod set | children of a body tube; they take its outer radius |
| a pod’s body | nose cone, body tube, transition | children of a pod set; they stack along the pod (Pods) |
| internal | inner tube, centering ring, mass component, parachute, streamer, shock cord | children of a body component or an inner tube |
- Stacking. Body components start at
s = 0and follow one another through every stage, forward to aft. Each one’s extent is its length without shoulders (the sleeves of a nose or transition that slide into the next tube). - Axial extent of an attached part:
- a fin set’s root chord;
- a row of lugs or buttons from the first one’s forward end to the last one’s aft end,
(n − 1)·spacing + length(for a button, its diameter); - a packed part’s packed length;
- a pod set’s pod, its body components’ lengths added;
- otherwise the part’s length.
- Refused trees (
DesignError::Tree): no stages, an empty stage, a part in the wrong role, an attached part without a position, a body component with one, children under anything but a body component, inner tube or pod set, anything but body components in a pod set, and motor mounts on anything but a body tube or inner tube.DesignError::DuplicateIdcovers an empty or repeated id. - A fin set may sit on a nose cone or a transition when its root follows the surface
(ADR-166).
The root is a freeform outline’s
root_mpoints and its ends, each within a micron of the surface (FinSet::ROOT_ON_SURFACE_M): withr(x)the body’s radiusxalong it andx_LEthe root’s leading edge, a point[x, h]is on it whenh = r(x_LE + x) − r(x_LE). The straight pieces between the points may cut off or add at most 0.1% of the fin’s area (FinSet::ROOT_SLIVER_SHARE). The set must stay on the body’s length and carry no tab or fillet. Its body radius isr(x_LE). Anything else there isDesignError::Geometry, and on a body tube a fin’s root must be level. Lugs, buttons and tube fins still attach to a body tube only. - Counts. A fin set has 1 to 64 fins and a tube fin set 1 to 64 tubes, as a pod set has 1 to
64 pods, and a row of launch lugs or rail buttons 1 to 64 copies; a count outside that is
refused (
DesignError::Domain, “fin count (1 to 64)”, or “instance count (1 to 64)” for lugs and buttons). Each fin, tube, pod, lug or button is weighed one by one, so the bound stops a file’s mistyped count from asking for billions of them. It is far above any real rocket: the most fins in one set among the.orkdesigns hpr’s checks read is 8 (a one-off count over 78 designs, not a committed survey; the.orkreader leaves out a part counted more than 64 times). The aerodynamics take 1 to 8 fins in a set (aerodynamics: fin count), so 9 to 64 fins are weighed but don’t fly.
Positions
An attached part of extent L in a parent spanning stations [p, p + P], with offset a
(positive aft):
from | forward end at |
|---|---|
top | p + a |
middle | p + (P − L)/2 + a |
bottom | p + P − L + a |
after | the previous sibling’s aft end + a, or p + a for the first child |
absolute | station_m |
Automatic dimensions
An auto list names dimensions that the tree resolves. The part’s stored value for them is ignored.
- Body radii follow neighbours through every stage, stage boundaries included:
- a nose cone’s base, and a transition’s aft radius, take the next component’s forward radius;
- a body tube, and a transition’s forward radius, take the previous component’s aft radius.
- Order. Sources are followed until nothing changes. A body tube still unresolved then takes
the next component’s forward radius instead: only the first such tube, and then the sweep
repeats. So a fixed radius forward of a tube wins over one aft of it. A radius with no fixed
radius to reach is refused.
Rocket::unresolvable_body_radiilists exactly those radii. The.orkimporter gives them OpenRocket’s own default of 25 mm before laying the design out (when an automatic radius has nothing to take). The tree itself never invents a radius. - Shoulders take the inner radius (
R − t, outer radius less wall thickness) of the adjoining body tube: behind a nose; ahead of a transition for its forward shoulder, behind it for its aft one. - Centering rings: the outer radius is the parent’s inner radius. The inner radius is the outer radius of the widest on-axis inner tube among its siblings that overlaps it along the axis (by a positive length). With none, it is zero: a bulkhead.
- Packed parts (mass components and recovery parts) take the parent tube’s inner radius, less the distance from the parent’s axis to the part’s axis. An offset outside the bore is refused.
- Tube fin sets: the outer radius
ris the one at which the tubes close the ring around the body tube of radiusRthey sit on, each touching the body and its two neighbours. ForN ≥ 3tubes,r = R sin(π/N) / (1 − sin(π/N)); one or two tubes take the body’s radius. Six tubes on a 50 mm body are 50 mm; four are 120.7 mm. A wall thicker thanris cut to it. This is the radius OpenRocket 24.12 gives, measured on probes (.orkdesign files).
Overrides
Overrides replace computed mass properties with
measured ones: the mass m, the center of mass c and the inertia tensor I (the 3×3 table of
moments and products of inertia; see Mass properties). Primes mark the new values.
They apply in this order:
- Mass
m′:I′ = I m′/m, same center. The body keeps its shape. A body withm = 0becomes a point massm′atc. A packed part (a mass component, parachute, streamer or shock cord) withm = 0instead becomes a solid cylinder ofm′filling its packing, as OpenRocket’s does (Packed parts). - Center
a(cg_aft_m):c′_z = −(s_fore + a), withs_forethe station of the component’s own forward end (a stage’s for a stage, and never a shoulder’s), whether or not the children are covered.cg_xy_msetsc′_xandc′_y; without it they are kept. The tensor about the center is unchanged. - Inertia: the tensor about the center is replaced.
InertiaOverridegives its six entries with the sign convention of Mass properties (I_xy = −∫ x y dm); the off-diagonal ones default to zero.
The result must pass MassProperties::validate, which also refuses inertia on a body with no mass.
Errors inside a stage or component name it (DesignError::InComponent).
- Scope. A component’s overrides cover the component alone. With
overrides_include_children, they cover the component and everything attached to it. A stage’s overrides cover the whole stage, measured from its forward end. Motors are never covered. - Drag. A component or stage can also state a drag coefficient (
drag_override, with its owninclude_children). It plays no part in mass or inertia; the aerodynamics read it (A part’s stated drag coefficient). - Precedence. Deeper overrides apply first. A child’s override is inside its parent’s subtree total, and the stage override applies last.
- Scaling the tensor with the mass keeps the radii of gyration (
√(I/m): how far out, on average, the mass sits). That is the natural reading of “this part weighs more than its geometry says”. - OpenRocket differs in two ways, which hpr keeps as measured departures: it scales only the overriding part’s own inertia, and when a mass override covers the parts inside and gives no center, it puts the center at the overriding part’s own. Which override wins, and where a center override is measured from, the two agree on (Loft lesson L51, measured on probe designs in M2.2b1; see Mass properties).
Reference diameter
maximum(the default): twice the largest outer radius of any body component in any stage, including a bulged ogive’s peak (Profile::max_radius_m). Internal parts, shoulders, fins, tube fins, lugs and rail buttons never count. In Loft, the project before hpr-sim, an internal part could set it (Loft lesson L47).nose_base: the first nose cone’s base diameter.custom: a given diameter.
The reference area is π d²/4.
Motors and configurations
- Mounts. A
MotorMounton a body tube or inner tube holds a motor. Itsoverhang_mis how far the nozzle exit sits aft of the mount’s aft end. A mount that is a cluster of tubes holds the motor in every tube (below). - Configurations. A
Configurationputs at most oneMountedMotorin each mount. A mounted motor is aSolidMotorwith its case diameter and length (for the checks), an optional ejection delay (the time from burnout to its ejection charge, which doesn’t delay ignition) and itsignition: at launch unless told otherwise (Staging). - Placement. The motor’s axis runs forward from the nozzle exit (Solid motors).
With
s_aftthe station of the mount’s aft end, the nozzle exit is at stations_aft + overhang, on the mount’s axis: for an inner tube offsetrfrom the body axis at angleθ(fromx_Btowardy_B), at(r cos θ, r sin θ); otherwise on the body axis. A motor element atz_malong the motor’s own axis is at bodyz = −(s_aft + overhang) + z_m. - Composition at time
t:Assembly::mass_properties(t)combines the structure with each motor’sSolidMotor::state(t).total([MK]), every motor lit att = 0.Assembly::mass_properties_littakes each motor’s own ignition time: a motor burns on its own clock,t − t_ignition, and one not yet lit is loaded. The dry assembly uses each motor’s dry element. - More than one motor. In a flight, each burning motor’s thrust points along the rocket’s axis
(
z_B) and acts at its own nozzle exit, and the thrusts and their moments are summed (Rigid-body flight):- A cluster is flown, whether its motors share one mount (below) or each has its own, and a motor off the body axis adds a turning moment.
- A two-stage design fires in sequence when its motors are given their ignitions, and drops its booster at a separation (Staging). Its design file alone lights every motor at launch, booster and sustainer together, unless it says otherwise.
- Against RocketPy [RP]:
total_mass(t)andcenter_of_mass(t)are the same combination. RocketPy names the moment of inertia in pitch and yawI_11and the one in rollI_33. ItsI_11(t)is taken about the center of dry mass (the rocket without propellant), so hpr’s tensor is moved there before comparing.I_33sums the axial moments (every element is on the axis).
Clusters
A cluster is several like motors side by side. In hpr it is one inner tube repeated: the tube’s
cluster_m lists each
tube’s axis, [x, y] in meters in body axes, measured from the point the tube’s
radial_offset_m and angle_rad set (the body’s axis when both are 0). An empty list is one tube.
Motors of different kinds need one mount per kind. The decision record is ADR-075.
In a JSON design, a mount of three tubes 25 mm from the axis, with the first tube’s motor out, is these two fields (the rest of the inner tube and the mounted motor as usual):
"cluster_m": [[0.025, 0.0], [-0.0125, 0.021650635], [-0.0125, -0.021650635]]
"failed_tubes": [0]
- Mass. The tube weighs all its copies, each with its own
parallel-axis term
m d². Whatever the tube holds (an engine block, a mass) is repeated in every tube the same way. A mass override on the cluster sets the whole cluster’s mass; one on a part inside it (its mass, center or inertia) sets each copy’s, as OpenRocket does for the mass. - Motors. The configuration names one motor for the mount, and placing it gives one motor per tube, one after another in the order of the tubes, each nozzle on its tube’s axis. Their thrusts, masses and moments add up like any other motors’.
- A motor out. A mounted motor’s
failed_tubesnames tubes whose motor never lights, counted from 0 in the order ofcluster_m. That motor stays loaded and pushes nothing, which is how a cluster most often fails. The lit motors then push off-center, and the rocket turns toward the motor that is out. - Motor numbers. Every tube’s motor counts as a motor, so a cluster of three before another mount moves that mount’s motor from index 1 to 3. An ignition on the cluster’s burnout takes its first motor that lights. A recovery trigger or separation on one motor’s burnout or delay (recovery) waits on that motor alone: point it at a tube that lights, or with that motor out your parachute never opens.
- From a
.orkfile. The reader turns OpenRocket’s named pattern into the list (.orkdesign files).
Worked example. The tests’ single-stage rocket
(synthetic-54mm-three-fin),
with its mount made a ring of three tubes A = 0.02 m from the axis, at 0°, 120° and 240°
(measured from the body’s x axis toward its y axis, as in frames), and a Cesaroni
411I175-14A in each. It is an equation check, not a buildable rocket: three 38 mm motors don’t fit
a 54 mm body, and the design checks say so. It is held at rest in a vacuum, so no air and no motion
add anything to the motors’ push. With the motor at 0° out, 1 s into the burn:
| quantity | value |
|---|---|
each lit motor’s thrust T, in a vacuum | 193.98 N |
| the loaded motor, and each lit one at 1 s | 0.4375 kg, 0.3332 kg |
the rocket’s mass m | 1.584 kg |
center of mass across the axis, c_x (the loaded motor pulls it toward itself) | 1.33 mm |
pitch moment M = T (A + 2 c_x) about the center of mass | 4.396 N m |
pitch inertia I_yy about the center of mass | 0.1052 kg m² |
pitch acceleration M / I_yy, at rest | 41.80 rad/s² |
The center of mass moves toward the loaded motor: (0.4375 × 20 − 2 × 0.3332 × 10) mm / 1.584
is 1.32 mm, and the structure’s own center, 0.10 mm off the axis from its rail buttons, adds the
rest. The two lit motors sit at x = −A/2 each, so about the center of mass their thrust has the
lever A/2 + c_x twice. The flight’s equations give the same angular acceleration to 3.7e-7 (the
full inertia tensor, not only I_yy, turns the moment into a turn); the difference is the
mass-flow terms (the center of mass moving as two motors burn and one doesn’t, and the jets). With
all three lit, the thrusts balance, and the rocket turns 225 times slower (0.186 rad/s²), from its
center of mass sitting 0.033 mm off the axis. The numbers are pinned by the test
cluster_motor_out_produces_pitch_moment in hpr-sim.
Pods
A pod is a body beside the airframe: a side pod, or an outboard motor pod. In hpr a
PodSet is attached to a body tube like a fin set,
with a position along it. Its children are the pod’s own body components (a nose cone, body tubes,
a transition), which stack aft from that position along the pod’s axis, and take their automatic
radii from one another as a stage’s do. Parts go on and inside them as on the airframe: fins on a
pod’s tube, a mass or a motor mount inside it. A design with pods flies: each pod’s parts add their
own normal force and drag, once per pod (aerodynamics: Pods,
M1.13c1). The decision records are ADR-089 for
the layout and weight, and ADR-092 for the aerodynamics.
-
Where the pods sit.
countpods, 1 to 64, spaced evenly around the body’s axis atradial_offset_mfrom it, the first atangle_radfrom the body’sxaxis toward itsyaxis (frames): podkis atr (cos φ_k, sin φ_k),φ_k = angle + 2π k / count. -
Each pod is the first one turned. The pod written in the tree is one pod on the body’s axis. Pod
kis that pod turned byφ_kabout the body’s axis, then moved out, as a fin set’s fins are. So whatever it holds keeps its place relative to the airframe: a fin that points away from the airframe on one pod points away on every pod, and a symmetric pod set keeps its center of mass on the axis. -
Mass. Each copy is weighed where it sits, with its own parallel-axis term, and everything the pod holds is repeated with it. The pod set itself weighs nothing.
-
A pod of no length. A pod’s body tube may have length 0, and then it weighs nothing. OpenRocket draws winglets this way: a pod of one such tube, with no radius, which it calls a “phantom body” (
.ork: Pods). Parts on the tube sit at its radius, as on any tube. That radius is usually 0, so the parts sit on the pod’s own axis: fin roots there, and a lug’s axis its own radius out from it.- A worked example: two pods 50 mm from the body’s axis, each holding a launch lug 4 mm in
radius turned 180°, inward. Each lug’s axis is 46 mm from the body’s. The test
a_pod_of_no_length_holds_its_parts_at_the_pod_s_axisworks the pair’s inertia out by hand. - Three or more fins whose roots meet on the axis overlap there. Each fin is weighed as a whole plate, so the overlap counts more than once, as it does in OpenRocket. The overlap is about a fin’s thickness across, so it grows with thickness over span. For three fins 3 mm thick and 20 mm tall it is a few per cent of their mass: an estimate, not a measurement.
- A body tube of no length is allowed only in a pod; a stage refuses one. A pod refuses a nose cone or a transition of no length. A pod mixing a tube of no length with other parts lays out, but no probe has checked it against OpenRocket.
- A worked example: two pods 50 mm from the body’s axis, each holding a launch lug 4 mm in
radius turned 180°, inward. Each lug’s axis is 46 mm from the body’s. The test
-
An empty pod set holds nothing, lays out, and weighs nothing.
-
Overrides. A mass override on the pod set is the total for all its pods, and must be set with
overrides_include_children(an override on the pod set alone is refused, since it has nothing of its own). An override on a part inside a pod is that part’s in each pod. A center overridecg_xy_minside a pod is measured in the pod as written, on the body’s axis, and turns with each pod; so does an inertia override’s tensor. The testwhat_a_pod_holds_turns_with_it_and_overrides_keep_their_scopepins all three. -
Motors. A motor mount inside a pod gives one motor per pod, each nozzle on its pod’s axis. The configuration names the mount by its id, the pod tube’s for a pod that is its own motor tube, and a mounted motor’s
failed_tubescounts the pods, in order from the first. -
Checks. A pod may run past the end of the tube it hangs from, or past the rocket’s end, without a warning: a pod is held by a pylon, and an outboard booster often extends past the tube it hangs from. A pod set that doesn’t touch its tube at all is still an error. A pod set in a pod and one of more than 64 pods are refused.
-
Flying. A pod’s parts add their normal force and drag once per pod, on the rocket’s axis at their stations (aerodynamics: Pods). Canted fins on a pod and a pod’s tube of no length with a radius are refused. The tumble model counts each pod’s tubes and fins as the airframe’s (recovery). An empty pod set adds no force and no drag area. Motor mounts may sit in up to four pod sets, and each set’s burning motors come off its own pods’ bases (aerodynamics: Pods); a rocket with motor mounts in more is refused. Until M4.5i, motor mounts in a second pod set were refused.
-
What it can’t do yet.
- Move or release a part inside several pods: a moving or released part must be one part, so it can be inside a pod only when there is one pod.
- Part at a pod: a pod’s body component is neither a joint nor an ejected payload; it stays with the tube its pod set hangs from.
- Check a pod’s geometry: nothing warns when pods overlap the airframe or each other, or when a pod’s radius steps (#206).
The refusals are pinned by tests in
hpr-aero(unsupported_inputs_are_refused) andhpr-sim(a_pod_s_parts_are_located_as_the_airframe_s_are,partings_the_design_cant_make_are_refused).
In a JSON design, this component goes in a body tube’s children list. It holds two pods 50 mm
from the axis (angle_rad is optional and 0 by default), each a 0.3 m tube, 0.1 m aft of the top
of the body tube:
{
"id": "pods",
"part": { "pod_set": { "count": 2, "radial_offset_m": 0.05, "angle_rad": 0.0 } },
"position": { "from": "top", "aft_offset_m": 0.1 },
"children": [
{
"id": "pod-tube",
"part": {
"body_tube": {
"length_m": 0.3,
"outer_radius_m": 0.012,
"thickness_m": 0.001,
"material": { "name": "cardboard", "density": { "kind": "bulk", "kg_m3": 790.0 } }
}
}
}
]
}
Worked example. The same two pods, at 0° and 180°, each a cardboard tube (790 kg/m³, radius 12 mm, wall 1 mm, 0.3 m long) with a 50 g mass added inside it (a solid cylinder 0.1 m long, radius 8 mm, its top 0.02 m below the pod’s). They hang on the tests’ 54 mm airframe (radius 27 mm), so each stands 11 mm clear of it, from a body tube whose top is 0.2 m aft of the nose tip. The pod starts 0.3 m aft of the nose tip (at station 0.3 m). Each pod, about its own axis and center:
| quantity | value |
|---|---|
the tube’s mass ρ π (r_o² − r_i²) L | 17.125 g |
one pod’s mass m (tube and mass) | 67.125 g |
| its center, from the tube’s at station 0.45 m and the mass’s at 0.37 m | station 0.39041 m |
its roll inertia, the two cylinders’ m (r_o² + r_i²)/2 and m r²/2 added | 3.869e-6 kg m² |
its pitch inertia, each cylinder’s m (3 (r_o² + r_i²) + L²)/12 and its own m Δz² | 2.53675e-4 kg m² |
Each pod’s axis is d = 0.05 m off the body’s. A pod’s parallel-axis term about a line through
the body’s axis is m times the square of the pod’s distance from that line. Both pods lie on the
x axis, so they add nothing about it, and m d² = 1.6781e-4 kg m² each about the y and z
axes:
| quantity (the two pods about their joint center, on the axis) | value |
|---|---|
| mass | 134.25 g |
I_xx: no m d² | 2 × 2.53675e-4 ≈ 5.0735e-4 kg m² |
I_yy | 2 × (2.53675e-4 + 1.6781e-4) ≈ 8.4298e-4 kg m² |
I_zz (roll) | 2 × (3.869e-6 + 1.6781e-4) ≈ 3.4336e-4 kg m² |
The pods’ roll inertia is 44 times what it would be with both on the axis: nearly all of it is the
parallel-axis term. The test a_pod_is_its_stack_repeated_with_its_parallel_axis_term in
hpr-design pins these to 1e-15. It also pins a single pod at 90° (y = d). About the nose tip,
where its center is at z = −0.39041 m, the pod’s
product of inertia is I_yz = −m y z = 0.067125 × 0.05 ×
0.39041 = 1.3103e-3 kg m².
Parallel stages
A parallel stage is a stage strapped beside the airframe instead of stacked behind it: a set of
boosters around a sustainer, burning beside it and dropping at a separation of their own. In hpr
it is a Stage whose parallel field is set (a
ParallelStage): the id of the body tube it
hangs on, in an axial stage before it, where along that tube, and its copies as a
pod set’s count, distance from the axis and angle. Its own components are each copy’s
body, a stack of nose cones, body tubes and transitions with parts on and inside them, as a pod’s
are. The decision record is ADR-171, and M4.5m
shipped it.
- Laid out as pods. The layout hangs the stage on its tube as a pod set, so everything in Pods holds: where the copies sit, each copy turned with its place, each weighed with its own parallel-axis term. A parallel stage doesn’t stack: the rocket’s length is the axial stages’.
- Its mass is its own stage’s. Its parts carry its stage’s index, its mass and ends count to
that stage and not to the tube’s, and
PlacedStage::hung_onnames the stage it hangs on. The rocket’s mass, center and inertia are the same as with the same pods hung on the tube. - Dropping it. A separation after stage k drops every stage after k, so the stages it
drops must hang together: each one’s carrier, its
hung_onstage or the axial stage before it, is among them, or it is the first dropped. A parallel stage hung on a stage the nose keeps can’t go with an axial booster behind it; drop the booster first, then the parallel stage (Staging: Boosters beside the core). - Refused. A parallel stage placed
aftera sibling (it sits along its tube), one that hangs on no body tube of an earlier axial stage, and, as for any pod set, one inside a pod. So is a mass override that covers what its tube holds, or the whole stage the tube is in, and a drag override on a parallel stage or covering the stage it hangs on: none says whether it covers the parallel stage, and no probe has measured how OpenRocket reads one. An override on the parallel stage itself is its total, every copy’s, as a pod set’s is; that reading is hpr’s, not measured against OpenRocket.
Worked example. The two pods of Pods’ worked example, made a parallel stage instead
(134.25 g, at 0° and 180°, 50 mm from the axis): the stage weighs 134.25 g, the airframe tube’s
mass with its children is 134.25 g less than with the pods hung on it, and the rocket’s mass,
center and inertia are the pod set’s to 1e-15. The test
a_parallel_stage_lays_out_as_a_pod_set_of_its_own_stage pins this for one, two and three
copies, and a_parallel_stage_hangs_on_a_body_tube_of_an_earlier_axial_stage pins the refusals.
In a JSON design, the stage goes in the rocket’s stages list after the stage it hangs on, and
is flown as any stage is: a motor in a mount inside it, and a separation after the stage it hangs
on (Staging: Using it today has a whole two-stage design to start
from). This one is the worked example’s, empty but for each copy’s tube, hung 0.1 m aft of the top of the
body tube airframe:
{
"id": "pods",
"components": [
{
"id": "pod-tube",
"part": {
"body_tube": {
"length_m": 0.3,
"outer_radius_m": 0.012,
"thickness_m": 0.001,
"material": { "name": "cardboard", "density": { "kind": "bulk", "kg_m3": 790.0 } }
}
}
}
],
"parallel": {
"on": "airframe",
"position": { "from": "top", "aft_offset_m": 0.1 },
"pods": { "count": 2, "radial_offset_m": 0.05, "angle_rad": 0.0 }
}
}
Checks
checks::check resolves a design and returns typed
Findings, each an error (impossible as described,
so a simulation would be wrong) or a warning (unusual, but it can be built and flown). Lengths compare with 1 nm of
slack (LENGTH_TOLERANCE_M), so round-off never raises one.
| finding | severity | when |
|---|---|---|
motor_wider_than_mount | error | case diameter > mount inner diameter, past the slack a nominal size has (Loft lesson L50) |
motor_outside_mount | error | the case doesn’t overlap its mount along the axis at all (an overhang typed in mm as m) |
attachment_off_body | error | an external part’s extent (a fin root) doesn’t overlap its body tube at all (Loft lesson L50) |
part_outside_rocket | error | an internal part lies wholly forward of the nose tip or aft of the rocket’s end, and touches none of the parts it hangs from |
internal_part_wider_than_parent | error | an internal part reaches farther from its parent’s axis than the parent’s bore (in a nose cone or transition, the most room along the part) by more than the fit tolerance for the bore’s size, and is no cap at its parent’s end, nor a packed part whose center is in the bore. A ring around its tube is measured in the part around it |
center_outside_rocket | error | a stage with an axial center-of-mass override (cg_aft_m, its own or a component’s) has its center off the rocket although its parts aren’t |
motor_tight_in_mount | warning | the motor’s nominal diameter is wider than the mount’s bore, but its real case fits: a 29 mm motor in a 1.140 in tube |
motor_past_mount_top | warning | the case’s forward end is forward of the mount’s |
attachment_past_body_end | warning | an external part runs past an end of its body tube |
internal_part_past_parent_end | warning | an internal part runs past an end of its parent |
internal_part_tight_in_parent | warning | an internal part other than a packed part reaches past its parent’s bore by no more than the fit tolerance: a fit to sand |
internal_part_wedged_in_parent | warning | an internal part other than a packed part fits a nose cone or transition where it is widest along the part, but runs into its wall where it narrows, past the fit tolerance |
packed_part_wider_than_parent | warning | a packed part (a mass component, parachute, streamer or shock cord) reaches past its parent’s bore, by any amount, with its center inside the bore |
ring_against_parent_end | warning | a centering ring or bulkhead too wide for its parent’s bore sits at the parent’s end face and fits what holds the parent: a cap glued against the end |
cluster_tubes_overlap | warning | two tubes of a cluster are closer than a tube’s diameter, so they cross and that mass counts twice |
ring_overlaps_inner_tube | warning | a centering ring crosses an inner tube beside it (a cluster’s off-axis tubes), counting that mass twice |
radius_step | warning | adjacent body components’ radii differ where they meet |
no_nose_cone | warning | the first body component isn’t a nose cone |
- Radial reach is measured about the parent’s own axis: the distance between the part’s axis and the parent’s, plus the part’s radius. A part inside a clustered tube is measured in that tube, and copied to each of the cluster’s places. A block centered in an off-axis pod fits; a part on the body axis inside that pod doesn’t. Centering rings have no offset, so they sit on the body axis.
- Parts wholly outside the rocket report only
part_outside_rocket, and a stage holding one is sparedcenter_outside_rocket. A single check covers each fault. - A retainer on a motor mount that sticks out past the airframe touches its mount, so it is on the
rocket, even flush against the mount’s end. Only the mount gets a warning. Touching an end face
counts as touching, within
LENGTH_TOLERANCE_M, for every part. - Fins may sweep past the rocket’s end, and a heavy part may run past its tube’s, so a stage’s
center can leave the rocket without an override, and neither a mass override nor a sideways
center override (
cg_xy_m) can move a center past its parts. It is an error only when an axial center override (cg_aft_m) is involved; no component-level center is checked.
Wider than its parent
A part drawn a little wider than the bore it sits in is a fit the builder sands. hpr flies it and
warns, internal_part_tight_in_parent, and the mass where the part crosses its parent’s wall
counts twice. How much wider is the fit tolerance
(fit_tolerance_m): the general tolerance
ISO 2768-1 sets for a dimension of the bore’s diameter in its coarse class, ±0.5 mm for 6 to
30 mm, ±0.8 mm for 30 to 120 mm and ±1.2 mm for 120 to 400 mm. A bore made at its upper limit
and a part made at its lower one close a radial overlap of that much. The standard is for
machined parts; no standard covers hobby airframes, so hpr borrows its coarse class
(ADR-155, the decision on which fits warn). Past the tolerance, the part can’t be where
it is drawn: an error, internal_part_wider_than_parent, unless it is a
packed part. Fix it by typing the part’s diameter to
fit the bore (an outside diameter typed for an inside one is the usual slip), or by choosing a
part that fits.
- In OpenRocket’s 3D printable nose cone and fins, a coupler 24.10 mm across sits 11.4 mm deep in a printed fin can with a 23.19 mm bore: its wall reaches 0.46 mm past the bore, within the 0.5 mm of a 23 mm bore, so it warns. The ring of PETG where they cross, 0.46 mm thick and 11.4 mm long, is 0.39 cm³, 0.48 g at the file’s 1250 kg/m³, counted twice.
- A cap is a centering ring or bulkhead on its parent’s axis that covers one of its parent’s
end faces, inside or outside the end, but not both. It can be glued against the end, so it needs room in what holds its parent instead:
that holder’s bore, with the same tolerance. It warns,
ring_against_parent_end. OpenRocket’s Two stage high power rocket draws its bulkheads this way: sized to the airframe’s 49.53 mm bore, on the ends of couplers whose bore is 48.44 mm. The same bulkhead away from the coupler’s ends would be an error, and so would a tube, which can’t be a cap, or a ring at the end of a clustered or off-axis tube, which sits beside the tube, not around it. - A ring around its tube is a centering ring written as a child of an inner tube, on the tube’s axis, whose bore is at least the tube’s outside diameter. It wraps the tube rather than sitting in its bore, as OpenRocket’s two pods examples draw the rings on their motor mounts. So it needs room in the part around the tube at the ring’s own station: out from the tube, the first part with room for parts whose length overlaps the ring’s. That part must hold the whole ring; the same tolerance applies, the findings name that part, and the ring is checked against the other tubes in it for overlap. Pods–airframes and winglets draws rings 32.54 mm across with an 18.75 mm bore on an 18.69 mm motor mount, inside an airframe with a 32.59 mm bore: they fit. Read as inside the mount, whose bore is 18.03 mm, they would be an error.
- A ring that isn’t around its tube is measured as a part in the tube’s bore, which for a ring this wide is an error: one whose bore is even 1 µm smaller than the tube’s outside, one on a tube that is off the axis or a cluster, and one with no part around it at its station that holds it whole, such as a ring on a motor tube where it sticks out past the airframe’s end (M4.5k, a ring around its tube).
In a nose cone or transition
A nose cone or transition narrows along its length, so the room a part has depends on where it sits. This is a design convention, checked by unit tests and by counting findings over real designs, not by comparison with a built rocket or with OpenRocket. hpr measures the room from the profile, the part’s outline (its radius against distance from the forward end): the outer radius less the wall (none when the part is filled), over the part’s own length. It takes two numbers from it, the least room along the part and the most (#313, a part measured against the cone’s widest radius). Before that fix, hpr used the largest outer radius anywhere on the part, so a bulkhead 20 mm in radius at the tip of a cone 27 mm in radius at its base passed, though the cone has no room for it there. Drawn there, a part sits forward of where it can, which moves the center of gravity forward and raises the stability margin.
- Too wide where it is widest: past the fit tolerance of the most
room along it, a part is
internal_part_wider_than_parent, an error, as in a tube. - Wedged: a part that fits where the profile is widest along it, but runs into the wall where
it narrows past the tolerance of the least room, is
internal_part_wedged_in_parent, a warning. It can’t slide that far in as drawn, and hpr flies it where it is drawn. The usual case is a coupler (a short inner tube that joins two sections) drawn reaching into a nose cone from its base. - Packed parts are measured against the least room, with the packed-part rule: a warning while the part’s center is in that room, an error once it is out. A mass on the axis at a cone’s tip warns, since the room closes to nothing there and the mass’s center is on the axis; off the axis it is an error.
- How the wall is measured. The wall is the thickness the design states, normal to the
surface, so the outer radius less the wall overstates the room on a slope
θbyt (1/cos θ − 1): 1% of the wall at 8°, 0.1 mm of a 3 mm wall at 15°. An automatic radius in a profile is already its room at the part’s narrower end (ADR-096, automatic radii in a profile), so it fits. - Why the ends decide. Every profile hpr draws is concave: its radius never dips between two stations, so the least room along a part is at one of its ends. The check samples 31 stations between the ends as well.
- Past the ends: a part running past the profile’s end is measured over the length inside it.
One wholly past an end, or touching it only, is measured against the largest outer radius, as
before;
internal_part_past_parent_endalready names it.
A worked example. A conical nose 200 mm long and 27 mm in radius at its base, with a 2 mm
wall, has 0.135 x − 0.002 m of room at x m from its tip. A tube 24.5 mm in radius over its
aft 100 mm has 25.0 mm of room at the base and 11.5 mm at its forward end, 13 mm too little:
wedged, a warning: far past the 0.5 mm tolerance of the 23 mm bore at its forward end. At
26.5 mm the same tube is 1.5 mm too wide even at the base, past the 0.8 mm tolerance of a 50 mm
bore (25.0 mm of room is 50 mm across): an error.
On real designs. cargo xtask design-checks, a developer command in this repository, counts
the findings over the reference library (ADR-185, the release 0.1 fixes, has the table
of counts). On its default set, the repository’s gitignored folder of reference designs and
OpenRocket’s examples (72 designs that open),
this rule adds no error: 3 more tight-fit warnings, 1 wedged and 8 more packed-part warnings. On
the private flight collection (560 files that open, many of them versions of one design), it adds
16 errors in 11 files newly refused, all tubes too wide even at the profile’s widest, and 14
tight-fit, 33 wedged and 240 packed-part warnings.
A packed part wider than its bore
A packed part drawn wider than its bore warns and flies as drawn. Its width changes only its own inertia; on the one OpenRocket example that has one, that moves the apogee by 12 µm at most.
A mass component, parachute, streamer or shock cord is a packed part
(Packed parts): hpr takes its mass m as stated, and its packed length
L and station say where that mass sits. Its packed radius r
enters the flight in one place, the part’s own moment of inertia, that of a solid cylinder:
m r²/2 about its axis and m (3r² + L²)/12 across it, about its center.
- The warning. A packed part that reaches past its parent’s bore is
packed_part_wider_than_parent, by any amount, while its center is inside the bore: its radial offset from the parent’s axis is no more than the bore’s radius. It can’t go in as drawn, and hpr flies it as drawn, as OpenRocket does. Packed into the room it has, radiusr_room, at the same mass, length and station, it would change the rocket’s moments of inertia bym (r² − r_room²)/2about its axis andm (r² − r_room²)/4across it, and nothing else. - No limit. The warning has no upper limit. A part typed far too wide, such as ballast 400 mm across in a 54 mm nose cone, adds inertia that packing it would not, and that changes how the rocket turns. Read the warning, and fix the part’s diameter if it is a slip.
- The error. A packed part whose center is past the bore is an error,
internal_part_wider_than_parent, however little it reaches past: its mass would sit where no part can be, moving the rocket’s center of gravity sideways. - No fit tolerance. A packed part’s mass doesn’t count twice where it crosses a wall, so it gets no fit tolerance (ADR-170, the decision on packed parts).
A worked example. OpenRocket’s Deployable payload packs its payload, 14.2 g, and its
parachute 25 mm across into a 21 mm bore: each reaches 12.5 mm from the axis, with 10.5 mm of
room. Packing the payload into its room would take 0.0142 × (0.0125² − 0.0105²) / 4 =
1.6e-7 kg m² off the rocket’s inertia across its axis; the parachute adds a term of its own.
Flown both ways, with both parts narrowed in a copy of the file, the five configurations’ apogees
differ by 12 µm at most, 7.5e-8 of the apogee, and their landing times agree to 1 ms
(ADR-170’s table).
A nominal motor in its matching tube
A motor’s diameter in hpr is its nominal size, as ThrustCurve.org and RASP files give it: 29 mm
for every 29 mm motor. The cases are not all that wide. AeroTech’s RMS dimensional drawings give
the case’s outside as 0.698 in for 18 mm, 0.938 in for 24 mm and 1.125 in for 29 mm, each to
±0.005 in, so a “29 mm” case is at most 1.130 in, 28.702 mm, across. A size whose case is
narrower than its name gets that difference as slack
(motor_fit_slack_m): 0.144 mm at 18 mm,
0.048 mm at 24 mm, 0.298 mm at 29 mm. Within it, the check warns (motor_tight_in_mount); past
it, the real case can’t go in and it is an error.
- LOC Precision’s 1.140 in motor tube has a 28.956 mm bore, 0.044 mm under 29 mm. The largest case leaves it 0.254 mm of clearance, so a 29 mm motor in it warns (OpenRocket’s Chute release; issue #280, the report that such a motor was refused).
- The Loft demo’s 28.0 mm bore is 1 mm under 29 mm, 0.70 mm under the largest case: an error.
- The same drawings give 38, 54, 75 and 98 mm cases as 1.500, 2.125, 2.965 and 3.870 in, so at the +0.005 in limit each is at least as wide as its name. Those sizes get no slack, so a 38 mm motor in a 37.9 mm bore is an error, as its 38.10 mm case would be. Fix it with a wider mount or a smaller motor. hpr doesn’t yet warn when a nominal size fits the bore but its wider case wouldn’t, such as 38 mm in a 38.1 mm bore (#312).
- These are one maker’s cases, drawn in 2003 and 2004 and archived from its site in 2005 ([AT], under Code and sources). No published standard gives a diameter tolerance: the NAR’s motor testing manual lists the sizes only. A Cesaroni or other maker’s case may differ, and a diameter that isn’t a nominal size gets no slack.
Errors mark designs that can’t exist as described. A simulation of one would be wrong, usually
on the flattering side: Loft flew a 54 mm motor in a 38 mm mount 69% high. The flight engine
refuses them with SimError::DesignChecks
unless the caller sets
FlightSettings::accept_design_errors.
Verification
- Placement and resolution (
tree::tests): each position rule at stations worked by hand; body components stacking through two stages; automatic radii across a stage boundary, the fallback, precedence and the unresolvable case; ring, shoulder and packed radii; every refused tree; a JSON round trip that refuses unknown fields. - Composition by hand:
tree_structure_matches_parts_placed_by_hand: the sample rocket’s structure equals its eight parts placed at hand-worked stations and combined, to 1e-13.config::tests: the placed motor’s nozzle station, and the rocket’s mass, center and inertia at loaded, burning and burnt out, by the parallel-axis theorem, to 1e-12; an off-axis mount’sI_yz = −m y z.
- Clusters by hand:
parts::tests::a_cluster_is_its_tubes_each_with_its_parallel_axis_term: four tubes’ mass, center and roll inertia about the body’s axis, to 1e-15.tree::tests::a_cluster_repeats_what_it_holds_in_every_tube: an engine block in every tube of a 3-ring, the structure gaining two tubes and two blocks, and the checks’ warnings.config::tests: a motor in a 3-ring is three motors at their tubes, the rocket’s roll inertia gaining each one’sm d²; a failed tube stays loaded and unlit, a tube the mount lacks is refused; a sustainer lit by a clustered booster’s burnout lights with one booster motor out.hpr_sim::staging::tests: three motors’ thrust and mass summed, and the motor out above (cluster_motor_out_produces_pitch_moment, Loft lesson L31); a clustered sustainer lit after a powered separation.
- Pods by hand (
tree::tests): two pods’ mass, center and inertia, and one pod off the axis with its product of inertia, against the textbook cylinders’ sum above, to 1e-15 (a_pod_is_its_stack_repeated_with_its_parallel_axis_term); a mass off a pod’s axis turning with its pod, a center override in the pod as written, and the two override scopes (what_a_pod_holds_turns_with_it_and_overrides_keep_their_scope); a pod’s nose taking its tube’s radius, its parts repeated in each of three pods, a motor per pod, no warning for pods past the rocket’s end, this page’s JSON, and each refused tree (pods_stack_hold_motors_and_refuse_the_wrong_trees); a pod’s nose never the reference nose (a_pod_s_nose_is_not_the_reference_nose). The aerodynamics, the tumble model, a mass shift and an ejection each refuse a pod, or a pod’s body component, by name (above). - Overrides (
overrides_rescale_move_and_replace,nested_overrides_apply_deepest_first): each step, the scopes, a stage override, deeper overrides first, the massless case, and refusal of non-finite and unphysical results. - Property (a proptest, which checks a rule on many random inputs): randomly placed masses sum to the structure’s mass and center, and sliding every part moves the center rigidly without changing the tensor.
- Checks (
checks::tests): each finding and its severity. A cluster pod’s block fits and an on-axis part in the pod doesn’t. Motors miss their mounts in both directions. Parts and a stage center lie off the rocket. A layout with a corrupt parent index is skipped, not a panic. A bulkhead 0.8 mm wider than a 49 mm bore warns and 0.1 µm more errs; at the coupler’s end it is a cap up to the airframe’s bore plus its tolerance, and an error inside the coupler or as a tube (a_part_wider_than_its_parent_warns_only_as_a_fit_or_a_cap); each band edge of the coarse tolerance is pinned (the_fit_tolerance_is_the_coarse_general_tolerance_of_the_bore). A 29 mm motor warns in a 28.956 mm bore and errs 1 µm past its slack, as do 24, 38 and 54 mm motors and a size that isn’t nominal (a_nominal_motor_in_its_matching_tube_only_warns). Mutation probes make these tests fail: the slack given to every diameter, the tolerance doubled, a cap allowed anywhere along its parent, off its parent’s axis, or over both ends. - Against RocketPy 1.13.0 (
config::tests::matches_rocketpy_example_rockets):-
Eight cases of the fixture
validation/fixtures/design/rocketpy-rocket-mass.json: seven example rockets (Calisto at two motor positions) and Prometheus’sGenericMotor(RocketPy’s motor described by its masses alone, with no grain geometry). Cavour (added for its drag curve in M1.5b, the drag milestone) has no motor dry mass; its design gives the motor 1e-15 kg, since hpr needs a positive one.docs/research/rocketpy-rocket-mass.mdgives the curve substitution and the examples left out. -
The test derives the stage override, nozzle station and motor inputs from the fixture itself, independently of the design generator.
-
The rockets are compared at 103 even times through the burn and after it, and at up to 60 of RocketPy’s LSODA knots: the times at which its ODE solver, LSODA, computed the grain geometry. Errors are relative: to the value itself, or to what a row names in brackets (the rocket’s length for a center).
-
Worst measured, with the test’s tolerance:
quantity worst tolerance dry mass, center, I_11,I_33; initial and column propellant mass2.4e-16 1e-12 products of inertia (of I_11)0 1e-15 at LSODA knots: total mass, center (of length), I_11,I_338.0e-10 1e-8 at LSODA knots: grain propellant mass (of initial) 2.4e-9 1e-8 even grid: total mass 1.3e-5 (Lince) 5e-5 even grid: center of mass (of the rocket’s length) 3.6e-6 (Cavour) 2e-5 even grid: I_11about the dry center and about the center of mass2.6e-5 1e-4 even grid: I_331.4e-5 1e-4 even grid: grain propellant mass (of initial) 4.9e-5 2.5e-4 -
At RocketPy’s knots, agreement is the ODE solver’s own accuracy (rtol 1e-11).
-
Between knots, the residual is RocketPy’s resampling: it interpolates grain volumes linearly between LSODA knots and samples
GenericMotorinertias at thrust knots. hpr’s values are exact for a piecewise-linear curve. -
The comparison sets mass, center and inertia together. So the override steps (rescaling the tensor with mass, moving the center) are checked by hand-worked tests, not against RocketPy.
-
- Public designs (
validation/designs/, written bycargo xtask designs, which a test keeps in sync): the eight RocketPy cases and two synthetic rockets resolve with no findings and assemble into valid bodies at ignition, mid-burn and burnout. - Lessons: Loft lesson L47
tests::reference_diameter_ignores_internal_components; Loft lesson L50checks::tests::motor_wider_than_mount_is_rejectedandchecks::tests::fin_root_must_touch_body.