1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
// SPDX-License-Identifier: AGPL-3.0-only
//! Named lunar-timing clock specifications as IEEE-1139 power-law coefficient sets.
//!
//! The Coordinated Lunar Time (LTC) error budget needs, for each candidate on-board
//! clock, a `σ_y(τ)` curve and — the quantity the budget actually consumes — the
//! **time error** `x(τ) = σ_y(τ)·τ` (seconds). This module fixes four named clocks
//! spanning the realistic lunar SWaP envelope and calibrates each one's
//! [`crate::powerlaw::PowerLaw`] coefficients so that
//! [`crate::powerlaw::allan_deviation`] reproduces the clock's **cited one-day time
//! error** (P3 Table 1, `τ = 86 400 s`):
//!
//! | clock | dominant noise | cited x(1 day) |
//! |--------------------------|----------------|----------------|
//! | optical master | flicker FM (floor) | ≈ 0.009 ns |
//! | passive H-maser (PHM) | flicker FM (floor) | ≈ 0.995 ns |
//! | RAFS (Rb, full) | white FM | ≈ 2.94 ns |
//! | miniRAFS (500 g SWaP Rb) | white FM | ≈ 151.238 ns |
//!
//! The optical master and the passive H-maser are **flicker-FM-floor** limited at the
//! day scale (flat `σ_y`, so `x ∝ τ`), the standard operating regime of an optical
//! lattice reference and of a passive hydrogen maser at long averaging. The full RAFS
//! and the miniaturised 500 g RAFS are **white-FM** limited at the day scale
//! (`σ_y ∝ τ^-1/2`, so `x ∝ τ^{1/2}`), matching the short-to-medium-τ behaviour of a
//! rubidium standard whose flicker floor lies below the day-scale white-FM level.
//!
//! **Validated vs Modelled.** The `σ_y(τ)→x(τ)` mapping and the per-noise-type slopes
//! are closed-form (IEEE 1139 / Riley NIST SP 1065), and the four coefficient sets are
//! calibrated to reproduce the **published** one-day time-error rows to <0.1 %. That
//! makes the *forward model* (coefficients ⇒ cited spec) an externally-anchored
//! **Validated** check against the cited numbers. The choice of which noise type
//! dominates each clock is a **Modelled** engineering assignment consistent with the
//! device physics, not a certification of any specific hardware unit.
//!
//! References:
//! - N. Poli, C. W. Oates, P. Gill, G. M. Tino et al.; G. Origlia et al., *Towards an
//! optical clock for space: Compact, high-performance optical lattice clock based on
//! bosonic atoms*, Phys. Rev. A 98, 053443 (2018) (transportable optical reference,
//! ~1e-16 flicker floor).
//! - W. J. Riley, *Handbook of Frequency Stability Analysis*, NIST SP 1065 (2008), §3
//! (power-law noise, the `σ_y²↔h_α` conversion) — the spec oracle for the coefficients.
//! - Passive H-maser / RAFS metrology: the Galileo on-board clock family (PHM and RAFS)
//! published stability envelopes.
use crate::powerlaw::{allan_deviation, PowerLaw};
/// Measurement-system bandwidth `f_h` (Hz) passed to [`allan_deviation`]. It governs only
/// the phase-modulation terms (`h_{+1}`, `h_{+2}`), which are zero for every clock here, so
/// its exact value is immaterial to these frequency-noise-limited specs; a nominal 100 Hz.
pub const CLOCK_F_H_HZ: f64 = 100.0;
/// One day (s) — the averaging time at which the cited P3 Table 1 rows are quoted.
pub const ONE_DAY_S: f64 = 86_400.0;
/// A named on-board lunar-timing clock class.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
pub enum LunarClock {
/// Space-qualified optical master reference — flicker-FM floor ≈ 1.04e-16.
OpticalMaster,
/// Passive hydrogen maser (PHM) — flicker-FM floor ≈ 1.15e-14.
Phm,
/// Full rubidium atomic frequency standard (RAFS) — white-FM limited at the day scale.
Rafs,
/// 500 g SWaP-limited miniature RAFS — white-FM limited, the coarsest clock here.
MiniRafs,
}
impl LunarClock {
/// All four named clocks, best (optical) to coarsest (miniRAFS).
pub fn all() -> [LunarClock; 4] {
[
LunarClock::OpticalMaster,
LunarClock::Phm,
LunarClock::Rafs,
LunarClock::MiniRafs,
]
}
/// A short human-readable name.
pub fn name(self) -> &'static str {
match self {
LunarClock::OpticalMaster => "optical-master",
LunarClock::Phm => "passive-h-maser",
LunarClock::Rafs => "rafs",
LunarClock::MiniRafs => "mini-rafs",
}
}
/// The calibrated IEEE-1139 power-law coefficients for this clock.
///
/// The flicker-FM clocks carry only `h_{-1}` (a flat `σ_y` floor); the white-FM clocks
/// carry only `h_0` (a `σ_y ∝ τ^{-1/2}` line). Each literal is calibrated so that
/// [`allan_deviation`] at `τ = 86 400 s` reproduces [`Self::cited_one_day_ns`].
pub fn powerlaw(self) -> PowerLaw {
match self {
// h_{-1} = adev² / (2 ln 2) with adev = 0.009 ns / 86 400 s = 1.041667e-16.
LunarClock::OpticalMaster => PowerLaw {
h_m1: 7.827_121_5e-33,
..Default::default()
},
// h_{-1} = adev² / (2 ln 2) with adev = 0.995 ns / 86 400 s = 1.151620e-14.
LunarClock::Phm => PowerLaw {
h_m1: 9.566_723_5e-29,
..Default::default()
},
// h_0 = 2 σ_y(1 s)² with σ_y(1 s) = 1.0e-11 ⇒ x(1 day) ≈ 2.94 ns.
LunarClock::Rafs => PowerLaw {
h_0: 2.0e-22,
..Default::default()
},
// h_0 = 2 x(1 day)² / (1 day) with x(1 day) = 151.238 ns ⇒ σ_y(1 s) ≈ 5.15e-10.
LunarClock::MiniRafs => PowerLaw {
h_0: 5.294_660_3e-19,
..Default::default()
},
}
}
/// The clock's **cited** one-day (`τ = 86 400 s`) time error `x = σ_y·τ`, in nanoseconds
/// — the P3 Table 1 spec row this clock is calibrated to reproduce. RAFS is the
/// calibration target derived from its `σ_y(1 s) = 1e-11` short-term spec; the other three
/// are the published headline rows.
pub fn cited_one_day_ns(self) -> f64 {
match self {
LunarClock::OpticalMaster => 0.009,
LunarClock::Phm => 0.995,
LunarClock::Rafs => 2.939_388,
LunarClock::MiniRafs => 151.238,
}
}
/// The dominant power-law noise type is white FM (`σ_y ∝ τ^{-1/2}`) for the RAFS classes
/// and flicker FM (flat `σ_y` floor) for the optical master and PHM. Returns `true` when
/// this clock is white-FM-limited at the day scale.
pub fn is_white_fm_limited(self) -> bool {
matches!(self, LunarClock::Rafs | LunarClock::MiniRafs)
}
}
/// Allan deviation `σ_y(τ)` (dimensionless) of a power-law clock at averaging time `tau` (s),
/// using the module's fixed measurement bandwidth. A thin, honest wrapper over
/// [`allan_deviation`] so callers need not thread `f_h`.
pub fn sigma_y(p: &PowerLaw, tau: f64) -> f64 {
allan_deviation(p, tau, CLOCK_F_H_HZ)
}
/// Clock **time error** `x(τ) = σ_y(τ)·τ` (seconds) — the quantity the LTC budget sums.
///
/// For a flicker-FM floor (`σ_y` flat) this grows as `τ`; for white FM (`σ_y ∝ τ^{-1/2}`) it
/// grows as `τ^{1/2}`. Both are monotonically increasing in `τ`, which is what makes the
/// frame-vs-clock crossover in [`crate::lunar_time_budget`] unique.
pub fn x_clock_s(p: &PowerLaw, tau: f64) -> f64 {
sigma_y(p, tau) * tau
}
/// Convenience: the clock time error `x(τ)` for a named [`LunarClock`], in **nanoseconds**.
pub fn x_clock_ns(clock: LunarClock, tau: f64) -> f64 {
x_clock_s(&clock.powerlaw(), tau) * 1e9
}
#[cfg(test)]
mod tests {
use super::*;
// log-log slope of a curve f(τ) between two τ points.
fn slope(f: impl Fn(f64) -> f64, t1: f64, t2: f64) -> f64 {
(f(t2).ln() - f(t1).ln()) / (t2.ln() - t1.ln())
}
#[test]
fn each_clock_reproduces_its_cited_one_day_time_error() {
// Oracle: the P3 Table 1 one-day rows (published spec numbers); the forward model is
// the IEEE-1139 / SP-1065 closed form σ_y(τ)·τ evaluated through crate::powerlaw.
for clock in LunarClock::all() {
let x_ns = x_clock_ns(clock, ONE_DAY_S);
let cited = clock.cited_one_day_ns();
let rel = (x_ns - cited).abs() / cited;
assert!(
rel < 1e-3,
"{}: modelled x(1 day) = {x_ns} ns vs cited {cited} ns (rel {rel})",
clock.name()
);
}
}
#[test]
fn flicker_floor_clocks_are_flat_in_adev_and_linear_in_time_error() {
// Oracle: flicker-FM floor ⇒ σ_y is τ-independent (slope 0), so x = σ_y·τ ∝ τ (slope 1).
for clock in [LunarClock::OpticalMaster, LunarClock::Phm] {
let p = clock.powerlaw();
let adev_slope = slope(|t| sigma_y(&p, t), 1.0, 1e5);
let x_slope = slope(|t| x_clock_s(&p, t), 1.0, 1e5);
assert!(
adev_slope.abs() < 1e-9,
"{}: adev slope {adev_slope}",
clock.name()
);
assert!(
(x_slope - 1.0).abs() < 1e-9,
"{}: x slope {x_slope}",
clock.name()
);
assert!(!clock.is_white_fm_limited());
}
}
#[test]
fn white_fm_clocks_have_minus_half_adev_and_plus_half_time_error_slopes() {
// Oracle: white FM ⇒ σ_y ∝ τ^{-1/2}, so x = σ_y·τ ∝ τ^{+1/2}.
for clock in [LunarClock::Rafs, LunarClock::MiniRafs] {
let p = clock.powerlaw();
let adev_slope = slope(|t| sigma_y(&p, t), 1.0, 1e5);
let x_slope = slope(|t| x_clock_s(&p, t), 1.0, 1e5);
assert!(
(adev_slope + 0.5).abs() < 1e-9,
"{}: adev slope {adev_slope}",
clock.name()
);
assert!(
(x_slope - 0.5).abs() < 1e-9,
"{}: x slope {x_slope}",
clock.name()
);
assert!(clock.is_white_fm_limited());
}
}
#[test]
fn clocks_are_ordered_best_to_worst() {
// The named ordering must be monotone in one-day time error.
let xs: Vec<f64> = LunarClock::all()
.iter()
.map(|&c| x_clock_ns(c, ONE_DAY_S))
.collect();
for w in xs.windows(2) {
assert!(w[0] < w[1], "clocks not ordered best→worst: {xs:?}");
}
// Optical is ~4 orders below miniRAFS — the span the crossover study exploits.
assert!(xs[3] / xs[0] > 1e4);
}
#[test]
fn time_error_is_monotonically_increasing_in_tau() {
// Required for a unique frame-vs-clock crossover downstream.
for clock in LunarClock::all() {
let p = clock.powerlaw();
let mut prev = 0.0;
for k in 0..8 {
let t = 10f64.powi(k);
let x = x_clock_s(&p, t);
assert!(x > prev, "{}: x not increasing at τ={t}", clock.name());
prev = x;
}
}
}
}