use std::{f64::consts::TAU, fmt};
use crate::{
Degrees, Radians,
coordinates::{
COS_OBL, RXY_MIN, SIN_OBL,
cov2::Cov2,
equatorial::{EquCoord, EquCoordCov},
},
};
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EclipticCoord {
pub lon: Radians,
pub lon_error: Radians,
pub lat: Radians,
pub lat_error: Radians,
}
impl EclipticCoord {
#[inline]
pub fn new(lon: Radians, lon_error: Radians, lat: Radians, lat_error: Radians) -> Self {
Self {
lon,
lon_error,
lat,
lat_error,
}
}
#[inline]
pub fn from_degrees(
lon_deg: Degrees,
lon_error_deg: Degrees,
lat_deg: Degrees,
lat_error_deg: Degrees,
) -> Self {
Self {
lon: lon_deg.to_radians(),
lon_error: lon_error_deg.to_radians(),
lat: lat_deg.to_radians(),
lat_error: lat_error_deg.to_radians(),
}
}
#[inline]
pub fn to_degrees(&self) -> (Degrees, Degrees) {
(self.lon.to_degrees(), self.lat.to_degrees())
}
#[inline]
pub fn error_in_degrees(&self) -> (Degrees, Degrees) {
(self.lon_error.to_degrees(), self.lat_error.to_degrees())
}
}
impl fmt::Display for EclipticCoord {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let lon_deg = self.lon.to_degrees();
let lat_deg = self.lat.to_degrees();
let lon_err_arcsec = self.lon_error.to_degrees() * 3600.0;
let lat_err_arcsec = self.lat_error.to_degrees() * 3600.0;
write!(
f,
"lon: {lon_deg:.6} deg ± {lon_err_arcsec:.3} arcsec, \
lat: {lat_deg:.6} deg ± {lat_err_arcsec:.3} arcsec"
)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EclipticCoordCov {
pub coord: EclipticCoord,
pub cov: Cov2,
}
impl EclipticCoordCov {
#[inline]
pub fn new(coord: EclipticCoord, cov: Cov2) -> Self {
Self { coord, cov }
}
#[inline]
pub fn from_ecl(c: EclipticCoord) -> Self {
let cov = Cov2 {
xx: c.lon_error * c.lon_error,
yy: c.lat_error * c.lat_error,
xy: 0.0,
};
Self { coord: c, cov }
}
}
#[inline]
fn rotate_to_ecliptic(x: f64, y: f64, z: f64) -> (f64, f64, f64) {
(x, COS_OBL * y + SIN_OBL * z, -SIN_OBL * y + COS_OBL * z)
}
#[inline]
fn rotate_to_equatorial(xe: f64, ye: f64, ze: f64) -> (f64, f64, f64) {
(xe, COS_OBL * ye - SIN_OBL * ze, SIN_OBL * ye + COS_OBL * ze)
}
#[inline]
fn ecl_cart_to_lonlat(xe: f64, ye: f64, ze: f64) -> (f64, f64) {
let lon = ye.atan2(xe).rem_euclid(TAU);
let lat = ze.atan2(xe.hypot(ye));
(lon, lat)
}
#[inline]
fn jac_cart_to_ecl(xe: f64, ye: f64, ze: f64) -> [[f64; 3]; 2] {
let rxy2 = (xe * xe + ye * ye).max(RXY_MIN * RXY_MIN);
let rxy = rxy2.sqrt();
[
[-ye / rxy2, xe / rxy2, 0.0],
[-xe * ze / rxy, -ye * ze / rxy, rxy],
]
}
impl From<&EquCoord> for EclipticCoord {
fn from(c: &EquCoord) -> Self {
let (sdec, cdec) = c.dec.sin_cos();
let (sra, cra) = c.ra.sin_cos();
let xeq = cdec * cra;
let yeq = cdec * sra;
let zeq = sdec;
let (xe, ye, ze) = rotate_to_ecliptic(xeq, yeq, zeq);
let (lon, lat) = ecl_cart_to_lonlat(xe, ye, ze);
EclipticCoord::new(lon, 0.0, lat, 0.0)
}
}
impl From<EquCoord> for EclipticCoord {
#[inline]
fn from(c: EquCoord) -> Self {
Self::from(&c)
}
}
impl From<&EclipticCoord> for EquCoord {
fn from(c: &EclipticCoord) -> Self {
let (slat, clat) = c.lat.sin_cos();
let (slon, clon) = c.lon.sin_cos();
let xe = clat * clon;
let ye = clat * slon;
let ze = slat;
let (xeq, yeq, zeq) = rotate_to_equatorial(xe, ye, ze);
let rho = xeq.hypot(yeq);
let dec = zeq.atan2(rho);
let ra = yeq.atan2(xeq).rem_euclid(TAU);
EquCoord::new(ra, 0.0, dec, 0.0)
}
}
impl From<EclipticCoord> for EquCoord {
#[inline]
fn from(c: EclipticCoord) -> Self {
Self::from(&c)
}
}
impl From<EquCoordCov> for EclipticCoordCov {
fn from(ec: EquCoordCov) -> Self {
let (sdec, cdec) = ec.coord.dec.sin_cos();
let (sra, cra) = ec.coord.ra.sin_cos();
let xeq = cdec * cra;
let yeq = cdec * sra;
let zeq = sdec;
let (xe, ye, ze) = rotate_to_ecliptic(xeq, yeq, zeq);
let (lon, lat) = ecl_cart_to_lonlat(xe, ye, ze);
let j_c2e = jac_cart_to_ecl(xe, ye, ze);
let m00 = -cdec * sra; let m01 = -sdec * cra; let m10 = cdec * cra; let m11 = -sdec * sra; let m20 = 0.0; let m21 = cdec;
let rxj: [[f64; 2]; 3] = [
[m00, m01],
[COS_OBL * m10 + SIN_OBL * m20, COS_OBL * m11 + SIN_OBL * m21],
[
-SIN_OBL * m10 + COS_OBL * m20,
-SIN_OBL * m11 + COS_OBL * m21,
],
];
let mut jtot = [[0.0f64; 2]; 2];
for i in 0..2 {
for k in 0..2 {
for j in 0..3 {
jtot[i][k] += j_c2e[i][j] * rxj[j][k];
}
}
}
let s = &ec.cov;
let cov2 = Cov2 {
xx: jtot[0][0] * jtot[0][0] * s.xx
+ 2.0 * jtot[0][0] * jtot[0][1] * s.xy
+ jtot[0][1] * jtot[0][1] * s.yy,
yy: jtot[1][0] * jtot[1][0] * s.xx
+ 2.0 * jtot[1][0] * jtot[1][1] * s.xy
+ jtot[1][1] * jtot[1][1] * s.yy,
xy: jtot[0][0] * jtot[1][0] * s.xx
+ (jtot[0][0] * jtot[1][1] + jtot[0][1] * jtot[1][0]) * s.xy
+ jtot[0][1] * jtot[1][1] * s.yy,
};
let coord = EclipticCoord::new(lon, cov2.xx.sqrt(), lat, cov2.yy.sqrt());
EclipticCoordCov::new(coord, cov2)
}
}
impl From<EclipticCoordCov> for EquCoordCov {
fn from(ec: EclipticCoordCov) -> Self {
let (slat, clat) = ec.coord.lat.sin_cos();
let (slon, clon) = ec.coord.lon.sin_cos();
let xe = clat * clon;
let ye = clat * slon;
let ze = slat;
let (xeq, yeq, zeq) = rotate_to_equatorial(xe, ye, ze);
let rho2 = (xeq * xeq + yeq * yeq).max(RXY_MIN * RXY_MIN);
let rho = rho2.sqrt();
let ra = yeq.atan2(xeq).rem_euclid(TAU);
let dec = zeq.atan2(rho);
let j_c2q: [[f64; 3]; 2] = [
[-yeq / rho2, xeq / rho2, 0.0],
[-xeq * zeq / rho, -yeq * zeq / rho, rho],
];
let n00 = -clat * slon; let n01 = -slat * clon; let n10 = clat * clon; let n11 = -slat * slon; let n20 = 0.0; let n21 = clat;
let rxinv_j: [[f64; 2]; 3] = [
[n00, n01],
[COS_OBL * n10 - SIN_OBL * n20, COS_OBL * n11 - SIN_OBL * n21],
[SIN_OBL * n10 + COS_OBL * n20, SIN_OBL * n11 + COS_OBL * n21],
];
let mut jtot = [[0.0f64; 2]; 2];
for i in 0..2 {
for k in 0..2 {
for j in 0..3 {
jtot[i][k] += j_c2q[i][j] * rxinv_j[j][k];
}
}
}
let s = &ec.cov;
let cov2 = Cov2 {
xx: jtot[0][0] * jtot[0][0] * s.xx
+ 2.0 * jtot[0][0] * jtot[0][1] * s.xy
+ jtot[0][1] * jtot[0][1] * s.yy,
yy: jtot[1][0] * jtot[1][0] * s.xx
+ 2.0 * jtot[1][0] * jtot[1][1] * s.xy
+ jtot[1][1] * jtot[1][1] * s.yy,
xy: jtot[0][0] * jtot[1][0] * s.xx
+ (jtot[0][0] * jtot[1][1] + jtot[0][1] * jtot[1][0]) * s.xy
+ jtot[0][1] * jtot[1][1] * s.yy,
};
let coord = EquCoord::new(ra, cov2.xx.sqrt(), dec, cov2.yy.sqrt());
EquCoordCov::new(coord, cov2)
}
}
impl From<EclipticCoord> for EquCoordCov {
#[inline]
fn from(c: EclipticCoord) -> Self {
EquCoordCov::from(EclipticCoordCov::from_ecl(c))
}
}
impl From<EquCoord> for EclipticCoordCov {
#[inline]
fn from(c: EquCoord) -> Self {
EclipticCoordCov::from(EquCoordCov::from_equ(c))
}
}
#[cfg(test)]
mod ecliptic_coord_tests {
use crate::coordinates::OBLIQUITY_J2000;
use super::*;
use approx::assert_abs_diff_eq;
use std::f64::consts::FRAC_PI_2;
fn equ(ra_deg: f64, dec_deg: f64, ra_err_arcsec: f64, dec_err_arcsec: f64) -> EquCoord {
EquCoord::from_degrees(
ra_deg,
ra_err_arcsec / 3600.0,
dec_deg,
dec_err_arcsec / 3600.0,
)
}
#[test]
fn roundtrip_position_vernal_equinox() {
let eq = equ(0.0, 0.0, 0.0, 0.0);
let ecl = EclipticCoord::from(eq);
assert_abs_diff_eq!(ecl.lon, 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(ecl.lat, 0.0, epsilon = 1e-12);
let eq2 = EquCoord::from(ecl);
assert_abs_diff_eq!(eq2.ra, eq.ra, epsilon = 1e-12);
assert_abs_diff_eq!(eq2.dec, eq.dec, epsilon = 1e-12);
}
#[test]
fn roundtrip_position_arbitrary() {
let eq = equ(123.456, 34.567, 0.0, 0.0);
let ecl = EclipticCoord::from(eq);
let eq2 = EquCoord::from(ecl);
assert_abs_diff_eq!(eq2.ra, eq.ra, epsilon = 1e-7);
assert_abs_diff_eq!(eq2.dec, eq.dec, epsilon = 1e-7);
}
#[test]
fn roundtrip_position_negative_dec() {
let eq = equ(270.0, -45.0, 0.0, 0.0);
let ecl = EclipticCoord::from(eq);
let eq2 = EquCoord::from(ecl);
assert_abs_diff_eq!(eq2.ra, eq.ra, epsilon = 1e-11);
assert_abs_diff_eq!(eq2.dec, eq.dec, epsilon = 1e-11);
}
#[test]
fn north_ecliptic_pole_lat() {
let dec_pole = FRAC_PI_2 - OBLIQUITY_J2000;
let eq = EquCoord::new(270_f64.to_radians(), 0.0, dec_pole, 0.0);
let ecl = EclipticCoord::from(eq);
assert_abs_diff_eq!(ecl.lat, FRAC_PI_2, epsilon = 1e-6);
}
#[test]
fn roundtrip_covariance_diagonal() {
let eq = equ(83.82, 22.01, 0.1, 0.1); let eq_cov = EquCoordCov::from_equ(eq);
let ecl_cov = EclipticCoordCov::from(eq_cov);
let eq_cov2 = EquCoordCov::from(ecl_cov);
assert_abs_diff_eq!(eq_cov2.coord.ra, eq.ra, epsilon = 1e-7);
assert_abs_diff_eq!(eq_cov2.coord.dec, eq.dec, epsilon = 1e-7);
assert_abs_diff_eq!(eq_cov2.cov.xx, eq_cov.cov.xx, epsilon = 1e-17);
assert_abs_diff_eq!(eq_cov2.cov.yy, eq_cov.cov.yy, epsilon = 1e-17);
assert_abs_diff_eq!(eq_cov2.cov.xy, eq_cov.cov.xy, epsilon = 1e-17);
}
#[test]
fn roundtrip_covariance_with_correlation() {
let eq = equ(200.0, -15.0, 0.05, 0.08);
let cov = Cov2 {
xx: eq.ra_error * eq.ra_error,
yy: eq.dec_error * eq.dec_error,
xy: 0.3 * eq.ra_error * eq.dec_error,
};
let eq_cov = EquCoordCov::new(eq, cov);
let ecl_cov = EclipticCoordCov::from(eq_cov);
let eq_cov2 = EquCoordCov::from(ecl_cov);
assert_abs_diff_eq!(eq_cov2.cov.xx, eq_cov.cov.xx, epsilon = 1e-17);
assert_abs_diff_eq!(eq_cov2.cov.yy, eq_cov.cov.yy, epsilon = 1e-17);
assert_abs_diff_eq!(eq_cov2.cov.xy, eq_cov.cov.xy, epsilon = 1e-17);
}
#[test]
fn marginal_errors_consistent_with_cov() {
let eq = equ(45.0, 10.0, 0.2, 0.15);
let ecl_cov = EclipticCoordCov::from(EquCoordCov::from_equ(eq));
assert_abs_diff_eq!(
ecl_cov.coord.lon_error,
ecl_cov.cov.xx.sqrt(),
epsilon = 1e-15
);
assert_abs_diff_eq!(
ecl_cov.coord.lat_error,
ecl_cov.cov.yy.sqrt(),
epsilon = 1e-15
);
}
#[test]
fn covariance_positive_definite() {
let eq = equ(300.0, -60.0, 0.1, 0.1);
let ecl_cov = EclipticCoordCov::from(EquCoordCov::from_equ(eq));
let det = ecl_cov.cov.xx * ecl_cov.cov.yy - ecl_cov.cov.xy * ecl_cov.cov.xy;
assert!(det > 0.0, "covariance must be positive definite, det={det}");
}
}
#[cfg(test)]
mod ecliptic_coord_proptests {
use super::*;
use crate::coordinates::equatorial::{EquCoord, EquCoordCov};
use proptest::prelude::*;
use std::f64::consts::{FRAC_PI_2, TAU};
fn arb_equ_position() -> impl Strategy<Value = EquCoord> {
(0.0..TAU, -FRAC_PI_2..=FRAC_PI_2).prop_map(|(ra, dec)| EquCoord::new(ra, 0.0, dec, 0.0))
}
fn arb_equ_away_from_ecl_poles() -> impl Strategy<Value = EquCoord> {
arb_equ_position().prop_filter("too close to an ecliptic pole", |eq| {
let ecl = EclipticCoord::from(eq);
ecl.lat.abs() < FRAC_PI_2 - 5_f64.to_radians()
})
}
fn arb_err_rad() -> impl Strategy<Value = f64> {
(0.01_f64..10.0_f64).prop_map(|arcsec| arcsec.to_radians() / 3600.0)
}
fn arb_equ_cov_diagonal() -> impl Strategy<Value = EquCoordCov> {
(arb_equ_away_from_ecl_poles(), arb_err_rad(), arb_err_rad()).prop_map(
|(eq, ra_err, dec_err)| {
let eq = EquCoord::new(eq.ra, ra_err, eq.dec, dec_err);
EquCoordCov::from_equ(eq)
},
)
}
fn arb_equ_cov_correlated() -> impl Strategy<Value = EquCoordCov> {
(
arb_equ_away_from_ecl_poles(),
arb_err_rad(),
arb_err_rad(),
-0.9_f64..0.9_f64,
)
.prop_map(|(eq, ra_err, dec_err, rho)| {
let eq = EquCoord::new(eq.ra, ra_err, eq.dec, dec_err);
let cov = Cov2 {
xx: ra_err * ra_err,
yy: dec_err * dec_err,
xy: rho * ra_err * dec_err,
};
EquCoordCov::new(eq, cov)
})
}
fn angle_tol(v: f64) -> f64 {
(3e-7 * v.abs()).max(1e-7)
}
fn cov_tol(v: f64) -> f64 {
(1e-6 * v.abs()).max(1e-50)
}
proptest! {
#[test]
fn prop_roundtrip_position(eq in arb_equ_away_from_ecl_poles()) {
let ecl = EclipticCoord::from(eq);
let eq2 = EquCoord::from(ecl);
let ra_diff = (eq2.ra - eq.ra).abs();
let ra_diff = ra_diff.min(TAU - ra_diff);
let tol_ra = angle_tol(eq.ra).max(angle_tol(eq.dec));
let tol_dec = angle_tol(eq.dec);
prop_assert!(
ra_diff < tol_ra,
"RA mismatch: {} vs {} (tol {})",
eq.ra, eq2.ra, tol_ra
);
prop_assert!(
(eq2.dec - eq.dec).abs() < tol_dec,
"Dec mismatch: {} vs {} (tol {})",
eq.dec, eq2.dec, tol_dec
);
}
}
proptest! {
#[test]
fn prop_marginal_errors_consistent_with_cov(eq_cov in arb_equ_cov_correlated()) {
let ecl_cov = EclipticCoordCov::from(eq_cov);
let tol = cov_tol(ecl_cov.cov.xx.sqrt());
prop_assert!(
(ecl_cov.coord.lon_error - ecl_cov.cov.xx.sqrt()).abs() < tol,
"lon_error {} ≠ sqrt(cov.xx) {}",
ecl_cov.coord.lon_error,
ecl_cov.cov.xx.sqrt()
);
let tol = cov_tol(ecl_cov.cov.yy.sqrt());
prop_assert!(
(ecl_cov.coord.lat_error - ecl_cov.cov.yy.sqrt()).abs() < tol,
"lat_error {} ≠ sqrt(cov.yy) {}",
ecl_cov.coord.lat_error,
ecl_cov.cov.yy.sqrt()
);
}
}
proptest! {
#[test]
fn prop_ecliptic_cov_positive_definite(eq_cov in arb_equ_cov_diagonal()) {
let ecl_cov = EclipticCoordCov::from(eq_cov);
let det = ecl_cov.cov.xx * ecl_cov.cov.yy - ecl_cov.cov.xy * ecl_cov.cov.xy;
prop_assert!(det > 0.0, "det = {det}");
}
}
proptest! {
#[test]
fn prop_ecliptic_cov_positive_definite_correlated(eq_cov in arb_equ_cov_correlated()) {
let ecl_cov = EclipticCoordCov::from(eq_cov);
let det = ecl_cov.cov.xx * ecl_cov.cov.yy - ecl_cov.cov.xy * ecl_cov.cov.xy;
prop_assert!(det > 0.0, "det = {det}");
}
}
proptest! {
#[test]
fn prop_ecliptic_variances_non_negative(eq_cov in arb_equ_cov_correlated()) {
let ecl_cov = EclipticCoordCov::from(eq_cov);
prop_assert!(
ecl_cov.cov.xx >= 0.0,
"σ_λλ = {} < 0", ecl_cov.cov.xx
);
prop_assert!(
ecl_cov.cov.yy >= 0.0,
"σ_ββ = {} < 0", ecl_cov.cov.yy
);
}
}
}