# Precision Report

This document inventories everything `cosmos-validation` computes and
the precision at which each piece has been validated against the two
independent reference implementations available to us: JPL Horizons
(NASA/JPL ephemeris service, DE441) and Swiss Ephemeris (Astrodienst,
IAU 2006/2000A + DE441-derived `.se1` files).

The validation harness is reproducible: every fixture committed to
`fixtures/` was generated by one of the `scripts/fetch_swiss_*.py` or
the `precision-report fetch` CLI, and every number in the tables below
comes from running one of the comparison binaries under `src/bin/`.

> Last regenerated: 2026-05-12, against Swiss Ephemeris 2.10.03,
> JPL Horizons API 1.2, and `de440.bsp` + `sb441-n16.bsp` from NAIF.

---

## Table of contents

1. [Architecture overview](#architecture-overview)
2. [Module inventory](#module-inventory)
3. [Validation harness](#validation-harness)
4. [Precision table — SPK geometry](#precision-table--spk-geometry)
5. [Precision table — IAU correction stack](#precision-table--iau-correction-stack)
6. [Precision table — astrological pipeline](#precision-table--astrological-pipeline)
7. [Precision table — time scales](#precision-table--time-scales)
8. [Precision table — eclipses](#precision-table--eclipses)
9. [Known limitations](#known-limitations)
10. [Suggested next work](#suggested-next-work)

---

## Architecture overview

```
┌───────────────────────────────────────────────────────────────────┐
│  Reference sources (external, fetched once)                        │
│    • JPL Horizons API  (VECTOR and OBSERVER modes)                 │
│    • Swiss Ephemeris   (libswe via pyswisseph + .se1 + sefstars)   │
└───────────────────────────────────────────────────────────────────┘
                            ↓ fetched into JSON
┌───────────────────────────────────────────────────────────────────┐
│  Fixtures (versioned, ~17 directories under fixtures/)             │
└───────────────────────────────────────────────────────────────────┘
                            ↓ compared against
┌───────────────────────────────────────────────────────────────────┐
│  Oracle pipeline                                                   │
│    • SPK backend (DE440 + sb441-n16, via cosmos-ephemeris)      │
│    • VSOP2013 / ELP-MPP02 backend (analytic, embedded)             │
│    • Apparent stack: LT + LD + S + NPB(IAU 2006/2000A)             │
│    • Topocentric: WGS-84 → ITRS → TET via R3(GAST)                 │
│    • Astrological layer: 8 ayanamshas + 7 house systems            │
│    • Special points: lunar nodes, Lilith, fixed stars              │
│    • Events: rise/set/transit, eclipses (lunar / solar / local)    │
└───────────────────────────────────────────────────────────────────┘
                            ↓ runs through
┌───────────────────────────────────────────────────────────────────┐
│  11 CLI binaries that print diff tables (see src/bin/)             │
│  + the regression integration test that gates CI                   │
└───────────────────────────────────────────────────────────────────┘
```

---

## Module inventory

| Module | Public surface | What it does |
|---|---|---|
| `oracle` | `Oracle::new(Backend)`, `state`, `corrected_state` | Backend-agnostic state lookup; wraps SPK kernel and VSOP/ELP, applies LT + S + LD + NPB corrections |
| `fixture` | `FixtureSet`, `Fixture`, `Frame`, `Corrections`, `Tolerance`, `BackendKind` | JSON schema for ground-truth fixtures + backend / correction tagging |
| `horizons` *(feature `fetch`)* | `HorizonsFetcher::{fetch, fetch_observer_astrometric, fetch_observer_apparent}` | Blocking client for JPL Horizons VECTOR + OBSERVER modes |
| `delta_t` | `delta_t_seconds(jd)` | IERS-observed ΔT table (1968–2030, sub-second), Espenak/Stephenson fall-back |
| `sidereal` | `Ayanamsha` enum (8 modes), `ayanamsha`, `sidereal_longitude`, `tet_equatorial_to_ecliptic_of_date`, `mean_obliquity_iau2006`, `true_obliquity_iau2006a`, `ecliptic_lon_lat` | Tropical → ecliptic-of-date + sidereal modes |
| `houses` | `ascendant`, `midheaven`, `whole_sign_houses`, `equal_houses`, `placidus_houses`, `koch_houses`, `regiomontanus_houses`, `campanus_houses`, `porphyry_houses` | 7 house systems, Asc/MC |
| `lunar` | `mean_lunar_node`, `mean_lunar_node_no_nutation`, `mean_lunar_node_descending`, `mean_lunar_perigee`, `mean_lilith`, `true_lunar_node_geocentric`, `true_lilith_geocentric`, `true_lunar_perigee_geocentric` | Mean (analytic) and true (osculating) lunar nodes + Lilith |
| `fixed_stars` | `CATALOG` (26 stars), `by_name`, `apparent_ecliptic_of_date` | Hipparcos catalogue + apparent-position pipeline (proper motion + parallax + LD + S + NPB) |
| `asteroids` | `KNOWN_ASTEROIDS`, `naif_id_by_name`, `apparent_ecliptic_of_date` | Multi-kernel (asteroid + planet) apparent pipeline |
| `topocentric` | `Observer`, `observer_position_tet_km`, `apparent_topocentric_state`, `apparent_topocentric_with_kernel`, `alt_az_from_topocentric`, `apparent_alt_az` | WGS-84 observer + topocentric Cartesian + alt/az (modern N=0°/E=90°) |
| `rise_set` | `Event`, `HorizonTarget`, `find_next_event`, `next_rise`, `next_set`, `next_transit` | Coarse-scan + bisect root-finder on apparent altitude |
| `eclipses` | `LunarEclipseKind`, `LunarEclipseSnapshot`, `lunar_eclipse_at`, `next_lunar_eclipse`, `SolarEclipseKind`, `SolarEclipseSnapshot`, `solar_eclipse_at`, `next_solar_eclipse`, `LocalSolarEclipseSnapshot`, `local_solar_eclipse_at`, `next_local_solar_eclipse` | Lunar, global-solar, and local-solar eclipse detectors + finders |
| `report` | `ErrorReport`, `ReportTable` | Per-fixture diff aggregation (pos/vel/angular) |

---

## Validation harness

### Eleven inspection CLIs

| Binary | Compares |
|---|---|
| `precision-report report \| bootstrap \| fetch` | Geometric SPK + VSOP fixtures vs Horizons |
| `sidereal-check` | Tropical + Lahiri sidereal + ayanamsha vs Swiss |
| `houses-check` | Asc/MC + 7 house systems vs Swiss `swe.houses` |
| `topocentric-check` | Moon + Sun topocentric ecliptic vs Swiss `SEFLG_TOPOCTR` |
| `altaz-check` | alt/az vs Swiss `swe.azalt` |
| `risetrans-check` | rise / set / transit vs Swiss `swe.rise_trans` |
| `lunar-check` | mean+true node + Lilith vs Swiss `SE_MEAN_NODE`, `SE_TRUE_NODE`, `SE_MEAN_APOG`, `SE_OSCU_APOG` |
| `stars-check` | 26 fixed stars vs Swiss `swe.fixstar2` |
| `asteroids-check` | Ceres/Pallas/Juno/Vesta vs Swiss `swe.calc(SE_CERES…)` |
| `eclipses-check` | Lunar + global-solar eclipses vs Swiss `swe.lun_eclipse_when` / `sol_eclipse_when_glob` |
| `local-eclipses-check` | Per-observer solar eclipses vs Swiss `swe.sol_eclipse_when_loc` |

### Ten Python fixture generators

`scripts/fetch_swiss_*.py` — each script is idempotent; rerunning regenerates fixtures from the live Swiss / Horizons sources. They live alongside the Rust CLIs so anyone can reproduce the diff tables below.

### Regression test

`tests/regression.rs` walks every fixture in `fixtures/regression-*/` whose name matches the loaded kernel family and asserts the tolerance per fixture. Falls back silently when no kernel is available.

---

## Precision table — SPK geometry

These are pure geometric state vectors at the requested instant in the kernel's native frame (ICRF), no corrections applied.

| Body / Coverage | Precision vs Horizons (`VEC_CORR='NONE'`) | Source |
|---|---|---|
| 9 planetary barycenters wrt SSB (DE440) | **< 1 µm position, < 1 nm/s velocity** (IEEE-754 noise) | `regression-de440/horizons.json` |
| Earth wrt EMB (DE440) | 22 nm – 25 µm | same fixture |
| Moon wrt EMB (DE440) | 0.66 µm – 2 mm (cross-version DE440↔DE441 drift in lunar fit) | same fixture |
| 4 main-belt asteroids (Ceres, Pallas, Juno, Vesta) wrt Sun | sub-mas | `asteroids-check` |
| VSOP2013 inner planets (Mercury–Mars) heliocentric | 22–160 km (truncated series) | `regression-vsop2013/horizons.json` |
| VSOP2013 outer planets (Jupiter–Neptune) heliocentric | 440 km – 8600 km (Neptune worst) | same fixture |
| ELP/MPP02 Moon geocentric | 0.06 – 0.36 km | same fixture |

---

## Precision table — IAU correction stack

These add light-time, light deflection by the Sun, stellar aberration, and the IAU 2006/2000A bias-precession-nutation matrix on top of the geometric SPK state.

| Stage | Precision | Reference |
|---|---|---|
| + Light-time (LT) only | 1 µm – 0.65 mm position | `regression-de440-astrometric/horizons.json` |
| + Stellar aberration (LT+S) | 0.06 mm – 9 mm position, **sub-mas angular** | `regression-de440-apparent-vector/horizons.json` |
| + Light deflection + NPB IAU 2006/2000A → TET | ~50 mas vs Horizons OBSERVER apparent | `regression-de440-observer-apparent/horizons.json` |

The ~50 mas residual vs Horizons OBSERVER is the well-documented difference between Horizons' internal **IAU 76/80/94** reduction and the modern **IAU 2006/2000A** reduction we implement. Independently confirmed by Swiss Ephemeris (also IAU 2006/2000A) which shows the same ~50 mas offset against Horizons.

Cross-validated against Swiss Ephemeris (`regression-de440-swiss-apparent/swiss.json`):

| Body class | Local pipeline vs Swiss IAU 2006/2000A |
|---|---|
| Gas giants (Jupiter, Saturn, Uranus, Neptune) | **sub-millarcsec** |
| Sun | sub-mas |
| Inner planets (Mercury, Venus, Mars) | 1 – 40 mas (formulation-level differences) |
| Moon | 7 – 12 mas |

---

## Precision table — astrological pipeline

| Pipeline | Precision vs Swiss |
|---|---|
| Tropical ecliptic longitude (TET → ecl-of-date via true obliquity) | **< 0.4″** |
| Lahiri ayanamsha (IAU 2006 pA + Δψ, anchored to Swiss J2000) | 0″ at anchor; **±8″ across ±100 years** |
| 7 other ayanamshas (Fagan-Bradley, DeLuce, Raman, Ushashashi, Krishnamurti, Djwhal Khul, Yukteshwar) | same envelope; anchors match Swiss to 10 decimals |
| Sidereal longitude | tropical residual ⊕ ayanamsha residual |
| Ascendant | **sub-arcsec** across 4 canonical charts |
| Midheaven | **sub-arcsec** |
| Whole-Sign cusps | **exact** (sub-microarcsec, IEEE-754) |
| Equal cusps | sub-arcsec (inherits Asc) |
| Placidus cusps (port of Swiss `swehouse.c`) | **sub-millarcsec** (peak 0.001″) |
| Koch / Regiomontanus / Campanus / Porphyry cusps | **sub-millarcsec** (peak 0.001″) |
| Mean lunar node (IAU 2000A polynomial + Δψ) | **±0.21″** |
| True (osculating) lunar node (from SPK Moon state) | **sub-millarcsec** (-0.01″ to +0.003″) |
| Mean Lilith (Brown perigee + 180° projected onto ecliptic + Δψ) | **±0.55″** |
| True (osculating) Lilith (eccentricity vector of Moon's osculating orbit) | sub-arcsec (-0.20″ to +0.20″) |
| 26 named fixed stars (apparent ecliptic-of-date with parallax) | **< 0.04″ longitude**, most sub-millarcsec |

---

## Precision table — observational pipeline

| Quantity | Precision vs Swiss |
|---|---|
| Topocentric Moon + Sun Cartesian (WGS-84 observer) | parallax shift (~1° on Moon) matches to **< 0.5″** |
| Topocentric alt / az (modern N=0°/E=90°) | **sub-arcsec on both axes** vs `swe.azalt` |
| Rise / set times for Sun + Moon | ±100–200 s (horizon-convention difference: we use −34′ refraction, Swiss uses −50′ for Sun radius and parallax-aware Moon) |
| Transit times | ±5–200 s |

---

## Precision table — time scales

| Conversion | Precision |
|---|---|
| ΔT (TT − UT1) via IERS table | **sub-second** across 1968–2030 |
| TT ↔ TDB (Fairhead-Bretagnon via cosmos-time) | microseconds |
| TT ↔ UT1 (via ΔT) | sub-second in covered range |

---

## Precision table — eclipses

| Event | Precision vs Swiss |
|---|---|
| Lunar eclipses (10 consecutive 2024-2030) | **type exact 10/10** (penumbral / partial / total); time of maximum ±30–44 s |
| Global solar eclipses (10 consecutive 2024-2030) | **type exact 10/10** (total / annular / partial); **time ±0–4 s** |
| Local solar eclipses (Madrid, NYC, Sydney, Tokyo × next 4) | time **sub-second to ±100 s** when fully visible; magnitude matches Swiss to ±0.001 |

The lunar ±30–44 s residual is now isolated to the parabolic γ-min refinement (Swiss uses Brent-style). The global-solar near-zero match was unlocked by light-time-correcting both the Sun and the Moon in the shadow geometry. Local-solar can miss eclipses where the Sun sets / rises during the partial phase (Sydney edge cases).

---

## Known limitations

1. **SPK Type 21 not supported.** The on-disk reader handles Type 2 (Chebyshev) only. This is enough for DE440/DE441 (planets) and `sb441-n16` (main-belt-16 asteroids), but JPL Horizons distributes per-body SPKs for centaurs / TNOs (Chiron, Pholus, Eris, Sedna, etc.) as Type 21 (Modified Divided Differences). Adding Type 21 is a ~300-line self-contained patch in `cosmos-ephemeris/src/jpl/spk.rs`; their NAIF IDs are already enumerated in `KNOWN_ASTEROIDS`.

2. **Lunar eclipse timing ±30–44 s.** Algorithmic, not physical. The parabolic γ-min refinement converges with limited precision; replacing it with golden-section or Brent's method (~50 lines) closes the gap to sub-second.

3. **Local solar eclipses miss edge-of-visibility cases.** Eclipses where the Sun sets / rises during the partial phase get skipped because the finder converges to the global maximum and only accepts if the Sun is above the horizon there. Fixable with a "max-while-Sun-above-horizon" windowed search.

4. **Rise/set times ±100–200 s.** Horizon convention hardcoded to −34′ refraction. Swiss uses per-body conventions (−50′ Sun, parallax-aware Moon). ~30-line fix.

5. **Polar motion omitted in topocentric.** Adds ~0.2″ in the topocentric path. Pull `xp`, `yp` from `eternal_coords::eop` to close.

6. **VSOP2013 truncation.** Outer planets up to 8600 km on Neptune. The embedded series is "ablated" relative to the full VSOP2013 set — for sub-arcsec work always prefer the SPK backend.

7. **Hybrid (annular-total) solar eclipses** fold into Total in the v1 classifier. Needs a track-along-Earth scan to detect the annular↔total transition.

---

## Suggested next work

### Tier 1 — high impact, low–medium effort

1. **SPK Type 21 reader** (~300 lines in `cosmos-ephemeris`). Unlocks Chiron, Pholus, Eris, Sedna, Quaoar, Makemake, Haumea, Salacia and every other centaur / TNO. Hot path for advanced astrology, planetary science, and outer solar system observation.
2. **Brent's method in eclipse-max search** (~50 lines). Closes the lunar-eclipse ±30 s residual to sub-second.
3. **Per-body horizon convention in rise/set** (~30 lines). Sun radius and Moon parallax-aware horizon → sub-second rise/set match against Swiss.
4. **Multi-kernel `Oracle`** (~100-line refactor). Move from `Backend::Spk { kernel_path }` to `Backend::Spk { kernel_paths: Vec<PathBuf> }` so a single Oracle covers DE440 + `sb441-n16` + per-body kernels uniformly.
5. **Atmospheric refraction in alt/az** (~50 lines). Bennett's formula for apparent altitude near the horizon. Required for accurate rise/set timing.

### Tier 2 — new features

6. **Eclipse contact times** (1st, 2nd, 3rd, 4th contact). Not just the maximum. Standard for eclipse prediction; bisection on umbra/penumbra edge.
7. **More sidereal modes**: SVP-based (Galactic Center–anchored), Krishnamurti New, etc. Trivial extension of the `Ayanamsha` enum.
8. **More fixed stars**. Catalogue load (Hipparcos: ~118 000, Tycho-2: ~2.5 M) with on-demand parsing. The pipeline already handles per-star data; we just need a parser + lazy index.
9. **Hybrid solar eclipse detection**. Track the centerline across Earth to detect annular↔total transition.
10. **Topocentric houses**. Pólich/Aldrich convention. The Whole-Sign / Equal / Placidus path stays the same; only Asc / MC use topocentric latitude.

### Tier 3 — observational / scientific

11. **Planetary phenomena**: stations (retrograde turnarounds), oppositions, conjunctions, perihelion / aphelion, greatest elongations. Root-finding on velocity / elongation.
12. **Saros cycle tracking**: identify each eclipse with its Saros number + family.
13. **Equation of time** (mean vs apparent solar time).
14. **Polar motion in topocentric** (sub-mas tightening). EOP loader already exists in `cosmos-coords`.
15. **Atmospheric refraction model** including pressure / temperature dependence (the Saemundsson-Bennett formula).

### Tier 4 — performance + ergonomics

16. **Cache NPB matrices** by (jd_tt rounded to a second). The apparent pipeline recomputes NPB on every call → expected ~10× speed-up.
17. **SPK segment index by (body, center)**. Replace the linear scan in `SpkFile::find_segment` with a hashmap → O(1) lookup.
18. **Binary fixture format** (CBOR / postcard). 16 KB JSON → ~3 KB binary. Lower CI fetch time.
19. **`criterion` benchmark harness** for hot paths. Detect performance regressions.

### Tier 5 — distribution

20. **Publish-ready crate split**. Move `houses`, `eclipses`, `fixed_stars`, `sidereal` into independently publishable sub-crates so downstream users can take only what they need.
21. **WASM compatibility audit**. Most of the code is already `no_std`-friendly via `libm`; ensure `feature = "wasm"` builds cleanly for browser distribution.
22. **`cargo doc` polish**. Audit doc-comments, add module-level examples.
