apsis-rs/validation/python/coord/check_geodetic.py

104 lines
3.1 KiB
Python

"""Cross-check the ApsisRS geodetic to Earth-fixed conversion.
Reads the grid written by ``cargo run -p apsis-rs --features physics
--example dump_geodesy`` and recomputes each position with astropy, then
reports the largest difference.
The script needs no network access. It reads the input from a file path
given on the command line, defaulting to the standard location, and writes a
report to standard output.
Usage:
pixi run python validation/python/coord/check_geodetic.py [path]
Exit status is zero when every position agrees within the tolerance and one
otherwise.
"""
from __future__ import annotations
import csv
import sys
from pathlib import Path
import astropy.units as u
from astropy.coordinates import EarthLocation
# Position tolerance in meters. Astropy evaluates the same closed form, so
# the only difference should be floating point rounding.
TOLERANCE_M = 1e-6
DEFAULT_INPUT = Path("validation/data/expected/rust_geodetic.csv")
def rust_positions(path: Path) -> list[tuple[float, float, float, tuple[float, float, float]]]:
"""Reads the Rust output rows."""
rows = []
with path.open(newline="") as handle:
reader = csv.DictReader(handle)
for row in reader:
rows.append(
(
float(row["latitude_degrees"]),
float(row["longitude_degrees"]),
float(row["height_m"]),
(float(row["x_m"]), float(row["y_m"]), float(row["z_m"])),
)
)
return rows
def astropy_position(latitude_deg: float, longitude_deg: float, height_m: float):
"""Recomputes a position with astropy."""
location = EarthLocation.from_geodetic(
lon=longitude_deg * u.deg,
lat=latitude_deg * u.deg,
height=height_m * u.m,
)
return (
location.x.to_value(u.m),
location.y.to_value(u.m),
location.z.to_value(u.m),
)
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_geodesy > " + str(path),
file=sys.stderr,
)
return 2
rows = rust_positions(path)
if not rows:
print("input file has no data rows", file=sys.stderr)
return 2
worst = 0.0
worst_at = None
for latitude, longitude, height, rust in rows:
reference = astropy_position(latitude, longitude, height)
error = max(abs(a - b) for a, b in zip(rust, reference))
if error > worst:
worst = error
worst_at = (latitude, longitude, height)
print(f"rows compared: {len(rows)}")
print(f"worst error: {worst:.3e} m at lat/lon/height {worst_at}")
print(f"tolerance: {TOLERANCE_M:.3e} m")
if worst <= TOLERANCE_M:
print("result: pass")
return 0
print("result: FAIL")
return 1
if __name__ == "__main__":
raise SystemExit(main(sys.argv))