use super::types::PrecessionResult;
use crate::constants::ARCSEC_TO_RAD;
use crate::constants::J2000_JD;
use crate::matrix::RotationMatrix3;
pub struct PrecessionIAU2006;
impl Default for PrecessionIAU2006 {
fn default() -> Self {
Self::new()
}
}
impl PrecessionIAU2006 {
pub fn new() -> Self {
Self
}
pub fn compute(&self, date1: f64, date2: f64) -> crate::AstroResult<PrecessionResult> {
let t = ((date1 - J2000_JD) + date2) / crate::constants::DAYS_PER_JULIAN_CENTURY;
let (gamb, phib, psib, _epsa_unused) = self.fukushima_williams_angles(t);
let epsa = self.obliquity_from_t(t);
let bias_precession_matrix = self.fw_angles_to_matrix(gamb, phib, psib, epsa);
let (gamb_j2000, phib_j2000, psib_j2000, _epsa_j2000_unused) =
self.fukushima_williams_angles(0.0);
let epsa_j2000 = self.obliquity_from_t(0.0);
let bias_matrix = self.fw_angles_to_matrix(gamb_j2000, phib_j2000, psib_j2000, epsa_j2000);
let bias_inverse = bias_matrix.transpose();
let precession_matrix = bias_precession_matrix.multiply(&bias_inverse);
Ok(PrecessionResult {
bias_matrix,
precession_matrix,
bias_precession_matrix,
})
}
pub fn fukushima_williams_angles(&self, t: f64) -> (f64, f64, f64, f64) {
let gamb = (-0.052928
+ (10.556378
+ (0.4932044 + (-0.00031238 + (-0.000002788 + (0.0000000260) * t) * t) * t) * t)
* t)
* ARCSEC_TO_RAD;
let phib = (84381.412819
+ (-46.811016
+ (0.0511268 + (0.00053289 + (-0.000000440 + (-0.0000000176) * t) * t) * t) * t)
* t)
* ARCSEC_TO_RAD;
let psib = (-0.041775
+ (5038.481484
+ (1.5584175 + (-0.00018522 + (-0.000026452 + (-0.0000000148) * t) * t) * t) * t)
* t)
* ARCSEC_TO_RAD;
let epsa = (84381.406
+ (-46.836769
+ (-0.0001831 + (0.00200340 + (-0.000000576 + (-0.0000000434) * t) * t) * t) * t)
* t)
* ARCSEC_TO_RAD;
(gamb, phib, psib, epsa)
}
fn obliquity_from_t(&self, t: f64) -> f64 {
(84381.406
+ (-46.836769
+ (-0.0001831 + (0.00200340 + (-0.000000576 + (-0.0000000434) * t) * t) * t) * t)
* t)
* ARCSEC_TO_RAD
}
pub fn fw_angles_to_matrix(
&self,
gamb: f64,
phib: f64,
psib: f64,
epsa: f64,
) -> RotationMatrix3 {
let mut matrix = RotationMatrix3::identity();
matrix.rotate_z(gamb);
matrix.rotate_x(phib);
matrix.rotate_z(-psib);
matrix.rotate_x(-epsa);
matrix
}
pub fn npb_matrix_iau2006a(&self, tt_centuries: f64, dpsi: f64, deps: f64) -> RotationMatrix3 {
let (gamb, phib, psib, epsa) = self.fukushima_williams_angles(tt_centuries);
self.fw_angles_to_matrix(gamb, phib, psib + dpsi, epsa + deps)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::assert_ulp_le;
#[test]
fn test_new_and_default() {
let p1 = PrecessionIAU2006::new();
let p2 = PrecessionIAU2006::default();
let r1 = p1.compute(J2000_JD, 0.0).unwrap();
let r2 = p2.compute(J2000_JD, 0.0).unwrap();
assert_eq!(r1.bias_matrix, r2.bias_matrix);
}
#[test]
fn test_compute_returns_rotation_matrices() {
let p = PrecessionIAU2006::new();
let result = p
.compute(J2000_JD, 0.5 * crate::constants::DAYS_PER_JULIAN_CENTURY)
.unwrap();
assert!(result.bias_matrix.is_rotation_matrix(1e-14));
assert!(result.precession_matrix.is_rotation_matrix(1e-14));
assert!(result.bias_precession_matrix.is_rotation_matrix(1e-14));
}
#[test]
fn test_bias_matrix_is_constant() {
let p = PrecessionIAU2006::new();
let r1 = p.compute(J2000_JD, 0.0).unwrap();
let r2 = p
.compute(J2000_JD, crate::constants::DAYS_PER_JULIAN_CENTURY)
.unwrap();
assert_eq!(r1.bias_matrix, r2.bias_matrix);
}
#[test]
fn test_precession_at_j2000_is_identity() {
let p = PrecessionIAU2006::new();
let result = p.compute(J2000_JD, 0.0).unwrap();
for i in 0..3 {
for j in 0..3 {
if i == j {
assert_ulp_le(
result.precession_matrix.get(i, j),
1.0,
4,
&format!("precession[{},{}] at t=0", i, j),
);
} else {
assert!(
result.precession_matrix.get(i, j).abs() < 1e-15,
"precession[{},{}] at t=0 should be ~0, got {}",
i,
j,
result.precession_matrix.get(i, j)
);
}
}
}
}
#[test]
fn test_precession_changes_with_time() {
let p = PrecessionIAU2006::new();
let r0 = p.compute(J2000_JD, 0.0).unwrap();
let r1 = p
.compute(J2000_JD, crate::constants::DAYS_PER_JULIAN_CENTURY)
.unwrap();
let mut differs = false;
for i in 0..3 {
for j in 0..3 {
let identity_val = if i == j { 1.0 } else { 0.0 };
if (r1.precession_matrix.get(i, j) - identity_val).abs() > 1e-4 {
differs = true;
}
}
}
assert!(
differs,
"precession matrix should differ from identity after 1 century"
);
assert_ne!(r0.precession_matrix, r1.precession_matrix);
}
#[test]
fn test_combined_matrix_consistency() {
let p = PrecessionIAU2006::new();
let result = p
.compute(J2000_JD, 0.5 * crate::constants::DAYS_PER_JULIAN_CENTURY)
.unwrap();
let bias_inverse = result.bias_matrix.transpose();
let expected = result.bias_precession_matrix.multiply(&bias_inverse);
for i in 0..3 {
for j in 0..3 {
assert_ulp_le(
result.precession_matrix.get(i, j),
expected.get(i, j),
2,
&format!("precession[{},{}]", i, j),
);
}
}
}
#[test]
fn test_fukushima_williams_angles_at_j2000() {
let p = PrecessionIAU2006::new();
let (gamb, phib, psib, epsa) = p.fukushima_williams_angles(0.0);
assert_ulp_le(gamb, -0.052928 * ARCSEC_TO_RAD, 1, "gamb at t=0");
assert_ulp_le(phib, 84381.412819 * ARCSEC_TO_RAD, 1, "phib at t=0");
assert_ulp_le(psib, -0.041775 * ARCSEC_TO_RAD, 1, "psib at t=0");
assert_ulp_le(epsa, 84381.406 * ARCSEC_TO_RAD, 1, "epsa at t=0");
}
#[test]
fn test_fukushima_williams_angles_change_with_time() {
let p = PrecessionIAU2006::new();
let (gamb0, phib0, psib0, epsa0) = p.fukushima_williams_angles(0.0);
let (gamb1, phib1, psib1, epsa1) = p.fukushima_williams_angles(1.0);
assert_ne!(gamb0, gamb1);
assert_ne!(phib0, phib1);
assert_ne!(psib0, psib1);
assert_ne!(epsa0, epsa1);
}
#[test]
fn test_fw_angles_to_matrix_returns_rotation() {
let p = PrecessionIAU2006::new();
let (gamb, phib, psib, epsa) = p.fukushima_williams_angles(0.5);
let matrix = p.fw_angles_to_matrix(gamb, phib, psib, epsa);
assert!(matrix.is_rotation_matrix(1e-14));
}
#[test]
fn test_npb_matrix_returns_rotation() {
let p = PrecessionIAU2006::new();
let matrix = p.npb_matrix_iau2006a(0.5, 0.001 * ARCSEC_TO_RAD, 0.0005 * ARCSEC_TO_RAD);
assert!(matrix.is_rotation_matrix(1e-14));
}
#[test]
fn test_npb_matrix_with_zero_nutation() {
let p = PrecessionIAU2006::new();
let (gamb, phib, psib, epsa) = p.fukushima_williams_angles(0.5);
let fw_matrix = p.fw_angles_to_matrix(gamb, phib, psib, epsa);
let npb_matrix = p.npb_matrix_iau2006a(0.5, 0.0, 0.0);
for i in 0..3 {
for j in 0..3 {
assert_ulp_le(
npb_matrix.get(i, j),
fw_matrix.get(i, j),
1,
&format!("npb[{},{}] with zero nutation", i, j),
);
}
}
}
#[test]
fn test_two_part_date_equivalence() {
let p = PrecessionIAU2006::new();
let r1 = p.compute(J2000_JD, 1000.0).unwrap();
let r2 = p.compute(J2000_JD + 500.0, 500.0).unwrap();
for i in 0..3 {
for j in 0..3 {
assert_ulp_le(
r1.bias_precession_matrix.get(i, j),
r2.bias_precession_matrix.get(i, j),
1,
&format!("two-part date[{},{}]", i, j),
);
}
}
}
}