use std::f64::consts::PI;
use super::{
gcrf_to_itrf_state, gcrf_to_itrf_state_approx, itrf_to_gcrf_state, itrf_to_gcrf_state_approx,
};
use super::{
qcirs2gcrs, qitrf2gcrf, qitrf2gcrf_approx, qitrf2tirs, qteme2itrf, qtirs2cirs, Error, Result,
};
use crate::frames::Frame;
use crate::mathtypes::{Quaternion, Vector3};
use crate::TimeLike;
const ASEC2RAD: f64 = PI / 180.0 / 3600.0;
const FRAME_BIAS_DALPHA0_AS: f64 = -0.014600;
const FRAME_BIAS_XI0_AS: f64 = -0.016617;
const FRAME_BIAS_ETA0_AS: f64 = -0.006819;
fn qeme2000_to_gcrf() -> Quaternion {
let dalpha0 = FRAME_BIAS_DALPHA0_AS * ASEC2RAD;
let xi0 = FRAME_BIAS_XI0_AS * ASEC2RAD;
let eta0 = FRAME_BIAS_ETA0_AS * ASEC2RAD;
Quaternion::rotz(dalpha0) * Quaternion::roty(xi0) * Quaternion::rotx(-eta0)
}
fn is_earth_rotating(f: Frame) -> bool {
match f {
Frame::ITRF | Frame::TIRS => true,
Frame::CIRS
| Frame::GCRF
| Frame::TEME
| Frame::EME2000
| Frame::ICRF
| Frame::LVLH
| Frame::RTN
| Frame::NTW => false,
}
}
fn is_orbit_dependent(f: Frame) -> bool {
match f {
Frame::LVLH | Frame::RTN | Frame::NTW => true,
Frame::ITRF
| Frame::TIRS
| Frame::CIRS
| Frame::GCRF
| Frame::TEME
| Frame::EME2000
| Frame::ICRF => false,
}
}
fn frame_order(f: Frame) -> u8 {
match f {
Frame::ITRF => 0,
Frame::TIRS => 1,
Frame::CIRS => 2,
Frame::GCRF => 3,
Frame::TEME => 4,
Frame::EME2000 => 5,
Frame::ICRF => 6,
Frame::LVLH => 7,
Frame::RTN => 8,
Frame::NTW => 9,
}
}
fn canonicalise(from: Frame, to: Frame) -> (Frame, Frame, bool) {
if frame_order(from) <= frame_order(to) {
(from, to, false)
} else {
(to, from, true)
}
}
pub fn rotation<T: TimeLike>(from: Frame, to: Frame, t: &T) -> Result<Quaternion> {
if from == to {
return Ok(Quaternion::identity());
}
let (a, b, reversed) = canonicalise(from, to);
let q = canonical_rotation(a, b, t)?;
Ok(if reversed { q.conjugate() } else { q })
}
pub fn rotation_approx<T: TimeLike>(from: Frame, to: Frame, t: &T) -> Result<Quaternion> {
if from == to {
return Ok(Quaternion::identity());
}
reject_for_approx(from)?;
reject_for_approx(to)?;
let (a, b, reversed) = canonicalise(from, to);
let q = canonical_rotation_approx(a, b, t)?;
Ok(if reversed { q.conjugate() } else { q })
}
pub fn transform_state<T: TimeLike>(
from: Frame,
to: Frame,
t: &T,
pos: &Vector3,
vel: &Vector3,
) -> Result<(Vector3, Vector3)> {
if from == to {
return Ok((*pos, *vel));
}
state_dispatch(from, to, t, pos, vel, false)
}
pub fn transform_state_approx<T: TimeLike>(
from: Frame,
to: Frame,
t: &T,
pos: &Vector3,
vel: &Vector3,
) -> Result<(Vector3, Vector3)> {
if from == to {
return Ok((*pos, *vel));
}
reject_for_approx(from)?;
reject_for_approx(to)?;
state_dispatch(from, to, t, pos, vel, true)
}
fn reject_for_approx(frame: Frame) -> Result<()> {
match frame {
Frame::TIRS | Frame::CIRS => Err(Error::ApproxNotSupportedForFrame { frame }),
_ => Ok(()),
}
}
fn canonical_rotation<T: TimeLike>(from: Frame, to: Frame, t: &T) -> Result<Quaternion> {
use Frame::*;
let q = match (from, to) {
(ITRF, TIRS) => qitrf2tirs(t),
(TIRS, CIRS) => qtirs2cirs(t),
(CIRS, GCRF) => qcirs2gcrs(t),
(ITRF, TEME) => qteme2itrf(t).conjugate(),
(GCRF, EME2000) => qeme2000_to_gcrf().conjugate(),
(GCRF, ICRF) => Quaternion::identity(),
(ITRF, GCRF) => qitrf2gcrf(t),
(ITRF, CIRS) => qtirs2cirs(t) * qitrf2tirs(t),
(TIRS, GCRF) => qcirs2gcrs(t) * qtirs2cirs(t),
(TIRS, TEME) => (qitrf2tirs(t) * qteme2itrf(t)).conjugate(),
(TIRS, ICRF) => qcirs2gcrs(t) * qtirs2cirs(t),
(CIRS, ICRF) => qcirs2gcrs(t),
(EME2000, ICRF) => qeme2000_to_gcrf(),
(CIRS, TEME) => (qtirs2cirs(t) * qitrf2tirs(t) * qteme2itrf(t)).conjugate(),
(ITRF, EME2000) => qeme2000_to_gcrf().conjugate() * qitrf2gcrf(t),
(ITRF, ICRF) => qitrf2gcrf(t),
(TIRS, EME2000) => qeme2000_to_gcrf().conjugate() * qcirs2gcrs(t) * qtirs2cirs(t),
(CIRS, EME2000) => qeme2000_to_gcrf().conjugate() * qcirs2gcrs(t),
(GCRF, TEME) => (qitrf2gcrf(t) * qteme2itrf(t)).conjugate(),
(TEME, EME2000) => qeme2000_to_gcrf().conjugate() * qitrf2gcrf(t) * qteme2itrf(t),
(TEME, ICRF) => qitrf2gcrf(t) * qteme2itrf(t),
(LVLH | RTN | NTW, _) | (_, LVLH | RTN | NTW) => {
return Err(Error::OrbitFrameRequiresState { from, to });
}
_ => unreachable!("non-canonical pair reached canonical_rotation: ({from}, {to})"),
};
Ok(q)
}
fn canonical_rotation_approx<T: TimeLike>(from: Frame, to: Frame, t: &T) -> Result<Quaternion> {
use Frame::*;
let q = match (from, to) {
(ITRF, GCRF) => qitrf2gcrf_approx(t),
(ITRF, TEME) => qteme2itrf(t).conjugate(),
(ITRF, EME2000) => qeme2000_to_gcrf().conjugate() * qitrf2gcrf_approx(t),
(ITRF, ICRF) => qitrf2gcrf_approx(t),
(GCRF, EME2000) => qeme2000_to_gcrf().conjugate(),
(GCRF, ICRF) => Quaternion::identity(),
(GCRF, TEME) => (qitrf2gcrf_approx(t) * qteme2itrf(t)).conjugate(),
(EME2000, ICRF) => qeme2000_to_gcrf(),
(TEME, EME2000) => qeme2000_to_gcrf().conjugate() * qitrf2gcrf_approx(t) * qteme2itrf(t),
(TEME, ICRF) => qitrf2gcrf_approx(t) * qteme2itrf(t),
(LVLH | RTN | NTW, _) | (_, LVLH | RTN | NTW) => {
return Err(Error::OrbitFrameRequiresState { from, to });
}
_ => unreachable!("non-canonical pair reached canonical_rotation_approx: ({from}, {to})"),
};
Ok(q)
}
fn state_dispatch<T: TimeLike>(
from: Frame,
to: Frame,
t: &T,
pos: &Vector3,
vel: &Vector3,
approx: bool,
) -> Result<(Vector3, Vector3)> {
use Frame::*;
if is_orbit_dependent(from) || is_orbit_dependent(to) {
return Err(Error::OrbitFrameRequiresState { from, to });
}
let is_rotating = is_earth_rotating;
if !is_rotating(from) && !is_rotating(to) {
let q = if approx {
rotation_approx(from, to, t)?
} else {
rotation(from, to, t)?
};
return Ok((q * *pos, q * *vel));
}
if is_rotating(from) && is_rotating(to) {
let q = rotation(from, to, t)?;
return Ok((q * *pos, q * *vel));
}
if is_rotating(from) {
let (p_itrf, v_itrf) = if from == ITRF {
(*pos, *vel)
} else {
let q = rotation(TIRS, ITRF, t)?;
(q * *pos, q * *vel)
};
let (p_gcrf, v_gcrf) = if approx {
itrf_to_gcrf_state_approx(&p_itrf, &v_itrf, t)
} else {
itrf_to_gcrf_state(&p_itrf, &v_itrf, t)
};
let q = if approx {
rotation_approx(GCRF, to, t)?
} else {
rotation(GCRF, to, t)?
};
return Ok((q * p_gcrf, q * v_gcrf));
}
let q = if approx {
rotation_approx(from, GCRF, t)?
} else {
rotation(from, GCRF, t)?
};
let p_gcrf = q * *pos;
let v_gcrf = q * *vel;
let (p_itrf, v_itrf) = if approx {
gcrf_to_itrf_state_approx(&p_gcrf, &v_gcrf, t)
} else {
gcrf_to_itrf_state(&p_gcrf, &v_gcrf, t)
};
if to == ITRF {
Ok((p_itrf, v_itrf))
} else {
let q_itrf_to_tirs = rotation(ITRF, TIRS, t)?;
Ok((q_itrf_to_tirs * p_itrf, q_itrf_to_tirs * v_itrf))
}
}
#[cfg(test)]
mod tests {
use super::super::qgcrf2itrf;
use super::*;
use crate::Instant;
fn t() -> Instant {
Instant::from_datetime(2026, 5, 22, 12, 0, 0.0).unwrap()
}
#[test]
fn identity_pairs() {
let tm = t();
for f in [
Frame::ITRF,
Frame::TIRS,
Frame::CIRS,
Frame::GCRF,
Frame::TEME,
Frame::EME2000,
Frame::ICRF,
] {
let q = rotation(f, f, &tm).unwrap();
assert!((q.w - 1.0).abs() < 1e-15, "{f}: w={}", q.w);
}
}
#[test]
fn matches_qitrf2gcrf() {
let tm = t();
let q_dispatch = rotation(Frame::ITRF, Frame::GCRF, &tm).unwrap();
let q_direct = qitrf2gcrf(&tm);
let v = numeris::vector![1000.0, 2000.0, 3000.0];
let v_dispatch = q_dispatch * v;
let v_direct = q_direct * v;
assert!(
(v_dispatch - v_direct).norm() < 1e-9,
"dispatch={v_dispatch:?} direct={v_direct:?}"
);
}
#[test]
fn matches_qgcrf2itrf() {
let tm = t();
let q_dispatch = rotation(Frame::GCRF, Frame::ITRF, &tm).unwrap();
let q_direct = qgcrf2itrf(&tm);
let v = numeris::vector![1000.0, 2000.0, 3000.0];
let v_dispatch = q_dispatch * v;
let v_direct = q_direct * v;
assert!((v_dispatch - v_direct).norm() < 1e-9);
}
#[test]
fn matches_qitrf2tirs_direct_path() {
let tm = t();
let q_dispatch = rotation(Frame::ITRF, Frame::TIRS, &tm).unwrap();
let q_direct = qitrf2tirs(&tm);
let v = numeris::vector![1000.0, 2000.0, 3000.0];
assert!((q_dispatch * v - q_direct * v).norm() < 1e-12);
}
#[test]
fn matches_qteme2itrf() {
let tm = t();
let q_dispatch = rotation(Frame::TEME, Frame::ITRF, &tm).unwrap();
let q_direct = qteme2itrf(&tm);
let v = numeris::vector![1000.0, 2000.0, 3000.0];
assert!((q_dispatch * v - q_direct * v).norm() < 1e-12);
}
#[test]
fn dispatch_teme_pairs_have_correct_direction() {
use super::super::qteme2gcrf;
let tm = t();
let v = numeris::vector![7000e3_f64, 1000e3, 2000e3];
let q_teme_to_gcrf_ref = qteme2gcrf(&tm);
let q_dispatch = rotation_approx(Frame::TEME, Frame::GCRF, &tm).unwrap();
let lhs = q_dispatch * v;
let rhs = q_teme_to_gcrf_ref * v;
assert!(
(lhs - rhs).norm() / v.norm() < 1e-12,
"rotation_approx(TEME,GCRF) direction mismatch: dispatch={lhs:?} ref={rhs:?}"
);
let q_dispatch = rotation_approx(Frame::GCRF, Frame::TEME, &tm).unwrap();
let lhs = q_dispatch * v;
let rhs = q_teme_to_gcrf_ref.conjugate() * v;
assert!(
(lhs - rhs).norm() / v.norm() < 1e-12,
"rotation_approx(GCRF,TEME) direction mismatch: dispatch={lhs:?} ref={rhs:?}"
);
let q_dispatch = rotation(Frame::TEME, Frame::GCRF, &tm).unwrap();
let lhs = q_dispatch * v;
let rhs = q_teme_to_gcrf_ref * v;
assert!(
(lhs - rhs).norm() < 100.0,
"rotation(TEME,GCRF) direction mismatch (>100 m): \
dispatch={lhs:?} approx_ref={rhs:?}"
);
let q_dispatch = rotation(Frame::GCRF, Frame::TEME, &tm).unwrap();
let lhs = q_dispatch * v;
let rhs = q_teme_to_gcrf_ref.conjugate() * v;
assert!(
(lhs - rhs).norm() < 100.0,
"rotation(GCRF,TEME) direction mismatch (>100 m): \
dispatch={lhs:?} approx_ref={rhs:?}"
);
let v_teme = v;
let lhs = rotation(Frame::TEME, Frame::TIRS, &tm).unwrap() * v_teme;
let rhs = qitrf2tirs(&tm) * (qteme2itrf(&tm) * v_teme);
assert!(
(lhs - rhs).norm() / v.norm() < 1e-12,
"rotation(TEME,TIRS) direction mismatch: dispatch={lhs:?} ref={rhs:?}"
);
let lhs = rotation(Frame::TEME, Frame::CIRS, &tm).unwrap() * v_teme;
let rhs = qtirs2cirs(&tm) * (qitrf2tirs(&tm) * (qteme2itrf(&tm) * v_teme));
assert!(
(lhs - rhs).norm() / v.norm() < 1e-12,
"rotation(TEME,CIRS) direction mismatch: dispatch={lhs:?} ref={rhs:?}"
);
}
#[test]
fn roundtrip_all_pairs() {
let tm = t();
let v = numeris::vector![6378.0, 2000.0, 3000.0];
let frames = [
Frame::ITRF,
Frame::TIRS,
Frame::CIRS,
Frame::GCRF,
Frame::TEME,
Frame::EME2000,
Frame::ICRF,
];
for &a in &frames {
for &b in &frames {
let q_ab = rotation(a, b, &tm).unwrap();
let q_ba = rotation(b, a, &tm).unwrap();
let v_round = q_ba * (q_ab * v);
let err = (v_round - v).norm() / v.norm();
assert!(err < 1e-12, "({a} → {b} → {a}) error {err}");
}
}
}
#[test]
fn approx_rejects_intermediate_frames() {
let tm = t();
for f in [Frame::TIRS, Frame::CIRS] {
let err = rotation_approx(f, Frame::GCRF, &tm).unwrap_err();
assert!(matches!(err, Error::ApproxNotSupportedForFrame { frame } if frame == f));
let err = rotation_approx(Frame::GCRF, f, &tm).unwrap_err();
assert!(matches!(err, Error::ApproxNotSupportedForFrame { frame } if frame == f));
}
}
#[test]
fn approx_matches_qitrf2gcrf_approx() {
let tm = t();
let q_dispatch = rotation_approx(Frame::ITRF, Frame::GCRF, &tm).unwrap();
let q_direct = qitrf2gcrf_approx(&tm);
let v = numeris::vector![1000.0, 2000.0, 3000.0];
assert!((q_dispatch * v - q_direct * v).norm() < 1e-9);
}
#[test]
fn orbit_frames_rejected() {
let tm = t();
for of in [Frame::LVLH, Frame::RTN, Frame::NTW] {
assert!(matches!(
rotation(of, Frame::GCRF, &tm),
Err(Error::OrbitFrameRequiresState { .. })
));
assert!(matches!(
rotation(Frame::GCRF, of, &tm),
Err(Error::OrbitFrameRequiresState { .. })
));
}
}
#[test]
fn icrf_eme2000_constant_bias() {
let t1 = Instant::from_datetime(2000, 1, 1, 0, 0, 0.0).unwrap();
let t2 = Instant::from_datetime(2026, 5, 22, 0, 0, 0.0).unwrap();
let q1 = rotation(Frame::ICRF, Frame::EME2000, &t1).unwrap();
let q2 = rotation(Frame::ICRF, Frame::EME2000, &t2).unwrap();
assert!((q1.w - q2.w).abs() < 1e-15);
}
#[test]
fn eme2000_bias_matches_iers_2010() {
let t = Instant::from_datetime(2000, 1, 1, 12, 0, 0.0).unwrap();
let q = rotation(Frame::EME2000, Frame::GCRF, &t).unwrap();
let e1 = numeris::vector![1.0_f64, 0.0, 0.0];
let e2 = numeris::vector![0.0_f64, 1.0, 0.0];
let e3 = numeris::vector![0.0_f64, 0.0, 1.0];
let c0 = q * e1;
let c1 = q * e2;
let c2 = q * e3;
assert!((c0[0] - 1.0).abs() < 1e-14);
assert!((c0[1] - (-7.07827974e-8)).abs() < 1e-15);
assert!((c0[2] - 8.05614894e-8).abs() < 1e-15);
assert!((c1[0] - 7.07827948e-8).abs() < 1e-15);
assert!((c1[1] - 1.0).abs() < 1e-14);
assert!((c1[2] - 3.30594449e-8).abs() < 1e-15);
assert!((c2[0] - (-8.05614917e-8)).abs() < 1e-15);
assert!((c2[1] - (-3.30594392e-8)).abs() < 1e-15);
assert!((c2[2] - 1.0).abs() < 1e-14);
}
#[test]
fn transform_state_itrf_to_gcrf_matches_direct() {
let tm = t();
let p_itrf = numeris::vector![6378137.0, 0.0, 0.0];
let v_itrf = numeris::vector![0.0, 0.0, 0.0];
let (p_dispatch, v_dispatch) =
transform_state(Frame::ITRF, Frame::GCRF, &tm, &p_itrf, &v_itrf).unwrap();
let (p_direct, v_direct) = itrf_to_gcrf_state(&p_itrf, &v_itrf, &tm);
assert!((p_dispatch - p_direct).norm() < 1e-9);
assert!((v_dispatch - v_direct).norm() < 1e-12);
}
#[test]
fn transform_state_roundtrip_itrf_gcrf() {
let tm = t();
let p = numeris::vector![6378137.0, 0.0, 0.0];
let v = numeris::vector![0.0, 7600.0, 0.0];
let (p2, v2) = transform_state(Frame::ITRF, Frame::GCRF, &tm, &p, &v).unwrap();
let (p3, v3) = transform_state(Frame::GCRF, Frame::ITRF, &tm, &p2, &v2).unwrap();
assert!((p3 - p).norm() / p.norm() < 1e-10);
assert!((v3 - v).norm() / v.norm() < 1e-10);
}
#[test]
fn transform_state_all_non_orbit_pairs_roundtrip() {
let tm = t();
let p = numeris::vector![7000000.0, 0.0, 0.0];
let v = numeris::vector![0.0, 7600.0, 0.0];
let frames = [
Frame::ITRF,
Frame::TIRS,
Frame::CIRS,
Frame::GCRF,
Frame::TEME,
Frame::EME2000,
Frame::ICRF,
];
for &a in &frames {
for &b in &frames {
let (pa, va) = transform_state(a, b, &tm, &p, &v).unwrap();
let (pr, vr) = transform_state(b, a, &tm, &pa, &va).unwrap();
let pos_err = (pr - p).norm() / p.norm();
let vel_err = (vr - v).norm() / v.norm();
assert!(pos_err < 1e-10, "({a}↔{b}) pos roundtrip err {pos_err}");
assert!(vel_err < 1e-10, "({a}↔{b}) vel roundtrip err {vel_err}");
}
}
}
#[test]
fn transform_state_inertial_pair_no_sweep() {
let tm = t();
let p = numeris::vector![7000000.0, 0.0, 0.0];
let v = numeris::vector![0.0, 7600.0, 0.0];
let (_, v_teme) = transform_state(Frame::GCRF, Frame::TEME, &tm, &p, &v).unwrap();
assert!(
(v_teme.norm() - v.norm()).abs() < 1e-9,
"inertial pair preserved |v|: |v|={}, |v_teme|={}",
v.norm(),
v_teme.norm()
);
}
#[test]
fn transform_state_tirs_via_itrf_chain() {
let tm = t();
let p_tirs = numeris::vector![7000000.0, 0.0, 0.0];
let v_tirs = numeris::vector![0.0, 0.0, 0.0];
let (p_a, v_a) = transform_state(Frame::TIRS, Frame::GCRF, &tm, &p_tirs, &v_tirs).unwrap();
let q_tirs_to_itrf = rotation(Frame::TIRS, Frame::ITRF, &tm).unwrap();
let p_itrf = q_tirs_to_itrf * p_tirs;
let v_itrf = q_tirs_to_itrf * v_tirs;
let (p_b, v_b) = itrf_to_gcrf_state(&p_itrf, &v_itrf, &tm);
assert!((p_a - p_b).norm() < 1e-9);
assert!((v_a - v_b).norm() < 1e-12);
}
#[test]
fn transform_state_itrf_tirs_no_sweep() {
let tm = t();
let p = numeris::vector![7000000.0, 0.0, 0.0];
let v = numeris::vector![0.0, 7600.0, 0.0];
let (_, v_tirs) = transform_state(Frame::ITRF, Frame::TIRS, &tm, &p, &v).unwrap();
assert!((v_tirs.norm() - v.norm()).abs() < 1e-9);
}
#[test]
fn transform_state_approx_rejects_intermediates() {
let tm = t();
let p = numeris::vector![7000000.0, 0.0, 0.0];
let v = numeris::vector![0.0, 7600.0, 0.0];
for f in [Frame::TIRS, Frame::CIRS] {
assert!(matches!(
transform_state_approx(f, Frame::GCRF, &tm, &p, &v),
Err(Error::ApproxNotSupportedForFrame { .. })
));
}
}
#[test]
fn transform_state_orbit_frames_rejected() {
let tm = t();
let p = numeris::vector![7000000.0, 0.0, 0.0];
let v = numeris::vector![0.0, 7600.0, 0.0];
for of in [Frame::LVLH, Frame::RTN, Frame::NTW] {
assert!(matches!(
transform_state(of, Frame::GCRF, &tm, &p, &v),
Err(Error::OrbitFrameRequiresState { .. })
));
}
}
}