docs: bilingual comments for the atmosphere and coordinate validation scripts

This commit is contained in:
ApsisRS contributors 2026-10-02 01:41:29 +08:00
parent ff329e5a8d
commit b3d7762d0e
8 changed files with 180 additions and 10 deletions

View File

@ -17,6 +17,23 @@ Usage:
Exit status is zero when every value agrees within tolerance and one Exit status is zero when every value agrees within tolerance and one
otherwise. 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 from __future__ import annotations
@ -31,6 +48,7 @@ import astropy.units as u
from itur.models.itu676 import _ITU676_12_ from itur.models.itu676 import _ITU676_12_
# The reference atmosphere of the recommendation, matching the Rust side. # The reference atmosphere of the recommendation, matching the Rust side.
# 该建议书的参考大气,与 Rust 侧一致。
PRESSURE_HPA = 1013.25 PRESSURE_HPA = 1013.25
WATER_VAPOUR_DENSITY = 7.5 WATER_VAPOUR_DENSITY = 7.5
TEMPERATURE_K = 288.15 TEMPERATURE_K = 288.15
@ -42,6 +60,11 @@ TEMPERATURE_K = 288.15
# contributions, so a pure relative test is too strict there. The check # contributions, so a pure relative test is too strict there. The check
# passes when either the relative difference is small or the absolute # passes when either the relative difference is small or the absolute
# difference is negligible. # difference is negligible.
#
# 容差。
#
# 比衰减跨越数个数量级,且低频处的水汽项是较大贡献之间的小差值,故纯粹
# 的相对判据在那里过严。当相对差异很小或绝对差异可忽略时,检查即通过。
RELATIVE_TOLERANCE = 1e-8 RELATIVE_TOLERANCE = 1e-8
ABSOLUTE_TOLERANCE = 1e-10 ABSOLUTE_TOLERANCE = 1e-10
@ -49,7 +72,10 @@ DEFAULT_INPUT = Path("validation/data/expected/rust_gas.csv")
def read_rows(path: Path) -> list[dict[str, str]]: def read_rows(path: Path) -> list[dict[str, str]]:
"""Reads the CSV rows.""" """Reads the CSV rows.
读取 CSV 各行。
"""
with path.open(newline="") as handle: with path.open(newline="") as handle:
reader = csv.DictReader(handle) reader = csv.DictReader(handle)
return [row for row in reader if row.get("frequency_ghz")] return [row for row in reader if row.get("frequency_ghz")]
@ -103,6 +129,8 @@ def main(argv: list[str]) -> int:
# The column passes on either measure, so the reported figure is # The column passes on either measure, so the reported figure is
# the relative one and the acceptance test below also allows the # the relative one and the acceptance test below also allows the
# absolute difference to be negligible. # absolute difference to be negligible.
# 该列以两种度量之一通过即可,故所报告的数字是相对值,而下面的
# 接受判据也允许绝对差异可忽略。
if relative > worst[column]: if relative > worst[column]:
worst[column] = relative worst[column] = relative
worst_at[column] = frequency worst_at[column] = frequency

View File

@ -14,6 +14,21 @@ Usage:
Exit status is zero when every value agrees within tolerance and one Exit status is zero when every value agrees within tolerance and one
otherwise. otherwise.
用 ITU-R P.676-12 交叉核对 ApsisRS 的精确斜路径。
读取由 ``cargo run -p apsis-rs --features rf --example dump_gas_exact``
写出的值,并用 ``itur`` 包逐个重算,后者直接实现该建议书的精确方法。精确
方法在分层标准大气中积分比衰减并追踪折射后的射线,故它是低仰角下宜用的
形式。
本脚本不需要网络访问。
用法:
pixi run python validation/python/atmosphere/check_gas_exact.py [path]
每个值都在容差内一致时退出状态为零,否则为一。
""" """
from __future__ import annotations from __future__ import annotations
@ -25,19 +40,24 @@ from pathlib import Path
from itur.models.itu676 import _ITU676_12_ from itur.models.itu676 import _ITU676_12_
# The reference atmosphere, matching the Rust side. # The reference atmosphere, matching the Rust side.
# 参考大气,与 Rust 侧一致。
PRESSURE_HPA = 1013.25 PRESSURE_HPA = 1013.25
WATER_VAPOUR_DENSITY = 7.5 WATER_VAPOUR_DENSITY = 7.5
TEMPERATURE_K = 288.15 TEMPERATURE_K = 288.15
# The implementation reproduces the reference to about nine significant # The implementation reproduces the reference to about nine significant
# digits, so this allows for the rounding of the recorded values. # digits, so this allows for the rounding of the recorded values.
# 该实现把参考值复现到约九位有效数字,故此值容许所记录值的舍入。
RELATIVE_TOLERANCE = 1e-7 RELATIVE_TOLERANCE = 1e-7
DEFAULT_INPUT = Path("validation/data/expected/rust_gas_exact.csv") DEFAULT_INPUT = Path("validation/data/expected/rust_gas_exact.csv")
def read_rows(path: Path) -> list[dict[str, str]]: def read_rows(path: Path) -> list[dict[str, str]]:
"""Reads the CSV rows.""" """Reads the CSV rows.
读取 CSV 各行。
"""
with path.open(newline="") as handle: with path.open(newline="") as handle:
reader = csv.DictReader(handle) reader = csv.DictReader(handle)
return [row for row in reader if row.get("frequency_ghz")] return [row for row in reader if row.get("frequency_ghz")]

View File

@ -18,6 +18,23 @@ Usage:
Exit status is zero when every compared value agrees within tolerance and one Exit status is zero when every compared value agrees within tolerance and one
otherwise. 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 from __future__ import annotations
@ -33,13 +50,17 @@ from itur.models.itu839 import _ITU839_4_
# The table is stored in metres, so the interpolation reproduces the # The table is stored in metres, so the interpolation reproduces the
# tabulated values to that resolution. # tabulated values to that resolution.
# 该表以米存储,故插值把所列值复现到该分辨率。
TOLERANCE_KM = 1e-9 TOLERANCE_KM = 1e-9
DEFAULT_INPUT = Path("validation/data/expected/rust_p839.csv") DEFAULT_INPUT = Path("validation/data/expected/rust_p839.csv")
def read_rows(path: Path) -> list[dict[str, str]]: def read_rows(path: Path) -> list[dict[str, str]]:
"""Reads the CSV rows.""" """Reads the CSV rows.
读取 CSV 各行。
"""
with path.open(newline="") as handle: with path.open(newline="") as handle:
reader = csv.DictReader(handle) reader = csv.DictReader(handle)
return [row for row in reader if row.get("latitude_deg")] return [row for row in reader if row.get("latitude_deg")]
@ -79,6 +100,7 @@ def main(argv: list[str]) -> int:
) )
if math.isnan(reference): if math.isnan(reference):
# The reference interpolator does not cover this point. # The reference interpolator does not cover this point.
# 参考插值器不覆盖该点。
outside += 1 outside += 1
continue continue

View File

@ -12,6 +12,19 @@ Usage:
Exit status is zero when every value agrees within tolerance and one Exit status is zero when every value agrees within tolerance and one
otherwise. otherwise.
用 ITU-R P.838-3 交叉核对 ApsisRS 的降雨系数。
读取由 ``cargo run -p apsis-rs --features rf --example dump_rain`` 写出的
值,并用 ``itur`` 包逐个重算,后者直接实现该建议书。
本脚本不需要网络访问。
用法:
pixi run python validation/python/atmosphere/check_rain.py [path]
每个值都在容差内一致时退出状态为零,否则为一。
""" """
from __future__ import annotations from __future__ import annotations
@ -26,13 +39,18 @@ from itur.models.itu838 import _ITU838_3_
# digits. The Rust output is written with twelve decimal places, which limits # digits. The Rust output is written with twelve decimal places, which limits
# the comparison for the smallest coefficients, so the tolerance is set to # the comparison for the smallest coefficients, so the tolerance is set to
# one part in ten to the eighth. # one part in ten to the eighth.
# 该实现把参考值复现到约十二位有效数字。Rust 输出以十二位小数写出,这限制
# 了最小系数的比较,故容差设为十的八次方分之一。
RELATIVE_TOLERANCE = 1e-8 RELATIVE_TOLERANCE = 1e-8
DEFAULT_INPUT = Path("validation/data/expected/rust_rain.csv") DEFAULT_INPUT = Path("validation/data/expected/rust_rain.csv")
def read_rows(path: Path) -> list[dict[str, str]]: def read_rows(path: Path) -> list[dict[str, str]]:
"""Reads the CSV rows.""" """Reads the CSV rows.
读取 CSV 各行。
"""
with path.open(newline="") as handle: with path.open(newline="") as handle:
reader = csv.DictReader(handle) reader = csv.DictReader(handle)
return [row for row in reader if row.get("frequency_ghz")] return [row for row in reader if row.get("frequency_ghz")]

View File

@ -14,6 +14,20 @@ Usage:
Exit status is zero when every value agrees within tolerance and one Exit status is zero when every value agrees within tolerance and one
otherwise. 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 from __future__ import annotations
@ -26,28 +40,37 @@ from pathlib import Path
from itur.models.itu838 import _ITU838_3_ from itur.models.itu838 import _ITU838_3_
# Tolerance on the path length and the attenuation, in their own units. # Tolerance on the path length and the attenuation, in their own units.
# 路径长度与衰减的容差,各以其自身单位计。
LENGTH_TOLERANCE_KM = 1e-9 LENGTH_TOLERANCE_KM = 1e-9
ATTENUATION_TOLERANCE_DB = 1e-9 ATTENUATION_TOLERANCE_DB = 1e-9
# The effective radius of the Earth used by the path geometry, in # The effective radius of the Earth used by the path geometry, in
# kilometers, from Recommendation P.618-13. # kilometers, from Recommendation P.618-13.
# 路径几何所用的地球有效半径,单位为千米,取自建议书 P.618-13。
EFFECTIVE_EARTH_RADIUS_KM = 8500.0 EFFECTIVE_EARTH_RADIUS_KM = 8500.0
# The rain layer height used by the Rust example, in kilometers. # The rain layer height used by the Rust example, in kilometers.
# Rust 示例所用的雨层高度,单位为千米。
RAIN_HEIGHT_KM = 3.0 RAIN_HEIGHT_KM = 3.0
DEFAULT_INPUT = Path("validation/data/expected/rust_rain_path.csv") DEFAULT_INPUT = Path("validation/data/expected/rust_rain_path.csv")
def read_rows(path: Path) -> list[dict[str, str]]: def read_rows(path: Path) -> list[dict[str, str]]:
"""Reads the CSV rows.""" """Reads the CSV rows.
读取 CSV 各行。
"""
with path.open(newline="") as handle: with path.open(newline="") as handle:
reader = csv.DictReader(handle) reader = csv.DictReader(handle)
return [row for row in reader if row.get("rain_rate_mm_per_h")] return [row for row in reader if row.get("rain_rate_mm_per_h")]
def reference(row: dict[str, str], coefficients) -> tuple[float, float]: def reference(row: dict[str, str], coefficients) -> tuple[float, float]:
"""Recomputes the path length and the attenuation.""" """Recomputes the path length and the attenuation.
重算路径长度与衰减。
"""
rain_rate = float(row["rain_rate_mm_per_h"]) rain_rate = float(row["rain_rate_mm_per_h"])
frequency = float(row["frequency_ghz"]) frequency = float(row["frequency_ghz"])
elevation = float(row["elevation_deg"]) elevation = float(row["elevation_deg"])
@ -60,6 +83,7 @@ def reference(row: dict[str, str], coefficients) -> tuple[float, float]:
specific = float(k) * rain_rate ** float(alpha) specific = float(k) * rain_rate ** float(alpha)
# Equation 2: the slant path length. # Equation 2: the slant path length.
# 式 2:斜路径长度。
height_difference = RAIN_HEIGHT_KM - station_height height_difference = RAIN_HEIGHT_KM - station_height
sine = math.sin(math.radians(elevation)) sine = math.sin(math.radians(elevation))
slant_length = 2.0 * height_difference / ( slant_length = 2.0 * height_difference / (
@ -68,9 +92,11 @@ def reference(row: dict[str, str], coefficients) -> tuple[float, float]:
) )
# Equation 3: the horizontal projection. # Equation 3: the horizontal projection.
# 式 3:水平投影。
horizontal_length = abs(slant_length * math.cos(math.radians(elevation))) horizontal_length = abs(slant_length * math.cos(math.radians(elevation)))
# Equation 6: the horizontal reduction factor. # Equation 6: the horizontal reduction factor.
# 式 6:水平缩减因子。
horizontal_reduction = 1.0 / ( horizontal_reduction = 1.0 / (
1.0 1.0
+ 0.78 * math.sqrt(horizontal_length * specific / frequency) + 0.78 * math.sqrt(horizontal_length * specific / frequency)
@ -78,11 +104,13 @@ def reference(row: dict[str, str], coefficients) -> tuple[float, float]:
) )
# Equation 7: the angle at which the path meets the rain layer. # Equation 7: the angle at which the path meets the rain layer.
# 式 7:路径与雨层相交的角度。
eta = math.degrees( eta = math.degrees(
math.atan2(height_difference, horizontal_length * horizontal_reduction) math.atan2(height_difference, horizontal_length * horizontal_reduction)
) )
# Equation 8: the longer of the two path estimates. # Equation 8: the longer of the two path estimates.
# 式 8:两个路径估计中较长者。
if eta > elevation: if eta > elevation:
path_length = ( path_length = (
horizontal_length * horizontal_reduction / math.cos(math.radians(elevation)) horizontal_length * horizontal_reduction / math.cos(math.radians(elevation))
@ -92,6 +120,7 @@ def reference(row: dict[str, str], coefficients) -> tuple[float, float]:
# Equation 9: the vertical adjustment factor. The exponential takes the # Equation 9: the vertical adjustment factor. The exponential takes the
# elevation in degrees. # elevation in degrees.
# 式 9:垂直调整因子。指数以度接收仰角。
xi = 36.0 - abs(latitude) if abs(latitude) < 36.0 else 0.0 xi = 36.0 - abs(latitude) if abs(latitude) < 36.0 else 0.0
vertical_adjustment = 1.0 / ( vertical_adjustment = 1.0 / (
1.0 1.0

View File

@ -14,6 +14,20 @@ Usage:
Exit status is zero when every position agrees within the tolerance and one Exit status is zero when every position agrees within the tolerance and one
otherwise. otherwise.
交叉核对 ApsisRS 的大地坐标到地固坐标转换。
读取由 ``cargo run -p apsis-rs --features physics --example dump_geodesy``
写出的网格,并用 astropy 逐个重算位置,然后报告最大差异。
本脚本不需要网络访问。它从命令行给出的文件路径读取输入,缺省为标准位置,
并把报告写到标准输出。
用法:
pixi run python validation/python/coord/check_geodetic.py [path]
每个位置都在容差内一致时退出状态为零,否则为一。
""" """
from __future__ import annotations from __future__ import annotations
@ -27,13 +41,17 @@ from astropy.coordinates import EarthLocation
# Position tolerance in meters. Astropy evaluates the same closed form, so # Position tolerance in meters. Astropy evaluates the same closed form, so
# the only difference should be floating point rounding. # the only difference should be floating point rounding.
# 位置容差,单位为米。Astropy 求的是同一个闭式,故唯一差异应是浮点舍入。
TOLERANCE_M = 1e-6 TOLERANCE_M = 1e-6
DEFAULT_INPUT = Path("validation/data/expected/rust_geodetic.csv") DEFAULT_INPUT = Path("validation/data/expected/rust_geodetic.csv")
def rust_positions(path: Path) -> list[tuple[float, float, float, tuple[float, float, float]]]: def rust_positions(path: Path) -> list[tuple[float, float, float, tuple[float, float, float]]]:
"""Reads the Rust output rows.""" """Reads the Rust output rows.
读取 Rust 输出各行。
"""
rows = [] rows = []
with path.open(newline="") as handle: with path.open(newline="") as handle:
reader = csv.DictReader(handle) reader = csv.DictReader(handle)
@ -50,7 +68,10 @@ def rust_positions(path: Path) -> list[tuple[float, float, float, tuple[float, f
def astropy_position(latitude_deg: float, longitude_deg: float, height_m: float): def astropy_position(latitude_deg: float, longitude_deg: float, height_m: float):
"""Recomputes a position with astropy.""" """Recomputes a position with astropy.
用 astropy 重算一个位置。
"""
location = EarthLocation.from_geodetic( location = EarthLocation.from_geodetic(
lon=longitude_deg * u.deg, lon=longitude_deg * u.deg,
lat=latitude_deg * u.deg, lat=latitude_deg * u.deg,

View File

@ -20,6 +20,25 @@ Usage:
Exit status is zero when every position agrees within tolerance and one Exit status is zero when every position agrees within tolerance and one
otherwise. otherwise.
交叉核对 ApsisRS 的 TEME 到地固坐标变换。
读取由 ``cargo run -p apsis-rs --features physics --example dump_teme``
写出的位置,并用 ERFA 逐个重算,施加相同的格林尼治平恒星时与相同的零极移,
使该检查比较的是实现而非输入的选择。
参考值是 1982 年的恒星时与 IAU SOFA 库的极移合成,由 ERFA 实现。注意
astropy 与该变换只相符到数百米,因为 astropy 施加实测极移与实测
UT1 - UTC,而本检查对两者都用零。该相符情况为提供信息而报告,但并非接受
判据。
本脚本不需要网络访问。
用法:
pixi run python validation/python/coord/check_teme.py [path]
每个位置都在容差内一致时退出状态为零,否则为一。
""" """
from __future__ import annotations from __future__ import annotations
@ -33,33 +52,45 @@ import erfa
# Tolerance in kilometers. The transformation is a product of two rotations # Tolerance in kilometers. The transformation is a product of two rotations
# evaluated in the same order, so the agreement is at the level of the last # evaluated in the same order, so the agreement is at the level of the last
# bits of a double. # bits of a double.
# 容差,单位为千米。该变换是两个按相同次序求值的转动之积,故相符程度在
# 双精度最后几位的水平上。
TOLERANCE_KM = 1e-9 TOLERANCE_KM = 1e-9
# The two-part Julian date used by the Rust example for the whole days. # The two-part Julian date used by the Rust example for the whole days.
# Rust 示例为整数天数所用的两部分儒略日。
DAY_PART = 2400000.5 DAY_PART = 2400000.5
DEFAULT_INPUT = Path("validation/data/expected/rust_teme.csv") DEFAULT_INPUT = Path("validation/data/expected/rust_teme.csv")
def read_rows(path: Path) -> list[dict[str, str]]: def read_rows(path: Path) -> list[dict[str, str]]:
"""Reads the CSV rows.""" """Reads the CSV rows.
读取 CSV 各行。
"""
with path.open(newline="") as handle: with path.open(newline="") as handle:
reader = csv.DictReader(handle) reader = csv.DictReader(handle)
return [row for row in reader if row.get("mjd")] return [row for row in reader if row.get("mjd")]
def reference_position(mjd: float, position: list[float]) -> list[float]: def reference_position(mjd: float, position: list[float]) -> list[float]:
"""Recomputes the Earth-fixed position with ERFA.""" """Recomputes the Earth-fixed position with ERFA.
用 ERFA 重算地固位置。
"""
# The sidereal time, from the two-part Julian date. # The sidereal time, from the two-part Julian date.
# 恒星时,由两部分儒略日得出。
sidereal = erfa.gmst82(DAY_PART, mjd) sidereal = erfa.gmst82(DAY_PART, mjd)
# The rotation of the frame about its third axis, composed as the # The rotation of the frame about its third axis, composed as the
# reference `rz` does, and the identity polar motion. # reference `rz` does, and the identity polar motion.
# 坐标系绕其第三轴的转动,按参考 `rz` 的方式合成,以及单位极移。
rotation = erfa.rz(sidereal, erfa.ir()) rotation = erfa.rz(sidereal, erfa.ir())
polar = erfa.pom00(0.0, 0.0, 0.0) polar = erfa.pom00(0.0, 0.0, 0.0)
matrix = erfa.rxr(polar, rotation) matrix = erfa.rxr(polar, rotation)
# The rotation maps the frame, so the position is the matrix applied to # The rotation maps the frame, so the position is the matrix applied to
# the components. # the components.
# 该转动映射坐标系,故位置是矩阵施加于各分量。
return [ return [
sum(matrix[i][j] * position[j] for j in range(3)) for i in range(3) sum(matrix[i][j] * position[j] for j in range(3)) for i in range(3)
] ]
@ -82,6 +113,7 @@ def main(argv: list[str]) -> int:
return 2 return 2
# The TEME inputs used by the example, in the same order. # The TEME inputs used by the example, in the same order.
# 示例所用的 TEME 输入,次序相同。
inputs = [ inputs = [
[7000.0, 1000.0, 2000.0], [7000.0, 1000.0, 2000.0],
[7000.0, -3000.0, 1000.0], [7000.0, -3000.0, 1000.0],