use crate::astro::precession;
use crate::coordinates::cartesian::Direction;
use crate::coordinates::frames::{EclipticTrueOfDate, EquatorialMeanOfDate, GCRS, ICRS};
use crate::time::JulianDate;
use qtty::{Degrees, Radians};
use std::f64::consts::TAU;
pub trait ToEclipticTrueOfDate {
fn to_ecliptic_of_date(&self, jd_tt: &JulianDate) -> Direction<EclipticTrueOfDate>;
}
impl ToEclipticTrueOfDate for Direction<EquatorialMeanOfDate> {
#[inline]
fn to_ecliptic_of_date(&self, jd_tt: &JulianDate) -> Direction<EclipticTrueOfDate> {
let spherical = self.to_spherical();
let ra = Radians::from(spherical.azimuth);
let dec = Radians::from(spherical.polar);
let (sin_dec, cos_dec) = dec.sin_cos();
let (sin_ra, cos_ra) = ra.sin_cos();
let v_eq = [cos_dec * cos_ra, cos_dec * sin_ra, sin_dec];
let rot = precession::mean_equatorial_to_ecliptic_of_date_matrix(*jd_tt);
let v_ecl = rot.apply_array(v_eq);
let lon = v_ecl[1].atan2(v_ecl[0]).rem_euclid(TAU);
let lat = v_ecl[2].clamp(-1.0, 1.0).asin();
let spherical_ecl = affn::spherical::Direction::<EclipticTrueOfDate>::new_raw(
Degrees::new(lat.to_degrees()),
Degrees::new(lon.to_degrees()),
);
spherical_ecl.to_cartesian()
}
}
impl ToEclipticTrueOfDate for Direction<ICRS> {
#[inline]
fn to_ecliptic_of_date(&self, jd_tt: &JulianDate) -> Direction<EclipticTrueOfDate> {
let spherical = self.to_spherical();
let ra = Radians::from(spherical.azimuth);
let dec = Radians::from(spherical.polar);
let (sin_dec, cos_dec) = dec.sin_cos();
let (sin_ra, cos_ra) = ra.sin_cos();
let v_eq = [cos_dec * cos_ra, cos_dec * sin_ra, sin_dec];
let rot = precession::gcrs_to_ecliptic_of_date_matrix(*jd_tt);
let v_ecl = rot.apply_array(v_eq);
let lon = v_ecl[1].atan2(v_ecl[0]).rem_euclid(TAU);
let lat = v_ecl[2].clamp(-1.0, 1.0).asin();
let spherical_ecl = affn::spherical::Direction::<EclipticTrueOfDate>::new_raw(
Degrees::new(lat.to_degrees()),
Degrees::new(lon.to_degrees()),
);
spherical_ecl.to_cartesian()
}
}
impl ToEclipticTrueOfDate for Direction<GCRS> {
#[inline]
fn to_ecliptic_of_date(&self, jd_tt: &JulianDate) -> Direction<EclipticTrueOfDate> {
let spherical = self.to_spherical();
let ra = Radians::from(spherical.azimuth);
let dec = Radians::from(spherical.polar);
let (sin_dec, cos_dec) = dec.sin_cos();
let (sin_ra, cos_ra) = ra.sin_cos();
let v_eq = [cos_dec * cos_ra, cos_dec * sin_ra, sin_dec];
let rot = precession::gcrs_to_ecliptic_of_date_matrix(*jd_tt);
let v_ecl = rot.apply_array(v_eq);
let lon = v_ecl[1].atan2(v_ecl[0]).rem_euclid(TAU);
let lat = v_ecl[2].clamp(-1.0, 1.0).asin();
let spherical_ecl = affn::spherical::Direction::<EclipticTrueOfDate>::new_raw(
Degrees::new(lat.to_degrees()),
Degrees::new(lon.to_degrees()),
);
spherical_ecl.to_cartesian()
}
}
pub trait FromEclipticTrueOfDate {
fn to_equatorial_mean_of_date(&self, jd_tt: &JulianDate) -> Direction<EquatorialMeanOfDate>;
fn to_icrs(&self, jd_tt: &JulianDate) -> Direction<ICRS>;
}
impl FromEclipticTrueOfDate for Direction<EclipticTrueOfDate> {
#[inline]
fn to_equatorial_mean_of_date(&self, jd_tt: &JulianDate) -> Direction<EquatorialMeanOfDate> {
let spherical = self.to_spherical();
let lon = Radians::from(spherical.azimuth);
let lat = Radians::from(spherical.polar);
let (sin_lat, cos_lat) = lat.sin_cos();
let (sin_lon, cos_lon) = lon.sin_cos();
let v_ecl = [cos_lat * cos_lon, cos_lat * sin_lon, sin_lat];
let rot = precession::ecliptic_of_date_to_mean_equatorial_matrix(*jd_tt);
let v_eq = rot.apply_array(v_ecl);
let ra = v_eq[1].atan2(v_eq[0]).rem_euclid(TAU);
let dec = v_eq[2].clamp(-1.0, 1.0).asin();
let spherical_equ = affn::spherical::Direction::<EquatorialMeanOfDate>::new_raw(
Degrees::new(dec.to_degrees()),
Degrees::new(ra.to_degrees()),
);
spherical_equ.to_cartesian()
}
#[inline]
fn to_icrs(&self, jd_tt: &JulianDate) -> Direction<ICRS> {
let spherical = self.to_spherical();
let lon = Radians::from(spherical.azimuth);
let lat = Radians::from(spherical.polar);
let (sin_lat, cos_lat) = lat.sin_cos();
let (sin_lon, cos_lon) = lon.sin_cos();
let v_ecl = [cos_lat * cos_lon, cos_lat * sin_lon, sin_lat];
let rot = precession::ecliptic_of_date_to_gcrs_matrix(*jd_tt);
let v_eq = rot.apply_array(v_ecl);
let ra = v_eq[1].atan2(v_eq[0]).rem_euclid(TAU);
let dec = v_eq[2].clamp(-1.0, 1.0).asin();
let spherical_equ = affn::spherical::Direction::<ICRS>::new_raw(
Degrees::new(dec.to_degrees()),
Degrees::new(ra.to_degrees()),
);
spherical_equ.to_cartesian()
}
}
#[cfg(test)]
mod tests {
use super::*;
use qtty::*;
#[test]
fn roundtrip_mean_of_date_is_stable() {
let jd_tt = JulianDate::new(2_451_545.0);
let ra = 1.0 * RAD;
let dec = 0.5 * RAD;
let spherical_equ = affn::spherical::Direction::<EquatorialMeanOfDate>::new_raw(
Degrees::new(dec.value().to_degrees()),
Degrees::new(ra.value().to_degrees()),
);
let equatorial = spherical_equ.to_cartesian();
let ecliptic = equatorial.to_ecliptic_of_date(&jd_tt);
let back = ecliptic.to_equatorial_mean_of_date(&jd_tt);
let orig_sph = equatorial.to_spherical();
let back_sph = back.to_spherical();
let dra = (Radians::from(back_sph.azimuth).value()
- Radians::from(orig_sph.azimuth).value())
.abs();
let dra = dra.min(TAU - dra);
let ddec =
(Radians::from(back_sph.polar).value() - Radians::from(orig_sph.polar).value()).abs();
assert!(dra < 1e-12, "RA roundtrip error too large: {dra}");
assert!(ddec < 1e-12, "Dec roundtrip error too large: {ddec}");
}
#[test]
fn roundtrip_icrs_is_stable() {
let jd_tt = JulianDate::new(2_451_545.0);
let ra = 1.0 * RAD;
let dec = 0.5 * RAD;
let spherical_icrs = affn::spherical::Direction::<ICRS>::new_raw(
Degrees::new(dec.value().to_degrees()),
Degrees::new(ra.value().to_degrees()),
);
let icrs = spherical_icrs.to_cartesian();
let ecliptic = icrs.to_ecliptic_of_date(&jd_tt);
let back = ecliptic.to_icrs(&jd_tt);
let orig_sph = icrs.to_spherical();
let back_sph = back.to_spherical();
let dra = (Radians::from(back_sph.azimuth).value()
- Radians::from(orig_sph.azimuth).value())
.abs();
let dra = dra.min(TAU - dra);
let ddec =
(Radians::from(back_sph.polar).value() - Radians::from(orig_sph.polar).value()).abs();
assert!(dra < 1e-12, "RA roundtrip error too large: {dra}");
assert!(ddec < 1e-12, "Dec roundtrip error too large: {ddec}");
}
#[test]
fn ecliptic_of_date_has_correct_obliquity() {
let jd_tt = JulianDate::J2000;
let spherical_equ =
affn::spherical::Direction::<EquatorialMeanOfDate>::new_raw(0.0 * DEG, 0.0 * DEG);
let equatorial = spherical_equ.to_cartesian();
let ecliptic = equatorial.to_ecliptic_of_date(&jd_tt);
let sph = ecliptic.to_spherical();
assert!(
Radians::from(sph.polar).abs() < 1e-10 * RAD,
"Expected ecliptic latitude near 0"
);
let spherical_north =
affn::spherical::Direction::<EquatorialMeanOfDate>::new_raw(90.0 * DEG, 0.0 * DEG);
let north_pole = spherical_north.to_cartesian();
let ecl_north = north_pole.to_ecliptic_of_date(&jd_tt);
let sph_north = ecl_north.to_spherical();
let obliquity_rad = 23.439279444444445_f64.to_radians();
let expected_lat = std::f64::consts::FRAC_PI_2 - obliquity_rad;
assert!(
(Radians::from(sph_north.polar).value() - expected_lat).abs() < 1e-6,
"EclipticMeanJ2000 latitude mismatch at north pole"
);
}
}