docs: bilingual comments for the coverage and link validation scripts

This commit is contained in:
ApsisRS contributors 2026-10-02 01:41:57 +08:00
parent b3d7762d0e
commit 78614d0a97
3 changed files with 114 additions and 9 deletions

View File

@ -27,6 +27,31 @@ Usage:
Exit status is zero when every quantity agrees within tolerance and one Exit status is zero when every quantity agrees within tolerance and one
otherwise. otherwise.
用球面三角交叉核对 ApsisRS 的覆盖几何。
读取由 ``cargo run -p apsis-rs --features network --example dump_coverage``
写出的量,并只用标准库与 numpy 从球面三角形关系逐个重算。要点在于 Python
侧是依据几何而非依据 Rust 代码写成的,故 Rust 公式中的错误会表现为不一致。
由卫星、地面点与地心构成的三角形的三个角满足
elevation + nadir + central = 90 degrees
正弦定理给出
sin(nadir) / body_radius = cos(elevation) / satellite_radius
由此得出天底角、中心角与斜距。覆盖区是中心角从天体上截出的球冠,故其面积
是带面积,其比例即该面积除以整个表面。
本脚本不需要网络访问。
用法:
pixi run python validation/python/constellation/check_coverage.py [path]
每个量都在容差内一致时退出状态为零,否则为一。
""" """
from __future__ import annotations from __future__ import annotations
@ -39,6 +64,8 @@ from pathlib import Path
# The agreement is at the floating point level because both sides evaluate # The agreement is at the floating point level because both sides evaluate
# the same closed form expressions. The tolerances are set to catch a # the same closed form expressions. The tolerances are set to catch a
# transposed radius or a missing conversion, not to allow a loose match. # transposed radius or a missing conversion, not to allow a loose match.
# 相符程度在浮点水平,因为两侧求的是相同的闭式表达式。这些容差是为捕获
# 半径写反或漏换算而设,并非为允许宽松匹配。
ANGLE_TOLERANCE_RAD = 1e-12 ANGLE_TOLERANCE_RAD = 1e-12
LENGTH_TOLERANCE_KM = 1e-9 LENGTH_TOLERANCE_KM = 1e-9
AREA_RELATIVE_TOLERANCE = 1e-12 AREA_RELATIVE_TOLERANCE = 1e-12
@ -48,18 +75,27 @@ DEFAULT_INPUT = Path("validation/data/expected/rust_coverage.csv")
def reference_nadir(satellite_radius: float, body_radius: float, elevation: float) -> float: def reference_nadir(satellite_radius: float, body_radius: float, elevation: float) -> float:
"""Returns the nadir angle from the law of sines.""" """Returns the nadir angle from the law of sines.
由正弦定理返回天底角。
"""
return math.asin(body_radius * math.cos(elevation) / satellite_radius) return math.asin(body_radius * math.cos(elevation) / satellite_radius)
def reference_central(satellite_radius: float, body_radius: float, elevation: float) -> float: def reference_central(satellite_radius: float, body_radius: float, elevation: float) -> float:
"""Returns the central angle from the angle sum.""" """Returns the central angle from the angle sum.
由角和返回中心角。
"""
nadir = reference_nadir(satellite_radius, body_radius, elevation) nadir = reference_nadir(satellite_radius, body_radius, elevation)
return max(0.0, math.pi / 2.0 - elevation - nadir) return max(0.0, math.pi / 2.0 - elevation - nadir)
def reference_range(satellite_radius: float, body_radius: float, elevation: float) -> float: def reference_range(satellite_radius: float, body_radius: float, elevation: float) -> float:
"""Returns the slant range from the law of cosines.""" """Returns the slant range from the law of cosines.
由余弦定理返回斜距。
"""
central = reference_central(satellite_radius, body_radius, elevation) central = reference_central(satellite_radius, body_radius, elevation)
squared = ( squared = (
satellite_radius**2 satellite_radius**2
@ -76,6 +112,11 @@ def reference_cap_area(central: float, body_radius: float) -> float:
``2 pi R^2 (1 - cos(central))``. The footprint is not centred on a pole in ``2 pi R^2 (1 - cos(central))``. The footprint is not centred on a pole in
general, but a spherical cap is rotationally symmetric, so the same general, but a spherical cap is rotationally symmetric, so the same
expression applies. expression applies.
返回给定中心角的球冠面积。
球冠是极点与某余纬之间的带,故其面积为 ``2 pi R^2 (1 - cos(central))``。
覆盖区通常并不以极点为中心,但球冠具有旋转对称性,故同一表达式适用。
""" """
return 2.0 * math.pi * body_radius**2 * (1.0 - math.cos(central)) return 2.0 * math.pi * body_radius**2 * (1.0 - math.cos(central))
@ -85,6 +126,11 @@ def read_blocks(path: Path):
The first block is the case table with a header row; a blank line The first block is the case table with a header row; a blank line
separates it from the inverse relation block, which has its own header. separates it from the inverse relation block, which has its own header.
读取该文件的两个块。
第一块是带表头的用例表;一个空行把它与反解关系块分开,后者有自己的
表头。
""" """
text = path.read_text().splitlines() text = path.read_text().splitlines()
blocks: list[list[str]] = [[]] blocks: list[list[str]] = [[]]
@ -97,7 +143,10 @@ def read_blocks(path: Path):
def check_cases(rows: list[dict[str, str]]) -> tuple[bool, list[str]]: def check_cases(rows: list[dict[str, str]]) -> tuple[bool, list[str]]:
"""Checks the case table and returns whether it passed and any failures.""" """Checks the case table and returns whether it passed and any failures.
检查用例表,返回是否通过以及任何失败项。
"""
failures: list[str] = [] failures: list[str] = []
compared = 0 compared = 0
worst = { worst = {
@ -158,7 +207,10 @@ def check_cases(rows: list[dict[str, str]]) -> tuple[bool, list[str]]:
def check_inverse(rows: list[dict[str, str]]) -> tuple[bool, list[str]]: def check_inverse(rows: list[dict[str, str]]) -> tuple[bool, list[str]]:
"""Checks that the central angle and elevation relations invert.""" """Checks that the central angle and elevation relations invert.
检查中心角与仰角关系可互相反解。
"""
failures: list[str] = [] failures: list[str] = []
compared = 0 compared = 0
worst = 0.0 worst = 0.0
@ -172,6 +224,7 @@ def check_inverse(rows: list[dict[str, str]]) -> tuple[bool, list[str]]:
# Recompute the round trip independently: from the elevation, the # Recompute the round trip independently: from the elevation, the
# central angle follows from the angle sum. # central angle follows from the angle sum.
# 独立重算往返:由仰角经角和得出中心角。
satellite_radius = 6_378.137 + 550.0 satellite_radius = 6_378.137 + 550.0
body_radius = 6_378.137 body_radius = 6_378.137
nadir = reference_nadir(satellite_radius, body_radius, elevation) nadir = reference_nadir(satellite_radius, body_radius, elevation)

View File

@ -17,6 +17,22 @@ Usage:
Exit status is zero when every figure agrees within tolerance and one Exit status is zero when every figure agrees within tolerance and one
otherwise. otherwise.
用链路方程交叉核对 ApsisRS 的链路预算。
读取由 ``cargo run -p apsis-rs --features rf --example dump_link`` 写出的
场景,并用纯 Python 从链路方程逐个重算每个数字,然后报告每列的最大差异。
此处的参考是链路方程本身而非某个库,因为该方程是闭式的且短到可以无歧义地
抄录。各常量取自 2019 年 SI 定义,故与 Rust 侧所用的值一致。
本脚本不需要网络访问。
用法:
pixi run python validation/python/link/check_link.py [path]
每个数字都在容差内一致时退出状态为零,否则为一。
""" """
from __future__ import annotations from __future__ import annotations
@ -27,25 +43,33 @@ import sys
from pathlib import Path from pathlib import Path
# Defining constants, exact in the SI. # Defining constants, exact in the SI.
# 定义常量,在 SI 中精确。
BOLTZMANN = 1.380649e-23 BOLTZMANN = 1.380649e-23
SPEED_OF_LIGHT = 299792458.0 SPEED_OF_LIGHT = 299792458.0
# Tolerance in decibels. The two sides evaluate the same expressions with # Tolerance in decibels. The two sides evaluate the same expressions with
# double precision, so the only difference should be rounding. # double precision, so the only difference should be rounding.
# 容差,单位为分贝。两侧以双精度求相同表达式,故唯一差异应是舍入。
TOLERANCE_DB = 1e-9 TOLERANCE_DB = 1e-9
DEFAULT_INPUT = Path("validation/data/expected/rust_link.csv") DEFAULT_INPUT = Path("validation/data/expected/rust_link.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("transmit_dbw")] return [row for row in reader if row.get("transmit_dbw")]
def reference(row: dict[str, str]) -> dict[str, float]: def reference(row: dict[str, str]) -> dict[str, float]:
"""Recomputes a budget from the link equation.""" """Recomputes a budget from the link equation.
由链路方程重算一份预算。
"""
transmit_dbw = float(row["transmit_dbw"]) transmit_dbw = float(row["transmit_dbw"])
transmit_dbi = float(row["transmit_dbi"]) transmit_dbi = float(row["transmit_dbi"])
transmit_loss = float(row["transmit_loss_db"]) transmit_loss = float(row["transmit_loss_db"])
@ -65,6 +89,7 @@ def reference(row: dict[str, str]) -> dict[str, float]:
cn_db = cn0_db - 10.0 * math.log10(bandwidth) cn_db = cn0_db - 10.0 * math.log10(bandwidth)
# The example reports the energy per bit ratio at a data rate equal to # The example reports the energy per bit ratio at a data rate equal to
# the bandwidth. # the bandwidth.
# 示例在数据速率等于带宽时报告每比特能量比。
ebn0_db = cn0_db - 10.0 * math.log10(bandwidth) ebn0_db = cn0_db - 10.0 * math.log10(bandwidth)
return { return {

View File

@ -11,6 +11,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.
交叉核对 ApsisRS 的 ITU-R S.465 参考方向图。
读取由 ``cargo run -p apsis-rs --features rf --example dump_s465`` 写出的
值,并从该建议书的各式逐个重算。
本脚本不需要网络访问。
用法:
pixi run python validation/python/link/check_s465.py [path]
每个值都在容差内一致时退出状态为零,否则为一。
""" """
from __future__ import annotations from __future__ import annotations
@ -21,24 +34,32 @@ import sys
from pathlib import Path from pathlib import Path
# The peak gain offset and the branch constants of the recommendation. # The peak gain offset and the branch constants of the recommendation.
# 该建议书的峰值增益偏移与各分支常量。
PEAK_GAIN_OFFSET_DB = 7.7 PEAK_GAIN_OFFSET_DB = 7.7
LARGE_APERTURE_RATIO = 100.0 LARGE_APERTURE_RATIO = 100.0
# Tolerance in decibels. # Tolerance in decibels.
# 容差,单位为分贝。
TOLERANCE_DB = 1e-6 TOLERANCE_DB = 1e-6
DEFAULT_INPUT = Path("validation/data/expected/rust_s465.csv") DEFAULT_INPUT = Path("validation/data/expected/rust_s465.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("ratio")] return [row for row in reader if row.get("ratio")]
def break_angle(ratio: float) -> tuple[float, float]: def break_angle(ratio: float) -> tuple[float, float]:
"""Returns the branch constant and the break angle, in degrees.""" """Returns the branch constant and the break angle, in degrees.
返回分支常量与转折角,单位为度。
"""
head, floor_angle = (29.0, 36.0) if ratio >= LARGE_APERTURE_RATIO else (32.0, 48.0) head, floor_angle = (29.0, 36.0) if ratio >= LARGE_APERTURE_RATIO else (32.0, 48.0)
peak = 20.0 * math.log10(ratio) + PEAK_GAIN_OFFSET_DB peak = 20.0 * math.log10(ratio) + PEAK_GAIN_OFFSET_DB
@ -66,6 +87,12 @@ def reference(ratio: float, angle_degrees: float) -> float:
are converted to radians and back before the branch decision. That are converted to radians and back before the branch decision. That
reproduces the rounding the implementation performs, which matters at reproduces the rounding the implementation performs, which matters at
the branch boundaries where the recommendation has a small step. the branch boundaries where the recommendation has a small step.
以分贝重算方向图增益。
该 API 以弧度接收角度,故在分支判定之前,文件中以度记录的角度先转成
弧度再转回。这复现了实现所执行的舍入,在各分支边界处要紧,因为该建议书
在那里有小的台阶。
""" """
peak = 20.0 * math.log10(ratio) + PEAK_GAIN_OFFSET_DB peak = 20.0 * math.log10(ratio) + PEAK_GAIN_OFFSET_DB
head, break_at = break_angle(ratio) head, break_at = break_angle(ratio)