Skip to content

Latest commit

 

History

History
520 lines (404 loc) · 22.3 KB

File metadata and controls

520 lines (404 loc) · 22.3 KB

Benchmark: Go vs Python Celestial Data Generator

Benchmarks run on 2026-02-22, comparing the Go port against the original Python (Skyfield) implementation.

Environment

  • Machine: macOS (Darwin 25.3.0)
  • Input data: 200-year time series centered on 2026-01-19
  • Python: Anaconda Python with Skyfield 1.53, NumPy (vectorized, multi-core)
  • Go: Compiled binary using goeph library, single-threaded

Benchmark 1: Small Scale (44k rows)

Small-scale test with ±10 day window around reference date.

Workload

  • Past: 11 days (2026-01-08 to 2026-01-19) — 15,841 rows
  • Future: 20 days (2026-01-09 to 2026-01-29) — 28,801 rows
  • Total: 31 days, 44,642 rows, 42 columns each

Timing

Metric Go Python Ratio
Wall clock 1.47s 15.66s 10.7x faster
User CPU 1.41s 53.94s 38x less
System CPU 0.19s 51.54s 271x less
Total CPU 1.60s 105.48s 66x less
CPU utilization 109% 673%

Raw time output

Go:

./generate_data_go  1.41s user 0.19s system 109% cpu 1.465 total

Python:

/opt/anaconda3/bin/python main.py  53.94s user 51.54s system 673% cpu 15.659 total

Numerical Accuracy (all 42 columns, 44,642 rows)

Component Max Error Notes
Planets (Sun, Moon, Mercury–Pluto) < 1.78e-7° Moon lon worst; outer planets < 3e-10°
Galactic Center < 1.7e-13° Effectively exact
Lunar Nodes < 2.5e-11° Effectively exact
Ground Locations (all 6) < 0.018° IAU 2000A nutation + ΔT model
Satellites up to 180° SGP4 implementation differences

Detailed column-level accuracy (Benchmark 1, 44,642 rows):

Column Max Error Mean Error Median Error
sun_lat_deg 3.85e-13 9.20e-14 9.90e-14
sun_lon_deg 8.27e-09 3.85e-09 3.63e-09
moon_lat_deg 1.25e-08 3.31e-09 3.14e-09
moon_lon_deg 1.78e-07 4.95e-08 3.67e-08
mercury_lat_deg 2.72e-10 1.70e-10 1.85e-10
mercury_lon_deg 1.44e-08 6.23e-09 5.77e-09
venus_lat_deg 1.53e-10 1.02e-10 1.05e-10
venus_lon_deg 1.03e-08 4.75e-09 4.48e-09
mars_lat_deg 3.10e-11 1.81e-11 1.80e-11
mars_lon_deg 6.40e-09 2.93e-09 2.76e-09
jupiter_lat_deg 1.54e-11 7.96e-12 7.73e-12
jupiter_lon_deg 9.75e-10 4.93e-10 4.76e-10
saturn_lat_deg 1.86e-11 1.03e-11 1.01e-11
saturn_lon_deg 7.76e-10 3.10e-10 2.80e-10
uranus_lat_deg 2.57e-12 1.15e-12 1.07e-12
uranus_lon_deg 7.24e-11 5.52e-11 5.77e-11
neptune_lat_deg 3.79e-12 2.07e-12 2.02e-12
neptune_lon_deg 2.16e-10 8.05e-11 7.11e-11
pluto_lat_deg 1.25e-11 4.40e-12 3.84e-12
pluto_lon_deg 2.60e-10 1.20e-10 1.13e-10
gc_lat_deg 2.40e-14 2.04e-14 2.22e-14
gc_lon_deg 1.71e-13 1.11e-13 1.14e-13
north_node_lon_deg 2.47e-11 8.79e-12 5.68e-14
south_node_lon_deg 2.48e-11 8.79e-12 1.14e-13
loc_ni_lat_deg 1.91e-03 8.22e-04 6.97e-04
loc_ni_lon_deg 1.01e-02 4.58e-03 3.63e-03
loc_chicago_lat_deg 4.98e-03 2.35e-03 2.37e-03
loc_chicago_lon_deg 1.02e-02 6.51e-03 7.35e-03
loc_london_lat_deg 5.42e-03 2.76e-03 2.86e-03
loc_london_lon_deg 1.79e-02 7.99e-03 8.75e-03
loc_cushing_lat_deg 4.65e-03 2.06e-03 2.04e-03
loc_cushing_lon_deg 9.92e-03 5.95e-03 6.48e-03
loc_ny_lat_deg 4.92e-03 2.29e-03 2.30e-03
loc_ny_lon_deg 1.02e-02 6.39e-03 7.19e-03
loc_mumbai_lat_deg 3.46e-03 1.19e-03 1.01e-03
loc_mumbai_lon_deg 9.66e-03 5.01e-03 4.39e-03

Benchmark 2: Full Scale (~10.5M rows)

Full 18-year historical range (2008–2026), ±365 day future window.

Workload

  • Past: 18 years (2008-01-02 to 2026-01-19) — 9,492,481 rows
  • Future: 730 days (2025-01-19 to 2027-01-19) — 1,051,201 rows
  • Total: ~18.5 years of data, ~10.5M rows, 42 columns each

Timing

Metric Go Python Ratio
Wall clock 3m 15s 38m 37s 11.9x faster
User CPU 174.24s 8,033.18s 46x less
System CPU 32.02s 6,129.17s 191x less
Total CPU 206.26s 14,162.35s 69x less
CPU utilization 105% 611%

Raw time output

Go:

./generate_data_go  174.24s user 32.02s system 105% cpu 3:15.04 total

Python:

/opt/anaconda3/bin/python main.py  8033.18s user 6129.17s system 611% cpu 38:37.11 total

Throughput

Metric Go Python
Rows/second (wall clock) ~54,000 ~4,500
Rows/CPU-second ~51,000 ~740

Numerical Accuracy (estimated, same code as Benchmarks 1 and 3–5)

Benchmark 2 covers near-present data (2008–2027), where ΔT and leap second corrections are small. Expected accuracy matches Benchmark 1:

Component Max Error Notes
Planets (Sun, Moon, Mercury–Pluto) < 1.8e-7° Same as Benchmark 1
Galactic Center < 1.7e-13° Effectively exact
Lunar Nodes < 2.5e-11° Effectively exact
Ground Locations (all 6) < 0.018° Near-present ΔT error is small
Satellites varies SGP4 divergence grows with time from TLE epoch

Benchmark 3: 200-Year Hourly (1.75M rows)

200-year range at 1-hour intervals. This benchmark includes all 6 location columns.

Workload

  • Range: 200 years (1926-02-13 to 2125-12-26) at 1-hour intervals
  • Total: 1,752,001 rows, 42 columns each

Timing

Metric Go Python Ratio
Wall clock 27.4s 6m 31s 14.3x faster
User CPU 26.10s 1,107.77s 42x less
System CPU 2.59s 1,282.68s 495x less
Total CPU 28.69s 2,390.45s 83x less
CPU utilization 104% 611%

Raw time output

Go:

./generate_data_go  26.10s user 2.59s system 104% cpu 27.365 total

Python:

python main.py  1107.77s user 1282.68s system 611% cpu 6:30.94 total

Throughput

Metric Go Python
Rows/second (wall clock) ~63,900 ~4,500
Rows/CPU-second ~61,100 ~733

Numerical Accuracy (all 42 columns, 1,752,001 rows)

Component Max Error Notes
Planets (Sun, Moon, Mercury–Pluto) < 3.97e-7° Moon lon worst; outer planets < 2e-9°
Galactic Center < 2.8e-13° Effectively exact
Lunar Nodes < 2.5e-11° Effectively exact
Ground Locations (all 6, incl. London & Cushing) < 0.032° IAU 2000A nutation + ΔT model
Satellites up to 180° Expected: SGP4 meaningless 100yr from TLE epoch

Detailed column-level accuracy (Benchmark 3, 1,752,001 rows):

Column Max Error Mean Error Median Error
sun_lat_deg 6.16e-12 1.13e-12 6.90e-13
sun_lon_deg 1.96e-08 1.20e-08 1.34e-08
moon_lat_deg 2.80e-08 9.57e-09 8.50e-09
moon_lon_deg 3.97e-07 1.63e-07 1.72e-07
mercury_lat_deg 6.84e-09 1.57e-09 1.29e-09
mercury_lon_deg 4.09e-08 1.49e-08 1.33e-08
venus_lat_deg 4.59e-09 6.90e-10 4.44e-10
venus_lon_deg 2.49e-08 1.27e-08 1.30e-08
mars_lon_deg 1.54e-08 6.91e-09 6.83e-09
jupiter_lon_deg 4.72e-09 1.63e-09 1.39e-09
saturn_lon_deg 2.45e-09 8.43e-10 7.22e-10
uranus_lon_deg 1.25e-09 4.09e-10 3.56e-10
neptune_lon_deg 7.37e-10 2.65e-10 2.32e-10
pluto_lon_deg 7.83e-10 2.14e-10 1.88e-10
gc_lat_deg 3.38e-14 1.74e-14 1.87e-14
gc_lon_deg 2.84e-13 1.80e-13 1.71e-13
north_node_lon_deg 2.51e-11 5.58e-12 0.00e+00
loc_ni_lon_deg 1.67e-02 6.74e-03 6.02e-03
loc_chicago_lon_deg 2.35e-02 7.50e-03 6.58e-03
loc_london_lon_deg 3.15e-02 8.49e-03 7.41e-03
loc_cushing_lon_deg 2.12e-02 7.20e-03 6.35e-03
loc_ny_lon_deg 2.30e-02 7.43e-03 6.53e-03
loc_mumbai_lon_deg 1.79e-02 6.83e-03 6.09e-03

Location summary: Max error dropped from 0.49° to 0.032° across the full 200-year range. Mean error is 0.005°. The residual comes from yearly ΔT interpolation vs Skyfield's daily IERS data.


Benchmark 4: 248-Year Weekly (13k rows)

248-year range at 1-week (168h) intervals, constrained by DE440s ephemeris limits (1849–2150).

Workload

  • Past: 11 days (2026-01-08 to 2026-01-19) — 2 rows (weekly granularity)
  • Future: 248 years (1902-02-19 to 2149-12-17) — 12,932 rows
  • Total: 248 years of weekly data, 12,934 rows, 42 columns each

Timing

Metric Go Python Ratio
Wall clock 1.50s 7.06s 4.7x faster
User CPU 1.00s 24.61s 25x less
System CPU 0.13s 14.01s 108x less
Total CPU 1.13s 38.62s 34x less
CPU utilization 75% 547%

Raw time output

Go:

./generate_data_go  1.00s user 0.13s system 75% cpu 1.504 total

Python:

python main.py  24.61s user 14.01s system 547% cpu 7.055 total

Numerical Accuracy (all 42 columns, 12,932 rows)

Component Max Error Notes
Planets (Sun, Moon, Mercury–Pluto) < 3.92e-7° Consistent with Benchmark 3
Galactic Center < 2.8e-13° Effectively exact
Lunar Nodes < 4.8e-13° Effectively exact
Ground Locations (all 6, incl. London & Cushing) < 0.031° IAU 2000A nutation + ΔT model
Satellites up to 180° Expected: SGP4 meaningless ±124yr from TLE epoch

Detailed column-level accuracy (Benchmark 4, 12,932 rows):

Column Max Error Mean Error Median Error
sun_lat_deg 6.96e-12 1.36e-12 8.43e-13
sun_lon_deg 1.94e-08 1.20e-08 1.34e-08
moon_lat_deg 2.72e-08 9.56e-09 8.43e-09
moon_lon_deg 3.92e-07 1.63e-07 1.72e-07
mercury_lat_deg 6.70e-09 1.56e-09 1.28e-09
mercury_lon_deg 4.02e-08 1.49e-08 1.33e-08
venus_lat_deg 4.45e-09 6.90e-10 4.45e-10
venus_lon_deg 2.46e-08 1.27e-08 1.30e-08
mars_lon_deg 1.53e-08 6.91e-09 6.84e-09
jupiter_lon_deg 4.68e-09 1.62e-09 1.39e-09
saturn_lon_deg 2.43e-09 8.47e-10 7.26e-10
uranus_lon_deg 1.24e-09 4.10e-10 3.57e-10
neptune_lon_deg 7.29e-10 2.59e-10 2.26e-10
pluto_lon_deg 7.75e-10 2.01e-10 1.73e-10
gc_lat_deg 3.38e-14 1.75e-14 1.87e-14
gc_lon_deg 2.84e-13 1.80e-13 1.71e-13
north_node_lon_deg 4.83e-13 5.81e-14 0.00e+00
loc_ni_lat_deg 2.14e-04 7.41e-05 7.45e-05
loc_ni_lon_deg 1.67e-02 7.31e-03 5.94e-03
loc_chicago_lat_deg 5.14e-03 3.32e-03 3.27e-03
loc_chicago_lon_deg 1.35e-02 6.22e-03 6.25e-03
loc_london_lat_deg 2.96e-03 1.54e-03 1.56e-03
loc_london_lon_deg 3.14e-02 1.00e-02 9.79e-03
loc_cushing_lat_deg 4.88e-03 2.95e-03 2.92e-03
loc_cushing_lon_deg 1.26e-02 6.16e-03 6.46e-03
loc_ny_lat_deg 5.01e-03 3.12e-03 3.01e-03
loc_ny_lon_deg 1.58e-02 6.38e-03 6.10e-03
loc_mumbai_lat_deg 3.69e-03 1.72e-03 1.60e-03
loc_mumbai_lon_deg 1.37e-02 6.21e-03 6.27e-03

Note: The reduced speedup (4.7x vs ~14x) at this small scale reflects Python's startup and initialization overhead becoming a larger fraction of total time. With only ~13k rows, both implementations finish quickly and the per-row advantage of Go is partially masked by fixed costs (ephemeris loading, file I/O).


Benchmark 5: 200-Year Daily (73k rows)

200-year range at 1-day intervals.

Workload

  • Past: 11 days (2026-01-08 to 2026-01-19) — 12 rows (daily granularity)
  • Future: 200 years (1926-02-13 to 2125-12-26) — 73,001 rows
  • Total: 200 years of daily data, 73,013 rows, 42 columns each

Timing

Metric Go Python Ratio
Wall clock 2.52s 21.0s 8.3x faster
User CPU 1.89s 64.69s 34x less
System CPU 0.24s 59.82s 249x less
Total CPU 2.13s 124.51s 58x less
CPU utilization 84% 592%

Raw time output

Go:

./generate_data_go  1.89s user 0.24s system 84% cpu 2.523 total

Python:

python main.py  64.69s user 59.82s system 592% cpu 21.022 total

Throughput

Metric Go Python
Rows/second (wall clock) ~28,900 ~3,500
Rows/CPU-second ~34,300 ~590

Numerical Accuracy (all 42 columns, 73,001 rows)

Component Max Error Notes
Planets (Sun, Moon, Mercury–Pluto) < 3.95e-7° Consistent with Benchmarks 3–4
Galactic Center < 2.8e-13° Effectively exact
Lunar Nodes < 4.8e-13° Effectively exact
Ground Locations (all 6) < 0.031° IAU 2000A nutation + ΔT model
Satellites up to 180° Expected: SGP4 meaningless ±100yr from TLE epoch

Detailed column-level accuracy (Benchmark 5, 73,001 rows):

Column Max Error Mean Error Median Error
sun_lat_deg 6.12e-12 1.13e-12 6.90e-13
sun_lon_deg 1.94e-08 1.20e-08 1.34e-08
moon_lat_deg 2.75e-08 9.57e-09 8.46e-09
moon_lon_deg 3.95e-07 1.62e-07 1.72e-07
mercury_lat_deg 6.79e-09 1.57e-09 1.29e-09
mercury_lon_deg 4.06e-08 1.49e-08 1.34e-08
venus_lat_deg 4.54e-09 6.90e-10 4.45e-10
venus_lon_deg 2.47e-08 1.27e-08 1.30e-08
mars_lat_deg 1.58e-09 1.84e-10 1.16e-10
mars_lon_deg 1.53e-08 6.91e-09 6.83e-09
jupiter_lat_deg 6.71e-11 2.37e-11 2.07e-11
jupiter_lon_deg 4.69e-09 1.63e-09 1.39e-09
saturn_lat_deg 7.22e-11 2.31e-11 1.93e-11
saturn_lon_deg 2.43e-09 8.43e-10 7.23e-10
uranus_lat_deg 1.25e-11 3.46e-12 2.73e-12
uranus_lon_deg 1.24e-09 4.09e-10 3.56e-10
neptune_lat_deg 1.68e-11 4.99e-12 4.05e-12
neptune_lon_deg 7.31e-10 2.65e-10 2.32e-10
pluto_lat_deg 1.47e-10 3.73e-11 3.07e-11
pluto_lon_deg 7.76e-10 2.14e-10 1.88e-10
gc_lat_deg 3.38e-14 1.75e-14 1.87e-14
gc_lon_deg 2.84e-13 1.80e-13 1.71e-13
north_node_lon_deg 4.83e-13 5.12e-14 0.00e+00
south_node_lon_deg 4.83e-13 5.13e-14 0.00e+00
loc_ni_lat_deg 2.15e-04 7.39e-05 7.37e-05
loc_ni_lon_deg 1.67e-02 7.44e-03 6.22e-03
loc_chicago_lat_deg 5.14e-03 3.32e-03 3.27e-03
loc_chicago_lon_deg 1.36e-02 6.30e-03 6.36e-03
loc_london_lat_deg 2.96e-03 1.54e-03 1.56e-03
loc_london_lon_deg 3.15e-02 1.02e-02 1.00e-02
loc_cushing_lat_deg 4.88e-03 2.95e-03 2.92e-03
loc_cushing_lon_deg 1.27e-02 6.22e-03 6.54e-03
loc_ny_lat_deg 5.01e-03 3.12e-03 3.01e-03
loc_ny_lon_deg 1.59e-02 6.49e-03 6.26e-03
loc_mumbai_lat_deg 3.69e-03 1.72e-03 1.60e-03
loc_mumbai_lon_deg 1.37e-02 6.31e-03 6.41e-03

Location summary: Max location error 0.031° (London longitude), mean 0.005°. Consistent with Benchmarks 3–4 — the ΔT residual is stable regardless of sampling interval.


Nutation + ΔT Time Scale Fix (applied to Benchmarks 3–5)

Two fixes were applied to the Go implementation (reflected in Benchmark 3–5 numbers above):

  1. IAU 2000A nutation (30 terms default; full 678 luni-solar + 687 planetary terms available via NutationFull) — adds GAST (true sidereal time) and the nutation rotation matrix N^T to the geodetic→ICRF pipeline.
  2. ΔT time scale model — proper UTC → TT (via leap second table) → UT1 (via Skyfield-extracted ΔT table with linear interpolation) conversion, replacing the previous UT1 ≈ UTC approximation.

Root Cause Analysis

The 0.77° location error from Benchmark 4 was not from missing nutation (which contributes only ~0.002°) but from a time scale mismatch:

  • Go treated UT1 ≈ UTC — the Julian Date was passed directly as UT1
  • Skyfield converts UTC → TT (via leap seconds + 32.184s) → UT1 (via ΔT model)

The difference grows with time distance from the present:

Date ΔT (TT-UT1) UT1-UTC Resulting rotation error
1902 0.8s 41.4s ~0.17°
2000 63.8s 0.4s ~0.002°
2026 69.1s 0.1s ~0.0004°
2100 94.9s -25.7s ~0.11°
2149 144.7s -75.5s ~0.32°

ΔT Model Implementation

The Go implementation now uses:

  • Leap second table: 28 entries (1972-01-01 to 2017-01-01) from IERS Bulletin C
  • ΔT table: 401 entries at yearly intervals (1800–2200), extracted from Skyfield (IERS data + Morrison-Stephenson-Hohenkerk 2020 model)
  • Linear interpolation between yearly ΔT entries, flat extrapolation beyond table bounds

Performance Impact

No measurable overhead from the ΔT table lookup or nutation computation. Benchmark 3 actually got faster (28.1s vs 32.2s previously), likely due to reduced system CPU from other improvements.

Before/After Accuracy Comparison

Benchmark 3 (200yr hourly, 1,752,001 rows):

Component Before (no ΔT) After (with ΔT) Improvement
Planets < 4.81e-3° < 3.97e-7° ~12,000x
Galactic Center < 2.8e-13° < 2.8e-13° Same (exact)
Lunar Nodes < 1.65e-5° < 2.5e-11° ~660,000x
Ground Locations < 0.49° < 0.032° 15x
Satellites up to 180° up to 180° N/A

Benchmark 4 (248yr weekly, 12,932 rows):

Component Before (no ΔT) After (with ΔT) Improvement
Planets < 4.81e-3° < 3.92e-7° ~12,000x
Galactic Center < 2.8e-13° < 2.8e-13° Same (exact)
Lunar Nodes < 1.65e-5° < 4.8e-13° ~34,000,000x
Ground Locations < 0.77° < 0.031° 25x
Satellites up to 180° up to 180° N/A

Location error breakdown by site (Benchmark 4, longitude — the dominant component):

Location Max lon error (before) Max lon error (after)
Null Island (0°N, 0°E) ~0.77° 0.017°
Chicago (41.9°N) ~0.63° 0.014°
London (51.5°N) ~0.75° 0.031°
Cushing (36.0°N) ~0.60° 0.013°
NY (40.7°N) ~0.65° 0.016°
Mumbai (19.1°N) ~0.63° 0.014°

The residual 0.031° (at London, the highest-latitude location) comes from yearly ΔT interpolation vs Skyfield's daily IERS data. Mean location error is 0.005°.


Scaling Comparison

Scale Rows Go Python Speedup
Small 44,642 1.5s 15.7s 10.7x
Full ~10,500,000 3m 15s 38m 37s 11.9x
200yr hourly 1,752,001 27.4s 6m 31s 14.3x
200yr daily 73,013 2.5s 21.0s 8.3x
248yr weekly 12,934 1.5s 7.1s 4.7x

The speedup is ~11–14x at scale (>44k rows) and narrows to ~4.7–8.3x for smaller datasets where fixed costs dominate.

Nutation mode note: These benchmarks use the default NutationStandard (30 terms). Skyfield defaults to the full IAU 2000A series (678 luni-solar + 687 planetary terms) but also provides an iau2000b() variant (77 luni-solar, 0 planetary) for faster computation. With goeph's NutationFull (matching Skyfield's default precision), end-to-end workloads that include altaz/geodetic computations are ~5-7x slower, reducing the speedup to ~2-3x vs Skyfield. Position-only workloads (Observe, Apparent, coordinate conversions) are unaffected by the nutation setting and retain the full ~14x speedup. The ~1 arcsec difference between the two modes is negligible for most applications — see Nutation precision.


Analysis

  • ~12x wall-clock speedup across all benchmarks — Go consistently finishes in a fraction of Python's time.
  • ~70x less total CPU time — Python uses ~600% CPU (spreading work across ~6 cores via NumPy), yet Go on a single core (~105%) finishes 12x faster wall-clock.
  • System CPU: Python consumes disproportionate system time — reflecting heavy overhead from NumPy's memory management, multiprocessing coordination, and Skyfield's internal data handling.

Why Go is Faster

  1. No interpreter overhead — Go compiles to native machine code; Python has per-operation interpreter dispatch.
  2. In-memory ephemeris — The Go SPK reader loads all Chebyshev coefficients into memory once at startup. Skyfield also caches, but through more abstraction layers.
  3. Minimal allocation — Go uses fixed-size arrays ([3]float64) for positions; Python/NumPy creates intermediate array objects.
  4. No multi-core coordination overhead — Go runs single-threaded with no synchronization cost. Python's multi-core approach (via NumPy) adds coordination overhead that exceeds the parallelism benefit for this workload.
  5. Direct binary I/O — Go reads the DAF file with encoding/binary; Python goes through multiple library layers (Skyfield → jplephem → NumPy → file I/O).

Numerical Accuracy

The speedup comes with no meaningful loss in accuracy. Core position computations (SPK Chebyshev evaluation, apparent positions) are identical in both Go and Python — the same math, same polynomials, same coefficients. Nutation mode does not affect these computations.

Component Max Error (Bench 1–2, near present) Max Error (Bench 3–5, 200–248yr)
Planets (Sun, Moon, Mercury–Pluto) < 1.78e-7° < 3.97e-7°
Galactic Center < 1.7e-13° (exact) < 2.8e-13° (exact)
Lunar Nodes < 2.5e-11° (exact) < 2.5e-11° (exact)
Ground Locations < 0.018° < 0.032°
Satellites varies up to 180°

Note: Full row-by-row verification was performed across all overlapping timestamps with zero timestamp mismatches. The Go implementation uses IAU 2000A nutation (30 terms default, full 1365 terms available via NutationFull) and proper ΔT time scale conversion (leap second table + Skyfield-extracted ΔT table). The core math (Chebyshev evaluation, coordinate transforms, precession, nutation) matches Skyfield exactly. The remaining location error (0.018° near-present, 0.032° over 200+ years) is from yearly ΔT interpolation vs Skyfield's daily IERS data.