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
256
257
258
259
260
261
262
263
264
265
266
267
// SPDX-License-Identifier: AGPL-3.0-only
//! Externally validate the quantum-inertial dead-reckoning error budget
//! (`inertial::quantum_imu::QuantumNavBudget`) over a GNSS-denied holdover
//! against two INDEPENDENT authorities.
//!
//! * **VRW (velocity random walk).** kshana carries the analytic position-error
//! 1-sigma `vrw_drift_m(t) = sqrt(q_va·t³/3)` for the double-integrated white
//! acceleration noise. The oracle is an **independent numpy Monte-Carlo SDE
//! integration**: M=2·10⁵ white-acceleration paths drawn with per-step std
//! `sqrt(q_va/dt)`, each double-integrated (two cumulative sums × dt), with the
//! empirical 1-σ of the final position taken over the realisations. The MC uses
//! no closed form, so its agreement with `sqrt(q_va·t³/3)` is a genuine external
//! check of the analytic variance — the same library-vs-independent-algorithm
//! spirit as the Lambert/scipy fixtures. Checked across **6 coast times**
//! {30,60,120,300,600,1200}s and **>3 q_va decades** (here ~5.2).
//!
//! * **Bias / scale-factor (deterministic).** `bias_drift_m(t)=½·b·t²` and
//! `scale_factor_drift_m(t)=½·ε·a_ref·t²` are the Groves 2013 closed-form INS
//! error-propagation identities (Groves, *Principles of GNSS, Inertial &
//! Multisensor Integrated Navigation*, 2nd ed., Artech House, ISBN
//! 978-1-60807-005-3), pinned in the fixture and matched to a tight relative
//! tolerance. (The fixture also carries an independent numpy double-integration
//! of the constant error; the ~0.08% forward-Euler bias it shows confirms the
//! closed forms are the correct continuum limit.)
//!
//! * **Holdover round-trip.** `holdover_seconds(thr)` inverts the (monotone) total
//! drift; the test checks `position_drift_1sigma(holdover) == thr`.
//!
//! Honest scope: this validates the **error-budget arithmetic** — the
//! double-integration of white noise (VRW), of a constant bias, and of a
//! scale-factor specific-force error, plus the holdover inversion. It does NOT
//! validate the cold-atom-interferometer device physics that *produces* `q_va`
//! (that is the separate `inertial::quantum_imu` / `inertial::cai_params`
//! capability, anchored to the Freier-2016 bracket), nor the root-sum-square
//! independence assumption of the composed budget. The VRW check is statistical
//! (Monte-Carlo band); the bias/scale-factor/holdover checks are tight.
//!
//! Reference data, provenance and the committed generator live in
//! `tests/fixtures/quantum_inertial_dead_reckoning/`.
use kshana::inertial::quantum_imu::{CaiAccelerometer, QuantumNavBudget};
use std::collections::HashMap;
const REF: &str = include_str!(
"fixtures/quantum_inertial_dead_reckoning/quantum_inertial_dead_reckoning_reference.txt"
);
/// VRW Monte-Carlo acceptance half-width: the empirical std must lie within ±3% of
/// the analytic `sqrt(q_va·t³/3)` (combined MC sampling noise ~0.2% at M=2·10⁵ and
/// discretisation bias <0.15% at 1200 steps both sit far inside this).
const VRW_BAND: f64 = 0.03;
/// Deterministic (bias / scale-factor) relative tolerance vs the Groves closed
/// form. Both are exact algebraic identities, so this is float round-off only.
const DET_REL: f64 = 1e-9;
const DET_ABS: f64 = 1e-12; // m, floor for terms that are exactly 0
/// Holdover round-trip relative tolerance (bisection converges to ~2⁻¹⁰⁰ of the
/// bracket; the residual is the threshold rel error).
const HOLD_REL: f64 = 1e-6;
fn parse(s: &str) -> f64 {
s.trim()
.parse()
.unwrap_or_else(|_| panic!("not a float: '{s}'"))
}
#[derive(Clone, Copy)]
struct Cfg {
wavelength_m: f64,
pulse_sep_t: f64,
atom_number: f64,
contrast: f64,
cycle_time_s: f64,
q_va: f64,
}
impl Cfg {
fn cai(&self) -> CaiAccelerometer {
CaiAccelerometer {
wavelength_m: self.wavelength_m,
pulse_sep_t: self.pulse_sep_t,
atom_number: self.atom_number,
contrast: self.contrast,
cycle_time_s: self.cycle_time_s,
}
}
}
#[test]
fn quantum_inertial_dead_reckoning_matches_independent_oracles() {
// ── Pass 1: read the CAI configs (so VRW/HOLD rows can rebuild the budget). ──
let mut cfgs: HashMap<String, Cfg> = HashMap::new();
for line in REF.lines() {
if let Some(rest) = line.strip_prefix("CAI ") {
let p: Vec<&str> = rest.split('|').collect();
assert_eq!(p.len(), 7, "CAI row needs 7 |-fields: {line}");
let name = p[0].trim().to_string();
let cfg = Cfg {
wavelength_m: parse(p[1]),
pulse_sep_t: parse(p[2]),
atom_number: parse(p[3]),
contrast: parse(p[4]),
cycle_time_s: parse(p[5]),
q_va: parse(p[6]),
};
// The Rust-derived q_va must equal the value the oracle ran its
// Monte-Carlo at — otherwise the MC band would apply to a different
// physics. This also re-checks CaiAccelerometer::q_va() in passing.
let q_rust = cfg.cai().q_va();
let rel = (q_rust - cfg.q_va).abs() / cfg.q_va;
assert!(
rel < 1e-12,
"CAI {name}: kshana q_va {q_rust:.6e} vs oracle {:.6e} (rel {rel:.2e})",
cfg.q_va
);
cfgs.insert(name, cfg);
}
}
assert!(
cfgs.len() >= 3,
"need >=3 CAI configs (q_va decades), got {}",
cfgs.len()
);
let budget_with = |cfg: &Cfg, bias: f64, ppm: f64, a_ref: f64| QuantumNavBudget {
cai: cfg.cai(),
bias_m_s2: bias,
scale_factor_ppm: ppm,
ref_accel_m_s2: a_ref,
tau_stability_s: 0.0,
};
// ── Pass 2: VRW Monte-Carlo band, deterministic Groves terms, holdover. ──
let mut n_vrw = 0usize;
let mut n_det = 0usize;
let mut n_hold = 0usize;
let mut worst_vrw = 0.0_f64; // worst MC-vs-kshana relative deviation
let mut worst_det = 0.0_f64; // worst deterministic relative deviation
let mut worst_hold = 0.0_f64; // worst holdover round-trip relative deviation
let mut q_decades_lo = f64::INFINITY;
let mut q_decades_hi = 0.0_f64;
for line in REF.lines() {
if let Some(rest) = line.strip_prefix("VRW ") {
// VRW cfgname | t | q_va | mc_std_m | analytic | rel_mc_vs_analytic
let p: Vec<&str> = rest.split('|').collect();
assert_eq!(p.len(), 6, "VRW row needs 6 |-fields: {line}");
let cfg = &cfgs[p[0].trim()];
let t = parse(p[1]);
let q_va = parse(p[2]);
let mc_std = parse(p[3]);
q_decades_lo = q_decades_lo.min(q_va);
q_decades_hi = q_decades_hi.max(q_va);
// kshana's analytic VRW drift on the IDENTICAL budget.
let budget = budget_with(cfg, 0.0, 0.0, 0.0);
let kshana = budget.vrw_drift_m(t);
// kshana must reproduce sqrt(q_va·t³/3) to float precision …
let closed = (q_va * t.powi(3) / 3.0).sqrt();
assert!(
(kshana - closed).abs() / closed < 1e-12,
"VRW {}: kshana vrw_drift_m {kshana:.6e} vs sqrt(q t³/3) {closed:.6e}",
p[0].trim()
);
// … and lie within the ±3% Monte-Carlo band of the empirical std.
let rel = (kshana - mc_std).abs() / mc_std;
worst_vrw = worst_vrw.max(rel);
assert!(
rel < VRW_BAND,
"VRW {} t={t}: kshana {kshana:.6e} m outside ±{:.0}% MC band of \
empirical {mc_std:.6e} m (rel {rel:.3e})",
p[0].trim(),
VRW_BAND * 100.0
);
n_vrw += 1;
} else if let Some(rest) = line.strip_prefix("DET ") {
// DET name | bias | ppm | a_ref | t | bias_groves | sf_groves | bias_num | sf_num
let p: Vec<&str> = rest.split('|').collect();
assert_eq!(p.len(), 9, "DET row needs 9 |-fields: {line}");
let bias = parse(p[1]);
let ppm = parse(p[2]);
let a_ref = parse(p[3]);
let t = parse(p[4]);
let bias_groves = parse(p[5]);
let sf_groves = parse(p[6]);
// Any CAI is fine for the deterministic terms (q_va does not enter them).
let cfg = cfgs.values().next().unwrap();
let budget = budget_with(cfg, bias, ppm, a_ref);
for (lbl, got, want) in [
("bias", budget.bias_drift_m(t), bias_groves),
("scale_factor", budget.scale_factor_drift_m(t), sf_groves),
] {
let tol = DET_REL * want.abs() + DET_ABS;
let d = (got - want).abs();
if want != 0.0 {
worst_det = worst_det.max(d / want.abs());
}
assert!(
d <= tol,
"DET {} {lbl} t={t}: kshana {got:.9e} m vs Groves {want:.9e} m \
(|Δ|={d:.2e} > {tol:.2e})",
p[0].trim()
);
}
n_det += 1;
} else if let Some(rest) = line.strip_prefix("HOLD ") {
// HOLD name | bias | ppm | a_ref | threshold_m (cai = Tlong)
let p: Vec<&str> = rest.split('|').collect();
assert_eq!(p.len(), 5, "HOLD row needs 5 |-fields: {line}");
let bias = parse(p[1]);
let ppm = parse(p[2]);
let a_ref = parse(p[3]);
let thr = parse(p[4]);
let cfg = &cfgs["Tlong"];
let budget = budget_with(cfg, bias, ppm, a_ref);
let hold = budget.holdover_seconds(thr);
assert!(
hold.is_finite() && hold > 0.0,
"HOLD {}: bad holdover {hold}",
p[0].trim()
);
let drift = budget.position_drift_1sigma(hold);
let rel = (drift - thr).abs() / thr;
worst_hold = worst_hold.max(rel);
assert!(
rel < HOLD_REL,
"HOLD {} thr={thr}: drift(holdover={hold:.3}) = {drift:.6} m, \
round-trip rel {rel:.2e} > {HOLD_REL:.0e}",
p[0].trim()
);
n_hold += 1;
}
}
// Quantity gates from the validation plan.
assert!(
n_vrw >= 18,
"expected >=18 VRW (6 times × >=3 decades), got {n_vrw}"
);
assert!(
n_det >= 8,
"expected >=8 deterministic Groves cases, got {n_det}"
);
assert!(
n_hold >= 4,
"expected >=4 holdover round-trip cases, got {n_hold}"
);
let decades = (q_decades_hi / q_decades_lo).log10();
assert!(
decades >= 3.0,
"VRW must span >=3 q_va decades, got {decades:.2}"
);
eprintln!(
"quantum_inertial_dead_reckoning: {n_vrw} VRW (worst MC dev {:.2}% < {:.0}%, \
{decades:.1} q_va decades), {n_det} bias/scale-factor (worst rel {:.1e} < {:.0e} vs Groves), \
{n_hold} holdover round-trips (worst rel {:.1e})",
worst_vrw * 100.0,
VRW_BAND * 100.0,
worst_det,
DET_REL,
worst_hold,
);
}