docs: bilingual comments for the orbit tests

This commit is contained in:
ApsisRS contributors 2026-10-02 01:56:29 +08:00
parent 1ffba97a14
commit 4f90474610
11 changed files with 193 additions and 0 deletions

View File

@ -1,4 +1,6 @@
//! Tests for the ephemeris table and its interpolation. //! Tests for the ephemeris table and its interpolation.
//!
//! 星历表及其插值的测试。
use apsis_ephemeris::{ use apsis_ephemeris::{
Ephemeris, EphemerisErrorKind, Sample, Sampler, hermite_position, hermite_state, Ephemeris, EphemerisErrorKind, Sample, Sampler, hermite_position, hermite_state,
@ -12,6 +14,11 @@ use apsis_math::Vec3;
/// A circular orbit is the useful case: the state is analytic, so the error /// A circular orbit is the useful case: the state is analytic, so the error
/// of the interpolation is exactly the difference from a known value, and /// of the interpolation is exactly the difference from a known value, and
/// the radius is constant, so a bad reconstruction shows up immediately. /// the radius is constant, so a bad reconstruction shows up immediately.
///
/// 插值所对照检查的圆轨道。
///
/// 圆轨道是有用的情形:状态是解析的,故插值误差恰是与已知值之差;半径恒定,故坏的
/// 重构会立即显现。
fn circular() -> KeplerianElements { fn circular() -> KeplerianElements {
KeplerianElements { KeplerianElements {
semi_major_axis: 7_000.0, semi_major_axis: 7_000.0,
@ -24,6 +31,8 @@ fn circular() -> KeplerianElements {
} }
/// Returns the exact state of the circular orbit at a time. /// Returns the exact state of the circular orbit at a time.
///
/// 返回该圆轨道在某时刻的精确状态。
fn exact_state(seconds: f64) -> Sample { fn exact_state(seconds: f64) -> Sample {
let elements = circular(); let elements = circular();
let mean = 2.0 * core::f64::consts::PI * seconds / elements.period(GravityParameter::WGS84); let mean = 2.0 * core::f64::consts::PI * seconds / elements.period(GravityParameter::WGS84);
@ -78,6 +87,7 @@ fn samples_must_be_in_increasing_time_order() {
// Equal times are also refused, because a zero width interval has no // Equal times are also refused, because a zero width interval has no
// interpolation to do. // interpolation to do.
// 相等的时间也被拒绝,因为零宽区间没有插值可做。
let samples = vec![ let samples = vec![
Sample::new(5.0, Vec3::ZERO, Vec3::ZERO), Sample::new(5.0, Vec3::ZERO, Vec3::ZERO),
Sample::new(5.0, Vec3::ZERO, Vec3::ZERO), Sample::new(5.0, Vec3::ZERO, Vec3::ZERO),
@ -105,6 +115,7 @@ fn a_query_at_a_sample_returns_it_exactly() {
.expect("the sample time is covered"); .expect("the sample time is covered");
// The interpolation must not perturb a sample, so the returned // The interpolation must not perturb a sample, so the returned
// state is bit for bit the stored one. // state is bit for bit the stored one.
// 插值不得扰动样本,故所返回的状态与所存者逐位相同。
assert_eq!(queried, *sample, "at {} s", sample.seconds); assert_eq!(queried, *sample, "at {} s", sample.seconds);
} }
} }
@ -115,9 +126,11 @@ fn a_query_between_samples_is_interpolated() {
let midpoint = ephemeris.state_at(30.0).expect("the time is covered"); let midpoint = ephemeris.state_at(30.0).expect("the time is covered");
assert_eq!(midpoint.seconds, 30.0); assert_eq!(midpoint.seconds, 30.0);
// The interpolated position differs from both endpoints. // The interpolated position differs from both endpoints.
// 插值所得位置不同于两个端点。
assert_ne!(midpoint.position, ephemeris.samples()[0].position); assert_ne!(midpoint.position, ephemeris.samples()[0].position);
assert_ne!(midpoint.position, ephemeris.samples()[1].position); assert_ne!(midpoint.position, ephemeris.samples()[1].position);
// And it is a plausible radius for the orbit. // And it is a plausible radius for the orbit.
// 且它是该轨道的合理半径。
assert!((midpoint.radius() - 7_000.0).abs() < 1.0); assert!((midpoint.radius() - 7_000.0).abs() < 1.0);
} }
@ -125,6 +138,7 @@ fn a_query_between_samples_is_interpolated() {
fn the_interpolation_is_accurate_on_a_circular_orbit() { fn the_interpolation_is_accurate_on_a_circular_orbit() {
// Hermite cubic on a smooth orbit: the position error over a step of // Hermite cubic on a smooth orbit: the position error over a step of
// sixty seconds should be far below a kilometer. // sixty seconds should be far below a kilometer.
// 光滑轨道上的 Hermite 三次插值:六十秒步长上的位置误差应远低于一千米。
let ephemeris = sample_table(60.0, 600.0); let ephemeris = sample_table(60.0, 600.0);
let mut worst = 0.0_f64; let mut worst = 0.0_f64;
for index in 0..600 { for index in 0..600 {
@ -144,11 +158,14 @@ fn halving_the_step_cuts_the_error_by_about_sixteen() {
// A quartic method: the error scales as the fourth power of the step. // A quartic method: the error scales as the fourth power of the step.
// This is the property that makes the interpolation worth using, and it // This is the property that makes the interpolation worth using, and it
// fails if the velocity is dropped or mis-scaled. // fails if the velocity is dropped or mis-scaled.
// 四阶方法:误差按步长的四次方缩放。这正是使该插值值得使用的性质,若丢掉速度或
// 缩放错误它就会失败。
let error_for = |step: f64| { let error_for = |step: f64| {
let ephemeris = sample_table(step, 600.0); let ephemeris = sample_table(step, 600.0);
let mut worst = 0.0_f64; let mut worst = 0.0_f64;
// Query at the midpoints between samples, where the error is // Query at the midpoints between samples, where the error is
// largest. // largest.
// 在样本之间的中点查询,那里误差最大。
let mut time = step / 2.0; let mut time = step / 2.0;
while time < 600.0 { while time < 600.0 {
let interpolated = ephemeris.state_at(time).expect("covered"); let interpolated = ephemeris.state_at(time).expect("covered");
@ -172,6 +189,7 @@ fn halving_the_step_cuts_the_error_by_about_sixteen() {
fn the_interpolated_velocity_matches_the_position_derivative() { fn the_interpolated_velocity_matches_the_position_derivative() {
// The velocity is the analytic derivative of the position cubic, so a // The velocity is the analytic derivative of the position cubic, so a
// numerical derivative of the position must agree with it. // numerical derivative of the position must agree with it.
// 速度是位置三次式的解析导数,故位置的数值导数必须与它一致。
let ephemeris = sample_table(60.0, 600.0); let ephemeris = sample_table(60.0, 600.0);
let time = 137.0; let time = 137.0;
let h = 1e-4; let h = 1e-4;
@ -195,6 +213,7 @@ fn a_query_outside_the_interval_is_refused() {
assert!(ephemeris.state_at(f64::NAN).is_none()); assert!(ephemeris.state_at(f64::NAN).is_none());
assert!(ephemeris.state_at(f64::INFINITY).is_none()); assert!(ephemeris.state_at(f64::INFINITY).is_none());
// The endpoints are covered. // The endpoints are covered.
// 端点被覆盖。
assert!(ephemeris.covers(0.0)); assert!(ephemeris.covers(0.0));
assert!(ephemeris.covers(600.0)); assert!(ephemeris.covers(600.0));
assert!(!ephemeris.covers(-1e-9)); assert!(!ephemeris.covers(-1e-9));
@ -208,6 +227,7 @@ fn a_clamped_query_returns_the_nearest_endpoint() {
let after = ephemeris.clamped_state_at(10_000.0).expect("clamped"); let after = ephemeris.clamped_state_at(10_000.0).expect("clamped");
assert_eq!(after, ephemeris.samples()[ephemeris.len() - 1]); assert_eq!(after, ephemeris.samples()[ephemeris.len() - 1]);
// Inside the interval the clamped query is the ordinary one. // Inside the interval the clamped query is the ordinary one.
// 在区间内,夹逼查询即普通查询。
let inside = ephemeris.clamped_state_at(30.0).expect("covered"); let inside = ephemeris.clamped_state_at(30.0).expect("covered");
assert_eq!(inside, ephemeris.state_at(30.0).expect("covered")); assert_eq!(inside, ephemeris.state_at(30.0).expect("covered"));
assert!(ephemeris.clamped_state_at(f64::NAN).is_none()); assert!(ephemeris.clamped_state_at(f64::NAN).is_none());
@ -220,8 +240,10 @@ fn the_nearest_sample_is_found() {
assert_eq!(ephemeris.nearest_sample(29.0).expect("found").seconds, 0.0); assert_eq!(ephemeris.nearest_sample(29.0).expect("found").seconds, 0.0);
assert_eq!(ephemeris.nearest_sample(31.0).expect("found").seconds, 60.0); assert_eq!(ephemeris.nearest_sample(31.0).expect("found").seconds, 60.0);
// A midpoint ties toward the earlier sample. // A midpoint ties toward the earlier sample.
// 中点并列时取较早的样本。
assert_eq!(ephemeris.nearest_sample(30.0).expect("found").seconds, 0.0); assert_eq!(ephemeris.nearest_sample(30.0).expect("found").seconds, 0.0);
// Outside the interval the nearest end is returned. // Outside the interval the nearest end is returned.
// 在区间外返回最近的一端。
assert_eq!( assert_eq!(
ephemeris.nearest_sample(-100.0).expect("found").seconds, ephemeris.nearest_sample(-100.0).expect("found").seconds,
0.0 0.0
@ -266,10 +288,12 @@ fn a_property_of_every_sample_can_be_checked() {
} }
// The sampler's grid. // The sampler's grid.
// 采样器的网格。
#[test] #[test]
fn the_grid_includes_both_ends() { fn the_grid_includes_both_ends() {
// The end falls on the grid. // The end falls on the grid.
// 终点落在网格上。
let sampler = Sampler::new(0.0, 300.0, 60.0); let sampler = Sampler::new(0.0, 300.0, 60.0);
assert_eq!(sampler.sample_count(), Some(6)); assert_eq!(sampler.sample_count(), Some(6));
assert_eq!( assert_eq!(
@ -278,6 +302,7 @@ fn the_grid_includes_both_ends() {
); );
// The end does not fall on the grid, so it is appended. // The end does not fall on the grid, so it is appended.
// 终点不落在网格上,故追加它。
let sampler = Sampler::new(0.0, 250.0, 60.0); let sampler = Sampler::new(0.0, 250.0, 60.0);
assert_eq!(sampler.sample_count(), Some(6)); assert_eq!(sampler.sample_count(), Some(6));
assert_eq!( assert_eq!(
@ -298,6 +323,8 @@ fn a_zero_length_interval_is_refused() {
// A table needs at least two distinct times, so a zero length grid has // A table needs at least two distinct times, so a zero length grid has
// no interval to interpolate over and is rejected outright rather than // no interval to interpolate over and is rejected outright rather than
// producing a table that cannot answer a query. // producing a table that cannot answer a query.
// 表至少需要两个不同的时刻,故零长度网格没有可插值的区间,会被直接拒绝而非产生
// 一张无法回答查询的表。
let sampler = Sampler::new(5.0, 5.0, 60.0); let sampler = Sampler::new(5.0, 5.0, 60.0);
assert!(!sampler.is_valid()); assert!(!sampler.is_valid());
assert_eq!(sampler.sample_count(), None); assert_eq!(sampler.sample_count(), None);
@ -341,6 +368,7 @@ fn a_bad_state_from_the_function_is_reported() {
fn the_sampler_overrides_a_bad_time_from_the_function() { fn the_sampler_overrides_a_bad_time_from_the_function() {
// A function that returns a wrong time must not corrupt the table: the // A function that returns a wrong time must not corrupt the table: the
// grid time is used instead. // grid time is used instead.
// 返回错误时刻的函数不得污染该表:改用网格时刻。
let sampler = Sampler::new(0.0, 120.0, 60.0); let sampler = Sampler::new(0.0, 120.0, 60.0);
let ephemeris = sampler let ephemeris = sampler
.sample(|_| Sample::new(999.0, Vec3::new(7_000.0, 0.0, 0.0), Vec3::ZERO)) .sample(|_| Sample::new(999.0, Vec3::new(7_000.0, 0.0, 0.0), Vec3::ZERO))
@ -352,12 +380,14 @@ fn the_sampler_overrides_a_bad_time_from_the_function() {
fn positions_can_be_sampled_without_velocities() { fn positions_can_be_sampled_without_velocities() {
// The velocity is reconstructed by finite differences, so a // The velocity is reconstructed by finite differences, so a
// position-only source still produces a usable table. // position-only source still produces a usable table.
// 速度由有限差分重构,故仅提供位置的来源仍能产生可用的表。
let ephemeris = Sampler::new(0.0, 600.0, 60.0) let ephemeris = Sampler::new(0.0, 600.0, 60.0)
.sample_positions(|seconds| exact_state(seconds).position) .sample_positions(|seconds| exact_state(seconds).position)
.expect("the positions are finite"); .expect("the positions are finite");
assert_eq!(ephemeris.len(), 11); assert_eq!(ephemeris.len(), 11);
// The reconstructed velocity is close to the true one. // The reconstructed velocity is close to the true one.
// 所重构的速度接近真值。
let sample = ephemeris.state_at(300.0).expect("covered"); let sample = ephemeris.state_at(300.0).expect("covered");
let truth = exact_state(300.0); let truth = exact_state(300.0);
assert!( assert!(
@ -372,6 +402,8 @@ fn positions_can_be_sampled_without_velocities() {
// accurate, so its error feeds straight into the Hermite reconstruction. // accurate, so its error feeds straight into the Hermite reconstruction.
// The error is a few kilometers over a one minute step, against // The error is a few kilometers over a one minute step, against
// millimeters when the velocity is known. // millimeters when the velocity is known.
// 插值仍可用,尽管远不如有精确速度时准确:有限差分速度只有二阶精度,故其误差
// 直接输入 Hermite 重构。误差在一分钟步长上为几千米,而有已知速度时为毫米级。
let mut worst = 0.0_f64; let mut worst = 0.0_f64;
for index in 0..600 { for index in 0..600 {
let time = f64::from(index) + 0.5; let time = f64::from(index) + 0.5;
@ -397,6 +429,7 @@ fn position_sampling_reports_a_bad_position() {
} }
// The Hermite basis functions directly. // The Hermite basis functions directly.
// 直接检验 Hermite 基函数。
#[test] #[test]
fn the_hermite_basis_reproduces_its_endpoints() { fn the_hermite_basis_reproduces_its_endpoints() {
@ -407,9 +440,11 @@ fn the_hermite_basis_reproduces_its_endpoints() {
let dt = 60.0; let dt = 60.0;
// At s = 0 the result is p0 and the velocity is v0. // At s = 0 the result is p0 and the velocity is v0.
// 在 s = 0 处结果为 p0,速度为 v0。
assert_eq!(hermite_position(p0, v0, p1, v1, dt, 0.0), p0); assert_eq!(hermite_position(p0, v0, p1, v1, dt, 0.0), p0);
assert_eq!(hermite_velocity(p0, v0, p1, v1, dt, 0.0), v0); assert_eq!(hermite_velocity(p0, v0, p1, v1, dt, 0.0), v0);
// At s = 1 the result is p1 and the velocity is v1. // At s = 1 the result is p1 and the velocity is v1.
// 在 s = 1 处结果为 p1,速度为 v1。
assert_eq!(hermite_position(p0, v0, p1, v1, dt, 1.0), p1); assert_eq!(hermite_position(p0, v0, p1, v1, dt, 1.0), p1);
assert_eq!(hermite_velocity(p0, v0, p1, v1, dt, 1.0), v1); assert_eq!(hermite_velocity(p0, v0, p1, v1, dt, 1.0), v1);
} }
@ -421,9 +456,11 @@ fn the_hermite_state_returns_both_parts() {
let (position, velocity) = hermite_state(p0, Vec3::ZERO, p1, Vec3::ZERO, 10.0, 0.5); let (position, velocity) = hermite_state(p0, Vec3::ZERO, p1, Vec3::ZERO, 10.0, 0.5);
// Both endpoints are stationary, so the cubic is the S-curve that // Both endpoints are stationary, so the cubic is the S-curve that
// passes through the midpoint with the slope the endpoints imply. // passes through the midpoint with the slope the endpoints imply.
// 两个端点都静止,故该三次式是经由中点、斜率为端点所蕴含值的 S 形曲线。
assert_eq!(position, Vec3::new(1.5, 0.0, 0.0)); assert_eq!(position, Vec3::new(1.5, 0.0, 0.0));
// The slope at the midpoint is 1.5 / dt, not zero: the curve rises from // The slope at the midpoint is 1.5 / dt, not zero: the curve rises from
// zero slope at one end to zero slope at the other. // zero slope at one end to zero slope at the other.
// 中点处的斜率是 1.5 / dt 而非零:曲线从一端的零斜率升到另一端的零斜率。
assert_eq!(velocity, Vec3::new(0.15, 0.0, 0.0)); assert_eq!(velocity, Vec3::new(0.15, 0.0, 0.0));
} }

View File

@ -1,4 +1,6 @@
//! Tests for the anomaly conversions. //! Tests for the anomaly conversions.
//!
//! 近点角换算的测试。
use apsis_kepler::{ use apsis_kepler::{
eccentric_from_mean, eccentric_from_true, mean_from_eccentric, mean_from_true, eccentric_from_mean, eccentric_from_true, mean_from_eccentric, mean_from_true,
@ -17,6 +19,7 @@ fn wrap_places_angles_in_the_half_open_interval() {
"angle {angle} wrapped to {wrapped}" "angle {angle} wrapped to {wrapped}"
); );
// The result differs from the input by a whole number of turns. // The result differs from the input by a whole number of turns.
// 结果与输入相差整数圈。
let turns_removed = (angle - wrapped) / (2.0 * PI); let turns_removed = (angle - wrapped) / (2.0 * PI);
assert!((turns_removed - turns_removed.round()).abs() < 1e-9); assert!((turns_removed - turns_removed.round()).abs() < 1e-9);
} }
@ -33,6 +36,7 @@ fn wrap_keeps_values_already_in_range() {
#[test] #[test]
fn keplers_equation_holds_for_the_solution() { fn keplers_equation_holds_for_the_solution() {
// M = E - e sin E must be satisfied by the returned eccentric anomaly. // M = E - e sin E must be satisfied by the returned eccentric anomaly.
// 所返回的偏近点角必须满足 M = E - e sin E。
for eccentricity in [0.0, 0.01, 0.1, 0.5, 0.9, 0.99] { for eccentricity in [0.0, 0.01, 0.1, 0.5, 0.9, 0.99] {
for mean in [-3.0, -1.0, 0.0, 0.5, 1.5, 3.0] { for mean in [-3.0, -1.0, 0.0, 0.5, 1.5, 3.0] {
let eccentric = eccentric_from_mean(mean, eccentricity); let eccentric = eccentric_from_mean(mean, eccentricity);
@ -76,6 +80,7 @@ fn mean_and_true_anomaly_round_trip() {
#[test] #[test]
fn a_circular_orbit_has_equal_anomalies() { fn a_circular_orbit_has_equal_anomalies() {
// With zero eccentricity every anomaly is the same angle. // With zero eccentricity every anomaly is the same angle.
// 偏心率为零时每种近点角都是同一个角。
for angle in [-2.0, -0.5, 0.0, 1.0, 2.5] { for angle in [-2.0, -0.5, 0.0, 1.0, 2.5] {
assert!((eccentric_from_mean(angle, 0.0) - angle).abs() < 1e-12); assert!((eccentric_from_mean(angle, 0.0) - angle).abs() < 1e-12);
assert!((true_from_eccentric(angle, 0.0) - angle).abs() < 1e-12); assert!((true_from_eccentric(angle, 0.0) - angle).abs() < 1e-12);
@ -97,6 +102,8 @@ fn true_anomaly_exceeds_mean_anomaly_before_apogee() {
// For an eccentric orbit the satellite spends less time near perigee, // For an eccentric orbit the satellite spends less time near perigee,
// so the true anomaly runs ahead of the mean anomaly in the first half // so the true anomaly runs ahead of the mean anomaly in the first half
// of the orbit. // of the orbit.
// 对偏心轨道,卫星在近地点附近花的时间较少,故在轨道的上半段真近点角领先于平
// 近点角。
let eccentricity = 0.5; let eccentricity = 0.5;
let mean = 1.0; let mean = 1.0;
let true_anomaly = true_from_mean(mean, eccentricity); let true_anomaly = true_from_mean(mean, eccentricity);

View File

@ -4,6 +4,11 @@
//! perifocal frame expressions in extended precision. Only the point mass //! perifocal frame expressions in extended precision. Only the point mass
//! gravity of the central body is involved, so the two should agree to //! gravity of the central body is involved, so the two should agree to
//! floating point rounding. //! floating point rounding.
//!
//! 开普勒根数及其状态矢量换算的测试。
//!
//! 期望值是以扩展精度从标准近焦点系表达式独立计算的。只涉及中心天体的质点引力,
//! 故二者应在浮点舍入内一致。
use apsis_kepler::{GravityParameter, KeplerianElements, State, wrap_to_pi}; use apsis_kepler::{GravityParameter, KeplerianElements, State, wrap_to_pi};
use apsis_math::Vec3; use apsis_math::Vec3;
@ -78,6 +83,8 @@ fn a_circular_equatorial_orbit_has_the_circular_speed() {
// At zero eccentricity, zero inclination, and zero orientation, the // At zero eccentricity, zero inclination, and zero orientation, the
// satellite sits on the first axis and moves along the second, with // satellite sits on the first axis and moves along the second, with
// speed sqrt(mu / a). // speed sqrt(mu / a).
// 在零偏心率、零倾角、零取向时,卫星位于第一轴上、沿第二轴运动,速率为
// sqrt(mu / a)。
let elements = KeplerianElements { let elements = KeplerianElements {
semi_major_axis: 6_378.137, semi_major_axis: 6_378.137,
eccentricity: 0.0, eccentricity: 0.0,
@ -106,6 +113,7 @@ fn the_state_vector_lies_in_the_orbit_plane() {
.expect("elements are valid"); .expect("elements are valid");
// The position and velocity span the orbit plane, so the normal to that // The position and velocity span the orbit plane, so the normal to that
// plane is perpendicular to both. // plane is perpendicular to both.
// 位置与速度张成轨道面,故该面的法线垂直于二者。
let normal = state let normal = state
.position .position
.cross(state.velocity) .cross(state.velocity)
@ -182,6 +190,7 @@ fn the_period_follows_keplers_third_law() {
2.0 * PI * (elements.semi_major_axis.powi(3) / GravityParameter::WGS84.value()).sqrt(); 2.0 * PI * (elements.semi_major_axis.powi(3) / GravityParameter::WGS84.value()).sqrt();
assert!((period - expected).abs() < 1e-9); assert!((period - expected).abs() < 1e-9);
// A 7000 km orbit has a period of about 97 minutes. // A 7000 km orbit has a period of about 97 minutes.
// 7000 km 轨道的周期约为 97 分钟。
assert!((period / 60.0 - 97.0).abs() < 2.0); assert!((period / 60.0 - 97.0).abs() < 2.0);
} }
@ -210,6 +219,8 @@ fn state_round_trips_through_the_elements() {
// The angles are compared modulo a full turn, because a value such // The angles are compared modulo a full turn, because a value such
// as 5.0 radians and one reduced to -1.28 radians describe the same // as 5.0 radians and one reduced to -1.28 radians describe the same
// orientation. // orientation.
// 各角按模一周比较,因为诸如 5.0 弧度这样的值与归约到 -1.28 弧度的值描述同一
// 取向。
assert!((wrap_to_pi(recovered.right_ascension - elements.right_ascension)).abs() < 1e-9); assert!((wrap_to_pi(recovered.right_ascension - elements.right_ascension)).abs() < 1e-9);
assert!( assert!(
(wrap_to_pi(recovered.argument_of_perigee - elements.argument_of_perigee)).abs() < 1e-9 (wrap_to_pi(recovered.argument_of_perigee - elements.argument_of_perigee)).abs() < 1e-9
@ -218,6 +229,7 @@ fn state_round_trips_through_the_elements() {
// The round trip must reproduce the state as well as the elements, // The round trip must reproduce the state as well as the elements,
// which is the stronger check. // which is the stronger check.
// 往返必须既复现状态也复现根数,后者是更强的检查。
let again = recovered.to_state(GravityParameter::WGS84).expect("valid"); let again = recovered.to_state(GravityParameter::WGS84).expect("valid");
assert!((again.position - state.position).norm() < 1e-6); assert!((again.position - state.position).norm() < 1e-6);
assert!((again.velocity - state.velocity).norm() < 1e-9); assert!((again.velocity - state.velocity).norm() < 1e-9);
@ -287,6 +299,7 @@ fn degenerate_states_are_rejected() {
None None
); );
// A purely radial state has no defined orbit plane. // A purely radial state has no defined orbit plane.
// 纯径向状态没有定义的轨道面。
assert_eq!( assert_eq!(
KeplerianElements::from_state( KeplerianElements::from_state(
&State { &State {
@ -298,6 +311,7 @@ fn degenerate_states_are_rejected() {
None None
); );
// An unbound state has a non-positive semi-major axis. // An unbound state has a non-positive semi-major axis.
// 非束缚状态的半长轴非正。
assert_eq!( assert_eq!(
KeplerianElements::from_state( KeplerianElements::from_state(
&State { &State {
@ -321,6 +335,7 @@ fn gravity_parameter_values_match_the_standards() {
fn a_full_revolution_returns_to_the_same_point() { fn a_full_revolution_returns_to_the_same_point() {
// Advancing the mean anomaly by one full turn must return the same // Advancing the mean anomaly by one full turn must return the same
// position, which verifies the anomaly conversions agree. // position, which verifies the anomaly conversions agree.
// 把平近点角推进一整圈必须返回相同位置,这验证各近点角换算彼此一致。
let elements = elliptical_orbit(); let elements = elliptical_orbit();
let start = elements let start = elements
.with_mean_anomaly(1.0) .with_mean_anomaly(1.0)

View File

@ -1,10 +1,14 @@
//! Tests for the maneuver models. //! Tests for the maneuver models.
//!
//! 机动模型的测试。
use apsis_kepler::{GravityParameter, KeplerianElements}; use apsis_kepler::{GravityParameter, KeplerianElements};
use apsis_maneuver::{FiniteBurn, Impulse, ManeuverErrorKind, hohmann_transfer}; use apsis_maneuver::{FiniteBurn, Impulse, ManeuverErrorKind, hohmann_transfer};
use apsis_math::Vec3; use apsis_math::Vec3;
/// A circular orbit at a given radius, in the equatorial plane. /// A circular orbit at a given radius, in the equatorial plane.
///
/// 给定半径、位于赤道面内的圆轨道。
fn circular(radius: f64) -> KeplerianElements { fn circular(radius: f64) -> KeplerianElements {
KeplerianElements { KeplerianElements {
semi_major_axis: radius, semi_major_axis: radius,
@ -17,6 +21,7 @@ fn circular(radius: f64) -> KeplerianElements {
} }
// The impulsive model. // The impulsive model.
// 脉冲模型。
#[test] #[test]
fn an_impulse_reports_its_change() { fn an_impulse_reports_its_change() {
@ -36,12 +41,14 @@ fn a_zero_or_non_finite_impulse_is_refused() {
#[test] #[test]
fn an_impulse_can_be_built_from_a_direction_and_a_magnitude() { fn an_impulse_can_be_built_from_a_direction_and_a_magnitude() {
// The direction need not be a unit vector. // The direction need not be a unit vector.
// 方向不必是单位矢量。
let impulse = Impulse::along(Vec3::new(0.0, 3.0, 0.0), 0.5).expect("usable"); let impulse = Impulse::along(Vec3::new(0.0, 3.0, 0.0), 0.5).expect("usable");
assert!((impulse.magnitude() - 0.5).abs() < 1e-12); assert!((impulse.magnitude() - 0.5).abs() < 1e-12);
assert!((impulse.delta_v().y - 0.5).abs() < 1e-12); assert!((impulse.delta_v().y - 0.5).abs() < 1e-12);
assert!(impulse.delta_v().z.abs() < 1e-12); assert!(impulse.delta_v().z.abs() < 1e-12);
// A zero direction and a bad magnitude are refused. // A zero direction and a bad magnitude are refused.
// 零方向与坏幅度被拒绝。
assert!(Impulse::along(Vec3::ZERO, 1.0).is_err()); assert!(Impulse::along(Vec3::ZERO, 1.0).is_err());
assert!(Impulse::along(Vec3::X, 0.0).is_err()); assert!(Impulse::along(Vec3::X, 0.0).is_err());
assert!(Impulse::along(Vec3::X, -1.0).is_err()); assert!(Impulse::along(Vec3::X, -1.0).is_err());
@ -56,6 +63,7 @@ fn an_impulse_can_be_scaled_and_reversed() {
let reversed = impulse.reversed(); let reversed = impulse.reversed();
assert!((reversed.delta_v().x + 1.0).abs() < 1e-12); assert!((reversed.delta_v().x + 1.0).abs() < 1e-12);
// Scaling by zero produces a zero change, which is refused. // Scaling by zero produces a zero change, which is refused.
// 按零缩放产生零变化,会被拒绝。
assert!(impulse.scaled(0.0).is_err()); assert!(impulse.scaled(0.0).is_err());
} }
@ -75,14 +83,17 @@ fn an_impulse_displays_its_magnitude() {
} }
// The Hohmann transfer. // The Hohmann transfer.
// Hohmann 转移。
#[test] #[test]
fn a_hohmann_transfer_raises_an_orbit() { fn a_hohmann_transfer_raises_an_orbit() {
// Raise from 7000 to 8000 km. // Raise from 7000 to 8000 km.
// 从 7000 升到 8000 km。
let transfer = let transfer =
hohmann_transfer(7_000.0, 8_000.0, GravityParameter::WGS84).expect("the radii are usable"); hohmann_transfer(7_000.0, 8_000.0, GravityParameter::WGS84).expect("the radii are usable");
assert!(transfer.is_raising()); assert!(transfer.is_raising());
// Raising an orbit takes two prograde burns. // Raising an orbit takes two prograde burns.
// 抬高轨道需要两次顺行点火。
assert!(transfer.first_burn > 0.0); assert!(transfer.first_burn > 0.0);
assert!(transfer.second_burn > 0.0); assert!(transfer.second_burn > 0.0);
assert_eq!(transfer.transfer_semi_major_axis, 7_500.0); assert_eq!(transfer.transfer_semi_major_axis, 7_500.0);
@ -98,12 +109,14 @@ fn a_hohmann_transfer_raises_an_orbit() {
fn a_hohmann_transfer_lowers_an_orbit() { fn a_hohmann_transfer_lowers_an_orbit() {
// The same two radii in the other order is a descent, and the burns are // The same two radii in the other order is a descent, and the burns are
// retrograde. // retrograde.
// 两个半径反序即为下降,两次点火都是逆行。
let transfer = let transfer =
hohmann_transfer(8_000.0, 7_000.0, GravityParameter::WGS84).expect("the radii are usable"); hohmann_transfer(8_000.0, 7_000.0, GravityParameter::WGS84).expect("the radii are usable");
assert!(!transfer.is_raising()); assert!(!transfer.is_raising());
assert!(transfer.first_burn < 0.0); assert!(transfer.first_burn < 0.0);
assert!(transfer.second_burn < 0.0); assert!(transfer.second_burn < 0.0);
// The cost is the same either way. // The cost is the same either way.
// 两种方向的代价相同。
let raised = let raised =
hohmann_transfer(7_000.0, 8_000.0, GravityParameter::WGS84).expect("the radii are usable"); hohmann_transfer(7_000.0, 8_000.0, GravityParameter::WGS84).expect("the radii are usable");
assert!((transfer.total_delta_v() - raised.total_delta_v()).abs() < 1e-12); assert!((transfer.total_delta_v() - raised.total_delta_v()).abs() < 1e-12);
@ -115,6 +128,8 @@ fn the_first_burn_matches_the_vis_viva_speeds() {
// The first burn is the transfer ellipse speed at the lower radius less // The first burn is the transfer ellipse speed at the lower radius less
// the circular speed there. Checking it against the vis-viva equation // the circular speed there. Checking it against the vis-viva equation
// directly pins the formula. // directly pins the formula.
// 第一次点火是转移椭圆在下半径处的速率减去那里的圆轨道速率。用活力公式直接核对
// 它即可钉住该公式。
let transfer = let transfer =
hohmann_transfer(7_000.0, 9_000.0, GravityParameter::WGS84).expect("the radii are usable"); hohmann_transfer(7_000.0, 9_000.0, GravityParameter::WGS84).expect("the radii are usable");
let mu = GravityParameter::WGS84.value(); let mu = GravityParameter::WGS84.value();
@ -137,6 +152,8 @@ fn a_hohmann_transfer_matches_a_known_leo_to_geo_case() {
// The well documented case: a transfer from a 300 km circular parking // The well documented case: a transfer from a 300 km circular parking
// orbit to geostationary altitude. The total is about 3.89 km/s, which // orbit to geostationary altitude. The total is about 3.89 km/s, which
// is the figure quoted for a standard LEO to GEO chemical transfer. // is the figure quoted for a standard LEO to GEO chemical transfer.
// 有充分记载的用例:从 300 km 圆停泊轨道转移到地球静止高度。总计约 3.89 km/s,
// 这是标准 LEO 到 GEO 化学转移所引用的数字。
let from = 6_378.137 + 300.0; let from = 6_378.137 + 300.0;
let to = 42_164.0; let to = 42_164.0;
let transfer = hohmann_transfer(from, to, GravityParameter::WGS84).expect("usable"); let transfer = hohmann_transfer(from, to, GravityParameter::WGS84).expect("usable");
@ -148,6 +165,8 @@ fn a_hohmann_transfer_matches_a_known_leo_to_geo_case() {
// The transfer ellipse touches both orbits: its perigee is the parking // The transfer ellipse touches both orbits: its perigee is the parking
// radius and its apogee the destination, so its eccentricity follows // radius and its apogee the destination, so its eccentricity follows
// from the two radii. // from the two radii.
// 转移椭圆与两个轨道都相切:其近地点是停泊半径,远地点是目的地,故其偏心率由
// 这两个半径得出。
let eccentricity = (to - from) / (to + from); let eccentricity = (to - from) / (to + from);
let perigee = transfer.transfer_semi_major_axis * (1.0 - eccentricity); let perigee = transfer.transfer_semi_major_axis * (1.0 - eccentricity);
let apogee = transfer.transfer_semi_major_axis * (1.0 + eccentricity); let apogee = transfer.transfer_semi_major_axis * (1.0 + eccentricity);
@ -178,6 +197,7 @@ fn an_unusable_radius_is_refused() {
} }
// The finite burn. // The finite burn.
// 有限点火。
#[test] #[test]
fn a_finite_burn_reports_its_acceleration_and_duration() { fn a_finite_burn_reports_its_acceleration_and_duration() {
@ -185,6 +205,7 @@ fn a_finite_burn_reports_its_acceleration_and_duration() {
assert!((burn.acceleration_magnitude() - 0.001).abs() < 1e-15); assert!((burn.acceleration_magnitude() - 0.001).abs() < 1e-15);
assert_eq!(burn.duration(), 100.0); assert_eq!(burn.duration(), 100.0);
// The nominal change is the acceleration times the duration. // The nominal change is the acceleration times the duration.
// 标称变化是加速度乘以时长。
assert!((burn.nominal_delta_v() - 0.1).abs() < 1e-12); assert!((burn.nominal_delta_v() - 0.1).abs() < 1e-12);
} }
@ -211,16 +232,21 @@ fn a_finite_burn_agrees_with_its_equivalent_impulse_in_free_space() {
// With gravity switched off the finite burn must deliver exactly the // With gravity switched off the finite burn must deliver exactly the
// nominal change: it is a constant acceleration, so the velocity change // nominal change: it is a constant acceleration, so the velocity change
// is the acceleration times the duration. // is the acceleration times the duration.
// 在引力关掉时,有限点火必须恰好给出标称变化:它是常值加速度,故速度变化是
// 加速度乘以时长。
// //
// The two-body model always has gravity, so instead the burn is applied // The two-body model always has gravity, so instead the burn is applied
// far from a mass, where gravity in the equation is negligible because // far from a mass, where gravity in the equation is negligible because
// the central body was given a vanishing parameter. // the central body was given a vanishing parameter.
// 二体模型总有引力,故改为在远离质量处施加点火,那里方程中的引力可忽略,因为
// 中心天体被赋予了一个趋于零的参数。
let burn = FiniteBurn::new(Vec3::new(0.0, 0.001, 0.0), 100.0).expect("usable"); let burn = FiniteBurn::new(Vec3::new(0.0, 0.001, 0.0), 100.0).expect("usable");
let state = apsis_kepler::State { let state = apsis_kepler::State {
position: Vec3::new(1.0e9, 0.0, 0.0), position: Vec3::new(1.0e9, 0.0, 0.0),
velocity: Vec3::ZERO, velocity: Vec3::ZERO,
}; };
// A negligible gravitational parameter leaves only the thrust. // A negligible gravitational parameter leaves only the thrust.
// 可忽略的引力参数只留下推力。
let outcome = burn let outcome = burn
.apply(state, GravityParameter(0.0), 100) .apply(state, GravityParameter(0.0), 100)
.expect("the burn runs"); .expect("the burn runs");
@ -231,6 +257,7 @@ fn a_finite_burn_agrees_with_its_equivalent_impulse_in_free_space() {
); );
assert!((outcome.nominal_delta_v - 0.1).abs() < 1e-12); assert!((outcome.nominal_delta_v - 0.1).abs() < 1e-12);
// There is no gravity to lose anything to. // There is no gravity to lose anything to.
// 没有引力可损失任何东西。
assert!(outcome.gravity_loss().abs() < 1e-9); assert!(outcome.gravity_loss().abs() < 1e-9);
} }
@ -240,18 +267,22 @@ fn a_finite_burn_converges_on_the_matching_impulse() {
// of acceleration `a` over a duration `t` delivers the impulse `a t` in // of acceleration `a` over a duration `t` delivers the impulse `a t` in
// the limit of a shrinking duration, because the gravity deflection // the limit of a shrinking duration, because the gravity deflection
// during the burn vanishes with the duration. // during the burn vanishes with the duration.
// 这正是把两个模型联系起来的性质。加速度为 `a`、时长为 `t` 的有限点火在时长
// 趋于零的极限下给出冲量 `a t`,因为点火期间的引力偏折随时长消失。
let parameter = GravityParameter::WGS84; let parameter = GravityParameter::WGS84;
let elements = circular(7_000.0); let elements = circular(7_000.0);
let state = elements.to_state(parameter).expect("valid"); let state = elements.to_state(parameter).expect("valid");
// The thrust direction is along the velocity, which for this orbit is // The thrust direction is along the velocity, which for this orbit is
// the second axis. // the second axis.
// 推力方向沿速度,对该轨道而言即第二轴。
let direction = state.velocity.normalize().expect("the speed is non-zero"); let direction = state.velocity.normalize().expect("the speed is non-zero");
let target_delta_v = 0.01; let target_delta_v = 0.01;
let achieved_for = |duration: f64| { let achieved_for = |duration: f64| {
// Hold the nominal delta-v fixed by scaling the acceleration, so // Hold the nominal delta-v fixed by scaling the acceleration, so
// every burn is compared at the same impulse. // every burn is compared at the same impulse.
// 通过缩放加速度保持标称 delta-v 不变,使每次点火都在相同冲量下比较。
let acceleration = target_delta_v / duration; let acceleration = target_delta_v / duration;
let burn = FiniteBurn::along(direction, acceleration, duration).expect("usable"); let burn = FiniteBurn::along(direction, acceleration, duration).expect("usable");
burn.apply(state, parameter, 2_000) burn.apply(state, parameter, 2_000)
@ -261,6 +292,7 @@ fn a_finite_burn_converges_on_the_matching_impulse() {
// A long burn loses some of the change to gravity; a short one keeps // A long burn loses some of the change to gravity; a short one keeps
// nearly all of it. // nearly all of it.
// 长点火把部分变化损失给引力;短点火几乎全部保留。
let long = achieved_for(600.0); let long = achieved_for(600.0);
let short = achieved_for(6.0); let short = achieved_for(6.0);
let long_error = (long - target_delta_v).abs(); let long_error = (long - target_delta_v).abs();
@ -282,11 +314,15 @@ fn a_finite_burn_delivers_less_than_nominal_against_gravity() {
// space value. The sign of the difference depends on the geometry; the // space value. The sign of the difference depends on the geometry; the
// point of the test is that the two are not equal, which is why the // point of the test is that the two are not equal, which is why the
// finite model exists. // finite model exists.
// 圆轨道上的长顺行点火:点火进行时引力使速度转向,故所达到的变化不同于自由空间
// 值。差值的符号取决于几何;本测试的要点是二者不相等,这正是有限模型存在的
// 原因。
let parameter = GravityParameter::WGS84; let parameter = GravityParameter::WGS84;
let elements = circular(7_000.0); let elements = circular(7_000.0);
let state = elements.to_state(parameter).expect("valid"); let state = elements.to_state(parameter).expect("valid");
let direction = state.velocity.normalize().expect("the speed is non-zero"); let direction = state.velocity.normalize().expect("the speed is non-zero");
// A burn lasting a tenth of the orbital period. // A burn lasting a tenth of the orbital period.
// 持续轨道周期十分之一的点火。
let duration = elements.period(parameter) / 10.0; let duration = elements.period(parameter) / 10.0;
let acceleration = 1e-5; let acceleration = 1e-5;
let burn = FiniteBurn::along(direction, acceleration, duration).expect("usable"); let burn = FiniteBurn::along(direction, acceleration, duration).expect("usable");
@ -298,12 +334,14 @@ fn a_finite_burn_delivers_less_than_nominal_against_gravity() {
outcome.gravity_loss() outcome.gravity_loss()
); );
// The position moved during the burn. // The position moved during the burn.
// 点火期间位置移动了。
assert!(outcome.state.position.distance(state.position) > 1.0); assert!(outcome.state.position.distance(state.position) > 1.0);
} }
#[test] #[test]
fn a_finite_burn_raises_the_orbit_when_prograde() { fn a_finite_burn_raises_the_orbit_when_prograde() {
// A prograde burn adds energy, so the semi-major axis grows. // A prograde burn adds energy, so the semi-major axis grows.
// 顺行点火增加能量,故半长轴增大。
let parameter = GravityParameter::WGS84; let parameter = GravityParameter::WGS84;
let elements = circular(7_000.0); let elements = circular(7_000.0);
let state = elements.to_state(parameter).expect("valid"); let state = elements.to_state(parameter).expect("valid");
@ -322,6 +360,7 @@ fn a_finite_burn_raises_the_orbit_when_prograde() {
#[test] #[test]
fn a_finite_burn_lowers_the_orbit_when_retrograde() { fn a_finite_burn_lowers_the_orbit_when_retrograde() {
// The mirror image: a retrograde burn removes energy. // The mirror image: a retrograde burn removes energy.
// 镜像情形:逆行点火移除能量。
let parameter = GravityParameter::WGS84; let parameter = GravityParameter::WGS84;
let elements = circular(7_000.0); let elements = circular(7_000.0);
let state = elements.to_state(parameter).expect("valid"); let state = elements.to_state(parameter).expect("valid");
@ -345,6 +384,7 @@ fn a_finite_burn_can_be_applied_to_elements() {
.apply_to_elements(elements, parameter, 500) .apply_to_elements(elements, parameter, 500)
.expect("the burn runs"); .expect("the burn runs");
// Both routes must give the same state. // Both routes must give the same state.
// 两条路径必须给出相同状态。
assert!( assert!(
from_state from_state
.state .state
@ -382,6 +422,7 @@ fn a_burn_with_no_steps_is_refused() {
fn a_large_burn_can_escape_and_is_reported() { fn a_large_burn_can_escape_and_is_reported() {
// A burn hard enough to exceed escape velocity leaves no elliptical // A burn hard enough to exceed escape velocity leaves no elliptical
// elements, which the outcome reports by returning `None`. // elements, which the outcome reports by returning `None`.
// 强度足以超过逃逸速度的点火不留下椭圆根数,结果通过返回 `None` 报告这一点。
let parameter = GravityParameter::WGS84; let parameter = GravityParameter::WGS84;
let state = circular(7_000.0).to_state(parameter).expect("valid"); let state = circular(7_000.0).to_state(parameter).expect("valid");
let direction = state.velocity.normalize().expect("non-zero"); let direction = state.velocity.normalize().expect("non-zero");

View File

@ -1,4 +1,6 @@
//! Integration tests for sampling and maneuvering through the facade. //! Integration tests for sampling and maneuvering through the facade.
//!
//! 通过门面进行采样与机动的集成测试。
use apsis_kepler::GravityParameter; use apsis_kepler::GravityParameter;
use apsis_maneuver::{FiniteBurn, hohmann_transfer}; use apsis_maneuver::{FiniteBurn, hohmann_transfer};
@ -6,6 +8,8 @@ use apsis_orbit::{EphemerisSamplingError, GravityModel, ManeuverApplicationError
use apsis_tle::TwoLineElement; use apsis_tle::TwoLineElement;
/// A satellite in a low Earth orbit, used as the source of the track. /// A satellite in a low Earth orbit, used as the source of the track.
///
/// 一颗低轨卫星,用作轨迹的来源。
const ISS: &str = concat!( const ISS: &str = concat!(
"1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n", "1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n",
"2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537\r\n", "2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537\r\n",
@ -20,15 +24,18 @@ fn orbit() -> Orbit {
fn an_orbit_samples_into_an_ephemeris() { fn an_orbit_samples_into_an_ephemeris() {
let orbit = orbit(); let orbit = orbit();
// Ten minutes over a one minute grid. // Ten minutes over a one minute grid.
// 一分钟网格上的十分钟。
let ephemeris = orbit let ephemeris = orbit
.sample_ephemeris(0.0, 10.0, 1.0) .sample_ephemeris(0.0, 10.0, 1.0)
.expect("the grid is usable"); .expect("the grid is usable");
assert_eq!(ephemeris.len(), 11); assert_eq!(ephemeris.len(), 11);
// The sample times are seconds from the element epoch. // The sample times are seconds from the element epoch.
// 样本时刻是距根数历元的秒数。
assert_eq!(ephemeris.start_time(), 0.0); assert_eq!(ephemeris.start_time(), 0.0);
assert_eq!(ephemeris.end_time(), 600.0); assert_eq!(ephemeris.end_time(), 600.0);
// Every sample must match a direct propagation. // Every sample must match a direct propagation.
// 每个样本必须与直接传播一致。
for sample in ephemeris.samples() { for sample in ephemeris.samples() {
let direct = orbit let direct = orbit
.propagate_minutes(sample.seconds / 60.0) .propagate_minutes(sample.seconds / 60.0)
@ -45,6 +52,8 @@ fn an_orbit_samples_into_an_ephemeris() {
fn the_interpolated_ephemeris_matches_a_direct_propagation() { fn the_interpolated_ephemeris_matches_a_direct_propagation() {
// This is the point of the ephemeris: a query between samples must agree // This is the point of the ephemeris: a query between samples must agree
// with the propagator to well within the accuracy a track needs. // with the propagator to well within the accuracy a track needs.
// 这正是星历的要点:样本之间的查询必须与传播器一致,且远在一条轨迹所需的精度
// 之内。
let orbit = orbit(); let orbit = orbit();
let ephemeris = orbit let ephemeris = orbit
.sample_ephemeris(0.0, 30.0, 1.0) .sample_ephemeris(0.0, 30.0, 1.0)
@ -52,6 +61,7 @@ fn the_interpolated_ephemeris_matches_a_direct_propagation() {
let mut worst = 0.0_f64; let mut worst = 0.0_f64;
// Query every ten seconds, which includes points between samples. // Query every ten seconds, which includes points between samples.
// 每十秒查询一次,其中含样本之间的点。
for index in 0..180 { for index in 0..180 {
let seconds = f64::from(index) * 10.0; let seconds = f64::from(index) * 10.0;
let interpolated = ephemeris.state_at(seconds).expect("the time is covered"); let interpolated = ephemeris.state_at(seconds).expect("the time is covered");
@ -60,6 +70,7 @@ fn the_interpolated_ephemeris_matches_a_direct_propagation() {
} }
// A one minute Hermite step on a low Earth orbit is accurate to far // A one minute Hermite step on a low Earth orbit is accurate to far
// better than a meter here. // better than a meter here.
// 低轨上一分钟的 Hermite 步在此精确到远优于一米。
assert!(worst < 1e-3, "the worst interpolation error is {worst} km"); assert!(worst < 1e-3, "the worst interpolation error is {worst} km");
} }
@ -85,6 +96,7 @@ fn a_propagation_failure_is_reported() {
let orbit = orbit(); let orbit = orbit();
// Far enough ahead that the drag term brings the satellite down, which // Far enough ahead that the drag term brings the satellite down, which
// is a decay failure rather than a silent bad sample. // is a decay failure rather than a silent bad sample.
// 足够远,使阻力项把卫星带下来,即衰减失败而非无声的坏样本。
let result = orbit.sample_ephemeris(0.0, 60.0 * 24.0 * 365.0, 60.0 * 24.0); let result = orbit.sample_ephemeris(0.0, 60.0 * 24.0 * 365.0, 60.0 * 24.0);
assert!( assert!(
matches!(result, Err(EphemerisSamplingError::Propagation(_)) | Ok(_)), matches!(result, Err(EphemerisSamplingError::Propagation(_)) | Ok(_)),
@ -98,6 +110,7 @@ fn a_burn_raises_the_osculating_orbit() {
let before = orbit.osculating_elements(0.0).expect("elliptical"); let before = orbit.osculating_elements(0.0).expect("elliptical");
let state = orbit.propagate_minutes(0.0).expect("propagates"); let state = orbit.propagate_minutes(0.0).expect("propagates");
// Thrust along the velocity. // Thrust along the velocity.
// 沿速度方向施加推力。
let direction = state.velocity.normalize().expect("non-zero"); let direction = state.velocity.normalize().expect("non-zero");
let burn = FiniteBurn::along(direction, 1e-4, 30.0).expect("usable"); let burn = FiniteBurn::along(direction, 1e-4, 30.0).expect("usable");
@ -114,6 +127,7 @@ fn a_burn_raises_the_osculating_orbit() {
fn a_burn_matches_a_direct_application() { fn a_burn_matches_a_direct_application() {
// The facade must be a thin wrapper: applying the burn through it is the // The facade must be a thin wrapper: applying the burn through it is the
// same as propagating and applying the burn by hand. // same as propagating and applying the burn by hand.
// 门面必须是薄封装:通过它施加点火与手工传播并施加点火相同。
let orbit = orbit(); let orbit = orbit();
let state = orbit.propagate_minutes(5.0).expect("propagates"); let state = orbit.propagate_minutes(5.0).expect("propagates");
let direction = state.velocity.normalize().expect("non-zero"); let direction = state.velocity.normalize().expect("non-zero");
@ -146,6 +160,7 @@ fn an_escaping_burn_is_reported() {
let state = orbit.propagate_minutes(0.0).expect("propagates"); let state = orbit.propagate_minutes(0.0).expect("propagates");
let direction = state.velocity.normalize().expect("non-zero"); let direction = state.velocity.normalize().expect("non-zero");
// A burn far larger than orbital velocity. // A burn far larger than orbital velocity.
// 远大于轨道速度的点火。
let burn = FiniteBurn::along(direction, 1e-2, 600.0).expect("usable"); let burn = FiniteBurn::along(direction, 1e-2, 600.0).expect("usable");
let result = orbit.apply_burn(0.0, burn, 2_000); let result = orbit.apply_burn(0.0, burn, 2_000);
assert!(matches!( assert!(matches!(
@ -159,6 +174,8 @@ fn a_hohmann_transfer_plans_a_raise_the_orbit_layer_can_sample() {
// A cross-layer check that ties the maneuver and ephemeris crates to the // A cross-layer check that ties the maneuver and ephemeris crates to the
// propagation: plan a transfer, apply the first burn, and sample the // propagation: plan a transfer, apply the first burn, and sample the
// resulting orbit. // resulting orbit.
// 把 maneuver 与 ephemeris crate 联系到传播的跨层检查:规划一次转移,施加第一次
// 点火,并对所得轨道采样。
let orbit = orbit(); let orbit = orbit();
let elements = orbit.osculating_elements(0.0).expect("elliptical"); let elements = orbit.osculating_elements(0.0).expect("elliptical");
let from = elements.semi_major_axis; let from = elements.semi_major_axis;
@ -166,9 +183,11 @@ fn a_hohmann_transfer_plans_a_raise_the_orbit_layer_can_sample() {
let transfer = hohmann_transfer(from, to, GravityParameter::WGS72).expect("usable"); let transfer = hohmann_transfer(from, to, GravityParameter::WGS72).expect("usable");
// The first burn is prograde for a raise, so thrust along the velocity. // The first burn is prograde for a raise, so thrust along the velocity.
// 抬高时第一次点火是顺行的,故沿速度方向施加推力。
let state = orbit.propagate_minutes(0.0).expect("propagates"); let state = orbit.propagate_minutes(0.0).expect("propagates");
let direction = state.velocity.normalize().expect("non-zero"); let direction = state.velocity.normalize().expect("non-zero");
// Turn the delta-v into an acceleration over a short burn. // Turn the delta-v into an acceleration over a short burn.
// 在短点火中把 delta-v 化为加速度。
let duration = 10.0; let duration = 10.0;
let burn = let burn =
FiniteBurn::along(direction, transfer.first_burn / duration, duration).expect("usable"); FiniteBurn::along(direction, transfer.first_burn / duration, duration).expect("usable");
@ -183,6 +202,8 @@ fn a_hohmann_transfer_plans_a_raise_the_orbit_layer_can_sample() {
// result. Turning them back into a propagatable orbit would need mean // result. Turning them back into a propagatable orbit would need mean
// elements, which SGP4 requires and an osculating set does not provide, // elements, which SGP4 requires and an osculating set does not provide,
// so the workflow stops here rather than feeding them back in. // so the workflow stops here rather than feeding them back in.
// 点火后状态的密切根数即规划结果。要把它们转回可传播的轨道需要平根数,SGP4
// 要求平根数而密切根数集不提供,故该工作流到此为止,而不把它们送回。
assert!(after.is_valid()); assert!(after.is_valid());
assert!(after.eccentricity < 1.0); assert!(after.eccentricity < 1.0);
} }

View File

@ -1,4 +1,6 @@
//! Tests for the combined orbit type. //! Tests for the combined orbit type.
//!
//! 合并后的轨道类型的测试。
use apsis_orbit::{GravityModel, Orbit}; use apsis_orbit::{GravityModel, Orbit};
use apsis_tle::TwoLineElement; use apsis_tle::TwoLineElement;
@ -64,6 +66,7 @@ fn propagate_at_uses_the_element_epoch() {
assert!((at_epoch.position - at_zero.position).norm() < 1e-6); assert!((at_epoch.position - at_zero.position).norm() < 1e-6);
// One day later is 1440 minutes. // One day later is 1440 minutes.
// 一天后是 1440 分钟。
let next_day = orbit.propagate_at(epoch + 1.0).expect("state exists"); let next_day = orbit.propagate_at(epoch + 1.0).expect("state exists");
let one_day = orbit.propagate_minutes(1440.0).expect("state exists"); let one_day = orbit.propagate_minutes(1440.0).expect("state exists");
assert!((next_day.position - one_day.position).norm() < 1e-3); assert!((next_day.position - one_day.position).norm() < 1e-3);
@ -80,6 +83,9 @@ fn osculating_elements_describe_a_low_earth_orbit() {
// is expected: the element set stores mean elements and the osculating // is expected: the element set stores mean elements and the osculating
// eccentricity includes the short period variation the two-body model // eccentricity includes the short period variation the two-body model
// does not reproduce. // does not reproduce.
// 这些值是针对同一天量矢量的一份独立计算核对的。倾角与编目值接近,因为扁率对它
// 影响很小,而偏心率不同于编目的 0.0006703。该差异是预期的:根数集保存的是平
// 根数,而密切偏心率包含二体模型不复现的短周期变化。
assert!( assert!(
(elements.semi_major_axis - 6_725.542).abs() < 0.01, (elements.semi_major_axis - 6_725.542).abs() < 0.01,
"semi-major axis {}", "semi-major axis {}",
@ -114,6 +120,7 @@ fn osculating_elements_change_along_the_orbit() {
// The osculating elements are not the mean elements, so they vary // The osculating elements are not the mean elements, so they vary
// because the perturbations SGP4 models are absent from the two-body // because the perturbations SGP4 models are absent from the two-body
// reconstruction. // reconstruction.
// 密切根数不是平根数,故它们变化,因为 SGP4 所建模的摄动在二体重构中缺席。
let orbit = orbit(ISS); let orbit = orbit(ISS);
let start = orbit.osculating_elements(0.0).expect("state exists"); let start = orbit.osculating_elements(0.0).expect("state exists");
let later = orbit.osculating_elements(720.0).expect("state exists"); let later = orbit.osculating_elements(720.0).expect("state exists");
@ -127,6 +134,8 @@ fn osculating_elements_work_for_a_deep_space_orbit() {
// The catalogued mean values are an eccentricity of 0.6546925 and an // The catalogued mean values are an eccentricity of 0.6546925 and an
// inclination of 64.1526 degrees. The osculating values are close but // inclination of 64.1526 degrees. The osculating values are close but
// not identical for the reason given in the low Earth orbit test. // not identical for the reason given in the low Earth orbit test.
// 编目的平值为偏心率 0.6546925、倾角 64.1526 度。密切值接近但不相同,理由同低轨
// 测试中所述。
assert!( assert!(
(elements.eccentricity - 0.655_871_3).abs() < 1e-6, (elements.eccentricity - 0.655_871_3).abs() < 1e-6,
"eccentricity {}", "eccentricity {}",
@ -164,6 +173,7 @@ fn the_gravity_parameter_matches_the_model() {
fn the_facade_reexports_the_layer() { fn the_facade_reexports_the_layer() {
// The facade must make the element parser and the propagator types // The facade must make the element parser and the propagator types
// reachable without a separate dependency. // reachable without a separate dependency.
// 门面必须使根数解析器与传播器类型无需单独依赖即可达到。
let element = apsis_orbit::TwoLineElement::parse(ISS).expect("parses"); let element = apsis_orbit::TwoLineElement::parse(ISS).expect("parses");
assert_eq!(element.satellite_number, 25544); assert_eq!(element.satellite_number, 25544);
let satellite = let satellite =

View File

@ -4,6 +4,11 @@
//! values the sequential forms do, in the same order. Every test compares //! values the sequential forms do, in the same order. Every test compares
//! against the sequential answer rather than against a constant, so a //! against the sequential answer rather than against a constant, so a
//! mismatch in either the propagation or the ordering is caught. //! mismatch in either the propagation or the ordering is caught.
//!
//! 批量传播入口的测试。
//!
//! 要紧的性质是并行形式产生与顺序形式完全相同的值,且次序相同。每个测试都与顺序
//! 答案而非常量比较,故传播或排序中的任何不一致都会被捕获。
use apsis_sgp4::{ use apsis_sgp4::{
GravityModel, Satellite, TwoLineElement, propagate_at, propagate_at_parallel, GravityModel, Satellite, TwoLineElement, propagate_at, propagate_at_parallel,
@ -33,6 +38,7 @@ fn a_short_track_matches_the_sequential_form() {
fn a_long_track_matches_the_sequential_form() { fn a_long_track_matches_the_sequential_form() {
// A day at a one minute step, which is long enough that the parallel path // A day at a one minute step, which is long enough that the parallel path
// takes the multi thread branch. // takes the multi thread branch.
// 一分钟步长的一天,长到足以使并行路径走多线程分支。
let satellite = satellite(); let satellite = satellite();
let minutes: Vec<f64> = (0..1_441).map(|index| index as f64).collect(); let minutes: Vec<f64> = (0..1_441).map(|index| index as f64).collect();
let sequential = propagate_minutes_at(&satellite, &minutes); let sequential = propagate_minutes_at(&satellite, &minutes);
@ -45,6 +51,7 @@ fn a_long_track_matches_the_sequential_form() {
fn a_backwards_track_matches() { fn a_backwards_track_matches() {
// Negative minutes propagate backwards and must behave the same way in // Negative minutes propagate backwards and must behave the same way in
// both forms. // both forms.
// 负分钟数向后传播,且在两种形式中必须表现相同。
let satellite = satellite(); let satellite = satellite();
let minutes: Vec<f64> = (0..2_000).map(|index| -(index as f64) * 0.5).collect(); let minutes: Vec<f64> = (0..2_000).map(|index| -(index as f64) * 0.5).collect();
assert_eq!( assert_eq!(
@ -64,6 +71,8 @@ fn a_decayed_instant_is_reported_in_both_forms() {
// Far enough ahead that the drag term brings the satellite down, both // Far enough ahead that the drag term brings the satellite down, both
// forms must agree that the instant is unusable rather than one of them // forms must agree that the instant is unusable rather than one of them
// stopping early. // stopping early.
// 足够远,使阻力项把卫星带下来,两种形式必须一致认为该瞬时不可用,而非其中之
// 一提前停止。
let satellite = satellite(); let satellite = satellite();
let minutes = vec![0.0, 1.0, 60.0 * 24.0 * 365.0 * 20.0]; let minutes = vec![0.0, 1.0, 60.0 * 24.0 * 365.0 * 20.0];
let sequential = propagate_minutes_at(&satellite, &minutes); let sequential = propagate_minutes_at(&satellite, &minutes);
@ -86,6 +95,7 @@ fn a_shell_at_one_instant_matches() {
fn a_shell_result_is_in_input_order() { fn a_shell_result_is_in_input_order() {
// The states of a repeated satellite are identical, so ordering is checked // The states of a repeated satellite are identical, so ordering is checked
// through the minutes form, where each value differs. // through the minutes form, where each value differs.
// 重复卫星的状态相同,故通过分钟形式检查次序,那里每个值都不同。
let satellite = satellite(); let satellite = satellite();
let minutes: Vec<f64> = (0..3_000).map(|index| index as f64 * 0.25).collect(); let minutes: Vec<f64> = (0..3_000).map(|index| index as f64 * 0.25).collect();
let parallel = propagate_minutes_at_parallel(&satellite, &minutes); let parallel = propagate_minutes_at_parallel(&satellite, &minutes);

View File

@ -1,4 +1,6 @@
//! Tests for the gravity models. //! Tests for the gravity models.
//!
//! 引力模型的测试。
use apsis_sgp4::GravityModel; use apsis_sgp4::GravityModel;
@ -22,6 +24,8 @@ fn wgs84_defining_values_match_the_standard() {
fn kepler_constant_matches_the_reference_value() { fn kepler_constant_matches_the_reference_value() {
// The reference implementation uses xke = 0.07436691613317342 for WGS // The reference implementation uses xke = 0.07436691613317342 for WGS
// 72, derived as 60 / sqrt(radius^3 / mu). // 72, derived as 60 / sqrt(radius^3 / mu).
// 参考实现对 WGS 72 使用 xke = 0.07436691613317342,由 60 / sqrt(radius^3 / mu)
// 导出。
let xke = GravityModel::WGS72.xke(); let xke = GravityModel::WGS72.xke();
assert!((xke - 0.074_366_916_133_173_42).abs() < 1e-15, "got {xke}"); assert!((xke - 0.074_366_916_133_173_42).abs() < 1e-15, "got {xke}");
} }
@ -31,6 +35,7 @@ fn time_unit_is_the_reciprocal_of_the_kepler_constant() {
let model = GravityModel::WGS72; let model = GravityModel::WGS72;
assert!((model.tumin() * model.xke() - 1.0).abs() < 1e-15); assert!((model.tumin() * model.xke() - 1.0).abs() < 1e-15);
// The value is close to 13.44684 minutes for WGS 72. // The value is close to 13.44684 minutes for WGS 72.
// 对 WGS 72,该值接近 13.44684 分钟。
assert!((model.tumin() - 13.446_839_696_959_309).abs() < 1e-12); assert!((model.tumin() - 13.446_839_696_959_309).abs() < 1e-12);
} }
@ -40,6 +45,7 @@ fn j3_over_j2_matches_the_quotient() {
let expected = model.j3 / model.j2; let expected = model.j3 / model.j2;
assert!((model.j3_over_j2() - expected).abs() < 1e-18); assert!((model.j3_over_j2() - expected).abs() < 1e-18);
// The value is about -0.002345069720011528. // The value is about -0.002345069720011528.
// 该值约为 -0.002345069720011528。
assert!((model.j3_over_j2() + 0.002_345_069_720_011_528).abs() < 1e-15); assert!((model.j3_over_j2() + 0.002_345_069_720_011_528).abs() < 1e-15);
} }

View File

@ -7,11 +7,19 @@
//! treated as correct, because both implementations evaluate the same //! treated as correct, because both implementations evaluate the same
//! closed form expressions and should differ only by floating point //! closed form expressions and should differ only by floating point
//! rounding. //! rounding.
//!
//! 传播器对照参考实现的测试。
//!
//! 本文件中的期望状态由 `sgp4` 参考库产生,它实现 Vallado 对该理论的修订。各根数
//! 集是随该库分发的历史测试用例。与参考相符到远低于一微米的传播被视为正确,因为
//! 两种实现求相同的闭式表达式,应只差浮点舍入。
use apsis_sgp4::{GravityModel, PropagationErrorKind, Satellite, State}; use apsis_sgp4::{GravityModel, PropagationErrorKind, Satellite, State};
use apsis_tle::TwoLineElement; use apsis_tle::TwoLineElement;
/// A near circular low Earth orbit, the International Space Station. /// A near circular low Earth orbit, the International Space Station.
///
/// 近圆形低轨,国际空间站。
const ISS: &str = concat!( const ISS: &str = concat!(
"1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n", "1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n",
"2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537\r\n", "2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537\r\n",
@ -19,18 +27,24 @@ const ISS: &str = concat!(
/// A highly eccentric orbit, which exercises the eccentric anomaly /// A highly eccentric orbit, which exercises the eccentric anomaly
/// iteration away from circular motion. /// iteration away from circular motion.
///
/// 一个高度偏心的轨道,它检验远离圆运动时的偏近点角迭代。
const VANGUARD: &str = concat!( const VANGUARD: &str = concat!(
"1 00005U 58002B 00179.78495062 .00000023 00000-0 28098-4 0 4753\r\n", "1 00005U 58002B 00179.78495062 .00000023 00000-0 28098-4 0 4753\r\n",
"2 00005 34.2682 348.7242 1859667 331.7664 19.3264 10.82419157413667\r\n", "2 00005 34.2682 348.7242 1859667 331.7664 19.3264 10.82419157413667\r\n",
); );
/// A deep space orbit with a half-day resonance. /// A deep space orbit with a half-day resonance.
///
/// 一个具有半日共振的深空轨道。
const DEEP_SPACE: &str = concat!( const DEEP_SPACE: &str = concat!(
"1 09880U 77021A 06174.85710247 .00000090 00000-0 10000-3 0 7322\r\n", "1 09880U 77021A 06174.85710247 .00000090 00000-0 10000-3 0 7322\r\n",
"2 09880 64.1526 313.1296 6546925 103.4683 305.3980 2.00754276 13047\r\n", "2 09880 64.1526 313.1296 6546925 103.4683 305.3980 2.00754276 13047\r\n",
); );
/// A geostationary transfer orbit, also deep space. /// A geostationary transfer orbit, also deep space.
///
/// 一个地球静止转移轨道,也是深空。
const TRANSFER: &str = concat!( const TRANSFER: &str = concat!(
"1 27651U 03004A 06185.99374200 .00000109 00000-0 19909-4 0 445\r\n", "1 27651U 03004A 06185.99374200 .00000109 00000-0 19909-4 0 445\r\n",
"2 27651 7.1743 182.9443 7867251 269.0386 18.6601 1.03461646 9884\r\n", "2 27651 7.1743 182.9443 7867251 269.0386 18.6601 1.03461646 9884\r\n",
@ -38,9 +52,13 @@ const TRANSFER: &str = concat!(
/// The position tolerance, in kilometers. The observed agreement is at the /// The position tolerance, in kilometers. The observed agreement is at the
/// nanometer level, so this leaves a wide margin. /// nanometer level, so this leaves a wide margin.
///
/// 位置容差,单位为千米。所观测的相符在纳米水平,故这留下很宽的余量。
const POSITION_TOLERANCE: f64 = 1e-3; const POSITION_TOLERANCE: f64 = 1e-3;
/// The velocity tolerance, in kilometers per second. /// The velocity tolerance, in kilometers per second.
///
/// 速度容差,单位为千米每秒。
const VELOCITY_TOLERANCE: f64 = 1e-9; const VELOCITY_TOLERANCE: f64 = 1e-9;
fn satellite(text: &str) -> Satellite { fn satellite(text: &str) -> Satellite {
@ -156,6 +174,7 @@ fn deep_space_satellite_uses_the_deep_space_branch() {
#[test] #[test]
fn deep_space_at_epoch_matches_the_reference() { fn deep_space_at_epoch_matches_the_reference() {
// MJD 53_962.36, about 12 hours after the element epoch. // MJD 53_962.36, about 12 hours after the element epoch.
// MJD 53_962.36,约在根数历元之后 12 小时。
let state = satellite(DEEP_SPACE) let state = satellite(DEEP_SPACE)
.propagate_minutes(0.0) .propagate_minutes(0.0)
.expect("epoch state"); .expect("epoch state");
@ -207,6 +226,7 @@ fn propagation_is_deterministic() {
// Repeated evaluation must give the same result, which also verifies // Repeated evaluation must give the same result, which also verifies
// that the resonance integrator does not carry hidden state between // that the resonance integrator does not carry hidden state between
// calls. // calls.
// 重复求值必须给出相同结果,这也验证共振积分器在调用之间不携带隐藏状态。
let satellite = satellite(DEEP_SPACE); let satellite = satellite(DEEP_SPACE);
let first = satellite.propagate_minutes(5_000.0).expect("state exists"); let first = satellite.propagate_minutes(5_000.0).expect("state exists");
let second = satellite.propagate_minutes(5_000.0).expect("state exists"); let second = satellite.propagate_minutes(5_000.0).expect("state exists");
@ -217,6 +237,7 @@ fn propagation_is_deterministic() {
fn out_of_order_evaluation_gives_the_same_result() { fn out_of_order_evaluation_gives_the_same_result() {
// Evaluating a later time first must not change a later evaluation of an // Evaluating a later time first must not change a later evaluation of an
// earlier time. // earlier time.
// 先求较晚的时间不得改变其后对较早时间的求值。
let satellite = satellite(DEEP_SPACE); let satellite = satellite(DEEP_SPACE);
let late = satellite.propagate_minutes(10_000.0).expect("state exists"); let late = satellite.propagate_minutes(10_000.0).expect("state exists");
let early = satellite.propagate_minutes(1_000.0).expect("state exists"); let early = satellite.propagate_minutes(1_000.0).expect("state exists");
@ -264,12 +285,15 @@ fn invalid_elements_are_rejected() {
// assumed decimal point, so it is 0.9999999, a valid but extremely // assumed decimal point, so it is 0.9999999, a valid but extremely
// elongated orbit. An eccentricity field that cannot be read at all is // elongated orbit. An eccentricity field that cannot be read at all is
// rejected by the parser instead, which is checked in the TLE tests. // rejected by the parser instead, which is checked in the TLE tests.
// 首位数字为九的偏心率是用隐含小数点写成的,故它是 0.9999999,一个有效但极其
// 细长的轨道。完全无法读取的偏心率字段改由解析器拒绝,这在 TLE 测试中检查。
let text = concat!( let text = concat!(
"1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n", "1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n",
"2 25544 51.6416 247.4627 0000000 130.5360 325.0288 15.72125391563531\r\n", "2 25544 51.6416 247.4627 0000000 130.5360 325.0288 15.72125391563531\r\n",
); );
let element = TwoLineElement::parse(text).expect("parses"); let element = TwoLineElement::parse(text).expect("parses");
// A circular orbit is perfectly valid, so this must succeed. // A circular orbit is perfectly valid, so this must succeed.
// 圆轨道完全有效,故这必须成功。
assert!(Satellite::from_element(&element, GravityModel::WGS72).is_some()); assert!(Satellite::from_element(&element, GravityModel::WGS72).is_some());
} }
@ -279,6 +303,8 @@ fn a_zero_mean_motion_is_rejected() {
// describes no orbit. The element set must still carry a valid // describes no orbit. The element set must still carry a valid
// checksum, so the test builds one from a parsed set rather than // checksum, so the test builds one from a parsed set rather than
// editing text. // editing text.
// 平运动字段以每天圈数计,故那里的零描述不出轨道。根数集仍须携带有效校验和,故
// 测试从已解析的集合构建一个,而非编辑文本。
let element = TwoLineElement::parse(ISS).expect("parses"); let element = TwoLineElement::parse(ISS).expect("parses");
let mut elements = apsis_sgp4::Elements::from_tle(&element).expect("epoch is covered"); let mut elements = apsis_sgp4::Elements::from_tle(&element).expect("epoch is covered");
elements.mean_motion = 0.0; elements.mean_motion = 0.0;
@ -313,5 +339,6 @@ fn state_accessors_report_radius_and_speed() {
assert!((state.altitude_radius() - state.position.norm()).abs() < 1e-12); assert!((state.altitude_radius() - state.position.norm()).abs() < 1e-12);
assert!((state.speed() - state.velocity.norm()).abs() < 1e-12); assert!((state.speed() - state.velocity.norm()).abs() < 1e-12);
// A low Earth satellite moves at about 7.7 km/s. // A low Earth satellite moves at about 7.7 km/s.
// 低轨卫星约以 7.7 km/s 运动。
assert!(state.speed() > 7.0 && state.speed() < 8.0); assert!(state.speed() > 7.0 && state.speed() < 8.0);
} }

View File

@ -1,4 +1,6 @@
//! Tests for line checksums. //! Tests for line checksums.
//!
//! 行校验和的测试。
use apsis_tle::{line_checksum, verify_line_checksum}; use apsis_tle::{line_checksum, verify_line_checksum};
@ -20,6 +22,7 @@ fn verification_accepts_a_well_formed_line() {
#[test] #[test]
fn verification_rejects_a_corrupted_digit() { fn verification_rejects_a_corrupted_digit() {
// Changing a digit in the body changes the sum. // Changing a digit in the body changes the sum.
// 改变正文中的一位数字会改变该和。
let corrupted = "1 35544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927"; let corrupted = "1 35544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927";
let error = verify_line_checksum(corrupted, 1).expect_err("checksum should fail"); let error = verify_line_checksum(corrupted, 1).expect_err("checksum should fail");
assert_eq!(error.kind(), apsis_tle::ElementErrorKind::Checksum); assert_eq!(error.kind(), apsis_tle::ElementErrorKind::Checksum);
@ -36,6 +39,7 @@ fn verification_rejects_a_corrupted_checksum_digit() {
fn minus_signs_contribute_one_each() { fn minus_signs_contribute_one_each() {
// A line of only minus signs, padded to 68 columns, sums to eight // A line of only minus signs, padded to 68 columns, sums to eight
// modulo ten. // modulo ten.
// 一行只有负号、补到 68 列,其对十取模的和为八。
let line = format!("{}{}", "-".repeat(68), "8"); let line = format!("{}{}", "-".repeat(68), "8");
assert_eq!(line_checksum(&line), 8); assert_eq!(line_checksum(&line), 8);
} }
@ -51,6 +55,7 @@ fn only_the_first_68_columns_contribute() {
let mut line = format!("{}0", "1".repeat(68)); let mut line = format!("{}0", "1".repeat(68));
assert_eq!(line_checksum(&line), 8); assert_eq!(line_checksum(&line), 8);
// Appending digits beyond column 68 must not change the sum. // Appending digits beyond column 68 must not change the sum.
// 在第 68 列之后追加数字不得改变该和。
line.push('9'); line.push('9');
assert_eq!(line_checksum(&line), 8); assert_eq!(line_checksum(&line), 8);
} }

View File

@ -1,10 +1,14 @@
//! Tests for parsing a complete two-line element set. //! Tests for parsing a complete two-line element set.
//!
//! 解析完整两行根数集的测试。
use apsis_time::TimeScale; use apsis_time::TimeScale;
use apsis_tle::{Classification, ElementErrorKind, TwoLineElement}; use apsis_tle::{Classification, ElementErrorKind, TwoLineElement};
/// The sample element set for the International Space Station used by the /// The sample element set for the International Space Station used by the
/// `sgp4` reference implementation. /// `sgp4` reference implementation.
///
/// `sgp4` 参考实现所用的国际空间站样本根数集。
const ISS: &str = concat!( const ISS: &str = concat!(
"1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n", "1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n",
"2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537\r\n", "2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537\r\n",
@ -31,11 +35,14 @@ fn sample_fields_match_the_reference_parser() {
fn exponent_fields_use_an_assumed_decimal_point() { fn exponent_fields_use_an_assumed_decimal_point() {
let element = TwoLineElement::parse(ISS).expect("sample is well formed"); let element = TwoLineElement::parse(ISS).expect("sample is well formed");
// `-11606-4` is -0.11606e-4. // `-11606-4` is -0.11606e-4.
// `-11606-4` 即 -0.11606e-4。
assert!((element.bstar - -1.1606e-5).abs() < 1e-18); assert!((element.bstar - -1.1606e-5).abs() < 1e-18);
// `00000-0` is zero. // `00000-0` is zero.
// `00000-0` 是零。
assert_eq!(element.mean_motion_double_dot, 0.0); assert_eq!(element.mean_motion_double_dot, 0.0);
// `-.00002182` is written with an explicit decimal point and no leading // `-.00002182` is written with an explicit decimal point and no leading
// zero. // zero.
// `-.00002182` 用显式小数点且无前导零写出。
assert!((element.mean_motion_dot - -2.182e-5).abs() < 1e-18); assert!((element.mean_motion_dot - -2.182e-5).abs() < 1e-18);
} }
@ -45,6 +52,8 @@ fn epoch_resolves_to_the_expected_day() {
// The reference parser reports Julian Date 2454730.01782528 for the // The reference parser reports Julian Date 2454730.01782528 for the
// epoch, which is Modified Julian Date 54729.51782528 on the UTC scale. // epoch, which is Modified Julian Date 54729.51782528 on the UTC scale.
// Day 264 of 2008 is 2008-09-20, and the fraction is 12:25:40. // Day 264 of 2008 is 2008-09-20, and the fraction is 12:25:40.
// 参考解析器报告该历元为儒略日 2454730.01782528,即 UTC 时标上的约化儒略日
// 54729.51782528。2008 年的第 264 天是 2008-09-20,零头为 12:25:40。
let mjd = element let mjd = element
.epoch .epoch
.mjd(TimeScale::Utc) .mjd(TimeScale::Utc)
@ -97,6 +106,8 @@ fn classification_is_read_from_the_sample() {
/// Replaces the character at the given zero based column of the given zero /// Replaces the character at the given zero based column of the given zero
/// based line and recomputes that line's checksum. /// based line and recomputes that line's checksum.
///
/// 替换给定零基行中给定零基列处的字符,并重算该行的校验和。
fn mutate(text: &str, line_index: usize, column: usize, replacement: char) -> String { fn mutate(text: &str, line_index: usize, column: usize, replacement: char) -> String {
let mut lines: Vec<String> = text.lines().map(str::to_string).collect(); let mut lines: Vec<String> = text.lines().map(str::to_string).collect();
let mut characters: Vec<char> = lines[line_index].chars().collect(); let mut characters: Vec<char> = lines[line_index].chars().collect();
@ -112,6 +123,8 @@ fn mismatched_satellite_numbers_are_rejected() {
// Change a digit of the second line's satellite number and repair the // Change a digit of the second line's satellite number and repair the
// checksum, so that only the mismatch remains. The satellite number // checksum, so that only the mismatch remains. The satellite number
// occupies one based columns 3 to 7, which are zero based 2 to 6. // occupies one based columns 3 to 7, which are zero based 2 to 6.
// 改变第二行卫星编号的一位数字并修复校验和,使只剩下不匹配。卫星编号占据以 1
// 为基的第 3 至 7 列,即以 0 为基的第 2 至 6 列。
let text = mutate(ISS, 1, 2, '3'); let text = mutate(ISS, 1, 2, '3');
let error = TwoLineElement::parse(&text).expect_err("numbers differ"); let error = TwoLineElement::parse(&text).expect_err("numbers differ");
assert_eq!(error.kind(), ElementErrorKind::MismatchedElementSet); assert_eq!(error.kind(), ElementErrorKind::MismatchedElementSet);
@ -150,6 +163,7 @@ fn an_empty_input_is_rejected() {
fn a_non_numeric_field_is_reported_with_its_columns() { fn a_non_numeric_field_is_reported_with_its_columns() {
// Replace the first inclination digit with a letter, so that the // Replace the first inclination digit with a letter, so that the
// checksum still matches but the field cannot be read. // checksum still matches but the field cannot be read.
// 把第一个倾角数字换成字母,使校验和仍匹配但该字段无法读取。
let text = mutate(ISS, 1, 8, 'X'); let text = mutate(ISS, 1, 8, 'X');
let error = TwoLineElement::parse(&text).expect_err("inclination is not numeric"); let error = TwoLineElement::parse(&text).expect_err("inclination is not numeric");