From a2c2f5984a718c0bc342e8b79b97e9a4593890cd Mon Sep 17 00:00:00 2001 From: ApsisRS contributors Date: Fri, 2 Oct 2026 01:44:36 +0800 Subject: [PATCH] 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. --- .../apsis-frames/src/iau2000/eect_table.rs | 7 +++ .../src/iau2000/nutation_table.rs | 10 ++++ crates/rf/apsis-atmosphere/src/p839_table.rs | 7 +++ validation/python/time/check_iau2000.py | 43 +++++++++++++- validation/python/time/check_time.py | 47 ++++++++++++++- validation/tools/generate_eect_table.py | 41 +++++++++++-- validation/tools/generate_nutation_table.py | 57 ++++++++++++++++++- validation/tools/generate_p839_table.py | 22 +++++++ 8 files changed, 221 insertions(+), 13 deletions(-) diff --git a/crates/physics/apsis-frames/src/iau2000/eect_table.rs b/crates/physics/apsis-frames/src/iau2000/eect_table.rs index e8ea266..fb47d1c 100644 --- a/crates/physics/apsis-frames/src/iau2000/eect_table.rs +++ b/crates/physics/apsis-frames/src/iau2000/eect_table.rs @@ -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; diff --git a/crates/physics/apsis-frames/src/iau2000/nutation_table.rs b/crates/physics/apsis-frames/src/iau2000/nutation_table.rs index 4d1d8b0..2c9a000 100644 --- a/crates/physics/apsis-frames/src/iau2000/nutation_table.rs +++ b/crates/physics/apsis-frames/src/iau2000/nutation_table.rs @@ -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}; diff --git a/crates/rf/apsis-atmosphere/src/p839_table.rs b/crates/rf/apsis-atmosphere/src/p839_table.rs index 0370267..2967e52 100644 --- a/crates/rf/apsis-atmosphere/src/p839_table.rs +++ b/crates/rf/apsis-atmosphere/src/p839_table.rs @@ -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. /// diff --git a/validation/python/time/check_iau2000.py b/validation/python/time/check_iau2000.py index 69074c3..8a7e20c 100644 --- a/validation/python/time/check_iau2000.py +++ b/validation/python/time/check_iau2000.py @@ -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: diff --git a/validation/python/time/check_time.py b/validation/python/time/check_time.py index c736a41..81cc955 100644 --- a/validation/python/time/check_time.py +++ b/validation/python/time/check_time.py @@ -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 diff --git a/validation/tools/generate_eect_table.py b/validation/tools/generate_eect_table.py index b6c5bb8..b2ad0a9 100644 --- a/validation/tools/generate_eect_table.py +++ b/validation/tools/generate_eect_table.py @@ -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 diff --git a/validation/tools/generate_nutation_table.py b/validation/tools/generate_nutation_table.py index 06f674a..60b9d07 100644 --- a/validation/tools/generate_nutation_table.py +++ b/validation/tools/generate_nutation_table.py @@ -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]) diff --git a/validation/tools/generate_p839_table.py b/validation/tools/generate_p839_table.py index 2afbd02..c81fa91 100644 --- a/validation/tools/generate_p839_table.py +++ b/validation/tools/generate_p839_table.py @@ -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])