Skip to content

ADR-026 — Multi-destination porkchop: data, semantics, and selectors

Status · Accepted Date · 2026-04-29 Closes · RFC-007 (Multi-destination porkchop) TA anchor · §components/lambert-worker · §components/plan-screen Related ADRs · ADR-008 (Lambert worker), ADR-016 (build-time assets), ADR-022 (Lambert protocol), ADR-023 (mobile magnifier)

Context

RFC-007 raised the question of extending /plan from a hardcoded Earth → Mars porkchop to "Earth → any planet" with optional landing/flyby semantics. The RFC proposed three interlocking sub-decisions: destination scope, mission-type semantics, and transfer-time range scaling. Five open questions sat under those: FLY-button experience for outer planets, build-time vs runtime grids, Y-axis units, C3-vs-tof axes, and whether Pluto/dwarf planets ship in v0.2.

This ADR locks all eight decisions in one document so the v0.1.6 implementation has a single contract to read.

Decision

Destination scope (RFC §what destinations to support — Option D-B)

Five destinations ship in v0.2 (= v0.1.6 milestone):

idHohmann transittof_range_daysmission_types
mercury~110 d[80, 250]LANDING, FLYBY
venus~150 d[80, 320]LANDING, FLYBY
mars~250 d[80, 520]LANDING, FLYBY
jupiter~860 d[400, 1500]FLYBY only
saturn~2200 d[800, 3000]FLYBY only

Mars defaults — no regression for the existing entry path.

Uranus, Neptune, Pluto, dwarf planets deferred to v0.3.0. Their multi-decade transfer windows + Lambert convergence concerns at extreme distances warrant the v0.2 lessons-learned before tackling them. Tracked under the v0.3.0 milestone.

Mission-type semantics (RFC §landing-vs-flyby — Option L-C)

Per-destination physical possibility drives what's offered:

  • Inner planets (Mercury, Venus, Mars) — both LANDING and FLYBY enabled. The displayed cell ∆v on LANDING includes the per-destination orbit_insertion_dv_km_s added to the Lambert ∆v.
  • Gas giants (Jupiter, Saturn) — FLYBY only. The LANDING pill renders disabled with aria-disabled="true" and a tooltip ("Landing on a gas giant is not a defined manoeuvre. See Galileo's orbit insertion in /missions for the closest analogue."). FLYBY ∆v = Lambert ∆v only.

Each per-destination JSON declares its mission_types array + a dv_orbit_insertion map keyed by the supported types.

Transfer-time ranges (RFC §transfer-time ranges — Option T-B)

Per-destination ranges declared per JSON file. Grid resolution stays 112 × 100 (preserves ADR-023 magnifier contract). Mars stays 80–520 d (no regression).

FLY-button experience (RFC OQ #1 — RESOLVED)

For non-Mars destinations, the FLY button navigates to /fly?mission={id}&dest={destination}&type={LANDING|FLYBY}. /fly renders an outbound-only arc using the v0.1.2 per-mission arc helper, with the destination planet substituted for Mars in the geometry. Free-return is reserved for the ORRERY DEMO scenario.

The HUD identity row reflects destination + mission type.

Build-time vs runtime grids (RFC OQ #2 — RESOLVED)

Build-time, per ADR-016. All five grids pre-computed by scripts/precompute-porkchops.ts (new) and committed under static/data/porkchop/earth-to-{id}.json. ~110 KB per destination → ~550 KB total, well within the existing image budget.

Trade-off accepted: instant first paint for any destination, full offline capability, no main-thread spike on slow devices when the user switches destinations. The Lambert worker still runs on main-thread interactions like a custom date-range slider (out of scope for v0.2 but the worker stays available).

Y-axis units (RFC OQ #3 — RESOLVED)

Auto-switch to years when tof_range_days[1] > 730. Mercury, Venus, Mars label the TOF axis in days; Jupiter and Saturn label in years (integer ticks: "1y", "2y", …). Threshold lives per-destination so tests can fix it.

C3-vs-tof axes (RFC OQ #4 — DROPPED)

Kept ∆v / dep-date axes. C3 is the mission-planner convention but adds an axis the user has to learn before reading the plot. PRD-002's promise is "intuitive before explanation"; ∆v stays the readable axis. C3 belongs to a future "advanced view" toggle if the educational case emerges.

URL contract

/plan?dest={id}&type={LANDING|FLYBY}
  • dest defaults to mars if absent (preserves existing /plan entry).
  • type defaults to LANDING for inner planets, FLYBY for gas giants.
  • Invalid combinations (e.g. ?dest=jupiter&type=LANDING) coerce to the destination's first valid type and update the URL via replaceState (no history pollution).

URL changes drive the porkchop swap; mission-type toggle writes back to URL via the existing pushFiltersToUrl pattern from /missions.

Data file contract

jsonc
// static/data/porkchop/earth-to-{id}.json
{
  "destination": "jupiter",
  "dep_range_days": [0, 1460],
  "tof_range_days": [400, 1500],
  "steps": [112, 100],
  "mission_types": ["FLYBY"],
  "dv_orbit_insertion": {
    "FLYBY": 0
    // for inner planets, add: "LANDING": 5.5
  },
  "tof_axis_unit": "years",        // "days" or "years"
  "grid": [[ /* 112 × 100 of ∆v values in km/s */ ]],
  "credit": "Computed at build time via Lambert solver. Ephemerides: planets.json."
}

ajv schema: static/data/schemas/porkchop.schema.json. Validated on PR via the existing validate-data.ts step.

Lambert worker change

The worker accepts an optional destinationId parameter:

ts
interface LambertRequest {
  id: number;
  depRange: [number, number];
  arrRange: [number, number];
  steps: [number, number];
  destinationId?: 'mercury' | 'venus' | 'mars' | 'jupiter' | 'saturn';   // NEW
}

When present, the worker reads the destination's heliocentric ephemerides (a, T, L0) from planets.json and parameterises the existing solver. Backward-compatible: if absent, defaults to 'mars'.

This generalises the worker from Earth → Mars only to Earth → any of the 5 supported destinations — closing the v2 issue noted in ADR-022's Consequences section.

Rationale

This ADR locks every variable RFC-007 raised in one place, so the v0.1.6 implementation can read a single contract. The decisions cluster around two principles already locked elsewhere:

  • PRD-002: "intuitive before explanation" — drives the C3 drop and the years-not-decimal-decades axis convention.
  • ADR-016: "external assets at build time" — drives the pre-computed grids decision.

Per-destination LANDING-vs-FLYBY semantics with the disabled-gas-giant pattern preserves the educational moment ("you cannot land on Jupiter") instead of hiding the question, matching PRD-002's "fail honestly" principle.

Alternatives considered

  • All 8 planets in v0.2 (RFC Option D-A) — Uranus + Neptune transfer ranges (~12 + 31 years) make the educational pattern unreadable at 112×100 grid resolution. Defer is the safer call.
  • Three destinations only (RFC Option D-C) — drops Mercury and Saturn, the two planets that produce the most striking educational outliers (Mercury: high ∆v despite being close; Saturn: 12-year wait between launch windows). Loses the load-bearing teaching moments.
  • Toggle adds delta on client (RFC Option L-B) — orbit-insertion ∆v varies with arrival velocity, so a flat per-destination delta is imprecise. The two-grid approach (L-A) was equivalent, but L-C's "physical possibility" semantics is what the user actually expects (LANDING pill genuinely disabled on a gas giant > LANDING pill computing a misleading number).
  • Auto-scaled ranges from Hohmann × constants (RFC Option T-C) — would miss interesting non-Hohmann transfer windows. Per-destination hand-tuning (T-B) is more work but produces tighter, more legible plots.
  • Runtime computation per destination — re-does work on every visit, blocks first paint on slow devices, breaks ADR-016 offline-first principle.

Consequences

Positive:

  • /plan covers 5 of 8 planets, opening the educational story to inner-planet variation (Mercury's ∆v cliff, Venus's narrow windows) and outer-planet impossibility (Saturn's 12-year cycle).
  • Pre-computed grids: zero compute on the user's first load. Full offline capability preserved.
  • Lambert worker becomes destination-parameterised — re-usable for v0.3 destinations + future custom porkchops.
  • Mars regression-protected by the default-destination + range-preservation rules.

Negative:

  • ~550 KB of pre-computed grid data committed to the repo (5 × ~110 KB). Acceptable per the existing 5.6 MB image bundle.
  • New scripts/precompute-porkchops.ts adds ~30 s to the build's first run (cached after that). Runs once locally + in CI; not a per-developer-visit cost.
  • Per-destination orbit_insertion_dv_km_s values must be sourced from public NASA technical reports per planet. Some hand-curation expected during v0.1.6 (tracked in milestone issues).
  • Three planets miss the v0.2 cut (Uranus, Neptune, Pluto) — explicitly tracked in v0.3.0 milestone with their own RFC frame.

Implementation notes

  • New scripts/precompute-porkchops.ts runs npm run precompute-porkchops (added to npm run build chain).
  • New src/types/porkchop-grid.ts defining the JSON shape.
  • New static/data/schemas/porkchop.schema.json for ajv validation.
  • src/routes/plan/+page.svelte reads the active destination's grid file via $lib/data#getPorkchopGrid(destinationId).
  • src/workers/lambert.worker.ts extends LambertRequest with the new destinationId field.
  • Mobile magnifier (ADR-023) keeps working — grid stays 112×100 across destinations.

Issue tracking: v0.1.6 milestone (issues will be opened as a slice plan covering data + worker + UI + /fly + tests + docs).

Orrery — architecture documentation · MIT · No tracking