use anise::astro::{AzElRange, Occultation};
use anise::constants::celestial_objects::{EARTH, VENUS};
use anise::constants::frames::{
EARTH_ITRF93, EARTH_J2000, IAU_EARTH_FRAME, IAU_MOON_FRAME, MOON_J2000, SUN_J2000, VENUS_J2000,
};
use anise::constants::orientations::ITRF93;
use anise::math::Vector3;
use anise::prelude::*;
const POSITION_EPSILON_KM: f64 = 2e-5;
const VELOCITY_EPSILON_KM_S: f64 = 5e-10;
use rstest::*;
#[fixture]
pub fn almanac() -> Almanac {
use std::path::PathBuf;
let manifest_dir =
PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap_or(".".to_string()));
Almanac::new(
&manifest_dir
.clone()
.join("../data/de440s.bsp")
.to_string_lossy(),
)
.unwrap()
.load(
&manifest_dir
.clone()
.join("../data/pck08.pca")
.to_string_lossy(),
)
.unwrap()
}
#[ignore = "Requires Rust SPICE -- must be executed serially"]
#[test]
fn de440s_transform_verif_venus2emb() {
let _ = pretty_env_logger::try_init();
let spk_path = "../data/de440s.bsp";
let bpc_path = "../data/earth_latest_high_prec.bpc";
let spk = SPK::load(spk_path).unwrap();
let bpc = BPC::load(bpc_path).unwrap();
spice::furnsh(spk_path);
spice::furnsh(bpc_path);
let almanac = Almanac::default().with_spk(spk).with_bpc(bpc);
let epoch = Epoch::from_gregorian_utc_at_midnight(2020, 2, 7);
let state = almanac
.transform(VENUS_J2000, EARTH_ITRF93, epoch, Aberration::NONE)
.unwrap();
let (spice_state, _) = spice::spkezr("VENUS", epoch.to_et_seconds(), "ITRF93", "NONE", "EARTH");
let pos_expct_km = Vector3::new(spice_state[0], spice_state[1], spice_state[2]);
let vel_expct_km_s = Vector3::new(spice_state[3], spice_state[4], spice_state[5]);
dbg!(pos_expct_km - state.radius_km);
dbg!(vel_expct_km_s - state.velocity_km_s);
assert!(
relative_eq!(state.radius_km, pos_expct_km, epsilon = POSITION_EPSILON_KM),
"pos = {}\nexp = {pos_expct_km}\nerr = {:e}",
state.radius_km,
pos_expct_km - state.radius_km
);
assert!(
relative_eq!(
state.velocity_km_s,
vel_expct_km_s,
epsilon = VELOCITY_EPSILON_KM_S
),
"vel = {}\nexp = {vel_expct_km_s}\nerr = {:e}",
state.velocity_km_s,
vel_expct_km_s - state.velocity_km_s
);
let state_rtn = almanac
.transform(EARTH_J2000, Frame::new(VENUS, ITRF93), epoch, None)
.unwrap();
println!("state = {state}");
println!("state_rtn = {state_rtn}");
let (spice_state, _) = spice::spkezr("EARTH", epoch.to_et_seconds(), "ITRF93", "NONE", "VENUS");
let pos_rtn_expct_km = Vector3::new(spice_state[0], spice_state[1], spice_state[2]);
let vel_rtn_expct_km_s = Vector3::new(spice_state[3], spice_state[4], spice_state[5]);
dbg!(pos_expct_km + pos_rtn_expct_km);
dbg!(vel_expct_km_s + vel_rtn_expct_km_s);
dbg!(pos_expct_km + state_rtn.radius_km);
dbg!(pos_rtn_expct_km - state_rtn.radius_km);
dbg!(vel_expct_km_s + state_rtn.velocity_km_s);
dbg!(vel_rtn_expct_km_s - state_rtn.velocity_km_s);
assert!(
relative_eq!(
state_rtn.radius_km,
pos_rtn_expct_km,
epsilon = POSITION_EPSILON_KM
),
"pos = {}\nexp = {pos_rtn_expct_km}\nerr = {:e}",
state_rtn.radius_km,
pos_rtn_expct_km - state_rtn.radius_km
);
assert!(
relative_eq!(
state_rtn.velocity_km_s,
vel_rtn_expct_km_s,
epsilon = VELOCITY_EPSILON_KM_S
),
"vel = {}\nexp = {vel_rtn_expct_km_s}\nerr = {:e}",
state_rtn.velocity_km_s,
vel_rtn_expct_km_s - state.velocity_km_s
);
assert!(
relative_eq!(
state_rtn.radius_km,
-state.radius_km,
epsilon = f64::EPSILON
),
"pos = {}\nexp = {}\nerr = {:e}",
state_rtn.radius_km,
-state.radius_km,
state_rtn.radius_km - state.radius_km
);
assert!(
relative_eq!(
state_rtn.velocity_km_s,
-state.velocity_km_s,
epsilon = f64::EPSILON
),
"vel = {}\nexp = {}\nerr = {:e}",
state.velocity_km_s,
-state_rtn.velocity_km_s,
state.velocity_km_s - state_rtn.velocity_km_s,
);
spice::unload(bpc_path);
spice::unload(spk_path);
let state_ezr = almanac.spk_ezr(EARTH, epoch, ITRF93, VENUS, None).unwrap();
assert_eq!(state_ezr, state_rtn);
}
#[rstest]
fn spice_verif_iau_moon(almanac: Almanac) {
let _ = pretty_env_logger::try_init();
let epoch = Epoch::from_str("2024-09-22T08:45:22 UTC").unwrap();
let orbit_moon_j2k = Orbit::new(
638.053603,
-1776.813629,
195.147575,
-0.017910,
-0.181449,
-1.584180,
epoch,
MOON_J2000,
);
let anise_iau_moon = almanac
.transform_to(orbit_moon_j2k, IAU_MOON_FRAME, None)
.unwrap();
let spice_iau_moon = Orbit::new(
8.52638439e+02,
1.47158517e+03,
8.42440758e+02,
6.17492780e-01,
4.46032072e-01,
-1.40193607e+00,
epoch,
IAU_MOON_FRAME,
);
println!("ANISE\n{anise_iau_moon}\nSPICE\n{spice_iau_moon}");
let rss_pos_km = anise_iau_moon.rss_radius_km(&spice_iau_moon).unwrap();
let rss_vel_km_s = anise_iau_moon.rss_velocity_km_s(&spice_iau_moon).unwrap();
dbg!(rss_pos_km, rss_vel_km_s);
assert!(rss_pos_km < 0.004);
assert!(rss_vel_km_s < 1e-5);
}
#[ignore = "Requires Rust SPICE -- must be executed serially"]
#[rstest]
fn validate_gh_283_multi_barycenter_and_los(almanac: Almanac) {
let spk_path = "../data/lro.bsp";
let almanac = almanac.load(spk_path).unwrap();
const LRO_ID: i32 = -85;
let lro_frame = Frame::from_ephem_j2000(LRO_ID);
spice::furnsh(spk_path);
spice::furnsh("../data/de440s.bsp");
spice::furnsh("../data/pck00008.tpc");
let gh346_epoch = Epoch::from_gregorian_utc_at_midnight(2023, 12, 15);
assert!(almanac
.common_ephemeris_path(lro_frame, SUN_J2000, gh346_epoch)
.is_ok());
assert!(almanac
.transform(lro_frame, SUN_J2000, gh346_epoch, None)
.is_ok());
let epoch = Epoch::from_gregorian_utc_at_midnight(2024, 1, 1);
let (node_count, path, common_node) = almanac
.common_ephemeris_path(lro_frame, SUN_J2000, epoch)
.unwrap();
assert_eq!(common_node, 0, "common node should be the SSB");
assert_eq!(node_count, 3, "node count should be Moon, EMB, SSB");
assert_eq!(
path,
[Some(301), Some(3), Some(0), None, None, None, None, None,],
"node count should be Moon, EMB, SSB"
);
let (spice_lro_state_raw, _) = spice::spkezr(
&LRO_ID.to_string(),
epoch.to_et_seconds(),
"J2000",
"NONE",
"SUN",
);
let spice_lro_state = Orbit::new(
spice_lro_state_raw[0],
spice_lro_state_raw[1],
spice_lro_state_raw[2],
spice_lro_state_raw[3],
spice_lro_state_raw[4],
spice_lro_state_raw[5],
epoch,
SUN_J2000,
);
let anise_lro_state = almanac
.transform(lro_frame, SUN_J2000, epoch, None)
.unwrap();
println!("== VALIDATION==\nANISE\n{anise_lro_state}\nSPICE\n{spice_lro_state}");
let rss_pos_km = anise_lro_state.rss_radius_km(&spice_lro_state).unwrap();
let rss_vel_km_s = anise_lro_state.rss_velocity_km_s(&spice_lro_state).unwrap();
dbg!(rss_pos_km, rss_vel_km_s);
assert!(rss_pos_km < 5e-7);
assert!(rss_vel_km_s < 1e-12);
let lro_state = almanac
.transform(lro_frame, MOON_J2000, epoch, None)
.unwrap();
let latitude_deg = 40.427_222;
let longitude_deg = 4.250_556;
let height_km = 0.834_939;
let iau_earth = almanac.frame_info(IAU_EARTH_FRAME).unwrap();
let mut obstructions = 0;
let mut no_obstructions = 0;
let mut printed_umbra = false;
let mut printed_visible = false;
let period = lro_state.period().unwrap();
println!("LRO period is {period}");
let front = "MOON";
let fshape = "ELLIPSOID";
let fframe = "IAU_MOON";
let back = "SUN";
let bshape = "ELLIPSOID";
let bframe = "IAU_SUN";
let abcorr = "NONE";
let obsrvr = "-85";
let mut prev_occult: Option<Occultation> = None;
let mut prev_aer: Option<AzElRange> = None;
let mut access_count = 0;
let mut access_start: Option<Epoch> = None;
let access_times = [
Unit::Minute * 4 + Unit::Second * 1,
Unit::Hour * 1 + Unit::Minute * 6 + 49 * Unit::Second,
];
for epoch in TimeSeries::inclusive(epoch, epoch + period, 1.seconds()) {
let tx_madrid =
Orbit::try_latlongalt(latitude_deg, longitude_deg, height_km, epoch, iau_earth)
.unwrap();
let rx_lro = almanac
.transform(lro_frame, MOON_J2000, epoch, None)
.unwrap();
let aer = almanac
.azimuth_elevation_range_sez(rx_lro, tx_madrid, Some(IAU_MOON_FRAME), None)
.unwrap();
if let Some(prev_aer) = prev_aer {
if prev_aer.is_obstructed() && !aer.is_obstructed() {
access_count += 1;
access_start = Some(aer.epoch);
} else if !prev_aer.is_obstructed() && aer.is_obstructed() {
if let Some(access_start) = access_start {
let access_end = aer.epoch;
let access_duration = (access_end - access_start).round(Unit::Second * 1);
println!("#{access_count}\t{access_start} - {access_end}\t{access_duration}");
assert_eq!(access_times[access_count], access_duration);
}
}
} else if !aer.is_obstructed() {
access_start = Some(aer.epoch);
}
prev_aer = Some(aer);
if aer.obstructed_by.is_some() {
obstructions += 1;
} else {
no_obstructions += 1;
}
let rx_lro = almanac
.transform(lro_frame, IAU_MOON_FRAME, epoch, None)
.unwrap();
let occult = almanac
.solar_eclipsing(IAU_MOON_FRAME, rx_lro, None)
.unwrap();
let spice_occult = spice::occult(
front,
fshape,
fframe,
back,
bshape,
bframe,
abcorr,
obsrvr,
epoch.to_et_seconds(),
);
match spice_occult {
0 => assert!(occult.is_visible(), "SPICE={spice_occult} BUT {occult}"),
1 => assert!(occult.is_partial(), "SPICE={spice_occult} BUT {occult}"),
3 => assert!(occult.is_obstructed(), "SPICE={spice_occult} BUT {occult}"),
_ => unreachable!(),
}
if occult.is_visible() {
if !printed_visible {
println!("{occult} @ {rx_lro:x}");
printed_visible = true;
}
if let Some(prev_occult) = &prev_occult {
if prev_occult.is_partial() {
assert_eq!(
epoch,
Epoch::from_gregorian_utc_hms(2024, 1, 1, 0, 46, 36),
"wrong post-penumbra state"
);
}
}
} else if occult.is_obstructed() {
if !printed_umbra {
println!("{occult} @ {rx_lro:x}");
assert_eq!(
epoch,
Epoch::from_gregorian_utc_hms(2024, 1, 1, 0, 1, 55),
"wrong first umbra state"
);
printed_umbra = true;
}
let sun = almanac
.transform(SUN_J2000, MOON_J2000, epoch, None)
.unwrap();
let obstructed = almanac
.line_of_sight_obstructed(rx_lro, sun, MOON_J2000, None)
.unwrap();
assert!(obstructed, "{occult} but not obstructed!");
} else {
if prev_occult.as_ref().unwrap().is_visible() {
assert_eq!(
epoch,
Epoch::from_gregorian_utc_hms(2024, 1, 1, 0, 1, 42),
"wrong first penumbra state"
);
}
println!("{occult}");
}
prev_occult = Some(occult)
}
if let Some(access_start) = access_start {
let access_end = epoch + period;
let access_duration = (access_end - access_start).round(Unit::Second * 1);
println!("#{access_count}\t{access_start} - {access_end}\t{access_duration}");
assert_eq!(access_times[access_count], access_duration);
}
assert_eq!(obstructions, 2762);
assert_eq!(no_obstructions, 4250);
}