use crate::precession::{
fw_angles, mat_vec, matmul, precession_matrix, rx, rz, transpose, 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 DAYS_PER_CENTURY: f64 = 36_525.0;
const U2R: f64 = ARCSEC_TO_RAD / 1e7;
const MAS_TO_RAD: f64 = ARCSEC_TO_RAD / 1e3;
const DPPLAN: f64 = -0.135 * MAS_TO_RAD;
const DEPLAN: f64 = 0.388 * MAS_TO_RAD;
type LsTerm = (i8, i8, i8, i8, i8, f64, f64, f64, f64, f64, f64);
#[rustfmt::skip]
const LS_TERMS: [LsTerm; 77] = [
( 0, 0, 0, 0, 1, -172064161.0, -174666.0, 33386.0, 92052331.0, 9086.0, 15377.0),
( 0, 0, 2,-2, 2, -13170906.0, -1675.0, -13696.0, 5730336.0, -3015.0, -4587.0),
( 0, 0, 2, 0, 2, -2276413.0, -234.0, 2796.0, 978459.0, -485.0, 1374.0),
( 0, 0, 0, 0, 2, 2074554.0, 207.0, -698.0, -897492.0, 470.0, -291.0),
( 0, 1, 0, 0, 0, 1475877.0, -3633.0, 11817.0, 73871.0, -184.0, -1924.0),
( 0, 1, 2,-2, 2, -516821.0, 1226.0, -524.0, 224386.0, -677.0, -174.0),
( 1, 0, 0, 0, 0, 711159.0, 73.0, -872.0, -6750.0, 0.0, 358.0),
( 0, 0, 2, 0, 1, -387298.0, -367.0, 380.0, 200728.0, 18.0, 318.0),
( 1, 0, 2, 0, 2, -301461.0, -36.0, 816.0, 129025.0, -63.0, 367.0),
( 0,-1, 2,-2, 2, 215829.0, -494.0, 111.0, -95929.0, 299.0, 132.0),
( 0, 0, 2,-2, 1, 128227.0, 137.0, 181.0, -68982.0, -9.0, 39.0),
(-1, 0, 2, 0, 2, 123457.0, 11.0, 19.0, -53311.0, 32.0, -4.0),
(-1, 0, 0, 2, 0, 156994.0, 10.0, -168.0, -1235.0, 0.0, 82.0),
( 1, 0, 0, 0, 1, 63110.0, 63.0, 27.0, -33228.0, 0.0, -9.0),
(-1, 0, 0, 0, 1, -57976.0, -63.0, -189.0, 31429.0, 0.0, -75.0),
(-1, 0, 2, 2, 2, -59641.0, -11.0, 149.0, 25543.0, -11.0, 66.0),
( 1, 0, 2, 0, 1, -51613.0, -42.0, 129.0, 26366.0, 0.0, 78.0),
(-2, 0, 2, 0, 1, 45893.0, 50.0, 31.0, -24236.0, -10.0, 20.0),
( 0, 0, 0, 2, 0, 63384.0, 11.0, -150.0, -1220.0, 0.0, 29.0),
( 0, 0, 2, 2, 2, -38571.0, -1.0, 158.0, 16452.0, -11.0, 68.0),
( 0,-2, 2,-2, 2, 32481.0, 0.0, 0.0, -13870.0, 0.0, 0.0),
(-2, 0, 0, 2, 0, -47722.0, 0.0, -18.0, 477.0, 0.0, -25.0),
( 2, 0, 2, 0, 2, -31046.0, -1.0, 131.0, 13238.0, -11.0, 59.0),
( 1, 0, 2,-2, 2, 28593.0, 0.0, -1.0, -12338.0, 10.0, -3.0),
(-1, 0, 2, 0, 1, 20441.0, 21.0, 10.0, -10758.0, 0.0, -3.0),
( 2, 0, 0, 0, 0, 29243.0, 0.0, -74.0, -609.0, 0.0, 13.0),
( 0, 0, 2, 0, 0, 25887.0, 0.0, -66.0, -550.0, 0.0, 11.0),
( 0, 1, 0, 0, 1, -14053.0, -25.0, 79.0, 8551.0, -2.0, -45.0),
(-1, 0, 0, 2, 1, 15164.0, 10.0, 11.0, -8001.0, 0.0, -1.0),
( 0, 2, 2,-2, 2, -15794.0, 72.0, -16.0, 6850.0, -42.0, -5.0),
( 0, 0,-2, 2, 0, 21783.0, 0.0, 13.0, -167.0, 0.0, 13.0),
( 1, 0, 0,-2, 1, -12873.0, -10.0, -37.0, 6953.0, 0.0, -14.0),
( 0,-1, 0, 0, 1, -12654.0, 11.0, 63.0, 6415.0, 0.0, 26.0),
(-1, 0, 2, 2, 1, -10204.0, 0.0, 25.0, 5222.0, 0.0, 15.0),
( 0, 2, 0, 0, 0, 16707.0, -85.0, -10.0, 168.0, -1.0, 10.0),
( 1, 0, 2, 2, 2, -7691.0, 0.0, 44.0, 3268.0, 0.0, 19.0),
(-2, 0, 2, 0, 0, -11024.0, 0.0, -14.0, 104.0, 0.0, 2.0),
( 0, 1, 2, 0, 2, 7566.0, -21.0, -11.0, -3250.0, 0.0, -5.0),
( 0, 0, 2, 2, 1, -6637.0, -11.0, 25.0, 3353.0, 0.0, 14.0),
( 0,-1, 2, 0, 2, -7141.0, 21.0, 8.0, 3070.0, 0.0, 4.0),
( 0, 0, 0, 2, 1, -6302.0, -11.0, 2.0, 3272.0, 0.0, 4.0),
( 1, 0, 2,-2, 1, 5800.0, 10.0, 2.0, -3045.0, 0.0, -1.0),
( 2, 0, 2,-2, 2, 6443.0, 0.0, -7.0, -2768.0, 0.0, -4.0),
(-2, 0, 0, 2, 1, -5774.0, -11.0, -15.0, 3041.0, 0.0, -5.0),
( 2, 0, 2, 0, 1, -5350.0, 0.0, 21.0, 2695.0, 0.0, 12.0),
( 0,-1, 2,-2, 1, -4752.0, -11.0, -3.0, 2719.0, 0.0, -3.0),
( 0, 0, 0,-2, 1, -4940.0, -11.0, -21.0, 2720.0, 0.0, -9.0),
(-1,-1, 0, 2, 0, 7350.0, 0.0, -8.0, -51.0, 0.0, 4.0),
( 2, 0, 0,-2, 1, 4065.0, 0.0, 6.0, -2206.0, 0.0, 1.0),
( 1, 0, 0, 2, 0, 6579.0, 0.0, -24.0, -199.0, 0.0, 2.0),
( 0, 1, 2,-2, 1, 3579.0, 0.0, 5.0, -1900.0, 0.0, 1.0),
( 1,-1, 0, 0, 0, 4725.0, 0.0, -6.0, -41.0, 0.0, 3.0),
(-2, 0, 2, 0, 2, -3075.0, 0.0, -2.0, 1313.0, 0.0, -1.0),
( 3, 0, 2, 0, 2, -2904.0, 0.0, 15.0, 1233.0, 0.0, 7.0),
( 0,-1, 0, 2, 0, 4348.0, 0.0, -10.0, -81.0, 0.0, 2.0),
( 1,-1, 2, 0, 2, -2878.0, 0.0, 8.0, 1232.0, 0.0, 4.0),
( 0, 0, 0, 1, 0, -4230.0, 0.0, 5.0, -20.0, 0.0, -2.0),
(-1,-1, 2, 2, 2, -2819.0, 0.0, 7.0, 1207.0, 0.0, 3.0),
(-1, 0, 2, 0, 0, -4056.0, 0.0, 5.0, 40.0, 0.0, -2.0),
( 0,-1, 2, 2, 2, -2647.0, 0.0, 11.0, 1129.0, 0.0, 5.0),
(-2, 0, 0, 0, 1, -2294.0, 0.0, -10.0, 1266.0, 0.0, -4.0),
( 1, 1, 2, 0, 2, 2481.0, 0.0, -7.0, -1062.0, 0.0, -3.0),
( 2, 0, 0, 0, 1, 2179.0, 0.0, -2.0, -1129.0, 0.0, -2.0),
(-1, 1, 0, 1, 0, 3276.0, 0.0, 1.0, -9.0, 0.0, 0.0),
( 1, 1, 0, 0, 0, -3389.0, 0.0, 5.0, 35.0, 0.0, -2.0),
( 1, 0, 2, 0, 0, 3339.0, 0.0, -13.0, -107.0, 0.0, 1.0),
(-1, 0, 2,-2, 1, -1987.0, 0.0, -6.0, 1073.0, 0.0, -2.0),
( 1, 0, 0, 0, 2, -1981.0, 0.0, 0.0, 854.0, 0.0, 0.0),
(-1, 0, 0, 1, 0, 4026.0, 0.0, -353.0, -553.0, 0.0, -139.0),
( 0, 0, 2, 1, 2, 1660.0, 0.0, -5.0, -710.0, 0.0, -2.0),
(-1, 0, 2, 4, 2, -1521.0, 0.0, 9.0, 647.0, 0.0, 4.0),
(-1, 1, 0, 1, 1, 1314.0, 0.0, 0.0, -700.0, 0.0, 0.0),
( 0,-2, 2,-2, 1, -1283.0, 0.0, 0.0, 672.0, 0.0, 0.0),
( 1, 0, 2, 2, 1, -1331.0, 0.0, 8.0, 663.0, 0.0, 4.0),
(-2, 0, 2, 2, 2, 1383.0, 0.0, -2.0, -594.0, 0.0, -2.0),
(-1, 0, 0, 0, 2, 1405.0, 0.0, 4.0, -610.0, 0.0, 2.0),
( 1, 1, 2,-2, 2, 1290.0, 0.0, 0.0, -556.0, 0.0, 0.0),
];
#[derive(Clone, Copy, Debug)]
pub struct Nutation {
pub dpsi: f64,
pub deps: f64,
}
fn julian_centuries_tt(jd_tt: f64) -> f64 {
(jd_tt - JD_J2000) / DAYS_PER_CENTURY
}
pub fn delaunay_args(jd_tt: f64) -> [f64; 5] {
let t = julian_centuries_tt(jd_tt);
let arg = |c0: f64, c1: f64| ((c0 + c1 * t) % TURNAS) * ARCSEC_TO_RAD;
let el = arg(485868.249036, 1717915923.2178); let elp = arg(1287104.79305, 129596581.0481); let f = arg(335779.526232, 1739527262.8478); let d = arg(1072260.70369, 1602961601.2090); let om = arg(450160.398036, -6962890.5431); [el, elp, f, d, om]
}
pub fn nutation_iau2000b(jd_tt: f64) -> Nutation {
let t = julian_centuries_tt(jd_tt);
let [el, elp, f, d, om] = delaunay_args(jd_tt);
let mut dp = 0.0_f64;
let mut de = 0.0_f64;
for &(nl, nlp, nf, nd, nom, ps, pst, pc, ec, ect, es) in LS_TERMS.iter().rev() {
let arg = (f64::from(nl) * el
+ f64::from(nlp) * elp
+ f64::from(nf) * f
+ f64::from(nd) * d
+ f64::from(nom) * om)
% std::f64::consts::TAU;
let (sarg, carg) = arg.sin_cos();
dp += (ps + pst * t) * sarg + pc * carg;
de += (ec + ect * t) * carg + es * sarg;
}
Nutation {
dpsi: dp * U2R + DPPLAN,
deps: de * U2R + DEPLAN,
}
}
fn delaunay_args_2000a(t: f64) -> [f64; 5] {
let poly = |c0: f64, c1: f64, c2: f64, c3: f64, c4: f64| {
((c0 + t * (c1 + t * (c2 + t * (c3 + t * c4)))) % TURNAS) * ARCSEC_TO_RAD
};
let el = poly(
485868.249036,
1717915923.2178,
31.8792,
0.051635,
-0.00024470,
);
let elp = poly(
1287104.79305,
129596581.0481,
-0.5532,
0.000136,
-0.00001149,
);
let f = poly(
335779.526232,
1739527262.8478,
-12.7512,
-0.001037,
0.00000417,
);
let d = poly(
1072260.70369,
1602961601.2090,
-6.3706,
0.006593,
-0.00003169,
);
let om = poly(450160.398036, -6962890.5431, 7.4722, 0.007702, -0.00005939);
[el, elp, f, d, om]
}
pub fn nutation_iau2000a(jd_tt: f64) -> Nutation {
use crate::nutation_iau2000a_data::{LS_2000A, PL_2000A};
let t = julian_centuries_tt(jd_tt);
let tau = std::f64::consts::TAU;
let [el, elp, f, d, om] = delaunay_args_2000a(t);
let mut dp = 0.0_f64;
let mut de = 0.0_f64;
for &(nl, nlp, nf, nd, nom, ps, pst, pc, ec, ect, es) in LS_2000A.iter().rev() {
let arg = (f64::from(nl) * el
+ f64::from(nlp) * elp
+ f64::from(nf) * f
+ f64::from(nd) * d
+ f64::from(nom) * om)
% tau;
let (sarg, carg) = arg.sin_cos();
dp += (ps + pst * t) * sarg + pc * carg;
de += (ec + ect * t) * carg + es * sarg;
}
let dpsi_ls = dp * U2R;
let deps_ls = de * U2R;
let arg2pi = |c0: f64, c1: f64| (c0 + c1 * t) % tau;
let al = arg2pi(2.35555598, 8328.6914269554);
let af = arg2pi(1.627905234, 8433.466158131);
let ad = arg2pi(5.198466741, 7771.3771468121);
let aom = arg2pi(2.18243920, -33.757045);
let alme = arg2pi(4.402608842, 2608.7903141574);
let alve = arg2pi(3.176146697, 1021.3285546211);
let alea = arg2pi(1.753470314, 628.3075849991);
let alma = arg2pi(6.203480913, 334.0612426700);
let alju = arg2pi(0.599546497, 52.9690962641);
let alsa = arg2pi(0.874016757, 21.3299104960);
let alur = arg2pi(5.481293872, 7.4781598567);
let alne = arg2pi(5.321159000, 3.8127774000);
let apa = (0.024381750 + 0.00000538691 * t) * t;
let mut dp = 0.0_f64;
let mut de = 0.0_f64;
for &(nl, nf, nd, nom, nme, nve, nea, nma, nju, nsa, nur, nne, npa, sp, cp, se, ce) in
PL_2000A.iter().rev()
{
let arg = (f64::from(nl) * al
+ f64::from(nf) * af
+ f64::from(nd) * ad
+ f64::from(nom) * aom
+ f64::from(nme) * alme
+ f64::from(nve) * alve
+ f64::from(nea) * alea
+ f64::from(nma) * alma
+ f64::from(nju) * alju
+ f64::from(nsa) * alsa
+ f64::from(nur) * alur
+ f64::from(nne) * alne
+ f64::from(npa) * apa)
% tau;
let (sarg, carg) = arg.sin_cos();
dp += sp * sarg + cp * carg;
de += se * sarg + ce * carg;
}
let dpsi_pl = dp * U2R;
let deps_pl = de * U2R;
Nutation {
dpsi: dpsi_ls + dpsi_pl,
deps: deps_ls + deps_pl,
}
}
pub fn mean_obliquity(jd_tt: f64) -> f64 {
fw_angles(jd_tt).eps_a
}
fn numat(eps: f64, n: Nutation) -> Mat3 {
let r = matmul(&rz(n.dpsi), &rx(eps));
matmul(&rx(-(eps + n.deps)), &r)
}
pub fn nutation_matrix(jd_tt: f64) -> Mat3 {
numat(mean_obliquity(jd_tt), nutation_iau2000b(jd_tt))
}
pub fn nutation_matrix_2000a(jd_tt: f64) -> Mat3 {
numat(mean_obliquity(jd_tt), nutation_iau2000a(jd_tt))
}
pub fn equation_of_equinoxes(jd_tt: f64) -> f64 {
let n = nutation_iau2000b(jd_tt);
let eps = mean_obliquity(jd_tt);
let om = delaunay_args(jd_tt)[4];
n.dpsi * eps.cos()
+ 0.002_640_96 * ARCSEC_TO_RAD * om.sin()
+ 0.000_063_52 * ARCSEC_TO_RAD * (2.0 * om).sin()
}
pub fn teme_to_gcrs_matrix(jd_tt: f64) -> Mat3 {
let r3_minus_ee = rz(-equation_of_equinoxes(jd_tt));
let n_t = transpose(&nutation_matrix(jd_tt));
let p_t = transpose(&precession_matrix(jd_tt));
matmul(&p_t, &matmul(&n_t, &r3_minus_ee))
}
pub fn teme_to_gcrs(r_teme: Vec3, v_teme: Vec3, jd_tt: f64) -> (Vec3, Vec3) {
let m = teme_to_gcrs_matrix(jd_tt);
(mat_vec(&m, r_teme), mat_vec(&m, v_teme))
}
pub fn gcrs_to_teme(r_gcrs: Vec3, v_gcrs: Vec3, jd_tt: f64) -> (Vec3, Vec3) {
let m = transpose(&teme_to_gcrs_matrix(jd_tt));
(mat_vec(&m, r_gcrs), mat_vec(&m, v_gcrs))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::precession::gcrs_to_mod;
const JD_TT_REF: f64 = 2_400_000.5 + 53_736.0;
fn norm(v: Vec3) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
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 nut00b_matches_sofa_reference_vector() {
let n = nutation_iau2000b(JD_TT_REF);
assert!(
(n.dpsi - (-9.632_552_291_148_363e-6)).abs() < 1e-13,
"Δψ = {} (want -0.9632552291148362783e-5)",
n.dpsi
);
assert!(
(n.deps - 4.063_197_106_621_16e-5).abs() < 1e-13,
"Δε = {} (want 0.4063197106621159367e-4)",
n.deps
);
}
#[test]
fn nut00a_matches_sofa_reference_vector() {
let n = nutation_iau2000a(JD_TT_REF);
assert!(
(n.dpsi - (-0.963_090_910_711_551_8e-5)).abs() < 1e-13,
"Δψ = {} (want -0.9630909107115518431e-5)",
n.dpsi
);
assert!(
(n.deps - 0.406_323_917_400_167_9e-4).abs() < 1e-13,
"Δε = {} (want 0.4063239174001678710e-4)",
n.deps
);
}
#[test]
fn nut00a_refines_nut00b_below_one_mas() {
let a = nutation_iau2000a(JD_TT_REF);
let b = nutation_iau2000b(JD_TT_REF);
let d_dpsi = (a.dpsi - b.dpsi).abs();
let d_deps = (a.deps - b.deps).abs();
assert!(d_dpsi > 1e-12, "Δψ difference should be non-trivial");
assert!(
d_dpsi < MAS_TO_RAD && d_deps < MAS_TO_RAD,
"2000A vs 2000B: Δψ {:.3} mas, Δε {:.3} mas (want < 1 mas)",
d_dpsi / MAS_TO_RAD,
d_deps / MAS_TO_RAD
);
}
#[test]
fn delaunay_node_at_j2000_is_125_degrees() {
let [l, lp, _f, _d, om] = delaunay_args(JD_J2000);
let to_deg = |r: f64| (r / ARCSEC_TO_RAD / 3600.0).rem_euclid(360.0);
assert!(
(to_deg(om) - 125.044_555).abs() < 1e-3,
"Ω = {}°",
to_deg(om)
);
assert!(
(to_deg(lp) - 357.529_109).abs() < 1e-3,
"l′ = {}°",
to_deg(lp)
);
assert!((to_deg(l) - 134.963_403).abs() < 1e-3, "l = {}°", to_deg(l));
}
#[test]
fn nutation_matrix_is_a_small_proper_rotation() {
let n = nutation_matrix(JD_TT_REF);
assert!(
is_orthonormal(&n),
"nutation matrix must be a proper rotation"
);
let trace = n[0][0] + n[1][1] + n[2][2];
let theta = (((trace - 1.0) / 2.0).clamp(-1.0, 1.0)).acos();
let theta_arcsec = theta / ARCSEC_TO_RAD;
assert!(
(1.0..60.0).contains(&theta_arcsec),
"nutation angle = {theta_arcsec}″ (want tens of arcsec)"
);
}
#[test]
fn nutation_matrix_2000a_is_a_proper_rotation_near_2000b() {
let na = nutation_matrix_2000a(JD_TT_REF);
assert!(
is_orthonormal(&na),
"2000A nutation matrix must be a rotation"
);
let nb = nutation_matrix(JD_TT_REF);
let r = [42_164.0e3, 0.0, 0.0]; let ra = mat_vec(&na, r);
let rb = mat_vec(&nb, r);
let sep =
((ra[0] - rb[0]).powi(2) + (ra[1] - rb[1]).powi(2) + (ra[2] - rb[2]).powi(2)).sqrt();
assert!(sep > 1e-4 && sep < 1.0, "2000A−2000B separation = {sep} m");
}
#[test]
fn equation_of_equinoxes_tracks_dpsi_cos_eps() {
let ee = equation_of_equinoxes(JD_TT_REF);
let n = nutation_iau2000b(JD_TT_REF);
let dominant = n.dpsi * mean_obliquity(JD_TT_REF).cos();
assert!(ee.abs() > 1e-6, "EE should be ~arcsec scale, got {ee}");
assert!(
(ee - dominant).abs() < 2e-8,
"EE − Δψ·cosε = {} (should be ≤ a few mas)",
ee - dominant
);
}
#[test]
fn teme_gcrs_round_trips() {
let r = [7000.0e3, -1200.0e3, 4200.0e3];
let v = [1.5e3, 7.0e3, -0.8e3];
let (rg, vg) = teme_to_gcrs(r, v, JD_TT_REF);
let (rb, vb) = gcrs_to_teme(rg, vg, JD_TT_REF);
for k in 0..3 {
assert!((rb[k] - r[k]).abs() < 1e-6, "r round-trip[{k}]");
assert!((vb[k] - v[k]).abs() < 1e-9, "v round-trip[{k}]");
}
assert!((norm(rg) - norm(r)).abs() < 1e-6);
}
#[test]
fn teme_gcrs_adds_nutation_on_top_of_precession() {
let r = [7000.0e3, -1200.0e3, 4200.0e3];
let (rg, _) = teme_to_gcrs(r, [0.0; 3], JD_TT_REF);
let prec_only = crate::precession::mod_to_gcrs(r, JD_TT_REF);
let diff = ((rg[0] - prec_only[0]).powi(2)
+ (rg[1] - prec_only[1]).powi(2)
+ (rg[2] - prec_only[2]).powi(2))
.sqrt();
assert!(
(5.0..5000.0).contains(&diff),
"nutation+EE contribution = {diff} m (want tens–hundreds of m)"
);
let _ = gcrs_to_mod(r, JD_TT_REF);
}
}