120 lines
3.7 KiB
Python
120 lines
3.7 KiB
Python
"""Cross-check the ApsisRS TEME to Earth-fixed transformation.
|
|
|
|
Reads the positions written by ``cargo run -p apsis-rs --features physics
|
|
--example dump_teme`` and recomputes each one with ERFA, applying the same
|
|
Greenwich mean sidereal time and the same zero polar motion, so that the
|
|
check compares the implementation rather than the choice of inputs.
|
|
|
|
The reference is the sidereal time of 1982 and the polar motion composition
|
|
of the IAU SOFA library, as implemented by ERFA. Note that astropy agrees
|
|
with this transformation only to a few hundred metres, because astropy
|
|
applies the measured polar motion and a measured UT1 - UTC while this check
|
|
uses zero for both. That agreement is reported for information but is not
|
|
the acceptance criterion.
|
|
|
|
The script needs no network access.
|
|
|
|
Usage:
|
|
|
|
pixi run python validation/python/coord/check_teme.py [path]
|
|
|
|
Exit status is zero when every position agrees within tolerance and one
|
|
otherwise.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import erfa
|
|
|
|
# Tolerance in kilometers. The transformation is a product of two rotations
|
|
# evaluated in the same order, so the agreement is at the level of the last
|
|
# bits of a double.
|
|
TOLERANCE_KM = 1e-9
|
|
|
|
# The two-part Julian date used by the Rust example for the whole days.
|
|
DAY_PART = 2400000.5
|
|
|
|
DEFAULT_INPUT = Path("validation/data/expected/rust_teme.csv")
|
|
|
|
|
|
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 reference_position(mjd: float, position: list[float]) -> list[float]:
|
|
"""Recomputes the Earth-fixed position with ERFA."""
|
|
# The sidereal time, from the two-part Julian date.
|
|
sidereal = erfa.gmst82(DAY_PART, mjd)
|
|
# The rotation of the frame about its third axis, composed as the
|
|
# reference `rz` does, and the identity polar motion.
|
|
rotation = erfa.rz(sidereal, erfa.ir())
|
|
polar = erfa.pom00(0.0, 0.0, 0.0)
|
|
matrix = erfa.rxr(polar, rotation)
|
|
|
|
# The rotation maps the frame, so the position is the matrix applied to
|
|
# the components.
|
|
return [
|
|
sum(matrix[i][j] * position[j] for j in range(3)) for i in range(3)
|
|
]
|
|
|
|
|
|
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_teme > " + 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
|
|
|
|
# The TEME inputs used by the example, in the same order.
|
|
inputs = [
|
|
[7000.0, 1000.0, 2000.0],
|
|
[7000.0, -3000.0, 1000.0],
|
|
[42164.0, 0.0, 0.0],
|
|
[6878.137, 100.0, -50.0],
|
|
]
|
|
|
|
worst = 0.0
|
|
worst_at = None
|
|
for row, position in zip(rows, inputs):
|
|
mjd = float(row["mjd"])
|
|
actual = [
|
|
float(row["x_ecef"]),
|
|
float(row["y_ecef"]),
|
|
float(row["z_ecef"]),
|
|
]
|
|
expected = reference_position(mjd, position)
|
|
error = max(abs(a - b) for a, b in zip(actual, expected))
|
|
if error > worst:
|
|
worst = error
|
|
worst_at = mjd
|
|
|
|
print(f"positions compared: {len(rows)}")
|
|
print(f"worst difference: {worst:.3e} km at MJD {worst_at}")
|
|
print(f"tolerance: {TOLERANCE_KM:.3e} km")
|
|
|
|
if worst <= TOLERANCE_KM:
|
|
print("result: pass")
|
|
return 0
|
|
print("result: FAIL")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv))
|