use nalgebra::Matrix3;
use super::fundamental_arguments::FundamentalArguments;
use super::tables_gen::{
CipTerm, SXY2_POLY_UAS, SXY2_TERMS_0, SXY2_TERMS_1, SXY2_TERMS_2, SXY2_TERMS_3, SXY2_TERMS_4,
X_POLY_UAS, X_TERMS_0, X_TERMS_1, X_TERMS_2, X_TERMS_3, X_TERMS_4, Y_POLY_UAS, Y_TERMS_0,
Y_TERMS_1, Y_TERMS_2, Y_TERMS_3, Y_TERMS_4,
};
use super::{Rad, Uas};
#[derive(Debug)]
pub struct CipCoordinates {
pub x: Rad,
pub y: Rad,
pub s: Rad,
}
pub fn cip_xy(t: f64) -> (Rad, Rad) {
let fa = FundamentalArguments::evaluate(t);
let x_uas = evaluate_series(
t,
&fa,
X_POLY_UAS,
[X_TERMS_0, X_TERMS_1, X_TERMS_2, X_TERMS_3, X_TERMS_4],
);
let y_uas = evaluate_series(
t,
&fa,
Y_POLY_UAS,
[Y_TERMS_0, Y_TERMS_1, Y_TERMS_2, Y_TERMS_3, Y_TERMS_4],
);
(Uas::new(x_uas).to_radians(), Uas::new(y_uas).to_radians())
}
pub fn cio_locator_s(t: f64, x: Rad, y: Rad) -> Rad {
let fa = FundamentalArguments::evaluate(t);
let sxy2_uas = evaluate_series(
t,
&fa,
SXY2_POLY_UAS,
[
SXY2_TERMS_0,
SXY2_TERMS_1,
SXY2_TERMS_2,
SXY2_TERMS_3,
SXY2_TERMS_4,
],
);
let sxy2 = Uas::new(sxy2_uas).to_radians();
Rad::new(sxy2.raw() - x.raw() * y.raw() / 2.0)
}
pub fn cip_coordinates(t: f64) -> CipCoordinates {
let (x, y) = cip_xy(t);
let s = cio_locator_s(t, x, y);
CipCoordinates { x, y, s }
}
pub fn gcrs_to_cirs_matrix(x: Rad, y: Rad, s: Rad) -> Matrix3<f64> {
let x = x.raw();
let y = y.raw();
let s = s.raw();
let r2 = x * x + y * y;
let e = if r2 > 0.0 { y.atan2(x) } else { 0.0 };
let d = (r2 / (1.0 - r2)).sqrt().atan();
rotation_z(-(e + s)) * rotation_y(d) * rotation_z(e)
}
pub fn gcrs_to_cirs_matrix_at(t: f64) -> Matrix3<f64> {
let c = cip_coordinates(t);
gcrs_to_cirs_matrix(c.x, c.y, c.s)
}
#[inline]
pub(crate) fn rotation_z(psi: f64) -> Matrix3<f64> {
let (s, c) = psi.sin_cos();
Matrix3::new(
c, s, 0.0, -s, c, 0.0, 0.0, 0.0, 1.0,
)
}
#[inline]
pub(crate) fn rotation_y(theta: f64) -> Matrix3<f64> {
let (s, c) = theta.sin_cos();
Matrix3::new(
c, 0.0, -s, 0.0, 1.0, 0.0, s, 0.0, c,
)
}
#[inline]
pub(crate) fn rotation_x(phi: f64) -> Matrix3<f64> {
let (s, c) = phi.sin_cos();
Matrix3::new(
1.0, 0.0, 0.0, 0.0, c, s, 0.0, -s, c,
)
}
fn evaluate_series(
t: f64,
fa: &FundamentalArguments,
poly_uas: [f64; 6],
terms_by_power: [&[CipTerm]; 5],
) -> f64 {
let mut total = horner6(
t,
poly_uas[0],
poly_uas[1],
poly_uas[2],
poly_uas[3],
poly_uas[4],
poly_uas[5],
);
let mut t_power = 1.0_f64;
for group in terms_by_power {
let mut group_sum = 0.0_f64;
for term in group {
let arg = compute_argument(fa, &term.arg);
let (sin_arg, cos_arg) = arg.sin_cos();
group_sum += term.sin_uas * sin_arg + term.cos_uas * cos_arg;
}
total += group_sum * t_power;
t_power *= t;
}
total
}
#[inline]
fn compute_argument(fa: &FundamentalArguments, mults: &[i8; 14]) -> f64 {
let f: [f64; 14] = [
fa.l.raw(),
fa.l_prime.raw(),
fa.f.raw(),
fa.d.raw(),
fa.omega.raw(),
fa.l_me.raw(),
fa.l_ve.raw(),
fa.l_e.raw(),
fa.l_ma.raw(),
fa.l_j.raw(),
fa.l_sa.raw(),
fa.l_u.raw(),
fa.l_ne.raw(),
fa.p_a.raw(),
];
let mut arg = 0.0_f64;
for (n, &fk) in mults.iter().zip(f.iter()) {
arg += f64::from(*n) * fk;
}
arg
}
#[inline]
fn horner6(t: f64, c0: f64, c1: f64, c2: f64, c3: f64, c4: f64, c5: f64) -> f64 {
c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * c5))))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cip_coordinates_are_finite_across_wide_t_range() {
for &t in &[-1.0, -0.5, -0.1, 0.0, 0.1, 0.5, 1.0] {
let c = cip_coordinates(t);
assert!(c.x.is_finite(), "X({t}) = {:?}", c.x);
assert!(c.y.is_finite(), "Y({t}) = {:?}", c.y);
assert!(c.s.is_finite(), "s({t}) = {:?}", c.s);
}
}
#[test]
fn cip_coordinates_matches_separate_calls() {
for &t in &[-0.3, 0.0, 0.2, 0.7] {
let c = cip_coordinates(t);
let (x, y) = cip_xy(t);
let s = cio_locator_s(t, x, y);
assert_eq!(c.x.raw(), x.raw());
assert_eq!(c.y.raw(), y.raw());
assert_eq!(c.s.raw(), s.raw());
}
}
#[test]
fn cip_at_j2000_is_small_but_nonzero() {
let c = cip_coordinates(0.0);
assert!(c.x.raw().abs() < 1e-4);
assert!(c.y.raw().abs() < 1e-4);
assert!(c.s.raw().abs() < 1e-6);
assert!(c.x.raw() != 0.0);
assert!(c.y.raw() != 0.0);
}
#[test]
fn gcrs_to_cirs_matrix_is_orthogonal_with_determinant_one() {
for &t in &[-1.0, -0.5, -0.1, 0.0, 0.1, 0.5, 1.0] {
let m = gcrs_to_cirs_matrix_at(t);
let mt = m.transpose();
let should_be_identity = m * mt;
let identity = Matrix3::<f64>::identity();
for i in 0..3 {
for j in 0..3 {
let delta = (should_be_identity[(i, j)] - identity[(i, j)]).abs();
assert!(delta < 1e-14, "M·Mᵀ at t={t} [{i},{j}] off by {delta}");
}
}
let det = m.determinant();
assert!(
(det - 1.0).abs() < 1e-14,
"det(M) at t={t} = {det}, expected +1"
);
}
}
#[test]
fn gcrs_to_cirs_matrix_maps_cip_pole_to_intermediate_pole() {
use nalgebra::Vector3;
let c = cip_coordinates(0.5);
let m = gcrs_to_cirs_matrix(c.x, c.y, c.s);
let x = c.x.raw();
let y = c.y.raw();
let r2 = x * x + y * y;
let cip_in_gcrs = Vector3::new(x, y, (1.0 - r2).sqrt());
let mapped = m * cip_in_gcrs;
assert!((mapped.x).abs() < 1e-13, "mapped.x = {}", mapped.x);
assert!((mapped.y).abs() < 1e-13, "mapped.y = {}", mapped.y);
assert!((mapped.z - 1.0).abs() < 1e-13, "mapped.z = {}", mapped.z);
}
#[test]
fn gcrs_to_cirs_matrix_is_identity_when_xys_are_zero() {
let m = gcrs_to_cirs_matrix(Rad::new(0.0), Rad::new(0.0), Rad::new(0.0));
let identity = Matrix3::<f64>::identity();
for i in 0..3 {
for j in 0..3 {
assert_eq!(m[(i, j)], identity[(i, j)]);
}
}
}
}