use std::env;
use std::fs;
use std::path::Path;
use std::sync::OnceLock;
use anise::prelude::{Almanac, SPK};
use nalgebra::SVector;
use crate::AngleFormat;
use crate::constants::DEG2RAD;
use crate::constants::physical::R_EARTH;
use crate::eop::*;
use crate::math::angles::oe_to_radians;
use crate::orbit_dynamics::gravity::{GravityModel, GravityModelType, set_global_gravity_model};
use crate::orbits::keplerian::{geo_sma, perigee_velocity, sun_synchronous_inclination};
use crate::space_weather::{
FileSpaceWeatherProvider, SpaceWeatherExtrapolation, set_global_space_weather_provider,
};
use crate::spice::set_global_almanac;
use crate::utils::get_naif_cache_dir;
fn convert_to_output_format(
oe_degrees: SVector<f64, 6>,
output_format: AngleFormat,
) -> SVector<f64, 6> {
match output_format {
AngleFormat::Radians => oe_to_radians(oe_degrees, AngleFormat::Degrees),
AngleFormat::Degrees => oe_degrees,
}
}
pub(crate) fn fixture_orbit_leo(angle_format: AngleFormat) -> SVector<f64, 6> {
let a = R_EARTH + 500e3;
let e = 0.001;
let oe = SVector::<f64, 6>::new(
a, e, sun_synchronous_inclination(a, e, AngleFormat::Degrees), 15.0, 30.0, 45.0, );
convert_to_output_format(oe, angle_format)
}
pub(crate) fn fixture_orbit_geo(angle_format: AngleFormat) -> SVector<f64, 6> {
let oe = SVector::<f64, 6>::new(
geo_sma(), 0.0001, 0.0, 0.0, 0.0, 0.0, );
convert_to_output_format(oe, angle_format)
}
pub(crate) fn fixture_orbit_sso(angle_format: AngleFormat) -> SVector<f64, 6> {
let a = R_EARTH + 700e3;
let e = 0.001;
let oe = SVector::<f64, 6>::new(
a, e, sun_synchronous_inclination(a, e, AngleFormat::Degrees), 45.0, 90.0, 0.0, );
convert_to_output_format(oe, angle_format)
}
pub(crate) fn fixture_orbit_molniya(angle_format: AngleFormat) -> SVector<f64, 6> {
let oe = SVector::<f64, 6>::new(
26600e3, 0.74, 63.4, 270.0, 270.0, 0.0, );
convert_to_output_format(oe, angle_format)
}
pub(crate) fn fixture_orbit_polar(angle_format: AngleFormat) -> SVector<f64, 6> {
let oe = SVector::<f64, 6>::new(
R_EARTH + 800e3, 0.001, 90.0, 0.0, 0.0, 0.0, );
convert_to_output_format(oe, angle_format)
}
pub(crate) fn fixture_orbit_elliptical(angle_format: AngleFormat) -> SVector<f64, 6> {
let oe = SVector::<f64, 6>::new(
R_EARTH + 1000e3, 0.3, 45.0, 60.0, 120.0, 0.0, );
convert_to_output_format(oe, angle_format)
}
pub(crate) fn fixture_orbit_retrograde(angle_format: AngleFormat) -> SVector<f64, 6> {
let oe = SVector::<f64, 6>::new(
R_EARTH + 600e3, 0.01, 150.0, 30.0, 45.0, 0.0, );
convert_to_output_format(oe, angle_format)
}
pub(crate) fn fixture_orbit_equatorial(angle_format: AngleFormat) -> SVector<f64, 6> {
let oe = SVector::<f64, 6>::new(
R_EARTH + 550e3, 0.005, 5.0, 0.0, 0.0, 0.0, );
convert_to_output_format(oe, angle_format)
}
pub const FIXTURE_STATE_ALTITUDE: f64 = 650e3;
pub const FIXTURE_STATE_RADIUS: f64 = R_EARTH + FIXTURE_STATE_ALTITUDE;
pub(crate) fn fixture_circular_velocity() -> f64 {
perigee_velocity(FIXTURE_STATE_RADIUS, 0.0)
}
pub(crate) fn fixture_state_eci_plus_x() -> SVector<f64, 6> {
let v = fixture_circular_velocity();
SVector::<f64, 6>::new(FIXTURE_STATE_RADIUS, 0.0, 0.0, 0.0, v, 0.0)
}
pub(crate) fn fixture_state_eci_minus_x() -> SVector<f64, 6> {
let v = fixture_circular_velocity();
SVector::<f64, 6>::new(-FIXTURE_STATE_RADIUS, 0.0, 0.0, 0.0, -v, 0.0)
}
pub(crate) fn fixture_state_eci_plus_y() -> SVector<f64, 6> {
let v = fixture_circular_velocity();
SVector::<f64, 6>::new(0.0, FIXTURE_STATE_RADIUS, 0.0, -v, 0.0, 0.0)
}
pub(crate) fn fixture_state_eci_minus_y() -> SVector<f64, 6> {
let v = fixture_circular_velocity();
SVector::<f64, 6>::new(0.0, -FIXTURE_STATE_RADIUS, 0.0, v, 0.0, 0.0)
}
pub(crate) fn fixture_state_eci_north_vel_px() -> SVector<f64, 6> {
let v = fixture_circular_velocity();
SVector::<f64, 6>::new(0.0, 0.0, FIXTURE_STATE_RADIUS, v, 0.0, 0.0)
}
pub(crate) fn fixture_state_eci_north_vel_nx() -> SVector<f64, 6> {
let v = fixture_circular_velocity();
SVector::<f64, 6>::new(0.0, 0.0, FIXTURE_STATE_RADIUS, -v, 0.0, 0.0)
}
pub(crate) fn fixture_state_eci_north_vel_py() -> SVector<f64, 6> {
let v = fixture_circular_velocity();
SVector::<f64, 6>::new(0.0, 0.0, FIXTURE_STATE_RADIUS, 0.0, v, 0.0)
}
pub(crate) fn fixture_state_eci_north_vel_ny() -> SVector<f64, 6> {
let v = fixture_circular_velocity();
SVector::<f64, 6>::new(0.0, 0.0, FIXTURE_STATE_RADIUS, 0.0, -v, 0.0)
}
pub(crate) fn fixture_state_eci_south_vel_px() -> SVector<f64, 6> {
let v = fixture_circular_velocity();
SVector::<f64, 6>::new(0.0, 0.0, -FIXTURE_STATE_RADIUS, v, 0.0, 0.0)
}
pub(crate) fn fixture_state_eci_south_vel_nx() -> SVector<f64, 6> {
let v = fixture_circular_velocity();
SVector::<f64, 6>::new(0.0, 0.0, -FIXTURE_STATE_RADIUS, -v, 0.0, 0.0)
}
pub(crate) fn fixture_state_eci_south_vel_py() -> SVector<f64, 6> {
let v = fixture_circular_velocity();
SVector::<f64, 6>::new(0.0, 0.0, -FIXTURE_STATE_RADIUS, 0.0, v, 0.0)
}
pub(crate) fn fixture_state_eci_south_vel_ny() -> SVector<f64, 6> {
let v = fixture_circular_velocity();
SVector::<f64, 6>::new(0.0, 0.0, -FIXTURE_STATE_RADIUS, 0.0, -v, 0.0)
}
pub(crate) fn setup_global_test_eop() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let filepath = Path::new(&manifest_dir)
.join("test_assets")
.join("finals.all.iau2000.txt");
let eop_extrapolation = EOPExtrapolation::Hold;
let eop_interpolation = true;
let eop = FileEOPProvider::from_file(&filepath, eop_interpolation, eop_extrapolation).unwrap();
set_global_eop_provider(eop);
}
pub(crate) fn setup_global_test_eop_original_brahe() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let filepath = Path::new(&manifest_dir)
.join("test_assets")
.join("brahe_original_eop_file.txt");
let eop_extrapolation = EOPExtrapolation::Hold;
let eop_interpolation = true;
let eop = FileEOPProvider::from_file(&filepath, eop_interpolation, eop_extrapolation).unwrap();
set_global_eop_provider(eop);
}
pub(crate) fn setup_global_test_gravity_model() {
let gravity_model = GravityModel::from_model_type(&GravityModelType::EGM2008_360).unwrap();
set_global_gravity_model(gravity_model);
}
pub(crate) fn setup_global_test_almanac() {
static ALMANAC_INIT: OnceLock<()> = OnceLock::new();
ALMANAC_INIT.get_or_init(|| {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let test_asset_path = Path::new(&manifest_dir)
.join("test_assets")
.join("de440s.bsp");
if !test_asset_path.exists() {
return;
}
let cache_dir = get_naif_cache_dir().expect("Failed to get NAIF cache dir");
let cache_path = Path::new(&cache_dir).join("de440s.bsp");
fs::copy(&test_asset_path, &cache_path).expect("Failed to copy test asset to cache");
let cache_path_str = cache_path
.to_str()
.expect("Failed to convert path to string");
let spk = SPK::load(cache_path_str).expect("Failed to load DE440s test kernel");
let almanac = Almanac::from_spk(spk);
set_global_almanac(almanac);
});
}
pub(crate) fn setup_global_test_space_weather() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let filepath = Path::new(&manifest_dir)
.join("test_assets")
.join("sw19571001.txt");
let sw =
FileSpaceWeatherProvider::from_file(&filepath, SpaceWeatherExtrapolation::Hold).unwrap();
set_global_space_weather_provider(sw);
}
pub(crate) fn get_test_space_weather_filepath() -> std::path::PathBuf {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
Path::new(&manifest_dir)
.join("test_assets")
.join("sw19571001.txt")
}
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use crate::AngleFormat;
use crate::constants::physical::GM_EARTH;
use crate::orbits::keplerian::sun_synchronous_inclination;
use approx::assert_abs_diff_eq;
#[test]
fn test_fixture_orbit_leo_elements() {
let leo = fixture_orbit_leo(AngleFormat::Radians);
assert_abs_diff_eq!(leo[0], R_EARTH + 500e3, epsilon = 1.0);
assert_abs_diff_eq!(leo[1], 0.001, epsilon = 1e-6);
let expected_inc =
sun_synchronous_inclination(R_EARTH + 500e3, 0.001, AngleFormat::Radians);
assert_abs_diff_eq!(leo[2], expected_inc, epsilon = 1e-6);
let leo_deg = fixture_orbit_leo(AngleFormat::Degrees);
assert_abs_diff_eq!(leo_deg[0], R_EARTH + 500e3, epsilon = 1.0);
assert_abs_diff_eq!(leo_deg[3], 15.0, epsilon = 1e-6); assert_abs_diff_eq!(leo_deg[4], 30.0, epsilon = 1e-6); assert_abs_diff_eq!(leo_deg[5], 45.0, epsilon = 1e-6); }
#[test]
fn test_fixture_orbit_geo_elements() {
let geo = fixture_orbit_geo(AngleFormat::Radians);
assert_abs_diff_eq!(geo[0], geo_sma(), epsilon = 1.0);
assert!(geo[2] < 1e-6); }
#[test]
fn test_fixture_orbit_sso_elements() {
let sso = fixture_orbit_sso(AngleFormat::Radians);
assert_abs_diff_eq!(sso[0], R_EARTH + 700e3, epsilon = 1.0);
assert_abs_diff_eq!(sso[1], 0.001, epsilon = 1e-6);
let expected_inc =
sun_synchronous_inclination(R_EARTH + 700e3, 0.001, AngleFormat::Radians);
assert_abs_diff_eq!(sso[2], expected_inc, epsilon = 1e-6);
}
#[test]
fn test_fixture_orbit_molniya_elements() {
let molniya = fixture_orbit_molniya(AngleFormat::Radians);
assert_abs_diff_eq!(molniya[0], 26600e3, epsilon = 1.0);
assert_abs_diff_eq!(molniya[1], 0.74, epsilon = 1e-6);
assert_abs_diff_eq!(molniya[2], 63.4 * DEG2RAD, epsilon = 1e-6);
let molniya_deg = fixture_orbit_molniya(AngleFormat::Degrees);
assert_abs_diff_eq!(molniya_deg[2], 63.4, epsilon = 1e-6); assert_abs_diff_eq!(molniya_deg[3], 270.0, epsilon = 1e-6); }
#[test]
fn test_fixture_orbit_polar_elements() {
let polar = fixture_orbit_polar(AngleFormat::Radians);
assert_abs_diff_eq!(polar[0], R_EARTH + 800e3, epsilon = 1.0);
assert_abs_diff_eq!(polar[1], 0.001, epsilon = 1e-6);
assert_abs_diff_eq!(polar[2], 90.0 * DEG2RAD, epsilon = 1e-6);
let polar_deg = fixture_orbit_polar(AngleFormat::Degrees);
assert_abs_diff_eq!(polar_deg[0], R_EARTH + 800e3, epsilon = 1.0);
assert_abs_diff_eq!(polar_deg[2], 90.0, epsilon = 1e-6); assert_abs_diff_eq!(polar_deg[3], 0.0, epsilon = 1e-6); assert_abs_diff_eq!(polar_deg[4], 0.0, epsilon = 1e-6); assert_abs_diff_eq!(polar_deg[5], 0.0, epsilon = 1e-6); }
#[test]
fn test_fixture_orbit_elliptical_elements() {
let elliptical = fixture_orbit_elliptical(AngleFormat::Radians);
assert_abs_diff_eq!(elliptical[0], R_EARTH + 1000e3, epsilon = 1.0);
assert_abs_diff_eq!(elliptical[1], 0.3, epsilon = 1e-6); assert_abs_diff_eq!(elliptical[2], 45.0 * DEG2RAD, epsilon = 1e-6);
let elliptical_deg = fixture_orbit_elliptical(AngleFormat::Degrees);
assert_abs_diff_eq!(elliptical_deg[0], R_EARTH + 1000e3, epsilon = 1.0);
assert_abs_diff_eq!(elliptical_deg[2], 45.0, epsilon = 1e-6); assert_abs_diff_eq!(elliptical_deg[3], 60.0, epsilon = 1e-6); assert_abs_diff_eq!(elliptical_deg[4], 120.0, epsilon = 1e-6); assert_abs_diff_eq!(elliptical_deg[5], 0.0, epsilon = 1e-6); }
#[test]
fn test_fixture_orbit_retrograde_elements() {
let retrograde = fixture_orbit_retrograde(AngleFormat::Radians);
assert_abs_diff_eq!(retrograde[0], R_EARTH + 600e3, epsilon = 1.0);
assert_abs_diff_eq!(retrograde[1], 0.01, epsilon = 1e-6);
assert_abs_diff_eq!(retrograde[2], 150.0 * DEG2RAD, epsilon = 1e-6);
let retrograde_deg = fixture_orbit_retrograde(AngleFormat::Degrees);
assert_abs_diff_eq!(retrograde_deg[0], R_EARTH + 600e3, epsilon = 1.0);
assert_abs_diff_eq!(retrograde_deg[2], 150.0, epsilon = 1e-6); assert_abs_diff_eq!(retrograde_deg[3], 30.0, epsilon = 1e-6); assert_abs_diff_eq!(retrograde_deg[4], 45.0, epsilon = 1e-6); assert_abs_diff_eq!(retrograde_deg[5], 0.0, epsilon = 1e-6); }
#[test]
fn test_fixture_orbit_equatorial_elements() {
let equatorial = fixture_orbit_equatorial(AngleFormat::Radians);
assert_abs_diff_eq!(equatorial[0], R_EARTH + 550e3, epsilon = 1.0);
assert_abs_diff_eq!(equatorial[1], 0.005, epsilon = 1e-6);
assert_abs_diff_eq!(equatorial[2], 5.0 * DEG2RAD, epsilon = 1e-6);
let equatorial_deg = fixture_orbit_equatorial(AngleFormat::Degrees);
assert_abs_diff_eq!(equatorial_deg[0], R_EARTH + 550e3, epsilon = 1.0);
assert_abs_diff_eq!(equatorial_deg[2], 5.0, epsilon = 1e-6); assert_abs_diff_eq!(equatorial_deg[3], 0.0, epsilon = 1e-6); assert_abs_diff_eq!(equatorial_deg[4], 0.0, epsilon = 1e-6); assert_abs_diff_eq!(equatorial_deg[5], 0.0, epsilon = 1e-6); }
#[test]
fn test_fixture_circular_velocity_calculation() {
let v = fixture_circular_velocity();
assert!(
v > 7000.0 && v < 8000.0,
"Velocity {} out of expected range",
v
);
let expected = (GM_EARTH / FIXTURE_STATE_RADIUS).sqrt();
assert_abs_diff_eq!(v, expected, epsilon = 1e-6);
}
#[test]
fn test_eci_state_equatorial_plus_x() {
let state = fixture_state_eci_plus_x();
let v = fixture_circular_velocity();
assert_abs_diff_eq!(state[0], FIXTURE_STATE_RADIUS, epsilon = 1e-6);
assert_abs_diff_eq!(state[1], 0.0, epsilon = 1e-6);
assert_abs_diff_eq!(state[2], 0.0, epsilon = 1e-6);
assert_abs_diff_eq!(state[3], 0.0, epsilon = 1e-6);
assert_abs_diff_eq!(state[4], v, epsilon = 1e-6);
assert_abs_diff_eq!(state[5], 0.0, epsilon = 1e-6);
}
#[test]
fn test_eci_state_equatorial_minus_x() {
let state = fixture_state_eci_minus_x();
let v = fixture_circular_velocity();
assert_abs_diff_eq!(state[0], -FIXTURE_STATE_RADIUS, epsilon = 1e-6);
assert_abs_diff_eq!(state[1], 0.0, epsilon = 1e-6);
assert_abs_diff_eq!(state[2], 0.0, epsilon = 1e-6);
assert_abs_diff_eq!(state[3], 0.0, epsilon = 1e-6);
assert_abs_diff_eq!(state[4], -v, epsilon = 1e-6);
assert_abs_diff_eq!(state[5], 0.0, epsilon = 1e-6);
}
#[test]
fn test_eci_state_north_pole() {
let state = fixture_state_eci_north_vel_px();
let v = fixture_circular_velocity();
assert_abs_diff_eq!(state[0], 0.0, epsilon = 1e-6);
assert_abs_diff_eq!(state[1], 0.0, epsilon = 1e-6);
assert_abs_diff_eq!(state[2], FIXTURE_STATE_RADIUS, epsilon = 1e-6);
assert_abs_diff_eq!(state[3], v, epsilon = 1e-6);
assert_abs_diff_eq!(state[4], 0.0, epsilon = 1e-6);
assert_abs_diff_eq!(state[5], 0.0, epsilon = 1e-6);
}
#[test]
fn test_eci_state_south_pole() {
let state = fixture_state_eci_south_vel_ny();
let v = fixture_circular_velocity();
assert_abs_diff_eq!(state[0], 0.0, epsilon = 1e-6);
assert_abs_diff_eq!(state[1], 0.0, epsilon = 1e-6);
assert_abs_diff_eq!(state[2], -FIXTURE_STATE_RADIUS, epsilon = 1e-6);
assert_abs_diff_eq!(state[3], 0.0, epsilon = 1e-6);
assert_abs_diff_eq!(state[4], -v, epsilon = 1e-6);
assert_abs_diff_eq!(state[5], 0.0, epsilon = 1e-6);
}
#[test]
fn test_all_state_velocities_have_correct_magnitude() {
let expected_v = fixture_circular_velocity();
let states = vec![
fixture_state_eci_plus_x(),
fixture_state_eci_minus_x(),
fixture_state_eci_plus_y(),
fixture_state_eci_minus_y(),
fixture_state_eci_north_vel_px(),
fixture_state_eci_north_vel_nx(),
fixture_state_eci_north_vel_py(),
fixture_state_eci_north_vel_ny(),
fixture_state_eci_south_vel_px(),
fixture_state_eci_south_vel_nx(),
fixture_state_eci_south_vel_py(),
fixture_state_eci_south_vel_ny(),
];
for state in states {
let v_mag = (state[3].powi(2) + state[4].powi(2) + state[5].powi(2)).sqrt();
assert_abs_diff_eq!(v_mag, expected_v, epsilon = 1e-6);
}
}
#[test]
fn test_all_state_positions_have_correct_radius() {
let states = vec![
fixture_state_eci_plus_x(),
fixture_state_eci_minus_x(),
fixture_state_eci_plus_y(),
fixture_state_eci_minus_y(),
fixture_state_eci_north_vel_px(),
fixture_state_eci_north_vel_nx(),
fixture_state_eci_north_vel_py(),
fixture_state_eci_north_vel_ny(),
fixture_state_eci_south_vel_px(),
fixture_state_eci_south_vel_nx(),
fixture_state_eci_south_vel_py(),
fixture_state_eci_south_vel_ny(),
];
for state in states {
let r_mag = (state[0].powi(2) + state[1].powi(2) + state[2].powi(2)).sqrt();
assert_abs_diff_eq!(r_mag, FIXTURE_STATE_RADIUS, epsilon = 1e-6);
}
}
#[test]
fn test_setup_global_test_eop() {
setup_global_test_eop();
let result = get_global_ut1_utc(58849.0);
assert!(result.is_ok());
}
#[test]
fn test_setup_global_test_eop_original_brahe() {
setup_global_test_eop_original_brahe();
let result = get_global_ut1_utc(58849.0);
assert!(result.is_ok());
}
#[test]
fn test_setup_global_test_gravity_model() {
setup_global_test_gravity_model();
}
#[test]
fn test_setup_global_test_space_weather() {
use crate::space_weather::get_global_kp;
setup_global_test_space_weather();
let result = get_global_kp(60000.0);
assert!(result.is_ok());
let kp = result.unwrap();
assert!((0.0..=9.0).contains(&kp));
}
#[test]
fn test_get_test_space_weather_filepath() {
let filepath = get_test_space_weather_filepath();
assert!(filepath.exists());
assert!(filepath.ends_with("sw19571001.txt"));
}
}