106 lines
3.0 KiB
Python
106 lines
3.0 KiB
Python
"""Cross-check the ApsisRS rain height against ITU-R P.839-4.
|
|
|
|
Reads the values written by ``cargo run -p apsis-rs --features rf --example
|
|
dump_p839`` and recomputes each one with the ``itur`` package, which
|
|
implements the recommendation directly.
|
|
|
|
The reference implementation returns a not-a-number value outside the region
|
|
its bilinear interpolator covers, which happens near the poles and at the
|
|
seam. Those points are reported separately rather than counted as failures,
|
|
because the tabulated grid does cover them and the Rust model interpolates
|
|
from it.
|
|
|
|
The script needs no network access.
|
|
|
|
Usage:
|
|
|
|
pixi run python validation/python/atmosphere/check_p839.py [path]
|
|
|
|
Exit status is zero when every compared value agrees within tolerance and one
|
|
otherwise.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
import math
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
from itur.models.itu839 import _ITU839_4_
|
|
|
|
# The table is stored in metres, so the interpolation reproduces the
|
|
# tabulated values to that resolution.
|
|
TOLERANCE_KM = 1e-9
|
|
|
|
DEFAULT_INPUT = Path("validation/data/expected/rust_p839.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("latitude_deg")]
|
|
|
|
|
|
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 rf "
|
|
"--example dump_p839 > " + 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
|
|
|
|
model = _ITU839_4_()
|
|
worst = 0.0
|
|
worst_at = None
|
|
compared = 0
|
|
outside = 0
|
|
|
|
for row in rows:
|
|
latitude = float(row["latitude_deg"])
|
|
longitude = float(row["longitude_deg"])
|
|
reference = float(
|
|
np.asarray(
|
|
model.isoterm_0(
|
|
np.array([[latitude]]), np.array([[longitude]])
|
|
)
|
|
).ravel()[0]
|
|
)
|
|
if math.isnan(reference):
|
|
# The reference interpolator does not cover this point.
|
|
outside += 1
|
|
continue
|
|
|
|
actual = float(row["isotherm_km"])
|
|
error = abs(actual - reference)
|
|
if error > worst:
|
|
worst = error
|
|
worst_at = (latitude, longitude)
|
|
compared += 1
|
|
|
|
print(f"points compared: {compared}")
|
|
print(f"outside the reference: {outside}")
|
|
print(f"worst difference: {worst:.3e} km at {worst_at}")
|
|
print(f"tolerance: {TOLERANCE_KM:.3e} km")
|
|
|
|
if compared > 0 and worst <= TOLERANCE_KM:
|
|
print("result: pass")
|
|
return 0
|
|
print("result: FAIL")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv))
|