use core::f64::consts::TAU;
#[cfg(all(feature = "libm", not(feature = "std"), not(test)))]
use crate::math::Float as _;
use crate::motion::Track;
use crate::position::{Ecef, Eci};
use crate::vector::Vec3;
const J2000_JD: f64 = 2_451_545.0;
const J2000_UNIX: f64 = 946_728_000.0;
#[inline]
pub fn julian_date(unix_seconds: f64) -> f64 {
2_440_587.5 + unix_seconds / 86_400.0
}
#[doc(alias = "teme")]
pub fn gmst_rad(julian_date_ut1: f64) -> f64 {
gmst_from_days(julian_date_ut1 - J2000_JD)
}
#[inline]
pub fn gmst_from_unix(unix_seconds: f64) -> f64 {
gmst_from_unix_with_dut1(unix_seconds, 0.0)
}
pub fn gmst_from_unix_with_dut1(unix_seconds: f64, dut1_s: f64) -> f64 {
gmst_from_days(((unix_seconds - J2000_UNIX) + dut1_s) / 86_400.0)
}
pub fn era_rad(julian_date_ut1: f64) -> f64 {
era_from_days(julian_date_ut1 - J2000_JD)
}
pub fn era_from_unix_with_dut1(unix_seconds: f64, dut1_s: f64) -> f64 {
era_from_days(((unix_seconds - J2000_UNIX) + dut1_s) / 86_400.0)
}
fn gmst_from_days(d: f64) -> f64 {
let t = d / 36_525.0; let extra_per_day = 8_640_184.812_866 / 36_525.0 / 240.0;
let deg =
67_310.548_41 / 240.0 + 360.0 * (d - d.floor()) + extra_per_day * d + (0.093_104 - 6.2e-6 * t) * t * t / 240.0;
wrap_turn(deg / 360.0)
}
fn era_from_days(du: f64) -> f64 {
wrap_turn((du - du.floor()) + 0.779_057_273_264 + 0.002_737_811_911_354_48 * du)
}
fn wrap_turn(turns: f64) -> f64 {
let r = (turns - turns.floor()) * TAU;
if r >= TAU {
0.0
} else {
r
}
}
#[doc(alias = "teme")]
#[inline]
pub fn ecef_to_eci(p: Ecef, gmst_rad: f64) -> Eci {
let (s, c) = gmst_rad.sin_cos();
Eci::new(p.x * c - p.y * s, p.x * s + p.y * c, p.z)
}
#[doc(alias = "teme")]
#[inline]
pub fn eci_to_ecef(p: Eci, gmst_rad: f64) -> Ecef {
let (s, c) = gmst_rad.sin_cos();
Ecef::new(p.x * c + p.y * s, -p.x * s + p.y * c, p.z)
}
const GMST_RATE_RAD_S: f64 = (360.0 + 8_640_184.812_866 / 36_525.0 / 240.0) / 360.0 * TAU / 86_400.0;
impl Track {
#[doc(alias = "teme")]
pub fn from_eci(position: Eci, velocity: Vec3, gmst_rad: f64) -> Track {
let p = eci_to_ecef(position, gmst_rad);
let v = eci_to_ecef(Eci::new(velocity.x, velocity.y, velocity.z), gmst_rad);
let w = GMST_RATE_RAD_S;
Track::new(p, Vec3::new(v.x + w * p.y, v.y - w * p.x, v.z))
}
}
impl Eci {
#[inline]
pub fn to_ecef(self, gmst_rad: f64) -> Ecef {
eci_to_ecef(self, gmst_rad)
}
}
impl Ecef {
#[inline]
pub fn to_eci(self, gmst_rad: f64) -> Eci {
ecef_to_eci(self, gmst_rad)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::close;
#[test]
fn track_from_eci_is_the_rate_of_change_of_the_ecef_position() {
let t0 = J2000_UNIX + 5_000.0;
let slope = (gmst_from_unix(t0 + 100.0) - gmst_from_unix(t0 - 100.0)) / 200.0;
assert!(close(slope, GMST_RATE_RAD_S, 1e-17), "{slope} vs {GMST_RATE_RAD_S}");
let p = Eci::new(-4_400_594.0, 1_932_870.0, 4_760_712.0);
let v = Vec3::new(-5_316.6, -3_969.5, -3_303.5);
let gmst = 1.234;
let track = Track::from_eci(p, v, gmst);
let at = |dt: f64| {
let q = Eci::new(p.x + v.x * dt, p.y + v.y * dt, p.z + v.z * dt);
eci_to_ecef(q, gmst + GMST_RATE_RAD_S * dt).vec()
};
let dt = 1e-2;
let numeric = (at(dt) - at(-dt)) / (2.0 * dt);
assert!(
(track.velocity - numeric).norm() < 1e-5,
"{:?} vs {numeric:?}",
track.velocity
);
assert_eq!(track.position, eci_to_ecef(p, gmst));
let still = Track::from_eci(Eci::new(7e6, 0.0, 0.0), Vec3::ZERO, 0.0);
assert!(close(still.velocity.y, -7e6 * GMST_RATE_RAD_S, 1e-9));
}
#[test]
fn julian_dates() {
assert_eq!(julian_date(0.0), 2_440_587.5);
assert!(close(julian_date(946_728_000.0), 2_451_545.0, 1e-9));
}
#[test]
fn gmst_matches_reference_values() {
assert!(close(gmst_rad(2_451_545.0).to_degrees(), 280.460_618_37, 1e-6));
let sidereal_day = 86_164.090_5 / 86_400.0;
assert!(close(
gmst_rad(2_451_545.0 + sidereal_day).to_degrees(),
280.460_618_37,
1e-3
));
for jd in [2_400_000.5, 2_451_545.0, 2_470_000.25] {
let g = gmst_rad(jd);
assert!((0.0..core::f64::consts::TAU).contains(&g));
}
assert!(close(gmst_from_unix(946_728_000.0), gmst_rad(2_451_545.0), 1e-15));
}
fn gmst_old(jd: f64) -> f64 {
let t = (jd - 2_451_545.0) / 36_525.0;
let seconds =
67_310.548_41 + (876_600.0 * 3600.0 + 8_640_184.812_866) * t + 0.093_104 * t * t - 6.2e-6 * t * t * t;
(seconds.rem_euclid(86_400.0) / 86_400.0) * TAU
}
#[test]
fn matches_sofa_reference_values() {
assert!(close(gmst_rad(2_400_000.5 + 53_736.0), 1.754_174_981_860_675, 1e-12));
assert!(close(era_rad(2_400_000.5 + 54_388.0), 0.402_283_724_002_815_8, 1e-12));
assert!(close(era_rad(2_451_545.0), gmst_rad(2_451_545.0), 1e-5));
}
#[test]
fn rearranged_gmst_matches_the_formula_and_keeps_microseconds() {
for jd in [2_400_000.5, 2_451_545.0, 2_460_000.25, 2_470_000.75] {
assert!(close(gmst_rad(jd), gmst_old(jd), 1e-9), "{jd}");
}
let t = 1_800_000_000.0;
let step = gmst_from_unix(t + 1e-5) - gmst_from_unix(t);
assert!(close(step, 7.292_115_9e-10, 0.05 * 7.29e-10), "{step}");
let shift = gmst_from_unix_with_dut1(t, 0.5) - gmst_from_unix(t);
assert!(close(shift, 0.5 * 7.292_115_9e-5, 1e-9), "{shift}");
assert!(close(
era_from_unix_with_dut1(946_728_000.0, 0.0),
era_rad(2_451_545.0),
1e-15
));
}
#[test]
fn rotation_conventions() {
let p = Ecef::new(1.0, 0.0, 5.0);
assert_eq!(ecef_to_eci(p, 0.0), Eci::new(1.0, 0.0, 5.0));
let q = ecef_to_eci(p, core::f64::consts::FRAC_PI_2);
assert!(close(q.x, 0.0, 1e-15) && close(q.y, 1.0, 1e-15) && q.z == 5.0);
let g = 1.234;
let back = p.to_eci(g).to_ecef(g);
assert!(close(back.x, p.x, 1e-15) && close(back.y, p.y, 1e-15) && back.z == p.z);
}
}