apsis-rs/crates/orbit/apsis-orbit/tests/ephemeris_maneuver.rs

210 lines
8.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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