use crate::archive::atmosphere::tables::NRLMSISE_TABLE;
use affn::cartesian::Position;
use crate::coordinates::centers::Geocentric;
use crate::coordinates::frames::GCRS;
use crate::qtty::unit::Kilometer;
use crate::qtty::{KilogramsPerCubicMeter, Kilometers, Ratios};
use super::errors::DynamicsError;
pub trait DensityProvider: Send + Sync {
fn name(&self) -> &'static str;
fn density(&self, altitude: Kilometers) -> Result<KilogramsPerCubicMeter, DynamicsError>;
}
pub type AtmosphereProvider = dyn DensityProvider + Send + Sync;
#[derive(Debug, Clone, Copy)]
pub struct ExponentialAtmosphere {
pub rho0: KilogramsPerCubicMeter,
pub h0: Kilometers,
pub scale_height: Kilometers,
}
impl ExponentialAtmosphere {
pub const LEO_500KM: Self = Self {
rho0: KilogramsPerCubicMeter::new(6.967e-13),
h0: Kilometers::new(500.0),
scale_height: Kilometers::new(63.822),
};
}
impl DensityProvider for ExponentialAtmosphere {
fn name(&self) -> &'static str {
"ExponentialAtmosphere"
}
#[inline]
fn density(&self, altitude: Kilometers) -> Result<KilogramsPerCubicMeter, DynamicsError> {
let h = altitude.value();
if h < 0.0 {
return Err(DynamicsError::AltitudeBelowSurface { altitude_km: h });
}
let exponent = -(h - self.h0.value()) / self.scale_height.value();
Ok(KilogramsPerCubicMeter::new(
self.rho0.value() * exponent.exp(),
))
}
}
#[derive(Debug, Clone, Copy)]
pub struct ConstantDensity {
pub rho: KilogramsPerCubicMeter,
}
impl DensityProvider for ConstantDensity {
fn name(&self) -> &'static str {
"ConstantDensity"
}
#[inline]
fn density(&self, _altitude: Kilometers) -> Result<KilogramsPerCubicMeter, DynamicsError> {
Ok(self.rho)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Nrlmsise00LiteApprox;
impl DensityProvider for Nrlmsise00LiteApprox {
fn name(&self) -> &'static str {
"Nrlmsise00LiteApprox"
}
fn density(&self, altitude: Kilometers) -> Result<KilogramsPerCubicMeter, DynamicsError> {
let h = altitude.value();
if h < 0.0 {
return Err(DynamicsError::AltitudeBelowSurface { altitude_km: h });
}
Ok(KilogramsPerCubicMeter::new(nrlmsise_interpolate(h)))
}
}
fn nrlmsise_interpolate(h_km: f64) -> f64 {
let table = NRLMSISE_TABLE;
let n = table.len();
if h_km <= table[0].0.value() {
return table[0].1.value();
}
if h_km >= table[n - 1].0.value() {
let (h1, rho1) = (table[n - 2].0.value(), table[n - 2].1.value());
let (h2, rho2) = (table[n - 1].0.value(), table[n - 1].1.value());
let frac = (h_km - h1) / (h2 - h1);
return (rho1.ln() + frac * (rho2.ln() - rho1.ln())).exp();
}
for i in 0..n - 1 {
let (h1, rho1) = (table[i].0.value(), table[i].1.value());
let (h2, rho2) = (table[i + 1].0.value(), table[i + 1].1.value());
if h_km >= h1 && h_km <= h2 {
let frac = (h_km - h1) / (h2 - h1);
return (rho1.ln() + frac * (rho2.ln() - rho1.ln())).exp();
}
}
table[n - 1].1.value() }
pub fn geodetic_altitude(
r: &Position<Geocentric, GCRS, Kilometer>,
a_eq: Kilometers,
f_flat: Ratios,
) -> Kilometers {
let x = r.x().value();
let y = r.y().value();
let z = r.z().value();
let a = a_eq.value();
let f = f_flat.value();
let b = a * (1.0 - f);
let e2 = 2.0 * f - f * f; let ep2 = (a * a - b * b) / (b * b);
let p = (x * x + y * y).sqrt();
let mut beta = (z * a).atan2(p * b);
for _ in 0..3 {
let sb3 = beta.sin().powi(3);
let cb3 = beta.cos().powi(3);
let phi = (z + ep2 * b * sb3).atan2(p - e2 * a * cb3);
beta = ((1.0 - f) * phi.sin()).atan2(phi.cos());
}
let sb3 = beta.sin().powi(3);
let cb3 = beta.cos().powi(3);
let phi = (z + ep2 * b * sb3).atan2(p - e2 * a * cb3);
let sin_phi = phi.sin();
let cos_phi = phi.cos();
let n_roc = a / (1.0 - e2 * sin_phi * sin_phi).sqrt();
let h = if cos_phi.abs() > 1e-10 {
p / cos_phi - n_roc
} else {
z.abs() / sin_phi.abs() - n_roc * (1.0 - e2)
};
Kilometers::new(h)
}
#[cfg(test)]
mod tests {
use super::*;
use affn::cartesian::Position as AffnPos;
#[test]
fn exponential_decreases_with_altitude() {
let atm = ExponentialAtmosphere::LEO_500KM;
assert!(
atm.density(Kilometers::new(400.0)).unwrap().value()
> atm.density(Kilometers::new(500.0)).unwrap().value()
);
assert!(
atm.density(Kilometers::new(500.0)).unwrap().value()
> atm.density(Kilometers::new(600.0)).unwrap().value()
);
}
#[test]
fn exponential_400km_order_of_magnitude() {
let atm = ExponentialAtmosphere::LEO_500KM;
let rho = atm.density(Kilometers::new(400.0)).unwrap().value();
assert!(rho > 1e-13 && rho < 1e-10, "rho = {rho:.3e}");
}
#[test]
fn exponential_below_surface_returns_error() {
let atm = ExponentialAtmosphere::LEO_500KM;
let result = atm.density(Kilometers::new(-1.0));
assert!(
matches!(result, Err(DynamicsError::AltitudeBelowSurface { .. })),
"expected AltitudeBelowSurface, got {result:?}"
);
}
#[test]
fn exponential_density_is_typed() {
let atm = ExponentialAtmosphere::LEO_500KM;
let _typed: KilogramsPerCubicMeter = atm.density(Kilometers::new(400.0)).unwrap();
}
#[test]
fn constant_density_independent_of_altitude() {
let rho = KilogramsPerCubicMeter::new(1.23e-12);
let atm = ConstantDensity { rho };
assert_eq!(atm.density(Kilometers::new(0.0)).unwrap().value(), 1.23e-12);
assert_eq!(
atm.density(Kilometers::new(1_000.0)).unwrap().value(),
1.23e-12
);
}
#[test]
fn nrlmsise_400km_order_of_magnitude() {
let atm = Nrlmsise00LiteApprox;
let rho = atm.density(Kilometers::new(400.0)).unwrap().value();
assert!(rho > 1e-12 && rho < 1e-11, "400 km rho = {rho:.3e}");
}
#[test]
fn nrlmsise_decreases_with_altitude() {
let atm = Nrlmsise00LiteApprox;
let rho300 = atm.density(Kilometers::new(300.0)).unwrap().value();
let rho500 = atm.density(Kilometers::new(500.0)).unwrap().value();
let rho800 = atm.density(Kilometers::new(800.0)).unwrap().value();
assert!(rho300 > rho500 && rho500 > rho800);
}
#[test]
fn nrlmsise_below_surface_returns_error() {
let atm = Nrlmsise00LiteApprox;
let result = atm.density(Kilometers::new(-5.0));
assert!(
matches!(result, Err(DynamicsError::AltitudeBelowSurface { .. })),
"expected AltitudeBelowSurface, got {result:?}"
);
}
#[test]
fn nrlmsise_density_is_typed() {
let atm = Nrlmsise00LiteApprox;
let _typed: KilogramsPerCubicMeter = atm.density(Kilometers::new(400.0)).unwrap();
}
#[test]
fn nrlmsise_above_table_extrapolates() {
let atm = Nrlmsise00LiteApprox;
let rho = atm.density(Kilometers::new(1200.0)).unwrap().value();
assert!(rho > 0.0 && rho < 1e-14, "1200 km rho = {rho:.3e}");
}
#[test]
fn geodetic_altitude_equatorial_point() {
let r_eq = 6_378.137_f64;
let pos: AffnPos<Geocentric, GCRS, Kilometer> = AffnPos::new(r_eq, 0.0_f64, 0.0_f64);
let h = geodetic_altitude(
&pos,
Kilometers::new(6_378.137),
Ratios::new(1.0 / 298.257_223_563),
);
assert!(h.value().abs() < 0.01, "h = {:.6}", h.value());
}
#[test]
fn geodetic_altitude_leo_orbit() {
let r = 6_378.137 + 400.0;
let pos: AffnPos<Geocentric, GCRS, Kilometer> = AffnPos::new(r, 0.0_f64, 0.0_f64);
let h = geodetic_altitude(
&pos,
Kilometers::new(6_378.137),
Ratios::new(1.0 / 298.257_223_563),
);
assert!((h.value() - 400.0).abs() < 0.1, "h = {:.6}", h.value());
}
}