use crate::cio_s06_data::{S06Term, S06_0, S06_1, S06_2, S06_3, S06_4, SP06};
use crate::frames::polar_motion_matrix;
use crate::nutation::nutation_iau2000a;
use crate::precession::{
fw_angles, fw_matrix, julian_centuries_tt, matmul, ry, rz, transpose, FwAngles, Mat3, Vec3,
};
use crate::timescales::JD_J2000;
const ARCSEC_TO_RAD: f64 = std::f64::consts::PI / (180.0 * 3600.0);
const TURNAS: f64 = 1_296_000.0;
const TAU: f64 = std::f64::consts::TAU;
fn fa_args(t: f64) -> [f64; 8] {
let asec = |c0: f64, c1: f64, c2: f64, c3: f64, c4: f64| {
((c0 + t * (c1 + t * (c2 + t * (c3 + t * c4)))) % TURNAS) * ARCSEC_TO_RAD
};
let rad = |c0: f64, c1: f64| (c0 + c1 * t) % TAU;
[
asec(
485868.249036,
1717915923.2178,
31.8792,
0.051635,
-0.00024470,
), asec(
1287104.793048,
129596581.0481,
-0.5532,
0.000136,
-0.00001149,
), asec(
335779.526232,
1739527262.8478,
-12.7512,
-0.001037,
0.00000417,
), asec(
1072260.703692,
1602961601.2090,
-6.3706,
0.006593,
-0.00003169,
), asec(450160.398036, -6962890.5431, 7.4722, 0.007702, -0.00005939), rad(3.176146697, 1021.3285546211), rad(1.753470314, 628.3075849991), (0.024381750 + 0.00000538691 * t) * t, ]
}
fn s_block(table: &[S06Term], fa: &[f64; 8]) -> f64 {
let mut acc = 0.0;
for &(nfa, s, c) in table.iter().rev() {
let a: f64 = nfa
.iter()
.zip(fa.iter())
.map(|(&m, &f)| f64::from(m) * f)
.sum();
acc += s * a.sin() + c * a.cos();
}
acc
}
pub fn cip_xy(jd_tt: f64) -> (f64, f64) {
let t = julian_centuries_tt(jd_tt);
let n = nutation_iau2000a(jd_tt);
let fj2 = -2.7774e-6 * t;
let dpsi = n.dpsi + n.dpsi * (0.4697e-6 + fj2);
let deps = n.deps + n.deps * fj2;
let fw = fw_angles(jd_tt);
let npb = fw_matrix(FwAngles {
gamma_bar: fw.gamma_bar,
phi_bar: fw.phi_bar,
psi_bar: fw.psi_bar + dpsi,
eps_a: fw.eps_a + deps,
});
(npb[2][0], npb[2][1])
}
pub fn cio_locator_s(jd_tt: f64, x: f64, y: f64) -> f64 {
let t = julian_centuries_tt(jd_tt);
let fa = fa_args(t);
let w0 = SP06[0] + s_block(&S06_0, &fa);
let w1 = SP06[1] + s_block(&S06_1, &fa);
let w2 = SP06[2] + s_block(&S06_2, &fa);
let w3 = SP06[3] + s_block(&S06_3, &fa);
let w4 = SP06[4] + s_block(&S06_4, &fa);
let w5 = SP06[5];
let series = w0 + (w1 + (w2 + (w3 + (w4 + w5 * t) * t) * t) * t) * t;
series * ARCSEC_TO_RAD - x * y / 2.0
}
pub fn xys_2006a(jd_tt: f64) -> (f64, f64, f64) {
let (x, y) = cip_xy(jd_tt);
let s = cio_locator_s(jd_tt, x, y);
(x, y, s)
}
pub fn celestial_to_intermediate(x: f64, y: f64, s: f64) -> Mat3 {
let r2 = x * x + y * y;
let e = if r2 > 0.0 { y.atan2(x) } else { 0.0 };
let d = (r2 / (1.0 - r2)).sqrt().atan();
matmul(&rz(-(e + s)), &matmul(&ry(d), &rz(e)))
}
pub fn gcrs_to_cirs_matrix(jd_tt: f64) -> Mat3 {
let (x, y, s) = xys_2006a(jd_tt);
celestial_to_intermediate(x, y, s)
}
fn anp(a: f64) -> f64 {
let w = a % TAU;
if w < 0.0 {
w + TAU
} else {
w
}
}
pub fn earth_rotation_angle(jd_ut1: f64) -> f64 {
let t = jd_ut1 - JD_J2000;
let f = jd_ut1 % 1.0;
anp(TAU * (f + 0.779_057_273_264_0 + 0.002_737_811_911_354_48 * t))
}
pub fn gcrs_to_itrs_matrix(jd_tt: f64, jd_ut1: f64, xp_rad: f64, yp_rad: f64) -> Mat3 {
let rc2i = gcrs_to_cirs_matrix(jd_tt);
let era = earth_rotation_angle(jd_ut1);
let pom = polar_motion_matrix(xp_rad, yp_rad, jd_tt);
matmul(&pom, &matmul(&rz(era), &rc2i))
}
pub fn gcrs_to_itrs(r_gcrs: Vec3, jd_tt: f64, jd_ut1: f64, xp_rad: f64, yp_rad: f64) -> Vec3 {
crate::precession::mat_vec(&gcrs_to_itrs_matrix(jd_tt, jd_ut1, xp_rad, yp_rad), r_gcrs)
}
pub fn itrs_to_gcrs(r_itrs: Vec3, jd_tt: f64, jd_ut1: f64, xp_rad: f64, yp_rad: f64) -> Vec3 {
let m = transpose(&gcrs_to_itrs_matrix(jd_tt, jd_ut1, xp_rad, yp_rad));
crate::precession::mat_vec(&m, r_itrs)
}
#[cfg(test)]
mod tests {
use super::*;
const JD_TT_REF: f64 = 2_400_000.5 + 53_736.0;
fn det(m: &Mat3) -> f64 {
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
}
fn is_orthonormal(m: &Mat3) -> bool {
let mt = transpose(m);
let p = matmul(m, &mt);
for (i, row) in p.iter().enumerate() {
for (j, &pij) in row.iter().enumerate() {
let expect = if i == j { 1.0 } else { 0.0 };
if (pij - expect).abs() > 1e-12 {
return false;
}
}
}
(det(m) - 1.0).abs() < 1e-12
}
#[test]
fn xys06a_matches_sofa_reference_vector() {
let (x, y, s) = xys_2006a(JD_TT_REF);
assert!(
(x - 0.579_130_848_283_529_3e-3).abs() < 1e-14,
"X = {x} (want 0.5791308482835292617e-3)"
);
assert!(
(y - 0.402_058_009_945_402_03e-4).abs() < 1e-15,
"Y = {y} (want 0.4020580099454020310e-4)"
);
assert!(
(s - (-1.220_032_294_164_58e-8)).abs() < 1e-18,
"s = {s} (want -0.1220032294164579896e-7)"
);
}
#[test]
fn c2ixys_matches_sofa_reference_matrix() {
let x = 0.579_130_848_670_601_1e-3;
let y = 0.402_057_981_673_296_1e-4;
let s = -0.122_004_084_847_227_2e-7;
let m = celestial_to_intermediate(x, y, s);
let want = [
[
0.999_999_832_303_715_7,
0.558_198_486_916_849_9e-9,
-0.579_130_849_161_128_2e-3,
],
[
-0.238_426_164_267_044_03e-7,
0.999_999_999_191_746_9,
-0.402_057_911_016_966_9e-4,
],
[
0.579_130_848_670_601_1e-3,
0.402_057_981_673_296_1e-4,
0.999_999_831_495_462_8,
],
];
for i in 0..3 {
for j in 0..3 {
assert!(
(m[i][j] - want[i][j]).abs() < 1e-12,
"rc2i[{i}][{j}] = {} (want {})",
m[i][j],
want[i][j]
);
}
}
}
#[test]
fn era00_matches_sofa_reference_value() {
let era = earth_rotation_angle(2_400_000.5 + 54_388.0);
assert!(
(era - 0.402_283_724_002_815_8).abs() < 1e-12,
"ERA = {era} (want 0.4022837240028158102)"
);
}
#[test]
fn gcrs_to_cirs_is_a_small_proper_rotation() {
let c = gcrs_to_cirs_matrix(JD_TT_REF);
assert!(is_orthonormal(&c), "GCRS→CIRS must be a proper rotation");
let trace = c[0][0] + c[1][1] + c[2][2];
let theta = (((trace - 1.0) / 2.0).clamp(-1.0, 1.0)).acos();
assert!(theta < 1e-3, "GCRS→CIRS angle = {theta} rad (want < ~1e-3)");
}
#[test]
fn gcrs_to_itrs_round_trips_and_is_a_rotation() {
let r = [7000.0e3, -1200.0e3, 4200.0e3];
let (jd_tt, jd_ut1) = (JD_TT_REF, JD_TT_REF - 0.000_8); let xp = crate::frames::arcsec(0.2);
let yp = crate::frames::arcsec(0.35);
let m = gcrs_to_itrs_matrix(jd_tt, jd_ut1, xp, yp);
assert!(is_orthonormal(&m), "GCRS→ITRS must be a proper rotation");
let back = itrs_to_gcrs(
gcrs_to_itrs(r, jd_tt, jd_ut1, xp, yp),
jd_tt,
jd_ut1,
xp,
yp,
);
for k in 0..3 {
assert!((back[k] - r[k]).abs() < 1e-6, "round-trip[{k}]");
}
}
#[test]
fn cio_chain_is_consistent_with_the_equinox_teme_chain() {
let (jd_tt, jd_ut1) = (JD_TT_REF, JD_TT_REF);
let r_gcrs = [6500.0e3, 2300.0e3, -1800.0e3];
let (r_teme, _) = crate::nutation::gcrs_to_teme(r_gcrs, [0.0; 3], jd_tt);
let r_itrf_equinox = crate::frames::teme_to_itrf(r_teme, jd_ut1, 0.0, 0.0, jd_tt);
let r_itrs_cio = gcrs_to_itrs(r_gcrs, jd_tt, jd_ut1, 0.0, 0.0);
let sep = ((r_itrf_equinox[0] - r_itrs_cio[0]).powi(2)
+ (r_itrf_equinox[1] - r_itrs_cio[1]).powi(2)
+ (r_itrf_equinox[2] - r_itrs_cio[2]).powi(2))
.sqrt();
assert!(
sep < 250.0,
"CIO vs equinox-TEME ITRS separation = {sep} m (expected ≈ 2·EE convention gap, < 250 m)"
);
let n = |v: [f64; 3]| (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
assert!((n(r_itrs_cio) - n(r_gcrs)).abs() < 1e-6);
}
}