165 lines
5.6 KiB
Python
165 lines
5.6 KiB
Python
"""Cross-check the ApsisRS gaseous attenuation against ITU-R P.676-12.
|
|
|
|
Reads the values written by ``cargo run -p apsis-rs --features rf --example
|
|
dump_gas`` and recomputes each one with the ``itur`` package, which
|
|
implements the ITU-R recommendations directly, then reports the largest
|
|
difference in each column.
|
|
|
|
The ``itur`` package is a reference implementation of the recommendations
|
|
maintained as part of the ITU-R study programme, so it is an independent
|
|
check on the line tables and the formulae rather than a restatement of them.
|
|
|
|
The script needs no network access.
|
|
|
|
Usage:
|
|
|
|
pixi run python validation/python/atmosphere/check_gas.py [path]
|
|
|
|
Exit status is zero when every value agrees within tolerance and one
|
|
otherwise.
|
|
|
|
用 ITU-R P.676-12 交叉核对 ApsisRS 的气体衰减。
|
|
|
|
读取由 ``cargo run -p apsis-rs --features rf --example dump_gas`` 写出的
|
|
值,并用 ``itur`` 包逐个重算,后者直接实现 ITU-R 各建议书,然后报告每列
|
|
的最大差异。
|
|
|
|
``itur`` 包是作为 ITU-R 研究计划一部分维护的建议书参考实现,故它是对线表
|
|
与各公式的独立核对,而非对它们的复述。
|
|
|
|
本脚本不需要网络访问。
|
|
|
|
用法:
|
|
|
|
pixi run python validation/python/atmosphere/check_gas.py [path]
|
|
|
|
每个值都在容差内一致时退出状态为零,否则为一。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import astropy.units as u
|
|
|
|
from itur.models.itu676 import _ITU676_12_
|
|
|
|
# The reference atmosphere of the recommendation, matching the Rust side.
|
|
# 该建议书的参考大气,与 Rust 侧一致。
|
|
PRESSURE_HPA = 1013.25
|
|
WATER_VAPOUR_DENSITY = 7.5
|
|
TEMPERATURE_K = 288.15
|
|
|
|
# Tolerances.
|
|
#
|
|
# The specific attenuation spans several orders of magnitude, and the water
|
|
# vapour term at low frequencies is a small difference between larger
|
|
# contributions, so a pure relative test is too strict there. The check
|
|
# passes when either the relative difference is small or the absolute
|
|
# difference is negligible.
|
|
#
|
|
# 容差。
|
|
#
|
|
# 比衰减跨越数个数量级,且低频处的水汽项是较大贡献之间的小差值,故纯粹
|
|
# 的相对判据在那里过严。当相对差异很小或绝对差异可忽略时,检查即通过。
|
|
RELATIVE_TOLERANCE = 1e-8
|
|
ABSOLUTE_TOLERANCE = 1e-10
|
|
|
|
DEFAULT_INPUT = Path("validation/data/expected/rust_gas.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("frequency_ghz")]
|
|
|
|
|
|
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_gas > " + 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 = _ITU676_12_()
|
|
|
|
columns = ["dry_db_per_km", "wet_db_per_km", "dry_height_km", "wet_height_km", "zenith_db"]
|
|
worst = {column: 0.0 for column in columns}
|
|
worst_absolute = {column: 0.0 for column in columns}
|
|
worst_at = {column: None for column in columns}
|
|
|
|
for row in rows:
|
|
frequency = float(row["frequency_ghz"])
|
|
dry = float(model.gamma0_exact(frequency, PRESSURE_HPA, WATER_VAPOUR_DENSITY, TEMPERATURE_K))
|
|
wet = float(model.gammaw_exact(frequency, PRESSURE_HPA, WATER_VAPOUR_DENSITY, TEMPERATURE_K))
|
|
heights = model.slant_inclined_path_equivalent_height(
|
|
frequency, PRESSURE_HPA, WATER_VAPOUR_DENSITY, TEMPERATURE_K
|
|
)
|
|
dry_height = float(np.asarray(heights[0]).ravel()[0])
|
|
wet_height = float(np.asarray(heights[1]).ravel()[0])
|
|
zenith = dry * dry_height + wet * wet_height
|
|
|
|
expected = {
|
|
"dry_db_per_km": dry,
|
|
"wet_db_per_km": wet,
|
|
"dry_height_km": dry_height,
|
|
"wet_height_km": wet_height,
|
|
"zenith_db": zenith,
|
|
}
|
|
for column, reference in expected.items():
|
|
actual = float(row[column])
|
|
absolute = abs(actual - reference)
|
|
relative = absolute / max(abs(reference), ABSOLUTE_TOLERANCE)
|
|
# The column passes on either measure, so the reported figure is
|
|
# the relative one and the acceptance test below also allows the
|
|
# absolute difference to be negligible.
|
|
# 该列以两种度量之一通过即可,故所报告的数字是相对值,而下面的
|
|
# 接受判据也允许绝对差异可忽略。
|
|
if relative > worst[column]:
|
|
worst[column] = relative
|
|
worst_at[column] = frequency
|
|
if absolute > worst_absolute[column]:
|
|
worst_absolute[column] = absolute
|
|
|
|
print(f"frequencies compared: {len(rows)}")
|
|
for column in columns:
|
|
print(
|
|
f"{column:>15}: worst relative {worst[column]:.3e} "
|
|
f"absolute {worst_absolute[column]:.3e} at {worst_at[column]} GHz"
|
|
)
|
|
print(
|
|
f"{'tolerance':>15}: {RELATIVE_TOLERANCE:.3e} relative or "
|
|
f"{ABSOLUTE_TOLERANCE:.3e} absolute"
|
|
)
|
|
|
|
passes = all(
|
|
worst[column] <= RELATIVE_TOLERANCE
|
|
or worst_absolute[column] <= ABSOLUTE_TOLERANCE
|
|
for column in columns
|
|
)
|
|
if passes:
|
|
print("result: pass")
|
|
return 0
|
|
print("result: FAIL")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv))
|