🌑 neargod: real orbits for close approaches — CAD des → SBDB sbdb.api full-prec elements → shared Kepler core (ephem.smallBodyEcl); true marker + 25%-opacity ellipse per NEO, per-object fail-soft to the wave-1 schematic ring, honest 'N real · M schematic · CAD+SBDB' status; +sbdb_lookup upstream; +verify gate (opus)
This commit is contained in:
parent
e7bcf424d7
commit
a57fa196ac
@ -1,25 +1,48 @@
|
|||||||
// SOLARGOD near-Earth close-approach layer (Stage 5) — the CAD API's ±30-day list
|
// SOLARGOD near-Earth close-approach layer (Stage 5 · wave 2 NEARGOD) — the CAD
|
||||||
// of close approaches, as highlighted clickable reticles arranged around Earth
|
// API's ±30-day list of close approaches, each object now drawn at its TRUE
|
||||||
// (a "close-approach radar"), colored by miss distance and labelled in lunar
|
// propagated position with its REAL orbit. CAD gives designations, miss distance,
|
||||||
// distances. Off by default; fail-soft (brief §5). NOTE: CAD returns approach
|
// date, speed; SBDB's lookup endpoint (sbdb.api, full precision) gives the
|
||||||
// EVENTS, not orbits — markers are schematic positions around Earth, not tracked
|
// osculating elements, propagated by the SAME shared Kepler core as the belt
|
||||||
// trajectories; the data (designation, miss distance, date, speed) is real.
|
// (ephem.smallBodyEcl). Off by default; additive and fail-soft PER OBJECT — a
|
||||||
|
// lookup that won't resolve falls back to the wave-1 schematic ring for that one
|
||||||
|
// object, and the status line reports the split honestly (brief §5, Lane N).
|
||||||
|
|
||||||
|
// Parse an SBDB lookup response → {a,e,i,om,w,ma,epoch} (deg/AU/JD) or null.
|
||||||
|
// Elements arrive as an array of {name, value} STRINGS; epoch is orbit.epoch (JD,
|
||||||
|
// TDB). Reject non-finite or e≥0.98 (the wave-1 elliptical-only rule, brief §3).
|
||||||
|
// Exported so verify.html's gate exercises the identical parser.
|
||||||
|
export function parseSbdbElements(j) {
|
||||||
|
const o = j && j.orbit;
|
||||||
|
if (!o || !Array.isArray(o.elements)) return null;
|
||||||
|
const m = {};
|
||||||
|
for (const e of o.elements) m[e.name] = Number(e.value);
|
||||||
|
const el = { a: m.a, e: m.e, i: m.i, om: m.om, w: m.w, ma: m.ma, epoch: Number(o.epoch) };
|
||||||
|
for (const v of Object.values(el)) if (!Number.isFinite(v)) return null;
|
||||||
|
if (el.e >= 0.98 || !(el.a > 0)) return null;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
export default function create(ctx) {
|
export default function create(ctx) {
|
||||||
const { THREE, scene, CONFIG, lib, ui, ephem, toLocal, makeLabel } = ctx;
|
const { THREE, scene, CONFIG, lib, ui, scale, ephem, worldGroup, toLocal, makeLabel } = ctx;
|
||||||
const LD_AU = 384400 / lib.AU_KM; // one lunar distance in AU
|
const LD_AU = 384400 / lib.AU_KM; // one lunar distance in AU
|
||||||
|
const SBDB_LOOKUP = 'proxy/sbdb_lookup'; // sibling of CONFIG.proxy.* (I don't own config.js)
|
||||||
|
const N = 256; // orbit samples per NEO
|
||||||
|
|
||||||
const glyph = makeReticle();
|
const glyph = makeReticle();
|
||||||
const neos = [];
|
const neos = [];
|
||||||
let on = false;
|
let on = false;
|
||||||
const _abs = {};
|
const _e = {}, _abs = {}, _tmp = {};
|
||||||
|
|
||||||
ui.addLayer('neos', 'Close approaches', false, (v) => { on = v; for (const n of neos) { n.spr.visible = v; n.lbl.obj.visible = v; } });
|
ui.addLayer('neos', 'Close approaches', false, (v) => {
|
||||||
|
on = v;
|
||||||
|
for (const n of neos) { n.spr.visible = v; n.lbl.obj.visible = v; if (n.line) n.line.visible = v; }
|
||||||
|
});
|
||||||
ui.setStatus('neos', 'off', 'off');
|
ui.setStatus('neos', 'off', 'off');
|
||||||
load();
|
load();
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
ui.setStatus('neos', 'fetching CAD…', 'warn');
|
ui.setStatus('neos', 'fetching CAD…', 'warn');
|
||||||
|
let rows;
|
||||||
try {
|
try {
|
||||||
const now = new Date(ctx.clock.simMs);
|
const now = new Date(ctx.clock.simMs);
|
||||||
const fmt = (d) => `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`;
|
const fmt = (d) => `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`;
|
||||||
@ -27,16 +50,33 @@ export default function create(ctx) {
|
|||||||
const dmax = fmt(new Date(now.getTime() + 30 * 86400000));
|
const dmax = fmt(new Date(now.getTime() + 30 * 86400000));
|
||||||
const j = await (await fetch(`${CONFIG.proxy.cad}?date-min=${dmin}&date-max=${dmax}&dist-max=0.05&sort=dist&limit=40`)).json();
|
const j = await (await fetch(`${CONFIG.proxy.cad}?date-min=${dmin}&date-max=${dmax}&dist-max=0.05&sort=dist&limit=40`)).json();
|
||||||
const F = j.fields, ix = (k) => F.indexOf(k);
|
const F = j.fields, ix = (k) => F.indexOf(k);
|
||||||
const rows = j.data || [];
|
rows = (j.data || []).map((r) => ({
|
||||||
rows.forEach((r, i) => build({
|
|
||||||
des: r[ix('des')], jd: +r[ix('jd')], cd: r[ix('cd')], dist: +r[ix('dist')],
|
des: r[ix('des')], jd: +r[ix('jd')], cd: r[ix('cd')], dist: +r[ix('dist')],
|
||||||
v: +r[ix('v_rel')], h: +r[ix('h')],
|
v: +r[ix('v_rel')], h: +r[ix('h')],
|
||||||
}, i, rows.length));
|
}));
|
||||||
ui.setStatus('neos', neos.length ? `${neos.length} approaches ±30d · CAD · schematic ring` : 'none in window', on ? 'ok' : 'off');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[solargod] neos', err.message);
|
console.warn('[solargod] neos CAD', err.message);
|
||||||
ui.setStatus('neos', 'CAD unavailable', 'err');
|
ui.setStatus('neos', 'CAD unavailable', 'err');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build every marker up-front (schematic ring by default), then upgrade each to
|
||||||
|
// a true orbit as its SBDB lookup lands. SEQUENTIAL — sbdb.api burst-throttles,
|
||||||
|
// and the disk cache makes reloads instant (brief §12: don't hammer JPL).
|
||||||
|
rows.forEach((d, i) => build(d, i, rows.length));
|
||||||
|
let real = 0, sched = 0;
|
||||||
|
for (let i = 0; i < neos.length; i++) {
|
||||||
|
const n = neos[i];
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${SBDB_LOOKUP}?sstr=${encodeURIComponent(n.d.des)}&full-prec=true`);
|
||||||
|
const el = parseSbdbElements(await res.json());
|
||||||
|
if (el) { attachOrbit(n, el); real++; } else sched++;
|
||||||
|
} catch (err) { sched++; console.warn('[solargod] neos', n.d.des, err.message); }
|
||||||
|
ui.setStatus('neos', `fetching ${i + 1}/${neos.length}… ${real} real`, 'warn');
|
||||||
|
}
|
||||||
|
ui.setStatus('neos',
|
||||||
|
neos.length ? `${real} real orbits · ${sched} schematic · CAD+SBDB` : 'none in window',
|
||||||
|
on ? 'ok' : 'off');
|
||||||
}
|
}
|
||||||
|
|
||||||
function build(d, i, total) {
|
function build(d, i, total) {
|
||||||
@ -45,22 +85,56 @@ export default function create(ctx) {
|
|||||||
const spr = new THREE.Sprite(new THREE.SpriteMaterial({ map: glyph, color: new THREE.Color(col), transparent: true, depthWrite: false }));
|
const spr = new THREE.Sprite(new THREE.SpriteMaterial({ map: glyph, color: new THREE.Color(col), transparent: true, depthWrite: false }));
|
||||||
spr.visible = on; scene.add(spr);
|
spr.visible = on; scene.add(spr);
|
||||||
const lbl = makeLabel(spr, `${d.des} · ${ld.toFixed(1)} LD`, 'craft-label'); lbl.div.style.color = col; lbl.obj.visible = on;
|
const lbl = makeLabel(spr, `${d.des} · ${ld.toFixed(1)} LD`, 'craft-label'); lbl.div.style.color = col; lbl.obj.visible = on;
|
||||||
neos.push({ d, ld, col, spr, lbl, angle: (i / Math.max(1, total)) * Math.PI * 2 });
|
neos.push({ d, ld, col, spr, lbl, angle: (i / Math.max(1, total)) * Math.PI * 2, el: null, line: null, geo: null, auPath: null, lastFillP: NaN });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach the real elliptical orbit (rides worldGroup in absolute view-world, like
|
||||||
|
// the comets). Refilled from the frozen AU path whenever the MEGA/TRUE P changes.
|
||||||
|
function attachOrbit(n, el) {
|
||||||
|
n.el = el;
|
||||||
|
n.auPath = ephem.orbitPathFromElements(el, N);
|
||||||
|
n.geo = new THREE.BufferGeometry();
|
||||||
|
n.geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array((N + 1) * 3), 3));
|
||||||
|
n.line = new THREE.LineLoop(n.geo, new THREE.LineBasicMaterial({ color: new THREE.Color(n.col), transparent: true, opacity: 0.25 }));
|
||||||
|
n.line.frustumCulled = false; n.line.visible = on;
|
||||||
|
worldGroup.add(n.line);
|
||||||
|
fillOrbit(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillOrbit(n) {
|
||||||
|
const pos = n.geo.attributes.position.array;
|
||||||
|
for (let k = 0; k <= N; k++) {
|
||||||
|
_tmp.x = n.auPath[k * 3]; _tmp.y = n.auPath[k * 3 + 1]; _tmp.z = n.auPath[k * 3 + 2];
|
||||||
|
scale.viewFromEcl(_tmp, _tmp);
|
||||||
|
pos[k * 3] = _tmp.x; pos[k * 3 + 1] = _tmp.y; pos[k * 3 + 2] = _tmp.z;
|
||||||
|
}
|
||||||
|
n.geo.attributes.position.needsUpdate = true;
|
||||||
|
n.geo.computeBoundingSphere();
|
||||||
|
n.lastFillP = scale.getP();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: 'neos',
|
id: 'neos',
|
||||||
onClockTick() {
|
onClockTick(simMs, jd) {
|
||||||
if (!on) return;
|
if (!on) return;
|
||||||
const earth = ctx.bodyWorld.earth;
|
|
||||||
if (!earth) return;
|
|
||||||
const camDist = ctx.camera.position.length();
|
const camDist = ctx.camera.position.length();
|
||||||
|
const P = scale.getP();
|
||||||
|
const earth = ctx.bodyWorld.earth;
|
||||||
const ringR = camDist * 0.06;
|
const ringR = camDist * 0.06;
|
||||||
for (const n of neos) {
|
for (const n of neos) {
|
||||||
_abs.x = earth.x; _abs.y = earth.y; _abs.z = earth.z;
|
if (n.el) {
|
||||||
toLocal(_abs, n.spr.position);
|
// true propagated position (same core as the belt)
|
||||||
n.spr.position.x += Math.cos(n.angle) * ringR;
|
ephem.smallBodyEcl(n.el, jd, _e);
|
||||||
n.spr.position.y += Math.sin(n.angle) * ringR * 0.5;
|
scale.viewFromEcl(_e, _abs);
|
||||||
|
toLocal(_abs, n.spr.position);
|
||||||
|
if (P !== n.lastFillP) fillOrbit(n); // track the MEGA↔TRUE tween
|
||||||
|
} else if (earth) {
|
||||||
|
// schematic ring fallback for THIS object alone (wave-1 placement)
|
||||||
|
_abs.x = earth.x; _abs.y = earth.y; _abs.z = earth.z;
|
||||||
|
toLocal(_abs, n.spr.position);
|
||||||
|
n.spr.position.x += Math.cos(n.angle) * ringR;
|
||||||
|
n.spr.position.y += Math.sin(n.angle) * ringR * 0.5;
|
||||||
|
}
|
||||||
n.spr.scale.setScalar(Math.max(0.02, camDist * 0.012));
|
n.spr.scale.setScalar(Math.max(0.02, camDist * 0.012));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
1
serve.py
1
serve.py
@ -36,6 +36,7 @@ CACHE_TTL_SEC = 24 * 3600
|
|||||||
UPSTREAMS = {
|
UPSTREAMS = {
|
||||||
"horizons": "https://ssd.jpl.nasa.gov/api/horizons.api",
|
"horizons": "https://ssd.jpl.nasa.gov/api/horizons.api",
|
||||||
"sbdb": "https://ssd-api.jpl.nasa.gov/sbdb_query.api",
|
"sbdb": "https://ssd-api.jpl.nasa.gov/sbdb_query.api",
|
||||||
|
"sbdb_lookup": "https://ssd-api.jpl.nasa.gov/sbdb.api",
|
||||||
"cad": "https://ssd-api.jpl.nasa.gov/cad.api",
|
"cad": "https://ssd-api.jpl.nasa.gov/cad.api",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
38
verify.html
38
verify.html
@ -196,6 +196,44 @@
|
|||||||
await craftCheck('Voyager 1 (-31) @ 2026-07-15', '-31', 2461236.5, { x: -32.07, y: -136.21, z: 98.55 }, 0.05);
|
await craftCheck('Voyager 1 (-31) @ 2026-07-15', '-31', 2461236.5, { x: -32.07, y: -136.21, z: 98.55 }, 0.05);
|
||||||
await jwstCheck();
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user