mod saastamoinen;
mod vmf;
mod zwd;
use crate::astro::time::civil::{
fractional_day_of_year_from_instant, julian_date_from_instant, mjd_from_jd,
};
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)]
pub enum MappingModel {
Niell,
Vmf1 {
ah: f64,
aw: f64,
},
}
#[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,
}
}
MappingModel::Vmf1 { ah, aw } => {
let mjd = modified_julian_date(epoch);
let m = vmf::vmf1_mapping(elevation_rad, receiver.lat_rad, mjd, ah, aw);
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
}
pub(crate) fn tropo_slant_with_mapping_unchecked(
model: MappingModel,
elevation_rad: f64,
receiver: Wgs84Geodetic,
met: Met,
epoch: Instant,
) -> f64 {
if elevation_rad <= 0.0
|| receiver.height_m < saastamoinen::MET_GATE_LOW_M
|| receiver.height_m > saastamoinen::MET_GATE_HI_M
{
return 0.0;
}
let zenith = tropo_zenith_unchecked(TropoModel::Saastamoinen, receiver, met);
let mapping = tropo_mapping_unchecked(model, elevation_rad, receiver, epoch);
zenith.dry_m * mapping.dry + zenith.wet_m * mapping.wet
}
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(()),
MappingModel::Vmf1 { ah, aw } => {
validate_finite(ah, "mapping.vmf1.ah")?;
if ah <= 0.0 {
return Err(invalid_input("mapping.vmf1.ah", "not positive"));
}
validate_finite(aw, "mapping.vmf1.aw")?;
if aw <= 0.0 {
return Err(invalid_input("mapping.vmf1.aw", "not positive"));
}
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 {
fractional_day_of_year_from_instant(epoch)
}
fn modified_julian_date(epoch: Instant) -> f64 {
mjd_from_jd(julian_date_from_instant(epoch))
}
#[cfg(all(test, sidereon_repo_tests))]
mod tests;