docs: bilingual comments for the foundation and physics tests

This commit is contained in:
ApsisRS contributors 2026-10-02 01:55:40 +08:00
parent a4f5a30aa9
commit 1ffba97a14
24 changed files with 191 additions and 0 deletions

View File

@ -1,4 +1,6 @@
//! Tests for the physical and astronomical constants. //! Tests for the physical and astronomical constants.
//!
//! 物理与天文常量的测试。
use apsis_core::constant::*; use apsis_core::constant::*;
@ -23,6 +25,7 @@ fn time_constants_are_consistent() {
#[test] #[test]
fn j2000_minus_unix_epoch_is_the_expected_span() { fn j2000_minus_unix_epoch_is_the_expected_span() {
// The two epochs are 10 957 days apart. // The two epochs are 10 957 days apart.
// 这两个历元相隔 10 957 天。
assert_eq!(JULIAN_DATE_J2000 - JULIAN_DATE_UNIX_EPOCH, 10_957.5); assert_eq!(JULIAN_DATE_J2000 - JULIAN_DATE_UNIX_EPOCH, 10_957.5);
assert_eq!(SECONDS_J2000_TO_UNIX, 10_957.5 * DAY); assert_eq!(SECONDS_J2000_TO_UNIX, 10_957.5 * DAY);
assert_eq!(SECONDS_J2000_TO_UNIX, 946_728_000.0); assert_eq!(SECONDS_J2000_TO_UNIX, 946_728_000.0);

View File

@ -1,4 +1,6 @@
//! Tests for the workspace error type and result alias. //! Tests for the workspace error type and result alias.
//!
//! 工作区错误类型与结果别名的测试。
use apsis_core::{Error, ErrorKind, Result}; use apsis_core::{Error, ErrorKind, Result};
use core::fmt; use core::fmt;

View File

@ -1,4 +1,6 @@
//! Tests for the unit and quantity marker traits. //! Tests for the unit and quantity marker traits.
//!
//! 单位与量标记 trait 的测试。
use apsis_core::{Quantity, Unit}; use apsis_core::{Quantity, Unit};
use core::fmt; use core::fmt;

View File

@ -3,11 +3,18 @@
//! These tests record what `macro_rules!` can express for the workspace. They //! These tests record what `macro_rules!` can express for the workspace. They
//! are kept as executable documentation of the decision to leave this crate //! are kept as executable documentation of the decision to leave this crate
//! empty for now: see the crate documentation. //! empty for now: see the crate documentation.
//!
//! 内建宏设施探查。
//!
//! 这些测试记录 `macro_rules!` 能为工作区表达什么。它们被保留为「目前让本 crate
//! 保持为空」这一决定的可执行文档:见 crate 文档。
#[test] #[test]
fn builtin_macros_can_substitute_a_type_and_a_literal() { fn builtin_macros_can_substitute_a_type_and_a_literal() {
// A `macro_rules!` macro can paste a type and a literal together, which // A `macro_rules!` macro can paste a type and a literal together, which
// is the only form the workspace needs. // is the only form the workspace needs.
// `macro_rules!` 宏可以把一个类型与一个字面量拼在一起,这是本工作区唯一需要的
// 形式。
macro_rules! quantity { macro_rules! quantity {
($quantity:ty, $value:literal) => { ($quantity:ty, $value:literal) => {
<$quantity>::new($value) <$quantity>::new($value)
@ -15,6 +22,7 @@ fn builtin_macros_can_substitute_a_type_and_a_literal() {
} }
// `f64` has no `new`, so the probe uses a type that does. // `f64` has no `new`, so the probe uses a type that does.
// `f64` 没有 `new`,故本探查用一个有的类型。
struct Meters(f64); struct Meters(f64);
impl Meters { impl Meters {
const fn new(value: f64) -> Self { const fn new(value: f64) -> Self {

View File

@ -1,4 +1,6 @@
//! Tests for three by three matrices. //! Tests for three by three matrices.
//!
//! 三阶矩阵的测试。
use apsis_math::{Mat3, Quat, Vec3}; use apsis_math::{Mat3, Quat, Vec3};
use core::f64::consts::FRAC_PI_2; use core::f64::consts::FRAC_PI_2;
@ -133,6 +135,7 @@ fn is_rotation_rejects_non_rotations() {
assert!(!Mat3::ZERO.is_rotation(1e-9)); assert!(!Mat3::ZERO.is_rotation(1e-9));
assert!(!Mat3::from_diagonal(Vec3::splat(2.0)).is_rotation(1e-9)); assert!(!Mat3::from_diagonal(Vec3::splat(2.0)).is_rotation(1e-9));
// A reflection has determinant minus one. // A reflection has determinant minus one.
// 反射的行列式为负一。
assert!(!Mat3::from_diagonal(Vec3::new(1.0, 1.0, -1.0)).is_rotation(1e-9)); assert!(!Mat3::from_diagonal(Vec3::new(1.0, 1.0, -1.0)).is_rotation(1e-9));
} }

View File

@ -6,6 +6,12 @@
//! sequential answer rather than against a constant, and forces both the //! sequential answer rather than against a constant, and forces both the
//! single and multi thread paths by choosing lengths that fall on each side //! single and multi thread paths by choosing lengths that fall on each side
//! of the threshold. //! of the threshold.
//!
//! 作用域并行辅助函数的测试。
//!
//! 核心性质是并行运行与串行运行产生相同的值,因为这些辅助函数的全部意义就是作为
//! 它们所替代的顺序循环的替代品。故每个测试都与顺序答案比较而非与常量比较,并
//! 通过选取落在阈值两侧的长度,强制走过单线程与多线程两条路径。
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
@ -20,6 +26,7 @@ fn the_thread_count_is_at_least_one() {
fn small_work_uses_a_single_thread() { fn small_work_uses_a_single_thread() {
// Below the per thread threshold there is nothing to gain from starting // Below the per thread threshold there is nothing to gain from starting
// a thread, so the helper says so. // a thread, so the helper says so.
// 低于每线程阈值时,启动线程没有任何收益,故该辅助函数如此表示。
assert_eq!(threads_for(0), 1); assert_eq!(threads_for(0), 1);
assert_eq!(threads_for(1), 1); assert_eq!(threads_for(1), 1);
assert_eq!(threads_for(64), 1); assert_eq!(threads_for(64), 1);
@ -31,6 +38,7 @@ fn large_work_uses_more_than_one_thread() {
if count > 1 { if count > 1 {
assert!(threads_for(1_000_000) > 1); assert!(threads_for(1_000_000) > 1);
// Never more threads than there are items. // Never more threads than there are items.
// 线程数绝不超过项数。
assert!(threads_for(10) <= 10); assert!(threads_for(10) <= 10);
} }
} }
@ -67,6 +75,7 @@ fn the_parallel_result_matches_the_sequential_one() {
fn the_output_is_in_index_order() { fn the_output_is_in_index_order() {
// Threads finish out of order, so this checks that the helper restores // Threads finish out of order, so this checks that the helper restores
// index order rather than completion order. // index order rather than completion order.
// 各线程完成次序不定,故这检查该辅助函数恢复的是下标次序而非完成次序。
let len = 200_000; let len = 200_000;
let values = map_index(len, |index| index); let values = map_index(len, |index| index);
for (position, value) in values.iter().enumerate() { for (position, value) in values.iter().enumerate() {
@ -79,6 +88,8 @@ fn a_closure_may_capture_shared_input() {
// The shape every caller in the workspace uses: read a shared slice and // The shape every caller in the workspace uses: read a shared slice and
// produce one value per element. `map_index` owns the output buffer, so // produce one value per element. `map_index` owns the output buffer, so
// no shared mutable state is needed. // no shared mutable state is needed.
// 本工作区中每个调用方所用的形状:读取共享切片并为每个元素产生一个值。
// `map_index` 拥有输出缓冲,故无需共享可变状态。
let input: Vec<f64> = (0..100_000).map(f64::from).collect(); let input: Vec<f64> = (0..100_000).map(f64::from).collect();
let output = map_index(input.len(), |index| input[index] * input[index]); let output = map_index(input.len(), |index| input[index] * input[index]);
for index in [0, 1, 999, 50_000, 99_999] { for index in [0, 1, 999, 50_000, 99_999] {
@ -90,6 +101,8 @@ fn a_closure_may_capture_shared_input() {
fn a_closure_may_accumulate_atomically() { fn a_closure_may_accumulate_atomically() {
// Writing to a shared counter needs atomics; this checks the helper // Writing to a shared counter needs atomics; this checks the helper
// tolerates that, since summing a large range is a common pattern. // tolerates that, since summing a large range is a common pattern.
// 写共享计数器需要原子操作;这检查该辅助函数容忍它,因为对大区间求和是常见
// 模式。
use std::sync::atomic::AtomicU64; use std::sync::atomic::AtomicU64;
let total = AtomicU64::new(0); let total = AtomicU64::new(0);
let len = 200_000_u64; let len = 200_000_u64;
@ -105,6 +118,7 @@ fn an_empty_range_does_nothing() {
let empty: Vec<f64> = map_index(0, |_| 1.0); let empty: Vec<f64> = map_index(0, |_| 1.0);
assert!(empty.is_empty()); assert!(empty.is_empty());
// The index form accepts an empty range without spawning anything. // The index form accepts an empty range without spawning anything.
// 索引形式接受空区间而不创建任何线程。
for_each_index(0, |_| unreachable!("an empty range runs no closure")); for_each_index(0, |_| unreachable!("an empty range runs no closure"));
} }
@ -112,6 +126,7 @@ fn an_empty_range_does_nothing() {
fn a_range_that_does_not_divide_evenly_covers_everything() { fn a_range_that_does_not_divide_evenly_covers_everything() {
// A length that is not a multiple of the thread count is where an // A length that is not a multiple of the thread count is where an
// off by one in the chunking would drop the tail. // off by one in the chunking would drop the tail.
// 长度不是线程数的整数倍之处,正是分块中差一会丢掉尾部的地方。
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
for &len in &[1_001_usize, 4_097, 65_537, 100_003] { for &len in &[1_001_usize, 4_097, 65_537, 100_003] {
let marks = (0..len).map(|_| AtomicUsize::new(0)).collect::<Vec<_>>(); let marks = (0..len).map(|_| AtomicUsize::new(0)).collect::<Vec<_>>();
@ -129,6 +144,7 @@ fn a_range_that_does_not_divide_evenly_covers_everything() {
fn values_of_a_non_copy_type_survive_the_collect() { fn values_of_a_non_copy_type_survive_the_collect() {
// A string is not `Copy`, so this exercises the unwrap-and-move path that // A string is not `Copy`, so this exercises the unwrap-and-move path that
// the parallel branch uses. // the parallel branch uses.
// 字符串不是 `Copy`,故这检验并行分支所用的解包并移动路径。
let values = map_index(10_000, |index| format!("item-{index}")); let values = map_index(10_000, |index| format!("item-{index}"));
assert_eq!(values[0], "item-0"); assert_eq!(values[0], "item-0");
assert_eq!(values[9_999], "item-9999"); assert_eq!(values[9_999], "item-9999");

View File

@ -1,4 +1,6 @@
//! Tests for unit quaternion rotations. //! Tests for unit quaternion rotations.
//!
//! 单位四元数转动的测试。
use apsis_math::{Mat3, Quat, Vec3}; use apsis_math::{Mat3, Quat, Vec3};
use core::f64::consts::{FRAC_PI_2, PI}; use core::f64::consts::{FRAC_PI_2, PI};
@ -95,6 +97,7 @@ fn multiplication_composes_right_to_left() {
let second = Quat::from_rotation_x(FRAC_PI_2); let second = Quat::from_rotation_x(FRAC_PI_2);
let v = Vec3::new(1.0, 0.0, 0.0); let v = Vec3::new(1.0, 0.0, 0.0);
// `second * first` applies `first` first, then `second`. // `second * first` applies `first` first, then `second`.
// `second * first` 先施加 `first`,再施加 `second`。
assert_close((second * first).rotate(v), second.rotate(first.rotate(v))); assert_close((second * first).rotate(v), second.rotate(first.rotate(v)));
} }
@ -134,6 +137,7 @@ fn from_mat3_recovers_the_original_rotation() {
let q = Quat::from_axis_angle(Vec3::new(0.3, -0.4, 0.5), 1.1).expect("non-zero axis"); let q = Quat::from_axis_angle(Vec3::new(0.3, -0.4, 0.5), 1.1).expect("non-zero axis");
let recovered = Quat::from_mat3(Mat3::from_quat(q)).expect("rotation matrix is accepted"); let recovered = Quat::from_mat3(Mat3::from_quat(q)).expect("rotation matrix is accepted");
// `q` and `-q` are the same rotation, so compare through their effect. // `q` and `-q` are the same rotation, so compare through their effect.
// `q` 与 `-q` 是同一转动,故通过其效果比较。
let v = Vec3::new(1.0, -2.0, 3.0); let v = Vec3::new(1.0, -2.0, 3.0);
assert_close(recovered.rotate(v), q.rotate(v)); assert_close(recovered.rotate(v), q.rotate(v));
} }
@ -142,6 +146,7 @@ fn from_mat3_recovers_the_original_rotation() {
fn from_mat3_covers_every_branch() { fn from_mat3_covers_every_branch() {
// These angles exercise the trace branch and each of the three // These angles exercise the trace branch and each of the three
// largest-diagonal branches. // largest-diagonal branches.
// 这些角检验迹分支以及三个最大对角分支中的每一个。
for (axis, angle) in [ for (axis, angle) in [
(Vec3::new(1.0, 0.0, 0.0), 0.2), (Vec3::new(1.0, 0.0, 0.0), 0.2),
(Vec3::new(1.0, 0.0, 0.0), PI - 0.1), (Vec3::new(1.0, 0.0, 0.0), PI - 0.1),
@ -177,6 +182,7 @@ fn slerp_selects_the_short_arc_when_endpoints_are_negated() {
let end = Quat::new(-1.0, 0.0, 0.0, 0.0); let end = Quat::new(-1.0, 0.0, 0.0, 0.0);
// The two quaternions describe the same rotation, so any // The two quaternions describe the same rotation, so any
// interpolation between them must stay at the identity. // interpolation between them must stay at the identity.
// 这两个四元数描述同一转动,故它们之间的任何插值都必须保持为恒等。
let middle = start.slerp(end, 0.5); let middle = start.slerp(end, 0.5);
assert_close(middle.rotate(Vec3::X), Vec3::X); assert_close(middle.rotate(Vec3::X), Vec3::X);
} }

View File

@ -1,4 +1,6 @@
//! Tests for three component vectors. //! Tests for three component vectors.
//!
//! 三分量矢量的测试。
use apsis_math::Vec3; use apsis_math::Vec3;
@ -45,6 +47,7 @@ fn cross_product_follows_the_right_hand_rule() {
assert_eq!(Vec3::Y.cross(Vec3::Z), Vec3::X); assert_eq!(Vec3::Y.cross(Vec3::Z), Vec3::X);
assert_eq!(Vec3::Z.cross(Vec3::X), Vec3::Y); assert_eq!(Vec3::Z.cross(Vec3::X), Vec3::Y);
// The cross product of parallel vectors vanishes. // The cross product of parallel vectors vanishes.
// 平行矢量的叉积为零。
assert_eq!(Vec3::X.cross(Vec3::X), Vec3::ZERO); assert_eq!(Vec3::X.cross(Vec3::X), Vec3::ZERO);
} }

View File

@ -1,4 +1,6 @@
//! Tests for plane angle quantities. //! Tests for plane angle quantities.
//!
//! 平面角量的测试。
use apsis_core::{Quantity, Unit}; use apsis_core::{Quantity, Unit};
use apsis_units::dimension::Angle; use apsis_units::dimension::Angle;
@ -35,6 +37,7 @@ fn wrap_places_angles_in_the_canonical_range() {
assert!(wrapped.value() < PI); assert!(wrapped.value() < PI);
// Wrapping shifts by whole turns, so the angle is preserved // Wrapping shifts by whole turns, so the angle is preserved
// modulo two pi. // modulo two pi.
// 环绕按整周平移,故该角在模 2 pi 的意义下保持。
let difference = angle.value() - wrapped.value(); let difference = angle.value() - wrapped.value();
assert!((difference / (2.0 * PI)).fract().abs() < 1e-12); assert!((difference / (2.0 * PI)).fract().abs() < 1e-12);
} }

View File

@ -1,4 +1,6 @@
//! Tests for frequency quantities. //! Tests for frequency quantities.
//!
//! 频率量的测试。
use apsis_core::Unit; use apsis_core::Unit;
use apsis_units::Hertz; use apsis_units::Hertz;

View File

@ -1,4 +1,6 @@
//! Tests for length quantities. //! Tests for length quantities.
//!
//! 长度量的测试。
use apsis_core::{Quantity, Unit}; use apsis_core::{Quantity, Unit};
use apsis_units::dimension::Length; use apsis_units::dimension::Length;
@ -38,6 +40,7 @@ fn units_report_their_metadata() {
assert_eq!(Kilometers::factor_to_base(), 1_000.0); assert_eq!(Kilometers::factor_to_base(), 1_000.0);
// Both units measure the same dimension, which is what makes the // Both units measure the same dimension, which is what makes the
// generic conversion above type check. // generic conversion above type check.
// 两种单位测量同一量纲,这正是使上面的泛型换算通过类型检查的原因。
fn assert_length<U: Unit<Dimension = Length>>() {} fn assert_length<U: Unit<Dimension = Length>>() {}
assert_length::<Meters>(); assert_length::<Meters>();
assert_length::<Kilometers>(); assert_length::<Kilometers>();

View File

@ -1,4 +1,6 @@
//! Tests for thermodynamic temperature quantities. //! Tests for thermodynamic temperature quantities.
//!
//! 热力学温度量的测试。
use apsis_core::Unit; use apsis_core::Unit;
use apsis_core::constant::ZERO_CELSIUS_IN_KELVIN; use apsis_core::constant::ZERO_CELSIUS_IN_KELVIN;

View File

@ -1,4 +1,6 @@
//! Tests for the duration quantity. //! Tests for the duration quantity.
//!
//! 时长量的测试。
use apsis_core::Unit; use apsis_core::Unit;
use apsis_units::Seconds; use apsis_units::Seconds;

View File

@ -1,4 +1,6 @@
//! Tests for the frame carrying position types. //! Tests for the frame carrying position types.
//!
//! 携带坐标架的位置类型的测试。
use apsis_coord::{EarthRotationAngle, Ecef, Eci, Geodetic, WGS84}; use apsis_coord::{EarthRotationAngle, Ecef, Eci, Geodetic, WGS84};
use apsis_math::Vec3; use apsis_math::Vec3;
@ -67,6 +69,7 @@ fn frame_conversion_preserves_length() {
fn a_ground_point_rotates_with_the_earth() { fn a_ground_point_rotates_with_the_earth() {
// A point on the equator at zero longitude in the Earth-fixed frame is // A point on the equator at zero longitude in the Earth-fixed frame is
// rotated by the Earth Rotation Angle in the inertial frame. // rotated by the Earth Rotation Angle in the inertial frame.
// 地固系中赤道上零经度的一个点,在惯性系中按地球自转角被转动。
let angle = EarthRotationAngle::from_ut1_days(1.5); let angle = EarthRotationAngle::from_ut1_days(1.5);
let ground = Ecef::new(Vec3::new(6_378_137.0, 0.0, 0.0)); let ground = Ecef::new(Vec3::new(6_378_137.0, 0.0, 0.0));
let inertial = ground.to_eci(angle); let inertial = ground.to_eci(angle);
@ -83,6 +86,7 @@ fn a_ground_point_rotates_with_the_earth() {
fn a_geodetic_station_round_trips_through_both_frames() { fn a_geodetic_station_round_trips_through_both_frames() {
// A station is fixed to the ground, so converting it to the inertial // A station is fixed to the ground, so converting it to the inertial
// frame and back at the same angle returns the original position. // frame and back at the same angle returns the original position.
// 地面站固定于地面,故把它转入惯性系再以相同角度转回,得到原位置。
let station = Geodetic::new(0.662, 0.175, 80.0); let station = Geodetic::new(0.662, 0.175, 80.0);
let angle = EarthRotationAngle::from_ut1_julian_date(2_460_000.5); let angle = EarthRotationAngle::from_ut1_julian_date(2_460_000.5);
let inertial = station.to_ecef().to_eci(angle); let inertial = station.to_ecef().to_eci(angle);

View File

@ -1,4 +1,6 @@
//! Tests for the Earth Rotation Angle and the frame rotations. //! Tests for the Earth Rotation Angle and the frame rotations.
//!
//! 地球自转角与坐标架转动的测试。
use apsis_frames::{ use apsis_frames::{
ERA_RATE_TURNS_PER_DAY, EarthRotationAngle, rotation_ecef_to_eci, rotation_eci_to_ecef, ERA_RATE_TURNS_PER_DAY, EarthRotationAngle, rotation_ecef_to_eci, rotation_eci_to_ecef,
@ -13,6 +15,7 @@ const EPS: f64 = 1e-12;
fn rate_matches_the_published_value() { fn rate_matches_the_published_value() {
// 1.002 737 811 911 354 6 turns per day is 360.985 612 288 088 degrees // 1.002 737 811 911 354 6 turns per day is 360.985 612 288 088 degrees
// per day. // per day.
// 每天 1.002 737 811 911 354 6 周即每天 360.985 612 288 088 度。
assert!((ERA_RATE_TURNS_PER_DAY - 1.002_737_811_911_354_6).abs() < 1e-15); assert!((ERA_RATE_TURNS_PER_DAY - 1.002_737_811_911_354_6).abs() < 1e-15);
let degrees = ERA_RATE_TURNS_PER_DAY * 360.0; let degrees = ERA_RATE_TURNS_PER_DAY * 360.0;
assert!( assert!(
@ -28,6 +31,7 @@ fn angle_advances_by_the_rate_each_day() {
assert!((second.turns() - first.turns() - ERA_RATE_TURNS_PER_DAY).abs() < 1e-15); assert!((second.turns() - first.turns() - ERA_RATE_TURNS_PER_DAY).abs() < 1e-15);
// The angle advances a little under one degree per day more than a // The angle advances a little under one degree per day more than a
// full turn. // full turn.
// 该角每天推进一整圈再多略小于一度。
let advance_degrees = (second.turns() - first.turns()) * 360.0 - 360.0; let advance_degrees = (second.turns() - first.turns()) * 360.0 - 360.0;
assert!((advance_degrees - 0.985_612_288_088).abs() < 1e-9); assert!((advance_degrees - 0.985_612_288_088).abs() < 1e-9);
} }
@ -36,6 +40,7 @@ fn angle_advances_by_the_rate_each_day() {
fn epoch_angle_matches_the_published_constant() { fn epoch_angle_matches_the_published_constant() {
// At J2000.0 the angle is 0.779 057 273 2640 turns, about 280.46 // At J2000.0 the angle is 0.779 057 273 2640 turns, about 280.46
// degrees. // degrees.
// 在 J2000.0 处该角为 0.779 057 273 2640 周,约 280.46 度。
let epoch = EarthRotationAngle::from_ut1_days(0.0); let epoch = EarthRotationAngle::from_ut1_days(0.0);
assert!((epoch.turns() - 0.779_057_273_264).abs() < 1e-15); assert!((epoch.turns() - 0.779_057_273_264).abs() < 1e-15);
let degrees = epoch.turns_positive() * 360.0; let degrees = epoch.turns_positive() * 360.0;
@ -45,6 +50,7 @@ fn epoch_angle_matches_the_published_constant() {
#[test] #[test]
fn julian_date_and_day_count_agree() { fn julian_date_and_day_count_agree() {
// J2000.0 is Julian Date 2451545.0. // J2000.0 is Julian Date 2451545.0.
// J2000.0 是儒略日 2451545.0。
assert_eq!(ut1_days_from_j2000(2_451_545.0), 0.0); assert_eq!(ut1_days_from_j2000(2_451_545.0), 0.0);
assert_eq!(ut1_days_from_j2000(2_451_546.0), 1.0); assert_eq!(ut1_days_from_j2000(2_451_546.0), 1.0);
let via_date = EarthRotationAngle::from_ut1_julian_date(2_451_546.0); let via_date = EarthRotationAngle::from_ut1_julian_date(2_451_546.0);
@ -68,6 +74,7 @@ fn positive_variants_reduce_to_the_principal_interval() {
assert!((0.0..1.0).contains(&turns), "turns out of range: {turns}"); assert!((0.0..1.0).contains(&turns), "turns out of range: {turns}");
// Reduction removes whole turns, so the angle is unchanged modulo // Reduction removes whole turns, so the angle is unchanged modulo
// one turn. // one turn.
// 归约去掉整周,故该角在模一周的意义下不变。
let difference = angle.turns() - turns; let difference = angle.turns() - turns;
assert!((difference - difference.round()).abs() < 1e-9); assert!((difference - difference.round()).abs() < 1e-9);
@ -88,6 +95,8 @@ fn a_positive_angle_drifts_inertial_directions_westward() {
// The Earth turns eastward, so a direction fixed in inertial space // The Earth turns eastward, so a direction fixed in inertial space
// drifts westward when read in the Earth-fixed frame. In the // drifts westward when read in the Earth-fixed frame. In the
// Earth-fixed frame that is toward negative second axis. // Earth-fixed frame that is toward negative second axis.
// 地球向东转,故在惯性空间中固定的方向在地固系中读数时向西漂移。在地固系中
// 即朝负第二轴方向。
let angle = EarthRotationAngle::from_radians(0.25); let angle = EarthRotationAngle::from_radians(0.25);
let rotation = rotation_eci_to_ecef(angle); let rotation = rotation_eci_to_ecef(angle);
let rotated = rotation * Vec3::X; let rotated = rotation * Vec3::X;
@ -103,6 +112,7 @@ fn opposite_rotations_cancel() {
let identity = forward * backward; let identity = forward * backward;
assert!(identity.is_rotation(1e-12)); assert!(identity.is_rotation(1e-12));
// Applying one after the other returns the original vector. // Applying one after the other returns the original vector.
// 先后施加二者返回原矢量。
let vector = Vec3::new(1.5, -2.5, 3.5); let vector = Vec3::new(1.5, -2.5, 3.5);
assert!((backward * (forward * vector) - vector).norm() < EPS); assert!((backward * (forward * vector) - vector).norm() < EPS);
} }
@ -118,6 +128,7 @@ fn earth_rotation_turns_the_third_axis_into_itself() {
fn rotation_carries_the_second_axis_toward_the_first() { fn rotation_carries_the_second_axis_toward_the_first() {
// The companion to the westward drift test: the second axis of the // The companion to the westward drift test: the second axis of the
// inertial frame maps toward the first axis of the Earth-fixed frame. // inertial frame maps toward the first axis of the Earth-fixed frame.
// 向西漂移测试的对应项:惯性系的第二轴映射向地固系的第一轴。
let angle = EarthRotationAngle::from_radians(0.25); let angle = EarthRotationAngle::from_radians(0.25);
let rotation = rotation_eci_to_ecef(angle); let rotation = rotation_eci_to_ecef(angle);
let rotated = rotation * Vec3::Y; let rotated = rotation * Vec3::Y;
@ -129,6 +140,7 @@ fn rotation_carries_the_second_axis_toward_the_first() {
fn a_full_turn_returns_to_the_same_rotation() { fn a_full_turn_returns_to_the_same_rotation() {
// The rotation depends on the angle modulo one turn, so angles that // The rotation depends on the angle modulo one turn, so angles that
// differ by whole turns give the same matrix. // differ by whole turns give the same matrix.
// 该转动依模一周的角而定,故相差整周的角给出相同的矩阵。
let first = rotation_eci_to_ecef(EarthRotationAngle::from_turns(0.25)); let first = rotation_eci_to_ecef(EarthRotationAngle::from_turns(0.25));
let second = rotation_eci_to_ecef(EarthRotationAngle::from_turns(2.25)); let second = rotation_eci_to_ecef(EarthRotationAngle::from_turns(2.25));
let difference = first * second.transpose(); let difference = first * second.transpose();
@ -141,6 +153,7 @@ fn a_full_turn_returns_to_the_same_rotation() {
fn identity_angle_leaves_inertial_vectors_unchanged() { fn identity_angle_leaves_inertial_vectors_unchanged() {
// An angle of zero, constructed directly rather than from the epoch // An angle of zero, constructed directly rather than from the epoch
// constant, is the identity rotation. // constant, is the identity rotation.
// 直接构建的(而非由历元常量得来的)零角是恒等转动。
let rotation = rotation_eci_to_ecef(EarthRotationAngle::from_turns(0.0)); let rotation = rotation_eci_to_ecef(EarthRotationAngle::from_turns(0.0));
let vector = Vec3::new(1.0, 2.0, 3.0); let vector = Vec3::new(1.0, 2.0, 3.0);
assert!((rotation * vector - vector).norm() < EPS); assert!((rotation * vector - vector).norm() < EPS);

View File

@ -4,6 +4,11 @@
//! derivative for the same instants. They are recorded here so that a change //! derivative for the same instants. They are recorded here so that a change
//! to a coefficient or a rotation convention shows up as a failing //! to a coefficient or a rotation convention shows up as a failing
//! assertion. //! assertion.
//!
//! IAU 2000/2006 地球定向模型的测试。
//!
//! 期望值是 IAU SOFA 库及其 ERFA 衍生物对相同瞬时的值。它们被记录在此,使对某个
//! 系数或转动约定的改动表现为断言失败。
use apsis_frames::iau2000::{ use apsis_frames::iau2000::{
PolarMotion, bias_precession_nutation_matrix, celestial_to_terrestrial_matrix, PolarMotion, bias_precession_nutation_matrix, celestial_to_terrestrial_matrix,
@ -14,10 +19,14 @@ use apsis_frames::iau2000::{
use core::f64::consts::PI; use core::f64::consts::PI;
/// Converts arcseconds to radians. /// Converts arcseconds to radians.
///
/// 把角秒换算为弧度。
const ARCSECOND: f64 = PI / (180.0 * 3_600.0); const ARCSECOND: f64 = PI / (180.0 * 3_600.0);
/// Returns the Terrestrial Time Julian date for a Modified Julian Date on /// Returns the Terrestrial Time Julian date for a Modified Julian Date on
/// the UTC scale, using the offsets in force from 2017. /// the UTC scale, using the offsets in force from 2017.
///
/// 使用自 2017 年起生效的偏移,返回 UTC 时标上某约化儒略日对应的地球时儒略日。
fn tt_julian_date(utc_mjd: f64) -> f64 { fn tt_julian_date(utc_mjd: f64) -> f64 {
utc_mjd + 2_400_000.5 + 69.184 / 86_400.0 utc_mjd + 2_400_000.5 + 69.184 / 86_400.0
} }
@ -25,6 +34,7 @@ fn tt_julian_date(utc_mjd: f64) -> f64 {
#[test] #[test]
fn the_mean_obliquity_at_j2000_matches_the_reference() { fn the_mean_obliquity_at_j2000_matches_the_reference() {
// The IAU 2006 obliquity at J2000.0 is 84381.406 arcseconds. // The IAU 2006 obliquity at J2000.0 is 84381.406 arcseconds.
// J2000.0 处的 IAU 2006 黄赤交角为 84381.406 角秒。
let obliquity = mean_obliquity(0.0); let obliquity = mean_obliquity(0.0);
assert!( assert!(
(obliquity / ARCSECOND - 84_381.406).abs() < 1e-9, (obliquity / ARCSECOND - 84_381.406).abs() < 1e-9,
@ -36,6 +46,7 @@ fn the_mean_obliquity_at_j2000_matches_the_reference() {
#[test] #[test]
fn the_nutation_at_j2000_matches_the_reference() { fn the_nutation_at_j2000_matches_the_reference() {
// The IAU 2000A nutation at J2000.0, from ERFA. // The IAU 2000A nutation at J2000.0, from ERFA.
// J2000.0 处的 IAU 2000A 章动,取自 ERFA。
let t = 0.0; let t = 0.0;
let nutation = nutation_2000a(t); let nutation = nutation_2000a(t);
assert!( assert!(
@ -54,6 +65,7 @@ fn the_nutation_at_j2000_matches_the_reference() {
fn the_nutation_2006_adjustment_is_a_scaling() { fn the_nutation_2006_adjustment_is_a_scaling() {
// The IAU 2006 adjustment scales the IAU 2000A nutation rather than // The IAU 2006 adjustment scales the IAU 2000A nutation rather than
// adding an independent term, so the two must stay in proportion. // adding an independent term, so the two must stay in proportion.
// IAU 2006 调整是对 IAU 2000A 章动缩放而非加上独立项,故二者必须保持比例。
for t in [0.0, 0.5, 1.0, 2.0] { for t in [0.0, 0.5, 1.0, 2.0] {
let base = nutation_2000a(t); let base = nutation_2000a(t);
let adjusted = nutation_2000a_with_2006_adjustment(t); let adjusted = nutation_2000a_with_2006_adjustment(t);
@ -75,6 +87,7 @@ fn the_nutation_2006_adjustment_is_a_scaling() {
#[test] #[test]
fn the_nutation_matches_the_reference_over_a_century() { fn the_nutation_matches_the_reference_over_a_century() {
// Recorded from ERFA at each date. The values are in radians. // Recorded from ERFA at each date. The values are in radians.
// 在每个日期从 ERFA 记录。各值以弧度计。
let cases = [ let cases = [
(40_587.0, 2.1896610891495316e-05, 4.070477585626008e-05), (40_587.0, 2.1896610891495316e-05, 4.070477585626008e-05),
(51_544.5, -6.754419567422628e-05, -2.797091091919891e-05), (51_544.5, -6.754419567422628e-05, -2.797091091919891e-05),
@ -100,6 +113,7 @@ fn the_nutation_matches_the_reference_over_a_century() {
#[test] #[test]
fn the_precession_angles_at_j2000_match_the_reference() { fn the_precession_angles_at_j2000_match_the_reference() {
// The P03 precession angles at J2000.0. // The P03 precession angles at J2000.0.
// J2000.0 处的 P03 岁差角。
let angles = precession_angles(0.0); let angles = precession_angles(0.0);
assert!( assert!(
(angles.gamma_bar - -0.052_928 * ARCSECOND).abs() < 1e-18, (angles.gamma_bar - -0.052_928 * ARCSECOND).abs() < 1e-18,
@ -127,6 +141,8 @@ fn the_precession_angles_at_j2000_match_the_reference() {
fn the_bias_precession_nutation_matrix_matches_the_reference() { fn the_bias_precession_nutation_matrix_matches_the_reference() {
// The GCRS-to-true-of-date matrix at J2000.0 on the Terrestrial Time // The GCRS-to-true-of-date matrix at J2000.0 on the Terrestrial Time
// scale, from ERFA's `pnm06a`. The values are the first row. // scale, from ERFA's `pnm06a`. The values are the first row.
// 地球时时标上 J2000.0 处的 GCRS 到该日真坐标矩阵,取自 ERFA 的 `pnm06a`。
// 各值是第一行。
let tt = 51_544.5 + 2_400_000.5 + 69.184 / 86_400.0; let tt = 51_544.5 + 2_400_000.5 + 69.184 / 86_400.0;
let t = (tt - 2_451_545.0) / 36_525.0; let t = (tt - 2_451_545.0) / 36_525.0;
let matrix = bias_precession_nutation_matrix(t); let matrix = bias_precession_nutation_matrix(t);
@ -192,11 +208,13 @@ fn the_celestial_to_terrestrial_matrix_is_a_rotation() {
fn the_celestial_to_terrestrial_matrix_matches_the_reference() { fn the_celestial_to_terrestrial_matrix_matches_the_reference() {
// The matrix at J2000.0 on the Terrestrial Time scale, from ERFA with // The matrix at J2000.0 on the Terrestrial Time scale, from ERFA with
// zero polar motion. Each entry is checked to a nanoradian. // zero polar motion. Each entry is checked to a nanoradian.
// 地球时时标上 J2000.0 处的矩阵,取自极移为零的 ERFA。每个元素检查到纳弧度。
let tt = 51_544.5 + 2_400_000.5 + 69.184 / 86_400.0; let tt = 51_544.5 + 2_400_000.5 + 69.184 / 86_400.0;
let matrix = celestial_to_terrestrial_matrix(tt, tt, PolarMotion::NONE); let matrix = celestial_to_terrestrial_matrix(tt, tt, PolarMotion::NONE);
// The first row, from ERFA's `c2t06a` composed with the equinox based // The first row, from ERFA's `c2t06a` composed with the equinox based
// sidereal time. // sidereal time.
// 第一行,取自 ERFA 的 `c2t06a` 与基于春分点的恒星时之合成。
let expected_first_row = [ let expected_first_row = [
0.18651846184811946, 0.18651846184811946,
-0.982451455739228, -0.982451455739228,
@ -216,6 +234,7 @@ fn the_equation_of_the_equinoxes_includes_the_complementary_terms() {
// The complementary terms are a few thousandths of an arcsecond and must // The complementary terms are a few thousandths of an arcsecond and must
// not be dropped: their omission would show as a millisecond of // not be dropped: their omission would show as a millisecond of
// sidereal time. // sidereal time.
// 补充项是几千分之一角秒,不得省略:略去它们会表现为一毫秒的恒星时。
let t = 0.0; let t = 0.0;
let complementary = equation_of_the_equinoxes_complementary_terms(t); let complementary = equation_of_the_equinoxes_complementary_terms(t);
assert!( assert!(
@ -224,6 +243,7 @@ fn the_equation_of_the_equinoxes_includes_the_complementary_terms() {
); );
// At J2000.0 the leading term is 2640.96 microarcseconds times the sine // At J2000.0 the leading term is 2640.96 microarcseconds times the sine
// of the Moon's ascending node. // of the Moon's ascending node.
// 在 J2000.0 处首项是 2640.96 微角秒乘以月球升交点角的正弦。
let expected = 1.021330096302465e-08; let expected = 1.021330096302465e-08;
assert!( assert!(
(complementary - expected).abs() < 1e-15, (complementary - expected).abs() < 1e-15,
@ -235,6 +255,7 @@ fn the_equation_of_the_equinoxes_includes_the_complementary_terms() {
fn the_general_precession_is_quadratic_in_time() { fn the_general_precession_is_quadratic_in_time() {
// The argument is (0.024381750 + 0.00000538691 t) t, so at small times // The argument is (0.024381750 + 0.00000538691 t) t, so at small times
// it grows linearly with the leading coefficient. // it grows linearly with the leading coefficient.
// 该辐角为 (0.024381750 + 0.00000538691 t) t,故在小时间处它随首系数线性增长。
for t in [0.1, 0.5, 1.0] { for t in [0.1, 0.5, 1.0] {
let value = general_precession(t); let value = general_precession(t);
let expected = (0.024_381_750 + 0.000_005_386_91 * t) * t; let expected = (0.024_381_750 + 0.000_005_386_91 * t) * t;
@ -247,6 +268,8 @@ fn sidereal_time_advances_by_about_a_turn_per_day() {
// Greenwich apparent sidereal time must increase by a full turn plus a // Greenwich apparent sidereal time must increase by a full turn plus a
// small amount each day. The small amount is about 3.94 minutes of // small amount each day. The small amount is about 3.94 minutes of
// time, which is 0.9856 degrees. // time, which is 0.9856 degrees.
// 格林尼治视恒星时每天必须增加一整圈再加上一小量。该小量约为 3.94 分钟时间,
// 即 0.9856 度。
let first = greenwich_apparent_sidereal_time(60_000.0, 60_000.0); let first = greenwich_apparent_sidereal_time(60_000.0, 60_000.0);
let second = greenwich_apparent_sidereal_time(60_001.0, 60_001.0); let second = greenwich_apparent_sidereal_time(60_001.0, 60_001.0);
let advance = (second - first).rem_euclid(2.0 * PI); let advance = (second - first).rem_euclid(2.0 * PI);
@ -274,10 +297,12 @@ fn zero_polar_motion_is_the_identity_rotation() {
fn a_polar_motion_offsets_the_rotation() { fn a_polar_motion_offsets_the_rotation() {
// A polar motion of one arcsecond must tilt the matrix by about that // A polar motion of one arcsecond must tilt the matrix by about that
// much, which is 4.85 microradians. // much, which is 4.85 microradians.
// 一角秒的极移必须使矩阵倾斜约那么多,即 4.85 微弧度。
let motion = PolarMotion::new(ARCSECOND, ARCSECOND, 0.0); let motion = PolarMotion::new(ARCSECOND, ARCSECOND, 0.0);
let matrix = motion.matrix(); let matrix = motion.matrix();
assert!(matrix.is_rotation(1e-12)); assert!(matrix.is_rotation(1e-12));
// The off-diagonal term is the sine of the displacement. // The off-diagonal term is the sine of the displacement.
// 非对角项是该位移的正弦。
let value = matrix.get(0, 2).expect("the index is in range"); let value = matrix.get(0, 2).expect("the index is in range");
assert!( assert!(
(value - ARCSECOND.sin()).abs() < 1e-15, (value - ARCSECOND.sin()).abs() < 1e-15,
@ -291,6 +316,7 @@ fn a_polar_motion_offsets_the_rotation() {
fn the_polar_motion_shifts_the_earth_fixed_frame() { fn the_polar_motion_shifts_the_earth_fixed_frame() {
// Applying a polar motion must change the terrestrial position of a // Applying a polar motion must change the terrestrial position of a
// fixed inertial direction. // fixed inertial direction.
// 施加极移必须改变某固定惯性方向的地球位置。
let tt = 60_000.0; let tt = 60_000.0;
let without = celestial_to_terrestrial_matrix(tt, tt, PolarMotion::NONE); let without = celestial_to_terrestrial_matrix(tt, tt, PolarMotion::NONE);
let with = celestial_to_terrestrial_matrix( let with = celestial_to_terrestrial_matrix(
@ -301,6 +327,7 @@ fn the_polar_motion_shifts_the_earth_fixed_frame() {
let difference = (without.transpose() * with).transpose() * with; let difference = (without.transpose() * with).transpose() * with;
// The two must differ, and the difference must be of the order of the // The two must differ, and the difference must be of the order of the
// applied angles. // applied angles.
// 二者必须不同,且差值必须与所施加的角度同量级。
let mut largest = 0.0_f64; let mut largest = 0.0_f64;
for row in 0..3 { for row in 0..3 {
for column in 0..3 { for column in 0..3 {
@ -318,10 +345,13 @@ fn the_nutation_reaches_the_expected_magnitude() {
// The nutation in longitude reaches about seventeen arcseconds and the // The nutation in longitude reaches about seventeen arcseconds and the
// nutation in obliquity about nine, so over a range of dates the // nutation in obliquity about nine, so over a range of dates the
// largest value found must be in that range. // largest value found must be in that range.
// 黄经章动达约十七角秒,黄赤交角章动约九角秒,故在一段日期上所能找到的最大值
// 必须落在该范围内。
let mut largest_longitude = 0.0_f64; let mut largest_longitude = 0.0_f64;
let mut largest_obliquity = 0.0_f64; let mut largest_obliquity = 0.0_f64;
for step in 0..400 { for step in 0..400 {
// A span of about fifty years in steps of a fortnight. // A span of about fifty years in steps of a fortnight.
// 约五十年、步长为两周的跨度。
let t = f64::from(step) * 0.125; let t = f64::from(step) * 0.125;
let nutation = nutation_2000a(t); let nutation = nutation_2000a(t);
largest_longitude = largest_longitude.max(nutation.longitude.abs()); largest_longitude = largest_longitude.max(nutation.longitude.abs());
@ -344,6 +374,8 @@ fn the_nutation_is_periodic() {
// The dominant terms have periods of about eighteen years and half a // The dominant terms have periods of about eighteen years and half a
// year, so the value must return close to its initial value after // year, so the value must return close to its initial value after
// eighteen years and must oscillate in between. // eighteen years and must oscillate in between.
// 主导项的周期约为十八年与半年,故该值必须在十八年后回到接近其初值,并在其间
// 振荡。
let initial = nutation_2000a(0.0).longitude; let initial = nutation_2000a(0.0).longitude;
let after_eighteen_years = nutation_2000a(0.18).longitude; let after_eighteen_years = nutation_2000a(0.18).longitude;
assert!( assert!(
@ -353,6 +385,7 @@ fn the_nutation_is_periodic() {
); );
// The sign must change at some point within that span. // The sign must change at some point within that span.
// 符号必须在该跨度内某处改变。
let mut seen_positive = false; let mut seen_positive = false;
let mut seen_negative = false; let mut seen_negative = false;
for step in 0..200 { for step in 0..200 {
@ -368,6 +401,7 @@ fn the_nutation_is_periodic() {
fn the_equation_of_the_equinoxes_follows_the_nutation() { fn the_equation_of_the_equinoxes_follows_the_nutation() {
// The equation of the equinoxes is dominated by the nutation in // The equation of the equinoxes is dominated by the nutation in
// longitude projected onto the true equator, so it shares its sign. // longitude projected onto the true equator, so it shares its sign.
// 分点方程式主要由投影到真赤道上的黄经章动主导,故它与后者同号。
for t in [0.02, 0.05, 0.1, 0.2] { for t in [0.02, 0.05, 0.1, 0.2] {
let nutation = nutation_2000a_with_2006_adjustment(t); let nutation = nutation_2000a_with_2006_adjustment(t);
let equation = equation_of_the_equinoxes(t); let equation = equation_of_the_equinoxes(t);
@ -384,11 +418,13 @@ fn the_equation_of_the_equinoxes_follows_the_nutation() {
fn a_matrix_at_two_epochs_separated_by_a_century_differs_by_the_precession() { fn a_matrix_at_two_epochs_separated_by_a_century_differs_by_the_precession() {
// The precession over a century is about 1.4 degrees, so the two // The precession over a century is about 1.4 degrees, so the two
// matrices must differ by an angle of that order. // matrices must differ by an angle of that order.
// 一世纪的岁差约为 1.4 度,故两个矩阵必须相差该量级的角。
let first = bias_precession_nutation_matrix(0.0); let first = bias_precession_nutation_matrix(0.0);
let second = bias_precession_nutation_matrix(1.0); let second = bias_precession_nutation_matrix(1.0);
let difference = first.transpose() * second; let difference = first.transpose() * second;
// The trace of a rotation matrix is 1 + 2 cos(angle), so the angle // The trace of a rotation matrix is 1 + 2 cos(angle), so the angle
// follows from it. // follows from it.
// 转动矩阵的迹为 1 + 2 cos(angle),故该角由它得出。
let cosine = ((difference.trace() - 1.0) / 2.0).clamp(-1.0, 1.0); let cosine = ((difference.trace() - 1.0) / 2.0).clamp(-1.0, 1.0);
let degrees = cosine.acos().to_degrees(); let degrees = cosine.acos().to_degrees();
assert!( assert!(
@ -401,6 +437,7 @@ fn a_matrix_at_two_epochs_separated_by_a_century_differs_by_the_precession() {
fn the_1982_sidereal_time_matches_the_reference() { fn the_1982_sidereal_time_matches_the_reference() {
// Greenwich mean sidereal time from the 1982 expression, which is the // Greenwich mean sidereal time from the 1982 expression, which is the
// one the SGP4 theory was fitted against. The values are from ERFA. // one the SGP4 theory was fitted against. The values are from ERFA.
// 1982 表达式给出的格林尼治平恒星时,即 SGP4 理论所拟合者。各值取自 ERFA。
let cases = [ let cases = [
(51_544.5, 4.894_961_212_823_059), (51_544.5, 4.894_961_212_823_059),
(60_476.999_999_810_5, 4.620_858_198_832_686), (60_476.999_999_810_5, 4.620_858_198_832_686),
@ -419,6 +456,7 @@ fn the_1982_sidereal_time_matches_the_reference() {
fn the_1982_sidereal_time_is_a_full_turn_per_day() { fn the_1982_sidereal_time_is_a_full_turn_per_day() {
// Sidereal time advances by one turn plus the excess of the sidereal // Sidereal time advances by one turn plus the excess of the sidereal
// day over the solar day, which is 0.9856 degrees per day. // day over the solar day, which is 0.9856 degrees per day.
// 恒星时推进一整圈再加上恒星日超出太阳日的部分,即每天 0.9856 度。
let first = apsis_frames::iau2000::greenwich_mean_sidereal_time_1982(2_400_000.5, 60_000.0); let first = apsis_frames::iau2000::greenwich_mean_sidereal_time_1982(2_400_000.5, 60_000.0);
let second = apsis_frames::iau2000::greenwich_mean_sidereal_time_1982(2_400_000.5, 60_001.0); let second = apsis_frames::iau2000::greenwich_mean_sidereal_time_1982(2_400_000.5, 60_001.0);
let advance = (second - first).rem_euclid(2.0 * PI); let advance = (second - first).rem_euclid(2.0 * PI);
@ -436,6 +474,7 @@ fn the_teme_transformation_to_earth_fixed_matches_the_reference() {
// The reference values are ERFA's `gmst82` applied as a third axis // The reference values are ERFA's `gmst82` applied as a third axis
// rotation with the identity polar motion. // rotation with the identity polar motion.
// 参考值是把 ERFA 的 `gmst82` 作为第三轴转动、配以单位极移施加所得。
let cases = [ let cases = [
( (
60_481.5, 60_481.5,
@ -479,6 +518,8 @@ fn the_teme_frame_uses_the_mean_equinox_not_the_true_one() {
// pairs with a mean sidereal time. Substituting the apparent value, or // pairs with a mean sidereal time. Substituting the apparent value, or
// the modern mean value, rotates the result by a fraction of a degree, // the modern mean value, rotates the result by a fraction of a degree,
// which is many kilometres of ground position. // which is many kilometres of ground position.
// 该区别要紧。TEME 的第一轴是平春分点,故它与平恒星时配对。换成视值或现代平值
// 会把结果转动零点几度,即许多千米的地面位置。
use apsis_frames::iau2000::{ use apsis_frames::iau2000::{
greenwich_apparent_sidereal_time, greenwich_mean_sidereal_time, greenwich_apparent_sidereal_time, greenwich_mean_sidereal_time,
greenwich_mean_sidereal_time_1982, greenwich_mean_sidereal_time_1982,
@ -492,6 +533,7 @@ fn the_teme_frame_uses_the_mean_equinox_not_the_true_one() {
// The apparent value includes the equation of the equinoxes, which is a // The apparent value includes the equation of the equinoxes, which is a
// few thousandths of a degree. // few thousandths of a degree.
// 视值包含分点方程式,即千分之几度。
let equation = (apparent - modern_mean).abs().to_degrees(); let equation = (apparent - modern_mean).abs().to_degrees();
assert!( assert!(
equation > 1e-4 && equation < 1e-2, equation > 1e-4 && equation < 1e-2,
@ -501,6 +543,8 @@ fn the_teme_frame_uses_the_mean_equinox_not_the_true_one() {
// The 1982 expression, which TEME is fitted against, differs from the // The 1982 expression, which TEME is fitted against, differs from the
// modern mean value by a fraction of a degree, because the precession // modern mean value by a fraction of a degree, because the precession
// model changed between them. // model changed between them.
// TEME 所拟合的 1982 表达式与现代平值相差零点几度,因为二者之间的岁差模型
// 改变了。
let secular = (sidereal_1982 - modern_mean).abs().to_degrees(); let secular = (sidereal_1982 - modern_mean).abs().to_degrees();
assert!( assert!(
secular > 0.1 && secular < 1.0, secular > 0.1 && secular < 1.0,

View File

@ -1,4 +1,6 @@
//! Tests for the reference ellipsoid. //! Tests for the reference ellipsoid.
//!
//! 参考椭球的测试。
use apsis_geodesy::{Ellipsoid, WGS84}; use apsis_geodesy::{Ellipsoid, WGS84};
@ -12,8 +14,10 @@ fn defining_parameters_match_the_standard() {
#[test] #[test]
fn derived_axes_match_the_published_values() { fn derived_axes_match_the_published_values() {
// The WGS 84 semi-minor axis is 6 356 752.314 245 m. // The WGS 84 semi-minor axis is 6 356 752.314 245 m.
// WGS 84 的半短轴为 6 356 752.314 245 m。
assert!((WGS84.semi_minor_axis() - 6_356_752.314_245).abs() < 1e-6); assert!((WGS84.semi_minor_axis() - 6_356_752.314_245).abs() < 1e-6);
// The first eccentricity is 0.081 819 190 842 622. // The first eccentricity is 0.081 819 190 842 622.
// 第一偏心率为 0.081 819 190 842 622。
assert!((WGS84.eccentricity() - 0.081_819_190_842_622).abs() < 1e-12); assert!((WGS84.eccentricity() - 0.081_819_190_842_622).abs() < 1e-12);
} }
@ -30,15 +34,18 @@ fn second_eccentricity_relates_to_the_first() {
let second = WGS84.second_eccentricity_squared(); let second = WGS84.second_eccentricity_squared();
assert!((second - first / (1.0 - first)).abs() < 1e-18); assert!((second - first / (1.0 - first)).abs() < 1e-18);
// Derived from the defining parameters this is 0.006 739 496 742 276. // Derived from the defining parameters this is 0.006 739 496 742 276.
// 由定义参数导出,该值为 0.006 739 496 742 276。
assert!((second - 0.006_739_496_742_276_435).abs() < 1e-15); assert!((second - 0.006_739_496_742_276_435).abs() < 1e-15);
} }
#[test] #[test]
fn prime_vertical_radius_spans_the_expected_range() { fn prime_vertical_radius_spans_the_expected_range() {
// At the equator it equals the semi-major axis. // At the equator it equals the semi-major axis.
// 在赤道处它等于半长轴。
assert!((WGS84.prime_vertical_radius(0.0) - WGS84.semi_major_axis()).abs() < 1e-9); assert!((WGS84.prime_vertical_radius(0.0) - WGS84.semi_major_axis()).abs() < 1e-9);
// At the pole it equals the polar radius of curvature, a / sqrt(1 - e^2), // At the pole it equals the polar radius of curvature, a / sqrt(1 - e^2),
// which is 6 399 593.625 9 m. // which is 6 399 593.625 9 m.
// 在极点处它等于极曲率半径 a / sqrt(1 - e^2),即 6 399 593.625 9 m。
let pole = WGS84.prime_vertical_radius(core::f64::consts::FRAC_PI_2); let pole = WGS84.prime_vertical_radius(core::f64::consts::FRAC_PI_2);
assert!((pole - WGS84.polar_radius_of_curvature()).abs() < 1e-9); assert!((pole - WGS84.polar_radius_of_curvature()).abs() < 1e-9);
assert!((pole - 6_399_593.625_9).abs() < 1e-3); assert!((pole - 6_399_593.625_9).abs() < 1e-3);
@ -47,11 +54,13 @@ fn prime_vertical_radius_spans_the_expected_range() {
#[test] #[test]
fn meridian_radius_spans_the_expected_range() { fn meridian_radius_spans_the_expected_range() {
// At the equator it is a (1 - e^2), which is 6 335 439.327 m. // At the equator it is a (1 - e^2), which is 6 335 439.327 m.
// 在赤道处它是 a (1 - e^2),即 6 335 439.327 m。
let equator = WGS84.meridian_radius(0.0); let equator = WGS84.meridian_radius(0.0);
let expected = WGS84.semi_major_axis() * (1.0 - WGS84.eccentricity_squared()); let expected = WGS84.semi_major_axis() * (1.0 - WGS84.eccentricity_squared());
assert!((equator - expected).abs() < 1e-6); assert!((equator - expected).abs() < 1e-6);
assert!((equator - 6_335_439.327_1).abs() < 1e-3); assert!((equator - 6_335_439.327_1).abs() < 1e-3);
// At the pole it is a / sqrt(1 - e^2). // At the pole it is a / sqrt(1 - e^2).
// 在极点处它是 a / sqrt(1 - e^2)。
let pole = WGS84.meridian_radius(core::f64::consts::FRAC_PI_2); let pole = WGS84.meridian_radius(core::f64::consts::FRAC_PI_2);
assert!((pole - WGS84.polar_radius_of_curvature()).abs() < 1e-6); assert!((pole - WGS84.polar_radius_of_curvature()).abs() < 1e-6);
} }
@ -69,6 +78,7 @@ fn geocentric_latitude_is_smaller_than_geodetic_away_from_the_axes() {
let geocentric = WGS84.geocentric_latitude(geodetic); let geocentric = WGS84.geocentric_latitude(geodetic);
assert!(geocentric < geodetic); assert!(geocentric < geodetic);
// The maximum separation is about 0.19 degrees at 45 degrees. // The maximum separation is about 0.19 degrees at 45 degrees.
// 最大差约在 45 度处为 0.19 度。
let difference = (geodetic - geocentric).to_degrees(); let difference = (geodetic - geocentric).to_degrees();
assert!((difference - 0.1924).abs() < 0.001, "got {difference}"); assert!((difference - 0.1924).abs() < 0.001, "got {difference}");
} }

View File

@ -1,15 +1,23 @@
//! Tests for geodetic and Cartesian conversions. //! Tests for geodetic and Cartesian conversions.
//!
//! 大地与直角坐标换算的测试。
use apsis_geodesy::{Geodetic, WGS84, geodetic_from_cartesian}; use apsis_geodesy::{Geodetic, WGS84, geodetic_from_cartesian};
use apsis_math::Vec3; use apsis_math::Vec3;
/// Tolerance for position comparisons, in meters. /// Tolerance for position comparisons, in meters.
///
/// 位置比较的容差,单位为米。
const POSITION_TOL: f64 = 1e-4; const POSITION_TOL: f64 = 1e-4;
/// Tolerance for latitude and longitude comparisons, in radians. /// Tolerance for latitude and longitude comparisons, in radians.
/// ///
/// One microdegree is about 1.1e-8 radians, or roughly a centimeter on the /// One microdegree is about 1.1e-8 radians, or roughly a centimeter on the
/// Earth's surface. /// Earth's surface.
///
/// 纬度与经度比较的容差,单位为弧度。
///
/// 一微度约为 1.1e-8 弧度,或地表上大致一厘米。
const ANGLE_TOL: f64 = 1e-11; const ANGLE_TOL: f64 = 1e-11;
#[test] #[test]
@ -42,6 +50,7 @@ fn height_moves_the_point_along_the_ellipsoid_normal() {
assert!((surface.distance(elevated) - 500.0).abs() < POSITION_TOL); assert!((surface.distance(elevated) - 500.0).abs() < POSITION_TOL);
// The normal at a non-polar latitude is not vertical, so the height // The normal at a non-polar latitude is not vertical, so the height
// changes the horizontal components as well as the third one. // changes the horizontal components as well as the third one.
// 非极地纬度处的法线并非竖直,故高度既改变第三分量也改变水平分量。
assert!(elevated.x > surface.x); assert!(elevated.x > surface.x);
} }
@ -91,6 +100,7 @@ fn polar_axis_has_no_defined_longitude() {
let on_axis = Vec3::new(0.0, 0.0, WGS84.semi_minor_axis()); let on_axis = Vec3::new(0.0, 0.0, WGS84.semi_minor_axis());
assert_eq!(geodetic_from_cartesian(on_axis, WGS84), None); assert_eq!(geodetic_from_cartesian(on_axis, WGS84), None);
// The origin is also on the axis. // The origin is also on the axis.
// 原点也在该轴上。
assert_eq!(geodetic_from_cartesian(Vec3::ZERO, WGS84), None); assert_eq!(geodetic_from_cartesian(Vec3::ZERO, WGS84), None);
} }
@ -107,6 +117,8 @@ fn known_point_matches_an_independent_computation() {
// Latitude 37.5 degrees north, longitude 121.333333... degrees west, at // Latitude 37.5 degrees north, longitude 121.333333... degrees west, at
// zero height, evaluated with the defining WGS 84 parameters in // zero height, evaluated with the defining WGS 84 parameters in
// extended precision. // extended precision.
// 北纬 37.5 度、西经 121.333333... 度、零高度,用 WGS 84 定义参数以扩展精度
// 求值。
let point = Geodetic::new( let point = Geodetic::new(
37.5_f64.to_radians(), 37.5_f64.to_radians(),
-121.333_333_333_333_33_f64.to_radians(), -121.333_333_333_333_33_f64.to_radians(),

View File

@ -1,4 +1,6 @@
//! Tests for the leap second table. //! Tests for the leap second table.
//!
//! 闰秒表的测试。
use apsis_leap::{ use apsis_leap::{
LEAP_SECONDS, TABLE_COVERS_FROM_MJD, TAI_MINUS_UTC_AFTER_LAST_ENTRY, mjd_from_ymd, LEAP_SECONDS, TABLE_COVERS_FROM_MJD, TAI_MINUS_UTC_AFTER_LAST_ENTRY, mjd_from_ymd,
@ -23,6 +25,7 @@ fn table_is_ordered_and_monotonic() {
fn every_entry_increases_the_offset_by_one_second() { fn every_entry_increases_the_offset_by_one_second() {
// The table starts at the inherited offset of ten seconds and every // The table starts at the inherited offset of ten seconds and every
// later announcement inserted exactly one second. // later announcement inserted exactly one second.
// 该表始于继承而来的十秒偏移,其后每次宣布都恰好插入一秒。
assert_eq!(LEAP_SECONDS[0].tai_minus_utc, 10.0); assert_eq!(LEAP_SECONDS[0].tai_minus_utc, 10.0);
for pair in LEAP_SECONDS.windows(2) { for pair in LEAP_SECONDS.windows(2) {
let (earlier, later) = (pair[0], pair[1]); let (earlier, later) = (pair[0], pair[1]);
@ -48,6 +51,7 @@ fn table_extremes_match_the_announced_values() {
fn table_contains_every_announcement_since_1972() { fn table_contains_every_announcement_since_1972() {
// Twenty seven leap seconds were introduced between 1972 and 2017 // Twenty seven leap seconds were introduced between 1972 and 2017
// inclusive of the initial 1972-01-01 entry. // inclusive of the initial 1972-01-01 entry.
// 1972 年到 2017 年间引入了二十七个闰秒,含 1972-01-01 的初项。
assert_eq!(LEAP_SECONDS.len(), 28); assert_eq!(LEAP_SECONDS.len(), 28);
} }
@ -60,12 +64,16 @@ fn coverage_constants_match_the_table_boundaries() {
#[test] #[test]
fn known_calendar_dates_map_to_known_modified_julian_dates() { fn known_calendar_dates_map_to_known_modified_julian_dates() {
// MJD 0 is the start of 1858-11-17. // MJD 0 is the start of 1858-11-17.
// MJD 0 是 1858-11-17 的起点。
assert_eq!(mjd_from_ymd(1858, 11, 17), 0.0); assert_eq!(mjd_from_ymd(1858, 11, 17), 0.0);
// The calendar day of J2000.0 is MJD 51544. // The calendar day of J2000.0 is MJD 51544.
// J2000.0 的日历日是 MJD 51544。
assert_eq!(mjd_from_ymd(2000, 1, 1), 51_544.0); assert_eq!(mjd_from_ymd(2000, 1, 1), 51_544.0);
// The Unix epoch is MJD 40587. // The Unix epoch is MJD 40587.
// Unix 历元是 MJD 40587。
assert_eq!(mjd_from_ymd(1970, 1, 1), 40_587.0); assert_eq!(mjd_from_ymd(1970, 1, 1), 40_587.0);
// The last leap second. // The last leap second.
// 最后一个闰秒。
assert_eq!(mjd_from_ymd(2017, 1, 1), 57_754.0); assert_eq!(mjd_from_ymd(2017, 1, 1), 57_754.0);
assert_eq!(mjd_from_ymd(2024, 1, 1), 60_310.0); assert_eq!(mjd_from_ymd(2024, 1, 1), 60_310.0);
} }
@ -73,8 +81,10 @@ fn known_calendar_dates_map_to_known_modified_julian_dates() {
#[test] #[test]
fn calendar_conversion_handles_leap_days_and_month_ends() { fn calendar_conversion_handles_leap_days_and_month_ends() {
// 2000 is a leap year, 1900 is not. // 2000 is a leap year, 1900 is not.
// 2000 年是闰年,1900 年不是。
assert_eq!(mjd_from_ymd(2000, 3, 1) - mjd_from_ymd(2000, 2, 1), 29.0); assert_eq!(mjd_from_ymd(2000, 3, 1) - mjd_from_ymd(2000, 2, 1), 29.0);
assert_eq!(mjd_from_ymd(1900, 3, 1) - mjd_from_ymd(1900, 2, 1), 28.0); assert_eq!(mjd_from_ymd(1900, 3, 1) - mjd_from_ymd(1900, 2, 1), 28.0);
// Consecutive month ends are one day apart. // Consecutive month ends are one day apart.
// 连续的月末相隔一天。
assert_eq!(mjd_from_ymd(2024, 3, 1) - mjd_from_ymd(2024, 2, 29), 1.0); assert_eq!(mjd_from_ymd(2024, 3, 1) - mjd_from_ymd(2024, 2, 29), 1.0);
} }

View File

@ -1,4 +1,6 @@
//! Tests for UTC and TAI conversions on the Modified Julian Date scale. //! Tests for UTC and TAI conversions on the Modified Julian Date scale.
//!
//! 约化儒略日尺度上 UTC 与 TAI 换算的测试。
use apsis_leap::{ use apsis_leap::{
MJD_AT_UNIX_EPOCH, calendar_mjd, tai_minus_utc_at_mjd, tai_mjd_from_utc_mjd, MJD_AT_UNIX_EPOCH, calendar_mjd, tai_minus_utc_at_mjd, tai_mjd_from_utc_mjd,
@ -26,12 +28,14 @@ fn first_entry_supplies_the_initial_offset() {
let start = calendar_mjd(1972, 1, 1); let start = calendar_mjd(1972, 1, 1);
assert_eq!(tai_minus_utc_at_mjd(start), Some(10.0)); assert_eq!(tai_minus_utc_at_mjd(start), Some(10.0));
// Just before midnight one day earlier the table does not apply. // Just before midnight one day earlier the table does not apply.
// 再早一天、午夜之前,该表不适用。
assert_eq!(tai_minus_utc_at_mjd(start - 1.0), None); assert_eq!(tai_minus_utc_at_mjd(start - 1.0), None);
} }
#[test] #[test]
fn offset_changes_exactly_on_the_effective_date() { fn offset_changes_exactly_on_the_effective_date() {
// The 2017-01-01 leap second raised the offset from 36 to 37. // The 2017-01-01 leap second raised the offset from 36 to 37.
// 2017-01-01 的闰秒把偏移从 36 升到 37。
let effective = calendar_mjd(2017, 1, 1); let effective = calendar_mjd(2017, 1, 1);
assert_eq!(tai_minus_utc_at_mjd(effective - 1.0), Some(36.0)); assert_eq!(tai_minus_utc_at_mjd(effective - 1.0), Some(36.0));
assert_eq!(tai_minus_utc_at_mjd(effective), Some(37.0)); assert_eq!(tai_minus_utc_at_mjd(effective), Some(37.0));
@ -49,6 +53,7 @@ fn every_entry_is_reachable_at_its_effective_instant() {
#[test] #[test]
fn offset_is_constant_between_entries() { fn offset_is_constant_between_entries() {
// Mid 2016 sits between the 2015-07-01 and 2017-01-01 entries. // Mid 2016 sits between the 2015-07-01 and 2017-01-01 entries.
// 2016 年年中位于 2015-07-01 与 2017-01-01 两项之间。
let instant = calendar_mjd(2016, 6, 1); let instant = calendar_mjd(2016, 6, 1);
assert_eq!(tai_minus_utc_at_mjd(instant), Some(36.0)); assert_eq!(tai_minus_utc_at_mjd(instant), Some(36.0));
assert_eq!(tai_minus_utc_at_mjd(instant + 0.25), Some(36.0)); assert_eq!(tai_minus_utc_at_mjd(instant + 0.25), Some(36.0));
@ -66,6 +71,7 @@ fn utc_to_tai_adds_the_offset() {
let tai = tai_mjd_from_utc_mjd(utc).expect("instant is inside the table"); let tai = tai_mjd_from_utc_mjd(utc).expect("instant is inside the table");
// An MJD near 58000 has a resolution of about 7e-12 days, so the sum // An MJD near 58000 has a resolution of about 7e-12 days, so the sum
// can only be compared to well below a microsecond. // can only be compared to well below a microsecond.
// 接近 58000 的 MJD 分辨率约为 7e-12 天,故该和只能比较到远低于一微秒。
assert!((tai - utc - 37.0 / SECONDS_PER_DAY).abs() < 1e-11); assert!((tai - utc - 37.0 / SECONDS_PER_DAY).abs() < 1e-11);
} }
@ -93,6 +99,7 @@ fn utc_to_tai_round_trips() {
fn tai_to_utc_recovers_the_offset_on_each_side_of_a_step() { fn tai_to_utc_recovers_the_offset_on_each_side_of_a_step() {
let effective = calendar_mjd(2017, 1, 1); let effective = calendar_mjd(2017, 1, 1);
// One hour before the step the offset is 36 s, one hour after it is 37 s. // One hour before the step the offset is 36 s, one hour after it is 37 s.
// 阶跃前一小时偏移为 36 s,其后一小时为 37 s。
let before_utc = effective - 1.0 / 24.0; let before_utc = effective - 1.0 / 24.0;
let after_utc = effective + 1.0 / 24.0; let after_utc = effective + 1.0 / 24.0;
let before_tai = tai_mjd_from_utc_mjd(before_utc).expect("inside the table"); let before_tai = tai_mjd_from_utc_mjd(before_utc).expect("inside the table");
@ -111,6 +118,7 @@ fn tai_to_utc_rejects_non_finite_input() {
fn tai_to_utc_at_the_boundary_stays_inside_the_table() { fn tai_to_utc_at_the_boundary_stays_inside_the_table() {
// The first TAI instant that the table covers is 1972-01-01 00:00:10 // The first TAI instant that the table covers is 1972-01-01 00:00:10
// TAI, because the offset there is ten seconds. // TAI, because the offset there is ten seconds.
// 该表覆盖的第一个 TAI 瞬时是 1972-01-01 00:00:10 TAI,因为那里的偏移是十秒。
let boundary = calendar_mjd(1972, 1, 1) + 10.0 / SECONDS_PER_DAY; let boundary = calendar_mjd(1972, 1, 1) + 10.0 / SECONDS_PER_DAY;
let utc = utc_mjd_from_tai_mjd(boundary).expect("boundary is covered"); let utc = utc_mjd_from_tai_mjd(boundary).expect("boundary is covered");
assert!((utc - calendar_mjd(1972, 1, 1)).abs() < 1e-10); assert!((utc - calendar_mjd(1972, 1, 1)).abs() < 1e-10);

View File

@ -1,4 +1,6 @@
//! Tests for the calendar conversions. //! Tests for the calendar conversions.
//!
//! 日历换算的测试。
use apsis_time::{Calendar, days_in_month, is_leap_year, mjd_from_ymd, ymd_from_mjd}; use apsis_time::{Calendar, days_in_month, is_leap_year, mjd_from_ymd, ymd_from_mjd};
@ -19,6 +21,7 @@ fn known_dates_map_to_known_modified_julian_dates() {
#[test] #[test]
fn calendar_conversion_round_trips_over_a_long_span() { fn calendar_conversion_round_trips_over_a_long_span() {
// Walk day by day over a span that crosses every leap year rule. // Walk day by day over a span that crosses every leap year rule.
// 逐日走过一个跨越每条闰年规则的跨度。
let mut mjd = mjd_from_ymd(1899, 12, 20); let mut mjd = mjd_from_ymd(1899, 12, 20);
let end = mjd_from_ymd(2101, 1, 10); let end = mjd_from_ymd(2101, 1, 10);
while mjd < end { while mjd < end {
@ -38,6 +41,7 @@ fn fractional_modified_julian_dates_map_to_the_containing_day() {
assert_eq!(ymd_from_mjd(day + 0.25), (2024, 6, 15)); assert_eq!(ymd_from_mjd(day + 0.25), (2024, 6, 15));
assert_eq!(ymd_from_mjd(day + 0.75), (2024, 6, 15)); assert_eq!(ymd_from_mjd(day + 0.75), (2024, 6, 15));
// One day later, but still the same instant of the following day. // One day later, but still the same instant of the following day.
// 晚一天,但仍是次日的同一瞬时。
assert_eq!(ymd_from_mjd(day + 1.25), (2024, 6, 16)); assert_eq!(ymd_from_mjd(day + 1.25), (2024, 6, 16));
} }
@ -49,6 +53,7 @@ fn leap_years_follow_the_gregorian_rule() {
assert!(!is_leap_year(2100)); assert!(!is_leap_year(2100));
assert!(!is_leap_year(2023)); assert!(!is_leap_year(2023));
// Year zero is a leap year in the proleptic calendar. // Year zero is a leap year in the proleptic calendar.
// 在外推日历中零年是闰年。
assert!(is_leap_year(0)); assert!(is_leap_year(0));
} }
@ -91,6 +96,7 @@ fn validation_rejects_impossible_dates_and_the_leap_second_label() {
assert!(!Calendar::new(2024, 1, 1, 0, 60, 0.0).is_valid()); assert!(!Calendar::new(2024, 1, 1, 0, 60, 0.0).is_valid());
// Second 60 is the leap second label, which is not a representable // Second 60 is the leap second label, which is not a representable
// instant on the continuous Modified Julian Date scale. // instant on the continuous Modified Julian Date scale.
// 第 60 秒是闰秒标签,它在连续的约化儒略日尺度上不是可表示的瞬时。
assert!(!Calendar::new(2016, 12, 31, 23, 59, 60.0).is_valid()); assert!(!Calendar::new(2016, 12, 31, 23, 59, 60.0).is_valid());
assert!(!Calendar::new(2024, 1, 1, 0, 0, 61.0).is_valid()); assert!(!Calendar::new(2024, 1, 1, 0, 0, 61.0).is_valid());
} }

View File

@ -1,4 +1,6 @@
//! Tests for the instant type and its scale conversions. //! Tests for the instant type and its scale conversions.
//!
//! 瞬时类型及其时标换算的测试。
use apsis_time::{Calendar, Epoch, SECONDS_PER_DAY, TAI_MINUS_GPS, TT_MINUS_TAI, TimeScale}; use apsis_time::{Calendar, Epoch, SECONDS_PER_DAY, TAI_MINUS_GPS, TT_MINUS_TAI, TimeScale};
@ -8,6 +10,12 @@ use apsis_time::{Calendar, Epoch, SECONDS_PER_DAY, TAI_MINUS_GPS, TT_MINUS_TAI,
/// about 1e-11 days, roughly one microsecond. Conversions that add or /// about 1e-11 days, roughly one microsecond. Conversions that add or
/// remove a scale offset each introduce one rounding at that resolution, so /// remove a scale offset each introduce one rounding at that resolution, so
/// comparisons are made at ten microseconds to leave room for a few steps. /// comparisons are made at ten microseconds to leave room for a few steps.
///
/// 当今附近的约化儒略日比较的容差。
///
/// 双精度把接近 60 000 的约化儒略日保持到约 1e-11 天的分辨率,大致一微秒。每次
/// 加上或减去时标偏移的换算都会在该分辨率上引入一次舍入,故比较取十微秒,为若干
/// 步留出余量。
const MJD_TOL: f64 = 1e-9; const MJD_TOL: f64 = 1e-9;
fn assert_mjd_close(actual: f64, expected: f64) { fn assert_mjd_close(actual: f64, expected: f64) {
@ -20,6 +28,7 @@ fn assert_mjd_close(actual: f64, expected: f64) {
#[test] #[test]
fn j2000_constant_lands_on_the_defining_instant() { fn j2000_constant_lands_on_the_defining_instant() {
// 2000-01-01T12:00:00 TT is Julian Date 2451545.0, or MJD 51544.5. // 2000-01-01T12:00:00 TT is Julian Date 2451545.0, or MJD 51544.5.
// 2000-01-01T12:00:00 TT 是儒略日 2451545.0,即 MJD 51544.5。
let tt = Epoch::J2000 let tt = Epoch::J2000
.mjd(TimeScale::Tt) .mjd(TimeScale::Tt)
.expect("TT is always available"); .expect("TT is always available");
@ -43,6 +52,7 @@ fn uniform_scales_differ_by_their_defining_offsets() {
fn construction_from_each_scale_round_trips() { fn construction_from_each_scale_round_trips() {
// 60 000.5 is a mid-day instant, which keeps the fractional part of // 60 000.5 is a mid-day instant, which keeps the fractional part of
// every intermediate value exactly representable. // every intermediate value exactly representable.
// 60 000.5 是一个日中的瞬时,这使每个中间值的零头都精确可表示。
let input = 60_000.5; let input = 60_000.5;
for scale in [ for scale in [
TimeScale::Tai, TimeScale::Tai,
@ -53,6 +63,7 @@ fn construction_from_each_scale_round_trips() {
let epoch = Epoch::from_mjd(input, scale).expect("instant is inside the leap table"); let epoch = Epoch::from_mjd(input, scale).expect("instant is inside the leap table");
// Reading the instant back on the same scale recovers the number // Reading the instant back on the same scale recovers the number
// that was supplied. // that was supplied.
// 在同一时标上读回该瞬时,恢复了所提供的那一数。
assert_mjd_close(epoch.mjd(scale).expect("scale is available"), input); assert_mjd_close(epoch.mjd(scale).expect("scale is available"), input);
} }
} }
@ -60,6 +71,7 @@ fn construction_from_each_scale_round_trips() {
#[test] #[test]
fn utc_construction_applies_the_leap_second_offset() { fn utc_construction_applies_the_leap_second_offset() {
// In 2017 the offset TAI - UTC is 37 seconds. // In 2017 the offset TAI - UTC is 37 seconds.
// 2017 年 TAI - UTC 的偏移是 37 秒。
let utc = 60_000.0; let utc = 60_000.0;
let epoch = Epoch::from_mjd(utc, TimeScale::Utc).expect("covered by the table"); let epoch = Epoch::from_mjd(utc, TimeScale::Utc).expect("covered by the table");
assert_mjd_close(epoch.tai_mjd(), utc + 37.0 / SECONDS_PER_DAY); assert_mjd_close(epoch.tai_mjd(), utc + 37.0 / SECONDS_PER_DAY);
@ -76,6 +88,7 @@ fn utc_conversion_is_rejected_before_the_table_begins() {
let before = 40_000.0; let before = 40_000.0;
assert_eq!(Epoch::from_mjd(before, TimeScale::Utc), None); assert_eq!(Epoch::from_mjd(before, TimeScale::Utc), None);
// The same instant is representable in TAI. // The same instant is representable in TAI.
// 同一瞬时在 TAI 中可表示。
let epoch = Epoch::from_mjd(before, TimeScale::Tai).expect("TAI is always available"); let epoch = Epoch::from_mjd(before, TimeScale::Tai).expect("TAI is always available");
assert_eq!(epoch.mjd(TimeScale::Utc), None); assert_eq!(epoch.mjd(TimeScale::Utc), None);
} }
@ -109,6 +122,7 @@ fn seconds_until_is_independent_of_the_scale_used() {
.expect("covered by the table"); .expect("covered by the table");
// One calendar day in early 2024 contains no leap second, so the // One calendar day in early 2024 contains no leap second, so the
// elapsed time is exactly 86 400 seconds. // elapsed time is exactly 86 400 seconds.
// 2024 年初的一个日历日不含闰秒,故流逝时间恰好是 86 400 秒。
assert!((start.seconds_until(end) - 86_400.0).abs() < 1e-6); assert!((start.seconds_until(end) - 86_400.0).abs() < 1e-6);
assert!((end.seconds_until(start) + 86_400.0).abs() < 1e-6); assert!((end.seconds_until(start) + 86_400.0).abs() < 1e-6);
} }
@ -127,10 +141,13 @@ fn add_seconds_crosses_a_leap_second_as_elapsed_time() {
// 2016-12-31T23:59:59.5 UTC, half a second before the inserted leap // 2016-12-31T23:59:59.5 UTC, half a second before the inserted leap
// second. The leap second makes the next UTC midnight 1.5 SI seconds // second. The leap second makes the next UTC midnight 1.5 SI seconds
// away, because the interval labelled 23:59:60 is a whole second long. // away, because the interval labelled 23:59:60 is a whole second long.
// 2016-12-31T23:59:59.5 UTC,即所插入闰秒之前的半秒。该闰秒使下一个 UTC 午夜
// 在 1.5 SI 秒之外,因为标为 23:59:60 的区间整整一秒长。
let before = Epoch::from_calendar(Calendar::new(2016, 12, 31, 23, 59, 59.5), TimeScale::Utc) let before = Epoch::from_calendar(Calendar::new(2016, 12, 31, 23, 59, 59.5), TimeScale::Utc)
.expect("covered by the table"); .expect("covered by the table");
// One and a half seconds of elapsed time reaches the next midnight. // One and a half seconds of elapsed time reaches the next midnight.
// 一秒半的流逝时间到达下一个午夜。
let after = before.add_seconds(1.5); let after = before.add_seconds(1.5);
let utc = after let utc = after
.calendar(TimeScale::Utc) .calendar(TimeScale::Utc)
@ -141,10 +158,13 @@ fn add_seconds_crosses_a_leap_second_as_elapsed_time() {
// An instant strictly inside the leap second has no UTC label on the // An instant strictly inside the leap second has no UTC label on the
// continuous Modified Julian Date scale, because it would be written // continuous Modified Julian Date scale, because it would be written
// 23:59:60 and that value does not exist there. // 23:59:60 and that value does not exist there.
// 严格位于闰秒内部的瞬时,在连续的约化儒略日尺度上没有 UTC 标签,因为它会被
// 写成 23:59:60,而该值在那里不存在。
assert_eq!(before.add_seconds(1.0).mjd(TimeScale::Utc), None); assert_eq!(before.add_seconds(1.0).mjd(TimeScale::Utc), None);
// Reading the interval on the UTC clock shows half a second of clock // Reading the interval on the UTC clock shows half a second of clock
// advance over one and a half seconds of elapsed time. // advance over one and a half seconds of elapsed time.
// 在 UTC 时钟上读该区间,显示一秒半的流逝时间里时钟推进了半秒。
let utc_before = before.mjd(TimeScale::Utc).expect("covered by the table"); let utc_before = before.mjd(TimeScale::Utc).expect("covered by the table");
let utc_after = after.mjd(TimeScale::Utc).expect("covered by the table"); let utc_after = after.mjd(TimeScale::Utc).expect("covered by the table");
let clock_advance = (utc_after - utc_before) * SECONDS_PER_DAY; let clock_advance = (utc_after - utc_before) * SECONDS_PER_DAY;
@ -164,6 +184,7 @@ fn ordering_follows_the_instant() {
fn equality_is_scale_independent() { fn equality_is_scale_independent() {
// The same physical instant described in two scales yields equal // The same physical instant described in two scales yields equal
// epochs. // epochs.
// 在两种时标中描述的同一物理瞬时给出相等的历元。
let in_tai = Epoch::from_mjd(60_000.0, TimeScale::Tai).expect("TAI is always available"); let in_tai = Epoch::from_mjd(60_000.0, TimeScale::Tai).expect("TAI is always available");
let in_tt = Epoch::from_mjd(60_000.0 + TT_MINUS_TAI / SECONDS_PER_DAY, TimeScale::Tt) let in_tt = Epoch::from_mjd(60_000.0 + TT_MINUS_TAI / SECONDS_PER_DAY, TimeScale::Tt)
.expect("TT is always available"); .expect("TT is always available");

View File

@ -1,10 +1,13 @@
//! Tests for the Julian Date conversions. //! Tests for the Julian Date conversions.
//!
//! 儒略日换算的测试。
use apsis_time::{JULIAN_DATE_OFFSET, julian_date_from_mjd, mjd_from_julian_date}; use apsis_time::{JULIAN_DATE_OFFSET, julian_date_from_mjd, mjd_from_julian_date};
#[test] #[test]
fn the_offset_matches_the_defining_values() { fn the_offset_matches_the_defining_values() {
// MJD 0 is Julian Date 2400000.5. // MJD 0 is Julian Date 2400000.5.
// MJD 0 是儒略日 2400000.5。
assert_eq!(JULIAN_DATE_OFFSET, 2_400_000.5); assert_eq!(JULIAN_DATE_OFFSET, 2_400_000.5);
assert_eq!(julian_date_from_mjd(0.0), 2_400_000.5); assert_eq!(julian_date_from_mjd(0.0), 2_400_000.5);
assert_eq!(mjd_from_julian_date(2_400_000.5), 0.0); assert_eq!(mjd_from_julian_date(2_400_000.5), 0.0);
@ -13,6 +16,7 @@ fn the_offset_matches_the_defining_values() {
#[test] #[test]
fn j2000_maps_to_the_expected_julian_date() { fn j2000_maps_to_the_expected_julian_date() {
// J2000.0 is MJD 51544.5 and Julian Date 2451545.0. // J2000.0 is MJD 51544.5 and Julian Date 2451545.0.
// J2000.0 是 MJD 51544.5 与儒略日 2451545.0。
assert_eq!(julian_date_from_mjd(51_544.5), 2_451_545.0); assert_eq!(julian_date_from_mjd(51_544.5), 2_451_545.0);
assert_eq!(mjd_from_julian_date(2_451_545.0), 51_544.5); assert_eq!(mjd_from_julian_date(2_451_545.0), 51_544.5);
} }

View File

@ -1,4 +1,6 @@
//! Tests for the time scale definitions. //! Tests for the time scale definitions.
//!
//! 时标定义的测试。
use apsis_time::{TAI_MINUS_GPS, TT_MINUS_TAI, TimeScale}; use apsis_time::{TAI_MINUS_GPS, TT_MINUS_TAI, TimeScale};
@ -22,7 +24,9 @@ fn scales_report_their_names() {
#[test] #[test]
fn offsets_match_their_definitions() { fn offsets_match_their_definitions() {
// Terrestrial Time is ahead of TAI by the defining constant. // Terrestrial Time is ahead of TAI by the defining constant.
// 地球时比 TAI 领先一个定义常量。
assert_eq!(TT_MINUS_TAI, 32.184); assert_eq!(TT_MINUS_TAI, 32.184);
// GPS time lags TAI by the defining integer number of seconds. // GPS time lags TAI by the defining integer number of seconds.
// GPS 时比 TAI 落后一个定义整数秒数。
assert_eq!(TAI_MINUS_GPS, 19.0); assert_eq!(TAI_MINUS_GPS, 19.0);
} }