use crate::core::error::{PoliastroError, PoliastroResult};
use crate::core::linalg::Vector3;
use std::f64::consts::PI;
pub fn j2_perturbation(r: &Vector3, mu: f64, j2: f64, R: f64) -> Vector3 {
let x = r.x;
let y = r.y;
let z = r.z;
let r_mag = r.norm();
let r2 = r_mag * r_mag;
let r4 = r2 * r2;
let z2 = z * z;
let k = 1.5 * j2 * mu * R * R / r4;
let z2_r2 = z2 / r2;
let factor_xy = 5.0 * z2_r2 - 1.0;
let factor_z = 5.0 * z2_r2 - 3.0;
Vector3::new(
k * (x / r_mag) * factor_xy,
k * (y / r_mag) * factor_xy,
k * (z / r_mag) * factor_z,
)
}
pub fn exponential_density(altitude: f64, rho0: f64, H0: f64) -> f64 {
rho0 * (-altitude / H0).exp()
}
pub fn drag_acceleration(r: &Vector3, v: &Vector3, R: f64, rho0: f64, H0: f64, B: f64) -> Vector3 {
let r_mag = r.norm();
let altitude = r_mag - R;
let rho = exponential_density(altitude, rho0, H0);
let v_mag = v.norm();
if v_mag < 1e-10 {
return Vector3::zeros();
}
let v_unit = v / v_mag;
let drag_mag = -0.5 * rho * v_mag * v_mag / B;
drag_mag * v_unit
}
pub type PerturbationFn = fn(f64, &Vector3, &Vector3) -> Vector3;
pub fn j2_acceleration_func(
mu: f64,
j2: f64,
R: f64,
) -> impl Fn(f64, &nalgebra::DVector<f64>) -> nalgebra::DVector<f64> {
move |_t: f64, state: &nalgebra::DVector<f64>| -> nalgebra::DVector<f64> {
let r = Vector3::new(state[0], state[1], state[2]);
let v = Vector3::new(state[3], state[4], state[5]);
let r_mag = r.norm();
let a_twobody = -mu / (r_mag * r_mag * r_mag) * r;
let a_j2 = j2_perturbation(&r, mu, j2, R);
let a_total = a_twobody + a_j2;
nalgebra::DVector::from_vec(vec![v.x, v.y, v.z, a_total.x, a_total.y, a_total.z])
}
}
pub fn propagate_j2_rk4(
r0: &Vector3,
v0: &Vector3,
dt: f64,
mu: f64,
j2: f64,
R: f64,
n_steps: Option<usize>,
) -> PoliastroResult<(Vector3, Vector3)> {
use crate::core::numerical::rk4_step;
let n_steps = n_steps.unwrap_or(10);
let h = dt / n_steps as f64;
let f = j2_acceleration_func(mu, j2, R);
let mut state = nalgebra::DVector::from_vec(vec![r0.x, r0.y, r0.z, v0.x, v0.y, v0.z]);
let mut t = 0.0;
for _ in 0..n_steps {
state = rk4_step(&f, t, &state, h);
t += h;
}
let r_final = Vector3::new(state[0], state[1], state[2]);
let v_final = Vector3::new(state[3], state[4], state[5]);
Ok((r_final, v_final))
}
pub fn propagate_j2_dopri5(
r0: &Vector3,
v0: &Vector3,
dt: f64,
mu: f64,
j2: f64,
R: f64,
tol: Option<f64>,
) -> PoliastroResult<(Vector3, Vector3)> {
use crate::core::numerical::dopri5_integrate;
let tol = tol.unwrap_or(1e-8);
let f = j2_acceleration_func(mu, j2, R);
let state0 = nalgebra::DVector::from_vec(vec![r0.x, r0.y, r0.z, v0.x, v0.y, v0.z]);
let state_final = dopri5_integrate(f, 0.0, &state0, dt, dt.abs() / 10.0, tol, None)?;
let r_final = Vector3::new(state_final[0], state_final[1], state_final[2]);
let v_final = Vector3::new(state_final[3], state_final[4], state_final[5]);
Ok((r_final, v_final))
}
pub fn propagate_j2_dop853(
r0: &Vector3,
v0: &Vector3,
dt: f64,
mu: f64,
j2: f64,
R: f64,
tol: Option<f64>,
) -> PoliastroResult<(Vector3, Vector3)> {
use crate::core::numerical::dop853_integrate;
let tol = tol.unwrap_or(1e-10);
let f = j2_acceleration_func(mu, j2, R);
let state0 = nalgebra::DVector::from_vec(vec![r0.x, r0.y, r0.z, v0.x, v0.y, v0.z]);
let state_final = dop853_integrate(f, 0.0, &state0, dt, dt.abs() / 10.0, tol, None)?;
let r_final = Vector3::new(state_final[0], state_final[1], state_final[2]);
let v_final = Vector3::new(state_final[3], state_final[4], state_final[5]);
Ok((r_final, v_final))
}
pub fn drag_acceleration_func(
mu: f64,
R: f64,
rho0: f64,
H0: f64,
B: f64,
) -> impl Fn(f64, &nalgebra::DVector<f64>) -> nalgebra::DVector<f64> {
move |_t: f64, state: &nalgebra::DVector<f64>| -> nalgebra::DVector<f64> {
let r = Vector3::new(state[0], state[1], state[2]);
let v = Vector3::new(state[3], state[4], state[5]);
let r_mag = r.norm();
let a_twobody = -mu / (r_mag * r_mag * r_mag) * r;
let a_drag = drag_acceleration(&r, &v, R, rho0, H0, B);
let a_total = a_twobody + a_drag;
nalgebra::DVector::from_vec(vec![v.x, v.y, v.z, a_total.x, a_total.y, a_total.z])
}
}
pub fn propagate_drag_rk4(
r0: &Vector3,
v0: &Vector3,
dt: f64,
mu: f64,
R: f64,
rho0: f64,
H0: f64,
B: f64,
n_steps: Option<usize>,
) -> PoliastroResult<(Vector3, Vector3)> {
use crate::core::numerical::rk4_step;
let n_steps = n_steps.unwrap_or(10);
let h = dt / n_steps as f64;
let f = drag_acceleration_func(mu, R, rho0, H0, B);
let mut state = nalgebra::DVector::from_vec(vec![r0.x, r0.y, r0.z, v0.x, v0.y, v0.z]);
let mut t = 0.0;
for _ in 0..n_steps {
state = rk4_step(&f, t, &state, h);
t += h;
}
let r_final = Vector3::new(state[0], state[1], state[2]);
let v_final = Vector3::new(state[3], state[4], state[5]);
Ok((r_final, v_final))
}
pub fn propagate_drag_dopri5(
r0: &Vector3,
v0: &Vector3,
dt: f64,
mu: f64,
R: f64,
rho0: f64,
H0: f64,
B: f64,
tol: Option<f64>,
) -> PoliastroResult<(Vector3, Vector3)> {
use crate::core::numerical::dopri5_integrate;
let tol = tol.unwrap_or(1e-8);
let f = drag_acceleration_func(mu, R, rho0, H0, B);
let state0 = nalgebra::DVector::from_vec(vec![r0.x, r0.y, r0.z, v0.x, v0.y, v0.z]);
let state_final = dopri5_integrate(f, 0.0, &state0, dt, dt.abs() / 10.0, tol, None)?;
let r_final = Vector3::new(state_final[0], state_final[1], state_final[2]);
let v_final = Vector3::new(state_final[3], state_final[4], state_final[5]);
Ok((r_final, v_final))
}
pub fn j2_drag_acceleration_func(
mu: f64,
j2: f64,
R: f64,
rho0: f64,
H0: f64,
B: f64,
) -> impl Fn(f64, &nalgebra::DVector<f64>) -> nalgebra::DVector<f64> {
move |_t: f64, state: &nalgebra::DVector<f64>| -> nalgebra::DVector<f64> {
let r = Vector3::new(state[0], state[1], state[2]);
let v = Vector3::new(state[3], state[4], state[5]);
let r_mag = r.norm();
let a_twobody = -mu / (r_mag * r_mag * r_mag) * r;
let a_j2 = j2_perturbation(&r, mu, j2, R);
let a_drag = drag_acceleration(&r, &v, R, rho0, H0, B);
let a_total = a_twobody + a_j2 + a_drag;
nalgebra::DVector::from_vec(vec![v.x, v.y, v.z, a_total.x, a_total.y, a_total.z])
}
}
pub fn propagate_j2_drag_rk4(
r0: &Vector3,
v0: &Vector3,
dt: f64,
mu: f64,
j2: f64,
R: f64,
rho0: f64,
H0: f64,
B: f64,
n_steps: Option<usize>,
) -> PoliastroResult<(Vector3, Vector3)> {
use crate::core::numerical::rk4_step;
let n_steps = n_steps.unwrap_or(10);
let h = dt / n_steps as f64;
let f = j2_drag_acceleration_func(mu, j2, R, rho0, H0, B);
let mut state = nalgebra::DVector::from_vec(vec![r0.x, r0.y, r0.z, v0.x, v0.y, v0.z]);
let mut t = 0.0;
for _ in 0..n_steps {
state = rk4_step(&f, t, &state, h);
t += h;
}
let r_final = Vector3::new(state[0], state[1], state[2]);
let v_final = Vector3::new(state[3], state[4], state[5]);
Ok((r_final, v_final))
}
pub fn propagate_j2_drag_dopri5(
r0: &Vector3,
v0: &Vector3,
dt: f64,
mu: f64,
j2: f64,
R: f64,
rho0: f64,
H0: f64,
B: f64,
tol: Option<f64>,
) -> PoliastroResult<(Vector3, Vector3)> {
use crate::core::numerical::dopri5_integrate;
let tol = tol.unwrap_or(1e-8);
let f = j2_drag_acceleration_func(mu, j2, R, rho0, H0, B);
let state0 = nalgebra::DVector::from_vec(vec![r0.x, r0.y, r0.z, v0.x, v0.y, v0.z]);
let state_final = dopri5_integrate(f, 0.0, &state0, dt, dt.abs() / 10.0, tol, None)?;
let r_final = Vector3::new(state_final[0], state_final[1], state_final[2]);
let v_final = Vector3::new(state_final[3], state_final[4], state_final[5]);
Ok((r_final, v_final))
}
pub fn sun_position_simple(t: f64) -> Vector3 {
use crate::core::constants::{AU, DAY_TO_SEC};
const YEAR_SEC: f64 = 365.25636 * DAY_TO_SEC;
let n = 2.0 * PI / YEAR_SEC;
let lambda = n * t;
Vector3::new(AU * lambda.cos(), AU * lambda.sin(), 0.0)
}
pub fn moon_position_simple(t: f64) -> Vector3 {
use crate::core::constants::DAY_TO_SEC;
const A_MOON: f64 = 384_400_000.0;
const MONTH_SEC: f64 = 27.321661 * DAY_TO_SEC;
let n = 2.0 * PI / MONTH_SEC;
let lambda = n * t;
Vector3::new(A_MOON * lambda.cos(), A_MOON * lambda.sin(), 0.0)
}
pub fn third_body_perturbation(r: &Vector3, r_third: &Vector3, mu_third: f64) -> Vector3 {
let r_rel = r_third - r;
let r_rel_mag = r_rel.norm();
let r_third_mag = r_third.norm();
if r_rel_mag < 1e-10 || r_third_mag < 1e-10 {
return Vector3::zeros();
}
let direct = mu_third / (r_rel_mag * r_rel_mag * r_rel_mag) * r_rel;
let indirect = -mu_third / (r_third_mag * r_third_mag * r_third_mag) * r_third;
direct + indirect
}
pub fn sun_moon_perturbation(r: &Vector3, t: f64) -> Vector3 {
use crate::core::constants::{GM_MOON, GM_SUN};
let r_sun = sun_position_simple(t);
let r_moon = moon_position_simple(t);
let a_sun = third_body_perturbation(r, &r_sun, GM_SUN);
let a_moon = third_body_perturbation(r, &r_moon, GM_MOON);
a_sun + a_moon
}
pub fn third_body_acceleration_func<F>(
mu: f64,
mu_third: f64,
ephemeris_func: F,
) -> impl Fn(f64, &nalgebra::DVector<f64>) -> nalgebra::DVector<f64>
where
F: Fn(f64) -> Vector3,
{
move |t: f64, state: &nalgebra::DVector<f64>| -> nalgebra::DVector<f64> {
let r = Vector3::new(state[0], state[1], state[2]);
let v = Vector3::new(state[3], state[4], state[5]);
let r_mag = r.norm();
let a_twobody = -mu / (r_mag * r_mag * r_mag) * r;
let r_third = ephemeris_func(t);
let a_thirdbody = third_body_perturbation(&r, &r_third, mu_third);
let a_total = a_twobody + a_thirdbody;
nalgebra::DVector::from_vec(vec![v.x, v.y, v.z, a_total.x, a_total.y, a_total.z])
}
}
pub fn sun_moon_acceleration_func(
mu: f64,
) -> impl Fn(f64, &nalgebra::DVector<f64>) -> nalgebra::DVector<f64> {
move |t: f64, state: &nalgebra::DVector<f64>| -> nalgebra::DVector<f64> {
let r = Vector3::new(state[0], state[1], state[2]);
let v = Vector3::new(state[3], state[4], state[5]);
let r_mag = r.norm();
let a_twobody = -mu / (r_mag * r_mag * r_mag) * r;
let a_thirdbody = sun_moon_perturbation(&r, t);
let a_total = a_twobody + a_thirdbody;
nalgebra::DVector::from_vec(vec![v.x, v.y, v.z, a_total.x, a_total.y, a_total.z])
}
}
pub fn propagate_thirdbody_rk4(
r0: &Vector3,
v0: &Vector3,
dt: f64,
mu: f64,
t0: f64,
include_sun: bool,
include_moon: bool,
n_steps: Option<usize>,
) -> PoliastroResult<(Vector3, Vector3)> {
use crate::core::constants::{GM_MOON, GM_SUN};
use crate::core::numerical::rk4_step;
let n_steps = n_steps.unwrap_or(10);
let h = dt / n_steps as f64;
let f = move |t: f64, state: &nalgebra::DVector<f64>| -> nalgebra::DVector<f64> {
let r = Vector3::new(state[0], state[1], state[2]);
let v = Vector3::new(state[3], state[4], state[5]);
let r_mag = r.norm();
let mut a_total = -mu / (r_mag * r_mag * r_mag) * r;
if include_sun {
let r_sun = sun_position_simple(t);
a_total += third_body_perturbation(&r, &r_sun, GM_SUN);
}
if include_moon {
let r_moon = moon_position_simple(t);
a_total += third_body_perturbation(&r, &r_moon, GM_MOON);
}
nalgebra::DVector::from_vec(vec![v.x, v.y, v.z, a_total.x, a_total.y, a_total.z])
};
let mut state = nalgebra::DVector::from_vec(vec![r0.x, r0.y, r0.z, v0.x, v0.y, v0.z]);
let mut t = t0;
for _ in 0..n_steps {
state = rk4_step(f, t, &state, h);
t += h;
}
let r_final = Vector3::new(state[0], state[1], state[2]);
let v_final = Vector3::new(state[3], state[4], state[5]);
Ok((r_final, v_final))
}
pub fn propagate_thirdbody_dopri5(
r0: &Vector3,
v0: &Vector3,
dt: f64,
mu: f64,
t0: f64,
include_sun: bool,
include_moon: bool,
tol: Option<f64>,
) -> PoliastroResult<(Vector3, Vector3)> {
use crate::core::constants::{GM_MOON, GM_SUN};
use crate::core::numerical::dopri5_integrate;
let tol = tol.unwrap_or(1e-8);
let f = move |t: f64, state: &nalgebra::DVector<f64>| -> nalgebra::DVector<f64> {
let r = Vector3::new(state[0], state[1], state[2]);
let v = Vector3::new(state[3], state[4], state[5]);
let r_mag = r.norm();
let mut a_total = -mu / (r_mag * r_mag * r_mag) * r;
if include_sun {
let r_sun = sun_position_simple(t);
a_total += third_body_perturbation(&r, &r_sun, GM_SUN);
}
if include_moon {
let r_moon = moon_position_simple(t);
a_total += third_body_perturbation(&r, &r_moon, GM_MOON);
}
nalgebra::DVector::from_vec(vec![v.x, v.y, v.z, a_total.x, a_total.y, a_total.z])
};
let state0 = nalgebra::DVector::from_vec(vec![r0.x, r0.y, r0.z, v0.x, v0.y, v0.z]);
let state_final = dopri5_integrate(f, t0, &state0, t0 + dt, dt.abs() / 10.0, tol, None)?;
let r_final = Vector3::new(state_final[0], state_final[1], state_final[2]);
let v_final = Vector3::new(state_final[3], state_final[4], state_final[5]);
Ok((r_final, v_final))
}
pub fn shadow_function(r_sat: &Vector3, r_sun: &Vector3, R_earth: f64) -> f64 {
use crate::core::constants::R_SUN;
let r_sat_mag = r_sat.norm();
let r_sun_mag = r_sun.norm();
let u_sat = r_sat / r_sat_mag;
let u_sun = r_sun / r_sun_mag;
let cos_alpha = u_sat.dot(&u_sun);
if cos_alpha >= 0.0 {
return 1.0;
}
let theta_earth = (R_earth / r_sat_mag).asin();
let theta_sun = (R_SUN / (r_sun_mag - r_sat_mag).abs()).asin();
let alpha = cos_alpha.abs().acos();
if alpha + theta_sun <= theta_earth {
0.0
} else if alpha - theta_sun >= theta_earth {
1.0
} else {
let penumbra_start = theta_earth - theta_sun;
let penumbra_end = theta_earth + theta_sun;
if alpha < penumbra_start {
0.0 } else if alpha > penumbra_end {
1.0 } else {
(alpha - penumbra_start) / (penumbra_end - penumbra_start)
}
}
}
pub fn srp_acceleration(
r_sat: &Vector3,
r_sun: &Vector3,
area_mass_ratio: f64,
C_r: f64,
R_earth: f64,
) -> Vector3 {
use crate::core::constants::{AU, SOLAR_RADIATION_PRESSURE};
let k = shadow_function(r_sat, r_sun, R_earth);
if k < 1e-10 {
return Vector3::zeros();
}
let r_sun_sat = r_sun - r_sat;
let r_sun_sat_mag = r_sun_sat.norm();
if r_sun_sat_mag < 1e3 {
return Vector3::zeros();
}
let u_sun = r_sun_sat / r_sun_sat_mag;
let distance_factor = (AU / r_sun_sat_mag).powi(2);
let a_mag = k * C_r * area_mass_ratio * SOLAR_RADIATION_PRESSURE * distance_factor;
a_mag * u_sun
}
pub fn srp_acceleration_func(
mu: f64,
area_mass_ratio: f64,
C_r: f64,
R_earth: f64,
t0: f64,
) -> impl Fn(f64, &nalgebra::DVector<f64>) -> nalgebra::DVector<f64> {
move |t: f64, state: &nalgebra::DVector<f64>| -> nalgebra::DVector<f64> {
let r = Vector3::new(state[0], state[1], state[2]);
let v = Vector3::new(state[3], state[4], state[5]);
let r_mag = r.norm();
let a_twobody = -mu / (r_mag * r_mag * r_mag) * r;
let r_sun = sun_position_simple(t0 + t);
let a_srp = srp_acceleration(&r, &r_sun, area_mass_ratio, C_r, R_earth);
let a_total = a_twobody + a_srp;
nalgebra::DVector::from_vec(vec![v.x, v.y, v.z, a_total.x, a_total.y, a_total.z])
}
}
pub fn propagate_srp_rk4(
r0: &Vector3,
v0: &Vector3,
dt: f64,
mu: f64,
area_mass_ratio: f64,
C_r: f64,
R_earth: f64,
t0: f64,
n_steps: Option<usize>,
) -> PoliastroResult<(Vector3, Vector3)> {
use crate::core::numerical::rk4_step;
let n_steps = n_steps.unwrap_or(100);
let h = dt / (n_steps as f64);
let f = srp_acceleration_func(mu, area_mass_ratio, C_r, R_earth, t0);
let mut state = nalgebra::DVector::from_vec(vec![r0.x, r0.y, r0.z, v0.x, v0.y, v0.z]);
let mut t = 0.0;
for _ in 0..n_steps {
state = rk4_step(&f, t, &state, h);
t += h;
}
let r_final = Vector3::new(state[0], state[1], state[2]);
let v_final = Vector3::new(state[3], state[4], state[5]);
Ok((r_final, v_final))
}
pub fn propagate_srp_dopri5(
r0: &Vector3,
v0: &Vector3,
dt: f64,
mu: f64,
area_mass_ratio: f64,
C_r: f64,
R_earth: f64,
t0: f64,
tol: Option<f64>,
) -> PoliastroResult<(Vector3, Vector3)> {
use crate::core::numerical::dopri5_integrate;
let tol = tol.unwrap_or(1e-8);
let f = srp_acceleration_func(mu, area_mass_ratio, C_r, R_earth, t0);
let state0 = nalgebra::DVector::from_vec(vec![r0.x, r0.y, r0.z, v0.x, v0.y, v0.z]);
let state_final = dopri5_integrate(f, 0.0, &state0, dt, dt.abs() / 10.0, tol, None)?;
let r_final = Vector3::new(state_final[0], state_final[1], state_final[2]);
let v_final = Vector3::new(state_final[3], state_final[4], state_final[5]);
Ok((r_final, v_final))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::constants::{GM_EARTH, J2_EARTH, R_EARTH};
use approx::assert_relative_eq;
#[test]
fn test_j2_perturbation_equatorial_orbit() {
let r = Vector3::new(7000e3, 0.0, 0.0);
let acc = j2_perturbation(&r, GM_EARTH, J2_EARTH, R_EARTH);
assert!(acc.x < 0.0);
assert_relative_eq!(acc.y, 0.0, epsilon = 1e-20);
assert_relative_eq!(acc.z, 0.0, epsilon = 1e-20);
}
#[test]
fn test_j2_perturbation_polar_orbit() {
let r = Vector3::new(0.0, 0.0, 7000e3);
let acc = j2_perturbation(&r, GM_EARTH, J2_EARTH, R_EARTH);
assert_relative_eq!(acc.x, 0.0, epsilon = 1e-20);
assert_relative_eq!(acc.y, 0.0, epsilon = 1e-20);
assert!(acc.z > 0.0);
}
#[test]
fn test_j2_perturbation_magnitude() {
let r = Vector3::new(7000e3, 0.0, 1000e3);
let acc = j2_perturbation(&r, GM_EARTH, J2_EARTH, R_EARTH);
let acc_mag = acc.norm();
assert!(acc_mag > 1e-6 && acc_mag < 2e-2);
}
#[test]
fn test_j2_acceleration_decreases_with_altitude() {
let r1 = Vector3::new(7000e3, 0.0, 0.0);
let r2 = Vector3::new(14000e3, 0.0, 0.0);
let acc1 = j2_perturbation(&r1, GM_EARTH, J2_EARTH, R_EARTH);
let acc2 = j2_perturbation(&r2, GM_EARTH, J2_EARTH, R_EARTH);
let ratio = acc1.norm() / acc2.norm();
assert!(ratio > 14.0 && ratio < 18.0);
}
#[test]
fn test_propagate_j2_rk4_basic() {
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7546.0, 0.0);
let result = propagate_j2_rk4(&r0, &v0, 100.0, GM_EARTH, J2_EARTH, R_EARTH, Some(10));
assert!(result.is_ok());
let (r1, v1) = result.unwrap();
assert!(r1.norm() > 6000e3); assert!(v1.norm() > 1000.0); }
#[test]
fn test_propagate_j2_dopri5_basic() {
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7546.0, 0.0);
let result = propagate_j2_dopri5(&r0, &v0, 100.0, GM_EARTH, J2_EARTH, R_EARTH, Some(1e-8));
assert!(result.is_ok());
let (r1, v1) = result.unwrap();
assert!(r1.norm() > 6000e3);
assert!(v1.norm() > 1000.0);
}
#[test]
fn test_j2_vs_two_body_difference() {
use crate::propagators::keplerian::propagate_state_keplerian;
let r0 = Vector3::new(7000e3, 0.0, 1000e3); let v0 = Vector3::new(0.0, 7546.0, 100.0);
let dt = 3600.0;
let (r_twobody, _v_twobody) = propagate_state_keplerian(&r0, &v0, dt, GM_EARTH).unwrap();
let (r_j2, _v_j2) = propagate_j2_rk4(&r0, &v0, dt, GM_EARTH, J2_EARTH, R_EARTH, Some(100)).unwrap();
let pos_diff = (r_twobody - r_j2).norm();
println!("Position difference after 1 hour: {} m", pos_diff);
assert!(pos_diff > 10.0);
assert!(pos_diff < 100000.0); }
#[test]
fn test_j2_energy_not_exactly_conserved() {
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7546.0, 0.0);
let (r1, v1) = propagate_j2_rk4(&r0, &v0, 3600.0, GM_EARTH, J2_EARTH, R_EARTH, Some(100)).unwrap();
let e0 = v0.norm_squared() / 2.0 - GM_EARTH / r0.norm();
let e1 = v1.norm_squared() / 2.0 - GM_EARTH / r1.norm();
let energy_diff = (e1 - e0).abs();
assert!(energy_diff < 1e6); }
#[test]
fn test_propagate_j2_dop853_basic() {
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7546.0, 0.0);
let result = propagate_j2_dop853(&r0, &v0, 100.0, GM_EARTH, J2_EARTH, R_EARTH, Some(1e-10));
assert!(result.is_ok());
let (r1, v1) = result.unwrap();
assert!(r1.norm() > 6000e3);
assert!(v1.norm() > 1000.0);
}
#[test]
fn test_propagate_j2_dop853_vs_dopri5() {
let r0 = Vector3::new(7000e3, 0.0, 1000e3);
let v0 = Vector3::new(0.0, 7546.0, 100.0);
let dt = 3600.0;
let (r_dop853, v_dop853) = propagate_j2_dop853(&r0, &v0, dt, GM_EARTH, J2_EARTH, R_EARTH, Some(1e-10)).unwrap();
let (r_dopri5, v_dopri5) = propagate_j2_dopri5(&r0, &v0, dt, GM_EARTH, J2_EARTH, R_EARTH, Some(1e-10)).unwrap();
let pos_diff = (r_dop853 - r_dopri5).norm();
let vel_diff = (v_dop853 - v_dopri5).norm();
assert!(pos_diff < 10.0, "Position difference: {} m", pos_diff);
assert!(vel_diff < 0.01, "Velocity difference: {} m/s", vel_diff);
}
#[test]
fn test_propagate_j2_dop853_long_duration() {
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7546.0, 0.0);
let period = 2.0 * std::f64::consts::PI * (r0.norm().powi(3) / GM_EARTH).sqrt();
let result = propagate_j2_dop853(&r0, &v0, period, GM_EARTH, J2_EARTH, R_EARTH, None);
assert!(result.is_ok());
let (r1, v1) = result.unwrap();
let pos_diff = (r1 - r0).norm();
assert!(pos_diff < 150e3, "Position drift after 1 orbit: {} km", pos_diff / 1e3);
let v_ratio = v1.norm() / v0.norm();
assert!((v_ratio - 1.0).abs() < 0.01, "Velocity change: {}", v_ratio);
}
#[test]
fn test_exponential_density_at_sea_level() {
use crate::core::constants::{H0_EARTH, RHO0_EARTH};
let rho = exponential_density(0.0, RHO0_EARTH, H0_EARTH);
assert_relative_eq!(rho, RHO0_EARTH, epsilon = 1e-10);
}
#[test]
fn test_exponential_density_decreases_with_altitude() {
use crate::core::constants::{H0_EARTH, RHO0_EARTH};
let rho_100km = exponential_density(100e3, RHO0_EARTH, H0_EARTH);
let rho_200km = exponential_density(200e3, RHO0_EARTH, H0_EARTH);
let rho_400km = exponential_density(400e3, RHO0_EARTH, H0_EARTH);
assert!(rho_100km < RHO0_EARTH);
assert!(rho_200km < rho_100km);
assert!(rho_400km < rho_200km);
assert!(rho_400km < 1e-10);
assert!(rho_400km > 0.0); }
#[test]
fn test_exponential_density_scale_height() {
use crate::core::constants::{H0_EARTH, RHO0_EARTH};
let rho_H = exponential_density(H0_EARTH, RHO0_EARTH, H0_EARTH);
let expected = RHO0_EARTH / std::f64::consts::E;
assert_relative_eq!(rho_H, expected, epsilon = 1e-10);
}
#[test]
fn test_drag_acceleration_opposes_velocity() {
use crate::core::constants::{H0_EARTH, R_EARTH, RHO0_EARTH};
let r = Vector3::new(6778e3, 0.0, 0.0);
let v = Vector3::new(0.0, 7670.0, 0.0);
let B = 50.0;
let a_drag = drag_acceleration(&r, &v, R_EARTH, RHO0_EARTH, H0_EARTH, B);
let dot_product = a_drag.dot(&v);
assert!(dot_product < 0.0, "Drag should oppose velocity");
let a_drag_normalized = a_drag.normalize();
let v_normalized = v.normalize();
let cos_angle = a_drag_normalized.dot(&v_normalized);
assert_relative_eq!(cos_angle, -1.0, epsilon = 1e-10);
}
#[test]
fn test_drag_acceleration_magnitude() {
use crate::core::constants::{H0_EARTH, R_EARTH, RHO0_EARTH};
let r = Vector3::new(6778e3, 0.0, 0.0);
let v = Vector3::new(0.0, 7670.0, 0.0);
let B = 82.0;
let a_drag = drag_acceleration(&r, &v, R_EARTH, RHO0_EARTH, H0_EARTH, B);
let mag = a_drag.norm();
assert!(mag > 0.0 && mag < 1e-5, "Drag magnitude at 400km: {} m/s²", mag);
}
#[test]
fn test_drag_acceleration_increases_at_lower_altitude() {
use crate::core::constants::{H0_EARTH, R_EARTH, RHO0_EARTH};
let B = 50.0;
let r_300km = Vector3::new(6678e3, 0.0, 0.0);
let v = Vector3::new(0.0, 7730.0, 0.0); let a_drag_300 = drag_acceleration(&r_300km, &v, R_EARTH, RHO0_EARTH, H0_EARTH, B);
let r_400km = Vector3::new(6778e3, 0.0, 0.0);
let v2 = Vector3::new(0.0, 7670.0, 0.0);
let a_drag_400 = drag_acceleration(&r_400km, &v2, R_EARTH, RHO0_EARTH, H0_EARTH, B);
assert!(a_drag_300.norm() > a_drag_400.norm());
}
#[test]
fn test_drag_acceleration_zero_velocity() {
use crate::core::constants::{H0_EARTH, R_EARTH, RHO0_EARTH};
let r = Vector3::new(6778e3, 0.0, 0.0);
let v = Vector3::zeros(); let B = 50.0;
let a_drag = drag_acceleration(&r, &v, R_EARTH, RHO0_EARTH, H0_EARTH, B);
assert_relative_eq!(a_drag.norm(), 0.0, epsilon = 1e-20);
}
#[test]
fn test_drag_ballistic_coefficient_effect() {
use crate::core::constants::{H0_EARTH, R_EARTH, RHO0_EARTH};
let r = Vector3::new(6778e3, 0.0, 0.0);
let v = Vector3::new(0.0, 7670.0, 0.0);
let B_small = 20.0; let B_large = 200.0;
let a_drag_small = drag_acceleration(&r, &v, R_EARTH, RHO0_EARTH, H0_EARTH, B_small);
let a_drag_large = drag_acceleration(&r, &v, R_EARTH, RHO0_EARTH, H0_EARTH, B_large);
assert!(a_drag_small.norm() > a_drag_large.norm());
let ratio = a_drag_small.norm() / a_drag_large.norm();
let expected_ratio = B_large / B_small;
assert_relative_eq!(ratio, expected_ratio, epsilon = 1e-10);
}
#[test]
fn test_propagate_drag_rk4_basic() {
use crate::core::constants::{GM_EARTH, H0_EARTH, R_EARTH, RHO0_EARTH};
let r0 = Vector3::new(6778e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7670.0, 0.0);
let B = 50.0;
let result = propagate_drag_rk4(&r0, &v0, 600.0, GM_EARTH, R_EARTH, RHO0_EARTH, H0_EARTH, B, Some(100));
assert!(result.is_ok());
let (r1, v1) = result.unwrap();
assert!(r1.norm() > 6000e3); assert!(v1.norm() > 1000.0); }
#[test]
fn test_propagate_drag_dopri5_basic() {
use crate::core::constants::{GM_EARTH, H0_EARTH, R_EARTH, RHO0_EARTH};
let r0 = Vector3::new(6778e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7670.0, 0.0);
let B = 50.0;
let result = propagate_drag_dopri5(&r0, &v0, 600.0, GM_EARTH, R_EARTH, RHO0_EARTH, H0_EARTH, B, Some(1e-8));
assert!(result.is_ok());
let (r1, v1) = result.unwrap();
assert!(r1.norm() > 6000e3);
assert!(v1.norm() > 1000.0);
}
#[test]
fn test_drag_causes_orbit_decay() {
use crate::core::constants::{GM_EARTH, H0_EARTH, R_EARTH, RHO0_EARTH};
use crate::propagators::keplerian::propagate_state_keplerian;
let r0 = Vector3::new(6778e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7670.0, 0.0);
let B = 50.0;
let dt = 3600.0;
let (r_twobody, v_twobody) = propagate_state_keplerian(&r0, &v0, dt, GM_EARTH).unwrap();
let (r_drag, v_drag) = propagate_drag_rk4(&r0, &v0, dt, GM_EARTH, R_EARTH, RHO0_EARTH, H0_EARTH, B, Some(100)).unwrap();
let e_twobody = v_twobody.norm_squared() / 2.0 - GM_EARTH / r_twobody.norm();
let e_drag = v_drag.norm_squared() / 2.0 - GM_EARTH / r_drag.norm();
assert!(e_drag < e_twobody, "Drag should decrease orbital energy");
assert!(r_drag.norm() < r_twobody.norm(), "Drag should cause orbit to decay");
}
#[test]
fn test_propagate_j2_drag_combined() {
use crate::core::constants::{GM_EARTH, H0_EARTH, J2_EARTH, R_EARTH, RHO0_EARTH};
let r0 = Vector3::new(6778e3, 0.0, 1000e3); let v0 = Vector3::new(100.0, 7670.0, 0.0);
let B = 50.0;
let result = propagate_j2_drag_rk4(&r0, &v0, 600.0, GM_EARTH, J2_EARTH, R_EARTH, RHO0_EARTH, H0_EARTH, B, Some(100));
assert!(result.is_ok());
let (r1, v1) = result.unwrap();
assert!(r1.norm() > 6000e3);
assert!(v1.norm() > 1000.0);
}
#[test]
fn test_propagate_j2_drag_dopri5() {
use crate::core::constants::{GM_EARTH, H0_EARTH, J2_EARTH, R_EARTH, RHO0_EARTH};
let r0 = Vector3::new(6778e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7670.0, 0.0);
let B = 50.0;
let result = propagate_j2_drag_dopri5(&r0, &v0, 600.0, GM_EARTH, J2_EARTH, R_EARTH, RHO0_EARTH, H0_EARTH, B, Some(1e-8));
assert!(result.is_ok());
let (r1, v1) = result.unwrap();
assert!(r1.norm() > 6000e3);
assert!(v1.norm() > 1000.0);
}
}
#[allow(dead_code)]
pub fn propagate_cowell<F>(
r0: &Vector3,
v0: &Vector3,
dt: f64,
mu: f64,
perturbations: &[F],
method: &str,
n_steps: Option<usize>,
tol: Option<f64>,
) -> PoliastroResult<(Vector3, Vector3)>
where
F: Fn(f64, &Vector3, &Vector3) -> Vector3,
{
let accel_func = |t: f64, state: &nalgebra::DVector<f64>| -> nalgebra::DVector<f64> {
let r = Vector3::new(state[0], state[1], state[2]);
let v = Vector3::new(state[3], state[4], state[5]);
let r_mag = r.norm();
let mut a_total = -mu / (r_mag * r_mag * r_mag) * r;
for pert in perturbations {
a_total += pert(t, &r, &v);
}
nalgebra::DVector::from_vec(vec![v.x, v.y, v.z, a_total.x, a_total.y, a_total.z])
};
let state0 = nalgebra::DVector::from_vec(vec![r0.x, r0.y, r0.z, v0.x, v0.y, v0.z]);
let state_final = match method {
"rk4" => {
use crate::core::numerical::rk4_step;
let n_steps = n_steps.unwrap_or(10);
let h = dt / n_steps as f64;
let mut state = state0;
let mut t = 0.0;
for _ in 0..n_steps {
state = rk4_step(accel_func, t, &state, h);
t += h;
}
state
}
"dopri5" => {
use crate::core::numerical::dopri5_integrate;
let tol = tol.unwrap_or(1e-8);
dopri5_integrate(accel_func, 0.0, &state0, dt, dt.abs() / 10.0, tol, None)?
}
_ => {
return Err(PoliastroError::ComputationError {
message: format!("Invalid integration method '{method}'. Expected either 'rk4' or 'dopri5'"),
});
}
};
let r_final = Vector3::new(state_final[0], state_final[1], state_final[2]);
let v_final = Vector3::new(state_final[3], state_final[4], state_final[5]);
Ok((r_final, v_final))
}
#[cfg(test)]
mod cowell_tests {
use super::*;
use crate::core::constants::{GM_EARTH, J2_EARTH, R_EARTH};
use approx::assert_relative_eq;
#[test]
fn test_cowell_two_body_only() {
use crate::propagators::keplerian::propagate_state_keplerian;
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7546.0, 0.0);
let dt = 600.0;
let perts: Vec<Box<dyn Fn(f64, &Vector3, &Vector3) -> Vector3>> = vec![];
let (r_cowell, v_cowell) = propagate_cowell(&r0, &v0, dt, GM_EARTH, &perts, "rk4", Some(100), None).unwrap();
let (r_kep, v_kep) = propagate_state_keplerian(&r0, &v0, dt, GM_EARTH).unwrap();
let pos_diff = (r_cowell - r_kep).norm();
let vel_diff = (v_cowell - v_kep).norm();
assert!(pos_diff < 100.0); assert!(vel_diff < 0.1); }
#[test]
fn test_cowell_with_j2() {
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7546.0, 0.0);
let dt = 3600.0;
let j2_pert = |_t: f64, r: &Vector3, _v: &Vector3| {
j2_perturbation(r, GM_EARTH, J2_EARTH, R_EARTH)
};
let perts: Vec<Box<dyn Fn(f64, &Vector3, &Vector3) -> Vector3>> = vec![Box::new(j2_pert)];
let (r_cowell, v_cowell) = propagate_cowell(&r0, &v0, dt, GM_EARTH, &perts, "rk4", Some(100), None).unwrap();
let (r_j2, v_j2) = propagate_j2_rk4(&r0, &v0, dt, GM_EARTH, J2_EARTH, R_EARTH, Some(100)).unwrap();
assert_relative_eq!(r_cowell.x, r_j2.x, epsilon = 1.0);
assert_relative_eq!(r_cowell.y, r_j2.y, epsilon = 1.0);
assert_relative_eq!(r_cowell.z, r_j2.z, epsilon = 1.0);
assert_relative_eq!(v_cowell.x, v_j2.x, epsilon = 0.001);
assert_relative_eq!(v_cowell.y, v_j2.y, epsilon = 0.001);
assert_relative_eq!(v_cowell.z, v_j2.z, epsilon = 0.001);
}
#[test]
fn test_cowell_dopri5_method() {
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7546.0, 0.0);
let dt = 600.0;
let perts: Vec<Box<dyn Fn(f64, &Vector3, &Vector3) -> Vector3>> = vec![];
let result = propagate_cowell(&r0, &v0, dt, GM_EARTH, &perts, "dopri5", None, Some(1e-8));
assert!(result.is_ok());
let (r, v) = result.unwrap();
assert!(r.norm() > 6000e3);
assert!(v.norm() > 1000.0);
}
#[test]
fn test_cowell_invalid_method() {
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7546.0, 0.0);
let perts: Vec<Box<dyn Fn(f64, &Vector3, &Vector3) -> Vector3>> = vec![];
let result = propagate_cowell(&r0, &v0, 600.0, GM_EARTH, &perts, "invalid", None, None);
assert!(result.is_err());
}
}
#[cfg(test)]
mod thirdbody_tests {
use super::*;
use crate::core::constants::{AU, GM_EARTH, GM_MOON, GM_SUN};
use approx::assert_relative_eq;
#[test]
fn test_sun_position_simple_at_j2000() {
let r_sun = sun_position_simple(0.0);
assert_relative_eq!(r_sun.x, AU, epsilon = 1.0);
assert_relative_eq!(r_sun.y, 0.0, epsilon = 1.0);
assert_relative_eq!(r_sun.z, 0.0, epsilon = 1.0);
let r_mag = r_sun.norm();
assert_relative_eq!(r_mag, AU, epsilon = 1.0);
}
#[test]
fn test_sun_position_simple_circular_orbit() {
use crate::core::constants::DAY_TO_SEC;
let t_year = 365.25636 * DAY_TO_SEC;
let r_sun_0 = sun_position_simple(0.0);
let r_sun_1yr = sun_position_simple(t_year);
assert_relative_eq!(r_sun_0.x, r_sun_1yr.x, epsilon = 1e6);
assert_relative_eq!(r_sun_0.y, r_sun_1yr.y, epsilon = 1e6);
let t_quarter = t_year / 4.0;
let r_sun_q = sun_position_simple(t_quarter);
assert_relative_eq!(r_sun_q.x, 0.0, epsilon = 1e6);
assert_relative_eq!(r_sun_q.y, AU, epsilon = 1e6);
}
#[test]
fn test_moon_position_simple_at_j2000() {
let r_moon = moon_position_simple(0.0);
const A_MOON: f64 = 384_400_000.0;
assert_relative_eq!(r_moon.x, A_MOON, epsilon = 1.0);
assert_relative_eq!(r_moon.y, 0.0, epsilon = 1.0);
assert_relative_eq!(r_moon.z, 0.0, epsilon = 1.0);
let r_mag = r_moon.norm();
assert_relative_eq!(r_mag, A_MOON, epsilon = 1.0);
}
#[test]
fn test_moon_position_simple_circular_orbit() {
use crate::core::constants::DAY_TO_SEC;
let t_month = 27.321661 * DAY_TO_SEC;
let r_moon_0 = moon_position_simple(0.0);
let r_moon_1m = moon_position_simple(t_month);
assert_relative_eq!(r_moon_0.x, r_moon_1m.x, epsilon = 1e4);
assert_relative_eq!(r_moon_0.y, r_moon_1m.y, epsilon = 1e4);
}
#[test]
fn test_third_body_perturbation_sun_magnitude() {
let r_sat = Vector3::new(7000e3, 0.0, 0.0); let r_sun = sun_position_simple(0.0);
let a_sun = third_body_perturbation(&r_sat, &r_sun, GM_SUN);
let a_mag = a_sun.norm();
assert!(
a_mag > 1e-7 && a_mag < 3e-6,
"Sun perturbation magnitude: {} m/s²",
a_mag
);
}
#[test]
fn test_third_body_perturbation_moon_magnitude() {
let r_sat = Vector3::new(7000e3, 0.0, 0.0); let r_moon = moon_position_simple(0.0);
let a_moon = third_body_perturbation(&r_sat, &r_moon, GM_MOON);
let a_mag = a_moon.norm();
assert!(
a_mag > 1e-6 && a_mag < 5e-6,
"Moon perturbation magnitude: {} m/s²",
a_mag
);
}
#[test]
fn test_third_body_perturbation_geo_larger() {
let r_leo = Vector3::new(7000e3, 0.0, 0.0);
let r_geo = Vector3::new(42164e3, 0.0, 0.0);
let r_sun = sun_position_simple(0.0);
let a_sun_leo = third_body_perturbation(&r_leo, &r_sun, GM_SUN);
let a_sun_geo = third_body_perturbation(&r_geo, &r_sun, GM_SUN);
println!(
"Sun perturbation at LEO: {} m/s²",
a_sun_leo.norm()
);
println!(
"Sun perturbation at GEO: {} m/s²",
a_sun_geo.norm()
);
assert!(a_sun_leo.norm() > 1e-7 && a_sun_leo.norm() < 1e-4);
assert!(a_sun_geo.norm() > 1e-7 && a_sun_geo.norm() < 1e-4);
}
#[test]
fn test_sun_moon_perturbation_combined() {
let r = Vector3::new(7000e3, 0.0, 0.0);
let t = 0.0;
let a_combined = sun_moon_perturbation(&r, t);
let r_sun = sun_position_simple(t);
let r_moon = moon_position_simple(t);
let a_sun = third_body_perturbation(&r, &r_sun, GM_SUN);
let a_moon = third_body_perturbation(&r, &r_moon, GM_MOON);
let a_manual = a_sun + a_moon;
assert_relative_eq!(a_combined.x, a_manual.x, epsilon = 1e-15);
assert_relative_eq!(a_combined.y, a_manual.y, epsilon = 1e-15);
assert_relative_eq!(a_combined.z, a_manual.z, epsilon = 1e-15);
}
#[test]
fn test_third_body_perturbation_direct_indirect_terms() {
let r_sat = Vector3::new(7000e3, 0.0, 0.0); let r_sun = Vector3::new(AU, 0.0, 0.0);
let a_sun = third_body_perturbation(&r_sat, &r_sun, GM_SUN);
println!("Sun perturbation when aligned: {:?}", a_sun);
assert!(a_sun.norm() > 1e-7 && a_sun.norm() < 1e-4);
}
#[test]
fn test_propagate_thirdbody_rk4_sun_only() {
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7546.0, 0.0);
let dt = 600.0; let t0 = 0.0;
let result = propagate_thirdbody_rk4(&r0, &v0, dt, GM_EARTH, t0, true, false, Some(10));
assert!(result.is_ok());
let (r1, v1) = result.unwrap();
assert!(r1.norm() > 6000e3); assert!(v1.norm() > 1000.0); }
#[test]
fn test_propagate_thirdbody_rk4_moon_only() {
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7546.0, 0.0);
let dt = 600.0;
let t0 = 0.0;
let result = propagate_thirdbody_rk4(&r0, &v0, dt, GM_EARTH, t0, false, true, Some(10));
assert!(result.is_ok());
let (r1, v1) = result.unwrap();
assert!(r1.norm() > 6000e3);
assert!(v1.norm() > 1000.0);
}
#[test]
fn test_propagate_thirdbody_rk4_sun_and_moon() {
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7546.0, 0.0);
let dt = 600.0;
let t0 = 0.0;
let result = propagate_thirdbody_rk4(&r0, &v0, dt, GM_EARTH, t0, true, true, Some(10));
assert!(result.is_ok());
let (r1, v1) = result.unwrap();
assert!(r1.norm() > 6000e3);
assert!(v1.norm() > 1000.0);
}
#[test]
fn test_propagate_thirdbody_dopri5_basic() {
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7546.0, 0.0);
let dt = 600.0;
let t0 = 0.0;
let result = propagate_thirdbody_dopri5(&r0, &v0, dt, GM_EARTH, t0, true, true, Some(1e-8));
assert!(result.is_ok());
let (r1, v1) = result.unwrap();
assert!(r1.norm() > 6000e3);
assert!(v1.norm() > 1000.0);
}
#[test]
fn test_thirdbody_vs_twobody_difference() {
use crate::propagators::keplerian::propagate_state_keplerian;
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7546.0, 0.0);
let dt = 3600.0; let t0 = 0.0;
let (r_twobody, _v_twobody) = propagate_state_keplerian(&r0, &v0, dt, GM_EARTH).unwrap();
let (r_thirdbody, _v_thirdbody) =
propagate_thirdbody_rk4(&r0, &v0, dt, GM_EARTH, t0, true, true, Some(100)).unwrap();
let pos_diff = (r_twobody - r_thirdbody).norm();
println!("Position difference after 1 hour: {} m", pos_diff);
assert!(pos_diff > 0.1); assert!(pos_diff < 1000.0); }
#[test]
fn test_thirdbody_geo_significant() {
use crate::propagators::keplerian::propagate_state_keplerian;
let r0 = Vector3::new(42164e3, 0.0, 0.0); let v0 = Vector3::new(0.0, 3075.0, 0.0); let dt = 86400.0; let t0 = 0.0;
let (r_twobody, _v_twobody) = propagate_state_keplerian(&r0, &v0, dt, GM_EARTH).unwrap();
let (r_thirdbody, _v_thirdbody) =
propagate_thirdbody_rk4(&r0, &v0, dt, GM_EARTH, t0, true, true, Some(1000)).unwrap();
let pos_diff = (r_twobody - r_thirdbody).norm();
println!("GEO position difference after 1 day: {} m", pos_diff);
assert!(pos_diff > 100.0); assert!(pos_diff < 1e6); }
#[test]
fn test_thirdbody_time_dependence() {
let r = Vector3::new(7000e3, 0.0, 0.0);
let t0 = 0.0;
let t1 = 86400.0;
let a0 = sun_moon_perturbation(&r, t0);
let a1 = sun_moon_perturbation(&r, t1);
let diff = (a0 - a1).norm();
println!("Acceleration difference after 1 day: {} m/s²", diff);
assert!(diff > 1e-8); }
#[test]
fn test_shadow_function_full_sunlight() {
use crate::core::constants::R_EARTH;
let r_sat = Vector3::new(7000e3, 0.0, 0.0);
let r_sun = Vector3::new(AU, 0.0, 0.0);
let nu = shadow_function(&r_sat, &r_sun, R_EARTH);
assert_relative_eq!(nu, 1.0, epsilon = 1e-10);
}
#[test]
fn test_shadow_function_full_umbra() {
use crate::core::constants::R_EARTH;
let r_sat = Vector3::new(-7000e3, 0.0, 0.0); let r_sun = Vector3::new(AU, 0.0, 0.0);
let nu = shadow_function(&r_sat, &r_sun, R_EARTH);
assert_relative_eq!(nu, 0.0, epsilon = 1e-10);
}
#[test]
fn test_shadow_function_penumbra() {
use crate::core::constants::R_EARTH;
let r_sat = Vector3::new(-7000e3, 1000e3, 0.0);
let r_sun = Vector3::new(AU, 0.0, 0.0);
let nu = shadow_function(&r_sat, &r_sun, R_EARTH);
assert!(nu >= 0.0 && nu <= 1.0, "Shadow function out of range: {}", nu);
}
#[test]
fn test_shadow_function_high_altitude_less_shadow() {
use crate::core::constants::R_EARTH;
let r_sun = Vector3::new(AU, 0.0, 0.0);
let r_leo = Vector3::new(-7000e3, 0.0, 0.0);
let nu_leo = shadow_function(&r_leo, &r_sun, R_EARTH);
let r_geo = Vector3::new(-42164e3, 0.0, 0.0);
let nu_geo = shadow_function(&r_geo, &r_sun, R_EARTH);
assert!(nu_geo >= nu_leo, "GEO should have >= shadow function than LEO");
}
#[test]
fn test_shadow_function_perpendicular_full_sun() {
use crate::core::constants::R_EARTH;
let r_sat = Vector3::new(0.0, 7000e3, 0.0);
let r_sun = Vector3::new(AU, 0.0, 0.0);
let nu = shadow_function(&r_sat, &r_sun, R_EARTH);
assert_relative_eq!(nu, 1.0, epsilon = 1e-10);
}
#[test]
fn test_shadow_function_symmetry() {
use crate::core::constants::R_EARTH;
let r_sun = Vector3::new(AU, 0.0, 0.0);
let r_sat_plus_y = Vector3::new(-7000e3, 500e3, 0.0);
let r_sat_minus_y = Vector3::new(-7000e3, -500e3, 0.0);
let r_sat_plus_z = Vector3::new(-7000e3, 0.0, 500e3);
let nu_plus_y = shadow_function(&r_sat_plus_y, &r_sun, R_EARTH);
let nu_minus_y = shadow_function(&r_sat_minus_y, &r_sun, R_EARTH);
let nu_plus_z = shadow_function(&r_sat_plus_z, &r_sun, R_EARTH);
assert_relative_eq!(nu_plus_y, nu_minus_y, epsilon = 1e-10);
assert_relative_eq!(nu_plus_y, nu_plus_z, epsilon = 1e-10);
}
}
#[cfg(test)]
mod srp_tests {
use super::*;
use crate::core::constants::{AU, GM_EARTH, R_EARTH};
use approx::assert_relative_eq;
#[test]
fn test_propagate_srp_rk4_basic() {
let r0 = Vector3::new(42164e3, 0.0, 0.0); let v0 = Vector3::new(0.0, 3075.0, 0.0);
let dt = 600.0;
let area_mass_ratio = 0.01; let C_r = 1.3; let t0 = 0.0;
let result = propagate_srp_rk4(&r0, &v0, dt, GM_EARTH, area_mass_ratio, C_r, R_EARTH, t0, Some(100));
assert!(result.is_ok());
let (r1, v1) = result.unwrap();
assert!(r1.norm() > 40000e3); assert!(v1.norm() > 2000.0); }
#[test]
fn test_propagate_srp_dopri5_basic() {
let r0 = Vector3::new(42164e3, 0.0, 0.0); let v0 = Vector3::new(0.0, 3075.0, 0.0);
let dt = 600.0;
let area_mass_ratio = 0.01;
let C_r = 1.3;
let t0 = 0.0;
let result = propagate_srp_dopri5(&r0, &v0, dt, GM_EARTH, area_mass_ratio, C_r, R_EARTH, t0, Some(1e-8));
assert!(result.is_ok());
let (r1, v1) = result.unwrap();
assert!(r1.norm() > 40000e3);
assert!(v1.norm() > 2000.0);
}
#[test]
fn test_propagate_srp_rk4_vs_dopri5() {
let r0 = Vector3::new(42164e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 3075.0, 0.0);
let dt = 3600.0;
let area_mass_ratio = 0.01;
let C_r = 1.3;
let t0 = 0.0;
let (r_rk4, v_rk4) = propagate_srp_rk4(&r0, &v0, dt, GM_EARTH, area_mass_ratio, C_r, R_EARTH, t0, Some(100)).unwrap();
let (r_dopri5, v_dopri5) = propagate_srp_dopri5(&r0, &v0, dt, GM_EARTH, area_mass_ratio, C_r, R_EARTH, t0, Some(1e-8)).unwrap();
let pos_diff = (r_rk4 - r_dopri5).norm();
let vel_diff = (v_rk4 - v_dopri5).norm();
assert!(pos_diff < 100.0, "Position difference: {} m", pos_diff);
assert!(vel_diff < 0.1, "Velocity difference: {} m/s", vel_diff);
}
#[test]
fn test_propagate_srp_vs_twobody() {
use crate::propagators::keplerian::propagate_state_keplerian;
let r0 = Vector3::new(42164e3, 0.0, 0.0); let v0 = Vector3::new(0.0, 3075.0, 0.0);
let dt = 86400.0;
let area_mass_ratio = 0.01; let C_r = 1.3;
let t0 = 0.0;
let (r_twobody, _v_twobody) = propagate_state_keplerian(&r0, &v0, dt, GM_EARTH).unwrap();
let (r_srp, _v_srp) = propagate_srp_rk4(&r0, &v0, dt, GM_EARTH, area_mass_ratio, C_r, R_EARTH, t0, Some(1000)).unwrap();
let pos_diff = (r_twobody - r_srp).norm();
println!("Position difference after 1 day (GEO): {} m", pos_diff);
assert!(pos_diff > 1.0); assert!(pos_diff < 100000.0); }
#[test]
fn test_propagate_srp_zero_area() {
use crate::propagators::keplerian::propagate_state_keplerian;
let r0 = Vector3::new(42164e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 3075.0, 0.0);
let dt = 3600.0;
let area_mass_ratio = 0.0; let C_r = 1.3;
let t0 = 0.0;
let (r_srp, v_srp) = propagate_srp_rk4(&r0, &v0, dt, GM_EARTH, area_mass_ratio, C_r, R_EARTH, t0, Some(100)).unwrap();
let (r_twobody, v_twobody) = propagate_state_keplerian(&r0, &v0, dt, GM_EARTH).unwrap();
let pos_diff = (r_srp - r_twobody).norm();
let vel_diff = (v_srp - v_twobody).norm();
assert!(pos_diff < 10.0, "Position difference: {} m", pos_diff);
assert!(vel_diff < 0.01, "Velocity difference: {} m/s", vel_diff);
}
#[test]
fn test_propagate_srp_shadow_effect() {
let r0 = Vector3::new(7000e3, 0.0, 0.0); let v0 = Vector3::new(0.0, 7546.0, 0.0);
let dt = 600.0;
let area_mass_ratio = 0.02; let C_r = 1.5;
let t0 = 0.0;
let result = propagate_srp_rk4(&r0, &v0, dt, GM_EARTH, area_mass_ratio, C_r, R_EARTH, t0, Some(100));
assert!(result.is_ok());
let (r1, v1) = result.unwrap();
assert!(r1.norm() > 6000e3);
assert!(v1.norm() > 6000.0);
}
#[test]
fn test_propagate_srp_long_duration() {
let r0 = Vector3::new(42164e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 3075.0, 0.0);
let dt = 86400.0;
let area_mass_ratio = 0.01;
let C_r = 1.3;
let t0 = 0.0;
let result = propagate_srp_dopri5(&r0, &v0, dt, GM_EARTH, area_mass_ratio, C_r, R_EARTH, t0, None);
assert!(result.is_ok());
let (r1, v1) = result.unwrap();
let a0 = r0.norm();
let a1 = r1.norm();
let a_ratio = a1 / a0;
assert!((a_ratio - 1.0).abs() < 0.01, "Semi-major axis change: {}", a_ratio);
let v_ratio = v1.norm() / v0.norm();
assert!((v_ratio - 1.0).abs() < 0.05, "Velocity magnitude change: {}", v_ratio);
}
}
pub trait Perturbation: Send + Sync {
fn acceleration(&self, t: f64, r: &Vector3, v: &Vector3, mu: f64) -> Vector3;
fn name(&self) -> &str;
fn is_time_dependent(&self) -> bool {
false
}
}
#[derive(Debug, Clone, Copy)]
pub struct J2Perturbation {
pub j2: f64,
pub radius: f64,
}
impl J2Perturbation {
pub fn new(j2: f64, radius: f64) -> Self {
Self { j2, radius }
}
pub fn earth() -> Self {
use crate::core::constants::{J2_EARTH, R_EARTH};
Self::new(J2_EARTH, R_EARTH)
}
}
impl Perturbation for J2Perturbation {
fn acceleration(&self, _t: f64, r: &Vector3, _v: &Vector3, mu: f64) -> Vector3 {
j2_perturbation(r, mu, self.j2, self.radius)
}
fn name(&self) -> &str {
"J2 Oblateness"
}
fn is_time_dependent(&self) -> bool {
false
}
}
#[derive(Debug, Clone, Copy)]
pub struct DragPerturbation {
pub radius: f64,
pub rho0: f64,
pub scale_height: f64,
pub ballistic_coeff: f64,
}
impl DragPerturbation {
pub fn new(radius: f64, rho0: f64, scale_height: f64, ballistic_coeff: f64) -> Self {
Self {
radius,
rho0,
scale_height,
ballistic_coeff,
}
}
pub fn earth(ballistic_coeff: f64) -> Self {
use crate::core::constants::{H0_EARTH, R_EARTH, RHO0_EARTH};
Self::new(R_EARTH, RHO0_EARTH, H0_EARTH, ballistic_coeff)
}
}
impl Perturbation for DragPerturbation {
fn acceleration(&self, _t: f64, r: &Vector3, v: &Vector3, _mu: f64) -> Vector3 {
drag_acceleration(
r,
v,
self.radius,
self.rho0,
self.scale_height,
self.ballistic_coeff,
)
}
fn name(&self) -> &str {
"Atmospheric Drag"
}
fn is_time_dependent(&self) -> bool {
false
}
}
#[derive(Clone)]
pub enum ThirdBodyPerturbation {
Sun,
Moon,
Custom {
mu: f64,
position_func: fn(f64) -> Vector3,
body_name: String,
},
}
impl ThirdBodyPerturbation {
pub fn sun() -> Self {
Self::Sun
}
pub fn moon() -> Self {
Self::Moon
}
pub fn custom(mu: f64, position_func: fn(f64) -> Vector3, name: impl Into<String>) -> Self {
Self::Custom {
mu,
position_func,
body_name: name.into(),
}
}
fn get_mu(&self) -> f64 {
use crate::core::constants::{GM_MOON, GM_SUN};
match self {
Self::Sun => GM_SUN,
Self::Moon => GM_MOON,
Self::Custom { mu, .. } => *mu,
}
}
fn get_position(&self, t: f64) -> Vector3 {
match self {
Self::Sun => sun_position_simple(t),
Self::Moon => moon_position_simple(t),
Self::Custom { position_func, .. } => position_func(t),
}
}
}
impl Perturbation for ThirdBodyPerturbation {
fn acceleration(&self, t: f64, r: &Vector3, _v: &Vector3, _mu: f64) -> Vector3 {
let r_third = self.get_position(t);
let mu_third = self.get_mu();
third_body_perturbation(r, &r_third, mu_third)
}
fn name(&self) -> &str {
match self {
Self::Sun => "Third-Body (Sun)",
Self::Moon => "Third-Body (Moon)",
Self::Custom { body_name, .. } => body_name,
}
}
fn is_time_dependent(&self) -> bool {
true }
}
impl std::fmt::Debug for ThirdBodyPerturbation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Sun => write!(f, "ThirdBodyPerturbation::Sun"),
Self::Moon => write!(f, "ThirdBodyPerturbation::Moon"),
Self::Custom { mu, body_name, .. } => {
f.debug_struct("ThirdBodyPerturbation::Custom")
.field("mu", mu)
.field("body_name", body_name)
.finish()
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SolarRadiationPressure {
pub area_mass_ratio: f64,
pub reflectivity: f64,
pub body_radius: f64,
}
impl SolarRadiationPressure {
pub fn new(area_mass_ratio: f64, reflectivity: f64, body_radius: f64) -> Self {
Self {
area_mass_ratio,
reflectivity,
body_radius,
}
}
pub fn earth(area_mass_ratio: f64, reflectivity: f64) -> Self {
use crate::core::constants::R_EARTH;
Self::new(area_mass_ratio, reflectivity, R_EARTH)
}
}
impl Perturbation for SolarRadiationPressure {
fn acceleration(&self, t: f64, r: &Vector3, _v: &Vector3, _mu: f64) -> Vector3 {
let r_sun = sun_position_simple(t);
srp_acceleration(
r,
&r_sun,
self.area_mass_ratio,
self.reflectivity,
self.body_radius,
)
}
fn name(&self) -> &str {
"Solar Radiation Pressure"
}
fn is_time_dependent(&self) -> bool {
true }
}
#[derive(Default)]
pub struct PerturbationSet {
perturbations: Vec<Box<dyn Perturbation>>,
}
impl PerturbationSet {
pub fn new() -> Self {
Self {
perturbations: Vec::new(),
}
}
pub fn add<P: Perturbation + 'static>(&mut self, perturbation: P) {
self.perturbations.push(Box::new(perturbation));
}
pub fn len(&self) -> usize {
self.perturbations.len()
}
pub fn is_empty(&self) -> bool {
self.perturbations.is_empty()
}
pub fn names(&self) -> Vec<&str> {
self.perturbations.iter().map(|p| p.name()).collect()
}
pub fn is_time_dependent(&self) -> bool {
self.perturbations.iter().any(|p| p.is_time_dependent())
}
pub fn total_acceleration(&self, t: f64, r: &Vector3, v: &Vector3, mu: f64) -> Vector3 {
let mut total = Vector3::zeros();
for pert in &self.perturbations {
total += pert.acceleration(t, r, v, mu);
}
total
}
}
impl Perturbation for PerturbationSet {
fn acceleration(&self, t: f64, r: &Vector3, v: &Vector3, mu: f64) -> Vector3 {
self.total_acceleration(t, r, v, mu)
}
fn name(&self) -> &str {
"Combined Perturbations"
}
fn is_time_dependent(&self) -> bool {
self.is_time_dependent()
}
}
impl std::fmt::Debug for PerturbationSet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PerturbationSet")
.field("count", &self.len())
.field("names", &self.names())
.finish()
}
}
pub fn propagate_with_perturbations<P: Perturbation + ?Sized>(
r0: &Vector3,
v0: &Vector3,
dt: f64,
mu: f64,
perturbations: &P,
method: &str,
n_steps: Option<usize>,
tol: Option<f64>,
) -> PoliastroResult<(Vector3, Vector3)> {
use crate::core::numerical::{dopri5_integrate, rk4_step};
let accel_func = |t: f64, state: &nalgebra::DVector<f64>| -> nalgebra::DVector<f64> {
let r = Vector3::new(state[0], state[1], state[2]);
let v = Vector3::new(state[3], state[4], state[5]);
let r_mag = r.norm();
let a_twobody = -mu / (r_mag * r_mag * r_mag) * r;
let a_pert = perturbations.acceleration(t, &r, &v, mu);
let a_total = a_twobody + a_pert;
nalgebra::DVector::from_vec(vec![v.x, v.y, v.z, a_total.x, a_total.y, a_total.z])
};
let mut state = nalgebra::DVector::from_vec(vec![r0.x, r0.y, r0.z, v0.x, v0.y, v0.z]);
let state_final = match method.to_lowercase().as_str() {
"rk4" => {
let steps = n_steps.unwrap_or(100);
let h = dt / steps as f64;
let mut t = 0.0;
for _ in 0..steps {
state = rk4_step(accel_func, t, &state, h);
t += h;
}
state
}
"dopri5" => {
let tolerance = tol.unwrap_or(1e-8);
let h0 = dt.abs() / 10.0; dopri5_integrate(accel_func, 0.0, &state, dt, h0, tolerance, None)?
}
_ => {
return Err(PoliastroError::invalid_state(format!(
"Unknown integration method: {method}. Use 'rk4' or 'dopri5'"
)))
}
};
let r_final = Vector3::new(state_final[0], state_final[1], state_final[2]);
let v_final = Vector3::new(state_final[3], state_final[4], state_final[5]);
Ok((r_final, v_final))
}
#[cfg(test)]
mod perturbation_trait_tests {
use super::*;
use crate::core::constants::{GM_EARTH, J2_EARTH, R_EARTH};
use approx::assert_relative_eq;
#[test]
fn test_j2_perturbation_trait() {
let j2_pert = J2Perturbation::new(J2_EARTH, R_EARTH);
let r = Vector3::new(7000e3, 0.0, 0.0);
let v = Vector3::new(0.0, 7546.0, 0.0);
let a = j2_pert.acceleration(0.0, &r, &v, GM_EARTH);
let a_direct = j2_perturbation(&r, GM_EARTH, J2_EARTH, R_EARTH);
assert_relative_eq!(a.x, a_direct.x, epsilon = 1e-12);
assert_relative_eq!(a.y, a_direct.y, epsilon = 1e-12);
assert_relative_eq!(a.z, a_direct.z, epsilon = 1e-12);
assert_eq!(j2_pert.name(), "J2 Oblateness");
assert!(!j2_pert.is_time_dependent());
}
#[test]
fn test_j2_earth_convenience() {
let j2_pert = J2Perturbation::earth();
assert_relative_eq!(j2_pert.j2, J2_EARTH, epsilon = 1e-15);
assert_relative_eq!(j2_pert.radius, R_EARTH, epsilon = 1e-15);
}
#[test]
fn test_drag_perturbation_trait() {
use crate::core::constants::{H0_EARTH, RHO0_EARTH};
let drag_pert = DragPerturbation::new(R_EARTH, RHO0_EARTH, H0_EARTH, 100.0);
let r = Vector3::new(6778e3, 0.0, 0.0); let v = Vector3::new(0.0, 7670.0, 0.0);
let a = drag_pert.acceleration(0.0, &r, &v, GM_EARTH);
let a_direct = drag_acceleration(&r, &v, R_EARTH, RHO0_EARTH, H0_EARTH, 100.0);
assert_relative_eq!(a.x, a_direct.x, epsilon = 1e-12);
assert_relative_eq!(a.y, a_direct.y, epsilon = 1e-12);
assert_relative_eq!(a.z, a_direct.z, epsilon = 1e-12);
assert_eq!(drag_pert.name(), "Atmospheric Drag");
assert!(!drag_pert.is_time_dependent());
}
#[test]
fn test_drag_earth_convenience() {
use crate::core::constants::{H0_EARTH, RHO0_EARTH};
let drag_pert = DragPerturbation::earth(100.0);
assert_relative_eq!(drag_pert.radius, R_EARTH, epsilon = 1e-15);
assert_relative_eq!(drag_pert.rho0, RHO0_EARTH, epsilon = 1e-15);
assert_relative_eq!(drag_pert.scale_height, H0_EARTH, epsilon = 1e-15);
assert_relative_eq!(drag_pert.ballistic_coeff, 100.0, epsilon = 1e-15);
}
#[test]
fn test_thirdbody_sun_perturbation() {
let sun_pert = ThirdBodyPerturbation::sun();
let r = Vector3::new(42164e3, 0.0, 0.0); let v = Vector3::new(0.0, 3075.0, 0.0);
let a = sun_pert.acceleration(0.0, &r, &v, GM_EARTH);
let r_sun = sun_position_simple(0.0);
let a_direct = third_body_perturbation(&r, &r_sun, crate::core::constants::GM_SUN);
assert_relative_eq!(a.x, a_direct.x, epsilon = 1e-12);
assert_relative_eq!(a.y, a_direct.y, epsilon = 1e-12);
assert_relative_eq!(a.z, a_direct.z, epsilon = 1e-12);
assert_eq!(sun_pert.name(), "Third-Body (Sun)");
assert!(sun_pert.is_time_dependent());
}
#[test]
fn test_thirdbody_moon_perturbation() {
let moon_pert = ThirdBodyPerturbation::moon();
let r = Vector3::new(42164e3, 0.0, 0.0); let v = Vector3::new(0.0, 3075.0, 0.0);
let a = moon_pert.acceleration(0.0, &r, &v, GM_EARTH);
let r_moon = moon_position_simple(0.0);
let a_direct = third_body_perturbation(&r, &r_moon, crate::core::constants::GM_MOON);
assert_relative_eq!(a.x, a_direct.x, epsilon = 1e-12);
assert_relative_eq!(a.y, a_direct.y, epsilon = 1e-12);
assert_relative_eq!(a.z, a_direct.z, epsilon = 1e-12);
assert_eq!(moon_pert.name(), "Third-Body (Moon)");
assert!(moon_pert.is_time_dependent());
}
#[test]
fn test_thirdbody_custom_perturbation() {
fn custom_body_pos(_t: f64) -> Vector3 {
Vector3::new(1e9, 0.0, 0.0)
}
let custom_pert = ThirdBodyPerturbation::custom(1e15, custom_body_pos, "Custom Body");
let r = Vector3::new(7000e3, 0.0, 0.0);
let v = Vector3::new(0.0, 7546.0, 0.0);
let a = custom_pert.acceleration(0.0, &r, &v, GM_EARTH);
assert!(a.norm() > 0.0);
assert_eq!(custom_pert.name(), "Custom Body");
assert!(custom_pert.is_time_dependent());
}
#[test]
fn test_srp_perturbation_trait() {
let srp_pert = SolarRadiationPressure::new(0.01, 1.3, R_EARTH);
let r = Vector3::new(42164e3, 0.0, 0.0); let v = Vector3::new(0.0, 3075.0, 0.0);
let a = srp_pert.acceleration(0.0, &r, &v, GM_EARTH);
let r_sun = sun_position_simple(0.0);
let a_direct = srp_acceleration(&r, &r_sun, 0.01, 1.3, R_EARTH);
assert_relative_eq!(a.x, a_direct.x, epsilon = 1e-12);
assert_relative_eq!(a.y, a_direct.y, epsilon = 1e-12);
assert_relative_eq!(a.z, a_direct.z, epsilon = 1e-12);
assert_eq!(srp_pert.name(), "Solar Radiation Pressure");
assert!(srp_pert.is_time_dependent());
}
#[test]
fn test_srp_earth_convenience() {
let srp_pert = SolarRadiationPressure::earth(0.01, 1.3);
assert_relative_eq!(srp_pert.area_mass_ratio, 0.01, epsilon = 1e-15);
assert_relative_eq!(srp_pert.reflectivity, 1.3, epsilon = 1e-15);
assert_relative_eq!(srp_pert.body_radius, R_EARTH, epsilon = 1e-15);
}
#[test]
fn test_perturbation_set_empty() {
let perts = PerturbationSet::new();
assert_eq!(perts.len(), 0);
assert!(perts.is_empty());
assert!(!perts.is_time_dependent());
let r = Vector3::new(7000e3, 0.0, 0.0);
let v = Vector3::new(0.0, 7546.0, 0.0);
let a = perts.total_acceleration(0.0, &r, &v, GM_EARTH);
assert_relative_eq!(a.x, 0.0, epsilon = 1e-15);
assert_relative_eq!(a.y, 0.0, epsilon = 1e-15);
assert_relative_eq!(a.z, 0.0, epsilon = 1e-15);
}
#[test]
fn test_perturbation_set_single() {
let mut perts = PerturbationSet::new();
perts.add(J2Perturbation::earth());
assert_eq!(perts.len(), 1);
assert!(!perts.is_empty());
assert!(!perts.is_time_dependent());
let names = perts.names();
assert_eq!(names, vec!["J2 Oblateness"]);
let r = Vector3::new(7000e3, 0.0, 0.0);
let v = Vector3::new(0.0, 7546.0, 0.0);
let a = perts.total_acceleration(0.0, &r, &v, GM_EARTH);
let a_j2 = j2_perturbation(&r, GM_EARTH, J2_EARTH, R_EARTH);
assert_relative_eq!(a.x, a_j2.x, epsilon = 1e-12);
assert_relative_eq!(a.y, a_j2.y, epsilon = 1e-12);
assert_relative_eq!(a.z, a_j2.z, epsilon = 1e-12);
}
#[test]
fn test_perturbation_set_multiple() {
let mut perts = PerturbationSet::new();
perts.add(J2Perturbation::earth());
perts.add(DragPerturbation::earth(100.0));
perts.add(ThirdBodyPerturbation::sun());
perts.add(ThirdBodyPerturbation::moon());
assert_eq!(perts.len(), 4);
assert!(!perts.is_empty());
assert!(perts.is_time_dependent());
let names = perts.names();
assert_eq!(names.len(), 4);
assert!(names.contains(&"J2 Oblateness"));
assert!(names.contains(&"Atmospheric Drag"));
assert!(names.contains(&"Third-Body (Sun)"));
assert!(names.contains(&"Third-Body (Moon)"));
let r = Vector3::new(6778e3, 0.0, 0.0); let v = Vector3::new(0.0, 7670.0, 0.0);
let a = perts.total_acceleration(0.0, &r, &v, GM_EARTH);
assert!(a.norm() > 0.0);
use crate::core::constants::{H0_EARTH, RHO0_EARTH};
let a_j2 = j2_perturbation(&r, GM_EARTH, J2_EARTH, R_EARTH);
let a_drag = drag_acceleration(&r, &v, R_EARTH, RHO0_EARTH, H0_EARTH, 100.0);
let r_sun = sun_position_simple(0.0);
let a_sun = third_body_perturbation(&r, &r_sun, crate::core::constants::GM_SUN);
let r_moon = moon_position_simple(0.0);
let a_moon = third_body_perturbation(&r, &r_moon, crate::core::constants::GM_MOON);
let a_expected = a_j2 + a_drag + a_sun + a_moon;
assert_relative_eq!(a.x, a_expected.x, epsilon = 1e-10);
assert_relative_eq!(a.y, a_expected.y, epsilon = 1e-10);
assert_relative_eq!(a.z, a_expected.z, epsilon = 1e-10);
}
#[test]
fn test_propagate_with_perturbations_j2_only() {
let mut perts = PerturbationSet::new();
perts.add(J2Perturbation::earth());
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7546.0, 0.0);
let dt = 3600.0;
let result = propagate_with_perturbations(
&r0, &v0, dt, GM_EARTH, &perts, "rk4", Some(100), None
);
assert!(result.is_ok());
let (r, v) = result.unwrap();
let (r_j2, v_j2) = propagate_j2_rk4(&r0, &v0, dt, GM_EARTH, J2_EARTH, R_EARTH, Some(100)).unwrap();
assert_relative_eq!(r.x, r_j2.x, epsilon = 1.0);
assert_relative_eq!(r.y, r_j2.y, epsilon = 1.0);
assert_relative_eq!(r.z, r_j2.z, epsilon = 1.0);
assert_relative_eq!(v.x, v_j2.x, epsilon = 0.001);
assert_relative_eq!(v.y, v_j2.y, epsilon = 0.001);
assert_relative_eq!(v.z, v_j2.z, epsilon = 0.001);
}
#[test]
fn test_propagate_with_perturbations_combined() {
let mut perts = PerturbationSet::new();
perts.add(J2Perturbation::earth());
perts.add(DragPerturbation::earth(100.0));
let r0 = Vector3::new(6778e3, 0.0, 0.0); let v0 = Vector3::new(0.0, 7670.0, 0.0);
let dt = 600.0;
let result = propagate_with_perturbations(
&r0, &v0, dt, GM_EARTH, &perts, "dopri5", None, Some(1e-8)
);
assert!(result.is_ok());
let (r, v) = result.unwrap();
assert!(r.norm() > 6000e3);
assert!(r.norm() < 8000e3);
assert!(v.norm() > 1000.0);
assert!(v.norm() < 10000.0);
let pos_diff = (r - r0).norm();
let vel_diff = (v - v0).norm();
assert!(pos_diff > 100.0); assert!(vel_diff > 0.1); }
#[test]
fn test_propagate_with_perturbations_invalid_method() {
let perts = PerturbationSet::new();
let r0 = Vector3::new(7000e3, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7546.0, 0.0);
let result = propagate_with_perturbations(
&r0, &v0, 600.0, GM_EARTH, &perts, "invalid", None, None
);
assert!(result.is_err());
}
}