astrodynamics-gnss 0.9.0

GNSS domain layer (SP3, broadcast ephemeris, multi-GNSS single-point positioning, ionosphere/troposphere, DOP) built on the astrodynamics core
Documentation
//! Time-scale bridge for reduced-orbit fitting/evaluation.

use astrodynamics::time::model::TimeScale;
use astrodynamics::time::scales::{find_leap_seconds, TimeScales};

/// TT - TAI, seconds (constant).
const TT_MINUS_TAI_S: f64 = 32.184;
/// GPS / Galileo system time minus TAI, seconds.
const GPST_MINUS_TAI_S: f64 = 19.0;
/// BeiDou time minus TAI, seconds.
const BDT_MINUS_TAI_S: f64 = 33.0;

/// A UTC calendar instant `(year, month, day, hour, minute, second)`, the form
/// the core [`TimeScales::from_utc`] consumes. The Elixir layer produces these
/// from each sample/query epoch; no `Instant`->`TimeScales` bridge exists in the
/// core crate, so the calendar tuple is carried explicitly to the boundary.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CalendarEpoch {
    /// Calendar year.
    pub year: i32,
    /// Calendar month, 1-12.
    pub month: i32,
    /// Calendar day of month, 1-31.
    pub day: i32,
    /// Hour of day, 0-23.
    pub hour: i32,
    /// Minute of hour, 0-59.
    pub minute: i32,
    /// Second of minute, fractional.
    pub second: f64,
}

impl CalendarEpoch {
    /// Construct a calendar epoch from its components.
    pub const fn new(year: i32, month: i32, day: i32, hour: i32, minute: i32, second: f64) -> Self {
        Self {
            year,
            month,
            day,
            hour,
            minute,
            second,
        }
    }

    /// Build the core [`TimeScales`] for this instant, interpreted in `scale`.
    ///
    /// Non-UTC scales (GPST/GST/BDT/TAI/TT) are converted to UTC before the
    /// Skyfield split is built, so the Earth orientation used by the frame
    /// transforms is correct rather than offset by the scale's leap-second gap.
    pub(crate) fn time_scales(self, scale: TimeScale) -> TimeScales {
        let offset = scale_minus_utc_seconds(self, scale);
        TimeScales::from_utc(
            self.year,
            self.month,
            self.day,
            self.hour,
            self.minute,
            self.second - offset,
        )
    }
}

/// Seconds by which `scale` leads UTC at instant `cal` (`UTC = scale - this`).
///
/// A UTC-assumed probe only selects the (months-wide) leap-second interval; the
/// sub-minute scale offset never changes which interval applies.
fn scale_minus_utc_seconds(cal: CalendarEpoch, scale: TimeScale) -> f64 {
    if matches!(scale, TimeScale::Utc) {
        return 0.0;
    }
    let probe = TimeScales::from_utc(
        cal.year, cal.month, cal.day, cal.hour, cal.minute, cal.second,
    );
    let leap = find_leap_seconds(probe.jd_tt);
    match scale {
        TimeScale::Utc => 0.0,
        TimeScale::Tai => leap,
        TimeScale::Tt | TimeScale::Tdb => leap + TT_MINUS_TAI_S,
        TimeScale::Gpst | TimeScale::Gst => leap - GPST_MINUS_TAI_S,
        TimeScale::Bdt => leap - BDT_MINUS_TAI_S,
    }
}

/// Seconds between two calendar epochs via their J2000-TT split day numbers.
pub(crate) fn dt_seconds(t0: &TimeScales, t: &TimeScales) -> f64 {
    let dwhole = t.jd_whole - t0.jd_whole;
    let dfrac = t.tt_fraction - t0.tt_fraction;
    (dwhole + dfrac) * 86400.0
}