#![allow(
clippy::many_single_char_names,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
pub const MU: f64 = 3.986_004_418e14;
pub const EARTH_RADIUS: f64 = 6_378_136.0;
pub const J2: f64 = 1.082_625_7e-3;
pub const EARTH_ROTATION_RATE: f64 = 7.292_115e-5;
const DEFAULT_STEP: f64 = 60.0;
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct GlonassEphemeris {
pub slot: u8,
pub toe: f64,
pub position: [f64; 3],
pub velocity: [f64; 3],
pub luni_solar: [f64; 3],
pub tau: f64,
pub gamma: f64,
pub freq_channel: i8,
pub health: u8,
}
fn deriv(s: [f64; 6], mu: f64, j2: f64, omega: f64, acc: [f64; 3]) -> [f64; 6] {
let (x, y, z) = (s[0], s[1], s[2]);
let (vx, vy) = (s[3], s[4]);
let r2 = x * x + y * y + z * z;
let r = libm::sqrt(r2);
let r3 = r2 * r;
let a = 1.5 * j2 * mu * EARTH_RADIUS * EARTH_RADIUS / (r2 * r3);
let b = 5.0 * z * z / r2;
let c = -mu / r3 - a * (1.0 - b);
let om2 = omega * omega;
[
s[3],
s[4],
s[5],
(c + om2) * x + 2.0 * omega * vy + acc[0],
(c + om2) * y - 2.0 * omega * vx + acc[1],
(c - 2.0 * a) * z + acc[2],
]
}
fn rk4_step(s: [f64; 6], h: f64, mu: f64, j2: f64, omega: f64, acc: [f64; 3]) -> [f64; 6] {
let add = |a: [f64; 6], k: [f64; 6], f: f64| {
let mut o = [0.0; 6];
for i in 0..6 {
o[i] = a[i] + k[i] * f;
}
o
};
let k1 = deriv(s, mu, j2, omega, acc);
let k2 = deriv(add(s, k1, h / 2.0), mu, j2, omega, acc);
let k3 = deriv(add(s, k2, h / 2.0), mu, j2, omega, acc);
let k4 = deriv(add(s, k3, h), mu, j2, omega, acc);
let mut o = [0.0; 6];
for i in 0..6 {
o[i] = s[i] + h / 6.0 * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]);
}
o
}
fn integrate(
s0: [f64; 6],
dt: f64,
mu: f64,
j2: f64,
omega: f64,
acc: [f64; 3],
max_step: f64,
) -> [f64; 6] {
if dt == 0.0 {
return s0;
}
let n = libm::ceil(libm::fabs(dt) / max_step).max(1.0);
let h = dt / n;
let steps = n as usize;
let mut s = s0;
for _ in 0..steps {
s = rk4_step(s, h, mu, j2, omega, acc);
}
s
}
#[must_use]
pub fn glonass_l1_frequency(channel: i8) -> f64 {
(1602.0 + f64::from(channel) * 0.5625) * 1.0e6
}
#[must_use]
pub fn glonass_l2_frequency(channel: i8) -> f64 {
(1246.0 + f64::from(channel) * 0.4375) * 1.0e6
}
impl GlonassEphemeris {
#[must_use]
pub fn l1_frequency(&self) -> f64 {
glonass_l1_frequency(self.freq_channel)
}
#[must_use]
pub fn l2_frequency(&self) -> f64 {
glonass_l2_frequency(self.freq_channel)
}
#[must_use]
pub fn position_velocity_pz90(&self, dt: f64) -> ([f64; 3], [f64; 3]) {
self.position_velocity_pz90_with_step(dt, DEFAULT_STEP)
}
#[must_use]
pub fn position_velocity_pz90_with_step(&self, dt: f64, max_step: f64) -> ([f64; 3], [f64; 3]) {
let s0 = [
self.position[0],
self.position[1],
self.position[2],
self.velocity[0],
self.velocity[1],
self.velocity[2],
];
let s = integrate(
s0,
dt,
MU,
J2,
EARTH_ROTATION_RATE,
self.luni_solar,
max_step,
);
([s[0], s[1], s[2]], [s[3], s[4], s[5]])
}
}
#[cfg(test)]
mod tests {
use super::*;
use spacelab_core::Vec3;
use spacelab_elements::KeplerianElements;
fn sample() -> GlonassEphemeris {
let a = 25_510_000.0;
let vc = libm::sqrt(MU / a);
GlonassEphemeris {
slot: 1,
toe: 43_200.0,
position: [a, 0.0, 0.0],
velocity: [0.0, vc * 0.9, vc * 0.4], luni_solar: [1.0e-6, -2.0e-6, 3.0e-6],
tau: 1.0e-4,
gamma: 0.0,
freq_channel: 0,
health: 0,
}
}
#[test]
fn glonass_orbit_stays_near_the_medium_earth_orbit_radius() {
let eph = sample();
for &dt in &[0.0, 300.0, 900.0, -600.0] {
let (p, _) = eph.position_velocity_pz90(dt);
let r = libm::sqrt(p[0] * p[0] + p[1] * p[1] + p[2] * p[2]);
assert!((24.5e6..26.5e6).contains(&r), "dt={dt}: r={r}");
}
}
#[test]
fn reduced_two_body_integration_matches_the_kepler_flow() {
let a = 25_510_000.0;
let vc = libm::sqrt(MU / a);
let s0 = [a, 0.0, 0.0, 0.0, vc * 0.9, vc * 0.4];
let dt = 1200.0;
let s = integrate(s0, dt, MU, 0.0, 0.0, [0.0; 3], 30.0);
let kep = KeplerianElements::from_state(
Vec3::new(s0[0], s0[1], s0[2]),
Vec3::new(s0[3], s0[4], s0[5]),
MU,
);
let (r_oracle, _) = kep.propagate(dt).to_state();
let diff = (Vec3::new(s[0], s[1], s[2]) - r_oracle).norm();
assert!(diff < 1.0, "two-body RK4 vs Kepler: {diff} m");
}
#[test]
#[allow(clippy::float_cmp)]
fn the_icd_right_hand_side_matches_the_published_equations() {
const STATE: [f64; 6] = [1.5e7, -8.0e6, 1.9e7, 1500.0, 3000.0, -900.0];
const ACC: [f64; 3] = [1.0e-6, -2.0e-6, 3.0e-6];
const WANT: [f64; 3] = [
1.565_617_251_861_625_7e-1,
-6.891_682_343_262_004e-2,
-4.570_127_719_221_374_6e-1,
];
let d = deriv(STATE, MU, J2, EARTH_ROTATION_RATE, ACC);
assert_eq!([d[0], d[1], d[2]], [STATE[3], STATE[4], STATE[5]]);
for (k, (&got, &want)) in d[3..].iter().zip(WANT.iter()).enumerate() {
assert!(
(got - want).abs() <= 1e-13 * want.abs(),
"row {k}: {got:.17e} vs Maxima {want:.17e}",
);
}
}
#[test]
fn the_sub_step_count_is_the_interval_divided_by_the_bound() {
let a = 25_510_000.0;
let vc = libm::sqrt(MU / a);
let s0 = [a, 0.0, 0.0, 0.0, vc * 0.9, vc * 0.4];
let dt = 1200.0;
let kep = KeplerianElements::from_state(
Vec3::new(s0[0], s0[1], s0[2]),
Vec3::new(s0[3], s0[4], s0[5]),
MU,
);
let (r_oracle, _) = kep.propagate(dt).to_state();
let err = |max_step: f64| {
let s = integrate(s0, dt, MU, 0.0, 0.0, [0.0; 3], max_step);
(Vec3::new(s[0], s[1], s[2]) - r_oracle).norm()
};
let (e300, e150) = (err(300.0), err(150.0));
assert!(
(0.05..0.5).contains(&e300),
"4 × 300 s should miss Kepler by ~0.17 m, not {e300} m",
);
assert!(
(12.0..20.0).contains(&(e300 / e150)),
"ratio {}",
e300 / e150
);
}
#[test]
fn rk4_converges_at_fourth_order_in_the_step() {
let eph = sample();
let dt = 900.0;
let reference = eph.position_velocity_pz90_with_step(dt, 5.0).0;
let coarse = eph.position_velocity_pz90_with_step(dt, 120.0).0;
let fine = eph.position_velocity_pz90_with_step(dt, 60.0).0;
let err = |a: [f64; 3]| {
libm::sqrt(
(a[0] - reference[0]).powi(2)
+ (a[1] - reference[1]).powi(2)
+ (a[2] - reference[2]).powi(2),
)
};
let (ec, ef) = (err(coarse), err(fine));
assert!(ef < ec, "finer step not better: {ef} vs {ec}");
assert!(
ec / ef.max(1e-12) > 8.0,
"convergence ratio {} too low",
ec / ef.max(1e-12)
);
}
#[test]
fn fdma_carrier_frequencies_follow_the_channel_relation() {
assert!((glonass_l1_frequency(0) - 1_602.0e6).abs() < 1e-3);
assert!((glonass_l2_frequency(0) - 1_246.0e6).abs() < 1e-3);
assert!((glonass_l1_frequency(1) - glonass_l1_frequency(0) - 0.5625e6).abs() < 1e-3);
assert!((glonass_l2_frequency(-7) - (1246.0 - 7.0 * 0.4375) * 1e6).abs() < 1e-3);
let mut eph = sample();
eph.freq_channel = 3;
assert!((eph.l1_frequency() - glonass_l1_frequency(3)).abs() < 1e-9);
assert!((eph.l2_frequency() - glonass_l2_frequency(3)).abs() < 1e-9);
}
}