use core::mem::MaybeUninit;
use supernovas_ffi::{
novas_invert_transform, novas_make_transform, novas_transform, novas_transform_sky_pos,
novas_transform_vector,
};
use crate::{
Apparent, Frame, ReferenceSystem,
error::{Error, Result},
};
#[derive(Debug, Clone, Copy)]
pub struct Transform(novas_transform);
impl Transform {
pub fn new(
frame: &Frame,
from_system: ReferenceSystem,
to_system: ReferenceSystem,
) -> Result<Self> {
let mut t = MaybeUninit::<novas_transform>::zeroed();
let rc = unsafe {
novas_make_transform(
frame.as_novas_frame(),
from_system.to_sys(),
to_system.to_sys(),
t.as_mut_ptr(),
)
};
if rc != 0 {
return Err(Error::ffi(rc));
}
Ok(Transform(unsafe { t.assume_init() }))
}
#[must_use]
pub fn from_system(self) -> ReferenceSystem {
map_sys(self.0.from_system)
}
#[must_use]
pub fn to_system(self) -> ReferenceSystem {
map_sys(self.0.to_system)
}
#[must_use]
pub fn frame(self) -> Frame {
Frame::from_novas(self.0.frame)
}
pub fn invert(&self) -> Result<Transform> {
let mut inv = MaybeUninit::<novas_transform>::zeroed();
let rc = unsafe { novas_invert_transform(&raw const self.0, inv.as_mut_ptr()) };
if rc != 0 {
return Err(Error::ffi(rc));
}
let mut inv = unsafe { inv.assume_init() };
core::mem::swap(&mut inv.from_system, &mut inv.to_system);
Ok(Transform(inv))
}
pub fn apply_vector(self, v: [f64; 3]) -> Result<[f64; 3]> {
let mut out = [0.0_f64; 3];
let rc = unsafe { novas_transform_vector(v.as_ptr(), &raw const self.0, out.as_mut_ptr()) };
if rc != 0 {
return Err(Error::ffi(rc));
}
Ok(out)
}
pub fn apply_sky_pos(self, app: &Apparent) -> Result<Apparent> {
let mut out = MaybeUninit::<supernovas_ffi::sky_pos>::zeroed();
let rc = unsafe {
novas_transform_sky_pos(app.as_sky_pos(), &raw const self.0, out.as_mut_ptr())
};
if rc != 0 {
return Err(Error::ffi(rc));
}
let sky = unsafe { out.assume_init() };
Ok(Apparent::from_parts(
self.frame(),
map_sys(self.0.to_system),
sky,
))
}
}
fn map_sys(s: supernovas_ffi::novas_reference_system) -> ReferenceSystem {
use supernovas_ffi::novas_reference_system as sys;
match s {
sys::NOVAS_GCRS => ReferenceSystem::Gcrs,
sys::NOVAS_TOD => ReferenceSystem::Tod,
sys::NOVAS_CIRS => ReferenceSystem::Cirs,
sys::NOVAS_ICRS => ReferenceSystem::Icrs,
sys::NOVAS_J2000 => ReferenceSystem::J2000,
sys::NOVAS_MOD => ReferenceSystem::Mod,
sys::NOVAS_TIRS => ReferenceSystem::Tirs,
sys::NOVAS_ITRS => ReferenceSystem::Itrs,
#[allow(unreachable_patterns)]
_ => unreachable!(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Accuracy, CatalogEntry, Observer, Time, Timescale};
fn frame() -> Frame {
let obs = Observer::geodetic(37.234, -118.282, 1222.0).unwrap();
let t = Time::from_utc_jd(2_461_236.75, 37, 0.0).unwrap();
Frame::new(Accuracy::Reduced, &obs, &t).unwrap()
}
fn vega() -> CatalogEntry {
CatalogEntry::icrs(
"Vega",
"18:36:56.336".parse().unwrap(),
"+38:47:01.28".parse().unwrap(),
)
.unwrap()
}
#[test]
fn new_and_accessors_round_trip() {
let f = frame();
let t = Transform::new(&f, ReferenceSystem::Cirs, ReferenceSystem::Itrs).unwrap();
assert_eq!(t.from_system(), ReferenceSystem::Cirs);
assert_eq!(t.to_system(), ReferenceSystem::Itrs);
assert!((t.frame().tt_jd() - f.tt_jd()).abs() < 1e-9);
}
#[test]
fn identity_transform_is_noop_on_vector() {
let f = frame();
let t = Transform::new(&f, ReferenceSystem::Cirs, ReferenceSystem::Cirs).unwrap();
let v = [0.3, 0.4, 0.5];
let out = t.apply_vector(v).unwrap();
for (a, b) in v.iter().zip(out.iter()) {
assert!(
(a - b).abs() < 1e-12,
"identity should leave vector unchanged"
);
}
}
#[test]
fn cirs_itrs_round_trip_via_invert() {
let f = frame();
let fwd = Transform::new(&f, ReferenceSystem::Cirs, ReferenceSystem::Itrs).unwrap();
let inv = fwd.invert().unwrap();
let v = [0.6, -0.2, 0.8];
let out = inv.apply_vector(fwd.apply_vector(v).unwrap()).unwrap();
let max_err = v
.iter()
.zip(out.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0_f64, f64::max);
assert!(
max_err < 1e-9,
"round-trip should recover input, max err = {max_err}"
);
assert_eq!(inv.from_system(), ReferenceSystem::Itrs);
assert_eq!(inv.to_system(), ReferenceSystem::Cirs);
}
#[test]
fn invert_aliasing_safe() {
let f = frame();
let t = Transform::new(&f, ReferenceSystem::Cirs, ReferenceSystem::Itrs).unwrap();
let _ = t.invert().unwrap();
let _ = t.apply_vector([1.0, 0.0, 0.0]).unwrap();
}
#[test]
fn apply_sky_pos_retags_system() {
let f = frame();
let apparent = vega().apparent_in(&f, ReferenceSystem::Cirs).unwrap();
let t = Transform::new(&f, ReferenceSystem::Cirs, ReferenceSystem::Itrs).unwrap();
let out = t.apply_sky_pos(&apparent).unwrap();
assert_eq!(out.reference_system(), ReferenceSystem::Itrs);
let rh = out.as_sky_pos().r_hat;
let mag = (rh[0] * rh[0] + rh[1] * rh[1] + rh[2] * rh[2]).sqrt();
assert!((mag - 1.0).abs() < 1e-9, "rotated r_hat must be unit");
}
#[test]
fn apply_sky_pos_round_trip_via_inverse() {
let f = frame();
let apparent = vega().apparent_in(&f, ReferenceSystem::Cirs).unwrap();
let fwd = Transform::new(&f, ReferenceSystem::Cirs, ReferenceSystem::J2000).unwrap();
let inv = fwd.invert().unwrap();
let out = fwd.apply_sky_pos(&apparent).unwrap();
let back = inv.apply_sky_pos(&out).unwrap();
assert!((back.ra().hours() - apparent.ra().hours()).abs() < 1e-9);
assert!((back.dec().deg() - apparent.dec().deg()).abs() < 1e-9);
}
#[test]
fn j2000_icrs_equivalent_under_transform() {
let f = Frame::new(
Accuracy::Reduced,
&Observer::Geocenter,
&Time::from_jd(Timescale::Tt, 2_451_545.0, 32, 0.0).unwrap(),
)
.unwrap();
let j2000 = vega().apparent_in(&f, ReferenceSystem::J2000).unwrap();
let icrs = vega().apparent_in(&f, ReferenceSystem::Icrs).unwrap();
let t = Transform::new(&f, ReferenceSystem::J2000, ReferenceSystem::Icrs).unwrap();
let via_t = t.apply_sky_pos(&j2000).unwrap();
let sep = via_t.equatorial().distance_to(icrs.equatorial());
assert!(
sep.arcsec() < 0.1,
"J2000->ICRS transform drift = {} arcsec",
sep.arcsec()
);
}
}