use crate::deepspace_od::Srif;
use crate::fim::{information_matrix, sym_eig};
use crate::observability_gramian::{
observability_matrix, rank_from_singular_values, singular_values, Mat, ObsEpoch, N_PLANAR,
};
#[derive(Clone, Debug)]
pub struct SrifArcPoint {
pub epoch_index: usize,
pub arc_time: f64,
pub n_rows: usize,
pub gramian_rank: usize,
pub gramian_condition: f64,
pub srif_posterior_wellposed: bool,
pub srif_condition: f64,
}
fn floored_condition(eigs_ascending: &[f64], floor: f64) -> f64 {
let lmax = eigs_ascending.last().copied().unwrap_or(0.0);
let lmin = eigs_ascending.first().copied().unwrap_or(0.0);
if lmax <= 0.0 || lmin <= floor * lmax {
f64::INFINITY
} else {
lmax / lmin
}
}
fn srif_over_arc(o: &Mat, n: usize) -> Srif {
let mut srif = Srif::new(n);
for row in o {
srif.measurement_update(row, 0.0, 1.0);
}
srif
}
pub fn srif_cross_validation(epochs: &[ObsEpoch], rel_tol: f64) -> Vec<SrifArcPoint> {
let floor = rel_tol * rel_tol;
let mut out = Vec::with_capacity(epochs.len());
let mut arc = 0.0;
for (k, ep) in epochs.iter().enumerate() {
arc += ep.dt;
let sub = &epochs[..=k];
let (o, _w) = observability_matrix(sub);
let sv = singular_values(&o);
let gramian_rank = rank_from_singular_values(&sv, rel_tol);
let ones = vec![1.0; o.len()];
let gram = information_matrix(&o, &ones);
let gram_eig = sym_eig(&gram);
let gramian_condition = if gramian_rank == N_PLANAR {
floored_condition(&gram_eig.values, floor)
} else {
f64::INFINITY
};
let srif = srif_over_arc(&o, N_PLANAR);
let (_x, p) = srif.solve();
let p_finite = p.iter().flatten().all(|v| v.is_finite());
let p_eig = sym_eig(&p);
let srif_condition = if p_finite {
floored_condition(&p_eig.values, floor)
} else {
f64::INFINITY
};
let srif_posterior_wellposed = srif_condition.is_finite();
out.push(SrifArcPoint {
epoch_index: k,
arc_time: arc,
n_rows: o.len(),
gramian_rank,
gramian_condition,
srif_posterior_wellposed,
srif_condition,
});
}
out
}
pub fn full_rank_transition(points: &[SrifArcPoint]) -> Option<usize> {
points
.iter()
.find(|p| p.gramian_rank == N_PLANAR)
.map(|p| p.epoch_index)
}
pub fn srif_finite_transition(points: &[SrifArcPoint]) -> Option<usize> {
points
.iter()
.find(|p| p.srif_posterior_wellposed)
.map(|p| p.epoch_index)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cr3bp::EARTH_MOON_MU;
use crate::intersat_range::{range_row, PlanarState};
use crate::observability_gramian::{planar_propagate, planar_state_stm};
fn single_link_arc(n_epochs: usize, arc: f64) -> Vec<ObsEpoch> {
let chief: PlanarState = [1.10, 0.02, 0.05, -0.50];
let reference: PlanarState = [1.02, -0.03, -0.06, -0.55];
let mu = EARTH_MOON_MU;
let mut epochs = Vec::with_capacity(n_epochs);
let mut prev = 0.0;
for k in 0..n_epochs {
let t = arc * (k as f64) / ((n_epochs - 1) as f64);
let (cs, phi) = planar_state_stm(&chief, mu, t, 3000);
let rs = planar_propagate(&reference, mu, t, 3000);
let (_rho, r_row) = range_row(&cs, &rs);
epochs.push(ObsEpoch {
h: vec![r_row.to_vec()],
phi: phi.iter().map(|r| r.to_vec()).collect(),
dt: t - prev,
});
prev = t;
}
epochs
}
#[test]
fn srif_posterior_finite_exactly_at_full_rank() {
let rel_tol = 1e-6;
let epochs = single_link_arc(24, 0.06);
let points = srif_cross_validation(&epochs, rel_tol);
let short = &points[1];
assert!(
short.gramian_rank < N_PLANAR,
"short arc should be rank-deficient, got rank {}",
short.gramian_rank
);
assert!(
!short.srif_posterior_wellposed && short.srif_condition.is_infinite(),
"short-arc SRIF posterior must be singular (cond {})",
short.srif_condition
);
let full = points.last().unwrap();
assert_eq!(full.gramian_rank, N_PLANAR, "full observability over arc");
assert!(
full.srif_posterior_wellposed && full.srif_condition.is_finite(),
"full-rank SRIF posterior must be finite (cond {})",
full.srif_condition
);
assert!(
full.gramian_condition.is_finite(),
"full-rank Gram condition must be finite"
);
let ratio = full.srif_condition / full.gramian_condition;
assert!(
(0.1..=10.0).contains(&ratio),
"SRIF condition {:.3e} not the same order as Gram condition {:.3e} (ratio {ratio:.3e})",
full.srif_condition,
full.gramian_condition
);
assert_eq!(
full_rank_transition(&points),
srif_finite_transition(&points),
"SRIF posterior-finite transition must equal the observable rank-4 transition"
);
}
#[test]
fn wellposed_is_persistent_along_the_arc() {
let points = srif_cross_validation(&single_link_arc(24, 0.06), 1e-6);
let mut seen_wellposed = false;
for p in &points {
if p.srif_posterior_wellposed {
seen_wellposed = true;
}
if seen_wellposed {
assert!(
p.srif_posterior_wellposed,
"SRIF lost well-posedness at epoch {} after gaining it",
p.epoch_index
);
}
}
assert!(
seen_wellposed,
"the arc should reach a well-posed posterior"
);
}
}