Skip to content

robotrocketscience/trajectory-bot

Repository files navigation

TrajectoryBot — differentiable-simulation RL for orbital maneuvers

A from-scratch orbital-dynamics stack (no Basilisk, no poliastro) and a policy-gradient agent that learns spacecraft maneuvers by backpropagating through the physics, benchmarked against analytic transfers — with every claim re-verified at high fidelity before it's stated.

policy flying a circularization maneuver

The agent is a small MLP (multi-layer perceptron) at the decision layer only: every 200 s it emits a thrust direction (in the orbit frame) and a throttle; a deterministic pointing controller slews the spacecraft and a quaternion rigid-body integrator does the physics. The policy never touches attitude directly — burn planning is learned, burn execution is classical GNC (guidance, navigation, and control).

flowchart LR
    O[observations<br/>r, v, elements, fuel] --> P[policy MLP<br/>13 → 128 → 128 → 4]
    P -->|"direction (orbit frame) + throttle"| C[pointing controller<br/>rate command]
    C --> D[quaternion rigid-body dynamics<br/>RK4, dt = 10 s]
    D -->|"1200+ substeps, fully differentiable (JAX)"| L[loss: terminal orbit error + Δv<br/>+ Δv-to-go potential shaping]
    L -.->|"∂loss/∂θ through the whole rollout"| P
Loading

The whole policy is a tiny 13 → 128 → 128 → 4 tanh MLP, trained end-to-end through the physics. Its weights are three unremarkable matrices; what matters is the control law they encode. Reading the trained policy back out — sampling its commanded thrust around an orbit, and measuring which inputs move the throttle — shows it rediscovered the textbook maneuver on its own: coast, then burn prograde at apoapsis, keyed almost entirely on the radius error — never told to.

left: the policy's commanded thrust around an eccentric orbit — coasting most of the way, then a prograde burn at apoapsis; right: input attribution bars showing the throttle keys mostly on radius error

Verified results

verified scoreboard

Claim Number How it's verified
Success (5% tolerance terminal set, 3D) 92.3% fresh 4096-episode set, never in-run telemetry
Fuel vs the impulsive analytic optimum 1.03× median (best-fuel policy, ~92% success) float64 re-flight at dt = 1 s, clean episodes only, closed-form baseline (scripts/verify_probe.py)
Oracle it improves on scripted analytic expert imitated at 79.9% same fresh set
Sim gaming ruled out 0 of 4096 episodes beat the admissible tolerance-box bound same probe, both baselines

Honest framing: the agent matches the analytic transfer's fuel cost while running the full closed-loop attitude + finite-burn pipeline; it does not yet beat it. The tolerance box admits solutions up to ~15% cheaper than exact circularization — that quantified headroom is the open research target, and the verification harness exists precisely so any future "beats the baseline" claim survives scrutiny (integrator-energy audit, float64 re-flight, clamp-region exclusion, per-geometry closed-form baselines).

Scope: the headline policy above circularizes at its own apoapsis (target radius = initial apoapsis). Generalizing across commanded target radii — a degeneracy where that specialist collapses out-of-distribution (~18% success once the target leaves its trained radius) — is solved separately. A target-conditioned scripted expert (a two-apse tangential controller that drives both apses to the commanded radius) DAgger'd into the same 13→128→128→4 network reaches ~99% success across commanded radii spanning ±15% of apoapsis on fresh 4096-episode sets. The cause was not network capacity or optimization but the imitation source — the specialist lineage was cloned from a fixed-target expert and had never imitated tracking. See scripts/dagger_target_jax.py.

one episode: path and orbital elements

The final policy solves the distribution, not one lucky episode. Forty random start orbits, each flown to termination, coloured by whether the finish landed inside the 5% tolerance box:

40 random start orbits flown by the final policy, solved vs missed

Why differentiable simulation

Model-free RL was given every chance and walled out; exploiting the known, exactly-differentiable dynamics wins by an order of magnitude (2-D milestone, identical env):

Method Success Δv vs optimal
Scripted analytic expert 100% 1.00
Cold PPO (model-free, 8 runs) 0–3%
Behavior cloning ~16% ~1.37
DAgger ~45% ~1.5
Diff-sim policy gradient 81% 1.26

In 3-D, differentiable-sim refinement then climbs past the imitation oracle it started from — each point is the fresh 4096-episode success of a checkpoint along the campaign:

fresh-set success climbing from the imitation oracle across the campaign

The 3-D stack ports the hot path to JAX/XLA (scripts/jaxsim.py): lax.scan + jit(value_and_grad) over the full 1200-substep rollout, numerically exact against the torch reference and ~50× faster on a consumer GPU — which is what made the research loop below possible.

The part that was actually hard

Backprop-through-physics gradients are heavy-tailed: one episode in a few hundred carries a gradient norm of 1e12–1e19 (finite, not NaN) and a single Adam step can erase 12 points of success. Getting from "imitates the oracle at 80%" to "92% and stable" was optimizer forensics, run as pre-registered hypothesis-refutation rounds (30+, logged verbatim):

same seed, four aggregation strategies

Same start checkpoint, same seed — only the gradient aggregation differs.

And here is what that refinement buys on a single episode — the same start orbit flown by four training stages, imitation → refined, cycling so you can watch the path tighten onto the target ring:

the same start orbit flown at four training stages

The playbook that survived: measure the per-episode norm distribution, delete the monster tail (trimmed mean), clip survivors at the measured p90, bank progress with an EMA (exponential moving average) policy, polish at low lr. The failure modes en route (truncation bias in short-horizon backpropagation through time, absorbing-state mismatches, potential-shaping traps that pay for zero progress, a normalize-epsilon that seeded 1e6 gradients at every coast decision) are documented in docs/EXPERIMENTS_3D.md (3-D campaign) and docs/EXPERIMENTS.md (2-D milestone).

Beyond circularize: the research frontier

The same backprop-through-physics approach extends to multi-body dynamics and to maneuvers where a fidelity term the textbook ignores changes the optimal strategy. Two recent threads (full methodology and verdicts in docs/ROADMAP.md):

Ballistic capture at the Moon (CR3BP). A hand-rolled differentiable circular-restricted three-body engine (rotating Earth–Moon frame, Jacobi constant conserved to ~1e-7), the stable manifold of an L2 Lyapunov orbit computed from its monodromy matrix (symplectic, det = 1), and a manifold-seeded transfer that arrives captured at the Moon: Moon-relative energy < 0, staying bound for 4.6 lunar revolutions (~26 days, verified at dt = 5e-5) with no capture burn, where a naive departure-burn search finds none.

L2 stable manifold funnelling onto the Moon and a ballistic capture arc into a bound lunar orbit
the capture arc animated into its bound lunar revolutions

Rediscovering J2 nodal-drift phasing. For a RAAN (orbital-plane) change, the textbook impulsive plane change is J2-blind — but J2 oblateness precesses the node for free, faster at lower altitude. A diff-sim policy minimizing true Δv over J2-on dynamics, with no maneuver structure baked in, rediscovers the operational phasing technique: dive to a lower altitude, let the node drift faster, return. It reaches the target node for ~0.88× the Δv of naive passive waiting at 30°, falling to a quarter–half at 60–90° (the bigger the node change, the more the dive pays) — and lands within ~10–15% of an analytic optimization of that same dive-drift strategy (which is slightly cheaper still). So this is autonomous rediscovery of a known technique from the raw objective, not a novel beat: the same genre as the agent rediscovering the raise-to-plane-change trick elsewhere in the project.

the policy dives to speed J2 nodal drift, reaches the target node passive falls short of, and matches the analytic dive optimum well under passive

Both results come with their deflations attached: the capture is in the idealized CR3BP (not real JPL ephemerides yet); the J2 result is a rediscovery that an analytic dive beats, and its low-thrust Δv is idealized (impulsive-per-step). The regimes where the savings vanish (small node angles, budgets so long that passive is free) are recorded per build.

Gravity assists in the real solar system

The circularize and J2 experiments run in a two-body field. The next tier flies the spacecraft through the actual solar system: a JAX rollout under many point-mass bodies whose positions come from JPL Horizons, differentiable end to end, so ∂(miss distance)/∂(maneuver) drops out of the same backprop. The engine is checked offline before any claim is made — the two-body Kepler limit closes to |Δr|/a = 9e-13, energy holds to 7e-15, and from a cold Δv = 0 start the optimizer recovers the analytic Lambert transfer to 1.2e-11 relative. Bodies are held fixed within each RK4 step, which is accurate at heliocentric scale but not for close geocentric orbits (Earth moves ~9000 km per step), so those use substep interpolation.

a 1:2 resonant leg launched from the real Earth, flown under the Sun and real perturbers, returning near Earth

Inclination is free up to a ceiling. The reachable inclination from gravity assists alone is arcsin(v∞/v_P) — set by the hyperbolic excess speed v∞ and the planet's orbital speed v_P, and independent of the planet's mass. A sequence of same-body flybys (the crank) walks inclination up to that ceiling at fixed v∞; a heavier or closer planet bends the velocity more per pass, so it needs fewer flybys (Jupiter one, Venus about five — the diff-sim Venus tour reaches 32.8° against a 32.9° ceiling, the same shape as Solar Orbiter's ~7–8 Venus assists to ~33°). The mission-design reading is v∞ ≥ v_P·sin(i): a polar orbit needs v∞ at least the planet's orbital speed. Given only a target inclination and a Δv-minimizing objective — never told about leverage or cranking, seeded at zero leverage — the backprop optimizer discovers the textbook plan: crank when the target sits under the ceiling, and pump v∞ first (V∞-leveraging) when it sits above, spending within 1% of the analytic budget. Reaching the inclination that way costs 2–12× less Δv than a direct plane change.

the arcsin(v-inf / v-P) ceiling with the crank climbing to it, and per-planet flyby counts

The leverage is real, and the real Earth caps it. A small burn at apoapsis changes v∞ at the next Earth encounter by far more than the burn itself — leverage L ≈ 15–37 measured against the real ephemeris. But the same leverage that turns a 5 m/s burn into ~75 m/s of v∞ also throws the return off the real Earth, and holding the re-encounter inside Earth's sphere of influence caps the pump near 0.085 km/s of v∞ per leg. A single-planet staircase creeps from v∞ 8 to ~9.7 over ~18 legs and then stalls. Four rounds pinned down why, each correcting the one before: the pump does survive real ephemeris per leg (an earlier "it's dead" reading had used a burn ~20× too large); the cap is not a quirk of one resonance (every resonance gives the same ~0.1 km/s/leg); it is a one-control limit (a single apoapsis burn can't both pump v∞ and re-aim the encounter); and the obvious second control — bending the flyby — turns out to be mis-timed, because it sets the outgoing orbit before the burn de-phases the return, so it can't fix it. The correctly-timed correction acts after the burn, and it does break the cap — but only by trading Δv one-for-one, no longer leveraged. The cheap escape is a second planet: inner planets pump faster (shorter years, more encounters), and the handoff between planets rides a conserved quantity (the Tisserand parameter), so it costs nothing.

left: a few m/s of apoapsis burn moves v-inf by ~100 m/s; right: pumping harder throws the re-encounter past Earth's SOI, capping the per-leg pump

Making discovery work on the real ephemeris took a reformulation. The obvious differentiable setup — solve each leg as a boundary-value problem (Lambert) and penalize flyby mismatch — fails in an instructive way: the resonant-return legs a deep tour needs sit at near-singularities of the boundary-value problem, gradients there spike past 1e18, and the optimizer climbs into them, reporting huge v∞ while quietly violating the flyby physics. The fix is structural, not a tuning knob: propagate each leg forward with a differentiable Kepler propagator (no boundary-value solve anywhere), make the flyby a bounded rotation of v∞ (so |v∞| is conserved exactly — there is no constraint left to cheat), and close each encounter with the real moving planet by Gauss-Newton inside the differentiable loop. Encounters then close to meters against the real ephemeris, and gradients flow end to end.

The tour that architecture finds. From a 5.95 km/s launch, tree search over the closed continuations reaches v∞ = 16.3 km/s in four pumping flybys of Venus and Earth — every encounter hit ballistically, with no deterministic maneuvers at all — and then, once the pump saturates, five more Venus flybys crank the inclination from 1° to 27.1°, which is 97% of the arcsin(v∞/v_P) ceiling. The crank pays a tax the idealized theory misses: each flyby must also re-hit the real moving planet, which pins about half of every turn, so the walk takes ~5 encounters instead of the ideal ~2 — slower, but the ceiling is still reached. And it is not a lucky date: across eight launch epochs spanning two synodic cycles, five pump to 15–18 km/s (median 15.3), two have no viable cheap launch at all, and one launches fine but never climbs — the failure modes are part of the result.

the flown tour: top-down heliocentric view of the pump spiral with legs colored by v-inf, and edge-on view of the crank tilting the orbit out of the ecliptic to 27 degrees

left: the discovered pump chain, v-inf 5.95 to 16.27 over six flybys; middle: the inclination crank walking to 97 percent of the analytic ceiling; right: final v-inf across eight launch epochs

All of this is patched-conic, and it is a study of mechanism, not a Δv record: the honest headline is that a gravity-assist plane change is cheaper across the board and order-of-magnitude cheaper only at favorable leverage, and that an agent differentiating through the physics finds the strategy on its own. The full pre-registered method and verdicts — including the rounds that overturned earlier conclusions — are in docs/ROADMAP.md.

Quickstart

uv sync --extra dev            # Python 3.12+, from pyproject/uv.lock
uv run pytest                  # physics, baselines, envs, solvability
uv run pyright                 # strict typing on the shipped library

# 2-D milestone (torch)
uv run python scripts/train_circularize2d.py

# 3-D JAX training run (GPU; CPU works, slower)
uv run --with "jax[cuda12]" python scripts/jaxsim.py \
  --init models/dagger_jax.npz --iters 3000 --chunk 60 --lr 1e-4 \
  --absorb --phi-dv --d-eps 1e-4 --trim-ep 16 --clip-ep 20000 --ema 0.995

# verify a fuel claim at high fidelity (float64, dt=1 s, both baselines)
uv run --with jax python scripts/verify_probe.py models/<ckpt>.npz

# regenerate the README figures
uv run --with jax --with matplotlib --with pillow python scripts/viz_readme.py models/<ckpt>.npz <logdir>
uv run --with jax --with matplotlib --with pillow python scripts/viz_readme_extra.py  # progression / weights / generalization / learning gif
uv run --with jax --with matplotlib --with pillow python scripts/viz_tier3.py          # CR3BP capture + J2-beat frontier (no checkpoint needed)
uv run --with jax --with matplotlib --with pillow python scripts/viz_lowthrust.py # low-thrust spiral (flies the E1 lt_2e-4 specialist)
uv run --with jax --with astroquery --with astropy --with matplotlib python scripts/viz_campaign.py  # N-body ephemeris tour + inclination ceiling + leverage cap (needs .ephem_cache)

Checkpoints and run logs are not committed (they regenerate by training); the circularize-lineage checkpoints behind the figures are published as a GitHub release. docs/media/ holds the small rendered figures only.

Repository map

Path What it is
tbot/ The shipped library: quaternions, 2-D/3-D dynamics, orbital elements, attitude controller, Gymnasium envs. pyright-strict.
scripts/jaxsim.py JAX/XLA 3-D diff-sim trainer (the research workhorse) with the full knob set.
scripts/verify_probe.py float64/dt=1 s claim-verification harness (exact + tolerance-box baselines).
scripts/eval_probe.py, norm_probe.py, step_probe.py, … The measurement toolkit the optimizer forensics ran on.
ephemeris.py, scripts/nbody_sim.py, scripts/full_ephemeris_tour.py, leverage_anatomy.py, resonance_hopping.py, flyby_leverage.py The differentiable N-body / JPL-ephemeris engine and the gravity-assist study (inclination ceiling, v∞-leverage, the SOI rate cap). Each --verify runs offline against the cached ephemeris.
docs/EXPERIMENTS_3D.md, docs/EXPERIMENTS.md The optimizer-forensics campaign (3-D) and the 2-D milestone methodology.
docs/ROADMAP.md, docs/AUDIT.md Target maneuvers, the full gravity-assist campaign log, and the audit of the 2021 code.
docs/GRAVITY_ASSIST.md Reader's digest of the differentiable N-body / gravity-assist arc (the inclination ceiling, leverage, and SOI rate cap).
v2/, archive/ The 2021 course project, kept as the "before" picture (excluded from CI/typing).

Roadmap

Circularize (3-D, done) → plane change (done) → low-thrust Edelbaum spiral (done) → ballistic lunar capture in the CR3BP (done) → the differentiable N-body engine on real JPL Horizons ephemerides, and the gravity-assist inclination/leverage study on top of it (done, above) → the correctly-timed leverage correction and a multi-planet tour (open) → benchmark against historically flown trajectories. Any N-body fuel comparison is tested across solar-system phases (syzygy effects are real physics but epoch-specific), and gravity-assist missions are treated as a test of whether the agent can discover an assist, not as a Δv target to beat.

Provenance

Started in 2021 as a graduate astrodynamics course project that trained but never converged — the original report and code are preserved in docs/final-report.md, v2/, and archive/, and the revival began by auditing why it failed (docs/AUDIT.md). Everything physics is hand-rolled on purpose: the point is to own the whole stack from the RK4 up.

MIT licensed. See CONTRIBUTING.md for how changes are evaluated (pre-registered experiments, verification-fidelity claims).

About

From-scratch orbital-dynamics sim + differentiable-simulation RL agent for spacecraft maneuvers, benchmarked against analytic transfers — with float64-verified claims.

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages