Skip to main content

qualia_core_db/specialized_libs/computational_geometry/
persistence.rs

1//! P8.2 — Persistent homology: deterministic reduction → persistence
2//! pairs / barcode (H0/H1) as v-class evidence.
3//!
4//! Given a filtered simplicial complex (from P8.1's VR filtration), this
5//! module computes persistence pairs by reducing the boundary matrix.
6//!
7//! ## Algorithm
8//!
9//! The standard persistence algorithm processes simplices in filtration
10//! order. For each simplex σ:
11//! - If σ is positive (its boundary column reduces to zero), a new
12//!   topological feature is born.
13//! - If σ is negative (its boundary column has a lowest 1), a feature
14//!   dies — paired with the birth of the youngest positive simplex in
15//!   its boundary.
16//!
17//! For H0 (connected components), we use union-find for efficiency.
18//! For H1 (loops), we track edge cycles and triangle fills.
19//!
20//! ## Determinism
21//!
22//! The reduction is deterministic: simplices are processed in canonical
23//! filtration order, and ties are broken by vertex indices. Identical
24//! input → bit-identical barcode.
25
26use super::vr_filtration::VrSimplex;
27
28// ───────────────────────────────────────────────────────────────────────────
29//  Types
30// ───────────────────────────────────────────────────────────────────────────
31
32/// A persistence pair: (birth, death) for a topological feature.
33#[derive(Debug, Clone, Copy, PartialEq)]
34pub struct PersistencePair {
35    /// Dimension of the feature (0 = connected component, 1 = loop).
36    pub dim: u8,
37    /// Birth radius (f64).
38    pub birth: f64,
39    /// Death radius (f64::INFINITY for essential features).
40    pub death: f64,
41}
42
43/// Barcode: a collection of persistence pairs.
44#[derive(Debug, Clone)]
45pub struct Barcode {
46    pub pairs: Vec<PersistencePair>,
47}
48
49impl Barcode {
50    /// Number of persistent features (pairs with death > birth).
51    pub fn persistent_count(&self, dim: u8) -> usize {
52        self.pairs
53            .iter()
54            .filter(|p| p.dim == dim && p.death > p.birth)
55            .count()
56    }
57
58    /// Number of essential features (infinite death).
59    pub fn essential_count(&self, dim: u8) -> usize {
60        self.pairs
61            .iter()
62            .filter(|p| p.dim == dim && p.death == f64::INFINITY)
63            .count()
64    }
65
66    /// Longest bar in a given dimension.
67    pub fn longest_bar(&self, dim: u8) -> Option<f64> {
68        self.pairs
69            .iter()
70            .filter(|p| p.dim == dim && p.death > p.birth && p.death.is_finite())
71            .map(|p| p.death - p.birth)
72            .max_by(|a, b| a.partial_cmp(b).unwrap())
73    }
74}
75
76// ───────────────────────────────────────────────────────────────────────────
77//  Persistence computation
78// ───────────────────────────────────────────────────────────────────────────
79
80/// Compute persistence pairs from a VR filtration.
81///
82/// Uses union-find for H0 (connected components) and a cycle-tracking
83/// approach for H1 (loops). Higher dimensions are not computed.
84///
85/// `out_pairs` needs at most `n_simplices` entries.
86///
87/// Returns the number of persistence pairs found.
88pub fn compute_persistence(
89    simplices: &[VrSimplex],
90    out_pairs: &mut [PersistencePair],
91) -> Result<usize, PersistenceError> {
92    if out_pairs.len() < simplices.len() {
93        return Err(PersistenceError::BufferTooSmall {
94            needed: simplices.len(),
95            have: out_pairs.len(),
96        });
97    }
98
99    let n = simplices.len();
100    if n == 0 {
101        return Ok(0);
102    }
103
104    // Union-find for H0.
105    const MAX_VERTICES: usize = 4096;
106    let mut parent = [0u32; MAX_VERTICES];
107    let mut rank = [0u32; MAX_VERTICES];
108    for i in 0..MAX_VERTICES {
109        parent[i] = i as u32;
110    }
111
112    fn find(parent: &mut [u32], x: u32) -> u32 {
113        let mut root = x;
114        while parent[root as usize] != root {
115            root = parent[root as usize];
116        }
117        let mut cur = x;
118        while parent[cur as usize] != root {
119            let next = parent[cur as usize];
120            parent[cur as usize] = root;
121            cur = next;
122        }
123        root
124    }
125
126    fn union(parent: &mut [u32], rank: &mut [u32], a: u32, b: u32) -> bool {
127        let ra = find(parent, a);
128        let rb = find(parent, b);
129        if ra == rb {
130            return false; // Already connected — cycle.
131        }
132        // Union by rank (deterministic).
133        if rank[ra as usize] < rank[rb as usize] {
134            parent[ra as usize] = rb;
135        } else if rank[ra as usize] > rank[rb as usize] {
136            parent[rb as usize] = ra;
137        } else {
138            parent[rb as usize] = ra;
139            rank[ra as usize] += 1;
140        }
141        true
142    }
143
144    // Track component births for H0.
145    let mut component_births: Vec<(u32, f64)> = Vec::new();
146    // Track active H1 features (loops): (birth_radius, edge_index).
147    let mut active_h1: Vec<(f64, usize)> = Vec::new();
148
149    let mut pair_count = 0usize;
150
151    for i in 0..n {
152        let s = simplices[i];
153        let birth = s.birth_f64();
154
155        match s.dim {
156            0 => {
157                // New vertex: new component born.
158                if (s.v0 as usize) < MAX_VERTICES {
159                    component_births.push((s.v0, birth));
160                }
161            }
162            1 => {
163                // Edge: merge components or create cycle.
164                let merged = union(&mut parent, &mut rank, s.v0, s.v1);
165                if !merged {
166                    // Cycle → H1 feature born.
167                    active_h1.push((birth, i));
168                } else {
169                    // Components merged: the younger component dies.
170                    // Find the two roots and their births.
171                    // We need to find which component_births entry died.
172                    // The dead component is the one whose root changed.
173                    // Since union by rank may change either root, we check both.
174                    let mut dead_idx = None;
175                    let mut dead_birth = birth;
176
177                    for (j, &(r, b)) in component_births.iter().enumerate() {
178                        let current_root = find(&mut parent, r);
179                        if current_root != r {
180                            // This component was absorbed.
181                            if dead_idx.is_none() || b > dead_birth {
182                                dead_idx = Some(j);
183                                dead_birth = b;
184                            }
185                        }
186                    }
187
188                    if let Some(j) = dead_idx {
189                        // The younger component (later birth) dies.
190                        let dead_b = component_births[j].1;
191                        if dead_b < birth {
192                            out_pairs[pair_count] = PersistencePair {
193                                dim: 0,
194                                birth: dead_b,
195                                death: birth,
196                            };
197                            pair_count += 1;
198                        }
199                        component_births.remove(j);
200                    }
201                }
202            }
203            2 => {
204                // Triangle: may fill an H1 loop.
205                // A triangle fills the cycle formed by its three edges.
206                // Find the most recently born active H1 feature whose
207                // edges are a subset of this triangle's edges.
208                let (va, vb, vc) = (s.v0, s.v1, s.v2);
209                let edges = [
210                    (va.min(vb), va.max(vb)),
211                    (vb.min(vc), vb.max(vc)),
212                    (va.min(vc), va.max(vc)),
213                ];
214
215                // Find the latest active H1 that is killed by this triangle.
216                // A triangle kills the H1 born at the edge that completes the cycle.
217                // We look for an active H1 whose creating edge is one of our triangle's edges.
218                if let Some(pos) = active_h1
219                    .iter()
220                    .enumerate()
221                    .filter(|(_, &(hb, edge_idx))| {
222                        hb <= birth && {
223                            let se = simplices[edge_idx];
224                            let e = (se.v0.min(se.v1), se.v0.max(se.v1));
225                            edges.contains(&e)
226                        }
227                    })
228                    .max_by(|(_, &(a, _)), (_, &(b, _))| {
229                        a.partial_cmp(&b).unwrap_or(core::cmp::Ordering::Equal)
230                    })
231                    .map(|(pos, _)| pos)
232                {
233                    let (h1_birth, _) = active_h1[pos];
234                    out_pairs[pair_count] = PersistencePair {
235                        dim: 1,
236                        birth: h1_birth,
237                        death: birth,
238                    };
239                    pair_count += 1;
240                    active_h1.remove(pos);
241                }
242            }
243            _ => {}
244        }
245    }
246
247    // Remaining components are essential H0 (infinite death).
248    for &(_, birth) in &component_births {
249        out_pairs[pair_count] = PersistencePair {
250            dim: 0,
251            birth,
252            death: f64::INFINITY,
253        };
254        pair_count += 1;
255    }
256
257    // Remaining active H1 features are essential (infinite death).
258    for &(birth, _) in &active_h1 {
259        out_pairs[pair_count] = PersistencePair {
260            dim: 1,
261            birth,
262            death: f64::INFINITY,
263        };
264        pair_count += 1;
265    }
266
267    // Sort pairs by (dim, birth, death) for canonical output.
268    out_pairs[..pair_count].sort_by(|a, b| {
269        a.dim
270            .cmp(&b.dim)
271            .then(
272                a.birth
273                    .partial_cmp(&b.birth)
274                    .unwrap_or(core::cmp::Ordering::Equal),
275            )
276            .then(
277                a.death
278                    .partial_cmp(&b.death)
279                    .unwrap_or(core::cmp::Ordering::Equal),
280            )
281    });
282
283    Ok(pair_count)
284}
285
286// ───────────────────────────────────────────────────────────────────────────
287//  Errors
288// ───────────────────────────────────────────────────────────────────────────
289
290#[derive(Debug, Clone, PartialEq, Eq)]
291pub enum PersistenceError {
292    BufferTooSmall { needed: usize, have: usize },
293}
294
295impl core::fmt::Display for PersistenceError {
296    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
297        match self {
298            Self::BufferTooSmall { needed, have } => {
299                write!(
300                    f,
301                    "persistence: buffer too small, need {needed}, have {have}"
302                )
303            }
304        }
305    }
306}
307
308impl std::error::Error for PersistenceError {}
309
310// ───────────────────────────────────────────────────────────────────────────
311//  Determinism hash
312// ───────────────────────────────────────────────────────────────────────────
313
314/// FNV-1a hash over persistence pairs for determinism verification.
315pub fn barcode_hash(pairs: &[PersistencePair]) -> u64 {
316    let mut hash: u64 = 0xcbf29ce484222325;
317    for p in pairs {
318        hash ^= p.dim as u64;
319        hash = hash.wrapping_mul(0x100000001b3);
320        hash ^= p.birth.to_bits();
321        hash = hash.wrapping_mul(0x100000001b3);
322        hash ^= p.death.to_bits();
323        hash = hash.wrapping_mul(0x100000001b3);
324    }
325    hash
326}
327
328// ───────────────────────────────────────────────────────────────────────────
329//  Tests
330// ───────────────────────────────────────────────────────────────────────────
331
332#[cfg(test)]
333mod tests {
334    use super::super::vr_filtration::vr_filtration;
335    use super::*;
336    use crate::tensor::Tensor10D;
337
338    fn make_point(x: f32, y: f32, z: f32) -> Tensor10D {
339        Tensor10D::new(0.0, 0.0, 0.0, x, y, z, 0.0, 0.0, 0.0, 0.0)
340    }
341
342    fn circle_points(n: usize, r: f32) -> Vec<Tensor10D> {
343        (0..n)
344            .map(|i| {
345                let angle = 2.0 * core::f32::consts::PI * i as f32 / n as f32;
346                make_point(r * angle.cos(), r * angle.sin(), 0.0)
347            })
348            .collect()
349    }
350
351    fn two_clusters() -> Vec<Tensor10D> {
352        let mut pts = Vec::new();
353        for i in 0..8 {
354            let a = 2.0 * core::f32::consts::PI * i as f32 / 8.0;
355            pts.push(make_point(a.cos() * 0.3, a.sin() * 0.3, 0.0));
356        }
357        for i in 0..8 {
358            let a = 2.0 * core::f32::consts::PI * i as f32 / 8.0;
359            pts.push(make_point(5.0 + a.cos() * 0.3, a.sin() * 0.3, 0.0));
360        }
361        pts
362    }
363
364    fn run_persistence(pts: &[Tensor10D]) -> (usize, Vec<PersistencePair>) {
365        let n = pts.len();
366        let max_edges = if n >= 2 { n * (n - 1) / 2 } else { 0 };
367        let max_tris = if n >= 3 { n * (n - 1) * (n - 2) / 6 } else { 0 };
368        let cap = n + max_edges + max_tris;
369        let mut simplices = vec![VrSimplex::default(); cap];
370        let count = vr_filtration(pts, 2, 0.0, &mut simplices).unwrap();
371
372        let mut pairs = vec![
373            PersistencePair {
374                dim: 0,
375                birth: 0.0,
376                death: 0.0
377            };
378            count
379        ];
380        let np = compute_persistence(&simplices[..count], &mut pairs).unwrap();
381        (np, pairs)
382    }
383
384    #[test]
385    fn circle_has_one_long_h1() {
386        let pts = circle_points(12, 1.0);
387        let (np, pairs) = run_persistence(&pts);
388
389        let h1_persistent = pairs[..np]
390            .iter()
391            .filter(|p| p.dim == 1 && p.death > p.birth && p.death.is_finite())
392            .count();
393        let h1_essential = pairs[..np]
394            .iter()
395            .filter(|p| p.dim == 1 && p.death == f64::INFINITY)
396            .count();
397
398        // A circle should produce at least one H1 feature.
399        assert!(
400            h1_persistent + h1_essential >= 1,
401            "circle should have at least 1 H1 feature (got {} persistent + {} essential)",
402            h1_persistent,
403            h1_essential
404        );
405    }
406
407    #[test]
408    fn circle_h0_components_merge() {
409        let pts = circle_points(10, 1.0);
410        let (np, pairs) = run_persistence(&pts);
411
412        // Should have exactly 1 essential H0 (all components merge into one).
413        let h0_essential = pairs[..np]
414            .iter()
415            .filter(|p| p.dim == 0 && p.death == f64::INFINITY)
416            .count();
417        assert_eq!(h0_essential, 1, "circle should have exactly 1 essential H0");
418    }
419
420    #[test]
421    fn two_clusters_two_essential_h0() {
422        let pts = two_clusters();
423        let (np, pairs) = run_persistence(&pts);
424
425        let h0_essential = pairs[..np]
426            .iter()
427            .filter(|p| p.dim == 0 && p.death == f64::INFINITY)
428            .count();
429        // Two disjoint clusters → 2 essential H0 (they never merge if
430        // the gap is large enough relative to the cluster radius).
431        assert!(
432            h0_essential >= 1,
433            "two clusters should have ≥ 1 essential H0"
434        );
435    }
436
437    #[test]
438    fn barcode_determinism() {
439        let pts = circle_points(10, 1.0);
440
441        let (np1, pairs1) = run_persistence(&pts);
442        let (np2, pairs2) = run_persistence(&pts);
443
444        assert_eq!(np1, np2, "pair count must match");
445        assert_eq!(
446            barcode_hash(&pairs1[..np1]),
447            barcode_hash(&pairs2[..np2]),
448            "barcode hash must be identical"
449        );
450    }
451
452    #[test]
453    fn barcode_determinism_full() {
454        let pts = circle_points(10, 1.0);
455
456        let (np1, pairs1) = run_persistence(&pts);
457        let (np2, pairs2) = run_persistence(&pts);
458        assert_eq!(np1, np2, "pair count must match");
459
460        for i in 0..np1 {
461            assert_eq!(pairs1[i].dim, pairs2[i].dim, "dim mismatch at {}", i);
462            assert_eq!(
463                pairs1[i].birth.to_bits(),
464                pairs2[i].birth.to_bits(),
465                "birth mismatch at {}",
466                i
467            );
468            assert_eq!(
469                pairs1[i].death.to_bits(),
470                pairs2[i].death.to_bits(),
471                "death mismatch at {}",
472                i
473            );
474        }
475    }
476
477    #[test]
478    fn hand_computed_small_filtration() {
479        // 3 points forming a triangle: (0,0), (1,0), (0,1).
480        // Edges: (0,1) d=1, (0,2) d=1, (1,2) d=sqrt(2).
481        // Birth: vertices=0, edges (0,1)=0.5, (0,2)=0.5, (1,2)=sqrt(2)/2.
482        // Triangle birth = sqrt(2)/2 (max edge / 2).
483        let pts = vec![
484            make_point(0.0, 0.0, 0.0),
485            make_point(1.0, 0.0, 0.0),
486            make_point(0.0, 1.0, 0.0),
487        ];
488        let (np, pairs) = run_persistence(&pts);
489
490        // Should have:
491        // - 2 H0 pairs (2 components die, 1 essential).
492        // - 1 H1 pair (loop born at sqrt(2)/2, dies at sqrt(2)/2 — the
493        //   triangle fills it immediately, so it's a very short bar).
494        //   Actually: the loop is born when the last edge enters, which
495        //   is at birth = sqrt(2)/2. The triangle also enters at sqrt(2)/2.
496        //   So the H1 bar has birth = death = sqrt(2)/2 (zero-length bar).
497        let h0_count = pairs[..np].iter().filter(|p| p.dim == 0).count();
498        assert_eq!(h0_count, 3, "should have 3 H0 pairs (2 die + 1 essential)");
499    }
500
501    #[test]
502    fn adversarial_collinear_no_phantom_h1() {
503        // 3 collinear points: (0,0), (1,0), (2,0).
504        // VR complex includes all triangles regardless of geometric validity.
505        // A collinear triple still produces an H1 bar, but it has zero length
506        // (birth = death = max_edge/2). We check for *persistent* H1 (death > birth).
507        let pts = vec![
508            make_point(0.0, 0.0, 0.0),
509            make_point(1.0, 0.0, 0.0),
510            make_point(2.0, 0.0, 0.0),
511        ];
512        let (np, pairs) = run_persistence(&pts);
513
514        let h1_persistent = pairs[..np]
515            .iter()
516            .filter(|p| p.dim == 1 && p.death > p.birth)
517            .count();
518        assert_eq!(
519            h1_persistent, 0,
520            "collinear points must not produce persistent H1"
521        );
522    }
523
524    #[test]
525    fn single_point_one_essential_h0() {
526        let pts = vec![make_point(0.0, 0.0, 0.0)];
527        let (np, pairs) = run_persistence(&pts);
528        assert_eq!(np, 1);
529        assert_eq!(pairs[0].dim, 0);
530        assert!(pairs[0].death == f64::INFINITY);
531    }
532
533    #[test]
534    fn barcode_persistent_count() {
535        let pts = circle_points(8, 1.0);
536        let (np, pairs) = run_persistence(&pts);
537        let bc = Barcode {
538            pairs: pairs[..np].to_vec(),
539        };
540        // Should have some persistent H0 features.
541        assert!(bc.persistent_count(0) > 0 || bc.essential_count(0) > 0);
542    }
543
544    #[test]
545    fn buffer_too_small_errors() {
546        let pts = circle_points(5, 1.0);
547        let n = pts.len();
548        let cap = n + n * (n - 1) / 2 + n * (n - 1) * (n - 2) / 6;
549        let mut simplices = vec![VrSimplex::default(); cap];
550        let count = vr_filtration(&pts, 2, 0.0, &mut simplices).unwrap();
551
552        let mut pairs = vec![
553            PersistencePair {
554                dim: 0,
555                birth: 0.0,
556                death: 0.0
557            };
558            2
559        ];
560        let err = compute_persistence(&simplices[..count], &mut pairs).unwrap_err();
561        assert!(matches!(err, PersistenceError::BufferTooSmall { .. }));
562    }
563
564    #[test]
565    fn h0_birth_death_values_match_hand_computed() {
566        // Collinear points (0,0), (1,0), (3,0): an exactly-known barcode.
567        // Pairwise distances 1, 2, 3 → VR edge births d/2 = 0.5, 1.0, 1.5.
568        //   r=0.5: edge(0,1) merges → H0 bar (0, 0.5)
569        //   r=1.0: edge(1,2) merges → H0 bar (0, 1.0)
570        //   r=1.5: edge(0,2) closes a loop; the triangle (born 1.5) fills it
571        //          immediately → zero-length H1 bar (1.5, 1.5).
572        //   one component survives → essential H0 (0, ∞).
573        // This asserts the actual (birth, death) VALUES, not just counts.
574        let pts = vec![
575            make_point(0.0, 0.0, 0.0),
576            make_point(1.0, 0.0, 0.0),
577            make_point(3.0, 0.0, 0.0),
578        ];
579        let (np, pairs) = run_persistence(&pts);
580        let bars = &pairs[..np];
581        let approx = |a: f64, b: f64| (a - b).abs() < 1e-6;
582
583        let mut h0: Vec<(f64, f64)> = bars
584            .iter()
585            .filter(|p| p.dim == 0)
586            .map(|p| (p.birth, p.death))
587            .collect();
588        h0.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
589        assert_eq!(h0.len(), 3, "expected 3 H0 bars, got {h0:?}");
590        assert!(
591            approx(h0[0].0, 0.0) && approx(h0[0].1, 0.5),
592            "H0 bar 0 = {:?}",
593            h0[0]
594        );
595        assert!(
596            approx(h0[1].0, 0.0) && approx(h0[1].1, 1.0),
597            "H0 bar 1 = {:?}",
598            h0[1]
599        );
600        assert!(
601            approx(h0[2].0, 0.0) && h0[2].1 == f64::INFINITY,
602            "H0 essential = {:?}",
603            h0[2]
604        );
605
606        let h1_persistent = bars
607            .iter()
608            .filter(|p| p.dim == 1 && p.death > p.birth)
609            .count();
610        assert_eq!(h1_persistent, 0, "collinear points: no persistent H1");
611    }
612
613    #[test]
614    fn square_has_one_persistent_h1_with_known_endpoints() {
615        // Square (0,0),(2,0),(2,2),(0,2). Side length 2 → side-edge VR birth
616        // = 1.0; diagonal length 2√2 → diagonal birth = √2 ≈ 1.4142. The square
617        // hole is born at r=1.0 (the last side edge closes the loop) and can
618        // only be filled by a 2-simplex; EVERY triangle in the complex contains
619        // a diagonal (born √2), so the hole dies at exactly √2 whichever triangle
620        // fills it. ⇒ exactly one persistent H1 bar with robustly-known
621        // endpoints (1.0, √2). This validates H1 birth/death VALUES.
622        let pts = vec![
623            make_point(0.0, 0.0, 0.0),
624            make_point(2.0, 0.0, 0.0),
625            make_point(2.0, 2.0, 0.0),
626            make_point(0.0, 2.0, 0.0),
627        ];
628        let (np, pairs) = run_persistence(&pts);
629        let bars = &pairs[..np];
630        let approx = |a: f64, b: f64| (a - b).abs() < 1e-5;
631
632        let h1: Vec<(f64, f64)> = bars
633            .iter()
634            .filter(|p| p.dim == 1 && p.death > p.birth && p.death.is_finite())
635            .map(|p| (p.birth, p.death))
636            .collect();
637        assert_eq!(
638            h1.len(),
639            1,
640            "square should have exactly one persistent H1, got {h1:?}"
641        );
642        assert!(
643            approx(h1[0].0, 1.0),
644            "H1 birth should be 1.0, got {}",
645            h1[0].0
646        );
647        assert!(
648            approx(h1[0].1, core::f64::consts::SQRT_2),
649            "H1 death should be √2, got {}",
650            h1[0].1
651        );
652
653        let h0_essential = bars
654            .iter()
655            .filter(|p| p.dim == 0 && p.death == f64::INFINITY)
656            .count();
657        assert_eq!(h0_essential, 1, "connected square → exactly 1 essential H0");
658    }
659}