use crate::body::Body;
use crate::precession::{mat_vec, transpose, Mat3};
type Vec3 = [f64; 3];
const JD_J2000: f64 = 2_451_545.0;
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 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 mars_prime_meridian(body: &Body, jd_tdb: f64) -> f64 {
body.prime_w0 + body.prime_w_dot * (jd_tdb - JD_J2000)
}
pub fn iau_mars_rotation(body: &Body, jd_tdb: f64) -> Mat3 {
let ra = body.pole_ra0;
let dec = body.pole_dec0;
let w = mars_prime_meridian(body, jd_tdb);
let half_pi = std::f64::consts::FRAC_PI_2;
matmul(&matmul(&rz(w), &rx(half_pi - dec)), &rz(half_pi + ra))
}
pub fn inertial_to_bodyfixed(r_inertial: Vec3, body: &Body, jd_tdb: f64) -> Vec3 {
mat_vec(&iau_mars_rotation(body, jd_tdb), r_inertial)
}
pub fn bodyfixed_to_inertial(r_bodyfixed: Vec3, body: &Body, jd_tdb: f64) -> Vec3 {
mat_vec(&transpose(&iau_mars_rotation(body, jd_tdb)), r_bodyfixed)
}
#[cfg(test)]
mod tests {
use super::*;
fn norm(v: Vec3) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
#[test]
fn rotation_is_orthonormal_and_proper() {
let body = Body::mars();
let jd = 2_459_580.5; let m = iau_mars_rotation(&body, jd);
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 round_trip_recovers_the_original_vector() {
let body = Body::mars();
let jd = 2_459_580.5;
let r = [1.50e6, -0.70e6, 0.95e6];
let bf = inertial_to_bodyfixed(r, &body, jd);
let back = bodyfixed_to_inertial(bf, &body, jd);
for k in 0..3 {
assert!(
(back[k] - r[k]).abs() < 1e-6,
"axis {k}: round-trip {} vs original {}",
back[k],
r[k]
);
}
assert!(
(norm(bf) - norm(r)).abs() < 1e-6,
"rotation must preserve length"
);
}
#[test]
fn pole_row_matches_ra_dec_and_is_constant() {
let body = Body::mars();
let (ra, dec) = (body.pole_ra0, body.pole_dec0);
let want = [dec.cos() * ra.cos(), dec.cos() * ra.sin(), dec.sin()];
for &jd in &[JD_J2000, 2_459_580.5, JD_J2000 + 1000.0] {
let m = iau_mars_rotation(&body, jd);
let pole = [m[2][0], m[2][1], m[2][2]];
for k in 0..3 {
assert!(
(pole[k] - want[k]).abs() < 1e-12,
"jd {jd} pole[{k}] {} vs {}",
pole[k],
want[k]
);
}
}
assert!((ra.to_degrees() - 317.681).abs() < 1e-3);
assert!((dec.to_degrees() - 52.886).abs() < 1e-3);
}
#[test]
fn prime_meridian_advances_by_w_dot_per_day() {
let body = Body::mars();
let jd = 2_459_580.5;
let dw =
(mars_prime_meridian(&body, jd + 1.0) - mars_prime_meridian(&body, jd)).to_degrees();
assert!(
(dw - 350.891_982_26).abs() < 1e-9,
"W rate {dw}°/day (want 350.89198226)"
);
let spin = matmul(
&iau_mars_rotation(&body, jd + 1.0),
&transpose(&iau_mars_rotation(&body, jd)),
);
let trace = spin[0][0] + spin[1][1] + spin[2][2];
let theta = ((trace - 1.0) / 2.0).acos().to_degrees();
let expected = (360.0 - 350.891_982_26_f64).abs();
assert!(
(theta - expected).abs() < 1e-6,
"spin angle {theta}° between jd and jd+1 (want {expected})"
);
}
}