solargod/verify.html

240 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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.23.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>