<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>TinyComputers.io (Posts about rockets-engine)</title><link>https://tinycomputers.io/</link><description></description><atom:link href="https://tinycomputers.io/categories/rockets-engine.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Original site content © 2022–2026 Tiny Machines Workshop, LLC, except where otherwise noted. Some rights reserved.</copyright><lastBuildDate>Sun, 01 Nov 2026 01:55:41 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>rockets-engine: Giving the Ballistics Engine a Motor</title><link>https://tinycomputers.io/posts/rockets-engine-giving-the-ballistics-engine-a-motor.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/rockets-engine-giving-the-ballistics-engine-a-motor_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;35 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;A bullet gets all of its energy in the first few inches. Everything &lt;a href="https://tinycomputers.io/posts/open-sourcing-a-high-performance-rust-based-ballistics-engine.html"&gt;the ballistics engine&lt;/a&gt; does happens after the muzzle: a fixed mass coasting through air that slows it, a gravity that pulls it down, a spin that pushes it sideways. Nothing is added after the barrel and nothing is lost except speed.&lt;/p&gt;
&lt;p&gt;A rocket breaks every one of those assumptions. It keeps pushing for a second or a minute. It gets lighter as it pushes, because the thing doing the pushing is its own mass leaving out the back. It can drop an empty stage and light the next one. It can be steered. And the interesting part of its flight, the part a simulator has to get right, is exactly the part a ballistics solver never sees.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://crates.io/crates/rockets-engine"&gt;rockets-engine&lt;/a&gt; is the crate I wrote to cover that part. It started as the flight model for a game that needed powered arcs, and it grew into something closer to a model-rocket simulator: motor files, parachutes, a geometry modeler, winds, Monte Carlo dispersion, a command-line tool and the same tool &lt;a href="https://rockets.rs/sh/"&gt;running in a browser&lt;/a&gt;. It went from 0.1.0 to 0.20.5 between October 23 and October 31: 63 commits and 28 published versions. This is how it is built, how I checked it, and where the checking found that I was wrong.&lt;/p&gt;
&lt;h3&gt;Don't fork the ballistics engine&lt;/h3&gt;
&lt;p&gt;The first decision was what not to do. The ballistics engine already has a drag kernel I trust: a standard atmosphere, Mach-dependent drag, wind, spin, and the derivative function that the Ballistics Insight app runs on every phone it is installed on. Forking it to add thrust would have meant two copies of that kernel drifting apart.&lt;/p&gt;
&lt;p&gt;So rockets-engine calls it. The atmospheric adapter, &lt;code&gt;BallisticForces&lt;/code&gt;, builds the engine's input structure once and, at every stage of every integration step, writes the rocket's current mass into it and calls the upstream &lt;code&gt;compute_derivatives&lt;/code&gt; for gravity and drag. The ballistics engine thinks in grains and inches because rifles do, so the adapter also updates the imperial mass mirror (&lt;code&gt;weight_grains&lt;/code&gt;) every time the SI mass changes, sets the caliber from the body diameter, and turns off the bullet-specific post-processing that has no meaning for a rocket. Thrust, mass flow and staging live entirely on the rocket side of that boundary.&lt;/p&gt;
&lt;p&gt;Everything else follows from treating the engine as a library rather than a codebase to modify. The crate is SI throughout. The local frame is X downrange, Y up, Z to the right. The &lt;code&gt;Simulator&lt;/code&gt; holds no evolving state: each call takes a &lt;code&gt;FlightState&lt;/code&gt;, returns a new one, and leaves the caller's state alone if anything fails, which makes save, replay and "what if I had launched one degree higher" cheap. Guidance is a hook that returns a thrust direction and an optional steering acceleration; the crate has no idea what a target is.&lt;/p&gt;
&lt;h3&gt;Motors and mass&lt;/h3&gt;
&lt;p&gt;A motor is a thrust curve: thrust against time since ignition, piecewise linear between published samples. The crate reads RASP &lt;code&gt;.eng&lt;/code&gt; files, the format &lt;a href="https://www.thrustcurve.org/"&gt;thrustcurve.org&lt;/a&gt; distributes, in both of the layouts that exist in the wild, and it ships five curves built in:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Motor&lt;/th&gt;
&lt;th&gt;Diameter&lt;/th&gt;
&lt;th&gt;Propellant&lt;/th&gt;
&lt;th&gt;Total impulse&lt;/th&gt;
&lt;th&gt;Burn time&lt;/th&gt;
&lt;th&gt;Peak thrust&lt;/th&gt;
&lt;th&gt;Average thrust&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Estes A8&lt;/td&gt;
&lt;td&gt;18 mm&lt;/td&gt;
&lt;td&gt;3.8 g&lt;/td&gt;
&lt;td&gt;2.15 N·s&lt;/td&gt;
&lt;td&gt;0.53 s&lt;/td&gt;
&lt;td&gt;9.6 N&lt;/td&gt;
&lt;td&gt;4.0 N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estes B6&lt;/td&gt;
&lt;td&gt;18 mm&lt;/td&gt;
&lt;td&gt;5.6 g&lt;/td&gt;
&lt;td&gt;4.33 N·s&lt;/td&gt;
&lt;td&gt;0.86 s&lt;/td&gt;
&lt;td&gt;12.1 N&lt;/td&gt;
&lt;td&gt;5.1 N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estes C6&lt;/td&gt;
&lt;td&gt;18 mm&lt;/td&gt;
&lt;td&gt;10.8 g&lt;/td&gt;
&lt;td&gt;8.82 N·s&lt;/td&gt;
&lt;td&gt;1.86 s&lt;/td&gt;
&lt;td&gt;14.1 N&lt;/td&gt;
&lt;td&gt;4.7 N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estes D12&lt;/td&gt;
&lt;td&gt;24 mm&lt;/td&gt;
&lt;td&gt;21.1 g&lt;/td&gt;
&lt;td&gt;16.84 N·s&lt;/td&gt;
&lt;td&gt;1.65 s&lt;/td&gt;
&lt;td&gt;29.7 N&lt;/td&gt;
&lt;td&gt;10.2 N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AeroTech G80&lt;/td&gt;
&lt;td&gt;29 mm&lt;/td&gt;
&lt;td&gt;69.0 g&lt;/td&gt;
&lt;td&gt;135.6 N·s&lt;/td&gt;
&lt;td&gt;1.70 s&lt;/td&gt;
&lt;td&gt;107.0 N&lt;/td&gt;
&lt;td&gt;79.7 N&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I computed the impulse and averages from the embedded curves rather than copying the labels, and they land where the letter says they should: an A is 1.26 to 2.5 N·s, a D is 10 to 20, a G is 80 to 160.&lt;/p&gt;
&lt;figure style="margin: 2rem 0; text-align: center;"&gt;
&lt;a href="https://tinycomputers.io/images/rockets-engine/thrust-curves.png"&gt;&lt;img src="https://tinycomputers.io/images/rockets-engine/thrust-curves.png.webp" alt="Thrust against time for the five built-in motors. The Estes A8, B6 and C6 spike to between 9 and 14 newtons in the first quarter second, then sustain about 4 newtons; the D12 spikes to 30 newtons and sustains about 9. The AeroTech G80, plotted separately, holds about 105 newtons for over a second before tailing off." loading="lazy" style="max-width: 100%;"&gt;&lt;/a&gt;
&lt;figcaption&gt;&lt;em&gt;The five built-in curves. Black powder motors spike to light the grain and then sustain; the composite G80 is a different animal and gets its own scale.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Mass is where most simple rocket simulators cheat, usually by integrating a separate fuel state and clamping it at zero when the integrator overshoots. rockets-engine does not integrate fuel at all. It assumes propellant is consumed in proportion to delivered impulse: if I(t) is the impulse delivered so far and I_total is the curve's total, the remaining propellant is m_p(1 − I(t)/I_total). That is the same as assuming a constant effective exhaust velocity, I_total/m_p, which is the honest assumption when all you have is a thrust curve that already includes whatever the nozzle and the ambient pressure did. The mass is then a closed-form function of time, evaluated fresh at each of the four RK4 stages. It cannot drift, cannot go negative, and cannot disagree with the motor's phase in a saved state, because both are derived from the clock. When the curve ends, thrust is zero and the empty casing stays on board.&lt;/p&gt;
&lt;h3&gt;Where the step breaks&lt;/h3&gt;
&lt;p&gt;RK4 is a beautiful method for smooth problems, and a rocket is not smooth. Thrust jumps at ignition. It has corners at every sample on the curve. It drops to zero at burnout. At a separation, a whole stage, with its dry mass, its motor and any propellant it did not burn, leaves the vehicle in an instant. Step across one of those discontinuities and RK4 averages two different rockets into one that never existed.&lt;/p&gt;
&lt;p&gt;The integrator never steps across them. Every ignition, every curve knot, every burnout and every separation splits the step, and the step grid itself stays anchored to fixed multiples of the maximum step from the start of each call, so splitting one interval does not shift every interval after it. Within each piece, all four RK stages see the same attached stages and burning motors, frozen at the start of the piece: a step that ends exactly at a separation still carries the departing stage, and the next one starts without it. A short burn is never skipped by a long step: one test requires a flight taken in two-second steps to match the same flight at half-millisecond steps to within a nanometre.&lt;/p&gt;
&lt;p&gt;Staging falls out of the same structure. A vehicle is a base mass plus any number of stages, each with its own motor, ignition time and optional separation time. Total thrust is the sum over attached motors that are burning, so serial staging (light the next stage after the last one drops) and parallel staging (strap-on boosters burning alongside a core) are the same model. Separating a stage that is still burning, hot staging, simply ends its thrust.&lt;/p&gt;
&lt;figure style="margin: 2rem 0; text-align: center;"&gt;
&lt;a href="https://tinycomputers.io/images/rockets-engine/staging.png"&gt;&lt;img src="https://tinycomputers.io/images/rockets-engine/staging.png.webp" alt="Three stacked panels against time for the crate's staging example. Altitude climbs smoothly. Speed rises in three ramps, flattening during the short coasts between burns. Mass falls linearly during each burn and drops in a step at 0.7 and 1.4 seconds when stages one and two separate." loading="lazy" style="max-width: 100%;"&gt;&lt;/a&gt;
&lt;figcaption&gt;&lt;em&gt;The crate's &lt;code&gt;staging&lt;/code&gt; example: 24 N, 16 N and 12 N stages with short coasts between them. Mass falls smoothly while a motor burns and in a step when a stage leaves.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3&gt;Is RK4 doing its job?&lt;/h3&gt;
&lt;p&gt;The cleanest way to know whether an integrator is right is to give it a problem with an exact answer. A vertical burn with constant thrust, no drag and uniform gravity has one. With constant thrust the mass falls linearly, and the velocity at time t is the rocket equation minus gravity:&lt;/p&gt;
&lt;p&gt;v(t) = v_e ln(m_0 / m(t)) − g t&lt;/p&gt;
&lt;p&gt;where v_e is the effective exhaust velocity and m_0 the mass at ignition. Integrating once more gives the altitude in closed form. I set up a 0.5 kg rocket with 0.1 kg of propellant and a 100 N, two-second burn, which reaches 426.674 m/s and 410.090 m at burnout, and flew it through rockets-engine with the maximum internal step anywhere from half a second down to a millisecond. For contrast I wrote the same physics as a forward Euler loop.&lt;/p&gt;
&lt;figure style="margin: 2rem 0; text-align: center;"&gt;
&lt;a href="https://tinycomputers.io/images/rockets-engine/rk4-convergence.png"&gt;&lt;img src="https://tinycomputers.io/images/rockets-engine/rk4-convergence.png.webp" alt="Log-log plot of burnout velocity error against the maximum internal step. Forward Euler falls along a slope of one, from 12 m/s at half-second steps to 0.025 m/s at one millisecond. rockets-engine's RK4 falls along a slope of four, from 3.7e-5 m/s at half-second steps to about 1e-12 at 0.005 s, where it flattens at the floating-point floor near 2e-13." loading="lazy" style="max-width: 100%;"&gt;&lt;/a&gt;
&lt;figcaption&gt;&lt;em&gt;Velocity error at burnout against the exact solution. RK4 falls sixteen-fold for every halving of the step until it reaches the floating-point floor.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Halving the step from 0.5 to 0.25 seconds cuts the RK4 error from 3.7×10⁻⁵ m/s to 2.3×10⁻⁶ m/s, a factor of 16. That is what fourth order means, and it holds all the way down until the error reaches about 2×10⁻¹³ m/s below a 5 ms step, where the rounding of double-precision arithmetic takes over. At the default 10 ms step the error is 6×10⁻¹² m/s. Forward Euler at the same step is off by a quarter of a metre per second, and at half-second steps by 12 m/s.&lt;/p&gt;
&lt;p&gt;The crate's own test suite asks the same kind of question in more than a dozen ways. A few of the external references:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Compared against&lt;/th&gt;
&lt;th&gt;Tolerance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vacuum burn&lt;/td&gt;
&lt;td&gt;Rocket equation, velocity and position&lt;/td&gt;
&lt;td&gt;2×10⁻¹¹ m/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Variable-mass convergence&lt;/td&gt;
&lt;td&gt;Error ratio when the step halves&lt;/td&gt;
&lt;td&gt;above 14 (16 is ideal)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two-burn Δv&lt;/td&gt;
&lt;td&gt;Rocket equation across two motors&lt;/td&gt;
&lt;td&gt;10⁻⁶ m/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400 km circular orbit, 1 s steps&lt;/td&gt;
&lt;td&gt;Energy and angular momentum&lt;/td&gt;
&lt;td&gt;drift below 10⁻¹²&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unpowered flight, six configurations&lt;/td&gt;
&lt;td&gt;The ballistics engine's own integrator&lt;/td&gt;
&lt;td&gt;2×10⁻⁹&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parachute descent&lt;/td&gt;
&lt;td&gt;Terminal velocity √(2mg/ρC_dA)&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coriolis deflection&lt;/td&gt;
&lt;td&gt;The exact −2Ω×v&lt;/td&gt;
&lt;td&gt;10⁻⁹&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The suite has a little over 200 tests and runs in about 14 seconds.&lt;/p&gt;
&lt;p&gt;The orbit tests matter because rocket flight does not stay small. For trajectories that leave the neighbourhood of the pad, the crate has a &lt;code&gt;CentralGravity&lt;/code&gt; model: inverse-square gravity measured from the center of the Earth, in an Earth-centered frame. The &lt;code&gt;icbm_midcourse&lt;/code&gt; example starts a 500 kg post-boost body at 300 km, moving at 6.5 km/s and 25° above horizontal, and coasts it until it falls back through 120 km.&lt;/p&gt;
&lt;figure style="margin: 2rem 0; text-align: center;"&gt;
&lt;a href="https://tinycomputers.io/images/rockets-engine/midcourse.png"&gt;&lt;img src="https://tinycomputers.io/images/rockets-engine/midcourse.png.webp" alt="Altitude against surface range for the icbm_midcourse example: an arc from 300 km altitude rising to an apogee of 1,364 km about 3,800 km downrange and descending to a reentry handoff at 105 km altitude, 8,064 km downrange." loading="lazy" style="max-width: 100%;"&gt;&lt;/a&gt;
&lt;figcaption&gt;&lt;em&gt;A 1,650-second coast under inverse-square gravity, at one-second steps.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;It reaches 1,364 km, crosses 8,064 km of the Earth's surface, and hands off at 105 km and 6,771 m/s after 1,650 seconds. Specific orbital energy, which should be exactly conserved in a vacuum coast, changes by two parts in 10¹⁵. At that scale a flat frame is simply the wrong model, and the crate says so: the flat-frame models are documented as invalid at intercontinental ranges, and the central-gravity model carries no atmosphere.&lt;/p&gt;
&lt;h3&gt;Checking against OpenRocket&lt;/h3&gt;
&lt;p&gt;Exact solutions check the integrator. They say nothing about whether the rocket's aerodynamics are right, and for a model rocket the aerodynamics are most of the answer. For that there is a better reference than anything I could derive: &lt;a href="https://openrocket.info/"&gt;OpenRocket&lt;/a&gt;, the open-source simulator most hobbyists use, and in particular its test suite.&lt;/p&gt;
&lt;p&gt;rockets-engine has a Barrowman geometry modeler, written against OpenRocket's technical documentation and its &lt;code&gt;FinSetCalc&lt;/code&gt; and &lt;code&gt;SymmetricComponentCalc&lt;/code&gt; sources. Give it a nose cone, a body tube and a fin set, and it returns the drag, normal-force slope (Cnα) and center of pressure as functions of Mach number, plus mass properties from the component masses. OpenRocket's own unit tests include the Estes Alpha III, Estes's long-running beginner kit, with known Cnα and center-of-pressure values. The crate's parity tests build the same rocket and must match OpenRocket's numbers within half a percent, for three fins and for four.&lt;/p&gt;
&lt;p&gt;Matching a coefficient is not the same as matching a flight, so the next step was to fly the same rocket in both. A small Java driver boots OpenRocket 24.12's core headless, loads its Alpha III test fixture, adjusts the parts my modeler does not have (no launch lug, rounded fin edges, a smooth finish, zero wind), flies it on OpenRocket's test motors, and prints the apogees. CI downloads the pinned OpenRocket jar and runs that driver on every push, and the parity tests then fly the same configuration through the &lt;code&gt;rockets&lt;/code&gt; command line and require agreement within 10%.&lt;/p&gt;
&lt;figure style="margin: 2rem 0; text-align: center;"&gt;
&lt;a href="https://tinycomputers.io/images/rockets-engine/openrocket-parity.png"&gt;&lt;img src="https://tinycomputers.io/images/rockets-engine/openrocket-parity.png.webp" alt="Dot plot of apogee for four flights, OpenRocket against rockets-engine. On the A8 test triangle: 409.6 against 390.1 m, minus 4.8 percent. D21: 697.0 against 678.1 m, minus 2.7 percent. G64: 1,112.1 against 1,085.7 m, minus 2.4 percent. Beta two-stage: 845.3 against 831.6 m, minus 1.6 percent." loading="lazy" style="max-width: 100%;"&gt;&lt;/a&gt;
&lt;figcaption&gt;&lt;em&gt;Apogee on OpenRocket's Alpha III fixture. All four flights land within 5%, and rockets-engine is consistently a little low.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flight&lt;/th&gt;
&lt;th&gt;Regime&lt;/th&gt;
&lt;th&gt;OpenRocket 24.12&lt;/th&gt;
&lt;th&gt;rockets-engine&lt;/th&gt;
&lt;th&gt;Difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Alpha III, "A8" test triangle (9 N·s)&lt;/td&gt;
&lt;td&gt;subsonic, Mach 0.42&lt;/td&gt;
&lt;td&gt;409.6 m&lt;/td&gt;
&lt;td&gt;390.1 m&lt;/td&gt;
&lt;td&gt;−4.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alpha III, "D21" test triangle (32 N·s)&lt;/td&gt;
&lt;td&gt;transonic, Mach 0.85&lt;/td&gt;
&lt;td&gt;697.0 m&lt;/td&gt;
&lt;td&gt;678.1 m&lt;/td&gt;
&lt;td&gt;−2.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alpha III, synthetic G64 (128 N·s)&lt;/td&gt;
&lt;td&gt;supersonic, Mach 1.60&lt;/td&gt;
&lt;td&gt;1,112.1 m&lt;/td&gt;
&lt;td&gt;1,085.7 m&lt;/td&gt;
&lt;td&gt;−2.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Beta, two stages (B4 booster, D21)&lt;/td&gt;
&lt;td&gt;staging&lt;/td&gt;
&lt;td&gt;845.3 m&lt;/td&gt;
&lt;td&gt;831.6 m&lt;/td&gt;
&lt;td&gt;−1.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two things about this table are easy to misread. OpenRocket's test motors are synthetic triangles, not the real motors their names suggest: its "A8" delivers 9 N·s, about four times a real Estes A8, which is why an Alpha III reaches 390 m on it. And the Beta flight feeds both sides OpenRocket's own drag curve, so it checks staging, not aerodynamics.&lt;/p&gt;
&lt;p&gt;The more interesting story is how the subsonic number got there. Early on, the A8 flight agreed with OpenRocket to within 8%, then to within 0.6%. Then I added the G64, a motor that exists only to push the Alpha III past Mach 1, and compared the drag breakdown at matched Mach numbers term by term. It found three errors. OpenRocket computes fin form drag per fin and multiplies by the fin count; I had one fin's worth. For an ogive nose it takes the surface angle at 99% of the nose length, where an ogive is nearly parallel to the body; I had used the angle of a straight line from tip to base, charging the nose wave drag it does not have. And I had misread the exponents in the supersonic leading-edge correction. With all three fixed, the subsonic flight moved from +0.6% to −4.8%. The earlier, better agreement had come partly from errors cancelling. An earlier bug of the same kind, caught by the coefficient tests rather than the flights, had counted every fin's normal force twice: the Diederich formula gives the slope for a pair of fins, and a three-fin rocket is one and a half pairs, not three.&lt;/p&gt;
&lt;p&gt;I would rather publish a −4.8% that I can explain than a +0.6% that I cannot.&lt;/p&gt;
&lt;h3&gt;Real motors, real delays&lt;/h3&gt;
&lt;p&gt;OpenRocket checks the engine against another simulator. The physical world gets a vote too, and Estes publishes one in a form a simulator can be checked against without a single flight: the delay charge.&lt;/p&gt;
&lt;p&gt;A black powder motor burns its propellant, then a slow-burning delay grain, then pops an ejection charge to deploy the recovery system. The delay is chosen so the charge fires near apogee, when the rocket is moving slowest. For the Alpha III, Estes's recommended motors include the &lt;a href="https://estesrockets.com/products/alpha-iii"&gt;A8-3, B6-4 and C6-5&lt;/a&gt;: three, four and five seconds of delay. If the simulator's coast from burnout to apogee comes out near those numbers on the real thrust curves, it is getting the drag and the mass right in the regime that matters.&lt;/p&gt;
&lt;figure style="margin: 2rem 0; text-align: center;"&gt;
&lt;a href="https://tinycomputers.io/images/rockets-engine/alpha-iii-estes.png"&gt;&lt;img src="https://tinycomputers.io/images/rockets-engine/alpha-iii-estes.png.webp" alt="Altitude against time for the Alpha III airframe on Estes A8, B6 and C6 curves, peaking at 84, 208 and 387 metres. Markers show burnout, apogee and the moment each motor's ejection charge would fire, which in every case is about half a second before apogee." loading="lazy" style="max-width: 100%;"&gt;&lt;/a&gt;
&lt;figcaption&gt;&lt;em&gt;The OpenRocket parity airframe on three real Estes curves. The X marks where each motor's printed delay would fire the ejection charge.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Motor&lt;/th&gt;
&lt;th&gt;Liftoff mass&lt;/th&gt;
&lt;th&gt;Burnout&lt;/th&gt;
&lt;th&gt;Max speed&lt;/th&gt;
&lt;th&gt;Apogee&lt;/th&gt;
&lt;th&gt;Coast to apogee&lt;/th&gt;
&lt;th&gt;Estes delay&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A8&lt;/td&gt;
&lt;td&gt;38.6 g&lt;/td&gt;
&lt;td&gt;0.54 s&lt;/td&gt;
&lt;td&gt;50 m/s&lt;/td&gt;
&lt;td&gt;84 m at 4.02 s&lt;/td&gt;
&lt;td&gt;3.48 s&lt;/td&gt;
&lt;td&gt;3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B6&lt;/td&gt;
&lt;td&gt;40.3 g&lt;/td&gt;
&lt;td&gt;0.86 s&lt;/td&gt;
&lt;td&gt;93 m/s&lt;/td&gt;
&lt;td&gt;208 m at 5.64 s&lt;/td&gt;
&lt;td&gt;4.78 s&lt;/td&gt;
&lt;td&gt;4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C6&lt;/td&gt;
&lt;td&gt;47.8 g&lt;/td&gt;
&lt;td&gt;1.86 s&lt;/td&gt;
&lt;td&gt;131 m/s&lt;/td&gt;
&lt;td&gt;387 m at 7.40 s&lt;/td&gt;
&lt;td&gt;5.54 s&lt;/td&gt;
&lt;td&gt;5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;All three coasts come out about half a second longer than the delay Estes chose, so in all three the charge fires just before the top, which is where you want it: a hair early is a little speed on the shock cord, a hair late is a rocket already falling. The built-in A8 and B6 curves are the zero-delay booster versions. A delay grain adds a few grams to the motor without changing its thrust curve, so the real A8-3 and B6-4 flights would be a touch lower and slower than these.&lt;/p&gt;
&lt;p&gt;The C6 apogee, 387 m, is about 10% above the 351 m (1,150 ft) Estes projects for the kit. That is the direction it should be: the parity airframe has no launch lug and smooth, rounded fins, where a kit built on a kitchen table has a lug, square fin edges and a coat of paint, and every one of those adds drag.&lt;/p&gt;
&lt;h3&gt;Wind, parachutes and a few thousand flights&lt;/h3&gt;
&lt;p&gt;A model rocket's flight is mostly the part after apogee. &lt;code&gt;RecoveryForces&lt;/code&gt; wraps any force model and swaps in parachute drag once the rocket is descending. The terminal velocity is never written down anywhere in the code; it has to emerge. For a 120 g rocket under a parachute with 0.25 m² of drag area, √(2mg/ρC_dA) at sea level is 2.77 m/s, and the simulated rocket lands at 2.77 m/s.&lt;/p&gt;
&lt;p&gt;The deployment rule shows the kind of bug that dominated the week. The first version deployed the chute when vertical velocity was less than or equal to zero, which is true of a rocket sitting on the pad. With no wind, nothing happened. With a standing wind, the open parachute dragged the rocket sideways before it lifted off, and a uniform wind and an identical wind zone flew differently. The gate is now strictly descending. The weathercock model, which turns the rocket's axis into the relative wind, had the same problem from the other direction: a standing wind tipped the rocket over on the rail, where a real launch rod holds it upright. The axis is now pinned until it clears the rail.&lt;/p&gt;
&lt;p&gt;Wind itself comes from &lt;a href="https://github.com/ajokela/winds-engine"&gt;winds-engine&lt;/a&gt;, a sibling crate: a uniform vector, wind zones by downrange distance, vertical profiles, a radiosonde sounding, a GRIB2 forecast grid, or terrain-adjusted wind over a digital elevation model. Turbulence is a frozen field rather than a random process: eight sine waves per axis with log-spaced frequencies and amplitudes falling with frequency, a pink spectrum scaled to the RMS you ask for, with phases from a seeded generator. Because the field is a pure function of position and time, the four RK4 stages, a saved and restored flight, and a replay with the same seed all see exactly the same gusts. Every force reads the same air: drag, the parachute, the weathercock and body lift all sample one shared atmosphere, which was itself a fix. Atmosphere zones originally reached only the drag kernel while the other forces quietly built their own standard day.&lt;/p&gt;
&lt;figure style="margin: 2rem 0; text-align: center;"&gt;
&lt;a href="https://tinycomputers.io/images/rockets-engine/wind-drift.png"&gt;&lt;img src="https://tinycomputers.io/images/rockets-engine/wind-drift.png.webp" alt="Side view of four flights of a D12 sport rocket to about 490 metres under a parachute. In calm air it lands at the pad. In a steady 4 m/s wind it drifts 712 metres. In wind rising from 1 to 7 m/s with height it drifts 766 metres. With gusts added it drifts 734 metres along a wavier path." loading="lazy" style="max-width: 100%;"&gt;&lt;/a&gt;
&lt;figcaption&gt;&lt;em&gt;A D12 in an illustrative 24 mm airframe (120 g, drag coefficient 0.4). The ascent takes ten seconds and the descent three minutes, so the descent decides where it lands.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The &lt;code&gt;dispersion&lt;/code&gt; command flies the same rocket hundreds of times, perturbing the drag scale (lognormal, 3%), the wind (1 m/s per axis) and the launch speed (1%), and reports apogee, landing range and the circular error probable, the radius that contains half the landings. It has its own tiny SplitMix64 generator rather than a dependency, so the same seed reproduces the same set of flights.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;500 runs, seed 42&lt;/th&gt;
&lt;th&gt;Apogee&lt;/th&gt;
&lt;th&gt;Landing range&lt;/th&gt;
&lt;th&gt;CEP&lt;/th&gt;
&lt;th&gt;Still aloft after an hour&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Calm&lt;/td&gt;
&lt;td&gt;489.5 m (sd 6.3)&lt;/td&gt;
&lt;td&gt;326 m (sd 562)&lt;/td&gt;
&lt;td&gt;232 m&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steady 4 m/s&lt;/td&gt;
&lt;td&gt;489.2 m (sd 6.6)&lt;/td&gt;
&lt;td&gt;962 m (sd 981)&lt;/td&gt;
&lt;td&gt;383 m&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steady 4 m/s plus gusts&lt;/td&gt;
&lt;td&gt;488.4 m (sd 10.3)&lt;/td&gt;
&lt;td&gt;853 m (sd 456)&lt;/td&gt;
&lt;td&gt;327 m&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;16.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The apogee statistics are tight and well behaved. The landing statistics carry a lesson about what a perturbation means. The 1 m/s wind uncertainty applies to all three axes, vertical included, and it is held for the whole flight. A rocket descending at 2.8 m/s under its parachute, given a steady updraft of comparable size, comes down very slowly or not at all: two runs in five hundred were still in the air an hour after launch, and the few that hung for many minutes drifted far enough to dominate the standard deviation of the landing range. A real updraft is a thermal, not a property of the entire sky. For landing-zone estimates the vertical term deserves its own, much smaller sigma.&lt;/p&gt;
&lt;h3&gt;The same commands in a browser&lt;/h3&gt;
&lt;p&gt;The command-line tool, &lt;code&gt;rockets&lt;/code&gt;, has three subcommands: &lt;code&gt;simulate&lt;/code&gt; prints a trajectory as a table, CSV or JSON with an apogee and impact summary; &lt;code&gt;dispersion&lt;/code&gt; runs the Monte Carlo; &lt;code&gt;solve-range&lt;/code&gt; finds the launch elevation that lands at a given distance. All of its options are parsed by one module, and that module compiles to WebAssembly unchanged. The browser build at &lt;a href="https://rockets.rs/sh/"&gt;rockets.rs/sh&lt;/a&gt; is a 39-line binding over the same parser, so the native tool and the web page accept exactly the same commands. The module is 836 KB, about 285 KB compressed. The page has a dozen example buttons, deep links that carry a command in the URL, and &lt;code&gt;--plot&lt;/code&gt; draws an interactive altitude chart by re-running the command with &lt;code&gt;--format json&lt;/code&gt;. Options that read files or terrain models are native-only and say so.&lt;/p&gt;
&lt;p&gt;One of the examples flies an Alpha III modified with a 24 mm mount on an Estes D12, which no stock Alpha can take: 66.6 g at liftoff, 591 m at apogee.&lt;/p&gt;
&lt;h3&gt;What it doesn't do&lt;/h3&gt;
&lt;p&gt;rockets-engine is a point-mass simulator with a few rungs added above point mass. The thrust axis is commanded, not earned: the weathercock model is a reduced-order second-order system relaxing the axis toward the relative wind, roll is a single scalar channel, and body lift is quasi-static, computed from the angle between the commanded axis and the relative wind rather than from an integrated attitude. There is no inertia tensor, no quaternion and no gyroscopic coupling. It is fixed-step RK4, not an adaptive solver, so a new vehicle model needs a convergence check, and the documentation says so. The standard atmosphere it borrows is defined to about 80 km; above that, an &lt;code&gt;AltitudeSwitch&lt;/code&gt; hands the flight to a vacuum model. The flat frame is wrong at intercontinental ranges, the central-gravity frame has no air, and Earth rotation is the caller's problem in an inertial frame.&lt;/p&gt;
&lt;p&gt;The Barrowman modeler is deliberately narrower than OpenRocket: conical, ogive, elliptical and parabolic noses; trapezoidal, elliptical and free-form fins; conical boat-tails; no launch lugs, shoulders or multi-diameter bodies. It is the subset I could check against OpenRocket's own numbers.&lt;/p&gt;
&lt;h3&gt;Nine days of it&lt;/h3&gt;
&lt;p&gt;The crate is 8,327 lines of Rust in the library, 3,014 in tests and 487 in six examples. Its heaviest file is the command-line module at 2,654 lines, because the command surface is where every feature has to agree with every other feature. The commit history reads like a list of the places the physics was fine and the bookkeeping was not.&lt;/p&gt;
&lt;figure style="margin: 2rem 0; text-align: center;"&gt;
&lt;a href="https://tinycomputers.io/images/rockets-engine/commits.png"&gt;&lt;img src="https://tinycomputers.io/images/rockets-engine/commits.png.webp" alt="Bar chart of commits per day from October 23 to October 30: 9, 15, 22, 7, 0, 0, 2 and 4." loading="lazy" style="max-width: 100%;"&gt;&lt;/a&gt;
&lt;figcaption&gt;&lt;em&gt;Most of the crate arrived in the first four days; the last three were dependency updates and bug fixes.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The first day produced the integrator, staging, central gravity, the command line and the browser terminal. The second added motor files, recovery and the geometry modeler, and put OpenRocket into CI. The third took the modeler through the transonic and supersonic regimes and brought in winds-engine. On the 30th, a fix for a division of zero by zero at apogee in the body-lift model, where the angle between the axis and the wind passes through exactly 180 degrees and the guard written for zero degrees did not catch it. Writing this post turned up two more, both fixed in 0.20.5 on the 31st. Nothing holds a rocket on the pad, so while thrust climbs past its weight it sags a few millimetres below the ground, and &lt;code&gt;dispersion&lt;/code&gt; took that sag for a landing and reported zeros. And the weathercock took a single explicit step of a stiff oscillator per output step, so at the default settings it flew the Alpha III to 213 m in a 4 m/s wind instead of 371; it now integrates the axis in substeps and agrees with itself to half a percent.&lt;/p&gt;
&lt;p&gt;In &lt;a href="https://tinycomputers.io/posts/the-bullet-doesnt-go-where-you-point-it.html"&gt;The Bullet Doesn't Go Where You Point It&lt;/a&gt; I wrote that almost none of the ballistics engine's memorable bugs were physics bugs. The rocket engine repeated the lesson with a twist. Its physics bugs were found by comparison, against an exact solution, against OpenRocket, against a delay grain Estes chose decades ago. Its other bugs lived at the edges of a flight: on the pad before liftoff, and at the instant of apogee, where velocities and angles pass through the values that formulas written for the middle of a flight never see.&lt;/p&gt;
&lt;p&gt;rockets-engine is on &lt;a href="https://crates.io/crates/rockets-engine"&gt;crates.io&lt;/a&gt; with documentation on &lt;a href="https://docs.rs/rockets-engine"&gt;docs.rs&lt;/a&gt;, the source is on &lt;a href="https://github.com/ajokela/rockets-engine"&gt;GitHub&lt;/a&gt;, and every command in this post runs at &lt;a href="https://rockets.rs/sh/"&gt;rockets.rs/sh&lt;/a&gt;. The rest of the ballistics work is collected, in order, on the &lt;a href="https://tinycomputers.io/pages/ballistics.html"&gt;ballistics page&lt;/a&gt;.&lt;/p&gt;</description><category>ballistics</category><category>model rockets</category><category>numerical methods</category><category>openrocket</category><category>rk4</category><category>rocketry</category><category>rockets-engine</category><category>rust</category><category>simulation</category><category>webassembly</category><guid>https://tinycomputers.io/posts/rockets-engine-giving-the-ballistics-engine-a-motor.html</guid><pubDate>Sun, 01 Nov 2026 01:00:00 GMT</pubDate></item></channel></rss>