apsis-rs/crates/orbit/apsis-sgp4/tests/propagation.rs

345 lines
13 KiB
Rust

//! Tests for the propagator against the reference implementation.
//!
//! The expected states in this file were produced by the `sgp4` reference
//! library, which implements Vallado's revision of the theory. The element
//! sets are the historical test cases distributed with that library. A
//! propagation that agrees with the reference to well under a micrometer is
//! 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",
);
/// 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",
);
/// 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 {
let element = TwoLineElement::parse(text).expect("test element set parses");
Satellite::from_element(&element, GravityModel::WGS72)
.expect("test element set is propagatable")
}
fn assert_matches_reference(state: State, position: [f64; 3], velocity: [f64; 3]) {
let position_error = [
state.position.x - position[0],
state.position.y - position[1],
state.position.z - position[2],
]
.iter()
.fold(0.0_f64, |worst, value| worst.max(value.abs()));
let velocity_error = [
state.velocity.x - velocity[0],
state.velocity.y - velocity[1],
state.velocity.z - velocity[2],
]
.iter()
.fold(0.0_f64, |worst, value| worst.max(value.abs()));
assert!(
position_error < POSITION_TOLERANCE,
"position error {position_error} km, got {}",
state.position
);
assert!(
velocity_error < VELOCITY_TOLERANCE,
"velocity error {velocity_error} km/s, got {}",
state.velocity
);
}
#[test]
fn iss_at_epoch_matches_the_reference() {
let state = satellite(ISS).propagate_minutes(0.0).expect("epoch state");
assert_matches_reference(
state,
[4_083.902_463_521, -993.631_999_606, 5_243.603_665_371],
[2.512_837_295_156, 7.259_888_524_981, -0.583_778_536_506],
);
}
#[test]
fn iss_after_one_minute_matches_the_reference() {
let state = satellite(ISS).propagate_minutes(1.0).expect("state exists");
assert_matches_reference(
state,
[4_224.931_504_689, -556.040_371_394, 5_196.213_688_087],
[2.186_284_834_622, 7.320_793_192_659, -0.995_282_270_726],
);
}
#[test]
fn iss_after_six_hours_matches_the_reference() {
let state = satellite(ISS)
.propagate_minutes(360.0)
.expect("state exists");
assert_matches_reference(
state,
[2_748.401_544_599, -3_564.892_404_578, 4_992.448_308_874],
[4.342_862_050_164, 6.063_045_163_749, 1.927_771_710_260],
);
}
#[test]
fn iss_after_one_day_matches_the_reference() {
let state = satellite(ISS)
.propagate_minutes(1_440.0)
.expect("state exists");
assert_matches_reference(
state,
[-3_199.119_301_995, -5_925.838_895_195, -104.283_883_010],
[4.160_900_126_061, -2.340_866_691_092, 6.034_239_787_489],
);
}
#[test]
fn iss_before_epoch_matches_the_reference() {
let state = satellite(ISS)
.propagate_minutes(-1_440.0)
.expect("state exists");
assert_matches_reference(
state,
[1_121.392_381_234, 6_541.559_708_790, -1_120.952_322_949],
[-4.940_430_025_083, -0.153_942_812_975, -5.902_529_984_919],
);
}
#[test]
fn vanguard_at_epoch_matches_the_reference() {
let state = satellite(VANGUARD)
.propagate_minutes(0.0)
.expect("epoch state");
assert_matches_reference(
state,
[7_022.465_292_664, -1_400.082_967_554, 0.039_951_554],
[1.893_841_014_513, 6.405_893_759_210, 4.534_807_250_355],
);
}
#[test]
fn deep_space_satellite_uses_the_deep_space_branch() {
let deep = satellite(DEEP_SPACE);
assert!(deep.is_deep_space());
let near = satellite(ISS);
assert!(!near.is_deep_space());
}
#[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");
assert_matches_reference(
state,
[12_020.677_224_927, -21_305.501_479_237, -11_902.993_493_655],
[0.646_341_708_451, 1.668_868_685_981, 3.329_292_846_315],
);
}
#[test]
fn deep_space_after_one_week_matches_the_reference() {
let state = satellite(DEEP_SPACE)
.propagate_minutes(10_080.0)
.expect("state exists");
assert_matches_reference(
state,
[12_501.446_941_807, -16_359.501_943_154, -3_718.626_893_065],
[-0.155_492_271_223, 2.911_308_113_855, 3.819_401_898_806],
);
}
#[test]
fn transfer_orbit_at_epoch_matches_the_reference() {
let state = satellite(TRANSFER)
.propagate_minutes(0.0)
.expect("epoch state");
assert_matches_reference(
state,
[-20_728.381_133_505, -10_167.792_006_995, 1_151.382_245_235],
[-1.744_013_319_342, -4.632_866_113_051, 0.570_823_646_148],
);
}
#[test]
fn transfer_orbit_after_one_week_matches_the_reference() {
let state = satellite(TRANSFER)
.propagate_minutes(10_080.0)
.expect("state exists");
assert_matches_reference(
state,
[-14_421.942_143_755, -62_862.455_417_683, 7_869.371_785_647],
[0.946_839_675_888, -1.301_129_854_934, 0.164_515_795_189],
);
}
#[test]
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");
assert_eq!(first, second);
}
#[test]
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");
let early_again = satellite.propagate_minutes(1_000.0).expect("state exists");
let late_again = satellite.propagate_minutes(10_000.0).expect("state exists");
assert_eq!(early, early_again);
assert_eq!(late, late_again);
}
#[test]
fn epoch_accessor_returns_the_element_set_epoch() {
let element = TwoLineElement::parse(ISS).expect("parses");
let expected = element
.epoch
.mjd(apsis_time::TimeScale::Utc)
.expect("epoch is in the leap table");
let satellite = Satellite::from_element(&element, GravityModel::WGS72).expect("propagatable");
assert!((satellite.epoch_mjd() - expected).abs() < 1e-12);
}
#[test]
fn minutes_since_epoch_is_measured_in_minutes() {
let element = TwoLineElement::parse(ISS).expect("parses");
let satellite = Satellite::from_element(&element, GravityModel::WGS72).expect("propagatable");
let epoch = satellite.epoch_mjd();
assert!((satellite.minutes_since_epoch(epoch)).abs() < 1e-9);
assert!((satellite.minutes_since_epoch(epoch + 1.0 / 1_440.0) - 1.0).abs() < 1e-6);
assert!((satellite.minutes_since_epoch(epoch - 1.0) + 1_440.0).abs() < 1e-6);
}
#[test]
fn propagate_at_the_epoch_matches_propagate_minutes_zero() {
let element = TwoLineElement::parse(ISS).expect("parses");
let satellite = Satellite::from_element(&element, GravityModel::WGS72).expect("propagatable");
let at_epoch = satellite
.propagate_at(satellite.epoch_mjd())
.expect("state exists");
let at_zero = satellite.propagate_minutes(0.0).expect("state exists");
assert_eq!(at_epoch, at_zero);
}
#[test]
fn invalid_elements_are_rejected() {
// An eccentricity whose leading digit is nine was written with an
// 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());
}
#[test]
fn a_zero_mean_motion_is_rejected() {
// The mean motion field is in revolutions per day, so a zero there
// 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;
let error = Satellite::try_from_elements(&elements, GravityModel::WGS72)
.expect_err("zero mean motion is not propagatable");
assert_eq!(error.kind(), PropagationErrorKind::InvalidElements);
}
#[test]
fn an_eccentricity_of_one_is_rejected() {
let element = TwoLineElement::parse(ISS).expect("parses");
let mut elements = apsis_sgp4::Elements::from_tle(&element).expect("epoch is covered");
elements.eccentricity = 1.0;
let error = Satellite::try_from_elements(&elements, GravityModel::WGS72)
.expect_err("an open orbit is not propagatable");
assert_eq!(error.kind(), PropagationErrorKind::InvalidElements);
}
#[test]
fn non_finite_elements_are_rejected() {
let element = TwoLineElement::parse(ISS).expect("parses");
let mut elements = apsis_sgp4::Elements::from_tle(&element).expect("epoch is covered");
elements.inclination = f64::NAN;
let error = Satellite::try_from_elements(&elements, GravityModel::WGS72)
.expect_err("a non-finite inclination is not propagatable");
assert_eq!(error.kind(), PropagationErrorKind::InvalidElements);
}
#[test]
fn state_accessors_report_radius_and_speed() {
let state = satellite(ISS).propagate_minutes(0.0).expect("epoch state");
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);
}