146 lines
4.8 KiB
Python
146 lines
4.8 KiB
Python
"""Cross-check the ApsisRS link budget against the link equation.
|
|
|
|
Reads the scenarios written by ``cargo run -p apsis-rs --features rf
|
|
--example dump_link`` and recomputes each figure from the link equation in
|
|
plain Python, then reports the largest difference in each column.
|
|
|
|
The reference here is the link equation itself rather than a library, because
|
|
the equation is closed form and short enough to transcribe without
|
|
ambiguity. The constants are taken from the 2019 SI definitions, so they
|
|
match the values the Rust side uses.
|
|
|
|
The script needs no network access.
|
|
|
|
Usage:
|
|
|
|
pixi run python validation/python/link/check_link.py [path]
|
|
|
|
Exit status is zero when every figure agrees within tolerance and one
|
|
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
|
|
|
|
import csv
|
|
import math
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Defining constants, exact in the SI.
|
|
# 定义常量,在 SI 中精确。
|
|
BOLTZMANN = 1.380649e-23
|
|
SPEED_OF_LIGHT = 299792458.0
|
|
|
|
# Tolerance in decibels. The two sides evaluate the same expressions with
|
|
# double precision, so the only difference should be rounding.
|
|
# 容差,单位为分贝。两侧以双精度求相同表达式,故唯一差异应是舍入。
|
|
TOLERANCE_DB = 1e-9
|
|
|
|
DEFAULT_INPUT = Path("validation/data/expected/rust_link.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("transmit_dbw")]
|
|
|
|
|
|
def reference(row: dict[str, str]) -> dict[str, float]:
|
|
"""Recomputes a budget from the link equation.
|
|
|
|
由链路方程重算一份预算。
|
|
"""
|
|
transmit_dbw = float(row["transmit_dbw"])
|
|
transmit_dbi = float(row["transmit_dbi"])
|
|
transmit_loss = float(row["transmit_loss_db"])
|
|
receive_dbi = float(row["receive_dbi"])
|
|
receive_loss = float(row["receive_loss_db"])
|
|
temperature = float(row["temperature_k"])
|
|
bandwidth = float(row["bandwidth_hz"])
|
|
distance = float(row["distance_m"])
|
|
frequency = float(row["frequency_hz"])
|
|
|
|
eirp_dbw = transmit_dbw + transmit_dbi - transmit_loss
|
|
wavelength = SPEED_OF_LIGHT / frequency
|
|
path_loss_db = 20.0 * math.log10(4.0 * math.pi * distance / wavelength)
|
|
carrier_dbw = eirp_dbw - path_loss_db + receive_dbi - receive_loss
|
|
noise_density_dbw = 10.0 * math.log10(BOLTZMANN * temperature)
|
|
cn0_db = carrier_dbw - noise_density_dbw
|
|
cn_db = cn0_db - 10.0 * math.log10(bandwidth)
|
|
# The example reports the energy per bit ratio at a data rate equal to
|
|
# the bandwidth.
|
|
# 示例在数据速率等于带宽时报告每比特能量比。
|
|
ebn0_db = cn0_db - 10.0 * math.log10(bandwidth)
|
|
|
|
return {
|
|
"eirp_dbw": eirp_dbw,
|
|
"carrier_dbw": carrier_dbw,
|
|
"cn0_db": cn0_db,
|
|
"cn_db": cn_db,
|
|
"ebn0_db": ebn0_db,
|
|
}
|
|
|
|
|
|
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_link > " + 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
|
|
|
|
columns = ["eirp_dbw", "carrier_dbw", "cn0_db", "cn_db", "ebn0_db"]
|
|
worst = {column: 0.0 for column in columns}
|
|
worst_at = {column: None for column in columns}
|
|
|
|
for row in rows:
|
|
expected = reference(row)
|
|
for column in columns:
|
|
difference = abs(float(row[column]) - expected[column])
|
|
if difference > worst[column]:
|
|
worst[column] = difference
|
|
worst_at[column] = row["distance_m"]
|
|
|
|
print(f"scenarios compared: {len(rows)}")
|
|
for column in columns:
|
|
print(f"{column:>12}: worst {worst[column]:.3e} dB at distance {worst_at[column]} m")
|
|
print(f"{'tolerance':>12}: {TOLERANCE_DB:.3e} dB")
|
|
|
|
if all(value <= TOLERANCE_DB for value in worst.values()):
|
|
print("result: pass")
|
|
return 0
|
|
print("result: FAIL")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv))
|