use tpt_math_linalg::tpt_math_linalg_dense::{DMatrix, DVector};
pub use error::AstroError;
mod error;
pub const EARTH_MU: f64 = 398_600.441_8;
pub const EARTH_J2: f64 = 1.082_626_68e-3;
pub const EARTH_RADIUS_EQ: f64 = 6378.137;
pub const EARTH_J4: f64 = -1.620_836_15e-6;
pub const EARTH_ATM_RHO0_KG_M3: f64 = 5.428e-13;
pub const EARTH_ATM_H0_KM: f64 = 400.0;
pub const EARTH_ATM_SCALE_HEIGHT_KM: f64 = 58.515;
pub const SOLAR_PRESSURE_1AU: f64 = 4.56e-6;
pub const ASTRONOMICAL_UNIT_KM: f64 = 1.495_978_707e8;
pub const SUN_MU: f64 = 1.327_124_400_18e11;
pub const MOON_MU: f64 = 4_902.800_66;
pub const MOON_DISTANCE_KM: f64 = 384_400.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OrbitalElements {
pub a: f64,
pub e: f64,
pub i: f64,
pub raan: f64,
pub argp: f64,
pub nu: f64,
pub mu: f64,
}
impl OrbitalElements {
pub fn new(
a: f64,
e: f64,
i: f64,
raan: f64,
argp: f64,
nu: f64,
mu: f64,
) -> Result<Self, AstroError> {
if !a.is_finite() || a <= 0.0 {
return Err(AstroError::InvalidElements(format!(
"semi-major axis must be > 0, got {a}"
)));
}
if !e.is_finite() || !(0.0..1.0).contains(&e) {
return Err(AstroError::InvalidElements(format!(
"eccentricity must satisfy 0 <= e < 1, got {e}"
)));
}
for (name, x) in [
("inclination", i),
("raan", raan),
("argument of periapsis", argp),
("true anomaly", nu),
] {
if !x.is_finite() {
return Err(AstroError::InvalidElements(format!(
"{name} must be finite"
)));
}
}
if !mu.is_finite() || mu <= 0.0 {
return Err(AstroError::InvalidElements(format!(
"gravitational parameter must be > 0, got {mu}"
)));
}
Ok(Self {
a,
e,
i,
raan,
argp,
nu,
mu,
})
}
#[must_use]
pub fn period(&self) -> f64 {
const TWO_PI: f64 = 2.0 * std::f64::consts::PI;
TWO_PI * (self.a.powi(3) / self.mu).sqrt()
}
#[must_use]
pub fn state_vector(&self) -> (DVector<f64>, DVector<f64>) {
let p = self.a * (1.0 - self.e.powi(2));
let r = p / (1.0 + self.e * self.nu.cos());
let r_pf = DVector::from_vec(vec![r * self.nu.cos(), r * self.nu.sin(), 0.0]);
let v_scale = (self.mu / p).sqrt();
let v_pf = DVector::from_vec(vec![-self.nu.sin(), self.e + self.nu.cos(), 0.0]) * v_scale;
let q = perifocal_to_eci(self.raan, self.i, self.argp);
let r_eci = q.clone() * r_pf;
let v_eci = q * v_pf;
(r_eci, v_eci)
}
pub fn from_state(pos: &DVector<f64>, vel: &DVector<f64>, mu: f64) -> Result<Self, AstroError> {
if !mu.is_finite() || mu <= 0.0 {
return Err(AstroError::InvalidElements(format!(
"gravitational parameter must be > 0, got {mu}"
)));
}
for (name, v) in [("position", pos), ("velocity", vel)] {
for (k, x) in v.iter().enumerate() {
if !x.is_finite() {
return Err(AstroError::InvalidElements(format!(
"{name} component {k} must be finite"
)));
}
}
}
let r = pos.norm();
let v = vel.norm();
if r <= 0.0 {
return Err(AstroError::DegenerateGeometry(
"position magnitude is zero".to_string(),
));
}
let energy = v * v / 2.0 - mu / r;
let a = -mu / (2.0 * energy);
if !a.is_finite() || a <= 0.0 {
return Err(AstroError::DegenerateGeometry(
"semi-major axis is not positive (orbit is not elliptical)".to_string(),
));
}
let h = cross3(pos, vel);
let h_mag = h.norm();
if h_mag <= 0.0 {
return Err(AstroError::DegenerateGeometry(
"angular momentum is zero".to_string(),
));
}
let e_vec = (pos.clone() * (v * v - mu / r) - vel.clone() * pos.dot(vel)) / mu;
let e = e_vec.norm();
let i = (h[2] / h_mag).clamp(-1.0, 1.0).acos();
let n = DVector::from_vec(vec![-h[1], h[0], 0.0]);
let n_mag = n.norm();
let raan = if n_mag < 1e-12 {
0.0
} else {
let mut raan = (n[0] / n_mag).clamp(-1.0, 1.0).acos();
if n[1] < 0.0 {
raan = 2.0 * std::f64::consts::PI - raan;
}
raan
};
let argp = if e < 1e-12 || n_mag < 1e-12 {
0.0
} else {
let mut argp = (n.dot(&e_vec) / (n_mag * e)).clamp(-1.0, 1.0).acos();
if e_vec[2] < 0.0 {
argp = 2.0 * std::f64::consts::PI - argp;
}
argp
};
let nu = if e < 1e-12 {
if n_mag < 1e-12 {
0.0
} else {
let mut nu = (n.dot(pos) / (n_mag * r)).clamp(-1.0, 1.0).acos();
if pos.dot(vel) < 0.0 {
nu = 2.0 * std::f64::consts::PI - nu;
}
nu
}
} else {
let mut nu = (e_vec.dot(pos) / (e * r)).clamp(-1.0, 1.0).acos();
if pos.dot(vel) < 0.0 {
nu = 2.0 * std::f64::consts::PI - nu;
}
nu
};
OrbitalElements::new(a, e, i, raan, argp, nu, mu)
}
#[must_use]
pub fn propagate(&self, dt: f64) -> OrbitalElements {
let n_motion = (self.mu / self.a.powi(3)).sqrt();
let e = self.e;
let e0 = true_to_eccentric(self.nu, e);
let m0 = e0 - e * e0.sin();
let m1 = m0 + n_motion * dt;
let e1 = solve_kepler(m1, e);
let nu1 = eccentric_to_true(e1, e);
let mut nu = nu1;
nu = nu.rem_euclid(2.0 * std::f64::consts::PI);
OrbitalElements {
a: self.a,
e: self.e,
i: self.i,
raan: self.raan,
argp: self.argp,
nu,
mu: self.mu,
}
}
#[must_use]
pub fn j2_secular_rates(&self, j2: f64, r_eq: f64) -> (f64, f64) {
let n = (self.mu / self.a.powi(3)).sqrt();
let p = self.a * (1.0 - self.e.powi(2));
let factor = 1.5 * n * j2 * (r_eq / p).powi(2);
let ci = self.i.cos();
let raan_dot = -factor * ci;
let argp_dot = 0.5 * factor * (5.0 * ci * ci - 1.0);
(raan_dot, argp_dot)
}
#[must_use]
pub fn propagate_j2(&self, dt: f64, j2: f64, r_eq: f64) -> OrbitalElements {
let in_plane = self.propagate(dt);
let (raan_dot, argp_dot) = self.j2_secular_rates(j2, r_eq);
let two_pi = 2.0 * std::f64::consts::PI;
OrbitalElements {
a: in_plane.a,
e: in_plane.e,
i: in_plane.i,
raan: (in_plane.raan + raan_dot * dt).rem_euclid(two_pi),
argp: (in_plane.argp + argp_dot * dt).rem_euclid(two_pi),
nu: in_plane.nu,
mu: in_plane.mu,
}
}
#[must_use]
pub fn drag_da_dt(&self, cd_a_over_m: f64) -> f64 {
let perigee_altitude_km = self.a * (1.0 - self.e) - EARTH_RADIUS_EQ;
let rho = atmospheric_density(perigee_altitude_km);
let n = (self.mu / self.a.powi(3)).sqrt();
let a_m = self.a * 1000.0;
let ecc_factor = ((1.0 + self.e) / (1.0 - self.e)).sqrt();
let da_dt_m_per_s = -rho * cd_a_over_m * n * a_m * a_m * ecc_factor;
da_dt_m_per_s / 1000.0
}
#[must_use]
pub fn propagate_drag(&self, dt: f64, cd_a_over_m: f64) -> OrbitalElements {
let in_plane = self.propagate(dt);
let da_dt = self.drag_da_dt(cd_a_over_m);
let new_a = (in_plane.a + da_dt * dt).max(EARTH_RADIUS_EQ * 1e-3);
OrbitalElements {
a: new_a,
..in_plane
}
}
#[must_use]
pub fn third_body_secular_rates(&self, mu_third: f64, dist_third: f64) -> (f64, f64, f64, f64) {
let n = (self.mu / self.a.powi(3)).sqrt();
let n3_sq = mu_third / dist_third.powi(3);
let beta = n3_sq / n;
let e = self.e;
let e2 = e * e;
let ome2 = (1.0 - e2).max(1e-12);
let sqrt_ome2 = ome2.sqrt();
let ci = self.i.cos();
let si = self.i.sin();
let ci2 = ci * ci;
let si2 = si * si;
let s2w = (2.0 * self.argp).sin();
let c2w = (2.0 * self.argp).cos();
let e_dot = (15.0 / 8.0) * beta * sqrt_ome2 * e * si2 * s2w;
let i_dot = -(15.0 / 16.0) * beta * e2 * s2w * (2.0 * si * ci) / sqrt_ome2;
let raan_dot = (3.0 / 8.0) * beta * (ci / sqrt_ome2) * (2.0 + 3.0 * e2 - 5.0 * e2 * c2w);
let argp_dot_e_term =
(3.0 / 8.0) * beta * sqrt_ome2 * ((3.0 * ci2 - 1.0) + 5.0 * si2 * c2w);
let argp_dot_i_term =
(3.0 / 8.0) * beta * (ci2 / sqrt_ome2) * (2.0 + 3.0 * e2 - 5.0 * e2 * c2w);
let argp_dot = argp_dot_e_term + argp_dot_i_term;
(raan_dot, argp_dot, i_dot, e_dot)
}
pub fn propagate_third_body(
&self,
dt: f64,
mu_third: f64,
dist_third: f64,
) -> Result<OrbitalElements, AstroError> {
let in_plane = self.propagate(dt);
let (raan_dot, argp_dot, i_dot, e_dot) =
self.third_body_secular_rates(mu_third, dist_third);
let two_pi = 2.0 * std::f64::consts::PI;
OrbitalElements::new(
in_plane.a,
(in_plane.e + e_dot * dt).clamp(0.0, 1.0 - 1e-9),
(in_plane.i + i_dot * dt).clamp(0.0, std::f64::consts::PI),
(in_plane.raan + raan_dot * dt).rem_euclid(two_pi),
(in_plane.argp + argp_dot * dt).rem_euclid(two_pi),
in_plane.nu,
in_plane.mu,
)
}
#[must_use]
pub fn srp_acceleration_vector(
&self,
cr: f64,
area_to_mass_m2_per_kg: f64,
sun_pos_km: &DVector<f64>,
) -> DVector<f64> {
let (r_eci, _v) = self.state_vector();
if in_earth_shadow(&r_eci, sun_pos_km, EARTH_RADIUS_EQ) {
return DVector::from_vec(vec![0.0, 0.0, 0.0]);
}
let sun_dist = sun_pos_km.norm();
if sun_dist <= 0.0 {
return DVector::from_vec(vec![0.0, 0.0, 0.0]);
}
let dir = sun_pos_km.clone() * (-1.0 / sun_dist);
let mag = srp_acceleration(cr, area_to_mass_m2_per_kg, sun_dist);
dir * mag
}
#[must_use]
pub fn j4_secular_rates(&self, j2: f64, j4: f64, r_eq: f64) -> (f64, f64) {
let (raan_dot_j2, argp_dot_j2) = self.j2_secular_rates(j2, r_eq);
let n = (self.mu / self.a.powi(3)).sqrt();
let p = self.a * (1.0 - self.e.powi(2));
let factor4 = n * j4 * (r_eq / p).powi(4);
let ci = self.i.cos();
let si2 = self.i.sin().powi(2);
let raan_dot_j4 = (15.0 / 32.0) * factor4 * ci * (12.0 - 21.0 * si2);
let argp_dot_j4 = -(45.0 / 128.0) * factor4 * (8.0 - 40.0 * si2 + 35.0 * si2 * si2);
(raan_dot_j2 + raan_dot_j4, argp_dot_j2 + argp_dot_j4)
}
#[must_use]
pub fn propagate_j4(&self, dt: f64, j2: f64, j4: f64, r_eq: f64) -> OrbitalElements {
let in_plane = self.propagate(dt);
let (raan_dot, argp_dot) = self.j4_secular_rates(j2, j4, r_eq);
let two_pi = 2.0 * std::f64::consts::PI;
OrbitalElements {
a: in_plane.a,
e: in_plane.e,
i: in_plane.i,
raan: (in_plane.raan + raan_dot * dt).rem_euclid(two_pi),
argp: (in_plane.argp + argp_dot * dt).rem_euclid(two_pi),
nu: in_plane.nu,
mu: in_plane.mu,
}
}
}
#[must_use]
pub fn atmospheric_density(altitude_km: f64) -> f64 {
EARTH_ATM_RHO0_KG_M3 * (-(altitude_km - EARTH_ATM_H0_KM) / EARTH_ATM_SCALE_HEIGHT_KM).exp()
}
#[must_use]
pub fn in_earth_shadow(pos: &DVector<f64>, sun_pos: &DVector<f64>, earth_radius: f64) -> bool {
let sun_dist = sun_pos.norm();
if sun_dist <= 0.0 {
return false;
}
let sun_dir = sun_pos.clone() * (1.0 / sun_dist);
let along = pos.dot(&sun_dir);
if along >= 0.0 {
return false;
}
let perp = pos.clone() - sun_dir * along;
perp.norm() < earth_radius
}
#[must_use]
pub fn srp_acceleration(cr: f64, area_to_mass_m2_per_kg: f64, sun_distance_km: f64) -> f64 {
let p_srp = SOLAR_PRESSURE_1AU * (ASTRONOMICAL_UNIT_KM / sun_distance_km).powi(2);
let accel_m_s2 = p_srp * cr * area_to_mass_m2_per_kg;
accel_m_s2 / 1000.0
}
#[must_use]
pub fn perifocal_to_eci(raan: f64, i: f64, argp: f64) -> DMatrix<f64> {
let c_o = raan.cos();
let s_o = raan.sin();
let ci = i.cos();
let si = i.sin();
let cw = argp.cos();
let sw = argp.sin();
DMatrix::from_fn(3, 3, |row, col| match (row, col) {
(0, 0) => c_o * cw - s_o * sw * ci,
(0, 1) => -c_o * sw - s_o * cw * ci,
(0, 2) => s_o * si,
(1, 0) => s_o * cw + c_o * sw * ci,
(1, 1) => -s_o * sw + c_o * cw * ci,
(1, 2) => -c_o * si,
(2, 0) => sw * si,
(2, 1) => cw * si,
(2, 2) => ci,
_ => 0.0,
})
}
#[must_use]
pub fn cross3(a: &DVector<f64>, b: &DVector<f64>) -> DVector<f64> {
DVector::from_vec(vec![
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
])
}
#[must_use]
pub fn true_to_eccentric(nu: f64, e: f64) -> f64 {
let s = (1.0 - e).sqrt() * (nu / 2.0).sin();
let c = (1.0 + e).sqrt() * (nu / 2.0).cos();
2.0 * s.atan2(c)
}
#[must_use]
pub fn eccentric_to_true(ecc: f64, e: f64) -> f64 {
let s = (1.0 + e).sqrt() * (ecc / 2.0).sin();
let c = (1.0 - e).sqrt() * (ecc / 2.0).cos();
2.0 * s.atan2(c)
}
#[must_use]
pub fn solve_kepler(m: f64, e: f64) -> f64 {
debug_assert!((0.0..1.0).contains(&e), "solve_kepler requires 0 ≤ e < 1");
let mut ecc = m + e * m.sin();
for _ in 0..60 {
let f = ecc - e * ecc.sin() - m;
let fp = 1.0 - e * ecc.cos();
let delta = f / fp;
ecc -= delta;
if delta.abs() < 1e-12 {
break;
}
}
ecc
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
fn vec3(x: f64, y: f64, z: f64) -> DVector<f64> {
DVector::from_vec(vec![x, y, z])
}
#[test]
fn circular_orbit_state_norms() {
let el = OrbitalElements::new(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0).unwrap();
for nu in [0.0, 0.3, 1.0, 2.5, 5.0] {
let e = OrbitalElements::new(1.0, 0.0, 0.0, 0.0, 0.0, nu, 1.0).unwrap();
let (r, v) = e.state_vector();
assert_abs_diff_eq!(r.norm(), 1.0, epsilon = 1e-9);
assert_abs_diff_eq!(v.norm(), 1.0, epsilon = 1e-9);
}
let _ = el;
}
#[test]
fn period_unit_circle() {
let el = OrbitalElements::new(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0).unwrap();
assert_abs_diff_eq!(el.period(), 2.0 * std::f64::consts::PI, epsilon = 1e-9);
}
#[test]
fn propagate_half_period_flips_position() {
let el = OrbitalElements::new(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0).unwrap();
let (r0, _) = el.state_vector();
let advanced = el.propagate(el.period() / 2.0);
let (r1, _) = advanced.state_vector();
assert_abs_diff_eq!(r1[0], -r0[0], epsilon = 1e-6);
assert_abs_diff_eq!(r1[1], -r0[1], epsilon = 1e-6);
assert_abs_diff_eq!(r1[2], -r0[2], epsilon = 1e-6);
}
#[test]
fn round_trip_elements() {
let el = OrbitalElements::new(2.0, 0.3, 0.4, 0.2, 0.1, 0.5, 1.0).unwrap();
let (r, v) = el.state_vector();
let recovered = OrbitalElements::from_state(&r, &v, 1.0).unwrap();
assert_abs_diff_eq!(recovered.a, el.a, epsilon = 1e-6);
assert_abs_diff_eq!(recovered.e, el.e, epsilon = 1e-6);
assert_abs_diff_eq!(recovered.i, el.i, epsilon = 1e-6);
}
#[test]
fn propagate_zero_is_identity() {
let el = OrbitalElements::new(2.0, 0.3, 0.4, 0.2, 0.1, 0.5, 1.0).unwrap();
let advanced = el.propagate(0.0);
let n0 = el.nu.rem_euclid(2.0 * std::f64::consts::PI);
let n1 = advanced.nu.rem_euclid(2.0 * std::f64::consts::PI);
assert_abs_diff_eq!(n0, n1, epsilon = 1e-9);
}
#[test]
fn invalid_elements_rejected() {
assert!(OrbitalElements::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0).is_err());
assert!(OrbitalElements::new(1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0).is_err());
assert!(OrbitalElements::new(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0).is_err());
assert!(OrbitalElements::new(1.0, 0.0, f64::NAN, 0.0, 0.0, 0.0, 1.0).is_err());
}
#[test]
fn degenerate_state_rejected() {
let pos = vec3(0.0, 0.0, 0.0);
let vel = vec3(1.0, 0.0, 0.0);
assert!(OrbitalElements::from_state(&pos, &vel, 1.0).is_err());
}
#[test]
fn solve_kepler_seed_is_accurate_at_high_eccentricity() {
for m in [0.1, 1.0, 2.5, 5.0] {
let e = 0.9;
let ecc = solve_kepler(m, e);
let residual = (ecc - e * ecc.sin() - m).abs();
assert!(residual < 1e-10, "M={m} residual={residual}");
}
}
#[test]
fn cross3_orthogonal() {
let a = vec3(1.0, 0.0, 0.0);
let b = vec3(0.0, 1.0, 0.0);
let c = cross3(&a, &b);
assert_abs_diff_eq!(c[0], 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(c[1], 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(c[2], 1.0, epsilon = 1e-12);
}
#[test]
fn j2_rates_match_formula() {
let el = OrbitalElements::new(7000.0, 0.01, 50.0_f64.to_radians(), 0.0, 0.0, 0.0, EARTH_MU)
.unwrap();
let (raan_dot, argp_dot) = el.j2_secular_rates(EARTH_J2, EARTH_RADIUS_EQ);
let n = (EARTH_MU / el.a.powi(3)).sqrt();
let p = el.a * (1.0 - el.e.powi(2));
let factor = 1.5 * n * EARTH_J2 * (EARTH_RADIUS_EQ / p).powi(2);
let ci = el.i.cos();
assert_abs_diff_eq!(raan_dot, -factor * ci, epsilon = 1e-12);
assert_abs_diff_eq!(
argp_dot,
0.5 * factor * (5.0 * ci * ci - 1.0),
epsilon = 1e-12
);
}
#[test]
fn j2_regresses_raan_for_prograde() {
let el = OrbitalElements::new(7000.0, 0.01, 50.0_f64.to_radians(), 1.0, 0.0, 0.0, EARTH_MU)
.unwrap();
let dt = 86_400.0; let advanced = el.propagate_j2(dt, EARTH_J2, EARTH_RADIUS_EQ);
let (raan_dot, _) = el.j2_secular_rates(EARTH_J2, EARTH_RADIUS_EQ);
let expected = (el.raan + raan_dot * dt).rem_euclid(2.0 * std::f64::consts::PI);
assert_abs_diff_eq!(advanced.raan, expected, epsilon = 1e-9);
assert_abs_diff_eq!(advanced.a, el.a, epsilon = 1e-9);
assert_abs_diff_eq!(advanced.e, el.e, epsilon = 1e-12);
assert_abs_diff_eq!(advanced.i, el.i, epsilon = 1e-12);
assert!(raan_dot < 0.0);
}
#[test]
fn atmospheric_density_reference_and_decay() {
assert_abs_diff_eq!(
atmospheric_density(EARTH_ATM_H0_KM),
EARTH_ATM_RHO0_KG_M3,
epsilon = 1e-20
);
let rho_low = atmospheric_density(300.0);
let rho_mid = atmospheric_density(400.0);
let rho_high = atmospheric_density(600.0);
assert!(rho_low > rho_mid);
assert!(rho_mid > rho_high);
assert!(rho_high > 0.0);
}
#[test]
fn drag_decay_shrinks_semi_major_axis() {
let el = OrbitalElements::new(
6778.0,
0.001,
51.6_f64.to_radians(),
0.0,
0.0,
0.0,
EARTH_MU,
)
.unwrap();
let cd_a_over_m = 0.02; let da_dt = el.drag_da_dt(cd_a_over_m);
assert!(da_dt < 0.0, "da_dt = {da_dt}");
let decay_per_day_km = da_dt.abs() * 86_400.0;
assert!(
decay_per_day_km > 1e-6,
"decay_per_day_km = {decay_per_day_km}"
);
assert!(
decay_per_day_km < 5.0,
"decay_per_day_km = {decay_per_day_km}"
);
let advanced = el.propagate_drag(3600.0, cd_a_over_m);
assert!(advanced.a < el.a);
}
#[test]
fn drag_rejects_denser_lower_orbit_faster() {
let hi = OrbitalElements::new(6978.0, 0.0, 0.5, 0.0, 0.0, 0.0, EARTH_MU).unwrap();
let lo = OrbitalElements::new(6678.0, 0.0, 0.5, 0.0, 0.0, 0.0, EARTH_MU).unwrap();
let bc = 0.02;
assert!(lo.drag_da_dt(bc).abs() > hi.drag_da_dt(bc).abs());
}
#[test]
fn shadow_function_eclipses_only_behind_earth() {
let sun_pos = vec3(ASTRONOMICAL_UNIT_KM, 0.0, 0.0);
let lit = vec3(7000.0, 0.0, 0.0);
assert!(!in_earth_shadow(&lit, &sun_pos, EARTH_RADIUS_EQ));
let eclipsed = vec3(-7000.0, 0.0, 0.0);
assert!(in_earth_shadow(&eclipsed, &sun_pos, EARTH_RADIUS_EQ));
let grazing = vec3(-7000.0, 20_000.0, 0.0);
assert!(!in_earth_shadow(&grazing, &sun_pos, EARTH_RADIUS_EQ));
}
#[test]
fn srp_acceleration_vector_zero_in_shadow() {
let sun_pos = vec3(ASTRONOMICAL_UNIT_KM, 0.0, 0.0);
let el = OrbitalElements::new(7000.0, 0.0, 0.0, 0.0, 0.0, std::f64::consts::PI, EARTH_MU)
.unwrap();
let a_shadow = el.srp_acceleration_vector(1.5, 0.02, &sun_pos);
assert_abs_diff_eq!(a_shadow.norm(), 0.0, epsilon = 1e-30);
let el_lit = OrbitalElements::new(7000.0, 0.0, 0.0, 0.0, 0.0, 0.0, EARTH_MU).unwrap();
let a_lit = el_lit.srp_acceleration_vector(1.5, 0.02, &sun_pos);
assert!(a_lit.norm() > 0.0);
assert!(a_lit[0] < 0.0);
}
#[test]
fn srp_magnitude_matches_cannonball_formula() {
let a = srp_acceleration(1.0, 0.02, ASTRONOMICAL_UNIT_KM);
let expected_m_s2 = SOLAR_PRESSURE_1AU * 1.0 * 0.02;
assert_abs_diff_eq!(a * 1000.0, expected_m_s2, epsilon = 1e-15);
}
#[test]
fn third_body_conserves_kozai_integral_rate() {
let el = OrbitalElements::new(
42_164.0,
0.3,
60.0_f64.to_radians(),
0.0,
30.0_f64.to_radians(),
0.0,
EARTH_MU,
)
.unwrap();
let (_, _, i_dot, e_dot) = el.third_body_secular_rates(MOON_MU, MOON_DISTANCE_KM);
let ci = el.i.cos();
let si = el.i.sin();
let theta_dot = -2.0 * el.e * e_dot * ci * ci - (1.0 - el.e * el.e) * 2.0 * ci * si * i_dot;
assert_abs_diff_eq!(theta_dot, 0.0, epsilon = 1e-18);
}
#[test]
fn third_body_rates_are_finite_and_propagation_preserves_a() {
let el = OrbitalElements::new(
42_164.0,
0.1,
20.0_f64.to_radians(),
0.5,
0.7,
0.0,
EARTH_MU,
)
.unwrap();
let (raan_dot, argp_dot, i_dot, e_dot) =
el.third_body_secular_rates(SUN_MU, ASTRONOMICAL_UNIT_KM);
for v in [raan_dot, argp_dot, i_dot, e_dot] {
assert!(v.is_finite());
}
let advanced = el
.propagate_third_body(3600.0, SUN_MU, ASTRONOMICAL_UNIT_KM)
.unwrap();
assert_abs_diff_eq!(advanced.a, el.a, epsilon = 1e-9);
}
#[test]
fn j4_rates_are_small_relative_correction_to_j2() {
let el = OrbitalElements::new(7000.0, 0.01, 50.0_f64.to_radians(), 0.0, 0.0, 0.0, EARTH_MU)
.unwrap();
let (raan_dot_j2, argp_dot_j2) = el.j2_secular_rates(EARTH_J2, EARTH_RADIUS_EQ);
let (raan_dot_j24, argp_dot_j24) = el.j4_secular_rates(EARTH_J2, EARTH_J4, EARTH_RADIUS_EQ);
assert!(raan_dot_j24.is_finite());
assert!(argp_dot_j24.is_finite());
let raan_j4_only = raan_dot_j24 - raan_dot_j2;
let argp_j4_only = argp_dot_j24 - argp_dot_j2;
assert!(raan_j4_only.abs() > 0.0);
assert!(raan_j4_only.abs() < 0.05 * raan_dot_j2.abs());
assert!(
argp_j4_only.abs() < 0.05 * argp_dot_j2.abs().max(1e-30) || argp_dot_j2.abs() < 1e-30
);
}
#[test]
fn propagate_j4_matches_secular_rates() {
let el = OrbitalElements::new(7000.0, 0.01, 50.0_f64.to_radians(), 1.0, 0.0, 0.0, EARTH_MU)
.unwrap();
let dt = 86_400.0;
let advanced = el.propagate_j4(dt, EARTH_J2, EARTH_J4, EARTH_RADIUS_EQ);
let (raan_dot, _) = el.j4_secular_rates(EARTH_J2, EARTH_J4, EARTH_RADIUS_EQ);
let expected = (el.raan + raan_dot * dt).rem_euclid(2.0 * std::f64::consts::PI);
assert_abs_diff_eq!(advanced.raan, expected, epsilon = 1e-9);
assert_abs_diff_eq!(advanced.a, el.a, epsilon = 1e-9);
}
}