Skip to main content

native/sidereon_nif/src/tropo.rs

//! Rustler boundary for the `sidereon-core` tropospheric delay model.
//!
//! This module is **pure glue**: it decodes Erlang terms, calls the
//! `sidereon_core::atmosphere::troposphere` public APIs, and encodes the results back. No
//! Saastamoinen zenith formula and no Niell mapping numerics live here — those
//! are the crate's responsibility. This is the neutral-atmosphere signal delay
//! and is distinct from `Sidereon.Atmosphere` (NRLMSISE-00 mass density).
//!
//! - `tropo_zenith/5` returns the hydrostatic and wet zenith delays from
//!   supplied surface meteorology.
//! - `tropo_mapping/6` returns the Niell hydrostatic and wet mapping factors at
//!   an elevation.
//! - `tropo_slant/8` composes the zenith delays and the mapping into the full
//!   line-of-sight delay.
//!
//! Angles arrive in degrees at the boundary; the NIF converts them to the core's
//! radians. The epoch is a split Julian date used for the Niell seasonal
//! day-of-year term.

use rustler::NifResult;
use sidereon_core::astro::time::model::{Instant, JulianDateSplit, TimeScale};
use sidereon_core::atmosphere::troposphere::{
    tropo_mapping, tropo_slant, tropo_zenith, MappingModel, Met, TropoModel,
};
use sidereon_core::Wgs84Geodetic;

/// Zenith hydrostatic and wet tropospheric delays (positive meters).
///
/// The receiver geodetic latitude and ellipsoidal height set the zenith
/// hydrostatic delay's gravity correction; pressure, temperature, and humidity
/// drive the Saastamoinen formulas. Returns `{dry_m, wet_m}`.
#[rustler::nif]
fn tropo_zenith_delay(
    lat_deg: f64,
    height_m: f64,
    pressure_hpa: f64,
    temperature_k: f64,
    relative_humidity: f64,
) -> NifResult<(f64, f64)> {
    let receiver = Wgs84Geodetic::new(lat_deg.to_radians(), 0.0, height_m)
        .map_err(crate::errors::invalid_input)?;
    let met = Met::new(pressure_hpa, temperature_k, relative_humidity)
        .map_err(crate::errors::invalid_input)?;
    let z = tropo_zenith(TropoModel::Saastamoinen, receiver, met)
        .map_err(crate::errors::invalid_input)?;
    Ok((z.dry_m, z.wet_m))
}

/// Niell hydrostatic and wet mapping factors at an elevation (dimensionless).
///
/// The mapping depends on the elevation, the receiver geodetic latitude and
/// ellipsoidal height, and the fractional day-of-year taken from the epoch.
/// Returns `{dry, wet}`.
#[rustler::nif]
fn tropo_mapping_factors(
    elevation_deg: f64,
    lat_deg: f64,
    height_m: f64,
    jd_whole: f64,
    jd_fraction: f64,
) -> NifResult<(f64, f64)> {
    let receiver = Wgs84Geodetic::new(lat_deg.to_radians(), 0.0, height_m)
        .map_err(crate::errors::invalid_input)?;
    let epoch = Instant::from_julian_date(
        TimeScale::Gpst,
        JulianDateSplit::new(jd_whole, jd_fraction).map_err(crate::errors::invalid_input)?,
    );
    let m = tropo_mapping(
        MappingModel::Niell,
        elevation_deg.to_radians(),
        receiver,
        epoch,
    )
    .map_err(crate::errors::invalid_input)?;
    Ok((m.dry, m.wet))
}

/// Full slant tropospheric delay (positive meters).
///
/// Composes the Saastamoinen zenith delays with the Niell mapping. The receiver
/// geodetic latitude, longitude, and ellipsoidal height come from the first
/// three arguments; pressure, temperature, and humidity follow; the epoch sets
/// the seasonal day-of-year.
#[rustler::nif]
#[allow(clippy::too_many_arguments)]
fn tropo_slant_delay(
    elevation_deg: f64,
    lat_deg: f64,
    lon_deg: f64,
    height_m: f64,
    pressure_hpa: f64,
    temperature_k: f64,
    relative_humidity: f64,
    jd_whole: f64,
    jd_fraction: f64,
) -> NifResult<f64> {
    let receiver = Wgs84Geodetic::new(lat_deg.to_radians(), lon_deg.to_radians(), height_m)
        .map_err(crate::errors::invalid_input)?;
    let met = Met::new(pressure_hpa, temperature_k, relative_humidity)
        .map_err(crate::errors::invalid_input)?;
    let epoch = Instant::from_julian_date(
        TimeScale::Gpst,
        JulianDateSplit::new(jd_whole, jd_fraction).map_err(crate::errors::invalid_input)?,
    );
    tropo_slant(elevation_deg.to_radians(), receiver, met, epoch)
        .map_err(crate::errors::invalid_input)
}