use std::f64::consts::PI;
use siderust::coordinates::spherical::direction;
use siderust::coordinates::transform::TransformFrame;
use siderust::qtty::*;
fn lcg_next(state: &mut u64) -> f64 {
*state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
((*state >> 33) as f64) / (0x8000_0000_u64 as f64)
}
#[test]
fn angular_separation_identity_is_exact_zero() {
let cases: &[(f64, f64)] = &[
(0.0, 0.0),
(37.9546, 89.2641), (279.2347, 38.7836), (180.0, -45.0),
(359.9999, -89.9999),
];
for &(ra, dec) in cases {
let d = direction::EquatorialMeanJ2000::new(Degrees::new(ra), Degrees::new(dec));
let sep = d.angular_separation(&d).value();
assert_eq!(
sep, 0.0,
"identity failed for (RA={ra}, Dec={dec}): got {sep}"
);
}
}
#[test]
fn angular_separation_poles_are_antipodal() {
let north = direction::EquatorialMeanJ2000::new(0.0 * DEG, 90.0 * DEG);
let south = direction::EquatorialMeanJ2000::new(0.0 * DEG, -90.0 * DEG);
let sep = north.angular_separation(&south).value();
assert!(
(sep - 180.0).abs() < 1e-12,
"pole antipode: expected 180°, got {sep}"
);
}
#[test]
fn angular_separation_general_antipode() {
let cases: &[(f64, f64)] = &[
(45.0, 30.0),
(270.0, -60.0),
(120.0, 0.0),
(0.0, 45.0),
(10.0, 89.0), ];
for &(ra, dec) in cases {
let a = direction::EquatorialMeanJ2000::new(Degrees::new(ra), Degrees::new(dec));
let antipode = direction::EquatorialMeanJ2000::new(
Degrees::new((ra + 180.0) % 360.0),
Degrees::new(-dec),
);
let sep = a.angular_separation(&antipode).value();
assert!(
(sep - 180.0).abs() < 1e-11,
"antipode ({ra},{dec}): expected 180°, got {sep}"
);
}
}
#[test]
fn angular_separation_symmetry_100_random_pairs() {
let mut state: u64 = 0xdead_beef_cafe_f00d;
for i in 0u64..100 {
let ra1 = Degrees::new(lcg_next(&mut state) * 360.0);
let dec1 = Degrees::new(lcg_next(&mut state) * 180.0 - 90.0);
let ra2 = Degrees::new(lcg_next(&mut state) * 360.0);
let dec2 = Degrees::new(lcg_next(&mut state) * 180.0 - 90.0);
let a = direction::EquatorialMeanJ2000::new(ra1, dec1);
let b = direction::EquatorialMeanJ2000::new(ra2, dec2);
let sep_ab = a.angular_separation(&b).value();
let sep_ba = b.angular_separation(&a).value();
let ba = sep_ab.to_bits();
let bb = sep_ba.to_bits();
let ulp_diff = ba.abs_diff(bb);
assert!(
ulp_diff <= 4,
"pair {i}: symmetry violated: sep(a,b)={sep_ab:.15e} sep(b,a)={sep_ba:.15e} ulps={ulp_diff}"
);
}
}
#[test]
fn angular_separation_cross_frame_equatorial_vs_ecliptic() {
let vega_eq =
direction::EquatorialMeanJ2000::new(Degrees::new(279.2347), Degrees::new(38.7836));
let polaris_eq =
direction::EquatorialMeanJ2000::new(Degrees::new(37.9546), Degrees::new(89.2641));
let sep_eq_deg = vega_eq.angular_separation(&polaris_eq).value();
let vega_ecl: direction::EclipticMeanJ2000 = TransformFrame::to_frame(&vega_eq);
let polaris_ecl: direction::EclipticMeanJ2000 = TransformFrame::to_frame(&polaris_eq);
let sep_ecl_deg = vega_ecl.angular_separation(&polaris_ecl).value();
let diff_rad = (sep_eq_deg - sep_ecl_deg).abs().to_radians();
assert!(
diff_rad < 1e-12,
"cross-frame disagreement: equatorial={sep_eq_deg:.15}° ecliptic={sep_ecl_deg:.15}° diff={diff_rad:.2e} rad"
);
}
#[test]
fn angular_separation_precision_near_zero() {
let epsilon_rad = 1e-9_f64;
let epsilon_deg = epsilon_rad.to_degrees();
let a = direction::EquatorialMeanJ2000::new(0.0 * DEG, 0.0 * DEG);
let b = direction::EquatorialMeanJ2000::new(Degrees::new(epsilon_deg), 0.0 * DEG);
let sep_deg = a.angular_separation(&b).value();
assert!(
sep_deg > 0.0,
"precision near 0: Vincenty returned exactly 0 (expected ~{epsilon_deg:.2e}°)"
);
let rel_err = (sep_deg - epsilon_deg).abs() / epsilon_deg;
assert!(
rel_err < 0.01,
"precision near 0: relative error {rel_err:.2e} (expected < 1 %)"
);
}
#[test]
fn angular_separation_precision_near_pi() {
let delta_rad = 1e-9_f64; let delta_deg = delta_rad.to_degrees();
let north = direction::EquatorialMeanJ2000::new(0.0 * DEG, 90.0 * DEG);
let near_south =
direction::EquatorialMeanJ2000::new(0.0 * DEG, Degrees::new(-(90.0 - delta_deg)));
let sep_rad = north.angular_separation(&near_south).value().to_radians();
let expected_rad = PI - delta_rad;
let error_rad = (sep_rad - expected_rad).abs();
assert!(
error_rad < 1e-12,
"precision near pi: error={error_rad:.2e} rad (expected {expected_rad:.15}, got {sep_rad:.15})"
);
}
#[test]
fn angular_separation_galactic_identity_and_quarter_turn() {
let gc = direction::Galactic::new(120.0 * DEG, 45.0 * DEG);
assert_eq!(gc.angular_separation(&gc).value(), 0.0, "galactic identity");
let a = direction::Galactic::new(0.0 * DEG, 0.0 * DEG);
let b = direction::Galactic::new(90.0 * DEG, 0.0 * DEG);
let sep = a.angular_separation(&b).value();
assert!(
(sep - 90.0).abs() < 1e-12,
"galactic 90° separation: expected 90°, got {sep}"
);
}
#[test]
fn angular_separation_horizontal_identity_and_quarter_turn() {
let h = direction::Horizontal::new(180.0 * DEG, 30.0 * DEG);
assert_eq!(h.angular_separation(&h).value(), 0.0, "horizontal identity");
let zenith = direction::Horizontal::new(0.0 * DEG, 90.0 * DEG);
let horizon = direction::Horizontal::new(0.0 * DEG, 0.0 * DEG);
let sep = zenith.angular_separation(&horizon).value();
assert!(
(sep - 90.0).abs() < 1e-12,
"horizontal 90° separation: expected 90°, got {sep}"
);
}