use nalgebra::{Matrix3, Rotation3, UnitQuaternion, Vector3};
#[allow(unused_imports)]
use crate::math::F64Ext;
use super::cip::{cio_locator_s, cip_xy, gcrs_to_cirs_matrix, rotation_x, rotation_y, rotation_z};
use super::{Arcsec, Mas, Rad, Uas};
use crate::earth::eop::{NutationCorrections, PolarMotion, Ut1Offset};
use crate::epoch::{Epoch, Tt, Ut1, Utc};
use crate::frame::{Cirs, Gcrs, Itrs, Rotation, Tirs};
fn tio_locator_s_prime(tt_centuries: f64) -> Rad {
Uas::new(-47.0 * tt_centuries).to_radians()
}
fn matrix3_to_unit_quaternion(m: Matrix3<f64>) -> UnitQuaternion<f64> {
UnitQuaternion::from_rotation_matrix(&Rotation3::from_matrix_unchecked(m))
}
impl Rotation<Gcrs, Cirs> {
pub fn iau2006<P>(tt: &Epoch<Tt>, utc: &Epoch<Utc>, eop: &P) -> Self
where
P: NutationCorrections + ?Sized,
{
let t = tt.centuries_since_j2000();
let utc_mjd = utc.mjd();
let (x_model, y_model) = cip_xy(t);
let dx = Mas::new(eop.dx(utc_mjd)).to_radians();
let dy = Mas::new(eop.dy(utc_mjd)).to_radians();
let x_corrected = Rad::new(x_model.raw() + dx.raw());
let y_corrected = Rad::new(y_model.raw() + dy.raw());
let s = cio_locator_s(t, x_model, y_model);
let m = gcrs_to_cirs_matrix(x_corrected, y_corrected, s);
Self::from_raw(matrix3_to_unit_quaternion(m))
}
}
impl Rotation<Cirs, Tirs> {
pub fn from_era(ut1: &Epoch<Ut1>) -> Self {
let era = ut1.era();
let axis = nalgebra::Unit::new_normalize(Vector3::z());
Self::from_raw(UnitQuaternion::from_axis_angle(&axis, -era))
}
}
impl Rotation<Tirs, Itrs> {
pub fn polar_motion<P>(tt: &Epoch<Tt>, utc: &Epoch<Utc>, eop: &P) -> Self
where
P: PolarMotion + ?Sized,
{
let utc_mjd = utc.mjd();
let xp = Arcsec::new(eop.x_pole(utc_mjd)).to_radians();
let yp = Arcsec::new(eop.y_pole(utc_mjd)).to_radians();
let sp = tio_locator_s_prime(tt.centuries_since_j2000());
let m = rotation_x(-yp.raw()) * rotation_y(-xp.raw()) * rotation_z(sp.raw());
Self::from_raw(matrix3_to_unit_quaternion(m))
}
}
impl Rotation<Gcrs, Itrs> {
pub fn iau2006_full<P>(tt: &Epoch<Tt>, ut1: &Epoch<Ut1>, utc: &Epoch<Utc>, eop: &P) -> Self
where
P: NutationCorrections + PolarMotion + ?Sized,
{
let q = Rotation::<Gcrs, Cirs>::iau2006(tt, utc, eop);
let r = Rotation::<Cirs, Tirs>::from_era(ut1);
let w = Rotation::<Tirs, Itrs>::polar_motion(tt, utc, eop);
q.then(&r).then(&w)
}
pub fn iau2006_full_from_utc<P>(utc: &Epoch<Utc>, eop: &P) -> Self
where
P: Ut1Offset + NutationCorrections + PolarMotion + ?Sized,
{
let tt = utc.to_tt();
let ut1 = utc.to_ut1(eop);
Self::iau2006_full(&tt, &ut1, utc, eop)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::epoch::{Tai, Tdb};
use crate::frame::Vec3;
struct ZeroEop;
impl Ut1Offset for ZeroEop {
fn dut1(&self, _utc_mjd: f64) -> f64 {
0.0
}
}
impl PolarMotion for ZeroEop {
fn x_pole(&self, _utc_mjd: f64) -> f64 {
0.0
}
fn y_pole(&self, _utc_mjd: f64) -> f64 {
0.0
}
}
impl NutationCorrections for ZeroEop {
fn dx(&self, _utc_mjd: f64) -> f64 {
0.0
}
fn dy(&self, _utc_mjd: f64) -> f64 {
0.0
}
}
impl crate::earth::eop::LengthOfDay for ZeroEop {
fn lod(&self, _utc_mjd: f64) -> f64 {
0.0
}
}
fn sample_epochs() -> (Epoch<Tt>, Epoch<Ut1>, Epoch<Utc>) {
let utc = Epoch::<Utc>::from_gregorian(2024, 3, 20, 12, 0, 0.0);
let tt = utc.to_tt();
let ut1 = utc.to_ut1_naive();
(tt, ut1, utc)
}
#[test]
fn tio_locator_is_linear_minus_47_uas_per_century() {
assert_eq!(tio_locator_s_prime(0.0).raw(), 0.0);
let one_century = tio_locator_s_prime(1.0).raw();
let expected_rad = Uas::new(-47.0).to_radians().raw();
assert!(
(one_century - expected_rad).abs() < 1e-20,
"s'(1) = {one_century}, expected {expected_rad}"
);
let t = 0.314;
let two_t = tio_locator_s_prime(2.0 * t).raw();
let t_doubled = 2.0 * tio_locator_s_prime(t).raw();
assert!((two_t - t_doubled).abs() < 1e-30);
}
#[test]
fn from_era_uses_z_axis_with_negative_era() {
use crate::epoch::J2000_JD;
let ut1 = Epoch::<Ut1>::from_jd_ut1(J2000_JD);
let rot = Rotation::<Cirs, Tirs>::from_era(&ut1);
let era = ut1.era();
let v_in = Vec3::<Cirs>::new(1.0, 0.0, 0.0);
let v_out = rot.transform(&v_in);
let expected_x = era.cos();
let expected_y = -era.sin();
assert!(
(v_out.x() - expected_x).abs() < 1e-14,
"x = {:?}, expected {expected_x}",
v_out.x()
);
assert!(
(v_out.y() - expected_y).abs() < 1e-14,
"y = {:?}, expected {expected_y}",
v_out.y()
);
assert!(v_out.z().abs() < 1e-14);
}
#[test]
fn polar_motion_with_zero_xp_yp_reduces_to_z_rotation_by_sp() {
let (tt, _ut1, utc) = sample_epochs();
let rot = Rotation::<Tirs, Itrs>::polar_motion(&tt, &utc, &ZeroEop);
let v_in = Vec3::<Tirs>::new(1.0, 0.0, 0.0);
let v_out = rot.transform(&v_in);
assert!(v_out.z().abs() < 1e-20);
assert!((v_out.x() * v_out.x() + v_out.y() * v_out.y() - 1.0).abs() < 1e-14);
}
#[test]
fn iau2006_with_zero_corrections_matches_phase_3a4_matrix() {
use super::super::cip::gcrs_to_cirs_matrix_at;
let (tt, _ut1, utc) = sample_epochs();
let rot = Rotation::<Gcrs, Cirs>::iau2006(&tt, &utc, &ZeroEop);
let m = gcrs_to_cirs_matrix_at(tt.centuries_since_j2000());
let reference = UnitQuaternion::from_rotation_matrix(&Rotation3::from_matrix_unchecked(m));
let v_in = nalgebra::Vector3::new(1.0, 0.0, 0.0);
let v_from_rotation = rot.inner().transform_vector(&v_in);
let v_from_reference = reference.transform_vector(&v_in);
for i in 0..3 {
let delta = (v_from_rotation[i] - v_from_reference[i]).abs();
assert!(delta < 1e-14, "component {i} delta = {delta}");
}
}
#[test]
fn iau2006_full_composition_matches_explicit_chain() {
let (tt, ut1, utc) = sample_epochs();
let combined = Rotation::<Gcrs, Itrs>::iau2006_full(&tt, &ut1, &utc, &ZeroEop);
let q = Rotation::<Gcrs, Cirs>::iau2006(&tt, &utc, &ZeroEop);
let r = Rotation::<Cirs, Tirs>::from_era(&ut1);
let w = Rotation::<Tirs, Itrs>::polar_motion(&tt, &utc, &ZeroEop);
let explicit: Rotation<Gcrs, Itrs> = q.then(&r).then(&w);
let v_in = Vec3::<Gcrs>::new(1.0, 0.0, 0.0);
let a = combined.transform(&v_in);
let b = explicit.transform(&v_in);
for (i, (ai, bi)) in [(a.x(), b.x()), (a.y(), b.y()), (a.z(), b.z())]
.iter()
.enumerate()
.map(|(i, (a, b))| (i, (*a, *b)))
{
assert!(
(ai - bi).abs() < 1e-14,
"component {i}: combined={ai}, explicit={bi}"
);
}
}
#[test]
fn iau2006_full_from_utc_matches_iau2006_full() {
let utc = Epoch::<Utc>::from_gregorian(2024, 3, 20, 12, 0, 0.0);
let tt = utc.to_tt();
let ut1 = utc.to_ut1(&ZeroEop);
let from_utc = Rotation::<Gcrs, Itrs>::iau2006_full_from_utc(&utc, &ZeroEop);
let explicit = Rotation::<Gcrs, Itrs>::iau2006_full(&tt, &ut1, &utc, &ZeroEop);
let v_in = Vec3::<Gcrs>::new(0.0, 1.0, 0.0);
let a = from_utc.transform(&v_in);
let b = explicit.transform(&v_in);
for i in 0..3 {
let ai = [a.x(), a.y(), a.z()][i];
let bi = [b.x(), b.y(), b.z()][i];
assert!(
(ai - bi).abs() < 1e-14,
"component {i}: from_utc={ai}, explicit={bi}"
);
}
}
#[allow(dead_code)]
fn _unused_imports() {
let _: Option<Epoch<Tai>> = None;
let _: Option<Epoch<Tdb>> = None;
}
}