mod ierstable;
mod qcirs2gcrs;
use crate::{TimeLike, TimeScale};
use std::f64::consts::PI;
use crate::mathtypes::*;
use super::earth_orientation_params;
pub use qcirs2gcrs::qcirs2gcrs;
pub use qcirs2gcrs::qcirs2gcrs_dxdy;
pub fn gmst<T: TimeLike>(tm: &T) -> f64 {
let tut1: f64 = (tm.as_mjd_with_scale(TimeScale::UT1) - 51544.5) / 36525.0;
let mut gmst: f64 = tut1.mul_add(
tut1.mul_add(
tut1.mul_add(-6.2e-6, 0.093104),
876600.0f64.mul_add(3600.0, 8640184.812866),
),
67310.54841,
);
gmst = ((gmst % 86400.0) / 240.0).to_radians();
gmst
}
pub fn eqeq<T: TimeLike>(tm: &T) -> f64 {
let d: f64 = tm.as_mjd_with_scale(TimeScale::TT) - 51544.5;
let omega = PI / 180.0 * 0.052954f64.mul_add(-d, 125.04);
let l = 0.98565f64.mul_add(d, 280.47).to_radians();
let epsilon = 0.0000004f64.mul_add(-d, 23.4393).to_radians();
let d_psi = ((-0.000319f64).mul_add(f64::sin(omega), -(0.000024 * f64::sin(2.0 * l))) * 15.0)
.to_radians();
d_psi * f64::cos(epsilon)
}
pub fn gast<T: TimeLike>(tm: &T) -> f64 {
gmst(tm) + eqeq(tm)
}
pub fn earth_rotation_angle<T: TimeLike>(tm: &T) -> f64 {
let t = tm.as_jd_with_scale(TimeScale::UT1);
let f = t % 1.0;
2.0 * PI * (0.00273781191135448f64.mul_add(t - 2451545.0, 0.7790572732640 + f) % 1.0)
}
pub fn qitrf2tirs<T: TimeLike>(tm: &T) -> Quaternion {
const ASEC2RAD: f64 = PI / 180.0 / 3600.0;
let eop = earth_orientation_params::get(tm).unwrap_or([0.0; 6]);
let xp = eop[1] * ASEC2RAD;
let yp = eop[2] * ASEC2RAD;
let t_tt = (tm.as_mjd_with_scale(TimeScale::TT) - 51544.5) / 36525.0;
let sp = -47.0e-6 * ASEC2RAD * t_tt;
Quaternion::rotz(sp) * Quaternion::roty(-xp) * Quaternion::rotx(-yp)
}
pub fn qteme2itrf<T: TimeLike>(tm: &T) -> Quaternion {
qitrf2tirs(tm).conjugate() * Quaternion::rotz(-gmst(tm))
}
pub fn qteme2gcrf<T: TimeLike>(tm: &T) -> Quaternion {
qitrf2gcrf_approx(tm) * qteme2itrf(tm)
}
pub fn qmod2gcrf<T: TimeLike>(tm: &T) -> Quaternion {
const ASEC2RAD: f64 = PI / 180.0 / 3600.0;
let tt = (tm.as_mjd_with_scale(TimeScale::TT) - 51544.5) / 36525.0;
let zeta = tt.mul_add(
tt.mul_add(
tt.mul_add(
tt.mul_add(tt.mul_add(-0.0000003173, -0.000005971), 0.01801828),
0.2988499,
),
2306.083227,
),
2.650545,
);
let z = tt.mul_add(
tt.mul_add(
tt.mul_add(
tt.mul_add(tt.mul_add(-0.0000002904, -0.000028596), 0.01826837),
1.0927348,
),
2306.077181,
),
-2.650545,
);
let theta = tt
* tt.mul_add(
tt.mul_add(
tt.mul_add(tt.mul_add(-0.0000001274, -0.000007089), -0.04182264),
-0.42949342,
),
2004.191903,
);
Quaternion::rotz(-zeta * ASEC2RAD) * Quaternion::roty(theta * ASEC2RAD) * Quaternion::rotz(-z * ASEC2RAD)
}
pub fn qgcrf2itrf_approx<T: TimeLike>(tm: &T) -> Quaternion {
let qitrf2tod_approx: Quaternion = Quaternion::rotz(gast(tm));
(qmod2gcrf(tm) * qtod2mod_approx(tm) * qitrf2tod_approx).conjugate()
}
pub fn qitrf2gcrf_approx<T: TimeLike>(tm: &T) -> Quaternion {
qgcrf2itrf_approx(tm).conjugate()
}
pub fn qtod2mod_approx<T: TimeLike>(tm: &T) -> Quaternion {
let d = tm.as_mjd_with_scale(TimeScale::TT) - 51544.5;
let t = d / 36525.0;
const DEG2RAD: f64 = PI / 180.0;
let delta_psi = DEG2RAD
* (-0.0048f64).mul_add(
f64::sin(0.05295f64.mul_add(-d, 125.0) * DEG2RAD),
-(0.0004 * f64::sin(1.97129f64.mul_add(d, 200.9) * DEG2RAD)),
);
let delta_epsilon = DEG2RAD
* 0.0026f64.mul_add(
f64::cos(0.05295f64.mul_add(-d, 125.0) * DEG2RAD),
0.0002 * f64::cos(1.97129f64.mul_add(d, 200.9) * DEG2RAD),
);
let epsilon_a = DEG2RAD
* t.mul_add(
t.mul_add(
t.mul_add(
t.mul_add(
-5.76e-7 / 3600.0 + t * -4.34E-8 / 3600.0,
0.00200340 / 3600.0,
),
-0.0001831 / 3600.0,
),
-46.836769 / 3600.0,
),
23.0 + 26.0 / 60.0 + 21.406 / 3600.0,
);
let epsilon = epsilon_a + delta_epsilon;
Quaternion::rotx(epsilon_a) * Quaternion::rotz(-delta_psi) * Quaternion::rotx(-epsilon)
}
pub fn qitrf2gcrf<T: TimeLike>(tm: &T) -> Quaternion {
let eop = earth_orientation_params::get(tm).unwrap_or([0.0; 6]);
let w = {
const ASEC2RAD: f64 = PI / 180.0 / 3600.0;
let xp = eop[1] * ASEC2RAD;
let yp = eop[2] * ASEC2RAD;
let t_tt = (tm.as_mjd_with_scale(TimeScale::TT) - 51544.5) / 36525.0;
let sp = -47.0e-6 * ASEC2RAD * t_tt;
Quaternion::rotz(sp) * Quaternion::roty(-xp) * Quaternion::rotx(-yp)
};
let r = qtirs2cirs(tm);
let q = qcirs2gcrs_dxdy(tm, Some((eop[4], eop[5])));
q * r * w
}
pub fn qgcrf2itrf<T: TimeLike>(tm: &T) -> Quaternion {
qitrf2gcrf(tm).conjugate()
}
#[inline]
pub fn qtirs2cirs<T: TimeLike>(tm: &T) -> Quaternion {
Quaternion::rotz(earth_rotation_angle(tm))
}
pub fn rtn_to_gcrf(pos_gcrf: &Vector3, vel_gcrf: &Vector3) -> Matrix3 {
let r_hat = pos_gcrf.normalize();
let h = pos_gcrf.cross(vel_gcrf);
let h_hat = h.normalize();
let t_hat = h_hat.cross(&r_hat);
let mut dcm = Matrix3::zeros();
dcm.set_block(0, 0, &r_hat);
dcm.set_block(0, 1, &t_hat);
dcm.set_block(0, 2, &h_hat);
dcm
}
pub fn gcrf_to_rtn(pos_gcrf: &Vector3, vel_gcrf: &Vector3) -> Matrix3 {
rtn_to_gcrf(pos_gcrf, vel_gcrf).transpose()
}
#[inline]
pub fn ric_to_gcrf(pos_gcrf: &Vector3, vel_gcrf: &Vector3) -> Matrix3 {
rtn_to_gcrf(pos_gcrf, vel_gcrf)
}
#[inline]
pub fn gcrf_to_ric(pos_gcrf: &Vector3, vel_gcrf: &Vector3) -> Matrix3 {
gcrf_to_rtn(pos_gcrf, vel_gcrf)
}
pub fn ntw_to_gcrf(pos_gcrf: &Vector3, vel_gcrf: &Vector3) -> Matrix3 {
let t_hat = vel_gcrf.normalize();
let h = pos_gcrf.cross(vel_gcrf);
let w_hat = h.normalize();
let n_hat = t_hat.cross(&w_hat);
let mut dcm = Matrix3::zeros();
dcm.set_block(0, 0, &n_hat);
dcm.set_block(0, 1, &t_hat);
dcm.set_block(0, 2, &w_hat);
dcm
}
pub fn gcrf_to_ntw(pos_gcrf: &Vector3, vel_gcrf: &Vector3) -> Matrix3 {
ntw_to_gcrf(pos_gcrf, vel_gcrf).transpose()
}
pub fn lvlh_to_gcrf(pos_gcrf: &Vector3, vel_gcrf: &Vector3) -> Matrix3 {
let r_hat = pos_gcrf.normalize();
let h = pos_gcrf.cross(vel_gcrf);
let h_hat = h.normalize();
let z_hat = r_hat * -1.0;
let y_hat = h_hat * -1.0;
let x_hat = y_hat.cross(&z_hat);
let mut dcm = Matrix3::zeros();
dcm.set_block(0, 0, &x_hat);
dcm.set_block(0, 1, &y_hat);
dcm.set_block(0, 2, &z_hat);
dcm
}
pub fn gcrf_to_lvlh(pos_gcrf: &Vector3, vel_gcrf: &Vector3) -> Matrix3 {
lvlh_to_gcrf(pos_gcrf, vel_gcrf).transpose()
}
pub fn itrf_to_gcrf_state<T: TimeLike>(
pos_itrf: &Vector3,
vel_itrf: &Vector3,
time: &T,
) -> (Vector3, Vector3) {
let eop = crate::earth_orientation_params::get(time).unwrap_or([0.0; 6]);
let q_itrf_to_tirs = {
const ASEC2RAD: f64 = PI / 180.0 / 3600.0;
let xp = eop[1] * ASEC2RAD;
let yp = eop[2] * ASEC2RAD;
let t_tt = (time.as_mjd_with_scale(TimeScale::TT) - 51544.5) / 36525.0;
let sp = -47.0e-6 * ASEC2RAD * t_tt;
Quaternion::rotz(sp) * Quaternion::roty(-xp) * Quaternion::rotx(-yp)
};
let pos_tirs = q_itrf_to_tirs * *pos_itrf;
let vel_tirs = q_itrf_to_tirs * *vel_itrf;
let omega_tirs: Vector3 = numeris::vector![0.0, 0.0, crate::consts::OMEGA_EARTH];
let vel_tirs_swept = vel_tirs + omega_tirs.cross(&pos_tirs);
let q_tirs_to_gcrf =
qcirs2gcrs_dxdy(time, Some((eop[4], eop[5]))) * qtirs2cirs(time);
let pos_gcrf = q_tirs_to_gcrf * pos_tirs;
let vel_gcrf = q_tirs_to_gcrf * vel_tirs_swept;
(pos_gcrf, vel_gcrf)
}
pub fn itrf_to_gcrf_state_approx<T: TimeLike>(
pos_itrf: &Vector3,
vel_itrf: &Vector3,
time: &T,
) -> (Vector3, Vector3) {
let omega: Vector3 = numeris::vector![0.0, 0.0, crate::consts::OMEGA_EARTH];
let vel_swept = vel_itrf + omega.cross(pos_itrf);
let q = qitrf2gcrf_approx(time);
(q * *pos_itrf, q * vel_swept)
}
pub fn gcrf_to_itrf_state<T: TimeLike>(
pos_gcrf: &Vector3,
vel_gcrf: &Vector3,
time: &T,
) -> (Vector3, Vector3) {
let eop = crate::earth_orientation_params::get(time).unwrap_or([0.0; 6]);
let q_gcrf_to_tirs =
(qcirs2gcrs_dxdy(time, Some((eop[4], eop[5]))) * qtirs2cirs(time)).conjugate();
let pos_tirs = q_gcrf_to_tirs * *pos_gcrf;
let vel_tirs_swept = q_gcrf_to_tirs * *vel_gcrf;
let omega_tirs: Vector3 = numeris::vector![0.0, 0.0, crate::consts::OMEGA_EARTH];
let vel_tirs = vel_tirs_swept - omega_tirs.cross(&pos_tirs);
let q_tirs_to_itrf = {
const ASEC2RAD: f64 = PI / 180.0 / 3600.0;
let xp = eop[1] * ASEC2RAD;
let yp = eop[2] * ASEC2RAD;
let t_tt = (time.as_mjd_with_scale(TimeScale::TT) - 51544.5) / 36525.0;
let sp = -47.0e-6 * ASEC2RAD * t_tt;
(Quaternion::rotz(sp) * Quaternion::roty(-xp) * Quaternion::rotx(-yp)).conjugate()
};
let pos_itrf = q_tirs_to_itrf * pos_tirs;
let vel_itrf = q_tirs_to_itrf * vel_tirs;
(pos_itrf, vel_itrf)
}
pub fn gcrf_to_itrf_state_approx<T: TimeLike>(
pos_gcrf: &Vector3,
vel_gcrf: &Vector3,
time: &T,
) -> (Vector3, Vector3) {
let q = qgcrf2itrf_approx(time);
let pos_itrf = q * *pos_gcrf;
let vel_swept = q * *vel_gcrf;
let omega: Vector3 = numeris::vector![0.0, 0.0, crate::consts::OMEGA_EARTH];
let vel_itrf = vel_swept - omega.cross(&pos_itrf);
(pos_itrf, vel_itrf)
}
pub fn to_gcrf(
frame: crate::Frame,
pos_gcrf: &Vector3,
vel_gcrf: &Vector3,
) -> anyhow::Result<Matrix3> {
use crate::Frame;
match frame {
Frame::GCRF => Ok(Matrix3::eye()),
Frame::LVLH => Ok(lvlh_to_gcrf(pos_gcrf, vel_gcrf)),
Frame::RTN => Ok(rtn_to_gcrf(pos_gcrf, vel_gcrf)),
Frame::NTW => Ok(ntw_to_gcrf(pos_gcrf, vel_gcrf)),
Frame::ITRF
| Frame::TIRS
| Frame::CIRS
| Frame::TEME
| Frame::EME2000
| Frame::ICRF => anyhow::bail!(
"to_gcrf: frame {} is not a satellite-local orbital frame; use the \
time-based quaternion helpers (qitrf2gcrf, qteme2gcrf, etc.) instead",
frame
),
}
}
pub fn from_gcrf(
frame: crate::Frame,
pos_gcrf: &Vector3,
vel_gcrf: &Vector3,
) -> anyhow::Result<Matrix3> {
Ok(to_gcrf(frame, pos_gcrf, vel_gcrf)?.transpose())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Duration, Instant, TimeScale};
#[test]
fn test_gmst() {
let mut tm = Instant::from_datetime(1992, 8, 20, 12, 14, 0.0).unwrap();
let tdiff = tm.as_mjd_with_scale(TimeScale::UT1) - tm.as_mjd_with_scale(TimeScale::UTC);
tm -= Duration::from_days(tdiff);
let gmval = gmst(&tm).to_degrees();
let truth = -207.4212121875;
assert!(((gmval - truth) / truth).abs() < 1.0e-6)
}
#[test]
fn test_qitrf2gcrf_roundtrip() {
let tm = Instant::from_datetime(2020, 6, 15, 12, 0, 0.0).unwrap();
let v_itrf = numeris::vector![1000.0, 2000.0, 3000.0];
let v_gcrf = qitrf2gcrf(&tm) * v_itrf;
let v_back = qgcrf2itrf(&tm) * v_gcrf;
assert!((v_back - v_itrf).norm() < 1.0e-12 * v_itrf.norm());
}
#[test]
fn test_earth_rotation_angle() {
let tm = Instant::from_jd_utc(2451545.0);
let era = earth_rotation_angle(&tm).to_degrees().rem_euclid(360.0);
assert!((era - 280.46061837).abs() < 0.01);
}
#[test]
fn test_approx_vs_full() {
let tm = Instant::from_datetime(2010, 3, 15, 6, 0, 0.0).unwrap();
let v_itrf = numeris::vector![6378137.0, 0.0, 0.0];
let v_approx = qitrf2gcrf_approx(&tm) * v_itrf;
let v_full = qitrf2gcrf(&tm) * v_itrf;
let angle_rad = (v_approx.dot(&v_full) / (v_approx.norm() * v_full.norm())).acos();
let angle_arcsec = angle_rad.to_degrees() * 3600.0;
assert!(
angle_arcsec < 1.0,
"Approx vs full differ by {:.4} arcsec, expected < 1",
angle_arcsec
);
}
#[test]
fn test_qteme2gcrf() {
let tm = &Instant::from_datetime(2004, 4, 6, 7, 51, 28.386009).unwrap();
let pitrf = numeris::vector![-1033.4793830, 7901.2952754, 6380.3565958];
let pgcrf_via_itrf = qitrf2gcrf(tm) * pitrf;
let pteme = qteme2itrf(tm).conjugate() * pitrf;
let pgcrf_via_teme = qteme2gcrf(tm) * pteme;
assert!((pgcrf_via_itrf - pgcrf_via_teme).norm() < 0.1);
}
#[test]
fn test_gcrs2itrf() {
let tm = &Instant::from_datetime(2004, 4, 6, 7, 51, 28.386009).unwrap();
let pitrf = numeris::vector![-1033.4793830, 7901.2952754, 6380.3565958];
let t_tt = (tm.as_jd_with_scale(TimeScale::TT) - 2451545.0) / 36525.0;
assert!((t_tt - 0.0426236319).abs() < 1.0e-8);
let dut1 =
(tm.as_mjd_with_scale(TimeScale::UT1) - tm.as_mjd_with_scale(TimeScale::UTC)) * 86400.0;
assert!((dut1 + 0.4399619).abs() < 0.01);
let delta_at =
(tm.as_mjd_with_scale(TimeScale::TAI) - tm.as_mjd_with_scale(TimeScale::UTC)) * 86400.0;
assert!((delta_at - 32.0).abs() < 1.0e-7);
let ptirs = qitrf2tirs(tm) * pitrf;
assert!((ptirs[0] + 1033.4750312).abs() < 1.0e-4);
assert!((ptirs[1] - 7901.3055856).abs() < 1.0e-4);
assert!((ptirs[2] - 6380.3445327).abs() < 1.0e-4);
let era = earth_rotation_angle(tm);
assert!((era.to_degrees() - 312.7552829).abs() < 1.0e-5);
let pcirs = Quaternion::rotz(era) * ptirs;
assert!((pcirs[0] - 5100.0184047).abs() < 1e-3);
assert!((pcirs[1] - 6122.7863648).abs() < 1e-3);
assert!((pcirs[2] - 6380.3446237).abs() < 1e-3);
let pgcrf = qcirs2gcrs_dxdy(tm, None) * pcirs;
assert!((pgcrf[0] - 5102.508959).abs() < 1e-3);
assert!((pgcrf[1] - 6123.011403).abs() < 1e-3);
assert!((pgcrf[2] - 6378.136925).abs() < 1e-3);
}
#[test]
fn test_vallado_3_14_state_single_call() {
let tm = Instant::from_datetime(2004, 4, 6, 7, 51, 28.386009).unwrap();
let pos_itrf_km: Vector3 =
numeris::vector![-1033.4793830, 7901.2952754, 6380.3565958];
let vel_itrf_kms: Vector3 =
numeris::vector![-3.225636520, -2.872451450, 5.531924446];
let pos_itrf = pos_itrf_km * 1.0e3;
let vel_itrf = vel_itrf_kms * 1.0e3;
let (pos_gcrf, vel_gcrf) = itrf_to_gcrf_state(&pos_itrf, &vel_itrf, &tm);
let pos_gcrf_km = pos_gcrf / 1.0e3;
let vel_gcrf_kms = vel_gcrf / 1.0e3;
assert!((pos_gcrf_km[0] - 5102.508959).abs() < 1.0e-3);
assert!((pos_gcrf_km[1] - 6123.011403).abs() < 1.0e-3);
assert!((pos_gcrf_km[2] - 6378.136925).abs() < 1.0e-3);
assert!(
(vel_gcrf_kms[0] - (-4.7432196)).abs() < 1.0e-6,
"v_x = {}", vel_gcrf_kms[0]
);
assert!(
(vel_gcrf_kms[1] - 0.7905366).abs() < 1.0e-6,
"v_y = {}", vel_gcrf_kms[1]
);
assert!(
(vel_gcrf_kms[2] - 5.5337561).abs() < 1.0e-6,
"v_z = {}", vel_gcrf_kms[2]
);
let (pos_back, vel_back) = gcrf_to_itrf_state(&pos_gcrf, &vel_gcrf, &tm);
let pos_err = (pos_back - pos_itrf).norm();
let vel_err = (vel_back - vel_itrf).norm();
assert!(pos_err < 1.0e-6, "pos round-trip err = {} m", pos_err);
assert!(vel_err < 1.0e-9, "vel round-trip err = {} m/s", vel_err);
}
#[test]
fn test_itrf_gcrf_state_roundtrip() {
let t = crate::Instant::from_datetime(2024, 3, 15, 12, 34, 56.0).unwrap();
let pos_gcrf: Vector3 = numeris::vector![6.878e6, 1.23e5, -4.56e5];
let vel_gcrf: Vector3 = numeris::vector![-123.4, 7600.0, 89.0];
let (pos_itrf, vel_itrf) = gcrf_to_itrf_state(&pos_gcrf, &vel_gcrf, &t);
let (pos_back, vel_back) = itrf_to_gcrf_state(&pos_itrf, &vel_itrf, &t);
let pos_err = (pos_gcrf - pos_back).norm();
let vel_err = (vel_gcrf - vel_back).norm();
assert!(pos_err < 1.0e-6, "position round-trip error = {} m", pos_err);
assert!(vel_err < 1.0e-9, "velocity round-trip error = {} m/s", vel_err);
}
#[test]
fn test_itrf_gcrf_state_geo() {
let t = crate::Instant::from_datetime(2024, 6, 1, 0, 0, 0.0).unwrap();
let geo_r: f64 = 42_164.17e3; let pos_itrf: Vector3 = numeris::vector![geo_r, 0.0, 0.0];
let vel_itrf: Vector3 = numeris::vector![0.0, 0.0, 0.0];
let (pos_gcrf, vel_gcrf) = itrf_to_gcrf_state(&pos_itrf, &vel_itrf, &t);
assert!((pos_gcrf.norm() - geo_r).abs() < 1.0e-6);
let v_expected = crate::consts::OMEGA_EARTH * geo_r;
let v_mag = vel_gcrf.norm();
assert!(
(v_mag - v_expected).abs() < 1.0e-3,
"GEO sweep speed {} m/s, expected {} m/s",
v_mag,
v_expected
);
let dot = pos_gcrf.dot(&vel_gcrf);
assert!(dot.abs() / (pos_gcrf.norm() * vel_gcrf.norm()) < 1.0e-10);
}
#[test]
fn test_naive_itrf_rotation_is_wrong_by_470mps() {
let t = crate::Instant::from_datetime(2024, 1, 1, 0, 0, 0.0).unwrap();
let earth_r = crate::consts::EARTH_RADIUS + 500.0e3;
let pos_itrf: Vector3 = numeris::vector![earth_r, 0.0, 0.0];
let vel_itrf_zero: Vector3 = numeris::vector![0.0, 0.0, 0.0];
let (_pos_gcrf, vel_gcrf_correct) =
itrf_to_gcrf_state(&pos_itrf, &vel_itrf_zero, &t);
let q = qitrf2gcrf_approx(&t);
let vel_gcrf_naive = q * vel_itrf_zero;
assert!(vel_gcrf_naive.norm() < 1.0e-12, "naive zero stays zero");
let expected_sweep = crate::consts::OMEGA_EARTH * earth_r;
let diff = (vel_gcrf_correct - vel_gcrf_naive).norm();
let rel_err = (diff - expected_sweep).abs() / expected_sweep;
assert!(
rel_err < 1.0e-6,
"naive-vs-correct velocity gap = {} m/s, expected {} m/s",
diff,
expected_sweep
);
assert!(
(diff - 501.4).abs() < 1.0,
"expected ~501 m/s Earth-rotation sweep at 500km equatorial, got {}",
diff
);
}
#[test]
fn test_itrf_gcrf_state_approx_roundtrip() {
let t = crate::Instant::from_datetime(2024, 3, 15, 12, 34, 56.0).unwrap();
let pos_gcrf: Vector3 = numeris::vector![6.878e6, 1.23e5, -4.56e5];
let vel_gcrf: Vector3 = numeris::vector![-123.4, 7600.0, 89.0];
let (pos_itrf, vel_itrf) = gcrf_to_itrf_state_approx(&pos_gcrf, &vel_gcrf, &t);
let (pos_back, vel_back) = itrf_to_gcrf_state_approx(&pos_itrf, &vel_itrf, &t);
let pos_err = (pos_gcrf - pos_back).norm();
let vel_err = (vel_gcrf - vel_back).norm();
assert!(pos_err < 1.0e-6, "position round-trip error = {} m", pos_err);
assert!(vel_err < 1.0e-9, "velocity round-trip error = {} m/s", vel_err);
}
#[test]
fn test_itrf_gcrf_state_approx_vs_full() {
let t = crate::Instant::from_datetime(2024, 3, 15, 12, 34, 56.0).unwrap();
let pos_itrf: Vector3 = numeris::vector![-1.0334e6, 7.9013e6, 6.3804e6];
let vel_itrf: Vector3 = numeris::vector![-3225.6, -2872.5, 5531.9];
let (p_full, v_full) = itrf_to_gcrf_state(&pos_itrf, &vel_itrf, &t);
let (p_approx, v_approx) = itrf_to_gcrf_state_approx(&pos_itrf, &vel_itrf, &t);
let pos_diff = (p_full - p_approx).norm();
let vel_diff = (v_full - v_approx).norm();
assert!(pos_diff < 100.0, "approx vs full position diff = {} m", pos_diff);
assert!(vel_diff < 1.0, "approx vs full velocity diff = {} m/s", vel_diff);
}
}