mod saastamoinen;
mod zwd;
use crate::astro::constants::time::{J2000_JD, SECONDS_PER_DAY};
use crate::astro::time::model::{Instant, InstantRepr};
use crate::error::{Error, Result};
use crate::frame::Wgs84Geodetic;
pub(crate) use saastamoinen::slant_components;
pub use zwd::{
tropo_delay_xyz as tropo_zwd_delay_xyz, zenith_wet_delay as zwd_zenith_wet_delay,
AltitudeClamp, ZwdEpoch, ZwdProfile, ZwdSlantOptions,
};
const MIN_CALENDAR_JULIAN_DATE: f64 = 1_721_425.0;
const MAX_CALENDAR_JULIAN_DATE: f64 = 5_373_485.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Met {
pub pressure_hpa: f64,
pub temperature_k: f64,
pub relative_humidity: f64,
}
impl Met {
pub fn new(pressure_hpa: f64, temperature_k: f64, relative_humidity: f64) -> Result<Self> {
validate_met_values(pressure_hpa, temperature_k, relative_humidity)?;
Ok(Self {
pressure_hpa,
temperature_k,
relative_humidity,
})
}
pub(crate) const fn new_unchecked(
pressure_hpa: f64,
temperature_k: f64,
relative_humidity: f64,
) -> Self {
Self {
pressure_hpa,
temperature_k,
relative_humidity,
}
}
pub fn standard(height_m: f64, relative_humidity: f64) -> Result<Self> {
validate_finite(height_m, "height_m")?;
validate_relative_humidity(relative_humidity)?;
let met = Self::standard_unchecked(height_m, relative_humidity);
validate_met(met)?;
Ok(met)
}
pub(crate) fn standard_unchecked(height_m: f64, relative_humidity: f64) -> Self {
let s = saastamoinen::standard_atmosphere(height_m, relative_humidity);
Self {
pressure_hpa: s.pressure_hpa,
temperature_k: s.temperature_k,
relative_humidity: s.relative_humidity,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TropoModel {
Saastamoinen,
ZwdAltitudeScaled(ZwdProfile),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MappingModel {
Niell,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ZenithDelay {
pub dry_m: f64,
pub wet_m: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MappingFactors {
pub dry: f64,
pub wet: f64,
}
pub fn tropo_zenith(model: TropoModel, receiver: Wgs84Geodetic, met: Met) -> Result<ZenithDelay> {
validate_receiver(receiver)?;
validate_met(met)?;
validate_tropo_model(model)?;
let delay = tropo_zenith_unchecked(model, receiver, met);
validate_finite(delay.dry_m, "zenith_dry_m")?;
validate_finite(delay.wet_m, "zenith_wet_m")?;
Ok(delay)
}
pub(crate) fn tropo_zenith_unchecked(
model: TropoModel,
receiver: Wgs84Geodetic,
met: Met,
) -> ZenithDelay {
match model {
TropoModel::Saastamoinen => {
let z = saastamoinen::zenith_delays(
met.pressure_hpa,
met.temperature_k,
met.relative_humidity,
receiver.lat_rad,
receiver.height_m,
);
ZenithDelay {
dry_m: z.zhd_m,
wet_m: z.zwd_m,
}
}
TropoModel::ZwdAltitudeScaled(profile) => {
let z = saastamoinen::zenith_delays(
met.pressure_hpa,
met.temperature_k,
met.relative_humidity,
receiver.lat_rad,
receiver.height_m,
);
ZenithDelay {
dry_m: z.zhd_m,
wet_m: zwd::zenith_wet_delay_unchecked(profile, receiver.height_m),
}
}
}
}
pub fn tropo_mapping(
model: MappingModel,
elevation_rad: f64,
receiver: Wgs84Geodetic,
epoch: Instant,
) -> Result<MappingFactors> {
validate_mapping_model(model)?;
validate_elevation(elevation_rad)?;
validate_receiver(receiver)?;
validate_instant(epoch)?;
let mapping = tropo_mapping_unchecked(model, elevation_rad, receiver, epoch);
validate_finite(mapping.dry, "mapping_dry")?;
validate_finite(mapping.wet, "mapping_wet")?;
Ok(mapping)
}
pub(crate) fn tropo_mapping_unchecked(
model: MappingModel,
elevation_rad: f64,
receiver: Wgs84Geodetic,
epoch: Instant,
) -> MappingFactors {
match model {
MappingModel::Niell => {
let doy = fractional_day_of_year(epoch);
let m = saastamoinen::niell_mapping(
elevation_rad,
receiver.lat_rad,
receiver.height_m,
doy,
);
MappingFactors {
dry: m.mh,
wet: m.mw,
}
}
}
}
pub fn tropo_slant(
elevation_rad: f64,
receiver: Wgs84Geodetic,
met: Met,
epoch: Instant,
) -> Result<f64> {
validate_elevation(elevation_rad)?;
validate_receiver(receiver)?;
validate_met(met)?;
validate_instant(epoch)?;
let delay_m = tropo_slant_unchecked(elevation_rad, receiver, met, epoch);
validate_finite(delay_m, "tropo_slant_m")?;
Ok(delay_m)
}
pub(crate) fn tropo_slant_unchecked(
elevation_rad: f64,
receiver: Wgs84Geodetic,
met: Met,
epoch: Instant,
) -> f64 {
let doy = fractional_day_of_year(epoch);
slant_components(
elevation_rad,
receiver,
met.pressure_hpa,
met.temperature_k,
met.relative_humidity,
doy,
)
.slant_m
}
fn validate_tropo_model(model: TropoModel) -> Result<()> {
match model {
TropoModel::Saastamoinen => Ok(()),
TropoModel::ZwdAltitudeScaled(profile) => zwd::validate_profile(profile),
}
}
fn validate_mapping_model(model: MappingModel) -> Result<()> {
match model {
MappingModel::Niell => Ok(()),
}
}
fn validate_met(met: Met) -> Result<()> {
validate_met_values(met.pressure_hpa, met.temperature_k, met.relative_humidity)
}
fn validate_met_values(
pressure_hpa: f64,
temperature_k: f64,
relative_humidity: f64,
) -> Result<()> {
validate_finite(pressure_hpa, "pressure_hpa")?;
if pressure_hpa <= 0.0 {
return Err(invalid_input("pressure_hpa", "not positive"));
}
validate_finite(temperature_k, "temperature_k")?;
if temperature_k <= 0.0 {
return Err(invalid_input("temperature_k", "not positive"));
}
validate_relative_humidity(relative_humidity)
}
fn validate_relative_humidity(relative_humidity: f64) -> Result<()> {
validate_finite(relative_humidity, "relative_humidity")?;
if !(0.0..=1.0).contains(&relative_humidity) {
return Err(invalid_input("relative_humidity", "out of range"));
}
Ok(())
}
fn validate_receiver(receiver: Wgs84Geodetic) -> Result<()> {
validate_finite(receiver.lat_rad, "receiver.lat_rad")?;
validate_finite(receiver.lon_rad, "receiver.lon_rad")?;
validate_finite(receiver.height_m, "receiver.height_m")?;
if !(-core::f64::consts::FRAC_PI_2..=core::f64::consts::FRAC_PI_2).contains(&receiver.lat_rad) {
return Err(invalid_input("receiver.lat_rad", "out of range"));
}
if !(-core::f64::consts::PI..=core::f64::consts::PI).contains(&receiver.lon_rad) {
return Err(invalid_input("receiver.lon_rad", "out of range"));
}
if !(saastamoinen::MET_GATE_LOW_M..=saastamoinen::MET_GATE_HI_M).contains(&receiver.height_m) {
return Err(invalid_input("receiver.height_m", "out of range"));
}
Ok(())
}
fn validate_elevation(elevation_rad: f64) -> Result<()> {
validate_finite(elevation_rad, "elevation_rad")?;
if !(0.0..=core::f64::consts::FRAC_PI_2).contains(&elevation_rad) {
return Err(invalid_input("elevation_rad", "out of range"));
}
Ok(())
}
fn validate_instant(epoch: Instant) -> Result<()> {
match epoch.repr {
InstantRepr::JulianDate(split) => {
validate_finite(split.jd_whole, "epoch.jd_whole")?;
validate_finite(split.fraction, "epoch.fraction")?;
if !(-1.0..=1.0).contains(&split.fraction) {
return Err(invalid_input("epoch.fraction", "out of range"));
}
let jd = split.jd_whole + split.fraction;
validate_finite(jd, "epoch.julian_date")?;
if !(MIN_CALENDAR_JULIAN_DATE..=MAX_CALENDAR_JULIAN_DATE).contains(&jd) {
return Err(invalid_input("epoch.julian_date", "out of range"));
}
}
InstantRepr::Nanos(_) => {}
}
Ok(())
}
fn validate_finite(value: f64, field: &'static str) -> Result<()> {
if value.is_finite() {
Ok(())
} else {
Err(invalid_input(field, "not finite"))
}
}
fn invalid_input(field: &'static str, reason: &'static str) -> Error {
Error::InvalidInput(format!("{field} {reason}"))
}
fn fractional_day_of_year(epoch: Instant) -> f64 {
let jd = match epoch.repr {
InstantRepr::JulianDate(split) => split.jd_whole + split.fraction,
InstantRepr::Nanos(nanos) => {
(nanos as f64) / 1.0e9 / SECONDS_PER_DAY + J2000_JD
}
};
let jd_midnight = jd + 0.5;
let day_floor = jd_midnight.floor();
let day_fraction = jd_midnight - day_floor;
let (year, month, day) = civil_from_jd_floor(day_floor);
let doy_integer = day_of_year(year, month, day);
doy_integer as f64 + day_fraction
}
fn civil_from_jd_floor(day_floor: f64) -> (i64, i64, i64) {
let jdn = day_floor as i64;
let l = jdn + 68_569;
let n = (4 * l) / 146_097;
let l = l - (146_097 * n + 3) / 4;
let i = (4_000 * (l + 1)) / 1_461_001;
let l = l - (1_461 * i) / 4 + 31;
let j = (80 * l) / 2_447;
let day = l - (2_447 * j) / 80;
let l = j / 11;
let month = j + 2 - 12 * l;
let year = 100 * (n - 49) + i + l;
(year, month, day)
}
fn day_of_year(year: i64, month: i64, day: i64) -> i64 {
const CUM: [i64; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
let leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
let mut doy = CUM[(month - 1) as usize] + day;
if leap && month > 2 {
doy += 1;
}
doy
}
#[cfg(all(test, sidereon_repo_tests))]
mod tests;