use crate::trace::gf2::{BoundaryMatrix, PersistencePairs};
use crate::util::DetHasher;
use std::collections::BTreeSet;
use std::fmt::Write;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TopologicalScore {
pub novelty: u32,
pub persistence_sum: u64,
pub fingerprint: u64,
}
impl TopologicalScore {
#[must_use]
pub const fn zero(fingerprint: u64) -> Self {
Self {
novelty: 0,
persistence_sum: 0,
fingerprint,
}
}
}
impl Ord for TopologicalScore {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.novelty
.cmp(&other.novelty)
.then(self.persistence_sum.cmp(&other.persistence_sum))
.then(other.fingerprint.cmp(&self.fingerprint))
}
}
impl PartialOrd for TopologicalScore {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ClassId {
pub birth: usize,
pub death: usize,
}
impl ClassId {
#[must_use]
pub const fn persistence(&self) -> Option<u64> {
if self.death == usize::MAX {
None
} else {
Some(self.death.saturating_sub(self.birth) as u64)
}
}
}
#[derive(Debug, Clone)]
pub struct EvidenceEntry {
pub class: ClassId,
pub is_novel: bool,
pub persistence: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct EvidenceLedger {
pub entries: Vec<EvidenceEntry>,
pub score: TopologicalScore,
}
impl EvidenceLedger {
#[must_use]
pub fn summary(&self) -> String {
let novel_count = self.entries.iter().filter(|e| e.is_novel).count();
let total = self.entries.len();
let finite_count = self.entries.iter().filter_map(|e| e.persistence).count();
let mut s = format!(
"score: novelty={}, persistence_sum={}, fingerprint={:#018x}\n",
self.score.novelty, self.score.persistence_sum, self.score.fingerprint
);
let _ = writeln!(
&mut s,
"classes: {total} total, {novel_count} novel, {finite_count} finite"
);
for e in &self.entries {
let tag = if e.is_novel { "NEW" } else { "old" };
let pers = e
.persistence
.map_or_else(|| "pers=∞".to_string(), |p| format!("pers={p}"));
let _ = writeln!(
&mut s,
" [{tag}] birth={}, death={}, {pers}",
e.class.birth,
if e.class.death == usize::MAX {
"∞".to_string()
} else {
e.class.death.to_string()
},
);
}
s
}
}
#[must_use]
pub fn score_persistence(
pairs: &PersistencePairs,
seen_classes: &mut BTreeSet<ClassId>,
fingerprint: u64,
) -> EvidenceLedger {
let mut entries = Vec::new();
let mut novelty = 0u32;
let mut persistence_sum = 0u64;
for &(birth, death) in &pairs.pairs {
let class = ClassId { birth, death };
let is_novel = seen_classes.insert(class);
let persistence = class.persistence();
if is_novel {
novelty += 1;
}
if let Some(p) = persistence {
persistence_sum = persistence_sum.saturating_add(p);
}
entries.push(EvidenceEntry {
class,
is_novel,
persistence,
});
}
for &birth in &pairs.unpaired {
let class = ClassId {
birth,
death: usize::MAX,
};
let is_novel = seen_classes.insert(class);
if is_novel {
novelty += 1;
}
entries.push(EvidenceEntry {
class,
is_novel,
persistence: None,
});
}
let score = TopologicalScore {
novelty,
persistence_sum,
fingerprint,
};
EvidenceLedger { entries, score }
}
#[must_use]
pub fn seed_fingerprint(seed: u64) -> u64 {
let mut h = DetHasher::default();
seed.hash(&mut h);
h.finish()
}
#[must_use]
pub fn score_boundary_matrix(
matrix: &BoundaryMatrix,
seen_classes: &mut BTreeSet<ClassId>,
fingerprint: u64,
) -> EvidenceLedger {
let reduced = matrix.reduce();
let pairs = reduced.persistence_pairs();
score_persistence(&pairs, seen_classes, fingerprint)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::trace::gf2::BoundaryMatrix;
#[test]
fn score_empty() {
let pairs = PersistencePairs {
pairs: vec![],
unpaired: vec![],
};
let mut seen = BTreeSet::new();
let ledger = score_persistence(&pairs, &mut seen, 42);
assert_eq!(ledger.score.novelty, 0);
assert_eq!(ledger.score.persistence_sum, 0);
assert_eq!(ledger.score.fingerprint, 42);
assert!(ledger.entries.is_empty());
}
#[test]
fn score_novel_classes() {
let pairs = PersistencePairs {
pairs: vec![(0, 5), (1, 8)],
unpaired: vec![3],
};
let mut seen = BTreeSet::new();
let ledger = score_persistence(&pairs, &mut seen, 100);
assert_eq!(ledger.score.novelty, 3); assert_eq!(ledger.score.persistence_sum, 5 + 7); assert_eq!(ledger.entries.len(), 3);
assert!(ledger.entries.iter().all(|e| e.is_novel));
}
#[test]
fn score_repeated_classes_not_novel() {
let pairs = PersistencePairs {
pairs: vec![(0, 5)],
unpaired: vec![],
};
let mut seen = BTreeSet::new();
let l1 = score_persistence(&pairs, &mut seen, 1);
assert_eq!(l1.score.novelty, 1);
let l2 = score_persistence(&pairs, &mut seen, 2);
assert_eq!(l2.score.novelty, 0);
assert_eq!(l2.score.persistence_sum, 5); assert!(!l2.entries[0].is_novel);
}
#[test]
fn score_ordering() {
let high = TopologicalScore {
novelty: 2,
persistence_sum: 10,
fingerprint: 100,
};
let low = TopologicalScore {
novelty: 1,
persistence_sum: 50,
fingerprint: 1,
};
assert!(high > low);
let a = TopologicalScore {
novelty: 1,
persistence_sum: 20,
fingerprint: 5,
};
let b = TopologicalScore {
novelty: 1,
persistence_sum: 10,
fingerprint: 1,
};
assert!(a > b);
let x = TopologicalScore {
novelty: 1,
persistence_sum: 10,
fingerprint: 5,
};
let y = TopologicalScore {
novelty: 1,
persistence_sum: 10,
fingerprint: 10,
};
assert!(x > y);
}
#[test]
fn score_determinism() {
let pairs = PersistencePairs {
pairs: vec![(0, 3), (2, 7)],
unpaired: vec![5],
};
let mut seen1 = BTreeSet::new();
let mut seen2 = BTreeSet::new();
let l1 = score_persistence(&pairs, &mut seen1, 42);
let l2 = score_persistence(&pairs, &mut seen2, 42);
assert_eq!(l1.score, l2.score);
}
#[test]
fn evidence_ledger_summary_format() {
let pairs = PersistencePairs {
pairs: vec![(0, 5)],
unpaired: vec![3],
};
let mut seen = BTreeSet::new();
let ledger = score_persistence(&pairs, &mut seen, 0xFF);
let summary = ledger.summary();
assert!(summary.contains("novelty=2"));
assert!(summary.contains("NEW"));
assert!(summary.contains("pers=5"));
assert!(summary.contains("pers=∞"));
}
#[test]
fn score_boundary_matrix_end_to_end() {
let mut d = BoundaryMatrix::zeros(7, 7);
d.set(0, 3);
d.set(1, 3);
d.set(0, 4);
d.set(2, 4);
d.set(1, 5);
d.set(2, 5);
d.set(3, 6);
d.set(4, 6);
d.set(5, 6);
let mut seen = BTreeSet::new();
let ledger = score_boundary_matrix(&d, &mut seen, 0);
assert!(ledger.score.novelty > 0 || !ledger.entries.is_empty());
}
#[test]
fn seed_fingerprint_deterministic() {
assert_eq!(seed_fingerprint(42), seed_fingerprint(42));
assert_ne!(seed_fingerprint(42), seed_fingerprint(43));
}
#[test]
fn class_id_persistence() {
let finite = ClassId {
birth: 3,
death: 10,
};
assert_eq!(finite.persistence(), Some(7));
let infinite = ClassId {
birth: 3,
death: usize::MAX,
};
assert_eq!(infinite.persistence(), None);
}
#[test]
fn topological_score_debug_clone_copy_eq() {
let s = TopologicalScore {
novelty: 3,
persistence_sum: 42,
fingerprint: 0xBEEF,
};
let dbg = format!("{s:?}");
assert!(dbg.contains("TopologicalScore"), "{dbg}");
assert!(dbg.contains("42"), "{dbg}");
let copied = s;
let cloned = s;
assert_eq!(copied, cloned);
assert_eq!(s, s);
}
#[test]
fn class_id_debug_clone_copy_hash() {
use std::collections::HashSet;
let c = ClassId {
birth: 1,
death: 10,
};
let dbg = format!("{c:?}");
assert!(dbg.contains("ClassId"), "{dbg}");
let copied = c;
let cloned = c;
assert_eq!(copied, cloned);
let mut set = HashSet::new();
set.insert(c);
assert!(set.contains(&c));
}
#[test]
fn evidence_entry_debug_clone() {
let e = EvidenceEntry {
class: ClassId { birth: 0, death: 5 },
is_novel: true,
persistence: Some(5),
};
let dbg = format!("{e:?}");
assert!(dbg.contains("EvidenceEntry"), "{dbg}");
let cloned = e;
assert_eq!(format!("{cloned:?}"), dbg);
}
#[test]
fn evidence_ledger_debug_clone() {
let ledger = EvidenceLedger {
entries: vec![],
score: TopologicalScore::zero(0),
};
let dbg = format!("{ledger:?}");
assert!(dbg.contains("EvidenceLedger"), "{dbg}");
let cloned = ledger;
assert_eq!(format!("{cloned:?}"), dbg);
}
}