Lior.
← All projects

Rocket Profile Analysis

A desktop app that picks the booster motor and staging delays for a two-stage rocket so it hits a target apogee without separating in the transonic band.

Year
2026
Role
Sole developer
Stack
Python, RASAero II, OpenRocket, .NET 8, C#, FastAPI, React, TypeScript, Tauri, GitHub Actions
Links
Code ↗Download ↗
Rocket Profile Analysis playing back a solved flight, eight seconds in, with the booster drawn separated behind the burning sustainer and the ascent traced toward the target band

Why

A two-stage rocket has three decisions that only make sense together: which booster to fly, how long to wait after burnout before separating, and how long after that to light the sustainer. Ignite early and the sustainer spends its impulse in dense air. Wait too long and gravity eats the coast. And separation has to stay out of the transonic band, roughly Mach 0.9 to 1.2, where the aerodynamics of two stages coming apart are the least predictable. My rocketry team was answering this in RASAero II by hand: edit a row, run, read the apogee, repeat, for every motor pair.

This app runs that loop. It takes the team’s OpenRocket model, its RASAero model and a folder of candidate motors, searches booster, separation delay and ignition delay against a target apogee, and checks every solution in RASAero’s own flight simulation before it reports it.

OpenRocket supplies the mass distribution and centers of gravity, asked headlessly through its Java runtime. RASAero’s file supplies geometry and the launch site. Motors are RASP .eng files, or openMotor designs straight out of my Rocket Optimizer, which get simulated once and cached.

Before any searching, one long-coast flight per booster reads off the stack’s peak Mach, Mach at burnout, and the times it falls back below 1.2 and 0.9. That decides which profile each booster can fly at all, subsonic or supersonic, and the window of separation delays worth trying, so the search never spends flights on a booster that cannot satisfy the rule.

Apogee is not monotonic in coast time. A longer coast lets the sustainer burn in thinner air until gravity losses win, so a candidate can cross the target twice. The search steps separation delay across its window, sweeps ignition delay at each step, finds every bracket where apogee crosses the target, and refines each with regula falsi. One pilot per motor pair flies the full grid; its neighbours fly only the points around the pilot’s crossings, which cut the team’s project from 136,000 flights to 47,600. Every solution is then re-flown with a full time history and checked against the profile rule, since a design that passed on the coarse grid is not a design until it passes there.

The trade-space tab: 1,270 designs plotted as apogee against Mach at separation, with the Mach 0.9 and 1.2 lines and the green target band; the solved designs sit where the cloud touches the band

Running RASAero without RASAero

RASAero II is a Windows program from the 2000s, written in VB.NET, with no API. The first version of this tool drove its GUI in a Windows virtual machine with screenshots and simulated clicks. One aero table took minutes and ten reference flights took tens of minutes, which made a search of thousands of flights impossible on the real thing.

So I wrote a small tool that rewrites the executable’s bytecode: it clears the 32-bit-only flag so a 64-bit runtime will load it, makes the obfuscated types public so a host can call them, swaps the message boxes and the “rocket was unstable” dialog for exceptions, and renames the result to a library. A .NET 8 host loads that library and speaks JSON lines over stdin and stdout, and the Python side runs one host per core. The numerics are never touched. Against RASAero’s GUI the apogees agree to 0.0003 percent, which is 0.2 feet over a 70,000-foot flight and the difference between x87 and IEEE float rounding. A flight takes about 20 milliseconds, and fourteen hosts fly 85 per second.

Before the native engine existed, the search ran on a 3-DOF gravity-turn integrator I wrote in Python on top of RASAero’s exported drag tables and atmosphere conventions, and a validate stage compared it term by term against reference exports. It’s still there as a backend, and the validate stage is still how I know the drag tables are current.

Making it fast

The team’s real project is 141 boosters against 10 sustainers. The first full run of it took 79 minutes; the same run now takes 19, and the engine is nearly all of it. Characterization went from 34 minutes to 41 seconds by returning each flight’s history as one block of doubles instead of a CSV written and read back per flight. Search batches moved from chunks pinned to a host to a free-host queue after I watched a finished chunk sit waiting on a busy host while others idled. One lock per host went in after two chunks on the same host lost each other’s replies and the run stalled for ten minutes. The mass table is cached by a hash of the OpenRocket file, so only new pairs go through the Java runtime.

The app

A React interface in a Tauri window, with the Python service frozen by PyInstaller and the patched engine bundled, so the team installs one app and OpenRocket. The seven steps run left to right, each with a Run button, the equivalent command line, and a status computed from the files on disk plus a manifest, so a step only goes stale when something it depends on has changed. Results are a sortable table, a booster-by-sustainer matrix, the trade-space scatter and a starred shortlist. Clicking a design draws the vehicle from the RASAero file, lays out the staging timeline, and flies it up the ascent chart. The shortlist exports as a single RASAero file with the designs as rows, so the final check happens in RASAero itself.

CI runs the test suite and a Playwright smoke test against the live service on macOS and Windows. A tag builds the DMG and the Windows installer, attaches both to a release, and running copies of the app offer the update.