#[cfg(test)]
mod tests {
use crate::catalogs::StarData;
use crate::framelib::inertial::ProperMotion;
use crate::jplephem_ext::SpiceKernelExt;
use crate::pybridge::bridge::PyRustBridge;
use crate::pybridge::test_utils::{de421_kernel, parse_f64_list};
use crate::time::Timescale;
const STARS: [(u64, f64, f64, f64, f64, f64); 20] = [
(32349, 101.2874, -16.7161, -546.01, -1223.07, 379.21), (91262, 279.2347, 38.7837, 200.94, 286.23, 130.23), (27989, 88.7929, 7.4070, 26.40, 9.56, 5.95), (26727, 85.1897, -1.9426, 3.19, 2.03, 3.99), (26311, 84.0534, -1.2019, 1.49, -1.06, 2.43), (25930, 83.0016, -0.2991, 0.92, -1.20, 3.56), (24608, 79.1723, 45.9980, 75.52, -427.11, 77.29), (24436, 78.6345, -8.2017, 1.87, -0.56, 4.22), (37279, 114.8255, 5.2250, -716.57, -1034.58, 285.93), (69673, 213.9153, 19.1824, -1093.45, -1999.40, 88.85), (71683, 219.9021, -60.8340, -3678.19, 481.84, 742.12), (87937, 269.4521, 4.6934, -798.71, 10337.77, 549.01), (11767, 37.9529, 89.2641, 44.22, -11.74, 7.54), (65474, 201.2983, -11.1613, -42.50, -31.73, 12.44), (80763, 247.3519, -26.4320, -10.16, -23.21, 5.40), (97649, 297.6958, 8.8683, 536.82, 385.54, 194.44), (21421, 68.9802, 16.5093, 63.45, -188.94, 50.09), (30438, 95.9880, -52.6957, 19.93, 23.24, 10.43), (49669, 152.0930, 11.9672, -249.40, 4.91, 42.09), (102098, 310.3580, 45.2803, 1.56, 1.55, 1.01), ];
fn python_star_table() -> String {
STARS
.iter()
.map(|(_, ra, dec, pmra, pmdec, plx)| {
format!(" ({ra:?}, {dec:?}, {pmra:?}, {pmdec:?}, {plx:?}),")
})
.collect::<Vec<_>>()
.join("\n")
}
fn skyfield_apparent(body: &str, jds: &[f64]) -> Vec<f64> {
let bridge = PyRustBridge::new().expect("Failed to create Python bridge");
let code = format!(
r#"
from skyfield.api import load, load_file, Star
ts = load.timescale()
eph = load_file('test_data/de421.bsp')
rows = [
{table}
]
out = []
for jd in [{jds}]:
t = ts.tdb_jd(jd)
observer = eph['{body}'].at(t)
for ra, dec, pmra, pmdec, plx in rows:
star = Star(ra_hours=ra / 15.0, dec_degrees=dec,
ra_mas_per_year=pmra, dec_mas_per_year=pmdec,
parallax_mas=plx)
apparent = observer.observe(star).apparent()
ra_a, dec_a, _ = apparent.radec()
out.append(ra_a._degrees)
out.append(dec_a.degrees)
# float() first: NumPy 2 reprs a scalar as np.float64(...), which the
# Rust side cannot parse.
rust.collect_string(','.join(repr(float(v)) for v in out))
"#,
table = python_star_table(),
jds = jds
.iter()
.map(|jd| format!("{jd:?}"))
.collect::<Vec<_>>()
.join(", "),
body = body,
);
let result = bridge.run_py_to_json(&code).expect("Skyfield run failed");
parse_f64_list(&result)
}
fn separation_mas(ra1: f64, dec1: f64, ra2: f64, dec2: f64) -> f64 {
let d_dec = dec2 - dec1;
let mut d_ra = ra2 - ra1;
if d_ra > 180.0 {
d_ra -= 360.0;
} else if d_ra < -180.0 {
d_ra += 360.0;
}
let d_ra = d_ra * dec1.to_radians().cos();
(d_ra * d_ra + d_dec * d_dec).sqrt() * 3_600_000.0
}
fn max_error_mas(body: &str, jds: &[f64]) -> f64 {
let python = skyfield_apparent(body, jds);
assert_eq!(python.len(), 2 * STARS.len() * jds.len());
let mut kernel = de421_kernel();
let ts = Timescale::default();
let mut worst = 0.0f64;
let mut index = 0;
for &jd in jds {
let t = ts.tdb_jd(jd);
let observer = kernel.at(body, &t).unwrap();
for &(hip, ra, dec, pmra, pmdec, plx) in STARS.iter() {
let star = StarData::new(hip, ra, dec, 0.0, None);
let pm = ProperMotion::new(pmra, pmdec);
let apparent = observer
.observe_star(&star, Some(&pm), Some(plx), &t)
.apparent(&mut kernel, &t)
.unwrap();
let (ra_h, dec_deg, _) = apparent.radec(None);
let error = separation_mas(ra_h * 15.0, dec_deg, python[index], python[index + 1]);
assert!(
error < 1.0,
"HIP {hip} at JD {jd} from {body}: {error:.4} mas \
(rust {ra_deg:.9} {dec_deg:.9}, skyfield {py_ra:.9} {py_dec:.9})",
ra_deg = ra_h * 15.0,
py_ra = python[index],
py_dec = python[index + 1],
);
worst = worst.max(error);
index += 2;
}
}
worst
}
#[test]
fn test_observe_star_matches_skyfield_from_earth() {
let worst = max_error_mas("earth", &[2451545.0, 2460676.5]);
println!("observe_star from Earth: max error {worst:.6} mas");
assert!(worst < 1.0, "max error {worst} mas");
}
#[test]
fn test_observe_star_matches_skyfield_from_mars() {
let worst = max_error_mas("mars", &[2451545.0]);
println!("observe_star from Mars: max error {worst:.6} mas");
assert!(worst < 1.0, "max error {worst} mas");
}
}