use crate::resilience::arch::{PntArchitecture, SourceKind};
use std::collections::{BTreeMap, BTreeSet};
pub fn effective_diversity(arch: &PntArchitecture) -> f64 {
let mut group_q: BTreeMap<u32, f64> = BTreeMap::new();
for s in &arch.sources {
*group_q.entry(s.independence_group).or_insert(0.0) += s.quality.clamp(0.0, 1.0);
}
let total: f64 = group_q.values().sum();
if total <= 0.0 {
return 0.0;
}
let sum_sq: f64 = group_q.values().map(|q| (q / total).powi(2)).sum();
if sum_sq <= 0.0 {
0.0
} else {
1.0 / sum_sq
}
}
pub fn common_mode_loss(arch: &PntArchitecture, defeated: &BTreeSet<SourceKind>) -> f64 {
let total: f64 = arch.sources.iter().map(|s| s.quality.clamp(0.0, 1.0)).sum();
if total <= 0.0 {
return 0.0;
}
let lost: f64 = arch
.sources
.iter()
.filter(|s| defeated.contains(&s.kind))
.map(|s| s.quality.clamp(0.0, 1.0))
.sum();
lost / total
}
pub fn gnss_rf_common_mode_loss(arch: &PntArchitecture) -> f64 {
let defeated: BTreeSet<SourceKind> = arch
.sources
.iter()
.map(|s| s.kind)
.filter(|k| k.is_gnss_rf())
.collect();
common_mode_loss(arch, &defeated)
}
pub fn spofs(arch: &PntArchitecture, viability: f64) -> Vec<u32> {
let groups: BTreeSet<u32> = arch.sources.iter().map(|s| s.independence_group).collect();
let mut out = Vec::new();
for &g in &groups {
let reduced = PntArchitecture {
name: arch.name.clone(),
sources: arch
.sources
.iter()
.copied()
.filter(|s| s.independence_group != g)
.collect(),
techniques: arch.techniques.clone(),
};
if effective_diversity(&reduced) < viability {
out.push(g);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resilience::arch::PntSource;
fn three_equal(groups: [u32; 3]) -> PntArchitecture {
PntArchitecture::new(
"x",
vec![
PntSource::new(SourceKind::GnssL1, groups[0], 1.0),
PntSource::new(SourceKind::GnssL5, groups[1], 1.0),
PntSource::new(SourceKind::Inertial, groups[2], 1.0),
],
[],
)
}
#[test]
fn inverse_simpson_counts_independent_domains() {
assert!((effective_diversity(&three_equal([1, 2, 3])) - 3.0).abs() < 1e-12);
assert!((effective_diversity(&three_equal([1, 1, 1])) - 1.0).abs() < 1e-12);
}
#[test]
fn common_mode_defeats_correlated_sources() {
let all_gnss = PntArchitecture::new(
"gnss",
vec![
PntSource::new(SourceKind::GnssL1, 1, 1.0),
PntSource::new(SourceKind::GnssL5, 2, 1.0),
],
[],
);
assert!((gnss_rf_common_mode_loss(&all_gnss) - 1.0).abs() < 1e-12);
let defeated: BTreeSet<SourceKind> = [SourceKind::GnssL1].into_iter().collect();
assert!((common_mode_loss(&all_gnss, &defeated) - 0.5).abs() < 1e-12);
}
#[test]
fn spofs_flag_only_fragile_architectures() {
let two = three_equal([1, 1, 2]); assert_eq!(spofs(&two, 1.5), vec![1, 2]);
let four = PntArchitecture::new(
"four",
vec![
PntSource::new(SourceKind::GnssMultiBand, 1, 1.0),
PntSource::new(SourceKind::Inertial, 2, 1.0),
PntSource::new(SourceKind::Clock, 3, 1.0),
PntSource::new(SourceKind::Eloran, 4, 1.0),
],
[],
);
assert!(spofs(&four, 1.5).is_empty());
}
}