240 lines
12 KiB
HTML
240 lines
12 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>
|
||
<script type="importmap">
|
||
{ "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js" } }
|
||
</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(`proxy/sbdb_lookup?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();
|
||
</script>
|
||
</body>
|
||
</html>
|