use std::{
f64::consts::TAU,
fmt::{self, Display, Formatter},
ops::{Add, Mul, Neg, Sub},
};
use crate::coordinates::{INV_COSC_MIN, cov2::Cov2, equatorial::EquCoord};
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TangentPlane {
pub equ_ref: EquCoord,
sin_dec0: f64,
cos_dec0: f64,
}
impl TangentPlane {
#[inline]
pub fn new(equ_ref: EquCoord) -> Self {
let (sin_dec0, cos_dec0) = equ_ref.dec.sin_cos();
Self {
equ_ref,
sin_dec0,
cos_dec0,
}
}
#[inline]
pub fn project(&self, c: &EquCoord) -> TangentPoint {
let (sdec, cdec) = c.dec.sin_cos();
let (sdra, cdra) = (c.ra - self.equ_ref.ra).sin_cos();
let cosc = self.sin_dec0 * sdec + self.cos_dec0 * cdec * cdra;
let inv = 1.0 / cosc.max(INV_COSC_MIN);
let x = cdec * sdra * inv;
let y = (self.cos_dec0 * sdec - self.sin_dec0 * cdec * cdra) * inv;
TangentPoint { plane: *self, x, y }
}
#[inline]
pub fn sin_dec0(&self) -> f64 {
self.sin_dec0
}
#[inline]
pub fn cos_dec0(&self) -> f64 {
self.cos_dec0
}
}
impl Display for TangentPlane {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
writeln!(f, "TangentPlane {{")?;
writeln!(f, " ra0 : {:.6e} rad", self.equ_ref.ra)?;
writeln!(f, " dec0 : {:.6e} rad", self.equ_ref.dec)?;
write!(f, "}}")
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TangentPoint {
pub plane: TangentPlane,
pub x: f64,
pub y: f64,
}
impl TangentPoint {
#[inline]
pub fn new(plane: TangentPlane, x: f64, y: f64) -> Self {
Self { plane, x, y }
}
#[inline]
pub fn unproject(&self) -> EquCoord {
let rho2 = self.x * self.x + self.y * self.y;
if rho2 < 1e-24 {
return EquCoord::new(self.plane.equ_ref.ra, 0.0, self.plane.equ_ref.dec, 0.0);
}
let rho = rho2.sqrt();
let c = rho.atan();
let (sc, cc) = c.sin_cos();
let s0 = self.plane.sin_dec0;
let c0 = self.plane.cos_dec0;
let dec = (cc * s0 + (self.y * sc * c0) / rho).asin();
let denom = rho * c0 * cc - self.y * s0 * sc;
let ra = (self.plane.equ_ref.ra + (self.x * sc).atan2(denom)).rem_euclid(TAU);
EquCoord::new(ra, 0.0, dec, 0.0)
}
#[inline]
pub fn dist2(&self, other: &Self) -> f64 {
debug_assert_eq!(
self.plane, other.plane,
"TangentPoint::dist2 requires both points to share the same plane"
);
let dx = self.x - other.x;
let dy = self.y - other.y;
dx * dx + dy * dy
}
pub fn midpoint(a: TangentPoint, b: TangentPoint) -> TangentPoint {
debug_assert_eq!(a.plane, b.plane);
a + (b - a) * 0.5
}
}
impl Display for TangentPoint {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
writeln!(f, "TangentPoint {{")?;
writeln!(f, " plane: {}", self.plane)?;
writeln!(f, " x : {:.6e} rad", self.x)?;
writeln!(f, " y : {:.6e} rad", self.y)?;
write!(f, "}}")
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TangentVec {
pub dx: f64,
pub dy: f64,
}
impl Display for TangentVec {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
writeln!(f, "TangentVec {{")?;
writeln!(f, " dx : {:.6e} rad", self.dx)?;
writeln!(f, " dy : {:.6e} rad", self.dy)?;
write!(f, "}}")
}
}
impl Add<TangentVec> for TangentPoint {
type Output = TangentPoint;
fn add(self, v: TangentVec) -> TangentPoint {
TangentPoint {
plane: self.plane,
x: self.x + v.dx,
y: self.y + v.dy,
}
}
}
impl Sub for TangentPoint {
type Output = TangentVec;
fn sub(self, rhs: TangentPoint) -> TangentVec {
debug_assert_eq!(
self.plane, rhs.plane,
"TangentPoint subtraction across different planes"
);
TangentVec {
dx: self.x - rhs.x,
dy: self.y - rhs.y,
}
}
}
impl TangentVec {
#[inline]
pub fn norm_sq(&self) -> f64 {
self.dx * self.dx + self.dy * self.dy
}
#[inline]
pub fn norm(&self) -> f64 {
self.dx.hypot(self.dy)
}
#[inline]
pub fn dot(self, other: TangentVec) -> f64 {
self.dx * other.dx + self.dy * other.dy
}
}
impl Add for TangentVec {
type Output = Self;
#[inline]
fn add(self, rhs: Self) -> Self {
Self {
dx: self.dx + rhs.dx,
dy: self.dy + rhs.dy,
}
}
}
impl Add for &TangentVec {
type Output = TangentVec;
#[inline]
fn add(self, rhs: Self) -> TangentVec {
(*self) + (*rhs)
}
}
impl Sub for TangentVec {
type Output = Self;
#[inline]
fn sub(self, rhs: Self) -> Self {
Self {
dx: self.dx - rhs.dx,
dy: self.dy - rhs.dy,
}
}
}
impl Sub for &TangentVec {
type Output = TangentVec;
#[inline]
fn sub(self, rhs: Self) -> TangentVec {
(*self) - (*rhs)
}
}
impl Mul<f64> for TangentVec {
type Output = TangentVec;
fn mul(self, k: f64) -> TangentVec {
TangentVec {
dx: k * self.dx,
dy: k * self.dy,
}
}
}
impl Mul<f64> for &TangentVec {
type Output = TangentVec;
fn mul(self, k: f64) -> TangentVec {
(*self) * k
}
}
impl Mul<TangentVec> for Cov2 {
type Output = TangentVec;
#[inline]
fn mul(self, v: TangentVec) -> TangentVec {
TangentVec {
dx: self.xx * v.dx + self.xy * v.dy,
dy: self.xy * v.dx + self.yy * v.dy,
}
}
}
impl Mul<TangentVec> for &Cov2 {
type Output = TangentVec;
#[inline]
fn mul(self, v: TangentVec) -> TangentVec {
(*self) * v
}
}
impl Neg for TangentVec {
type Output = Self;
#[inline]
fn neg(self) -> Self {
Self {
dx: -self.dx,
dy: -self.dy,
}
}
}
impl Neg for &TangentVec {
type Output = TangentVec;
#[inline]
fn neg(self) -> TangentVec {
-(*self)
}
}
#[cfg(test)]
mod gnomonic_tests {
use super::*;
use approx::assert_abs_diff_eq;
use proptest::prelude::*;
use std::f64::consts::PI;
fn make_plane(ra0_deg: f64, dec0_deg: f64) -> TangentPlane {
let equ = EquCoord::new(ra0_deg.to_radians(), 0.0, dec0_deg.to_radians(), 0.0);
TangentPlane::new(equ)
}
fn make_coord(ra_deg: f64, dec_deg: f64) -> EquCoord {
EquCoord::new(ra_deg.to_radians(), 0.0, dec_deg.to_radians(), 0.0)
}
#[test]
fn new_caches_sin_cos() {
let plane = make_plane(10.0, 30.0);
let dec0 = 30.0_f64.to_radians();
assert_abs_diff_eq!(plane.sin_dec0(), dec0.sin(), epsilon = 1e-15);
assert_abs_diff_eq!(plane.cos_dec0(), dec0.cos(), epsilon = 1e-15);
}
#[test]
fn project_tangent_point_is_origin() {
let plane = make_plane(45.0, 20.0);
let tp = plane.project(&plane.equ_ref);
assert_abs_diff_eq!(tp.x, 0.0, epsilon = 1e-13);
assert_abs_diff_eq!(tp.y, 0.0, epsilon = 1e-13);
}
#[test]
fn unproject_origin_returns_tangent_point() {
let plane = make_plane(45.0, 20.0);
let tp = TangentPoint::new(plane, 0.0, 0.0);
let sky = tp.unproject();
assert_abs_diff_eq!(sky.ra, plane.equ_ref.ra, epsilon = 1e-13);
assert_abs_diff_eq!(sky.dec, plane.equ_ref.dec, epsilon = 1e-13);
}
#[test]
fn project_unproject_round_trip_small_offset() {
let plane = make_plane(30.0, 10.0);
let target = make_coord(30.5, 10.5);
let tp = plane.project(&target);
let sky = tp.unproject();
assert_abs_diff_eq!(sky.ra, target.ra, epsilon = 1e-10);
assert_abs_diff_eq!(sky.dec, target.dec, epsilon = 1e-10);
}
#[test]
fn project_east_gives_positive_x() {
let plane = make_plane(30.0, 0.0);
let east = make_coord(31.0, 0.0); let tp = plane.project(&east);
assert!(tp.x > 0.0, "x should be positive for points to the east");
}
#[test]
fn project_north_gives_positive_y() {
let plane = make_plane(30.0, 0.0);
let north = make_coord(30.0, 1.0); let tp = plane.project(&north);
assert!(tp.y > 0.0, "y should be positive for points to the north");
}
#[test]
fn dist2_self_is_zero() {
let plane = make_plane(45.0, 20.0);
let p = plane.project(&make_coord(45.5, 20.5));
assert_abs_diff_eq!(p.dist2(&p), 0.0, epsilon = 1e-20);
}
#[test]
fn dist2_symmetric() {
let plane = make_plane(45.0, 20.0);
let p = plane.project(&make_coord(45.5, 20.5));
let q = plane.project(&make_coord(46.0, 21.0));
assert_abs_diff_eq!(p.dist2(&q), q.dist2(&p), epsilon = 1e-20);
}
#[test]
fn dist2_pythagoras() {
let plane = make_plane(0.0, 0.0);
let a = TangentPoint::new(plane, 3.0e-3, 0.0);
let b = TangentPoint::new(plane, 0.0, 4.0e-3);
assert_abs_diff_eq!(a.dist2(&b), 25.0e-6, epsilon = 1e-20);
}
#[test]
fn midpoint_is_between_endpoints() {
let plane = make_plane(0.0, 0.0);
let a = TangentPoint::new(plane, 0.0, 0.0);
let b = TangentPoint::new(plane, 2.0, 4.0);
let m = TangentPoint::midpoint(a, b);
assert_abs_diff_eq!(m.x, 1.0, epsilon = 1e-15);
assert_abs_diff_eq!(m.y, 2.0, epsilon = 1e-15);
}
#[test]
fn vec_norm_sq_pythagoras() {
let v = TangentVec { dx: 3.0, dy: 4.0 };
assert_abs_diff_eq!(v.norm_sq(), 25.0, epsilon = 1e-15);
assert_abs_diff_eq!(v.norm(), 5.0, epsilon = 1e-15);
}
#[test]
fn vec_add_sub_roundtrip() {
let a = TangentVec { dx: 1.0, dy: 2.0 };
let b = TangentVec { dx: 3.0, dy: -1.0 };
let s = a + b;
let d = s - b;
assert_abs_diff_eq!(d.dx, a.dx, epsilon = 1e-15);
assert_abs_diff_eq!(d.dy, a.dy, epsilon = 1e-15);
}
#[test]
fn vec_neg() {
let v = TangentVec { dx: 1.5, dy: -2.5 };
let n = -v;
assert_abs_diff_eq!(n.dx, -1.5, epsilon = 1e-15);
assert_abs_diff_eq!(n.dy, 2.5, epsilon = 1e-15);
}
#[test]
fn vec_mul_scales() {
let v = TangentVec { dx: 2.0, dy: 3.0 };
let s = v * 2.5;
assert_abs_diff_eq!(s.dx, 5.0, epsilon = 1e-15);
assert_abs_diff_eq!(s.dy, 7.5, epsilon = 1e-15);
}
#[test]
fn vec_dot_orthogonal_is_zero() {
let a = TangentVec { dx: 1.0, dy: 0.0 };
let b = TangentVec { dx: 0.0, dy: 1.0 };
assert_abs_diff_eq!(a.dot(b), 0.0, epsilon = 1e-15);
}
#[test]
fn vec_dot_self_equals_norm_sq() {
let v = TangentVec { dx: 3.0, dy: 4.0 };
assert_abs_diff_eq!(v.dot(v), v.norm_sq(), epsilon = 1e-15);
}
#[test]
fn vec_dot_commutative() {
let a = TangentVec { dx: 1.5, dy: -2.0 };
let b = TangentVec { dx: 0.5, dy: 3.0 };
assert_abs_diff_eq!(a.dot(b), b.dot(a), epsilon = 1e-15);
}
#[test]
fn cov2_mul_vec_isotropic() {
let q = 3.0_f64;
let m = crate::coordinates::cov2::Cov2::isotropic(q);
let v = TangentVec { dx: 2.0, dy: -1.0 };
let w = m * v;
assert_abs_diff_eq!(w.dx, q * v.dx, epsilon = 1e-15);
assert_abs_diff_eq!(w.dy, q * v.dy, epsilon = 1e-15);
}
#[test]
fn cov2_mul_vec_full() {
let m = crate::coordinates::cov2::Cov2 {
xx: 4.0,
yy: 9.0,
xy: 2.0,
};
let v = TangentVec { dx: 1.0, dy: 1.0 };
let w = m * v;
assert_abs_diff_eq!(w.dx, 6.0, epsilon = 1e-15);
assert_abs_diff_eq!(w.dy, 11.0, epsilon = 1e-15);
}
#[test]
fn cov2_mul_vec_consistent_with_quad_form() {
let m = crate::coordinates::cov2::Cov2 {
xx: 4.0,
yy: 9.0,
xy: 2.0,
};
let v = TangentVec { dx: 2.0, dy: 3.0 };
let mv = m * v;
let quad = v.dot(mv);
assert_abs_diff_eq!(quad, m.quad_form(v), epsilon = 1e-10);
}
#[test]
fn point_plus_vec_gives_point_on_same_plane() {
let plane = make_plane(0.0, 0.0);
let p = TangentPoint::new(plane, 1.0, 2.0);
let v = TangentVec { dx: 0.5, dy: -0.5 };
let q = p + v;
assert_eq!(q.plane, plane);
assert_abs_diff_eq!(q.x, 1.5, epsilon = 1e-15);
assert_abs_diff_eq!(q.y, 1.5, epsilon = 1e-15);
}
#[test]
fn point_sub_gives_tangent_vec() {
let plane = make_plane(0.0, 0.0);
let a = TangentPoint::new(plane, 3.0, 4.0);
let b = TangentPoint::new(plane, 1.0, 1.0);
let v = a - b;
assert_abs_diff_eq!(v.dx, 2.0, epsilon = 1e-15);
assert_abs_diff_eq!(v.dy, 3.0, epsilon = 1e-15);
}
#[test]
fn display_tangent_plane_contains_coords() {
let plane = make_plane(45.0, 20.0);
let s = format!("{}", plane);
assert!(s.contains("TangentPlane"));
assert!(s.contains("ra0"));
assert!(s.contains("dec0"));
}
#[test]
fn display_tangent_point_contains_xy() {
let plane = make_plane(0.0, 0.0);
let tp = TangentPoint::new(plane, 1.0e-3, 2.0e-3);
let s = format!("{}", tp);
assert!(s.contains("x"));
assert!(s.contains("y"));
}
prop_compose! {
fn valid_ra_deg()(ra in 0.0_f64..360.0) -> f64 { ra }
}
prop_compose! {
fn valid_dec_deg()(dec in -89.0_f64..89.0) -> f64 { dec }
}
prop_compose! {
fn small_offset_deg()(off in -3.0_f64..3.0) -> f64 { off }
}
prop_compose! {
fn psd_cov2()(a in 0.01_f64..10.0, b in -5.0_f64..5.0, c in 0.01_f64..10.0) -> Cov2 {
Cov2 { xx: a * a, yy: b * b + c * c, xy: a * b }
}
}
proptest! {
#[test]
fn project_unproject_roundtrip(
ra0 in valid_ra_deg(),
dec0 in valid_dec_deg(),
dra in small_offset_deg(),
ddec in small_offset_deg(),
) {
let plane = make_plane(ra0, dec0);
let target_dec = (dec0 + ddec).clamp(-89.0, 89.0);
let target = make_coord(ra0 + dra, target_dec);
let tp = plane.project(&target);
let sky = tp.unproject();
prop_assert!((sky.ra - target.ra).abs() < 1e-7
|| (sky.ra - target.ra + TAU).abs() < 1e-7
|| (sky.ra - target.ra - TAU).abs() < 1e-7,
"RA roundtrip failed: got {}, expected {}", sky.ra, target.ra);
prop_assert!((sky.dec - target.dec).abs() < 1e-7,
"Dec roundtrip failed: got {}, expected {}", sky.dec, target.dec);
}
#[test]
fn dist2_symmetry(
x1 in -1.0_f64..1.0, y1 in -1.0_f64..1.0,
x2 in -1.0_f64..1.0, y2 in -1.0_f64..1.0,
) {
let plane = make_plane(0.0, 0.0);
let p = TangentPoint::new(plane, x1, y1);
let q = TangentPoint::new(plane, x2, y2);
prop_assert!((p.dist2(&q) - q.dist2(&p)).abs() < 1e-20);
}
#[test]
fn dist2_nonneg(
x1 in -1.0_f64..1.0, y1 in -1.0_f64..1.0,
x2 in -1.0_f64..1.0, y2 in -1.0_f64..1.0,
) {
let plane = make_plane(0.0, 0.0);
let p = TangentPoint::new(plane, x1, y1);
let q = TangentPoint::new(plane, x2, y2);
prop_assert!(p.dist2(&q) >= 0.0);
}
#[test]
fn vec_norm_scales(dx in -10.0_f64..10.0, dy in -10.0_f64..10.0, k in -5.0_f64..5.0) {
let v = TangentVec { dx, dy };
let scaled = v * k;
prop_assert!((scaled.norm_sq() - v.norm_sq() * k * k).abs() < 1e-10);
}
#[test]
fn unproject_origin_is_ref(ra0 in valid_ra_deg(), dec0 in valid_dec_deg()) {
let plane = make_plane(ra0, dec0);
let tp = TangentPoint::new(plane, 0.0, 0.0);
let sky = tp.unproject();
prop_assert!((sky.dec - plane.equ_ref.dec).abs() < 1e-12);
}
#[test]
fn dot_commutative(
dx1 in -10.0_f64..10.0, dy1 in -10.0_f64..10.0,
dx2 in -10.0_f64..10.0, dy2 in -10.0_f64..10.0,
) {
let v = TangentVec { dx: dx1, dy: dy1 };
let w = TangentVec { dx: dx2, dy: dy2 };
prop_assert!((v.dot(w) - w.dot(v)).abs() < 1e-12);
}
#[test]
fn dot_self_equals_norm_sq(dx in -10.0_f64..10.0, dy in -10.0_f64..10.0) {
let v = TangentVec { dx, dy };
prop_assert!((v.dot(v) - v.norm_sq()).abs() < 1e-12);
}
#[test]
fn cov2_mul_tangent_vec_consistent_with_quad_form(
cov in psd_cov2(),
vx in -5.0_f64..5.0,
vy in -5.0_f64..5.0,
) {
let v = TangentVec { dx: vx, dy: vy };
let sv = cov * v; let qf = cov.quad_form(v); let dot_v_sv = v.dot(sv);
prop_assert!((dot_v_sv - qf).abs() < 1e-8, "vᵀ(Σv)={} ≠ quad_form={}", dot_v_sv, qf);
}
}
#[allow(unused)]
const _PI: f64 = PI;
}