tempoch_core/constats.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4//! Typed epoch and offset constants.
5//!
6//! Coordinate-style constants are exposed as [`Coord<S, F>`] values that
7//! carry both their **scale** (TT, TAI, UTC, UT1, …) and their **format**
8//! (JD, MJD, J2000 seconds, …) at the type level. This prevents cross-axis
9//! misuse such as feeding a UTC-axis Julian Date to a TT-tagged
10//! [`JulianDate<TT>`] constructor.
11//!
12//! Pure SI-second offsets between scales (e.g. [`TT_MINUS_TAI`]) remain
13//! plain [`qtty::Second`] values: they are scale-independent durations
14//! used as algebraic offsets, not instants.
15//!
16//! [`Coord<S, F>`]: crate::Coord
17//! [`JulianDate<TT>`]: crate::JulianDate
18
19use qtty::{Day, Second};
20
21/// Days in a Julian year (365.25 d).
22pub const JULIAN_YEAR_DAYS: Day = Day::new(365.25);
23
24use crate::coord::Coord;
25use crate::format::{J2000s, JD, MJD};
26use crate::scale::{TAI, TT, UTC};
27
28pub use crate::delta_t::DELTA_T_PREDICTION_HORIZON_MJD;
29
30/// J2000 epoch as a JD on the TT axis (`JD 2 451 545.0 TT`).
31pub const J2000_JD_TT: Coord<TT, JD> = Coord::from_raw_unchecked(Day::new(2_451_545.0));
32
33/// Offset between Julian Day and Modified Julian Day counts.
34///
35/// `MJD = JD - JD_MINUS_MJD`. Kept crate-private: external callers should
36/// use the typed conversions on [`Coord<S, JD>`] / [`Coord<S, MJD>`]
37/// instead of doing the offset arithmetic by hand.
38pub(crate) const JD_MINUS_MJD: Day = Day::new(2_400_000.5);
39
40/// Exact `TT - TAI` offset (32.184 s).
41///
42/// This is a pure SI-second offset between two coordinate scales, not an
43/// instant; it is intentionally kept as a [`qtty::Second`] for algebraic
44/// use in scale conversions.
45pub const TT_MINUS_TAI: Second = Second::new(32.184);
46
47/// Unix epoch as a JD on the UTC axis: `1970-01-01T00:00:00 UTC`.
48pub const UNIX_EPOCH_JD: Coord<UTC, JD> = Coord::from_raw_unchecked(Day::new(2_440_587.5));
49
50/// Unix epoch as an MJD on the UTC axis.
51pub const UNIX_EPOCH_MJD: Coord<UTC, MJD> = Coord::from_raw_unchecked(Day::new(40_587.0));
52
53/// GPS epoch as a JD on the UTC axis: `1980-01-06T00:00:00 UTC`.
54pub const GPS_EPOCH_JD_UTC: Coord<UTC, JD> = Coord::from_raw_unchecked(Day::new(2_444_244.5));
55
56/// Exact `TAI - UTC` offset at the GPS epoch.
57///
58/// Like [`TT_MINUS_TAI`], this is a pure SI-second offset and stays as a
59/// bare [`qtty::Second`].
60pub const GPS_EPOCH_TAI_MINUS_UTC: Second = Second::new(19.0);
61
62/// GPS epoch expressed as a JD on the TAI axis.
63///
64/// At the GPS epoch, `TAI - UTC = 19 s` exactly, so this is
65/// `GPS_EPOCH_JD_UTC + 19 s` converted to Julian days, but on the TAI axis.
66pub const GPS_EPOCH_JD_TAI: Coord<TAI, JD> = Coord::from_raw_unchecked(
67 GPS_EPOCH_JD_UTC
68 .raw()
69 .const_add(GPS_EPOCH_TAI_MINUS_UTC.to_const::<qtty::unit::Day>()),
70);
71
72/// IAU 2000 B1.9 reference epoch `T0` as a JD on the TT axis.
73pub const IAU_TIME_EPOCH_T0_JD: Coord<TT, JD> =
74 Coord::from_raw_unchecked(Day::new(2_443_144.500_372_5));
75
76/// Start of the interval where the built-in TT↔TDB truncated series achieves
77/// about 10 microseconds accuracy relative to numerical integration.
78///
79/// The seven-term Fairhead-Bretagnon truncation (USNO Circular 179, Eq. 2.27)
80/// has two distinct error budgets:
81///
82/// - **~2 µs** relative to the full Fairhead-Bretagnon (1990) series (series
83/// truncation error only).
84/// - **~10 µs** relative to JPL numerical integration (full series + modeling
85/// error combined).
86///
87/// The **end-to-end** accuracy ceiling is therefore **~10 µs**. These bounds
88/// apply within the 1600-01-01 to 2200-01-01 TT interval. This constant marks
89/// the start of that interval, corresponding approximately to 1600-01-01 TT.
90pub const TDB_TT_MODEL_HIGH_ACCURACY_START_JD: Coord<TT, JD> =
91 Coord::from_raw_unchecked(Day::new(2_305_447.5));
92
93/// End of the interval where the built-in TT↔TDB truncated series achieves
94/// about 10 microseconds accuracy relative to numerical integration.
95///
96/// See [`TDB_TT_MODEL_HIGH_ACCURACY_START_JD`] for the full accuracy breakdown.
97/// This constant corresponds approximately to 2200-01-01 TT.
98pub const TDB_TT_MODEL_HIGH_ACCURACY_END_JD: Coord<TT, JD> =
99 Coord::from_raw_unchecked(Day::new(2_524_598.5));
100
101/// GPS epoch expressed as J2000-second offset on the TAI axis.
102///
103/// The storage convention is `(JD_TAI(P) − J2000_JD_TT) × 86400`. For the GPS
104/// epoch, `JD_UTC = GPS_EPOCH_JD_UTC` and `TAI − UTC = 19 s` (exact), giving:
105///
106/// `(44_244.0 − 51_544.5) × 86400 + 19 = −630_763_181`.
107pub const GPS_EPOCH_TAI: Coord<TAI, J2000s> =
108 Coord::from_raw_unchecked(Second::new(-630_763_181.0));
109
110/// First MJD covered by the compiled UTC-TAI segment table, on the UTC axis.
111///
112/// This corresponds to 1961-01-01. UTC was defined starting from this date.
113/// For queries before this boundary, `Time<UTC>` conversions return
114/// [`crate::ConversionError::UtcBeforeDefinition`] by default. Back-extrapolation
115/// of the first segment can be enabled by building the conversion context with
116/// [`crate::TimeContext::allow_pre_definition_utc`]. The extrapolated offset is
117/// internally consistent (round-trips close) but is not a historically defined
118/// UTC-TAI value; no standard UTC existed before 1961.
119pub const UTC_DEFINED_FROM_MJD: Coord<UTC, MJD> = Coord::from_raw_unchecked(Day::new(37_300.0));
120
121/// One Julian century in days (36 525 d), used for the Fairhead–Bretagnon
122/// parameter.
123pub(crate) const DAYS_PER_JC: Day = Day::new(36_525.0);
124
125pub(crate) const UTC_INTERVAL_EPS: Day = Day::new(1e-15);
126pub(crate) const L_G: f64 = 6.969_290_134e-10;
127pub(crate) const L_B: f64 = 1.550_519_768e-8;
128pub(crate) const TDB0: Second = Second::new(-6.55e-5);
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn unix_epoch_jd_and_mjd_constants_are_consistent() {
136 assert!(
137 (UNIX_EPOCH_JD.raw() - JD_MINUS_MJD - UNIX_EPOCH_MJD.raw()).abs() < Day::new(1e-15)
138 );
139 }
140
141 #[test]
142 fn j2000_reference_values_match_known_offsets() {
143 assert!((J2000_JD_TT.raw() - JD_MINUS_MJD - Day::new(51_544.5)).abs() < Day::new(1e-12));
144 assert!((TT_MINUS_TAI - Second::new(32.184)).abs() < Second::new(1e-12));
145 assert!((UTC_DEFINED_FROM_MJD.raw() - Day::new(37_300.0)).abs() < Day::new(1e-12));
146 assert!((GPS_EPOCH_JD_UTC.raw() - Day::new(2_444_244.5)).abs() < Day::new(1e-12));
147 assert!((GPS_EPOCH_TAI_MINUS_UTC - Second::new(19.0)).abs() < Second::new(1e-12));
148 assert!(
149 (GPS_EPOCH_JD_TAI.raw()
150 - GPS_EPOCH_JD_UTC.raw()
151 - GPS_EPOCH_TAI_MINUS_UTC.to::<qtty::unit::Day>())
152 .abs()
153 < Day::new(1e-9)
154 );
155 }
156
157 #[test]
158 fn high_accuracy_model_interval_is_ordered() {
159 assert!(TDB_TT_MODEL_HIGH_ACCURACY_END_JD > TDB_TT_MODEL_HIGH_ACCURACY_START_JD);
160 assert!(GPS_EPOCH_TAI.raw().is_finite());
161 assert!(DELTA_T_PREDICTION_HORIZON_MJD.raw().is_finite());
162 }
163}