The one rule
The renderer never integrates anything. Every spacecraft position on screen came out of
propagator.py; the browser interpolates between
precomputed samples and draws them. That constraint is the whole architecture — the
moment a viewer starts doing its own physics, you have two sources of truth and no way
to tell which one is lying.
There is exactly one deliberate exception, and it barely counts: the viewer evaluates celestial-body positions itself from the closed-form circular ephemeris parameters the engine sends. Nothing is integrated — it is the same analytic expression the engine evaluates. Doing it client-side stops a 500-run Monte Carlo payload from carrying half a million redundant body positions.
The split falls out of the workload. SLSQP over a dozen variables where each objective evaluation is a full mission integration, thousands of dispersed trials, 6×6 covariance propagation — that is NumPy and SciPy work. three.js is a rendering library, and it is used as one. The FastAPI layer is deliberately thin: parse, call the engine, serialise. No simulation logic lives in it.
A mission is a text file
Scenarios are written in a small DSL that parses to exactly the same object the JSON API accepts, so the language adds no capability — it just removes punctuation and adds units. Errors carry a line number and surface in the editor as you type.
mission "Translunar Flyby"
system earth-moon
duration 7 d
vehicle
dry_mass 2200 kg
thrust 45 kN
isp 320 s
maneuver TLI
at 3688.5 s
frame vnb
dv [3171.03, -55.87, 77.51] m/s
target MoonFlyby
centre body Moon
sphere 5000 km
after TLI Units are mandatory. dv 3171 is an error, not a
guess — silent unit assumptions are how you lose a spacecraft.

Seven numbers, and the terms people drop
The state is position, velocity and mass — seven numbers. Mass lives in the state rather than as bookkeeping on the side, because thrust acceleration is F/m and m changes by tens of percent during a large burn. Treating mass as constant over a burn is the most common way to get Δv accounting subtly wrong.
Third bodies contribute two terms, not one. The frame is centred on the primary rather than the barycentre, so the Moon accelerates the Earth too, and that acceleration has to be subtracted back off. Dropping the indirect term is a classic bug worth hundreds of kilometres over a translunar coast — there is a test named after it specifically to catch it.
Oblateness is J₂ only, and the check that it is right is not that the orbit "looks precessing." The nodal regression rate has a closed form, and the test compares the propagated precession against that formula to within 2%. Wherever a closed-form answer exists, it is the test.
Finding the burns
A universal-variable Lambert solver and a porkchop sweep over departure date and flight time give you a starting guess. SLSQP then moves nine Δv components and three timing offsets until every gate is threaded.
In the shipped scenario it threads all three and spends less propellant than the plan it started from — 648 m/s against 676 — ending 18.8 km from the centre of a 40 km sphere. Comfortably inside. Solved.
The translunar case shows why the layers exist. The impulsive two-body answer proposes a 3,105 m/s injection; fly it as a real 284-second finite burn under n-body gravity and you miss the Moon by 247,000 km and fall back into the Earth. A good starting point and a bad answer, which is exactly what it is for.

Solved is not the same as safe
This is the layer the whole tool exists for. Take that solved plan, disperse the burns the way real burns disperse, and fly it 500 times. About 81% of runs succeed. The 95th-percentile miss is 56 km against a 40 km radius.
A plan that is exactly right on paper is one bad burn from the edge of the box, and no amount of staring at the nominal trajectory would have told you that.


A policy, not a sequence
Switch guidance from open loop to trim and the vehicle measures where it actually is twenty minutes after each burn, then flies a correction. Success goes to 100% — a 99.2–100% interval — and the 95th-percentile miss drops from 56 km to 20. It costs about 58 m/s.
That is the point of the whole exercise. One number measures the reliability of a frozen sequence of burns; the other measures the reliability of a policy. They are different questions, they have different answers, and the difference is worth about 60 m/s.
Behind it: ground stations, a state-transition matrix, process noise and an extended Kalman filter — because a correction is only as good as your estimate of where you are.
What it doesn't model
Saying this plainly matters more than listing what it does. There is no solar radiation pressure, which matters for high area-to-mass vehicles. The gravity field is J₂ and nothing else — no J₃, no tesserals, no lunar mascons. There are no attitude dynamics: the vehicle points where it is told, instantly, for free, which is not how attitude control works or what it costs.
The atmosphere is an exponential toy over a co-rotating gas. It is right to within a factor of a few at 200 km and wrong by orders of magnitude during a solar storm. It should not be used to predict a re-entry date, and the docs say so in those words.
Where it runs
A one-container Docker stack on a Raspberry Pi 5, brought up and down on demand from my desktop over SSH. It sits on the same machine as the rest of my self-hosted work, which is why there is no public link on this page — the screenshots above are the tour. The Monte Carlo run shown took 27.5 seconds of Pi compute for 500 dispersed trials.
If any of this is interesting to you, I would happily walk you through it live.
