use crate::qtty::*;
use crate::time::JulianDate;
use affn::Rotation3;
#[inline]
pub fn precession_fw_angles(jd: JulianDate) -> (Radians, Radians, Radians, Radians) {
let t = jd.julian_centuries();
let t2 = t * t;
let t3 = t2 * t;
let t4 = t3 * t;
let t5 = t4 * t;
let gamb_as =
-0.052_928 + 10.556_378 * t + 0.493_204_4 * t2 - 0.000_312_38 * t3 - 2.788e-6 * t4
+ 2.60e-8 * t5;
let phib_as = 84_381.412_819 - 46.811_016 * t + 0.051_126_8 * t2 + 0.000_532_89 * t3
- 4.40e-7 * t4
- 1.76e-8 * t5;
let psib_as = -0.041_775 + 5_038.481_484 * t + 1.558_417_5 * t2
- 0.000_185_22 * t3
- 2.6452e-5 * t4
- 1.48e-8 * t5;
let epsa_as = 84381.406 - 46.836_769 * t - 0.000_183_1 * t2 + 0.002_003_40 * t3
- 5.76e-7 * t4
- 4.34e-8 * t5;
let as_to_rad = |a: f64| Radians::new(a.to_radians() / 3600.0);
(
as_to_rad(gamb_as),
as_to_rad(phib_as),
as_to_rad(psib_as),
as_to_rad(epsa_as),
)
}
#[inline]
pub fn mean_obliquity_iau2006(jd: JulianDate) -> Radians {
let t = jd.julian_centuries();
let t2 = t * t;
let t3 = t2 * t;
let t4 = t3 * t;
let t5 = t4 * t;
let epsa_as = 84381.406 - 46.836_769 * t - 0.000_183_1 * t2 + 0.002_003_40 * t3
- 5.76e-7 * t4
- 4.34e-8 * t5;
Radians::new(epsa_as.to_radians() / 3600.0)
}
pub const J2000_MEAN_OBLIQUITY_ARCSEC: f64 = 84381.406;
#[inline]
pub fn fw_matrix(gamb: Radians, phib: Radians, psib: Radians, epsa: Radians) -> Rotation3 {
Rotation3::fused_rx_rz_rx_rz(epsa, psib, -phib, -gamb)
}
pub fn precession_matrix_iau2006(jd: JulianDate) -> Rotation3 {
let (gamb, phib, psib, epsa) = precession_fw_angles(jd);
fw_matrix(gamb, phib, psib, epsa)
}
pub fn precession_nutation_matrix(jd: JulianDate, dpsi: Radians, deps: Radians) -> Rotation3 {
let (gamb, phib, psib, epsa) = precession_fw_angles(jd);
fw_matrix(gamb, phib, psib + dpsi, epsa + deps)
}
pub fn gcrs_to_ecliptic_of_date_matrix(jd: JulianDate) -> Rotation3 {
let prec = precession_matrix_iau2006(jd);
let eps_a = mean_obliquity_iau2006(jd);
Rotation3::rx(-eps_a) * prec
}
pub fn gcrs_to_true_ecliptic_of_date_matrix(jd: JulianDate) -> Rotation3 {
let nut = crate::astro::nutation::nutation_iau2000b(jd);
let npb = precession_nutation_matrix(jd, nut.dpsi, nut.deps);
let eps_true = nut.mean_obliquity + nut.deps;
Rotation3::rx(-eps_true) * npb
}
pub fn true_ecliptic_of_date_to_gcrs_matrix(jd: JulianDate) -> Rotation3 {
gcrs_to_true_ecliptic_of_date_matrix(jd).transpose()
}
pub fn ecliptic_of_date_to_gcrs_matrix(jd: JulianDate) -> Rotation3 {
gcrs_to_ecliptic_of_date_matrix(jd).transpose()
}
pub fn mean_equatorial_to_ecliptic_of_date_matrix(jd: JulianDate) -> Rotation3 {
let eps_a = mean_obliquity_iau2006(jd);
Rotation3::rx(-eps_a)
}
pub fn mean_equatorial_to_true_ecliptic_of_date_matrix(jd: JulianDate) -> Rotation3 {
let nut = crate::astro::nutation::nutation_iau2000b(jd);
let eps = nut.mean_obliquity;
let dpsi = nut.dpsi;
let deps = nut.deps;
let nutation_matrix = affn::Rotation3::fused_rx_rz_rx(eps + deps, dpsi, -eps);
let eps_true = eps + deps;
Rotation3::rx(-eps_true) * nutation_matrix
}
pub fn true_ecliptic_of_date_to_mean_equatorial_matrix(jd: JulianDate) -> Rotation3 {
mean_equatorial_to_true_ecliptic_of_date_matrix(jd).transpose()
}
pub fn ecliptic_of_date_to_mean_equatorial_matrix(jd: JulianDate) -> Rotation3 {
mean_equatorial_to_ecliptic_of_date_matrix(jd).transpose()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::qtty::Radians;
#[test]
fn mean_obliquity_at_j2000() {
let eps = mean_obliquity_iau2006(JulianDate::J2000);
let expected_deg = 84381.406 / 3600.0;
assert!(
(eps.to::<Degree>().value() - expected_deg).abs() < 1e-10,
"obliquity at J2000 = {}°, expected {}°",
eps.to::<Degree>(),
expected_deg
);
}
#[test]
fn fw_angles_at_j2000_are_approximately_identity() {
let mat = precession_matrix_iau2006(JulianDate::J2000);
let m = mat.as_matrix();
for (i, row) in m.iter().enumerate().take(3) {
assert!(
(row[i] - 1.0).abs() < 1e-7,
"diagonal[{}] = {}, expected ~1.0",
i,
row[i]
);
}
}
#[test]
fn precession_matrix_j2025_reasonable() {
let jd = JulianDate::new(2_460_676.5);
let mat = precession_matrix_iau2006(jd);
let m = mat.as_matrix();
for (i, row) in m.iter().enumerate().take(3) {
for (j, &val) in row.iter().enumerate().take(3) {
if i == j {
assert!(
(val - 1.0).abs() < 0.001,
"diagonal[{}][{}] too far from 1: {}",
i,
j,
val
);
} else {
assert!(
val.abs() < 0.01,
"off-diagonal[{}][{}] too large: {}",
i,
j,
val
);
}
}
}
}
#[test]
fn precession_nutation_matrix_includes_corrections() {
let jd = JulianDate::new(2_460_000.5);
let mat_prec = precession_matrix_iau2006(jd);
let mat_pn = precession_nutation_matrix(jd, Radians::new(1e-5), Radians::new(1e-5));
let m1 = mat_prec.as_matrix();
let m2 = mat_pn.as_matrix();
let mut max_diff = 0.0f64;
for i in 0..3 {
for j in 0..3 {
max_diff = max_diff.max((m1[i][j] - m2[i][j]).abs());
}
}
assert!(
max_diff > 1e-8,
"nutation should cause detectable difference"
);
assert!(
max_diff < 1e-3,
"nutation difference too large: {}",
max_diff
);
}
#[test]
fn mean_obliquity_decreases_with_time() {
let eps_2000 = mean_obliquity_iau2006(JulianDate::J2000);
let eps_2100 = mean_obliquity_iau2006(JulianDate::new(2_488_069.5));
assert!(eps_2100 < eps_2000, "obliquity should decrease over time");
let diff_arcsec = (eps_2000 - eps_2100).to::<Degree>().value() * 3600.0;
assert!(
(diff_arcsec - 47.0_f64).abs() < 2.0,
"obliquity change over 1 century ≈ 47″, got {}″",
diff_arcsec
);
}
}