use nalgebra::Vector3;
use starfield::positions::Position;
use starfield::starlib::Star;
use starfield::time::Time;
use super::types::{GaiaAstrometry, ObservationContext, ObserverState, RefinementError};
pub fn apparent_radec(
source: &GaiaAstrometry,
obs: &ObservationContext,
) -> Result<(f64, f64), RefinementError> {
let observer = build_observer(&obs.observer)?;
let star = build_star(source);
let astrometric = star.observe_from(&observer, &obs.time);
let apparent_vec = apply_aberration_only(&astrometric, &observer);
Ok(xyz_to_radec(apparent_vec))
}
fn build_observer(state: &ObserverState) -> Result<Position, RefinementError> {
match state {
ObserverState::Barycentric {
position_au,
velocity_au_per_day,
} => Ok(Position::barycentric(
Vector3::new(position_au[0], position_au[1], position_au[2]),
Vector3::new(
velocity_au_per_day[0],
velocity_au_per_day[1],
velocity_au_per_day[2],
),
0,
)),
}
}
fn build_star(source: &GaiaAstrometry) -> Star {
let mut star = Star::new(
source.ra_deg,
source.dec_deg,
source.pmra_mas_per_year,
source.pmdec_mas_per_year,
source.parallax_mas,
source.radial_km_per_s,
);
star = star.with_epoch(jyear_to_tt_jd(source.ref_epoch_jyear));
star
}
fn apply_aberration_only(astrometric: &Position, observer: &Position) -> Vector3<f64> {
let mut target = astrometric.position;
starfield::relativity::add_aberration(&mut target, &observer.velocity, astrometric.light_time);
target
}
fn jyear_to_tt_jd(jyear: f64) -> f64 {
2_451_545.0 + (jyear - 2000.0) * 365.25
}
fn xyz_to_radec(v: Vector3<f64>) -> (f64, f64) {
let r = (v.x * v.x + v.y * v.y + v.z * v.z).sqrt();
let ra = v.y.atan2(v.x);
let dec = (v.z / r).asin();
let ra_wrapped = if ra < 0.0 {
ra + 2.0 * std::f64::consts::PI
} else {
ra
};
(ra_wrapped, dec)
}
#[allow(dead_code)]
fn _keep_time_import_alive(_t: &Time) {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_apparent_position() {
let src = GaiaAstrometry {
ra_deg: 0.0,
dec_deg: 0.0,
pmra_mas_per_year: 0.0,
pmdec_mas_per_year: 0.0,
parallax_mas: 0.0,
radial_km_per_s: 0.0,
ref_epoch_jyear: 2016.0,
sigma_ra_mas: 0.0,
sigma_dec_mas: 0.0,
sigma_pmra_mas_per_year: 0.0,
sigma_pmdec_mas_per_year: 0.0,
sigma_parallax_mas: 0.0,
};
let ts = starfield::time::Timescale::default();
let time = ts.tt_jd(jyear_to_tt_jd(2016.0), None);
let obs = ObservationContext {
time,
observer: ObserverState::Barycentric {
position_au: [0.0, 0.0, 0.0],
velocity_au_per_day: [0.0, 0.0, 0.0],
},
};
let (ra, dec) = apparent_radec(&src, &obs).unwrap();
assert!(
ra.abs() < 1e-9 || (ra - 2.0 * std::f64::consts::PI).abs() < 1e-9,
"ra = {ra}"
);
assert!(dec.abs() < 1e-9, "dec = {dec}");
}
#[test]
fn proper_motion_10_year_displacement() {
let src = GaiaAstrometry {
ra_deg: 0.0,
dec_deg: 0.0,
pmra_mas_per_year: 100.0, pmdec_mas_per_year: 0.0,
parallax_mas: 0.0, radial_km_per_s: 0.0,
ref_epoch_jyear: 2016.0,
sigma_ra_mas: 0.0,
sigma_dec_mas: 0.0,
sigma_pmra_mas_per_year: 0.0,
sigma_pmdec_mas_per_year: 0.0,
sigma_parallax_mas: 0.0,
};
let ts = starfield::time::Timescale::default();
let time = ts.tt_jd(jyear_to_tt_jd(2026.0), None);
let obs = ObservationContext {
time,
observer: ObserverState::Barycentric {
position_au: [0.0, 0.0, 0.0],
velocity_au_per_day: [0.0, 0.0, 0.0],
},
};
let (ra, _dec) = apparent_radec(&src, &obs).unwrap();
let expected_ra_rad = 1000.0 * (std::f64::consts::PI / (180.0 * 3_600_000.0));
let err_mas = (ra - expected_ra_rad).abs() * (180.0 * 3_600_000.0) / std::f64::consts::PI;
assert!(
err_mas < 1.0,
"RA error {err_mas} mas (expected ≈ 1000 mas displacement)"
);
}
}