apsis-rs/validation/python/atmosphere/check_rain_path.py

197 lines
6.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Cross-check the ApsisRS rain path against ITU-R P.618-13.
Reads the values written by ``cargo run -p apsis-rs --features rf --example
dump_rain_path`` and recomputes each one from the equations of the
recommendation. The specific attenuation coefficients come from the ``itur``
implementation of ITU-R P.838-3, and the path geometry and the two reduction
factors are transcribed from Recommendation P.618-13.
The script needs no network access.
Usage:
pixi run python validation/python/atmosphere/check_rain_path.py [path]
Exit status is zero when every value agrees within tolerance and one
otherwise.
用 ITU-R P.618-13 交叉核对 ApsisRS 的降雨路径。
读取由 ``cargo run -p apsis-rs --features rf --example dump_rain_path``
写出的值,并从该建议书的各式逐个重算。比衰减系数来自 ITU-R P.838-3 的
``itur`` 实现,路径几何与两个缩减因子则抄自建议书 P.618-13。
本脚本不需要网络访问。
用法:
pixi run python validation/python/atmosphere/check_rain_path.py [path]
每个值都在容差内一致时退出状态为零,否则为一。
"""
from __future__ import annotations
import csv
import math
import sys
from pathlib import Path
from itur.models.itu838 import _ITU838_3_
# Tolerance on the path length and the attenuation, in their own units.
# 路径长度与衰减的容差,各以其自身单位计。
LENGTH_TOLERANCE_KM = 1e-9
ATTENUATION_TOLERANCE_DB = 1e-9
# The effective radius of the Earth used by the path geometry, in
# kilometers, from Recommendation P.618-13.
# 路径几何所用的地球有效半径,单位为千米,取自建议书 P.618-13。
EFFECTIVE_EARTH_RADIUS_KM = 8500.0
# The rain layer height used by the Rust example, in kilometers.
# Rust 示例所用的雨层高度,单位为千米。
RAIN_HEIGHT_KM = 3.0
DEFAULT_INPUT = Path("validation/data/expected/rust_rain_path.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("rain_rate_mm_per_h")]
def reference(row: dict[str, str], coefficients) -> tuple[float, float]:
"""Recomputes the path length and the attenuation.
重算路径长度与衰减。
"""
rain_rate = float(row["rain_rate_mm_per_h"])
frequency = float(row["frequency_ghz"])
elevation = float(row["elevation_deg"])
latitude = float(row["latitude_deg"])
station_height = 0.0
k, alpha = coefficients.rain_specific_attenuation_coefficients(
frequency, elevation, 0.0
)
specific = float(k) * rain_rate ** float(alpha)
# Equation 2: the slant path length.
# 式 2:斜路径长度。
height_difference = RAIN_HEIGHT_KM - station_height
sine = math.sin(math.radians(elevation))
slant_length = 2.0 * height_difference / (
math.sqrt(sine * sine + 2.0 * height_difference / EFFECTIVE_EARTH_RADIUS_KM)
+ sine
)
# Equation 3: the horizontal projection.
# 式 3:水平投影。
horizontal_length = abs(slant_length * math.cos(math.radians(elevation)))
# Equation 6: the horizontal reduction factor.
# 式 6:水平缩减因子。
horizontal_reduction = 1.0 / (
1.0
+ 0.78 * math.sqrt(horizontal_length * specific / frequency)
- 0.38 * (1.0 - math.exp(-2.0 * horizontal_length))
)
# Equation 7: the angle at which the path meets the rain layer.
# 式 7:路径与雨层相交的角度。
eta = math.degrees(
math.atan2(height_difference, horizontal_length * horizontal_reduction)
)
# Equation 8: the longer of the two path estimates.
# 式 8:两个路径估计中较长者。
if eta > elevation:
path_length = (
horizontal_length * horizontal_reduction / math.cos(math.radians(elevation))
)
else:
path_length = height_difference / sine
# Equation 9: the vertical adjustment factor. The exponential takes the
# elevation in degrees.
# 式 9:垂直调整因子。指数以度接收仰角。
xi = 36.0 - abs(latitude) if abs(latitude) < 36.0 else 0.0
vertical_adjustment = 1.0 / (
1.0
+ math.sqrt(sine)
* (
31.0
* (1.0 - math.exp(-(elevation / (1.0 + xi))))
* math.sqrt(path_length * specific)
/ frequency**2
- 0.45
)
)
effective_length = path_length * vertical_adjustment
return effective_length, specific * effective_length
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_rain_path > " + 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
coefficients = _ITU838_3_()
worst_length = 0.0
worst_attenuation = 0.0
worst_at = None
for row in rows:
length, attenuation = reference(row, coefficients)
length_error = abs(float(row["path_length_km"]) - length)
attenuation_error = abs(float(row["attenuation_db"]) - attenuation)
if length_error > worst_length or attenuation_error > worst_attenuation:
worst_at = (
row["rain_rate_mm_per_h"],
row["frequency_ghz"],
row["elevation_deg"],
row["latitude_deg"],
)
worst_length = max(worst_length, length_error)
worst_attenuation = max(worst_attenuation, attenuation_error)
print(f"scenarios compared: {len(rows)}")
print(f"worst path length: {worst_length:.3e} km")
print(f"worst attenuation: {worst_attenuation:.3e} dB")
print(f"worst scenario: {worst_at}")
print(
f"tolerance: {LENGTH_TOLERANCE_KM:.3e} km or "
f"{ATTENUATION_TOLERANCE_DB:.3e} dB"
)
if (
worst_length <= LENGTH_TOLERANCE_KM
and worst_attenuation <= ATTENUATION_TOLERANCE_DB
):
print("result: pass")
return 0
print("result: FAIL")
return 1
if __name__ == "__main__":
raise SystemExit(main(sys.argv))