Heliocentric ecliptic J2000, AU · M☉ · days.
All state vectors live in a heliocentric ecliptic reference frame at J2000, with
position in astronomical units, mass in solar masses, and time in days since
J2000.0 (Julian Date 2451545.0). This makes the gravitational constant
clean: G = 4π² in these units, exactly. Every orbital integration
and every render call uses these conventions — no mid-pipeline conversions,
no silent unit drift.
The frame is rotated to the J2000 ecliptic so we never need to chain equator-to-ecliptic transforms during integration. The penalty is a single one-time rotation when ingesting JPL Horizons vectors (which arrive in ICRF) — a 23.4° obliquity rotation about the x-axis, applied once at cache load.
Newton-Raphson, four iterations, ε < 10⁻¹².
For two-body propagation we solve Kepler's equation numerically. Given mean anomaly
M and eccentricity e, we want eccentric anomaly E
such that:
Newton-Raphson with seed E₀ = M + e · sin(M) converges in three to
four iterations for nearly every NEO in the catalog. The update step is:
export function solveKepler(M: number, e: number): number { // Seed approximation — accurate to ~e² for moderate eccentricity let E = M + e * Math.sin(M); const tol = 1e-12; const maxIter = 20; for (let i = 0; i < maxIter; i++) { const f = E - e * Math.sin(E) - M; const fp = 1 - e * Math.cos(E); const dE = f / fp; E -= dE; if (Math.abs(dE) < tol) return E; } throw new Error(`Kepler failed to converge: e=${e}, M=${M}`); }
RK4 with adaptive timestep based on local truncation error.
Pure Kepler propagation ignores everything except the Sun. To get JPL-grade accuracy near Jupiter flybys, we run a 4th-order Runge-Kutta integrator over the full Newtonian N-body acceleration:
Timestep Δt is chosen per-step from local truncation error: we run
one RK4 step at Δt, two at Δt/2, compare position
residuals, and accept if below tolerance — else halve and retry. In practice
Δt stays at 1.0 d in interplanetary space and contracts
toward 10⁻³ d within a planet's Hill sphere.
Six perturbers — four dynamic, two for visual completeness.
We include the Sun and five perturbing bodies in every integration. Earth, Jupiter, and Saturn cover > 99.6% of cumulative Δv for 50-year propagation of typical NEO orbits. Uranus and Neptune are now included for visual completeness and honest solar-system representation; their combined gravitational Δv contribution to typical NEO trajectories is < 0.1% over 50 years. Mercury, Venus, and Mars contribute below the 0.4% threshold and are omitted.
M☉
M☉
M☉
M☉
M☉
M☉
3σ covariance projected forward via Monte Carlo.
The uncertainty cone visible in the Radar is not decorative — it is the 3σ
envelope of position deviation, sampled by Monte-Carlo propagation of the
JPL Horizons covariance matrix. We draw N = 256 particles from a
multivariate Gaussian over the six orbital elements, integrate each forward
through the same RK4 engine, and at every output epoch fit a position
ellipsoid to the cloud.
The cone widens dramatically near planetary close approaches — this is the gravitational keyhole effect, where small initial position errors get amplified by the close encounter. NEO Radar shows this honestly: far from a flyby the cone is invisible, near one it blooms.
NASA NeoWs → JPL Horizons → local cache → renderer.
Discovery and classification metadata come from NASA's Near-Earth Object Web Service (NeoWs); precision ephemeris and covariance come from JPL Horizons. Both are pulled and pinned to a local SQLite cache, keyed by SPK-ID, with a content hash so we can detect upstream re-solutions and invalidate.
NeoWs /* classification, threat metadata */ └─► CatalogEntry { spkId, name, designation, classification, riskLevel } │ ▼ Horizons /* state vectors + covariance, K224/56 solution */ └─► EphemerisRecord { r[3], v[3], cov[6×6], epoch, arc, condition } │ ▼ LocalCache /* SQLite, content-hashed, 14h TTL */ └─► get(spkId) ─► EphemerisRecord /* O(1) on hit */ │ ▼ PhysicsEngine /* pure, no I/O, no rendering */ └─► propagate(epoch, dt, perturbers) ─► State[] │ ▼ Renderer /* canvas, never touches physics */
Physics and rendering live in different worlds.
The single most important architectural decision in NEO Radar is the strict separation of the physics engine from the rendering layer. The physics engine has zero dependencies on the DOM, on canvas, on any rendering primitive. The renderer has zero dependencies on integrators, solvers, or covariance math. They communicate exclusively through plain state arrays — frozen, immutable, re-emitted every frame.
What NEO Radar does not yet model.
Honesty about scope is the only way to earn trust about accuracy. These effects are not modeled in v3.0:
-
Outgassing
Long-period comet activity — volatile sublimation imparts
a non-gravitational acceleration of order
10⁻⁸ AU/d²on comets but is negligible for the asteroidal NEOs in the current catalog. - GR Relativistic corrections — solar Schwarzschild precession contributes ~43"/century to Mercury but is below the position-uncertainty floor for any current NEO target over the 50-year window.
- Yarkovsky Radiation-pressure thermal drag — the dominant non-gravitational force on small NEOs (e.g. Bennu, ~5×10⁻¹⁴ AU/d²). Currently approximated from JPL's per-object A2 coefficient at cache load; full thermal model is roadmap.
- Lunar Earth–Moon barycenter modeled as point mass at EMB. Close approaches inside the lunar distance see ~1500 km position error from this approximation; acceptable for visualization, not for impact prediction. Full Earth–Moon system (two-body sub-integration) is planned for v3.