use crate::catalogs::StarData;
use crate::framelib::inertial::ProperMotion;
use crate::positions::{Position, PositionKind};
use crate::starlib::Star;
use crate::time::Time;
pub const STAR_TARGET_ID: i32 = -1;
impl Position {
pub fn observe_star(
&self,
star: &StarData,
pm: Option<&ProperMotion>,
parallax_mas: Option<f64>,
epoch: &Time,
) -> Position {
assert_eq!(
self.kind,
PositionKind::Barycentric,
"observe_star() requires a Barycentric position"
);
let pm = pm.copied().unwrap_or(ProperMotion::ZERO);
let star = Star::new(
star.ra_deg(),
star.dec_deg(),
pm.pmra,
pm.pmdec,
parallax_mas.unwrap_or(0.0),
0.0,
);
star.observe_from(self, epoch)
}
}
#[cfg(all(test, feature = "python-tests"))]
mod python_tests;
#[cfg(test)]
mod tests {
use super::*;
use crate::constants::C_AUDAY;
use crate::jplephem::kernel::SpiceKernel;
use crate::jplephem_ext::SpiceKernelExt;
use crate::time::Timescale;
use nalgebra::Vector3;
const ARCSEC: f64 = std::f64::consts::PI / 180.0 / 3600.0;
fn de421_kernel() -> SpiceKernel {
SpiceKernel::open("test_data/de421.bsp").expect("Failed to open DE421")
}
fn sirius() -> (StarData, ProperMotion, f64) {
(
StarData::new(32349, 101.2874, -16.7161, -1.46, None),
ProperMotion::new(-546.01, -1223.07),
379.21,
)
}
#[test]
fn test_observe_star_is_astrometric() {
let mut kernel = de421_kernel();
let ts = Timescale::default();
let t = ts.tdb_jd(2451545.0);
let earth = kernel.at("earth", &t).unwrap();
let (star, pm, plx) = sirius();
let astro = earth.observe_star(&star, Some(&pm), Some(plx), &t);
assert_eq!(astro.kind, PositionKind::Astrometric);
assert_eq!(astro.target, STAR_TARGET_ID);
assert_eq!(astro.center, 399);
assert!(astro.observer_barycentric.is_some());
assert!(astro.light_time > 0.0);
assert!(
astro.distance() > 4.0e5 && astro.distance() < 7.0e5,
"Sirius distance {} AU",
astro.distance()
);
assert!((astro.light_time - astro.distance() / C_AUDAY).abs() < 1e-9);
}
#[test]
fn test_observe_star_apparent_pipeline() {
let mut kernel = de421_kernel();
let ts = Timescale::default();
let t = ts.tdb_jd(2451545.0);
let earth = kernel.at("earth", &t).unwrap();
let (star, pm, plx) = sirius();
let apparent = earth
.observe_star(&star, Some(&pm), Some(plx), &t)
.apparent(&mut kernel, &t)
.unwrap();
assert_eq!(apparent.kind, PositionKind::Apparent);
let (ra_h, dec_d, _) = apparent.radec(None);
assert!((ra_h - 101.2874 / 15.0).abs() < 0.01, "RA {ra_h} h");
assert!((dec_d + 16.7161).abs() < 0.01, "Dec {dec_d} deg");
}
#[test]
fn test_zero_motion_returns_catalog_direction() {
let mut kernel = de421_kernel();
let ts = Timescale::default();
let t = ts.tdb_jd(2451545.0);
let earth = kernel.at("earth", &t).unwrap();
let star = StarData::new(1, 123.456, -45.678, 5.0, None);
let astro = earth.observe_star(&star, None, None, &t);
let (ra_h, dec_d, _) = astro.radec(None);
assert!(
(ra_h * 15.0 - 123.456).abs() < 1e-9,
"RA {} deg",
ra_h * 15.0
);
assert!((dec_d + 45.678).abs() < 1e-9, "Dec {dec_d} deg");
}
#[test]
fn test_aberration_from_mars_matches_v_over_c() {
let mut kernel = de421_kernel();
let ts = Timescale::default();
let t = ts.tdb_jd(2451545.0);
let mars = kernel.at("mars", &t).unwrap();
let ssb = Position::barycentric(Vector3::zeros(), Vector3::zeros(), 0);
let star = StarData::new(1, 30.0, 20.0, 5.0, None);
let from_mars = mars
.observe_star(&star, None, None, &t)
.apparent(&mut kernel, &t)
.unwrap();
let from_ssb = ssb
.observe_star(&star, None, None, &t)
.apparent(&mut kernel, &t)
.unwrap();
let measured = from_mars.separation_from(&from_ssb);
let speed_au_day = mars.velocity.norm();
let cos_theta = mars
.velocity
.normalize()
.dot(&from_ssb.position.normalize());
let expected = speed_au_day / C_AUDAY * (1.0 - cos_theta * cos_theta).sqrt();
let speed_km_s = speed_au_day * crate::constants::AU_KM / crate::constants::DAY_S;
assert!(
(21.9..26.6).contains(&speed_km_s),
"Mars barycentric speed {speed_km_s} km/s"
);
let v_over_c_arcsec = speed_au_day / C_AUDAY / ARCSEC;
assert!(
(15.0..18.3).contains(&v_over_c_arcsec),
"Mars v/c = {v_over_c_arcsec} arcsec"
);
println!(
"Mars aberration: measured {:.4}\" expected {:.4}\" (v/c = {v_over_c_arcsec:.3}\")",
measured / ARCSEC,
expected / ARCSEC
);
assert!(
(measured - expected).abs() < 0.02 * expected,
"aberration: measured {} arcsec, expected {} arcsec",
measured / ARCSEC,
expected / ARCSEC
);
}
#[test]
fn test_proper_motion_moves_the_star() {
let mut kernel = de421_kernel();
let ts = Timescale::default();
let t0 = ts.tdb_jd(2451545.0);
let t1 = ts.tdb_jd(2451545.0 + 50.0 * 365.25);
let earth0 = kernel.at("earth", &t0).unwrap();
let earth1 = kernel.at("earth", &t1).unwrap();
let (star, pm, plx) = sirius();
let a0 = earth0.observe_star(&star, Some(&pm), Some(plx), &t0);
let a1 = earth1.observe_star(&star, Some(&pm), Some(plx), &t1);
let moved = a0.separation_from(&a1) / ARCSEC;
assert!(
(60.0..75.0).contains(&moved),
"Sirius moved {moved} arcsec in 50 years"
);
}
#[test]
#[should_panic(expected = "observe_star() requires a Barycentric position")]
fn test_observe_star_rejects_astrometric_observer() {
let mut kernel = de421_kernel();
let ts = Timescale::default();
let t = ts.tdb_jd(2451545.0);
let earth = kernel.at("earth", &t).unwrap();
let mars = earth.observe("mars", &mut kernel, &t).unwrap();
let (star, pm, plx) = sirius();
let _ = mars.observe_star(&star, Some(&pm), Some(plx), &t);
}
}