use nalgebra::{Matrix3, Rotation3, Vector3};
use crate::{
earth_orientation::{obleq, prec, rnut80},
outfit_errors::OutfitError,
};
use super::constants::{EPS, T2000};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RefEpoch {
J2000,
Epoch(f64),
}
impl RefEpoch {
pub fn date(&self) -> f64 {
match *self {
RefEpoch::J2000 => T2000,
RefEpoch::Epoch(d) => d,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RefSystem {
Equm(RefEpoch),
Equt(RefEpoch),
Eclm(RefEpoch),
}
impl RefSystem {
pub fn epoch(&self) -> RefEpoch {
match *self {
RefSystem::Equm(e) => e,
RefSystem::Equt(e) => e,
RefSystem::Eclm(e) => e,
}
}
pub fn variant_eq(&self, other: &RefSystem) -> bool {
matches!(
(self, other),
(RefSystem::Equm(_), RefSystem::Equm(_))
| (RefSystem::Equt(_), RefSystem::Equt(_))
| (RefSystem::Eclm(_), RefSystem::Eclm(_))
)
}
fn transform_to_equm_date(
&self,
target_epoch: RefEpoch,
) -> Result<(RefSystem, Matrix3<f64>), OutfitError> {
match self.epoch() {
RefEpoch::J2000 => match self {
RefSystem::Eclm(e) => Ok((RefSystem::Equm(*e), rotmt(-obleq(T2000), 1))),
RefSystem::Equt(e) => Ok((RefSystem::Equm(*e), rnut80(T2000).transpose())),
RefSystem::Equm(_) => match target_epoch {
RefEpoch::J2000 => Err(OutfitError::InvalidRefSystem(
"Cannot convert Equm to Equm J2000, already in that system".into(),
)),
RefEpoch::Epoch(target_date) => Ok((
RefSystem::Equm(RefEpoch::Epoch(target_date)),
prec(target_date),
)),
},
},
RefEpoch::Epoch(_) => match self {
RefSystem::Eclm(e) => Ok((RefSystem::Equm(*e), rotmt(-obleq(e.date()), 1))),
RefSystem::Equt(e) => Ok((RefSystem::Equm(*e), rnut80(e.date()).transpose())),
RefSystem::Equm(e) => {
Ok((RefSystem::Equm(RefEpoch::J2000), prec(e.date()).transpose()))
}
},
}
}
fn transform_to_target_system(
&self,
target_system: RefSystem,
) -> Result<(RefSystem, Matrix3<f64>), OutfitError> {
match self {
RefSystem::Equt(e) => Ok((RefSystem::Equm(*e), rnut80(e.date()).transpose())),
RefSystem::Eclm(e) => Ok((RefSystem::Equm(*e), rotmt(-obleq(e.date()), 0))),
RefSystem::Equm(e) => match target_system {
RefSystem::Equt(_) => Ok((RefSystem::Equt(*e), rnut80(e.date()))),
RefSystem::Eclm(_) => Ok((RefSystem::Eclm(*e), rotmt(obleq(e.date()), 0))),
RefSystem::Equm(_) => Err(OutfitError::InvalidRefSystem(
"Cannot convert Equm to Equm, already in that system".into(),
)),
},
}
}
}
pub fn rotpn(src: &RefSystem, dst: &RefSystem) -> Result<Matrix3<f64>, OutfitError> {
let mut current = *src;
let mut rotation = Matrix3::identity();
for _ in 0..20 {
let epochs_equal = match (current.epoch(), dst.epoch()) {
(RefEpoch::J2000, RefEpoch::J2000) => true,
(e1, e2) => (e1.date() - e2.date()).abs() <= EPS,
};
if !epochs_equal {
let (next_ref, step_rot) = current.transform_to_equm_date(dst.epoch())?;
rotation *= step_rot;
current = next_ref;
continue;
}
if current.variant_eq(dst) {
return Ok(rotation);
}
let (next_ref, step_rot) = current.transform_to_target_system(*dst)?;
rotation *= step_rot;
current = next_ref;
}
Err(OutfitError::InvalidRefSystem(
"Transformation did not converge in 20 iterations".into(),
))
}
pub fn rotmt(alpha: f64, k: usize) -> Matrix3<f64> {
let axis = match k {
0 => Vector3::x_axis(),
1 => Vector3::y_axis(),
2 => Vector3::z_axis(),
_ => panic!("**** ROTMT: invalid axis index {k} (must be 0,1,2) ****"),
};
Rotation3::from_axis_angle(&axis, alpha).into()
}
#[cfg(test)]
mod ref_system_test {
use super::*;
use approx::assert_relative_eq;
fn assert_matrix_eq(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3], tol: f64) {
for i in 0..3 {
for j in 0..3 {
assert_relative_eq!(a[i][j], b[i][j], epsilon = tol);
}
}
}
const TOLERANCE: f64 = 1e-10;
#[test]
fn test_rotpn_equm() {
let ref_roteqec = [
[1.0, 0.0, 0.0],
[0.0, 0.9174820620691818, 0.3977771559319137],
[0.0, -0.3977771559319137, 0.9174820620691818],
];
let ref_sys1 = RefSystem::Equm(RefEpoch::J2000);
let ref_sys2 = RefSystem::Eclm(RefEpoch::J2000);
let roteqec = rotpn(&ref_sys1, &ref_sys2).unwrap();
assert_eq!(roteqec, ref_roteqec.into());
let ref_roteqec = [
[
0.9999999977217079,
6.19323109890795e-5,
2.6850942970991024e-5,
],
[
-6.193306258211379e-5,
0.9999999976903892,
2.799138089948361e-5,
],
[
-2.6849209338068913e-5,
-2.7993043796858963e-5,
0.9999999992477547,
],
];
let roteqec = rotpn(&ref_sys1, &RefSystem::Equt(RefEpoch::J2000)).unwrap();
assert_eq!(roteqec, ref_roteqec.into());
}
#[test]
fn test_rotpn_eclm() {
let ref_roteqec = [
[1.0, 0.0, 0.0],
[0.0, 0.9174820620691818, -0.3977771559319137],
[0.0, 0.3977771559319137, 0.9174820620691818],
];
let ref_sys1 = RefSystem::Eclm(RefEpoch::J2000);
let ref_sys2 = RefSystem::Equm(RefEpoch::J2000);
let roteqec = rotpn(&ref_sys1, &ref_sys2).unwrap();
assert_eq!(roteqec, ref_roteqec.into());
let ref_roteqec = [
[
0.9999999977217079,
6.750247612406132e-5,
-3.3881317890172014e-21,
],
[
-6.193306258211379e-5,
0.9174931942820401,
-0.39775147342333544,
],
[
-2.6849209338068913e-5,
0.3977514725171414,
0.9174931963723576,
],
];
let roteqec = rotpn(&ref_sys1, &RefSystem::Equt(RefEpoch::J2000)).unwrap();
assert_eq!(roteqec, ref_roteqec.into());
}
#[test]
fn test_rotpn_equt() {
let ref_roteqec = [
[
0.9999999977217079,
-6.193306258211379e-5,
-2.6849209338068913e-5,
],
[
6.19323109890795e-5,
0.9999999976903892,
-2.7993043796858963e-5,
],
[
2.6850942970991024e-5,
2.799138089948361e-5,
0.9999999992477547,
],
];
let ref_sys1 = RefSystem::Equt(RefEpoch::J2000);
let ref_sys2 = RefSystem::Equm(RefEpoch::J2000);
let roteqec = rotpn(&ref_sys1, &ref_sys2).unwrap();
assert_eq!(roteqec, ref_roteqec.into());
let ref_roteqec = [
[
0.9999999977217079,
-6.193306258211379e-5,
-2.6849209338068913e-5,
],
[6.750247612406132e-5, 0.9174931942820401, 0.3977514725171414],
[
-3.3881317890172014e-21,
-0.39775147342333544,
0.9174931963723576,
],
];
let roteqec = rotpn(&ref_sys1, &RefSystem::Eclm(RefEpoch::J2000)).unwrap();
assert_eq!(roteqec, ref_roteqec.into());
}
#[test]
fn test_rotpn_ofdate() {
let date1 = 60000.0;
let date2 = 61000.0;
let ref_sys1 = RefSystem::Equm(RefEpoch::Epoch(date1));
let ref_sys2 = RefSystem::Equm(RefEpoch::Epoch(date2));
let rot = rotpn(&ref_sys1, &ref_sys2).unwrap();
let tol = 1e-12;
let mut is_identity = true;
#[allow(clippy::needless_range_loop)]
for i in 0..3 {
#[allow(clippy::needless_range_loop)]
for j in 0..3 {
let expected = if i == j { 1.0 } else { 0.0 };
if (rot[(i, j)] - expected).abs() > tol {
is_identity = false;
}
}
}
assert!(
!is_identity,
"Rotation matrix should not be identity when date1 != date2 for OFDATE"
);
let delta = (1.0 - rot[(1, 1)]).abs();
assert!(
delta > 1e-7,
"rot[1][1] difference too small: {delta}, expected > 1e-7"
);
}
#[test]
fn test_rotpn_equt_of_date() {
let ref_sys1 = RefSystem::Equt(RefEpoch::Epoch(60725.5));
let ref_sys2 = RefSystem::Equm(RefEpoch::Epoch(60730.5));
let roteqec = rotpn(&ref_sys1, &ref_sys2).unwrap();
let expected = [
[
0.9999999999959558,
2.6103210920298055e-6,
1.1287777487165376e-6,
],
[
-2.610372560299571e-6,
0.9999999989569648,
4.559886322796942e-5,
],
[
-1.1286587198650923e-6,
-4.559886617430879e-5,
0.9999999989597347,
],
];
assert_matrix_eq(&roteqec.into(), &expected, TOLERANCE);
let ref_roteqec = [
[
0.9999999999959558,
2.6103210920298055e-6,
1.1287777487165376e-6,
],
[
-2.8439248114746454e-6,
0.9174866295910213,
0.3977666206629458,
],
[
2.660107394168916e-9,
-0.3977666206645475,
0.9174866295947346,
],
];
let ref_sys2 = RefSystem::Eclm(RefEpoch::Epoch(60730.5));
let roteqec = rotpn(&ref_sys1, &ref_sys2).unwrap();
assert_matrix_eq(&roteqec.into(), &ref_roteqec, TOLERANCE);
}
#[test]
fn test_rotpn_equm_of_date() {
let ref_roteqec = [
[
0.9999999999382557,
-1.019473782042265e-5,
-4.422167976508847e-6,
],
[
1.0194536102237101e-5,
0.9999999989077697,
-4.561284900943888e-5,
],
[
4.4226329827165825e-6,
4.561280392464384e-5,
0.9999999989499561,
],
];
let ref_sys1 = RefSystem::Equm(RefEpoch::Epoch(60725.5));
let ref_sys2 = RefSystem::Equt(RefEpoch::Epoch(60730.5));
let roteqec = rotpn(&ref_sys1, &ref_sys2).unwrap();
assert_matrix_eq(&roteqec.into(), &ref_roteqec, TOLERANCE);
let ref_roteqec = [
[
0.9999999999944286,
-3.0616188567489498e-6,
-1.330066112371995e-6,
],
[
3.3380501509251515e-6,
0.9175047663420967,
0.39772478390011357,
],
[
2.660299467132395e-9,
-0.39772478390233756,
0.9175047663472047,
],
];
let ref_sys2 = RefSystem::Eclm(RefEpoch::Epoch(60730.5));
let roteqec = rotpn(&ref_sys1, &ref_sys2).unwrap();
assert_matrix_eq(&roteqec.into(), &ref_roteqec, TOLERANCE);
}
#[test]
fn test_rotpn_eclm_of_date() {
let ref_roteqec = [
[
0.9175052829851363,
-3.0616188567489498e-6,
0.3977235920648803,
],
[
2.809050665755966e-6,
0.9999999999953132,
1.2176799173935054e-6,
],
[
-0.3977235920667443,
-2.0361171295958094e-12,
0.9175052829894363,
],
];
let ref_sys1 = RefSystem::Eclm(RefEpoch::Epoch(60725.5));
let ref_sys2 = RefSystem::Equm(RefEpoch::Epoch(60730.5));
let roteqec = rotpn(&ref_sys1, &ref_sys2).unwrap();
assert_matrix_eq(&roteqec.into(), &ref_roteqec, TOLERANCE);
let ref_roteqec = [
[
0.9175065127392313,
-1.019473782042265e-5,
0.3977207550243788,
],
[
2.7494897154247754e-5,
0.9999999989077697,
-3.779538585032655e-5,
],
[-0.397720754204662, 4.561280392464384e-5, 0.9175065120174062],
];
let ref_sys2 = RefSystem::Equt(RefEpoch::Epoch(60730.5));
let roteqec = rotpn(&ref_sys1, &ref_sys2).unwrap();
assert_matrix_eq(&roteqec.into(), &ref_roteqec, TOLERANCE);
}
#[test]
fn test_rotpn_equt_eclm_date() {
let ref_roteqec = [
[
0.9999932036120499,
0.003381495004957589,
0.0014690885747894438,
],
[
-0.0036868307528666357,
0.9174941827437706,
0.3977321107357815,
],
[
-2.9510755403679666e-6,
-0.3977348238749929,
0.917500414097138,
],
];
let tmjd = 57028.479297592596;
let ref_sys1 = RefSystem::Equt(RefEpoch::Epoch(tmjd));
let ref_sys2 = RefSystem::Eclm(RefEpoch::J2000);
let roteqec = rotpn(&ref_sys1, &ref_sys2).unwrap();
assert_relative_eq!(roteqec, ref_roteqec.into(), epsilon = 1e-17);
}
#[test]
fn test_rotpn_identity_cases() {
let ref_sys1 = RefSystem::Equm(RefEpoch::J2000);
let ref_sys2 = RefSystem::Equm(RefEpoch::J2000);
let roteqec = rotpn(&ref_sys1, &ref_sys2).unwrap();
assert_eq!(roteqec, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]].into());
let ref_sys1 = RefSystem::Eclm(RefEpoch::Epoch(60000.));
let ref_sys2 = RefSystem::Eclm(RefEpoch::Epoch(60000.));
let roteqec = rotpn(&ref_sys1, &ref_sys2).unwrap();
assert_eq!(roteqec, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]].into());
let ref_sys1 = RefSystem::Equt(RefEpoch::Epoch(60000.));
let ref_sys2 = RefSystem::Equt(RefEpoch::Epoch(60000.));
let roteqec = rotpn(&ref_sys1, &ref_sys2).unwrap();
assert_eq!(roteqec, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]].into());
}
#[test]
fn test_rotpn_inverse_transform() {
let ref_sys1 = RefSystem::Equm(RefEpoch::J2000);
let ref_sys2 = RefSystem::Eclm(RefEpoch::J2000);
let roteqec = rotpn(&ref_sys1, &ref_sys2).unwrap();
let roteqec2 = rotpn(&ref_sys2, &ref_sys1).unwrap();
let prod = roteqec2 * roteqec;
#[allow(clippy::needless_range_loop)]
for i in 0..3 {
#[allow(clippy::needless_range_loop)]
for j in 0..3 {
if i == j {
assert!((prod[(i, j)] - 1.0).abs() < 1e-12);
} else {
assert!(prod[(i, j)].abs() < 1e-12);
}
}
}
}
#[test]
fn test_rotpn_large_epoch_difference() {
let ref_sys1 = RefSystem::Equm(RefEpoch::Epoch(80000.));
let ref_sys2 = RefSystem::Equm(RefEpoch::J2000);
let roteqec = rotpn(&ref_sys1, &ref_sys2).unwrap();
let prod = roteqec * roteqec.transpose();
#[allow(clippy::needless_range_loop)]
for i in 0..3 {
#[allow(clippy::needless_range_loop)]
for j in 0..3 {
if i == j {
assert!((prod[(i, j)] - 1.0).abs() < 1e-12);
} else {
assert!(prod[(i, j)].abs() < 1e-12);
}
}
}
}
#[test]
fn test_rotpn_round_trip_equt_equm() {
let ref_sys1 = RefSystem::Equt(RefEpoch::Epoch(60725.5));
let ref_sys2 = RefSystem::Equm(RefEpoch::Epoch(60730.5));
let forward = rotpn(&ref_sys1, &ref_sys2).unwrap();
let backward = rotpn(&ref_sys2, &ref_sys1).unwrap();
let prod = backward * forward;
let tol = 1e-5;
#[allow(clippy::needless_range_loop)]
for i in 0..3 {
#[allow(clippy::needless_range_loop)]
for j in 0..3 {
if i == j {
assert!((prod[(i, j)] - 1.0).abs() < tol);
} else {
assert!(prod[(i, j)].abs() < tol);
}
}
}
}
}