docs: bilingual comments for the orbit tests
This commit is contained in:
parent
fb4fe93b51
commit
1dd77d6d6e
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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<f64> = (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<f64> = (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<f64> = (0..3_000).map(|index| index as f64 * 0.25).collect();
|
||||
let parallel = propagate_minutes_at_parallel(&satellite, &minutes);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> = text.lines().map(str::to_string).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
|
||||
// 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");
|
||||
|
|
|
|||
Loading…
Reference in New Issue