docs: bilingual comments for the foundation and physics tests
This commit is contained in:
parent
a4f5a30aa9
commit
1ffba97a14
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for the physical and astronomical constants.
|
||||
//!
|
||||
//! 物理与天文常量的测试。
|
||||
|
||||
use apsis_core::constant::*;
|
||||
|
||||
|
|
@ -23,6 +25,7 @@ fn time_constants_are_consistent() {
|
|||
#[test]
|
||||
fn j2000_minus_unix_epoch_is_the_expected_span() {
|
||||
// The two epochs are 10 957 days apart.
|
||||
// 这两个历元相隔 10 957 天。
|
||||
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, 946_728_000.0);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for the workspace error type and result alias.
|
||||
//!
|
||||
//! 工作区错误类型与结果别名的测试。
|
||||
|
||||
use apsis_core::{Error, ErrorKind, Result};
|
||||
use core::fmt;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for the unit and quantity marker traits.
|
||||
//!
|
||||
//! 单位与量标记 trait 的测试。
|
||||
|
||||
use apsis_core::{Quantity, Unit};
|
||||
use core::fmt;
|
||||
|
|
|
|||
|
|
@ -3,11 +3,18 @@
|
|||
//! These tests record what `macro_rules!` can express for the workspace. They
|
||||
//! are kept as executable documentation of the decision to leave this crate
|
||||
//! empty for now: see the crate documentation.
|
||||
//!
|
||||
//! 内建宏设施探查。
|
||||
//!
|
||||
//! 这些测试记录 `macro_rules!` 能为工作区表达什么。它们被保留为「目前让本 crate
|
||||
//! 保持为空」这一决定的可执行文档:见 crate 文档。
|
||||
|
||||
#[test]
|
||||
fn builtin_macros_can_substitute_a_type_and_a_literal() {
|
||||
// A `macro_rules!` macro can paste a type and a literal together, which
|
||||
// is the only form the workspace needs.
|
||||
// `macro_rules!` 宏可以把一个类型与一个字面量拼在一起,这是本工作区唯一需要的
|
||||
// 形式。
|
||||
macro_rules! quantity {
|
||||
($quantity:ty, $value:literal) => {
|
||||
<$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` 没有 `new`,故本探查用一个有的类型。
|
||||
struct Meters(f64);
|
||||
impl Meters {
|
||||
const fn new(value: f64) -> Self {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for three by three matrices.
|
||||
//!
|
||||
//! 三阶矩阵的测试。
|
||||
|
||||
use apsis_math::{Mat3, Quat, Vec3};
|
||||
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::from_diagonal(Vec3::splat(2.0)).is_rotation(1e-9));
|
||||
// A reflection has determinant minus one.
|
||||
// 反射的行列式为负一。
|
||||
assert!(!Mat3::from_diagonal(Vec3::new(1.0, 1.0, -1.0)).is_rotation(1e-9));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@
|
|||
//! sequential answer rather than against a constant, and forces both the
|
||||
//! single and multi thread paths by choosing lengths that fall on each side
|
||||
//! of the threshold.
|
||||
//!
|
||||
//! 作用域并行辅助函数的测试。
|
||||
//!
|
||||
//! 核心性质是并行运行与串行运行产生相同的值,因为这些辅助函数的全部意义就是作为
|
||||
//! 它们所替代的顺序循环的替代品。故每个测试都与顺序答案比较而非与常量比较,并
|
||||
//! 通过选取落在阈值两侧的长度,强制走过单线程与多线程两条路径。
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
|
|
@ -20,6 +26,7 @@ fn the_thread_count_is_at_least_one() {
|
|||
fn small_work_uses_a_single_thread() {
|
||||
// Below the per thread threshold there is nothing to gain from starting
|
||||
// a thread, so the helper says so.
|
||||
// 低于每线程阈值时,启动线程没有任何收益,故该辅助函数如此表示。
|
||||
assert_eq!(threads_for(0), 1);
|
||||
assert_eq!(threads_for(1), 1);
|
||||
assert_eq!(threads_for(64), 1);
|
||||
|
|
@ -31,6 +38,7 @@ fn large_work_uses_more_than_one_thread() {
|
|||
if count > 1 {
|
||||
assert!(threads_for(1_000_000) > 1);
|
||||
// Never more threads than there are items.
|
||||
// 线程数绝不超过项数。
|
||||
assert!(threads_for(10) <= 10);
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +75,7 @@ fn the_parallel_result_matches_the_sequential_one() {
|
|||
fn the_output_is_in_index_order() {
|
||||
// Threads finish out of order, so this checks that the helper restores
|
||||
// index order rather than completion order.
|
||||
// 各线程完成次序不定,故这检查该辅助函数恢复的是下标次序而非完成次序。
|
||||
let len = 200_000;
|
||||
let values = map_index(len, |index| index);
|
||||
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
|
||||
// produce one value per element. `map_index` owns the output buffer, so
|
||||
// no shared mutable state is needed.
|
||||
// 本工作区中每个调用方所用的形状:读取共享切片并为每个元素产生一个值。
|
||||
// `map_index` 拥有输出缓冲,故无需共享可变状态。
|
||||
let input: Vec<f64> = (0..100_000).map(f64::from).collect();
|
||||
let output = map_index(input.len(), |index| input[index] * input[index]);
|
||||
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() {
|
||||
// Writing to a shared counter needs atomics; this checks the helper
|
||||
// tolerates that, since summing a large range is a common pattern.
|
||||
// 写共享计数器需要原子操作;这检查该辅助函数容忍它,因为对大区间求和是常见
|
||||
// 模式。
|
||||
use std::sync::atomic::AtomicU64;
|
||||
let total = AtomicU64::new(0);
|
||||
let len = 200_000_u64;
|
||||
|
|
@ -105,6 +118,7 @@ fn an_empty_range_does_nothing() {
|
|||
let empty: Vec<f64> = map_index(0, |_| 1.0);
|
||||
assert!(empty.is_empty());
|
||||
// The index form accepts an empty range without spawning anything.
|
||||
// 索引形式接受空区间而不创建任何线程。
|
||||
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() {
|
||||
// A length that is not a multiple of the thread count is where an
|
||||
// off by one in the chunking would drop the tail.
|
||||
// 长度不是线程数的整数倍之处,正是分块中差一会丢掉尾部的地方。
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
for &len in &[1_001_usize, 4_097, 65_537, 100_003] {
|
||||
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() {
|
||||
// A string is not `Copy`, so this exercises the unwrap-and-move path that
|
||||
// the parallel branch uses.
|
||||
// 字符串不是 `Copy`,故这检验并行分支所用的解包并移动路径。
|
||||
let values = map_index(10_000, |index| format!("item-{index}"));
|
||||
assert_eq!(values[0], "item-0");
|
||||
assert_eq!(values[9_999], "item-9999");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for unit quaternion rotations.
|
||||
//!
|
||||
//! 单位四元数转动的测试。
|
||||
|
||||
use apsis_math::{Mat3, Quat, Vec3};
|
||||
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 v = Vec3::new(1.0, 0.0, 0.0);
|
||||
// `second * first` applies `first` first, then `second`.
|
||||
// `second * first` 先施加 `first`,再施加 `second`。
|
||||
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 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` 与 `-q` 是同一转动,故通过其效果比较。
|
||||
let v = Vec3::new(1.0, -2.0, 3.0);
|
||||
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() {
|
||||
// These angles exercise the trace branch and each of the three
|
||||
// largest-diagonal branches.
|
||||
// 这些角检验迹分支以及三个最大对角分支中的每一个。
|
||||
for (axis, angle) in [
|
||||
(Vec3::new(1.0, 0.0, 0.0), 0.2),
|
||||
(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);
|
||||
// The two quaternions describe the same rotation, so any
|
||||
// interpolation between them must stay at the identity.
|
||||
// 这两个四元数描述同一转动,故它们之间的任何插值都必须保持为恒等。
|
||||
let middle = start.slerp(end, 0.5);
|
||||
assert_close(middle.rotate(Vec3::X), Vec3::X);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for three component vectors.
|
||||
//!
|
||||
//! 三分量矢量的测试。
|
||||
|
||||
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::Z.cross(Vec3::X), Vec3::Y);
|
||||
// The cross product of parallel vectors vanishes.
|
||||
// 平行矢量的叉积为零。
|
||||
assert_eq!(Vec3::X.cross(Vec3::X), Vec3::ZERO);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for plane angle quantities.
|
||||
//!
|
||||
//! 平面角量的测试。
|
||||
|
||||
use apsis_core::{Quantity, Unit};
|
||||
use apsis_units::dimension::Angle;
|
||||
|
|
@ -35,6 +37,7 @@ fn wrap_places_angles_in_the_canonical_range() {
|
|||
assert!(wrapped.value() < PI);
|
||||
// Wrapping shifts by whole turns, so the angle is preserved
|
||||
// modulo two pi.
|
||||
// 环绕按整周平移,故该角在模 2 pi 的意义下保持。
|
||||
let difference = angle.value() - wrapped.value();
|
||||
assert!((difference / (2.0 * PI)).fract().abs() < 1e-12);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for frequency quantities.
|
||||
//!
|
||||
//! 频率量的测试。
|
||||
|
||||
use apsis_core::Unit;
|
||||
use apsis_units::Hertz;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for length quantities.
|
||||
//!
|
||||
//! 长度量的测试。
|
||||
|
||||
use apsis_core::{Quantity, Unit};
|
||||
use apsis_units::dimension::Length;
|
||||
|
|
@ -38,6 +40,7 @@ fn units_report_their_metadata() {
|
|||
assert_eq!(Kilometers::factor_to_base(), 1_000.0);
|
||||
// Both units measure the same dimension, which is what makes the
|
||||
// generic conversion above type check.
|
||||
// 两种单位测量同一量纲,这正是使上面的泛型换算通过类型检查的原因。
|
||||
fn assert_length<U: Unit<Dimension = Length>>() {}
|
||||
assert_length::<Meters>();
|
||||
assert_length::<Kilometers>();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for thermodynamic temperature quantities.
|
||||
//!
|
||||
//! 热力学温度量的测试。
|
||||
|
||||
use apsis_core::Unit;
|
||||
use apsis_core::constant::ZERO_CELSIUS_IN_KELVIN;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for the duration quantity.
|
||||
//!
|
||||
//! 时长量的测试。
|
||||
|
||||
use apsis_core::Unit;
|
||||
use apsis_units::Seconds;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for the frame carrying position types.
|
||||
//!
|
||||
//! 携带坐标架的位置类型的测试。
|
||||
|
||||
use apsis_coord::{EarthRotationAngle, Ecef, Eci, Geodetic, WGS84};
|
||||
use apsis_math::Vec3;
|
||||
|
|
@ -67,6 +69,7 @@ fn frame_conversion_preserves_length() {
|
|||
fn a_ground_point_rotates_with_the_earth() {
|
||||
// A point on the equator at zero longitude in the Earth-fixed frame is
|
||||
// rotated by the Earth Rotation Angle in the inertial frame.
|
||||
// 地固系中赤道上零经度的一个点,在惯性系中按地球自转角被转动。
|
||||
let angle = EarthRotationAngle::from_ut1_days(1.5);
|
||||
let ground = Ecef::new(Vec3::new(6_378_137.0, 0.0, 0.0));
|
||||
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() {
|
||||
// A station is fixed to the ground, so converting it to the inertial
|
||||
// frame and back at the same angle returns the original position.
|
||||
// 地面站固定于地面,故把它转入惯性系再以相同角度转回,得到原位置。
|
||||
let station = Geodetic::new(0.662, 0.175, 80.0);
|
||||
let angle = EarthRotationAngle::from_ut1_julian_date(2_460_000.5);
|
||||
let inertial = station.to_ecef().to_eci(angle);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for the Earth Rotation Angle and the frame rotations.
|
||||
//!
|
||||
//! 地球自转角与坐标架转动的测试。
|
||||
|
||||
use apsis_frames::{
|
||||
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() {
|
||||
// 1.002 737 811 911 354 6 turns per day is 360.985 612 288 088 degrees
|
||||
// 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);
|
||||
let degrees = ERA_RATE_TURNS_PER_DAY * 360.0;
|
||||
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);
|
||||
// The angle advances a little under one degree per day more than a
|
||||
// full turn.
|
||||
// 该角每天推进一整圈再多略小于一度。
|
||||
let advance_degrees = (second.turns() - first.turns()) * 360.0 - 360.0;
|
||||
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() {
|
||||
// At J2000.0 the angle is 0.779 057 273 2640 turns, about 280.46
|
||||
// degrees.
|
||||
// 在 J2000.0 处该角为 0.779 057 273 2640 周,约 280.46 度。
|
||||
let epoch = EarthRotationAngle::from_ut1_days(0.0);
|
||||
assert!((epoch.turns() - 0.779_057_273_264).abs() < 1e-15);
|
||||
let degrees = epoch.turns_positive() * 360.0;
|
||||
|
|
@ -45,6 +50,7 @@ fn epoch_angle_matches_the_published_constant() {
|
|||
#[test]
|
||||
fn julian_date_and_day_count_agree() {
|
||||
// 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_546.0), 1.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}");
|
||||
// Reduction removes whole turns, so the angle is unchanged modulo
|
||||
// one turn.
|
||||
// 归约去掉整周,故该角在模一周的意义下不变。
|
||||
let difference = angle.turns() - turns;
|
||||
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
|
||||
// drifts westward when read in the Earth-fixed frame. In the
|
||||
// Earth-fixed frame that is toward negative second axis.
|
||||
// 地球向东转,故在惯性空间中固定的方向在地固系中读数时向西漂移。在地固系中
|
||||
// 即朝负第二轴方向。
|
||||
let angle = EarthRotationAngle::from_radians(0.25);
|
||||
let rotation = rotation_eci_to_ecef(angle);
|
||||
let rotated = rotation * Vec3::X;
|
||||
|
|
@ -103,6 +112,7 @@ fn opposite_rotations_cancel() {
|
|||
let identity = forward * backward;
|
||||
assert!(identity.is_rotation(1e-12));
|
||||
// Applying one after the other returns the original vector.
|
||||
// 先后施加二者返回原矢量。
|
||||
let vector = Vec3::new(1.5, -2.5, 3.5);
|
||||
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() {
|
||||
// The companion to the westward drift test: the second axis of the
|
||||
// inertial frame maps toward the first axis of the Earth-fixed frame.
|
||||
// 向西漂移测试的对应项:惯性系的第二轴映射向地固系的第一轴。
|
||||
let angle = EarthRotationAngle::from_radians(0.25);
|
||||
let rotation = rotation_eci_to_ecef(angle);
|
||||
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() {
|
||||
// The rotation depends on the angle modulo one turn, so angles that
|
||||
// differ by whole turns give the same matrix.
|
||||
// 该转动依模一周的角而定,故相差整周的角给出相同的矩阵。
|
||||
let first = rotation_eci_to_ecef(EarthRotationAngle::from_turns(0.25));
|
||||
let second = rotation_eci_to_ecef(EarthRotationAngle::from_turns(2.25));
|
||||
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() {
|
||||
// An angle of zero, constructed directly rather than from the epoch
|
||||
// constant, is the identity rotation.
|
||||
// 直接构建的(而非由历元常量得来的)零角是恒等转动。
|
||||
let rotation = rotation_eci_to_ecef(EarthRotationAngle::from_turns(0.0));
|
||||
let vector = Vec3::new(1.0, 2.0, 3.0);
|
||||
assert!((rotation * vector - vector).norm() < EPS);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@
|
|||
//! 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
|
||||
//! assertion.
|
||||
//!
|
||||
//! IAU 2000/2006 地球定向模型的测试。
|
||||
//!
|
||||
//! 期望值是 IAU SOFA 库及其 ERFA 衍生物对相同瞬时的值。它们被记录在此,使对某个
|
||||
//! 系数或转动约定的改动表现为断言失败。
|
||||
|
||||
use apsis_frames::iau2000::{
|
||||
PolarMotion, bias_precession_nutation_matrix, celestial_to_terrestrial_matrix,
|
||||
|
|
@ -14,10 +19,14 @@ use apsis_frames::iau2000::{
|
|||
use core::f64::consts::PI;
|
||||
|
||||
/// Converts arcseconds to radians.
|
||||
///
|
||||
/// 把角秒换算为弧度。
|
||||
const ARCSECOND: f64 = PI / (180.0 * 3_600.0);
|
||||
|
||||
/// Returns the Terrestrial Time Julian date for a Modified Julian Date on
|
||||
/// the UTC scale, using the offsets in force from 2017.
|
||||
///
|
||||
/// 使用自 2017 年起生效的偏移,返回 UTC 时标上某约化儒略日对应的地球时儒略日。
|
||||
fn tt_julian_date(utc_mjd: f64) -> f64 {
|
||||
utc_mjd + 2_400_000.5 + 69.184 / 86_400.0
|
||||
}
|
||||
|
|
@ -25,6 +34,7 @@ fn tt_julian_date(utc_mjd: f64) -> f64 {
|
|||
#[test]
|
||||
fn the_mean_obliquity_at_j2000_matches_the_reference() {
|
||||
// The IAU 2006 obliquity at J2000.0 is 84381.406 arcseconds.
|
||||
// J2000.0 处的 IAU 2006 黄赤交角为 84381.406 角秒。
|
||||
let obliquity = mean_obliquity(0.0);
|
||||
assert!(
|
||||
(obliquity / ARCSECOND - 84_381.406).abs() < 1e-9,
|
||||
|
|
@ -36,6 +46,7 @@ fn the_mean_obliquity_at_j2000_matches_the_reference() {
|
|||
#[test]
|
||||
fn the_nutation_at_j2000_matches_the_reference() {
|
||||
// The IAU 2000A nutation at J2000.0, from ERFA.
|
||||
// J2000.0 处的 IAU 2000A 章动,取自 ERFA。
|
||||
let t = 0.0;
|
||||
let nutation = nutation_2000a(t);
|
||||
assert!(
|
||||
|
|
@ -54,6 +65,7 @@ fn the_nutation_at_j2000_matches_the_reference() {
|
|||
fn the_nutation_2006_adjustment_is_a_scaling() {
|
||||
// The IAU 2006 adjustment scales the IAU 2000A nutation rather than
|
||||
// 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] {
|
||||
let base = nutation_2000a(t);
|
||||
let adjusted = nutation_2000a_with_2006_adjustment(t);
|
||||
|
|
@ -75,6 +87,7 @@ fn the_nutation_2006_adjustment_is_a_scaling() {
|
|||
#[test]
|
||||
fn the_nutation_matches_the_reference_over_a_century() {
|
||||
// Recorded from ERFA at each date. The values are in radians.
|
||||
// 在每个日期从 ERFA 记录。各值以弧度计。
|
||||
let cases = [
|
||||
(40_587.0, 2.1896610891495316e-05, 4.070477585626008e-05),
|
||||
(51_544.5, -6.754419567422628e-05, -2.797091091919891e-05),
|
||||
|
|
@ -100,6 +113,7 @@ fn the_nutation_matches_the_reference_over_a_century() {
|
|||
#[test]
|
||||
fn the_precession_angles_at_j2000_match_the_reference() {
|
||||
// The P03 precession angles at J2000.0.
|
||||
// J2000.0 处的 P03 岁差角。
|
||||
let angles = precession_angles(0.0);
|
||||
assert!(
|
||||
(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() {
|
||||
// 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.
|
||||
// 地球时时标上 J2000.0 处的 GCRS 到该日真坐标矩阵,取自 ERFA 的 `pnm06a`。
|
||||
// 各值是第一行。
|
||||
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 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() {
|
||||
// The matrix at J2000.0 on the Terrestrial Time scale, from ERFA with
|
||||
// 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 matrix = celestial_to_terrestrial_matrix(tt, tt, PolarMotion::NONE);
|
||||
|
||||
// The first row, from ERFA's `c2t06a` composed with the equinox based
|
||||
// sidereal time.
|
||||
// 第一行,取自 ERFA 的 `c2t06a` 与基于春分点的恒星时之合成。
|
||||
let expected_first_row = [
|
||||
0.18651846184811946,
|
||||
-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
|
||||
// not be dropped: their omission would show as a millisecond of
|
||||
// sidereal time.
|
||||
// 补充项是几千分之一角秒,不得省略:略去它们会表现为一毫秒的恒星时。
|
||||
let t = 0.0;
|
||||
let complementary = equation_of_the_equinoxes_complementary_terms(t);
|
||||
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
|
||||
// of the Moon's ascending node.
|
||||
// 在 J2000.0 处首项是 2640.96 微角秒乘以月球升交点角的正弦。
|
||||
let expected = 1.021330096302465e-08;
|
||||
assert!(
|
||||
(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() {
|
||||
// The argument is (0.024381750 + 0.00000538691 t) t, so at small times
|
||||
// it grows linearly with the leading coefficient.
|
||||
// 该辐角为 (0.024381750 + 0.00000538691 t) t,故在小时间处它随首系数线性增长。
|
||||
for t in [0.1, 0.5, 1.0] {
|
||||
let value = general_precession(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
|
||||
// small amount each day. The small amount is about 3.94 minutes of
|
||||
// time, which is 0.9856 degrees.
|
||||
// 格林尼治视恒星时每天必须增加一整圈再加上一小量。该小量约为 3.94 分钟时间,
|
||||
// 即 0.9856 度。
|
||||
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 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() {
|
||||
// A polar motion of one arcsecond must tilt the matrix by about that
|
||||
// much, which is 4.85 microradians.
|
||||
// 一角秒的极移必须使矩阵倾斜约那么多,即 4.85 微弧度。
|
||||
let motion = PolarMotion::new(ARCSECOND, ARCSECOND, 0.0);
|
||||
let matrix = motion.matrix();
|
||||
assert!(matrix.is_rotation(1e-12));
|
||||
// The off-diagonal term is the sine of the displacement.
|
||||
// 非对角项是该位移的正弦。
|
||||
let value = matrix.get(0, 2).expect("the index is in range");
|
||||
assert!(
|
||||
(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() {
|
||||
// Applying a polar motion must change the terrestrial position of a
|
||||
// fixed inertial direction.
|
||||
// 施加极移必须改变某固定惯性方向的地球位置。
|
||||
let tt = 60_000.0;
|
||||
let without = celestial_to_terrestrial_matrix(tt, tt, PolarMotion::NONE);
|
||||
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;
|
||||
// The two must differ, and the difference must be of the order of the
|
||||
// applied angles.
|
||||
// 二者必须不同,且差值必须与所施加的角度同量级。
|
||||
let mut largest = 0.0_f64;
|
||||
for row 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
|
||||
// nutation in obliquity about nine, so over a range of dates the
|
||||
// largest value found must be in that range.
|
||||
// 黄经章动达约十七角秒,黄赤交角章动约九角秒,故在一段日期上所能找到的最大值
|
||||
// 必须落在该范围内。
|
||||
let mut largest_longitude = 0.0_f64;
|
||||
let mut largest_obliquity = 0.0_f64;
|
||||
for step in 0..400 {
|
||||
// A span of about fifty years in steps of a fortnight.
|
||||
// 约五十年、步长为两周的跨度。
|
||||
let t = f64::from(step) * 0.125;
|
||||
let nutation = nutation_2000a(t);
|
||||
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
|
||||
// year, so the value must return close to its initial value after
|
||||
// eighteen years and must oscillate in between.
|
||||
// 主导项的周期约为十八年与半年,故该值必须在十八年后回到接近其初值,并在其间
|
||||
// 振荡。
|
||||
let initial = nutation_2000a(0.0).longitude;
|
||||
let after_eighteen_years = nutation_2000a(0.18).longitude;
|
||||
assert!(
|
||||
|
|
@ -353,6 +385,7 @@ fn the_nutation_is_periodic() {
|
|||
);
|
||||
|
||||
// The sign must change at some point within that span.
|
||||
// 符号必须在该跨度内某处改变。
|
||||
let mut seen_positive = false;
|
||||
let mut seen_negative = false;
|
||||
for step in 0..200 {
|
||||
|
|
@ -368,6 +401,7 @@ fn the_nutation_is_periodic() {
|
|||
fn the_equation_of_the_equinoxes_follows_the_nutation() {
|
||||
// The equation of the equinoxes is dominated by the nutation in
|
||||
// longitude projected onto the true equator, so it shares its sign.
|
||||
// 分点方程式主要由投影到真赤道上的黄经章动主导,故它与后者同号。
|
||||
for t in [0.02, 0.05, 0.1, 0.2] {
|
||||
let nutation = nutation_2000a_with_2006_adjustment(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() {
|
||||
// The precession over a century is about 1.4 degrees, so the two
|
||||
// matrices must differ by an angle of that order.
|
||||
// 一世纪的岁差约为 1.4 度,故两个矩阵必须相差该量级的角。
|
||||
let first = bias_precession_nutation_matrix(0.0);
|
||||
let second = bias_precession_nutation_matrix(1.0);
|
||||
let difference = first.transpose() * second;
|
||||
// The trace of a rotation matrix is 1 + 2 cos(angle), so the angle
|
||||
// follows from it.
|
||||
// 转动矩阵的迹为 1 + 2 cos(angle),故该角由它得出。
|
||||
let cosine = ((difference.trace() - 1.0) / 2.0).clamp(-1.0, 1.0);
|
||||
let degrees = cosine.acos().to_degrees();
|
||||
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() {
|
||||
// Greenwich mean sidereal time from the 1982 expression, which is the
|
||||
// one the SGP4 theory was fitted against. The values are from ERFA.
|
||||
// 1982 表达式给出的格林尼治平恒星时,即 SGP4 理论所拟合者。各值取自 ERFA。
|
||||
let cases = [
|
||||
(51_544.5, 4.894_961_212_823_059),
|
||||
(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() {
|
||||
// Sidereal time advances by one turn plus the excess of the sidereal
|
||||
// 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 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);
|
||||
|
|
@ -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
|
||||
// rotation with the identity polar motion.
|
||||
// 参考值是把 ERFA 的 `gmst82` 作为第三轴转动、配以单位极移施加所得。
|
||||
let cases = [
|
||||
(
|
||||
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
|
||||
// the modern mean value, rotates the result by a fraction of a degree,
|
||||
// which is many kilometres of ground position.
|
||||
// 该区别要紧。TEME 的第一轴是平春分点,故它与平恒星时配对。换成视值或现代平值
|
||||
// 会把结果转动零点几度,即许多千米的地面位置。
|
||||
use apsis_frames::iau2000::{
|
||||
greenwich_apparent_sidereal_time, greenwich_mean_sidereal_time,
|
||||
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
|
||||
// few thousandths of a degree.
|
||||
// 视值包含分点方程式,即千分之几度。
|
||||
let equation = (apparent - modern_mean).abs().to_degrees();
|
||||
assert!(
|
||||
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
|
||||
// modern mean value by a fraction of a degree, because the precession
|
||||
// model changed between them.
|
||||
// TEME 所拟合的 1982 表达式与现代平值相差零点几度,因为二者之间的岁差模型
|
||||
// 改变了。
|
||||
let secular = (sidereal_1982 - modern_mean).abs().to_degrees();
|
||||
assert!(
|
||||
secular > 0.1 && secular < 1.0,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for the reference ellipsoid.
|
||||
//!
|
||||
//! 参考椭球的测试。
|
||||
|
||||
use apsis_geodesy::{Ellipsoid, WGS84};
|
||||
|
||||
|
|
@ -12,8 +14,10 @@ fn defining_parameters_match_the_standard() {
|
|||
#[test]
|
||||
fn derived_axes_match_the_published_values() {
|
||||
// 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);
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
|
@ -30,15 +34,18 @@ fn second_eccentricity_relates_to_the_first() {
|
|||
let second = WGS84.second_eccentricity_squared();
|
||||
assert!((second - first / (1.0 - first)).abs() < 1e-18);
|
||||
// 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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prime_vertical_radius_spans_the_expected_range() {
|
||||
// At the equator it equals the semi-major axis.
|
||||
// 在赤道处它等于半长轴。
|
||||
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),
|
||||
// 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);
|
||||
assert!((pole - WGS84.polar_radius_of_curvature()).abs() < 1e-9);
|
||||
assert!((pole - 6_399_593.625_9).abs() < 1e-3);
|
||||
|
|
@ -47,11 +54,13 @@ fn prime_vertical_radius_spans_the_expected_range() {
|
|||
#[test]
|
||||
fn meridian_radius_spans_the_expected_range() {
|
||||
// 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 expected = WGS84.semi_major_axis() * (1.0 - WGS84.eccentricity_squared());
|
||||
assert!((equator - expected).abs() < 1e-6);
|
||||
assert!((equator - 6_335_439.327_1).abs() < 1e-3);
|
||||
// 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);
|
||||
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);
|
||||
assert!(geocentric < geodetic);
|
||||
// The maximum separation is about 0.19 degrees at 45 degrees.
|
||||
// 最大差约在 45 度处为 0.19 度。
|
||||
let difference = (geodetic - geocentric).to_degrees();
|
||||
assert!((difference - 0.1924).abs() < 0.001, "got {difference}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
//! Tests for geodetic and Cartesian conversions.
|
||||
//!
|
||||
//! 大地与直角坐标换算的测试。
|
||||
|
||||
use apsis_geodesy::{Geodetic, WGS84, geodetic_from_cartesian};
|
||||
use apsis_math::Vec3;
|
||||
|
||||
/// Tolerance for position comparisons, in meters.
|
||||
///
|
||||
/// 位置比较的容差,单位为米。
|
||||
const POSITION_TOL: f64 = 1e-4;
|
||||
|
||||
/// Tolerance for latitude and longitude comparisons, in radians.
|
||||
///
|
||||
/// One microdegree is about 1.1e-8 radians, or roughly a centimeter on the
|
||||
/// Earth's surface.
|
||||
///
|
||||
/// 纬度与经度比较的容差,单位为弧度。
|
||||
///
|
||||
/// 一微度约为 1.1e-8 弧度,或地表上大致一厘米。
|
||||
const ANGLE_TOL: f64 = 1e-11;
|
||||
|
||||
#[test]
|
||||
|
|
@ -42,6 +50,7 @@ fn height_moves_the_point_along_the_ellipsoid_normal() {
|
|||
assert!((surface.distance(elevated) - 500.0).abs() < POSITION_TOL);
|
||||
// The normal at a non-polar latitude is not vertical, so the height
|
||||
// changes the horizontal components as well as the third one.
|
||||
// 非极地纬度处的法线并非竖直,故高度既改变第三分量也改变水平分量。
|
||||
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());
|
||||
assert_eq!(geodetic_from_cartesian(on_axis, WGS84), None);
|
||||
// The origin is also on the axis.
|
||||
// 原点也在该轴上。
|
||||
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
|
||||
// zero height, evaluated with the defining WGS 84 parameters in
|
||||
// extended precision.
|
||||
// 北纬 37.5 度、西经 121.333333... 度、零高度,用 WGS 84 定义参数以扩展精度
|
||||
// 求值。
|
||||
let point = Geodetic::new(
|
||||
37.5_f64.to_radians(),
|
||||
-121.333_333_333_333_33_f64.to_radians(),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for the leap second table.
|
||||
//!
|
||||
//! 闰秒表的测试。
|
||||
|
||||
use apsis_leap::{
|
||||
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() {
|
||||
// The table starts at the inherited offset of ten seconds and every
|
||||
// later announcement inserted exactly one second.
|
||||
// 该表始于继承而来的十秒偏移,其后每次宣布都恰好插入一秒。
|
||||
assert_eq!(LEAP_SECONDS[0].tai_minus_utc, 10.0);
|
||||
for pair in LEAP_SECONDS.windows(2) {
|
||||
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() {
|
||||
// Twenty seven leap seconds were introduced between 1972 and 2017
|
||||
// inclusive of the initial 1972-01-01 entry.
|
||||
// 1972 年到 2017 年间引入了二十七个闰秒,含 1972-01-01 的初项。
|
||||
assert_eq!(LEAP_SECONDS.len(), 28);
|
||||
}
|
||||
|
||||
|
|
@ -60,12 +64,16 @@ fn coverage_constants_match_the_table_boundaries() {
|
|||
#[test]
|
||||
fn known_calendar_dates_map_to_known_modified_julian_dates() {
|
||||
// MJD 0 is the start of 1858-11-17.
|
||||
// MJD 0 是 1858-11-17 的起点。
|
||||
assert_eq!(mjd_from_ymd(1858, 11, 17), 0.0);
|
||||
// The calendar day of J2000.0 is MJD 51544.
|
||||
// J2000.0 的日历日是 MJD 51544。
|
||||
assert_eq!(mjd_from_ymd(2000, 1, 1), 51_544.0);
|
||||
// The Unix epoch is MJD 40587.
|
||||
// Unix 历元是 MJD 40587。
|
||||
assert_eq!(mjd_from_ymd(1970, 1, 1), 40_587.0);
|
||||
// The last leap second.
|
||||
// 最后一个闰秒。
|
||||
assert_eq!(mjd_from_ymd(2017, 1, 1), 57_754.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]
|
||||
fn calendar_conversion_handles_leap_days_and_month_ends() {
|
||||
// 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(1900, 3, 1) - mjd_from_ymd(1900, 2, 1), 28.0);
|
||||
// Consecutive month ends are one day apart.
|
||||
// 连续的月末相隔一天。
|
||||
assert_eq!(mjd_from_ymd(2024, 3, 1) - mjd_from_ymd(2024, 2, 29), 1.0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for UTC and TAI conversions on the Modified Julian Date scale.
|
||||
//!
|
||||
//! 约化儒略日尺度上 UTC 与 TAI 换算的测试。
|
||||
|
||||
use apsis_leap::{
|
||||
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);
|
||||
assert_eq!(tai_minus_utc_at_mjd(start), Some(10.0));
|
||||
// Just before midnight one day earlier the table does not apply.
|
||||
// 再早一天、午夜之前,该表不适用。
|
||||
assert_eq!(tai_minus_utc_at_mjd(start - 1.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_changes_exactly_on_the_effective_date() {
|
||||
// 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);
|
||||
assert_eq!(tai_minus_utc_at_mjd(effective - 1.0), Some(36.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]
|
||||
fn offset_is_constant_between_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);
|
||||
assert_eq!(tai_minus_utc_at_mjd(instant), 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");
|
||||
// An MJD near 58000 has a resolution of about 7e-12 days, so the sum
|
||||
// can only be compared to well below a microsecond.
|
||||
// 接近 58000 的 MJD 分辨率约为 7e-12 天,故该和只能比较到远低于一微秒。
|
||||
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() {
|
||||
let effective = calendar_mjd(2017, 1, 1);
|
||||
// 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 after_utc = effective + 1.0 / 24.0;
|
||||
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() {
|
||||
// The first TAI instant that the table covers is 1972-01-01 00:00:10
|
||||
// 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 utc = utc_mjd_from_tai_mjd(boundary).expect("boundary is covered");
|
||||
assert!((utc - calendar_mjd(1972, 1, 1)).abs() < 1e-10);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for the calendar conversions.
|
||||
//!
|
||||
//! 日历换算的测试。
|
||||
|
||||
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]
|
||||
fn calendar_conversion_round_trips_over_a_long_span() {
|
||||
// Walk day by day over a span that crosses every leap year rule.
|
||||
// 逐日走过一个跨越每条闰年规则的跨度。
|
||||
let mut mjd = mjd_from_ymd(1899, 12, 20);
|
||||
let end = mjd_from_ymd(2101, 1, 10);
|
||||
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.75), (2024, 6, 15));
|
||||
// One day later, but still the same instant of the following day.
|
||||
// 晚一天,但仍是次日的同一瞬时。
|
||||
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(2023));
|
||||
// Year zero is a leap year in the proleptic calendar.
|
||||
// 在外推日历中零年是闰年。
|
||||
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());
|
||||
// Second 60 is the leap second label, which is not a representable
|
||||
// instant on the continuous Modified Julian Date scale.
|
||||
// 第 60 秒是闰秒标签,它在连续的约化儒略日尺度上不是可表示的瞬时。
|
||||
assert!(!Calendar::new(2016, 12, 31, 23, 59, 60.0).is_valid());
|
||||
assert!(!Calendar::new(2024, 1, 1, 0, 0, 61.0).is_valid());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for the instant type and its scale conversions.
|
||||
//!
|
||||
//! 瞬时类型及其时标换算的测试。
|
||||
|
||||
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
|
||||
/// 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.
|
||||
///
|
||||
/// 当今附近的约化儒略日比较的容差。
|
||||
///
|
||||
/// 双精度把接近 60 000 的约化儒略日保持到约 1e-11 天的分辨率,大致一微秒。每次
|
||||
/// 加上或减去时标偏移的换算都会在该分辨率上引入一次舍入,故比较取十微秒,为若干
|
||||
/// 步留出余量。
|
||||
const MJD_TOL: f64 = 1e-9;
|
||||
|
||||
fn assert_mjd_close(actual: f64, expected: f64) {
|
||||
|
|
@ -20,6 +28,7 @@ fn assert_mjd_close(actual: f64, expected: f64) {
|
|||
#[test]
|
||||
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 是儒略日 2451545.0,即 MJD 51544.5。
|
||||
let tt = Epoch::J2000
|
||||
.mjd(TimeScale::Tt)
|
||||
.expect("TT is always available");
|
||||
|
|
@ -43,6 +52,7 @@ fn uniform_scales_differ_by_their_defining_offsets() {
|
|||
fn construction_from_each_scale_round_trips() {
|
||||
// 60 000.5 is a mid-day instant, which keeps the fractional part of
|
||||
// every intermediate value exactly representable.
|
||||
// 60 000.5 是一个日中的瞬时,这使每个中间值的零头都精确可表示。
|
||||
let input = 60_000.5;
|
||||
for scale in [
|
||||
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");
|
||||
// Reading the instant back on the same scale recovers the number
|
||||
// that was supplied.
|
||||
// 在同一时标上读回该瞬时,恢复了所提供的那一数。
|
||||
assert_mjd_close(epoch.mjd(scale).expect("scale is available"), input);
|
||||
}
|
||||
}
|
||||
|
|
@ -60,6 +71,7 @@ fn construction_from_each_scale_round_trips() {
|
|||
#[test]
|
||||
fn utc_construction_applies_the_leap_second_offset() {
|
||||
// In 2017 the offset TAI - UTC is 37 seconds.
|
||||
// 2017 年 TAI - UTC 的偏移是 37 秒。
|
||||
let utc = 60_000.0;
|
||||
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);
|
||||
|
|
@ -76,6 +88,7 @@ fn utc_conversion_is_rejected_before_the_table_begins() {
|
|||
let before = 40_000.0;
|
||||
assert_eq!(Epoch::from_mjd(before, TimeScale::Utc), None);
|
||||
// The same instant is representable in TAI.
|
||||
// 同一瞬时在 TAI 中可表示。
|
||||
let epoch = Epoch::from_mjd(before, TimeScale::Tai).expect("TAI is always available");
|
||||
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");
|
||||
// One calendar day in early 2024 contains no leap second, so the
|
||||
// elapsed time is exactly 86 400 seconds.
|
||||
// 2024 年初的一个日历日不含闰秒,故流逝时间恰好是 86 400 秒。
|
||||
assert!((start.seconds_until(end) - 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
|
||||
// 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.
|
||||
// 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)
|
||||
.expect("covered by the table");
|
||||
|
||||
// One and a half seconds of elapsed time reaches the next midnight.
|
||||
// 一秒半的流逝时间到达下一个午夜。
|
||||
let after = before.add_seconds(1.5);
|
||||
let utc = after
|
||||
.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
|
||||
// continuous Modified Julian Date scale, because it would be written
|
||||
// 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);
|
||||
|
||||
// Reading the interval on the UTC clock shows half a second of clock
|
||||
// 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_after = after.mjd(TimeScale::Utc).expect("covered by the table");
|
||||
let clock_advance = (utc_after - utc_before) * SECONDS_PER_DAY;
|
||||
|
|
@ -164,6 +184,7 @@ fn ordering_follows_the_instant() {
|
|||
fn equality_is_scale_independent() {
|
||||
// The same physical instant described in two scales yields equal
|
||||
// epochs.
|
||||
// 在两种时标中描述的同一物理瞬时给出相等的历元。
|
||||
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)
|
||||
.expect("TT is always available");
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
//! Tests for the Julian Date conversions.
|
||||
//!
|
||||
//! 儒略日换算的测试。
|
||||
|
||||
use apsis_time::{JULIAN_DATE_OFFSET, julian_date_from_mjd, mjd_from_julian_date};
|
||||
|
||||
#[test]
|
||||
fn the_offset_matches_the_defining_values() {
|
||||
// MJD 0 is Julian Date 2400000.5.
|
||||
// MJD 0 是儒略日 2400000.5。
|
||||
assert_eq!(JULIAN_DATE_OFFSET, 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);
|
||||
|
|
@ -13,6 +16,7 @@ fn the_offset_matches_the_defining_values() {
|
|||
#[test]
|
||||
fn j2000_maps_to_the_expected_julian_date() {
|
||||
// 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!(mjd_from_julian_date(2_451_545.0), 51_544.5);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Tests for the time scale definitions.
|
||||
//!
|
||||
//! 时标定义的测试。
|
||||
|
||||
use apsis_time::{TAI_MINUS_GPS, TT_MINUS_TAI, TimeScale};
|
||||
|
||||
|
|
@ -22,7 +24,9 @@ fn scales_report_their_names() {
|
|||
#[test]
|
||||
fn offsets_match_their_definitions() {
|
||||
// Terrestrial Time is ahead of TAI by the defining constant.
|
||||
// 地球时比 TAI 领先一个定义常量。
|
||||
assert_eq!(TT_MINUS_TAI, 32.184);
|
||||
// GPS time lags TAI by the defining integer number of seconds.
|
||||
// GPS 时比 TAI 落后一个定义整数秒数。
|
||||
assert_eq!(TAI_MINUS_GPS, 19.0);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue