From 1dd77d6d6e7e9672deab3ebe7a8e0a138a4ddfbe Mon Sep 17 00:00:00 2001 From: ApsisRS contributors Date: Fri, 2 Oct 2026 01:56:29 +0800 Subject: [PATCH] docs: bilingual comments for the orbit tests --- .../orbit/apsis-ephemeris/tests/ephemeris.rs | 37 +++++++++++++++++ crates/orbit/apsis-kepler/tests/anomaly.rs | 7 ++++ crates/orbit/apsis-kepler/tests/elements.rs | 15 +++++++ crates/orbit/apsis-maneuver/tests/maneuver.rs | 41 +++++++++++++++++++ .../apsis-orbit/tests/ephemeris_maneuver.rs | 21 ++++++++++ crates/orbit/apsis-orbit/tests/orbit.rs | 10 +++++ crates/orbit/apsis-sgp4/tests/batch.rs | 10 +++++ crates/orbit/apsis-sgp4/tests/model.rs | 6 +++ crates/orbit/apsis-sgp4/tests/propagation.rs | 27 ++++++++++++ crates/orbit/apsis-tle/tests/checksum.rs | 5 +++ crates/orbit/apsis-tle/tests/element.rs | 14 +++++++ 11 files changed, 193 insertions(+) diff --git a/crates/orbit/apsis-ephemeris/tests/ephemeris.rs b/crates/orbit/apsis-ephemeris/tests/ephemeris.rs index b254166..e4e2858 100644 --- a/crates/orbit/apsis-ephemeris/tests/ephemeris.rs +++ b/crates/orbit/apsis-ephemeris/tests/ephemeris.rs @@ -1,4 +1,6 @@ //! Tests for the ephemeris table and its interpolation. +//! +//! 星历表及其插值的测试。 use apsis_ephemeris::{ 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 /// of the interpolation is exactly the difference from a known value, and /// the radius is constant, so a bad reconstruction shows up immediately. +/// +/// 插值所对照检查的圆轨道。 +/// +/// 圆轨道是有用的情形:状态是解析的,故插值误差恰是与已知值之差;半径恒定,故坏的 +/// 重构会立即显现。 fn circular() -> KeplerianElements { KeplerianElements { semi_major_axis: 7_000.0, @@ -24,6 +31,8 @@ fn circular() -> KeplerianElements { } /// Returns the exact state of the circular orbit at a time. +/// +/// 返回该圆轨道在某时刻的精确状态。 fn exact_state(seconds: f64) -> Sample { let elements = circular(); 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 // interpolation to do. + // 相等的时间也被拒绝,因为零宽区间没有插值可做。 let samples = vec![ 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"); // The interpolation must not perturb a sample, so the returned // state is bit for bit the stored one. + // 插值不得扰动样本,故所返回的状态与所存者逐位相同。 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"); assert_eq!(midpoint.seconds, 30.0); // The interpolated position differs from both endpoints. + // 插值所得位置不同于两个端点。 assert_ne!(midpoint.position, ephemeris.samples()[0].position); assert_ne!(midpoint.position, ephemeris.samples()[1].position); // And it is a plausible radius for the orbit. + // 且它是该轨道的合理半径。 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() { // Hermite cubic on a smooth orbit: the position error over a step of // sixty seconds should be far below a kilometer. + // 光滑轨道上的 Hermite 三次插值:六十秒步长上的位置误差应远低于一千米。 let ephemeris = sample_table(60.0, 600.0); let mut worst = 0.0_f64; 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. // This is the property that makes the interpolation worth using, and it // fails if the velocity is dropped or mis-scaled. + // 四阶方法:误差按步长的四次方缩放。这正是使该插值值得使用的性质,若丢掉速度或 + // 缩放错误它就会失败。 let error_for = |step: f64| { let ephemeris = sample_table(step, 600.0); let mut worst = 0.0_f64; // Query at the midpoints between samples, where the error is // largest. + // 在样本之间的中点查询,那里误差最大。 let mut time = step / 2.0; while time < 600.0 { 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() { // The velocity is the analytic derivative of the position cubic, so a // numerical derivative of the position must agree with it. + // 速度是位置三次式的解析导数,故位置的数值导数必须与它一致。 let ephemeris = sample_table(60.0, 600.0); let time = 137.0; 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::INFINITY).is_none()); // The endpoints are covered. + // 端点被覆盖。 assert!(ephemeris.covers(0.0)); assert!(ephemeris.covers(600.0)); 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"); assert_eq!(after, ephemeris.samples()[ephemeris.len() - 1]); // Inside the interval the clamped query is the ordinary one. + // 在区间内,夹逼查询即普通查询。 let inside = ephemeris.clamped_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()); @@ -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(31.0).expect("found").seconds, 60.0); // A midpoint ties toward the earlier sample. + // 中点并列时取较早的样本。 assert_eq!(ephemeris.nearest_sample(30.0).expect("found").seconds, 0.0); // Outside the interval the nearest end is returned. + // 在区间外返回最近的一端。 assert_eq!( ephemeris.nearest_sample(-100.0).expect("found").seconds, 0.0 @@ -266,10 +288,12 @@ fn a_property_of_every_sample_can_be_checked() { } // The sampler's grid. +// 采样器的网格。 #[test] fn the_grid_includes_both_ends() { // The end falls on the grid. + // 终点落在网格上。 let sampler = Sampler::new(0.0, 300.0, 60.0); assert_eq!(sampler.sample_count(), Some(6)); assert_eq!( @@ -278,6 +302,7 @@ fn the_grid_includes_both_ends() { ); // The end does not fall on the grid, so it is appended. + // 终点不落在网格上,故追加它。 let sampler = Sampler::new(0.0, 250.0, 60.0); assert_eq!(sampler.sample_count(), Some(6)); 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 // no interval to interpolate over and is rejected outright rather than // producing a table that cannot answer a query. + // 表至少需要两个不同的时刻,故零长度网格没有可插值的区间,会被直接拒绝而非产生 + // 一张无法回答查询的表。 let sampler = Sampler::new(5.0, 5.0, 60.0); assert!(!sampler.is_valid()); 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() { // A function that returns a wrong time must not corrupt the table: the // grid time is used instead. + // 返回错误时刻的函数不得污染该表:改用网格时刻。 let sampler = Sampler::new(0.0, 120.0, 60.0); let ephemeris = sampler .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() { // The velocity is reconstructed by finite differences, so a // position-only source still produces a usable table. + // 速度由有限差分重构,故仅提供位置的来源仍能产生可用的表。 let ephemeris = Sampler::new(0.0, 600.0, 60.0) .sample_positions(|seconds| exact_state(seconds).position) .expect("the positions are finite"); assert_eq!(ephemeris.len(), 11); // The reconstructed velocity is close to the true one. + // 所重构的速度接近真值。 let sample = ephemeris.state_at(300.0).expect("covered"); let truth = exact_state(300.0); assert!( @@ -372,6 +402,8 @@ fn positions_can_be_sampled_without_velocities() { // accurate, so its error feeds straight into the Hermite reconstruction. // The error is a few kilometers over a one minute step, against // millimeters when the velocity is known. + // 插值仍可用,尽管远不如有精确速度时准确:有限差分速度只有二阶精度,故其误差 + // 直接输入 Hermite 重构。误差在一分钟步长上为几千米,而有已知速度时为毫米级。 let mut worst = 0.0_f64; for index in 0..600 { let time = f64::from(index) + 0.5; @@ -397,6 +429,7 @@ fn position_sampling_reports_a_bad_position() { } // The Hermite basis functions directly. +// 直接检验 Hermite 基函数。 #[test] fn the_hermite_basis_reproduces_its_endpoints() { @@ -407,9 +440,11 @@ fn the_hermite_basis_reproduces_its_endpoints() { let dt = 60.0; // 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_velocity(p0, v0, p1, v1, dt, 0.0), v0); // 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_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); // Both endpoints are stationary, so the cubic is the S-curve that // passes through the midpoint with the slope the endpoints imply. + // 两个端点都静止,故该三次式是经由中点、斜率为端点所蕴含值的 S 形曲线。 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 // 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)); } diff --git a/crates/orbit/apsis-kepler/tests/anomaly.rs b/crates/orbit/apsis-kepler/tests/anomaly.rs index 643835e..9f3670e 100644 --- a/crates/orbit/apsis-kepler/tests/anomaly.rs +++ b/crates/orbit/apsis-kepler/tests/anomaly.rs @@ -1,4 +1,6 @@ //! Tests for the anomaly conversions. +//! +//! 近点角换算的测试。 use apsis_kepler::{ 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}" ); // The result differs from the input by a whole number of turns. + // 结果与输入相差整数圈。 let turns_removed = (angle - wrapped) / (2.0 * PI); assert!((turns_removed - turns_removed.round()).abs() < 1e-9); } @@ -33,6 +36,7 @@ fn wrap_keeps_values_already_in_range() { #[test] 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。 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] { let eccentric = eccentric_from_mean(mean, eccentricity); @@ -76,6 +80,7 @@ fn mean_and_true_anomaly_round_trip() { #[test] fn a_circular_orbit_has_equal_anomalies() { // With zero eccentricity every anomaly is the same angle. + // 偏心率为零时每种近点角都是同一个角。 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!((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, // so the true anomaly runs ahead of the mean anomaly in the first half // of the orbit. + // 对偏心轨道,卫星在近地点附近花的时间较少,故在轨道的上半段真近点角领先于平 + // 近点角。 let eccentricity = 0.5; let mean = 1.0; let true_anomaly = true_from_mean(mean, eccentricity); diff --git a/crates/orbit/apsis-kepler/tests/elements.rs b/crates/orbit/apsis-kepler/tests/elements.rs index aef70d7..c3d1e5c 100644 --- a/crates/orbit/apsis-kepler/tests/elements.rs +++ b/crates/orbit/apsis-kepler/tests/elements.rs @@ -4,6 +4,11 @@ //! perifocal frame expressions in extended precision. Only the point mass //! gravity of the central body is involved, so the two should agree to //! floating point rounding. +//! +//! 开普勒根数及其状态矢量换算的测试。 +//! +//! 期望值是以扩展精度从标准近焦点系表达式独立计算的。只涉及中心天体的质点引力, +//! 故二者应在浮点舍入内一致。 use apsis_kepler::{GravityParameter, KeplerianElements, State, wrap_to_pi}; 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 // satellite sits on the first axis and moves along the second, with // speed sqrt(mu / a). + // 在零偏心率、零倾角、零取向时,卫星位于第一轴上、沿第二轴运动,速率为 + // sqrt(mu / a)。 let elements = KeplerianElements { semi_major_axis: 6_378.137, eccentricity: 0.0, @@ -106,6 +113,7 @@ fn the_state_vector_lies_in_the_orbit_plane() { .expect("elements are valid"); // The position and velocity span the orbit plane, so the normal to that // plane is perpendicular to both. + // 位置与速度张成轨道面,故该面的法线垂直于二者。 let normal = state .position .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(); assert!((period - expected).abs() < 1e-9); // A 7000 km orbit has a period of about 97 minutes. + // 7000 km 轨道的周期约为 97 分钟。 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 // as 5.0 radians and one reduced to -1.28 radians describe the same // orientation. + // 各角按模一周比较,因为诸如 5.0 弧度这样的值与归约到 -1.28 弧度的值描述同一 + // 取向。 assert!((wrap_to_pi(recovered.right_ascension - elements.right_ascension)).abs() < 1e-9); assert!( (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, // which is the stronger check. + // 往返必须既复现状态也复现根数,后者是更强的检查。 let again = recovered.to_state(GravityParameter::WGS84).expect("valid"); assert!((again.position - state.position).norm() < 1e-6); assert!((again.velocity - state.velocity).norm() < 1e-9); @@ -287,6 +299,7 @@ fn degenerate_states_are_rejected() { None ); // A purely radial state has no defined orbit plane. + // 纯径向状态没有定义的轨道面。 assert_eq!( KeplerianElements::from_state( &State { @@ -298,6 +311,7 @@ fn degenerate_states_are_rejected() { None ); // An unbound state has a non-positive semi-major axis. + // 非束缚状态的半长轴非正。 assert_eq!( KeplerianElements::from_state( &State { @@ -321,6 +335,7 @@ fn gravity_parameter_values_match_the_standards() { fn a_full_revolution_returns_to_the_same_point() { // Advancing the mean anomaly by one full turn must return the same // position, which verifies the anomaly conversions agree. + // 把平近点角推进一整圈必须返回相同位置,这验证各近点角换算彼此一致。 let elements = elliptical_orbit(); let start = elements .with_mean_anomaly(1.0) diff --git a/crates/orbit/apsis-maneuver/tests/maneuver.rs b/crates/orbit/apsis-maneuver/tests/maneuver.rs index c9fa826..a63d312 100644 --- a/crates/orbit/apsis-maneuver/tests/maneuver.rs +++ b/crates/orbit/apsis-maneuver/tests/maneuver.rs @@ -1,10 +1,14 @@ //! Tests for the maneuver models. +//! +//! 机动模型的测试。 use apsis_kepler::{GravityParameter, KeplerianElements}; use apsis_maneuver::{FiniteBurn, Impulse, ManeuverErrorKind, hohmann_transfer}; use apsis_math::Vec3; /// A circular orbit at a given radius, in the equatorial plane. +/// +/// 给定半径、位于赤道面内的圆轨道。 fn circular(radius: f64) -> KeplerianElements { KeplerianElements { semi_major_axis: radius, @@ -17,6 +21,7 @@ fn circular(radius: f64) -> KeplerianElements { } // The impulsive model. +// 脉冲模型。 #[test] fn an_impulse_reports_its_change() { @@ -36,12 +41,14 @@ fn a_zero_or_non_finite_impulse_is_refused() { #[test] fn an_impulse_can_be_built_from_a_direction_and_a_magnitude() { // The direction need not be a unit vector. + // 方向不必是单位矢量。 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.delta_v().y - 0.5).abs() < 1e-12); assert!(impulse.delta_v().z.abs() < 1e-12); // A zero direction and a bad magnitude are refused. + // 零方向与坏幅度被拒绝。 assert!(Impulse::along(Vec3::ZERO, 1.0).is_err()); assert!(Impulse::along(Vec3::X, 0.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(); assert!((reversed.delta_v().x + 1.0).abs() < 1e-12); // Scaling by zero produces a zero change, which is refused. + // 按零缩放产生零变化,会被拒绝。 assert!(impulse.scaled(0.0).is_err()); } @@ -75,14 +83,17 @@ fn an_impulse_displays_its_magnitude() { } // The Hohmann transfer. +// Hohmann 转移。 #[test] fn a_hohmann_transfer_raises_an_orbit() { // Raise from 7000 to 8000 km. + // 从 7000 升到 8000 km。 let transfer = hohmann_transfer(7_000.0, 8_000.0, GravityParameter::WGS84).expect("the radii are usable"); assert!(transfer.is_raising()); // Raising an orbit takes two prograde burns. + // 抬高轨道需要两次顺行点火。 assert!(transfer.first_burn > 0.0); assert!(transfer.second_burn > 0.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() { // The same two radii in the other order is a descent, and the burns are // retrograde. + // 两个半径反序即为下降,两次点火都是逆行。 let transfer = hohmann_transfer(8_000.0, 7_000.0, GravityParameter::WGS84).expect("the radii are usable"); assert!(!transfer.is_raising()); assert!(transfer.first_burn < 0.0); assert!(transfer.second_burn < 0.0); // The cost is the same either way. + // 两种方向的代价相同。 let raised = 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); @@ -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 circular speed there. Checking it against the vis-viva equation // directly pins the formula. + // 第一次点火是转移椭圆在下半径处的速率减去那里的圆轨道速率。用活力公式直接核对 + // 它即可钉住该公式。 let transfer = hohmann_transfer(7_000.0, 9_000.0, GravityParameter::WGS84).expect("the radii are usable"); 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 // 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. + // 有充分记载的用例:从 300 km 圆停泊轨道转移到地球静止高度。总计约 3.89 km/s, + // 这是标准 LEO 到 GEO 化学转移所引用的数字。 let from = 6_378.137 + 300.0; let to = 42_164.0; 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 // radius and its apogee the destination, so its eccentricity follows // from the two radii. + // 转移椭圆与两个轨道都相切:其近地点是停泊半径,远地点是目的地,故其偏心率由 + // 这两个半径得出。 let eccentricity = (to - from) / (to + from); let perigee = 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. +// 有限点火。 #[test] 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_eq!(burn.duration(), 100.0); // The nominal change is the acceleration times the duration. + // 标称变化是加速度乘以时长。 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 // nominal change: it is a constant acceleration, so the velocity change // is the acceleration times the duration. + // 在引力关掉时,有限点火必须恰好给出标称变化:它是常值加速度,故速度变化是 + // 加速度乘以时长。 // // 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 // 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 state = apsis_kepler::State { position: Vec3::new(1.0e9, 0.0, 0.0), velocity: Vec3::ZERO, }; // A negligible gravitational parameter leaves only the thrust. + // 可忽略的引力参数只留下推力。 let outcome = burn .apply(state, GravityParameter(0.0), 100) .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); // There is no gravity to lose anything to. + // 没有引力可损失任何东西。 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 // the limit of a shrinking duration, because the gravity deflection // during the burn vanishes with the duration. + // 这正是把两个模型联系起来的性质。加速度为 `a`、时长为 `t` 的有限点火在时长 + // 趋于零的极限下给出冲量 `a t`,因为点火期间的引力偏折随时长消失。 let parameter = GravityParameter::WGS84; let elements = circular(7_000.0); let state = elements.to_state(parameter).expect("valid"); // The thrust direction is along the velocity, which for this orbit is // the second axis. + // 推力方向沿速度,对该轨道而言即第二轴。 let direction = state.velocity.normalize().expect("the speed is non-zero"); let target_delta_v = 0.01; let achieved_for = |duration: f64| { // Hold the nominal delta-v fixed by scaling the acceleration, so // every burn is compared at the same impulse. + // 通过缩放加速度保持标称 delta-v 不变,使每次点火都在相同冲量下比较。 let acceleration = target_delta_v / duration; let burn = FiniteBurn::along(direction, acceleration, duration).expect("usable"); 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 // nearly all of it. + // 长点火把部分变化损失给引力;短点火几乎全部保留。 let long = achieved_for(600.0); let short = achieved_for(6.0); 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 // point of the test is that the two are not equal, which is why the // finite model exists. + // 圆轨道上的长顺行点火:点火进行时引力使速度转向,故所达到的变化不同于自由空间 + // 值。差值的符号取决于几何;本测试的要点是二者不相等,这正是有限模型存在的 + // 原因。 let parameter = GravityParameter::WGS84; let elements = circular(7_000.0); let state = elements.to_state(parameter).expect("valid"); let direction = state.velocity.normalize().expect("the speed is non-zero"); // A burn lasting a tenth of the orbital period. + // 持续轨道周期十分之一的点火。 let duration = elements.period(parameter) / 10.0; let acceleration = 1e-5; 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() ); // The position moved during the burn. + // 点火期间位置移动了。 assert!(outcome.state.position.distance(state.position) > 1.0); } #[test] fn a_finite_burn_raises_the_orbit_when_prograde() { // A prograde burn adds energy, so the semi-major axis grows. + // 顺行点火增加能量,故半长轴增大。 let parameter = GravityParameter::WGS84; let elements = circular(7_000.0); let state = elements.to_state(parameter).expect("valid"); @@ -322,6 +360,7 @@ fn a_finite_burn_raises_the_orbit_when_prograde() { #[test] fn a_finite_burn_lowers_the_orbit_when_retrograde() { // The mirror image: a retrograde burn removes energy. + // 镜像情形:逆行点火移除能量。 let parameter = GravityParameter::WGS84; let elements = circular(7_000.0); 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) .expect("the burn runs"); // Both routes must give the same state. + // 两条路径必须给出相同状态。 assert!( from_state .state @@ -382,6 +422,7 @@ fn a_burn_with_no_steps_is_refused() { fn a_large_burn_can_escape_and_is_reported() { // A burn hard enough to exceed escape velocity leaves no elliptical // elements, which the outcome reports by returning `None`. + // 强度足以超过逃逸速度的点火不留下椭圆根数,结果通过返回 `None` 报告这一点。 let parameter = GravityParameter::WGS84; let state = circular(7_000.0).to_state(parameter).expect("valid"); let direction = state.velocity.normalize().expect("non-zero"); diff --git a/crates/orbit/apsis-orbit/tests/ephemeris_maneuver.rs b/crates/orbit/apsis-orbit/tests/ephemeris_maneuver.rs index 2d47c43..977f99c 100644 --- a/crates/orbit/apsis-orbit/tests/ephemeris_maneuver.rs +++ b/crates/orbit/apsis-orbit/tests/ephemeris_maneuver.rs @@ -1,4 +1,6 @@ //! Integration tests for sampling and maneuvering through the facade. +//! +//! 通过门面进行采样与机动的集成测试。 use apsis_kepler::GravityParameter; use apsis_maneuver::{FiniteBurn, hohmann_transfer}; @@ -6,6 +8,8 @@ use apsis_orbit::{EphemerisSamplingError, GravityModel, ManeuverApplicationError use apsis_tle::TwoLineElement; /// A satellite in a low Earth orbit, used as the source of the track. +/// +/// 一颗低轨卫星,用作轨迹的来源。 const ISS: &str = concat!( "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", @@ -20,15 +24,18 @@ fn orbit() -> Orbit { fn an_orbit_samples_into_an_ephemeris() { let orbit = orbit(); // Ten minutes over a one minute grid. + // 一分钟网格上的十分钟。 let ephemeris = orbit .sample_ephemeris(0.0, 10.0, 1.0) .expect("the grid is usable"); assert_eq!(ephemeris.len(), 11); // The sample times are seconds from the element epoch. + // 样本时刻是距根数历元的秒数。 assert_eq!(ephemeris.start_time(), 0.0); assert_eq!(ephemeris.end_time(), 600.0); // Every sample must match a direct propagation. + // 每个样本必须与直接传播一致。 for sample in ephemeris.samples() { let direct = orbit .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() { // This is the point of the ephemeris: a query between samples must agree // with the propagator to well within the accuracy a track needs. + // 这正是星历的要点:样本之间的查询必须与传播器一致,且远在一条轨迹所需的精度 + // 之内。 let orbit = orbit(); let ephemeris = orbit .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; // Query every ten seconds, which includes points between samples. + // 每十秒查询一次,其中含样本之间的点。 for index in 0..180 { let seconds = f64::from(index) * 10.0; 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 // better than a meter here. + // 低轨上一分钟的 Hermite 步在此精确到远优于一米。 assert!(worst < 1e-3, "the worst interpolation error is {worst} km"); } @@ -85,6 +96,7 @@ fn a_propagation_failure_is_reported() { let orbit = orbit(); // Far enough ahead that the drag term brings the satellite down, which // 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); assert!( 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 state = orbit.propagate_minutes(0.0).expect("propagates"); // Thrust along the velocity. + // 沿速度方向施加推力。 let direction = state.velocity.normalize().expect("non-zero"); 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() { // The facade must be a thin wrapper: applying the burn through it is the // same as propagating and applying the burn by hand. + // 门面必须是薄封装:通过它施加点火与手工传播并施加点火相同。 let orbit = orbit(); let state = orbit.propagate_minutes(5.0).expect("propagates"); 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 direction = state.velocity.normalize().expect("non-zero"); // A burn far larger than orbital velocity. + // 远大于轨道速度的点火。 let burn = FiniteBurn::along(direction, 1e-2, 600.0).expect("usable"); let result = orbit.apply_burn(0.0, burn, 2_000); 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 // propagation: plan a transfer, apply the first burn, and sample the // resulting orbit. + // 把 maneuver 与 ephemeris crate 联系到传播的跨层检查:规划一次转移,施加第一次 + // 点火,并对所得轨道采样。 let orbit = orbit(); let elements = orbit.osculating_elements(0.0).expect("elliptical"); 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"); // The first burn is prograde for a raise, so thrust along the velocity. + // 抬高时第一次点火是顺行的,故沿速度方向施加推力。 let state = orbit.propagate_minutes(0.0).expect("propagates"); let direction = state.velocity.normalize().expect("non-zero"); // Turn the delta-v into an acceleration over a short burn. + // 在短点火中把 delta-v 化为加速度。 let duration = 10.0; let burn = 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 // elements, which SGP4 requires and an osculating set does not provide, // so the workflow stops here rather than feeding them back in. + // 点火后状态的密切根数即规划结果。要把它们转回可传播的轨道需要平根数,SGP4 + // 要求平根数而密切根数集不提供,故该工作流到此为止,而不把它们送回。 assert!(after.is_valid()); assert!(after.eccentricity < 1.0); } diff --git a/crates/orbit/apsis-orbit/tests/orbit.rs b/crates/orbit/apsis-orbit/tests/orbit.rs index 06fc2bb..9023587 100644 --- a/crates/orbit/apsis-orbit/tests/orbit.rs +++ b/crates/orbit/apsis-orbit/tests/orbit.rs @@ -1,4 +1,6 @@ //! Tests for the combined orbit type. +//! +//! 合并后的轨道类型的测试。 use apsis_orbit::{GravityModel, Orbit}; 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); // One day later is 1440 minutes. + // 一天后是 1440 分钟。 let next_day = orbit.propagate_at(epoch + 1.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); @@ -80,6 +83,9 @@ fn osculating_elements_describe_a_low_earth_orbit() { // is expected: the element set stores mean elements and the osculating // eccentricity includes the short period variation the two-body model // does not reproduce. + // 这些值是针对同一天量矢量的一份独立计算核对的。倾角与编目值接近,因为扁率对它 + // 影响很小,而偏心率不同于编目的 0.0006703。该差异是预期的:根数集保存的是平 + // 根数,而密切偏心率包含二体模型不复现的短周期变化。 assert!( (elements.semi_major_axis - 6_725.542).abs() < 0.01, "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 // because the perturbations SGP4 models are absent from the two-body // reconstruction. + // 密切根数不是平根数,故它们变化,因为 SGP4 所建模的摄动在二体重构中缺席。 let orbit = orbit(ISS); let start = orbit.osculating_elements(0.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 // inclination of 64.1526 degrees. The osculating values are close but // not identical for the reason given in the low Earth orbit test. + // 编目的平值为偏心率 0.6546925、倾角 64.1526 度。密切值接近但不相同,理由同低轨 + // 测试中所述。 assert!( (elements.eccentricity - 0.655_871_3).abs() < 1e-6, "eccentricity {}", @@ -164,6 +173,7 @@ fn the_gravity_parameter_matches_the_model() { fn the_facade_reexports_the_layer() { // The facade must make the element parser and the propagator types // reachable without a separate dependency. + // 门面必须使根数解析器与传播器类型无需单独依赖即可达到。 let element = apsis_orbit::TwoLineElement::parse(ISS).expect("parses"); assert_eq!(element.satellite_number, 25544); let satellite = diff --git a/crates/orbit/apsis-sgp4/tests/batch.rs b/crates/orbit/apsis-sgp4/tests/batch.rs index 9a79e89..0773558 100644 --- a/crates/orbit/apsis-sgp4/tests/batch.rs +++ b/crates/orbit/apsis-sgp4/tests/batch.rs @@ -4,6 +4,11 @@ //! values the sequential forms do, in the same order. Every test compares //! against the sequential answer rather than against a constant, so a //! mismatch in either the propagation or the ordering is caught. +//! +//! 批量传播入口的测试。 +//! +//! 要紧的性质是并行形式产生与顺序形式完全相同的值,且次序相同。每个测试都与顺序 +//! 答案而非常量比较,故传播或排序中的任何不一致都会被捕获。 use apsis_sgp4::{ 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() { // A day at a one minute step, which is long enough that the parallel path // takes the multi thread branch. + // 一分钟步长的一天,长到足以使并行路径走多线程分支。 let satellite = satellite(); let minutes: Vec = (0..1_441).map(|index| index as f64).collect(); let sequential = propagate_minutes_at(&satellite, &minutes); @@ -45,6 +51,7 @@ fn a_long_track_matches_the_sequential_form() { fn a_backwards_track_matches() { // Negative minutes propagate backwards and must behave the same way in // both forms. + // 负分钟数向后传播,且在两种形式中必须表现相同。 let satellite = satellite(); let minutes: Vec = (0..2_000).map(|index| -(index as f64) * 0.5).collect(); 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 // forms must agree that the instant is unusable rather than one of them // stopping early. + // 足够远,使阻力项把卫星带下来,两种形式必须一致认为该瞬时不可用,而非其中之 + // 一提前停止。 let satellite = satellite(); let minutes = vec![0.0, 1.0, 60.0 * 24.0 * 365.0 * 20.0]; 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() { // The states of a repeated satellite are identical, so ordering is checked // through the minutes form, where each value differs. + // 重复卫星的状态相同,故通过分钟形式检查次序,那里每个值都不同。 let satellite = satellite(); let minutes: Vec = (0..3_000).map(|index| index as f64 * 0.25).collect(); let parallel = propagate_minutes_at_parallel(&satellite, &minutes); diff --git a/crates/orbit/apsis-sgp4/tests/model.rs b/crates/orbit/apsis-sgp4/tests/model.rs index cef20f0..4cb62a4 100644 --- a/crates/orbit/apsis-sgp4/tests/model.rs +++ b/crates/orbit/apsis-sgp4/tests/model.rs @@ -1,4 +1,6 @@ //! Tests for the gravity models. +//! +//! 引力模型的测试。 use apsis_sgp4::GravityModel; @@ -22,6 +24,8 @@ fn wgs84_defining_values_match_the_standard() { fn kepler_constant_matches_the_reference_value() { // The reference implementation uses xke = 0.07436691613317342 for WGS // 72, derived as 60 / sqrt(radius^3 / mu). + // 参考实现对 WGS 72 使用 xke = 0.07436691613317342,由 60 / sqrt(radius^3 / mu) + // 导出。 let xke = GravityModel::WGS72.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; assert!((model.tumin() * model.xke() - 1.0).abs() < 1e-15); // 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); } @@ -40,6 +45,7 @@ fn j3_over_j2_matches_the_quotient() { let expected = model.j3 / model.j2; assert!((model.j3_over_j2() - expected).abs() < 1e-18); // The value is about -0.002345069720011528. + // 该值约为 -0.002345069720011528。 assert!((model.j3_over_j2() + 0.002_345_069_720_011_528).abs() < 1e-15); } diff --git a/crates/orbit/apsis-sgp4/tests/propagation.rs b/crates/orbit/apsis-sgp4/tests/propagation.rs index b1949c1..c4ea39f 100644 --- a/crates/orbit/apsis-sgp4/tests/propagation.rs +++ b/crates/orbit/apsis-sgp4/tests/propagation.rs @@ -7,11 +7,19 @@ //! treated as correct, because both implementations evaluate the same //! closed form expressions and should differ only by floating point //! rounding. +//! +//! 传播器对照参考实现的测试。 +//! +//! 本文件中的期望状态由 `sgp4` 参考库产生,它实现 Vallado 对该理论的修订。各根数 +//! 集是随该库分发的历史测试用例。与参考相符到远低于一微米的传播被视为正确,因为 +//! 两种实现求相同的闭式表达式,应只差浮点舍入。 use apsis_sgp4::{GravityModel, PropagationErrorKind, Satellite, State}; use apsis_tle::TwoLineElement; /// A near circular low Earth orbit, the International Space Station. +/// +/// 近圆形低轨,国际空间站。 const ISS: &str = concat!( "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", @@ -19,18 +27,24 @@ const ISS: &str = concat!( /// A highly eccentric orbit, which exercises the eccentric anomaly /// iteration away from circular motion. +/// +/// 一个高度偏心的轨道,它检验远离圆运动时的偏近点角迭代。 const VANGUARD: &str = concat!( "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", ); /// A deep space orbit with a half-day resonance. +/// +/// 一个具有半日共振的深空轨道。 const DEEP_SPACE: &str = concat!( "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", ); /// A geostationary transfer orbit, also deep space. +/// +/// 一个地球静止转移轨道,也是深空。 const TRANSFER: &str = concat!( "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", @@ -38,9 +52,13 @@ const TRANSFER: &str = concat!( /// The position tolerance, in kilometers. The observed agreement is at the /// nanometer level, so this leaves a wide margin. +/// +/// 位置容差,单位为千米。所观测的相符在纳米水平,故这留下很宽的余量。 const POSITION_TOLERANCE: f64 = 1e-3; /// The velocity tolerance, in kilometers per second. +/// +/// 速度容差,单位为千米每秒。 const VELOCITY_TOLERANCE: f64 = 1e-9; fn satellite(text: &str) -> Satellite { @@ -156,6 +174,7 @@ fn deep_space_satellite_uses_the_deep_space_branch() { #[test] fn deep_space_at_epoch_matches_the_reference() { // MJD 53_962.36, about 12 hours after the element epoch. + // MJD 53_962.36,约在根数历元之后 12 小时。 let state = satellite(DEEP_SPACE) .propagate_minutes(0.0) .expect("epoch state"); @@ -207,6 +226,7 @@ fn propagation_is_deterministic() { // Repeated evaluation must give the same result, which also verifies // that the resonance integrator does not carry hidden state between // calls. + // 重复求值必须给出相同结果,这也验证共振积分器在调用之间不携带隐藏状态。 let satellite = satellite(DEEP_SPACE); let first = 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() { // Evaluating a later time first must not change a later evaluation of an // earlier time. + // 先求较晚的时间不得改变其后对较早时间的求值。 let satellite = satellite(DEEP_SPACE); let late = satellite.propagate_minutes(10_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 // elongated orbit. An eccentricity field that cannot be read at all is // rejected by the parser instead, which is checked in the TLE tests. + // 首位数字为九的偏心率是用隐含小数点写成的,故它是 0.9999999,一个有效但极其 + // 细长的轨道。完全无法读取的偏心率字段改由解析器拒绝,这在 TLE 测试中检查。 let text = concat!( "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", ); let element = TwoLineElement::parse(text).expect("parses"); // A circular orbit is perfectly valid, so this must succeed. + // 圆轨道完全有效,故这必须成功。 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 // checksum, so the test builds one from a parsed set rather than // editing text. + // 平运动字段以每天圈数计,故那里的零描述不出轨道。根数集仍须携带有效校验和,故 + // 测试从已解析的集合构建一个,而非编辑文本。 let element = TwoLineElement::parse(ISS).expect("parses"); let mut elements = apsis_sgp4::Elements::from_tle(&element).expect("epoch is covered"); 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.speed() - state.velocity.norm()).abs() < 1e-12); // A low Earth satellite moves at about 7.7 km/s. + // 低轨卫星约以 7.7 km/s 运动。 assert!(state.speed() > 7.0 && state.speed() < 8.0); } diff --git a/crates/orbit/apsis-tle/tests/checksum.rs b/crates/orbit/apsis-tle/tests/checksum.rs index 9e3f122..799ec0a 100644 --- a/crates/orbit/apsis-tle/tests/checksum.rs +++ b/crates/orbit/apsis-tle/tests/checksum.rs @@ -1,4 +1,6 @@ //! Tests for line checksums. +//! +//! 行校验和的测试。 use apsis_tle::{line_checksum, verify_line_checksum}; @@ -20,6 +22,7 @@ fn verification_accepts_a_well_formed_line() { #[test] fn verification_rejects_a_corrupted_digit() { // Changing a digit in the body changes the sum. + // 改变正文中的一位数字会改变该和。 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"); 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() { // A line of only minus signs, padded to 68 columns, sums to eight // modulo ten. + // 一行只有负号、补到 68 列,其对十取模的和为八。 let line = format!("{}{}", "-".repeat(68), "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)); assert_eq!(line_checksum(&line), 8); // Appending digits beyond column 68 must not change the sum. + // 在第 68 列之后追加数字不得改变该和。 line.push('9'); assert_eq!(line_checksum(&line), 8); } diff --git a/crates/orbit/apsis-tle/tests/element.rs b/crates/orbit/apsis-tle/tests/element.rs index 1f9f616..eb81467 100644 --- a/crates/orbit/apsis-tle/tests/element.rs +++ b/crates/orbit/apsis-tle/tests/element.rs @@ -1,10 +1,14 @@ //! Tests for parsing a complete two-line element set. +//! +//! 解析完整两行根数集的测试。 use apsis_time::TimeScale; use apsis_tle::{Classification, ElementErrorKind, TwoLineElement}; /// The sample element set for the International Space Station used by the /// `sgp4` reference implementation. +/// +/// `sgp4` 参考实现所用的国际空间站样本根数集。 const ISS: &str = concat!( "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", @@ -31,11 +35,14 @@ fn sample_fields_match_the_reference_parser() { fn exponent_fields_use_an_assumed_decimal_point() { let element = TwoLineElement::parse(ISS).expect("sample is well formed"); // `-11606-4` is -0.11606e-4. + // `-11606-4` 即 -0.11606e-4。 assert!((element.bstar - -1.1606e-5).abs() < 1e-18); // `00000-0` is zero. + // `00000-0` 是零。 assert_eq!(element.mean_motion_double_dot, 0.0); // `-.00002182` is written with an explicit decimal point and no leading // zero. + // `-.00002182` 用显式小数点且无前导零写出。 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 // 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. + // 参考解析器报告该历元为儒略日 2454730.01782528,即 UTC 时标上的约化儒略日 + // 54729.51782528。2008 年的第 264 天是 2008-09-20,零头为 12:25:40。 let mjd = element .epoch .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 /// based line and recomputes that line's checksum. +/// +/// 替换给定零基行中给定零基列处的字符,并重算该行的校验和。 fn mutate(text: &str, line_index: usize, column: usize, replacement: char) -> String { let mut lines: Vec = text.lines().map(str::to_string).collect(); let mut characters: Vec = 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 // checksum, so that only the mismatch remains. The satellite number // 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 error = TwoLineElement::parse(&text).expect_err("numbers differ"); 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() { // Replace the first inclination digit with a letter, so that the // checksum still matches but the field cannot be read. + // 把第一个倾角数字换成字母,使校验和仍匹配但该字段无法读取。 let text = mutate(ISS, 1, 8, 'X'); let error = TwoLineElement::parse(&text).expect_err("inclination is not numeric");