use crate::coordinates::cartesian::direction;
use qtty::{Arcseconds, AstronomicalUnits, Radians};
const SOLAR_SCHWARZSCHILD_AU: f64 = 1.974_125_743_36e-8;
pub fn solar_deflection(
star: direction::EquatorialMeanJ2000,
earth_sun_au: [f64; 3],
) -> direction::EquatorialMeanJ2000 {
let q_mag =
(earth_sun_au[0].powi(2) + earth_sun_au[1].powi(2) + earth_sun_au[2].powi(2)).sqrt();
if q_mag < 1e-10 {
return star; }
let q_hat = [
earth_sun_au[0] / q_mag,
earth_sun_au[1] / q_mag,
earth_sun_au[2] / q_mag,
];
let sx = star.x();
let sy = star.y();
let sz = star.z();
let s_dot_q = sx * q_hat[0] + sy * q_hat[1] + sz * q_hat[2];
let denom = 1.0 + s_dot_q;
if denom.abs() < 1e-15 {
return star; }
let factor = SOLAR_SCHWARZSCHILD_AU / q_mag;
let f = factor / denom;
let dx = sx + f * (q_hat[0] - s_dot_q * sx);
let dy = sy + f * (q_hat[1] - s_dot_q * sy);
let dz = sz + f * (q_hat[2] - s_dot_q * sz);
direction::EquatorialMeanJ2000::normalize(dx, dy, dz)
}
#[inline]
pub fn solar_deflection_magnitude(
sun_angle: Radians,
sun_distance: AstronomicalUnits,
) -> Arcseconds {
const RAD_TO_ARCSEC: f64 = 206_264.806_247_096_36;
let angle = sun_angle.value();
let dist = sun_distance.value();
if angle <= 0.0 || dist <= 0.0 {
return Arcseconds::new(0.0); }
let half = angle / 2.0;
let cot_half = half.cos() / half.sin().abs().max(1e-10);
let scale_arcsec = (SOLAR_SCHWARZSCHILD_AU / dist) * RAD_TO_ARCSEC;
Arcseconds::new(scale_arcsec * cot_half)
}
pub fn solar_deflection_inverse(
apparent: direction::EquatorialMeanJ2000,
earth_sun_au: [f64; 3],
) -> direction::EquatorialMeanJ2000 {
let deflected = solar_deflection(apparent, earth_sun_au);
let gx = 2.0 * apparent.x() - deflected.x();
let gy = 2.0 * apparent.y() - deflected.y();
let gz = 2.0 * apparent.z() - deflected.z();
direction::EquatorialMeanJ2000::normalize(gx, gy, gz)
}
#[cfg(test)]
mod tests {
use super::*;
use qtty::Radians;
const RAD_TO_ARCSEC: f64 = 206_264.806_247_096_36;
#[test]
fn deflection_at_ninety_deg_is_milliarcseconds() {
let angle = Radians::new(std::f64::consts::FRAC_PI_2);
let defl = solar_deflection_magnitude(angle, AstronomicalUnits::new(1.0));
let expected = SOLAR_SCHWARZSCHILD_AU * RAD_TO_ARCSEC;
assert!(
(defl.value() - expected).abs() < 1e-9,
"deflection at 90° = {}″, expected {}″",
defl.value(),
expected
);
}
#[test]
fn deflection_at_limb_is_about_1_75_arcsec() {
let angle = Radians::new(959.63 / RAD_TO_ARCSEC);
let defl = solar_deflection_magnitude(angle, AstronomicalUnits::new(1.0));
assert!(
(defl.value() - 1.75).abs() < 0.03,
"limb deflection = {}″, expected ~1.75″",
defl.value()
);
}
#[test]
fn deflection_decreases_with_distance() {
let angle = Radians::new(0.5); let d1 = solar_deflection_magnitude(angle, AstronomicalUnits::new(1.0));
let d2 = solar_deflection_magnitude(angle, AstronomicalUnits::new(2.0));
assert!(
d1.value() > d2.value(),
"deflection should decrease with Sun distance"
);
assert!(
(d1.value() / d2.value() - 2.0).abs() < 0.01,
"should scale as 1/R"
);
}
#[test]
fn vector_deflection_direction() {
let star = direction::EquatorialMeanJ2000::new(1.0, 0.0, 0.0);
let earth_sun_au = [0.0, 0.0, -1.0];
let deflected = solar_deflection(star, earth_sun_au);
let angular_diff = (deflected.x() - star.x()).powi(2)
+ (deflected.y() - star.y()).powi(2)
+ (deflected.z() - star.z()).powi(2);
let angular_diff = angular_diff.sqrt();
let expected = SOLAR_SCHWARZSCHILD_AU;
assert!(
(angular_diff - expected).abs() < expected * 1e-3,
"deflection magnitude = {} rad, expected {} rad",
angular_diff,
expected
);
}
#[test]
fn scalar_magnitude_matches_vector_case_at_ninety_deg() {
let star = direction::EquatorialMeanJ2000::new(1.0, 0.0, 0.0);
let earth_sun_au = [0.0, 0.0, -1.0];
let deflected = solar_deflection(star, earth_sun_au);
let chord = ((deflected.x() - star.x()).powi(2)
+ (deflected.y() - star.y()).powi(2)
+ (deflected.z() - star.z()).powi(2))
.sqrt();
let vec_deflection_arcsec = (2.0 * (0.5 * chord).asin()) * RAD_TO_ARCSEC;
let scalar_deflection = solar_deflection_magnitude(
Radians::new(std::f64::consts::FRAC_PI_2),
AstronomicalUnits::new(1.0),
)
.value();
assert!(
(vec_deflection_arcsec - scalar_deflection).abs() < 1e-9,
"vector={} arcsec, scalar={} arcsec",
vec_deflection_arcsec,
scalar_deflection
);
}
#[test]
fn no_deflection_when_at_sun() {
let star = direction::EquatorialMeanJ2000::new(1.0, 0.0, 0.0);
let earth_sun_au = [0.0, 0.0, 0.0]; let result = solar_deflection(star, earth_sun_au);
assert_eq!(result.x(), star.x());
assert_eq!(result.y(), star.y());
assert_eq!(result.z(), star.z());
}
#[test]
fn deflection_roundtrip() {
let star = direction::EquatorialMeanJ2000::new(0.6, 0.7, 0.3742);
let earth_sun_au = [0.5, -0.3, 0.8];
let deflected = solar_deflection(star, earth_sun_au);
let recovered = solar_deflection_inverse(deflected, earth_sun_au);
assert!(
(recovered.x() - star.x()).abs() < 1e-10,
"roundtrip mismatch x: {} vs {}",
recovered.x(),
star.x()
);
assert!(
(recovered.y() - star.y()).abs() < 1e-10,
"roundtrip mismatch y: {} vs {}",
recovered.y(),
star.y()
);
assert!(
(recovered.z() - star.z()).abs() < 1e-10,
"roundtrip mismatch z: {} vs {}",
recovered.z(),
star.z()
);
}
}