128 lines
3.8 KiB
Python
128 lines
3.8 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.
|
||
|
||
用 ITU-R P.839-4 交叉核对 ApsisRS 的雨顶高度。
|
||
|
||
读取由 ``cargo run -p apsis-rs --features rf --example dump_p839`` 写出的
|
||
值,并用 ``itur`` 包逐个重算,后者直接实现该建议书。
|
||
|
||
参考实现在其双线性插值器所覆盖区域之外返回非数(NaN),这发生在两极附近
|
||
与接缝处。那些点被分开报告而不计为失败,因为所列网格确实覆盖它们,而
|
||
Rust 模型是由该网格插值的。
|
||
|
||
本脚本不需要网络访问。
|
||
|
||
用法:
|
||
|
||
pixi run python validation/python/atmosphere/check_p839.py [path]
|
||
|
||
每个被比较的值都在容差内一致时退出状态为零,否则为一。
|
||
"""
|
||
|
||
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.
|
||
|
||
读取 CSV 各行。
|
||
"""
|
||
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))
|