use nalgebra::{Matrix3, UnitQuaternion, Vector3};
#[allow(unused_imports)]
use crate::math::F64Ext;
use crate::epoch::{Epoch, Tdb};
#[derive(Debug, Clone, Copy)]
pub struct IauRotationModel {
pub alpha0: f64,
pub alpha1: f64,
pub delta0: f64,
pub delta1: f64,
pub w0: f64,
pub wd: f64,
}
pub fn model_for_body(name: &str) -> Option<&'static IauRotationModel> {
match name {
"earth" => Some(&crate::earth::rotation::EARTH),
"moon" => Some(&crate::moon::MOON),
"mars" => Some(&crate::planets::rotation::MARS),
"sun" => Some(&crate::sun::SUN),
_ => None,
}
}
pub fn body_orientation(name: &str, epoch: &Epoch<Tdb>) -> Option<UnitQuaternion<f64>> {
match name {
"moon" => Some(crate::moon::moon_orientation(epoch)),
_ => model_for_body(name).map(|m| m.orientation(epoch)),
}
}
impl IauRotationModel {
pub fn orientation(&self, epoch: &Epoch<Tdb>) -> UnitQuaternion<f64> {
let d = epoch.jd() - 2451545.0; let t = d / 36525.0;
let alpha = (self.alpha0 + self.alpha1 * t).to_radians();
let delta = (self.delta0 + self.delta1 * t).to_radians();
let w = (self.w0 + self.wd * d).to_radians();
let z_body = Vector3::new(
alpha.cos() * delta.cos(),
alpha.sin() * delta.cos(),
delta.sin(),
);
let node = Vector3::new(-alpha.sin(), alpha.cos(), 0.0);
let m = z_body.cross(&node);
let x_body = node * w.cos() + m * w.sin();
let y_body = z_body.cross(&x_body);
let rot = Matrix3::from_columns(&[x_body, y_body, z_body]);
UnitQuaternion::from_rotation_matrix(&nalgebra::Rotation3::from_matrix_unchecked(rot))
}
pub fn prime_meridian_angle(&self, epoch: &Epoch<Tdb>) -> f64 {
let d = epoch.jd() - 2451545.0;
(self.w0 + self.wd * d).to_radians()
}
pub fn pole_direction(&self, epoch: &Epoch<Tdb>) -> Vector3<f64> {
let d = epoch.jd() - 2451545.0;
let t = d / 36525.0;
let alpha = (self.alpha0 + self.alpha1 * t).to_radians();
let delta = (self.delta0 + self.delta1 * t).to_radians();
Vector3::new(
alpha.cos() * delta.cos(),
alpha.sin() * delta.cos(),
delta.sin(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn orientation_axes_are_orthonormal_via_dispatch() {
let epoch = Epoch::from_gregorian(2024, 6, 15, 0, 0, 0.0).to_tdb();
for name in ["moon", "mars", "sun", "earth"] {
let model = model_for_body(name).expect("known body");
let q = model.orientation(&epoch);
let rot = q.to_rotation_matrix();
let m = rot.matrix();
let mtm = m.transpose() * m;
let identity = nalgebra::Matrix3::<f64>::identity();
let err = (mtm - identity).norm();
assert!(
err < 1e-10,
"{name} rotation matrix should be orthogonal, error = {err}"
);
let det = m.determinant();
assert!(
(det - 1.0).abs() < 1e-10,
"{name} rotation determinant should be 1, got {det}"
);
}
}
#[test]
fn model_for_body_unknown_returns_none() {
assert!(model_for_body("pluto").is_none());
assert!(model_for_body("").is_none());
}
#[test]
fn body_orientation_unknown_returns_none() {
let epoch = Epoch::from_gregorian(2024, 1, 1, 0, 0, 0.0).to_tdb();
assert!(body_orientation("pluto", &epoch).is_none());
}
#[test]
fn body_orientation_moon_uses_libration_variant() {
let epoch = Epoch::from_gregorian(2024, 3, 15, 0, 0, 0.0).to_tdb();
let q_dispatch = body_orientation("moon", &epoch).unwrap();
let q_base = model_for_body("moon").unwrap().orientation(&epoch);
let angle_diff = q_dispatch.angle_to(&q_base).to_degrees();
assert!(
angle_diff > 0.1,
"dispatcher should apply libration, got angle_diff={angle_diff:.4}°"
);
}
#[test]
fn generate_fixture_quaternions() {
let cases = [
("moon", 2440418.064 + 723374.0 / 86400.0, "apollo11_end"),
("moon", 2440418.064, "apollo11_start"),
("mars", 2451545.0, "j2000"),
("earth", 2451545.0, "j2000"),
];
println!("--- IAU orientation fixture ---");
for (body, jd, label) in &cases {
let epoch = Epoch::<Tdb>::from_jd_tdb(*jd);
let q = body_orientation(body, &epoch).unwrap();
println!(
r#" {{ body: "{body}", jd: {jd}, label: "{label}", q: [{:.15}, {:.15}, {:.15}, {:.15}] }},"#,
q.w, q.i, q.j, q.k
);
}
println!("---");
}
}