Merged suite: 21/21 gates in-browser incl. NEO real-orbit (err 1.16%), Jupiter@1000BC Δ0.004 AU, 2b-proof. Deep-time readout now '1000 BC-01-15'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
365 lines
20 KiB
HTML
365 lines
20 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>SOLARGOD ▸ verify</title>
|
||
<style>
|
||
body { background: #05060a; color: #e6ddca; font-family: ui-monospace, Menlo, monospace; font-size: 13px; padding: 24px; }
|
||
h1 { color: #d9a441; font-size: 18px; letter-spacing: 2px; }
|
||
table { border-collapse: collapse; width: 100%; max-width: 960px; margin-top: 12px; }
|
||
th, td { text-align: left; padding: 5px 10px; border-bottom: 1px solid rgba(217,164,65,0.14); }
|
||
th { color: #8a8674; font-weight: 600; text-transform: uppercase; font-size: 10px; }
|
||
.pass { color: #6fcf6f; font-weight: 700; }
|
||
.fail { color: #e06a5a; font-weight: 700; }
|
||
.pending { color: #8a8674; }
|
||
.detail { color: #8a8674; font-size: 11px; }
|
||
#summary { margin-top: 16px; font-size: 15px; }
|
||
.importmap-note { color: #8a8674; }
|
||
</style>
|
||
<!-- Three.js vendored (offline-first, zero CDN). See vendor/three/PROVENANCE.txt. -->
|
||
<script type="importmap">
|
||
{ "imports": { "three": "./vendor/three/build/three.module.js", "three/addons/": "./vendor/three/examples/jsm/" } }
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<h1>SOLARGOD ▸ VERIFY</h1>
|
||
<div class="importmap-note">lib.js + ephem.js + scale.js assertions · live Horizons via serve.py proxy. Serve with <code>python3 serve.py</code> then open <code>/verify.html</code>.</div>
|
||
<table><thead><tr><th>#</th><th>gate</th><th>result</th><th>detail</th></tr></thead><tbody id="rows"></tbody></table>
|
||
<div id="summary" class="pending">running…</div>
|
||
|
||
<script type="module">
|
||
import * as lib from './js/lib.js';
|
||
import * as ephem from './js/ephem.js';
|
||
import * as scale from './js/scale.js';
|
||
import { CONFIG } from './js/config.js';
|
||
scale.init(CONFIG);
|
||
|
||
const rows = document.getElementById('rows');
|
||
let n = 0, passed = 0, failed = 0;
|
||
function row(name, ok, detail) {
|
||
n++; if (ok) passed++; else failed++;
|
||
const tr = document.createElement('tr');
|
||
tr.innerHTML = `<td>${n}</td><td>${name}</td><td class="${ok ? 'pass' : 'fail'}">${ok ? 'PASS' : 'FAIL'}</td><td class="detail">${detail}</td>`;
|
||
rows.appendChild(tr);
|
||
}
|
||
function pendingRow(name) {
|
||
const tr = document.createElement('tr');
|
||
tr.innerHTML = `<td>—</td><td>${name}</td><td class="pending">…</td><td class="detail">querying Horizons…</td>`;
|
||
rows.appendChild(tr);
|
||
return tr;
|
||
}
|
||
|
||
// ---- 1. Horizons ground truth (brief §9), JD 2461236.5 = 2026-07-15 TDB ----
|
||
const JD = 2461236.5;
|
||
const truth = {
|
||
mars: { x: +1.05351078, y: +1.00862522, z: -0.00469549, tol: 0.005 },
|
||
jupiter: { x: -3.01967256, y: +4.33218144, z: +0.04956505, tol: 0.01 },
|
||
earth: { x: +0.38381080, y: -0.94118486, z: +0.00005421, tol: 0.003 },
|
||
};
|
||
for (const [id, t] of Object.entries(truth)) {
|
||
const p = ephem.helioEcl(id, JD);
|
||
const worst = Math.max(Math.abs(p.x - t.x), Math.abs(p.y - t.y), Math.abs(p.z - t.z));
|
||
row(`${id} @ 2026-07-15 (heliocentric ecliptic AU)`, worst <= t.tol,
|
||
`Δmax=${worst.toExponential(2)} AU (tol ${t.tol}) · got (${p.x.toFixed(5)}, ${p.y.toFixed(5)}, ${p.z.toFixed(5)})`);
|
||
}
|
||
|
||
// ---- 2. Kepler solver converges for e = 0.97 ----
|
||
{
|
||
const M = 0.5, e = 0.97, E = ephem.solveKepler(M, e);
|
||
const resid = Math.abs(E - e * Math.sin(E) - M);
|
||
row('Kepler solver e=0.97', resid < 1e-9, `residual ${resid.toExponential(2)} rad`);
|
||
}
|
||
|
||
// ---- 3. orbitPath endpoints join ----
|
||
{
|
||
const path = ephem.orbitPath('mercury', JD, 128);
|
||
const m = path.length / 3;
|
||
const d = Math.max(Math.abs(path[0] - path[(m-1)*3]), Math.abs(path[1] - path[(m-1)*3+1]), Math.abs(path[2] - path[(m-1)*3+2]));
|
||
row('orbitPath endpoints join', d < 1e-12, `Δ ${d.toExponential(2)} AU`);
|
||
}
|
||
|
||
// ---- 4. eclToWorld round-trips ----
|
||
{
|
||
const v = { x: 1.234, y: -5.678, z: 0.910 };
|
||
const w = lib.eclToWorld(v, {}); const back = lib.worldToEcl(w, {});
|
||
const d = Math.max(Math.abs(back.x - v.x), Math.abs(back.y - v.y), Math.abs(back.z - v.z));
|
||
row('eclToWorld round-trip', d === 0, `Δ ${d}`);
|
||
}
|
||
|
||
// ---- 5. MEGA→TRUE preserves direction (unit dot ≈ 1) ----
|
||
{
|
||
const ecl = ephem.helioEcl('jupiter', JD);
|
||
const a = scale.viewFromEcl(ecl, {});
|
||
scale.setMode('true');
|
||
for (let i = 0; i < 60 && scale.isTransitioning(); i++) scale.update(100);
|
||
const b = scale.viewFromEcl(ecl, {});
|
||
const na = Math.hypot(a.x, a.y, a.z), nb = Math.hypot(b.x, b.y, b.z);
|
||
const dot = (a.x * b.x + a.y * b.y + a.z * b.z) / (na * nb);
|
||
row('MEGA→TRUE keeps direction', Math.abs(dot - 1) < 1e-9, `unit dot = ${dot.toFixed(12)}`);
|
||
scale.setMode('mega'); for (let i = 0; i < 60 && scale.isTransitioning(); i++) scale.update(100);
|
||
}
|
||
|
||
// ---- 5b. Belt sample propagates as a torus between Mars & Jupiter (Stage 5) ----
|
||
{
|
||
// three synthetic MBAs (a in 2.2–3.2) at random anomalies must land, in the
|
||
// compressed view, at radii strictly between Mars's and Jupiter's.
|
||
const rMars = scale.radial(1.524), rJup = scale.radial(5.203);
|
||
const els = [
|
||
{ a: 2.3, e: 0.1, i: 5, om: 30, w: 60, ma: 10, epoch: JD },
|
||
{ a: 2.77, e: 0.08, i: 10, om: 80, w: 73, ma: 200, epoch: JD },
|
||
{ a: 3.15, e: 0.15, i: 15, om: 150, w: 200, ma: 300, epoch: JD },
|
||
];
|
||
let ok = true, rmin = Infinity, rmax = 0;
|
||
for (const el of els) {
|
||
const p = ephem.smallBodyEcl(el, JD, {});
|
||
const r = scale.radial(Math.hypot(p.x, p.y, p.z));
|
||
rmin = Math.min(rmin, r); rmax = Math.max(rmax, r);
|
||
if (!(r > rMars && r < rJup)) ok = false;
|
||
}
|
||
row('main belt sits between Mars & Jupiter (view radii)', ok, `belt view-r ${rmin.toFixed(1)}–${rmax.toFixed(1)} ∈ (Mars ${rMars.toFixed(1)}, Jupiter ${rJup.toFixed(1)})`);
|
||
}
|
||
|
||
// ---- 5c. Halley's aphelion reaches beyond Neptune's orbit (Stage 5 gate) ----
|
||
{
|
||
const a = 17.93, e = 0.9679; // 1P/Halley (SBDB)
|
||
const aphelion = a * (1 + e), neptune = 30.07;
|
||
row("Halley aphelion beyond Neptune", aphelion > neptune, `aphelion ${aphelion.toFixed(1)} AU > Neptune ${neptune} AU`);
|
||
}
|
||
|
||
// ---- 6+7. Live Horizons via proxy — two extra epochs (also gates the proxy) ----
|
||
// Mars @ J2000.0 (JD 2451545.0) and Jupiter @ 1900-01-01 (JD 2415020.5).
|
||
async function horizonsVec(command, jd) {
|
||
const q = {
|
||
format: 'text', COMMAND: `'${command}'`, OBJ_DATA: 'NO', MAKE_EPHEM: 'YES',
|
||
EPHEM_TYPE: 'VECTORS', CENTER: "'500@10'", TLIST: String(jd),
|
||
REF_PLANE: 'ECLIPTIC', REF_SYSTEM: 'J2000', VEC_TABLE: '1', OUT_UNITS: 'AU-D', CSV_FORMAT: 'NO',
|
||
};
|
||
const qs = Object.entries(q).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
|
||
const res = await fetch(`${CONFIG.proxy.horizons}?${qs}`);
|
||
const text = await res.text();
|
||
const soe = text.indexOf('$$SOE'), eoe = text.indexOf('$$EOE');
|
||
if (soe < 0 || eoe < 0) throw new Error('no $$SOE block — ' + text.slice(0, 160));
|
||
const block = text.slice(soe, eoe);
|
||
const gx = block.match(/X\s*=\s*(-?[\d.]+E?[-+]?\d*)/i);
|
||
const gy = block.match(/Y\s*=\s*(-?[\d.]+E?[-+]?\d*)/i);
|
||
const gz = block.match(/Z\s*=\s*(-?[\d.]+E?[-+]?\d*)/i);
|
||
if (!gx || !gy || !gz) throw new Error('could not parse X/Y/Z');
|
||
return { x: parseFloat(gx[1]), y: parseFloat(gy[1]), z: parseFloat(gz[1]) };
|
||
}
|
||
async function liveCheck(label, id, command, jd, tol) {
|
||
const tr = pendingRow(label);
|
||
try {
|
||
const got = await horizonsVec(command, jd);
|
||
const mine = ephem.helioEcl(id, jd);
|
||
const worst = Math.max(Math.abs(got.x - mine.x), Math.abs(got.y - mine.y), Math.abs(got.z - mine.z));
|
||
tr.remove();
|
||
row(label, worst <= tol, `Δmax=${worst.toExponential(2)} AU (tol ${tol}) · Horizons (${got.x.toFixed(5)}, ${got.y.toFixed(5)}, ${got.z.toFixed(5)})`);
|
||
} catch (err) {
|
||
tr.remove();
|
||
row(label, false, `proxy/Horizons error: ${err.message}`);
|
||
}
|
||
finish();
|
||
}
|
||
function finish() {
|
||
const s = document.getElementById('summary');
|
||
s.textContent = `${passed}/${n} gates pass` + (failed ? ` — ${failed} FAILING` : ' — ALL PASS ✦');
|
||
s.className = failed ? 'fail' : 'pass';
|
||
}
|
||
// ---- 10. Voyager 1 (-31) @ 2026-07-15 ≈ (−32.07, −136.21, +98.55) AU (brief §9) ----
|
||
async function craftCheck(label, command, jd, want, tol) {
|
||
const tr = pendingRow(label);
|
||
try {
|
||
const got = await horizonsVec(command, jd);
|
||
const worst = Math.max(Math.abs(got.x - want.x), Math.abs(got.y - want.y), Math.abs(got.z - want.z));
|
||
tr.remove();
|
||
row(label, worst <= tol, `Δmax=${worst.toFixed(3)} AU (tol ${tol}) · Horizons (${got.x.toFixed(2)}, ${got.y.toFixed(2)}, ${got.z.toFixed(2)}) · ${Math.hypot(got.x,got.y,got.z).toFixed(1)} AU out`);
|
||
} catch (err) { tr.remove(); row(label, false, `Horizons error: ${err.message}`); }
|
||
finish();
|
||
}
|
||
// ---- 11. JWST (-170) hugs Earth (Δ < 0.02 AU) ----
|
||
async function jwstCheck() {
|
||
const label = 'JWST (-170) hugs Earth Δ<0.02 AU';
|
||
const tr = pendingRow(label);
|
||
try {
|
||
const j = await horizonsVec('-170', 2461236.5);
|
||
const e = await horizonsVec('399', 2461236.5);
|
||
const d = Math.hypot(j.x - e.x, j.y - e.y, j.z - e.z);
|
||
tr.remove();
|
||
row(label, d < 0.02, `Δ=${(d).toFixed(4)} AU from Earth (${(d*lib.AU_KM/1e6).toFixed(2)}M km)`);
|
||
} catch (err) { tr.remove(); row(label, false, `Horizons error: ${err.message}`); }
|
||
finish();
|
||
}
|
||
finish();
|
||
// Sequential (not parallel) — Horizons throttles bursts of concurrent requests.
|
||
(async () => {
|
||
await liveCheck('Mars @ J2000.0 vs live Horizons', 'mars', '499', 2451545.0, 0.01);
|
||
await liveCheck('Jupiter @ 1900-01-01 vs live Horizons', 'jupiter', '599', 2415020.5, 0.02);
|
||
await craftCheck('Voyager 1 (-31) @ 2026-07-15', '-31', 2461236.5, { x: -32.07, y: -136.21, z: 98.55 }, 0.05);
|
||
await jwstCheck();
|
||
})();
|
||
|
||
/* PERIHELION-N — NEARGOD close-approach gate. A real NEO's propagated
|
||
heliocentric distance to Earth(≈EMB) at its CAD close-approach epoch must
|
||
match CAD's miss distance within 25% — generous on purpose: Earth≈EMB,
|
||
two-body propagation, CAD dist is geocentric; the point is catching
|
||
wrong-by-an-AU bugs, not arcseconds. Exercises the full lane-N pipeline:
|
||
CAD des → sbdb.api full-prec elements → parseSbdbElements → shared Kepler
|
||
core (ephem.smallBodyEcl), the identical code path the layer draws. */
|
||
import { parseSbdbElements } from './js/layers/neos.js';
|
||
async function neoCloseApproachGate() {
|
||
const label = 'NEO propagated miss vs CAD (real orbit)';
|
||
const tr = pendingRow(label);
|
||
try {
|
||
const LD_AU = 384400 / lib.AU_KM;
|
||
// Fixed window anchored at the sim's default date so the gate is
|
||
// deterministic and always populated (past close approaches stay in CAD).
|
||
const cad = await (await fetch(`${CONFIG.proxy.cad}?date-min=2026-06-16&date-max=2026-08-15&dist-max=0.05&sort=dist&limit=40`)).json();
|
||
const F = cad.fields, ix = (k) => F.indexOf(k);
|
||
const rows = cad.data || [];
|
||
if (!rows.length) throw new Error('CAD returned no rows');
|
||
// Pick the largest-miss approach: 25% of a bigger distance is the most
|
||
// robust absolute tolerance (least sensitive to EMB≈Earth + two-body drift).
|
||
let best = null;
|
||
for (const r of rows) { const dist = +r[ix('dist')]; if (!best || dist > best.dist) best = { des: r[ix('des')], jd: +r[ix('jd')], dist }; }
|
||
const j = await (await fetch(`${CONFIG.proxy.sbdbLookup}?sstr=${encodeURIComponent(best.des)}&full-prec=true`)).json();
|
||
const el = parseSbdbElements(j);
|
||
if (!el) throw new Error(`no elliptical elements for ${best.des}`);
|
||
const neo = ephem.smallBodyEcl(el, best.jd, {});
|
||
const emb = ephem.helioEcl('earth', best.jd, {});
|
||
const d = Math.hypot(neo.x - emb.x, neo.y - emb.y, neo.z - emb.z);
|
||
const relErr = Math.abs(d - best.dist) / best.dist;
|
||
tr.remove();
|
||
row(label, relErr < 0.25,
|
||
`${best.des} @ jd ${best.jd.toFixed(3)}: propagated ${(d / LD_AU).toFixed(2)} LD vs CAD ${(best.dist / LD_AU).toFixed(2)} LD — err ${(relErr * 100).toFixed(2)}% (tol 25%)`);
|
||
} catch (err) { tr.remove(); row(label, false, `neos/SBDB error: ${err.message}`); }
|
||
finish();
|
||
}
|
||
neoCloseApproachGate();
|
||
|
||
/* PERIHELION-E — sky-events math gates (findEvents, pure math, no network).
|
||
Dynamic import keeps the whole lane-E addition one contiguous end block. */
|
||
{
|
||
const { findEvents, nextEvent } = await import('./js/layers/events.js');
|
||
const jdOf = (y, mo, d) => lib.jdFromUnixMs(Date.UTC(y, mo - 1, d));
|
||
const dstr = (jd) => lib.formatUTCDate(lib.unixMsFromJd(jd));
|
||
const evs = findEvents(jdOf(2026, 1, 1), jdOf(2028, 1, 1));
|
||
|
||
// (E-a) Mars opposition within ±2 days of the known 2027-02-19 event.
|
||
{
|
||
const target = jdOf(2027, 2, 19);
|
||
const opp = evs.filter((e) => e.bodies[0] === 'mars' && e.type === 'opposition')
|
||
.map((e) => ({ jd: e.jd, d: Math.abs(e.jd - target) })).sort((a, b) => a.d - b.d)[0];
|
||
row('Mars opposition ±2 d of 2027-02-19', opp && opp.d <= 2,
|
||
opp ? `found ${dstr(opp.jd)}, Δ=${opp.d.toFixed(2)} d (tol 2)` : 'no Mars opposition found');
|
||
}
|
||
|
||
// (E-b) Venus greatest elongations: consecutive ones alternate E/W and are
|
||
// ~142 d apart (the elongation↔elongation cadence). NB the brief's "~72 d"
|
||
// is the elongation↔inferior-conjunction gap, gated separately below.
|
||
const vElong = evs.filter((e) => e.bodies[0] === 'venus' && e.type.startsWith('elongation'));
|
||
{
|
||
const ok = vElong.length >= 2;
|
||
const gap = ok ? vElong[1].jd - vElong[0].jd : NaN;
|
||
const alt = ok && vElong[0].type !== vElong[1].type; // east then west (or vice versa)
|
||
row('Venus consecutive greatest elongations (alternate E/W, 120–160 d)',
|
||
ok && alt && gap >= 120 && gap <= 160,
|
||
ok ? `${dstr(vElong[0].jd)} (${vElong[0].type.split('-')[1]}) → ${dstr(vElong[1].jd)} (${vElong[1].type.split('-')[1]}) = ${gap.toFixed(1)} d` : 'fewer than 2 elongations');
|
||
}
|
||
|
||
// (E-b′) The brief's ~72 d: a Venus greatest elongation to the neighbouring
|
||
// inferior conjunction — 60–80 d — which also gates the conjunction finder.
|
||
{
|
||
const vInf = evs.filter((e) => e.bodies[0] === 'venus' && e.label.includes('inferior'));
|
||
const ok = vElong.length && vInf.length;
|
||
const gap = ok ? vInf.map((c) => Math.abs(c.jd - vElong[0].jd)).sort((a, b) => a - b)[0] : NaN;
|
||
row('Venus greatest elongation → inferior conjunction 60–80 d',
|
||
ok && gap >= 60 && gap <= 80, ok ? `gap = ${gap.toFixed(1)} d (tol 60–80)` : 'missing elongation/conjunction');
|
||
}
|
||
|
||
// (E-c) nextEvent agrees with the scan: Mars headline is the same opposition.
|
||
{
|
||
const ne = nextEvent('mars', jdOf(2026, 7, 16));
|
||
const target = jdOf(2027, 2, 19);
|
||
row('nextEvent(mars) → 2027 opposition', ne && ne.type === 'opposition' && Math.abs(ne.jd - target) <= 2,
|
||
ne ? `${dstr(ne.jd)} · ${ne.label}` : 'none');
|
||
}
|
||
finish();
|
||
}
|
||
|
||
/* PERIHELION-V — deep-time (extended Table 2a/2b) accuracy gates.
|
||
Capture the "mine" positions with the extended range ON, then restore it OFF
|
||
synchronously and atomically: ES modules are singletons, so setExtendedRange
|
||
mutates the same ephem the live gates above (Mars@J2000 / Jupiter@1900, which
|
||
expect Table 1) share — the flag must never be ON across an await. */
|
||
const JD_JUP_1000BC = 1355818.5; // 1000 BC-01-01
|
||
const JD_MERC_2500 = lib.jdFromUnixMs(Date.UTC(2500, 0, 1));
|
||
// Scratch extended-Jupiter resolve — a faithful copy of ephem's Table 2a+2b
|
||
// math so the 2b M-correction can be ZEROED without editing ephem.js.
|
||
const T2A_JUP = { el: [5.20248019, 0.04853590, 1.29861416, 34.33479152, 14.27495244, 100.29282654],
|
||
rate: [-0.00002864, 0.00018026, -0.00322699, 3034.90371757, 0.18199196, 0.13024619] };
|
||
const B2_JUP = [-0.00012452, 0.06064060, -0.35635438, 38.35125000]; // [b, c, s, f]
|
||
function jupiterExtScratch(jd, with2b) {
|
||
const T = lib.centuriesSinceJ2000(jd);
|
||
const a = T2A_JUP.el[0] + T2A_JUP.rate[0] * T, e = T2A_JUP.el[1] + T2A_JUP.rate[1] * T;
|
||
const I = T2A_JUP.el[2] + T2A_JUP.rate[2] * T, L = T2A_JUP.el[3] + T2A_JUP.rate[3] * T;
|
||
const wb = T2A_JUP.el[4] + T2A_JUP.rate[4] * T, Om = T2A_JUP.el[5] + T2A_JUP.rate[5] * T;
|
||
let M = L - wb;
|
||
if (with2b) { const [b, c, s, f] = B2_JUP; M += b * T * T + c * Math.cos(f * T * lib.DEG) + s * Math.sin(f * T * lib.DEG); }
|
||
return ephem.eclFromElements(a, e, I, Om, wb - Om, M, {});
|
||
}
|
||
let jupMine, mercMine, jupZeroed;
|
||
{
|
||
ephem.setExtendedRange(true);
|
||
jupMine = ephem.helioEcl('jupiter', JD_JUP_1000BC, {});
|
||
mercMine = ephem.helioEcl('mercury', JD_MERC_2500, {});
|
||
ephem.setExtendedRange(false); // restore before any await
|
||
jupZeroed = jupiterExtScratch(JD_JUP_1000BC, false); // 2b correction removed
|
||
}
|
||
const dmax3 = (a, b) => Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y), Math.abs(a.z - b.z));
|
||
(async () => {
|
||
// Wait for the base live gates + lane-N/E gates (18 total) to finish — ThreadingHTTPServer
|
||
// does NOT serialize upstream calls, so we must never fetch Horizons in parallel.
|
||
for (let i = 0; i < 480 && n < 18; i++) await new Promise((r) => setTimeout(r, 250));
|
||
// Jupiter ground truth: barycenter '5' (DE441, long span). Planet-center '599'
|
||
// has NO Horizons ephemeris before A.D. 1600; the SSD approximate elements model
|
||
// the giant-planet SYSTEM barycenter anyway (Jupiter↔bary ≈ 5e-4 AU ≪ tol).
|
||
let jupH = null;
|
||
{
|
||
const label = 'Jupiter @1000 BC (extended 2a+2b) vs live Horizons — tol 0.15 AU';
|
||
const tr = pendingRow(label);
|
||
try {
|
||
jupH = await horizonsVec('5', JD_JUP_1000BC);
|
||
const w = dmax3(jupMine, jupH);
|
||
tr.remove();
|
||
row(label, w <= 0.15, `Δmax=${w.toFixed(4)} AU (tol 0.15) · bary '5' (${jupH.x.toFixed(4)}, ${jupH.y.toFixed(4)}, ${jupH.z.toFixed(4)}) · mine (${jupMine.x.toFixed(4)}, ${jupMine.y.toFixed(4)}, ${jupMine.z.toFixed(4)})`);
|
||
} catch (err) { tr.remove(); row(label, false, `Horizons error: ${err.message}`); }
|
||
finish();
|
||
}
|
||
{
|
||
const label = 'Mercury @2500-01-01 (extended 2a) vs live Horizons — tol 0.02 AU';
|
||
const tr = pendingRow(label);
|
||
try {
|
||
const mercH = await horizonsVec('199', JD_MERC_2500);
|
||
const w = dmax3(mercMine, mercH);
|
||
tr.remove();
|
||
row(label, w <= 0.02, `Δmax=${w.toExponential(2)} AU (tol 0.02) · Horizons (${mercH.x.toFixed(5)}, ${mercH.y.toFixed(5)}, ${mercH.z.toFixed(5)})`);
|
||
} catch (err) { tr.remove(); row(label, false, `Horizons error: ${err.message}`); }
|
||
finish();
|
||
}
|
||
{
|
||
// Prove Table 2b's M-correction is LIVE: zeroing it must worsen Jupiter@1000 BC
|
||
// (reuses the Jupiter Horizons vector fetched above — no extra request).
|
||
const label = 'Table 2b M-correction engages — zeroing it worsens Jupiter@1000 BC';
|
||
if (jupH) {
|
||
const dWith = dmax3(jupMine, jupH), dZero = dmax3(jupZeroed, jupH);
|
||
row(label, dZero > dWith, `with 2b Δ=${dWith.toFixed(4)} AU → zeroed Δ=${dZero.toFixed(4)} AU (worse by ${(dZero - dWith).toFixed(4)} AU)`);
|
||
} else {
|
||
row(label, false, 'skipped — Jupiter Horizons fetch failed above');
|
||
}
|
||
finish();
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|