Skip to main content

deep_time/
scale.rs

1use core::fmt;
2
3/// Time scales supported for conversions.
4///
5/// This `#[non_exhaustive]` enum defines the complete set of time scales used by
6/// the library for representing instants (`Epoch`) and performing conversions
7/// between them.
8///
9/// It covers atomic, dynamical, coordinate, civil/coordinated, GNSS, and emerging
10/// lunar scales, plus a `Custom` variant for mission-specific or experimental use.
11///
12/// ## Overview
13///
14/// Time scales fall into several broad categories:
15///
16/// - **Atomic / proper time scales**: TAI (basis), TT, TDB/ET — continuous and
17///   suitable for internal representation and dynamical modeling.
18/// - **Coordinate time scales** (relativistic): TCG, TCB, **TCL** — defined in
19///   specific reference frames (GCRS, BCRS, LCRS). Ideal for ephemeris
20///   integration and high-accuracy modeling; not directly realized by clocks.
21/// - **Coordinated / civil scales**: UTC (atomic time with leap seconds inserted
22///   to keep it close to UT1), **UT1** (observed Earth rotation angle — does **not**
23///   use leap seconds), and the lunar operational scale **LTC** (uses defined
24///   secular rate offsets for traceability and cislunar operations).
25/// - **GNSS / navigation scales**: GPS, GST, BDT, QZSS — tied to specific
26///   satellite constellations.
27/// - **Custom**: User-defined scales, most powerful when combined with a
28///   `ClockModel` (self-describing polynomial drift, bias, etc.).
29///
30/// The default variant is [`TAI`], which serves as the internal canonical
31/// representation in many high-precision time libraries because it is
32/// continuous and forms the foundation for most conversions.
33///
34/// The library's epoch when performing conversions between all scales is
35/// 2000-01-01 noon.
36///
37/// ## Lunar Time Scales (LTC and TCL)
38///
39/// The library provides high-accuracy implementations of both lunar time scales
40/// based on the **LTE440** model (Lu et al. 2025, A&A 704, A76):
41///
42/// - [`LTC`] (Coordinated Lunar Time): Applies the secular rate offset
43///   (`L_M ≈ +56.02 µs/day`) **plus** the 13 dominant periodic terms from LTE440.
44///   Conversions use fixed-point iteration for numerical stability.
45///   Achieves sub-nanosecond accuracy (< 0.15 ns before 2050) when the periodic
46///   terms are included.
47///
48/// - [`TCL`] (Lunar Coordinate Time): IAU-defined relativistic coordinate time
49///   in the LCRS. The implementation includes the secular rate vs TDB, the same
50///   LTE440 periodic terms, and a constant bias calibrated so that the model
51///   exactly reproduces the official LTE440 reference value at J2000.0 TDB.
52///   Inverse conversion also uses fixed-point iteration.
53///
54/// Periodic corrections are applied directly in the `Scale` conversions (not
55/// delegated to `ClockModel`). This gives users accurate results out of the box
56/// for both operational (`LTC`) and coordinate (`TCL`) lunar timekeeping.
57///
58/// See the documentation on the individual variants for rates, historical
59/// models, and conversion notes.
60///
61/// ## Features
62///
63/// - `serde` — full serialization/deserialization support.
64/// - `js` — TypeScript definitions via `tsify`.
65///
66/// ## Non-exhaustive
67///
68/// The enum is marked `#[non_exhaustive]` so new scales can be added in
69/// future minor versions without breaking changes.
70#[non_exhaustive]
71#[repr(u8)]
72#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
73#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
74#[cfg_attr(feature = "js", derive(tsify::Tsify))]
75pub enum Scale {
76    /// TAI is the representation of an Epoch internally.
77    #[default]
78    TAI,
79
80    /// Terrestrial Time (TT) (previously called Terrestrial Dynamical Time (TDT)).
81    TT,
82
83    /// Ephemeris Time as defined by NASA/NAIF SPICE (identical to TDB).
84    ET,
85
86    /// Barycentric Dynamical Time (TDB) — SPICE ephemeris time (ET is an alias for this).
87    TDB,
88
89    /// Universal Coordinated Time using modern IERS leap second rules.
90    UTC,
91
92    /// UT1 — Universal Time based on the Earth’s rotation (observed, not uniform).
93    /// - Conversions to and from this scale must be performed using
94    ///   [`Dt::to_offset_by_bop`] and [`Dt::from_offset_by_bop`] with the `"bop"`
95    ///   feature enabled.
96    /// - See [`deep_time::BopData`] for more information.
97    UT1,
98
99    /// Universal Coordinated Time using the SPICE historical model
100    /// (fixed +9 s offset against TAI for all dates before 1972-01-01).
101    UTCSpice,
102
103    /// Universal Coordinated Time using the full SOFA historical model
104    /// (varying fractional "rubber second" offsets from 1960–1971).
105    UTCSofa,
106
107    /// GPS Time scale whose reference epoch is UTC midnight between 05 January and
108    /// 06 January 1980.
109    GPS,
110
111    /// Galileo Time scale.
112    GST,
113
114    /// BeiDou Time scale.
115    BDT,
116
117    /// QZSS Time scale has the same properties as GPS but with dedicated clocks.
118    QZSS,
119
120    /// **Geocentric Coordinate Time (TCG)** – relativistic coordinate time in the
121    /// Geocentric Celestial Reference System (GCRS).
122    TCG,
123
124    /// **Barycentric Coordinate Time (TCB)** – relativistic coordinate time in the
125    /// Barycentric Celestial Reference System (BCRS).
126    TCB,
127
128    /// **Coordinated Lunar Time (LTC)** – NASA’s operational lunar time scale
129    /// for Artemis and cislunar operations (based on the NIST/Ashby & Patla
130    /// relativistic framework).
131    ///
132    /// Implements the full **LTE440** model (Lu et al. 2025):
133    /// - Secular rate: **+56.02 µs per Earth day** (`L_M = 6.48378 × 10^{-10}`)
134    ///   relative to terrestrial time.
135    /// - Plus the 13 dominant periodic terms (> 1 µs amplitude) from the LTE440
136    ///   ephemeris.
137    LTC,
138
139    /// **Lunar Coordinate Time (TCL)** – IAU-defined (2024 Resolution II)
140    /// relativistic coordinate time in the Lunar Celestial Reference System (LCRS).
141    ///
142    /// Directly analogous to **TCG**. This is the theoretical coordinate time
143    /// at the Moon’s center of mass.
144    ///
145    /// The implementation follows the **LTE440** model (Lu et al. 2025):
146    /// - Secular rate vs TDB (`L_D^M`).
147    /// - The same 13-term LTE440 periodic series used for LTC.
148    /// - A constant bias (`TCL_TDB_BIAS_SPAN`) calibrated so the model exactly
149    ///   reproduces the published LTE440 reference value at J2000.0 TDB.
150    TCL,
151
152    /// **Custom / user-defined type** – for experimental or mission-specific timescales.
153    /// Most powerful when paired with `ClockModel` (self-describing polynomial).
154    Custom,
155}
156
157impl Scale {
158    /// Returns `true` if this scale is TAI.
159    #[inline]
160    pub const fn is_tai(&self) -> bool {
161        matches!(self, Self::TAI)
162    }
163
164    /// Converts this [`Scale`] to UTC.
165    /// - If the scale is already one of the UTC variants
166    ///   including historical UTC then no change occurs.
167    #[inline]
168    pub const fn to_ut(&self) -> Self {
169        if self.uses_leap_seconds() {
170            *self
171        } else {
172            Scale::UTC
173        }
174    }
175
176    /// Returns `true` if this scale accounts for leap seconds
177    /// (or historical UTC civil time rules).
178    #[inline]
179    pub const fn uses_leap_seconds(&self) -> bool {
180        matches!(self, Self::UTC | Self::UTCSpice | Self::UTCSofa)
181    }
182
183    /// Returns `true` if this scale is based off a GNSS constellation.
184    #[inline]
185    pub const fn is_gnss(&self) -> bool {
186        matches!(self, Self::GPS | Self::GST | Self::BDT | Self::QZSS)
187    }
188
189    /// Parse scale from abbreviation.
190    /// Returns `None` for any non-ASCII input.
191    pub fn from_abbrev(s: &str) -> Option<Self> {
192        let bytes = s.as_bytes();
193        if !bytes.is_ascii() {
194            return None;
195        }
196        let mut buf = [0u8; 8];
197        let mut len = 0;
198        for &byte in bytes {
199            if len >= 8 {
200                return None;
201            }
202            buf[len] = if byte.is_ascii_lowercase() {
203                byte - 32
204            } else {
205                byte
206            };
207            len += 1;
208        }
209        let upper = core::str::from_utf8(&buf[..len]).ok()?;
210        match upper {
211            "TAI" => Some(Self::TAI),
212            "TT" => Some(Self::TT),
213            "ET" => Some(Self::ET),
214            "TDB" => Some(Self::TDB),
215            "UTC" => Some(Self::UTC),
216            "UT1" => Some(Self::UT1),
217            "UTCSPICE" => Some(Self::UTCSpice),
218            "UTCSOFA" => Some(Self::UTCSofa),
219            "GPS" => Some(Self::GPS),
220            "GST" => Some(Self::GST),
221            "BDT" => Some(Self::BDT),
222            "QZSS" => Some(Self::QZSS),
223            "TCG" => Some(Self::TCG),
224            "TCB" => Some(Self::TCB),
225            "LTC" => Some(Self::LTC),
226            "TCL" => Some(Self::TCL),
227            "CUSTOM" => Some(Self::Custom),
228            _ => None,
229        }
230    }
231
232    /// Short abbreviation used for formatting / display (e.g. "TAI", "UTC", "UTCSpice").
233    pub const fn abbrev(&self) -> &'static str {
234        match self {
235            Self::TAI => "TAI",
236            Self::TT => "TT",
237            Self::ET => "ET",
238            Self::TDB => "TDB",
239            Self::UTC => "UTC",
240            Self::UT1 => "UT1",
241            Self::UTCSpice => "UTCSPICE",
242            Self::UTCSofa => "UTCSOFA",
243            Self::TCG => "TCG",
244            Self::TCB => "TCB",
245            Self::GPS => "GPS",
246            Self::GST => "GST",
247            Self::BDT => "BDT",
248            Self::QZSS => "QZSS",
249            Self::LTC => "LTC",
250            Self::TCL => "TCL",
251            Self::Custom => "CUSTOM",
252        }
253    }
254
255    /// Const-friendly equality comparison.
256    #[inline]
257    pub const fn eq(self, other: Self) -> bool {
258        self.to_wire_byte() == other.to_wire_byte()
259    }
260
261    /// Size of the canonical wire representation in bytes.
262    pub const WIRE_SIZE: usize = 1;
263
264    /// Attempts to reconstruct a `Scale` from its wire byte representation.
265    ///
266    /// Returns `None` for any value that does not correspond to a known variant.
267    /// This provides safe deserialization from untrusted sources.
268    pub const fn from_u8(v: u8) -> Self {
269        match v {
270            0 => Self::TAI,
271            1 => Self::TT,
272            2 => Self::ET,
273            3 => Self::TDB,
274            4 => Self::UTC,
275            5 => Self::UT1,
276            6 => Self::UTCSpice,
277            7 => Self::UTCSofa,
278            8 => Self::GPS,
279            9 => Self::GST,
280            10 => Self::BDT,
281            11 => Self::QZSS,
282            12 => Self::TCG,
283            13 => Self::TCB,
284            14 => Self::LTC,
285            15 => Self::TCL,
286            _ => Self::Custom,
287        }
288    }
289
290    /// Returns the wire representation of this `Scale` as a single byte.
291    ///
292    /// The returned byte is the `repr(u8)` discriminant of the enum.
293    /// This is the canonical on-wire form used by [`Dt`] and [`ClockModel`].
294    #[inline]
295    pub const fn to_wire_byte(self) -> u8 {
296        self as u8
297    }
298}
299
300impl fmt::Display for Scale {
301    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302        f.write_str(self.abbrev())
303    }
304}