Validation record

How we know the numbers are right.

Astrology software rarely shows its work. This page is the work. Every figure below comes from a specific run in our validation corpus, and every figure is followed by the file it came from.

First, what we compared against.

The reference for almost every number on this page is the Swiss Ephemeris — the calculation library that a large share of astrology software has been built on for decades. We froze its output for a fixed corpus of charts, then ran our own independent engine, built on JPL's planetary ephemeris kernels, against that frozen record.

What that does and doesn't prove

Agreement with Swiss Ephemeris is a compatibility result: it means charts you have cast for years will not move under you. It is not a claim of absolute astronomical accuracy, and we don't make one. Where two well-built implementations differ by thousandths of an arcsecond, neither is meaningfully "wrong" for chart work.

One arcsecond is 13600 of a degree. The residuals below are mostly thousandths of that — hundreds of thousands of times smaller than the thickness of a line on a printed wheel.

We also ran the same corpus through two further independent stacks — the US Naval Observatory's NOVAS library and the Skyfield astronomy library — so that the result does not depend on one implementation of our own.

The result an astrologer actually cares about.

Sub-arcsecond residuals are reassuring but abstract. The question that matters is simpler: would any chart read differently?

  • 0 sign changes. 0 retrograde flips. 0 house reassignments.

    Across 3,083 charts spanning 1900–2100, no planet ever landed in a different zodiac sign, no planet's retrograde or direct state ever differed, no planet was ever assigned to a different house, and no aspect was ever classified differently from the reference.

    That is 2,060 charts from the development corpus plus 1,023 from a separate holdout corpus that was frozen with a published random seed before the engine was finished.

    Source: research/full_engine_acceptance.md lines 37–40; experiments/full_engine_acceptance/full_categories.json (planet_house_or_aspect_mismatch_count: 0 over 2,060 and 1,023 rows)

  • 0.0068 arcseconds

    The largest single planetary-longitude disagreement with Swiss Ephemeris anywhere in the 1,023-chart holdout corpus — about one five-hundred-thousandth of a degree, across all ten bodies from the Sun to Pluto.

    The honest footnote: the development corpus contains one worse case — a single Uranus row in December 1900, at 0.289152″. It is a known and documented difference between how DE405 and Swiss Ephemeris treat Uranus's centre versus its system barycentre. It is isolated, explained, and we would rather print it than hide it.

    Source: research/full_engine_acceptance.md line 40 (fresh holdout row: largest body longitude max 0.006753″); RELEASE_STATUS.md line 113

  • 0.0047 arcseconds

    The 99th-percentile disagreement on Placidus house cusps and the angles — the Ascendant and Midheaven included — across the development corpus. The worst single cusp in the whole run was 0.021535″.

    Source: research/full_engine_acceptance.md line 39 (development: largest house/angle p99 0.004734″, max 0.021535″); RELEASE_STATUS.md lines 115–116

All seven house systems, validated.

The six systems beyond Placidus were validated together against the same frozen reference, over a 2,328-row corpus that deliberately includes polar and boundary cases. Placidus is the default and is measured separately, on the acceptance corpus above.

House system Rows compared Cusp error, p99 Cusp error, max Cusp sign flips
Whole Sign2,3280.000000″0.000000″0
Equal2,3280.005351″0.021535″0
Koch2,0700.004752″0.035478″0
Porphyry2,3280.004237″0.021535″0
Regiomontanus2,3280.004523″0.037290″0
Campanus2,3280.004443″0.021535″0

Circular cusp error against the frozen Swiss Ephemeris oracle, in arcseconds. Source: research/house_systems_production.md lines 40–46.

  • 0 cusp-sign flips in 164,520 cusps

    Not one house cusp, in any of the six systems, across the entire corpus, fell on a different side of a sign boundary than the reference put it. Koch shows fewer rows because Koch has fewer rows — see below.

    Source: RELEASE_STATUS.md line 121 (added-system cusp-sign flips: 0 across 164,520 successful cusps); research/house_systems_production.md line 48

  • 253 of 253, and 258 of 258

    Placidus and Koch houses genuinely have no solution inside the polar circle. The interesting test is not whether an engine computes something there — it is whether it computes the same nothing.

    On every one of the 253 charts where the reference refused to produce Placidus houses, our engine refused too. On all 258 Koch refusals, likewise. No false successes and no false failures in either direction.

    When that happens, Ephemeris Lab names the reason on the page and suggests a house system that is defined at that latitude. It never silently substitutes a different system and labels it with the one you asked for.

    Source: research/house_failure_parity.md lines 50–52 and 60–63; research/house_systems_production.md line 81 (“No system silently falls back to another system.”); src/natal_engine_lab/houses.py lines 204–207; src/natal_engine_lab/placidus.py lines 169–171

We tested where it actually matters.

A random sample of charts mostly tests the easy middle. The moments that change a reading are the boundaries — the instant a planet changes sign, turns retrograde, or reaches exact aspect. So we built a corpus that sits on top of them.

  • 69,159 boundary probes, 0 unexplained differences

    Probes placed one minute either side of, and exactly on, sign ingresses, retrograde stations, aspect-orb edges and cusp crossings between 1900 and 2100. At a sign boundary the median position difference from the reference was 0.000520″ and the worst 0.006336″. The aspect-separation family runs wider — its worst case is 0.081131″, on the Saturn–Uranus pair.

    Scope, so the count isn't read as more than it is: the ingress and station scans cover the Sun through Pluto and deliberately leave out the Moon, whose ingresses are numerous enough on their own to swamp the corpus — the Moon has its own precision study instead. The aspect family probes seven planet pairs across the five major aspects rather than every possible pairing.

    What "unexplained" means, precisely: right at a boundary, a difference of a few thousandths of an arcsecond is enough to put a planet on the other side of the line for a fraction of a second, and that happened in 11,284 of the probes. In every single one, the difference was fully accounted for by the measured position residual. None were unaccounted for. We say "zero unexplained differences" rather than "zero differences" because the second one would not be true.

    Source: research/de405_targeted_final.md lines 36–41 and 52–57; experiments/targeted_oracle/fixtures/manifest.json (69,159 inputs; families: sign 29,910, station 10,980, aspect 16,539, cusp 11,730)

  • 292,196 time-scale samples, agreeing to 3 × 10⁻¹⁴ seconds

    Before you can place a planet you have to know what time it is, in the astronomical sense. Our clean-room implementation of the ΔT time-scale model reproduces the reference curve to within three hundred-trillionths of a second across six-hourly samples from 1900 to 2100 — that is exact, to the limits of floating-point arithmetic.

    Source: research/delta_t.md lines 40 and 83; experiments/delta_t/results/comparison.json (max_abs_seconds 2.842e-14 on the holdout split, n = 146,100 of 292,196 total)

What happens when you get it wrong.

The most useful thing we measured was not how right we are. It was how wrong the ordinary shortcuts are. We deliberately broke our own pipeline, one assumption at a time, to find out what each correction is worth.

Skip the time-scale correction

Feed civil time straight to a planetary ephemeris without converting it and the Moon moves by up to 58.6 arcseconds — enough to matter at a sign boundary.

Source: research/direct_jpl_full_corpus.md line 49; experiments/direct_jpl_erfa/results/summary.json (de440s_zero_delta_t_moon_longitude_p95_max_arcsec: 51.24, 58.60)

Use the wrong reference frame

Compute against the fixed J2000 ecliptic instead of the ecliptic of the date and the Moon shifts by up to 5,089 arcseconds — about 1.4 degrees, well over a sign boundary's worth.

Source: research/moon_precision.md line 47 (DE430, apparent, J2000 frame: 4,746″ p95 / 5,089″ max)

Skip the apparent-place reduction

Use raw geometric positions instead of apparent ones — no light time, no aberration — and the Moon moves by up to 24.9 arcseconds.

Source: research/moon_precision.md line 46 (DE430, geometric, true-of-date: 14.59″ p95 / 24.87″ max)

None of these were bugs we found in the shipping engine. They are induced failures, run on purpose, to put a number on each correction. It is also why "which ephemeris do you use?" is a less useful question than it sounds: the corrections applied on top of it move charts further than the choice of ephemeris does.

Two more independent stacks, one answer.

  • 20,600 placements, p99 within 0.0047 arcseconds

    The same corpus run through the US Naval Observatory's own NOVAS library with JPL's DE405 ephemeris, compared to the same Swiss Ephemeris reference. An entirely separate implementation of the astronomy lands in the same place we do.

    Source: research/novas.md line 180 (global tails over all 20,600 places: longitude p50 0.000511″, p95 0.002248″, p99 0.004700″); NOVAS Python 3.1.1.6 with novas-de405 1997.1

  • 0 sign flips, 0 retrograde flips

    And again through Skyfield, reading the official unmodified DE405 file downloaded from JPL — 65,445,888 bytes, SHA-256 7ef3d151…918236 — over the same 20,600 placements.

    Source: research/de405_bsp_skyfield.md lines 91 and 112 (longitude residual p99 0.006078″; “There were zero sign flips and zero retrograde-category flips for every body.”)

  • Within 1 arcsecond of JPL Horizons

    One test compares against JPL Horizons itself, end to end — kernel evaluation, light-time iteration, stellar aberration and the ecliptic rotation, all the way to an apparent geocentric longitude.

    Its limits, stated plainly: it covers one body (Chiron) at 27 sampled instants, and the one-arcsecond bound is deliberately loose because Horizons models gravitational light deflection and our engine documents light time and aberration only. This is the only Horizons comparison we run, and we are not going to describe the rest of the corpus as "validated against JPL Horizons" on the strength of it.

    Source: tests/test_spk_type21.py line 251 (test_apparent_longitudes_reproduce_horizons_end_to_end), APPARENT_TOLERANCE_DEG = 1/3600 at line 57; oracle fixture tests/fixtures/chiron_horizons_oracle.json, 27 samples, body 2060 Chiron, JPL orbit solution #171

Engineering discipline behind the numbers.

  • Byte-identical, not merely equal

    Results are pinned by hashing their canonical JSON form — RFC 8785 canonicalisation, then SHA-256. Not "the numbers look the same": the same bytes. That is what makes a chart you cast today reproducible years from now.

    When the transit search engine was rewritten for speed and came out 3.59× faster, every single response was verified byte-identical to the engine before the rewrite — every field of every event, not just the timestamp.

    Source: desktop/contracts/search/validate_python.py lines 237–238 and desktop/contracts/search/regenerate_fixture_digests.py lines 4–7 (RFC 8785 JCS canonicalisation plus SHA-256); tests/test_contract_e2e.py lines 291–299

  • Exact equality, never “close enough”

    The fast batched calculation path is tested against the plain scalar one with == on the float, never an approximate comparison. The reasoning is in the test file itself: an engine that is merely close to itself is two engines, and the user meets the difference as a wheel that changes as they scrub past it.

    That rail caught four real rounding bugs during optimisation. A tolerance of one part in a billion would have passed all four.

    Source: tests/test_jpl_batch_equivalence.py lines 1–11 and 62–64; tests/test_frame_batch_equivalence.py lines 1–11; docs/desktop/LANE_PERF_STATUS_2026-08-20.md lines 512–531

  • A named kernel, checksummed, or nothing

    The planetary ephemeris is JPL/NAIF DE405, identified by SHA-256 30a71137…2ac75c89 and a byte count of 10,898,432 — not by its filename. The digest is pinned as a schema constant, so a build cannot name a different kernel and still validate, and it is re-verified against the file on disk at runtime.

    That kernel covers roughly 1950 to 2050. A request outside its coverage is reported as unavailable. It does not quietly fall through to a different kernel, an analytical approximation, or a network service.

    Source: docs/desktop/ADR-007-kernel-provisioning.md lines 20–23 and 48–52; src/natal_engine_lab/data_manifest.json line 46; desktop/contracts/resources/c1-resource-manifest-v1.schema.json line 30 (sha256 pinned as a schema constant); src/natal_engine_lab/kernel.py lines 74–84

  • 2,180 engine tests, 496 + 180 Rust, 208 browser tests

    The Python engine suite collects to 2,180 executed tests from 1,154 test functions across 147 files. The desktop application adds roughly 500 Rust tests and 208 browser tests driving a real React runtime.

    The Rust figure is reported as "496 + 180" because it is two test binaries and the second re-runs the repository layer; adding them to 676 would be double-counting.

    Source: docs/desktop/LANE_R7C_STATUS_2026-08-20.md line 451 (Python suite: 2180 passed / 0 failed / 59 skipped, with DE405 and the Chiron SPK staged); docs/desktop/LANE_QUEUE_2026-08-19.md line 337 (union cargo 496+180); 500 static #[test] in desktop/src-tauri/src less the 4 cfg-gated off on Linux = 496; static count of desktop/e2e/*.spec.ts = 208 Playwright tests in 37 files

  • 0.35–0.50 ms for a full wheel

    Ten bodies, twelve cusps and four angles, recomputed in well under a millisecond per instant when scrubbing — which is why the wheel can move under your finger in real time. A single one-off chart dispatch measures 12.26 ms.

    A full year of transit search went from 6 minutes 40 seconds to 1 minute 51 seconds. Measured single-threaded on our build machine, not on a fast laptop.

    Source: docs/desktop/LANE_PERF_STATUS_2026-08-20.md lines 606–621 (ten bodies, twelve cusps, four angles, batched; all seven house systems land at 0.35–0.50 ms/instant amortised) and lines 447, 535 (single dispatch 12.26 ms/instant); lines 12–20 (transit search: 12 months 399,688 ms → 111,184 ms)

What we don't claim.

A validation page is only worth reading if it also says where the evidence stops.

  • We do not claim absolute astronomical accuracy. Every agreement figure here is agreement with another implementation, on a defined corpus, over a defined date range.
  • We do not claim the whole engine is validated against JPL Horizons. One test is, for one body, at 27 instants. The rest is against Swiss Ephemeris.
  • We do not publish accuracy figures for the asteroids. Ceres, Pallas, Juno and Vesta are calculated, but we have not completed a differential run for them, so there is no number to give you.
  • These are development figures with a date on them. The acceptance run quoted above was recorded on 2026-08-10 against engine version 0.4.0. The engine has moved since. We will re-run the full acceptance corpus before release and publish the figures from that run.
  • The corpus is not public. The application is proprietary and the validation corpus lives with it. If you are evaluating Ephemeris Lab professionally and want to see a specific report, ask and we will show you.