Skip to content

Commit df1dc9e

Browse files
Merge pull request #482 from nyx-space/feat/gh-291-lunar-time
Implement Lunar Time and Lunar Coordinated Time (experimental scales)
2 parents 15e36e0 + 9264aba commit df1dc9e

7 files changed

Lines changed: 282 additions & 157 deletions

File tree

README.md

Lines changed: 5 additions & 151 deletions
Large diffs are not rendered by default.

hifitime.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,7 +1635,9 @@ class TimeScale:
16351635
TAI: TimeScale = ...
16361636
TCB: TimeScale = ...
16371637
TCG: TimeScale = ...
1638+
TCL: TimeScale = ...
16381639
TDB: TimeScale = ...
1640+
TL: TimeScale = ...
16391641
TT: TimeScale = ...
16401642
UTC: TimeScale = ...
16411643

src/epoch/mod.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ use crate::duration::{Duration, Unit};
3131
#[allow(unused_imports)]
3232
use crate::errors::{DurationError, ParseSnafu};
3333
use crate::leap_seconds::{LatestLeapSeconds, LeapSecondProvider};
34+
use crate::timescale::tcl::{
35+
tcl_since_t77_to_tl_since_t77, tcl_since_t77_to_tt_since_t77, tl_since_t77_to_tcl_since_t77,
36+
tt_since_t77_to_tcl_since_t77,
37+
};
3438
use crate::{
3539
HifitimeError, MonthName, TimeScale, TimeUnits, BDT_REF_EPOCH, ET_EPOCH_S, GPST_REF_EPOCH,
3640
GST_REF_EPOCH, MJD_J1900, MJD_OFFSET, QZSST_REF_EPOCH, UNIX_REF_EPOCH,
@@ -257,6 +261,42 @@ impl Epoch {
257261

258262
tdb_since_t77 - delta_tdb_tai + TimeScale::TCB.prime_epoch_offset()
259263
}
264+
TimeScale::TCL => {
265+
// self.duration is mean TCL duration since T0.
266+
//
267+
// Exact TCL would require the LCRS/BCRS relativistic integral using the
268+
// Moon center-of-mass state and external potentials. This experimental
269+
// implementation keeps only the conventional mean TCL-TT secular rate.
270+
let tcl_since_t77 = self.duration;
271+
272+
// TCL -> TT
273+
let tt_since_t77 = tcl_since_t77_to_tt_since_t77(tcl_since_t77);
274+
275+
// Add the TT absolute reference epoch, then TT -> TAI.
276+
let tt_duration = self.time_scale.prime_epoch_offset() + tt_since_t77;
277+
tt_duration - TT_OFFSET_MS.milliseconds()
278+
}
279+
280+
TimeScale::TL => {
281+
// self.duration is TL option-iii duration since T0.
282+
//
283+
// TL option iii removes the secular TCL-TT drift. With bounded periodic
284+
// TCL-TT terms intentionally omitted, TL is equal to TT in this
285+
// experimental implementation.
286+
//
287+
// Keep the path TL -> TCL -> TT explicit so the relationship to
288+
// equation 47 remains visible and testable.
289+
let tl_since_t77 = self.duration;
290+
291+
// TL -> TCL -> TT
292+
let tcl_since_t77 = tl_since_t77_to_tcl_since_t77(tl_since_t77);
293+
let tt_since_t77 = tcl_since_t77_to_tt_since_t77(tcl_since_t77);
294+
295+
// TT -> TAI
296+
// NOTE: This whole branch can collapse to a simple TT offset because this is option (iii) of the paper
297+
let tt_duration = self.time_scale.prime_epoch_offset() + tt_since_t77;
298+
tt_duration - TT_OFFSET_MS.milliseconds()
299+
}
260300
};
261301

262302
// Convert to the desired time scale from the TAI duration
@@ -346,6 +386,29 @@ impl Epoch {
346386
let relativistic_delta = t77_duration - TDB0_S.seconds();
347387
t77_duration + relativistic_delta * ELB - TDB0_S.seconds()
348388
}
389+
TimeScale::TCL => {
390+
// TAI -> TT
391+
let tt_duration = prime_epoch_offset + TT_OFFSET_MS.milliseconds();
392+
393+
// TT absolute duration -> TT duration since T0
394+
let tt_since_t77 = tt_duration - ts.prime_epoch_offset();
395+
396+
// TT -> mean TCL
397+
tt_since_t77_to_tcl_since_t77(tt_since_t77)
398+
}
399+
400+
TimeScale::TL => {
401+
// TAI -> TT
402+
let tt_duration = prime_epoch_offset + TT_OFFSET_MS.milliseconds();
403+
404+
// TT absolute duration -> TT duration since T0
405+
let tt_since_t77 = tt_duration - ts.prime_epoch_offset();
406+
407+
// TT -> TCL -> TL, keeping equation 47 explicit.
408+
// NOTE: This whole branch can collapse to a simple TT offset because this is option (iii) of the paper
409+
let tcl_since_t77 = tt_since_t77_to_tcl_since_t77(tt_since_t77);
410+
tcl_since_t77_to_tl_since_t77(tcl_since_t77)
411+
}
349412
};
350413

351414
Self {

src/timescale/fmt.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ impl fmt::Display for TimeScale {
3030
Self::QZSST => write!(f, "QZSST"),
3131
Self::TCG => write!(f, "TCG"),
3232
Self::TCB => write!(f, "TCB"),
33+
Self::TL => write!(f, "TL"),
34+
Self::TCL => write!(f, "TCL"),
3335
}
3436
}
3537
}
@@ -75,6 +77,10 @@ impl FromStr for TimeScale {
7577
Ok(Self::TCG)
7678
} else if val == "TCB" {
7779
Ok(Self::TCB)
80+
} else if val == "TL" {
81+
Ok(Self::TL)
82+
} else if val == "TCL" {
83+
Ok(Self::TCL)
7884
} else {
7985
Err(ParsingError::TimeSystem)
8086
}

src/timescale/mod.rs

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ use serde_derive::{Deserialize, Serialize};
1919

2020
mod fmt;
2121

22+
/// EXPERIMENTAL Temps Lunaire Coordonnee / Lunar Coordinated Time
23+
pub(crate) mod tcl;
24+
2225
use crate::{Duration, Epoch, Unit, SECONDS_PER_DAY};
2326

2427
/// The J1900 reference epoch (1900-01-01 at noon) TAI.
@@ -95,7 +98,8 @@ pub enum TimeScale {
9598
TDB,
9699
/// Universal Coordinated Time
97100
UTC,
98-
/// GPS Time scale whose reference epoch is UTC midnight between 05 January and 06 January 1980; cf. <https://gssc.esa.int/navipedia/index.php/Time_References_in_GNSS#GPS_Time_.28GPST.29>. |UTC - TAI| = 19 Leap Seconds on that day.
101+
/// GPS Time scale whose reference epoch is UTC midnight between 05 January and 06 January 1980;
102+
/// cf. <https://gssc.esa.int/navipedia/index.php/Time_References_in_GNSS#GPS_Time_.28GPST.29>. |UTC - TAI| = 19 Leap Seconds on that day.
99103
GPST,
100104
/// Galileo Time scale
101105
GST,
@@ -107,6 +111,22 @@ pub enum TimeScale {
107111
TCG,
108112
/// Barycentric Coordinate Time
109113
TCB,
114+
/// Experimental Lunar Time, option (iii) from the Lunar Reference Timescale paper, A Bourgoin*, P Defraigne and F Meynadier
115+
///
116+
/// TL is defined as a linear scaling of TCL such that TL has no secular drift
117+
/// with respect to TT. Since this implementation omits the bounded periodic
118+
/// TCL-TT terms, TL is equivalent to TT after the common 1977 reference epoch.
119+
TL,
120+
/// Experimental mean Lunar Coordinate Time of Lunar reference timescale, A Bourgoin*, P Defraigne and F Meynadier
121+
///
122+
/// This is not a full IAU-quality TCL realization. It models only the
123+
/// conventional secular mean rate between TCL and TT:
124+
/// ```text
125+
/// d(TCL - TT) / dTT ≈ 6.8e-10
126+
/// ```
127+
/// The bounded periodic TCL-TT terms and ephemeris-dependent relativistic
128+
/// integral are intentionally omitted
129+
TCL,
110130
}
111131

112132
impl Default for TimeScale {
@@ -121,8 +141,15 @@ impl TimeScale {
121141
match &self {
122142
Self::QZSST => 5,
123143
Self::GPST => 4,
124-
Self::TAI | Self::TDB | Self::UTC | Self::GST | Self::BDT | Self::TCG | Self::TCB => 3,
125-
Self::ET | Self::TT => 2,
144+
Self::TAI
145+
| Self::TDB
146+
| Self::UTC
147+
| Self::GST
148+
| Self::BDT
149+
| Self::TCG
150+
| Self::TCB
151+
| Self::TCL => 3,
152+
Self::ET | Self::TT | Self::TL => 2,
126153
}
127154
}
128155

@@ -164,7 +191,7 @@ impl TimeScale {
164191
centuries: 1,
165192
nanoseconds: 189_302_433_000_000_000,
166193
},
167-
TimeScale::TCG => {
194+
TimeScale::TCG | TimeScale::TL | TimeScale::TCL => {
168195
// TCG reference epoch is 1977-01-01 00:00:32.184 TT.
169196
Duration {
170197
centuries: 0,
@@ -214,6 +241,8 @@ impl From<TimeScale> for u8 {
214241
TimeScale::QZSST => 8,
215242
TimeScale::TCG => 9,
216243
TimeScale::TCB => 10,
244+
TimeScale::TL => 11,
245+
TimeScale::TCL => 12,
217246
}
218247
}
219248
}
@@ -233,13 +262,15 @@ impl From<u8> for TimeScale {
233262
8 => Self::QZSST,
234263
9 => Self::TCG,
235264
10 => Self::TCB,
265+
11 => Self::TL,
266+
12 => Self::TCL,
236267
_ => Self::TAI,
237268
}
238269
}
239270
}
240271

241272
#[cfg(test)]
242-
mod unit_test_timescale {
273+
mod ut_timescale {
243274
use super::TimeScale;
244275

245276
#[test]
@@ -258,7 +289,7 @@ mod unit_test_timescale {
258289
let ts = TimeScale::from(ts_u8);
259290
let ts_u8_back: u8 = ts.into();
260291
// If the u8 is greater than 10, it isn't valid and necessarily encoded as TAI.
261-
if ts_u8 < 11 {
292+
if ts_u8 < 13 {
262293
assert_eq!(ts_u8_back, ts_u8, "got {ts_u8_back} want {ts_u8}");
263294
} else {
264295
assert_eq!(ts, TimeScale::TAI);

src/timescale/tcl.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Hifitime
3+
* Copyright (C) 2017-onward Christopher Rabotin <christopher.rabotin@gmail.com> et al. (cf. https://github.com/nyx-space/hifitime/graphs/contributors)
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* Documentation: https://nyxspace.com/
9+
*/
10+
11+
use crate::{Duration, TimeUnits};
12+
13+
/// Mean fractional rate of TCL relative to TT.
14+
///
15+
/// Paper option iii removes this secular TCL-TT drift when defining TL.
16+
/// The paper gives approximately 6.8e-10, i.e. about 58.7 us/day.
17+
pub(crate) const TCL_MINUS_TT_MEAN_RATE: f64 = 6.8e-10;
18+
19+
/// TL option iii:
20+
///
21+
/// ```text
22+
/// TL = TCL + Δf * (TCL - TCL0) + const0
23+
/// ```
24+
/// If:
25+
/// ```text
26+
/// dTCL / dTT = 1 + k
27+
/// ```
28+
/// then choosing:
29+
/// ```text
30+
/// 1 + Δf = 1 / (1 + k)
31+
///
32+
/// gives:
33+
/// ```text
34+
/// dTL / dTT = 1
35+
/// ```
36+
/// so TL has no secular drift relative to TT.
37+
pub(crate) const TL_DELTA_F: f64 = -TCL_MINUS_TT_MEAN_RATE / (1.0 + TCL_MINUS_TT_MEAN_RATE);
38+
39+
/// Recommended experimental convention:
40+
/// ```text
41+
/// TL = TCL = TT at T0
42+
/// ```
43+
/// with:
44+
/// ```text
45+
/// T0 = 1977-01-01T00:00:00 TAI
46+
/// = 1977-01-01T00:00:32.184 TT
47+
/// ```
48+
pub(crate) const TL_CONST0_S: f64 = 0.0;
49+
50+
#[inline]
51+
pub fn tt_since_t77_to_tcl_since_t77(tt_since_t77: Duration) -> Duration {
52+
// Mean TCL model:
53+
//
54+
// TCL - TCL0 = (TT - TT0) * (1 + k)
55+
//
56+
// Do not factor this into tt_since_t77 * (1.0 + k), matching the
57+
// style used for TCG/TCB to reduce avoidable rounding noise.
58+
tt_since_t77 + tt_since_t77 * TCL_MINUS_TT_MEAN_RATE
59+
}
60+
61+
#[inline]
62+
pub fn tcl_since_t77_to_tt_since_t77(tcl_since_t77: Duration) -> Duration {
63+
// Inverse of:
64+
//
65+
// TCL = TT * (1 + k)
66+
//
67+
// so:
68+
//
69+
// TT = TCL / (1 + k)
70+
// = TCL * (1 - k / (1 + k))
71+
tcl_since_t77 - tcl_since_t77 * (TCL_MINUS_TT_MEAN_RATE / (1.0 + TCL_MINUS_TT_MEAN_RATE))
72+
}
73+
74+
#[inline]
75+
pub fn tcl_since_t77_to_tl_since_t77(tcl_since_t77: Duration) -> Duration {
76+
// Option iii:
77+
//
78+
// TL = TCL + Δf * (TCL - TCL0) + const0
79+
//
80+
// with const0 = 0 and TCL0 = T0.
81+
tcl_since_t77 + tcl_since_t77 * TL_DELTA_F + TL_CONST0_S.seconds()
82+
}
83+
84+
#[inline]
85+
pub fn tl_since_t77_to_tcl_since_t77(tl_since_t77: Duration) -> Duration {
86+
// Since TL = TCL / (1 + k), the inverse is:
87+
//
88+
// TCL = TL * (1 + k)
89+
//
90+
// const0 is zero in this convention.
91+
let tl_minus_const0 = tl_since_t77 - TL_CONST0_S.seconds();
92+
tl_minus_const0 + tl_minus_const0 * TCL_MINUS_TT_MEAN_RATE
93+
}
94+
95+
#[cfg(test)]
96+
mod ut_tcl {
97+
use super::*;
98+
use crate::{Epoch, TimeScale, TimeUnits};
99+
100+
/// This test uses the private prime_epoch_offset, hence its definition here.
101+
#[test]
102+
fn tcl_accumulates_mean_drift_from_tt() {
103+
let tt = Epoch::from_gregorian_at_midnight(2024, 2, 29, TimeScale::TT);
104+
let tcl = tt.to_time_scale(TimeScale::TCL);
105+
106+
let tt_since_t77 = tt.duration - TimeScale::TCL.prime_epoch_offset();
107+
let expected = tt_since_t77 * TCL_MINUS_TT_MEAN_RATE;
108+
109+
let actual = tcl.duration - tt_since_t77;
110+
111+
assert!(
112+
(actual - expected).abs() <= 1.nanoseconds(),
113+
"actual={actual}, expected={expected}"
114+
);
115+
}
116+
}

0 commit comments

Comments
 (0)