pub mod coefficients;
pub mod geo;
mod model;
use arika::earth::geodetic::Geodetic;
use arika::epoch::{Epoch, Utc};
use crate::space_weather::SpaceWeatherProvider;
use crate::{AtmosphereInput, AtmosphereModel};
#[derive(Debug, Clone)]
pub struct Nrlmsise00Output {
pub temp_exo: f64,
pub temp_alt: f64,
pub density_he: f64,
pub density_o: f64,
pub density_n2: f64,
pub density_o2: f64,
pub density_ar: f64,
pub density_h: f64,
pub density_n: f64,
pub density_anomalous_o: f64,
pub total_mass_density: f64,
}
#[derive(Debug, Clone)]
pub struct Nrlmsise00Input {
pub day_of_year: u32,
pub ut_seconds: f64,
pub altitude_km: f64,
pub latitude_deg: f64,
pub longitude_deg: f64,
pub local_solar_time_hours: f64,
pub f107_daily: f64,
pub f107_avg: f64,
pub ap_daily: f64,
pub ap_array: [f64; 7],
}
pub struct Nrlmsise00<P: SpaceWeatherProvider> {
weather: P,
}
impl<P: SpaceWeatherProvider> Nrlmsise00<P> {
pub fn new(weather: P) -> Self {
Self { weather }
}
pub fn calculate(&self, input: &Nrlmsise00Input) -> Nrlmsise00Output {
let (d, temp_exo, temp_alt) = model::compute(input);
Nrlmsise00Output {
temp_exo,
temp_alt,
density_he: d[0],
density_o: d[1],
density_n2: d[2],
density_o2: d[3],
density_ar: d[4],
density_h: d[6],
density_n: d[7],
density_anomalous_o: d[8],
total_mass_density: d[5] * 1000.0,
}
}
pub fn density_with_composition(
&self,
geodetic: &Geodetic,
epoch: &Epoch<Utc>,
) -> Nrlmsise00Output {
let lat_deg = geodetic.latitude.to_degrees();
let lon_deg = geodetic.longitude.to_degrees();
let (doy, ut_seconds) = geo::epoch_to_day_of_year_and_ut(epoch);
let lst = geo::local_solar_time(ut_seconds, lon_deg, epoch);
let sw = self.weather.get(epoch);
let input = Nrlmsise00Input {
day_of_year: doy,
ut_seconds,
altitude_km: geodetic.altitude,
latitude_deg: lat_deg,
longitude_deg: lon_deg,
local_solar_time_hours: lst,
f107_daily: sw.f107_daily,
f107_avg: sw.f107_avg,
ap_daily: sw.ap_daily,
ap_array: sw.ap_3hour_history,
};
self.calculate(&input)
}
#[deprecated(note = "Use density_with_composition(&Geodetic, &Epoch<Utc>) instead")]
pub fn density_with_composition_simple_eci(
&self,
altitude_km: f64,
position_eci: &arika::SimpleEci,
epoch: &Epoch<Utc>,
) -> Nrlmsise00Output {
let (lat_deg, lon_deg) = geo::simple_eci_to_geodetic_latlon(position_eci, epoch);
let geodetic = Geodetic {
latitude: lat_deg.to_radians(),
longitude: lon_deg.to_radians(),
altitude: altitude_km,
};
self.density_with_composition(&geodetic, epoch)
}
}
impl<P: SpaceWeatherProvider> AtmosphereModel for Nrlmsise00<P> {
fn density(&self, input: &AtmosphereInput<'_>) -> f64 {
self.density_with_composition(&input.geodetic, input.utc)
.total_mass_density
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ConstantWeather;
#[test]
fn density_via_eci_matches_direct_input_at_high_latitude() {
let epoch = Epoch::from_gregorian(2024, 3, 20, 12, 0, 0.0);
let f107 = 150.0;
let ap = 15.0;
let model = Nrlmsise00::new(Box::new(ConstantWeather::new(f107, ap)));
let lat_deg: f64 = 51.6; let lon_deg: f64 = 30.0;
let alt_km: f64 = 400.0;
let (doy, ut_sec) = geo::epoch_to_day_of_year_and_ut(&epoch);
let lst = geo::local_solar_time(ut_sec, lon_deg, &epoch);
let sw = model.weather.get(&epoch);
let direct_input = Nrlmsise00Input {
day_of_year: doy,
ut_seconds: ut_sec,
altitude_km: alt_km,
latitude_deg: lat_deg,
longitude_deg: lon_deg,
local_solar_time_hours: lst,
f107_daily: sw.f107_daily,
f107_avg: sw.f107_avg,
ap_daily: sw.ap_daily,
ap_array: sw.ap_3hour_history,
};
let direct_density = model.calculate(&direct_input).total_mass_density;
let gmst = epoch.gmst();
let geod = arika::earth::Geodetic {
latitude: lat_deg.to_radians(),
longitude: lon_deg.to_radians(),
altitude: alt_km,
};
let ecef = arika::SimpleEcef::from(geod);
let eci =
arika::frame::Rotation::<arika::frame::SimpleEcef, arika::frame::SimpleEci>::from_era(
gmst,
)
.transform(&ecef);
let (rt_lat_deg, rt_lon_deg) = geo::simple_eci_to_geodetic_latlon(&eci, &epoch);
let rt_geod = Geodetic {
latitude: rt_lat_deg.to_radians(),
longitude: rt_lon_deg.to_radians(),
altitude: arika::earth::geodetic_altitude(eci.inner()),
};
let eci_density = model
.density_with_composition(&rt_geod, &epoch)
.total_mass_density;
let rel_err = (eci_density - direct_density).abs() / direct_density;
assert!(
rel_err < 0.001,
"density mismatch: direct={direct_density:.6e}, eci={eci_density:.6e}, \
rel_err={rel_err:.4e} (>0.1%)"
);
}
}