Skip to main content

tempoch_core/model/scale/
mod.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4//! Frozen scale set.
5//!
6//! Every scale is a zero-sized marker type that identifies a scientifically
7//! distinct time axis. The [`Scale`] trait is sealed — downstream crates
8//! cannot add new scales.
9//!
10//! * Coordinate scales (`TAI`, `TT`, `TDB`, `TCG`, `TCB`, `UT1`, `UTC`)
11//!   implement [`CoordinateScale`] and support raw-axis constructors,
12//!   accessors, and instant arithmetic on `Time<S>`.
13//! * The civil scale `UTC` still does **not** implement [`ContinuousScale`]:
14//!   it shares the internal instant axis used by `TAI`, but civil labels and
15//!   leap-second interpretation remain table-driven.
16
17use crate::foundation::sealed::Sealed;
18
19pub(crate) mod conversion;
20
21/// Marker trait for a scientifically distinct time scale.
22///
23/// Sealed: implementations live in this crate only — downstream crates cannot
24/// add new scales.
25#[allow(private_bounds)]
26pub trait Scale: Sealed + Copy + Clone + core::fmt::Debug + 'static {
27    /// Display name of the scale. Used by `Debug` on `Time`.
28    const NAME: &'static str;
29}
30
31// ── Scale macros ─────────────────────────────────────────────────────────
32
33macro_rules! define_scale {
34    ($(#[$meta:meta])* $ident:ident = $name:literal) => {
35        $(#[$meta])*
36        #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
37        pub struct $ident;
38        impl Sealed for $ident {}
39        impl Scale for $ident {
40            const NAME: &'static str = $name;
41        }
42    };
43}
44
45// ── Scale definitions ────────────────────────────────────────────────────
46
47define_scale!(
48    /// Coordinated Universal Time.
49    ///
50    /// Leap-second-aware civil scale. `Time<UTC>` stores a continuous instant;
51    /// leap-second interpretation is computed on demand from the UTC-TAI
52    /// segment table. Raw-axis arithmetic acts on that stored instant.
53    ///
54    /// # Storage invariant
55    ///
56    /// `Time<UTC>` and `Time<TAI>` store **the same continuous instant** for
57    /// the same physical event. Scale conversion between them is therefore a
58    /// numeric no-op at the storage layer.
59    ///
60    /// UTC participates in [`CoordinateScale`], so `Time<UTC>` exposes the
61    /// same raw J2000/JD/MJD instant-axis helpers and second-based arithmetic
62    /// as the other built-in scales. Those operations act on the stored
63    /// continuous instant, not on a leap-second-labelled civil clock.
64    ///
65    /// UTC still does **not** implement [`ContinuousScale`]. Generic code that
66    /// wants a scale with no civil semantics should keep using that stricter
67    /// bound; generic code that merely needs a raw coordinate axis can use
68    /// [`CoordinateScale`].
69    ///
70    /// # Authoritative UTC API
71    ///
72    /// Use the civil layer for any operation that depends on the UTC-TAI
73    /// offset (i.e., leap seconds):
74    ///
75    /// * [`crate::Time::<UTC>::from_chrono`] / [`crate::Time::try_from_chrono`] / [`crate::Time::try_to_chrono`]
76    /// * Unix time: `time.try_to::<`[`crate::Unix`]`>()` returns a [`crate::UnixTime`]
77    UTC = "UTC"
78);
79
80define_scale!(
81    /// International Atomic Time. Continuous SI-second clock.
82    TAI = "TAI"
83);
84
85define_scale!(
86    /// Terrestrial Time. The dynamical reference scale in this crate.
87    ///
88    /// Related to TAI by `TT = TAI + 32.184 s` (exact).
89    TT = "TT"
90);
91
92define_scale!(
93    /// Barycentric Dynamical Time.
94    ///
95    /// Differs from TT by a modeled periodic term using the seven-term
96    /// Fairhead–Bretagnon truncation from USNO Circular 179.
97    ///
98    /// The built-in approximation is context-free because the model has no
99    /// runtime-settable parameters, but its advertised high-accuracy regime is
100    /// finite: the implementation is documented to stay within about 10 µs only
101    /// over the interval bracketed by
102    /// [`TDB_TT_MODEL_HIGH_ACCURACY_START_JD`] and
103    /// [`TDB_TT_MODEL_HIGH_ACCURACY_END_JD`] (roughly 1600-01-01 to
104    /// 2200-01-01 TT). Outside that interval conversions remain available, but
105    /// the crate does not claim microsecond-level scientific accuracy.
106    ///
107    /// [`TDB_TT_MODEL_HIGH_ACCURACY_START_JD`]: crate::foundation::constats::tdb_tt_model_high_accuracy_start_jd
108    /// [`TDB_TT_MODEL_HIGH_ACCURACY_END_JD`]: crate::foundation::constats::tdb_tt_model_high_accuracy_end_jd
109    TDB = "TDB"
110);
111
112define_scale!(
113    /// Geocentric Coordinate Time (IAU 2000 B1.9). Linear rate difference to TT.
114    TCG = "TCG"
115);
116
117define_scale!(
118    /// Barycentric Coordinate Time (IAU 2006 B3). Linear relation to TDB.
119    TCB = "TCB"
120);
121
122define_scale!(
123    /// Universal Time 1 — Earth-rotation time axis.
124    ///
125    /// Continuous in SI seconds, but `UT1 ↔ TT` requires a `TimeContext`
126    /// because the mapping depends on the compiled ΔT model (and, in future
127    /// phases, observed-ΔT data).
128    ///
129    /// # Accuracy and modeling limitations
130    ///
131    /// UT1 conversions are backed by a piecewise ΔT model:
132    ///
133    /// * **Historical (pre-1973)**: polynomial approximations (Stephenson &
134    ///   Houlden 1986; Meeus *Astronomical Algorithms*). Accuracy varies from
135    ///   ±15 s (1620–1973) to ±hundreds of seconds (pre-948).
136    /// * **Modern (1973 – horizon)**: USNO monthly determinations with linear
137    ///   interpolation. The default monthly-ΔT path differs from the bundled daily
138    ///   IERS-derived path by less than 15 ms over the compiled observed
139    ///   overlap, and by less than 0.2 s over the compiled short-range
140    ///   prediction overlap. See
141    ///   [`DELTA_T_PREDICTION_HORIZON_MJD`] for the hard stop.
142    ///
143    /// This model is suitable for archival astronomy and telescope scheduling,
144    /// but **not** for precision geodesy, VLBI, or pulsar timing, which
145    /// require daily IERS EOP (DUT1) solutions. Use
146    /// [`crate::TimeContext::with_builtin_eop`] when you want the most accurate
147    /// bundled UT1 route.
148    ///
149    /// [`DELTA_T_PREDICTION_HORIZON_MJD`]: crate::DELTA_T_PREDICTION_HORIZON_MJD
150    UT1 = "UT1"
151);
152
153define_scale!(
154    /// NAIF/SPICE Ephemeris Time — compatibility marker.
155    ///
156    /// `ET` exists so users interchanging values with NAIF/CSPICE can keep the
157    /// scale label distinct in their typed code. The current implementation
158    /// routes `ET ↔ {TT, TDB, …}` through the same Fairhead–Bretagnon model
159    /// used for TDB: numerically `Time<ET>` is identical to `Time<TDB>` within
160    /// the documented model accuracy (~10 µs over 1600–2200 TT).
161    ///
162    /// Treat this as a SPICE-compatibility *label*, not a new physical
163    /// realization. A future high-precision SPICE-equivalent ET implementation
164    /// can replace the conversion without breaking callers that already use
165    /// `Time<ET>` versus `Time<TDB>`.
166    ET = "ET"
167);
168
169define_scale!(
170    /// GPS System Time. `GPST = TAI − 19 s` (nominal, exact integer offset).
171    ///
172    /// Epoch: 1980-01-06 00:00:00 UTC. Continuous SI-second clock with no
173    /// leap seconds. This marker represents the *nominal* GPS system time,
174    /// not the broadcast/realized GPST observed by a particular receiver
175    /// (which includes ionospheric/clock model corrections that are not part
176    /// of the scale itself).
177    ///
178    /// Week-number / seconds-of-week is a *format* concern (see
179    /// `format::markers`) and is intentionally separated from scale identity.
180    GPST = "GPST"
181);
182
183define_scale!(
184    /// Galileo System Time. Nominally `GST = TAI − 19 s` (identical tick rate
185    /// and integer offset to [`GPST`] today; broadcast inter-system offset
186    /// GGTO is *not* modeled here).
187    ///
188    /// Epoch: 1999-08-22 00:00:00 UTC. See [`GPST`] notes on nominal vs.
189    /// broadcast realization.
190    GST = "GST"
191);
192
193define_scale!(
194    /// BeiDou Navigation Satellite System Time. `BDT = TAI − 33 s` (nominal,
195    /// exact integer offset). Equivalently `BDT = GPST − 14 s`.
196    ///
197    /// Epoch: 2006-01-01 00:00:00 UTC. See [`GPST`] notes on nominal vs.
198    /// broadcast realization (BeiDou/GPS offset is broadcast separately and
199    /// not part of the scale).
200    BDT = "BDT"
201);
202
203define_scale!(
204    /// Quasi-Zenith Satellite System Time. Nominally aligned with [`GPST`]
205    /// (`QZSST = TAI − 19 s`). The QZSS ICD defines QZSST as steered to GPST;
206    /// observed inter-system offsets are not part of the scale.
207    QZSST = "QZSST"
208);
209
210// ── ContinuousScale witness ──────────────────────────────────────────────
211
212/// Witness that a scale is continuous and supports direct arithmetic.
213/// `UTC` deliberately does not implement this: it has raw-axis accessors
214/// through [`CoordinateScale`], but its civil interpretation remains
215/// leap-second-aware and table-driven.
216///
217/// Sealed — downstream cannot implement it.
218#[allow(private_bounds)]
219pub trait CoordinateScale: Scale + Sealed {}
220
221macro_rules! coordinate {
222    ($($scale:ty),+ $(,)?) => {
223        $(impl CoordinateScale for $scale {})+
224    };
225}
226coordinate!(TAI, TT, TDB, TCG, TCB, UT1, UTC, ET, GPST, GST, BDT, QZSST);
227
228/// Witness that a scale is both coordinate-bearing and physically continuous.
229///
230/// Sealed — downstream cannot implement it.
231#[allow(private_bounds)]
232pub trait ContinuousScale: CoordinateScale + Sealed {}
233
234macro_rules! continuous {
235    ($($scale:ty),+ $(,)?) => {
236        $(impl ContinuousScale for $scale {})+
237    };
238}
239continuous!(TAI, TT, TDB, TCG, TCB, UT1, ET, GPST, GST, BDT, QZSST);