use nalgebra as na;
pub type Vector3Static = na::SVector<f64, 3>;
#[inline]
pub fn j2_perturbation_static(r: &Vector3Static, mu: f64, j2: f64, r_eq: f64) -> Vector3Static {
let x = r[0];
let y = r[1];
let z = r[2];
let r_mag = r.norm();
let r2 = r_mag * r_mag;
let r5 = r2 * r2 * r_mag; let z2 = z * z;
let k = 1.5 * j2 * mu * r_eq * r_eq / r5;
let z2_r2 = z2 / r2;
Vector3Static::new(
k * x * (5.0 * z2_r2 - 1.0),
k * y * (5.0 * z2_r2 - 1.0),
k * z * (5.0 * z2_r2 - 3.0),
)
}
#[inline]
pub fn j3_perturbation_static(r: &Vector3Static, mu: f64, j3: f64, r_eq: f64) -> Vector3Static {
let x = r[0];
let y = r[1];
let z = r[2];
let r_mag = r.norm();
let r2 = r_mag * r_mag;
let r5 = r2 * r2 * r_mag;
let r7 = r5 * r2; let z2 = z * z;
let z4 = z2 * z2;
let k = 2.5 * j3 * mu * r_eq.powi(3) / r7;
let z2_r2 = z2 / r2;
let z4_r4 = z4 / (r2 * r2);
Vector3Static::new(
k * x * z * (7.0 * z2_r2 - 3.0),
k * y * z * (7.0 * z2_r2 - 3.0),
k * (35.0 * z4_r4 - 30.0 * z2_r2 + 3.0),
)
}
#[inline]
pub fn j4_perturbation_static(r: &Vector3Static, mu: f64, j4: f64, r_eq: f64) -> Vector3Static {
let x = r[0];
let y = r[1];
let z = r[2];
let r_mag = r.norm();
let r2 = r_mag * r_mag;
let r7 = r2 * r2 * r2 * r_mag; let z2 = z * z;
let z4 = z2 * z2;
let k = 1.875 * j4 * mu * r_eq.powi(4) / r7;
let z2_r2 = z2 / r2;
let z4_r4 = z4 / (r2 * r2);
Vector3Static::new(
k * x * (3.0 - 42.0 * z2_r2 + 63.0 * z4_r4),
k * y * (3.0 - 42.0 * z2_r2 + 63.0 * z4_r4),
k * z * (15.0 - 70.0 * z2_r2 + 63.0 * z4_r4),
)
}
#[inline]
pub fn j2_j3_j4_perturbation_static(
r: &Vector3Static,
mu: f64,
j2: f64,
j3: f64,
j4: f64,
r_eq: f64,
) -> Vector3Static {
let x = r[0];
let y = r[1];
let z = r[2];
let r_mag = r.norm();
let r2 = r_mag * r_mag;
let r5 = r2 * r2 * r_mag;
let r7 = r5 * r2;
let z2 = z * z;
let z4 = z2 * z2;
let z2_r2 = z2 / r2;
let z4_r4 = z4 / (r2 * r2);
let r_eq2 = r_eq * r_eq;
let r_eq3 = r_eq2 * r_eq;
let r_eq4 = r_eq2 * r_eq2;
let k2 = 1.5 * j2 * mu * r_eq2 / r5;
let a2_x = k2 * x * (5.0 * z2_r2 - 1.0);
let a2_y = k2 * y * (5.0 * z2_r2 - 1.0);
let a2_z = k2 * z * (5.0 * z2_r2 - 3.0);
let k3 = 2.5 * j3 * mu * r_eq3 / r7;
let a3_x = k3 * x * z * (7.0 * z2_r2 - 3.0);
let a3_y = k3 * y * z * (7.0 * z2_r2 - 3.0);
let a3_z = k3 * (35.0 * z4_r4 - 30.0 * z2_r2 + 3.0);
let k4 = 1.875 * j4 * mu * r_eq4 / r7;
let a4_x = k4 * x * (3.0 - 42.0 * z2_r2 + 63.0 * z4_r4);
let a4_y = k4 * y * (3.0 - 42.0 * z2_r2 + 63.0 * z4_r4);
let a4_z = k4 * z * (15.0 - 70.0 * z2_r2 + 63.0 * z4_r4);
Vector3Static::new(a2_x + a3_x + a4_x, a2_y + a3_y + a4_y, a2_z + a3_z + a4_z)
}
pub fn j2_dynamics(mu: f64, j2: f64, r_eq: f64) -> impl Fn(f64, &na::SVector<f64, 6>) -> na::SVector<f64, 6> {
move |_t: f64, state: &na::SVector<f64, 6>| {
let x = state[0];
let y = state[1];
let z = state[2];
let vx = state[3];
let vy = state[4];
let vz = state[5];
let r_vec = Vector3Static::new(x, y, z);
let r = r_vec.norm();
let a_twobody = -mu / (r * r * r) * r_vec;
let a_j2 = j2_perturbation_static(&r_vec, mu, j2, r_eq);
let a_total = a_twobody + a_j2;
na::SVector::<f64, 6>::new(
vx, vy, vz, a_total[0], a_total[1], a_total[2],
)
}
}
pub fn j2_j3_j4_dynamics(
mu: f64,
j2: f64,
j3: f64,
j4: f64,
r_eq: f64,
) -> impl Fn(f64, &na::SVector<f64, 6>) -> na::SVector<f64, 6> {
move |_t: f64, state: &na::SVector<f64, 6>| {
let x = state[0];
let y = state[1];
let z = state[2];
let vx = state[3];
let vy = state[4];
let vz = state[5];
let r_vec = Vector3Static::new(x, y, z);
let r = r_vec.norm();
let a_twobody = -mu / (r * r * r) * r_vec;
let a_pert = j2_j3_j4_perturbation_static(&r_vec, mu, j2, j3, j4, r_eq);
let a_total = a_twobody + a_pert;
na::SVector::<f64, 6>::new(
vx, vy, vz, a_total[0], a_total[1], a_total[2],
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
const MU_EARTH: f64 = 3.986004418e14; const R_EARTH: f64 = 6378137.0; const J2_EARTH: f64 = 1.08263e-3;
const J3_EARTH: f64 = -2.532e-6;
const J4_EARTH: f64 = -1.619e-6;
#[test]
fn test_j2_at_equator() {
let r = Vector3Static::new(7000e3, 0.0, 0.0);
let accel = j2_perturbation_static(&r, MU_EARTH, J2_EARTH, R_EARTH);
assert!(accel[0] < 0.0);
assert_relative_eq!(accel[1], 0.0, epsilon = 1e-20);
assert_relative_eq!(accel[2], 0.0, epsilon = 1e-20);
}
#[test]
fn test_j2_at_pole() {
let r = Vector3Static::new(0.0, 0.0, 7000e3);
let accel = j2_perturbation_static(&r, MU_EARTH, J2_EARTH, R_EARTH);
assert_relative_eq!(accel[0], 0.0, epsilon = 1e-20);
assert_relative_eq!(accel[1], 0.0, epsilon = 1e-20);
assert!(accel[2].abs() > 0.0);
}
#[test]
fn test_j2_magnitude() {
let r = Vector3Static::new(7000e3, 0.0, 1000e3);
let r_mag = r.norm();
let a_gravity = MU_EARTH / (r_mag * r_mag);
let a_j2 = j2_perturbation_static(&r, MU_EARTH, J2_EARTH, R_EARTH);
let a_j2_mag = a_j2.norm();
let ratio = a_j2_mag / a_gravity;
assert!(ratio < 0.1, "J2 ratio too large: {}", ratio); assert!(ratio > 1e-6, "J2 ratio too small: {}", ratio); }
#[test]
fn test_j3_antisymmetry() {
let r_north = Vector3Static::new(7000e3, 0.0, 1000e3);
let r_south = Vector3Static::new(7000e3, 0.0, -1000e3);
let a_north = j3_perturbation_static(&r_north, MU_EARTH, J3_EARTH, R_EARTH);
let a_south = j3_perturbation_static(&r_south, MU_EARTH, J3_EARTH, R_EARTH);
assert_relative_eq!(a_north[0], -a_south[0], epsilon = 1e-10);
assert_relative_eq!(a_north[1], -a_south[1], epsilon = 1e-10);
}
#[test]
fn test_j4_equatorial_symmetry() {
let r_north = Vector3Static::new(7000e3, 0.0, 1000e3);
let r_south = Vector3Static::new(7000e3, 0.0, -1000e3);
let a_north = j4_perturbation_static(&r_north, MU_EARTH, J4_EARTH, R_EARTH);
let a_south = j4_perturbation_static(&r_south, MU_EARTH, J4_EARTH, R_EARTH);
assert_relative_eq!(a_north[0], a_south[0], epsilon = 1e-10);
assert_relative_eq!(a_north[1], a_south[1], epsilon = 1e-10);
assert_relative_eq!(a_north[2], -a_south[2], epsilon = 1e-10);
}
#[test]
fn test_combined_equals_sum() {
let r = Vector3Static::new(7000e3, 500e3, 1000e3);
let a_j2 = j2_perturbation_static(&r, MU_EARTH, J2_EARTH, R_EARTH);
let a_j3 = j3_perturbation_static(&r, MU_EARTH, J3_EARTH, R_EARTH);
let a_j4 = j4_perturbation_static(&r, MU_EARTH, J4_EARTH, R_EARTH);
let a_sum = a_j2 + a_j3 + a_j4;
let a_combined =
j2_j3_j4_perturbation_static(&r, MU_EARTH, J2_EARTH, J3_EARTH, J4_EARTH, R_EARTH);
assert_relative_eq!(a_combined[0], a_sum[0], epsilon = 1e-15);
assert_relative_eq!(a_combined[1], a_sum[1], epsilon = 1e-15);
assert_relative_eq!(a_combined[2], a_sum[2], epsilon = 1e-15);
}
#[test]
fn test_two_body_dynamics_simple() {
use crate::core::integrators_static::{propagate_rk4_final_only, StateVector6};
let dynamics = |_t: f64, state: &StateVector6| {
let x = state[0];
let y = state[1];
let z = state[2];
let vx = state[3];
let vy = state[4];
let vz = state[5];
let r_vec = Vector3Static::new(x, y, z);
let r = r_vec.norm();
let a = -MU_EARTH / (r * r * r) * r_vec;
StateVector6::new(vx, vy, vz, a[0], a[1], a[2])
};
let r0 = 7000e3;
let v0 = (MU_EARTH / r0).sqrt();
let state0 = StateVector6::new(r0, 0.0, 0.0, 0.0, v0, 0.0);
let period = 2.0 * std::f64::consts::PI * (r0.powi(3) / MU_EARTH).sqrt();
let state_final = propagate_rk4_final_only(dynamics, 0.0, &state0, period, 1000);
let r_final = state_final.fixed_rows::<3>(0).norm();
assert!(
r_final > 6000e3 && r_final < 8000e3,
"Final radius out of bounds: {} m",
r_final
);
let energy = |s: &StateVector6| {
let r = s.fixed_rows::<3>(0).norm();
let v = s.fixed_rows::<3>(3).norm();
0.5 * v * v - MU_EARTH / r
};
let e0 = energy(&state0);
let ef = energy(&state_final);
let energy_error = ((ef - e0) / e0).abs();
assert!(
energy_error < 1e-6,
"Energy not conserved: error = {}",
energy_error
);
}
#[test]
fn test_j2_dynamics_integration() {
use crate::core::integrators_static::{propagate_rk4_final_only, StateVector6};
let dynamics = |_t: f64, state: &StateVector6| {
let x = state[0];
let y = state[1];
let z = state[2];
let vx = state[3];
let vy = state[4];
let vz = state[5];
let r_vec = Vector3Static::new(x, y, z);
let r = r_vec.norm();
let a_twobody = -MU_EARTH / (r * r * r) * r_vec;
let a_j2 = j2_perturbation_static(&r_vec, MU_EARTH, J2_EARTH, R_EARTH);
let a_total = a_twobody + a_j2;
StateVector6::new(vx, vy, vz, a_total[0], a_total[1], a_total[2])
};
let r0 = 7000e3;
let v0 = (MU_EARTH / r0).sqrt();
let state0 = StateVector6::new(r0, 0.0, 0.0, 0.0, v0, 0.0);
let period = 2.0 * std::f64::consts::PI * (r0.powi(3) / MU_EARTH).sqrt();
let state_final = propagate_rk4_final_only(dynamics, 0.0, &state0, period, 1000);
let r_final = state_final.fixed_rows::<3>(0).norm();
assert!(
r_final > 5000e3 && r_final < 10000e3,
"Final radius out of bounds: {} m", r_final
);
let energy = |s: &StateVector6| {
let r = s.fixed_rows::<3>(0).norm();
let v = s.fixed_rows::<3>(3).norm();
0.5 * v * v - MU_EARTH / r
};
let e0 = energy(&state0);
let ef = energy(&state_final);
let energy_error = ((ef - e0) / e0).abs();
assert!(
energy_error < 1e-4,
"Energy not conserved: error = {}",
energy_error
);
}
#[test]
fn test_zero_allocations() {
let r = Vector3Static::new(7000e3, 0.0, 1000e3);
let _a1 = j2_perturbation_static(&r, MU_EARTH, J2_EARTH, R_EARTH);
let _a2 = j3_perturbation_static(&r, MU_EARTH, J3_EARTH, R_EARTH);
let _a3 = j4_perturbation_static(&r, MU_EARTH, J4_EARTH, R_EARTH);
let _a4 = j2_j3_j4_perturbation_static(&r, MU_EARTH, J2_EARTH, J3_EARTH, J4_EARTH, R_EARTH);
}
#[test]
fn test_j3_at_equator() {
let r = Vector3Static::new(7000e3, 0.0, 0.0);
let accel = j3_perturbation_static(&r, MU_EARTH, J3_EARTH, R_EARTH);
assert_relative_eq!(accel[0], 0.0, epsilon = 1e-15);
assert_relative_eq!(accel[1], 0.0, epsilon = 1e-15);
assert!(accel[2].abs() < 1e-15);
}
#[test]
fn test_j3_at_pole() {
let r = Vector3Static::new(0.0, 0.0, 7000e3);
let accel = j3_perturbation_static(&r, MU_EARTH, J3_EARTH, R_EARTH);
assert!(accel[2].abs() > 0.0);
}
#[test]
fn test_j4_at_equator() {
let r = Vector3Static::new(7000e3, 0.0, 0.0);
let accel = j4_perturbation_static(&r, MU_EARTH, J4_EARTH, R_EARTH);
assert!(accel[0] < 0.0);
assert_relative_eq!(accel[1], 0.0, epsilon = 1e-20);
assert_relative_eq!(accel[2], 0.0, epsilon = 1e-20);
}
#[test]
fn test_j4_at_pole() {
let r = Vector3Static::new(0.0, 0.0, 7000e3);
let accel = j4_perturbation_static(&r, MU_EARTH, J4_EARTH, R_EARTH);
assert_relative_eq!(accel[0], 0.0, epsilon = 1e-20);
assert_relative_eq!(accel[1], 0.0, epsilon = 1e-20);
assert!(accel[2].abs() > 0.0);
}
#[test]
fn test_j2_high_altitude() {
let r_geo = 42164e3; let r = Vector3Static::new(r_geo, 0.0, 1000e3);
let accel = j2_perturbation_static(&r, MU_EARTH, J2_EARTH, R_EARTH);
let accel_mag = accel.norm();
assert!(accel_mag > 0.0);
assert!(accel_mag < 1e-5); }
#[test]
fn test_j2_low_altitude() {
let r_low = R_EARTH + 200e3; let r = Vector3Static::new(r_low, 0.0, 500e3);
let accel = j2_perturbation_static(&r, MU_EARTH, J2_EARTH, R_EARTH);
let accel_mag = accel.norm();
assert!(accel_mag > 1e-5);
}
#[test]
fn test_j2_inclined_orbit() {
let r_mag = 7000e3;
let inc = std::f64::consts::PI / 4.0;
let x = r_mag * inc.cos();
let y = r_mag * 0.3;
let z = r_mag * inc.sin();
let r = Vector3Static::new(x, y, z);
let accel = j2_perturbation_static(&r, MU_EARTH, J2_EARTH, R_EARTH);
assert!(accel[0].abs() > 0.0);
assert!(accel[2].abs() > 0.0);
}
#[test]
fn test_combined_zero_perturbations() {
let r = Vector3Static::new(7000e3, 0.0, 1000e3);
let accel = j2_j3_j4_perturbation_static(&r, MU_EARTH, 0.0, 0.0, 0.0, R_EARTH);
assert_relative_eq!(accel[0], 0.0, epsilon = 1e-20);
assert_relative_eq!(accel[1], 0.0, epsilon = 1e-20);
assert_relative_eq!(accel[2], 0.0, epsilon = 1e-20);
}
#[test]
fn test_j2_dynamics_polar_orbit() {
use crate::core::integrators_static::{propagate_rk4_final_only, StateVector6};
let dynamics = j2_dynamics(MU_EARTH, J2_EARTH, R_EARTH);
let r0 = 7000e3;
let v0 = (MU_EARTH / r0).sqrt();
let state0 = StateVector6::new(r0, 0.0, 0.0, 0.0, 0.0, v0);
let t_final = 600.0; let state_final = propagate_rk4_final_only(dynamics, 0.0, &state0, t_final, 100);
let r_final = state_final.fixed_rows::<3>(0).norm();
assert!(r_final > 6000e3 && r_final < 8000e3);
}
#[test]
fn test_j2_j3_j4_dynamics_equatorial() {
use crate::core::integrators_static::{propagate_rk4_final_only, StateVector6};
let dynamics = j2_j3_j4_dynamics(MU_EARTH, J2_EARTH, J3_EARTH, J4_EARTH, R_EARTH);
let r0 = 7000e3;
let v0 = (MU_EARTH / r0).sqrt();
let state0 = StateVector6::new(r0, 0.0, 0.0, 0.0, v0, 0.0);
let t_final = 300.0; let state_final = propagate_rk4_final_only(dynamics, 0.0, &state0, t_final, 50);
let r_final = state_final.fixed_rows::<3>(0).norm();
assert!(r_final > 6000e3 && r_final < 8000e3);
}
#[test]
fn test_j2_j3_j4_dynamics_inclined() {
use crate::core::integrators_static::{propagate_rk4_final_only, StateVector6};
let dynamics = j2_j3_j4_dynamics(MU_EARTH, J2_EARTH, J3_EARTH, J4_EARTH, R_EARTH);
let r0 = 7000e3;
let v0 = (MU_EARTH / r0).sqrt();
let inc = 60.0_f64.to_radians();
let state0 = StateVector6::new(r0, 0.0, 0.0, 0.0, v0 * inc.cos(), v0 * inc.sin());
let t_final = 450.0;
let state_final = propagate_rk4_final_only(dynamics, 0.0, &state0, t_final, 75);
let r_final = state_final.fixed_rows::<3>(0).norm();
assert!(r_final > 6000e3 && r_final < 8000e3);
}
#[test]
fn test_j3_magnitude_vs_j2() {
let r = Vector3Static::new(7000e3, 0.0, 1000e3);
let a_j2 = j2_perturbation_static(&r, MU_EARTH, J2_EARTH, R_EARTH);
let a_j3 = j3_perturbation_static(&r, MU_EARTH, J3_EARTH, R_EARTH);
let mag_j2 = a_j2.norm();
let mag_j3 = a_j3.norm();
assert!(mag_j3 < mag_j2 / 100.0);
}
#[test]
fn test_j4_magnitude_vs_j2() {
let r = Vector3Static::new(7000e3, 0.0, 1000e3);
let a_j2 = j2_perturbation_static(&r, MU_EARTH, J2_EARTH, R_EARTH);
let a_j4 = j4_perturbation_static(&r, MU_EARTH, J4_EARTH, R_EARTH);
let mag_j2 = a_j2.norm();
let mag_j4 = a_j4.norm();
assert!(mag_j4 < mag_j2 / 50.0);
}
#[test]
fn test_j2_different_radii() {
let radii = vec![6600e3, 7000e3, 8000e3, 12000e3, 20000e3, 42164e3];
for &r_mag in &radii {
let r = Vector3Static::new(r_mag, 0.0, 500e3);
let accel = j2_perturbation_static(&r, MU_EARTH, J2_EARTH, R_EARTH);
assert!(accel.norm() > 0.0, "Zero acceleration at radius {}", r_mag);
}
}
#[test]
fn test_combined_with_only_j2() {
let r = Vector3Static::new(7000e3, 500e3, 1000e3);
let a_j2_only = j2_perturbation_static(&r, MU_EARTH, J2_EARTH, R_EARTH);
let a_combined = j2_j3_j4_perturbation_static(&r, MU_EARTH, J2_EARTH, 0.0, 0.0, R_EARTH);
assert_relative_eq!(a_combined[0], a_j2_only[0], epsilon = 1e-15);
assert_relative_eq!(a_combined[1], a_j2_only[1], epsilon = 1e-15);
assert_relative_eq!(a_combined[2], a_j2_only[2], epsilon = 1e-15);
}
}