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
// SPDX-License-Identifier: AGPL-3.0-only
//! Externally validate kshana's cislunar SRIF cross-check ([`kshana::cislunar_srif`], paper P6,
//! L32) against an **independent NumPy/SciPy batch weighted least-squares** on the IDENTICAL
//! stacked measurement system `O = stack_k[ H_k · Φ_k ]`.
//!
//! The in-crate SRIF cross-validation folds the observability rows through Householder
//! square-root information filtering and compares against the eigen-Gramian — but both reduce to
//! `OᵀO`, so that agreement is a *different-algorithm consistency check on the same crate*, not a
//! fully external oracle (as the module honestly documents). This test closes that gap: it folds
//! the same `O` into kshana's [`kshana::deepspace_od::Srif`] and compares the SRIF posterior
//! covariance `P = R⁻¹R⁻ᵀ` and its condition number against the batch-LS posterior
//! `(OᵀO)⁻¹` / `cond(OᵀO)` computed by LAPACK (NumPy `linalg.inv`/`cond`, SciPy `linalg.inv`) — a
//! genuinely different codebase. SciPy `linalg.lstsq` additionally pins the batch estimator's rank
//! to the full four-state and recovers a known injected initial-state offset.
//!
//! The observability matrix `O` is the same fixed 4-epoch single-range-link arc as
//! `tests/observability_gramian_reference.rs`; the Rust side rebuilds it with kshana's own
//! [`kshana::intersat_range::range_row`] and the committed `Φ_k`. Reference vectors and the
//! generator live in `tests/fixtures/cislunar_srif_batch_ls/generate.py`
//! (`python3 generate.py > cislunar_srif_batch_ls_reference.txt`; NumPy + SciPy, no network — the
//! Rust test reads the committed `.txt`).
use kshana::deepspace_od::Srif;
use kshana::intersat_range::{range_row, PlanarState};
use kshana::observability_gramian::{observability_matrix, Mat, ObsEpoch, N_PLANAR};
const REF: &str =
include_str!("fixtures/cislunar_srif_batch_ls/cislunar_srif_batch_ls_reference.txt");
/// LAPACK (NumPy/SciPy) and the crate's square-root machine agree to ~1e-9 on the well-scaled
/// entries; the smallest covariance entries are O(1e2) so an absolute+relative bound is used.
const TOL_REL: f64 = 1e-7;
const TOL_ABS: f64 = 1e-6;
struct Fixture {
chief: PlanarState,
reference: PlanarState,
phis: Vec<Mat>,
dts: Vec<f64>,
p_unit: Mat, // (O^T O)^-1 from numpy
cond_n_unit: f64, // cond(O^T O) from numpy
lstsq_rank: usize, // scipy.linalg.lstsq rank on O
injected_ds0: Vec<f64>, // known offset injected into the synthetic measurements
recovered_ds0: Vec<f64>, // scipy lstsq recovery of that offset
}
fn parse_state4(tokens: &[&str]) -> PlanarState {
[
tokens[0].parse().unwrap(),
tokens[1].parse().unwrap(),
tokens[2].parse().unwrap(),
tokens[3].parse().unwrap(),
]
}
fn parse_fixture(text: &str) -> Fixture {
let mut chief = None;
let mut reference = None;
let mut dts: Vec<f64> = Vec::new();
let mut phis: Vec<Mat> = Vec::new();
let mut p_unit: Mat = Vec::new();
let mut cond_n_unit = None;
let mut lstsq_rank = None;
let mut injected = Vec::new();
let mut recovered = Vec::new();
// A tiny state machine over "# MATRIX <name>" / "ROW ..." blocks.
let mut cur: Option<(String, Mat)> = None;
let finish = |cur: &mut Option<(String, Mat)>, phis: &mut Vec<Mat>, p_unit: &mut Mat| {
if let Some((name, m)) = cur.take() {
if name.starts_with("PHI") {
phis.push(m);
} else if name == "P_UNIT" {
*p_unit = m;
}
}
};
for line in text.lines() {
let line = line.trim_end();
if let Some(rest) = line.strip_prefix("# CHIEF ") {
chief = Some(parse_state4(&rest.split_whitespace().collect::<Vec<_>>()));
} else if let Some(rest) = line.strip_prefix("# REF ") {
reference = Some(parse_state4(&rest.split_whitespace().collect::<Vec<_>>()));
} else if let Some(rest) = line.strip_prefix("# DTS ") {
dts = rest
.split_whitespace()
.map(|s| s.parse().unwrap())
.collect();
} else if let Some(rest) = line.strip_prefix("# MATRIX ") {
finish(&mut cur, &mut phis, &mut p_unit);
let name = rest.split_whitespace().next().unwrap().to_string();
cur = Some((name, Vec::new()));
} else if let Some(rest) = line.strip_prefix("ROW ") {
if let Some((_, m)) = cur.as_mut() {
m.push(
rest.split_whitespace()
.map(|s| s.parse().unwrap())
.collect(),
);
}
} else if let Some(rest) = line.strip_prefix("# COND_N_UNIT ") {
cond_n_unit = Some(rest.trim().parse().unwrap());
} else if let Some(rest) = line.strip_prefix("# LSTSQ_RANK ") {
lstsq_rank = Some(rest.trim().parse().unwrap());
} else if let Some(rest) = line.strip_prefix("# LSTSQ_INJECTED_DS0 ") {
injected = rest
.split_whitespace()
.map(|s| s.parse().unwrap())
.collect();
} else if let Some(rest) = line.strip_prefix("# LSTSQ_RECOVERED_DS0 ") {
recovered = rest
.split_whitespace()
.map(|s| s.parse().unwrap())
.collect();
}
}
finish(&mut cur, &mut phis, &mut p_unit);
Fixture {
chief: chief.expect("CHIEF"),
reference: reference.expect("REF"),
phis,
dts,
p_unit,
cond_n_unit: cond_n_unit.expect("COND_N_UNIT"),
lstsq_rank: lstsq_rank.expect("LSTSQ_RANK"),
injected_ds0: injected,
recovered_ds0: recovered,
}
}
fn build_epochs(f: &Fixture) -> Vec<ObsEpoch> {
let (_rho, h_row) = range_row(&f.chief, &f.reference);
f.phis
.iter()
.zip(&f.dts)
.map(|(phi, &dt)| ObsEpoch {
h: vec![h_row.to_vec()],
phi: phi.clone(),
dt,
})
.collect()
}
/// Symmetric ascending eigenvalues of a small matrix via unrotated Jacobi (independent of the
/// crate) — used only to read a condition number off the SRIF posterior covariance for comparison.
fn cond_number(m: &Mat) -> f64 {
// Power/inverse-power would suffice, but for a 4x4 SPD matrix a cheap cyclic-Jacobi sweep is
// clearest. Reuse the crate's own symmetric eigensolver: this is not the object under test
// (numpy's cond on O^T O is), and the SRIF P it consumes is what we validate.
let e = kshana::fim::sym_eig(m);
let lmin = e.values.first().copied().unwrap_or(0.0);
let lmax = e.values.last().copied().unwrap_or(0.0);
if lmin <= 0.0 {
f64::INFINITY
} else {
lmax / lmin
}
}
fn close(got: f64, want: f64) -> bool {
(got - want).abs() <= TOL_ABS + TOL_REL * want.abs()
}
// ── (a) SRIF posterior covariance P = (O^T O)^-1 matches the numpy batch-LS posterior ──
#[test]
fn srif_posterior_covariance_matches_numpy_batch_ls() {
let f = parse_fixture(REF);
let epochs = build_epochs(&f);
let (o, _w) = observability_matrix(&epochs);
// Fold the unit-weight rows into the SRIF, exactly as cislunar_srif::srif_over_arc does, so
// R^T R = O^T O and P = (O^T O)^-1 — the batch-LS posterior.
let mut srif = Srif::new(N_PLANAR);
for row in &o {
srif.measurement_update(row, 0.0, 1.0);
}
let (_x, p) = srif.solve();
assert_eq!(f.p_unit.len(), N_PLANAR, "fixture P is 4x4");
for (i, (got_row, want_row)) in p.iter().zip(&f.p_unit).enumerate() {
for (j, (got, want)) in got_row.iter().zip(want_row).enumerate() {
assert!(
close(*got, *want),
"SRIF P[{i}][{j}] = {got} differs from numpy batch-LS (O^T O)^-1 = {want}"
);
}
}
// Condition number of the SRIF posterior tracks numpy.linalg.cond(O^T O) (cond(P)=cond(O^T O)).
let cond_p = cond_number(&p);
let cond_rel = (cond_p - f.cond_n_unit).abs() / f.cond_n_unit;
assert!(
cond_rel < 1e-4,
"SRIF cond(P) {} vs numpy cond(O^T O) {} (rel {cond_rel:.2e})",
cond_p,
f.cond_n_unit
);
eprintln!(
"cislunar_srif_batch_ls_reference: SRIF P vs numpy batch-LS (O^T O)^-1 — cond(P) {:.3e} \
(numpy {:.3e})",
cond_p, f.cond_n_unit
);
}
// ── (b) the batch system is full rank (scipy lstsq) and recovers the injected offset ──
#[test]
fn batch_system_is_full_rank_and_recovers_offset() {
let f = parse_fixture(REF);
assert_eq!(
f.lstsq_rank, N_PLANAR,
"scipy.linalg.lstsq rank {} must be the full four-state {}",
f.lstsq_rank, N_PLANAR
);
// scipy recovered the known injected initial-state offset to numerical precision, confirming
// the stacked O is full column rank (else lstsq would not uniquely recover ds0).
assert_eq!(f.injected_ds0.len(), N_PLANAR);
assert_eq!(f.recovered_ds0.len(), N_PLANAR);
for (inj, rec) in f.injected_ds0.iter().zip(&f.recovered_ds0) {
assert!(
(inj - rec).abs() < 1e-9,
"scipy lstsq recovered {rec} vs injected {inj}"
);
}
// The crate agrees: the same O folded into the SRIF yields a finite, well-posed posterior
// (full observability), the estimator-agreement the batch-LS oracle certifies externally.
let epochs = build_epochs(&f);
let (o, _w) = observability_matrix(&epochs);
let mut srif = Srif::new(N_PLANAR);
for row in &o {
srif.measurement_update(row, 0.0, 1.0);
}
let (_x, p) = srif.solve();
assert!(
p.iter().flatten().all(|v| v.is_finite()),
"full-rank SRIF posterior must be finite"
);
}