use arika::body::KnownBody;
use arika::earth::ellipsoid::{WGS84_A, WGS84_B};
use arika::earth::geodetic::Geodetic;
use arika::earth::{OMEGA as OMEGA_EARTH, R as R_EARTH};
use arika::epoch::Epoch;
use arika::frame::{self, Vec3};
use nalgebra::Vector3;
use tobari::{AtmosphereInput, AtmosphereModel, Exponential};
use crate::environment::EarthFrameBridge;
use crate::model::ExternalLoads;
use crate::model::{HasOrbit, Model};
use crate::orbital::OrbitalState;
pub const DEFAULT_BALLISTIC_COEFF: f64 = 0.01;
pub struct AtmosphericDrag<F: EarthFrameBridge = frame::SimpleEci> {
pub body: Option<KnownBody>,
pub body_radius: f64,
pub omega_body: f64,
pub ballistic_coeff: f64,
pub atmosphere: Box<dyn AtmosphereModel>,
pub eop: F::EopStorage,
}
impl AtmosphericDrag<frame::SimpleEci> {
pub fn for_earth(ballistic_coeff: Option<f64>) -> Self {
Self {
body: Some(KnownBody::Earth),
body_radius: R_EARTH,
omega_body: OMEGA_EARTH,
ballistic_coeff: ballistic_coeff.unwrap_or(DEFAULT_BALLISTIC_COEFF),
atmosphere: Box::new(Exponential),
eop: (),
}
}
#[deprecated(
since = "0.1.0",
note = "B* cannot be converted to physical ballistic coefficient. Use AtmosphericDrag::for_earth() instead."
)]
pub fn from_bstar(bstar: f64, body_radius: f64) -> Self {
let rho0 = 2.461e-5;
let ballistic_coeff = bstar / rho0;
Self {
body: Some(KnownBody::Earth),
body_radius,
omega_body: OMEGA_EARTH,
ballistic_coeff,
atmosphere: Box::new(Exponential),
eop: (),
}
}
}
impl<F: EarthFrameBridge> AtmosphericDrag<F> {
pub fn for_earth_in_frame(ballistic_coeff: Option<f64>, eop: F::EopStorage) -> Self {
Self {
body: Some(KnownBody::Earth),
body_radius: R_EARTH,
omega_body: OMEGA_EARTH,
ballistic_coeff: ballistic_coeff.unwrap_or(DEFAULT_BALLISTIC_COEFF),
atmosphere: Box::new(Exponential),
eop,
}
}
pub fn with_atmosphere(mut self, model: Box<dyn AtmosphereModel>) -> Self {
self.atmosphere = model;
self
}
}
impl<F: EarthFrameBridge> AtmosphericDrag<F> {
pub(crate) fn acceleration(
&self,
state: &OrbitalState<F>,
epoch: Option<&Epoch<arika::epoch::Utc>>,
) -> Vector3<f64> {
let pos = state.position();
let inside = match self.body {
Some(KnownBody::Earth) => {
let p2 = pos.x * pos.x + pos.y * pos.y;
let z2 = pos.z * pos.z;
p2 / (WGS84_A * WGS84_A) + z2 / (WGS84_B * WGS84_B) < 1.0
}
_ => pos.magnitude() < self.body_radius,
};
if inside {
return Vector3::zeros();
}
let dummy_epoch = arika::epoch::Epoch::from_jd(2451545.0); let utc = epoch.unwrap_or(&dummy_epoch);
let geodetic = match self.body {
Some(KnownBody::Earth) => {
let pos_vec = Vec3::<F>::from_raw(*pos);
F::to_geodetic(&pos_vec, utc, &self.eop)
}
_ => {
let r_mag = pos.magnitude();
Geodetic {
latitude: (pos.z / r_mag).asin(),
longitude: pos.y.atan2(pos.x),
altitude: r_mag - self.body_radius,
}
}
};
let input = AtmosphereInput { geodetic, utc };
let rho = self.atmosphere.density(&input); if rho == 0.0 {
return Vector3::zeros();
}
let omega = Vector3::new(0.0, 0.0, self.omega_body);
let v_rel = *state.velocity() - omega.cross(pos);
let v_rel_m = v_rel * 1000.0; let v_rel_mag = v_rel_m.magnitude();
if v_rel_mag < 1e-10 {
return Vector3::zeros();
}
let a_drag_m = -self.ballistic_coeff * rho * v_rel_mag * v_rel_m;
a_drag_m / 1000.0
}
}
impl<F: EarthFrameBridge, S: HasOrbit<Frame = F>> Model<S, F> for AtmosphericDrag<F> {
fn name(&self) -> &str {
"drag"
}
fn eval(&self, _t: f64, state: &S, epoch: Option<&Epoch>) -> ExternalLoads<F> {
ExternalLoads::acceleration(self.acceleration(state.orbit(), epoch))
}
}
#[cfg(test)]
mod tests {
use super::*;
use arika::earth::{MU as MU_EARTH, R as R_EARTH};
use nalgebra::vector;
fn iss_drag() -> AtmosphericDrag {
AtmosphericDrag {
body: Some(KnownBody::Earth),
body_radius: R_EARTH,
omega_body: OMEGA_EARTH,
ballistic_coeff: 0.005, atmosphere: Box::new(Exponential),
eop: (),
}
}
#[test]
fn drag_opposes_relative_velocity() {
let drag = iss_drag();
let r = R_EARTH + 400.0;
let v = (MU_EARTH / r).sqrt();
let state = OrbitalState::new(vector![r, 0.0, 0.0], vector![0.0, v, 0.0]);
let a = drag.acceleration(&state, None);
let v_rel_y = v - OMEGA_EARTH * r;
assert!(a.y < 0.0, "Drag should oppose velocity, got a.y={}", a.y);
assert!(
a.x.abs() < a.y.abs() * 1e-10,
"a.x should be ~0, got {}",
a.x
);
assert!(
a.z.abs() < a.y.abs() * 1e-10,
"a.z should be ~0, got {}",
a.z
);
assert!(
v_rel_y < v,
"Relative velocity ({v_rel_y:.4}) should be less than inertial ({v:.4})"
);
}
#[test]
fn drag_magnitude_at_iss() {
let drag = iss_drag();
let r = R_EARTH + 400.0;
let v = (MU_EARTH / r).sqrt();
let state = OrbitalState::new(vector![r, 0.0, 0.0], vector![0.0, v, 0.0]);
let a = drag.acceleration(&state, None);
let a_mag = a.magnitude();
assert!(
a_mag > 1e-11 && a_mag < 1e-7,
"ISS drag magnitude should be ~1e-10 to 1e-8 km/s², got {a_mag:.6e}"
);
}
#[test]
fn drag_increases_at_lower_altitude() {
let drag = iss_drag();
let v = 7.5;
let state_high =
OrbitalState::new(vector![R_EARTH + 600.0, 0.0, 0.0], vector![0.0, v, 0.0]);
let state_low = OrbitalState::new(vector![R_EARTH + 300.0, 0.0, 0.0], vector![0.0, v, 0.0]);
let a_high = drag.acceleration(&state_high, None).magnitude();
let a_low = drag.acceleration(&state_low, None).magnitude();
assert!(
a_low > a_high * 10.0,
"Drag at 300km ({a_low:.6e}) should be much larger than at 600km ({a_high:.6e})"
);
}
#[test]
fn no_drag_above_atmosphere() {
let drag = iss_drag();
let state = OrbitalState::new(vector![R_EARTH + 3000.0, 0.0, 0.0], vector![0.0, 5.0, 0.0]);
let a = drag.acceleration(&state, None);
assert_eq!(a, Vector3::zeros(), "No drag above atmosphere");
}
#[test]
fn for_earth_default_ballistic_coeff() {
let drag = AtmosphericDrag::for_earth(None);
assert!(
(drag.ballistic_coeff - DEFAULT_BALLISTIC_COEFF).abs() < 1e-15,
"Default should be {DEFAULT_BALLISTIC_COEFF}, got {}",
drag.ballistic_coeff
);
assert!((drag.body_radius - R_EARTH).abs() < 1e-10);
assert!((drag.omega_body - OMEGA_EARTH).abs() < 1e-15);
}
#[test]
fn for_earth_explicit_ballistic_coeff() {
let drag = AtmosphericDrag::for_earth(Some(0.005));
assert!(
(drag.ballistic_coeff - 0.005).abs() < 1e-15,
"Explicit B should be 0.005, got {}",
drag.ballistic_coeff
);
}
#[test]
fn earth_rotation_effect() {
let drag_rotating = AtmosphericDrag {
body: Some(KnownBody::Earth),
body_radius: R_EARTH,
omega_body: OMEGA_EARTH,
ballistic_coeff: 0.005,
atmosphere: Box::new(Exponential),
eop: (),
};
let drag_static = AtmosphericDrag {
body: Some(KnownBody::Earth),
body_radius: R_EARTH,
omega_body: 0.0,
ballistic_coeff: 0.005,
atmosphere: Box::new(Exponential),
eop: (),
};
let r = R_EARTH + 400.0;
let v = (MU_EARTH / r).sqrt();
let state = OrbitalState::new(
vector![r, 0.0, 0.0],
vector![0.0, v, 0.0], );
let a_rotating = drag_rotating.acceleration(&state, None).magnitude();
let a_static = drag_static.acceleration(&state, None).magnitude();
assert!(
a_rotating < a_static,
"Prograde drag with rotation ({a_rotating:.6e}) should be less than without ({a_static:.6e})"
);
}
#[test]
fn with_atmosphere_builder() {
use tobari::HarrisPriester;
let drag = AtmosphericDrag::for_earth(Some(0.005))
.with_atmosphere(Box::new(HarrisPriester::new()));
let r = R_EARTH + 400.0;
let v = (MU_EARTH / r).sqrt();
let state = OrbitalState::new(vector![r, 0.0, 0.0], vector![0.0, v, 0.0]);
let a = drag.acceleration(&state, None);
assert!(
a.magnitude() > 0.0,
"HP drag should be non-zero at ISS altitude"
);
}
}