docs: bilingual comments for the orbit tests
This commit is contained in:
parent
1ffba97a14
commit
4f90474610
|
|
@ -1,4 +1,6 @@
|
||||||
//! Tests for the ephemeris table and its interpolation.
|
//! Tests for the ephemeris table and its interpolation.
|
||||||
|
//!
|
||||||
|
//! 星历表及其插值的测试。
|
||||||
|
|
||||||
use apsis_ephemeris::{
|
use apsis_ephemeris::{
|
||||||
Ephemeris, EphemerisErrorKind, Sample, Sampler, hermite_position, hermite_state,
|
Ephemeris, EphemerisErrorKind, Sample, Sampler, hermite_position, hermite_state,
|
||||||
|
|
@ -12,6 +14,11 @@ use apsis_math::Vec3;
|
||||||
/// A circular orbit is the useful case: the state is analytic, so the error
|
/// A circular orbit is the useful case: the state is analytic, so the error
|
||||||
/// of the interpolation is exactly the difference from a known value, and
|
/// of the interpolation is exactly the difference from a known value, and
|
||||||
/// the radius is constant, so a bad reconstruction shows up immediately.
|
/// the radius is constant, so a bad reconstruction shows up immediately.
|
||||||
|
///
|
||||||
|
/// 插值所对照检查的圆轨道。
|
||||||
|
///
|
||||||
|
/// 圆轨道是有用的情形:状态是解析的,故插值误差恰是与已知值之差;半径恒定,故坏的
|
||||||
|
/// 重构会立即显现。
|
||||||
fn circular() -> KeplerianElements {
|
fn circular() -> KeplerianElements {
|
||||||
KeplerianElements {
|
KeplerianElements {
|
||||||
semi_major_axis: 7_000.0,
|
semi_major_axis: 7_000.0,
|
||||||
|
|
@ -24,6 +31,8 @@ fn circular() -> KeplerianElements {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the exact state of the circular orbit at a time.
|
/// Returns the exact state of the circular orbit at a time.
|
||||||
|
///
|
||||||
|
/// 返回该圆轨道在某时刻的精确状态。
|
||||||
fn exact_state(seconds: f64) -> Sample {
|
fn exact_state(seconds: f64) -> Sample {
|
||||||
let elements = circular();
|
let elements = circular();
|
||||||
let mean = 2.0 * core::f64::consts::PI * seconds / elements.period(GravityParameter::WGS84);
|
let mean = 2.0 * core::f64::consts::PI * seconds / elements.period(GravityParameter::WGS84);
|
||||||
|
|
@ -78,6 +87,7 @@ fn samples_must_be_in_increasing_time_order() {
|
||||||
|
|
||||||
// Equal times are also refused, because a zero width interval has no
|
// Equal times are also refused, because a zero width interval has no
|
||||||
// interpolation to do.
|
// interpolation to do.
|
||||||
|
// 相等的时间也被拒绝,因为零宽区间没有插值可做。
|
||||||
let samples = vec![
|
let samples = vec![
|
||||||
Sample::new(5.0, Vec3::ZERO, Vec3::ZERO),
|
Sample::new(5.0, Vec3::ZERO, Vec3::ZERO),
|
||||||
Sample::new(5.0, Vec3::ZERO, Vec3::ZERO),
|
Sample::new(5.0, Vec3::ZERO, Vec3::ZERO),
|
||||||
|
|
@ -105,6 +115,7 @@ fn a_query_at_a_sample_returns_it_exactly() {
|
||||||
.expect("the sample time is covered");
|
.expect("the sample time is covered");
|
||||||
// The interpolation must not perturb a sample, so the returned
|
// The interpolation must not perturb a sample, so the returned
|
||||||
// state is bit for bit the stored one.
|
// state is bit for bit the stored one.
|
||||||
|
// 插值不得扰动样本,故所返回的状态与所存者逐位相同。
|
||||||
assert_eq!(queried, *sample, "at {} s", sample.seconds);
|
assert_eq!(queried, *sample, "at {} s", sample.seconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -115,9 +126,11 @@ fn a_query_between_samples_is_interpolated() {
|
||||||
let midpoint = ephemeris.state_at(30.0).expect("the time is covered");
|
let midpoint = ephemeris.state_at(30.0).expect("the time is covered");
|
||||||
assert_eq!(midpoint.seconds, 30.0);
|
assert_eq!(midpoint.seconds, 30.0);
|
||||||
// The interpolated position differs from both endpoints.
|
// The interpolated position differs from both endpoints.
|
||||||
|
// 插值所得位置不同于两个端点。
|
||||||
assert_ne!(midpoint.position, ephemeris.samples()[0].position);
|
assert_ne!(midpoint.position, ephemeris.samples()[0].position);
|
||||||
assert_ne!(midpoint.position, ephemeris.samples()[1].position);
|
assert_ne!(midpoint.position, ephemeris.samples()[1].position);
|
||||||
// And it is a plausible radius for the orbit.
|
// And it is a plausible radius for the orbit.
|
||||||
|
// 且它是该轨道的合理半径。
|
||||||
assert!((midpoint.radius() - 7_000.0).abs() < 1.0);
|
assert!((midpoint.radius() - 7_000.0).abs() < 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,6 +138,7 @@ fn a_query_between_samples_is_interpolated() {
|
||||||
fn the_interpolation_is_accurate_on_a_circular_orbit() {
|
fn the_interpolation_is_accurate_on_a_circular_orbit() {
|
||||||
// Hermite cubic on a smooth orbit: the position error over a step of
|
// Hermite cubic on a smooth orbit: the position error over a step of
|
||||||
// sixty seconds should be far below a kilometer.
|
// sixty seconds should be far below a kilometer.
|
||||||
|
// 光滑轨道上的 Hermite 三次插值:六十秒步长上的位置误差应远低于一千米。
|
||||||
let ephemeris = sample_table(60.0, 600.0);
|
let ephemeris = sample_table(60.0, 600.0);
|
||||||
let mut worst = 0.0_f64;
|
let mut worst = 0.0_f64;
|
||||||
for index in 0..600 {
|
for index in 0..600 {
|
||||||
|
|
@ -144,11 +158,14 @@ fn halving_the_step_cuts_the_error_by_about_sixteen() {
|
||||||
// A quartic method: the error scales as the fourth power of the step.
|
// A quartic method: the error scales as the fourth power of the step.
|
||||||
// This is the property that makes the interpolation worth using, and it
|
// This is the property that makes the interpolation worth using, and it
|
||||||
// fails if the velocity is dropped or mis-scaled.
|
// fails if the velocity is dropped or mis-scaled.
|
||||||
|
// 四阶方法:误差按步长的四次方缩放。这正是使该插值值得使用的性质,若丢掉速度或
|
||||||
|
// 缩放错误它就会失败。
|
||||||
let error_for = |step: f64| {
|
let error_for = |step: f64| {
|
||||||
let ephemeris = sample_table(step, 600.0);
|
let ephemeris = sample_table(step, 600.0);
|
||||||
let mut worst = 0.0_f64;
|
let mut worst = 0.0_f64;
|
||||||
// Query at the midpoints between samples, where the error is
|
// Query at the midpoints between samples, where the error is
|
||||||
// largest.
|
// largest.
|
||||||
|
// 在样本之间的中点查询,那里误差最大。
|
||||||
let mut time = step / 2.0;
|
let mut time = step / 2.0;
|
||||||
while time < 600.0 {
|
while time < 600.0 {
|
||||||
let interpolated = ephemeris.state_at(time).expect("covered");
|
let interpolated = ephemeris.state_at(time).expect("covered");
|
||||||
|
|
@ -172,6 +189,7 @@ fn halving_the_step_cuts_the_error_by_about_sixteen() {
|
||||||
fn the_interpolated_velocity_matches_the_position_derivative() {
|
fn the_interpolated_velocity_matches_the_position_derivative() {
|
||||||
// The velocity is the analytic derivative of the position cubic, so a
|
// The velocity is the analytic derivative of the position cubic, so a
|
||||||
// numerical derivative of the position must agree with it.
|
// numerical derivative of the position must agree with it.
|
||||||
|
// 速度是位置三次式的解析导数,故位置的数值导数必须与它一致。
|
||||||
let ephemeris = sample_table(60.0, 600.0);
|
let ephemeris = sample_table(60.0, 600.0);
|
||||||
let time = 137.0;
|
let time = 137.0;
|
||||||
let h = 1e-4;
|
let h = 1e-4;
|
||||||
|
|
@ -195,6 +213,7 @@ fn a_query_outside_the_interval_is_refused() {
|
||||||
assert!(ephemeris.state_at(f64::NAN).is_none());
|
assert!(ephemeris.state_at(f64::NAN).is_none());
|
||||||
assert!(ephemeris.state_at(f64::INFINITY).is_none());
|
assert!(ephemeris.state_at(f64::INFINITY).is_none());
|
||||||
// The endpoints are covered.
|
// The endpoints are covered.
|
||||||
|
// 端点被覆盖。
|
||||||
assert!(ephemeris.covers(0.0));
|
assert!(ephemeris.covers(0.0));
|
||||||
assert!(ephemeris.covers(600.0));
|
assert!(ephemeris.covers(600.0));
|
||||||
assert!(!ephemeris.covers(-1e-9));
|
assert!(!ephemeris.covers(-1e-9));
|
||||||
|
|
@ -208,6 +227,7 @@ fn a_clamped_query_returns_the_nearest_endpoint() {
|
||||||
let after = ephemeris.clamped_state_at(10_000.0).expect("clamped");
|
let after = ephemeris.clamped_state_at(10_000.0).expect("clamped");
|
||||||
assert_eq!(after, ephemeris.samples()[ephemeris.len() - 1]);
|
assert_eq!(after, ephemeris.samples()[ephemeris.len() - 1]);
|
||||||
// Inside the interval the clamped query is the ordinary one.
|
// Inside the interval the clamped query is the ordinary one.
|
||||||
|
// 在区间内,夹逼查询即普通查询。
|
||||||
let inside = ephemeris.clamped_state_at(30.0).expect("covered");
|
let inside = ephemeris.clamped_state_at(30.0).expect("covered");
|
||||||
assert_eq!(inside, ephemeris.state_at(30.0).expect("covered"));
|
assert_eq!(inside, ephemeris.state_at(30.0).expect("covered"));
|
||||||
assert!(ephemeris.clamped_state_at(f64::NAN).is_none());
|
assert!(ephemeris.clamped_state_at(f64::NAN).is_none());
|
||||||
|
|
@ -220,8 +240,10 @@ fn the_nearest_sample_is_found() {
|
||||||
assert_eq!(ephemeris.nearest_sample(29.0).expect("found").seconds, 0.0);
|
assert_eq!(ephemeris.nearest_sample(29.0).expect("found").seconds, 0.0);
|
||||||
assert_eq!(ephemeris.nearest_sample(31.0).expect("found").seconds, 60.0);
|
assert_eq!(ephemeris.nearest_sample(31.0).expect("found").seconds, 60.0);
|
||||||
// A midpoint ties toward the earlier sample.
|
// A midpoint ties toward the earlier sample.
|
||||||
|
// 中点并列时取较早的样本。
|
||||||
assert_eq!(ephemeris.nearest_sample(30.0).expect("found").seconds, 0.0);
|
assert_eq!(ephemeris.nearest_sample(30.0).expect("found").seconds, 0.0);
|
||||||
// Outside the interval the nearest end is returned.
|
// Outside the interval the nearest end is returned.
|
||||||
|
// 在区间外返回最近的一端。
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ephemeris.nearest_sample(-100.0).expect("found").seconds,
|
ephemeris.nearest_sample(-100.0).expect("found").seconds,
|
||||||
0.0
|
0.0
|
||||||
|
|
@ -266,10 +288,12 @@ fn a_property_of_every_sample_can_be_checked() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The sampler's grid.
|
// The sampler's grid.
|
||||||
|
// 采样器的网格。
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_grid_includes_both_ends() {
|
fn the_grid_includes_both_ends() {
|
||||||
// The end falls on the grid.
|
// The end falls on the grid.
|
||||||
|
// 终点落在网格上。
|
||||||
let sampler = Sampler::new(0.0, 300.0, 60.0);
|
let sampler = Sampler::new(0.0, 300.0, 60.0);
|
||||||
assert_eq!(sampler.sample_count(), Some(6));
|
assert_eq!(sampler.sample_count(), Some(6));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -278,6 +302,7 @@ fn the_grid_includes_both_ends() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// The end does not fall on the grid, so it is appended.
|
// The end does not fall on the grid, so it is appended.
|
||||||
|
// 终点不落在网格上,故追加它。
|
||||||
let sampler = Sampler::new(0.0, 250.0, 60.0);
|
let sampler = Sampler::new(0.0, 250.0, 60.0);
|
||||||
assert_eq!(sampler.sample_count(), Some(6));
|
assert_eq!(sampler.sample_count(), Some(6));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -298,6 +323,8 @@ fn a_zero_length_interval_is_refused() {
|
||||||
// A table needs at least two distinct times, so a zero length grid has
|
// A table needs at least two distinct times, so a zero length grid has
|
||||||
// no interval to interpolate over and is rejected outright rather than
|
// no interval to interpolate over and is rejected outright rather than
|
||||||
// producing a table that cannot answer a query.
|
// producing a table that cannot answer a query.
|
||||||
|
// 表至少需要两个不同的时刻,故零长度网格没有可插值的区间,会被直接拒绝而非产生
|
||||||
|
// 一张无法回答查询的表。
|
||||||
let sampler = Sampler::new(5.0, 5.0, 60.0);
|
let sampler = Sampler::new(5.0, 5.0, 60.0);
|
||||||
assert!(!sampler.is_valid());
|
assert!(!sampler.is_valid());
|
||||||
assert_eq!(sampler.sample_count(), None);
|
assert_eq!(sampler.sample_count(), None);
|
||||||
|
|
@ -341,6 +368,7 @@ fn a_bad_state_from_the_function_is_reported() {
|
||||||
fn the_sampler_overrides_a_bad_time_from_the_function() {
|
fn the_sampler_overrides_a_bad_time_from_the_function() {
|
||||||
// A function that returns a wrong time must not corrupt the table: the
|
// A function that returns a wrong time must not corrupt the table: the
|
||||||
// grid time is used instead.
|
// grid time is used instead.
|
||||||
|
// 返回错误时刻的函数不得污染该表:改用网格时刻。
|
||||||
let sampler = Sampler::new(0.0, 120.0, 60.0);
|
let sampler = Sampler::new(0.0, 120.0, 60.0);
|
||||||
let ephemeris = sampler
|
let ephemeris = sampler
|
||||||
.sample(|_| Sample::new(999.0, Vec3::new(7_000.0, 0.0, 0.0), Vec3::ZERO))
|
.sample(|_| Sample::new(999.0, Vec3::new(7_000.0, 0.0, 0.0), Vec3::ZERO))
|
||||||
|
|
@ -352,12 +380,14 @@ fn the_sampler_overrides_a_bad_time_from_the_function() {
|
||||||
fn positions_can_be_sampled_without_velocities() {
|
fn positions_can_be_sampled_without_velocities() {
|
||||||
// The velocity is reconstructed by finite differences, so a
|
// The velocity is reconstructed by finite differences, so a
|
||||||
// position-only source still produces a usable table.
|
// position-only source still produces a usable table.
|
||||||
|
// 速度由有限差分重构,故仅提供位置的来源仍能产生可用的表。
|
||||||
let ephemeris = Sampler::new(0.0, 600.0, 60.0)
|
let ephemeris = Sampler::new(0.0, 600.0, 60.0)
|
||||||
.sample_positions(|seconds| exact_state(seconds).position)
|
.sample_positions(|seconds| exact_state(seconds).position)
|
||||||
.expect("the positions are finite");
|
.expect("the positions are finite");
|
||||||
assert_eq!(ephemeris.len(), 11);
|
assert_eq!(ephemeris.len(), 11);
|
||||||
|
|
||||||
// The reconstructed velocity is close to the true one.
|
// The reconstructed velocity is close to the true one.
|
||||||
|
// 所重构的速度接近真值。
|
||||||
let sample = ephemeris.state_at(300.0).expect("covered");
|
let sample = ephemeris.state_at(300.0).expect("covered");
|
||||||
let truth = exact_state(300.0);
|
let truth = exact_state(300.0);
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -372,6 +402,8 @@ fn positions_can_be_sampled_without_velocities() {
|
||||||
// accurate, so its error feeds straight into the Hermite reconstruction.
|
// accurate, so its error feeds straight into the Hermite reconstruction.
|
||||||
// The error is a few kilometers over a one minute step, against
|
// The error is a few kilometers over a one minute step, against
|
||||||
// millimeters when the velocity is known.
|
// millimeters when the velocity is known.
|
||||||
|
// 插值仍可用,尽管远不如有精确速度时准确:有限差分速度只有二阶精度,故其误差
|
||||||
|
// 直接输入 Hermite 重构。误差在一分钟步长上为几千米,而有已知速度时为毫米级。
|
||||||
let mut worst = 0.0_f64;
|
let mut worst = 0.0_f64;
|
||||||
for index in 0..600 {
|
for index in 0..600 {
|
||||||
let time = f64::from(index) + 0.5;
|
let time = f64::from(index) + 0.5;
|
||||||
|
|
@ -397,6 +429,7 @@ fn position_sampling_reports_a_bad_position() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Hermite basis functions directly.
|
// The Hermite basis functions directly.
|
||||||
|
// 直接检验 Hermite 基函数。
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_hermite_basis_reproduces_its_endpoints() {
|
fn the_hermite_basis_reproduces_its_endpoints() {
|
||||||
|
|
@ -407,9 +440,11 @@ fn the_hermite_basis_reproduces_its_endpoints() {
|
||||||
let dt = 60.0;
|
let dt = 60.0;
|
||||||
|
|
||||||
// At s = 0 the result is p0 and the velocity is v0.
|
// At s = 0 the result is p0 and the velocity is v0.
|
||||||
|
// 在 s = 0 处结果为 p0,速度为 v0。
|
||||||
assert_eq!(hermite_position(p0, v0, p1, v1, dt, 0.0), p0);
|
assert_eq!(hermite_position(p0, v0, p1, v1, dt, 0.0), p0);
|
||||||
assert_eq!(hermite_velocity(p0, v0, p1, v1, dt, 0.0), v0);
|
assert_eq!(hermite_velocity(p0, v0, p1, v1, dt, 0.0), v0);
|
||||||
// At s = 1 the result is p1 and the velocity is v1.
|
// At s = 1 the result is p1 and the velocity is v1.
|
||||||
|
// 在 s = 1 处结果为 p1,速度为 v1。
|
||||||
assert_eq!(hermite_position(p0, v0, p1, v1, dt, 1.0), p1);
|
assert_eq!(hermite_position(p0, v0, p1, v1, dt, 1.0), p1);
|
||||||
assert_eq!(hermite_velocity(p0, v0, p1, v1, dt, 1.0), v1);
|
assert_eq!(hermite_velocity(p0, v0, p1, v1, dt, 1.0), v1);
|
||||||
}
|
}
|
||||||
|
|
@ -421,9 +456,11 @@ fn the_hermite_state_returns_both_parts() {
|
||||||
let (position, velocity) = hermite_state(p0, Vec3::ZERO, p1, Vec3::ZERO, 10.0, 0.5);
|
let (position, velocity) = hermite_state(p0, Vec3::ZERO, p1, Vec3::ZERO, 10.0, 0.5);
|
||||||
// Both endpoints are stationary, so the cubic is the S-curve that
|
// Both endpoints are stationary, so the cubic is the S-curve that
|
||||||
// passes through the midpoint with the slope the endpoints imply.
|
// passes through the midpoint with the slope the endpoints imply.
|
||||||
|
// 两个端点都静止,故该三次式是经由中点、斜率为端点所蕴含值的 S 形曲线。
|
||||||
assert_eq!(position, Vec3::new(1.5, 0.0, 0.0));
|
assert_eq!(position, Vec3::new(1.5, 0.0, 0.0));
|
||||||
// The slope at the midpoint is 1.5 / dt, not zero: the curve rises from
|
// The slope at the midpoint is 1.5 / dt, not zero: the curve rises from
|
||||||
// zero slope at one end to zero slope at the other.
|
// zero slope at one end to zero slope at the other.
|
||||||
|
// 中点处的斜率是 1.5 / dt 而非零:曲线从一端的零斜率升到另一端的零斜率。
|
||||||
assert_eq!(velocity, Vec3::new(0.15, 0.0, 0.0));
|
assert_eq!(velocity, Vec3::new(0.15, 0.0, 0.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
//! Tests for the anomaly conversions.
|
//! Tests for the anomaly conversions.
|
||||||
|
//!
|
||||||
|
//! 近点角换算的测试。
|
||||||
|
|
||||||
use apsis_kepler::{
|
use apsis_kepler::{
|
||||||
eccentric_from_mean, eccentric_from_true, mean_from_eccentric, mean_from_true,
|
eccentric_from_mean, eccentric_from_true, mean_from_eccentric, mean_from_true,
|
||||||
|
|
@ -17,6 +19,7 @@ fn wrap_places_angles_in_the_half_open_interval() {
|
||||||
"angle {angle} wrapped to {wrapped}"
|
"angle {angle} wrapped to {wrapped}"
|
||||||
);
|
);
|
||||||
// The result differs from the input by a whole number of turns.
|
// The result differs from the input by a whole number of turns.
|
||||||
|
// 结果与输入相差整数圈。
|
||||||
let turns_removed = (angle - wrapped) / (2.0 * PI);
|
let turns_removed = (angle - wrapped) / (2.0 * PI);
|
||||||
assert!((turns_removed - turns_removed.round()).abs() < 1e-9);
|
assert!((turns_removed - turns_removed.round()).abs() < 1e-9);
|
||||||
}
|
}
|
||||||
|
|
@ -33,6 +36,7 @@ fn wrap_keeps_values_already_in_range() {
|
||||||
#[test]
|
#[test]
|
||||||
fn keplers_equation_holds_for_the_solution() {
|
fn keplers_equation_holds_for_the_solution() {
|
||||||
// M = E - e sin E must be satisfied by the returned eccentric anomaly.
|
// M = E - e sin E must be satisfied by the returned eccentric anomaly.
|
||||||
|
// 所返回的偏近点角必须满足 M = E - e sin E。
|
||||||
for eccentricity in [0.0, 0.01, 0.1, 0.5, 0.9, 0.99] {
|
for eccentricity in [0.0, 0.01, 0.1, 0.5, 0.9, 0.99] {
|
||||||
for mean in [-3.0, -1.0, 0.0, 0.5, 1.5, 3.0] {
|
for mean in [-3.0, -1.0, 0.0, 0.5, 1.5, 3.0] {
|
||||||
let eccentric = eccentric_from_mean(mean, eccentricity);
|
let eccentric = eccentric_from_mean(mean, eccentricity);
|
||||||
|
|
@ -76,6 +80,7 @@ fn mean_and_true_anomaly_round_trip() {
|
||||||
#[test]
|
#[test]
|
||||||
fn a_circular_orbit_has_equal_anomalies() {
|
fn a_circular_orbit_has_equal_anomalies() {
|
||||||
// With zero eccentricity every anomaly is the same angle.
|
// With zero eccentricity every anomaly is the same angle.
|
||||||
|
// 偏心率为零时每种近点角都是同一个角。
|
||||||
for angle in [-2.0, -0.5, 0.0, 1.0, 2.5] {
|
for angle in [-2.0, -0.5, 0.0, 1.0, 2.5] {
|
||||||
assert!((eccentric_from_mean(angle, 0.0) - angle).abs() < 1e-12);
|
assert!((eccentric_from_mean(angle, 0.0) - angle).abs() < 1e-12);
|
||||||
assert!((true_from_eccentric(angle, 0.0) - angle).abs() < 1e-12);
|
assert!((true_from_eccentric(angle, 0.0) - angle).abs() < 1e-12);
|
||||||
|
|
@ -97,6 +102,8 @@ fn true_anomaly_exceeds_mean_anomaly_before_apogee() {
|
||||||
// For an eccentric orbit the satellite spends less time near perigee,
|
// For an eccentric orbit the satellite spends less time near perigee,
|
||||||
// so the true anomaly runs ahead of the mean anomaly in the first half
|
// so the true anomaly runs ahead of the mean anomaly in the first half
|
||||||
// of the orbit.
|
// of the orbit.
|
||||||
|
// 对偏心轨道,卫星在近地点附近花的时间较少,故在轨道的上半段真近点角领先于平
|
||||||
|
// 近点角。
|
||||||
let eccentricity = 0.5;
|
let eccentricity = 0.5;
|
||||||
let mean = 1.0;
|
let mean = 1.0;
|
||||||
let true_anomaly = true_from_mean(mean, eccentricity);
|
let true_anomaly = true_from_mean(mean, eccentricity);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@
|
||||||
//! perifocal frame expressions in extended precision. Only the point mass
|
//! perifocal frame expressions in extended precision. Only the point mass
|
||||||
//! gravity of the central body is involved, so the two should agree to
|
//! gravity of the central body is involved, so the two should agree to
|
||||||
//! floating point rounding.
|
//! floating point rounding.
|
||||||
|
//!
|
||||||
|
//! 开普勒根数及其状态矢量换算的测试。
|
||||||
|
//!
|
||||||
|
//! 期望值是以扩展精度从标准近焦点系表达式独立计算的。只涉及中心天体的质点引力,
|
||||||
|
//! 故二者应在浮点舍入内一致。
|
||||||
|
|
||||||
use apsis_kepler::{GravityParameter, KeplerianElements, State, wrap_to_pi};
|
use apsis_kepler::{GravityParameter, KeplerianElements, State, wrap_to_pi};
|
||||||
use apsis_math::Vec3;
|
use apsis_math::Vec3;
|
||||||
|
|
@ -78,6 +83,8 @@ fn a_circular_equatorial_orbit_has_the_circular_speed() {
|
||||||
// At zero eccentricity, zero inclination, and zero orientation, the
|
// At zero eccentricity, zero inclination, and zero orientation, the
|
||||||
// satellite sits on the first axis and moves along the second, with
|
// satellite sits on the first axis and moves along the second, with
|
||||||
// speed sqrt(mu / a).
|
// speed sqrt(mu / a).
|
||||||
|
// 在零偏心率、零倾角、零取向时,卫星位于第一轴上、沿第二轴运动,速率为
|
||||||
|
// sqrt(mu / a)。
|
||||||
let elements = KeplerianElements {
|
let elements = KeplerianElements {
|
||||||
semi_major_axis: 6_378.137,
|
semi_major_axis: 6_378.137,
|
||||||
eccentricity: 0.0,
|
eccentricity: 0.0,
|
||||||
|
|
@ -106,6 +113,7 @@ fn the_state_vector_lies_in_the_orbit_plane() {
|
||||||
.expect("elements are valid");
|
.expect("elements are valid");
|
||||||
// The position and velocity span the orbit plane, so the normal to that
|
// The position and velocity span the orbit plane, so the normal to that
|
||||||
// plane is perpendicular to both.
|
// plane is perpendicular to both.
|
||||||
|
// 位置与速度张成轨道面,故该面的法线垂直于二者。
|
||||||
let normal = state
|
let normal = state
|
||||||
.position
|
.position
|
||||||
.cross(state.velocity)
|
.cross(state.velocity)
|
||||||
|
|
@ -182,6 +190,7 @@ fn the_period_follows_keplers_third_law() {
|
||||||
2.0 * PI * (elements.semi_major_axis.powi(3) / GravityParameter::WGS84.value()).sqrt();
|
2.0 * PI * (elements.semi_major_axis.powi(3) / GravityParameter::WGS84.value()).sqrt();
|
||||||
assert!((period - expected).abs() < 1e-9);
|
assert!((period - expected).abs() < 1e-9);
|
||||||
// A 7000 km orbit has a period of about 97 minutes.
|
// A 7000 km orbit has a period of about 97 minutes.
|
||||||
|
// 7000 km 轨道的周期约为 97 分钟。
|
||||||
assert!((period / 60.0 - 97.0).abs() < 2.0);
|
assert!((period / 60.0 - 97.0).abs() < 2.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,6 +219,8 @@ fn state_round_trips_through_the_elements() {
|
||||||
// The angles are compared modulo a full turn, because a value such
|
// The angles are compared modulo a full turn, because a value such
|
||||||
// as 5.0 radians and one reduced to -1.28 radians describe the same
|
// as 5.0 radians and one reduced to -1.28 radians describe the same
|
||||||
// orientation.
|
// orientation.
|
||||||
|
// 各角按模一周比较,因为诸如 5.0 弧度这样的值与归约到 -1.28 弧度的值描述同一
|
||||||
|
// 取向。
|
||||||
assert!((wrap_to_pi(recovered.right_ascension - elements.right_ascension)).abs() < 1e-9);
|
assert!((wrap_to_pi(recovered.right_ascension - elements.right_ascension)).abs() < 1e-9);
|
||||||
assert!(
|
assert!(
|
||||||
(wrap_to_pi(recovered.argument_of_perigee - elements.argument_of_perigee)).abs() < 1e-9
|
(wrap_to_pi(recovered.argument_of_perigee - elements.argument_of_perigee)).abs() < 1e-9
|
||||||
|
|
@ -218,6 +229,7 @@ fn state_round_trips_through_the_elements() {
|
||||||
|
|
||||||
// The round trip must reproduce the state as well as the elements,
|
// The round trip must reproduce the state as well as the elements,
|
||||||
// which is the stronger check.
|
// which is the stronger check.
|
||||||
|
// 往返必须既复现状态也复现根数,后者是更强的检查。
|
||||||
let again = recovered.to_state(GravityParameter::WGS84).expect("valid");
|
let again = recovered.to_state(GravityParameter::WGS84).expect("valid");
|
||||||
assert!((again.position - state.position).norm() < 1e-6);
|
assert!((again.position - state.position).norm() < 1e-6);
|
||||||
assert!((again.velocity - state.velocity).norm() < 1e-9);
|
assert!((again.velocity - state.velocity).norm() < 1e-9);
|
||||||
|
|
@ -287,6 +299,7 @@ fn degenerate_states_are_rejected() {
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
// A purely radial state has no defined orbit plane.
|
// A purely radial state has no defined orbit plane.
|
||||||
|
// 纯径向状态没有定义的轨道面。
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
KeplerianElements::from_state(
|
KeplerianElements::from_state(
|
||||||
&State {
|
&State {
|
||||||
|
|
@ -298,6 +311,7 @@ fn degenerate_states_are_rejected() {
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
// An unbound state has a non-positive semi-major axis.
|
// An unbound state has a non-positive semi-major axis.
|
||||||
|
// 非束缚状态的半长轴非正。
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
KeplerianElements::from_state(
|
KeplerianElements::from_state(
|
||||||
&State {
|
&State {
|
||||||
|
|
@ -321,6 +335,7 @@ fn gravity_parameter_values_match_the_standards() {
|
||||||
fn a_full_revolution_returns_to_the_same_point() {
|
fn a_full_revolution_returns_to_the_same_point() {
|
||||||
// Advancing the mean anomaly by one full turn must return the same
|
// Advancing the mean anomaly by one full turn must return the same
|
||||||
// position, which verifies the anomaly conversions agree.
|
// position, which verifies the anomaly conversions agree.
|
||||||
|
// 把平近点角推进一整圈必须返回相同位置,这验证各近点角换算彼此一致。
|
||||||
let elements = elliptical_orbit();
|
let elements = elliptical_orbit();
|
||||||
let start = elements
|
let start = elements
|
||||||
.with_mean_anomaly(1.0)
|
.with_mean_anomaly(1.0)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
//! Tests for the maneuver models.
|
//! Tests for the maneuver models.
|
||||||
|
//!
|
||||||
|
//! 机动模型的测试。
|
||||||
|
|
||||||
use apsis_kepler::{GravityParameter, KeplerianElements};
|
use apsis_kepler::{GravityParameter, KeplerianElements};
|
||||||
use apsis_maneuver::{FiniteBurn, Impulse, ManeuverErrorKind, hohmann_transfer};
|
use apsis_maneuver::{FiniteBurn, Impulse, ManeuverErrorKind, hohmann_transfer};
|
||||||
use apsis_math::Vec3;
|
use apsis_math::Vec3;
|
||||||
|
|
||||||
/// A circular orbit at a given radius, in the equatorial plane.
|
/// A circular orbit at a given radius, in the equatorial plane.
|
||||||
|
///
|
||||||
|
/// 给定半径、位于赤道面内的圆轨道。
|
||||||
fn circular(radius: f64) -> KeplerianElements {
|
fn circular(radius: f64) -> KeplerianElements {
|
||||||
KeplerianElements {
|
KeplerianElements {
|
||||||
semi_major_axis: radius,
|
semi_major_axis: radius,
|
||||||
|
|
@ -17,6 +21,7 @@ fn circular(radius: f64) -> KeplerianElements {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The impulsive model.
|
// The impulsive model.
|
||||||
|
// 脉冲模型。
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn an_impulse_reports_its_change() {
|
fn an_impulse_reports_its_change() {
|
||||||
|
|
@ -36,12 +41,14 @@ fn a_zero_or_non_finite_impulse_is_refused() {
|
||||||
#[test]
|
#[test]
|
||||||
fn an_impulse_can_be_built_from_a_direction_and_a_magnitude() {
|
fn an_impulse_can_be_built_from_a_direction_and_a_magnitude() {
|
||||||
// The direction need not be a unit vector.
|
// The direction need not be a unit vector.
|
||||||
|
// 方向不必是单位矢量。
|
||||||
let impulse = Impulse::along(Vec3::new(0.0, 3.0, 0.0), 0.5).expect("usable");
|
let impulse = Impulse::along(Vec3::new(0.0, 3.0, 0.0), 0.5).expect("usable");
|
||||||
assert!((impulse.magnitude() - 0.5).abs() < 1e-12);
|
assert!((impulse.magnitude() - 0.5).abs() < 1e-12);
|
||||||
assert!((impulse.delta_v().y - 0.5).abs() < 1e-12);
|
assert!((impulse.delta_v().y - 0.5).abs() < 1e-12);
|
||||||
assert!(impulse.delta_v().z.abs() < 1e-12);
|
assert!(impulse.delta_v().z.abs() < 1e-12);
|
||||||
|
|
||||||
// A zero direction and a bad magnitude are refused.
|
// A zero direction and a bad magnitude are refused.
|
||||||
|
// 零方向与坏幅度被拒绝。
|
||||||
assert!(Impulse::along(Vec3::ZERO, 1.0).is_err());
|
assert!(Impulse::along(Vec3::ZERO, 1.0).is_err());
|
||||||
assert!(Impulse::along(Vec3::X, 0.0).is_err());
|
assert!(Impulse::along(Vec3::X, 0.0).is_err());
|
||||||
assert!(Impulse::along(Vec3::X, -1.0).is_err());
|
assert!(Impulse::along(Vec3::X, -1.0).is_err());
|
||||||
|
|
@ -56,6 +63,7 @@ fn an_impulse_can_be_scaled_and_reversed() {
|
||||||
let reversed = impulse.reversed();
|
let reversed = impulse.reversed();
|
||||||
assert!((reversed.delta_v().x + 1.0).abs() < 1e-12);
|
assert!((reversed.delta_v().x + 1.0).abs() < 1e-12);
|
||||||
// Scaling by zero produces a zero change, which is refused.
|
// Scaling by zero produces a zero change, which is refused.
|
||||||
|
// 按零缩放产生零变化,会被拒绝。
|
||||||
assert!(impulse.scaled(0.0).is_err());
|
assert!(impulse.scaled(0.0).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,14 +83,17 @@ fn an_impulse_displays_its_magnitude() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Hohmann transfer.
|
// The Hohmann transfer.
|
||||||
|
// Hohmann 转移。
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_hohmann_transfer_raises_an_orbit() {
|
fn a_hohmann_transfer_raises_an_orbit() {
|
||||||
// Raise from 7000 to 8000 km.
|
// Raise from 7000 to 8000 km.
|
||||||
|
// 从 7000 升到 8000 km。
|
||||||
let transfer =
|
let transfer =
|
||||||
hohmann_transfer(7_000.0, 8_000.0, GravityParameter::WGS84).expect("the radii are usable");
|
hohmann_transfer(7_000.0, 8_000.0, GravityParameter::WGS84).expect("the radii are usable");
|
||||||
assert!(transfer.is_raising());
|
assert!(transfer.is_raising());
|
||||||
// Raising an orbit takes two prograde burns.
|
// Raising an orbit takes two prograde burns.
|
||||||
|
// 抬高轨道需要两次顺行点火。
|
||||||
assert!(transfer.first_burn > 0.0);
|
assert!(transfer.first_burn > 0.0);
|
||||||
assert!(transfer.second_burn > 0.0);
|
assert!(transfer.second_burn > 0.0);
|
||||||
assert_eq!(transfer.transfer_semi_major_axis, 7_500.0);
|
assert_eq!(transfer.transfer_semi_major_axis, 7_500.0);
|
||||||
|
|
@ -98,12 +109,14 @@ fn a_hohmann_transfer_raises_an_orbit() {
|
||||||
fn a_hohmann_transfer_lowers_an_orbit() {
|
fn a_hohmann_transfer_lowers_an_orbit() {
|
||||||
// The same two radii in the other order is a descent, and the burns are
|
// The same two radii in the other order is a descent, and the burns are
|
||||||
// retrograde.
|
// retrograde.
|
||||||
|
// 两个半径反序即为下降,两次点火都是逆行。
|
||||||
let transfer =
|
let transfer =
|
||||||
hohmann_transfer(8_000.0, 7_000.0, GravityParameter::WGS84).expect("the radii are usable");
|
hohmann_transfer(8_000.0, 7_000.0, GravityParameter::WGS84).expect("the radii are usable");
|
||||||
assert!(!transfer.is_raising());
|
assert!(!transfer.is_raising());
|
||||||
assert!(transfer.first_burn < 0.0);
|
assert!(transfer.first_burn < 0.0);
|
||||||
assert!(transfer.second_burn < 0.0);
|
assert!(transfer.second_burn < 0.0);
|
||||||
// The cost is the same either way.
|
// The cost is the same either way.
|
||||||
|
// 两种方向的代价相同。
|
||||||
let raised =
|
let raised =
|
||||||
hohmann_transfer(7_000.0, 8_000.0, GravityParameter::WGS84).expect("the radii are usable");
|
hohmann_transfer(7_000.0, 8_000.0, GravityParameter::WGS84).expect("the radii are usable");
|
||||||
assert!((transfer.total_delta_v() - raised.total_delta_v()).abs() < 1e-12);
|
assert!((transfer.total_delta_v() - raised.total_delta_v()).abs() < 1e-12);
|
||||||
|
|
@ -115,6 +128,8 @@ fn the_first_burn_matches_the_vis_viva_speeds() {
|
||||||
// The first burn is the transfer ellipse speed at the lower radius less
|
// The first burn is the transfer ellipse speed at the lower radius less
|
||||||
// the circular speed there. Checking it against the vis-viva equation
|
// the circular speed there. Checking it against the vis-viva equation
|
||||||
// directly pins the formula.
|
// directly pins the formula.
|
||||||
|
// 第一次点火是转移椭圆在下半径处的速率减去那里的圆轨道速率。用活力公式直接核对
|
||||||
|
// 它即可钉住该公式。
|
||||||
let transfer =
|
let transfer =
|
||||||
hohmann_transfer(7_000.0, 9_000.0, GravityParameter::WGS84).expect("the radii are usable");
|
hohmann_transfer(7_000.0, 9_000.0, GravityParameter::WGS84).expect("the radii are usable");
|
||||||
let mu = GravityParameter::WGS84.value();
|
let mu = GravityParameter::WGS84.value();
|
||||||
|
|
@ -137,6 +152,8 @@ fn a_hohmann_transfer_matches_a_known_leo_to_geo_case() {
|
||||||
// The well documented case: a transfer from a 300 km circular parking
|
// The well documented case: a transfer from a 300 km circular parking
|
||||||
// orbit to geostationary altitude. The total is about 3.89 km/s, which
|
// orbit to geostationary altitude. The total is about 3.89 km/s, which
|
||||||
// is the figure quoted for a standard LEO to GEO chemical transfer.
|
// is the figure quoted for a standard LEO to GEO chemical transfer.
|
||||||
|
// 有充分记载的用例:从 300 km 圆停泊轨道转移到地球静止高度。总计约 3.89 km/s,
|
||||||
|
// 这是标准 LEO 到 GEO 化学转移所引用的数字。
|
||||||
let from = 6_378.137 + 300.0;
|
let from = 6_378.137 + 300.0;
|
||||||
let to = 42_164.0;
|
let to = 42_164.0;
|
||||||
let transfer = hohmann_transfer(from, to, GravityParameter::WGS84).expect("usable");
|
let transfer = hohmann_transfer(from, to, GravityParameter::WGS84).expect("usable");
|
||||||
|
|
@ -148,6 +165,8 @@ fn a_hohmann_transfer_matches_a_known_leo_to_geo_case() {
|
||||||
// The transfer ellipse touches both orbits: its perigee is the parking
|
// The transfer ellipse touches both orbits: its perigee is the parking
|
||||||
// radius and its apogee the destination, so its eccentricity follows
|
// radius and its apogee the destination, so its eccentricity follows
|
||||||
// from the two radii.
|
// from the two radii.
|
||||||
|
// 转移椭圆与两个轨道都相切:其近地点是停泊半径,远地点是目的地,故其偏心率由
|
||||||
|
// 这两个半径得出。
|
||||||
let eccentricity = (to - from) / (to + from);
|
let eccentricity = (to - from) / (to + from);
|
||||||
let perigee = transfer.transfer_semi_major_axis * (1.0 - eccentricity);
|
let perigee = transfer.transfer_semi_major_axis * (1.0 - eccentricity);
|
||||||
let apogee = transfer.transfer_semi_major_axis * (1.0 + eccentricity);
|
let apogee = transfer.transfer_semi_major_axis * (1.0 + eccentricity);
|
||||||
|
|
@ -178,6 +197,7 @@ fn an_unusable_radius_is_refused() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The finite burn.
|
// The finite burn.
|
||||||
|
// 有限点火。
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_finite_burn_reports_its_acceleration_and_duration() {
|
fn a_finite_burn_reports_its_acceleration_and_duration() {
|
||||||
|
|
@ -185,6 +205,7 @@ fn a_finite_burn_reports_its_acceleration_and_duration() {
|
||||||
assert!((burn.acceleration_magnitude() - 0.001).abs() < 1e-15);
|
assert!((burn.acceleration_magnitude() - 0.001).abs() < 1e-15);
|
||||||
assert_eq!(burn.duration(), 100.0);
|
assert_eq!(burn.duration(), 100.0);
|
||||||
// The nominal change is the acceleration times the duration.
|
// The nominal change is the acceleration times the duration.
|
||||||
|
// 标称变化是加速度乘以时长。
|
||||||
assert!((burn.nominal_delta_v() - 0.1).abs() < 1e-12);
|
assert!((burn.nominal_delta_v() - 0.1).abs() < 1e-12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,16 +232,21 @@ fn a_finite_burn_agrees_with_its_equivalent_impulse_in_free_space() {
|
||||||
// With gravity switched off the finite burn must deliver exactly the
|
// With gravity switched off the finite burn must deliver exactly the
|
||||||
// nominal change: it is a constant acceleration, so the velocity change
|
// nominal change: it is a constant acceleration, so the velocity change
|
||||||
// is the acceleration times the duration.
|
// is the acceleration times the duration.
|
||||||
|
// 在引力关掉时,有限点火必须恰好给出标称变化:它是常值加速度,故速度变化是
|
||||||
|
// 加速度乘以时长。
|
||||||
//
|
//
|
||||||
// The two-body model always has gravity, so instead the burn is applied
|
// The two-body model always has gravity, so instead the burn is applied
|
||||||
// far from a mass, where gravity in the equation is negligible because
|
// far from a mass, where gravity in the equation is negligible because
|
||||||
// the central body was given a vanishing parameter.
|
// the central body was given a vanishing parameter.
|
||||||
|
// 二体模型总有引力,故改为在远离质量处施加点火,那里方程中的引力可忽略,因为
|
||||||
|
// 中心天体被赋予了一个趋于零的参数。
|
||||||
let burn = FiniteBurn::new(Vec3::new(0.0, 0.001, 0.0), 100.0).expect("usable");
|
let burn = FiniteBurn::new(Vec3::new(0.0, 0.001, 0.0), 100.0).expect("usable");
|
||||||
let state = apsis_kepler::State {
|
let state = apsis_kepler::State {
|
||||||
position: Vec3::new(1.0e9, 0.0, 0.0),
|
position: Vec3::new(1.0e9, 0.0, 0.0),
|
||||||
velocity: Vec3::ZERO,
|
velocity: Vec3::ZERO,
|
||||||
};
|
};
|
||||||
// A negligible gravitational parameter leaves only the thrust.
|
// A negligible gravitational parameter leaves only the thrust.
|
||||||
|
// 可忽略的引力参数只留下推力。
|
||||||
let outcome = burn
|
let outcome = burn
|
||||||
.apply(state, GravityParameter(0.0), 100)
|
.apply(state, GravityParameter(0.0), 100)
|
||||||
.expect("the burn runs");
|
.expect("the burn runs");
|
||||||
|
|
@ -231,6 +257,7 @@ fn a_finite_burn_agrees_with_its_equivalent_impulse_in_free_space() {
|
||||||
);
|
);
|
||||||
assert!((outcome.nominal_delta_v - 0.1).abs() < 1e-12);
|
assert!((outcome.nominal_delta_v - 0.1).abs() < 1e-12);
|
||||||
// There is no gravity to lose anything to.
|
// There is no gravity to lose anything to.
|
||||||
|
// 没有引力可损失任何东西。
|
||||||
assert!(outcome.gravity_loss().abs() < 1e-9);
|
assert!(outcome.gravity_loss().abs() < 1e-9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,18 +267,22 @@ fn a_finite_burn_converges_on_the_matching_impulse() {
|
||||||
// of acceleration `a` over a duration `t` delivers the impulse `a t` in
|
// of acceleration `a` over a duration `t` delivers the impulse `a t` in
|
||||||
// the limit of a shrinking duration, because the gravity deflection
|
// the limit of a shrinking duration, because the gravity deflection
|
||||||
// during the burn vanishes with the duration.
|
// during the burn vanishes with the duration.
|
||||||
|
// 这正是把两个模型联系起来的性质。加速度为 `a`、时长为 `t` 的有限点火在时长
|
||||||
|
// 趋于零的极限下给出冲量 `a t`,因为点火期间的引力偏折随时长消失。
|
||||||
let parameter = GravityParameter::WGS84;
|
let parameter = GravityParameter::WGS84;
|
||||||
let elements = circular(7_000.0);
|
let elements = circular(7_000.0);
|
||||||
let state = elements.to_state(parameter).expect("valid");
|
let state = elements.to_state(parameter).expect("valid");
|
||||||
|
|
||||||
// The thrust direction is along the velocity, which for this orbit is
|
// The thrust direction is along the velocity, which for this orbit is
|
||||||
// the second axis.
|
// the second axis.
|
||||||
|
// 推力方向沿速度,对该轨道而言即第二轴。
|
||||||
let direction = state.velocity.normalize().expect("the speed is non-zero");
|
let direction = state.velocity.normalize().expect("the speed is non-zero");
|
||||||
let target_delta_v = 0.01;
|
let target_delta_v = 0.01;
|
||||||
|
|
||||||
let achieved_for = |duration: f64| {
|
let achieved_for = |duration: f64| {
|
||||||
// Hold the nominal delta-v fixed by scaling the acceleration, so
|
// Hold the nominal delta-v fixed by scaling the acceleration, so
|
||||||
// every burn is compared at the same impulse.
|
// every burn is compared at the same impulse.
|
||||||
|
// 通过缩放加速度保持标称 delta-v 不变,使每次点火都在相同冲量下比较。
|
||||||
let acceleration = target_delta_v / duration;
|
let acceleration = target_delta_v / duration;
|
||||||
let burn = FiniteBurn::along(direction, acceleration, duration).expect("usable");
|
let burn = FiniteBurn::along(direction, acceleration, duration).expect("usable");
|
||||||
burn.apply(state, parameter, 2_000)
|
burn.apply(state, parameter, 2_000)
|
||||||
|
|
@ -261,6 +292,7 @@ fn a_finite_burn_converges_on_the_matching_impulse() {
|
||||||
|
|
||||||
// A long burn loses some of the change to gravity; a short one keeps
|
// A long burn loses some of the change to gravity; a short one keeps
|
||||||
// nearly all of it.
|
// nearly all of it.
|
||||||
|
// 长点火把部分变化损失给引力;短点火几乎全部保留。
|
||||||
let long = achieved_for(600.0);
|
let long = achieved_for(600.0);
|
||||||
let short = achieved_for(6.0);
|
let short = achieved_for(6.0);
|
||||||
let long_error = (long - target_delta_v).abs();
|
let long_error = (long - target_delta_v).abs();
|
||||||
|
|
@ -282,11 +314,15 @@ fn a_finite_burn_delivers_less_than_nominal_against_gravity() {
|
||||||
// space value. The sign of the difference depends on the geometry; the
|
// space value. The sign of the difference depends on the geometry; the
|
||||||
// point of the test is that the two are not equal, which is why the
|
// point of the test is that the two are not equal, which is why the
|
||||||
// finite model exists.
|
// finite model exists.
|
||||||
|
// 圆轨道上的长顺行点火:点火进行时引力使速度转向,故所达到的变化不同于自由空间
|
||||||
|
// 值。差值的符号取决于几何;本测试的要点是二者不相等,这正是有限模型存在的
|
||||||
|
// 原因。
|
||||||
let parameter = GravityParameter::WGS84;
|
let parameter = GravityParameter::WGS84;
|
||||||
let elements = circular(7_000.0);
|
let elements = circular(7_000.0);
|
||||||
let state = elements.to_state(parameter).expect("valid");
|
let state = elements.to_state(parameter).expect("valid");
|
||||||
let direction = state.velocity.normalize().expect("the speed is non-zero");
|
let direction = state.velocity.normalize().expect("the speed is non-zero");
|
||||||
// A burn lasting a tenth of the orbital period.
|
// A burn lasting a tenth of the orbital period.
|
||||||
|
// 持续轨道周期十分之一的点火。
|
||||||
let duration = elements.period(parameter) / 10.0;
|
let duration = elements.period(parameter) / 10.0;
|
||||||
let acceleration = 1e-5;
|
let acceleration = 1e-5;
|
||||||
let burn = FiniteBurn::along(direction, acceleration, duration).expect("usable");
|
let burn = FiniteBurn::along(direction, acceleration, duration).expect("usable");
|
||||||
|
|
@ -298,12 +334,14 @@ fn a_finite_burn_delivers_less_than_nominal_against_gravity() {
|
||||||
outcome.gravity_loss()
|
outcome.gravity_loss()
|
||||||
);
|
);
|
||||||
// The position moved during the burn.
|
// The position moved during the burn.
|
||||||
|
// 点火期间位置移动了。
|
||||||
assert!(outcome.state.position.distance(state.position) > 1.0);
|
assert!(outcome.state.position.distance(state.position) > 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_finite_burn_raises_the_orbit_when_prograde() {
|
fn a_finite_burn_raises_the_orbit_when_prograde() {
|
||||||
// A prograde burn adds energy, so the semi-major axis grows.
|
// A prograde burn adds energy, so the semi-major axis grows.
|
||||||
|
// 顺行点火增加能量,故半长轴增大。
|
||||||
let parameter = GravityParameter::WGS84;
|
let parameter = GravityParameter::WGS84;
|
||||||
let elements = circular(7_000.0);
|
let elements = circular(7_000.0);
|
||||||
let state = elements.to_state(parameter).expect("valid");
|
let state = elements.to_state(parameter).expect("valid");
|
||||||
|
|
@ -322,6 +360,7 @@ fn a_finite_burn_raises_the_orbit_when_prograde() {
|
||||||
#[test]
|
#[test]
|
||||||
fn a_finite_burn_lowers_the_orbit_when_retrograde() {
|
fn a_finite_burn_lowers_the_orbit_when_retrograde() {
|
||||||
// The mirror image: a retrograde burn removes energy.
|
// The mirror image: a retrograde burn removes energy.
|
||||||
|
// 镜像情形:逆行点火移除能量。
|
||||||
let parameter = GravityParameter::WGS84;
|
let parameter = GravityParameter::WGS84;
|
||||||
let elements = circular(7_000.0);
|
let elements = circular(7_000.0);
|
||||||
let state = elements.to_state(parameter).expect("valid");
|
let state = elements.to_state(parameter).expect("valid");
|
||||||
|
|
@ -345,6 +384,7 @@ fn a_finite_burn_can_be_applied_to_elements() {
|
||||||
.apply_to_elements(elements, parameter, 500)
|
.apply_to_elements(elements, parameter, 500)
|
||||||
.expect("the burn runs");
|
.expect("the burn runs");
|
||||||
// Both routes must give the same state.
|
// Both routes must give the same state.
|
||||||
|
// 两条路径必须给出相同状态。
|
||||||
assert!(
|
assert!(
|
||||||
from_state
|
from_state
|
||||||
.state
|
.state
|
||||||
|
|
@ -382,6 +422,7 @@ fn a_burn_with_no_steps_is_refused() {
|
||||||
fn a_large_burn_can_escape_and_is_reported() {
|
fn a_large_burn_can_escape_and_is_reported() {
|
||||||
// A burn hard enough to exceed escape velocity leaves no elliptical
|
// A burn hard enough to exceed escape velocity leaves no elliptical
|
||||||
// elements, which the outcome reports by returning `None`.
|
// elements, which the outcome reports by returning `None`.
|
||||||
|
// 强度足以超过逃逸速度的点火不留下椭圆根数,结果通过返回 `None` 报告这一点。
|
||||||
let parameter = GravityParameter::WGS84;
|
let parameter = GravityParameter::WGS84;
|
||||||
let state = circular(7_000.0).to_state(parameter).expect("valid");
|
let state = circular(7_000.0).to_state(parameter).expect("valid");
|
||||||
let direction = state.velocity.normalize().expect("non-zero");
|
let direction = state.velocity.normalize().expect("non-zero");
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
//! Integration tests for sampling and maneuvering through the facade.
|
//! Integration tests for sampling and maneuvering through the facade.
|
||||||
|
//!
|
||||||
|
//! 通过门面进行采样与机动的集成测试。
|
||||||
|
|
||||||
use apsis_kepler::GravityParameter;
|
use apsis_kepler::GravityParameter;
|
||||||
use apsis_maneuver::{FiniteBurn, hohmann_transfer};
|
use apsis_maneuver::{FiniteBurn, hohmann_transfer};
|
||||||
|
|
@ -6,6 +8,8 @@ use apsis_orbit::{EphemerisSamplingError, GravityModel, ManeuverApplicationError
|
||||||
use apsis_tle::TwoLineElement;
|
use apsis_tle::TwoLineElement;
|
||||||
|
|
||||||
/// A satellite in a low Earth orbit, used as the source of the track.
|
/// A satellite in a low Earth orbit, used as the source of the track.
|
||||||
|
///
|
||||||
|
/// 一颗低轨卫星,用作轨迹的来源。
|
||||||
const ISS: &str = concat!(
|
const ISS: &str = concat!(
|
||||||
"1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n",
|
"1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n",
|
||||||
"2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537\r\n",
|
"2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537\r\n",
|
||||||
|
|
@ -20,15 +24,18 @@ fn orbit() -> Orbit {
|
||||||
fn an_orbit_samples_into_an_ephemeris() {
|
fn an_orbit_samples_into_an_ephemeris() {
|
||||||
let orbit = orbit();
|
let orbit = orbit();
|
||||||
// Ten minutes over a one minute grid.
|
// Ten minutes over a one minute grid.
|
||||||
|
// 一分钟网格上的十分钟。
|
||||||
let ephemeris = orbit
|
let ephemeris = orbit
|
||||||
.sample_ephemeris(0.0, 10.0, 1.0)
|
.sample_ephemeris(0.0, 10.0, 1.0)
|
||||||
.expect("the grid is usable");
|
.expect("the grid is usable");
|
||||||
assert_eq!(ephemeris.len(), 11);
|
assert_eq!(ephemeris.len(), 11);
|
||||||
// The sample times are seconds from the element epoch.
|
// The sample times are seconds from the element epoch.
|
||||||
|
// 样本时刻是距根数历元的秒数。
|
||||||
assert_eq!(ephemeris.start_time(), 0.0);
|
assert_eq!(ephemeris.start_time(), 0.0);
|
||||||
assert_eq!(ephemeris.end_time(), 600.0);
|
assert_eq!(ephemeris.end_time(), 600.0);
|
||||||
|
|
||||||
// Every sample must match a direct propagation.
|
// Every sample must match a direct propagation.
|
||||||
|
// 每个样本必须与直接传播一致。
|
||||||
for sample in ephemeris.samples() {
|
for sample in ephemeris.samples() {
|
||||||
let direct = orbit
|
let direct = orbit
|
||||||
.propagate_minutes(sample.seconds / 60.0)
|
.propagate_minutes(sample.seconds / 60.0)
|
||||||
|
|
@ -45,6 +52,8 @@ fn an_orbit_samples_into_an_ephemeris() {
|
||||||
fn the_interpolated_ephemeris_matches_a_direct_propagation() {
|
fn the_interpolated_ephemeris_matches_a_direct_propagation() {
|
||||||
// This is the point of the ephemeris: a query between samples must agree
|
// This is the point of the ephemeris: a query between samples must agree
|
||||||
// with the propagator to well within the accuracy a track needs.
|
// with the propagator to well within the accuracy a track needs.
|
||||||
|
// 这正是星历的要点:样本之间的查询必须与传播器一致,且远在一条轨迹所需的精度
|
||||||
|
// 之内。
|
||||||
let orbit = orbit();
|
let orbit = orbit();
|
||||||
let ephemeris = orbit
|
let ephemeris = orbit
|
||||||
.sample_ephemeris(0.0, 30.0, 1.0)
|
.sample_ephemeris(0.0, 30.0, 1.0)
|
||||||
|
|
@ -52,6 +61,7 @@ fn the_interpolated_ephemeris_matches_a_direct_propagation() {
|
||||||
|
|
||||||
let mut worst = 0.0_f64;
|
let mut worst = 0.0_f64;
|
||||||
// Query every ten seconds, which includes points between samples.
|
// Query every ten seconds, which includes points between samples.
|
||||||
|
// 每十秒查询一次,其中含样本之间的点。
|
||||||
for index in 0..180 {
|
for index in 0..180 {
|
||||||
let seconds = f64::from(index) * 10.0;
|
let seconds = f64::from(index) * 10.0;
|
||||||
let interpolated = ephemeris.state_at(seconds).expect("the time is covered");
|
let interpolated = ephemeris.state_at(seconds).expect("the time is covered");
|
||||||
|
|
@ -60,6 +70,7 @@ fn the_interpolated_ephemeris_matches_a_direct_propagation() {
|
||||||
}
|
}
|
||||||
// A one minute Hermite step on a low Earth orbit is accurate to far
|
// A one minute Hermite step on a low Earth orbit is accurate to far
|
||||||
// better than a meter here.
|
// better than a meter here.
|
||||||
|
// 低轨上一分钟的 Hermite 步在此精确到远优于一米。
|
||||||
assert!(worst < 1e-3, "the worst interpolation error is {worst} km");
|
assert!(worst < 1e-3, "the worst interpolation error is {worst} km");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,6 +96,7 @@ fn a_propagation_failure_is_reported() {
|
||||||
let orbit = orbit();
|
let orbit = orbit();
|
||||||
// Far enough ahead that the drag term brings the satellite down, which
|
// Far enough ahead that the drag term brings the satellite down, which
|
||||||
// is a decay failure rather than a silent bad sample.
|
// is a decay failure rather than a silent bad sample.
|
||||||
|
// 足够远,使阻力项把卫星带下来,即衰减失败而非无声的坏样本。
|
||||||
let result = orbit.sample_ephemeris(0.0, 60.0 * 24.0 * 365.0, 60.0 * 24.0);
|
let result = orbit.sample_ephemeris(0.0, 60.0 * 24.0 * 365.0, 60.0 * 24.0);
|
||||||
assert!(
|
assert!(
|
||||||
matches!(result, Err(EphemerisSamplingError::Propagation(_)) | Ok(_)),
|
matches!(result, Err(EphemerisSamplingError::Propagation(_)) | Ok(_)),
|
||||||
|
|
@ -98,6 +110,7 @@ fn a_burn_raises_the_osculating_orbit() {
|
||||||
let before = orbit.osculating_elements(0.0).expect("elliptical");
|
let before = orbit.osculating_elements(0.0).expect("elliptical");
|
||||||
let state = orbit.propagate_minutes(0.0).expect("propagates");
|
let state = orbit.propagate_minutes(0.0).expect("propagates");
|
||||||
// Thrust along the velocity.
|
// Thrust along the velocity.
|
||||||
|
// 沿速度方向施加推力。
|
||||||
let direction = state.velocity.normalize().expect("non-zero");
|
let direction = state.velocity.normalize().expect("non-zero");
|
||||||
let burn = FiniteBurn::along(direction, 1e-4, 30.0).expect("usable");
|
let burn = FiniteBurn::along(direction, 1e-4, 30.0).expect("usable");
|
||||||
|
|
||||||
|
|
@ -114,6 +127,7 @@ fn a_burn_raises_the_osculating_orbit() {
|
||||||
fn a_burn_matches_a_direct_application() {
|
fn a_burn_matches_a_direct_application() {
|
||||||
// The facade must be a thin wrapper: applying the burn through it is the
|
// The facade must be a thin wrapper: applying the burn through it is the
|
||||||
// same as propagating and applying the burn by hand.
|
// same as propagating and applying the burn by hand.
|
||||||
|
// 门面必须是薄封装:通过它施加点火与手工传播并施加点火相同。
|
||||||
let orbit = orbit();
|
let orbit = orbit();
|
||||||
let state = orbit.propagate_minutes(5.0).expect("propagates");
|
let state = orbit.propagate_minutes(5.0).expect("propagates");
|
||||||
let direction = state.velocity.normalize().expect("non-zero");
|
let direction = state.velocity.normalize().expect("non-zero");
|
||||||
|
|
@ -146,6 +160,7 @@ fn an_escaping_burn_is_reported() {
|
||||||
let state = orbit.propagate_minutes(0.0).expect("propagates");
|
let state = orbit.propagate_minutes(0.0).expect("propagates");
|
||||||
let direction = state.velocity.normalize().expect("non-zero");
|
let direction = state.velocity.normalize().expect("non-zero");
|
||||||
// A burn far larger than orbital velocity.
|
// A burn far larger than orbital velocity.
|
||||||
|
// 远大于轨道速度的点火。
|
||||||
let burn = FiniteBurn::along(direction, 1e-2, 600.0).expect("usable");
|
let burn = FiniteBurn::along(direction, 1e-2, 600.0).expect("usable");
|
||||||
let result = orbit.apply_burn(0.0, burn, 2_000);
|
let result = orbit.apply_burn(0.0, burn, 2_000);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
|
|
@ -159,6 +174,8 @@ fn a_hohmann_transfer_plans_a_raise_the_orbit_layer_can_sample() {
|
||||||
// A cross-layer check that ties the maneuver and ephemeris crates to the
|
// A cross-layer check that ties the maneuver and ephemeris crates to the
|
||||||
// propagation: plan a transfer, apply the first burn, and sample the
|
// propagation: plan a transfer, apply the first burn, and sample the
|
||||||
// resulting orbit.
|
// resulting orbit.
|
||||||
|
// 把 maneuver 与 ephemeris crate 联系到传播的跨层检查:规划一次转移,施加第一次
|
||||||
|
// 点火,并对所得轨道采样。
|
||||||
let orbit = orbit();
|
let orbit = orbit();
|
||||||
let elements = orbit.osculating_elements(0.0).expect("elliptical");
|
let elements = orbit.osculating_elements(0.0).expect("elliptical");
|
||||||
let from = elements.semi_major_axis;
|
let from = elements.semi_major_axis;
|
||||||
|
|
@ -166,9 +183,11 @@ fn a_hohmann_transfer_plans_a_raise_the_orbit_layer_can_sample() {
|
||||||
let transfer = hohmann_transfer(from, to, GravityParameter::WGS72).expect("usable");
|
let transfer = hohmann_transfer(from, to, GravityParameter::WGS72).expect("usable");
|
||||||
|
|
||||||
// The first burn is prograde for a raise, so thrust along the velocity.
|
// The first burn is prograde for a raise, so thrust along the velocity.
|
||||||
|
// 抬高时第一次点火是顺行的,故沿速度方向施加推力。
|
||||||
let state = orbit.propagate_minutes(0.0).expect("propagates");
|
let state = orbit.propagate_minutes(0.0).expect("propagates");
|
||||||
let direction = state.velocity.normalize().expect("non-zero");
|
let direction = state.velocity.normalize().expect("non-zero");
|
||||||
// Turn the delta-v into an acceleration over a short burn.
|
// Turn the delta-v into an acceleration over a short burn.
|
||||||
|
// 在短点火中把 delta-v 化为加速度。
|
||||||
let duration = 10.0;
|
let duration = 10.0;
|
||||||
let burn =
|
let burn =
|
||||||
FiniteBurn::along(direction, transfer.first_burn / duration, duration).expect("usable");
|
FiniteBurn::along(direction, transfer.first_burn / duration, duration).expect("usable");
|
||||||
|
|
@ -183,6 +202,8 @@ fn a_hohmann_transfer_plans_a_raise_the_orbit_layer_can_sample() {
|
||||||
// result. Turning them back into a propagatable orbit would need mean
|
// result. Turning them back into a propagatable orbit would need mean
|
||||||
// elements, which SGP4 requires and an osculating set does not provide,
|
// elements, which SGP4 requires and an osculating set does not provide,
|
||||||
// so the workflow stops here rather than feeding them back in.
|
// so the workflow stops here rather than feeding them back in.
|
||||||
|
// 点火后状态的密切根数即规划结果。要把它们转回可传播的轨道需要平根数,SGP4
|
||||||
|
// 要求平根数而密切根数集不提供,故该工作流到此为止,而不把它们送回。
|
||||||
assert!(after.is_valid());
|
assert!(after.is_valid());
|
||||||
assert!(after.eccentricity < 1.0);
|
assert!(after.eccentricity < 1.0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
//! Tests for the combined orbit type.
|
//! Tests for the combined orbit type.
|
||||||
|
//!
|
||||||
|
//! 合并后的轨道类型的测试。
|
||||||
|
|
||||||
use apsis_orbit::{GravityModel, Orbit};
|
use apsis_orbit::{GravityModel, Orbit};
|
||||||
use apsis_tle::TwoLineElement;
|
use apsis_tle::TwoLineElement;
|
||||||
|
|
@ -64,6 +66,7 @@ fn propagate_at_uses_the_element_epoch() {
|
||||||
assert!((at_epoch.position - at_zero.position).norm() < 1e-6);
|
assert!((at_epoch.position - at_zero.position).norm() < 1e-6);
|
||||||
|
|
||||||
// One day later is 1440 minutes.
|
// One day later is 1440 minutes.
|
||||||
|
// 一天后是 1440 分钟。
|
||||||
let next_day = orbit.propagate_at(epoch + 1.0).expect("state exists");
|
let next_day = orbit.propagate_at(epoch + 1.0).expect("state exists");
|
||||||
let one_day = orbit.propagate_minutes(1440.0).expect("state exists");
|
let one_day = orbit.propagate_minutes(1440.0).expect("state exists");
|
||||||
assert!((next_day.position - one_day.position).norm() < 1e-3);
|
assert!((next_day.position - one_day.position).norm() < 1e-3);
|
||||||
|
|
@ -80,6 +83,9 @@ fn osculating_elements_describe_a_low_earth_orbit() {
|
||||||
// is expected: the element set stores mean elements and the osculating
|
// is expected: the element set stores mean elements and the osculating
|
||||||
// eccentricity includes the short period variation the two-body model
|
// eccentricity includes the short period variation the two-body model
|
||||||
// does not reproduce.
|
// does not reproduce.
|
||||||
|
// 这些值是针对同一天量矢量的一份独立计算核对的。倾角与编目值接近,因为扁率对它
|
||||||
|
// 影响很小,而偏心率不同于编目的 0.0006703。该差异是预期的:根数集保存的是平
|
||||||
|
// 根数,而密切偏心率包含二体模型不复现的短周期变化。
|
||||||
assert!(
|
assert!(
|
||||||
(elements.semi_major_axis - 6_725.542).abs() < 0.01,
|
(elements.semi_major_axis - 6_725.542).abs() < 0.01,
|
||||||
"semi-major axis {}",
|
"semi-major axis {}",
|
||||||
|
|
@ -114,6 +120,7 @@ fn osculating_elements_change_along_the_orbit() {
|
||||||
// The osculating elements are not the mean elements, so they vary
|
// The osculating elements are not the mean elements, so they vary
|
||||||
// because the perturbations SGP4 models are absent from the two-body
|
// because the perturbations SGP4 models are absent from the two-body
|
||||||
// reconstruction.
|
// reconstruction.
|
||||||
|
// 密切根数不是平根数,故它们变化,因为 SGP4 所建模的摄动在二体重构中缺席。
|
||||||
let orbit = orbit(ISS);
|
let orbit = orbit(ISS);
|
||||||
let start = orbit.osculating_elements(0.0).expect("state exists");
|
let start = orbit.osculating_elements(0.0).expect("state exists");
|
||||||
let later = orbit.osculating_elements(720.0).expect("state exists");
|
let later = orbit.osculating_elements(720.0).expect("state exists");
|
||||||
|
|
@ -127,6 +134,8 @@ fn osculating_elements_work_for_a_deep_space_orbit() {
|
||||||
// The catalogued mean values are an eccentricity of 0.6546925 and an
|
// The catalogued mean values are an eccentricity of 0.6546925 and an
|
||||||
// inclination of 64.1526 degrees. The osculating values are close but
|
// inclination of 64.1526 degrees. The osculating values are close but
|
||||||
// not identical for the reason given in the low Earth orbit test.
|
// not identical for the reason given in the low Earth orbit test.
|
||||||
|
// 编目的平值为偏心率 0.6546925、倾角 64.1526 度。密切值接近但不相同,理由同低轨
|
||||||
|
// 测试中所述。
|
||||||
assert!(
|
assert!(
|
||||||
(elements.eccentricity - 0.655_871_3).abs() < 1e-6,
|
(elements.eccentricity - 0.655_871_3).abs() < 1e-6,
|
||||||
"eccentricity {}",
|
"eccentricity {}",
|
||||||
|
|
@ -164,6 +173,7 @@ fn the_gravity_parameter_matches_the_model() {
|
||||||
fn the_facade_reexports_the_layer() {
|
fn the_facade_reexports_the_layer() {
|
||||||
// The facade must make the element parser and the propagator types
|
// The facade must make the element parser and the propagator types
|
||||||
// reachable without a separate dependency.
|
// reachable without a separate dependency.
|
||||||
|
// 门面必须使根数解析器与传播器类型无需单独依赖即可达到。
|
||||||
let element = apsis_orbit::TwoLineElement::parse(ISS).expect("parses");
|
let element = apsis_orbit::TwoLineElement::parse(ISS).expect("parses");
|
||||||
assert_eq!(element.satellite_number, 25544);
|
assert_eq!(element.satellite_number, 25544);
|
||||||
let satellite =
|
let satellite =
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@
|
||||||
//! values the sequential forms do, in the same order. Every test compares
|
//! values the sequential forms do, in the same order. Every test compares
|
||||||
//! against the sequential answer rather than against a constant, so a
|
//! against the sequential answer rather than against a constant, so a
|
||||||
//! mismatch in either the propagation or the ordering is caught.
|
//! mismatch in either the propagation or the ordering is caught.
|
||||||
|
//!
|
||||||
|
//! 批量传播入口的测试。
|
||||||
|
//!
|
||||||
|
//! 要紧的性质是并行形式产生与顺序形式完全相同的值,且次序相同。每个测试都与顺序
|
||||||
|
//! 答案而非常量比较,故传播或排序中的任何不一致都会被捕获。
|
||||||
|
|
||||||
use apsis_sgp4::{
|
use apsis_sgp4::{
|
||||||
GravityModel, Satellite, TwoLineElement, propagate_at, propagate_at_parallel,
|
GravityModel, Satellite, TwoLineElement, propagate_at, propagate_at_parallel,
|
||||||
|
|
@ -33,6 +38,7 @@ fn a_short_track_matches_the_sequential_form() {
|
||||||
fn a_long_track_matches_the_sequential_form() {
|
fn a_long_track_matches_the_sequential_form() {
|
||||||
// A day at a one minute step, which is long enough that the parallel path
|
// A day at a one minute step, which is long enough that the parallel path
|
||||||
// takes the multi thread branch.
|
// takes the multi thread branch.
|
||||||
|
// 一分钟步长的一天,长到足以使并行路径走多线程分支。
|
||||||
let satellite = satellite();
|
let satellite = satellite();
|
||||||
let minutes: Vec<f64> = (0..1_441).map(|index| index as f64).collect();
|
let minutes: Vec<f64> = (0..1_441).map(|index| index as f64).collect();
|
||||||
let sequential = propagate_minutes_at(&satellite, &minutes);
|
let sequential = propagate_minutes_at(&satellite, &minutes);
|
||||||
|
|
@ -45,6 +51,7 @@ fn a_long_track_matches_the_sequential_form() {
|
||||||
fn a_backwards_track_matches() {
|
fn a_backwards_track_matches() {
|
||||||
// Negative minutes propagate backwards and must behave the same way in
|
// Negative minutes propagate backwards and must behave the same way in
|
||||||
// both forms.
|
// both forms.
|
||||||
|
// 负分钟数向后传播,且在两种形式中必须表现相同。
|
||||||
let satellite = satellite();
|
let satellite = satellite();
|
||||||
let minutes: Vec<f64> = (0..2_000).map(|index| -(index as f64) * 0.5).collect();
|
let minutes: Vec<f64> = (0..2_000).map(|index| -(index as f64) * 0.5).collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -64,6 +71,8 @@ fn a_decayed_instant_is_reported_in_both_forms() {
|
||||||
// Far enough ahead that the drag term brings the satellite down, both
|
// Far enough ahead that the drag term brings the satellite down, both
|
||||||
// forms must agree that the instant is unusable rather than one of them
|
// forms must agree that the instant is unusable rather than one of them
|
||||||
// stopping early.
|
// stopping early.
|
||||||
|
// 足够远,使阻力项把卫星带下来,两种形式必须一致认为该瞬时不可用,而非其中之
|
||||||
|
// 一提前停止。
|
||||||
let satellite = satellite();
|
let satellite = satellite();
|
||||||
let minutes = vec![0.0, 1.0, 60.0 * 24.0 * 365.0 * 20.0];
|
let minutes = vec![0.0, 1.0, 60.0 * 24.0 * 365.0 * 20.0];
|
||||||
let sequential = propagate_minutes_at(&satellite, &minutes);
|
let sequential = propagate_minutes_at(&satellite, &minutes);
|
||||||
|
|
@ -86,6 +95,7 @@ fn a_shell_at_one_instant_matches() {
|
||||||
fn a_shell_result_is_in_input_order() {
|
fn a_shell_result_is_in_input_order() {
|
||||||
// The states of a repeated satellite are identical, so ordering is checked
|
// The states of a repeated satellite are identical, so ordering is checked
|
||||||
// through the minutes form, where each value differs.
|
// through the minutes form, where each value differs.
|
||||||
|
// 重复卫星的状态相同,故通过分钟形式检查次序,那里每个值都不同。
|
||||||
let satellite = satellite();
|
let satellite = satellite();
|
||||||
let minutes: Vec<f64> = (0..3_000).map(|index| index as f64 * 0.25).collect();
|
let minutes: Vec<f64> = (0..3_000).map(|index| index as f64 * 0.25).collect();
|
||||||
let parallel = propagate_minutes_at_parallel(&satellite, &minutes);
|
let parallel = propagate_minutes_at_parallel(&satellite, &minutes);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
//! Tests for the gravity models.
|
//! Tests for the gravity models.
|
||||||
|
//!
|
||||||
|
//! 引力模型的测试。
|
||||||
|
|
||||||
use apsis_sgp4::GravityModel;
|
use apsis_sgp4::GravityModel;
|
||||||
|
|
||||||
|
|
@ -22,6 +24,8 @@ fn wgs84_defining_values_match_the_standard() {
|
||||||
fn kepler_constant_matches_the_reference_value() {
|
fn kepler_constant_matches_the_reference_value() {
|
||||||
// The reference implementation uses xke = 0.07436691613317342 for WGS
|
// The reference implementation uses xke = 0.07436691613317342 for WGS
|
||||||
// 72, derived as 60 / sqrt(radius^3 / mu).
|
// 72, derived as 60 / sqrt(radius^3 / mu).
|
||||||
|
// 参考实现对 WGS 72 使用 xke = 0.07436691613317342,由 60 / sqrt(radius^3 / mu)
|
||||||
|
// 导出。
|
||||||
let xke = GravityModel::WGS72.xke();
|
let xke = GravityModel::WGS72.xke();
|
||||||
assert!((xke - 0.074_366_916_133_173_42).abs() < 1e-15, "got {xke}");
|
assert!((xke - 0.074_366_916_133_173_42).abs() < 1e-15, "got {xke}");
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +35,7 @@ fn time_unit_is_the_reciprocal_of_the_kepler_constant() {
|
||||||
let model = GravityModel::WGS72;
|
let model = GravityModel::WGS72;
|
||||||
assert!((model.tumin() * model.xke() - 1.0).abs() < 1e-15);
|
assert!((model.tumin() * model.xke() - 1.0).abs() < 1e-15);
|
||||||
// The value is close to 13.44684 minutes for WGS 72.
|
// The value is close to 13.44684 minutes for WGS 72.
|
||||||
|
// 对 WGS 72,该值接近 13.44684 分钟。
|
||||||
assert!((model.tumin() - 13.446_839_696_959_309).abs() < 1e-12);
|
assert!((model.tumin() - 13.446_839_696_959_309).abs() < 1e-12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,6 +45,7 @@ fn j3_over_j2_matches_the_quotient() {
|
||||||
let expected = model.j3 / model.j2;
|
let expected = model.j3 / model.j2;
|
||||||
assert!((model.j3_over_j2() - expected).abs() < 1e-18);
|
assert!((model.j3_over_j2() - expected).abs() < 1e-18);
|
||||||
// The value is about -0.002345069720011528.
|
// The value is about -0.002345069720011528.
|
||||||
|
// 该值约为 -0.002345069720011528。
|
||||||
assert!((model.j3_over_j2() + 0.002_345_069_720_011_528).abs() < 1e-15);
|
assert!((model.j3_over_j2() + 0.002_345_069_720_011_528).abs() < 1e-15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,19 @@
|
||||||
//! treated as correct, because both implementations evaluate the same
|
//! treated as correct, because both implementations evaluate the same
|
||||||
//! closed form expressions and should differ only by floating point
|
//! closed form expressions and should differ only by floating point
|
||||||
//! rounding.
|
//! rounding.
|
||||||
|
//!
|
||||||
|
//! 传播器对照参考实现的测试。
|
||||||
|
//!
|
||||||
|
//! 本文件中的期望状态由 `sgp4` 参考库产生,它实现 Vallado 对该理论的修订。各根数
|
||||||
|
//! 集是随该库分发的历史测试用例。与参考相符到远低于一微米的传播被视为正确,因为
|
||||||
|
//! 两种实现求相同的闭式表达式,应只差浮点舍入。
|
||||||
|
|
||||||
use apsis_sgp4::{GravityModel, PropagationErrorKind, Satellite, State};
|
use apsis_sgp4::{GravityModel, PropagationErrorKind, Satellite, State};
|
||||||
use apsis_tle::TwoLineElement;
|
use apsis_tle::TwoLineElement;
|
||||||
|
|
||||||
/// A near circular low Earth orbit, the International Space Station.
|
/// A near circular low Earth orbit, the International Space Station.
|
||||||
|
///
|
||||||
|
/// 近圆形低轨,国际空间站。
|
||||||
const ISS: &str = concat!(
|
const ISS: &str = concat!(
|
||||||
"1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n",
|
"1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n",
|
||||||
"2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537\r\n",
|
"2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537\r\n",
|
||||||
|
|
@ -19,18 +27,24 @@ const ISS: &str = concat!(
|
||||||
|
|
||||||
/// A highly eccentric orbit, which exercises the eccentric anomaly
|
/// A highly eccentric orbit, which exercises the eccentric anomaly
|
||||||
/// iteration away from circular motion.
|
/// iteration away from circular motion.
|
||||||
|
///
|
||||||
|
/// 一个高度偏心的轨道,它检验远离圆运动时的偏近点角迭代。
|
||||||
const VANGUARD: &str = concat!(
|
const VANGUARD: &str = concat!(
|
||||||
"1 00005U 58002B 00179.78495062 .00000023 00000-0 28098-4 0 4753\r\n",
|
"1 00005U 58002B 00179.78495062 .00000023 00000-0 28098-4 0 4753\r\n",
|
||||||
"2 00005 34.2682 348.7242 1859667 331.7664 19.3264 10.82419157413667\r\n",
|
"2 00005 34.2682 348.7242 1859667 331.7664 19.3264 10.82419157413667\r\n",
|
||||||
);
|
);
|
||||||
|
|
||||||
/// A deep space orbit with a half-day resonance.
|
/// A deep space orbit with a half-day resonance.
|
||||||
|
///
|
||||||
|
/// 一个具有半日共振的深空轨道。
|
||||||
const DEEP_SPACE: &str = concat!(
|
const DEEP_SPACE: &str = concat!(
|
||||||
"1 09880U 77021A 06174.85710247 .00000090 00000-0 10000-3 0 7322\r\n",
|
"1 09880U 77021A 06174.85710247 .00000090 00000-0 10000-3 0 7322\r\n",
|
||||||
"2 09880 64.1526 313.1296 6546925 103.4683 305.3980 2.00754276 13047\r\n",
|
"2 09880 64.1526 313.1296 6546925 103.4683 305.3980 2.00754276 13047\r\n",
|
||||||
);
|
);
|
||||||
|
|
||||||
/// A geostationary transfer orbit, also deep space.
|
/// A geostationary transfer orbit, also deep space.
|
||||||
|
///
|
||||||
|
/// 一个地球静止转移轨道,也是深空。
|
||||||
const TRANSFER: &str = concat!(
|
const TRANSFER: &str = concat!(
|
||||||
"1 27651U 03004A 06185.99374200 .00000109 00000-0 19909-4 0 445\r\n",
|
"1 27651U 03004A 06185.99374200 .00000109 00000-0 19909-4 0 445\r\n",
|
||||||
"2 27651 7.1743 182.9443 7867251 269.0386 18.6601 1.03461646 9884\r\n",
|
"2 27651 7.1743 182.9443 7867251 269.0386 18.6601 1.03461646 9884\r\n",
|
||||||
|
|
@ -38,9 +52,13 @@ const TRANSFER: &str = concat!(
|
||||||
|
|
||||||
/// The position tolerance, in kilometers. The observed agreement is at the
|
/// The position tolerance, in kilometers. The observed agreement is at the
|
||||||
/// nanometer level, so this leaves a wide margin.
|
/// nanometer level, so this leaves a wide margin.
|
||||||
|
///
|
||||||
|
/// 位置容差,单位为千米。所观测的相符在纳米水平,故这留下很宽的余量。
|
||||||
const POSITION_TOLERANCE: f64 = 1e-3;
|
const POSITION_TOLERANCE: f64 = 1e-3;
|
||||||
|
|
||||||
/// The velocity tolerance, in kilometers per second.
|
/// The velocity tolerance, in kilometers per second.
|
||||||
|
///
|
||||||
|
/// 速度容差,单位为千米每秒。
|
||||||
const VELOCITY_TOLERANCE: f64 = 1e-9;
|
const VELOCITY_TOLERANCE: f64 = 1e-9;
|
||||||
|
|
||||||
fn satellite(text: &str) -> Satellite {
|
fn satellite(text: &str) -> Satellite {
|
||||||
|
|
@ -156,6 +174,7 @@ fn deep_space_satellite_uses_the_deep_space_branch() {
|
||||||
#[test]
|
#[test]
|
||||||
fn deep_space_at_epoch_matches_the_reference() {
|
fn deep_space_at_epoch_matches_the_reference() {
|
||||||
// MJD 53_962.36, about 12 hours after the element epoch.
|
// MJD 53_962.36, about 12 hours after the element epoch.
|
||||||
|
// MJD 53_962.36,约在根数历元之后 12 小时。
|
||||||
let state = satellite(DEEP_SPACE)
|
let state = satellite(DEEP_SPACE)
|
||||||
.propagate_minutes(0.0)
|
.propagate_minutes(0.0)
|
||||||
.expect("epoch state");
|
.expect("epoch state");
|
||||||
|
|
@ -207,6 +226,7 @@ fn propagation_is_deterministic() {
|
||||||
// Repeated evaluation must give the same result, which also verifies
|
// Repeated evaluation must give the same result, which also verifies
|
||||||
// that the resonance integrator does not carry hidden state between
|
// that the resonance integrator does not carry hidden state between
|
||||||
// calls.
|
// calls.
|
||||||
|
// 重复求值必须给出相同结果,这也验证共振积分器在调用之间不携带隐藏状态。
|
||||||
let satellite = satellite(DEEP_SPACE);
|
let satellite = satellite(DEEP_SPACE);
|
||||||
let first = satellite.propagate_minutes(5_000.0).expect("state exists");
|
let first = satellite.propagate_minutes(5_000.0).expect("state exists");
|
||||||
let second = satellite.propagate_minutes(5_000.0).expect("state exists");
|
let second = satellite.propagate_minutes(5_000.0).expect("state exists");
|
||||||
|
|
@ -217,6 +237,7 @@ fn propagation_is_deterministic() {
|
||||||
fn out_of_order_evaluation_gives_the_same_result() {
|
fn out_of_order_evaluation_gives_the_same_result() {
|
||||||
// Evaluating a later time first must not change a later evaluation of an
|
// Evaluating a later time first must not change a later evaluation of an
|
||||||
// earlier time.
|
// earlier time.
|
||||||
|
// 先求较晚的时间不得改变其后对较早时间的求值。
|
||||||
let satellite = satellite(DEEP_SPACE);
|
let satellite = satellite(DEEP_SPACE);
|
||||||
let late = satellite.propagate_minutes(10_000.0).expect("state exists");
|
let late = satellite.propagate_minutes(10_000.0).expect("state exists");
|
||||||
let early = satellite.propagate_minutes(1_000.0).expect("state exists");
|
let early = satellite.propagate_minutes(1_000.0).expect("state exists");
|
||||||
|
|
@ -264,12 +285,15 @@ fn invalid_elements_are_rejected() {
|
||||||
// assumed decimal point, so it is 0.9999999, a valid but extremely
|
// assumed decimal point, so it is 0.9999999, a valid but extremely
|
||||||
// elongated orbit. An eccentricity field that cannot be read at all is
|
// elongated orbit. An eccentricity field that cannot be read at all is
|
||||||
// rejected by the parser instead, which is checked in the TLE tests.
|
// rejected by the parser instead, which is checked in the TLE tests.
|
||||||
|
// 首位数字为九的偏心率是用隐含小数点写成的,故它是 0.9999999,一个有效但极其
|
||||||
|
// 细长的轨道。完全无法读取的偏心率字段改由解析器拒绝,这在 TLE 测试中检查。
|
||||||
let text = concat!(
|
let text = concat!(
|
||||||
"1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n",
|
"1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n",
|
||||||
"2 25544 51.6416 247.4627 0000000 130.5360 325.0288 15.72125391563531\r\n",
|
"2 25544 51.6416 247.4627 0000000 130.5360 325.0288 15.72125391563531\r\n",
|
||||||
);
|
);
|
||||||
let element = TwoLineElement::parse(text).expect("parses");
|
let element = TwoLineElement::parse(text).expect("parses");
|
||||||
// A circular orbit is perfectly valid, so this must succeed.
|
// A circular orbit is perfectly valid, so this must succeed.
|
||||||
|
// 圆轨道完全有效,故这必须成功。
|
||||||
assert!(Satellite::from_element(&element, GravityModel::WGS72).is_some());
|
assert!(Satellite::from_element(&element, GravityModel::WGS72).is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -279,6 +303,8 @@ fn a_zero_mean_motion_is_rejected() {
|
||||||
// describes no orbit. The element set must still carry a valid
|
// describes no orbit. The element set must still carry a valid
|
||||||
// checksum, so the test builds one from a parsed set rather than
|
// checksum, so the test builds one from a parsed set rather than
|
||||||
// editing text.
|
// editing text.
|
||||||
|
// 平运动字段以每天圈数计,故那里的零描述不出轨道。根数集仍须携带有效校验和,故
|
||||||
|
// 测试从已解析的集合构建一个,而非编辑文本。
|
||||||
let element = TwoLineElement::parse(ISS).expect("parses");
|
let element = TwoLineElement::parse(ISS).expect("parses");
|
||||||
let mut elements = apsis_sgp4::Elements::from_tle(&element).expect("epoch is covered");
|
let mut elements = apsis_sgp4::Elements::from_tle(&element).expect("epoch is covered");
|
||||||
elements.mean_motion = 0.0;
|
elements.mean_motion = 0.0;
|
||||||
|
|
@ -313,5 +339,6 @@ fn state_accessors_report_radius_and_speed() {
|
||||||
assert!((state.altitude_radius() - state.position.norm()).abs() < 1e-12);
|
assert!((state.altitude_radius() - state.position.norm()).abs() < 1e-12);
|
||||||
assert!((state.speed() - state.velocity.norm()).abs() < 1e-12);
|
assert!((state.speed() - state.velocity.norm()).abs() < 1e-12);
|
||||||
// A low Earth satellite moves at about 7.7 km/s.
|
// A low Earth satellite moves at about 7.7 km/s.
|
||||||
|
// 低轨卫星约以 7.7 km/s 运动。
|
||||||
assert!(state.speed() > 7.0 && state.speed() < 8.0);
|
assert!(state.speed() > 7.0 && state.speed() < 8.0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
//! Tests for line checksums.
|
//! Tests for line checksums.
|
||||||
|
//!
|
||||||
|
//! 行校验和的测试。
|
||||||
|
|
||||||
use apsis_tle::{line_checksum, verify_line_checksum};
|
use apsis_tle::{line_checksum, verify_line_checksum};
|
||||||
|
|
||||||
|
|
@ -20,6 +22,7 @@ fn verification_accepts_a_well_formed_line() {
|
||||||
#[test]
|
#[test]
|
||||||
fn verification_rejects_a_corrupted_digit() {
|
fn verification_rejects_a_corrupted_digit() {
|
||||||
// Changing a digit in the body changes the sum.
|
// Changing a digit in the body changes the sum.
|
||||||
|
// 改变正文中的一位数字会改变该和。
|
||||||
let corrupted = "1 35544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927";
|
let corrupted = "1 35544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927";
|
||||||
let error = verify_line_checksum(corrupted, 1).expect_err("checksum should fail");
|
let error = verify_line_checksum(corrupted, 1).expect_err("checksum should fail");
|
||||||
assert_eq!(error.kind(), apsis_tle::ElementErrorKind::Checksum);
|
assert_eq!(error.kind(), apsis_tle::ElementErrorKind::Checksum);
|
||||||
|
|
@ -36,6 +39,7 @@ fn verification_rejects_a_corrupted_checksum_digit() {
|
||||||
fn minus_signs_contribute_one_each() {
|
fn minus_signs_contribute_one_each() {
|
||||||
// A line of only minus signs, padded to 68 columns, sums to eight
|
// A line of only minus signs, padded to 68 columns, sums to eight
|
||||||
// modulo ten.
|
// modulo ten.
|
||||||
|
// 一行只有负号、补到 68 列,其对十取模的和为八。
|
||||||
let line = format!("{}{}", "-".repeat(68), "8");
|
let line = format!("{}{}", "-".repeat(68), "8");
|
||||||
assert_eq!(line_checksum(&line), 8);
|
assert_eq!(line_checksum(&line), 8);
|
||||||
}
|
}
|
||||||
|
|
@ -51,6 +55,7 @@ fn only_the_first_68_columns_contribute() {
|
||||||
let mut line = format!("{}0", "1".repeat(68));
|
let mut line = format!("{}0", "1".repeat(68));
|
||||||
assert_eq!(line_checksum(&line), 8);
|
assert_eq!(line_checksum(&line), 8);
|
||||||
// Appending digits beyond column 68 must not change the sum.
|
// Appending digits beyond column 68 must not change the sum.
|
||||||
|
// 在第 68 列之后追加数字不得改变该和。
|
||||||
line.push('9');
|
line.push('9');
|
||||||
assert_eq!(line_checksum(&line), 8);
|
assert_eq!(line_checksum(&line), 8);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
//! Tests for parsing a complete two-line element set.
|
//! Tests for parsing a complete two-line element set.
|
||||||
|
//!
|
||||||
|
//! 解析完整两行根数集的测试。
|
||||||
|
|
||||||
use apsis_time::TimeScale;
|
use apsis_time::TimeScale;
|
||||||
use apsis_tle::{Classification, ElementErrorKind, TwoLineElement};
|
use apsis_tle::{Classification, ElementErrorKind, TwoLineElement};
|
||||||
|
|
||||||
/// The sample element set for the International Space Station used by the
|
/// The sample element set for the International Space Station used by the
|
||||||
/// `sgp4` reference implementation.
|
/// `sgp4` reference implementation.
|
||||||
|
///
|
||||||
|
/// `sgp4` 参考实现所用的国际空间站样本根数集。
|
||||||
const ISS: &str = concat!(
|
const ISS: &str = concat!(
|
||||||
"1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n",
|
"1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n",
|
||||||
"2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537\r\n",
|
"2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537\r\n",
|
||||||
|
|
@ -31,11 +35,14 @@ fn sample_fields_match_the_reference_parser() {
|
||||||
fn exponent_fields_use_an_assumed_decimal_point() {
|
fn exponent_fields_use_an_assumed_decimal_point() {
|
||||||
let element = TwoLineElement::parse(ISS).expect("sample is well formed");
|
let element = TwoLineElement::parse(ISS).expect("sample is well formed");
|
||||||
// `-11606-4` is -0.11606e-4.
|
// `-11606-4` is -0.11606e-4.
|
||||||
|
// `-11606-4` 即 -0.11606e-4。
|
||||||
assert!((element.bstar - -1.1606e-5).abs() < 1e-18);
|
assert!((element.bstar - -1.1606e-5).abs() < 1e-18);
|
||||||
// `00000-0` is zero.
|
// `00000-0` is zero.
|
||||||
|
// `00000-0` 是零。
|
||||||
assert_eq!(element.mean_motion_double_dot, 0.0);
|
assert_eq!(element.mean_motion_double_dot, 0.0);
|
||||||
// `-.00002182` is written with an explicit decimal point and no leading
|
// `-.00002182` is written with an explicit decimal point and no leading
|
||||||
// zero.
|
// zero.
|
||||||
|
// `-.00002182` 用显式小数点且无前导零写出。
|
||||||
assert!((element.mean_motion_dot - -2.182e-5).abs() < 1e-18);
|
assert!((element.mean_motion_dot - -2.182e-5).abs() < 1e-18);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,6 +52,8 @@ fn epoch_resolves_to_the_expected_day() {
|
||||||
// The reference parser reports Julian Date 2454730.01782528 for the
|
// The reference parser reports Julian Date 2454730.01782528 for the
|
||||||
// epoch, which is Modified Julian Date 54729.51782528 on the UTC scale.
|
// epoch, which is Modified Julian Date 54729.51782528 on the UTC scale.
|
||||||
// Day 264 of 2008 is 2008-09-20, and the fraction is 12:25:40.
|
// Day 264 of 2008 is 2008-09-20, and the fraction is 12:25:40.
|
||||||
|
// 参考解析器报告该历元为儒略日 2454730.01782528,即 UTC 时标上的约化儒略日
|
||||||
|
// 54729.51782528。2008 年的第 264 天是 2008-09-20,零头为 12:25:40。
|
||||||
let mjd = element
|
let mjd = element
|
||||||
.epoch
|
.epoch
|
||||||
.mjd(TimeScale::Utc)
|
.mjd(TimeScale::Utc)
|
||||||
|
|
@ -97,6 +106,8 @@ fn classification_is_read_from_the_sample() {
|
||||||
|
|
||||||
/// Replaces the character at the given zero based column of the given zero
|
/// Replaces the character at the given zero based column of the given zero
|
||||||
/// based line and recomputes that line's checksum.
|
/// based line and recomputes that line's checksum.
|
||||||
|
///
|
||||||
|
/// 替换给定零基行中给定零基列处的字符,并重算该行的校验和。
|
||||||
fn mutate(text: &str, line_index: usize, column: usize, replacement: char) -> String {
|
fn mutate(text: &str, line_index: usize, column: usize, replacement: char) -> String {
|
||||||
let mut lines: Vec<String> = text.lines().map(str::to_string).collect();
|
let mut lines: Vec<String> = text.lines().map(str::to_string).collect();
|
||||||
let mut characters: Vec<char> = lines[line_index].chars().collect();
|
let mut characters: Vec<char> = lines[line_index].chars().collect();
|
||||||
|
|
@ -112,6 +123,8 @@ fn mismatched_satellite_numbers_are_rejected() {
|
||||||
// Change a digit of the second line's satellite number and repair the
|
// Change a digit of the second line's satellite number and repair the
|
||||||
// checksum, so that only the mismatch remains. The satellite number
|
// checksum, so that only the mismatch remains. The satellite number
|
||||||
// occupies one based columns 3 to 7, which are zero based 2 to 6.
|
// occupies one based columns 3 to 7, which are zero based 2 to 6.
|
||||||
|
// 改变第二行卫星编号的一位数字并修复校验和,使只剩下不匹配。卫星编号占据以 1
|
||||||
|
// 为基的第 3 至 7 列,即以 0 为基的第 2 至 6 列。
|
||||||
let text = mutate(ISS, 1, 2, '3');
|
let text = mutate(ISS, 1, 2, '3');
|
||||||
let error = TwoLineElement::parse(&text).expect_err("numbers differ");
|
let error = TwoLineElement::parse(&text).expect_err("numbers differ");
|
||||||
assert_eq!(error.kind(), ElementErrorKind::MismatchedElementSet);
|
assert_eq!(error.kind(), ElementErrorKind::MismatchedElementSet);
|
||||||
|
|
@ -150,6 +163,7 @@ fn an_empty_input_is_rejected() {
|
||||||
fn a_non_numeric_field_is_reported_with_its_columns() {
|
fn a_non_numeric_field_is_reported_with_its_columns() {
|
||||||
// Replace the first inclination digit with a letter, so that the
|
// Replace the first inclination digit with a letter, so that the
|
||||||
// checksum still matches but the field cannot be read.
|
// checksum still matches but the field cannot be read.
|
||||||
|
// 把第一个倾角数字换成字母,使校验和仍匹配但该字段无法读取。
|
||||||
let text = mutate(ISS, 1, 8, 'X');
|
let text = mutate(ISS, 1, 8, 'X');
|
||||||
|
|
||||||
let error = TwoLineElement::parse(&text).expect_err("inclination is not numeric");
|
let error = TwoLineElement::parse(&text).expect_err("inclination is not numeric");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue