solargod/verify.html
monsterrobotparty c4b572b8bb 🪐 kepler: planets on rails — Standish ephemeris engine, orbit paths, verify gates (opus)
Stage 1. ephem.js: JPL SSD approximate elements (Table 1 + 2a/2b plumbing),
Newton Kepler solver, reusable eclFromElements core (planets + small bodies),
orbitPath sampler. planets.js: 8 planets + Pluto as textured exaggerated spheres
on MEGA-compressed orbits with labels, rebuild-on-drift. verify.html: 9 gates all
green IN BROWSER incl. live Horizons via proxy — Mars @2026-07-15 Δ3.3e-4 AU,
Jupiter Δ2.9e-3, Earth Δ3.9e-5; Mars @J2000 vs live Δ4.8e-5; Jupiter @1900 Δ2.0e-3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 00:04:41 +10:00

146 lines
6.9 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);
}
// ---- 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';
}
finish();
liveCheck('Mars @ J2000.0 vs live Horizons', 'mars', '499', 2451545.0, 0.01);
liveCheck('Jupiter @ 1900-01-01 vs live Horizons', 'jupiter', '599', 2415020.5, 0.02);
</script>
</body>
</html>