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>
146 lines
6.9 KiB
HTML
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>
|