ADR-086 — Earth→(planetary moon) multi-leg porkchops on /plan
Status · Accepted · 2026-08-28 · Closes #308's deferred "planetocentric moons of the giants" scope · builds on ADR-085 (geocentric Earth→Moon) · ADR-026/028 (the heliocentric porkchop set)
Gating sentence. /plan gains nine planetary moons as destinations — Jupiter's Galileans (Io, Europa, Ganymede, Callisto), Saturn's Titan + Enceladus, Neptune's Triton, and Mars' Phobos + Deimos — each shaded by the total ∆v of a multi-leg patched-conic mission: the heliocentric Earth→host-planet transfer's departure burn plus a moon-orbit insertion, where the host arrival v∞ is carried down the host's gravity well to the moon and captured (µ_host then µ_moon). The nine grids ride their host planet's departure/TOF axes; the heliocentric leg reuses solveLambert (µ_Sun) unchanged, so every existing planet/dwarf grid stays byte-identical.
Context
ADR-085 modelled the Moon as a single geocentric Lambert arc (Earth→Moon, µ_Earth) because the Moon orbits Earth and you launch from Earth. A moon of a giant planet is different: you reach it via its host planet, so the honest model is a chain, not one arc. #308 named "Moon + moons"; the moons were deferred in ADR-085 as "three-body sequencing." This closes that.
Decision
Two-leg patched-conic (src/lib/moon-transfer.ts, interplanetaryMoonDv):
- Heliocentric Earth→host — the existing porkchop leg (
solveLambert, µ_Sun). Departure ∆v =|v1 − v_Earth|. The spacecraft is not captured at the host; its hyperbolic arrival v∞ =|v_host − v2|is carried into leg 2 (no separate host-capture burn — that would double-count the energy that reaches the moon). - Moon-orbit insertion — fall down the host well from v∞ to the moon's orbital radius (
v_at_moon = √(v∞² + 2µ_host/r_moon)), take the excess relative to the moon (v∞_moon = |v_at_moon − v_moon_circ|, tangential approximation), capture into a low circular moon orbit (MOI = √(v∞_moon² + 2µ_moon/r_lo) − √(µ_moon/r_lo)).
Cell ∆v = departure + MOI. The /plan readout breaks it out as DEP x + MOI y, mirroring the Moon's TLI + LOI.
Data. Moon orbital radius is derived from its sidereal period (the value explore-scene.ts already ships for the 3D renderer) via Kepler's third law with µ_host — one source of truth, so this can't drift from the renderer. µ_host / µ_moon / mean radii are JPL/NASA reference constants (src/lib/moon-transfer.constants.ts). Low-orbit radius = mean radius + 100 km.
Type system. A new MoonMissionDestId (the 9 ids) joins PlanDestinationId alongside DestinationId and the geo 'moon'; isMoonMissionDest() gates the split readout. The moons are not heliocentric DestinationIds (no single Lambert arc) and not geocentric (not Earth-orbiting) — a third category.
Honesty bar (validation)
A 2D two-body teaching estimate, the giant-planet analogue of ADR-085 — it ships only if the numbers match reality:
- Kepler-derived orbital radii match JPL to ≤0.1% for all nine (Europa 670 965 vs 671 100 km).
- Arrival v∞ at Jupiter ~5.6 km/s and v∞ at Europa ~6.5 km/s match the Galileo / Europa-Clipper flyby band.
- Direct Europa EOI ~5 km/s matches the "direct moon capture is brutal at the giants" literature — this is the honest upper bound; real missions use gravity-assist tours to slash the moon leg, and the caption + credit say so.
- The Galilean gradient (Io > Europa > Ganymede > Callisto — deeper well = costlier) and the Mars-moon cheapness (Phobos/Deimos ~4.8 km/s, shallow well) fall out correctly.
Unit tests (moon-transfer.test.ts) assert the Kepler match, the Europa/Jupiter bands, the Galilean gradient, and a feasible positive departure+MOI split for every moon.
Consequences
- Positive — closes #308 fully; the nine moons reuse the existing porkchop UI (heatmap, magnifier, sandbox) with only a split-readout addition; no change to any heliocentric grid.
- Neutral / accepted — the moon grids inherit their host planet's feasibility profile exactly (the heliocentric leg is low-branch only, like the existing planet grids), so outer-moon porkchops (Triton 9%, matching Neptune) are mostly the fast side of the TOF band. This is consistent + honest, not a regression; a fuller outer-planet feasibility pass would be a change to all heliocentric grids, out of scope here.
- Accepted — direct-capture ∆v ignores gravity-assist tours; the caption frames it as the representative upper bound.
Alternatives considered
- Full 3-leg with an explicit host-orbit-insertion (JOI) then a Hohmann to the moon — rejected for v1: more realistic for a real orbiter but adds a mission-design choice (capture-orbit apoapsis) with little pedagogical gain over the direct-capture upper bound.
- Planetocentric-only leg (host→moon, ignoring the interplanetary journey) — rejected: it hides that the Earth→host transfer is the dominant cost, which is the whole point.
- Hard-coded moon orbital radii — rejected: derive from the shipped period so the table can't silently drift from the 3D scene.
0.8.1 physics-audit amendment (B1 / S1 / S2)
An independent physics review (2026-08-28) found three defects in the shared Lambert/porkchop conic that this ADR and ADR-085/026/028 all build on. All are fixed; the affected grids were recomputed. The model shape and honesty bar are unchanged — these are correctness fixes, not scope changes.
- B1 / S1 — arrival v∞ is a vector, not a scalar. Both the heliocentric
computeDv(S1) and this ADR'sinterplanetaryMoonDv(B1) priced the arrival hyperbolic excess as|v_dest − v2|. That scalar collapses on faster-than-Hohmann transfers — the speeds match while the velocity vectors diverge — under-pricing the fast half of every grid and inverting the TOF gradient (it bites hardest here, where v∞ is squared into the host well-drop).solveLambertnow returns the transfer's semi-latus rectump; callers split the endpoint velocity into tangential√(µp)/r+ radial and take the true vector v∞. (Fast Europa arrival v∞ went 0.24 → 13.4 km/s.) - S2 — perihelion at ϖ.
a0 + n·dayis the mean longitude L, but the conic used it directly as the true anomaly, pinning every body's perihelion at ecliptic longitude 0. The ephemerides (planets.json/small-bodies.json) now carry the longitude of perihelion ϖ (varpi); the conic phases by mean anomalyM = L − ϖ, solves Kepler for the eccentric anomaly, and places the body atν + ϖwithr = a(1 − e·cos E). Circular bodies (e = 0, ϖ inert) stay byte-identical, so only the eccentric-model grids (Pluto, Bennu) changed; Pluto's 2026 heliocentric distance now reads ~35.6 AU, not the mis-phased value. The/flyrender conic (mission-arc.destinationPos) shares the same fix, so the porkchop and the flight scene agree on where a body is.