use super::{
DynamicsAlmanacSnafu, DynamicsAstroSnafu, DynamicsError, DynamicsPlanetarySnafu, ForceModel,
};
use crate::cosmic::{AstroPhysicsSnafu, Frame, Spacecraft};
use crate::dynamics::nrlmsise00::Nrlmsise00Flags;
use crate::dynamics::nrlmsise00::msise00_density;
use crate::io::space_weather::SpaceWeatherData;
use crate::linalg::{Matrix4x3, Vector3};
use anise::almanac::Almanac;
use anise::constants::frames::IAU_EARTH_FRAME;
use anise::errors::OrientationSnafu;
use hifitime::Unit;
use serde::{Deserialize, Serialize};
use serde_dhall::StaticType;
use snafu::ResultExt;
use std::fmt;
use std::sync::Arc;
use trig_const::{ln, sqrt};
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
use pyo3::types::PyType;
pub mod nrlmsise00;
const SIN_30_DEG: f64 = 0.5;
const COS_30_DEG: f64 = sqrt(3.0_f64) / 2.0_f64;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
struct HpNode {
alt_km: f64,
min_density_kg_m3: f64,
max_density_kg_m3: f64,
}
impl HpNode {
const fn ln_min_density(&self) -> f64 {
ln(self.min_density_kg_m3)
}
const fn ln_max_density(&self) -> f64 {
ln(self.max_density_kg_m3)
}
}
const HP_TABLE: &[HpNode] = &[
HpNode {
alt_km: 100.0,
min_density_kg_m3: 4.974e-07,
max_density_kg_m3: 4.974e-07,
},
HpNode {
alt_km: 120.0,
min_density_kg_m3: 2.490e-08,
max_density_kg_m3: 2.490e-08,
},
HpNode {
alt_km: 140.0,
min_density_kg_m3: 3.840e-09,
max_density_kg_m3: 3.840e-09,
},
HpNode {
alt_km: 160.0,
min_density_kg_m3: 1.170e-09,
max_density_kg_m3: 1.170e-09,
},
HpNode {
alt_km: 180.0,
min_density_kg_m3: 4.820e-10,
max_density_kg_m3: 5.220e-10,
},
HpNode {
alt_km: 200.0,
min_density_kg_m3: 2.260e-10,
max_density_kg_m3: 2.620e-10,
},
HpNode {
alt_km: 240.0,
min_density_kg_m3: 6.880e-11,
max_density_kg_m3: 9.380e-11,
},
HpNode {
alt_km: 280.0,
min_density_kg_m3: 2.570e-11,
max_density_kg_m3: 4.180e-11,
},
HpNode {
alt_km: 320.0,
min_density_kg_m3: 1.090e-11,
max_density_kg_m3: 2.060e-11,
},
HpNode {
alt_km: 360.0,
min_density_kg_m3: 4.980e-12,
max_density_kg_m3: 1.070e-11,
},
HpNode {
alt_km: 400.0,
min_density_kg_m3: 2.380e-12,
max_density_kg_m3: 5.820e-12,
},
HpNode {
alt_km: 440.0,
min_density_kg_m3: 1.180e-12,
max_density_kg_m3: 3.250e-12,
},
HpNode {
alt_km: 480.0,
min_density_kg_m3: 6.020e-13,
max_density_kg_m3: 1.860e-12,
},
HpNode {
alt_km: 520.0,
min_density_kg_m3: 3.150e-13,
max_density_kg_m3: 1.080e-12,
},
HpNode {
alt_km: 560.0,
min_density_kg_m3: 1.680e-13,
max_density_kg_m3: 6.400e-13,
},
HpNode {
alt_km: 600.0,
min_density_kg_m3: 9.100e-14,
max_density_kg_m3: 3.830e-13,
},
HpNode {
alt_km: 680.0,
min_density_kg_m3: 2.820e-14,
max_density_kg_m3: 1.440e-13,
},
HpNode {
alt_km: 760.0,
min_density_kg_m3: 9.200e-15,
max_density_kg_m3: 5.760e-14,
},
HpNode {
alt_km: 840.0,
min_density_kg_m3: 3.100e-15,
max_density_kg_m3: 2.400e-14,
},
HpNode {
alt_km: 920.0,
min_density_kg_m3: 1.100e-15,
max_density_kg_m3: 1.050e-14,
},
HpNode {
alt_km: 1000.0,
min_density_kg_m3: 4.000e-16,
max_density_kg_m3: 4.800e-15,
},
];
#[derive(Clone, Debug, Serialize, Deserialize, StaticType)]
#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
pub enum AtmDensity {
Constant(f64),
Exponential {
rho0_kg_m3: f64,
ref_alt_km: f64,
scale_height_km: f64,
},
StdAtm {
max_alt_km: f64,
},
NRLMSISE00 {
weather: SpaceWeatherData,
flags: Option<Nrlmsise00Flags>,
},
HarrisPriester {
n_parameter: usize,
},
}
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl AtmDensity {
#[classmethod]
fn earth_exponential(_cls: &Bound<'_, PyType>) -> Self {
AtmDensity::Exponential {
rho0_kg_m3: 3.614e-13,
ref_alt_km: 700.000,
scale_height_km: 88.667,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, StaticType)]
#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
pub struct Drag {
pub density: AtmDensity,
pub frame: Frame,
pub estimate: bool,
}
impl Drag {
pub fn earth_exp(almanac: &Almanac) -> Result<Arc<Self>, DynamicsError> {
Ok(Arc::new(Self {
density: AtmDensity::Exponential {
rho0_kg_m3: 3.614e-13,
ref_alt_km: 700.000,
scale_height_km: 88.667,
},
frame: almanac
.frame_info(IAU_EARTH_FRAME)
.context(DynamicsPlanetarySnafu {
action: "planetary data from third body not loaded",
})?,
estimate: false,
}))
}
pub fn std_atm1976(almanac: &Almanac) -> Result<Arc<Self>, DynamicsError> {
Ok(Arc::new(Self {
density: AtmDensity::StdAtm {
max_alt_km: 1_000.0,
},
frame: almanac
.frame_info(IAU_EARTH_FRAME)
.context(DynamicsPlanetarySnafu {
action: "planetary data from third body not loaded",
})?,
estimate: false,
}))
}
fn rho_kg_m3(&self, ctx: &Spacecraft, almanac: &Almanac) -> Result<f64, DynamicsError> {
let osc_drag_frame =
almanac
.transform_to(ctx.orbit, self.frame, None)
.context(DynamicsAlmanacSnafu {
action: "transforming into drag frame",
})?;
let rho_kg_m3 = match &self.density {
AtmDensity::Constant(rho) => *rho,
AtmDensity::Exponential {
rho0_kg_m3,
scale_height_km,
ref_alt_km,
} => {
let altitude_km = osc_drag_frame
.altitude_km()
.context(AstroPhysicsSnafu)
.context(DynamicsAstroSnafu)?;
rho0_kg_m3 * (-(altitude_km - ref_alt_km) / scale_height_km).exp()
}
AtmDensity::StdAtm { max_alt_km } => {
let altitude_km = osc_drag_frame
.altitude_km()
.context(AstroPhysicsSnafu)
.context(DynamicsAstroSnafu)?;
if altitude_km > *max_alt_km {
10.0_f64.powf((-7e-5) * altitude_km - 14.464)
} else {
let scale = (altitude_km - 526.8000) / 292.8563;
let logdensity =
0.34047 * scale.powi(6) - 0.5889 * scale.powi(5) - 0.5269 * scale.powi(4)
+ 1.0036 * scale.powi(3)
+ 0.60713 * scale.powi(2)
- 2.3024 * scale
- 12.575;
10.0_f64.powf(logdensity)
}
}
AtmDensity::NRLMSISE00 { weather, flags } => {
let (lat_deg, long_deg, alt_km) = osc_drag_frame
.latlongalt()
.context(AstroPhysicsSnafu)
.context(DynamicsAstroSnafu)?;
let lst_h = almanac.local_solar_time(osc_drag_frame, None).context(
DynamicsAlmanacSnafu {
action: "computing local solar time",
},
)?;
let epoch = ctx.orbit.epoch;
let sw = weather.msise_weather(epoch);
msise00_density(
sw,
lst_h.to_unit(Unit::Hour),
lat_deg,
long_deg,
alt_km,
ctx.orbit.epoch,
flags.unwrap_or_default(),
)?
.total_mass_density_kg_m3
}
AtmDensity::HarrisPriester { n_parameter } => {
let altitude_km = osc_drag_frame
.altitude_km()
.context(AstroPhysicsSnafu)
.context(DynamicsAstroSnafu)?;
if altitude_km < HP_TABLE[0].alt_km
|| altitude_km > HP_TABLE[HP_TABLE.len() - 1].alt_km
{
0.0
} else {
let idx = HP_TABLE
.windows(2)
.position(|w| altitude_km >= w[0].alt_km && altitude_km <= w[1].alt_km)
.unwrap_or(0);
let n0 = &HP_TABLE[idx];
let n1 = &HP_TABLE[idx + 1];
let h_min =
(n0.alt_km - n1.alt_km) / (n1.ln_min_density() - n0.ln_min_density());
let h_max =
(n0.alt_km - n1.alt_km) / (n1.ln_max_density() - n0.ln_max_density());
let rho_min = n0.min_density_kg_m3 * (-(altitude_km - n0.alt_km) / h_min).exp();
let rho_max = n0.max_density_kg_m3 * (-(altitude_km - n0.alt_km) / h_max).exp();
let u_sun = almanac
.sun_unit_vector(ctx.orbit.epoch, self.frame, None)
.context(DynamicsAlmanacSnafu {
action: "fetching sun position for Harris-Priester model",
})?;
let u_bulge = Vector3::new(
u_sun.x * COS_30_DEG - u_sun.y * SIN_30_DEG,
u_sun.x * SIN_30_DEG + u_sun.y * COS_30_DEG,
u_sun.z,
);
let u_pos = osc_drag_frame.r_hat();
let cos_psi = u_pos.dot(&u_bulge).clamp(-1.0, 1.0);
let cos_half_psi = ((1.0 + cos_psi) / 2.0).sqrt();
let mod_factor = cos_half_psi.powi(*n_parameter as i32);
rho_min + (rho_max - rho_min) * mod_factor
}
}
};
Ok(rho_kg_m3)
}
}
impl fmt::Display for Drag {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\tDrag density {:?} in frame {}",
self.density, self.frame
)
}
}
impl ForceModel for Drag {
fn estimation_index(&self) -> Option<usize> {
if self.estimate { Some(7) } else { None }
}
fn eom(&self, ctx: &Spacecraft, almanac: &Almanac) -> Result<Vector3<f64>, DynamicsError> {
let integration_frame = ctx.orbit.frame;
let drag_frame = almanac
.frame_info(self.frame)
.context(DynamicsPlanetarySnafu {
action: "fetching drag frame information",
})?;
let osc_drag_frame =
almanac
.transform_to(ctx.orbit, self.frame, None)
.context(DynamicsAlmanacSnafu {
action: "transforming into drag frame",
})?;
let rho_kg_m3 = self.rho_kg_m3(ctx, almanac)?;
let v_km_s = osc_drag_frame.velocity_km_s;
let accel_drag_frame_kg_km_s2 = -0.5
* 1e3
* rho_kg_m3
* ctx.drag.coeff_drag
* ctx.drag.area_m2
* v_km_s.norm()
* v_km_s;
let accel_integr_frame = almanac
.rotate(drag_frame, integration_frame, ctx.orbit.epoch)
.context(OrientationSnafu {
action: "rotating drafg force into integration frame",
})
.context(DynamicsAlmanacSnafu {
action: "rotating drag force into integration frame",
})?
* accel_drag_frame_kg_km_s2;
Ok(accel_integr_frame)
}
fn gradient(
&self,
ctx: &Spacecraft,
almanac: &Almanac,
) -> Result<(Vector3<f64>, Matrix4x3<f64>), DynamicsError> {
let dx = self.eom(ctx, almanac)?;
let mut grad = Matrix4x3::zeros();
for j in 0..3 {
let h = 6.0e-6 * ctx.orbit.radius_km[j].abs().max(1.0);
let mut ctx_plus = *ctx;
ctx_plus.orbit.radius_km[j] += h;
let f_plus = self.eom(&ctx_plus, almanac)?;
let mut ctx_minus = *ctx;
ctx_minus.orbit.radius_km[j] -= h;
let f_minus = self.eom(&ctx_minus, almanac)?;
let df_dr = (f_plus - f_minus) / (2.0 * h);
for i in 0..3 {
grad[(i, j)] = df_dr[i];
}
}
let wrt_cd = dx / ctx.drag.coeff_drag;
for j in 0..3 {
grad[(3, j)] = wrt_cd[j];
}
Ok((dx, grad))
}
}
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl Drag {
#[pyo3(signature = (density, frame, estimate=true))]
#[new]
fn py_new(density: AtmDensity, frame: Frame, estimate: bool) -> Self {
Self {
density,
frame,
estimate,
}
}
fn __str__(&self) -> String {
format!("{self}")
}
fn __repr__(&self) -> String {
format!("{self} @ {self:p}")
}
}