pub const MU_EARTH: f64 = 3.986_004_418e14;
pub const RE_EARTH: f64 = 6_378_137.0;
pub const J2: f64 = 1.082_626_68e-3;
type Vec3 = [f64; 3];
fn norm(r: Vec3) -> f64 {
(r[0] * r[0] + r[1] * r[1] + r[2] * r[2]).sqrt()
}
pub fn two_body_accel(r: Vec3) -> Vec3 {
let rn = norm(r);
let k = -MU_EARTH / (rn * rn * rn);
[k * r[0], k * r[1], k * r[2]]
}
pub fn j2_accel(r: Vec3) -> Vec3 {
let rn = norm(r);
let r2 = rn * rn;
let zr2 = 5.0 * r[2] * r[2] / r2;
let c = -1.5 * J2 * MU_EARTH * RE_EARTH * RE_EARTH / rn.powi(5);
[
c * r[0] * (1.0 - zr2),
c * r[1] * (1.0 - zr2),
c * r[2] * (3.0 - zr2),
]
}
pub fn gravity_accel(r: Vec3) -> Vec3 {
let a = two_body_accel(r);
let b = j2_accel(r);
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
pub fn mean_motion(a: f64) -> f64 {
(MU_EARTH / (a * a * a)).sqrt()
}
#[derive(Clone, Copy, Debug)]
pub struct SecularRates {
pub raan: f64,
pub arg_perigee: f64,
pub mean_anomaly: f64,
}
pub fn j2_secular_rates(a: f64, e: f64, i_rad: f64) -> SecularRates {
let n = mean_motion(a);
let p = a * (1.0 - e * e);
let factor = n * J2 * (RE_EARTH / p).powi(2);
let (si, ci) = i_rad.sin_cos();
let sin2 = si * si;
SecularRates {
raan: -1.5 * factor * ci,
arg_perigee: 1.5 * factor * (2.0 - 2.5 * sin2),
mean_anomaly: 1.5 * factor * (1.0 - e * e).sqrt() * (1.0 - 1.5 * sin2),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn two_body_acceleration_is_mu_over_r_squared() {
let r = [7.0e6, 0.0, 0.0];
let a = two_body_accel(r);
let expect = MU_EARTH / (7.0e6 * 7.0e6); assert!((a[0] + expect).abs() / expect < 1e-12, "ax = {}", a[0]);
assert!(a[1].abs() < 1e-12 && a[2].abs() < 1e-12);
assert!((expect - 8.1347).abs() < 1e-3, "|a| = {expect}");
}
#[test]
fn j2_acceleration_matches_closed_form_at_equator() {
let r = [7.0e6, 0.0, 0.0];
let a = j2_accel(r);
assert!((a[0] + 0.010_967).abs() < 1e-5, "a_J2x = {}", a[0]);
assert!(a[1].abs() < 1e-15 && a[2].abs() < 1e-15);
let ratio = a[0].abs() / two_body_accel(r)[0].abs();
assert!(ratio < 2e-3 && ratio > 1e-3, "J2/two-body = {ratio}");
}
#[test]
fn critical_inclination_freezes_the_perigee() {
let a = 7.0e6;
let crit = (0.8_f64).sqrt().asin();
let rates = j2_secular_rates(a, 0.001, crit);
assert!(
rates.arg_perigee.abs() < 1e-12,
"ω̇ = {} at critical i",
rates.arg_perigee
);
assert!(j2_secular_rates(a, 0.001, 0.5).arg_perigee > 0.0);
assert!(j2_secular_rates(a, 0.001, 1.2).arg_perigee < 0.0);
}
#[test]
fn iss_nodal_regression_is_about_minus_five_degrees_per_day() {
let rates = j2_secular_rates(6.790e6, 0.0007, 51.6_f64.to_radians());
let deg_per_day = rates.raan.to_degrees() * 86_400.0;
assert!(
(deg_per_day - (-5.0)).abs() < 0.6,
"Ω̇ = {deg_per_day} °/day"
);
}
#[test]
fn sun_synchronous_inclination_drifts_eastward() {
let rates = j2_secular_rates(7.078e6, 0.0, 98.0_f64.to_radians());
assert!(rates.raan > 0.0, "Ω̇ should be eastward: {}", rates.raan);
}
}