use anise::{
constants::{
celestial_objects::EARTH,
frames::*,
orientations::{ECLIPJ2000, FK4, ITRF93, J2000},
},
math::{
cartesian::CartesianState,
rotation::{Quaternion, DCM},
Matrix3, Vector3,
},
naif::kpl::parser::convert_tpc,
prelude::{Almanac, Frame, BPC},
};
use hifitime::{Duration, Epoch, TimeScale, TimeSeries, TimeUnits};
use spice::cstr;
const MAX_ERR_DEG: f64 = 7.2e-6;
const DCM_EPSILON: f64 = 2e-9;
const IAU_MOON_DCM_EPSILON: f64 = 1e-5;
const IAU_MOON_MAX_ERR_DEG: f64 = 1e-3;
const POSITION_ERR_TOL_KM: f64 = 2e-5;
const VELOCITY_ERR_TOL_KM_S: f64 = 5e-7;
const RTN_POSITION_EPSILON_KM: f64 = 1e-10;
const RTN_VELOCITY_EPSILON_KM_S: f64 = 1e-10;
#[ignore = "Requires Rust SPICE -- must be executed serially"]
#[test]
fn validate_iau_rotation_to_parent() {
let pck = "../data/pck00008.tpc";
spice::furnsh(pck);
let planetary_data = convert_tpc(pck, "../data/gm_de431.tpc").unwrap();
let almanac = Almanac::default().with_planetary_data(planetary_data);
for frame in [
IAU_MERCURY_FRAME,
IAU_VENUS_FRAME,
IAU_EARTH_FRAME,
IAU_MOON_FRAME,
IAU_MARS_FRAME,
IAU_JUPITER_FRAME,
IAU_SATURN_FRAME,
IAU_URANUS_FRAME,
IAU_NEPTUNE_FRAME,
] {
for (num, epoch) in TimeSeries::inclusive(
Epoch::from_tdb_duration(Duration::ZERO),
Epoch::from_tdb_duration(0.2.centuries()),
1.days(),
)
.enumerate()
{
let dcm = almanac
.rotate(frame.with_orient(J2000), frame, epoch)
.unwrap();
let mut rot_data: [[f64; 6]; 6] = [[0.0; 6]; 6];
unsafe {
spice::c::sxform_c(
cstr!("J2000"),
cstr!(format!("{frame:o}")),
epoch.to_et_seconds(),
rot_data.as_mut_ptr(),
);
}
assert!(
[J2000, FK4, 4, 5, 6].contains(&dcm.from),
"unexpected DCM from frame {}",
dcm.from
);
assert_eq!(dcm.to, frame.orientation_id);
let spice_mat = Matrix3::new(
rot_data[0][0],
rot_data[0][1],
rot_data[0][2],
rot_data[1][0],
rot_data[1][1],
rot_data[1][2],
rot_data[2][0],
rot_data[2][1],
rot_data[2][2],
);
let rot_mat_dt = Some(Matrix3::new(
rot_data[3][0],
rot_data[3][1],
rot_data[3][2],
rot_data[4][0],
rot_data[4][1],
rot_data[4][2],
rot_data[5][0],
rot_data[5][1],
rot_data[5][2],
));
let spice_dcm = DCM {
rot_mat: spice_mat,
from: dcm.from,
to: dcm.to,
rot_mat_dt,
};
if epoch == Epoch::from_tdb_duration(0.2.centuries()) {
println!("ANISE: {dcm}{}", dcm.rot_mat_dt.unwrap());
println!("SPICE: {spice_dcm}{}", spice_dcm.rot_mat_dt.unwrap());
println!("DCM error\n{:e}", dcm.rot_mat - spice_dcm.rot_mat);
println!(
"derivative error\n{:e}",
dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
);
}
let q_anise = Quaternion::from(dcm);
let q_spice = Quaternion::from(spice_dcm);
let (anise_uvec, anise_angle) = q_anise.uvec_angle_rad();
let (spice_uvec, spice_angle) = q_spice.uvec_angle_rad();
let uvec_angle_deg_err = anise_uvec.dot(&spice_uvec).acos().to_degrees();
let deg_err = (anise_angle - spice_angle).to_degrees();
let angular_err = if frame == IAU_MOON_FRAME {
IAU_MOON_MAX_ERR_DEG
} else {
MAX_ERR_DEG
};
assert!(
uvec_angle_deg_err.abs() < angular_err || uvec_angle_deg_err.is_nan(),
"#{num} @ {epoch} unit vector angle error for {frame}: {uvec_angle_deg_err:e} deg"
);
assert!(
deg_err.abs() < angular_err,
"#{num} @ {epoch} rotation error for {frame}: {deg_err:e} deg"
);
let dcm_err = if frame == IAU_MOON_FRAME {
IAU_MOON_DCM_EPSILON
} else {
DCM_EPSILON
};
assert!(
(dcm.rot_mat - spice_mat).norm() < dcm_err,
"#{num} {epoch}\ngot: {}want:{spice_mat}err = {:.3e}: {:.3e}",
dcm.rot_mat,
(dcm.rot_mat - spice_mat).norm(),
dcm.rot_mat - spice_mat
);
assert!(
(dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm() < DCM_EPSILON,
"#{num} {epoch}\ngot: {}want:{}err = {:.3e}: {:.3e}",
dcm.rot_mat_dt.unwrap(),
spice_dcm.rot_mat_dt.unwrap(),
(dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm(),
dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
);
let dcm_t = almanac
.rotate(frame, frame.with_orient(J2000), epoch)
.unwrap();
assert_eq!(dcm.transpose(), dcm_t);
}
}
}
#[ignore = "Requires Rust SPICE -- must be executed serially"]
#[test]
fn validate_bpc_rotation_to_parent() {
let pck = "../data/earth_latest_high_prec.bpc";
spice::furnsh(pck);
let bpc = BPC::load(pck).unwrap();
let almanac = Almanac::from_bpc(bpc);
for (num, epoch) in TimeSeries::inclusive(
Epoch::from_tdb_duration(0.11.centuries()),
Epoch::from_tdb_duration(0.2.centuries()),
1.days(),
)
.enumerate()
{
let dcm = almanac.rotation_to_parent(EARTH_ITRF93, epoch).unwrap();
if num == 0 {
println!("{dcm}");
}
let mut rot_data: [[f64; 6]; 6] = [[0.0; 6]; 6];
unsafe {
spice::c::sxform_c(
cstr!("ECLIPJ2000"),
cstr!("ITRF93"),
epoch.to_et_seconds(),
rot_data.as_mut_ptr(),
);
}
let rot_mat = Matrix3::new(
rot_data[0][0],
rot_data[0][1],
rot_data[0][2],
rot_data[1][0],
rot_data[1][1],
rot_data[1][2],
rot_data[2][0],
rot_data[2][1],
rot_data[2][2],
);
let rot_mat_dt = Some(Matrix3::new(
rot_data[3][0],
rot_data[3][1],
rot_data[3][2],
rot_data[4][0],
rot_data[4][1],
rot_data[4][2],
rot_data[5][0],
rot_data[5][1],
rot_data[5][2],
));
let spice_dcm = DCM {
rot_mat,
from: dcm.from,
to: dcm.to,
rot_mat_dt,
};
if num == 0 {
println!("ANISE: {dcm}{}", dcm.rot_mat_dt.unwrap());
println!("SPICE: {spice_dcm}{}", spice_dcm.rot_mat_dt.unwrap());
println!("DCM error\n{:e}", dcm.rot_mat - spice_dcm.rot_mat);
println!(
"derivative error\n{:e}",
dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
);
}
let q_anise = Quaternion::from(dcm);
let q_spice = Quaternion::from(spice_dcm);
let (anise_uvec, anise_angle) = q_anise.uvec_angle_rad();
let (spice_uvec, spice_angle) = q_spice.uvec_angle_rad();
let uvec_angle_deg_err = anise_uvec.dot(&spice_uvec).acos().to_degrees();
let deg_err = (anise_angle - spice_angle).to_degrees();
assert!(
uvec_angle_deg_err.abs() < MAX_ERR_DEG || uvec_angle_deg_err.is_nan(),
"#{num} @ {epoch} unit vector angle error: {uvec_angle_deg_err:e} deg"
);
assert!(
deg_err.abs() < MAX_ERR_DEG,
"#{num} @ {epoch} rotation error: {deg_err:e} deg"
);
assert!(
(dcm.rot_mat - rot_mat).norm() < DCM_EPSILON,
"#{num} {epoch}\ngot: {}want:{rot_mat}err = {:.3e}: {:.3e}",
dcm.rot_mat,
(dcm.rot_mat - rot_mat).norm(),
dcm.rot_mat - rot_mat
);
assert!(
(dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm() < 1e-13,
"#{num} {epoch}\ngot: {}want:{}err = {:.3e}: {:.3e}",
dcm.rot_mat_dt.unwrap(),
spice_dcm.rot_mat_dt.unwrap(),
(dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm(),
dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
);
assert_eq!(dcm.from, ECLIPJ2000);
assert_eq!(dcm.to, ITRF93);
}
}
#[ignore = "Requires Rust SPICE -- must be executed serially"]
#[test]
fn validate_j2000_ecliptic() {
let almanac = Almanac::default();
for (num, epoch) in TimeSeries::inclusive(
Epoch::from_tdb_duration(0.11.centuries()),
Epoch::from_tdb_duration(0.2.centuries()),
100.days(),
)
.enumerate()
{
let dcm = almanac.rotation_to_parent(EARTH_ECLIPJ2000, epoch).unwrap();
let mut rot_data: [[f64; 6]; 6] = [[0.0; 6]; 6];
unsafe {
spice::c::sxform_c(
cstr!("J2000"),
cstr!("ECLIPJ2000"),
epoch.to_et_seconds(),
rot_data.as_mut_ptr(),
);
}
assert_eq!(dcm.from, J2000);
assert_eq!(dcm.to, ECLIPJ2000);
let rot_mat = Matrix3::new(
rot_data[0][0],
rot_data[0][1],
rot_data[0][2],
rot_data[1][0],
rot_data[1][1],
rot_data[1][2],
rot_data[2][0],
rot_data[2][1],
rot_data[2][2],
);
let rot_mat_dt = Matrix3::new(
rot_data[3][0],
rot_data[3][1],
rot_data[3][2],
rot_data[4][0],
rot_data[4][1],
rot_data[4][2],
rot_data[5][0],
rot_data[5][1],
rot_data[5][2],
);
let spice_dcm = DCM {
rot_mat,
from: dcm.from,
to: dcm.to,
rot_mat_dt: if rot_mat_dt.norm() == 0.0 {
None
} else {
Some(rot_mat_dt)
},
};
assert!(
(dcm.rot_mat - rot_mat).norm() < f64::EPSILON,
"#{num} {epoch}\ngot: {}want:{rot_mat}err = {:.3e}: {:.3e}",
dcm.rot_mat,
(dcm.rot_mat - rot_mat).norm(),
dcm.rot_mat - rot_mat
);
assert_eq!(
dcm.rot_mat_dt, spice_dcm.rot_mat_dt,
"expected both derivatives to be unuset"
);
}
}
#[ignore = "Requires Rust SPICE -- must be executed serially"]
#[test]
fn validate_bpc_rotations() {
let pck = "../data/earth_latest_high_prec.bpc";
spice::furnsh(pck);
let bpc = BPC::load(pck).unwrap();
let almanac = Almanac::from_bpc(bpc);
let frame = Frame::new(EARTH, ITRF93);
let mut actual_max_uvec_err_deg = 0.0;
let mut actual_max_err_deg = 0.0;
let start = Epoch::from_gregorian_utc_at_midnight(2000, 1, 2);
let end = Epoch::from_gregorian_at_midnight(2023, 1, 1, TimeScale::ET);
for (num, epoch) in TimeSeries::inclusive(start, end, 1.days()).enumerate() {
let dcm = almanac.rotate(EARTH_ITRF93, EME2000, epoch).unwrap();
let mut rot_data: [[f64; 6]; 6] = [[0.0; 6]; 6];
unsafe {
spice::c::sxform_c(
cstr!("ITRF93"),
cstr!("J2000"),
epoch.to_et_seconds(),
rot_data.as_mut_ptr(),
);
}
let rot_mat = Matrix3::new(
rot_data[0][0],
rot_data[0][1],
rot_data[0][2],
rot_data[1][0],
rot_data[1][1],
rot_data[1][2],
rot_data[2][0],
rot_data[2][1],
rot_data[2][2],
);
let rot_mat_dt = Some(Matrix3::new(
rot_data[3][0],
rot_data[3][1],
rot_data[3][2],
rot_data[4][0],
rot_data[4][1],
rot_data[4][2],
rot_data[5][0],
rot_data[5][1],
rot_data[5][2],
));
let spice_dcm = DCM {
rot_mat,
from: ITRF93,
to: J2000,
rot_mat_dt,
};
if end - epoch < 1.days() {
println!("{epoch}");
println!("ANISE: {dcm}{}", dcm.rot_mat_dt.unwrap());
println!("SPICE: {spice_dcm}{}", spice_dcm.rot_mat_dt.unwrap());
println!("DCM error\n{:e}", dcm.rot_mat - spice_dcm.rot_mat);
println!(
"derivative error\n{:e}",
dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
);
}
let q_anise = Quaternion::from(dcm);
let q_spice = Quaternion::from(spice_dcm);
let (anise_uvec, anise_angle) = q_anise.uvec_angle_rad();
let (spice_uvec, spice_angle) = q_spice.uvec_angle_rad();
let uvec_angle_deg_err = anise_uvec.dot(&spice_uvec).acos().to_degrees();
let deg_err = (anise_angle - spice_angle).to_degrees();
assert!(
uvec_angle_deg_err.abs() < MAX_ERR_DEG || uvec_angle_deg_err.is_nan(),
"#{num} @ {epoch} unit vector angle error for {frame}: {uvec_angle_deg_err:e} deg"
);
if uvec_angle_deg_err.abs() > actual_max_uvec_err_deg {
actual_max_uvec_err_deg = uvec_angle_deg_err.abs();
}
assert!(
deg_err.abs() < MAX_ERR_DEG,
"#{num} @ {epoch} rotation error for {frame}: {deg_err:e} deg"
);
if deg_err.abs() > actual_max_err_deg {
actual_max_err_deg = deg_err.abs();
}
assert!(
(dcm.rot_mat - rot_mat).norm() < DCM_EPSILON,
"#{num} {epoch}\ngot: {}want:{rot_mat}err = {:.3e}: {:.3e}",
dcm.rot_mat,
(dcm.rot_mat - rot_mat).norm(),
dcm.rot_mat - rot_mat
);
assert!(
(dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm() < 1e-13,
"#{num} {epoch}\ngot: {}want:{}err = {:.3e}: {:.3e}",
dcm.rot_mat_dt.unwrap(),
spice_dcm.rot_mat_dt.unwrap(),
(dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm(),
dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
);
}
println!("actualized max error in rotation angle = {actual_max_err_deg:.3e} deg");
println!("actualized max error in rotation direction = {actual_max_uvec_err_deg:.3e} deg");
}
#[ignore = "Requires Rust SPICE -- must be executed serially"]
#[test]
fn validate_bpc_to_iau_rotations() {
let pck = "../data/pck00008.tpc";
let bpc = "../data/earth_latest_high_prec.bpc";
spice::furnsh(bpc);
spice::furnsh(pck);
let planetary_data = convert_tpc(pck, "../data/gm_de431.tpc").unwrap();
let almanac = Almanac::default()
.with_planetary_data(planetary_data)
.with_bpc(BPC::load(bpc).unwrap());
println!("{almanac}");
let mut actual_max_uvec_err_deg = 0.0;
let mut actual_max_err_deg = 0.0;
let mut actual_pos_err_km = 0.0;
let mut actual_vel_err_km_s = 0.0;
let start = Epoch::from_tdb_duration(0.11.centuries());
let end = Epoch::from_tdb_duration(0.20.centuries());
for frame in [
IAU_MERCURY_FRAME,
IAU_VENUS_FRAME,
IAU_EARTH_FRAME,
IAU_MARS_FRAME,
IAU_JUPITER_FRAME,
IAU_SATURN_FRAME,
] {
for (num, epoch) in TimeSeries::inclusive(start, end, 27.days()).enumerate() {
let dcm = almanac.rotate(EARTH_ITRF93, frame, epoch).unwrap();
let mut rot_data: [[f64; 6]; 6] = [[0.0; 6]; 6];
let spice_name = format!("{frame:o}");
unsafe {
spice::c::sxform_c(
cstr!("ITRF93"),
cstr!(spice_name),
epoch.to_et_seconds(),
rot_data.as_mut_ptr(),
);
}
let rot_mat = Matrix3::new(
rot_data[0][0],
rot_data[0][1],
rot_data[0][2],
rot_data[1][0],
rot_data[1][1],
rot_data[1][2],
rot_data[2][0],
rot_data[2][1],
rot_data[2][2],
);
let rot_mat_dt = Some(Matrix3::new(
rot_data[3][0],
rot_data[3][1],
rot_data[3][2],
rot_data[4][0],
rot_data[4][1],
rot_data[4][2],
rot_data[5][0],
rot_data[5][1],
rot_data[5][2],
));
let spice_dcm = DCM {
rot_mat,
from: ITRF93,
to: frame.orientation_id,
rot_mat_dt,
};
if num == 0 {
println!("ANISE: {dcm}");
println!("SPICE: {spice_dcm}");
println!("DCM error\n{:e}", dcm.rot_mat - spice_dcm.rot_mat);
println!(
"derivative error\n{:e}",
dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
);
}
assert_eq!(dcm.from, EARTH_ITRF93.orientation_id);
assert_eq!(dcm.to, frame.orientation_id);
let q_anise = Quaternion::from(dcm);
let q_spice = Quaternion::from(spice_dcm);
let (anise_uvec, anise_angle) = q_anise.uvec_angle_rad();
let (spice_uvec, spice_angle) = q_spice.uvec_angle_rad();
let uvec_angle_deg_err = anise_uvec.dot(&spice_uvec).acos().to_degrees();
let deg_err = (anise_angle - spice_angle).to_degrees();
assert!(
uvec_angle_deg_err.abs() < MAX_ERR_DEG || uvec_angle_deg_err.is_nan(),
"#{num} @ {epoch} unit vector angle error for {frame}: {uvec_angle_deg_err:e} deg"
);
if uvec_angle_deg_err.abs() > actual_max_uvec_err_deg {
actual_max_uvec_err_deg = uvec_angle_deg_err.abs();
}
assert!(
deg_err.abs() < MAX_ERR_DEG,
"#{num} @ {epoch} rotation error for {frame}: {deg_err:e} deg"
);
if deg_err.abs() > actual_max_err_deg {
actual_max_err_deg = deg_err.abs();
}
assert!(
(dcm.rot_mat - rot_mat).norm() < DCM_EPSILON,
"#{num} {epoch}\ngot: {}want:{rot_mat}err = {:.3e}: {:.3e}",
dcm.rot_mat,
(dcm.rot_mat - rot_mat).norm(),
dcm.rot_mat - rot_mat
);
assert!(
(dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm()
< DCM_EPSILON * 0.1,
"#{num} {epoch}\ngot: {}want:{}err = {:.3e}: {:.3e}",
dcm.rot_mat_dt.unwrap(),
spice_dcm.rot_mat_dt.unwrap(),
(dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm(),
dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()
);
let state = CartesianState {
radius_km: Vector3::new(1234.0, 5678.9, 1234.0),
velocity_km_s: Vector3::new(1.2340, 5.6789, 1.2340),
epoch,
frame: EARTH_ITRF93,
};
let spice_out = (spice_dcm * state).unwrap();
let anise_out = (dcm * state).unwrap();
assert_eq!(spice_out.frame, anise_out.frame);
let pos_err_km = (spice_out.radius_km - anise_out.radius_km).norm();
assert!(
pos_err_km < POSITION_ERR_TOL_KM,
"#{num} {epoch}: pos error is {pos_err_km:.3e} km/s"
);
let vel_err_km_s = (spice_out.velocity_km_s - anise_out.velocity_km_s).norm();
assert!(
vel_err_km_s < VELOCITY_ERR_TOL_KM_S,
"#{num} {epoch}: vel error is {vel_err_km_s:.3e} km/s"
);
if pos_err_km > actual_pos_err_km {
actual_pos_err_km = pos_err_km;
}
if vel_err_km_s > actual_vel_err_km_s {
actual_vel_err_km_s = vel_err_km_s;
}
let dcm_t = almanac.rotate(frame, EARTH_ITRF93, epoch).unwrap();
let mut rot_data: [[f64; 6]; 6] = [[0.0; 6]; 6];
unsafe {
spice::c::sxform_c(
cstr!(format!("{frame:o}")),
cstr!("ITRF93"),
epoch.to_et_seconds(),
rot_data.as_mut_ptr(),
);
}
let rot_mat = Matrix3::new(
rot_data[0][0],
rot_data[0][1],
rot_data[0][2],
rot_data[1][0],
rot_data[1][1],
rot_data[1][2],
rot_data[2][0],
rot_data[2][1],
rot_data[2][2],
);
let rot_mat_dt = Some(Matrix3::new(
rot_data[3][0],
rot_data[3][1],
rot_data[3][2],
rot_data[4][0],
rot_data[4][1],
rot_data[4][2],
rot_data[5][0],
rot_data[5][1],
rot_data[5][2],
));
let spice_dcm_t = DCM {
rot_mat,
from: dcm_t.from,
to: dcm_t.to,
rot_mat_dt,
};
let spice_rtn = (spice_dcm_t * spice_out).unwrap();
let anise_rtn = (dcm_t * anise_out).unwrap();
assert_eq!(spice_rtn.frame, anise_rtn.frame);
assert!((spice_rtn.radius_km - state.radius_km).norm() < RTN_POSITION_EPSILON_KM);
assert!(
(spice_rtn.velocity_km_s - state.velocity_km_s).norm() < RTN_VELOCITY_EPSILON_KM_S
);
assert!((anise_rtn.radius_km - state.radius_km).norm() < RTN_POSITION_EPSILON_KM);
assert!(
(anise_rtn.velocity_km_s - state.velocity_km_s).norm() < RTN_VELOCITY_EPSILON_KM_S
);
}
}
println!("actualized max error in rotation angle = {actual_max_err_deg:.3e} deg");
println!("actualized max error in rotation direction = {actual_max_uvec_err_deg:.3e} deg");
println!("actualized max error in position = {actual_pos_err_km:.6e} km");
println!("actualized max error in velocity = {actual_vel_err_km_s:.6e} km/s");
}