157 lines
5.1 KiB
Python
157 lines
5.1 KiB
Python
"""Cross-check the ApsisRS IAU 2000/2006 Earth orientation against ERFA.
|
|
|
|
Reads the quantities written by ``cargo run -p apsis-rs --features physics
|
|
--example dump_iau2000`` and recomputes each one with ERFA, which is the
|
|
reference implementation distributed by the International Astronomical Union
|
|
through the SOFA library and its ERFA derivative.
|
|
|
|
The script needs no network access.
|
|
|
|
Usage:
|
|
|
|
pixi run python validation/python/time/check_iau2000.py [path]
|
|
|
|
Exit status is zero when every quantity agrees within tolerance and one
|
|
otherwise.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import erfa
|
|
|
|
# Tolerances.
|
|
#
|
|
# The nutation, obliquity, and precession are reproduced at the level of the
|
|
# last bits of a double, so they are compared at 1e-14.
|
|
#
|
|
# The sidereal time and the matrices accumulate the double precision error of
|
|
# several rotations, so they are allowed more.
|
|
#
|
|
# Greenwich apparent sidereal time gets its own tolerance because ERFA's
|
|
# `gst06a` is built on the non-rotating origin while the Rust value is built
|
|
# on the equinox. The two formulations agree to about two microarcseconds,
|
|
# which is 1e-11 radians, and that is what is allowed here.
|
|
SCALAR_TOLERANCE = 1e-14
|
|
SIDEREAL_TOLERANCE = 1e-10
|
|
MATRIX_TOLERANCE = 1e-9
|
|
|
|
|
|
def tolerance_for(quantity: str) -> float:
|
|
"""Returns the tolerance that applies to a quantity."""
|
|
if quantity == "gast":
|
|
return SIDEREAL_TOLERANCE
|
|
if quantity in ("bpn", "c2t"):
|
|
return MATRIX_TOLERANCE
|
|
return SCALAR_TOLERANCE
|
|
|
|
DEFAULT_INPUT = Path("validation/data/expected/rust_iau2000.csv")
|
|
|
|
# The Terrestrial Time offset from UTC, in seconds, matching the Rust side.
|
|
TT_MINUS_UTC_SECONDS = 32.184 + 37.0
|
|
|
|
|
|
def read_rows(path: Path) -> list[dict[str, str]]:
|
|
"""Reads the CSV rows."""
|
|
with path.open(newline="") as handle:
|
|
reader = csv.DictReader(handle)
|
|
return [row for row in reader if row.get("mjd")]
|
|
|
|
|
|
def tt_julian_date(utc_mjd: float) -> float:
|
|
"""Returns the Terrestrial Time Julian date for a UTC Modified Julian Date."""
|
|
return utc_mjd + 2400000.5 + TT_MINUS_UTC_SECONDS / 86400.0
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
path = Path(argv[1]) if len(argv) > 1 else DEFAULT_INPUT
|
|
if not path.exists():
|
|
print(f"missing input file: {path}", file=sys.stderr)
|
|
print(
|
|
"generate it with: cargo run -p apsis-rs --features physics "
|
|
"--example dump_iau2000 > " + str(path),
|
|
file=sys.stderr,
|
|
)
|
|
return 2
|
|
|
|
rows = read_rows(path)
|
|
if not rows:
|
|
print("input file has no data rows", file=sys.stderr)
|
|
return 2
|
|
|
|
worst: dict[str, float] = {}
|
|
worst_at: dict[str, object] = {}
|
|
compared = 0
|
|
|
|
for row in rows:
|
|
mjd = float(row["mjd"])
|
|
quantity = row["quantity"]
|
|
index = int(row["index"])
|
|
actual = float(row["value"])
|
|
|
|
tt = tt_julian_date(mjd)
|
|
d1 = 2400000.5
|
|
d2 = tt - d1
|
|
|
|
if quantity == "obliquity":
|
|
reference = erfa.obl06(d1, d2)
|
|
elif quantity == "nutation_longitude_a":
|
|
reference = erfa.nut00a(d1, d2)[0]
|
|
elif quantity == "nutation_obliquity_a":
|
|
reference = erfa.nut00a(d1, d2)[1]
|
|
elif quantity == "nutation_longitude_06a":
|
|
reference = erfa.nut06a(d1, d2)[0]
|
|
elif quantity == "nutation_obliquity_06a":
|
|
reference = erfa.nut06a(d1, d2)[1]
|
|
elif quantity == "precession_gamma_bar":
|
|
reference = erfa.pfw06(d1, d2)[0]
|
|
elif quantity == "precession_phi_bar":
|
|
reference = erfa.pfw06(d1, d2)[1]
|
|
elif quantity == "precession_psi_bar":
|
|
reference = erfa.pfw06(d1, d2)[2]
|
|
elif quantity == "precession_epsilon":
|
|
reference = erfa.pfw06(d1, d2)[3]
|
|
elif quantity == "bpn":
|
|
matrix = erfa.pnm06a(d1, d2)
|
|
reference = matrix[index // 3][index % 3]
|
|
elif quantity == "gast":
|
|
reference = erfa.gst06a(d1, d2, d1, d2)
|
|
elif quantity == "c2t":
|
|
# The equinox based chain: the bias, precession and nutation,
|
|
# then Greenwich apparent sidereal time, then the polar motion.
|
|
matrix = erfa.c2t06a(d1, d2, d1, d2, 0.0, 0.0)
|
|
reference = matrix[index // 3][index % 3]
|
|
else:
|
|
continue
|
|
|
|
error = abs(actual - reference)
|
|
if error > worst.get(quantity, 0.0):
|
|
worst[quantity] = error
|
|
worst_at[quantity] = (mjd, index)
|
|
compared += 1
|
|
|
|
print(f"values compared: {compared}")
|
|
for quantity in sorted(worst):
|
|
tolerance = tolerance_for(quantity)
|
|
status = "ok" if worst[quantity] <= tolerance else "FAIL"
|
|
print(
|
|
f" {quantity:26} worst {worst[quantity]:.3e} "
|
|
f"(tolerance {tolerance:.0e}) at {worst_at[quantity]} {status}"
|
|
)
|
|
|
|
passed = all(
|
|
value <= tolerance_for(quantity) for quantity, value in worst.items()
|
|
)
|
|
if passed and compared > 0:
|
|
print("result: pass")
|
|
return 0
|
|
print("result: FAIL")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv))
|