Methods
Loft is implemented clean-room from published methods (OpenRocket is GPL; none of its code is used). Everything runs in SI internally. Where a method is an approximation, it is flagged here and in the limitations log. The source is in the open repository, file paths noted below.
Importing designs
Loft reads three design formats: OpenRocket .ork (a ZIP, gzip, or raw XML with an <openrocket> root; lib/ork/), RockSim .rkt (XML with a <RockSimDocument> root; lib/rkt/), and RASAero II .CDX1 (XML with a <RASAeroDocument> root; lib/rasaero/). The importer sniffs the root element and picks the adapter. Each adapter is a thin translator into one internal rocket model — the simulator only ever sees that model, never a file format — so the same physics flies any of them and a future importer (RocketPy) is another adapter, not a second engine. The RockSim adapter is implemented clean-room from RockSim's published file specification (its RockSim_Xml_Doc.txt, the RockSim engine-file format, and the documented shape/finish codes); RockSim stores lengths in millimetres, masses in grams, and diameters as diameters, all converted to SI on import. Unknown parts are skipped with a warning rather than failing the whole file.
The RASAero II adapter is likewise clean-room, from the published RASAero II user manual and from inspecting real .CDX1 exports; RASAero works in US customary throughout — inches, pounds, feet, ft/s, °F, inHg, mph — all converted to SI on import. Only the design file is read; RASAero's own aerodynamics are not reimplemented, and the numbers it stored sit beside Loft's as an independent cross-check, exactly as OpenRocket's and RockSim's stored results do.
Mass on a RASAero import works differently, and it has to. A .CDX1 carries no materials and no per-part masses at all — RASAero is an aerodynamics and trajectory tool, so the flyer types in a single launch weight and CG per simulation. Loft therefore takes the geometry as massless and carries the stated launch weight as one airframe mass component: its mass is the launch weight less the resolved motor's own loaded mass, and its station is solved from ma·xa + mm·xm = M·xcg so the loaded vehicle balances exactly where the file says. That keeps one component tree every surface reads the same way, and it means swapping the motor in the app correctly keeps the airframe and changes only the motor. When the motor can't be weighed from the bundled data, the whole stated weight is placed at the stated CG and the import says so.
Coordinate model & integrator
The flight is integrated with a fixed-step 4th-order Runge–Kutta scheme (lib/sim/simulate.ts). The state is carried as full 3-D position and velocity vectors, but this session integrates translational motion in the vertical plane: thrust and drag act along the flight path (velocity-aligned), assuming the rocket flies at a small angle of attack — which holds for a stable rocket in light wind. Rotational dynamics (weathercocking, pitch/yaw) are not integrated; static stability is computed and reported separately. The vector-shaped state is deliberate: extending to a full 6-DOF solve is additive, not a rewrite.
The step is phase-adaptive. Boost and coast — where thrust and drag change quickly — use a fine fixed step (0.01 s). The long descent under recovery is different: once a canopy is open the vehicle settles to a near-constant terminal velocity, an all-but-linear fall that a much coarser step integrates just as accurately, so the descent runs at a 0.2 s ceiling — set from a convergence study (lib/sim/descent-convergence.test.ts: halving it moves the landing point and flight time by under a tenth of a percent). The descent is the bulk of a full flight's cost, so this ceiling matters most for the hundreds of flights a Monte-Carlo runs — it is a fifth to a quarter of the whole run. An open parachute's quadratic drag is a stiff decay, though, and a fast deployment (a mistimed early ejection, a payload popping its chute at separation speed) can push an explicit step past its stability limit and diverge. So through the opening transient the descent step is shortened to hold dt·λ within the RK4 stability region, where λ = ρ·(Cd·A)·v/m is the linearised drag-response rate — small while the speed is high, relaxing back to the ceiling as the canopy brings the rocket to terminal. The result is stable for any deploy speed without paying for a fine step over the whole descent.
Launch rail & thrust-to-weight
While the rocket is still on the launch rail (position along the rail ≤ the guide length) its motion is constrained to the rail axis: thrust and gravity act along the rail and any lateral acceleration is reacted by the rail, so it cannot pitch until it clears the guide. The speed at which it leaves the rail — the rail-exit velocity — is the speed the fins first have to stabilise the airframe; below about 15 m/s(50 ft/s) that is flagged, as a stable departure is not assured. It is read at the exact instant the rocket has travelled the rail length — interpolated within the integration step rather than sampled at the step's end — so a coarse step doesn't overshoot the crossing and report an optimistically high departure speed; the figure matches an independent 6-DOF engine (RocketPy) to a fraction of a percent.
The liftoff thrust-to-weight ratio is the peak thrust developed while clearing the rail divided by the loaded weight (F / m·g) — the most basic launch-safety check, and, unlike rail-exit velocity, independent of how long the rail is. Below 1 : 1 the rocket cannot leave the pad at all, which Loft flags as a warning (the apogee it would otherwise report is essentially zero and meaningless); below the 5 : 1 figure commonly taught for high-power rockets it is flagged as a caution to verify the rail is long enough. These are rules of thumb, not verdicts.
Sources: minimum rail-departure velocity and the 5:1 thrust-to-weight guideline as given in the NAR/Tripoli high-power safety guidance and standard model-rocketry texts (e.g. Stine & Stine, Handbook of Model Rocketry); the rail-constraint and thrust-to-weight formulation follows Niskanen's OpenRocket technical documentation.
Atmosphere
Temperature, pressure, and density follow the U.S. Standard Atmosphere (1976) layer model (lib/sim/atmosphere.ts), integrated from the hydrostatic equation for an ideal gas — the isothermal exponential form in isothermal layers, the power-law form under a lapse rate. Speed of sound is √(γ·R·T); dynamic viscosity follows Sutherland's law. A non-standard ground state (a warm, high, or low-pressure day) shifts the sea-level anchors while keeping the standard structure aloft — this is what the optional “today's conditions” re-run uses.
Sources: U.S. Standard Atmosphere, 1976 (NOAA/NASA/USAF); Sutherland (1893) as tabulated in standard fluid-dynamics references.
Mass, CG & inertia
Mass properties (lib/sim/mass.ts) are built from the component tree. Where geometry defines mass unambiguously — tubes, rings, fins — it is computed from the component's dimensions and its material density. Bodies of revolution (nose cones, transitions) are integrated numerically along the contour for volume and centroid (lib/sim/shapes.ts), with a shell subtracted when a wall thickness is given. A nose cone or transition also carries a shoulder— the collar that plugs into the neighbouring tube — whose material (a tube of the shoulder's own wall, plus a bulkhead disc when it is capped) is massed too and placed at its own station, so it shifts the CG the way the real part does; on a small model rocket the nose shoulder alone can be several grams that would otherwise be dropped. Recovery and fitting parts are stored the same way — as a material plus geometry, not an explicit mass — so their mass is computed too: a parachute or streamer from its canopy area and areal density, a shock cord from its line density times cord length, and a launch lug from its bulk material over its tube wall. A rail button is stored differently again — an outer and an inner diameter, a total height and the two flange heights, and no wall thickness at all — so it is massed as the spool those describe: a base flange, a shank standing it off the airframe, and an outer flange. That is the moulded body only; the steel screw a button is mounted with is not part of the geometry the file states, so where a design gives the part's own mass instead, that figure wins. On a small model rocket these are grams; on a high-power rocket a long tubular-nylon harness is a real, CG-shifting mass that would otherwise be silently dropped. A fitting's mass is counted once per instance, so a pair of rail buttons weighs twice what one does and changing that count in the editor moves the mass and the parasitic drag together; the editor states the mass of one of them, and a mass stated in the design file scales the same way.
What a stated weight or balance point replaces
An explicit <overridemass> or <overridecg> in the design always wins; when it is flagged to override all subcomponents— a section weighed as a whole — that one figure stands in for the section and everything inside it, rather than being added to the parts' own computed masses. On a single part, a stated balance point describes the whole of it, shoulder included, and a stated weight moves none of it: the part keeps the balance point its geometry gives it — shoulder counted — and only its weight is replaced. That is what the two fields mean in the format, and it is what a knife-edge reading of the part in your hand measures, so the number you write is the number you took. (A section weighed as a whole is the other case and does move its own balance point: one figure replaces the section's parts, so it is placed at the carrier's own centroid rather than at the mass-weighted centre of the parts it stands in for.) The parts table says which is which. A mass the design file states outright — an OpenRocket override, a RockSim KnownMass— is marked, and so is one carried through from the source tool's own calculation rather than recomputed here; an unmarked mass is Loft's, derived from the part's geometry and its material. Measured across the 35-design corpus: 108 masses stated by the design, 60 carried from the source tool, 401 computed here. They are different claims and a bare number cannot tell them apart. The mark says what the FILE states, not that somebody weighed it — a design exported from Loft and reopened comes back with the masses Loft computed written into it, and marked accordingly. You can state either figure yourself. The editor takes both a weight and a balance point for the nose cone and the body tube — the number you read off a scale and the one you find on a shell balance — and the mass & balance breakdown marks those as yours, distinct from the design's claim and from Loft's own arithmetic. (The parts table marks a stated MASS; it carries no balance-point column yet.) A balance point is measured from the part's own fore end, as OpenRocket's override tab asks for it, and describes the WHOLE part — a cone's shoulder included. So the figure to state is a knife-edge reading of the part as it sits in your hand, and on a cone whose shoulder carries much of its weight that balance point can sit behind the cone's own base; the field allows for it. It is still bounded by the part that physically exists, shoulder and all: a station off the end of that cannot mean anything, so it is clamped rather than flown. Where a stated balance point would not reach the flight at all — the part carries no weight of its own for one to place — the field says so instead of accepting a number that changes nothing. The centre of gravity is mass-weighted; pitch inertia is the sum of each part's own inertia plus a parallel-axis term. Propellant burns off over the flight, so mass and CG are time-varying.
Weighing a part yourself
You can put your own weight on a canopy. The mass Loft computes for a parachute comes from its area and an areal density, which is a fair estimate of fabric and nothing else — it cannot see the shroud lines, the swivel or the deployment bag, and on a small rocket those can outweigh the canopy. So /design takes a canopy mass beside the drag coefficient: put the packed chute on a kitchen scale and type what it says. The figure is flown as-is rather than scaled, it survives a resize in the same edit — resizing a canopy scales the mass Loft estimated, never the one you weighed — and the caption beneath both fields says whose number each one is. This is the field the design formats reach for most: 22 of the 64 <overridemass> elements in the reference corpus sit on parachutes, more than on any other kind of part, and Loft could read them from the first importer while offering no way to write one.
The same field is on the internal structure — centring rings, tube couplers, bulkheads, engine blocks and motor-mount tubes, which share one selection and one set of controls. Counted across the reference corpus by kind, those five carry 45 masses that the design or its own tool supplied rather than Loft computing them, more than the nose cone and the body tube together — a plywood ring cut on somebody's bandsaw weighs what it weighs, not what its diameter and a book density say. Unlike the canopy, these parts have no weight of their own in the model at all: their mass is derived from the geometry and the stock every time, so a figure you type is stored as an explicit override, exactly as an <overridemass> from the file is, and the parts table marks it as yours.
Stated masses, and where they win
A RockSim .rkt is the exception to the “compute from geometry” rule: it stores RockSim's own per-part mass — the calculated value, or the measured (“known”) value when the design is set to use it — so Loft honours those as per-part overrides and flies the exact masses the design specifies. That keeps a RockSim import faithful to its source and keeps the stored-results comparison about the aerodynamics and integration rather than a mass-model difference. (An .ork stores no per-part mass, so there Loft computes it as above.)
Auto radii and auto bores
Nose-cone contours use the standard published profile equations (conical, tangent ogive, ellipsoid, power, parabolic, and the Haack series).
OpenRocket marks a radius that should match a neighbour as auto. Loft resolves those before simulating — a body tube takes its neighbour's radius, a transition end takes the body it meets, and an internal part (coupler, ring, inner tube, bulkhead) fits inside its enclosing part, whether that's a tube, a coupler, or the nose. When a body radius still can't be matched to a neighbour — a whole airframe left auto, anchored only by a boat-tail end or an internal part — it falls back to the rocket's largest known radius (the same value the aerodynamic reference is taken from) so the airframe keeps a defined, self-consistent size instead of collapsing to a drag-free, near-massless needle flown against a borrowed reference area. This mirrors OpenRocket, whoseauto radius searches fore and aft for a dimensioned section and only then uses a default. The substitution is flagged in the import warnings, never silent; only when nothing anywhere resolves is a section finally treated as zero — and even then it is never left undefined, so one unresolved part can't poison the total mass and reference area.
An internal part's BORE has its own auto rule, and it decides that part's mass. OpenRocket states it in one line — “matches inner diameter to the outer diameter of the child inner tube” — and its own source takes the largest inner tube among the ring's siblings, clamps the result to the ring's own outer radius, and leaves 0 (a solid disc) when it finds none. Loft reproduces that, because the file was written by that tool and auto means whatever it computes. Read instead as a missing number, the bore fell back to the outer radius less a wall thickness the file often does not state either — which is no hole at all, and therefore, since the volume is π(ro² − ri²)L, no material and no mass. Anything that is not a centring ring, and states no wall, gets a 1.5 mm one; that is Loft's number rather than the file's and the import warnings say so. It matters because these parts have no aerodynamic term at all, so every dimension of them reaches the flight through mass alone — which sets the centre of gravity, and with it the stability margin.
Aerodynamic stability — Barrowman
The centre of pressure and normal-force-coefficient slope come from the Barrowman equations (lib/sim/aero.ts), the standard subsonic, small-angle method. Each body-of-revolution and fin set contributes a normal-force slope CNα and a centre of pressure, combined as a coefficient-weighted mean.
nose: C_Nα = 2·(r_base/r_ref)², X_cp = L − V/A_base transition: C_Nα = 2·[(r_aft/r_ref)² − (r_fore/r_ref)²] fins (N): C_Nα = K_fb · 4N(s/d)² / [1 + √(1 + (2·l_m/(C_r+C_t))²)] interference: K_fb = 1 + r_body/(s + r_body) static margin (cal) = (X_cp − X_cg) / d_ref
What d_ref is, since every figure above is quoted in it. A design may state its own reference, or ask for its nose's diameter; otherwise Loft takes the widest component in the design. Stated plainly here because the caliber is invisible in the number that depends on it — a margin reads the same whether the reference behind it is right or wrong.
Where that rule is imperfect, and it is worth knowing which way.“Widest component” ought to mean the widest part of the airframe — the outer mould line the air actually meets — because a part that lives inside the airframe is not a caliber the rocket presents to the airstream. When you build in Loft that is what you get: a coupler or a centring ring wider than the tube holding it is refused rather than flown, because it would quote your stability in calibers of a diameter your rocket does not have. Animported file is taken as its author drew it, so a design whose internal part is genuinely wider than its own airframe would still set d_ref from that part. None of the 35 real designs Loft tests against is drawn that way, so this is a gap in the rule rather than a number any of them is currently getting wrong.
Fin planforms beyond the trapezoid
Elliptical and freeform fin sets use their area- and span-equivalent trapezoid for the normal-force slope. An elliptical fin's centre of pressure, though, is taken exactly for its planform: integrating the quarter-chord aerodynamic centre over the elliptical chord c(y)=c_root·√(1−(y/s)²) gives X_cp = (½ − 2/3π)·c_root ≈ 0.288·c_root aft of the root leading edge — further aft than the equivalent trapezoid, so it no longer under-predicts the margin. A freeform fin carries no explicit chord or span — only an outline of points — so its semi-span, root chord, planform area, and sweep are derived from that outline first; without that step the fin would read as zero-span and add no normal force. Its centre of pressure is then taken exactly from the same outline by strip theory — X_cp = ∫(x_LE + ¼c)·c dy / ∫c dy over the polygon, the chord-weighted mean of the local quarter-chord line — which reduces to the trapezoid formula for a trapezoidal outline and to 0.288·c_root for an elliptical one, so an unusual planform is no longer flattened to an equal-area trapezoid for stability. Being a fraction of the chord, it is invariant when a geometry edit stretches the fin spanwise. A degenerate part — a fin set with no fins, span, or chord, or a nose with no radius — contributes no normal force rather than a division by zero, so a malformed or placeholder part can't leave the centre of pressure and static margin undefined (which would also silently suppress the low-stability warning).
Tube fins
Tube fins are not plates, so the Barrowman fin equations do not apply to them. A tube fin is a short open duct: at a small angle of attack the streamtube it captures enters inclined at α and leaves aligned with the tube's — and so the rocket's — axis. Slender-body/momentum theory, the same theory behind the nose and transition terms above, gives the reaction from that turning as N = ρV²·A_duct·α, i.e.
C_Nα = 2·A_duct / A_ref per radian, A_duct = Σ N·π·r_inner²
— exactly the nose-cone form 2·(A_base/A_ref) with the captured area in place of the base area. The turning is distributed along the duct rather than concentrated at either lip, so the resultant is taken at the tube's mid-chord. RockSim's own tube-fin example file stores the numbers its engine computed for the same set — BarrowmanCNa = 10.2204 at BarrowmanXN = 0.524222 m — and this relation reproduces them to within 1% and 2.6% of the chord respectively. Against OpenRocket's stored per-step CP on its tube-fin example Loft sits about 0.9 caliber forward, i.e. conservative; see Limitations. A tube with no bore left (a degenerate part) captures nothing and contributes no normal force.
Sources:J. S. Barrowman & J. A. Barrowman, “The Practical Calculation of the Aerodynamic Characteristics of Slender Finned Vehicles” (1966/1967); as compiled in the public Apogee Peak of Flight newsletters (#149, #150, #157) and the OpenRocket technical documentation. For the duct/inlet normal force: J. N. Nielsen, Missile Aerodynamics (McGraw-Hill, 1960), slender-body treatment of flow-through inlets.
Stability trim by nose ballast
Stability trim (nose-ballast goal-seek).When a design's margin is thin, Loft solves the nose ballast that would bring it to a healthy value directly, rather than making you read it off a sweep (lib/sim/trim.ts). The margin is measured at the loaded centre of gravity, and ballast of mass b at the nose station x_n moves the loaded CG to the mass-weighted blend x_cg(b) = (M·x_cg₀ + b·x_n)/(M + b) while the aerodynamic CP does not move with mass — so the ballast for a target margin is closed-form, b = M·(x_cg,target − x_cg₀)/(x_n − x_cg,target) with x_cg,target = X_cp − margin·d_ref. As b → ∞ the CG asymptotes to the nose station, so nose ballast can make a design no stiffer than (X_cp − x_n)/d_ref; a target above that ceiling is unreachable by weight alone (the fins are too small or too far forward), which Loft says plainly instead of prescribing an ever-growing lump of lead. It is the inverse of the ballast parameter sweep, sharing the same mass and CG model the flight uses.
Stability trim by fin position
Stability trim (fin-position goal-seek).The weight-free companion: instead of adding nose weight, it solves how far to slide the fin group to reach a target margin. The static margin is very nearly linear in fin station — the fins' centre-of-pressure contribution moves one-for-one with them — so the slope dm/dx is taken from a small finite difference of the exact Barrowman CP and the dry CG (both computed without a flight; moving the fins also drags their own mass, nudging the CG, which is folded in) and inverted: x_target = x₀ + (m_target − m₀)·d / (dCP/dx − dCG/dx). Uniquely, this is the only lever that can reduce an over-stable, weathercock-prone margin — moving the fins forward — which nose ballast, adding only stability, cannot. The solved station, typed into the fin-position what-if, reproduces the target margin (round-tripped against a real flight in the tests).
Drag
Zero-lift drag is a component buildup referenced to the reference area (lib/sim/aero.ts):
Body friction and base drag
- Skin friction — a fully turbulent flat-plate coefficient (Prandtl–Schlichting
0.455/(log₁₀Re)²·⁵⁸) across the whole Reynolds range, because a rocket's boundary layer is tripped turbulent near the nose; a laminar1.328/√Rebranch would under-state friction at the low Reynolds numbers a small, slow rocket sees near apogee. A surface-roughness floor from the design's finish holds friction flat at high Reynolds number where roughness dominates, while the smooth turbulent value climbs above it as the rocket slows — so coast drag rises toward apogee, matching OpenRocket's stored per-step drag. A compressible-turbulent (reference-temperature / Frankl–Voishel) correction is applied at speed, over the body and fin wetted areas with fineness- and thickness-ratio form factors (Hoerner-style). Friction is summed surface by surface, each at the roughness of the finish it actually carries —Σ Cf(ki)·Awet,i— because skin friction belongs to a surface, not to the airframe. Real builds mix finishes constantly (a polished airframe with one bare coupler, a filled nose on an unpainted tube), and charging every surface the roughest one present over-drags them badly: on a real polished 33 mm model whose 25 mm mid tube is left at “normal”, it added about0.14to a0.46coefficient — 30% too much drag — and flew the design ~17% low against the numbers its own file stores. Summing per surface brought that design to −5%, and across the corpus moved the 90th-percentile apogee error from 16.3% to 11.6% with six designs crossing back inside 10% and none crossing out. - Base drag, referenced to the base area: the subsonic correlation
0.12 + 0.13·M²up to Mach 1, then the supersonic recovery0.25/Mabove it — the two branches meet continuously at Mach 1. It is applied in full whether the motor is burning or not, matching OpenRocket's stored per-step drag (which carries the full base drag throughout boost); a blanket thrust-phase discount badly under-drags a body much wider than its motor, where the exhaust fills only a small part of the base. (Carrying the subsonic form supersonically, as a naive model does, makes base drag grow without bound, which is wrong.)
Fin terms
- Fin pressure drag — set by the fin's edge cross-section. A square edge stagnates the flow head-on (stagnation-pressure coefficient ≈ 0.85 subsonic, reduced by leading-edge sweep as
cos²Λ) and leaves a blunt trailing-edge base; a rounded edge attaches the flow at the leading edge — no stagnation face, so only the small compressibility rise there, like an airfoil — while its rounded trailing edge still sheds a wake at about half a square base; an airfoil is streamlined at both edges, leaving only the compressibility rise. Modelling a rounded leading edge as half a square one (a coarser earlier heuristic) over-counted a rounded fin's pressure drag about twofold against OpenRocket's stored per-step Cd on its rounded-fin examples. Referenced to the fins' frontal area (N·thickness·span, summed over every fin set — so a design that models its fins as several separate single-fin sets, a common OpenRocket pattern, is counted in full rather than from one set alone) over the reference area, after the OpenRocket technical documentation and Hoerner. This is the dominant pressure term for a finned model rocket — a thickness-only estimate under-counts it several-fold — and reading the design's stated cross-section is what brought Loft's drag on the reference “simple model rocket” into line with OpenRocket's. A design that names no cross-section is treated as square, OpenRocket's own default. Each fin set is charged for the section it actually carries, accumulated set by set: a rocket with an airfoil set and a square set pays airfoil drag on one and square drag on the other. Until August 2026 the draggiest section present was applied to every set, so one square fin made every airfoil and rounded fin on the airframe pay square-edge stagnation drag — on a real three-stage design with three rounded sets among five, that took the whole drag coefficient from1.17to0.75at 100 m/s, with the pressure term falling0.68to0.26. (Both numbers are that design's; it is also the one design in the corpus whose apogee the change moved away from its file's own stored figure, and the limitations log says why.) - What the fin terms still collapse across sets, and by how much. Two inputs to the fin drag are not yet per-set, and on a rocket carrying several different fin sets they belong to no fin on it. The leading-edge
cos²Λreduction uses one angle for the whole design — the last set read, against the largest span across sets. On a real design whose four-fin set sweeps 44.5° and whose single-fin set does not sweep at all, the model reads unswept; on a five-set three-stage design whose sets sweep 35.0–70.6°, it reads 22.4°. Under-stating sweep over-states drag. The thickness ratio feeding the friction form factor is the largest thickness on the rocket over the last set's mean chord: on a two-set design whose sets are both0.50it reads1.00, and on a five-set design whose sets run0.04–0.30it reads0.12. Both are in the limitations log with the designs they were measured on. An elliptical fin's sweep is read from its own planform rather than from its stored zero — its tip sits at mid-root-chord, so its edge sweeps back about half the root chord over the span, and treating that curved edge as unswept over-counted its stagnation drag by ~22% on a real minimum-diameter design; reading the sweep from the planform brought Loft's per-step drag on OpenRocket's elliptical_v1.9 example to within ~1% of its stored curve. That correction is read from each elliptical fin's own planform — but it is written into the same single design-wide angle as every other set, so on a rocket where an elliptical set is not the last one read, its sweep relief is overwritten and does not reach the drag at all. A design whose only fin set is elliptical — which is what the measurement above was made on — gets it in full.
Tube fins as a drag term
- Tube-fin drag — a tube fin is aerodynamically a rolled-up flat plate with two bluff ends, so it is built up from exactly that. Friction on both the outer and the inner wall, with no thickness form factor (a thin open cylinder aligned with the flow has no thickness-driven pressure gradient) and at a Reynolds number taken on the tube's own chord, which is far shorter than the airframe's — so the tubes sit higher on the friction curve than the body does. Pressure on the square-cut wall annulus
Σ N·π(r_o² − r_i²): the same stagnation coefficient a square fin edge gets at the leading end (unswept — a tube's lip is perpendicular to the flow, so nocos²Λrelief) plus base drag at the trailing end. This is the term that makes tube fins so much draggier than their plan area suggests: six body-diameter tubes roughly triple the wetted area and present about a third of the airframe's own frontal area in bare wall edges. Omitting them flew OpenRocket's and RockSim's tube-fin examples ~88% high; with them, −8% and −2% respectively.
Body-shape pressure terms
- Shoulder pressure drag — a diameter-increasing transition (a shoulder) forces the flow outward, adding a stagnation-like pressure drag
Cd = 0.8·sin²φover the frontal-area increase, whereφis the conical joint angle, so a gentle shoulder drags little. After the OpenRocket technical documentation (Niskanen, eq. 3.86), following Hoerner. It is a low-subsonic separation effect, so it is not compressibility-corrected. This term is charged on transitions — parts with a length to take the angle over. Where an airframe changes diameter with no transition at all, there is noφto read and nothing is charged; the flight says so and the limitations log gives the measured size of that gap. - Boattail pressure drag — a diameter-decreasing transition reduces the base area (captured by the base-drag term, which follows the aft diameter), but its sloped surface still carries a pressure drag. It is estimated as the base-drag coefficient acting over the frontal-area reduction, scaled by the boattail's length-to-height ratio
γ: full base drag for an abrupt contraction (γ ≤ 1, about a 27° cone), fading to nothing for a gentle one (γ ≥ 3, about 9°). By construction a zero-length boattail then adds back exactly the base drag its contraction removed, so it nets to no change. After the OpenRocket technical documentation (Niskanen, eq. 3.88); its Mach dependence rides on the base-drag coefficient. - Nose pressure drag — the same
0.8·sin²φstagnation estimate as the shoulder, applied over the nose base area, withφthe contour's joint angle where the nose meets the body (read numerically from the shape). A tangent nose — an ogive, ellipsoid or Haack — meets the body smoothly (φ ≈ 0) and carries essentially none; a cone or blunt shape has a real joint angle and a small pressure drag. After the OpenRocket technical documentation (Niskanen, eq. 3.86); not compressibility-corrected.
Parasitic and wave drag
- Parasitic— the drag of external fittings (launch lugs, rail buttons) computed from each fitting's own frontal area and count rather than a blind allowance, using an axial protuberance drag coefficient reduced for sitting in the body's boundary layer (Hoerner; the model-rocket launch-lug literature); and a small flat residual for un-modelled hardware (joints, screw heads), with a bounded Prandtl–Glauert amplification below the critical Mach. Negligible on a slender high-power body but a real contributor on a small model rocket where the lug is large relative to the airframe.
- Wave (compressibility) drag — zero below the critical Mach (~0.8), a smooth transonic rise to a peak near Mach 1.15, then a supersonic decline toward a slender-body plateau. The peak is geometry-driven: the forebody term scales with the nose's own fineness ratio (slender ⇒ less) and its contour shape — a Von Kármán (LD-Haack) ogive is the minimum-wave-drag body, with parabolic, power, tangent-ogive, ellipsoid, and conical noses ranked progressively higher, after the published nose-shape drag comparisons — and the fin term with fin thickness ratio reduced by leading-edge sweep (
cos²Λ). This gives the total drag the publishedCd–Mach shape (subsonic-flat → transonic peak → supersonic decline) rather than growing without limit; any flight above Mach 0.8 is still flagged extrapolated, as the peak is a bounded parametric estimate, not a per-geometry wave-drag solution.
The summed coefficient is capped at a physical ceiling well above any real nose-forward rocket. That cap is a numerical guard, not a model term — it never engages on a real flight, only on malformed geometry (say a unit-scale import error), where an astronomically large drag would otherwise destabilise the fixed-step integrator and report a nonsensical apogee.
Sources: S. F. Hoerner, Fluid-Dynamic Drag (1965); the drag treatment in the OpenRocket technical documentation; standard flat-plate friction correlations; the Sears–Haack / Von Kármán minimum-drag-body result and the published nose-cone drag comparisons for the wave-drag shape ranking. The drag model is the largest source of error — see limitations.
Motors
A design references a motor by manufacturer and designation but does not embed its thrust curve, so Loft resolves the motor against a bundled database of real RASP .eng curves from ThrustCurve.org (lib/motors/). Thrust is linearly interpolated in time; propellant mass is depleted in proportion to delivered impulse (constant-Isp assumption), so motor mass falls from loaded to casing mass over the burn. Matching prefers an exact designation, then a looser substring or class-and-thrust core (so a Cesaroni “838J293-13A” still resolves to “J293”). A motor is matched on every name it is published under — ThrustCurve's manufacturer designation and its common class-and-thrust name — because a design file may carry either: a Cesaroni reload sold as part number “648J285-15A” is written into an OpenRocket file as “J285”, and both name the same motor. Those catalogued names come from ThrustCurve's certification record rather than the RASP file header, which carries whatever the curve's author typed — and so does the casing envelope. That is not bookkeeping: the motor-swap list filters candidates by mount diameter and the design diagram draws the casing, so one bundled 54 mm motor whose header claimed 75 mm was being offered for the wrong mount. A curve whose catalogued name is more specific than the one a design writes — AeroTech's bundled H100W curve is the H100W_DMS, a different product line from the RMS reload — resolves as an approximate match and says so, rather than claiming to be the motor the design named. An exact designation matches regardless of a maker-string difference, but a loose match never crosses manufacturers — a design's “K550” is left unresolved rather than silently matched to a different maker's “K550W”. The UI flags an approximate or failed match. A motor cluster(OpenRocket's cluster configuration, e.g. a “4-ring”) is flown as that many identical motors on the centreline — full thrust and propellant, plus the structural mass of that many motor tubes where the design holds the cluster in an inner tube — with the count shown on the motor tag. A cluster carried by the airframe tube itself describes no extra tubes, so none are counted; see limitations. Where a design assigns different motors to separate mounts and only some resolve, the flight runs on the resolved ones and a warning reports the under-counted thrust.
Staging
In-line (serial) stages fly in sequence. By default the bottom stage lights at launch and each stage above air-starts when the stage below burns out, plus any ignition delay it specifies — so a boosted-dart coast between separation and the sustainer's air-start is honoured. That default is what OpenRocket calls an automatic ignition event, and a design can say otherwise: the file records an event per motor per configuration, and Loft follows it. A launch event fires that motor at liftoff wherever it sits in the stack — one OpenRocket example lights its middle stage first — and an event that can never arrive (a burnout trigger on the bottom-most stage, with nothing beneath it to burn out) leaves that motor unlit, riding as inert mass, which is what the file's own stored mass and thrust traces show. Reading those events took that design from 49.8% high to 9.4% low, with rail-exit velocity landing within 0.4% of the stored figure. A serial stack also parts at one joint: the stage that separates takes everything below it with it, rather than the stack shedding one stage per event. A stage separates on the event the design specifies: by default (and for the usual boosted staging) when it finishes burning, but a stage set to separate at its own ejection charge hangs on until that charge fires — often a long delay, so a payload or dual-section rocket stays whole until near apogee and only then parts, rather than splitting at burnout. When it separates its structure and empty casing leave the vehicle, and the flight continues on the stages still attached. The simulator recomputes mass, the reference area, and the drag buildup for the attached stack at each separation (each phase's vehicle is the top-most stages, evaluated with the same mass and aero code as a single stage), so a dead booster is no longer lofted to apogee. Because the vertical-plane solve is a point mass, only the total mass, thrust, and reference drag change across a separation — the trajectory doesn't depend on where the centre of gravity sits within the vehicle. Stability does depend on it: the stages stack nose-to-tail into one continuous airframe, so the centre of gravity and Barrowman centre of pressure are computed for whichever stages are attached. Because an upper stage can be stable inside the loaded stack yet unstable once it flies alone, the sustainer's own static margin — evaluated loaded, right after separation, its worst case — is checked separately and flagged if it drops below 1 cal. The exception is a payload section that pops its chute on the separation (a lower-stage-separation recovery): it is under canopy from that instant and never flies ballistically, so a finless payload isn't flagged as an unstable upper stage. Only the final (sustainer) stage's descent is tracked; a separated booster's own recovery isn't. Parallel and strap-on staging is not modelled (see the limitations).
Ignition timing is resolved per motor, not just per stage, so a second motor within one stage can be air-started after its own delay while the first burns from launch — the delay is read from the flown configuration, so a design that sets a different air-start time in each of its stored simulations flies each one distinctly. The airstarted motor rides as dead weight until it lights, then adds its thrust; the vehicle's peak speed and apogee shift with the timing, exactly as a staged air-start does.
Recovery & drift
Each recovery device deploys on its event — apogee, a set altitude, the motor's ejection charge, or the separation of the stage below it (the payload/dual-section charge that both parts the sections and pops the chute) — plus any deploy delay it specifies: the vehicle free-falls on body drag until the canopy opens, so a delayed deployment reports the higher speed reached at line-stretch. An ejection deployment fires at the real charge time (burnout plus the design's delay), so a too-short delay opens the canopy before apogee while still ascending, and a too-long delay opens it late at speed — or, if the charge would fire after the rocket is already down, not at all. A plugged motor (OpenRocket's delay = none, RockSim's negative ejection delay) carries no charge at all, so a device waiting on one never opens and the flight is ballistic — Loft does not assume an altimeter that the design does not describe. A design that simply states no delay is different: that is silence rather than a statement, and such a device falls back to deploying at apogee. Both a pre-apogee opening and a ballistic (no-deploy) descent are flagged. Descent then uses the summed deployed drag areas (Cd·A); the steady descent rate this gives is checked against the ~3–6 m/s (10–20 ft/s) most designs aim for, and a firm (>25 ft/s) or hard (>35 ft/s) landing under a too-small canopy is flagged as a caution or warning — a rule of thumb, not a verdict. Descent drift is the canopy drifting with the wind; with the “today's conditions” re-run, the wind varies with altitude from the winds-aloft profile.
Two landing speeds, because they answer different questions. Ground-hit speed is the vertical descent rate at the moment of impact. That is the convention the rules of thumb above are written in, the one the per-section landing energy a waiver is judged on is computed from, and the one every design file's own stored figure uses — so it is the number Loft compares against OpenRocket, RockSim and RASAero. Arrival speed is the total speed over the ground with the sideways drift included, and it appears only when the wind makes it materially larger. Both are real: a canopy descending at a comfortable 5 m/s in a 20 mph wind arrives at about 10 m/s over the ground, and a glancing arrival is harder on an airframe than the descent rate alone suggests — but it is the descent rate that says whether the canopy is big enough. Folding the two together is a mistake Loft used to make: the hard-landing flag then fired on the weather rather than on the recovery, and the landing energy read almost four times too high on a windy day.
Recovery sizing (goal-seek). When a design lands harder than it should, Loft solves the canopy that would bring it down gently rather than leaving you to guess and re-fly (lib/sim/recovery.ts). It is the recovery-side companion to the stability trim, and closed-form: at terminal velocity drag balances weight, v = √(2·m·g / (ρ·CdAtotal)), with m the burnout (descent) mass, ρ the air density at the field, and CdAtotal = CdAchute + ½·Aref — the same airframe body-drag term the descent integrates — so the canopy for a target speed is CdAchute = 2·m·g/(ρ·v²) − ½·Aref, reported as a drag area and an equivalent diameter. Because it uses the flight's own descent model, a canopy sized this way, flown, lands at the target speed — checked against a real flight in lib/sim/recovery.test.ts. The goal-seek names one size; the recovery-size what-if (Design workspace) is its open-ended companion — a multiplier on every deployed device's drag area that re-flies the design with a bigger or smaller canopy, so you can watch the descent rate, drift, and deployment speed move together (a bigger canopy lands softer but drifts farther and snaps open harder). It scales only the recovery, never the ascent, so apogee and stability are untouched; the Monte-Carlo landing scatter honours it.
The optimum ejection delay Loft reports is the delay (from burnout) that would deploy the recovery at apogee — a property of the airframe, motor, and launch conditions, not of the delay actually flown. It is measured from a free coast to the true apogee with recovery suppressed, so a design flown with a too-short delay — whose canopy opens early and cuts the climb short — still gets a sound recommendation rather than an even shorter one that would compound the mistake.
Fin flutter
Fins have their own elasticity, and above a critical airspeed — the flutter boundary— a bending-and-twisting oscillation stops damping out and diverges, shredding the fin (and often the rocket). Loft estimates each fin set's flutter speed along the ascent and reports the worst-case margin— the flutter speed divided by the peak airspeed the fin actually sees — sampling the real ambient pressure and speed of sound at every altitude the vehicle passes through, since the boundary rises as the air thins. Neither OpenRocket nor RockSim reports this, so it is Loft's own safety heuristic.
On a staged rocket each fin set is judged only over the part of the flight its own stage was still attached for.A booster's fins come off with the booster, so the speed the sustainer reaches afterwards is not a speed those fins ever saw. Until 2026-08-02 Loft charged them with it anyway, and because the reported margin is the worst across all fin sets, a shed booster could supply the whole warning: on a three-stage sample design the red flag came from a fin set that left the stack at 0.86 s, judged at 2.37 s and 77 m/s. Over its own flight that same fin set has a margin of 2.11 rather than 0.68. The worst fin set also selects which fin the thicken-to hint names, so the wrong one being worst pointed the fix at the wrong part of the rocket.
The estimate is the simplified flutter-boundary closed form, Vf = a·√( G / [ 1.337·A³·P·(λ+1) / (2·(A+2)·(t/c)³) ] ), with a the local speed of sound, G the fin material's shear modulus, A the exposed-fin aspect ratio, λ the taper ratio, and t/c the thickness ratio on the root chord — so flutter speed climbs with the cube of the thickness ratio and the square root of stiffness, and falls with aspect ratio (thin, high-aspect fins flutter first). The shear modulus is taken from the design's own fin material (G10 fibreglass is assumed, and said so, when the material is missing or unrecognised). This is a preliminary-design estimate, method-dependent to roughly ±20% — the fuller NACA method, which adds a chordwise mass-balance term, tends to sit a little lower — so Loft keeps a recommended margin of ≥ 1.5× and cautions (never reassures) when it is thin; it never certifies a fin as flutter-safe.
When the margin is thin, Loft names the fix rather than just saying “thicken the fins”: since the flutter speed rises with the 1.5 power of the thickness ratio and the peak airspeed the margin is taken against barely moves with thickness, the thickness for a target margin is closed-form, ttarget = tnow·(margintarget/marginnow)^(2/3). It errs a touch thick — a thicker fin also drags a little more and lowers the peak airspeed, so the flown margin comes out just above the target — which is the safe direction for a fin caution. Shortening the span or a stiffer material reaches the same margin; the suggestion is a starting point to check with the fin-thickness what-if, not a substitute for a real structural design.
Sources: D. J. Martin, NACA TN 4197, Summary of Flutter Experiences as a Guide to the Preliminary Design of Lifting Surfaces on Missiles(1958); the closed form as popularised for rocketry by Apogee Components' Peak of Flight newsletter #291.
Monte-Carlo dispersion
A single flight is one draw from an uncertain reality: a rail is never perfectly plumb, wind gusts and shifts, a motor's total impulse varies from one unit to the next, and a built airframe rarely hits its CAD mass exactly. The dispersion tool flies the design a few hundred times with those inputs jittered around their nominal values and reports the spread of the outcomes — the apogee band to expect, the radius from the pad that contains 95% of the landings (the recovery area to plan for), and the landing-speed band (its 95th percentile is the hardest landing to size recovery against, which a heavier-than-CAD build pushes up) — with, when some flights land firm, the fraction that land firm (over 7.6 m/s) or hard (over 10.7 m/s), the same rule-of-thumb thresholds the flight's own hard-landing warning uses. It also reports the landing-energy band (½·m·v² for the whole vehicle) — the figure many fields and waivers cap per section, so its 95th percentile is the worst case to check against a limit; a design that lands in separated sections divides it among them, so read the whole-airframe number as conservative. All FOUR landing figures — speed, energy, drift and the recovery radius — describe only the dispersed flights that reached the ground inside the simulation's time cap, and the panel says how many of the total that was. A flight still descending at the cap has no landing speed and no landing energy — it carries zeros that are placeholders rather than measurements, and averaging those in reported a soft landing that never happened: on one real design at a legal recovery size, every sample was such a placeholder and the panel read 0.00 m/s and 0 J. Drift and the recovery radius joined that population on 2026-08-02, and they are why the rule is stated as four rather than two: an un-landed flight's drift is not a placeholder zero but the distance it had covered when the cap stopped it, so it read as an ordinary smaller number and pulled the recovery area DOWN — understating the figure whose only job is to say how much ground to plan for. Where none of them land, all four are withheld with the reason instead, and the landing scatter says so rather than drawing an empty plot. Every sample runs through the same solver as the main flight; nothing about the physics changes. The uncertainty is entirely in the inputs, which are your own stated assumptions, so the result is an honest propagation of that spread — not a claim of new precision.
How each input is drawn
Each input is drawn from a normal distribution about its nominal value at the one-sigma spread you set: the motor impulse scales the thrust curve (a motor's propellant mass is essentially fixed, so its lot-to-lot variation is in average thrust); the dry mass scales the airframe's structure uniformly (so the centre of gravity holds and only the total mass moves); the aerodynamic drag scales the zero-lift drag coefficient (the drag model's own uncertainty — the single largest error source, so its spread belongs in the band rather than being treated as exact; it does not touch a deployed canopy's drag area); the recovery drag scales every open canopy's Cd·A (a parachute's drag coefficient is only known to ±10–20%, and it sets the descent — so this is the main driver of the landing-speed band, the piece the ascent drag deliberately leaves out); the rail angle adds a lean to the launch rod; and the wind speed varies around the nominal being flown. Those nominals are the launch conditions in force — the design's own stored setup, with whatever you have changed under Conditions on top, so the dispersion answers for the same day the flight above it does rather than for the day the file was saved. Impulse, dry mass, and drag are the main drivers of the apogee band; rail angle and wind drive the landing scatter. The rail-lean and wind directions are sampled uniformly from all bearings, so the landing scatter maps the recovery area regardless of the day's wind heading. Under today's weather that is not so, and the panel says which case it is in: the solver then flies a whole wind profile rather than one surface figure, on the forecast's own bearings. A spread on the surface wind has nothing to vary — that field is greyed out with a note — and the scatter is the spread of one forecast rather than a disc over all headings, so read it as the recovery area for THAT day rather than for any wind. The whole run is driven by a fixed-seed pseudo-random generator, so the same design and the same dispersions reproduce the same cloud — a dispersion is a stable property of the design, not wall-clock noise. The reported bands are 5th-to-95th percentiles: of every flown sample for apogee and max speed, and of the flights that reached the ground for the four landing figures.
Waiver-ceiling reporting
If you enter a waiver ceiling, Loft reports the fraction of the dispersed flights whose apogee topped it — the “chance of busting the ceiling” a high-power flyer weighs against an altitude limit. It is read from the samples already flown (changing it doesn't re-fly), and it inherits the model's own systematic apogee error, so it is a planning cue to keep real margin against — never a guarantee.
Live weather (optional)
The “re-fly for today” feature fetches current surface conditions and a pressure-level winds-aloft profile from Open-Meteo (keyless, no account), the same source the sibling Window uses. It is the only part of Loft that touches the network, always behind an explicit tap.
The two halves of that fetch come from different parts of one response, so Loft pairs them explicitly. Surface temperature, pressure and wind are the live observation; the winds-aloft profile is one hour of a 24-hour forecast day, and Loft takes the hour that matches the surface reading's own timestamp. The Conditions panel states which hour the profile is for, because a drift estimate is only as current as the wind above the pad.
Between two pressure levels the wind speed is interpolated linearly and the direction is interpolated along the shorter compass arc — a bearing is not a scalar, and interpolating one as though it were reverses the wind at every layer whose levels straddle north.