use crate::core::error::{PoliastroError, PoliastroResult};
use crate::core::linalg::Vector3;
use crate::propagators::perturbations::{drag_acceleration, j2_perturbation};
use crate::core::constants::{GM_EARTH, J2_EARTH, R_EARTH};
pub const DEFAULT_TERMINAL_ALTITUDE: f64 = 100_000.0;
pub const DEFAULT_RHO0: f64 = 1.225;
pub const DEFAULT_H0: f64 = 0.0;
pub const DEFAULT_SCALE_HEIGHT: f64 = 8500.0;
pub const TYPICAL_DRAG_COEFFICIENT: f64 = 2.2;
pub fn estimate_lifetime(
r0: &Vector3,
v0: &Vector3,
ballistic_coeff: f64,
terminal_altitude: f64,
max_time: f64,
initial_time_step: f64,
) -> PoliastroResult<f64> {
if ballistic_coeff <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"ballistic_coeff",
ballistic_coeff,
"must be positive",
));
}
if terminal_altitude < 0.0 {
return Err(PoliastroError::invalid_parameter(
"terminal_altitude",
terminal_altitude,
"must be non-negative",
));
}
if max_time <= 0.0 {
return Err(PoliastroError::invalid_parameter(
"max_time",
max_time,
"must be positive",
));
}
let r0_mag = r0.norm();
let h0 = r0_mag - R_EARTH;
if h0 < terminal_altitude {
return Err(PoliastroError::invalid_state(
format!(
"Initial altitude ({:.1} km) is already below terminal altitude ({:.1} km)",
h0 / 1000.0,
terminal_altitude / 1000.0
),
));
}
let mut r = *r0;
let mut v = *v0;
let mut time = 0.0;
let mut dt = initial_time_step;
let rho0 = DEFAULT_RHO0;
let h_ref = DEFAULT_H0;
let H = DEFAULT_SCALE_HEIGHT;
let get_adaptive_dt = |altitude: f64| -> f64 {
if altitude > 600_000.0 {
86400.0 } else if altitude > 400_000.0 {
3600.0 } else if altitude > 200_000.0 {
600.0 } else if altitude > 150_000.0 {
60.0 } else {
10.0 }
};
while time < max_time {
let r_mag = r.norm();
let altitude = r_mag - R_EARTH;
if altitude < terminal_altitude {
return Ok(time);
}
dt = get_adaptive_dt(altitude).min(max_time - time);
let a_drag1 = drag_acceleration(&r, &v, R_EARTH, rho0, h_ref, ballistic_coeff);
let a_j2_1 = j2_perturbation(&r, GM_EARTH, J2_EARTH, R_EARTH);
let a_gravity1 = r.scale(-GM_EARTH / (r_mag * r_mag * r_mag));
let a1 = a_gravity1 + a_drag1 + a_j2_1;
let k1_r = v;
let k1_v = a1;
let r2 = r + k1_r.scale(dt / 2.0);
let v2 = v + k1_v.scale(dt / 2.0);
let r2_mag = r2.norm();
let a_drag2 = drag_acceleration(&r2, &v2, R_EARTH, rho0, h_ref, ballistic_coeff);
let a_j2_2 = j2_perturbation(&r2, GM_EARTH, J2_EARTH, R_EARTH);
let a_gravity2 = r2.scale(-GM_EARTH / (r2_mag * r2_mag * r2_mag));
let a2 = a_gravity2 + a_drag2 + a_j2_2;
let k2_r = v2;
let k2_v = a2;
let r3 = r + k2_r.scale(dt / 2.0);
let v3 = v + k2_v.scale(dt / 2.0);
let r3_mag = r3.norm();
let a_drag3 = drag_acceleration(&r3, &v3, R_EARTH, rho0, h_ref, ballistic_coeff);
let a_j2_3 = j2_perturbation(&r3, GM_EARTH, J2_EARTH, R_EARTH);
let a_gravity3 = r3.scale(-GM_EARTH / (r3_mag * r3_mag * r3_mag));
let a3 = a_gravity3 + a_drag3 + a_j2_3;
let k3_r = v3;
let k3_v = a3;
let r4 = r + k3_r.scale(dt);
let v4 = v + k3_v.scale(dt);
let r4_mag = r4.norm();
let a_drag4 = drag_acceleration(&r4, &v4, R_EARTH, rho0, h_ref, ballistic_coeff);
let a_j2_4 = j2_perturbation(&r4, GM_EARTH, J2_EARTH, R_EARTH);
let a_gravity4 = r4.scale(-GM_EARTH / (r4_mag * r4_mag * r4_mag));
let a4 = a_gravity4 + a_drag4 + a_j2_4;
let k4_r = v4;
let k4_v = a4;
r += (k1_r + k2_r.scale(2.0) + k3_r.scale(2.0) + k4_r).scale(dt / 6.0);
v += (k1_v + k2_v.scale(2.0) + k3_v.scale(2.0) + k4_v).scale(dt / 6.0);
time += dt;
}
Err(PoliastroError::convergence_failure(
"lifetime estimation",
0,
terminal_altitude,
))
}
pub fn estimate_decay_rate(altitude: f64, ballistic_coeff: f64) -> f64 {
let r = R_EARTH + altitude;
let v_circ = (GM_EARTH / r).sqrt();
let rho = DEFAULT_RHO0 * (-(altitude - DEFAULT_H0) / DEFAULT_SCALE_HEIGHT).exp();
let a_drag_mag = 0.5 * rho * v_circ * v_circ * ballistic_coeff;
let a = r;
let da_dt = 2.0 * a * a_drag_mag / v_circ;
-da_dt * 86400.0
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_decay_rate_higher_altitude_slower() {
let B = 0.01;
let rate_200km = estimate_decay_rate(200_000.0, B);
let rate_400km = estimate_decay_rate(400_000.0, B);
let rate_600km = estimate_decay_rate(600_000.0, B);
assert!(rate_200km < 0.0);
assert!(rate_400km < 0.0);
assert!(rate_600km < 0.0);
assert!(rate_200km.abs() > rate_400km.abs());
assert!(rate_400km.abs() > rate_600km.abs());
}
#[test]
fn test_decay_rate_higher_ballistic_coeff_faster() {
let altitude = 400_000.0;
let rate_low_B = estimate_decay_rate(altitude, 0.001);
let rate_high_B = estimate_decay_rate(altitude, 0.1);
assert!(rate_low_B < 0.0);
assert!(rate_high_B < 0.0);
assert!(rate_high_B.abs() > rate_low_B.abs());
}
#[test]
fn test_decay_rate_scaling() {
let altitude = 400_000.0;
let B1 = 0.01;
let B2 = 0.02;
let rate1 = estimate_decay_rate(altitude, B1);
let rate2 = estimate_decay_rate(altitude, B2);
assert_relative_eq!(rate2 / rate1, 2.0, epsilon = 0.01);
}
#[test]
#[ignore] fn test_lifetime_estimation_basic() {
let r0 = Vector3::new(R_EARTH + 150_000.0, 0.0, 0.0);
let v_circ = (GM_EARTH / r0.norm()).sqrt();
let v0 = Vector3::new(0.0, v_circ, 0.0);
let B = 0.5;
let lifetime = estimate_lifetime(
&r0,
&v0,
B,
100_000.0, 10.0 * 86400.0, 10.0, ).unwrap();
assert!(lifetime > 0.0);
assert!(lifetime < 10.0 * 86400.0);
let lifetime_hours = lifetime / 3600.0;
assert!(lifetime_hours > 0.1); assert!(lifetime_hours < 240.0); }
#[test]
#[ignore] fn test_lifetime_higher_orbit_longer() {
let B = 0.1;
let r1 = Vector3::new(R_EARTH + 140_000.0, 0.0, 0.0);
let v1 = Vector3::new(0.0, (GM_EARTH / r1.norm()).sqrt(), 0.0);
let r2 = Vector3::new(R_EARTH + 160_000.0, 0.0, 0.0);
let v2 = Vector3::new(0.0, (GM_EARTH / r2.norm()).sqrt(), 0.0);
let lifetime1 = estimate_lifetime(&r1, &v1, B, 100_000.0, 10.0 * 86400.0, 10.0).unwrap();
let lifetime2 = estimate_lifetime(&r2, &v2, B, 100_000.0, 20.0 * 86400.0, 10.0).unwrap();
assert!(lifetime2 > lifetime1);
assert!(lifetime1 < 10.0 * 86400.0);
assert!(lifetime2 < 20.0 * 86400.0);
}
#[test]
fn test_lifetime_errors() {
let r0 = Vector3::new(R_EARTH + 400_000.0, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7670.0, 0.0);
let result = estimate_lifetime(&r0, &v0, -0.01, 100_000.0, 86400.0, 60.0);
assert!(result.is_err());
let result = estimate_lifetime(&r0, &v0, 0.01, -100_000.0, 86400.0, 60.0);
assert!(result.is_err());
let r_low = Vector3::new(R_EARTH + 50_000.0, 0.0, 0.0);
let result = estimate_lifetime(&r_low, &v0, 0.01, 100_000.0, 86400.0, 60.0);
assert!(result.is_err());
}
#[test]
fn test_lifetime_zero_ballistic_coeff_error() {
let r0 = Vector3::new(R_EARTH + 400_000.0, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7670.0, 0.0);
let result = estimate_lifetime(&r0, &v0, 0.0, 100_000.0, 86400.0, 60.0);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("ballistic_coeff"));
}
#[test]
fn test_lifetime_zero_max_time_error() {
let r0 = Vector3::new(R_EARTH + 400_000.0, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7670.0, 0.0);
let result = estimate_lifetime(&r0, &v0, 0.01, 100_000.0, 0.0, 60.0);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("max_time"));
}
#[test]
fn test_lifetime_altitude_just_below_terminal() {
let r0 = Vector3::new(R_EARTH + 99_000.0, 0.0, 0.0); let v0 = Vector3::new(0.0, 7670.0, 0.0);
let result = estimate_lifetime(&r0, &v0, 0.01, 100_000.0, 86400.0, 60.0);
assert!(result.is_err());
}
#[test]
fn test_decay_rate_very_low_altitude() {
let B = 0.01;
let rate = estimate_decay_rate(100_000.0, B);
assert!(rate < -0.01); }
#[test]
fn test_decay_rate_very_high_altitude() {
let B = 0.01;
let rate = estimate_decay_rate(1_000_000.0, B);
assert!(rate > -1e-6); assert!(rate < 0.0); }
#[test]
fn test_decay_rate_zero_ballistic_coeff() {
let rate = estimate_decay_rate(400_000.0, 0.0);
assert_relative_eq!(rate, 0.0, epsilon = 1e-15);
}
#[test]
fn test_decay_rate_altitude_scaling() {
let B = 0.01;
let rate_300 = estimate_decay_rate(300_000.0, B);
let rate_400 = estimate_decay_rate(400_000.0, B);
let rate_500 = estimate_decay_rate(500_000.0, B);
assert!(rate_300 < 0.0);
assert!(rate_400 < 0.0);
assert!(rate_500 < 0.0);
let ratio_1 = rate_300 / rate_400;
let ratio_2 = rate_400 / rate_500;
assert!(ratio_1 > 10.0);
assert!(ratio_2 > 10.0);
}
#[test]
fn test_decay_rate_ballistic_coeff_range() {
let altitude = 400_000.0;
let B_cubesat = 0.001; let rate_cubesat = estimate_decay_rate(altitude, B_cubesat);
let B_solar = 0.05; let rate_solar = estimate_decay_rate(altitude, B_solar);
let B_balloon = 0.2; let rate_balloon = estimate_decay_rate(altitude, B_balloon);
assert!(rate_cubesat < 0.0);
assert!(rate_solar < 0.0);
assert!(rate_balloon < 0.0);
assert!(rate_balloon.abs() > rate_solar.abs());
assert!(rate_solar.abs() > rate_cubesat.abs());
}
#[test]
fn test_decay_rate_consistency() {
let B = 0.01;
let h1 = 350_000.0;
let h2 = 351_000.0;
let rate1 = estimate_decay_rate(h1, B);
let rate2 = estimate_decay_rate(h2, B);
assert!(rate1 < 0.0);
assert!(rate2 < 0.0);
assert!(rate1.abs() > rate2.abs());
let relative_diff = (rate1 - rate2).abs() / rate1.abs();
assert!(relative_diff < 0.15); }
#[test]
fn test_constants() {
assert_eq!(DEFAULT_TERMINAL_ALTITUDE, 100_000.0);
assert_eq!(DEFAULT_RHO0, 1.225);
assert_eq!(DEFAULT_H0, 0.0);
assert_eq!(DEFAULT_SCALE_HEIGHT, 8500.0);
assert_eq!(TYPICAL_DRAG_COEFFICIENT, 2.2);
assert!(DEFAULT_SCALE_HEIGHT > 0.0);
assert!(DEFAULT_RHO0 > 0.0);
assert!(DEFAULT_TERMINAL_ALTITUDE > 0.0);
assert!(DEFAULT_TERMINAL_ALTITUDE < 200_000.0); }
#[test]
fn test_decay_rate_iss_altitude() {
let iss_altitude = 408_000.0;
let B_iss = 0.0001;
let rate = estimate_decay_rate(iss_altitude, B_iss);
assert!(rate < 0.0);
assert!(rate > -1e-4); }
#[test]
fn test_decay_rate_geo_altitude() {
let geo_altitude = 36_000_000.0;
let B = 0.01;
let rate = estimate_decay_rate(geo_altitude, B);
assert_relative_eq!(rate, 0.0, epsilon = 1e-15);
}
#[test]
#[ignore] fn test_lifetime_extreme_parameters() {
let r0 = Vector3::new(R_EARTH + 110_000.0, 0.0, 0.0); let v_circ = (GM_EARTH / r0.norm()).sqrt();
let v0 = Vector3::new(0.0, v_circ, 0.0);
let B = 1.0;
let lifetime = estimate_lifetime(
&r0,
&v0,
B,
100_000.0, 3600.0, 1.0, );
assert!(lifetime.is_ok());
let time = lifetime.unwrap();
assert!(time > 0.0);
assert!(time < 3600.0); }
#[test]
fn test_lifetime_max_time_exceeded() {
let r0 = Vector3::new(R_EARTH + 400_000.0, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7670.0, 0.0);
let result = estimate_lifetime(&r0, &v0, 0.001, 100_000.0, 1.0, 1.0);
assert!(result.is_err());
}
#[test]
fn test_lifetime_negative_terminal_altitude() {
let r0 = Vector3::new(R_EARTH + 400_000.0, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7670.0, 0.0);
let result = estimate_lifetime(&r0, &v0, 0.01, -1000.0, 86400.0, 600.0);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("terminal_altitude"));
}
#[test]
fn test_lifetime_validation_checks() {
let r0 = Vector3::new(R_EARTH + 400_000.0, 0.0, 0.0);
let v0 = Vector3::new(0.0, 7670.0, 0.0);
let result = estimate_lifetime(&r0, &v0, -0.01, 100_000.0, 86400.0, 600.0);
assert!(result.is_err());
let result = estimate_lifetime(&r0, &v0, 0.01, 100_000.0, -100.0, 600.0);
assert!(result.is_err());
}
}