use crate::lunar_llr_geometry::Vec3;
pub const N_GAUGE: usize = 9;
pub const IDX_SCALE: usize = 3;
pub const IDX_OFFSET: usize = 7;
pub const IDX_RATE: usize = 8;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RateFrameJacobian {
pub d_alpha_d_scale: f64,
pub d_alpha_d_velocity: f64,
pub d_alpha_d_radial: f64,
}
pub fn rate_frame_jacobian(t_tt_jc: f64) -> RateFrameJacobian {
let mu = crate::forces::MU_MOON;
let r = crate::lunar_time::RE_MOON_M;
let c2 = crate::lunar_time::C2_M2_S2;
let u_moon = mu / r;
let g_moon = mu / (r * r);
let v = crate::lunar_time::moon_geocentric_velocity_m_s(t_tt_jc);
let v_mag = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
RateFrameJacobian {
d_alpha_d_scale: u_moon / c2, d_alpha_d_velocity: -v_mag / c2, d_alpha_d_radial: g_moon / c2, }
}
pub const T_BASE_S: f64 = 86_400.0;
pub fn oneway_range_row(
r_orbiter_inertial: Vec3,
beacon_pa_body_m: Vec3,
t_tt_jc: f64,
elapsed_s: f64,
) -> [f64; 9] {
let d7 =
crate::lunar_datum::orbiter_range_row_datum7(r_orbiter_inertial, beacon_pa_body_m, t_tt_jc);
let mut row = [0.0_f64; N_GAUGE];
row[..7].copy_from_slice(&d7);
row[IDX_OFFSET] = 1.0;
row[IDX_RATE] = elapsed_s / T_BASE_S;
row
}
pub fn twoway_range_row(
r_orbiter_inertial: Vec3,
beacon_pa_body_m: Vec3,
t_tt_jc: f64,
) -> [f64; 9] {
let d7 =
crate::lunar_datum::orbiter_range_row_datum7(r_orbiter_inertial, beacon_pa_body_m, t_tt_jc);
let mut row = [0.0_f64; N_GAUGE];
row[..7].copy_from_slice(&d7);
row
}
pub fn time_tie_row() -> [f64; 9] {
let mut row = [0.0_f64; N_GAUGE];
row[IDX_OFFSET] = 1.0;
row
}
pub fn rate_tie_row(t_tt_jc: f64) -> [f64; 9] {
let jac = rate_frame_jacobian(t_tt_jc);
let mut row = [0.0_f64; N_GAUGE];
row[IDX_RATE] = 1.0;
row[IDX_SCALE] = -jac.d_alpha_d_scale;
row
}
pub fn assemble_coupled_info(blocks: &[(Vec<[f64; 9]>, f64)]) -> Vec<Vec<f64>> {
let mut combined = vec![vec![0.0_f64; N_GAUGE]; N_GAUGE];
for (rows, sigma) in blocks {
if rows.is_empty() {
continue;
}
let weight = 1.0 / (sigma * sigma);
let r_moon = crate::lunar_time::RE_MOON_M;
let pre_rows: Vec<Vec<f64>> = rows
.iter()
.map(|r| {
let mut row = r.to_vec();
row[3..7].iter_mut().for_each(|v| *v /= r_moon);
row
})
.collect();
let block_weights = vec![weight; pre_rows.len()];
let block_info = crate::fim::information_matrix(&pre_rows, &block_weights);
for (ci, bi) in combined.iter_mut().zip(block_info.iter()) {
for (cv, bv) in ci.iter_mut().zip(bi.iter()) {
*cv += bv;
}
}
}
combined
}
#[derive(Clone, Copy, Debug)]
pub struct CoupledGaugeClass {
pub defect: usize,
pub dim_spatial: usize,
pub dim_temporal: usize,
pub coupled_dim: usize,
pub p_st_norm: f64,
}
fn classify_from_null_space(null_space: &[Vec<f64>], defect: usize) -> CoupledGaugeClass {
if defect == 0 {
return CoupledGaugeClass {
defect: 0,
dim_spatial: 0,
dim_temporal: 0,
coupled_dim: 0,
p_st_norm: 0.0,
};
}
let subspace_rank = |row_start: usize, row_end: usize| -> usize {
let p = row_end - row_start;
let mut gram = vec![vec![0.0_f64; p]; p];
for i in 0..p {
for j in i..p {
let s: f64 = null_space[row_start + i]
.iter()
.zip(null_space[row_start + j].iter())
.map(|(&a, &b)| a * b)
.sum();
gram[i][j] = s;
gram[j][i] = s;
}
}
crate::fim::sym_eig(&gram)
.values
.iter()
.filter(|&&v| v > 1e-9)
.count()
};
let rs = subspace_rank(0, 7); let rt = subspace_rank(7, 9);
let dim_spatial = defect - rt;
let dim_temporal = defect - rs;
debug_assert!(
rs + rt >= defect,
"rs={rs} + rt={rt} < defect={defect}: coupled_dim would underflow"
);
let coupled_dim = (rs + rt).saturating_sub(defect);
let mut p_st_sq = 0.0_f64;
for i in 0..7 {
for j in 7..9 {
let p_ij: f64 = null_space[i]
.iter()
.zip(null_space[j].iter())
.map(|(&a, &b)| a * b)
.sum();
p_st_sq += p_ij * p_ij;
}
}
CoupledGaugeClass {
defect,
dim_spatial,
dim_temporal,
coupled_dim,
p_st_norm: p_st_sq.sqrt(),
}
}
pub fn classify_null_space(info: &[Vec<f64>], rel_tol: f64) -> CoupledGaugeClass {
let cr = crate::fim::crlb(info, rel_tol);
classify_from_null_space(&cr.null_space, cr.defect)
}
const K_MARG: [usize; 2] = [IDX_SCALE, IDX_OFFSET];
const M_MARG: [usize; 7] = [0, 1, 2, 4, 5, 6, 8];
pub fn coupled_marginal_fisher(info: &[Vec<f64>]) -> [[f64; 2]; 2] {
let i_kk = [
[info[K_MARG[0]][K_MARG[0]], info[K_MARG[0]][K_MARG[1]]],
[info[K_MARG[1]][K_MARG[0]], info[K_MARG[1]][K_MARG[1]]],
];
let i_km: [[f64; 7]; 2] =
std::array::from_fn(|ki| std::array::from_fn(|mi| info[K_MARG[ki]][M_MARG[mi]]));
let i_mm: Vec<Vec<f64>> = M_MARG
.iter()
.map(|&r| M_MARG.iter().map(|&c| info[r][c]).collect::<Vec<f64>>())
.collect();
let d_inv = crate::fim::crlb(&i_mm, 1e-12).pseudo_covariance;
let b: [[f64; 7]; 2] = std::array::from_fn(|ki| {
std::array::from_fn(|j| {
i_km[ki]
.iter()
.zip(d_inv.iter())
.map(|(v, dl)| v * dl[j])
.sum::<f64>()
})
});
let c: [[f64; 2]; 2] = std::array::from_fn(|ki| {
std::array::from_fn(|kj| {
b[ki]
.iter()
.zip(i_km[kj].iter())
.map(|(bv, ikv)| bv * ikv)
.sum::<f64>()
})
});
[
[i_kk[0][0] - c[0][0], i_kk[0][1] - c[0][1]],
[i_kk[1][0] - c[1][0], i_kk[1][1] - c[1][1]],
]
}
pub fn coupled_marginal_eigs(info: &[Vec<f64>]) -> [f64; 2] {
let s = coupled_marginal_fisher(info);
let s_mat = vec![vec![s[0][0], s[0][1]], vec![s[1][0], s[1][1]]];
let eig = crate::fim::sym_eig(&s_mat);
[eig.values[0], eig.values[1]]
}
#[cfg(test)]
mod tests {
use super::*;
const T_J2000: f64 = 0.0;
const REL_TOL_GR: f64 = 1e-3;
const REL_TOL_VEL: f64 = 5e-2;
fn rel_err(got: f64, expect: f64) -> f64 {
((got - expect) / expect).abs()
}
#[test]
fn d_alpha_d_scale_approx_3_140e_minus_11() {
let jac = rate_frame_jacobian(T_J2000);
let expected = 3.140e-11_f64;
assert!(
jac.d_alpha_d_scale > 0.0,
"d_alpha_d_scale must be positive, got {}",
jac.d_alpha_d_scale
);
assert!(
rel_err(jac.d_alpha_d_scale, expected) < REL_TOL_GR,
"d_alpha_d_scale {:.6e} deviates from {:.6e} by {:.2e} (tol {:.2e})",
jac.d_alpha_d_scale,
expected,
rel_err(jac.d_alpha_d_scale, expected),
REL_TOL_GR
);
}
#[test]
fn d_alpha_d_radial_approx_1_807e_minus_17() {
let jac = rate_frame_jacobian(T_J2000);
let expected = 1.807e-17_f64;
assert!(
jac.d_alpha_d_radial > 0.0,
"d_alpha_d_radial must be positive, got {}",
jac.d_alpha_d_radial
);
assert!(
rel_err(jac.d_alpha_d_radial, expected) < REL_TOL_GR,
"d_alpha_d_radial {:.6e} deviates from {:.6e} by {:.2e} (tol {:.2e})",
jac.d_alpha_d_radial,
expected,
rel_err(jac.d_alpha_d_radial, expected),
REL_TOL_GR
);
}
#[test]
fn d_alpha_d_velocity_approx_neg_1_11e_minus_14() {
let jac = rate_frame_jacobian(T_J2000);
let expected = -1.11e-14_f64;
assert!(
jac.d_alpha_d_velocity < 0.0,
"d_alpha_d_velocity must be negative, got {}",
jac.d_alpha_d_velocity
);
assert!(
rel_err(jac.d_alpha_d_velocity, expected) < REL_TOL_VEL,
"d_alpha_d_velocity {:.6e} deviates from {:.6e} by {:.2e} (tol {:.2e})",
jac.d_alpha_d_velocity,
expected,
rel_err(jac.d_alpha_d_velocity, expected),
REL_TOL_VEL
);
}
#[test]
fn d_alpha_d_radial_equals_scale_over_r_moon() {
let jac = rate_frame_jacobian(T_J2000);
let r = crate::lunar_time::RE_MOON_M;
let expected = jac.d_alpha_d_scale / r;
let abs_err = (jac.d_alpha_d_radial - expected).abs();
assert!(
abs_err < expected.abs() * 1e-12,
"d_alpha_d_radial {:.6e} != d_alpha_d_scale/RE_MOON_M {:.6e} (abs err {:.2e})",
jac.d_alpha_d_radial,
expected,
abs_err
);
}
#[test]
fn index_consts_are_correct() {
assert_eq!(N_GAUGE, 9);
assert_eq!(IDX_SCALE, 3);
assert_eq!(IDX_OFFSET, 7);
assert_eq!(IDX_RATE, 8);
}
const T0_FIXTURE: f64 = (2_460_310.5 - 2_451_545.0) / 36_525.0;
#[test]
fn time_tie_row_equals_pure_offset_vector() {
let row = time_tie_row();
let expected: [f64; 9] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0];
assert_eq!(row, expected, "time_tie_row must equal [0,0,0,0,0,0,0,1,0]");
}
#[test]
fn rate_tie_row_has_correct_structure() {
let jac = rate_frame_jacobian(T_J2000);
let row = rate_tie_row(T_J2000);
assert_eq!(row[IDX_RATE], 1.0, "IDX_RATE must be 1.0");
assert_eq!(
row[IDX_SCALE], -jac.d_alpha_d_scale,
"IDX_SCALE must be -d_alpha_d_scale"
);
assert!(
row[IDX_SCALE] < 0.0,
"IDX_SCALE must be negative (d_alpha_d_scale > 0)"
);
for (i, &v) in row.iter().enumerate() {
if i != IDX_RATE && i != IDX_SCALE {
assert_eq!(v, 0.0, "rate_tie_row[{i}] must be 0.0, got {v}");
}
}
}
#[test]
fn twoway_range_row_has_zero_at_offset_and_rate() {
let r_orb =
crate::lunar_datum::orbiter_position(100.0, 85.0, 30.0, 40.0, T0_FIXTURE, T0_FIXTURE);
let beacon: [f64; 3] = [3.0e5, 1.70e6, 2.0e5];
let row = twoway_range_row(r_orb, beacon, T0_FIXTURE);
assert_eq!(row[IDX_OFFSET], 0.0, "twoway IDX_OFFSET must be 0.0");
assert_eq!(row[IDX_RATE], 0.0, "twoway IDX_RATE must be 0.0");
}
#[test]
fn oneway_range_row_has_correct_temporal_entries() {
let r_orb =
crate::lunar_datum::orbiter_position(100.0, 85.0, 30.0, 40.0, T0_FIXTURE, T0_FIXTURE);
let beacon: [f64; 3] = [3.0e5, 1.70e6, 2.0e5];
let elapsed = 3_600.0_f64;
let row = oneway_range_row(r_orb, beacon, T0_FIXTURE, elapsed);
assert_eq!(row[IDX_OFFSET], 1.0, "oneway IDX_OFFSET must be 1.0");
assert_eq!(
row[IDX_RATE],
elapsed / T_BASE_S,
"oneway IDX_RATE must equal elapsed/T_BASE_S"
);
}
#[test]
fn twoway_spatial_equals_oneway_spatial_at_same_geometry() {
let r_orb =
crate::lunar_datum::orbiter_position(100.0, 85.0, 30.0, 40.0, T0_FIXTURE, T0_FIXTURE);
let beacon: [f64; 3] = [3.0e5, 1.70e6, 2.0e5];
let elapsed = 3_600.0_f64;
let oneway = oneway_range_row(r_orb, beacon, T0_FIXTURE, elapsed);
let twoway = twoway_range_row(r_orb, beacon, T0_FIXTURE);
for i in 0..7 {
assert_eq!(
oneway[i], twoway[i],
"spatial[{i}]: oneway={}, twoway={}",
oneway[i], twoway[i]
);
}
}
#[test]
fn oneway_spatial_equals_orbiter_range_row_datum7() {
let r_orb =
crate::lunar_datum::orbiter_position(100.0, 85.0, 30.0, 40.0, T0_FIXTURE, T0_FIXTURE);
let beacon: [f64; 3] = [3.0e5, 1.70e6, 2.0e5];
let elapsed = 3_600.0_f64;
let oneway = oneway_range_row(r_orb, beacon, T0_FIXTURE, elapsed);
let d7 = crate::lunar_datum::orbiter_range_row_datum7(r_orb, beacon, T0_FIXTURE);
for i in 0..7 {
assert_eq!(
oneway[i], d7[i],
"spatial[{i}]: oneway={}, datum7={}",
oneway[i], d7[i]
);
}
}
fn make_diag_info(diag: [f64; 9]) -> Vec<Vec<f64>> {
let mut m = vec![vec![0.0_f64; 9]; 9];
for (i, row) in m.iter_mut().enumerate() {
row[i] = diag[i];
}
m
}
#[test]
fn classify_direct_sum_null() {
let info = make_diag_info([1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0]);
let cls = classify_null_space(&info, 1e-9);
assert_eq!(cls.defect, 2, "defect");
assert_eq!(cls.dim_spatial, 1, "dim_spatial (e₃ is scale ∈ 0..7)");
assert_eq!(cls.dim_temporal, 1, "dim_temporal (e₇ is offset ∈ 7..9)");
assert_eq!(cls.coupled_dim, 0, "coupled_dim");
assert!(
cls.p_st_norm < 1e-9,
"p_st_norm should be ≈0 for direct-sum null, got {}",
cls.p_st_norm
);
}
#[test]
fn classify_coupled_null() {
let sqrt2 = 2.0_f64.sqrt();
let w3 = 1.0 / sqrt2;
let w7 = 1.0 / sqrt2;
let mut info = vec![vec![0.0_f64; 9]; 9];
for (i, row) in info.iter_mut().enumerate() {
row[i] = 1.0;
}
info[3][3] -= w3 * w3;
info[7][7] -= w7 * w7;
info[3][7] -= w3 * w7;
info[7][3] -= w7 * w3;
let cls = classify_null_space(&info, 1e-9);
assert_eq!(cls.defect, 1, "defect");
assert_eq!(cls.coupled_dim, 1, "coupled_dim");
assert!(
(cls.p_st_norm - 0.5_f64).abs() < 1e-9,
"p_st_norm should be ≈0.5, got {}",
cls.p_st_norm
);
assert_eq!(cls.dim_spatial, 0, "dim_spatial");
assert_eq!(cls.dim_temporal, 0, "dim_temporal");
}
#[test]
fn classify_basis_invariant() {
let info = make_diag_info([2.0, 3.0, 4.0, 0.0, 5.0, 6.0, 7.0, 0.0, 8.0]);
let cls = classify_null_space(&info, 1e-9);
assert_eq!(cls.defect, 2, "defect (basis-invariant)");
assert_eq!(cls.dim_spatial, 1, "dim_spatial (basis-invariant)");
assert_eq!(cls.dim_temporal, 1, "dim_temporal (basis-invariant)");
assert_eq!(cls.coupled_dim, 0, "coupled_dim (basis-invariant)");
assert!(
cls.p_st_norm < 1e-9,
"p_st_norm should be ≈0 (basis-invariant), got {}",
cls.p_st_norm
);
}
const T_TASK4: f64 = (2_460_310.5 - 2_451_545.0) / 36_525.0;
const BEACONS_TASK4: [[f64; 3]; 5] = [
[1.5e6, 0.3e6, 0.2e6],
[1.4e6, -0.4e6, 0.3e6],
[1.55e6, 0.2e6, -0.35e6],
[1.35e6, -0.25e6, -0.3e6],
[1.6e6, 0.05e6, 0.1e6],
];
fn well_posed_oneway_rows(u0_offset: f64) -> Vec<[f64; 9]> {
let epochs: [f64; 4] = std::array::from_fn(|k| T_TASK4 + (k as f64) * 2.0 / 36_525.0);
let mut rows = Vec::with_capacity(120);
for &beacon in &BEACONS_TASK4 {
for (ki, &t) in epochs.iter().enumerate() {
for j in 0..6_usize {
let r_sat = crate::lunar_datum::orbiter_position(
2000.0,
20.0 + 10.0 * j as f64,
60.0 * j as f64,
40.0 * j as f64 + u0_offset,
epochs[0],
t,
);
let elapsed = 3_600.0 * (ki as f64 + 1.0) * (j as f64 + 1.0);
rows.push(oneway_range_row(r_sat, beacon, t, elapsed));
}
}
}
rows
}
fn well_posed_twoway_rows() -> Vec<[f64; 9]> {
let epochs: [f64; 4] = std::array::from_fn(|k| T_TASK4 + (k as f64) * 2.0 / 36_525.0);
let mut rows = Vec::with_capacity(120);
for &beacon in &BEACONS_TASK4 {
for &t in &epochs {
for j in 0..6_usize {
let r_sat = crate::lunar_datum::orbiter_position(
2000.0,
20.0 + 10.0 * j as f64,
60.0 * j as f64,
40.0 * j as f64,
epochs[0],
t,
);
rows.push(twoway_range_row(r_sat, beacon, t));
}
}
}
rows
}
#[test]
fn coupled_marginal_well_posed_network_is_psd() {
let oneway_rows = well_posed_oneway_rows(0.0);
let info = assemble_coupled_info(&[(oneway_rows, 1.0)]);
let cls = classify_null_space(&info, 1e-9);
assert_eq!(
cls.defect, 0,
"well-posed multi-beacon network must have defect 0, got {}",
cls.defect
);
let e = coupled_marginal_eigs(&info);
assert!(
e[0] <= e[1],
"eigenvalues must be ascending: e[0]={:.4e}, e[1]={:.4e}",
e[0],
e[1]
);
assert!(
e[0] > 0.0,
"λ_min(S) must be strictly positive for well-posed network: {:.4e}",
e[0]
);
assert!(
e[1] > 0.0,
"λ_max(S) must be strictly positive for well-posed network: {:.4e}",
e[1]
);
}
#[test]
fn coupled_marginal_twoway_lift_three_orders() {
let n = 100.0_f64;
let c = 0.7_f64;
let eps = 1e-3_f64; let m_idx: [usize; 7] = [0, 1, 2, 4, 5, 6, 8];
let build_info = |n_ow: f64, n_tw: f64| -> Vec<Vec<f64>> {
let mut mat = vec![vec![0.0_f64; 9]; 9];
for &m in &m_idx {
mat[m][m] = n;
}
mat[3][3] = (n_ow + n_tw) * c * c + eps;
mat[3][7] = n_ow * c;
mat[7][3] = n_ow * c;
mat[7][7] = n_ow + eps;
mat
};
let e_ow = coupled_marginal_eigs(&build_info(n, 0.0));
let e_tw = coupled_marginal_eigs(&build_info(n, n));
assert!(
e_tw[0] > e_ow[0] * 1e3,
"two-way lift must raise λ_min by ≥3 orders: before={:.4e}, after={:.4e}",
e_ow[0],
e_tw[0]
);
assert!(
e_tw[0] > 0.0,
"lifted λ_min must be strictly positive: {:.4e}",
e_tw[0]
);
}
#[test]
fn coupled_marginal_twoway_equals_more_oneway_in_diverse_network() {
let oneway_base = well_posed_oneway_rows(0.0);
let twoway_rows = well_posed_twoway_rows();
let oneway_jitter = well_posed_oneway_rows(7.0);
let info_base = assemble_coupled_info(&[(oneway_base.clone(), 1.0)]);
let info_tw = assemble_coupled_info(&[(oneway_base.clone(), 1.0), (twoway_rows, 1.0)]);
let info_2ow = assemble_coupled_info(&[(oneway_base, 1.0), (oneway_jitter, 1.0)]);
let e_base = coupled_marginal_eigs(&info_base);
let e_tw = coupled_marginal_eigs(&info_tw);
let e_2ow = coupled_marginal_eigs(&info_2ow);
let lift_tw = e_tw[0] / e_base[0];
let lift_2ow = e_2ow[0] / e_base[0];
assert!(
lift_tw > 1.0,
"two-way lift must be > 1.0: lift_tw={:.4e}, e_base[0]={:.4e}, e_tw[0]={:.4e}",
lift_tw,
e_base[0],
e_tw[0]
);
assert!(
lift_2ow > 1.0,
"more-one-way lift must be > 1.0: lift_2ow={:.4e}, e_base[0]={:.4e}, e_2ow[0]={:.4e}",
lift_2ow,
e_base[0],
e_2ow[0]
);
let ratio = lift_tw / lift_2ow;
assert!(
(0.7..=1.4).contains(&ratio),
"lift ratio must be within [0.7, 1.4] (two-way ≈ more-one-way, the finding): \
lift_tw={:.4e}, lift_2ow={:.4e}, ratio={:.4e}",
lift_tw,
lift_2ow,
ratio
);
}
#[test]
fn coupled_marginal_fisher_is_symmetric_and_psd() {
let mut info = vec![vec![0.0_f64; 9]; 9];
for (i, row) in info.iter_mut().enumerate() {
row[i] = (i + 1) as f64; }
let s = coupled_marginal_fisher(&info);
assert!(
(s[0][1] - s[1][0]).abs() < 1e-12,
"Schur complement must be symmetric: S[0][1]={:.6e}, S[1][0]={:.6e}",
s[0][1],
s[1][0]
);
let e = coupled_marginal_eigs(&info);
assert!(
e[0] >= -1e-9,
"λ_min(S) must be ≥ −1e-9 (PSD within floating-point noise): {:.4e}",
e[0]
);
assert!(
e[1] >= -1e-9,
"λ_max(S) must be ≥ −1e-9 (PSD within floating-point noise): {:.4e}",
e[1]
);
assert!(
e[0] <= e[1],
"eigenvalues must be ascending: e[0]={:.4e}, e[1]={:.4e}",
e[0],
e[1]
);
}
#[test]
fn classify_is_invariant_under_null_basis_rotation() {
let inv_sqrt2 = 1.0_f64 / 2.0_f64.sqrt();
let mut u_axis = vec![vec![0.0_f64; 2]; 9];
u_axis[3][0] = 1.0; u_axis[4][1] = inv_sqrt2; u_axis[7][1] = inv_sqrt2;
let cls_axis = classify_from_null_space(&u_axis, 2);
assert_eq!(cls_axis.defect, 2, "axis: defect");
assert_eq!(
cls_axis.dim_spatial, 1,
"axis: dim_spatial (e₃ is pure spatial)"
);
assert_eq!(cls_axis.dim_temporal, 0, "axis: dim_temporal");
assert_eq!(cls_axis.coupled_dim, 1, "axis: coupled_dim");
assert!(
(cls_axis.p_st_norm - 0.5_f64).abs() < 1e-9,
"axis: p_st_norm should be 0.5, got {}",
cls_axis.p_st_norm
);
let theta: f64 = 0.6;
let (cos_t, sin_t) = (theta.cos(), theta.sin());
let mut u_rot = vec![vec![0.0_f64; 2]; 9];
for r in 0..9 {
u_rot[r][0] = u_axis[r][0] * cos_t + u_axis[r][1] * sin_t;
u_rot[r][1] = u_axis[r][0] * (-sin_t) + u_axis[r][1] * cos_t;
}
let cls_rot = classify_from_null_space(&u_rot, 2);
assert_eq!(cls_rot.defect, 2, "rotated: defect");
assert_eq!(cls_rot.dim_spatial, 1, "rotated: dim_spatial");
assert_eq!(cls_rot.dim_temporal, 0, "rotated: dim_temporal");
assert_eq!(cls_rot.coupled_dim, 1, "rotated: coupled_dim");
assert!(
(cls_rot.p_st_norm - 0.5_f64).abs() < 1e-9,
"rotated: p_st_norm should be 0.5, got {}",
cls_rot.p_st_norm
);
}
}