ruvector_math/homology/
persistence.rs

1//! Persistent Homology Computation
2//!
3//! Compute birth-death pairs from a filtration using the standard algorithm.
4
5use super::{Filtration, Simplex, BettiNumbers};
6use std::collections::{HashMap, HashSet};
7
8/// Birth-death pair in persistence diagram
9#[derive(Debug, Clone, PartialEq)]
10pub struct BirthDeathPair {
11    /// Dimension of the feature (0 = component, 1 = loop, ...)
12    pub dimension: usize,
13    /// Birth time
14    pub birth: f64,
15    /// Death time (None = essential, lives forever)
16    pub death: Option<f64>,
17}
18
19impl BirthDeathPair {
20    /// Create finite interval
21    pub fn finite(dimension: usize, birth: f64, death: f64) -> Self {
22        Self {
23            dimension,
24            birth,
25            death: Some(death),
26        }
27    }
28
29    /// Create essential (infinite) interval
30    pub fn essential(dimension: usize, birth: f64) -> Self {
31        Self {
32            dimension,
33            birth,
34            death: None,
35        }
36    }
37
38    /// Persistence (lifetime) of feature
39    pub fn persistence(&self) -> f64 {
40        match self.death {
41            Some(d) => d - self.birth,
42            None => f64::INFINITY,
43        }
44    }
45
46    /// Is this an essential feature (never dies)?
47    pub fn is_essential(&self) -> bool {
48        self.death.is_none()
49    }
50
51    /// Midpoint of interval
52    pub fn midpoint(&self) -> f64 {
53        match self.death {
54            Some(d) => (self.birth + d) / 2.0,
55            None => f64::INFINITY,
56        }
57    }
58}
59
60/// Persistence diagram: collection of birth-death pairs
61#[derive(Debug, Clone)]
62pub struct PersistenceDiagram {
63    /// Birth-death pairs
64    pub pairs: Vec<BirthDeathPair>,
65    /// Maximum dimension
66    pub max_dim: usize,
67}
68
69impl PersistenceDiagram {
70    /// Create empty diagram
71    pub fn new() -> Self {
72        Self {
73            pairs: Vec::new(),
74            max_dim: 0,
75        }
76    }
77
78    /// Add a pair
79    pub fn add(&mut self, pair: BirthDeathPair) {
80        self.max_dim = self.max_dim.max(pair.dimension);
81        self.pairs.push(pair);
82    }
83
84    /// Get pairs of dimension d
85    pub fn pairs_of_dim(&self, d: usize) -> impl Iterator<Item = &BirthDeathPair> {
86        self.pairs.iter().filter(move |p| p.dimension == d)
87    }
88
89    /// Get Betti numbers at scale t
90    pub fn betti_at(&self, t: f64) -> BettiNumbers {
91        let mut b0 = 0;
92        let mut b1 = 0;
93        let mut b2 = 0;
94
95        for pair in &self.pairs {
96            let alive = pair.birth <= t && pair.death.map(|d| d > t).unwrap_or(true);
97            if alive {
98                match pair.dimension {
99                    0 => b0 += 1,
100                    1 => b1 += 1,
101                    2 => b2 += 1,
102                    _ => {}
103                }
104            }
105        }
106
107        BettiNumbers::new(b0, b1, b2)
108    }
109
110    /// Get total persistence (sum of lifetimes)
111    pub fn total_persistence(&self) -> f64 {
112        self.pairs
113            .iter()
114            .filter(|p| !p.is_essential())
115            .map(|p| p.persistence())
116            .sum()
117    }
118
119    /// Get average persistence
120    pub fn average_persistence(&self) -> f64 {
121        let finite: Vec<f64> = self
122            .pairs
123            .iter()
124            .filter(|p| !p.is_essential())
125            .map(|p| p.persistence())
126            .collect();
127
128        if finite.is_empty() {
129            0.0
130        } else {
131            finite.iter().sum::<f64>() / finite.len() as f64
132        }
133    }
134
135    /// Filter by minimum persistence
136    pub fn filter_by_persistence(&self, min_persistence: f64) -> Self {
137        Self {
138            pairs: self
139                .pairs
140                .iter()
141                .filter(|p| p.persistence() >= min_persistence)
142                .cloned()
143                .collect(),
144            max_dim: self.max_dim,
145        }
146    }
147
148    /// Number of features of each dimension
149    pub fn feature_counts(&self) -> Vec<usize> {
150        let mut counts = vec![0; self.max_dim + 1];
151        for pair in &self.pairs {
152            if pair.dimension <= self.max_dim {
153                counts[pair.dimension] += 1;
154            }
155        }
156        counts
157    }
158}
159
160impl Default for PersistenceDiagram {
161    fn default() -> Self {
162        Self::new()
163    }
164}
165
166/// Persistent homology computation
167pub struct PersistentHomology {
168    /// Working column representation (reduced boundary matrix)
169    columns: Vec<Option<HashSet<usize>>>,
170    /// Pivot to column mapping
171    pivot_to_col: HashMap<usize, usize>,
172    /// Birth times
173    birth_times: Vec<f64>,
174    /// Simplex dimensions
175    dimensions: Vec<usize>,
176}
177
178impl PersistentHomology {
179    /// Compute persistence from filtration
180    pub fn compute(filtration: &Filtration) -> PersistenceDiagram {
181        let mut ph = Self {
182            columns: Vec::new(),
183            pivot_to_col: HashMap::new(),
184            birth_times: Vec::new(),
185            dimensions: Vec::new(),
186        };
187
188        ph.run(filtration)
189    }
190
191    fn run(&mut self, filtration: &Filtration) -> PersistenceDiagram {
192        let n = filtration.simplices.len();
193        if n == 0 {
194            return PersistenceDiagram::new();
195        }
196
197        // Build simplex index mapping
198        let simplex_to_idx: HashMap<&Simplex, usize> = filtration
199            .simplices
200            .iter()
201            .enumerate()
202            .map(|(i, fs)| (&fs.simplex, i))
203            .collect();
204
205        // Initialize
206        self.columns = Vec::with_capacity(n);
207        self.birth_times = filtration.simplices.iter().map(|fs| fs.birth).collect();
208        self.dimensions = filtration.simplices.iter().map(|fs| fs.simplex.dim()).collect();
209
210        // Build boundary matrix columns
211        for fs in &filtration.simplices {
212            let boundary = self.boundary(&fs.simplex, &simplex_to_idx);
213            self.columns.push(if boundary.is_empty() {
214                None
215            } else {
216                Some(boundary)
217            });
218        }
219
220        // Reduce matrix
221        self.reduce();
222
223        // Extract persistence pairs
224        self.extract_pairs()
225    }
226
227    /// Compute boundary of simplex as set of face indices
228    fn boundary(&self, simplex: &Simplex, idx_map: &HashMap<&Simplex, usize>) -> HashSet<usize> {
229        let mut boundary = HashSet::new();
230        for face in simplex.faces() {
231            if let Some(&idx) = idx_map.get(&face) {
232                boundary.insert(idx);
233            }
234        }
235        boundary
236    }
237
238    /// Reduce using standard persistence algorithm
239    fn reduce(&mut self) {
240        let n = self.columns.len();
241
242        for j in 0..n {
243            // Reduce column j
244            while let Some(pivot) = self.get_pivot(j) {
245                if let Some(&other) = self.pivot_to_col.get(&pivot) {
246                    // Add column 'other' to column j (mod 2)
247                    self.add_columns(j, other);
248                } else {
249                    // No collision, record pivot
250                    self.pivot_to_col.insert(pivot, j);
251                    break;
252                }
253            }
254        }
255    }
256
257    /// Get pivot (largest index) of column
258    fn get_pivot(&self, col: usize) -> Option<usize> {
259        self.columns[col].as_ref().and_then(|c| c.iter().max().copied())
260    }
261
262    /// Add column src to column dst (XOR / mod 2)
263    fn add_columns(&mut self, dst: usize, src: usize) {
264        let src_col = self.columns[src].clone();
265        if let (Some(ref mut dst_col), Some(ref src_col)) = (&mut self.columns[dst], &src_col) {
266            // Symmetric difference
267            let mut new_col = HashSet::new();
268            for &idx in dst_col.iter() {
269                if !src_col.contains(&idx) {
270                    new_col.insert(idx);
271                }
272            }
273            for &idx in src_col.iter() {
274                if !dst_col.contains(&idx) {
275                    new_col.insert(idx);
276                }
277            }
278            if new_col.is_empty() {
279                self.columns[dst] = None;
280            } else {
281                *dst_col = new_col;
282            }
283        }
284    }
285
286    /// Extract birth-death pairs from reduced matrix
287    fn extract_pairs(&self) -> PersistenceDiagram {
288        let n = self.columns.len();
289        let mut diagram = PersistenceDiagram::new();
290        let mut paired = HashSet::new();
291
292        // Process pivot pairs (death creates pair with birth)
293        for (&pivot, &col) in &self.pivot_to_col {
294            let birth = self.birth_times[pivot];
295            let death = self.birth_times[col];
296            let dim = self.dimensions[pivot];
297
298            if death > birth {
299                diagram.add(BirthDeathPair::finite(dim, birth, death));
300            }
301
302            paired.insert(pivot);
303            paired.insert(col);
304        }
305
306        // Remaining columns are essential (infinite persistence)
307        for j in 0..n {
308            if !paired.contains(&j) && self.columns[j].is_none() {
309                let dim = self.dimensions[j];
310                let birth = self.birth_times[j];
311                diagram.add(BirthDeathPair::essential(dim, birth));
312            }
313        }
314
315        diagram
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    use crate::homology::{PointCloud, VietorisRips};
323
324    #[test]
325    fn test_birth_death_pair() {
326        let finite = BirthDeathPair::finite(0, 0.0, 1.0);
327        assert_eq!(finite.persistence(), 1.0);
328        assert!(!finite.is_essential());
329
330        let essential = BirthDeathPair::essential(0, 0.0);
331        assert!(essential.is_essential());
332        assert_eq!(essential.persistence(), f64::INFINITY);
333    }
334
335    #[test]
336    fn test_persistence_diagram() {
337        let mut diagram = PersistenceDiagram::new();
338        diagram.add(BirthDeathPair::essential(0, 0.0));
339        diagram.add(BirthDeathPair::finite(0, 0.0, 1.0));
340        diagram.add(BirthDeathPair::finite(1, 0.5, 2.0));
341
342        assert_eq!(diagram.pairs.len(), 3);
343
344        let betti = diagram.betti_at(0.75);
345        assert_eq!(betti.b0, 2); // Both 0-dim features alive
346        assert_eq!(betti.b1, 1); // 1-dim feature alive
347    }
348
349    #[test]
350    fn test_persistent_homology_simple() {
351        // Two points
352        let cloud = PointCloud::from_flat(&[0.0, 0.0, 1.0, 0.0], 2);
353        let rips = VietorisRips::new(1, 2.0);
354        let filtration = rips.build(&cloud);
355
356        let diagram = PersistentHomology::compute(&filtration);
357
358        // Should have:
359        // - One essential H0 (final connected component)
360        // - One finite H0 that dies when edge connects the points
361        let h0_pairs: Vec<_> = diagram.pairs_of_dim(0).collect();
362        assert!(!h0_pairs.is_empty());
363    }
364
365    #[test]
366    fn test_persistent_homology_triangle() {
367        // Three points forming triangle
368        let cloud = PointCloud::from_flat(&[0.0, 0.0, 1.0, 0.0, 0.5, 0.866], 2);
369        let rips = VietorisRips::new(2, 2.0);
370        let filtration = rips.build(&cloud);
371
372        let diagram = PersistentHomology::compute(&filtration);
373
374        // Should have H0 features (components merging)
375        let h0_count = diagram.pairs_of_dim(0).count();
376        assert!(h0_count > 0);
377    }
378
379    #[test]
380    fn test_filter_by_persistence() {
381        let mut diagram = PersistenceDiagram::new();
382        diagram.add(BirthDeathPair::finite(0, 0.0, 0.1));
383        diagram.add(BirthDeathPair::finite(0, 0.0, 1.0));
384        diagram.add(BirthDeathPair::essential(0, 0.0));
385
386        let filtered = diagram.filter_by_persistence(0.5);
387        assert_eq!(filtered.pairs.len(), 2); // Only persistence >= 0.5
388    }
389
390    #[test]
391    fn test_feature_counts() {
392        let mut diagram = PersistenceDiagram::new();
393        diagram.add(BirthDeathPair::finite(0, 0.0, 1.0));
394        diagram.add(BirthDeathPair::finite(0, 0.0, 1.0));
395        diagram.add(BirthDeathPair::finite(1, 0.0, 1.0));
396
397        let counts = diagram.feature_counts();
398        assert_eq!(counts[0], 2);
399        assert_eq!(counts[1], 1);
400    }
401}