use crate::lunar_datum::datum7_point_jacobian_body;
pub use crate::lunar_datum::{Datum7, Vec3};
#[derive(Debug, Clone, Copy)]
pub struct HelmertFit {
pub datum: Datum7,
pub raw_rms_m: f64,
pub residual_rms_m: f64,
}
fn solve_spd<const N: usize>(a: &[[f64; N]; N], b: &[f64; N]) -> [f64; N] {
let mut l = [[0.0_f64; N]; N];
for i in 0..N {
for j in 0..=i {
let s: f64 = (0..j).map(|k| l[i][k] * l[j][k]).sum();
l[i][j] = if i == j {
(a[i][i] - s).sqrt()
} else {
(a[i][j] - s) / l[j][j]
};
}
}
let mut y = [0.0_f64; N];
for i in 0..N {
let s: f64 = (0..i).map(|k| l[i][k] * y[k]).sum();
y[i] = (b[i] - s) / l[i][i];
}
let mut x = [0.0_f64; N];
for i in (0..N).rev() {
let s: f64 = ((i + 1)..N).map(|k| l[k][i] * x[k]).sum();
x[i] = (y[i] - s) / l[i][i];
}
x
}
fn preconditioned_solve<const N: usize>(normal: &[[f64; N]; N], rhs: &[f64; N]) -> [f64; N] {
let d: [f64; N] = std::array::from_fn(|i| {
let diag = normal[i][i];
if diag > 0.0 {
1.0 / diag.sqrt()
} else {
1.0 }
});
let mut normal_s = [[0.0_f64; N]; N];
let mut rhs_s = [0.0_f64; N];
for i in 0..N {
rhs_s[i] = rhs[i] * d[i];
for j in 0..N {
normal_s[i][j] = normal[i][j] * d[i] * d[j];
}
}
let y0 = solve_spd(&normal_s, &rhs_s);
let mut delta: [f64; N] = std::array::from_fn(|i| y0[i] * d[i]);
let mut resid = [0.0_f64; N];
for i in 0..N {
let nd_i: f64 = (0..N).map(|j| normal[i][j] * delta[j]).sum();
resid[i] = rhs[i] - nd_i;
}
let resid_s: [f64; N] = std::array::from_fn(|i| resid[i] * d[i]);
let e_s = solve_spd(&normal_s, &resid_s);
for i in 0..N {
delta[i] += e_s[i] * d[i];
}
delta
}
pub fn helmert_fit(from: &[Vec3], to: &[Vec3]) -> HelmertFit {
assert!(!from.is_empty(), "helmert_fit: empty input");
assert_eq!(from.len(), to.len(), "helmert_fit: from.len() != to.len()");
let n = from.len() as f64;
let from_mean: Vec3 = {
let sx: f64 = from.iter().map(|p| p[0]).sum();
let sy: f64 = from.iter().map(|p| p[1]).sum();
let sz: f64 = from.iter().map(|p| p[2]).sum();
[sx / n, sy / n, sz / n]
};
let mut normal = [[0.0_f64; 7]; 7];
let mut rhs = [0.0_f64; 7];
let mut raw_sq = 0.0_f64;
for (p, q) in from.iter().zip(to.iter()) {
let p_c = [
p[0] - from_mean[0],
p[1] - from_mean[1],
p[2] - from_mean[2],
];
let j = datum7_point_jacobian_body(p_c);
let d = [q[0] - p[0], q[1] - p[1], q[2] - p[2]];
raw_sq += d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
for c1 in 0..7 {
for c2 in 0..7 {
normal[c1][c2] += j[0][c1] * j[0][c2] + j[1][c1] * j[1][c2] + j[2][c1] * j[2][c2];
}
rhs[c1] += j[0][c1] * d[0] + j[1][c1] * d[1] + j[2][c1] * d[2];
}
}
let delta_c = preconditioned_solve(&normal, &rhs);
let (s, tx_c, ty_c, tz_c) = (delta_c[3], delta_c[0], delta_c[1], delta_c[2]);
let (theta_x, theta_y, theta_z) = (delta_c[4], delta_c[5], delta_c[6]);
let [mx, my, mz] = from_mean;
let delta = [
tx_c - s * mx - (theta_y * mz - theta_z * my),
ty_c - s * my - (theta_z * mx - theta_x * mz),
tz_c - s * mz - (theta_x * my - theta_y * mx),
s,
theta_x,
theta_y,
theta_z,
];
let datum = Datum7 {
t_m: [delta[0], delta[1], delta[2]],
scale: delta[3],
rot_rad: [delta[4], delta[5], delta[6]],
};
let resid_sq: f64 = from
.iter()
.zip(to.iter())
.map(|(p, q)| {
let j = datum7_point_jacobian_body(*p);
let d = [q[0] - p[0], q[1] - p[1], q[2] - p[2]];
let fitted: [f64; 3] = std::array::from_fn(|row| {
j[row].iter().zip(delta.iter()).map(|(a, b)| a * b).sum()
});
let r = [d[0] - fitted[0], d[1] - fitted[1], d[2] - fitted[2]];
r[0] * r[0] + r[1] * r[1] + r[2] * r[2]
})
.sum();
HelmertFit {
datum,
raw_rms_m: (raw_sq / n).sqrt(),
residual_rms_m: (resid_sq / n).sqrt(),
}
}
pub fn rotation_fit(from: &[Vec3], to: &[Vec3]) -> (Vec3, f64) {
assert!(!from.is_empty(), "rotation_fit: empty input");
assert_eq!(from.len(), to.len(), "rotation_fit: from.len() != to.len()");
let n = from.len() as f64;
let from_mean: Vec3 = {
let sx: f64 = from.iter().map(|p| p[0]).sum();
let sy: f64 = from.iter().map(|p| p[1]).sum();
let sz: f64 = from.iter().map(|p| p[2]).sum();
[sx / n, sy / n, sz / n]
};
const COLS: [usize; 6] = [0, 1, 2, 4, 5, 6];
let mut normal = [[0.0_f64; 6]; 6];
let mut rhs = [0.0_f64; 6];
for (p, q) in from.iter().zip(to.iter()) {
let p_c = [
p[0] - from_mean[0],
p[1] - from_mean[1],
p[2] - from_mean[2],
];
let j7 = datum7_point_jacobian_body(p_c);
let d = [q[0] - p[0], q[1] - p[1], q[2] - p[2]];
for (c1, &col1) in COLS.iter().enumerate() {
for (c2, &col2) in COLS.iter().enumerate() {
normal[c1][c2] += j7[0][col1] * j7[0][col2]
+ j7[1][col1] * j7[1][col2]
+ j7[2][col1] * j7[2][col2];
}
rhs[c1] += j7[0][col1] * d[0] + j7[1][col1] * d[1] + j7[2][col1] * d[2];
}
}
let delta = preconditioned_solve(&normal, &rhs);
let resid_sq: f64 = from
.iter()
.zip(to.iter())
.map(|(p, q)| {
let p_c = [
p[0] - from_mean[0],
p[1] - from_mean[1],
p[2] - from_mean[2],
];
let j7 = datum7_point_jacobian_body(p_c);
let d = [q[0] - p[0], q[1] - p[1], q[2] - p[2]];
let fitted: [f64; 3] = std::array::from_fn(|row| {
COLS.iter()
.enumerate()
.map(|(ci, &c)| j7[row][c] * delta[ci])
.sum()
});
let r = [d[0] - fitted[0], d[1] - fitted[1], d[2] - fitted[2]];
r[0] * r[0] + r[1] * r[1] + r[2] * r[2]
})
.sum();
let theta = [delta[3], delta[4], delta[5]];
(theta, (resid_sq / n).sqrt())
}
#[derive(Debug, Clone, Copy)]
pub struct ProvenanceSplit {
pub raw_rms_m: f64,
pub rot_residual_m: f64,
pub theta_moon: Vec3,
pub theta_frametie: Vec3,
pub theta_excess: Vec3,
pub reducible_m: f64,
pub irreducible_m: f64,
}
pub fn provenance_split(
moon_from: &[Vec3],
moon_to: &[Vec3],
planet_pairs: &[(Vec<Vec3>, Vec<Vec3>)],
lever_arm_m: f64,
) -> ProvenanceSplit {
assert!(!moon_from.is_empty(), "provenance_split: empty moon_from");
assert!(
!planet_pairs.is_empty(),
"provenance_split: empty planet_pairs"
);
let (theta_moon, rot_residual_m) = rotation_fit(moon_from, moon_to);
let n_moon = moon_from.len() as f64;
let raw_sq: f64 = moon_from
.iter()
.zip(moon_to.iter())
.map(|(p, q)| {
let d = [q[0] - p[0], q[1] - p[1], q[2] - p[2]];
d[0] * d[0] + d[1] * d[1] + d[2] * d[2]
})
.sum();
let raw_rms_m = (raw_sq / n_moon).sqrt();
let planet_thetas: Vec<Vec3> = planet_pairs
.iter()
.map(|(from, to)| rotation_fit(from, to).0)
.collect();
let theta_frametie = component_median(&planet_thetas);
let theta_excess = [
theta_moon[0] - theta_frametie[0],
theta_moon[1] - theta_frametie[1],
theta_moon[2] - theta_frametie[2],
];
let reducible_m = norm3(theta_frametie) * lever_arm_m;
let irreducible_m = norm3(theta_excess) * lever_arm_m;
ProvenanceSplit {
raw_rms_m,
rot_residual_m,
theta_moon,
theta_frametie,
theta_excess,
reducible_m,
irreducible_m,
}
}
fn component_median(thetas: &[Vec3]) -> Vec3 {
let n = thetas.len();
assert!(n > 0, "component_median: empty slice");
let mut result = [0.0_f64; 3];
for (ci, res) in result.iter_mut().enumerate() {
let mut vals: Vec<f64> = thetas.iter().map(|t| t[ci]).collect();
vals.sort_by(f64::total_cmp);
*res = if n % 2 == 1 {
vals[n / 2]
} else {
(vals[n / 2 - 1] + vals[n / 2]) / 2.0
};
}
result
}
fn norm3(v: Vec3) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
#[derive(Debug, Clone, Copy)]
pub struct ConsistencyTolerance {
pub budget_m: f64,
pub max_origin_m: f64,
pub max_scale: f64,
pub max_rotation_rad: f64,
pub binding: &'static str,
}
pub fn consistency_tolerance(
budget_m: f64,
r_user_m: f64,
per_provider: Option<&crate::lunar_identifiability::DatumIdentifiability>,
) -> ConsistencyTolerance {
assert!(
r_user_m > 0.0,
"consistency_tolerance: r_user_m must be positive"
);
assert!(
budget_m >= 0.0,
"consistency_tolerance: budget_m must be non-negative"
);
let b_eff = match per_provider {
None => budget_m,
Some(d) => {
let sigma = d.origin_crlb_m;
(budget_m * budget_m - sigma * sigma).max(0.0).sqrt()
}
};
let max_origin_m = b_eff;
let max_scale = b_eff / r_user_m;
let max_rotation_rad = b_eff / r_user_m;
let binding = if r_user_m >= 1.0 {
"rotation"
} else {
"origin"
};
ConsistencyTolerance {
budget_m,
max_origin_m,
max_scale,
max_rotation_rad,
binding,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameConvention {
PerProvider,
CommonFrameTie,
CommonEphemeris,
}
#[derive(Debug, Clone, Copy)]
pub struct InteropBudget {
pub convention: FrameConvention,
pub reducible_m: f64,
pub irreducible_m: f64,
pub total_m: f64,
pub irreducible_fraction: f64,
}
pub fn interop_budget(splits: &[ProvenanceSplit], convention: FrameConvention) -> InteropBudget {
assert!(!splits.is_empty(), "interop_budget: empty splits");
let n = splits.len() as f64;
let reducible_m = (splits
.iter()
.map(|s| s.reducible_m * s.reducible_m)
.sum::<f64>()
/ n)
.sqrt();
let irreducible_m = (splits
.iter()
.map(|s| s.irreducible_m * s.irreducible_m)
.sum::<f64>()
/ n)
.sqrt();
let residual_sq = splits
.iter()
.map(|s| s.rot_residual_m * s.rot_residual_m)
.sum::<f64>()
/ n;
let total_m = match convention {
FrameConvention::PerProvider => {
(reducible_m * reducible_m + irreducible_m * irreducible_m + residual_sq).sqrt()
}
FrameConvention::CommonFrameTie => (irreducible_m * irreducible_m + residual_sq).sqrt(),
FrameConvention::CommonEphemeris => 0.0,
};
let denom = reducible_m + irreducible_m;
let irreducible_fraction = if denom > 0.0 {
irreducible_m / denom
} else {
0.0
};
InteropBudget {
convention,
reducible_m,
irreducible_m,
total_m,
irreducible_fraction,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cloud(n: usize) -> Vec<Vec3> {
(0..n)
.map(|k| {
let a = (k as f64) * 0.11; let r = 3.84e8;
[r * a.cos(), r * a.sin(), 0.20 * r * (0.5 * a).sin()]
})
.collect()
}
fn apply(d: &Datum7, p: &[Vec3]) -> Vec<Vec3> {
p.iter()
.map(|q| crate::lunar_datum::apply_datum7(d, *q))
.collect()
}
#[test]
fn helmert_fit_recovers_a_known_datum() {
let from = cloud(120);
let truth = Datum7 {
t_m: [1.5, -0.7, 0.3],
scale: 2.0e-9,
rot_rad: [3.0e-9, -5.0e-9, 4.0e-9],
};
let to = apply(&truth, &from);
let fit = helmert_fit(&from, &to);
assert!((fit.datum.t_m[0] - 1.5).abs() < 1e-6);
assert!((fit.datum.scale - 2.0e-9).abs() < 1e-12);
assert!((fit.datum.rot_rad[1] - (-5.0e-9)).abs() < 1e-12);
assert!(fit.residual_rms_m < 1e-6, "known transform must fit to ~0");
}
#[test]
fn rotation_fit_isolates_orientation_and_residual_bounds_full_helmert() {
let from = cloud(120);
let truth = Datum7 {
t_m: [0.0; 3],
scale: 0.0,
rot_rad: [0.0, 4.0e-9, -6.0e-9],
};
let to = apply(&truth, &from);
let (theta, rot_res) = rotation_fit(&from, &to);
assert!((theta[1] - 4.0e-9).abs() < 1e-12 && (theta[2] - (-6.0e-9)).abs() < 1e-12);
let full = helmert_fit(&from, &to).residual_rms_m;
assert!(
full <= rot_res + 1e-9,
"adding scale cannot worsen the residual"
);
assert!(rot_res < 1e-6);
}
fn cloud_scaled(n: usize, r: f64) -> Vec<Vec3> {
(0..n)
.map(|k| {
let a = (k as f64) * 0.11;
[r * a.cos(), r * a.sin(), 0.20 * r * (0.5 * a).sin()]
})
.collect()
}
#[test]
fn provenance_split_recovers_frametie_and_excess() {
let frametie: Vec3 = [1.5e-9, -2.3e-9, 0.8e-9];
let known_excess: Vec3 = [0.4e-9, -0.7e-9, 1.1e-9];
let moon_rot: Vec3 = [
frametie[0] + known_excess[0],
frametie[1] + known_excess[1],
frametie[2] + known_excess[2],
];
let lever = 3.84e8_f64;
let moon_from = cloud(120);
let moon_truth = Datum7 {
t_m: [0.0; 3],
scale: 0.0,
rot_rad: moon_rot,
};
let moon_to = apply(&moon_truth, &moon_from);
let planet_truth = Datum7 {
t_m: [0.0; 3],
scale: 0.0,
rot_rad: frametie,
};
let planet_pairs: Vec<(Vec<Vec3>, Vec<Vec3>)> = [5.7e10_f64, 1.08e11, 1.50e11, 2.28e11]
.iter()
.map(|&r| {
let from = cloud_scaled(120, r);
let to = apply(&planet_truth, &from);
(from, to)
})
.collect();
let split = provenance_split(&moon_from, &moon_to, &planet_pairs, lever);
for i in 0..3 {
assert!(
(split.theta_frametie[i] - frametie[i]).abs() < 1e-12,
"theta_frametie[{i}]: got {:.6e}, expected {:.6e}",
split.theta_frametie[i],
frametie[i]
);
assert!(
(split.theta_excess[i] - known_excess[i]).abs() < 1e-12,
"theta_excess[{i}]: got {:.6e}, expected {:.6e}",
split.theta_excess[i],
known_excess[i]
);
}
let excess_norm =
(known_excess[0].powi(2) + known_excess[1].powi(2) + known_excess[2].powi(2)).sqrt();
let expected_irr = excess_norm * lever;
assert!(
(split.irreducible_m - expected_irr).abs() / expected_irr < 1e-6,
"irreducible_m: got {:.6e}, expected {:.6e}",
split.irreducible_m,
expected_irr
);
}
#[test]
fn provenance_split_zero_excess_gives_near_zero_irreducible() {
let frametie: Vec3 = [1.5e-9, -2.3e-9, 0.8e-9];
let lever = 3.84e8_f64;
let moon_from = cloud(120);
let moon_truth = Datum7 {
t_m: [0.0; 3],
scale: 0.0,
rot_rad: frametie,
};
let moon_to = apply(&moon_truth, &moon_from);
let planet_truth = Datum7 {
t_m: [0.0; 3],
scale: 0.0,
rot_rad: frametie,
};
let planet_pairs: Vec<(Vec<Vec3>, Vec<Vec3>)> = [5.7e10_f64, 1.08e11, 2.28e11]
.iter()
.map(|&r| {
let from = cloud_scaled(120, r);
let to = apply(&planet_truth, &from);
(from, to)
})
.collect();
let split = provenance_split(&moon_from, &moon_to, &planet_pairs, lever);
assert!(
split.irreducible_m < 1e-3,
"zero excess must give irreducible_m < 1e-3 m, got {:.3e}",
split.irreducible_m
);
}
#[test]
fn consistency_tolerance_monotonic_in_budget() {
let small = consistency_tolerance(5.0, 1_737_400.0, None);
let large = consistency_tolerance(10.0, 1_737_400.0, None);
assert!(large.max_origin_m > small.max_origin_m);
assert!(large.max_scale > small.max_scale);
assert!(large.max_rotation_rad > small.max_rotation_rad);
}
#[test]
fn consistency_tolerance_per_provider_shrinks_tolerances() {
use crate::lunar_identifiability::DatumIdentifiability;
let di = DatumIdentifiability {
info: vec![vec![0.0; 7]; 7],
n_obs: 0,
eigenvalues: vec![0.0; 7],
defect: 0,
origin_scale_corr: 0.0,
degeneracy_metric: 0.0,
origin_crlb_m: 2.0,
crlb_diag: vec![0.0; 7],
};
let base = consistency_tolerance(5.0, 1_737_400.0, None);
let with_pp = consistency_tolerance(5.0, 1_737_400.0, Some(&di));
assert!(
with_pp.max_origin_m < base.max_origin_m,
"per_provider shrinks max_origin_m: {} vs {}",
with_pp.max_origin_m,
base.max_origin_m
);
assert!(with_pp.max_scale < base.max_scale);
assert!(with_pp.max_rotation_rad < base.max_rotation_rad);
}
#[test]
fn consistency_tolerance_worked_value() {
let tol = consistency_tolerance(5.0, 1_737_400.0, None);
let expected = 5.0_f64 / 1_737_400.0_f64;
let rel = (tol.max_rotation_rad - expected).abs() / expected;
assert!(
rel < 1e-15,
"max_rotation_rad rel error {rel} exceeds 1e-15"
);
}
#[test]
fn consistency_tolerance_binding_is_rotation_at_lunar_lever_arm() {
let tol = consistency_tolerance(5.0, 1_737_400.0, None);
assert_eq!(tol.binding, "rotation");
}
fn real_splits() -> [ProvenanceSplit; 3] {
[
ProvenanceSplit {
raw_rms_m: 2.3955,
rot_residual_m: 0.1387,
theta_moon: [0.0; 3],
theta_frametie: [0.0; 3],
theta_excess: [0.0; 3],
reducible_m: 0.8845,
irreducible_m: 1.8741,
},
ProvenanceSplit {
raw_rms_m: 2.0148,
rot_residual_m: 0.2789,
theta_moon: [0.0; 3],
theta_frametie: [0.0; 3],
theta_excess: [0.0; 3],
reducible_m: 0.4288,
irreducible_m: 2.4056,
},
ProvenanceSplit {
raw_rms_m: 0.7236,
rot_residual_m: 0.2124,
theta_moon: [0.0; 3],
theta_frametie: [0.0; 3],
theta_excess: [0.0; 3],
reducible_m: 1.0209,
irreducible_m: 0.5884,
},
]
}
#[test]
fn interop_budget_common_ephemeris_is_zero() {
let splits = real_splits();
let b = interop_budget(&splits, FrameConvention::CommonEphemeris);
assert_eq!(b.total_m, 0.0);
}
#[test]
fn interop_budget_ordering_and_bounds() {
let splits = real_splits();
let pp = interop_budget(&splits, FrameConvention::PerProvider);
let cft = interop_budget(&splits, FrameConvention::CommonFrameTie);
let ce = interop_budget(&splits, FrameConvention::CommonEphemeris);
assert!(
cft.total_m > 0.0,
"CommonFrameTie must be > 0, got {}",
cft.total_m
);
assert!(
cft.total_m < pp.total_m,
"CommonFrameTie {} must be < PerProvider {}",
cft.total_m,
pp.total_m
);
assert!(ce.total_m <= cft.total_m);
assert!(cft.total_m <= pp.total_m);
}
#[test]
fn interop_budget_design_law_dynamics_dominates() {
let splits = real_splits();
let b = interop_budget(&splits, FrameConvention::CommonFrameTie);
assert!(
b.irreducible_fraction > 0.5,
"design law: dynamics must dominate (irreducible_fraction={})",
b.irreducible_fraction
);
}
}