use super::*;
use crate::inference::atlas_nerve::AtlasCoveringSide;
use crate::null_battery::ClaimNullCalibration;
use std::collections::{BTreeSet, HashMap};
pub const PERSISTENCE_MAX_POINTS: usize = 48;
pub const PERSISTENCE_H1_MAX_POINTS: usize = 256;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PersistenceBar {
pub birth: f64,
pub death: f64,
}
impl PersistenceBar {
pub fn persistence(&self) -> f64 {
self.death - self.birth
}
pub fn is_essential(&self) -> bool {
!self.death.is_finite()
}
}
#[derive(Clone, Debug)]
pub struct PersistenceDiagram {
pub h0: Vec<PersistenceBar>,
pub h1: Vec<PersistenceBar>,
pub h2: Vec<PersistenceBar>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BettiSignature {
pub b0: usize,
pub b1: usize,
pub b2: Option<usize>,
}
impl BettiSignature {
fn matches_expected(self, expected: Self) -> bool {
self.b0 == expected.b0
&& self.b1 == expected.b1
&& expected.b2.map_or(true, |b2| self.b2 == Some(b2))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PersistenceStabilityBand {
BelowLandmarkCap,
AtLandmarkCap,
}
impl PersistenceStabilityBand {
pub fn as_str(self) -> &'static str {
match self {
Self::BelowLandmarkCap => "below_landmark_cap",
Self::AtLandmarkCap => "at_landmark_cap",
}
}
}
#[derive(Clone, Debug)]
pub struct AtomTopologyPersistence {
pub raced_kind: SaeAtomBasisKind,
pub support_size: usize,
pub landmark_count: usize,
pub stability_band: PersistenceStabilityBand,
pub covering_side: AtlasCoveringSide,
pub support_mass: f64,
pub effective_n: f64,
pub support_ess: f64,
pub measured_betti: BettiSignature,
pub expected_betti: BettiSignature,
pub null_calibration: Option<ClaimNullCalibration>,
pub dominant_h1_persistence: f64,
pub dominant_h2_persistence: f64,
pub h0: Vec<PersistenceBar>,
pub h1: Vec<PersistenceBar>,
pub h2: Vec<PersistenceBar>,
pub contested: bool,
pub note: String,
}
#[derive(Debug, Clone, Copy)]
pub struct TopologyPersistenceCertificate<'a> {
pub atoms: &'a [Option<AtomTopologyPersistence>],
}
impl<'a> TopologyPersistenceCertificate<'a> {
pub fn new(atoms: &'a [Option<AtomTopologyPersistence>]) -> Self {
Self { atoms }
}
}
fn expected_betti_signature(
kind: &SaeAtomBasisKind,
finite_set_components: Option<usize>,
) -> Option<BettiSignature> {
match kind {
SaeAtomBasisKind::Periodic | SaeAtomBasisKind::Cylinder | SaeAtomBasisKind::Mobius => {
Some(BettiSignature {
b0: 1,
b1: 1,
b2: None,
})
}
SaeAtomBasisKind::Torus => Some(BettiSignature {
b0: 1,
b1: 2,
b2: Some(1),
}),
SaeAtomBasisKind::KleinBottle => Some(BettiSignature {
b0: 1,
b1: 2,
b2: Some(1),
}),
SaeAtomBasisKind::Sphere => Some(BettiSignature {
b0: 1,
b1: 0,
b2: Some(1),
}),
SaeAtomBasisKind::ProjectivePlane => Some(BettiSignature {
b0: 1,
b1: 1,
b2: Some(1),
}),
SaeAtomBasisKind::Linear
| SaeAtomBasisKind::Duchon
| SaeAtomBasisKind::EuclideanPatch
| SaeAtomBasisKind::Poincare => Some(BettiSignature {
b0: 1,
b1: 0,
b2: None,
}),
SaeAtomBasisKind::FiniteSet => finite_set_components.map(|b0| BettiSignature {
b0,
b1: 0,
b2: None,
}),
SaeAtomBasisKind::Precomputed(_) => None,
}
}
fn point_distance(points: ArrayView2<'_, f64>, i: usize, j: usize) -> f64 {
let mut acc = 0.0_f64;
for col in 0..points.ncols() {
let d = points[[i, col]] - points[[j, col]];
acc += d * d;
}
acc.sqrt()
}
fn farthest_point_subsample(points: ArrayView2<'_, f64>, target: usize) -> Vec<usize> {
farthest_point_subsample_weighted(points, None, target)
}
fn farthest_point_subsample_weighted(
points: ArrayView2<'_, f64>,
weights: Option<ArrayView1<'_, f64>>,
target: usize,
) -> Vec<usize> {
let n = points.nrows();
if n <= target {
return (0..n).collect();
}
let mut chosen = Vec::with_capacity(target);
let mut first = 0usize;
if let Some(w) = weights {
let mut best_w = f64::NEG_INFINITY;
for (row, &weight) in w.iter().enumerate() {
if weight > best_w {
best_w = weight;
first = row;
}
}
}
chosen.push(first);
let mean_weight = match weights {
Some(w) => w.iter().copied().sum::<f64>() / w.len().max(1) as f64,
None => 1.0,
};
let mut min_dist: Vec<f64> = (0..n).map(|i| point_distance(points, i, first)).collect();
while chosen.len() < target {
let mut best = 0usize;
let mut best_score = -1.0_f64;
for (i, &d) in min_dist.iter().enumerate() {
let weight_factor = weights.map(|w| w[i] / mean_weight).unwrap_or(1.0);
let score = d * weight_factor;
if score > best_score {
best_score = score;
best = i;
}
}
chosen.push(best);
for i in 0..n {
let d = point_distance(points, i, best);
if d < min_dist[i] {
min_dist[i] = d;
}
}
}
chosen
}
fn fold_mass_to_landmarks(
points: ArrayView2<'_, f64>,
weights: Option<ArrayView1<'_, f64>>,
landmarks: &[usize],
) -> Array1<f64> {
let mut folded = Array1::<f64>::zeros(landmarks.len());
for r in 0..points.nrows() {
let mut nearest = 0usize;
let mut nearest_dist = f64::INFINITY;
for (li, &l) in landmarks.iter().enumerate() {
let d = point_distance(points, r, l);
if d < nearest_dist {
nearest_dist = d;
nearest = li;
}
}
let mass = match weights {
Some(w) => w[r],
None => 1.0,
};
if mass.is_finite() && mass > 0.0 {
folded[nearest] += mass;
}
}
folded
}
struct Simplex {
verts: Vec<usize>,
filt: f64,
dim: usize,
}
fn dtm_radii(points: ArrayView2<'_, f64>, weights: Option<ArrayView1<'_, f64>>) -> Vec<f64> {
let m = points.nrows();
if m <= 1 {
return vec![0.0; m];
}
let local_weights = weights
.map(|w| w.to_owned())
.unwrap_or_else(|| Array1::<f64>::ones(m));
let total = local_weights.iter().copied().sum::<f64>();
if !(total.is_finite() && total > 0.0) {
return vec![0.0; m];
}
let target_mass = total / m as f64;
let mut radii = vec![0.0_f64; m];
for i in 0..m {
let mut neighbors: Vec<(f64, f64)> = Vec::with_capacity(m);
for j in 0..m {
let weight = local_weights[j];
if weight.is_finite() && weight > 0.0 {
let distance = if i == j {
0.0
} else {
point_distance(points, i, j)
};
neighbors.push((distance, weight));
}
}
neighbors.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut mass = 0.0_f64;
let mut moment = 0.0_f64;
for (dist, weight) in neighbors {
let take = (target_mass - mass).min(weight);
if take > 0.0 {
moment += take * dist * dist;
mass += take;
}
if mass >= target_mass {
break;
}
}
if mass > 0.0 {
radii[i] = (moment / mass).sqrt();
}
}
radii
}
fn dtm_weighted_distances_and_radii(
points: ArrayView2<'_, f64>,
weights: Option<ArrayView1<'_, f64>>,
) -> (Array2<f64>, Vec<f64>) {
let m = points.nrows();
let dtm = dtm_radii(points, weights);
let mut dist = Array2::<f64>::zeros((m, m));
for i in 0..m {
for j in (i + 1)..m {
let d = point_distance(points, i, j).max(dtm[i]).max(dtm[j]);
dist[[i, j]] = d;
dist[[j, i]] = d;
}
}
(dist, dtm)
}
fn dtm_weighted_distances(
points: ArrayView2<'_, f64>,
weights: Option<ArrayView1<'_, f64>>,
) -> Array2<f64> {
dtm_weighted_distances_and_radii(points, weights).0
}
pub fn vietoris_rips_persistence(points: ArrayView2<'_, f64>) -> PersistenceDiagram {
dtm_vietoris_rips_persistence(points, None, 1)
}
fn dtm_vietoris_rips_persistence(
points: ArrayView2<'_, f64>,
weights: Option<ArrayView1<'_, f64>>,
max_homology_dim: usize,
) -> PersistenceDiagram {
let m = points.nrows();
let mut h0 = Vec::new();
let mut h1 = Vec::new();
let mut h2 = Vec::new();
if m == 0 {
return PersistenceDiagram { h0, h1, h2 };
}
if m == 1 {
h0.push(PersistenceBar {
birth: 0.0,
death: f64::INFINITY,
});
return PersistenceDiagram { h0, h1, h2 };
}
let (dist, dtm) = dtm_weighted_distances_and_radii(points, weights);
let max_simplex_dim = (max_homology_dim + 1).min(3);
let mut simplices: Vec<Simplex> = Vec::new();
for i in 0..m {
simplices.push(Simplex {
verts: vec![i],
filt: dtm[i],
dim: 0,
});
}
for i in 0..m {
for j in (i + 1)..m {
simplices.push(Simplex {
verts: vec![i, j],
filt: dist[[i, j]],
dim: 1,
});
}
}
if max_simplex_dim >= 2 {
for i in 0..m {
for j in (i + 1)..m {
for k in (j + 1)..m {
let filt = dist[[i, j]].max(dist[[i, k]]).max(dist[[j, k]]);
simplices.push(Simplex {
verts: vec![i, j, k],
filt,
dim: 2,
});
}
}
}
}
if max_simplex_dim >= 3 {
for i in 0..m {
for j in (i + 1)..m {
for k in (j + 1)..m {
for l in (k + 1)..m {
let filt = dist[[i, j]]
.max(dist[[i, k]])
.max(dist[[i, l]])
.max(dist[[j, k]])
.max(dist[[j, l]])
.max(dist[[k, l]]);
simplices.push(Simplex {
verts: vec![i, j, k, l],
filt,
dim: 3,
});
}
}
}
}
}
let mut order: Vec<usize> = (0..simplices.len()).collect();
order.sort_by(|&a, &b| {
let sa = &simplices[a];
let sb = &simplices[b];
sa.filt
.partial_cmp(&sb.filt)
.unwrap_or(std::cmp::Ordering::Equal)
.then(sa.dim.cmp(&sb.dim))
.then(sa.verts.cmp(&sb.verts))
});
let mut filt_index = vec![0usize; simplices.len()];
let mut key_to_index: HashMap<Vec<usize>, usize> = HashMap::with_capacity(simplices.len());
for (fi, &orig) in order.iter().enumerate() {
filt_index[orig] = fi;
key_to_index.insert(simplices[orig].verts.clone(), fi);
}
let mut ordered_filt = vec![0.0_f64; simplices.len()];
let mut ordered_dim = vec![0usize; simplices.len()];
let mut boundary: Vec<Vec<usize>> = vec![Vec::new(); simplices.len()];
for &orig in &order {
let s = &simplices[orig];
let fi = filt_index[orig];
ordered_filt[fi] = s.filt;
ordered_dim[fi] = s.dim;
if s.dim == 0 {
continue;
}
let mut faces = Vec::with_capacity(s.verts.len());
for drop in 0..s.verts.len() {
let mut face = Vec::with_capacity(s.verts.len() - 1);
for (idx, &v) in s.verts.iter().enumerate() {
if idx != drop {
face.push(v);
}
}
if let Some(&face_fi) = key_to_index.get(&face) {
faces.push(face_fi);
}
}
faces.sort_unstable();
boundary[fi] = faces;
}
let n = simplices.len();
let mut reduced: Vec<Vec<usize>> = vec![Vec::new(); n];
let mut pivot: HashMap<usize, usize> = HashMap::new();
let mut paired_birth = vec![false; n];
for j in 0..n {
let mut col = boundary[j].clone();
while let Some(&low) = col.last() {
if let Some(&owner) = pivot.get(&low) {
col = symmetric_difference(&col, &reduced[owner]);
} else {
break;
}
}
if let Some(&low) = col.last() {
pivot.insert(low, j);
reduced[j] = col;
paired_birth[low] = true;
let birth = ordered_filt[low];
let death = ordered_filt[j];
let bar = PersistenceBar { birth, death };
match ordered_dim[low] {
0 => {
if death > birth {
h0.push(bar);
}
}
1 => {
if death > birth {
h1.push(bar);
}
}
2 => {
if max_homology_dim >= 2 && death > birth {
h2.push(bar);
}
}
_ => {}
}
}
}
for j in 0..n {
if reduced[j].is_empty() && !paired_birth[j] {
let bar = PersistenceBar {
birth: ordered_filt[j],
death: f64::INFINITY,
};
match ordered_dim[j] {
0 => h0.push(bar),
1 => h1.push(bar),
2 if max_homology_dim >= 2 => h2.push(bar),
_ => {}
}
}
}
PersistenceDiagram { h0, h1, h2 }
}
fn symmetric_difference(a: &[usize], b: &[usize]) -> Vec<usize> {
let mut out = Vec::with_capacity(a.len() + b.len());
let mut ia = 0;
let mut ib = 0;
while ia < a.len() && ib < b.len() {
match a[ia].cmp(&b[ib]) {
std::cmp::Ordering::Less => {
out.push(a[ia]);
ia += 1;
}
std::cmp::Ordering::Greater => {
out.push(b[ib]);
ib += 1;
}
std::cmp::Ordering::Equal => {
ia += 1;
ib += 1;
}
}
}
out.extend_from_slice(&a[ia..]);
out.extend_from_slice(&b[ib..]);
out
}
fn components_and_scale(finite_h0: &[PersistenceBar], distances: &Array2<f64>) -> (usize, f64) {
let mut deaths: Vec<f64> = finite_h0
.iter()
.map(|b| b.death)
.filter(|d| d.is_finite() && *d > 0.0)
.collect();
if deaths.is_empty() {
return (1, 0.0);
}
deaths.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let l = deaths.len();
if l == 1 {
return (1, deaths[0]);
}
let logs: Vec<f64> = deaths.iter().map(|d| d.ln()).collect();
let gaps: Vec<f64> = (0..l - 1).map(|i| logs[i] - logs[i + 1]).collect();
let mut gmax = f64::NEG_INFINITY;
let mut gmax_idx = 0usize;
for (i, &g) in gaps.iter().enumerate() {
if g > gmax {
gmax = g;
gmax_idx = i;
}
}
let sum_others: f64 = gaps.iter().sum::<f64>() - gmax;
let split_floor = sum_others.max(std::f64::consts::LN_2);
if gmax > split_floor && smallest_linkage_component(distances, deaths[gmax_idx]) >= 2 {
let within = deaths[gmax_idx + 1];
(gmax_idx + 2, within)
} else {
(1, deaths[0])
}
}
fn smallest_linkage_component(distances: &Array2<f64>, scale: f64) -> usize {
let m = distances.nrows();
if m == 0 {
return 0;
}
let mut parent: Vec<usize> = (0..m).collect();
for i in 0..m {
for j in (i + 1)..m {
if distances[[i, j]] < scale {
let ri = nerve_find(&mut parent, i);
let rj = nerve_find(&mut parent, j);
if ri != rj {
parent[ri] = rj;
}
}
}
}
let mut sizes: HashMap<usize, usize> = HashMap::new();
for x in 0..m {
let r = nerve_find(&mut parent, x);
*sizes.entry(r).or_insert(0) += 1;
}
sizes.values().copied().min().unwrap_or(0)
}
fn dominant_persistence(bars: &[PersistenceBar]) -> f64 {
bars.iter().map(|b| b.persistence()).fold(0.0_f64, f64::max)
}
fn spacing_floor_bar_count(bars: &[PersistenceBar], distances: &Array2<f64>) -> usize {
let essential = bars.iter().filter(|b| b.is_essential()).count();
let m = distances.nrows();
let mut nn: Vec<f64> = Vec::with_capacity(m);
for i in 0..m {
let mut best = f64::INFINITY;
for j in 0..m {
if i != j {
let d = distances[[i, j]];
if d > 0.0 && d < best {
best = d;
}
}
}
if best.is_finite() {
nn.push(best);
}
}
if nn.is_empty() {
return essential;
}
nn.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let spacing = nn[nn.len() / 2];
let finite: Vec<PersistenceBar> = bars
.iter()
.copied()
.filter(|b| {
b.birth.is_finite()
&& b.death.is_finite()
&& b.death > b.birth
&& b.persistence() > spacing
})
.collect();
if finite.is_empty() {
return essential;
}
let mut critical = Vec::with_capacity(finite.len() * 2);
for bar in &finite {
critical.push(bar.birth);
critical.push(bar.death);
}
critical.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
critical.dedup_by(|a, b| *a == *b);
let mut best = 0usize;
let mut best_width = f64::NEG_INFINITY;
for window in critical.windows(2) {
let lo = window[0];
let hi = window[1];
if !(hi > lo) {
continue;
}
let probe = if lo > 0.0 { (lo * hi).sqrt() } else { hi / 2.0 };
let count = finite
.iter()
.filter(|bar| bar.birth < probe && probe < bar.death)
.count();
if count == 0 {
continue;
}
let width = if lo > 0.0 { (hi / lo).ln() } else { hi - lo };
if width > best_width || (width == best_width && count < best) {
best_width = width;
best = count;
}
}
essential + best
}
fn shell_plateau_bar_count(bars: &[PersistenceBar]) -> usize {
let essential = bars.iter().filter(|b| b.is_essential()).count();
let finite: Vec<PersistenceBar> = bars
.iter()
.copied()
.filter(|b| b.birth.is_finite() && b.death.is_finite() && b.death > b.birth)
.collect();
if finite.is_empty() {
return essential;
}
let mut critical = Vec::with_capacity(finite.len() * 2);
for bar in &finite {
critical.push(bar.birth);
critical.push(bar.death);
}
critical.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
critical.dedup_by(|a, b| *a == *b);
let mut best = essential;
let mut best_width = f64::NEG_INFINITY;
for window in critical.windows(2) {
let lo = window[0];
let hi = window[1];
if !(hi > lo) {
continue;
}
let probe = if lo > 0.0 { (lo * hi).sqrt() } else { hi / 2.0 };
let count = essential
+ finite
.iter()
.filter(|bar| bar.birth < probe && probe < bar.death)
.count();
if count == 0 {
continue;
}
let width = if lo > 0.0 { (hi / lo).ln() } else { hi - lo };
if width > best_width || (width == best_width && count < best) {
best_width = width;
best = count;
}
}
best
}
fn support_summary(weights: Option<ArrayView1<'_, f64>>, full: usize) -> (f64, f64, f64) {
match weights {
Some(w) => {
let mut mass = 0.0_f64;
let mut fisher_n = 0.0_f64;
for &weight in w.iter() {
if weight.is_finite() && weight > 0.0 {
mass += weight;
fisher_n += weight * weight;
}
}
let ess = if fisher_n > 0.0 {
(mass * mass) / fisher_n
} else {
0.0
};
(mass, fisher_n, ess)
}
None => {
let n = full as f64;
(n, n, n)
}
}
}
pub fn topology_persistence_verdict(
points: ArrayView2<'_, f64>,
raced_kind: &SaeAtomBasisKind,
) -> Option<AtomTopologyPersistence> {
topology_persistence_verdict_impl(points, None, raced_kind, None)
}
fn topology_persistence_verdict_impl(
points: ArrayView2<'_, f64>,
weights: Option<ArrayView1<'_, f64>>,
raced_kind: &SaeAtomBasisKind,
finite_set_components: Option<usize>,
) -> Option<AtomTopologyPersistence> {
let expected_betti = expected_betti_signature(raced_kind, finite_set_components)?;
let full = points.nrows();
if full < 4 {
return None;
}
let h1_landmarks =
farthest_point_subsample_weighted(points, weights, PERSISTENCE_H1_MAX_POINTS);
let h1_sub = points.select(ndarray::Axis(0), &h1_landmarks);
let h1_weights = fold_mass_to_landmarks(points, weights, &h1_landmarks);
let h1_diagram = dtm_vietoris_rips_persistence(h1_sub.view(), Some(h1_weights.view()), 1);
let h1_distances = dtm_weighted_distances(h1_sub.view(), Some(h1_weights.view()));
let h2: Vec<PersistenceBar> = if expected_betti.b2.is_some() {
let landmarks = farthest_point_subsample_weighted(points, weights, PERSISTENCE_MAX_POINTS);
let sub = points.select(ndarray::Axis(0), &landmarks);
let sub_weights = fold_mass_to_landmarks(points, weights, &landmarks);
dtm_vietoris_rips_persistence(sub.view(), Some(sub_weights.view()), 2).h2
} else {
Vec::new()
};
let (support_mass, effective_n, support_ess) = support_summary(weights, full);
let finite_h0: Vec<PersistenceBar> = h1_diagram
.h0
.iter()
.copied()
.filter(|b| !b.is_essential())
.collect();
let (n_components, _) = components_and_scale(&finite_h0, &h1_distances);
let measured_betti = BettiSignature {
b0: n_components,
b1: spacing_floor_bar_count(&h1_diagram.h1, &h1_distances),
b2: expected_betti.b2.map(|expected_h2| {
let counted = shell_plateau_bar_count(&h2);
if expected_h2 == 0 && counted == 0 {
0
} else {
counted
}
}),
};
let dominant_h1_persistence = dominant_persistence(&h1_diagram.h1);
let dominant_h2_persistence = dominant_persistence(&h2);
let contested = !measured_betti.matches_expected(expected_betti);
let note = if contested {
let mut reasons = Vec::new();
if measured_betti.b0 != expected_betti.b0 {
reasons.push(format!(
"measured b0={} but raced type predicts b0={}",
measured_betti.b0, expected_betti.b0
));
}
if measured_betti.b1 != expected_betti.b1 {
reasons.push(format!(
"measured b1={} but raced type predicts b1={}",
measured_betti.b1, expected_betti.b1
));
}
if let Some(expected_h2) = expected_betti.b2 {
if measured_betti.b2 != Some(expected_h2) {
reasons.push(format!(
"measured b2={} but raced type predicts b2={expected_h2}",
measured_betti.b2.unwrap_or(0)
));
}
}
format!("CONTESTED topology: {}", reasons.join("; "))
} else {
format!(
"topology agrees: measured Betti {:?} matches raced Betti {:?}",
measured_betti, expected_betti
)
};
let stability_band = if full > PERSISTENCE_MAX_POINTS {
PersistenceStabilityBand::AtLandmarkCap
} else {
PersistenceStabilityBand::BelowLandmarkCap
};
let covering_side = if full >= PERSISTENCE_MAX_POINTS {
AtlasCoveringSide::AtOrAboveCoveringNumber
} else {
AtlasCoveringSide::BelowCoveringNumber
};
Some(AtomTopologyPersistence {
raced_kind: raced_kind.clone(),
support_size: full,
landmark_count: h1_landmarks.len(),
stability_band,
covering_side,
support_mass,
effective_n,
support_ess,
measured_betti,
expected_betti,
null_calibration: None,
dominant_h1_persistence,
dominant_h2_persistence,
h0: h1_diagram.h0,
h1: h1_diagram.h1,
h2,
contested,
note,
})
}
pub fn atom_topology_persistence(
term: &SaeManifoldTerm,
atom_idx: usize,
) -> Option<AtomTopologyPersistence> {
let atom = term.atoms.get(atom_idx)?;
let assignments = term.assignment.assignments();
let n = assignments.nrows();
let k = assignments.ncols();
if n == 0 || atom_idx >= k {
return None;
}
let mut supported_rows = Vec::new();
let mut support_weights = Vec::new();
for row in 0..n {
let mass = assignments[[row, atom_idx]];
if mass.is_finite() && mass > 0.0 {
supported_rows.push(row);
support_weights.push(mass);
}
}
if supported_rows.len() < 4 || supported_rows.iter().any(|&r| r >= atom.n_obs()) {
return None;
}
let p = atom.output_dim();
let mut points = Array2::<f64>::zeros((supported_rows.len(), p));
let mut weights = Array1::<f64>::zeros(supported_rows.len());
for (i, &row) in supported_rows.iter().enumerate() {
let image = atom.decoded_row(row);
for col in 0..p {
points[[i, col]] = image[col];
}
weights[i] = support_weights[i];
}
let finite_set_components = if matches!(atom.basis_kind(), SaeAtomBasisKind::FiniteSet) {
Some(atom.basis_size())
} else {
None
};
topology_persistence_verdict_impl(
points.view(),
Some(weights.view()),
atom.basis_kind(),
finite_set_components,
)
}
#[derive(Clone, Debug)]
pub struct AtlasNerveReport {
pub n_charts: usize,
pub n_edges: usize,
pub n_triangles: usize,
pub n_components: usize,
pub b1: i64,
pub max_vertex_degree: usize,
pub covering_side: AtlasCoveringSide,
}
impl AtlasNerveReport {
pub fn is_circle(&self) -> bool {
self.n_components == 1 && self.b1 == 1
}
pub fn is_arc(&self) -> bool {
self.n_components == 1 && self.b1 == 0 && self.max_vertex_degree <= 2
}
}
fn nerve_find(parent: &mut [usize], x: usize) -> usize {
let mut root = x;
while parent[root] != root {
root = parent[root];
}
let mut cur = x;
while parent[cur] != root {
let next = parent[cur];
parent[cur] = root;
cur = next;
}
root
}
pub fn atlas_nerve(points: ArrayView2<'_, f64>) -> AtlasNerveReport {
let n = points.nrows();
if n == 0 {
return AtlasNerveReport {
n_charts: 0,
n_edges: 0,
n_triangles: 0,
n_components: 0,
b1: 0,
max_vertex_degree: 0,
covering_side: AtlasCoveringSide::BelowCoveringNumber,
};
}
let n_charts = ((n as f64).sqrt().ceil() as usize).max(3).min(n);
let landmarks = farthest_point_subsample(points, n_charts);
let v = landmarks.len();
let mut distances = vec![vec![0.0_f64; v]; n];
let mut radii = vec![0.0_f64; v];
for i in 0..n {
let mut best = (f64::INFINITY, 0usize);
for (ci, &l) in landmarks.iter().enumerate() {
let d = point_distance(points, i, l);
distances[i][ci] = d;
if d < best.0 {
best = (d, ci);
}
}
if best.0.is_finite() {
radii[best.1] = radii[best.1].max(best.0);
}
}
let mut edges = BTreeSet::<(usize, usize)>::new();
let mut triangles = BTreeSet::<(usize, usize, usize)>::new();
for row_distances in &distances {
let mut membership = Vec::new();
for chart in 0..v {
let tolerance = f64::EPSILON * (1.0 + radii[chart].abs()) * v.max(1) as f64;
if row_distances[chart] <= radii[chart] + tolerance {
membership.push(chart);
}
}
for ia in 0..membership.len() {
for ib in (ia + 1)..membership.len() {
edges.insert((membership[ia], membership[ib]));
for ic in (ib + 1)..membership.len() {
triangles.insert((membership[ia], membership[ib], membership[ic]));
}
}
}
}
let n_edges = edges.len();
let mut parent: Vec<usize> = (0..v).collect();
let mut degree = vec![0usize; v];
for &(a, b) in &edges {
degree[a] += 1;
degree[b] += 1;
let ra = nerve_find(&mut parent, a);
let rb = nerve_find(&mut parent, b);
if ra != rb {
parent[ra] = rb;
}
}
let mut roots = std::collections::HashSet::new();
for x in 0..v {
let r = nerve_find(&mut parent, x);
roots.insert(r);
}
let n_components = roots.len();
let edge_index: HashMap<(usize, usize), usize> = edges
.iter()
.copied()
.enumerate()
.map(|(i, edge)| (edge, i))
.collect();
let mut boundary_columns = Vec::with_capacity(triangles.len());
for &(a, b, c) in &triangles {
boundary_columns.push(vec![
edge_index[&(a, b)],
edge_index[&(a, c)],
edge_index[&(b, c)],
]);
}
let triangle_boundary_rank = gf2_column_rank(boundary_columns);
let b1 =
(n_edges as i64 - v as i64 + n_components as i64 - triangle_boundary_rank as i64).max(0);
let covering_side = AtlasCoveringSide::BelowCoveringNumber;
AtlasNerveReport {
n_charts: v,
n_edges,
n_triangles: triangles.len(),
n_components,
b1,
max_vertex_degree: degree.into_iter().max().unwrap_or(0),
covering_side,
}
}
fn gf2_column_rank(columns: Vec<Vec<usize>>) -> usize {
let mut pivots: HashMap<usize, Vec<usize>> = HashMap::new();
let mut rank = 0usize;
for mut column in columns {
column.sort_unstable();
while let Some(&pivot) = column.last() {
if let Some(existing) = pivots.get(&pivot) {
let mut out = Vec::with_capacity(column.len() + existing.len());
let (mut i, mut j) = (0usize, 0usize);
while i < column.len() || j < existing.len() {
if j == existing.len() || (i < column.len() && column[i] < existing[j]) {
out.push(column[i]);
i += 1;
} else if i == column.len() || existing[j] < column[i] {
out.push(existing[j]);
j += 1;
} else {
i += 1;
j += 1;
}
}
column = out;
} else {
pivots.insert(pivot, column);
rank += 1;
break;
}
}
}
rank
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::{Array1, Array2};
fn clifford_torus(nu: usize, nv: usize) -> Array2<f64> {
let mut pts = Array2::<f64>::zeros((nu * nv, 4));
let mut row = 0usize;
for i in 0..nu {
let u = std::f64::consts::TAU * (i as f64) / (nu as f64);
for j in 0..nv {
let v = std::f64::consts::TAU * (j as f64) / (nv as f64);
pts[[row, 0]] = u.cos();
pts[[row, 1]] = u.sin();
pts[[row, 2]] = v.cos();
pts[[row, 3]] = v.sin();
row += 1;
}
}
pts
}
fn circle(n: usize, r: f64) -> Array2<f64> {
let mut pts = Array2::<f64>::zeros((n, 2));
for i in 0..n {
let theta = std::f64::consts::TAU * (i as f64) / (n as f64);
pts[[i, 0]] = r * theta.cos();
pts[[i, 1]] = r * theta.sin();
}
pts
}
#[test]
fn symmetric_torus_measures_two_generators_uncontested() {
let pts = clifford_torus(8, 8);
let verdict = topology_persistence_verdict(pts.view(), &SaeAtomBasisKind::Torus)
.expect("torus verdict");
assert_eq!(verdict.measured_betti.b0, 1, "a torus is connected");
assert_eq!(
verdict.measured_betti.b1, 2,
"a torus has TWO independent loops; symmetric grids must not merge them (#2159)"
);
assert_eq!(
verdict.measured_betti.b2,
Some(1),
"a torus bounds exactly one void"
);
assert!(
!verdict.contested,
"measured (1,2,1) must agree with the raced Torus prediction: {}",
verdict.note
);
}
#[test]
fn asymmetric_torus_still_measures_two_generators() {
let pts = clifford_torus(10, 8);
let landmarks =
farthest_point_subsample_weighted(pts.view(), None, PERSISTENCE_H1_MAX_POINTS);
let sub = pts.select(ndarray::Axis(0), &landmarks);
let weights = fold_mass_to_landmarks(pts.view(), None, &landmarks);
let diagram = dtm_vietoris_rips_persistence(sub.view(), Some(weights.view()), 1);
let distances = dtm_weighted_distances(sub.view(), Some(weights.view()));
assert_eq!(
spacing_floor_bar_count(&diagram.h1, &distances),
2,
"an asymmetric torus still has two independent loops"
);
}
#[test]
fn circle_still_measures_one_generator() {
let pts = circle(40, 2.0);
let verdict = topology_persistence_verdict(pts.view(), &SaeAtomBasisKind::Periodic)
.expect("circle verdict");
assert_eq!(
verdict.measured_betti.b1, 1,
"a circle has exactly one loop; the plateau reading must not over-split"
);
assert!(
!verdict.contested,
"measured circle topology must agree with the raced Periodic prediction: {}",
verdict.note
);
}
#[test]
fn folded_landmark_mass_preserves_full_measure() {
let n = 400usize;
let pts = circle(n, 1.0);
let mut weights = Array1::<f64>::zeros(n);
for i in 0..n {
weights[i] = 0.5 + (i as f64) * 0.01;
}
let total: f64 = weights.iter().sum();
let landmarks = farthest_point_subsample_weighted(
pts.view(),
Some(weights.view()),
PERSISTENCE_H1_MAX_POINTS,
);
assert!(
landmarks.len() < n,
"the cap must actually drop rows for this test to exercise the fold"
);
let folded = fold_mass_to_landmarks(pts.view(), Some(weights.view()), &landmarks);
let folded_total: f64 = folded.iter().sum();
assert!(
(folded_total - total).abs() <= 1e-9 * total,
"folded landmark mass {folded_total} must equal the full support mass {total}"
);
}
fn bars(deaths: &[f64]) -> Vec<PersistenceBar> {
deaths
.iter()
.map(|&d| PersistenceBar {
birth: 0.0,
death: d,
})
.collect()
}
fn line_points(xs: &[f64]) -> Array2<f64> {
Array2::from_shape_vec((xs.len(), 1), xs.to_vec()).unwrap()
}
fn old_rule_splits(deaths: &[f64]) -> bool {
let mut d: Vec<f64> = deaths.to_vec();
d.sort_by(|a, b| b.partial_cmp(a).unwrap());
if d.len() < 2 {
return false;
}
let logs: Vec<f64> = d.iter().map(|x| x.ln()).collect();
let gaps: Vec<f64> = (0..d.len() - 1).map(|i| logs[i] - logs[i + 1]).collect();
let gmax = gaps.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let sum_others: f64 = gaps.iter().sum::<f64>() - gmax;
gmax > sum_others && gmax > 0.0
}
#[test]
fn near_uniform_clean_spacing_is_connected() {
for deaths in [vec![1.0, 0.98], vec![1.0, 0.98, 0.96]] {
assert!(
old_rule_splits(&deaths),
"sum-only rule should have (wrongly) split {deaths:?}"
);
let pts = line_points(
&(0..deaths.len() + 1)
.map(|i| i as f64 * 100.0)
.collect::<Vec<_>>(),
);
let distances = dtm_weighted_distances(pts.view(), None);
let (n, _) = components_and_scale(&bars(&deaths), &distances);
assert_eq!(n, 1, "ln2 floor should keep {deaths:?} connected");
}
}
#[test]
fn genuine_two_cluster_split_survives_floor() {
let deaths = vec![10.0, 0.1, 0.09, 0.08];
assert!(old_rule_splits(&deaths));
let pts = line_points(&[0.0, 0.1, 0.2, 50.0, 50.1]);
let distances = dtm_weighted_distances(pts.view(), None);
let (n, within) = components_and_scale(&bars(&deaths), &distances);
assert_eq!(
n, 2,
"a real inter-cluster gap with ≥2 per side must still split"
);
assert!(
(within - 0.1).abs() < 1e-12,
"within-scale is the coarsest sub-cut merge"
);
}
#[test]
fn lone_outlier_cut_is_not_split() {
let deaths = vec![10.0, 0.1, 0.09];
assert!(old_rule_splits(&deaths));
let pts = line_points(&[0.0, 0.1, 0.2, 50.0]);
let distances = dtm_weighted_distances(pts.view(), None);
let (n, _) = components_and_scale(&bars(&deaths), &distances);
assert_eq!(n, 1, "a cut isolating one landmark must be rejected");
}
}