Philip Bein

Get in touch

Back to projects

Project write-up

OrbitSandbox

An interactive 3-D workbench for planning a spacecraft maneuver sequence — and then finding out how often that plan actually works.

Core
Python · NumPy · SciPy
API
FastAPI
Viewer
React · three.js · TypeScript
Editor
Monaco
Runs on
Docker, Raspberry Pi 5
The OrbitSandbox workbench: vehicle mass budget and three maneuver nodes on the
                 left, a 3-D Earth with the planned orbit, two gates and a success volume in the
                 centre, and a live telemetry column on the right.
The waypoint-chain scenario as it ships. Three gates, a 40 km success volume, and a plan that misses all three — deliberately.

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.

The script editor open beside the 3-D view, showing the waypoint-chain mission
                 file with syntax highlighting, line numbers and a valid badge.
The editor sits beside the trajectory it describes. Edit, validate, fly.

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.

The plan panel: vehicle mass budget, three maneuver nodes with delta-v
                   sliders, and three targets each marked MISS with the distance short.
The plan panel, before optimization.

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 Monte Carlo run in progress: the dispersion arrivals drawn as a band around
                 the orbit, with a results panel reporting probability of success, miss-distance
                 percentiles, delta-v consumed and per-gate hit rates.
Dispersing the shipped plan — the one that already misses on paper — over 500 runs. Zero reach the target volume, median miss 269 km. The panel reports percentiles rather than an average on purpose.
The results panel: probability of success, a Wilson confidence interval,
                   miss-distance percentiles, delta-v consumed and per-gate hit rates.
Percentiles, a Wilson interval, and how it failed.

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.

© 2026 Philip Bein