docs: bilingual comments for the validation tools

Also makes the table generators emit the bilingual module docs, so a
regenerated table matches the committed one.
This commit is contained in:
ApsisRS contributors 2026-10-02 01:44:36 +08:00
parent 1b0d288d73
commit a2c2f5984a
8 changed files with 221 additions and 13 deletions

View File

@ -6,6 +6,13 @@
//!
//! Source: the MHB2000 model, as distributed with the IAU SOFA library
//! and implemented by ERFA.
//!
//! 分点方程式的 IAU 2000 补充项。
//!
//! 该表由 `validation/tools/generate_eect_table.py` 从参考实现生成,
//! 原样再现,未经改动。
//!
//! 出处:MHB2000 模型,随 IAU SOFA 库分发并由 ERFA 实现。
use super::ComplementaryTerm;

View File

@ -10,6 +10,16 @@
//! Source: the MHB2000 model of Mathews, Herring and Buffett, adopted by
//! the International Astronomical Union in 2000 and distributed as part
//! of the IAU SOFA library, as implemented by ERFA.
//!
//! IAU 2000A 章动级数。
//!
//! 该表由 `validation/tools/generate_nutation_table.py` 从该模型的参考
//! 实现生成,原样再现,未经改动。
//!
//! 字段次序遵循参考实现,它并非论文中列出系数的次序。
//!
//! 出处:Mathews、Herring 与 Buffett 的 MHB2000 模型,2000 年由国际
//! 天文学联合会采纳,随 IAU SOFA 库分发,由 ERFA 实现。
use super::{PlanetaryTerm, SeriesTerm};

View File

@ -6,6 +6,13 @@
//!
//! Source: ITU-R Recommendation P.839-4, *Rain height model for prediction
//! methods*.
//!
//! ITU-R P.839-4 零度等温线高度表。
//!
//! 该表由 `validation/tools/generate_p839_table.py` 从随该建议书发布的
//! 数据生成,此处以米为单位的整数再现,这正是该建议书发布的分辨率。
//!
//! 出处:ITU-R 建议书 P.839-4,*Rain height model for prediction methods*。
/// The height of the zero degree isotherm, in metres.
///

View File

@ -13,6 +13,20 @@ Usage:
Exit status is zero when every quantity agrees within tolerance and one
otherwise.
用 ERFA 交叉核对 ApsisRS 的 IAU 2000/2006 地球定向。
读取由 ``cargo run -p apsis-rs --features physics --example dump_iau2000``
写出的量,并用 ERFA 逐个重算,后者是国际天文学联合会通过 SOFA 库及其
ERFA 衍生物分发的参考实现。
本脚本不需要网络访问。
用法:
pixi run python validation/python/time/check_iau2000.py [path]
每个量都在容差内一致时退出状态为零,否则为一。
"""
from __future__ import annotations
@ -35,13 +49,27 @@ import erfa
# `gst06a` is built on the non-rotating origin while the Rust value is built
# on the equinox. The two formulations agree to about two microarcseconds,
# which is 1e-11 radians, and that is what is allowed here.
#
# 容差。
#
# 章动、黄赤交角与岁差在双精度最后几位的水平上被复现,故它们以 1e-14
# 比较。
#
# 恒星时与各矩阵累积了若干次转动的双精度误差,故容许更大。
#
# 格林尼治视恒星时有其自己的容差,因为 ERFA 的 `gst06a` 建立在非旋转原点
# 之上,而 Rust 值建立在春分点之上。两种表述相符到约两微角秒,即 1e-11
# 弧度,此处容许的就是这个。
SCALAR_TOLERANCE = 1e-14
SIDEREAL_TOLERANCE = 1e-10
MATRIX_TOLERANCE = 1e-9
def tolerance_for(quantity: str) -> float:
"""Returns the tolerance that applies to a quantity."""
"""Returns the tolerance that applies to a quantity.
返回适用于某个量的容差。
"""
if quantity == "gast":
return SIDEREAL_TOLERANCE
if quantity in ("bpn", "c2t"):
@ -51,18 +79,25 @@ def tolerance_for(quantity: str) -> float:
DEFAULT_INPUT = Path("validation/data/expected/rust_iau2000.csv")
# The Terrestrial Time offset from UTC, in seconds, matching the Rust side.
# 地球时相对 UTC 的偏移,单位为秒,与 Rust 侧一致。
TT_MINUS_UTC_SECONDS = 32.184 + 37.0
def read_rows(path: Path) -> list[dict[str, str]]:
"""Reads the CSV rows."""
"""Reads the CSV rows.
读取 CSV 各行。
"""
with path.open(newline="") as handle:
reader = csv.DictReader(handle)
return [row for row in reader if row.get("mjd")]
def tt_julian_date(utc_mjd: float) -> float:
"""Returns the Terrestrial Time Julian date for a UTC Modified Julian Date."""
"""Returns the Terrestrial Time Julian date for a UTC Modified Julian Date.
返回某 UTC 约化儒略日对应的地球时儒略日。
"""
return utc_mjd + 2400000.5 + TT_MINUS_UTC_SECONDS / 86400.0
@ -122,6 +157,8 @@ def main(argv: list[str]) -> int:
elif quantity == "c2t":
# The equinox based chain: the bias, precession and nutation,
# then Greenwich apparent sidereal time, then the polar motion.
# 基于春分点的链条:先偏差、岁差与章动,再格林尼治视恒星时,
# 然后极移。
matrix = erfa.c2t06a(d1, d2, d1, d2, 0.0, 0.0)
reference = matrix[index // 3][index % 3]
else:

View File

@ -27,6 +27,29 @@ Usage:
Exit status is zero when every instant agrees within tolerance and one
otherwise.
交叉核对 ApsisRS 的时标换算。
读取由 ``cargo run -p apsis-rs --features physics --example dump_time`` 写出
的样本,并用 astropy 逐个重算瞬时,然后报告最大差异。
把某 UTC 瞬时写为浮点日有两种约定。本工作区用带阶跃偏移的线性约化儒略日,
故偏移 ``TAI - UTC`` 在闰秒处改变,其间恒定。Astropy 经 ERFA 用的是准儒略
日,其中含闰秒的一天有 86 401 秒长,故其 ``tai - utc`` 之差在这样一天中
斜坡变化。因此把两者作为儒略日之差比较,度量的是约定的差异而非数值误差。
本检查改为比较 TAI 瞬时。两侧被赋予相同的 UTC 日历瞬时,所得 TAI 约化
儒略日按两部分比较:整数日与小数,这避免了单个浮点日在接近 60 000 处会
遭受的精度损失。
本脚本不需要网络访问;它禁用 astropy 对地球定向数据的自动下载,这些换算
并不使用该数据。
用法:
pixi run python validation/python/time/check_time.py [path]
每个瞬时都在容差内一致时退出状态为零,否则为一。
"""
from __future__ import annotations
@ -38,25 +61,33 @@ from pathlib import Path
from astropy.time import Time
# The TAI instant is compared to one microsecond.
# TAI 瞬时比较到一微秒。
TOLERANCE_SECONDS = 1e-6
SECONDS_PER_DAY = 86400.0
# The Modified Julian Date scale starts half a day after the Julian Date
# scale, so this is the offset between the two day counts.
# 约化儒略日的起点比儒略日晚半天,故这是两种日计数之间的偏移。
MJD_OFFSET_FROM_JD = 2400000.5
DEFAULT_INPUT = Path("validation/data/expected/rust_time.csv")
def disable_remote_data() -> None:
"""Stops astropy from trying to download Earth orientation data."""
"""Stops astropy from trying to download Earth orientation data.
阻止 astropy 尝试下载地球定向数据。
"""
from astropy.utils import iers
iers.conf.auto_download = False
def read_rows(path: Path) -> tuple[list[dict[str, str]], dict[str, str]]:
"""Reads the CSV rows and the trailing comment metadata."""
"""Reads the CSV rows and the trailing comment metadata.
读取 CSV 各行以及末尾的注释元数据。
"""
metadata: dict[str, str] = {}
data_lines = []
with path.open(newline="") as handle:
@ -74,7 +105,10 @@ def read_rows(path: Path) -> tuple[list[dict[str, str]], dict[str, str]]:
def rust_tai_parts(row: dict[str, str]) -> tuple[int, float]:
"""Returns the Rust TAI instant as an integer day and a fraction."""
"""Returns the Rust TAI instant as an integer day and a fraction.
把 Rust 的 TAI 瞬时返回为整数日与小数。
"""
return int(row["tai_day"]), float(row["tai_frac"])
@ -83,6 +117,11 @@ def reference_tai_parts(row: dict[str, str]) -> tuple[float, float]:
The two part form of an astropy time keeps the day count in ``jd1`` and
the within-day offset in ``jd2``, so the subtraction below is exact.
把 astropy 的 TAI 瞬时返回为整数日与小数。
astropy 时间的两部分形式把日计数保存在 ``jd1``、把日内偏移保存在
``jd2``,故下面的减法精确。
"""
iso = (
f"{int(row['year']):04d}-{int(row['month']):02d}-{int(row['day']):02d}"
@ -137,6 +176,8 @@ def main(argv: list[str]) -> int:
# against the reference only at the points where the reference is
# constant in the workspace's sense, which the instant comparison
# above has already validated.
# 本工作区报告的偏移是阶跃函数。只在参考值于本工作区意义下恒定的点
# 处对它作核对,而上面的瞬时比较已验证了这些点。
rust_offset = float(row["tai_minus_utc"])
if abs(error) <= TOLERANCE_SECONDS and abs(rust_offset) > worst_offset:
worst_offset = rust_offset

View File

@ -15,6 +15,21 @@ Fetch the reference with:
Usage:
python validation/tools/generate_eect_table.py path/to/eect00.c
生成 IAU 2000 分点方程式补充项的 Rust 源码。
补充项是黄经章动不包含的、分点方程式的小周期附加项。它们来自与章动相同的
理论,为使分点方程式与章动级数一致所必需。
参考实现是 ERFA 库,它是 IAU SOFA 库的衍生物。
获取参考源码:
curl -O https://raw.githubusercontent.com/liberfa/erfa/master/src/eect00.c
用法:
python validation/tools/generate_eect_table.py path/to/eect00.c
"""
from __future__ import annotations
@ -25,7 +40,10 @@ from pathlib import Path
def extract_array(source: str, name: str) -> str:
"""Returns the body of a C array initializer."""
"""Returns the body of a C array initializer.
返回一个 C 数组初始化器的正文。
"""
start = source.index(name + "[] = {")
brace = source.index("{", start)
depth = 0
@ -42,13 +60,19 @@ def extract_array(source: str, name: str) -> str:
def parse_terms(body: str) -> list[tuple[str, str]]:
"""Returns the multiplier and coefficient groups of each term."""
"""Returns the multiplier and coefficient groups of each term.
返回每一项的乘数组与系数组。
"""
body = re.sub(r"/\*.*?\*/", "", body, flags=re.S)
return re.findall(r"\{\{([^{}]*)\},\s*([^}]*)\}", body)
def emit(name: str, doc: str, terms: list[tuple[str, str]]) -> None:
"""Prints one Rust constant."""
"""Prints one Rust constant.
打印一个 Rust 常量。
"""
print(doc)
print(f"pub static {name}: [ComplementaryTerm; {len(terms)}] = [")
for multipliers, coefficients in terms:
@ -78,17 +102,24 @@ def main(argv: list[str]) -> int:
print("//!")
print("//! Source: the MHB2000 model, as distributed with the IAU SOFA library")
print("//! and implemented by ERFA.")
print("//!")
print("//! 分点方程式的 IAU 2000 补充项。")
print("//!")
print("//! 该表由 `validation/tools/generate_eect_table.py` 从参考实现生成,")
print("//! 原样再现,未经改动。")
print("//!")
print("//! 出处:MHB2000 模型,随 IAU SOFA 库分发并由 ERFA 实现。")
print()
print("use super::ComplementaryTerm;")
print()
emit(
"CONSTANT_TERMS",
"/// The terms that do not depend on time.\n///\n/// The eight multipliers apply to the arguments `l`, `l'`, `F`, `D`, `Om`,\n/// the mean longitude of Venus, the mean longitude of the Earth, and the\n/// general accumulated precession, in that order.",
"/// The terms that do not depend on time.\n///\n/// The eight multipliers apply to the arguments `l`, `l'`, `F`, `D`, `Om`,\n/// the mean longitude of Venus, the mean longitude of the Earth, and the\n/// general accumulated precession, in that order.\n///\n/// 不依赖时间的各项。\n///\n/// 八个乘数按 `l`、`l'`、`F`、`D`、`Om`、金星的平黄经、地球的平黄经,以及\n/// 一般累积岁差的次序施加于这些参数。",
constant,
)
emit(
"LINEAR_TERMS",
"/// The terms whose amplitude grows linearly with time.\n///\n/// The arguments are the same as for the constant terms.",
"/// The terms whose amplitude grows linearly with time.\n///\n/// The arguments are the same as for the constant terms.\n///\n/// 幅度随时间线性增长的各项。\n///\n/// 参数与常数项相同。",
linear,
)
return 0

View File

@ -27,6 +27,31 @@ Fetch the reference with:
Usage:
python validation/tools/generate_nutation_table.py path/to/nut00a.c
生成 IAU 2000A 章动级数的 Rust 源码。
该级数即 MHB2000 模型。它有一个日月部分与一个行星部分,二者都是长长的
整数辐角乘数与实数系数表,故它们是机械地从参考实现转录而非手写。
参考实现是 ERFA 库,它是国际天文学联合会维护的 IAU SOFA 库的衍生物。其
源码是该级数的权威表述。
字段次序即参考实现所用者,它并非论文中列出系数的次序:
日月:乘数 l、l'、F、D、Om;然后 sp、spt、cp、ce、cet、se
行星:乘数 l、F、D、Om、Me、Ve、E、Ma、Ju、Sa、Ur、Ne、pa;
然后 sp、cp、se、ce
两个级数使用不同的辐角集合,且对黄赤交角使用不同的系数分组,故生成的
Rust 类型将它们分开。
获取参考源码:
curl -O https://raw.githubusercontent.com/liberfa/erfa/master/src/nut00a.c
用法:
python validation/tools/generate_nutation_table.py path/to/nut00a.c
"""
from __future__ import annotations
@ -37,7 +62,10 @@ from pathlib import Path
def extract_array(source: str, name: str) -> str:
"""Returns the body of a C array initializer."""
"""Returns the body of a C array initializer.
返回一个 C 数组初始化器的正文。
"""
start = source.index(name + "[] = {")
brace = source.index("{", start)
depth = 0
@ -54,7 +82,10 @@ def extract_array(source: str, name: str) -> str:
def parse_terms(body: str) -> list[list[str]]:
"""Returns the numeric terms of an array of brace groups."""
"""Returns the numeric terms of an array of brace groups.
返回一组花括号组的数组中的数值项。
"""
body = re.sub(r"/\*.*?\*/", "", body, flags=re.S)
terms = []
for group in re.findall(r"\{([^{}]*)\}", body):
@ -92,6 +123,16 @@ def main(argv: list[str]) -> int:
print("//! Source: the MHB2000 model of Mathews, Herring and Buffett, adopted by")
print("//! the International Astronomical Union in 2000 and distributed as part")
print("//! of the IAU SOFA library, as implemented by ERFA.")
print("//!")
print("//! IAU 2000A 章动级数。")
print("//!")
print("//! 该表由 `validation/tools/generate_nutation_table.py` 从该模型的参考")
print("//! 实现生成,原样再现,未经改动。")
print("//!")
print("//! 字段次序遵循参考实现,它并非论文中列出系数的次序。")
print("//!")
print("//! 出处:Mathews、Herring 与 Buffett 的 MHB2000 模型,2000 年由国际")
print("//! 天文学联合会采纳,随 IAU SOFA 库分发,由 ERFA 实现。")
print()
print("use super::{PlanetaryTerm, SeriesTerm};")
print()
@ -102,6 +143,12 @@ def main(argv: list[str]) -> int:
print("/// in that order. The coefficients are the longitude sine, its time")
print("/// derivative, the longitude cosine, the obliquity cosine, its time")
print("/// derivative, and the obliquity sine, in units of 0.1 microarcsecond.")
print("///")
print("/// 级数中的日月各项。")
print("///")
print("/// 各乘数按 `l`、`l'`、`F`、`D`、`Om` 的次序施加于这些参数。各系数依次是")
print("/// 黄经正弦、其时间导数、黄经余弦、黄赤交角余弦、其时间导数,以及黄赤交角")
print("/// 正弦,单位为 0.1 微角秒。")
print(f"pub static LUNI_SOLAR: [SeriesTerm; {len(lunisolar)}] = [")
for term in lunisolar:
multipliers = ", ".join(term[:5])
@ -124,6 +171,12 @@ def main(argv: list[str]) -> int:
print("/// coefficients are the longitude sine, the longitude cosine, the")
print("/// obliquity sine, and the obliquity cosine, in units of 0.1")
print("/// microarcsecond.")
print("///")
print("/// 级数中的行星各项。")
print("///")
print("/// 各乘数按 `l`、`F`、`D`、`Om`、水星到天王星的平黄经、海王星的平黄经,")
print("/// 以及一般累积岁差的次序施加于这些参数。各系数依次是黄经正弦、黄经余弦、")
print("/// 黄赤交角正弦与黄赤交角余弦,单位为 0.1 微角秒。")
print(f"pub static PLANETARY: [PlanetaryTerm; {len(planetary)}] = [")
for term in planetary:
multipliers = ", ".join(term[:13])

View File

@ -10,6 +10,16 @@ publishes.
Usage:
python validation/tools/generate_p839_table.py > /tmp/p839_table.rs
生成 ITU-R P.839-4 雨顶高度表的 Rust 源码。
该建议书把零度等温线高度给出为 1.5 度分辨率的全球列表网格。本脚本从
``itur`` 参考包读取该网格,并输出一个 Rust 模块,把它以十分之一千米的整数
嵌入,这正是该建议书发布的分辨率。
用法:
python validation/tools/generate_p839_table.py > /tmp/p839_table.rs
"""
from __future__ import annotations
@ -23,6 +33,7 @@ BASE = (
)
# The grid resolution, in degrees.
# 网格分辨率,单位为度。
STEP_DEGREES = 1.5
@ -36,11 +47,13 @@ def main() -> int:
# The grid is regular: latitude runs from 90 down to -90 and longitude
# from 0 to 360, both in steps of 1.5 degrees.
# 该网格是规则的:纬度自 90 降到 -90,经度自 0 到 360,步长均为 1.5 度。
assert np.allclose(latitudes[:, 0], np.linspace(90.0, -90.0, rows)), "latitude grid"
assert np.allclose(longitudes[0, :], np.linspace(0.0, 360.0, columns)), "longitude grid"
# The recommendation publishes the height to the nearest metre, so the
# table is stored in metres to avoid losing precision.
# 该建议书把高度发布到最近一米,故该表以米存储以避免损失精度。
metres = np.round(heights * 1000.0).astype(np.uint16)
print("/// The height of the zero degree isotherm, in metres.")
@ -53,6 +66,15 @@ def main() -> int:
print("///")
print("/// Source: ITU-R Recommendation P.839-4, *Rain height model for prediction")
print("/// methods*, which tabulates the isotherm height on this grid.")
print("///")
print("/// 零度等温线的高度,单位为米。")
print("///")
print("/// 网格在行方向上自北纬 90 度到南纬 90 度,在列方向上自东经 0 度到")
print("/// 360 度,步长均为 1.5 度,故有 121 行 241 列。分数纬度或经度处的值")
print("/// 由周围四点之间的双线性插值求得。")
print("///")
print("/// 出处:ITU-R 建议书 P.839-4,*Rain height model for prediction methods*,")
print("/// 它在该网格上列出等温线高度。")
print(f"pub static ZERO_ISOTHERM_HEIGHT_METRES: [[u16; {columns}]; {rows}] = [")
for row in range(rows):
values = ", ".join(str(int(value)) for value in metres[row])