diff --git a/data/raw/horizons/planets/README.md b/data/raw/horizons/planets/README.md new file mode 100644 index 0000000000..adc2b94e55 --- /dev/null +++ b/data/raw/horizons/planets/README.md @@ -0,0 +1,34 @@ +# JPL Horizons planet vectors + +Input to `npm run build-planet-ephemeris`, which fits each planet's `Horizons − Kepler` +residual into `src/data/bodies/planetEphemerisCorrections.generated.ts`. The CSVs are +gitignored; regenerate with `npm run fetch-horizons-planets` (8 files, ~3 MB each). + +- **Upstream:** JPL Horizons API, `https://ssd.jpl.nasa.gov/api/horizons.api` (JPL DE ephemeris; public domain, NASA/JPL). +- **Files:** `.csv`, columns `jd,x_km,y_km,z_km`, one row per day from JD 2415020.5 (1900-01-01) to 2488069.5 (2100-01-01). +- **Targets:** Mercury `199`, Venus `299`, Earth–Moon barycentre `3`, then the system barycentres `4`–`8` (Mars through Neptune). + +## Query + +Every target is fetched in four 50-year chunks (1900–1950, 1950–2000, 2000–2050, 2050–2100), +each retried up to five times. Each chunk includes both endpoints, so the shared boundary row is +dropped once when the chunks are merged. + +``` +format=json +COMMAND='' +OBJ_DATA='NO' +MAKE_EPHEM='YES' +EPHEM_TYPE='VECTORS' +CENTER='500@10' # Sun body centre +REF_PLANE='FRAME' # equatorial ICRF, the scene's world frame +TIME_TYPE='UT' # UT1 before 1962, UTC after: the app's simDays is UTC +OUT_UNITS='KM-S' +CSV_FORMAT='YES' +VEC_TABLE='1' # position only +START_TIME='' +STOP_TIME='' +STEP_SIZE='1 d' +``` + +Fetched 2026-10-04. diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index db2e7183e9..3e9ab2f5c4 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -191,7 +191,7 @@ Items with a **→ details** link have a full write-up in [`backlog/`](backlog/) - [ ] **Filaments + flow field lack scale fade bands** `ready` — both layers gate on user intent alone, with no zoom-based fade like the survey point clouds. → [details](backlog/2026-07-24-filaments-flow-scale-bands.md) - [ ] **Flow field does not reseed on a particle-count change** `needs-verification` — confirmed on the deployed build; the latch arms and the seed pass encodes, so the suspect is a stale trail ring or index-hashed seed positions. → [details](backlog/2026-09-15-flow-field-no-reseed-on-count-change.md) - [ ] **Moon orbit rate double-counts apsidal precession** `needs-design` — some satellite `P` columns are longitude periods but read as anomalistic, so Europa/Enceladus sit ~120° off orbital phase per decade. → [details](backlog/2026-10-03-moon-orbit-rate-double-counts-apsidal-precession.md) -- [ ] **Barycentric orbit pairs** `needs-design` — one-hop `focusId` pins Pluto (and Earth) instead of wobbling around the pair barycentre; needs an invisible focus-graph node. → [details](backlog/2026-08-16-barycentric-orbit-pairs.md) +- [ ] **Barycentric orbit pairs** `needs-design` — Pluto's wobble is one `BARYCENTRIC_PAIRS` row (Earth–Moon already uses it); the minor moons still need an invisible focus-graph node. → [details](backlog/2026-08-16-barycentric-orbit-pairs.md) - [ ] **Pluto's minor moons (Styx/Nix/Kerberos/Hydra)** `blocked` — they orbit the barycentre, so honest placement waits on barycentric pairs. → [details](backlog/2026-08-16-barycentric-orbit-pairs.md) - [ ] **ZoA label em-height not radius-invariant** `needs-design` — `LABEL_EM_MPC` is baked at construction while `LABEL_RADIUS_MPC` is per-frame; a tunable label radius would change apparent glyph size with nothing re-deriving to match. → [details](backlog/2026-08-17-zone-of-avoidance-shape-constants.md) - [ ] **Consolidate the renderer hygiene-basket duplications** `ready` — grow-on-demand instance buffer (×7+), 16-byte fade-scratch (×4), fullscreen-triangle (×5 + a second `.wesl` copy), sub-pixel cull (×3). → [details](backlog/2026-08-20-renderer-hygiene-basket.md) diff --git a/docs/DATA.md b/docs/DATA.md index f5ba57cd93..55695e0492 100644 --- a/docs/DATA.md +++ b/docs/DATA.md @@ -192,6 +192,10 @@ Planet/moon/ring textures and Earth's imagery are gitignored raw pulls with comm Earth's whole-globe base texture and its surface tile pyramid are two publications of one Blue Marble month (a 21600×10800 equirect and eight 21600×21600 quadrants, ~421 MB). The month is chosen once in [`bmngVintage.ts`](../tools/utils/io/bmngVintage.ts) and every registry path, upstream URL, and attribution string reads it from there. The tile layer falls back to the base outside its baked window, so a vintage split between the two would draw a visible seasonal seam along the tile frontier. +### Planet positions (JPL Horizons) + +Each planet's position is its Keplerian row plus a fitted correction series that matches JPL DE to ≤ 1,000 km over 1900–2100. `npm run fetch-horizons-planets` pulls 1-day heliocentric ICRF vectors (8 × ~3 MB, gitignored) into `data/raw/horizons/planets/`, and `npm run build-planet-ephemeris` fits them into the committed `src/data/bodies/planetEphemerisCorrections.generated.ts`. Regenerate after any planet element-row or frame change. The exact query is in [`data/raw/horizons/planets/README.md`](../data/raw/horizons/planets/README.md). + ### Earth surface tile pyramid The bake emits `public/data/images/earth-tiles/v10/albedo///.webp`: 512 px lossy WebP tiles whose alpha channel doubles as the land mask, plus two sidecars, `index.txt` (one path per line) and `manifest.json` (the flat band list every product's planner filters by `builtFrom`). `npm run build-surface-tiles` is idempotent per tile (a tile whose output already exists is skipped, byte-identical, on a re-run) and refuses to write `index.txt`/`manifest.json` at all if any tile the prior index promised is missing on disk, leaving both untouched — an interrupted or partially-deleted bake never advances the pointer to half-baked data ([`buildSurfaceTiles.ts`](../tools/textures/buildSurfaceTiles.ts)). @@ -216,6 +220,7 @@ Every refresh shares one shape: fetch, build, then `npm run sync-r2-secure` from | Clusters/superclusters | `fetch-structures` | `build-structures` (after `build-tiers`) | | DESI | `fetch-desi` | `build-tiers` (`desi-{deep,wedge,sgw}.bin`) | | Planet textures | `fetch-textures` (`--dev` for a subset) | `build-textures` | +| Planet positions | `fetch-horizons-planets` | `build-planet-ephemeris` (commits a generated `.ts`, no R2 sync) | | Earth surface tiles | `fetch-textures` + `fetch-eox` + `fetch-height` | `build-surface-tiles` (`--dev` for a quick z5 pass, albedo only) | Raw files and built artefacts are gitignored; only provenance READMEs and `.sha256` sidecars are committed. Two small deterministic bakes are the exceptions and live in git under `public/`: the MSDF font atlases in `public/fonts/` (`npm run build-fonts`) and the split-sum environment-BRDF LUT in `public/lut/` (`npm run build-env-brdf-lut`). Full-resolution texture and tile builds run post-merge from the main worktree. diff --git a/docs/backlog/2026-08-16-barycentric-orbit-pairs.md b/docs/backlog/2026-08-16-barycentric-orbit-pairs.md index 708b61591b..ecad1dd353 100644 --- a/docs/backlog/2026-08-16-barycentric-orbit-pairs.md +++ b/docs/backlog/2026-08-16-barycentric-orbit-pairs.md @@ -6,14 +6,18 @@ From the [add-Pluto/Charon grill session](../grill-sessions/add-pluto-charon-202 The focus graph is strictly one-hop: `OrbitalElements.focusId` makes a body orbit its parent's *center* (`src/services/engine/frame/deriveBodyStates.ts` resolves positions along `focusResolveOrder`). Real binary-ish systems orbit a shared barycentre: -- **Pluto–Charon**: Charon is ~12% of Pluto's mass; the barycentre sits ~2,130 km from Pluto's centre — 1.8 Pluto radii, i.e. ~940 km *above* the surface. The shipped model (Charon as a Moon-style satellite of Pluto) pins Pluto at its heliocentric position — an error of ~10% of the pair separation, visible only when parked near the barycentre with time running. The same approximation already applies to Earth–Moon (Earth doesn't wobble around the EMB), just less visibly. +- **Pluto–Charon**: Charon is ~12% of Pluto's mass; the barycentre sits ~2,130 km from Pluto's centre — 1.8 Pluto radii, i.e. ~940 km *above* the surface. The shipped model (Charon as a Moon-style satellite of Pluto) pins Pluto at its heliocentric position — an error of ~10% of the pair separation, visible only when parked near the barycentre with time running. Earth–Moon no longer has this error: see the pair table below. - **Pluto's minor moons** — Styx, Nix, Kerberos, Hydra — orbit the *barycentre*, not Pluto, at wider radii where the approximation grows proportionally worse. They are deliberately absent until this lands (grill Q5): four sub-pixel irregular rocks drawn dishonestly around Pluto's center add clutter without honesty. -## Shape of a fix +## What exists now + +`BARYCENTRIC_PAIRS` (`src/data/bodies/barycentricPairs.ts`) treats a primary's element row as the pair barycentre, and `deriveBodyStates` subtracts the secondary's mass-weighted reflex from the primary. Earth–Moon is its one row. Pluto's wobble is therefore one more row (`pluto`/`charon`, Charon's mass fraction ≈ 0.1086), once Pluto's row is re-fitted to the Pluto-system barycentre. The minor moons still need the invisible node below, because they orbit the barycentre rather than either body. + +## Shape of a fix (minor moons) An invisible barycentre node in the focus graph that both partners orbit. Touchpoints that currently assume focus-graph nodes are visible bodies: pick tables (`resolvePickTable.ts` maps source → seed table by index), region extents (`bodyRegions.ts` maxes over members), labels/captions (every `SCENE_PLANETS` row gets one), glint partition. Either a `visible: false` body kind or a separate node table that only `deriveBodyStates` sees. ## Follow-on once landed -- Let Pluto (and optionally Earth) wobble: re-parent Pluto → `pluto-barycentre` with a ~2,130 km semi-major element row; delete the "looks wrong, don't fix" comment on Charon's row. +- Let Pluto wobble: add the `pluto`/`charon` row to `BARYCENTRIC_PAIRS`; delete the "looks wrong, don't fix" comment on Charon's row. - Add Styx/Nix/Kerberos/Hydra as ordinary satellite rows parented on the barycentre node (no textures — no usable maps exist; flat albedo + glint only). diff --git a/docs/superpowers/plans/completed/2026-10-04-accurate-planet-positions.ledger.md b/docs/superpowers/plans/completed/2026-10-04-accurate-planet-positions.ledger.md new file mode 100644 index 0000000000..466de1929d --- /dev/null +++ b/docs/superpowers/plans/completed/2026-10-04-accurate-planet-positions.ledger.md @@ -0,0 +1,51 @@ +# SDD ledger — plan: docs/superpowers/plans/2026-10-04-accurate-planet-positions.md + +Asks (2026-10-04): parallelism = stack (feature starts right away on `accurate-planets` atop `accurate-planets-prep`); perf gate = SKIP. Lean protocol: one final review per PR; T5/T6 tagged review: yes. +Base: prep branch off origin/main d435719c0 (main moved from a600d661f; #827 touches no planned file). + +## Pre-flight scan +| Rows | Produces → consumes | Finding | +|---|---|---| +| T1 ↔ T6 | T1 test reads eye-relative km basis (floats 34..45); T6 on-trail test reuses it | consistent | +| T2 ↔ T6 | both regenerate bodyStatesJ2000 + golden traces + EARTH_TARGET | T6 re-does after T2; consistent (sequential) | +| T2 ↔ T5 | T5 fit must run AFTER T2's obliquity (residual in app frame) | stacked branch guarantees order | +| T3 ↔ T5 | `EphemerisCorrection`, `ephemerisCorrectionMpc` → T5 verifies with it | consistent | +| T4 ↔ T5 | `data/raw/horizons/planets/.csv` via rawDataPath('horizons.planets') | consistent | +| T5 ↔ T6 | `PLANET_EPHEMERIS_CORRECTIONS` keyed by planet id | consistent | +| T1 self | test fails today, passes after | ok | +| T2 self | no new test (constant) | ok per testing.md | +| T3–T6 self | tests match code | ok | +Dispatch A (Tasks 1–2, Sonnet) launched on accurate-planets-prep; draft PR #834; base c798ac56a +Task 1: complete (commits c798ac56a..2183ed6b7, final review pending) +Task 2: complete (commits 2183ed6b7..7edf35cb5, final review pending) +Ruling: Task 1 on-trail tolerance 0.1 km, not 1 m — the eye-relative basis is packed f32 (~30 m step at the Moon's 4e5 km); injected offset 1e6 km is 4 orders above — costs: a sub-100 m anchoring regression would pass. Same tolerance for Task 6's on-trail test. +Ruling: 18–20-digit keplerianEllipse comparisons left unloosened — they still pass, loosening would be churn — costs nothing. +Golden traces checked: same structure (26/26, 47 entries), only J2000 Earth coordinates rotated by the obliquity fix. +Prep pushed 7edf35cb5; CI watch on #834; final review (Opus) dispatched. Feature branch accurate-planets created at 7edf35cb5; Dispatch B (Tasks 3–5, Opus) launched. +Prep final review: 3 Minor (propagateElements.test.ts:93-94 garbled comment; orbitalElements.ts:4 header not re-wrapped; settleGoldenTrace.json minified by re-record). No Critical/Important. +Ruling: controller fixes the 3 Minors inline on accurate-planets-prep (<50 lines, lean protocol) AFTER Dispatch B finishes (worktree is on accurate-planets), then merges prep into accurate-planets — avoids switching branches under a live agent — costs: prep PR waits ~1 dispatch. +Prep CI RED (1/1426): cameraFraming.test.ts:76 GALACTIC_DISC_FORWARD decodes to yaw −1.4208018 vs −1.4208 @1e-6 — the exact-obliquity ecliptic frame shifts the decode ~2e-6 rad. +Ruling: relax yaw/pitch to toBeCloseTo(…, 4) with a one-line why (still catches a mistyped 4th decimal, 1e-4 > 5e-5); do NOT re-derive GALACTIC_DISC_FORWARD (a 1e-5 rad view change nobody sees) — costs: the pin is 50× looser. Folded into the controller's prep fix commit after Dispatch B. +Prep fix commit 309d53be8 (3 review Minors + cameraFraming 4-place ruling; settle recorder now writes 2-space JSON and trace re-recorded) pushed; CI re-watch running. +Task 3: complete (commits 7edf35cb5..428f92fdc, final review pending) +Task 4: complete (commits 428f92fdc..e79ff43f2, final review pending) +Task 5: complete (commits e79ff43f2..2f0c8f0d3, final review pending) — 525 terms, max verified 900 km (Earth), 36.2 kB text +Ruling (Dispatch B deviations accepted): golden-section frequency refine instead of parabolic (2 vs 21 terms on two-tone test); fit grid always includes the final row (Uranus failed 1,012 km at 2100 otherwise); generated file 36 kB text not ~17 kB (plan assumed f32 binary; text with 12-digit ω is the honest size) — costs: bundle +36 kB raw (~less gzipped). +Prep merged into accurate-planets (948758534); feature draft PR #835 (base accurate-planets-prep). Dispatch C (Task 6, Opus) launched. +Prep PR #834 CI GREEN at 309d53be8; review clean after fixes → ready for user smoke/merge ruling. +Task 6: complete (commits 948758534..b707c020b, final review pending) — vs Horizons fixture: Me 593, V 616, E 621 (vs 399), Ma 868, J 307, S 333, U 279, N 300 km; k=0 mutation → Earth 4,830 km, reflex test fails. +Ruling (accepted): BARYCENTRIC_REFLEX_BY_PRIMARY as 2nd export of data/bodies/barycentricPairs.ts (frameFilePurity bans the module-level map in deriveBodyStates) — costs: data file with two exports. Saturn on-trail check at 1 m on the f64 conic (packed f32 step 128 km at Saturn); Moon/Hubble 0.1 km on packed basis. HorizonsSeries type moved to tools/bodies/@types (noInlineTypes). +Feature final review: clean of Critical/Important; 6 Minors (unused maxErrKm; BARYCENTRIC_PAIRS export unused + test restates k; inline dot helper; golden-section double eval; lo=0 NaN probe; parser throw-case test). Ruling: fix all 6 in ONE fix dispatch after local CI finishes (stacked PR gets no GitHub CI — base ≠ main; local typecheck+lint+test run as the gate). +Local CI on b707c020b: typecheck+lint+test GREEN (1432 files / 14950 tests). Fix dispatch (6 Minors, Sonnet) launched. +Fix dispatch 8eea5fe69: 6 Minors fixed; fixer reverted the regenerated file (1e-10 rounding noise broke exact fixtures). +Ruling: controller regenerated corrections from the final tool + re-blessed bodyStatesJ2000 and both golden traces (60a10eec9) so the committed generated file equals tool output — costs: fixture churn of rounding noise only (same term counts, same max errors). +Local CI on 60a10eec9: 1429/1429 files, 14943 tests pass; 3 worker-start timeouts (load) re-run green (3 files, 7 tests). Both PRs review-clean. NEXT: user smoke (plan DoD) → /feature-done → merge on user's word (prep #834 first, then retarget #835 to main). + +## /feature-done IN PROGRESS (2026-10-04) — user smoke PASSED ("looks good") +DoD findings so far: tests+typecheck GREEN (local full run on 60a10eec9, no change since); checkboxes 0/31 ticked (tick all on READY); DoD section PRESENT; spec Ground preparation PRESENT; unexpected files (all explained): earthUniverseLoop.test.ts, settleGoldenTrace.test.ts (recorder), mergeHorizonsChunks(.test).ts, tools/@types/math/FitResult.d.ts, tools/bodies/@types/HorizonsSeries.d.ts, tools/parsers/@types/HorizonsVectorRow.d.ts, tools/utils/math/dot.ts; untouched planned: none; new TODOs: 0; comment density OK; test parity grew; smoke FOUND; deferred = plan "Out of scope" list (moons, Pluto pair row → backlog detail updated, Voyager → mission-trails spec next, runtime TDB, orbitReachByRegion margin). +IN-FLIGHT: deletion-audit agent (opus, GREENFIELD/--justify, read-only) → report at scratchpad `deletion-audit-accurate-planets.md`. ON RESULT: apply SAFE-NOW bin as its own deletion commit on `accurate-planets` (dispatch Sonnet if >2 files), run targeted tests, push; NEEDS-RULING items → ask user. Then READY → tick plan checkboxes; git mv plan → plans/completed/, spec → specs/completed/; copy this ledger → plans/completed/2026-10-04-accurate-planet-positions.ledger.md; backlog sweep (barycentric-pairs item STAYS, only its clause was updated); commit "docs(plan): mark accurate-planet-positions complete" (NO Co-Authored-By per user memory); push accurate-planets. +THEN: merge ONLY on user's "merge this PR": #834 first (squash), then retarget #835 base → main, rebase/merge main, GitHub CI, merge. After both land: Voyager mission-trails spec (memory project_mission_trails.md). +Dev server for this wt: localhost:5176 (bg, started this session) — leave running. +Deletion audit done (report: scratchpad deletion-audit-accurate-planets.md): safe-now ~27 LOC (FitResult.d.ts, import guard, keplerKm reuse, edge-continuity test, HorizonsSeries.d.ts) → Sonnet dispatch applying. NEEDS-RULING asked as dash ask 4UUQ (site test keep(rec) · real-data on-trail test delete(rec) · pair table keep(rec) · mergeHorizonsChunks keep(rec)). +Safe-now deletion commit 788c5f627 pushed (typecheck both tsconfigs + tests/tools tests/utils/orbit green). Waiting on ask 4UUQ. +DoD audit READY 2026-10-05: deletion audit ~27 LOC safe-now (788c5f627) + ruled on-trail test drop (3ff4005a5); 4UUQ: keep site test, keep pair table, keep mergeHorizonsChunks. Plan/spec moved to completed/. diff --git a/docs/superpowers/plans/completed/2026-10-04-accurate-planet-positions.md b/docs/superpowers/plans/completed/2026-10-04-accurate-planet-positions.md new file mode 100644 index 0000000000..5978c28201 --- /dev/null +++ b/docs/superpowers/plans/completed/2026-10-04-accurate-planet-positions.md @@ -0,0 +1,333 @@ +# Accurate planet positions — implementation plan + +> **For agentic workers:** REQUIRED SUB-SKILL: use superpowers:subagent-driven-development under the +> lean protocol in `docs/superpowers/conventions/sdd-execution.md`. Steps use checkbox (`- [x]`) syntax. + +**Goal:** The eight planets match JPL DE (Horizons) to ≤ 1,000 km over 1900–2100, Earth wobbles about +the Earth–Moon barycentre, and every orbit trail stays through its body. + +**Architecture:** Each planet's position is today's Keplerian position plus a generated correction +series: a cubic polynomial plus fitted sinusoids, about 17 kB of constants. The correction is +evaluated in `deriveBodyStates`. A one-row pair table subtracts the Moon's reflex from Earth. Orbit +trails anchor on the snapshot body position, so they follow both corrections without needing to know +about either. + +**Tech stack:** TypeScript, Vitest, tsx tools, the JPL Horizons API. + +**Spec:** `docs/superpowers/specs/completed/2026-10-04-accurate-planet-positions-design.md` + +**Completed 2026-10-05** (#834 prep, #835 feature). Deviations and rulings: see the archived ledger `2026-10-04-accurate-planet-positions.ledger.md`. The real-data on-trail test (Task 6) was dropped by the deletion audit (user ruling): the snapshot-offset test covers the mechanism. + +## Global constraints + +- Accuracy: ≤ **1,000 km** from Horizons for every planet over **1900-01-01 → 2100-01-01**. The fit + stops at ≤ **900 km** on its sample grid, and the tool verifies every planet on the dense 1-day grid + and throws above 1,000 km. +- Outside the span the correction is **frozen at its edge value**: clamp `simDays` to + `[startJd, endJd]`. No fade and no new setting. +- Horizons query: `EPHEM_TYPE='VECTORS'`, `CENTER='500@10'` (Sun centre), `REF_PLANE='FRAME'` + (equatorial ICRF), `TIME_TYPE='UT'`, `OUT_UNITS='KM-S'`, `CSV_FORMAT='YES'`, `VEC_TABLE='1'`. + - Targets: Mercury `199`, Venus `299`, Earth–Moon barycentre `3`, then system barycentres `4`–`8` + for Mars through Neptune. + - Fetch 1-day steps in 50-year chunks, retrying each chunk up to 5 times. +- `OBLIQUITY_DEG = 23.4392911`. +- Earth–Moon pair: `secondaryMassFraction = 1 / 82.30057`, the Moon's share of the pair mass. +- `simDays` stays a UTC Julian date. No runtime TDB conversion: the UT query absorbs the offset. +- Conventions: one symbol per `utils/` and `@types/` file, `type` and never `interface`, and pass and + frame files export only their one symbol. +- Prep (Tasks 1–2) is a **separate PR** on branch `accurate-planets-prep` off `main`. This spec and + plan are committed there too. Features (Tasks 3–6) go on `accurate-planets`, stacked on the prep + branch. Never commit on `main`. + +## Review focus + +1. **Dates outside 1900–2100** (a tour in 1850, a scrub to 2200). Positions stay finite and equal + Kepler plus the frozen edge correction, with no jump at either boundary. Tests are in Task 3. +2. **A camera parked at an Earth surface site with the clock running.** The site keeps its offset from + Earth while Earth wobbles. Tested in Task 6. +3. **Moon, Hubble and the easter eggs after the Earth reflex.** Each still sits on its own trail, + because its trail is centred on the reflexed Earth. Tested in Task 6. +4. **Any element-row edit or frame change without regenerating.** The Horizons check fails loudly + instead of drifting silently. Tested in Task 6. +5. **Chunk seams in the Horizons fetch.** The duplicated boundary row is dropped exactly once, with + no gap and no duplicate. Tested in Task 4. + +--- + +## PR 1: prep (`accurate-planets-prep`) + +### Task 1: Orbit trails anchor on the snapshot body position (P1) + +**Files:** `src/services/engine/frame/passes/orbitTrailsPass.ts:96-109` (modify), +`src/data/bodies/orbitalElements.ts:1-9` (header, modify), +`tests/services/engine/frame/passes/orbitTrailsPass.test.ts` (modify) + +**Before → after** (`orbitTrailsPass.ts:105-109`): + +```ts +// before: centre = focus + centerOffset (focus re-read, conic re-derived: a mirror of deriveBodyStates) +// after: centre = state.positionMpc − keplerianPositionMpc(propagated) + centerOffsetMpc +``` + +The body is now on its trail by construction: any later change to the snapshot position moves the +trail with it. Keep `meanAnomalyRad` as the fade anchor, unchanged. + +- [x] Add the test `trail follows a snapshot position that differs from raw Kepler`. Stub the states + map so one row's `positionMpc` is the Kepler position plus a 1e6 km vector (converted with + `SCALE_UNITS.KM_TO_MPC`). Read the packed eye-relative km basis (floats 34..45, written by + `eyeRelativeOrbitBasisKm`): the body point `C + A·cosE + B·sinE` at the row's propagated E must + equal the stubbed position, eye-relative in km, to ≤ 1 m. Use km tolerances throughout: + 1e-15 Mpc is 30,000 km. +- [x] Run that test and confirm it fails against the current code. +- [x] Implement the after-line. Rewrite the `orbitalElements.ts` header sentence about "a body sitting + on its own trail is structural" so it names the snapshot anchoring. +- [x] The comparisons against `keplerianEllipse` (`orbitTrailsPass.test.ts:345-378`, `:511-573`) were + exact to 18–20 digits. Loosen them to `toBeCloseTo(…, 15)`, or the relative tolerance f64 can + hold for `(a + b) − b`, and give the reason in one line. +- [x] `npm test -- orbitTrailsPass`, then commit "Orbit trails anchor on the snapshot body position". + +### Task 2: Exact obliquity (P2) + +**Files:** +- `src/data/bodies/orbitPlaneFrames.ts:72` (and the header comments at `:25`, `:29`, `:77-78`) +- `src/utils/orbit/rotationLookAt.ts:19` +- `src/data/defaults.ts:138-141` +- `tests/data/bodies/orbitPlaneFrames.test.ts:50-53` +- `tests/data/orientation/orientationFrames.test.ts:54` +- `tests/utils/orbit/propagateElements.test.ts:94` +- `tests/services/engine/frame/earthTerminator.test.ts:17` +- `tests/fixtures/bodyStatesJ2000.json` +- `tests/fixtures/camera/settleGoldenTrace.json` +- `tests/fixtures/camera/driverGoldenTrace.json` +- `tools/perf/perfScenarios.ts:52` + +Set `OBLIQUITY_DEG = 23.4392911` and say "IAU J2000 mean obliquity" in its comment. Comments that +say `23.44°` become `23.439°`. Test literals use the exact value; in `orientationFrames.test.ts:54`, +66.56° becomes `90 − 23.4392911`. No new test: this is a constant, and the existing frame tests pin it. + +- [x] Change the constant, comments and test literals. +- [x] Regenerate `bodyStatesJ2000.json` with a one-off tsx snippet that is not committed. It + serialises `deriveBodyStates(2451545.0)` for every `ORBITAL_ELEMENTS` id in the fixture's + existing shape (`positionMpc`, `orientation`, `meanAnomalyRad`), leaves anchors out, and keeps + the key order. Diff it: only rotated values may change. +- [x] Re-record the camera traces with `SETTLE_GOLDEN_RECORD=1` and `DRIVER_GOLDEN_RECORD=1` + (`npm test -- settleGoldenTrace driverGoldenTrace`). The spec rules this. Then run once more + without the flags. +- [x] Update `EARTH_TARGET` in `perfScenarios.ts:52` to the new J2000 Earth position, taken from the + regenerated fixture. +- [x] `npm run typecheck:fast`, then `npm test -- orbitPlaneFrames orientationFrames propagateElements earthTerminator deriveBodyStates`. + Commit "Exact J2000 obliquity". + +--- + +## PR 2: feature (`accurate-planets`, stacked on prep) + +### Task 3: Correction type and evaluator + +**Files:** +- `src/@types/scene/EphemerisCorrection.d.ts` (create) +- `src/utils/orbit/ephemerisCorrectionMpc.ts` (create) +- `tests/utils/orbit/ephemerisCorrectionMpc.test.ts` (create) + +**Interfaces, produced:** + +```ts +export type EphemerisCorrection = { + startJd: number; + endJd: number; // fitted span (UTC JD); outside it the edge value is held + polyKm: readonly [Vec3, Vec3, Vec3, Vec3]; // coefficients of τ⁰..τ³, τ = 2(t − startJd)/(endJd − startJd) − 1 + terms: readonly number[]; // flat, 7 per term: ω (rad/day), cos x,y,z (km), sin x,y,z (km); phase ω·(t − startJd) +}; +export function ephemerisCorrectionMpc(c: EphemerisCorrection, simDays: number): Vec3; +``` + +Sum in f64 km, then multiply by `SCALE_UNITS.KM_TO_MPC`. + +- [x] Test `evaluates polynomial and terms at a known instant`: a hand-built two-term correction, + checked against values worked out by hand. +- [x] Test `holds the edge value outside the span`: `simDays` 100 years before `startJd` returns + exactly (`toEqual`) the value at `startJd`, and likewise at `endJd`. +- [x] Test `is continuous across both span edges`: evaluations at edge ± 1e-6 day differ by < 1 m. +- [x] Implement, `npm test -- ephemerisCorrectionMpc`, then commit "Ephemeris correction evaluator". + +### Task 4: Horizons planet fetcher + +**Files:** +- `tools/parsers/horizonsVectorsCsv.ts` (create) +- `tests/tools/parsers/horizonsVectorsCsv.test.ts` (create) +- `tools/fetch/fetchHorizonsPlanets.ts` (create) +- `tools/utils/io/rawDataRegistry.ts` (add `horizons.planets` and `horizons.planets.readme`, using the + entry shape at `:17-25`) +- `data/raw/horizons/planets/README.md` (create, committed; it records the exact query in Global + constraints) +- `package.json` (add `"fetch-horizons-planets": "tsx tools/fetch/fetchHorizonsPlanets.ts"`) +- `docs/DATA.md` (add a Horizons entry wherever the fetchers are listed) + +**Interfaces, produced:** + +```ts +export type HorizonsVectorRow = { jd: number; xKm: number; yKm: number; zKm: number }; +export function parseHorizonsVectorsCsv(result: string): HorizonsVectorRow[]; // rows between $$SOE/$$EOE +``` + +The output goes to `data/raw/horizons/planets/.csv` with the columns `jd,x_km,y_km,z_km`, +written through `rawDataPath('horizons.planets')`. Chunks are concatenated and a row whose `jd` is ≤ +the previous row's is dropped (Review focus 5). + +- [x] Test `parses the SOE/EOE block of a Horizons vectors result`: use a trimmed real response + pasted as a string literal, three rows. +- [x] Test `concatenating two chunks drops the shared boundary row once`, through a small exported + merge step, or by asserting the fetcher's `mergeChunks` if you extract it. Keep it + one-symbol-per-file if extracted. +- [x] Implement the parser and the fetcher, which writes all eight CSVs. Announce the download size + before running it: 8 × ~3 MB. +- [x] Run `npm run fetch-horizons-planets`. Commit "Horizons planet fetcher", without the gitignored + CSVs. + +### Task 5: Fit tool and generated corrections · `review: yes` + +**Files:** +- `tools/utils/math/fft.ts` (create) +- `tools/utils/math/fitSinusoidSeries.ts` (create) +- `tests/tools/utils/math/fft.test.ts` (create) +- `tests/tools/utils/math/fitSinusoidSeries.test.ts` (create) +- `tools/bodies/buildPlanetEphemeris.ts` (create, using the `tools/bodies/buildPlanetFacts.ts` + template: banner, prettier, CLI guard) +- `src/data/bodies/planetEphemerisCorrections.generated.ts` (generated) +- `package.json` (add `"build-planet-ephemeris": "tsx tools/bodies/buildPlanetEphemeris.ts"`) + +**Interfaces, produced:** + +```ts +export function fft(re: Float64Array, im: Float64Array): void; // in-place radix-2, length a power of two +export type FitResult = { polyKm: [Vec3, Vec3, Vec3, Vec3]; terms: number[]; maxErrKm: number }; +export function fitSinusoidSeries(tJd: Float64Array, residualKm: [Float64Array, Float64Array, Float64Array], + startJd: number, endJd: number, stopKm: number, maxTerms: number): FitResult; +export const PLANET_EPHEMERIS_CORRECTIONS: Readonly>; // keys: planet ids +``` + +**Method.** The spike is `planetSeriesFit.mts` in the brainstorm scratchpad; it measured ~590 terms. +1. Fit the cubic in τ first. +2. Then loop: + - take the FFT of the residual, zero-padded ×8, with power summed over x, y and z; + - find the peak and refine it parabolically; + - add a cos column and a sin column; + - Gram–Schmidt each new column against the kept ones, which makes it an exact least-squares refit; + - stop when the max 3D error is ≤ `stopKm`. +3. Recover the coefficients of the original basis from the orthogonal one. Keep R, or refit by normal + equations once at the end, so the generated `terms` are plain (ω, cos, sin) amplitudes. +4. The tool computes the residual as `Horizons − keplerianPositionMpc(propagateElements(row, jd))`. + It converts km with the same `MPC_TO_KM` the app uses, imports the `src/` modules directly, and + uses the app's own element rows. + +**Sample steps (days) for fitting:** Mercury 2, Venus 4, Earth 4, Mars 6, Jupiter through Neptune 10. +The tool then **verifies on every 1-day row** with `ephemerisCorrectionMpc` (from Task 3) and throws +if any planet exceeds 1,000 km. That way the shipped evaluator is exactly what gets verified. + +- [x] Test `fft finds a pure sinusoid's bin`: N = 64, a sinusoid in bin 5, and the power peaks at k = 5. +- [x] Test `fitSinusoidSeries recovers a synthetic two-term signal`: a cubic plus two sinusoids at + non-bin frequencies. With `stopKm` 1, it should use ≤ 4 terms, and evaluating the result through + `ephemerisCorrectionMpc` reproduces the signal to ≤ 1 km. +- [x] Implement, then run `npm run build-planet-ephemeris`. Report each planet's term count and + verified max error; expect ~35–180 terms and a total of ~17 kB of numbers. +- [x] Commit "Planet ephemeris fit tool + generated corrections". + +### Task 6: Wire corrections and the Earth–Moon pair into `deriveBodyStates` · `review: yes` + +**Files:** +- `src/@types/scene/BarycentricPair.d.ts` (create) +- `src/data/bodies/barycentricPairs.ts` (create) +- `src/services/engine/frame/deriveBodyStates.ts:60-68` (modify; the header gains one clause) +- `src/data/bodies/scenePlanets.ts:3` (fix the stale "via keplerianPositionMpc" header) +- `tests/fixtures/horizonsPlanets.json` (create) +- `tests/data/bodies/planetEphemeris.test.ts` (create) +- `tests/services/engine/frame/deriveBodyStates.test.ts` (modify) +- `tests/fixtures/bodyStatesJ2000.json` +- `tests/fixtures/camera/{settle,driver}GoldenTrace.json` +- `tools/perf/perfScenarios.ts:52` +- `docs/backlog/2026-08-16-barycentric-orbit-pairs.md` +- `docs/BACKLOG.md` (the "Barycentric orbit pairs" line) + +**Interfaces:** + +```ts +export type BarycentricPair = { primaryId: string; secondaryId: string; secondaryMassFraction: number }; +export const BARYCENTRIC_PAIRS: readonly BarycentricPair[]; // [{ primaryId: 'earth', secondaryId: 'moon', secondaryMassFraction: 1 / 82.30057 }] +``` + +**1b loop, after this task:** +`position = focus + kepler(el) + correction(el.id) − k · kepler(propagate(secondaryRow))`. +- `correction` is `ephemerisCorrectionMpc(PLANET_EPHEMERIS_CORRECTIONS[el.id], simDays)` when a + correction exists, and zero otherwise. +- The reflex term applies only when `el.id` is a pair's `primaryId`. +- The secondary row comes from `ORBITAL_ELEMENTS` and is propagated inline, because `FOCUS_ORDER` + resolves `earth` before `moon`. +- Resolve pair and correction lookups into id-keyed maps at module load, beside `FOCUS_ORDER`, never + per frame. + +**Fixture.** `tests/fixtures/horizonsPlanets.json` uses the Global constraints query, except with +target `399` (Earth centre) for Earth. +- Dates: 1900-06-01, 1979-03-05, 1980-11-12, 1986-01-24, 1989-08-25 and 2099-06-01 (Voyager flybys + plus both ends). +- Shape: `{ [id]: { [jd]: [xKm, yKm, zKm] } }`. +- The test header records the query URL. + +- [x] Test `every planet is within 1,000 km of Horizons at the fixture dates`, from `deriveBodyStates` + positions. Earth is compared with Horizons `399` and gets its own tolerance: measure the max, + round up to the next 500 km, and write the measured number in a one-line comment, since it + includes the app Moon's error. This test is Review focus 4. +- [x] Test `Earth–Moon reflex keeps the barycentre and the separation`. At three dates, with + k = 1/82.30057, (1−k)·Earth + k·Moon equals the corrected EMB (Earth-row Kepler plus the + correction) to ≤ 1 m. Moon − Earth equals the Moon's raw Kepler offset to ≤ 1 m. +- [x] Test `an Earth surface site keeps its offset from the wobbling Earth` (Review focus 2): a site + hosted on `earth` minus Earth's position is independent of the reflex. Compare with the site + offset computed from `sitePointBodyFixed` and Earth's orientation. +- [x] Test `Moon, Hubble and Saturn sit on their trails at a non-J2000 date` (Review focus 3): drive + `orbitTrailsPass` packing at a 1989 date. Each body's position lies on its packed conic at its + propagated E, to ≤ 1 m, reading the eye-relative km basis as Task 1's test does. +- [x] Implement the pair table and the 1b term. Fix the `scenePlanets.ts:3` header. +- [x] Regenerate `bodyStatesJ2000.json` with Task 2's one-off snippet. Re-record the golden traces + with the record flags, then rerun without them. Update `EARTH_TARGET`. +- [x] Backlog: in the detail file, add that `BARYCENTRIC_PAIRS` exists, so Pluto's wobble is one row, + while the minor moons still need the invisible node. Update the BACKLOG line's clause to match. +- [x] `npm run typecheck:fast`, then `npm test -- planetEphemeris deriveBodyStates orbitTrailsPass`. + Commit "Accurate planet positions: corrections + Earth–Moon pair". + +--- + +## Dispatch grouping (controller) + +| PR | Dispatch | Tasks | Model | +|---|---|---|---| +| 1 | A, or inline (< 50 lines of source) | 1, 2 | Sonnet | +| 2 | B | 3, 4, 5 (the tool chain; Task 5 has `review: yes`) | Opus | +| 2 | C | 6 (`review: yes`) | Opus | + +Ask at plan start: parallelism, and whether to run the perf gate. Recommended: no perf gate, because +the per-frame cost is about 590 sinusoids per new `simDays` and trails add nothing. + +## Definition of done + +**Deliverables:** +- `PLANET_EPHEMERIS_CORRECTIONS` (generated) +- `ephemerisCorrectionMpc` +- `BARYCENTRIC_PAIRS` +- `npm run fetch-horizons-planets` and `npm run build-planet-ephemeris` +- `OBLIQUITY_DEG = 23.4392911` +- trails anchored on the snapshot + +**Manual smoke** (the user looks; the dev server stays running): +- The clock set to 1980-11-12: Saturn is where Horizons puts it, and the inspector's position + matches JPL to within ~1,000 km. +- Zoom close to Saturn and Jupiter with time running fast: each stays on its trail. +- Park at Earth with a 1-day/s clock: the Earth–Moon wobble is visible against the trail, and the + Moon stays on its trail. +- Set the clock to 1850 and to 2200: the planets are finite, there is no jump as the clock crosses + 1900 and 2100, and the trails are intact. + +**Out of scope (deferral boundary):** +- moon accuracy +- the Pluto–Charon pair row and Pluto's minor moons +- spacecraft and Voyager (the mission-trails spec is next) +- runtime TDB conversion +- any margin in `orbitReachByRegion` diff --git a/docs/superpowers/specs/completed/2026-10-04-accurate-planet-positions-design.md b/docs/superpowers/specs/completed/2026-10-04-accurate-planet-positions-design.md new file mode 100644 index 0000000000..9ec3c8750f --- /dev/null +++ b/docs/superpowers/specs/completed/2026-10-04-accurate-planet-positions-design.md @@ -0,0 +1,188 @@ +# Accurate planet positions — design spec + +Child of the Voyager mission-trails brainstorm (2026-09-28 → 2026-10-03). Voyager's flybys +are only as honest as the planets they pass: today's Keplerian planets drift from JPL by +98k km at Jupiter up to 1.6M km at Saturn at the flyby dates, 9–16× the miss distances. On +2026-10-03 the user ruled that **the accurate physical planet position is the source of +truth**, independent of any flyby window, and that it ships as its own spec and PR before the +mission-trails spec. + +## 1. What this is + +Heliocentric positions of the eight planets match JPL DE (Horizons) to **≤ 1,000 km over +1900–2100**. That is 3% of the closest planet flyby (Voyager 2 at Neptune, ~29,000 km from +centre). Four parts: + +- **A correction series per planet**, added on top of today's Keplerian position. It is a + cubic polynomial plus fitted sinusoids, generated offline from Horizons and shipped as + constants. All eight planets together take ~590 terms, about 17 kB. A build tool fits it + and checks it against JPL. +- **Earth wobbles around the Earth–Moon barycentre (EMB).** The `earth` element row already + describes the EMB, so Earth = EMB − k·(Moon's geocentric offset), with k the Moon's share of + the pair's mass. +- **Orbit trails follow the corrected body.** Each trail is the same Keplerian conic, + translated so it passes through the body's snapshot position. +- **Exact obliquity.** `OBLIQUITY_DEG` becomes 23.4392911° instead of the rounded 23.44°. + +**Out of scope:** +- Moon accuracy. Moons keep their own element rows. +- Pluto–Charon. That becomes one more pair row later (§6). +- Spacecraft. Mission trails is the next spec and reuses the fetch tool. +- Any runtime TDB time scale. + +## 2. Ground preparation + +Refactor-ground ran on 2026-10-03. It sent an Explore trace over every reader of a planet's +position and over the trail pass, and ran a greenfield cross-check. The user signed off on the +checkpoint. + +| Touchpoint | Verdict | Seam | +|---|---|---| +| Body position (`deriveBodyStates.ts:63-67`) | growth | one added term in the 1b loop; it is the only place positions are composed | +| Orbit trail (`orbitTrailsPass.ts:101-109`) | **bolt-on → P1** | the pass re-propagates each conic and re-folds the focus itself, a mirror of `deriveBodyStates`. The body sits on its trail only because both happen to agree, so adding the correction here would make a second copy | +| Obliquity (`orbitPlaneFrames.ts:72`) | **wrong constant → P2** | the fit is generated against the app's frame, so the constant must be right before the first generation | +| Earth wobble | growth | a new pair table read in the same 1b loop. No focus-graph node: the backlog's invisible-node design is needed only for Pluto's minor moons | +| Synchronous readers at module load (`bodyRegions.ts:29`, `starCatalog/create.ts:63`) | growth | the generated `.ts` is a static import, as with `bodyFacts.generated.ts` | +| Generated data | growth | `tools/bodies/buildPlanetFacts.ts` precedent: banner, prettier, committed output | + +**The greenfield cross-check diverged on three points, all resolved by measurement or ruling:** +- **Residual table → fitted series.** A table needs ~140 kB in f32 (Mercury needs a 13-day + step); the series needs ~17 kB. +- **Sampled trail with a geometric tail → translated conic.** With a translated conic the + worst case is Saturn: 4 px with the whole orbit 1,000 px wide, and 0.34° tangent error close + up. Mercury through Earth stay under 0.1 px. +- **Spacecraft in planet-relative legs → rejected.** With accurate planets, heliocentric + samples are enough. That decision belongs to the mission-trails spec. + +**Prep, as a separate PR (ruled 2026-10-04):** + +- **P1: trails anchor on the snapshot body.** `orbitTrailsPass` takes + `centreMpc = state.positionMpc − keplerianPositionMpc(propagated) + centerOffsetMpc` instead + of `focus + centerOffsetMpc`. + - This is behaviour-neutral today, apart from f64 rounding. + - Once this lands, the body sits on its own trail by construction rather than by coincidence. + - Any later change to a body's position (correction, pair reflex, a moon's parent) moves the + trail with it. + - The 18–20-digit trail tests (`orbitTrailsPass.test.ts:345-378`, `:511-573`) move to a + tolerance that f64 can actually hold. + - The `orbitalElements.ts` header line about the invariant is rewritten to name the new + mechanism. +- **P2: exact obliquity.** `OBLIQUITY_DEG = 23.4392911` (IAU 1980 / J2000 mean obliquity). + - The obliquity literals in tests follow: `orbitPlaneFrames.test.ts:53`, + `orientationFrames.test.ts:54`, and the comments in `propagateElements.test.ts:94` and + `earthTerminator.test.ts:17`. So do the `23.44°` comments in `rotationLookAt.ts` and + `defaults.ts`. + - The fixture `tests/fixtures/bodyStatesJ2000.json` and the camera golden traces are + re-recorded, under this ruling, with `SETTLE_GOLDEN_RECORD=1` and `DRIVER_GOLDEN_RECORD=1`. + +## 3. Data shapes + +```ts +// src/@types/scene/EphemerisCorrection.d.ts — one planet's DE − Kepler correction, km +export type EphemerisCorrection = { + startJd: number; endJd: number; // fitted span; outside it the edge value is held + polyKm: readonly [Vec3, Vec3, Vec3, Vec3]; // cubic in τ = 2(t−start)/(end−start) − 1 + // flat (ω rad/day, cos x,y,z, sin x,y,z) per term; phase measured from startJd + terms: readonly number[]; +}; + +// src/@types/scene/BarycentricPair.d.ts — the primary's element row describes the pair barycentre +export type BarycentricPair = { primaryId: string; secondaryId: string; secondaryMassFraction: number }; + +// src/data/bodies/planetEphemerisCorrections.generated.ts (GENERATED — DO NOT EDIT) +export const PLANET_EPHEMERIS_CORRECTIONS: Readonly>; + +// src/data/bodies/barycentricPairs.ts +export const BARYCENTRIC_PAIRS: readonly BarycentricPair[] = + [{ primaryId: 'earth', secondaryId: 'moon', secondaryMassFraction: 1 / 82.30057 }]; +``` + +**Frame and time are pinned by construction, not converted at runtime.** +- **Frame.** The fit tool computes `DE − app Kepler` in the app's own equatorial frame, so any + residual frame bias is absorbed into the correction. +- **Time.** Horizons is fetched with `TIME_TYPE='UT'`, which is verified to be accepted for + vector tables, so the TDB−UTC gap (~69 s, ~2,000 km at Earth) is absorbed as well. `simDays` + stays a UTC Julian date. +- **Planet targets.** Mercury and Venus use their planet centres (199, 299). Earth through + Neptune use their system barycentres (3–8), because the planet-centre wobble caused by moons + is ≤ 300 km. + +## 4. Runtime + +- **`src/utils/orbit/ephemerisCorrectionMpc.ts`** — + `(c: EphemerisCorrection, simDays: number) => Vec3`. It clamps `simDays` to + `[startJd, endJd]` (the ruled freeze-at-edge), sums the polynomial and the terms in f64 km, + then converts to Mpc. The conversion happens before the sum joins an au-scale position, as in + the site-offset precedent at `deriveBodyStates.ts:88-94`. +- **`deriveBodyStates` 1b:** + `position = focus + kepler(el) + correction(el.id) − pairReflex(el.id)`. + - `pairReflex` is `secondaryMassFraction · keplerianPositionMpc(propagateElements(secondary))` + for a primary listed in `BARYCENTRIC_PAIRS`. + - The secondary's offset is computed inline, because `FOCUS_ORDER` resolves `earth` before + `moon`. + - Dependants of `earth` (the Moon, Hubble, the easter eggs, Earth sites) follow the + reflexed Earth automatically. So Moon = EMB + (1−k)·geo, which is physically right. +- **Cost:** about 590 sinusoid evaluations per new `simDays`, a few µs. The existing one-deep + memo makes paused frames free. Trails add nothing, because P1 reads the snapshot. + +## 5. Tools + +- **`tools/fetch/fetchHorizonsPlanets.ts`** (`npm run fetch-horizons-planets`). + - Fetches 1-day equatorial heliocentric vectors, 1900–2100, in 50-year chunks with retry. + - Writes them to `data/raw/horizons/planets/.csv`. + - The output is gitignored, registered in `rawDataRegistry.ts` with a README next to it + that records the exact API query. +- **`tools/bodies/buildPlanetEphemeris.ts`** (`npm run build-planet-ephemeris`). + - Subtracts the app's Kepler position using the `src/` propagation, imported as + `buildPlanetFacts` imports `src` types. + - Fits each planet greedily: cubic polynomial, then sinusoids. Each frequency is the + zero-padded FFT peak of the remaining residual, and a Gram–Schmidt step refits all + coefficients exactly by least squares. + - Stops once a planet is ≤ 900 km, leaving margin. + - **Verifies every planet on the dense 1-day grid** and throws above 1,000 km. + - Writes the generated file with a banner plus prettier. + - Helpers go one function per file under `tools/utils/math/` (`fft.ts`, + `fitSinusoidSeries.ts`). +- **Regeneration rule:** any edit to a planet's element row or to the frame requires + re-running the build tool. The test in §7 fails until it is re-run. + +## 6. Docs and backlog + +- `scenePlanets.ts:3`: rewrite the stale "positions via keplerianPositionMpc" header. +- `docs/backlog/2026-08-16-barycentric-orbit-pairs.md`: note that the pair table now exists. + Pluto's wobble becomes one `BARYCENTRIC_PAIRS` row; the minor moons still need the invisible + node. The BACKLOG line stays and its clause is updated. +- `docs/DATA.md`: add a Horizons fetch entry. +- `orbitReachByRegion` gets no margin for the correction. ≤ 8M km is negligible against + Pluto's ~50 au reach. Recorded here, not backlogged. + +## 7. Tests + +- **`tests/data/bodies/planetEphemeris.test.ts`.** + - Fixture: a committed Horizons fixture, `tests/fixtures/horizonsPlanets.json`, with 6 dates + × 8 planets, UT, equatorial. + - `deriveBodyStates` must be ≤ 1,000 km from each fixture vector. + - It fails if anyone edits a planet's elements or the frame without regenerating. + - Earth is checked against Horizons 399 (Earth centre) to ≤ 1,000 km plus the Moon-model + error. The tolerance is stated in the test after measurement. +- **`ephemerisCorrectionMpc.test.ts`:** a clamped date returns exactly the edge value + (freeze), with continuity across `startJd` and `endJd`. +- **Pair reflex:** + - Earth–Moon distance is unchanged. + - The EMB, computed as (1−k)·Earth + k·Moon, equals the old Earth position. +- **Re-recorded:** + - `tests/fixtures/bodyStatesJ2000.json` + - the camera golden traces + - `tools/perf/perfScenarios.ts:52` `EARTH_TARGET` + - the 2024-04-08 eclipse test keeps its 2° tolerance, and should tighten. + +## 8. PR sequence + +1. **Prep PR** (separate, ruled): P1 and P2, each its own commit. They are behaviour-neutral + except for the obliquity. +2. **Feature PR**, built on top of it: + - fetch tool and raw-registry entry + - fit tool and helpers + - generated corrections + - pair table and the `deriveBodyStates` term + - tests, docs and backlog edits diff --git a/package.json b/package.json index e16b0b2b75..0f0026f330 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "build-stars-rs": "cargo run --manifest-path tools/stars-rs/Cargo.toml --release -- --out public/data && npm run build-data-manifest", "build-famous": "tsx tools/famous/buildFamous.ts && npm run build-data-manifest", "build-famous-stars": "tsx tools/famous/buildFamousStars.ts && npm run build-data-manifest", + "build-planet-ephemeris": "tsx tools/bodies/buildPlanetEphemeris.ts", "build-planet-facts": "tsx tools/bodies/buildPlanetFacts.ts", "build-famous-hires": "tsx tools/famous/copyHiResToPublic.ts", "build-famous-thumbs": "tsx tools/famous/buildFamousThumbs.ts", @@ -98,6 +99,7 @@ "fetch-famous-images": "tsx tools/famous/fetchFamousImages.ts", "fetch-gaia": "tsx tools/fetch/fetchGaia.ts", "fetch-height": "tsx tools/fetch/fetchHeightSources.ts", + "fetch-horizons-planets": "tsx tools/fetch/fetchHorizonsPlanets.ts", "fetch-hyperleda": "tsx tools/fetch/fetchHyperLeda.ts", "fetch-milliquas": "tsx tools/fetch/fetchMilliquas.ts", "fetch-skraafoto": "tsx tools/fetch/fetchSkraafoto.ts", diff --git a/src/@types/scene/BarycentricPair.d.ts b/src/@types/scene/BarycentricPair.d.ts new file mode 100644 index 0000000000..be675fbfed --- /dev/null +++ b/src/@types/scene/BarycentricPair.d.ts @@ -0,0 +1,12 @@ +/** + * BarycentricPair — a primary whose element row describes the pair's barycentre, + * not the primary itself: `deriveBodyStates` subtracts the secondary's reflex, + * `secondaryMassFraction` × the secondary's primary-relative offset. + */ + +export type BarycentricPair = { + readonly primaryId: string; + readonly secondaryId: string; + /** The secondary's share of the pair mass, m₂ / (m₁ + m₂). */ + readonly secondaryMassFraction: number; +}; diff --git a/src/@types/scene/EphemerisCorrection.d.ts b/src/@types/scene/EphemerisCorrection.d.ts new file mode 100644 index 0000000000..7fb3c31920 --- /dev/null +++ b/src/@types/scene/EphemerisCorrection.d.ts @@ -0,0 +1,19 @@ +/** + * EphemerisCorrection — one planet's fitted `Horizons − Kepler` residual over a span, + * in km: a cubic in normalised time plus a sum of sinusoids. `ephemerisCorrectionMpc` + * evaluates it; `tools/bodies/buildPlanetEphemeris.ts` generates it. Outside the span + * the edge value is held (simDays is clamped), so dates past either end stay finite. + */ + +import type { Vec3 } from '../math/Vec3'; + +export type EphemerisCorrection = { + /** Fitted span start, UTC Julian date. */ + readonly startJd: number; + /** Fitted span end, UTC Julian date. */ + readonly endJd: number; + /** Coefficients of τ⁰..τ³ in km, τ = 2(t − startJd)/(endJd − startJd) − 1. */ + readonly polyKm: readonly [Vec3, Vec3, Vec3, Vec3]; + /** Flat, 7 per term: ω (rad/day), cos x,y,z (km), sin x,y,z (km); phase ω·(t − startJd). */ + readonly terms: readonly number[]; +}; diff --git a/src/data/bodies/barycentricPairs.ts b/src/data/bodies/barycentricPairs.ts new file mode 100644 index 0000000000..09ec790461 --- /dev/null +++ b/src/data/bodies/barycentricPairs.ts @@ -0,0 +1,25 @@ +/** + * BARYCENTRIC_PAIRS — primaries whose element row is the pair barycentre. Earth's + * row is fitted to the Horizons Earth–Moon barycentre (target 3), so Earth itself + * sits k·(Moon offset) the other way. k = 1 / (1 + M⊕/M☾), M⊕/M☾ = 81.30057 (DE440). + */ + +import type { BarycentricPair } from '../../@types/scene/BarycentricPair'; +import { ORBITAL_ELEMENTS } from './orbitalElements'; +import { findByIdOrThrow } from '../../utils/object/findByIdOrThrow'; + +const BARYCENTRIC_PAIRS: readonly BarycentricPair[] = [ + { primaryId: 'earth', secondaryId: 'moon', secondaryMassFraction: 1 / 82.30057 }, +]; + +// Primary id → its secondary's element row and mass fraction, resolved at import so +// `deriveBodyStates` does no lookup per instant. A misspelt secondary throws here. +export const BARYCENTRIC_REFLEX_BY_PRIMARY = new Map( + BARYCENTRIC_PAIRS.map((pair) => [ + pair.primaryId, + { + secondary: findByIdOrThrow(ORBITAL_ELEMENTS, pair.secondaryId, 'BARYCENTRIC_PAIRS'), + k: pair.secondaryMassFraction, + }, + ]), +); diff --git a/src/data/bodies/planetEphemerisCorrections.generated.ts b/src/data/bodies/planetEphemerisCorrections.generated.ts new file mode 100644 index 0000000000..2bd809cbd3 --- /dev/null +++ b/src/data/bodies/planetEphemerisCorrections.generated.ts @@ -0,0 +1,472 @@ +// src/data/bodies/planetEphemerisCorrections.generated.ts +// !!! GENERATED FILE — DO NOT EDIT BY HAND !!! +// Regenerate with: npm run fetch-horizons-planets && npm run build-planet-ephemeris +// Source of truth: data/raw/horizons/planets/ (JPL Horizons) minus orbitalElements.ts +import type { EphemerisCorrection } from '../../@types/scene/EphemerisCorrection'; + +export const PLANET_EPHEMERIS_CORRECTIONS: Readonly> = { + // mercury: 46 terms, max 893 km vs Horizons (1-day grid) + mercury: { + startJd: 2415020.5, + endJd: 2488069.5, + polyKm: [ + [-434.9, 402, 269.9], + [-511.4, -15.9, 42.8], + [2.9, 6.7, -0.4], + [-3.6, 9.1, 4.7], + ], + terms: [ + 0.0714251270098, -679.6, -2487, -1250.9, 2717, -462.6, -518.4, 0.068385465252, 445.6, 871.9, + 409.6, -948.2, 358.8, 276.5, 0.0713720514238, -283.4, 1265.1, 717.6, -1414.1, -333.7, -33.4, + 0.0744629154726, -458.2, 722.5, 430, -812.4, -483.5, -154.6, 0.142844334988, 524, 491.9, + 210.7, -522.6, 443.2, 290, 0.0029008342251, -513.1, -401.3, -160.9, 429.5, -445.4, -264.8, + 0.0559244267002, 166.1, 522.5, 252.7, -573, 125.1, 119.5, 0.0714978857854, 212.4, 433, 207.2, + -468.7, 172, 140.7, 0.0869248224442, -134.3, 357.7, 199, -392.6, -150.4, -36, 0.0124627998013, + -154.4, -223.1, -99.5, 246.4, -129.4, -90.2, 0.142899897771, -278.9, -111, -30, 109.4, -244.4, + -142, 0.14588834796, -200.9, -187.2, -77.5, 200.5, -169.9, -111.7, 0.0154999917108, 245.9, + -116.5, -94.3, -144.1, -200.7, -96.3, 0.00303792367563, 189.2, -168.8, -106.6, -194.3, -172.7, + -70.8, 0.139949181915, 176.6, -117.7, -83.8, 141.4, 168.8, 70.2, 0.139811347793, -199.8, 32.2, + 37.5, -46.3, -180.4, -91, 0.214267665718, -176.4, 58.1, 49.1, -74.3, -160.6, -78.3, + 0.15834964623, -142.1, -92.4, -34.2, 95.1, -123, -75.6, 0.0713061381868, 81.1, 412.6, 215.3, + -447.9, 49.8, 78.3, 0.00145306062568, -125.2, -78.6, -25.9, 68.8, -87.5, -50.6, + 0.0712342328374, 234.8, 162.4, 62.4, -164.2, 202.8, 130.1, 0.0279621812316, 121.1, -23.9, + -23.1, 29.9, 110, 49.8, 0.14277406341, -50.9, -125.7, -62.3, 138.1, -39.1, -35, + 0.127349950889, -110.9, 0.1, 11, -5.6, -99.6, -51.2, 0.0589620607692, 95.2, 37.4, 22.7, -18.9, + 97.6, 60, 0.0743219807818, 99.3, -44.6, -29.4, 52.8, 91, 41.4, 0.0688071069501, 94.6, 31.1, + 10.4, -31.2, 82.5, 49, 0.071162327488, 154.8, 77.3, 23.5, -75.7, 136.3, 83.6, 0.130386564907, + 53.8, -63.7, -38.6, 73.3, 52.3, 18.6, 0.0740405697919, -82.6, 6.5, 17.9, -17.5, -81.7, -40.2, + 0.0404253811578, 73.7, -37.6, -25.9, 44.6, 67.5, 30, 0.0344042079601, 48.6, -57.2, -32.9, + 65.9, 45.9, 18.1, 0.0728675876251, 14.9, -73.5, -33.2, 78.6, 15.5, 2.6, 0.00116867733012, 8.7, + -66.9, -34.8, 75, 9.9, -1.8, 0.0699747129708, 28.4, -70.5, -26.7, 66.5, 22.8, 7.3, + 0.211374008935, 21.1, 62.8, 30.7, -68.8, 15.4, 16, 0.00434991596434, -10.6, 63.3, 33.2, -71.2, + -12.1, 0.1, 0.217313670374, 60.6, -30.9, -22.8, 37.7, 55.4, 25.4, 0.214325878207, 42.2, -62.9, + -38, 72.8, 40.9, 14.3, 0.0716529123308, -61.5, 34.1, 24.9, -41.5, -56.9, -25.9, + 0.114887310026, -63.1, -21, -4.5, 19.8, -57.1, -29.5, 0.14139626632, 52, -13.7, -13, 18, 48.4, + 22.5, 0.00261669932529, -4.8, -42.8, -21.7, -49.6, 3.7, 5.9, 0.0710904221385, 70, 11.9, -3.1, + -8.5, 63.4, 36.1, 0.229774401504, 35.5, -26.8, -18, 31.8, 33.2, 14.4, 0.285691769155, -13.4, + -41.4, -20.7, 45.8, -9.9, -10, + ], + }, + // venus: 40 terms, max 893 km vs Horizons (1-day grid) + venus: { + startJd: 2415020.5, + endJd: 2488069.5, + polyKm: [ + [-1077.2, 107.4, 116.9], + [-626.1, 695.1, 352.8], + [-19.3, -59.5, -24.8], + [-98.8, 75.1, 41], + ], + terms: [ + 0.0064418294471, -3292.2, -2411.7, -908.6, 2587.7, -2951.1, -1451.4, 0.0279612087988, -500.1, + 2571.9, 1294.4, -2870.8, -518.8, -81.2, 0.00431861455076, -2640.1, 1056.6, 682.4, 1213.4, + 2422.9, 1004.8, 0.0494830514867, 646.2, -1462.1, -689.8, 1614, 623.7, 190.9, 0.0280191178287, + -436.7, -1551.8, -695.4, 1695.3, -336, -318.1, 0.0172022263818, -308.9, 1397.5, 618.9, + -1534.3, -290.5, -97.2, 0.00145163189281, -631.7, -881.5, -357.1, 950.1, -568.9, -312.1, + 0.0278812992423, 930.2, -778.5, -410.3, 881.9, 851.6, 389, 0.0387225731644, 659, 696.6, 292.3, + -753.8, 598.2, 278, 0.0236434768591, -653.9, -750.8, -301, 697, -543.5, -269.6, + 0.0602435218932, 833.3, -41.5, -78.3, 69.8, 762.3, 339.6, 0.0322814533429, 141.6, -723.2, + -332.6, 803.4, 148.5, 23.5, 0.00212241136646, -130.8, 542.4, 250.2, -590.1, -151.9, -32.4, + 0.0150786355292, 7.2, -409.4, -196, -444.2, -34.2, 56.7, 0.0258354982192, -194.1, 313.3, 9.3, + -185.7, -400.7, -264.1, 0.0544719027579, 47.6, -366.5, -167.1, 401.9, 52.1, -1, + 0.0265148258307, -159.8, 339.2, 148.9, -390.3, -151.9, -49.2, 0.0559439027836, 67.6, -367.1, + -169.3, 402.7, 71.2, 6, 0.0250615219391, 265.8, -242, -215.5, -270.6, -266.7, -69.8, + 0.0294112550103, 321.9, 212.9, 81.1, -227.5, 293.3, 134.1, 0.0300903971784, 364.7, 29.9, + -15.9, -16.9, 334.7, 155, 0.0284914414854, 18.8, -255.2, -110.3, 280.3, 19.7, -9.4, + 0.0274398237101, -214, -186.6, -70.4, 199.2, -190.7, -98.1, 0.000525007371695, 259.2, 133.7, + 43.6, 106, -184.2, -89.5, 0.0128833976072, 223.1, -47.2, -31.3, 57, 205.4, 88.5, + 0.0538032829888, 156.5, 142.8, 55.6, -154.2, 138.2, 71.4, 0.0029001508618, -137.2, 103.1, + 53.5, -111.2, -140.8, -56.5, 0.0278213781178, 42.8, -271.9, -156.6, 309.9, 46.1, 16.9, + 0.0558824407337, -124.3, 63.5, 36.6, -73.4, -115.8, -47.5, 0.0430412903279, -120, 25.7, 17.4, + -32.2, -107.5, -46.8, 0.0277554648808, -124.6, -80.9, -43.9, 87.9, -107.9, -59.8, + 0.0249290139242, 85.7, 59.7, 21.3, -65.2, 77.3, 38.5, 0.0309958853144, -37.2, 81.7, 39, -85.2, + -42.9, -12, 0.0710037289495, -47.7, -74.5, -29.4, 79.8, -40.3, -26.6, 0.0124631273406, 51.7, + 69.8, 27.7, -74.4, 45.6, 24.7, 0.0564460007794, -54.4, 62.5, 31.7, -70.3, -51.7, -18.8, + 0.00863452862343, 65.4, 55, 17.7, 34.4, -70.4, -36.5, 0.0365988985097, -19.5, 58.3, 2.3, 25.5, + 77.4, 44.2, 0.0281738805661, 59.6, -50.4, -28, 57, 56.2, 21.9, 0.0809866299726, -64.7, -19.5, + -4.6, 19.3, -58.8, -27.7, + ], + }, + // earth: 50 terms, max 900 km vs Horizons (1-day grid) + earth: { + startJd: 2415020.5, + endJd: 2488069.5, + polyKm: [ + [-184.2, 540.7, 232.8], + [-76.7, -359.5, -169.4], + [60.2, 64.7, 24.8], + [27.8, -12, -7.3], + ], + terms: [ + 0.00144870071269, -1842.4, -3160.4, -1363.8, 3413.1, -1681.9, -738.7, 0.00431854489572, + 2870.3, -1170.6, -558.3, -1430.7, -2696.4, -1158.1, 0.00644196744226, 1706.6, 1196.8, 563.5, + -1324.9, 1580.1, 629.6, 0.0143020000072, -1549.8, -424.7, -313.9, -523.5, 1491.1, 629.2, + 0.0329556786128, -1094.9, 792.4, 345.4, -865.1, -1004.8, -433.1, 0.0172364219811, -525.3, + 1119.4, -339.3, -891.4, 86.5, -1528.5, 0.0279620534637, 1304.1, -311, -111.7, 331.9, 1200.3, + 520.7, 0.00109037104107, -665.8, 796.1, 347.7, -872.1, -638.8, -276.4, 0.0128818827343, + -1129.1, 314.7, 123.5, -40.1, -818.3, -344.8, 0.0171733942893, 296.8, -614.2, 388.5, 408.8, + 839.4, -1193.5, 0.0157511739764, -617.4, -701.6, -296.5, 809.2, -512.2, -233.7, + 0.0186529628609, -809.1, 370.2, 175.6, -407.8, -745.4, -313.2, 0.00289982860626, -846.6, 70.9, + 35.4, -77.4, -774.5, -334.4, 0.0387225972832, 536.6, 569.5, 238.3, -617.2, 486.3, 227, + 0.0215201330643, -608.3, -434.4, -195.6, 483.4, -541.3, -229.3, 0.00211673392742, 360.5, + -624.7, -269.8, 683.8, 313.9, 136.3, 0.016112442114, -227.7, 585.8, 248.8, -636.8, -215.4, + -95.6, 0.0182919263861, 25.1, -559.7, -239.1, 610, 21.8, 12.1, 0.0150808568338, 64.6, 696.7, + 356.4, 80, 194.1, -70.3, 0.0171074810524, -63.6, -422.3, -117, 437.2, -60.2, -15.7, + 0.0333131144222, -95, -337.6, -147, 367.1, -87.2, -38.6, 0.0315042298422, -280.7, -155.1, + -68.7, 159.9, -259.8, -113.5, 0.0173052207634, -185.2, 405, 1.6, -372.1, -152.9, -114.3, + 0.0193183026304, 248.8, -177.5, -77.1, 193.1, 220.8, 93, 0.0487059811989, -298.8, -30.1, + -13.5, 32.5, -273.1, -118.5, 0.0322875379952, 20.5, 264.5, 115, -293, 18.4, 4.6, + 0.00058308855423, -3.1, -211.9, -91.7, 228.8, 0.8, -0.2, 0.00218712104525, 14, -216.8, -95.6, + 192.6, -115.1, -50.4, 0.0343949732763, 67.5, -175.7, -74, 190.1, 63.8, 26.3, 0.0102364238981, + -10.6, 175.4, 77, -193.4, -10.2, -3.2, 0.0128231206487, 197.2, 43.1, 27.5, -6.2, -192.2, + -85.3, 0.0150162338064, 156.5, -98.5, -41.4, 71.7, 96.1, 45.9, 0.0258384966024, -94.7, -74.5, + -50.7, -36, 146.6, 48.1, 0.0173831182252, -122.9, 79.6, -11.2, -69.3, -113.9, -44, + 0.00914454452934, 62.6, -102.5, -47, 112.4, 56.3, 23.3, 0.0241681687338, 39.9, -101.8, -44.9, + 110.9, 38.7, 16.4, 0.0193844837844, 42.7, 89.1, 47.3, -137.1, 17.5, -3.9, 0.0300538924286, + 125.4, -56.9, -28.4, -23, -66.6, -26.1, 0.0170175993656, -49.2, -98.1, -38.6, 106.8, -48.8, + -8.9, 0.0166133134942, 47.5, -75, -35.5, 80.9, 48.6, 15, 0.00862977496737, -68.6, -54.2, + -18.1, -11.8, 82.9, 39.6, 0.0113260583918, -32.1, -74.9, -32.6, 87.1, -23.8, -10.7, + 0.00139616220145, -114.3, 25.5, 10.9, -28.4, -103.1, -44.8, 0.0177889725634, 6.3, 83.3, 36.1, + -88.9, 6.3, 4.2, 0.00326823425873, -1.7, 74.1, 32.4, -83.1, -0.5, 0.1, 0.032219588658, -46, + 62.5, 27.1, -67.2, -43.3, -18.7, 0.0338258832744, -16.7, 68.6, 29, -75.6, -15.2, -7.1, + 0.0252591206376, 5.9, 68.1, 30.5, -74.4, 5, 3.1, 0.0069660369399, -56.8, 41.4, 15.4, 43.6, 54, + 23.5, 0.0160302702584, -29.7, -40, -35.5, -68.5, 28.9, -5.5, + ], + }, + // mars: 118 terms, max 899 km vs Horizons (1-day grid) + mars: { + startJd: 2415020.5, + endJd: 2488069.5, + polyKm: [ + [-3343.6, -375.8, -88.4], + [-3354.3, 1438.1, 721.9], + [88.4, 2.3, -19.7], + [-227.9, -54.8, -15.6], + ], + terms: [ + 0.00145100844461, -10317, -15869.6, -6897.3, 17584.4, -9091.8, -4556.5, 0.00290053832362, + 14319.3, -8338.9, -4147.1, 9276.3, 13104.7, 5669.1, 0.00624543424892, 12358.4, 842, 44, 613.2, + -11180, -5653.9, 0.00805648582952, -4596.8, 6048.5, 2805.3, -6554.3, -4057.8, -1698, + 0.0168401602229, -7903.3, 944, 633.2, -1102.6, -7186.9, -3229.7, 0.0153909949766, 639.9, 6630, + 3045.9, -7436.1, 575.2, 432.3, 0.0102359478328, 1862, -6216.1, -2854.7, 6828.6, 1754.1, 669.3, + 0.00108716080488, 3742.6, -3510.1, -1754.1, 3958.4, 3722.3, 1602.3, 0.00217991329283, -1736.1, + 4653.4, 2193.7, -5166.4, -1568.2, -573.7, 0.00917622357656, 4017.8, 2551.3, 909.7, -2590.2, + 3774.5, 1638.2, 0.00861523311113, 3685.4, 517, 140.2, -558.3, 3357.2, 1551.9, + 0.00968057540469, 2816.6, 1897.7, 782.7, -2121.9, 2560.5, 1170.9, 0.0245371266403, 2297.9, + 2278.1, 989, -2496.3, 2058.3, 1015.5, 0.0172029623398, -373.3, 2901.3, 1338.6, -3202.8, + -362.4, -82.2, 0.00696634249155, 3504.4, -1540.7, -716.2, 519.7, 1738.1, 780.3, + 0.0161115334365, 1389, -1894.2, -915, 2097.1, 1309.9, 542.8, 0.00434951222842, -2282.3, 752.7, + 404.1, -899.4, -2098.1, -923.6, 0.0113260060724, 756.5, 2125.5, 948.8, -2329.2, 667.9, 358.5, + 0.0105955354049, -573.9, -1958, -835.6, 2116.2, -518.2, -272.1, 0.00769343411762, -1084.3, + 1621.1, 633.3, -1617.1, -1194.8, -469.9, 0.00479642429474, -1719.6, -671.4, -271, -697.1, + 1775, 871.4, 0.00326896412976, -0.9, -1716.9, -790.3, 1960.2, -67.8, -88.5, 0.0139423810611, + 502.4, -1949, -862.7, 270, -1104.5, -472.6, 0.0182928178276, -1052.4, -843.7, -366.2, 918.6, + -946.7, -461.2, 0.00117210459582, 1187.1, 568, 221.9, -598.5, 1111.4, 513.3, 0.0120467382024, + 332.7, -1064.6, -491, 1173.1, 317, 110.8, 0.000548074358938, 473.2, -883.9, -414.8, -773.1, + 678.5, 340.1, 0.0259864441106, -692.9, 741.9, 357.2, -824.1, -637.1, -269.3, 0.00922785317716, + -786.5, 189.6, 109, -195.4, -757.2, -297.4, 0.015023602872, -919.8, 587.1, 308.7, -544, + -553.8, -236.3, 0.00587002079963, -778.3, 437.5, 191.9, 745.2, -320.3, -144.8, 0.019376108819, + -184, -652.8, -293.2, 676.4, -137.5, -77.5, 0.0087294230245, 619.8, -283.9, -148.3, 313.1, + 547.6, 241.1, 0.000607200728573, -658.2, -507.2, -212.3, 324.5, -217.4, -104.4, + 0.00910801092804, 548.8, -906.7, -324.8, 870.2, 574.2, 117.4, 0.0182399903059, 174, 601.1, + 271.6, -664.6, 150.4, 80.1, 0.0230881418999, -413, -283.8, -122.2, 310.3, -507.7, -242.4, + 0.00954708436733, -213.6, 533, 254.8, -589, -193.6, -70, 0.0177309116274, 256.2, 390.9, 171, + -427.3, 226.3, 114.9, 0.0336833285041, 523, 64, 15.2, -66, 472.4, 219.1, 0.0171208633652, + 450.8, 222.5, 93.6, -242.9, 403.6, 193.1, 0.0124276706936, -390.3, -166.6, -67, 180.3, -392.3, + -180.9, 0.00798351133585, 179.3, 234, 188.1, 433.6, -60.9, -35.4, 0.00545227325286, -314.9, + -219.8, -92.8, 247.7, -277, -134.8, 0.00904409506187, -550.4, -345.2, -94.9, 326.9, -515.3, + -220.6, 0.0263537387097, 228.2, 229.5, 99.3, -249.5, 202.7, 102, 0.0128461960626, -282.8, + 42.4, 35.1, 54.2, -191.6, -92.8, 0.00333327463161, 262.8, -35.5, -19.9, -63, -258.6, -121.3, + 0.00151001233811, 209.1, -211.2, -97.3, 323.1, 16.4, -2.1, 0.027959106796, 245.3, -31.8, + -29.5, 38.9, 225.4, 99, 0.00850084206697, 15.7, -246.4, -102.9, 263.5, -11.5, -12.9, + 0.0167859043507, -64.4, 219.5, 101.8, -246.5, -64.1, -22.9, 0.00579960514359, 160.5, 159, + 64.8, -105.5, 225.2, 105.6, 0.0124955518347, -42, 111.4, 42.4, 114.8, 302.5, 120.8, + 0.0204709363978, 200.6, 124, 51.5, -132.4, 188.3, 88.7, 0.0134967288054, -33.6, 207.2, 95, + -228.5, -31.1, -8.3, 0.00897218971244, -284.9, 105.7, 66.1, -137.1, -273.7, -104.2, + 0.0017481658, -206.1, -69.8, -24.7, 85.1, -189.5, -87.6, 0.00654138292794, 216.9, 58.5, 21.1, + -83.8, 208.9, 99.4, 0.00048735847945, -124.6, 97.9, 49.8, 443.4, 291.8, 118.4, + 0.00369618600064, -1.3, 159.8, 71.2, 243.7, -65, -30.8, 0.0197429192863, -153.4, -145.8, + -62.4, 156.3, -145.3, -68.3, 0.018359832555, 273.6, 6.9, 1.1, -12.1, 249.1, 117.8, + 0.00945502297497, -132.1, 116, 56.6, -129.6, -119.5, -51.1, 0.0216326432124, -14.5, -91.3, + -35.6, -208.7, 142.8, 66.4, 0.00123038042365, -164.3, 55.2, 29, -47.8, -140.1, -63.4, + 0.000407463646747, -91.2, 13.3, 7.8, 174.2, 244.6, 106.3, 0.0274443002631, -180.8, 74.2, 38.8, + -84.9, -166.8, -75.6, 0.0322326218563, -122.6, 127.1, 61.6, -148.2, -107.3, -47.1, + 0.00761381722163, -175.6, -54.6, -24.6, 58.3, -156.3, -72.2, 0.0252579046171, -40.3, -158, + -76.5, 178.2, -32, -25.8, 0.00974716958972, -223.9, 20.9, 22.5, -40.7, -203.4, -95.3, + 0.0117483003772, 134.3, 72.4, 26.5, -140.5, 122.1, 65, 0.00981907493916, -132.5, 105.6, 48.5, + -125.9, -118.4, -54.1, 0.0106717373957, -152.5, -50.2, -17.5, 71.4, -132.7, -67.2, + 0.0153398078788, 160.1, 14.6, 2.8, -20.5, 139.6, 64.8, 0.00295610881002, 111.8, 54.7, 20.6, + -83, 104.7, 48.1, 0.0088763159132, -158, 21.9, 17.7, -42.5, -146.5, -61.8, 0.0081173150026, + -173.5, 1.4, 7.4, -15, -203.3, -96.7, 0.00101466437529, 131.8, -98.3, -51.1, 127.2, -56.9, + -30.3, 0.00261026142441, -68.5, -95.8, -41.8, -142.4, 41.4, 20.8, 0.0351317441242, 0.5, 123, + 56.3, -136, -1.9, 2.7, 0.00135821215591, 30.9, 126, 56.1, -135.7, 29.5, 16.5, 0.017680726477, + -98.6, -90, -37.8, 101, -87.2, -42.4, 0.010170612203, -68, -119.5, -53.5, 151.1, -74.7, -38.4, + 0.00440220528194, 162.9, -54.8, -30.6, 20.2, 190.7, 88.5, 0.0262606802724, 127, -0.9, -3.7, + 2.3, 113.7, 52.2, 0.0181680849565, 96.1, 76.6, 33.5, -84.5, 83.5, 39.3, 0.0169137360831, + 137.2, 52.2, 20.2, -55.3, 125, 58.4, 0.00933171645967, -126.9, -21.1, -8.6, 29.5, -122.4, + -48.8, 0.0211892072108, -36, -95.6, -42.5, 102.5, -31.4, -17.5, 0.00739122567286, -63.8, + -45.5, -29.4, -72.5, 88.5, 47.6, 0.0103144229019, 100.4, 25.1, 5.3, -32.2, 103.2, 46.1, + 0.00844488381661, -21, -108.3, -42.4, 116.8, -41.1, -15.8, 0.0061918495344, 38.4, 88.2, 42, + 112.2, -31.7, -16.5, 0.0273959381336, 121.4, 19.4, 6.2, -17.7, 109, 50.3, 0.0285220152133, + -70.1, -60.5, -26, 31.6, -62.6, -29.5, 0.0165594111508, -76.7, 11.4, 5.6, 23.5, -81.3, -38.6, + 0.0178005687261, -88.3, -62.1, -26.8, 65.7, -81.1, -39.1, 0.0028362665609, -26.8, 92.8, 42.4, + -87.6, -51.5, -20.9, 0.0188529572303, -7.6, -58.8, -24, 62.3, -13, -7.7, 0.0138857219236, + -11.7, 57.3, 27.5, -101.4, 43.1, 23.6, 0.0428289279878, 60.1, -34.1, -17.4, 38, 54.9, 24.2, + 0.0268739216829, 59.1, 38.1, 15.8, -39.3, 52.7, 25.1, 0.00776577773871, 71.2, 1, 2.4, -1.2, + 78.1, 36.1, 0.000687095561275, -113.9, -11.8, -1.2, -22.3, 1.3, 2.1, 0.00474575306249, -95.9, + 26.2, 14.1, -14.1, 76.2, 33.9, 0.0123677201023, 81, -15.2, -9.6, 35.9, 62.3, 27.9, + 0.0184317379045, 76.5, 41.7, 18.8, -48, 71.4, 33.9, 0.00158990717081, 25.7, -58.7, -26.5, 59, + 29.3, 10.7, 0.000311589847504, -57.1, -6.9, -2.1, 61.1, 50.1, 21, 0.0154596501279, 35.4, + -58.3, -28.5, 65.7, 33.5, 14.2, 0.00724346554409, -16.9, -53, -23.2, 47.8, -20.1, -9.1, + 0.00447411063137, 10.3, 4.2, 1.7, -35, 76.7, 37.3, 0.0145949144491, -52.8, -4.5, -0.8, 4.8, + -49.4, -22.7, 0.0244877662232, 61.5, -30.1, -16.4, 40.1, 56.6, 25.5, 0.0194304233133, -60.5, + 39.5, 18.6, -40.8, -35.3, -15.4, 0.0219864607654, -14.2, 18, 12, 32.4, 50.8, 22.5, + ], + }, + // jupiter: 80 terms, max 882 km vs Horizons (1-day grid) + jupiter: { + startJd: 2415020.5, + endJd: 2488069.5, + polyKm: [ + [-18645.8, -52869, -22112.5], + [-26938.3, 87395.8, 37736.1], + [-79340.8, 90455.1, 40451.4], + [-78987.2, -111404.6, -45999.3], + ], + terms: [ + 0.000282980941478, -340245.5, 599165.6, 265277.6, 365996.9, 175743.9, 69500.6, + 0.00139255620479, 307831.8, -57886, -29481.7, 58524.4, 295615.5, 122177.4, 0.00116813756931, + 218938.9, 14908.9, -2754.5, -19838.5, 268490.1, 117167.8, 0.001504773829, 187193, -173938, + -79784.3, 201405.8, 182960.7, 71792.4, 0.000580193985381, 52570.7, -200494.9, -85827.6, + 162705.7, -17256.1, -13697.9, 0.00173051708285, 88428.6, -167192.2, -73879, 193962.6, 83469.1, + 30101.3, 0.00318018737772, -170075.3, -12666.1, -1560.2, 13954, -158490.2, -68355.8, + 0.0023187329175, 96141.1, 40928.3, 13950.2, -40061.9, 82420.5, 36255.9, 0.00259581844779, + -58918.6, -63710.7, -25807, 69004.1, -52108.5, -24189.4, 0.000335558297392, 78508.3, 20198.3, + 6657.7, -12516.6, 51507.1, 22159.9, 0.00134223318936, -54432, -85180.8, -33907.3, 84832.3, + -42693.8, -18290.6, 0.00155315554778, 24263.7, 45540.5, 17621.5, -39531.8, 16057.4, 8561.3, + 0.00126553414996, -39888.8, -11377.2, -4200.6, 4795.7, -27179.8, -10859.3, 0.000912825966519, + -23201.8, 1275.5, 1136.7, -3624, -23694.3, -9837.9, 0.00199972371695, -17732.2, 16630.5, + 7909.8, -2081.9, 472.4, -719.1, 0.00111213607118, -17214.1, 19500.2, 9878.3, 12016, -31073.2, + -12393.1, 0.0028885099753, -7657.4, 14385.8, 6553.4, -20994.4, -3056.3, -763.3, + 0.000201334978368, 1497.5, 19332.1, 8241.9, -21544, -13619.3, -5315.5, 0.000632767075045, + -12193.2, -12844.1, -5136.5, -4409.8, -6799.7, -2862.4, 0.00405390703748, -5477.2, 12754.1, + 5547, -12326.2, -5547.6, -2180.4, 0.00463013578705, 8110.8, 9009.6, 3680, -10369.6, 7678.4, + 3547.4, 0.00346153061428, 9777.1, 912.5, 233.5, -2727.3, 9670.6, 4241.5, 0.000843689433295, + -9992.9, 3471.1, 2306.1, 6609.8, -13645.1, -5761.3, 0.00297597680772, -11026.4, -2565.1, + -828.1, 1991.6, -10246.1, -4428.8, 0.00102584965186, -6648.9, 8506.7, 3849.3, -3345.6, -11228, + -4486.4, 0.000488956376097, -8438.4, -2631.1, -888, 12735, 1220.7, 259.1, 0.00207976638279, + 5880.7, 6816.5, 2479.8, -8841.6, 881.5, 1152.5, 0.00375667640448, -1450.3, -6448.5, -2655.4, + 4018.6, -608.8, -334, 0.000766990393901, -2070.8, 1938.7, 965.3, -1327.1, -6094.9, -2495, + 0.00237767022126, 4755.4, 2745.6, 1093.8, -4606.7, 2362.6, 1251.5, 0.00311589847535, -1161.1, + 5626, 2418.4, -5828.3, -1681.3, -567.3, 0.00268604832474, 3257.6, 6164.2, 2521.1, -5378.3, + 2705.3, 1348.2, 0.00183068567817, 2496.2, 4118.8, 1649.6, -2364.8, 3233.3, 1457.1, + 0.00247793146941, 3684.5, -521.4, -315, 361.7, 2452.5, 1090.5, 0.00163944196709, -212.2, 4802, + 1613.7, -1245.6, -2931.4, -928, 0.00432835889665, 1204.3, -1515.4, -640.5, 68, 1276.1, 573.5, + 0.00192706336474, -744.7, 2896.6, 1259.5, -750.5, 2684.6, 1085.5, 0.00325012179437, 414.7, + 3518.9, 1512.7, -4231.4, -1461.8, -473.7, 0.00222934132097, -474.3, -1299.5, -429.6, 11.8, + -1069.7, -275.4, 0.00276116541824, 2890.3, 3240.4, 1287.3, -1431.7, 1563, 806.6, + 0.00491882165944, -1669.5, 635.5, 318.6, -507.7, -1267.1, -574, 0.00410339860764, -1894.4, + 323.1, 165.2, -847.5, -3022.8, -1247.8, 0.00549833280049, 1104.9, -4.3, -26.8, -157.3, 870.8, + 381.4, 0.00440627633877, -1341, -319.1, -86.8, -500.5, -806.3, -333.4, 0.00333640821369, + -704.3, 2121.9, 939.6, -2743.9, -2564.1, -979.1, 0.00607386327836, 350.3, -718.9, -321.8, + 737.8, 149.9, 52.7, 0.00381577720991, -1651.7, 56.4, 82.2, -532.8, -1032.4, -432.6, + 0.00281868969778, -679.3, 1849.6, 889.3, -1297, -3112.7, -1248.9, 0.00419314092124, -870.1, + 768.5, 341.1, -1384.6, -2157, -861.5, 0.00214757310308, 487.4, 1198.2, 567.7, -2993.7, + -1672.5, -297.3, 0.0172057522808, -29.4, 433, 187.6, -469.6, -29.3, -12.7, 0.00457318022384, + -379.9, -945.1, -378.3, 452.7, 405.1, 158.4, 0.00364019344878, 126.8, -587.6, -246.7, 225.6, + -13.2, -12.3, 0.00578060509966, -257.3, 213.2, 103.9, -165.1, -139.7, -70.9, 0.00356650533179, + -451.4, -686.3, -268.6, 232.5, -653.6, -281.7, 0.00523814666771, -18.1, 122.6, 51, -91, 83.4, + 36.2, 0.00339393249324, -1798.8, -1293.9, -464.5, -431.3, -2297.1, -959.9, 0.0279627814176, + 255.7, -58.6, -42.6, 72.1, 234.6, 101, 0.00635717786871, 189, 51, 18.7, -136.1, 71.6, 36.4, + 0.00497585018075, -158.3, -165.8, -70.3, 295.1, -54.6, -35.3, 0.0042472093065, -1407.2, + -1269.3, -484.5, -194.2, -1319.9, -543.2, 0.00448689380461, -653.9, -857.1, -337.6, 344.8, + -101.9, -54.3, 0.00471699092279, -104.2, -317.1, -129.9, 262.8, 150.3, 55.8, 0.005186772539, + -61.3, -198.2, -75.3, -119.4, -68.5, -15.6, 0.00481286472203, -57.8, -201.1, -81, 68.8, 92.4, + 38.6, 0.00392123838907, -181.4, -78.9, -20.8, -193.6, -133.4, -50, 0.00612633577166, -195.1, + -85.1, -29.6, -37.9, -150.1, -61.4, 0.00664337987906, -33.8, 86.4, 42.4, -70.4, 28.3, 8.7, + 0.00694872646851, -39.4, -43.7, -20.6, 90.5, -68, -31.4, 0.00507172397999, -88, -79.1, -31.3, + 93.6, -13.6, -9.9, 0.00914231509402, 38.9, -53, -25.4, 60.9, 36.3, 15, 0.00544563179695, + -97.5, -87.2, -32, -42.9, 53.6, 23.3, 0.00586276614521, 40.3, -23, -10.7, 27.9, 63.8, 25.8, + 0.0075355149728, -26.9, 35.7, 18.1, -62, -18, -4.8, 0.00721281909089, 66.1, 11.1, 3.6, -35.5, + 1, 2.2, 0.00533058323794, -54.8, -30.5, -11.7, 37.4, 14.2, 4.1, 0.00619344743113, -75.4, + -65.5, -24.6, 28.3, 5.9, 1.7, 0.00571407843483, 13.4, -27.9, -13, 13.9, -52.7, -22.4, + 0.00627014647052, -34.4, -19.5, -6.6, 22.2, 34.6, 13.7, 0.0172572838638, -32.3, 8.3, 3.6, + -10.1, -31, -13.4, + ], + }, + // saturn: 86 terms, max 802 km vs Horizons (1-day grid) + saturn: { + startJd: 2415020.5, + endJd: 2488069.5, + polyKm: [ + [-333592.3, -445783.5, -173403.2], + [-33212.6, -1082781, -446731.5], + [145469.7, -629502.6, -262318.4], + [-79519.6, -159545, -56433.6], + ], + terms: [ + 0.000634140904314, -1885474.6, -222690, -5203, 6405, -1861611.5, -763310.3, 0.000304781660106, + -1049328.1, -881703, -304242.9, 835813.5, -1824986.9, -808315.8, 0.000528276169637, + -1003145.4, 50253, 57765, 55777.1, -1404431.9, -569424.3, 0.000865075521647, -749929.4, + 558411.9, 265150.5, -689069.9, -727552.8, -265562.2, 0.00145142968393, -270049.1, -601026.7, + -249853.2, 593216.3, -189930.3, -100450.1, 0.000680703974666, 203460, -525763, -224265.3, + 426779.5, 285675, 94363.5, 0.00113208290852, 62835.5, -628322.8, -261323.2, 282465.1, + -168447.1, -80749.3, 0.00118883511065, 463325.3, -10591.5, -28233.6, -17257.1, 356379.8, + 146074.6, 0.000469781616248, 193420.4, 405709.3, 152728, -195943.6, -42842.1, -16191.5, + 0.000239684498065, -218745.5, -14481.8, -2670.3, -79115.4, 245313.3, 103026.5, + 0.000393082576854, 162123.5, 77169.9, 25507.2, 87363, -34908.6, -25124.9, 0.00106419917155, + -84920.1, -72131.3, -25400.4, 63476.8, -194292.3, -83599.7, 0.000977912752235, -54018.4, + -30567.7, -9639.8, 30242.1, -103550, -44630.5, 0.000766990393985, 59039.3, -65842.1, -29585.6, + 18350.9, 76486.7, 29155.8, 0.00126553415005, 101601.7, 5171.6, -3815.8, -67864.1, 92450.1, + 41043.1, 0.00200807496494, 40949.3, -13251, -7863.3, -16860.5, -30916.9, -11322.6, + 0.00174378512445, -8564.1, -5650.4, -2715.8, 7912.2, 4229.2, 2271.9, 0.00135182056937, + 69209.6, -22528.5, -13184.2, -24772.9, 59931.1, 25745.1, 0.00288078320569, 4303.1, 10524.6, + 3807.9, -3746.2, -15635.8, -6416.1, 0.00151480602808, 44109.3, -5253.3, -5219.1, -31427.4, + 86984.4, 37676.2, 0.00230867511215, -601.8, -8878.7, -2063.5, -9451.5, 22495.1, 9252.6, + 0.00159150506747, 14863.3, 16751.1, 5558.5, -41390.1, 55330.9, 25199.1, 0.00166820410687, + -7691.7, 19741.5, 8205.3, -32422.6, 33082.6, 15672.2, 0.00208784450554, -5566.9, 12325.9, + 5359.6, -9187.5, 7373, 3415.2, 0.00182160218566, -2866.7, 21449.9, 8873.6, -17987.2, 9404.8, + 5128, 0.00293373825687, 3218.8, 11200.3, 4806.7, -8242.2, 6833.8, 3293.2, 0.00373292423281, + 3685.4, 1744.5, 505.7, 333.7, -2054.9, -823.4, 0.00257190956455, -2235, 4939.8, 2158, 223, + -8279, -3043.5, 0.0031656618867, -5836.3, 80.7, 1000.7, -496.5, 237.8, 100.6, + 0.00235849546142, 813.7, 12194.7, 4736.2, -8764.4, 22231.7, 10125.3, 0.00190788860497, + -3721.4, 9945.7, 4203.8, -9931.5, 2613.8, 1814.6, 0.00216674786293, -3986.8, 6837.9, 2982.7, + -4413.7, 2626.6, 1308.3, 0.00242560712089, -6474.2, 17282.1, 7401.5, -7090, 5857.1, 3083.4, + 0.0040266540467, -2376.2, -25.9, 331.1, -356.9, -242.7, -12.3, 0.00250230616028, -6519.2, + 13021.9, 5664.1, -3145.6, -720.5, 119.3, 0.0034560154714, 1105.3, 1969.8, 639.4, 836.5, + -720.6, -332.8, 0.00460456485812, 899.9, 1075.7, 390.2, 956.1, -390.5, -200, 0.00300084991634, + -2609.1, 5589.6, 2600.3, -1771.2, -1971, -722.6, 0.00434167288015, 362.6, 230.5, 79.8, 893.8, + 665.7, 207.5, 0.00265570423907, -3003.5, 3147.1, 1504.3, 1307, -1719.3, -772.7, + 0.00379660245006, -1978.1, 2424.7, 1097, -381.9, 1718.2, 736, 0.00309288968233, -1806.8, + 3641.3, 1748.7, -826.1, -965.4, -278.5, 0.00276116541824, -2013.8, 2026.7, 936.9, -386, -28.9, + 50.6, 0.00488337145142, -1238.8, 587.3, 344.2, -46, -382.7, -130.7, 0.00547784030838, -129.1, + 664.4, 274, 650.3, 76.2, 4.6, 0.00519244403757, -175.7, 287.2, 118.6, 510.5, 128.9, 2.6, + 0.00386371410953, -1191.8, 1845.5, 840.7, 298, -400.3, -155.6, 0.0171990175011, -133.6, 377.3, + 163.5, -415.2, -124.3, -53.8, 0.00324053441445, -925.7, 954.8, 363.6, 242.1, -1207.1, -466.5, + 0.00395703069047, -986.3, 1458.5, 679.6, -140.9, -100.3, 16.4, 0.00333640821369, -478.2, + 583.7, 253.2, 45, -445.6, -177.8, 0.00574673898281, -193.5, -159.4, -40.4, -225.7, 0.6, 25.8, + 0.0279632069514, 254, -64.4, -45, 76, 232.7, 99.9, 0.00465946664324, -618.9, 558.6, 256.2, + -360.5, 791.7, 339.4, 0.00605543685612, -166.1, 97.6, 48.7, 165.3, 175.2, 50.3, + 0.00634892301732, -202.8, 206.5, 89.9, 181.9, 187.8, 70.3, 0.00428555882611, 121, 373.5, + 129.9, 133.3, -612.3, -239.1, 0.00365279175111, 357.8, 246.4, 75.3, -303.6, 512.7, 233.4, + 0.00492791328112, 251.7, -409.9, -200.7, 370.5, -939.2, -350.9, 0.00661026412641, -29.8, + -78.9, -28, -109.5, -12.9, 6.8, 0.00692550384154, -113.7, -10.9, 5.1, 15.8, 106.8, 37.7, + 0.00352815581218, -239.4, 454.9, 205.7, -216.8, -6, 9.2, 0.00473616568264, -643.4, 842.4, + 376.2, -282.8, 392.1, 183.3, 0.00410339860764, -247.9, 603.2, 204.1, 225.1, -658.7, -268.9, + 0.00418968502695, -159.8, 321.3, 127, 177, -496.7, -202.9, 0.00721235498804, -101.3, 51.2, + 22.8, 38.6, 101.3, 40.2, 0.00480327734211, -727.5, 899.6, 414.1, 129.7, -299.8, -117.6, + 0.00914150243884, 37.1, -53.2, -25.3, 57.1, 44.5, 18.6, 0.00778504688522, -60.1, -19.3, -2.1, + -9, 57.5, 21.9, 0.00747164100621, 0, -43.3, -16.9, -62.8, -18.8, 0.7, 0.00580036485423, 47.8, + 17.9, -17.6, -17.9, -119.4, -39.3, 0.00444854428491, -125, 97.5, 47.5, 19.2, -101.2, -44, + 0.00501419970044, 75, -192.6, -80.1, 203.5, -310, -136.7, 0.00510048611976, 31.2, -127.7, + -51.7, 99.2, -165.4, -73.7, 0.00808166599173, -44.3, -2.5, -0.6, -5.3, 49.6, 19.6, + 0.00666322904742, 58.4, 27.2, -3.3, -5.8, -29, -10.8, 0.00526347157847, 37.9, -58.4, -23.8, + 71.9, -70.3, -32.8, 0.00864454101409, -30.1, -16.6, -3.6, -13.1, 29.6, 12.2, 0.00833824545711, + 13.1, -15.1, -7.6, -24.7, -15, -2.8, 0.00567572891513, -32, 22.2, 12.1, -34.4, -12.6, 1.1, + 0.00535934537772, 1.4, -15.1, -5.2, 35.3, -39.5, -17.7, 0.0172476964838, 31.6, 2.3, 1.1, -4.8, + 30.9, 13.4, 0.00894690975299, -16.1, -8.6, -3.3, -7.3, 21.7, 8.2, 0.0055510929762, -16.4, + -5.3, 2, 29.6, -27.1, -14, 0.00641395716939, -12.1, 17.2, 9.8, 29.6, -9.2, -5.1, + 0.00587706389363, 8.6, -23.6, -11, 10.3, -34.2, -14, + ], + }, + // uranus: 60 terms, max 709 km vs Horizons (1-day grid) + uranus: { + startJd: 2415020.5, + endJd: 2488069.5, + polyKm: [ + [-19445.5, -178220.5, -61713.5], + [1358199.4, -1667903.7, -745205], + [898237, 216795.3, 39509.2], + [-1435565.5, 2085998, 915201.7], + ], + terms: [ + 0.00145332579776, -480836.4, -568634.8, -231930.3, 615431.9, -419603.7, -194919.3, + 0.000267280927492, -377901, 462476.9, 212301.5, -672472.8, -13216.6, 22523.7, + 0.00059154772195, -202726.5, -414908, -166567.5, 346973, -155145.1, -81687.8, + 0.000325970917468, -430657.5, -78923.3, -18618, 299410.9, -129541.7, -55678.4, + 0.000191747598444, 97804, -55069.4, -23035.1, -244567.1, 363147.3, 172746.1, + 0.000469770208321, -96134.5, 77688.2, 37869, 15059, -150373.1, -67150, 0.000651941834893, + -17868.6, -67047.9, -28615.6, 84245.6, 27192.6, 8267, 0.000402669956862, -11746.6, 43733.6, + 22498, 129761.8, 40984.4, 15221.5, 0.00150521864815, 23999.4, -38990.4, -17327.5, 59551.9, + 27425.3, 10108.9, 0.00289462468678, -8968.3, 13924.3, 6168, -13649.1, -7627.3, -2980, + 0.00138058270906, 5252.1, 6011.5, 2372.7, -11459.9, 12740.1, 5738, 0.00093128814584, -26889.3, + -1826.4, 68.2, 6606.1, 10439.6, 4694.2, 0.000728640874288, -6595.9, -17770.1, -7720.4, + 29456.7, 18460.9, 6928.5, 0.00116708018248, 523.1, 10349.7, 4200.7, -8015.8, -453.4, 152.3, + 0.00158191768755, 14070.8, -8074.7, -3849.5, 21412.9, 12090, 4523.6, 0.000814927293606, + -3017.9, -7741.6, -3386.1, 13246.9, 3334.6, 682.2, 0.000987500132243, -3894.8, -747.5, -800.9, + 15878.8, 2239.4, 646, 0.00130388366966, -2558.9, 2471.6, 1334, -2217.1, 4444.9, 2032.8, + 0.00165861672694, 12210.3, -1386, -935, 11154.2, 7231.8, 2740.3, 0.00107378655156, -1429.2, + 415.1, 201.8, 7026.1, 365.8, -113.3, 0.00227432600259, -1509.5, -2042.8, -873.3, -1047.3, + 1902.2, 823.9, 0.00173531576634, 9319.2, -453.4, -499.2, 4523.1, 5120.2, 2110.4, + 0.00295291301672, 2608.4, -508.3, -321.5, 2044.5, 1629.8, 639.1, 0.00181201480573, 6274.2, + 1211.6, 316, 529.2, 2312.9, 968.6, 0.00188871384513, 3272.1, 955.3, 300.9, -728.5, 927.2, + 427.5, 0.00283786445755, -761.1, 904.2, 428.2, -859.2, 141.5, 49.9, 0.00208586752893, 1679.6, + -245.1, -117.4, 488.4, -141.1, -84.9, 0.00233932070157, 1830.3, 275.2, 78.4, 752.8, -245.3, + -147.4, 0.0043573453878, 499.7, -500.3, -226.8, 624.3, 484.5, 191.2, 0.00198458764437, 1594.7, + 332.6, 93.9, 273.7, 572.3, 229.7, 0.00272934665864, -660.5, 171.1, 71.2, 42.8, -447.3, -227.2, + 0.0172021009576, -93.8, 408.4, 177.1, -445.3, -84.1, -36.4, 0.00351825883798, 620.6, -201.3, + -116.6, -587.4, 240.8, 122.4, 0.00302961205612, 1528.4, -72.6, -93.6, 887.2, 599.3, 235.9, + 0.00241601974096, 1094.2, 187.3, 52.1, 138.2, -105.6, -66.8, 0.00216674786293, 750.9, 51.9, + -11.2, 508.5, 436.3, 166.4, 0.00310631109551, 1202.3, 41.2, -28, 286.9, 277.6, 114.9, + 0.027961136683, 257, -45.5, -36.8, 56.1, 237.2, 103.2, 0.00250230616028, 504.7, 221.2, 85.4, + 40.8, -93.5, -50, 0.00336629139771, 673.5, 93.4, 16.5, 327.1, 24.9, 2.6, 0.00328551764519, + 507, -89, -60.8, 260.5, 158.1, 60.3, 0.00346104415263, 684.2, -25.9, -38.5, 534.2, 73.4, 13.5, + 0.00319259751483, 594.1, 54.1, -3.2, 149.1, 34.6, 11.5, 0.00259817995952, 197.2, 126.8, 57.6, + 148.9, -27.9, -25.6, 0.00359526747165, 510.4, 69.2, 13.1, -304.9, -54.8, -11, + 0.00368155389097, 337.6, 7.2, -9.3, -233.1, -18.7, 2, 0.00441019476521, 285.9, 59.3, 16.6, + 65.2, 171.9, 71.5, 0.00915258768451, 15.3, -64.1, -29.8, 71.9, 14.4, 4.6, 0.00375825293036, + 198.4, 74.4, 26.2, -168.9, -49.5, -14.4, 0.00477319966358, -20.2, -50.3, -21.6, -95.6, 3.6, + 3.6, 0.00384453934968, 125.9, 6.6, -2.6, -140.3, -21.7, -3, 0.00446771904476, 179.2, 89.6, + 32.6, -127.3, 21.5, 13.4, 0.00454441808415, 95.4, 24, 7, -125.8, -5.5, 1.4, 0.00462111712355, + 51.3, 47.3, 18.8, -84.3, -11.9, -2, 0.00580396491061, -23.8, -8.5, -3.1, 7, -26.2, -11.5, + 0.0040144059251, 44.5, 5.2, 1.5, -68.6, -13.1, -3.9, 0.00392123838907, 48, -41.2, -22.3, -73, + -12.9, 1.7, 0.00499379516742, 20.4, 18, 7.3, 14.9, -9.1, -4.5, 0.00409381122771, 25.5, -1.1, + -1.4, -50.6, -1.4, 0.9, 0.00469781616294, 13.6, 10.4, 4.1, -62.3, -14.2, -3.9, + ], + }, + // neptune: 45 terms, max 862 km vs Horizons (1-day grid) + neptune: { + startJd: 2415020.5, + endJd: 2488069.5, + polyKm: [ + [70963, -352191.5, -150630.5], + [177011.8, 174437.3, 70374.1], + [-274060.7, 1356524.7, 558708.7], + [-268644.9, 365379.5, 153073.8], + ], + terms: [ + 0.00145181845552, -424116.5, -584543.9, -240067.7, 626774.7, -379579.2, -177993.5, + 0.000585979638547, -58812.4, -388455.8, -157935.7, 404607.5, -25834.8, -27980.1, + 0.000191107893983, 111463.1, -257721, -108758.3, 100235.1, -10680.1, -5065.1, + 0.000352310939398, -28486.8, -31762.2, -11773.9, -69550.4, -19710.2, -6773.6, + 0.00150521864815, 13482.4, -21162.1, -9360.5, 26676.1, 19514.5, 7522, 0.00289483862751, + -7595.2, 13929.8, 6158.3, -15283.5, -6807.4, -2553.6, 0.00116474872031, -719.4, 11065.7, + 4597.2, -11511.4, 889.8, 847.1, 0.000268446637922, 7743.5, -13952.3, -6764, -48338.2, -3874.6, + 462.3, 0.000412257336786, -33533.9, -23017.7, -8211.8, -8982.4, 14929.6, 6769.6, + 0.000651941834893, -4385.5, -6609.1, -2694.2, 14193.5, 17485.1, 6683.1, 0.00138058270906, + 6717.7, 1431.3, 500.7, -4765.1, 6149.3, 2784.4, 0.000498543756105, -21031.1, -6750.7, -2216, + -10677.1, 13596.6, 5730.9, 0.00157233030762, 9554.7, -102.4, -342.5, 5906.5, 11066.8, 4494.3, + 0.000738228254212, -4036.2, -1834.9, -573.1, 6152.6, 9492.8, 3670.3, 0.000881694797763, + -1922.9, 4476.7, 1878.9, 3553.5, 2981.1, 1097.3, 0.00163944196709, 5059.3, 3454.1, 1289.9, + -53.4, 3562.9, 1502.7, 0.0010833739314, -625.9, 1400.2, 629.2, -804.6, 1566.7, 626.9, + 0.00295291301672, 1813.8, -28.9, -60.4, 309.9, 1864.5, 786.3, 0.000805339913682, 1442.6, + 3673.7, 1478.8, 4621.2, 3418.2, 1249.3, 0.00172572838641, 2551.5, 1316.6, 465, 652.1, 1706.5, + 692.9, 0.00128470890981, -131.6, 100.6, 77.1, -1179.3, 1101.7, 503.3, 0.000987500132243, + -1143.8, 1030, 457.2, 641.8, 1284.1, 523, 0.00282827707762, 129, 440.9, 192.9, -640.5, 225.8, + 105.4, 0.00435737360593, 483.6, -452.1, -205.5, 501.9, 471.9, 189.8, 0.01720086581, -112.6, + 403, 174.8, -440.6, -102.1, -44.3, 0.00180242742581, 1145.5, 1377.4, 537.8, -188, 95.6, 50.3, + 0.00259543302756, -385.5, 65.9, 34.9, 274.5, 232.7, 86.3, 0.00302961205612, 576, 241.1, 85.9, + -35.5, 596, 253.6, 0.0279616204397, 256.6, -50.5, -39, 60.8, 236.3, 102.5, 0.00188871384513, + 674.1, 709.6, 272.7, -61.5, 61.4, 28.7, 0.00311589847543, 342.8, 164.1, 59.2, 21.1, 372.7, + 156.8, 0.00196541288452, 364.2, 393.3, 149.9, -129, -130.6, -49.2, 0.0031830101349, 48.1, + 274.2, 113.1, -130.6, -20, -5.6, 0.00205169930384, 210.6, 206.1, 79.4, -50.5, -75.6, -29.9, + 0.0027419906583, -14.6, 56.2, 24.8, -131.8, 11.6, 9.3, 0.00233930095267, 134.3, 22.4, 4.9, 54, + 78.1, 31.6, 0.00441019476521, 91.8, 49.5, 18.7, -28, 87.3, 38, 0.00914779308999, 26.2, -62.2, + -29.2, 66.3, 22.8, 8.8, 0.0032597091743, 94.4, 174.2, 70.4, -103.1, 12, 8.2, 0.00213798572316, + 111.9, 92.6, 34.4, -3.6, -52.1, -21.7, 0.00254065567989, -21.8, 47.1, 20.1, 72.8, -34.4, + -16.5, 0.00333640821369, 34.5, 93.6, 38.2, -68.1, -26.7, -9.1, 0.0022338595224, 29.2, 49.7, + 19.1, 43.6, -23, -11.2, 0.00242560712089, 47.6, 11.2, 3.1, 41.9, -5.5, -3.9, 0.00342269463301, + 20.4, 52.9, 21.6, -37.8, -13.7, -4.6, + ], + }, +}; diff --git a/src/data/bodies/scenePlanets.ts b/src/data/bodies/scenePlanets.ts index ca11747159..e0d1e37df8 100644 --- a/src/data/bodies/scenePlanets.ts +++ b/src/data/bodies/scenePlanets.ts @@ -1,7 +1,7 @@ /** - * scenePlanets — planet + moon seeds at their real J2000 mean positions, DERIVED - * from `ORBITAL_ELEMENTS` via `keplerianPositionMpc`, so a body sits exactly on the - * ellipse its trail draws. `datumRadiusM` is authored in SI metres and resolved + * scenePlanets — planet + moon identity rows; positions are not baked here but + * derived per instant by `deriveBodyStates`, and each trail anchors on that snapshot + * position. `datumRadiusM` is authored in SI metres and resolved * into a draw-space sphere at render time. Albedos are plausible * flat linear-RGB colours, inline rather than in `palette.ts` because each is * per-body data read once at its seed site. diff --git a/src/services/engine/frame/deriveBodyStates.ts b/src/services/engine/frame/deriveBodyStates.ts index 639fc88159..b0525fa7ab 100644 --- a/src/services/engine/frame/deriveBodyStates.ts +++ b/src/services/engine/frame/deriveBodyStates.ts @@ -2,6 +2,7 @@ * deriveBodyStates — derives every scene body's time-varying `BodyState` from * the three authored position tables — anchors, Keplerian elements, surface * sites (`positionDrivers.ts` reads the same three as a union) — keyed by id. + * A planet is Kepler plus its fitted Horizons correction, less any pair reflex. * `meanAnomalyRad` is the PROPAGATED `M` at `t` (not epoch) — the * orbit-trail falloff anchor, so a trail fading behind the body must * track where it actually is. Memoized on `simDays`: every pass (draw, @@ -25,6 +26,9 @@ import { sitePointBodyFixed } from '../../../utils/camera/sitePointBodyFixed'; import { addVec3 } from '../../../utils/math/addVec3'; import { rotateVec3ByTightMat3 } from '../../../utils/math/rotateVec3ByTightMat3'; import { findByIdOrThrow } from '../../../utils/object/findByIdOrThrow'; +import { PLANET_EPHEMERIS_CORRECTIONS } from '../../../data/bodies/planetEphemerisCorrections.generated'; +import { BARYCENTRIC_REFLEX_BY_PRIMARY } from '../../../data/bodies/barycentricPairs'; +import { ephemerisCorrectionMpc } from '../../../utils/orbit/ephemerisCorrectionMpc'; // The focus graph is authored, static data, so its order is resolved once at // module load and replayed every instant: the per-frame cost stays one linear @@ -63,7 +67,23 @@ export function deriveBodyStates(simDays: number): ReadonlyMap { // sin(30°) up the ecliptic pole — the basis's second column, since a // column-major Mat3 stores column c at basis[c*3 + r]. const pole: Vec3 = [basis[3], basis[4], basis[5]]; + // The lift is from the sun line, and the Moon's reflex puts Earth ~1″ off the + // ecliptic, so the sun line's own latitude β is folded in. const eyeAlongPole = dirWorld[0] * pole[0] + dirWorld[1] * pole[1] + dirWorld[2] * pole[2]; - expect(eyeAlongPole).toBeCloseTo(Math.sin((30 * Math.PI) / 180), 6); + const beta = Math.asin(sunward[0] * pole[0] + sunward[1] * pole[1] + sunward[2] * pole[2]); + expect(eyeAlongPole).toBeCloseTo(Math.sin((30 * Math.PI) / 180 - beta), 6); }); it('loops seamlessly: pose(durationSec) equals pose(0) with yaw offset by exactly 2π', () => { diff --git a/tests/data/bodies/planetEphemeris.test.ts b/tests/data/bodies/planetEphemeris.test.ts new file mode 100644 index 0000000000..41da7d0ebb --- /dev/null +++ b/tests/data/bodies/planetEphemeris.test.ts @@ -0,0 +1,35 @@ +/** + * Planet positions vs JPL Horizons at six dates (both span ends + the Voyager flybys). + * Fixture query (one per target; Earth is 399, the rest as in fetchHorizonsPlanets): + * https://ssd.jpl.nasa.gov/api/horizons.api?format=json&COMMAND='199'&OBJ_DATA='NO'&MAKE_EPHEM='YES'&EPHEM_TYPE='VECTORS'&CENTER='500@10'&REF_PLANE='FRAME'&TIME_TYPE='UT'&OUT_UNITS='KM-S'&CSV_FORMAT='YES'&VEC_TABLE='1'&TLIST_TYPE='JD'&TLIST='2415171.5' '2443937.5' '2444555.5' '2446454.5' '2447763.5' '2487855.5' + * Fails when an element row or the frame changes without `npm run build-planet-ephemeris`. + */ + +import { describe, it, expect } from 'vitest'; +import { deriveBodyStates } from '../../../src/services/engine/frame/deriveBodyStates'; +import { SCALE_UNITS } from '../../../src/data/scaleUnits'; +import HORIZONS from '../../fixtures/horizonsPlanets.json'; + +// Earth is compared with 399 (Earth centre), so its error also carries the app Moon's +// error through the reflex: measured max 621 km, inside the planets' own bound. +const TOLERANCE_KM = 1000; + +function heliocentricKm(id: string, jd: number): [number, number, number] { + const states = deriveBodyStates(jd); + const body = states.get(id)!.positionMpc; + const sun = states.get('sun')!.positionMpc; + const k = 1 / SCALE_UNITS.KM_TO_MPC; + return [(body[0] - sun[0]) * k, (body[1] - sun[1]) * k, (body[2] - sun[2]) * k]; +} + +describe('planet ephemeris', () => { + it('every planet is within 1,000 km of Horizons at the fixture dates', () => { + for (const [id, rows] of Object.entries(HORIZONS)) { + for (const [jd, expected] of Object.entries(rows)) { + const [x, y, z] = heliocentricKm(id, Number(jd)); + const errKm = Math.hypot(x - expected[0]!, y - expected[1]!, z - expected[2]!); + expect(errKm, `${id} at JD ${jd}`).toBeLessThan(TOLERANCE_KM); + } + } + }); +}); diff --git a/tests/fixtures/bodyStatesJ2000.json b/tests/fixtures/bodyStatesJ2000.json index 17b64148f2..5fc9266f06 100644 --- a/tests/fixtures/bodyStatesJ2000.json +++ b/tests/fixtures/bodyStatesJ2000.json @@ -1,6 +1,6 @@ { "callisto": { - "positionMpc": [1.9395074114394832e-11, 1.3353112790826594e-11, 5.254163647580489e-12], + "positionMpc": [1.9408901674566044e-11, 1.332152898389301e-11, 5.240114260493354e-12], "orientation": [ -0.17492209617849083, -0.8904283175465342, -0.4201662427894122, 0.984536438326655, -0.162305061611009, -0.06591713421039394, -0.009500625010351543, -0.4251993394711415, @@ -18,7 +18,7 @@ "meanAnomalyRad": 5.307546255314756 }, "deimos": { - "positionMpc": [6.7424822969448034e-12, 6.3822415228828266e-15, -1.7956313479591653e-13], + "positionMpc": [6.742719405018569e-12, 6.343311414258646e-15, -1.7961210378396087e-13], "orientation": [ -0.4463842202746905, 0.6779087814648913, 0.5841068497308886, -0.7834711648414804, -0.6114518589447622, 0.11090337260394045, 0.4323355892726048, -0.40812535844490344, @@ -27,7 +27,7 @@ "meanAnomalyRad": 3.5779249665883754 }, "dione": { - "positionMpc": [3.1097913153309065e-11, 2.983963447795244e-11, 1.0980444303639635e-11], + "positionMpc": [3.10572934929334e-11, 2.994769806016409e-11, 1.1027667574095916e-11], "orientation": [ 0.1500314083433411, -0.9868524640256249, 0.060106495132111006, 0.9849676241424642, 0.14392708758779407, -0.0955184424582236, 0.08561165751031316, 0.07353371815057563, @@ -36,14 +36,14 @@ "meanAnomalyRad": 3.7000980142279785 }, "earth": { - "positionMpc": [-8.589504546518782e-13, 4.302246510960393e-12, 1.8652507750193955e-12], + "positionMpc": [-8.588258817916513e-13, 4.302365851487608e-12, 1.8652815217375455e-12], "orientation": [ 0.1761742596326785, -0.9843589945964214, 0, 0.9843589945964214, 0.1761742596326785, 0, 0, 0, 1 ], "meanAnomalyRad": -0.04316391697638581 }, "enceladus": { - "positionMpc": [3.1104454125073664e-11, 2.9821430112281805e-11, 1.0981228244387354e-11], + "positionMpc": [3.1063834464698e-11, 2.9929493694493454e-11, 1.1028451514843634e-11], "orientation": [ -0.6101639042418044, 0.79225204377477, -0.006058803107722217, -0.7876361177972574, -0.6057470523491235, 0.11269363119366291, 0.08561165751031316, 0.07353371815057563, @@ -52,7 +52,7 @@ "meanAnomalyRad": 0.9948376736367678 }, "europa": { - "positionMpc": [1.9366208544821226e-11, 1.3288551377534754e-11, 5.2231668713859995e-12], + "positionMpc": [1.938003610499244e-11, 1.325696757060117e-11, 5.209117484298865e-12], "orientation": [ 0.8440426953875887, 0.47876521241452585, 0.24161084359880872, -0.5360821137690424, 0.7653676116425182, 0.35612973246493035, -0.014418587301453609, -0.43011195104333155, @@ -61,7 +61,7 @@ "meanAnomalyRad": 6.028367236388414 }, "ganymede": { - "positionMpc": [1.935782125182158e-11, 1.3278963368210082e-11, 5.218463987793369e-12], + "positionMpc": [1.9371648811992793e-11, 1.3247379561276497e-11, 5.204414600706234e-12], "orientation": [ 0.7670029894074187, 0.5750114317615406, 0.28472489807466117, -0.6415017437254595, 0.6965288928160546, 0.3214389744098182, -0.013488033113756947, -0.42919617288133427, @@ -70,7 +70,7 @@ "meanAnomalyRad": 5.668829410477583 }, "iapetus": { - "positionMpc": [3.112403542854821e-11, 2.993895163790855e-11, 1.0993205841361646e-11], + "positionMpc": [3.1083415768172544e-11, 3.00470152201202e-11, 1.1040429111817927e-11], "orientation": [ -0.21672383799832437, -0.9676171771685296, -0.12941320060829367, 0.9570764250126249, -0.18446342993435108, -0.2235575087128771, 0.19244608265259502, -0.17230856468922595, @@ -79,7 +79,7 @@ "meanAnomalyRad": 1.3055062804917585 }, "io": { - "positionMpc": [1.9397356297216674e-11, 1.3302646760404455e-11, 5.23027425001944e-12], + "positionMpc": [1.9411183857387887e-11, 1.327106295347087e-11, 5.216224862932305e-12], "orientation": [ -0.9506246068355796, -0.2738659162314874, -0.1459805357072911, 0.30999719228532685, -0.8601582721252644, -0.40500554029506025, -0.014649151986168717, -0.43026178870679127, @@ -88,7 +88,7 @@ "meanAnomalyRad": 5.775294494849236 }, "jupiter": { - "positionMpc": [1.938440693074091e-11, 1.3298914119529161e-11, 5.2282900664869925e-12], + "positionMpc": [1.9398234490912123e-11, 1.3267330312595576e-11, 5.214240679399858e-12], "orientation": [ 0.22825610082668613, -0.8802468632436776, -0.41601035105519185, 0.9734916178676621, 0.19993790151295504, 0.1110806260335145, -0.014602136035502334, -0.4303374202742101, @@ -97,7 +97,7 @@ "meanAnomalyRad": 0.34327067101878284 }, "mars": { - "positionMpc": [6.7421474995208884e-12, 6.893277360777564e-15, -1.791107618500849e-13], + "positionMpc": [6.742384607594654e-12, 6.854347252153383e-15, -1.7915973083812925e-13], "orientation": [ -0.7067491138500313, -0.7065745401448309, 0.035469836358746884, 0.5490428766969101, -0.5794164477979991, -0.6023524712072909, 0.4461587269353554, -0.4062376142607543, @@ -106,7 +106,7 @@ "meanAnomalyRad": 0.3384227896851049 }, "mercury": { - "positionMpc": [-6.306874292609911e-13, -1.942153167646951e-12, -9.720109452033183e-13], + "positionMpc": [-6.306351560041769e-13, -1.9421570611675653e-12, -9.720188544300925e-13], "orientation": [ 0.9314951469471263, -0.27146527694401956, -0.24212268507444376, 0.3520893147390993, 0.8400821208129223, 0.4126683229144372, 0.09137781820826621, -0.46964735036425953, @@ -115,7 +115,7 @@ "meanAnomalyRad": 3.050705107870811 }, "mimas": { - "positionMpc": [3.109980743972915e-11, 2.982154582345605e-11, 1.0981783752642522e-11], + "positionMpc": [3.1059187779353485e-11, 2.99296094056677e-11, 1.1029007023098802e-11], "orientation": [ -0.0067762384652490515, 0.9972926234328449, -0.07322230424328367, -0.9963055388236075, -0.00046426434886024867, 0.08587815652431914, 0.08561165751031316, 0.07353371815057563, @@ -124,7 +124,7 @@ "meanAnomalyRad": 4.804891430740389 }, "moon": { - "positionMpc": [-8.684282394279855e-13, 4.293738774580142e-12, 1.8628218170240537e-12], + "positionMpc": [-8.683036665677585e-13, 4.2938581151073575e-12, 1.8628525637422037e-12], "orientation": [ 0.7845965380655952, 0.5687413064909984, 0.24686352251070906, -0.6200066702864299, 0.7197447865505031, 0.3123446350326911, -0.000035437512628813665, -0.3981215499370551, @@ -133,7 +133,7 @@ "meanAnomalyRad": 2.3609068791727297 }, "neptune": { - "positionMpc": [8.147178919019027e-11, -1.1141523115780291e-10, -4.7631193423792595e-11], + "positionMpc": [8.150711165206798e-11, -1.1141066747848902e-10, -4.7630168062640554e-11], "orientation": [ 0.018461554711798778, -0.731133128996666, -0.6819849842050542, 0.9343480499713713, 0.2554057808141049, -0.24851882955106883, 0.3558832568586489, -0.6326232961330795, @@ -142,7 +142,7 @@ "meanAnomalyRad": -1.746809150875549 }, "phobos": { - "positionMpc": [6.742083325040046e-12, 6.609864655853662e-15, -1.7921380717377167e-13], + "positionMpc": [6.7423204331138115e-12, 6.5709345472294815e-15, -1.79262776161816e-13], "orientation": [ 0.21437170084218637, 0.9136615583170651, 0.34535102538093126, -0.8689765057514275, 0.016949534034514047, 0.49456298461171994, 0.4460096482477342, -0.4061222354779966, @@ -170,7 +170,7 @@ "meanAnomalyRad": 0.08515672394345941 }, "rhea": { - "positionMpc": [3.1115776493412146e-11, 2.9821711975158945e-11, 1.098031510423812e-11], + "positionMpc": [3.107515683303648e-11, 2.9929775557370594e-11, 1.10275383746944e-11], "orientation": [ -0.9382412075174634, 0.34145382677816394, 0.055791761889368914, -0.33523213510967576, -0.937076695569866, 0.09750221643464865, 0.08557366479227414, 0.07277740582357002, @@ -364,7 +364,7 @@ "meanAnomalyRad": 5.630129826433361 }, "saturn": { - "positionMpc": [3.1099752807904804e-11, 2.9827534561639487e-11, 1.0981182323435745e-11], + "positionMpc": [3.105913314752914e-11, 2.9935598143851136e-11, 1.1028405593892026e-11], "orientation": [ -0.9801892579246128, 0.18502086117224825, 0.07068450735682215, -0.1786683742380381, -0.9800019411711303, 0.08760027025045486, 0.0854788318610716, 0.07323575787752883, @@ -373,7 +373,7 @@ "meanAnomalyRad": -0.7442892730041829 }, "tethys": { - "positionMpc": [3.1108738218666247e-11, 2.9830585978804496e-11, 1.0980101650204179e-11], + "positionMpc": [3.106811855829058e-11, 2.9938649561016145e-11, 1.102732492066046e-11], "orientation": [ -0.9415593714971506, -0.32012363104449437, 0.10481798887967395, 0.3257861170219318, -0.9445176828105657, 0.04183004678376551, 0.08561165751031316, 0.07353371815057563, @@ -382,7 +382,7 @@ "meanAnomalyRad": 0 }, "titan": { - "positionMpc": [3.111648567869407e-11, 2.9792900525807336e-11, 1.0982129467684212e-11], + "positionMpc": [3.107586601831841e-11, 2.9900964108018985e-11, 1.1029352738140493e-11], "orientation": [ -0.4351255139787793, 0.8999577882506432, -0.027235389692707796, -0.8960258662696234, -0.4298602432577761, 0.11116572422350515, 0.08833704806293895, 0.07277465653217519, @@ -391,7 +391,7 @@ "meanAnomalyRad": 0.20420352248333654 }, "uranus": { - "positionMpc": [6.993663216219453e-11, -6.06470857509395e-11, -2.755153855411126e-11], + "positionMpc": [6.99676262541044e-11, -6.063208074016367e-11, -2.754560691260202e-11], "orientation": [ -0.8693317262684063, 0.3040596708473847, -0.3896281127785479, 0.44645103553580323, 0.1449623050604645, -0.8829877705724841, -0.2119995815377986, -0.9415591572895124, @@ -400,7 +400,7 @@ "meanAnomalyRad": 2.48332127460649 }, "venus": { - "positionMpc": [-3.4824959657808e-12, -2.2457953250190632e-13, 1.1936663949337553e-13], + "positionMpc": [-3.482424126152384e-12, -2.244051027888871e-13, 1.1943343188573741e-13], "orientation": [ -0.954821497429634, 0.2665104385944917, 0.13148419740182907, -0.296591573568662, -0.8824137725799873, -0.36521140788482953, 0.018690814168902045, -0.38770880836179883, @@ -414,12 +414,12 @@ "meanAnomalyRad": 2.1093550674103057 }, "whale": { - "positionMpc": [-8.589504546518782e-13, 4.3024659441260814e-12, 1.8652507750193955e-12], + "positionMpc": [-8.588258817916513e-13, 4.3025852846532965e-12, 1.8652815217375455e-12], "orientation": [-1, 1.2246467991473532e-16, 0, -1.2246467991473532e-16, -1, 0, 0, 0, 1], "meanAnomalyRad": 0 }, "petunias": { - "positionMpc": [-8.589504533555665e-13, 4.3024659441260774e-12, 1.8652507750193955e-12], + "positionMpc": [-8.588258804953396e-13, 4.3025852846532925e-12, 1.8652815217375455e-12], "orientation": [ 0.31895930929807, -0.9477684100095857, 0, -0.5730199417863159, -0.19284251607872754, 0.7965299180241964, -0.7549258939308582, -0.25406063248824595, -0.6045991148623748 @@ -429,23 +429,23 @@ "voyager1": { "positionMpc": [-9.849911903784252e-11, -3.47268536120473e-10, 7.701161928836898e-11], "orientation": [ - 0.2620963781618873, 0.9437245974921663, -0.2017160693714547, -0.9486015462831995, - 0.29035822333520933, 0.1258856962912936, 0.17737134758337958, 0.15835399025559568, - 0.9713204511522379 + 0.2620966147096448, 0.9437245655335981, -0.2017159115344286, -0.9486014645837683, + 0.2903584480148431, 0.12588579370183642, 0.17737143498115382, 0.1583537687421207, + 0.9713204713058885 ], "meanAnomalyRad": 21.812722680769564 }, "voyager2": { "positionMpc": [7.68472828852993e-11, -1.820895489669245e-10, -2.120152434709163e-10], "orientation": [ - -0.26417090791453673, 0.6336594597733262, 0.7271103220635683, -0.956916648595112, - -0.2664026163055799, -0.11549966955381757, 0.12051663392705765, -0.7262956251178432, - 0.6767351076171333 + -0.2641704259783976, 0.6336597253131567, 0.7271102657468443, -0.9569168162798725, + -0.2664021092858773, -0.11549944973387398, 0.12051635888618681, -0.7262955794193504, + 0.6767352056430385 ], "meanAnomalyRad": 12.997179255974443 }, "hubble": { - "positionMpc": [-8.5896919141719e-13, 4.302441923444338e-12, 1.865146970252168e-12], + "positionMpc": [-8.58844618556963e-13, 4.302561263971553e-12, 1.865177716970318e-12], "orientation": [ -6.123233995736766e-17, 6.123233995736766e-17, 1, -6.123233995736766e-17, -1, 6.123233995736766e-17, 1, -6.123233995736766e-17, 6.123233995736766e-17 diff --git a/tests/fixtures/camera/driverGoldenTrace.json b/tests/fixtures/camera/driverGoldenTrace.json index 9257d51b8c..68d2b00fdd 100644 --- a/tests/fixtures/camera/driverGoldenTrace.json +++ b/tests/fixtures/camera/driverGoldenTrace.json @@ -3,18 +3,18 @@ "label": "boot 0", "winner": "resting", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.7, 0.3, 1.23882029118e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.7, 0.3, 1.23882029118e-15, @@ -59,18 +59,18 @@ "label": "boot 3", "winner": "resting", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.7, 0.3, 1.23882029118e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.7, 0.3, 1.23882029118e-15, @@ -115,18 +115,18 @@ "label": "drag 0", "winner": "orbitDrag", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.7, 0.3, 1.23882029118e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.7, 0.3, 1.23882029118e-15, @@ -171,18 +171,18 @@ "label": "drag end", "winner": "resting", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 1.23882029118e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 1.23882029118e-15, @@ -229,18 +229,18 @@ "label": "follow 0", "winner": "followApproach", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 1.23882029118e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 1.23882029118e-15, @@ -285,18 +285,18 @@ "label": "follow 7", "winner": "followApproach", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 1.08011618333e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 1.08011618333e-15, @@ -341,18 +341,18 @@ "label": "follow 9", "winner": "followApproach", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 1.04608780247e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 1.04608780247e-15, @@ -397,18 +397,18 @@ "label": "notch under follow a", "winner": "followApproach", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 9.91065552e-16, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 9.91065552e-16, @@ -453,18 +453,18 @@ "label": "notch under follow b", "winner": "followApproach", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 9.74097988688e-16, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 9.74097988688e-16, @@ -509,18 +509,18 @@ "label": "follow settle 0", "winner": "followApproach", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 9.583638658e-16, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 9.583638658e-16, @@ -565,18 +565,18 @@ "label": "follow settle 3", "winner": "followApproach", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 9.1809669046e-16, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 9.1809669046e-16, @@ -621,18 +621,18 @@ "label": "follow settle 11", "winner": "followApproach", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 8.53383734397e-16, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 8.53383734397e-16, @@ -677,18 +677,18 @@ "label": "follow settle 19", "winner": "followApproach", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 8.31864465622e-16, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 8.31864465622e-16, @@ -733,18 +733,18 @@ "label": "follow settle 23", "winner": "followApproach", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 8.29855276505e-16, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 8.29855276505e-16, @@ -789,18 +789,18 @@ "label": "autoRotate 0", "winner": "followApproach", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 8.2976024729e-16, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 8.2976024729e-16, @@ -845,18 +845,18 @@ "label": "autoRotate 2", "winner": "followApproach", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 8.29734065771e-16, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 8.29734065771e-16, @@ -901,18 +901,18 @@ "label": "notch under autoRotate a", "winner": "autoRotate", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 8.29734065771e-16, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 8.29734065771e-16, @@ -958,18 +958,18 @@ "label": "notch under autoRotate b", "winner": "autoRotate", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 8.29734065771e-16, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61, 0.3, 8.29734065771e-16, @@ -1014,18 +1014,18 @@ "label": "autoRotate settle 0", "winner": "autoRotate", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61083808, 0.3, 8.29734065771e-16, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61083808, 0.3, 8.29734065771e-16, @@ -1070,18 +1070,18 @@ "label": "autoRotate settle 2", "winner": "autoRotate", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61251424, 0.3, 8.29734065771e-16, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61251424, 0.3, 8.29734065771e-16, @@ -1126,18 +1126,18 @@ "label": "tween 0", "winner": "tween", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61251424, 0.3, 8.29734065771e-16, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.61251424, 0.3, 8.29734065771e-16, @@ -1183,18 +1183,18 @@ "label": "tween 1", "winner": "tween", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.66956424, 0.3, 8.88904476337e-16, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.66956424, 0.3, 8.88904476337e-16, @@ -1239,18 +1239,18 @@ "label": "tween 3", "winner": "tween", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.76686424, 0.3, 9.89820882086e-16, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.76686424, 0.3, 9.89820882086e-16, @@ -1295,18 +1295,18 @@ "label": "tween 11", "winner": "tween", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.97606424, 0.3, 1.20679634029e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.97606424, 0.3, 1.20679634029e-15, @@ -1351,18 +1351,18 @@ "label": "tween 19", "winner": "tween", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.01246424, 0.3, 1.24454924028e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.01246424, 0.3, 1.24454924028e-15, @@ -1407,18 +1407,18 @@ "label": "tween 20", "winner": "tween", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.01251424, 0.3, 1.24460109866e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.01251424, 0.3, 1.24460109866e-15, @@ -1464,18 +1464,18 @@ "label": "tween 21", "winner": "autoRotate", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.01251424, 0.3, 1.24460109866e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.01251424, 0.3, 1.24460109866e-15, @@ -1521,18 +1521,18 @@ "label": "tween 22", "winner": "autoRotate", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.01251424, 0.3, 1.24460109866e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.01251424, 0.3, 1.24460109866e-15, @@ -1577,18 +1577,18 @@ "label": "tween 23", "winner": "autoRotate", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.01335232, 0.3, 1.24460109866e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.01335232, 0.3, 1.24460109866e-15, @@ -1633,18 +1633,18 @@ "label": "clip 0", "winner": "clip", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.01335232, 0.3, 1.24460109866e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.01335232, 0.3, 1.24460109866e-15, @@ -1690,18 +1690,18 @@ "label": "clip 1", "winner": "clip", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.01375444386, 0.3, 1.24460109866e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.01375444386, 0.3, 1.24460109866e-15, @@ -1746,18 +1746,18 @@ "label": "clip 3", "winner": "clip", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.02420966421, 0.3, 1.24460109866e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.02420966421, 0.3, 1.24460109866e-15, @@ -1802,18 +1802,18 @@ "label": "clip 11", "winner": "clip", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.54857917721, 0.3, 1.24460109866e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.54857917721, 0.3, 1.24460109866e-15, @@ -1858,18 +1858,18 @@ "label": "clip 19", "winner": "clip", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 2.49728989311, 0.3, 1.24460109866e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 2.49728989311, 0.3, 1.24460109866e-15, @@ -1914,18 +1914,18 @@ "label": "clip 27", "winner": "clip", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.01656931088, 0.3, 1.24460109866e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.01656931088, 0.3, 1.24460109866e-15, @@ -1970,18 +1970,18 @@ "label": "clip 35", "winner": "clip", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.41547617966, 0.3, 1.24460109866e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.41547617966, 0.3, 1.24460109866e-15, @@ -2026,18 +2026,18 @@ "label": "clip 37", "winner": "clip", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.70822234949, 0.3, 1.24460109866e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.70822234949, 0.3, 1.24460109866e-15, @@ -2082,18 +2082,18 @@ "label": "teardown 0", "winner": "resting", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.70822234949, 0.3, 1.24460109866e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.70822234949, 0.3, 1.24460109866e-15, @@ -2139,18 +2139,18 @@ "label": "teardown 1", "winner": "resting", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.70822234949, 0.3, 1.24460109866e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.70822234949, 0.3, 1.24460109866e-15, @@ -2195,18 +2195,18 @@ "label": "teardown 5", "winner": "resting", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.70822234949, 0.3, 1.24460109866e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.70822234949, 0.3, 1.24460109866e-15, @@ -2251,18 +2251,18 @@ "label": "refocus 0", "winner": "followApproach", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.70822234949, 0.3, 1.24460109866e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.70822234949, 0.3, 1.24460109866e-15, @@ -2307,18 +2307,18 @@ "label": "refocus 3", "winner": "followApproach", "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.70822234949, 0.3, 1.16729288125e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 1.70822234949, 0.3, 1.16729288125e-15, @@ -2363,18 +2363,18 @@ "label": "pan 0", "winner": "orbitDrag", "displayed": [ - -8.58953059185e-13, - 4.30216839327e-12, - 1.86525901142e-12, + -8.58828486325e-13, + 4.30228773379e-12, + 1.86528975814e-12, 1.70822234949, 0.3, 1.16729288125e-15, 0 ], "register": [ - -8.58953059185e-13, - 4.30216839327e-12, - 1.86525901142e-12, + -8.58828486325e-13, + 4.30228773379e-12, + 1.86528975814e-12, 1.70822234949, 0.3, 1.16729288125e-15, @@ -2420,18 +2420,18 @@ "label": "pan 1", "winner": "orbitDrag", "displayed": [ - -8.58955663718e-13, - 4.30209027557e-12, - 1.86526724783e-12, + -8.58831090858e-13, + 4.3022096161e-12, + 1.86529799455e-12, 1.70822234949, 0.3, 1.16729288125e-15, 0 ], "register": [ - -8.58955663718e-13, - 4.30209027557e-12, - 1.86526724783e-12, + -8.58831090858e-13, + 4.3022096161e-12, + 1.86529799455e-12, 1.70822234949, 0.3, 1.16729288125e-15, @@ -2476,18 +2476,18 @@ "label": "pan end", "winner": "followApproach", "displayed": [ - -8.58958268251e-13, - 4.30201215788e-12, - 1.86527548424e-12, + -8.58833695391e-13, + 4.30213149841e-12, + 1.86530623095e-12, 1.70822234949, 0.3, 1.20888694702e-15, 0 ], "register": [ - -8.58958268251e-13, - 4.30201215788e-12, - 1.86527548424e-12, + -8.58833695391e-13, + 4.30213149841e-12, + 1.86530623095e-12, 1.70822234949, 0.3, 1.20888694702e-15, @@ -2534,18 +2534,18 @@ "label": "pan settle 0", "winner": "followApproach", "displayed": [ - -8.58958268251e-13, - 4.30201215788e-12, - 1.86527548424e-12, + -8.58833695391e-13, + 4.30213149841e-12, + 1.86530623095e-12, 1.70822234949, 0.3, 1.20492839979e-15, 0 ], "register": [ - -8.58958268251e-13, - 4.30201215788e-12, - 1.86527548424e-12, + -8.58833695391e-13, + 4.30213149841e-12, + 1.86530623095e-12, 1.70822234949, 0.3, 1.20492839979e-15, @@ -2590,18 +2590,18 @@ "label": "pan settle 3", "winner": "followApproach", "displayed": [ - -8.58958268251e-13, - 4.30201215788e-12, - 1.86527548424e-12, + -8.58833695391e-13, + 4.30213149841e-12, + 1.86530623095e-12, 1.70822234949, 0.3, 1.19457445869e-15, 0 ], "register": [ - -8.58958268251e-13, - 4.30201215788e-12, - 1.86527548424e-12, + -8.58833695391e-13, + 4.30213149841e-12, + 1.86530623095e-12, 1.70822234949, 0.3, 1.19457445869e-15, diff --git a/tests/fixtures/camera/settleGoldenTrace.json b/tests/fixtures/camera/settleGoldenTrace.json index a92851fe34..0271c3448a 100644 --- a/tests/fixtures/camera/settleGoldenTrace.json +++ b/tests/fixtures/camera/settleGoldenTrace.json @@ -5,18 +5,18 @@ "arm": "absolute", "hr": 5, "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.7, 0.3, 1.23882029118e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.7, 0.3, 1.23882029118e-15, @@ -29,18 +29,18 @@ "arm": "absolute", "hr": 4.69959329559, "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.7, 0.3, 1.17679530435e-15, 7.36444650118e-18 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.7, 0.3, 1.17679530435e-15, @@ -53,9 +53,9 @@ "arm": "earth", "hr": 0.408531953689, "displayed": [ - -8.58823383762e-13, - 4.30208382526e-12, - 1.86524674597e-12, + -8.58698810902e-13, + 4.30220316579e-12, + 1.86527749269e-12, 0.7, 0.299999999999, 8.43496123044e-17, @@ -85,9 +85,9 @@ "arm": "earth", "hr": 0.0615681608771, "displayed": [ - -8.58815842393e-13, - 4.30209013615e-12, - 1.86525828107e-12, + -8.58691269533e-13, + 4.30220947668e-12, + 1.86528902778e-12, 0.763865807331, 0.340617091539, 1.27119833965e-17, @@ -117,9 +117,9 @@ "arm": "earth", "hr": 0.0456277801724, "displayed": [ - -8.58815437959e-13, - 4.30209050575e-12, - 1.86525869645e-12, + -8.58690865099e-13, + 4.30220984628e-12, + 1.86528944317e-12, 0.767221245664, 0.342024316219, 9.42077070425e-18, @@ -149,9 +149,9 @@ "arm": "earth", "hr": 0.00000240000070995, "displayed": [ - -8.58814244804e-13, - 4.30209160677e-12, - 1.86525980005e-12, + -8.58689671944e-13, + 4.3022109473e-12, + 1.86529054677e-12, 0.77694673987, 0.34536113635, 4.95528126503e-22, @@ -181,9 +181,9 @@ "arm": "earth", "hr": 0.00000248496508704, "displayed": [ - -8.58814244804e-13, - 4.30209160677e-12, - 1.86525980005e-12, + -8.58689671944e-13, + 4.3022109473e-12, + 1.86529054677e-12, 0.752741906691, 0.264826976113, 5.1487696481e-22, @@ -213,9 +213,9 @@ "arm": "earth", "hr": 0.00000263277160673, "displayed": [ - -8.58814244804e-13, - 4.30209160677e-12, - 1.86525980006e-12, + -8.58689671944e-13, + 4.3022109473e-12, + 1.86529054678e-12, 0.684990514444, 0.02256104076, 5.7560563135e-22, @@ -245,9 +245,9 @@ "arm": "earth", "hr": 0.0000023999998191, "displayed": [ - -8.58814244845e-13, - 4.30209160682e-12, - 1.86525980147e-12, + -8.58689671985e-13, + 4.30221094735e-12, + 1.86529054819e-12, 0.304162643125, -0.931255732717, 2.17004580513e-21, @@ -277,9 +277,9 @@ "arm": "earth", "hr": 0.00000239999969298, "displayed": [ - -8.5881424494e-13, - 4.30209160674e-12, - 1.86525980147e-12, + -8.58689672079e-13, + 4.30221094726e-12, + 1.86529054819e-12, 0.304162393914, -0.931255659819, 2.17004580504e-21, @@ -309,9 +309,9 @@ "arm": "earth", "hr": 0.00000240000053964, "displayed": [ - -8.588142453e-13, - 4.30209160641e-12, - 1.86525980122e-12, + -8.5868967244e-13, + 4.30221094693e-12, + 1.86529054794e-12, 0.304160550871, -0.931256443589, 2.17004580513e-21, @@ -341,9 +341,9 @@ "arm": "earth", "hr": 0.00000240000208973, "displayed": [ - -8.58814245335e-13, - 4.30209160644e-12, - 1.86525980243e-12, + -8.58689672475e-13, + 4.30221094697e-12, + 1.86529054914e-12, 0.23536204091, -1.00544579425, 3.39220446157e-21, @@ -373,9 +373,9 @@ "arm": "earth", "hr": 0.0000023999991996, "displayed": [ - -8.58814244608e-13, - 4.30209160562e-12, - 1.86525979876e-12, + -8.58689671748e-13, + 4.30221094615e-12, + 1.86529054548e-12, -2.18873636672, -0.782395233589, 4.95528116513e-22, @@ -405,9 +405,9 @@ "arm": "earth", "hr": 0.00000239999928509, "displayed": [ - -8.58814244617e-13, - 4.30209160561e-12, - 1.86525979876e-12, + -8.58689671757e-13, + 4.30221094614e-12, + 1.86529054548e-12, -2.21974811218, -0.790268249161, 4.95528116513e-22, @@ -437,9 +437,9 @@ "arm": "earth", "hr": 0.00000240000213503, "displayed": [ - -8.58814244616e-13, - 4.3020916056e-12, - 1.86525979873e-12, + -8.58689671756e-13, + 4.30221094612e-12, + 1.86529054545e-12, -2.26868065666, -0.74568333147, 4.95528116513e-22, @@ -469,9 +469,9 @@ "arm": "earth", "hr": 0.00000323966126414, "displayed": [ - -8.58814244392e-13, - 4.30209160535e-12, - 1.86525979882e-12, + -8.58689671532e-13, + 4.30221094588e-12, + 1.86529054554e-12, -2.23721013181, -0.739329340615, 6.68892992494e-22, @@ -501,9 +501,9 @@ "arm": "earth", "hr": 0.11764992274, "displayed": [ - -8.58790612634e-13, - 4.30206304827e-12, - 1.86528148545e-12, + -8.58666039774e-13, + 4.30218238879e-12, + 1.86531223217e-12, -1.73704603665, -1.24764085932, 2.42911852576e-17, @@ -533,9 +533,9 @@ "arm": "earth", "hr": 0.130023273126, "displayed": [ - -8.58790902658e-13, - 4.30206246655e-12, - 1.86528562957e-12, + -8.58666329798e-13, + 4.30218180708e-12, + 1.86531637629e-12, -1.30186041478, -1.35142240492, 2.68459115123e-17, @@ -565,9 +565,9 @@ "arm": "earth", "hr": 0.175513060418, "displayed": [ - -8.58796269929e-13, - 4.3020644555e-12, - 1.86529713172e-12, + -8.58671697069e-13, + 4.30218379603e-12, + 1.86532787843e-12, 0.394311083513, -1.13068223763, 3.62381901023e-17, @@ -597,18 +597,18 @@ "arm": "absolute", "hr": 0.904220833938, "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.772030404994, 0.265666289276, 3.93164567996e-16, -5.55111512313e-17 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.772030404994, 0.265666289276, 3.93164567996e-16, @@ -621,18 +621,18 @@ "arm": "absolute", "hr": 0.999318569187, "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.772030404994, 0.265666289276, 4.12799402008e-16, 1.38777878078e-17 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.772030404994, 0.265666289276, 4.12799402008e-16, @@ -645,18 +645,18 @@ "arm": "absolute", "hr": 2.01238047432, "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.772030404994, 0.265666289276, 6.21966342726e-16, -5.55738316694e-18 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.772030404994, 0.265666289276, 6.21966342726e-16, @@ -669,18 +669,18 @@ "arm": "absolute", "hr": 1.82087715249, "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.772030404994, 0.265666289276, 5.82426642574e-16, -5.96791886065e-17 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.772030404994, 0.265666289276, 5.82426642574e-16, @@ -693,9 +693,9 @@ "arm": "earth", "hr": 0.449022777516, "displayed": [ - -8.58813615026e-13, - 4.30209194307e-12, - 1.86525445823e-12, + -8.58689042166e-13, + 4.3022112836e-12, + 1.86528520494e-12, 0.770526869984, 0.146645730242, 9.36724036408e-17, @@ -725,9 +725,9 @@ "arm": "earth", "hr": 0.404990357704, "displayed": [ - -8.58814134638e-13, - 4.30209146175e-12, - 1.86525332033e-12, + -8.58689561778e-13, + 4.30221080228e-12, + 1.86528406705e-12, 0.766060308078, 0.0569265954535, 8.56332551382e-17, @@ -757,9 +757,9 @@ "arm": "earth", "hr": 0.268610130415, "displayed": [ - -8.58815551831e-13, - 4.3020902092e-12, - 1.86525170703e-12, + -8.58690978971e-13, + 4.30220954972e-12, + 1.86528245375e-12, 0.730401394984, -0.381240094609, 6.74071828526e-17, @@ -791,18 +791,18 @@ "arm": "absolute", "hr": 5, "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.7, 0.3, 1.23882029118e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.7, 0.3, 1.23882029118e-15, @@ -815,18 +815,18 @@ "arm": "absolute", "hr": 4.69959329559, "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.7, 0.3, 1.17679530435e-15, 0 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.7, 0.3, 1.17679530435e-15, @@ -839,9 +839,9 @@ "arm": "earth", "hr": 0.408531953689, "displayed": [ - -8.58823383762e-13, - 4.30208382526e-12, - 1.86524674597e-12, + -8.58698810902e-13, + 4.30220316579e-12, + 1.86527749269e-12, 0.7, 0.299999999999, 8.43496123044e-17, @@ -871,9 +871,9 @@ "arm": "earth", "hr": 0.0615621239606, "displayed": [ - -8.58815721031e-13, - 4.30209022991e-12, - 1.86525805271e-12, + -8.5869114817e-13, + 4.30220957043e-12, + 1.86528879942e-12, 0.763746321348, 0.339489149201, 1.27107388113e-17, @@ -903,9 +903,9 @@ "arm": "earth", "hr": 0.0456251399424, "displayed": [ - -8.58815389035e-13, - 4.30209054158e-12, - 1.86525856693e-12, + -8.58690816175e-13, + 4.30220988211e-12, + 1.86528931365e-12, 0.767005933745, 0.341447327804, 9.42022602832e-18, @@ -935,9 +935,9 @@ "arm": "earth", "hr": 0.0000023999987333, "displayed": [ - -8.58814448428e-13, - 4.30209144188e-12, - 1.86526003613e-12, + -8.58689875568e-13, + 4.30221078241e-12, + 1.86529078285e-12, 0.775991805555, 0.346814022309, 4.9552812602e-22, @@ -967,9 +967,9 @@ "arm": "earth", "hr": 0.0000024849657001, "displayed": [ - -8.58814448428e-13, - 4.30209144188e-12, - 1.86526003613e-12, + -8.58689875568e-13, + 4.30221078241e-12, + 1.86529078285e-12, 0.773897707698, 0.263062016837, 5.14876964263e-22, @@ -999,9 +999,9 @@ "arm": "earth", "hr": 0.00000263277086576, "displayed": [ - -8.58814448431e-13, - 4.30209144188e-12, - 1.86526003614e-12, + -8.58689875571e-13, + 4.3022107824e-12, + 1.86529078286e-12, 0.768044168272, 0.0118011273059, 5.75605630761e-22, @@ -1031,9 +1031,9 @@ "arm": "earth", "hr": 0.0000023999997163, "displayed": [ - -8.58814448742e-13, - 4.30209144169e-12, - 1.8652600375e-12, + -8.58689875882e-13, + 4.30221078221e-12, + 1.86529078422e-12, 0.732896834484, -0.993109049844, 2.17004583329e-21, @@ -1063,9 +1063,9 @@ "arm": "earth", "hr": 0.00000239999935414, "displayed": [ - -8.58814448833e-13, - 4.3020914416e-12, - 1.86526003747e-12, + -8.58689875972e-13, + 4.30221078213e-12, + 1.86529078419e-12, 0.7328965318, -0.99310904296, 2.17004583338e-21, @@ -1095,9 +1095,9 @@ "arm": "earth", "hr": 0.00000240000002849, "displayed": [ - -8.58814449131e-13, - 4.30209144132e-12, - 1.86526003711e-12, + -8.58689876271e-13, + 4.30221078185e-12, + 1.86529078383e-12, 0.732895290976, -0.993110203657, 2.17004583347e-21, @@ -1127,9 +1127,9 @@ "arm": "earth", "hr": 0.00000240000012441, "displayed": [ - -8.58814449397e-13, - 4.30209144116e-12, - 1.86526003827e-12, + -8.58689876536e-13, + 4.30221078169e-12, + 1.86529078499e-12, 0.725551042947, -1.07680251628, 3.39220453137e-21, @@ -1159,9 +1159,9 @@ "arm": "earth", "hr": 0.00000239999946028, "displayed": [ - -8.58814447958e-13, - 4.30209144096e-12, - 1.86526003473e-12, + -8.58689875098e-13, + 4.30221078148e-12, + 1.86529078144e-12, -2.35006327273, -0.807403211062, 4.95528116483e-22, @@ -1191,9 +1191,9 @@ "arm": "earth", "hr": 0.00000240000239859, "displayed": [ - -8.58814447966e-13, - 4.30209144095e-12, - 1.86526003472e-12, + -8.58689875106e-13, + 4.30221078148e-12, + 1.86529078144e-12, -2.38373364458, -0.807339445766, 4.95528116483e-22, @@ -1223,9 +1223,9 @@ "arm": "earth", "hr": 0.00000239999978224, "displayed": [ - -8.5881444796e-13, - 4.30209144094e-12, - 1.8652600347e-12, + -8.586898751e-13, + 4.30221078147e-12, + 1.86529078142e-12, -2.40944022372, -0.753557832895, 4.95528116483e-22, @@ -1255,9 +1255,9 @@ "arm": "earth", "hr": 0.0000032396613292, "displayed": [ - -8.58814447761e-13, - 4.30209144068e-12, - 1.86526003478e-12, + -8.58689874901e-13, + 4.3022107812e-12, + 1.8652907815e-12, -2.40944022372, -0.753557832895, 6.68892992433e-22, @@ -1287,9 +1287,9 @@ "arm": "earth", "hr": 0.117649922735, "displayed": [ - -8.58795358288e-13, - 4.30205885827e-12, - 1.8652804861e-12, + -8.58670785428e-13, + 4.3021781988e-12, + 1.86531123281e-12, -2.62627932389, -1.30978913277, 2.42911852565e-17, @@ -1319,9 +1319,9 @@ "arm": "earth", "hr": 0.130023273121, "displayed": [ - -8.58796303391e-13, - 4.30205769492e-12, - 1.86528438925e-12, + -8.58671730531e-13, + 4.30217703545e-12, + 1.86531513596e-12, -3.00081952413, -1.44805394725, 2.6845911511e-17, @@ -1351,9 +1351,9 @@ "arm": "earth", "hr": 0.175513060412, "displayed": [ - -8.58802801726e-13, - 4.30205868017e-12, - 1.86529532019e-12, + -8.58678228866e-13, + 4.3021780207e-12, + 1.86532606691e-12, 0.974192518494, -1.15078860015, 3.62381901006e-17, @@ -1383,18 +1383,18 @@ "arm": "absolute", "hr": 0.904220834056, "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.782643157335, 0.267207821768, 3.9316456802e-16, 0.0785409033636 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.782643157335, 0.267207821768, 3.9316456802e-16, @@ -1407,18 +1407,18 @@ "arm": "absolute", "hr": 0.999318569316, "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.782643157335, 0.267207821768, 4.12799402035e-16, 0.0785409033636 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.782643157335, 0.267207821768, 4.12799402035e-16, @@ -1431,18 +1431,18 @@ "arm": "absolute", "hr": 2.01238047459, "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.782643157335, 0.267207821768, 6.2196634278e-16, 0.0785409033636 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.782643157335, 0.267207821768, 6.2196634278e-16, @@ -1455,18 +1455,18 @@ "arm": "absolute", "hr": 1.82087715273, "displayed": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.782643157335, 0.267207821768, 5.82426642623e-16, 0.0785409033636 ], "register": [ - -8.58950454652e-13, - 4.30224651096e-12, - 1.86525077502e-12, + -8.58825881792e-13, + 4.30236585149e-12, + 1.86528152174e-12, 0.782643157335, 0.267207821768, 5.82426642623e-16, @@ -1479,9 +1479,9 @@ "arm": "earth", "hr": 0.449022777574, "displayed": [ - -8.58812915346e-13, - 4.30209258004e-12, - 1.86525502852e-12, + -8.58688342486e-13, + 4.30221192057e-12, + 1.86528577523e-12, 0.79206408103, 0.148535164831, 9.36724038973e-17, @@ -1511,9 +1511,9 @@ "arm": "earth", "hr": 0.404742850815, "displayed": [ - -8.58813375989e-13, - 4.30209214146e-12, - 1.86525383748e-12, + -8.58688803128e-13, + 4.30221148199e-12, + 1.8652845842e-12, 0.801544274338, 0.0594167972848, 8.55816295471e-17, @@ -1543,9 +1543,9 @@ "arm": "earth", "hr": 0.266757742279, "displayed": [ - -8.58814922355e-13, - 4.30209075677e-12, - 1.8652520037e-12, + -8.58690349494e-13, + 4.3022100973e-12, + 1.86528275042e-12, 0.863342306569, -0.376693585659, 6.70660385912e-17, diff --git a/tests/fixtures/horizonsPlanets.json b/tests/fixtures/horizonsPlanets.json new file mode 100644 index 0000000000..c52881de2c --- /dev/null +++ b/tests/fixtures/horizonsPlanets.json @@ -0,0 +1,258 @@ +{ + "mercury": { + "2415171.500000000": [ + 7483679.109714671, + 40369654.7213906, + 20778815.08579103 + ], + "2443937.500000000": [ + 7271709.769125367, + 40389445.90468683, + 20818386.63973618 + ], + "2444555.500000000": [ + -3894809.945644011, + 40590467.52391278, + 22084711.39517884 + ], + "2446454.500000000": [ + 19287679.52797054, + -56257872.2745742, + -32050509.08493746 + ], + "2447763.500000000": [ + -15664836.37926049, + -60674673.01552974, + -30783765.71038574 + ], + "2487855.500000000": [ + -59055866.45641895, + -11661944.9384968, + -123154.8931291581 + ] + }, + "venus": { + "2415171.500000000": [ + -72836657.81070568, + -74706497.65053442, + -28968751.72078097 + ], + "2443937.500000000": [ + -62822235.17691786, + -81948931.54033257, + -32885003.16269746 + ], + "2444555.500000000": [ + -86852340.72349352, + 55518089.06172591, + 30471310.43065875 + ], + "2446454.500000000": [ + 64516137.5418477, + -78459101.33648193, + -39377696.22862676 + ], + "2447763.500000000": [ + -47097520.37902651, + -90204823.7986849, + -37597272.99954098 + ], + "2487855.500000000": [ + 88168918.65270643, + 59217129.08197042, + 21087913.36805414 + ] + }, + "earth": { + "2415171.500000000": [ + -48348631.81548293, + -131945643.2686936, + -57241745.96787091 + ], + "2443937.500000000": [ + -142697670.355659, + 37256203.87297093, + 16154266.09699155 + ], + "2444555.500000000": [ + 95093680.20101847, + 104120219.1506324, + 45147175.09116362 + ], + "2446454.500000000": [ + -82129362.62129314, + 112135018.8593458, + 48620025.19905417 + ], + "2447763.500000000": [ + 133419545.3729756, + -65293501.62586283, + -28309887.57258326 + ], + "2487855.500000000": [ + -53112342.11036406, + -130343415.1897614, + -56474094.94749723 + ] + }, + "mars": { + "2415171.500000000": [ + 196925556.7603641, + 73918660.63203643, + 28530351.565069 + ], + "2443937.500000000": [ + 174122538.9992633, + -99593847.57765982, + -50395029.28909588 + ], + "2444555.500000000": [ + 54158028.14729189, + -187131636.9467229, + -87296875.91222645 + ], + "2446454.500000000": [ + -227503865.0092814, + -77708428.77508338, + -29483890.21139638 + ], + "2447763.500000000": [ + -245144581.5686254, + 33384597.23238907, + 21945342.175897 + ], + "2487855.500000000": [ + 95077986.17608264, + -169996236.9923828, + -80526902.74801362 + ] + }, + "jupiter": { + "2415171.500000000": [ + -303074464.8736803, + -682523895.0824546, + -285220581.0110855 + ], + "2443937.500000000": [ + -481720407.7906085, + 572731470.4252973, + 257257551.944829 + ], + "2444555.500000000": [ + -810875218.2413923, + 50473174.92184998, + 41403168.88808651 + ], + "2446454.500000000": [ + 633801879.8291223, + -366855759.752545, + -172695134.4249972 + ], + "2447763.500000000": [ + 62556278.61500947, + 700180918.6058778, + 298605219.6573165 + ], + "2487855.500000000": [ + -807255779.4990433, + 79079101.18609643, + 53513560.91675908 + ] + }, + "saturn": { + "2415171.500000000": [ + 64289641.37288647, + -1390193954.016424, + -576631345.547019 + ], + "2443937.500000000": [ + -1318558724.196886, + 400085094.9888089, + 221885350.8344445 + ], + "2444555.500000000": [ + -1421314829.154814, + -72167730.39596327, + 31282074.89120226 + ], + "2446454.500000000": [ + -681345123.5676914, + -1237147580.369312, + -481634368.5321383 + ], + "2447763.500000000": [ + 321639827.9425938, + -1350418209.921785, + -571543197.5011016 + ], + "2487855.500000000": [ + -1406012907.725283, + -288209808.7923881, + -58413568.19796775 + ] + }, + "uranus": { + "2415171.500000000": [ + -885809426.3931122, + -2481472506.995965, + -1074272566.638308 + ], + "2443937.500000000": [ + -1852864077.381997, + -1924864309.810483, + -816777509.6868061 + ], + "2444555.500000000": [ + -1568454233.97813, + -2143220747.819155, + -916430021.4497007 + ], + "2446454.500000000": [ + -549164855.0818605, + -2574471743.83951, + -1119753799.40533 + ], + "2447763.500000000": [ + 211411118.737716, + -2644337440.975036, + -1161130851.457387 + ], + "2487855.500000000": [ + 2861970833.861998, + 808048839.0802308, + 313493471.9391013 + ] + }, + "neptune": { + "2415171.500000000": [ + 155543083.5274187, + 4134808604.015062, + 1688525096.936738 + ], + "2443937.500000000": [ + -877168112.8228146, + -4120930858.357338, + -1664884573.591237 + ], + "2444555.500000000": [ + -593009878.0742743, + -4161036668.169079, + -1688382261.066192 + ], + "2446454.500000000": [ + 292824206.7088943, + -4176519072.444312, + -1716778984.965878 + ], + "2447763.500000000": [ + 897558488.791832, + -4091867133.224503, + -1697168074.23832 + ], + "2487855.500000000": [ + -4318175095.830819, + 1189662641.724081, + 594460174.6205162 + ] + } +} \ No newline at end of file diff --git a/tests/services/engine/frame/deriveBodyStates.test.ts b/tests/services/engine/frame/deriveBodyStates.test.ts index 091bed358a..efd23324fb 100644 --- a/tests/services/engine/frame/deriveBodyStates.test.ts +++ b/tests/services/engine/frame/deriveBodyStates.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'; import { deriveBodyStates } from '../../../../src/services/engine/frame/deriveBodyStates'; import { CONST_J2000 } from '../../../../src/data/time/constJ2000'; import { ORBITAL_ELEMENTS, elementsById } from '../../../../src/data/bodies/orbitalElements'; +import { BARYCENTRIC_REFLEX_BY_PRIMARY } from '../../../../src/data/bodies/barycentricPairs'; import { SCENE_ANCHORS } from '../../../../src/data/bodies/sceneAnchors'; import { SCENE_STARS } from '../../../../src/data/bodies/sceneStars'; import { SURFACE_FIXED_SITES } from '../../../../src/data/bodies/surfaceFixedSites'; @@ -9,10 +10,25 @@ import { IDENTITY_MAT3 } from '../../../../src/utils/math/identityMat3'; import { bodyHostId } from '../../../../src/data/bodies/positionDrivers'; import { propagateElements } from '../../../../src/utils/orbit/propagateElements'; import { keplerianPositionMpc } from '../../../../src/utils/orbit/keplerianPositionMpc'; +import { PLANET_EPHEMERIS_CORRECTIONS } from '../../../../src/data/bodies/planetEphemerisCorrections.generated'; +import { SCENE_EARTH } from '../../../../src/data/bodies/sceneEarth'; +import { SCALE_UNITS } from '../../../../src/data/scaleUnits'; +import { ephemerisCorrectionMpc } from '../../../../src/utils/orbit/ephemerisCorrectionMpc'; +import { siteGroundRadiusM } from '../../../../src/utils/camera/siteGroundRadiusM'; +import { sitePointBodyFixed } from '../../../../src/utils/camera/sitePointBodyFixed'; +import { rotateVec3ByTightMat3 } from '../../../../src/utils/math/rotateVec3ByTightMat3'; +import { findByIdOrThrow } from '../../../../src/utils/object/findByIdOrThrow'; import BODY_STATES_J2000 from '../../../fixtures/bodyStatesJ2000.json'; const states = deriveBodyStates(CONST_J2000); const ANCHOR_IDS = new Set(SCENE_ANCHORS.map((anchor) => anchor.id)); +const MPC_TO_KM = 1 / SCALE_UNITS.KM_TO_MPC; +// 1900, a Voyager flyby, 2099: the reflex and correction are checked across the span. +const SPAN_DATES = [2415171.5, 2447763.5, 2487855.5]; + +function distanceKm(a: readonly number[], b: readonly number[]): number { + return Math.hypot(a[0]! - b[0]!, a[1]! - b[1]!, a[2]! - b[2]!) * MPC_TO_KM; +} describe('deriveBodyStates', () => { it('returns a state for every driver id, and nothing else', () => { @@ -129,6 +145,44 @@ describe('deriveBodyStates', () => { expect(io.positionMpc[2] - jupiter.positionMpc[2]).toBeCloseTo(ioRelative[2], 18); }); + it('Earth–Moon reflex keeps the barycentre and the separation', () => { + // Earth's row is the barycentre: weighting Earth and Moon by mass must land back + // on the corrected row position, and the Moon's offset from Earth stays raw Kepler. + const k = BARYCENTRIC_REFLEX_BY_PRIMARY.get('earth')!.k; + for (const t of SPAN_DATES) { + const snap = deriveBodyStates(t); + const earth = snap.get('earth')!.positionMpc; + const moon = snap.get('moon')!.positionMpc; + const sun = snap.get('sun')!.positionMpc; + const rowKepler = keplerianPositionMpc(propagateElements(elementsById('earth'), t)); + const correction = ephemerisCorrectionMpc(PLANET_EPHEMERIS_CORRECTIONS['earth']!, t); + const emb = [0, 1, 2].map((i) => sun[i]! + rowKepler[i]! + correction[i]!); + const weighted = [0, 1, 2].map((i) => (1 - k) * earth[i]! + k * moon[i]!); + expect(distanceKm(weighted, emb)).toBeLessThan(1e-3); + + const moonKepler = keplerianPositionMpc(propagateElements(elementsById('moon'), t)); + const separation = [0, 1, 2].map((i) => moon[i]! - earth[i]!); + expect(distanceKm(separation, moonKepler)).toBeLessThan(1e-3); + } + }); + + it('an Earth surface site keeps its offset from the wobbling Earth', () => { + // The site rides the reflexed Earth, so its offset is the body-fixed point under + // Earth's spin alone — the ~4,700 km wobble must not leak into it. + const site = findByIdOrThrow(SURFACE_FIXED_SITES, 'soendermarken', 'test'); + for (const t of SPAN_DATES) { + const snap = deriveBodyStates(t); + const earth = snap.get('earth')!; + const offsetM = rotateVec3ByTightMat3( + sitePointBodyFixed(site, siteGroundRadiusM(site, SCENE_EARTH.surface.datumRadiusM)), + earth.orientation, + ); + const offsetMpc = offsetM.map((m) => m * SCALE_UNITS.M_TO_MPC); + const actual = snap.get(site.id)!.positionMpc.map((v, i) => v - earth.positionMpc[i]!); + expect(distanceKm(actual, offsetMpc)).toBeLessThan(1e-3); + } + }); + it('orientation is identity iff the body has no rotation row', () => { // Matches orientationForBody's row-gate contract: a body with a row (Earth) // carries a baked IAU rotation; a row-less one (the Galactic Centre) carries identity. diff --git a/tests/services/engine/frame/passes/orbitTrailsPass.test.ts b/tests/services/engine/frame/passes/orbitTrailsPass.test.ts index 0ad439b246..855100d48c 100644 --- a/tests/services/engine/frame/passes/orbitTrailsPass.test.ts +++ b/tests/services/engine/frame/passes/orbitTrailsPass.test.ts @@ -31,13 +31,11 @@ import { makeSlab } from '../../../../fixtures/makeSlab'; import { CONST_J2000 } from '../../../../../src/data/time/constJ2000'; import { ORBITAL_ELEMENTS } from '../../../../../src/data/bodies/orbitalElements'; import { CORE_TRAIL_ELEMENTS } from '../../../../../src/data/bodies/coreTrailElements'; -import { SCENE_ANCHORS } from '../../../../../src/data/bodies/sceneAnchors'; import { deriveBodyStates } from '../../../../../src/services/engine/frame/deriveBodyStates'; import { eccentricAnomalyFromMean } from '../../../../../src/utils/orbit/eccentricAnomalyFromMean'; import { propagateElements } from '../../../../../src/utils/orbit/propagateElements'; import { keplerianEllipse } from '../../../../../src/utils/orbit/keplerianEllipse'; import { keplerianPositionMpc } from '../../../../../src/utils/orbit/keplerianPositionMpc'; -import { addVec3 } from '../../../../../src/utils/math/addVec3'; import type { SlabView } from '../../../../../src/@types/engine/frame/SlabView'; import type { Slab } from '../../../../../src/@types/engine/frame/Slab'; import type { FrameView } from '../../../../../src/@types/engine/frame/FrameView'; @@ -95,13 +93,17 @@ vi.mock('../../../../../src/services/engine/frame/sceneBodyStates', async (impor const composeMock = composeOrbitConic as unknown as ReturnType; // The J2000 oracle `SCENE_ORBIT_CONICS` used to provide, hand-derived here with -// its own two helpers (`keplerianEllipse` for the ellipse, `keplerianPositionMpc` -// to place a focus that is itself an orbiting body) so the expected values never -// come from the layer under test. -function conicAtJ2000(el: OrbitalElements, focusPositionMpc: Readonly) { +// its own helpers (`keplerianEllipse` for the ellipse, the snapshot body less its +// raw Kepler offset for the focus the trail anchors on) so the expected values +// never come from the layer under test. +function conicAtJ2000(el: OrbitalElements) { const { centerOffsetMpc, semiMajorMpc, semiMinorMpc } = keplerianEllipse(el); + const bodyMpc = deriveBodyStates(CONST_J2000).get(el.id)!.positionMpc; + const keplerMpc = keplerianPositionMpc(el); return { - centerMpc: addVec3(focusPositionMpc, centerOffsetMpc), + centerMpc: [0, 1, 2].map( + (i) => bodyMpc[i]! - keplerMpc[i]! + centerOffsetMpc[i]!, + ) as unknown as Vec3, semiMajorMpc, semiMinorMpc, eccentricity: el.eccentricity, @@ -110,14 +112,9 @@ function conicAtJ2000(el: OrbitalElements, focusPositionMpc: Readonly) { }; } -const SUN_POS_MPC = SCENE_ANCHORS.find((a) => a.id === 'sun')!.positionMpc; -const EARTH_POS_MPC = addVec3( - SUN_POS_MPC, - keplerianPositionMpc(CORE_TRAIL_ELEMENTS.find((e) => e.id === 'earth')!), -); // Mercury is CORE_TRAIL_ELEMENTS[0] — the first ORBITAL_ELEMENTS row, unfiltered. -const MERCURY_CONIC = conicAtJ2000(CORE_TRAIL_ELEMENTS[0]!, SUN_POS_MPC); -const MOON_CONIC = conicAtJ2000(CORE_TRAIL_ELEMENTS.find((e) => e.id === 'moon')!, EARTH_POS_MPC); +const MERCURY_CONIC = conicAtJ2000(CORE_TRAIL_ELEMENTS[0]!); +const MOON_CONIC = conicAtJ2000(CORE_TRAIL_ELEMENTS.find((e) => e.id === 'moon')!); const PASS_STUB = { setPipeline: vi.fn(), diff --git a/tests/tools/parsers/horizonsVectorsCsv.test.ts b/tests/tools/parsers/horizonsVectorsCsv.test.ts new file mode 100644 index 0000000000..92d194f17c --- /dev/null +++ b/tests/tools/parsers/horizonsVectorsCsv.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { parseHorizonsVectorsCsv } from '../../../tools/parsers/horizonsVectorsCsv'; + +// Trimmed from a real Horizons response (Mercury, 1900-01-01..03, CENTER 500@10). +const RESULT = `Reference frame : ICRF +******************************************************************************* + JDUT , Calendar Date (UT ), X, Y, Z, +************************************************************************************************************************** +$$SOE +2415020.500000000, A.D. 1900-Jan-01 00:00:00.0000, -5.795103211328162E+07, -2.365718743126438E+07, -6.605861464973872E+06, +2415021.500000000, A.D. 1900-Jan-02 00:00:00.0000, -5.707774547111462E+07, -2.685144548110793E+07, -8.402394181703608E+06, +2415022.500000000, A.D. 1900-Jan-03 00:00:00.0000, -5.598502057837061E+07, -2.994254589329407E+07, -1.016666302505421E+07, +$$EOE +************************************************************************************************************************** + Times PRIOR to 1962 are UT1, a mean-solar time closely related to the +`; + +describe('parseHorizonsVectorsCsv', () => { + it('parses the SOE/EOE block of a Horizons vectors result', () => { + const rows = parseHorizonsVectorsCsv(RESULT); + expect(rows).toHaveLength(3); + expect(rows[0]).toEqual({ + jd: 2415020.5, + xKm: -5.795103211328162e7, + yKm: -2.365718743126438e7, + zKm: -6.605861464973872e6, + }); + expect(rows[2]).toMatchObject({ jd: 2415022.5, zKm: -1.016666302505421e7 }); + }); +}); diff --git a/tests/tools/utils/data/mergeHorizonsChunks.test.ts b/tests/tools/utils/data/mergeHorizonsChunks.test.ts new file mode 100644 index 0000000000..3b5f303ca0 --- /dev/null +++ b/tests/tools/utils/data/mergeHorizonsChunks.test.ts @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest'; +import { mergeHorizonsChunks } from '../../../../tools/utils/data/mergeHorizonsChunks'; + +const row = (jd: number) => ({ jd, xKm: jd, yKm: 0, zKm: 0 }); + +describe('mergeHorizonsChunks', () => { + it('concatenating two chunks drops the shared boundary row once', () => { + const merged = mergeHorizonsChunks([ + [row(1), row(2), row(3)], + [row(3), row(4), row(5)], + ]); + expect(merged.map((r) => r.jd)).toEqual([1, 2, 3, 4, 5]); + }); +}); diff --git a/tests/tools/utils/math/fft.test.ts b/tests/tools/utils/math/fft.test.ts new file mode 100644 index 0000000000..ff4bef6e96 --- /dev/null +++ b/tests/tools/utils/math/fft.test.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from 'vitest'; +import { fft } from '../../../../tools/utils/math/fft'; + +describe('fft', () => { + it("fft finds a pure sinusoid's bin", () => { + const n = 64; + const re = Float64Array.from({ length: n }, (_, i) => + Math.cos((2 * Math.PI * 5 * i) / n + 0.3), + ); + const im = new Float64Array(n); + fft(re, im); + const power = Array.from({ length: n / 2 }, (_, k) => re[k]! ** 2 + im[k]! ** 2); + expect(power.indexOf(Math.max(...power))).toBe(5); + // A pure in-bin tone carries amplitude N/2 and leaks nowhere. + expect(Math.sqrt(power[5]!)).toBeCloseTo(n / 2, 9); + expect(Math.max(...power.filter((_, k) => k !== 5))).toBeLessThan(1e-18); + }); +}); diff --git a/tests/tools/utils/math/fitSinusoidSeries.test.ts b/tests/tools/utils/math/fitSinusoidSeries.test.ts new file mode 100644 index 0000000000..c8116add77 --- /dev/null +++ b/tests/tools/utils/math/fitSinusoidSeries.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from 'vitest'; +import { fitSinusoidSeries } from '../../../../tools/utils/math/fitSinusoidSeries'; +import { ephemerisCorrectionMpc } from '../../../../src/utils/orbit/ephemerisCorrectionMpc'; +import { SCALE_UNITS } from '../../../../src/data/scaleUnits'; + +const START = 2_415_020.5; +const END = START + 20_000; + +// Cubic plus two sinusoids whose periods (333.3 d, 1777.7 d) fall between FFT bins. +function signal(t: number): [number, number, number] { + const tau = (2 * (t - START)) / (END - START) - 1; + const p1 = (2 * Math.PI * (t - START)) / 333.3; + const p2 = (2 * Math.PI * (t - START)) / 1777.7; + return [ + 5000 + 800 * tau - 300 * tau ** 3 + 2000 * Math.cos(p1 + 0.4), + -1200 * tau ** 2 + 1500 * Math.sin(p1 + 0.4) + 700 * Math.cos(p2), + 40 * tau + 900 * Math.sin(p2 - 1.1), + ]; +} + +describe('fitSinusoidSeries', () => { + it('fitSinusoidSeries recovers a synthetic two-term signal', () => { + const step = 5; + const tJd = Float64Array.from( + { length: Math.floor((END - START) / step) + 1 }, + (_, i) => START + i * step, + ); + const axes = [0, 1, 2].map((a) => Float64Array.from(tJd, (t) => signal(t)[a]!)); + const fit = fitSinusoidSeries(tJd, [axes[0]!, axes[1]!, axes[2]!], START, END, 1, 50); + + expect(fit.terms.length / 7).toBeLessThanOrEqual(4); + const correction = { startJd: START, endJd: END, polyKm: fit.polyKm, terms: fit.terms }; + for (let t = START; t <= END; t += 1) { + const [x, y, z] = ephemerisCorrectionMpc(correction, t).map((v) => v / SCALE_UNITS.KM_TO_MPC); + const [sx, sy, sz] = signal(t); + expect(Math.hypot(x! - sx, y! - sy, z! - sz)).toBeLessThanOrEqual(1); + } + }); +}); diff --git a/tests/utils/orbit/ephemerisCorrectionMpc.test.ts b/tests/utils/orbit/ephemerisCorrectionMpc.test.ts new file mode 100644 index 0000000000..22d0ef8e2c --- /dev/null +++ b/tests/utils/orbit/ephemerisCorrectionMpc.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest'; +import { ephemerisCorrectionMpc } from '../../../src/utils/orbit/ephemerisCorrectionMpc'; +import { SCALE_UNITS } from '../../../src/data/scaleUnits'; +import type { Vec3 } from '../../../src/@types/math/Vec3'; +import type { EphemerisCorrection } from '../../../src/@types/scene/EphemerisCorrection'; + +// A 400-day span, so dt = 100 sits at τ = −0.5 with round phases. +const CORRECTION: EphemerisCorrection = { + startJd: 2_400_000, + endJd: 2_400_400, + polyKm: [ + [100, -50, 7], + [10, 0, 0], + [0, 4, 0], + [0, 0, 2], + ], + // prettier-ignore + terms: [ + Math.PI / 100, 1000, 0, 0, 0, 500, 0, // phase π at dt = 100 + Math.PI / 400, 0, 0, 30, 0, 0, -20, // phase π/4 at dt = 100 + ], +}; + +function km(simDays: number): Vec3 { + const [x, y, z] = ephemerisCorrectionMpc(CORRECTION, simDays); + const k = SCALE_UNITS.KM_TO_MPC; + return [x / k, y / k, z / k]; +} + +describe('ephemerisCorrectionMpc', () => { + it('evaluates polynomial and terms at a known instant', () => { + // poly at τ = −0.5: x = 100 − 5, y = −50 + 1, z = 7 − 0.25. + // term 1 (π): x −= 1000; term 2 (π/4): z += (30 − 20)/√2. + const [x, y, z] = km(2_400_100); + expect(x).toBeCloseTo(95 - 1000, 6); + expect(y).toBeCloseTo(-49, 6); + expect(z).toBeCloseTo(6.75 + 10 / Math.SQRT2, 6); + }); + + it('holds the edge value outside the span', () => { + const century = 36_525; + expect(ephemerisCorrectionMpc(CORRECTION, CORRECTION.startJd - century)).toEqual( + ephemerisCorrectionMpc(CORRECTION, CORRECTION.startJd), + ); + expect(ephemerisCorrectionMpc(CORRECTION, CORRECTION.endJd + century)).toEqual( + ephemerisCorrectionMpc(CORRECTION, CORRECTION.endJd), + ); + }); +}); diff --git a/tools/bodies/buildPlanetEphemeris.ts b/tools/bodies/buildPlanetEphemeris.ts new file mode 100644 index 0000000000..efb1ef4413 --- /dev/null +++ b/tools/bodies/buildPlanetEphemeris.ts @@ -0,0 +1,152 @@ +#!/usr/bin/env node +/** + * buildPlanetEphemeris — fit each planet's `Horizons − Kepler` residual (1900–2100) into the + * committed `PLANET_EPHEMERIS_CORRECTIONS` table. Kepler is the app's own path + * (`keplerianPositionMpc(propagateElements(row, jd))`), so any element-row or frame edit needs a + * rerun. Fits on a coarse grid to 900 km, then verifies the ROUNDED values, as they will be + * parsed from the emitted file, through `ephemerisCorrectionMpc` on every 1-day row; throws + * above 1,000 km. Reads `data/raw/horizons/planets/` (`npm run fetch-horizons-planets`). + */ +import { readFileSync, writeFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; + +import { format, resolveConfig } from 'prettier'; + +import { elementsById } from '../../src/data/bodies/orbitalElements'; +import { SCALE_UNITS } from '../../src/data/scaleUnits'; +import { ephemerisCorrectionMpc } from '../../src/utils/orbit/ephemerisCorrectionMpc'; +import { keplerianPositionMpc } from '../../src/utils/orbit/keplerianPositionMpc'; +import { propagateElements } from '../../src/utils/orbit/propagateElements'; +import type { EphemerisCorrection } from '../../src/@types/scene/EphemerisCorrection'; +import type { Vec3 } from '../../src/@types/math/Vec3'; +import { fitSinusoidSeries } from '../utils/math/fitSinusoidSeries'; +import { rawDataPath } from '../utils/io/rawDataRegistry'; + +const GENERATED_PATH = 'src/data/bodies/planetEphemerisCorrections.generated.ts'; +const GENERATED_BANNER = + `// ${GENERATED_PATH}\n` + + '// !!! GENERATED FILE — DO NOT EDIT BY HAND !!!\n' + + '// Regenerate with: npm run fetch-horizons-planets && npm run build-planet-ephemeris\n' + + '// Source of truth: data/raw/horizons/planets/ (JPL Horizons) minus orbitalElements.ts\n'; + +// [planet id, Horizons target, fit sample step in days]. The step keeps ≥ ~20 samples +// across the shortest period the residual carries. +const PLANETS: readonly [string, string, number][] = [ + ['mercury', '199', 2], + ['venus', '299', 4], + ['earth', '3', 4], + ['mars', '4', 6], + ['jupiter', '5', 10], + ['saturn', '6', 10], + ['uranus', '7', 10], + ['neptune', '8', 10], +]; +const FIT_STOP_KM = 900; +const VERIFY_MAX_KM = 1000; +const MAX_TERMS = 400; +const KM_TO_MPC = SCALE_UNITS.KM_TO_MPC; + +// ω to 12 significant digits: over the 73,000-day span its phase error stays < 1e-7 rad. +// Amplitudes to 0.1 km: hundreds of 0.05 km roundings sum to well under a kilometre. +const fmtOmega = (x: number): string => String(Number(x.toPrecision(12))); +const fmtKm = (x: number): string => String(Math.round(x * 10) / 10); + +function readSeries(naif: string) { + const lines = readFileSync(join(rawDataPath('horizons.planets'), `${naif}.csv`), 'utf8') + .trim() + .split('\n') + .slice(1); + const cols = [0, 1, 2, 3].map(() => new Float64Array(lines.length)); + lines.forEach((line, i) => line.split(',').forEach((v, c) => (cols[c]![i] = Number(v)))); + return { jd: cols[0]!, km: [cols[1]!, cols[2]!, cols[3]!] as const }; +} + +function keplerKm(id: string, jd: number): Vec3 { + const [x, y, z] = keplerianPositionMpc(propagateElements(elementsById(id), jd)); + return [x / KM_TO_MPC, y / KM_TO_MPC, z / KM_TO_MPC]; +} + +/** Emit one correction's literal, and the correction those exact digits parse back to. */ +function emit( + startJd: number, + endJd: number, + polyKm: readonly Vec3[], + terms: readonly number[], +): { text: string; shipped: EphemerisCorrection } { + const polyText = polyKm.map((v) => v.map(fmtKm)); + const termText = terms.map((v, i) => (i % 7 === 0 ? fmtOmega(v) : fmtKm(v))); + const parse = (s: readonly string[]): number[] => s.map(Number); + const shipped: EphemerisCorrection = { + startJd, + endJd, + polyKm: polyText.map(parse) as unknown as EphemerisCorrection['polyKm'], + terms: parse(termText), + }; + const text = + `{ startJd: ${startJd}, endJd: ${endJd}, ` + + `polyKm: [${polyText.map((v) => `[${v.join(', ')}]`).join(', ')}], ` + + `terms: [${termText.join(', ')}] }`; + return { text, shipped }; +} + +function buildPlanet(id: string, naif: string, step: number): { text: string; summary: string } { + const { jd, km } = readSeries(naif); + const startJd = jd[0]!; + const endJd = jd[jd.length - 1]!; + + // Every `step`-th row, plus the last row so the fit is never extrapolating at the end. + const last = jd.length - 1; + const sampleIdx = Array.from({ length: Math.ceil(last / step) + 1 }, (_, i) => + Math.min(i * step, last), + ); + const tJd = Float64Array.from(sampleIdx, (i) => jd[i]!); + const kepler = sampleIdx.map((i) => keplerKm(id, jd[i]!)); + const residual = [0, 1, 2].map((axis) => + Float64Array.from(sampleIdx, (i, s) => km[axis]![i]! - kepler[s]![axis]!), + ); + const fit = fitSinusoidSeries( + tJd, + [residual[0]!, residual[1]!, residual[2]!], + startJd, + endJd, + FIT_STOP_KM, + MAX_TERMS, + ); + const { text, shipped } = emit(startJd, endJd, fit.polyKm, fit.terms); + + let maxKm = 0; + for (let i = 0; i < jd.length; i++) { + const kepler = keplerKm(id, jd[i]!); + const corr = ephemerisCorrectionMpc(shipped, jd[i]!); + const err = [0, 1, 2].map((a) => km[a]![i]! - (kepler[a]! + corr[a]! / KM_TO_MPC)); + maxKm = Math.max(maxKm, Math.hypot(err[0]!, err[1]!, err[2]!)); + } + const nTerms = fit.terms.length / 7; + const summary = `${id}: ${nTerms} terms, max ${Math.round(maxKm)} km vs Horizons (1-day grid)`; + if (maxKm > VERIFY_MAX_KM) + throw new Error(`buildPlanetEphemeris: ${summary} exceeds ${VERIFY_MAX_KM} km`); + return { text: ` // ${summary}\n ${id}: ${text},`, summary }; +} + +async function main(): Promise { + const rows: string[] = []; + for (const [id, naif, step] of PLANETS) { + const { text, summary } = buildPlanet(id, naif, step); + process.stderr.write(`${summary}\n`); + rows.push(text); + } + const source = + GENERATED_BANNER + + "import type { EphemerisCorrection } from '../../@types/scene/EphemerisCorrection';\n\n" + + 'export const PLANET_EPHEMERIS_CORRECTIONS: Readonly> = {\n' + + `${rows.join('\n')}\n};\n`; + const outPath = resolve(GENERATED_PATH); + const formatted = await format(source, { + ...(await resolveConfig(outPath)), + parser: 'typescript', + }); + writeFileSync(outPath, formatted); + process.stderr.write(`wrote ${GENERATED_PATH} (${(formatted.length / 1024).toFixed(1)} kB)\n`); +} + +await main(); diff --git a/tools/fetch/fetchHorizonsPlanets.ts b/tools/fetch/fetchHorizonsPlanets.ts new file mode 100644 index 0000000000..d63a790201 --- /dev/null +++ b/tools/fetch/fetchHorizonsPlanets.ts @@ -0,0 +1,78 @@ +#!/usr/bin/env node +/** + * fetchHorizonsPlanets — pull 1-day heliocentric equatorial (ICRF) positions for the eight + * planets over 1900–2100 from the JPL Horizons API into data/raw/horizons/planets/.csv + * (`jd,x_km,y_km,z_km`, ~3 MB each). Mars–Neptune are their system barycentres and Earth is + * the Earth–Moon barycentre: those are the points the app's Kepler rows describe. UT time tags, + * so the fit absorbs TDB−UT and the app's UTC `simDays` needs no conversion. Query in README. + */ +import { mkdirSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { parseHorizonsVectorsCsv } from '../parsers/horizonsVectorsCsv'; +import { mergeHorizonsChunks } from '../utils/data/mergeHorizonsChunks'; +import { rawDataPath } from '../utils/io/rawDataRegistry'; +import type { HorizonsVectorRow } from '../parsers/@types/HorizonsVectorRow'; + +const API = 'https://ssd.jpl.nasa.gov/api/horizons.api'; +const TARGETS = ['199', '299', '3', '4', '5', '6', '7', '8']; +const CHUNKS: readonly [string, string][] = [ + ['1900-01-01', '1950-01-01'], + ['1950-01-01', '2000-01-01'], + ['2000-01-01', '2050-01-01'], + ['2050-01-01', '2100-01-01'], +]; +const MAX_ATTEMPTS = 5; + +function queryUrl(naif: string, start: string, stop: string): string { + const params: Record = { + format: 'json', + COMMAND: `'${naif}'`, + OBJ_DATA: "'NO'", + MAKE_EPHEM: "'YES'", + EPHEM_TYPE: "'VECTORS'", + CENTER: "'500@10'", + REF_PLANE: "'FRAME'", + TIME_TYPE: "'UT'", + OUT_UNITS: "'KM-S'", + CSV_FORMAT: "'YES'", + VEC_TABLE: "'1'", + START_TIME: `'${start}'`, + STOP_TIME: `'${stop}'`, + STEP_SIZE: "'1 d'", + }; + return `${API}?${new URLSearchParams(params).toString()}`; +} + +async function fetchChunk(naif: string, start: string, stop: string): Promise { + for (let attempt = 1; ; attempt++) { + try { + const response = await fetch(queryUrl(naif, start, stop)); + if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); + const body = (await response.json()) as { result?: string; error?: string }; + if (body.error || !body.result) throw new Error(body.error ?? 'empty result'); + return parseHorizonsVectorsCsv(body.result); + } catch (err) { + if (attempt >= MAX_ATTEMPTS) throw err; + console.warn(`fetchHorizonsPlanets: ${naif} ${start} attempt ${attempt} failed: ${err}`); + await new Promise((r) => setTimeout(r, 2000 * attempt)); + } + } +} + +async function main(): Promise { + const outDir = rawDataPath('horizons.planets'); + mkdirSync(outDir, { recursive: true }); + console.log('fetchHorizonsPlanets: downloading 8 × ~3 MB of 1-day vectors from JPL Horizons'); + for (const naif of TARGETS) { + const chunks: HorizonsVectorRow[][] = []; + for (const [start, stop] of CHUNKS) chunks.push(await fetchChunk(naif, start, stop)); + const rows = mergeHorizonsChunks(chunks); + const csv = ['jd,x_km,y_km,z_km', ...rows.map((r) => `${r.jd},${r.xKm},${r.yKm},${r.zKm}`)]; + const outPath = join(outDir, `${naif}.csv`); + writeFileSync(outPath, `${csv.join('\n')}\n`); + console.log(`fetchHorizonsPlanets: wrote ${outPath} (${rows.length} rows)`); + } +} + +await main(); diff --git a/tools/parsers/@types/HorizonsVectorRow.d.ts b/tools/parsers/@types/HorizonsVectorRow.d.ts new file mode 100644 index 0000000000..19e38bd139 --- /dev/null +++ b/tools/parsers/@types/HorizonsVectorRow.d.ts @@ -0,0 +1,11 @@ +/** + * HorizonsVectorRow — one JPL Horizons state-vector row (VEC_TABLE=1: position only), + * heliocentric equatorial ICRF in km, time-tagged by its UT Julian date. + */ + +export type HorizonsVectorRow = { + readonly jd: number; + readonly xKm: number; + readonly yKm: number; + readonly zKm: number; +}; diff --git a/tools/parsers/horizonsVectorsCsv.ts b/tools/parsers/horizonsVectorsCsv.ts new file mode 100644 index 0000000000..5ce6482409 --- /dev/null +++ b/tools/parsers/horizonsVectorsCsv.ts @@ -0,0 +1,32 @@ +/** + * parseHorizonsVectorsCsv — the data rows of a Horizons VECTORS result (CSV_FORMAT=YES, + * VEC_TABLE=1): everything between `$$SOE` and `$$EOE`, columns `JDUT, Calendar, X, Y, Z,`. + * The calendar column carries no comma of its own, so a plain split is safe. + */ + +import type { HorizonsVectorRow } from './@types/HorizonsVectorRow'; + +export function parseHorizonsVectorsCsv(result: string): HorizonsVectorRow[] { + const start = result.indexOf('$$SOE'); + const end = result.indexOf('$$EOE'); + if (start < 0 || end < start) throw new Error('parseHorizonsVectorsCsv: no $$SOE/$$EOE block'); + + return result + .slice(start + '$$SOE'.length, end) + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .map((line) => { + const cols = line.split(',').map((c) => c.trim()); + const row = { + jd: Number(cols[0]), + xKm: Number(cols[2]), + yKm: Number(cols[3]), + zKm: Number(cols[4]), + }; + if (![row.jd, row.xKm, row.yKm, row.zKm].every(Number.isFinite)) { + throw new Error(`parseHorizonsVectorsCsv: unparseable row "${line}"`); + } + return row; + }); +} diff --git a/tools/perf/perfScenarios.ts b/tools/perf/perfScenarios.ts index 0e652fb789..299f2ff04e 100644 --- a/tools/perf/perfScenarios.ts +++ b/tools/perf/perfScenarios.ts @@ -49,7 +49,7 @@ export type PerfScenario = { readonly name: string; readonly pose: PerfPose }; // time). // Factored out so those poses differ only in the axes that actually vary // (distance/yaw/pitch) and the target can never drift between them. -const EARTH_TARGET: Vec3 = [-8.5895045e-13, 4.3022465e-12, 1.8652508e-12]; +const EARTH_TARGET: Vec3 = [-8.5882588e-13, 4.3023659e-12, 1.8652815e-12]; // The shared look-at point for `milky-way-outside` and `milky-way-close`: the // galactic centre (~8 kpc off the origin — contrast `EARTH_TARGET`'s ~1 AU). diff --git a/tools/utils/data/mergeHorizonsChunks.ts b/tools/utils/data/mergeHorizonsChunks.ts new file mode 100644 index 0000000000..36d21dbbea --- /dev/null +++ b/tools/utils/data/mergeHorizonsChunks.ts @@ -0,0 +1,18 @@ +/** + * mergeHorizonsChunks — concatenate per-chunk Horizons rows into one strictly increasing + * series. Adjacent chunks share their boundary instant (each query is inclusive at both + * ends), so a row whose `jd` is not past the previous row's is dropped. + */ + +import type { HorizonsVectorRow } from '../../parsers/@types/HorizonsVectorRow'; + +export function mergeHorizonsChunks( + chunks: readonly (readonly HorizonsVectorRow[])[], +): HorizonsVectorRow[] { + const merged: HorizonsVectorRow[] = []; + for (const row of chunks.flat()) { + const last = merged.at(-1); + if (!last || row.jd > last.jd) merged.push(row); + } + return merged; +} diff --git a/tools/utils/io/rawDataRegistry.ts b/tools/utils/io/rawDataRegistry.ts index 2d5b95a044..95fda8d19a 100644 --- a/tools/utils/io/rawDataRegistry.ts +++ b/tools/utils/io/rawDataRegistry.ts @@ -1653,6 +1653,25 @@ export const RAW_DATA = { description: 'What `meshes.sha256` pins, the R2 backup, and the restore command.', }, + // ─── JPL Horizons planet vectors (ephemeris-correction fit input) ───── + + 'horizons.planets': { + path: 'data/raw/horizons/planets', + kind: 'directory', + source: 'gitignored', + description: + 'Holds `.csv` (`jd,x_km,y_km,z_km`): 1-day heliocentric ICRF positions of the eight planets, 1900–2100, UT time tags. Input to `build-planet-ephemeris`.', + upstream: 'https://ssd.jpl.nasa.gov/api/horizons.api', + fetcher: 'tools/fetch/fetchHorizonsPlanets.ts', + readme: 'horizons.planets.readme', + }, + 'horizons.planets.readme': { + path: 'data/raw/horizons/planets/README.md', + kind: 'file', + source: 'committed', + description: 'Provenance for the Horizons planet vectors — the exact query, targets and span.', + }, + // ─── StarNet++ weights (famous-galaxy curator) ──────────────────────── 'starnet.weights': { diff --git a/tools/utils/math/dot.ts b/tools/utils/math/dot.ts new file mode 100644 index 0000000000..522827ba40 --- /dev/null +++ b/tools/utils/math/dot.ts @@ -0,0 +1,5 @@ +export const dot = (a: Float64Array, b: Float64Array): number => { + let s = 0; + for (let i = 0; i < a.length; i++) s += a[i]! * b[i]!; + return s; +}; diff --git a/tools/utils/math/fft.ts b/tools/utils/math/fft.ts new file mode 100644 index 0000000000..6b67d9bd5b --- /dev/null +++ b/tools/utils/math/fft.ts @@ -0,0 +1,45 @@ +/** + * fft — in-place iterative radix-2 Cooley–Tukey transform of a complex signal held as two + * real arrays. Forward sign convention (e^{−2πikn/N}), unnormalised. Length must be a power + * of two; the caller zero-pads. + */ + +export function fft(re: Float64Array, im: Float64Array): void { + const n = re.length; + if (n !== im.length || (n & (n - 1)) !== 0) + throw new Error(`fft: length ${n} not a power of two`); + + // Bit-reversal permutation. + for (let i = 1, j = 0; i < n; i++) { + let bit = n >> 1; + for (; j & bit; bit >>= 1) j ^= bit; + j ^= bit; + if (i < j) { + const tr = re[i]!; + re[i] = re[j]!; + re[j] = tr; + const ti = im[i]!; + im[i] = im[j]!; + im[j] = ti; + } + } + + for (let len = 2; len <= n; len <<= 1) { + const half = len >> 1; + const ang = (-2 * Math.PI) / len; + for (let k = 0; k < half; k++) { + // Twiddle from a direct cos/sin per k, not a running product: no drift at 2^19 points. + const wr = Math.cos(ang * k); + const wi = Math.sin(ang * k); + for (let i = k; i < n; i += len) { + const b = i + half; + const xr = re[b]! * wr - im[b]! * wi; + const xi = re[b]! * wi + im[b]! * wr; + re[b] = re[i]! - xr; + im[b] = im[i]! - xi; + re[i] = re[i]! + xr; + im[i] = im[i]! + xi; + } + } + } +} diff --git a/tools/utils/math/fitSinusoidSeries.ts b/tools/utils/math/fitSinusoidSeries.ts new file mode 100644 index 0000000000..0872b538ea --- /dev/null +++ b/tools/utils/math/fitSinusoidSeries.ts @@ -0,0 +1,164 @@ +/** + * fitSinusoidSeries — fit a 3-axis residual (km, near-uniform time grid) as a cubic in τ plus + * sinusoids, greedily: FFT the remaining residual (×8 zero-pad, power summed over xyz) to + * bracket the strongest tone, refine its ω, add its cos and sin columns, stop at `stopKm`. + * Columns are orthonormalised incrementally (Gram–Schmidt, two passes), so every step is an + * exact least-squares refit; the kept R turns the orthogonal coefficients back into plain + * (ω, cos, sin) amplitudes. τ and phase conventions match `ephemerisCorrectionMpc`. + */ + +import type { EphemerisCorrection } from '../../../src/@types/scene/EphemerisCorrection'; +import type { Vec3 } from '../../../src/@types/math/Vec3'; +import { dot } from './dot'; +import { fft } from './fft'; + +const ZERO_PAD = 8; +const GOLDEN = (Math.sqrt(5) - 1) / 2; +// Shrinks the two-bin bracket by 0.618^40 ≈ 4e-9: far below any phase error that matters. +const GOLDEN_ITERATIONS = 40; + +export function fitSinusoidSeries( + tJd: Float64Array, + residualKm: [Float64Array, Float64Array, Float64Array], + startJd: number, + endJd: number, + stopKm: number, + maxTerms: number, +): Pick { + const n = tJd.length; + // Uniform, except the last gap may be short so the grid can end exactly on `endJd`: an + // unsampled tail is extrapolated, and drifts past the stop. The FFT only brackets ω, so + // one sample slightly off its index slot is harmless; the refine and refit use true times. + const stepDays = tJd[1]! - tJd[0]!; + for (let i = 1; i < n; i++) { + const gap = tJd[i]! - tJd[i - 1]!; + const ok = i === n - 1 ? gap > 0 && gap <= stepDays : Math.abs(gap - stepDays) < 1e-6; + if (!ok) throw new Error('fit: grid not uniform'); + } + const dt = Float64Array.from(tJd, (t) => t - startJd); + const tau = Float64Array.from(dt, (d) => (2 * d) / (endJd - startJd) - 1); + + const residual = residualKm.map((a) => Float64Array.from(a)); + const q: Float64Array[] = []; + const rCols: Float64Array[] = []; // column j of the upper-triangular R + const proj: number[][] = [[], [], []]; // q_j · y, per axis + + const addColumn = (a: Float64Array): void => { + const c = Float64Array.from(a); + const rc = new Float64Array(q.length + 1); + for (let pass = 0; pass < 2; pass++) { + q.forEach((qj, j) => { + const d = dot(qj, c); + for (let i = 0; i < n; i++) c[i]! -= d * qj[i]!; + rc[j]! += d; + }); + } + const norm = Math.sqrt(dot(c, c)); + if (norm < 1e-9 * Math.sqrt(dot(a, a))) throw new Error('fit: degenerate column'); + for (let i = 0; i < n; i++) c[i]! /= norm; + rc[q.length] = norm; + q.push(c); + rCols.push(rc); + residual.forEach((r, axis) => { + const d = dot(c, r); + for (let i = 0; i < n; i++) r[i]! -= d * c[i]!; + proj[axis]!.push(d); + }); + }; + const maxErr = (): number => { + let m = 0; + for (let i = 0; i < n; i++) + m = Math.max(m, Math.hypot(residual[0]![i]!, residual[1]![i]!, residual[2]![i]!)); + return m; + }; + + // Residual energy (xyz) a cos/sin pair at ω would remove, net of what the kept columns + // already span. The bare periodogram ignores that overlap and is biased wherever a tone + // correlates with the cubic or a neighbour, which costs extra terms to mop up. + const capturedEnergy = (omega: number): number => { + const c = Float64Array.from(dt, (d) => Math.cos(omega * d)); + const s = Float64Array.from(dt, (d) => Math.sin(omega * d)); + let gcc = dot(c, c); + let gcs = dot(c, s); + let gss = dot(s, s); + for (const qj of q) { + const qc = dot(qj, c); + const qs = dot(qj, s); + gcc -= qc * qc; + gcs -= qc * qs; + gss -= qs * qs; + } + const det = gcc * gss - gcs * gcs; + let total = 0; + for (const r of residual) { + const bc = dot(r, c); + const bs = dot(r, s); + total += (gss * bc * bc - 2 * gcs * bc * bs + gcc * bs * bs) / det; + } + return total; + }; + + for (let p = 0; p <= 3; p++) addColumn(Float64Array.from(tau, (u) => u ** p)); + + let m = 1; + while (m < n * ZERO_PAD) m <<= 1; + const re = new Float64Array(m); + const im = new Float64Array(m); + const power = new Float64Array(m / 2); + const omegas: number[] = []; + while (omegas.length < maxTerms && maxErr() > stopKm) { + power.fill(0); + for (const r of residual) { + re.fill(0); + im.fill(0); + re.set(r); + fft(re, im); + for (let k = 1; k < m / 2; k++) power[k]! += re[k]! ** 2 + im[k]! ** 2; + } + let kb = 1; + for (let k = 2; k < m / 2 - 1; k++) if (power[k]! > power[kb]!) kb = k; + // The padded FFT only brackets the peak; a golden-section search inside the bracket + // lands on it, so one term absorbs a tone instead of a cluster of neighbours. + // Bin 0 is DC, where the sin column vanishes: keep the bracket at one padded bin or above. + let lo = (2 * Math.PI * Math.max(kb - 1, 1)) / (m * stepDays); + let hi = (2 * Math.PI * (kb + 1)) / (m * stepDays); + let w1 = hi - GOLDEN * (hi - lo); + let w2 = lo + GOLDEN * (hi - lo); + let e1 = capturedEnergy(w1); + let e2 = capturedEnergy(w2); + for (let it = 0; it < GOLDEN_ITERATIONS; it++) { + if (e1 < e2) { + lo = w1; + w1 = w2; + e1 = e2; + w2 = lo + GOLDEN * (hi - lo); + e2 = capturedEnergy(w2); + } else { + hi = w2; + w2 = w1; + e2 = e1; + w1 = hi - GOLDEN * (hi - lo); + e1 = capturedEnergy(w1); + } + } + const omega = (lo + hi) / 2; + addColumn(Float64Array.from(dt, (d) => Math.cos(omega * d))); + addColumn(Float64Array.from(dt, (d) => Math.sin(omega * d))); + omegas.push(omega); + } + + // Back-substitute R·x = qᵀy per axis: x are the plain-basis coefficients. + const k = q.length; + const coef = proj.map((d) => { + const x = new Float64Array(k); + for (let j = k - 1; j >= 0; j--) { + let s = d[j]!; + for (let l = j + 1; l < k; l++) s -= rCols[l]![j]! * x[l]!; + x[j] = s / rCols[j]![j]!; + } + return x; + }); + const at = (j: number): Vec3 => [coef[0]![j]!, coef[1]![j]!, coef[2]![j]!]; + const terms = omegas.flatMap((omega, t) => [omega, ...at(4 + 2 * t), ...at(5 + 2 * t)]); + return { polyKm: [at(0), at(1), at(2), at(3)], terms }; +}