use crate::calculus::ephemeris::Ephemeris;
use crate::coordinates::transform::context::DefaultEphemeris;
use crate::coordinates::transform::TransformFrame;
use crate::coordinates::{
cartesian::{direction, position, Velocity},
frames,
};
use crate::time::JulianDate;
use qtty::*;
type AuPerDay = qtty::Per<AstronomicalUnit, Day>;
type AusPerDay = qtty::velocity::Velocity<AstronomicalUnit, Day>;
pub const AU_PER_DAY_C: AusPerDay = AusPerDay::new(173.144_632_674_240_33);
#[inline]
fn aberrate_unit_vector_lorentz(
u: nalgebra::Vector3<f64>,
beta: nalgebra::Vector3<f64>,
) -> nalgebra::Vector3<f64> {
let beta2 = beta.dot(&beta);
if beta2 == 0.0 {
return u;
}
let gamma = 1.0 / (1.0 - beta2).sqrt();
let beta_dot_u = beta.dot(&u);
let factor = gamma / (gamma + 1.0);
let numerator = (u / gamma) + beta * (1.0 + factor * beta_dot_u);
let denom = 1.0 + beta_dot_u;
numerator / denom
}
#[inline]
fn velocity_to_beta(
velocity: &Velocity<frames::EquatorialMeanJ2000, AuPerDay>,
) -> nalgebra::Vector3<f64> {
nalgebra::Vector3::new(
(velocity.x() / AU_PER_DAY_C).simplify().value(),
(velocity.y() / AU_PER_DAY_C).simplify().value(),
(velocity.z() / AU_PER_DAY_C).simplify().value(),
)
}
#[must_use]
pub fn apply_aberration_to_direction_with_velocity(
mean: direction::EquatorialMeanJ2000,
velocity: &Velocity<frames::EquatorialMeanJ2000, AuPerDay>,
) -> direction::EquatorialMeanJ2000 {
let beta = velocity_to_beta(velocity);
let u = nalgebra::Vector3::new(mean.x(), mean.y(), mean.z());
let up = aberrate_unit_vector_lorentz(u, beta);
direction::EquatorialMeanJ2000::normalize(up.x, up.y, up.z)
}
#[must_use]
pub fn remove_aberration_from_direction_with_velocity(
app: direction::EquatorialMeanJ2000,
velocity: &Velocity<frames::EquatorialMeanJ2000, AuPerDay>,
) -> direction::EquatorialMeanJ2000 {
let beta = -velocity_to_beta(velocity);
let u = nalgebra::Vector3::new(app.x(), app.y(), app.z());
let up = aberrate_unit_vector_lorentz(u, beta);
direction::EquatorialMeanJ2000::normalize(up.x, up.y, up.z)
}
#[must_use]
pub fn apply_aberration_to_direction(
mean: direction::EquatorialMeanJ2000,
jd: JulianDate,
) -> direction::EquatorialMeanJ2000 {
let velocity_ecl = DefaultEphemeris::earth_barycentric_velocity(jd);
let velocity: Velocity<frames::EquatorialMeanJ2000, AuPerDay> = velocity_ecl.to_frame();
apply_aberration_to_direction_with_velocity(mean, &velocity)
}
#[must_use]
pub fn remove_aberration_from_direction(
app: direction::EquatorialMeanJ2000,
jd: JulianDate,
) -> direction::EquatorialMeanJ2000 {
let velocity_ecl = DefaultEphemeris::earth_barycentric_velocity(jd);
let velocity: Velocity<frames::EquatorialMeanJ2000, AuPerDay> = velocity_ecl.to_frame();
remove_aberration_from_direction_with_velocity(app, &velocity)
}
#[must_use]
pub fn apply_aberration<U: LengthUnit>(
mean: position::EquatorialMeanJ2000<U>,
jd: JulianDate,
) -> position::EquatorialMeanJ2000<U> {
if mean.distance() == 0.0 {
return mean;
}
let dir = mean
.direction()
.expect("non-zero position should have a direction");
apply_aberration_to_direction(dir, jd).position(mean.distance())
}
#[must_use]
pub fn remove_aberration<U: LengthUnit>(
app: position::EquatorialMeanJ2000<U>,
jd: JulianDate,
) -> position::EquatorialMeanJ2000<U> {
if app.distance() == 0.0 {
return app;
}
let dir = app
.direction()
.expect("non-zero position should have a direction");
remove_aberration_from_direction(dir, jd).position(app.distance())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coordinates::spherical::{self, position};
use nalgebra::Vector3;
fn apply_aberration_sph<U: LengthUnit>(
mean: &position::EquatorialMeanJ2000<U>,
jd: JulianDate,
) -> position::EquatorialMeanJ2000<U> {
spherical::Position::from_cartesian(&apply_aberration(mean.to_cartesian(), jd))
}
#[test]
fn test_aberration_preserva_distance_and_epoch() {
let jd = JulianDate::new(2451545.0); let mean =
position::EquatorialMeanJ2000::<Au>::new(Degrees::new(10.0), Degrees::new(20.0), 1.23);
let out = apply_aberration_sph(&mean, jd);
assert!(
(out.distance - mean.distance).abs() < AstronomicalUnits::new(1e-12),
"Distance should be preserved: got {:?}, expected {:?}",
out.distance,
mean.distance
);
}
#[test]
fn test_aberration_introduces_shift() {
let jd = JulianDate::new(2451545.0); let mean = position::EquatorialMeanJ2000::<Au>::new(
Degrees::new(0.0), Degrees::new(0.0), 1.0,
);
let out = apply_aberration_sph(&mean, jd);
let delta_ra = out.ra().abs_separation(mean.ra());
let delta_dec = out.dec().abs_separation(mean.dec());
assert!(
delta_ra > 0.0 || delta_dec > 0.0,
"Expected a change in RA or Dec"
);
assert!(delta_ra < 0.01 && delta_dec < 0.01, "Shift is too large")
}
#[test]
fn test_aberration_at_north_pole() {
let jd = JulianDate::new(2451545.0);
let mean = position::EquatorialMeanJ2000::<Au>::new(
Degrees::new(123.4), Degrees::new(90.0), 1.0,
);
let out = apply_aberration_sph(&mean, jd);
assert!(
out.dec() < 90.0,
"Declination should decrease slightly at pole"
);
assert!(!out.ra().is_nan(), "RA must not be NaN at the pole");
}
#[test]
fn test_speed_of_light() {
let expected = AusPerDay::new(173.144_632_674_240_33);
assert!((AU_PER_DAY_C - expected).abs() < AusPerDay::new(1e-12));
}
#[test]
fn aberration_roundtrip_is_machine_precision() {
use crate::bodies::solar_system::Earth;
let jd = JulianDate::new(2458850.0); let velocity_ecl = Earth::vsop87e_vel(jd);
let velocity: Velocity<frames::EquatorialMeanJ2000, AuPerDay> = velocity_ecl.to_frame();
let directions = [
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(0.3, -0.7, 0.64),
Vector3::new(-0.8, 0.2, -0.56),
];
for u in directions {
let mean = direction::EquatorialMeanJ2000::from_vec3(u.normalize());
let app = apply_aberration_to_direction_with_velocity(mean, &velocity);
let rec = remove_aberration_from_direction_with_velocity(app, &velocity);
let dot = mean.x() * rec.x() + mean.y() * rec.y() + mean.z() * rec.z();
let ang = dot.clamp(-1.0, 1.0).acos();
assert!(ang < 5e-15, "roundtrip angle too large: {}", ang);
}
}
}