use crate::precession::{transpose, Mat3};
const JD_J2000: f64 = 2_451_545.0;
pub const E_CONST: [f64; 13] = [
125.045, 250.089, 260.008, 176.625, 357.529, 311.589, 134.963, 276.617, 34.226, 15.134,
119.743, 239.961, 25.053,
];
pub const E_RATE: [f64; 13] = [
-1935.5364525,
-3871.072905,
475263.3328725,
487269.629985,
35999.0509575,
964468.49931,
477198.869325,
12006.300765,
63863.5132425,
-5806.6093575,
131.84064,
6003.1503825,
473327.79642,
];
const RA_AMP: [f64; 13] = [
-3.8787, -0.1204, 0.0700, -0.0172, 0.0, 0.0072, 0.0, 0.0, 0.0, -0.0052, 0.0, 0.0, 0.0043,
];
const DEC_AMP: [f64; 13] = [
1.5419, 0.0239, -0.0278, 0.0068, 0.0, -0.0029, 0.0009, 0.0, 0.0, 0.0008, 0.0, 0.0, -0.0009,
];
const PM_AMP: [f64; 13] = [
3.5610, 0.1208, -0.0642, 0.0158, 0.0252, -0.0066, -0.0047, -0.0046, 0.0028, 0.0052, 0.0040,
0.0019, -0.0044,
];
fn days_and_centuries(jd_tdb: f64) -> (f64, f64) {
let d = jd_tdb - JD_J2000;
(d, d / 36525.0)
}
fn libration_args(t: f64) -> [f64; 13] {
let mut e = [0.0; 13];
for i in 0..13 {
e[i] = (E_CONST[i] + E_RATE[i] * t).to_radians();
}
e
}
pub fn lunar_pole_ra_dec(jd_tdb: f64) -> (f64, f64) {
let (_d, t) = days_and_centuries(jd_tdb);
let e = libration_args(t);
let mut ra = 269.9949 + 0.0031 * t;
let mut dec = 66.5392 + 0.0130 * t;
for i in 0..13 {
ra += RA_AMP[i] * e[i].sin();
dec += DEC_AMP[i] * e[i].cos();
}
(ra.to_radians(), dec.to_radians())
}
pub fn lunar_prime_meridian(jd_tdb: f64) -> f64 {
let (d, t) = days_and_centuries(jd_tdb);
let e = libration_args(t);
let mut w = 38.3213 + 13.176_358_15 * d - 1.4e-12 * d * d;
for i in 0..13 {
w += PM_AMP[i] * e[i].sin();
}
w.to_radians()
}
fn rz(theta: f64) -> Mat3 {
let (s, c) = theta.sin_cos();
[[c, s, 0.0], [-s, c, 0.0], [0.0, 0.0, 1.0]]
}
fn rx(theta: f64) -> Mat3 {
let (s, c) = theta.sin_cos();
[[1.0, 0.0, 0.0], [0.0, c, s], [0.0, -s, c]]
}
fn ry(theta: f64) -> Mat3 {
let (s, c) = theta.sin_cos();
[[c, 0.0, -s], [0.0, 1.0, 0.0], [s, 0.0, c]]
}
fn matmul(a: &Mat3, b: &Mat3) -> Mat3 {
let mut m = [[0.0; 3]; 3];
for (i, mrow) in m.iter_mut().enumerate() {
for (j, e) in mrow.iter_mut().enumerate() {
*e = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
}
}
m
}
pub fn icrf_to_iau_moon(jd_tdb: f64) -> Mat3 {
let (ra, dec) = lunar_pole_ra_dec(jd_tdb);
let w = lunar_prime_meridian(jd_tdb);
let half_pi = std::f64::consts::FRAC_PI_2;
matmul(&matmul(&rz(w), &rx(half_pi - dec)), &rz(half_pi + ra))
}
fn me_from_pa() -> Mat3 {
let a = (67.92_f64 / 3600.0).to_radians(); let b = (78.56_f64 / 3600.0).to_radians(); let c = (0.30_f64 / 3600.0).to_radians(); matmul(&matmul(&rx(c), &ry(b)), &rz(a))
}
pub fn icrf_to_moon_pa(jd_tdb: f64) -> Mat3 {
let pa_from_me = transpose(&me_from_pa());
matmul(&pa_from_me, &icrf_to_iau_moon(jd_tdb))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ephem::moon_position;
use crate::precession::{julian_centuries_tt, mat_vec, transpose};
type Vec3 = [f64; 3];
fn norm(v: Vec3) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
fn dot(a: Vec3, b: Vec3) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn jd_2022_001() -> f64 {
2_459_580.5 }
#[test]
fn rotation_is_orthonormal_and_proper() {
let m = icrf_to_iau_moon(jd_2022_001());
let mt = transpose(&m);
let prod = matmul(&mt, &m);
for (i, row) in prod.iter().enumerate() {
for (j, &e) in row.iter().enumerate() {
let want = if i == j { 1.0 } else { 0.0 };
assert!(
(e - want).abs() < 1e-12,
"RᵀR[{i}][{j}] = {e} (want {want})"
);
}
}
let det = 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]);
assert!(
(det - 1.0).abs() < 1e-12,
"det = {det} (want +1, proper rotation)"
);
}
#[test]
fn pole_row_matches_ra_dec_and_known_j2000_value() {
let jd = JD_J2000;
let (ra, dec) = lunar_pole_ra_dec(jd);
let m = icrf_to_iau_moon(jd);
let pole = [m[2][0], m[2][1], m[2][2]];
let want = [dec.cos() * ra.cos(), dec.cos() * ra.sin(), dec.sin()];
for k in 0..3 {
assert!(
(pole[k] - want[k]).abs() < 1e-12,
"pole[{k}] {} vs {}",
pole[k],
want[k]
);
}
let ra_deg = ra.to_degrees().rem_euclid(360.0);
let dec_deg = dec.to_degrees();
assert!(
(ra_deg - 266.86).abs() < 0.2,
"J2000 pole RA {ra_deg}° (want ≈ 266.86)"
);
assert!(
(dec_deg - 65.64).abs() < 0.2,
"J2000 pole Dec {dec_deg}° (want ≈ 65.64)"
);
}
#[test]
fn prime_meridian_advances_one_turn_per_sidereal_month() {
let jd = jd_2022_001();
let daily = (lunar_prime_meridian(jd + 1.0) - lunar_prime_meridian(jd)).to_degrees();
assert!(
(daily - 13.176_358).abs() < 0.05,
"W rate {daily}°/day (want ≈ 13.176)"
);
}
#[test]
fn sub_earth_point_tracks_the_near_side() {
for day in [0.0, 7.0, 14.0, 21.0, 28.0] {
let jd = jd_2022_001() + day;
let mp = moon_position(julian_centuries_tt(jd));
let earth_dir = [-mp[0], -mp[1], -mp[2]];
let m = icrf_to_iau_moon(jd);
let bf = mat_vec(&m, earth_dir);
let bf = [bf[0] / norm(bf), bf[1] / norm(bf), bf[2] / norm(bf)];
let lon = bf[1].atan2(bf[0]).to_degrees(); let lat = bf[2].asin().to_degrees();
assert!(
lon.abs() < 10.0,
"day {day}: sub-Earth longitude {lon}° off near side (libration ≤ ~8°)"
);
assert!(
lat.abs() < 9.0,
"day {day}: sub-Earth latitude {lat}° off near side (libration ≤ ~7°)"
);
assert!(
bf[0] > 0.95,
"day {day}: Earth not toward +x (cos = {})",
bf[0]
);
}
}
#[test]
fn principal_axis_frame_is_a_small_fixed_offset_from_mean_earth() {
let jd = jd_2022_001();
let pa = icrf_to_moon_pa(jd);
let me = icrf_to_iau_moon(jd);
let prod = matmul(&transpose(&pa), &pa);
for (i, row) in prod.iter().enumerate() {
for (j, &e) in row.iter().enumerate() {
let want = if i == j { 1.0 } else { 0.0 };
assert!((e - want).abs() < 1e-12, "PAᵀPA[{i}][{j}] = {e}");
}
}
let r = [1.50e6, 0.70e6, 0.55e6];
let rp = mat_vec(&pa, r);
let rm = mat_vec(&me, r);
let n = norm(r);
let diff = norm([rp[0] - rm[0], rp[1] - rm[1], rp[2] - rm[2]]) / n;
assert!(
(1e-4..1e-3).contains(&diff),
"ME↔PA offset {diff} rad off the ~arc-minute band"
);
let off1 = matmul(&pa, &transpose(&me));
let pa2 = icrf_to_moon_pa(jd + 28.0);
let me2 = icrf_to_iau_moon(jd + 28.0);
let off2 = matmul(&pa2, &transpose(&me2));
for (r1, r2) in off1.iter().zip(off2.iter()) {
for (a, b) in r1.iter().zip(r2.iter()) {
assert!(
(a - b).abs() < 1e-12,
"ME→PA offset matrix not time-invariant"
);
}
}
}
#[test]
fn me_frame_pole_consistent_with_inverse() {
let jd = jd_2022_001();
let m = icrf_to_iau_moon(jd);
let mt = transpose(&m);
let pole_icrf = mat_vec(&mt, [0.0, 0.0, 1.0]);
let (ra, dec) = lunar_pole_ra_dec(jd);
let want = [dec.cos() * ra.cos(), dec.cos() * ra.sin(), dec.sin()];
for k in 0..3 {
assert!((pole_icrf[k] - want[k]).abs() < 1e-12, "pole[{k}]");
}
assert!((norm(pole_icrf) - 1.0).abs() < 1e-12);
assert!((dot(pole_icrf, want) - 1.0).abs() < 1e-12);
}
}