ruvector_math/homology/
filtration.rs

1//! Filtrations for Persistent Homology
2//!
3//! A filtration is a sequence of nested simplicial complexes.
4
5use super::{PointCloud, Simplex, SimplicialComplex};
6
7/// A filtered simplex (simplex with birth time)
8#[derive(Debug, Clone)]
9pub struct FilteredSimplex {
10    /// The simplex
11    pub simplex: Simplex,
12    /// Birth time (filtration value)
13    pub birth: f64,
14}
15
16impl FilteredSimplex {
17    pub fn new(simplex: Simplex, birth: f64) -> Self {
18        Self { simplex, birth }
19    }
20}
21
22/// Filtration: sequence of simplicial complexes
23#[derive(Debug, Clone)]
24pub struct Filtration {
25    /// Filtered simplices sorted by birth time
26    pub simplices: Vec<FilteredSimplex>,
27    /// Maximum dimension
28    pub max_dim: usize,
29}
30
31impl Filtration {
32    /// Create empty filtration
33    pub fn new() -> Self {
34        Self {
35            simplices: Vec::new(),
36            max_dim: 0,
37        }
38    }
39
40    /// Add filtered simplex
41    pub fn add(&mut self, simplex: Simplex, birth: f64) {
42        self.max_dim = self.max_dim.max(simplex.dim());
43        self.simplices.push(FilteredSimplex::new(simplex, birth));
44    }
45
46    /// Sort by birth time (required before computing persistence)
47    pub fn sort(&mut self) {
48        // Sort by birth time, then by dimension (lower dimension first)
49        self.simplices.sort_by(|a, b| {
50            a.birth
51                .partial_cmp(&b.birth)
52                .unwrap_or(std::cmp::Ordering::Equal)
53                .then_with(|| a.simplex.dim().cmp(&b.simplex.dim()))
54        });
55    }
56
57    /// Get complex at filtration value t
58    pub fn complex_at(&self, t: f64) -> SimplicialComplex {
59        let simplices: Vec<Simplex> = self
60            .simplices
61            .iter()
62            .filter(|fs| fs.birth <= t)
63            .map(|fs| fs.simplex.clone())
64            .collect();
65        SimplicialComplex::from_simplices(simplices)
66    }
67
68    /// Number of simplices
69    pub fn len(&self) -> usize {
70        self.simplices.len()
71    }
72
73    /// Is empty?
74    pub fn is_empty(&self) -> bool {
75        self.simplices.is_empty()
76    }
77
78    /// Get filtration values
79    pub fn filtration_values(&self) -> Vec<f64> {
80        let mut values: Vec<f64> = self.simplices.iter().map(|fs| fs.birth).collect();
81        values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
82        values.dedup();
83        values
84    }
85}
86
87impl Default for Filtration {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93/// Vietoris-Rips filtration
94///
95/// At scale ε, includes all simplices whose vertices are pairwise within distance ε.
96#[derive(Debug, Clone)]
97pub struct VietorisRips {
98    /// Maximum dimension to compute
99    pub max_dim: usize,
100    /// Maximum filtration value
101    pub max_scale: f64,
102}
103
104impl VietorisRips {
105    /// Create with parameters
106    pub fn new(max_dim: usize, max_scale: f64) -> Self {
107        Self { max_dim, max_scale }
108    }
109
110    /// Build filtration from point cloud
111    pub fn build(&self, cloud: &PointCloud) -> Filtration {
112        let n = cloud.len();
113        let dist = cloud.distance_matrix();
114
115        let mut filtration = Filtration::new();
116
117        // Add vertices at time 0
118        for i in 0..n {
119            filtration.add(Simplex::vertex(i), 0.0);
120        }
121
122        // Add edges at their diameter
123        for i in 0..n {
124            for j in i + 1..n {
125                let d = dist[i * n + j];
126                if d <= self.max_scale {
127                    filtration.add(Simplex::edge(i, j), d);
128                }
129            }
130        }
131
132        // Add higher simplices (up to max_dim)
133        if self.max_dim >= 2 {
134            // Triangles
135            for i in 0..n {
136                for j in i + 1..n {
137                    for k in j + 1..n {
138                        let d_ij = dist[i * n + j];
139                        let d_ik = dist[i * n + k];
140                        let d_jk = dist[j * n + k];
141                        let diameter = d_ij.max(d_ik).max(d_jk);
142
143                        if diameter <= self.max_scale {
144                            filtration.add(Simplex::triangle(i, j, k), diameter);
145                        }
146                    }
147                }
148            }
149        }
150
151        if self.max_dim >= 3 {
152            // Tetrahedra
153            for i in 0..n {
154                for j in i + 1..n {
155                    for k in j + 1..n {
156                        for l in k + 1..n {
157                            let d_ij = dist[i * n + j];
158                            let d_ik = dist[i * n + k];
159                            let d_il = dist[i * n + l];
160                            let d_jk = dist[j * n + k];
161                            let d_jl = dist[j * n + l];
162                            let d_kl = dist[k * n + l];
163                            let diameter = d_ij.max(d_ik).max(d_il).max(d_jk).max(d_jl).max(d_kl);
164
165                            if diameter <= self.max_scale {
166                                filtration.add(Simplex::new(vec![i, j, k, l]), diameter);
167                            }
168                        }
169                    }
170                }
171            }
172        }
173
174        filtration.sort();
175        filtration
176    }
177}
178
179/// Alpha complex filtration (more efficient than Rips for low dimensions)
180///
181/// Based on Delaunay triangulation with radius filtering.
182#[derive(Debug, Clone)]
183pub struct AlphaComplex {
184    /// Maximum alpha value
185    pub max_alpha: f64,
186}
187
188impl AlphaComplex {
189    /// Create with maximum alpha
190    pub fn new(max_alpha: f64) -> Self {
191        Self { max_alpha }
192    }
193
194    /// Build filtration from point cloud (simplified version)
195    ///
196    /// Note: Full alpha complex requires Delaunay triangulation.
197    /// This is a simplified version that approximates using distance thresholds.
198    pub fn build(&self, cloud: &PointCloud) -> Filtration {
199        let n = cloud.len();
200        let dist = cloud.distance_matrix();
201
202        let mut filtration = Filtration::new();
203
204        // Vertices at time 0
205        for i in 0..n {
206            filtration.add(Simplex::vertex(i), 0.0);
207        }
208
209        // Edges: birth time is half the distance (radius, not diameter)
210        for i in 0..n {
211            for j in i + 1..n {
212                let alpha = dist[i * n + j] / 2.0;
213                if alpha <= self.max_alpha {
214                    filtration.add(Simplex::edge(i, j), alpha);
215                }
216            }
217        }
218
219        // Triangles: birth time based on circumradius approximation
220        for i in 0..n {
221            for j in i + 1..n {
222                for k in j + 1..n {
223                    let d_ij = dist[i * n + j];
224                    let d_ik = dist[i * n + k];
225                    let d_jk = dist[j * n + k];
226
227                    // Approximate circumradius
228                    let s = (d_ij + d_ik + d_jk) / 2.0;
229                    let area_sq = s * (s - d_ij) * (s - d_ik) * (s - d_jk);
230                    let alpha = if area_sq > 0.0 {
231                        (d_ij * d_ik * d_jk) / (4.0 * area_sq.sqrt())
232                    } else {
233                        d_ij.max(d_ik).max(d_jk) / 2.0
234                    };
235
236                    if alpha <= self.max_alpha {
237                        filtration.add(Simplex::triangle(i, j, k), alpha);
238                    }
239                }
240            }
241        }
242
243        filtration.sort();
244        filtration
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_filtration_creation() {
254        let mut filtration = Filtration::new();
255        filtration.add(Simplex::vertex(0), 0.0);
256        filtration.add(Simplex::vertex(1), 0.0);
257        filtration.add(Simplex::edge(0, 1), 1.0);
258
259        assert_eq!(filtration.len(), 3);
260    }
261
262    #[test]
263    fn test_filtration_sort() {
264        let mut filtration = Filtration::new();
265        filtration.add(Simplex::edge(0, 1), 1.0);
266        filtration.add(Simplex::vertex(0), 0.0);
267        filtration.add(Simplex::vertex(1), 0.0);
268
269        filtration.sort();
270
271        // Vertices should come before edge
272        assert!(filtration.simplices[0].simplex.is_vertex());
273        assert!(filtration.simplices[1].simplex.is_vertex());
274        assert!(filtration.simplices[2].simplex.is_edge());
275    }
276
277    #[test]
278    fn test_vietoris_rips() {
279        // Triangle of points
280        let cloud = PointCloud::from_flat(&[0.0, 0.0, 1.0, 0.0, 0.5, 0.866], 2);
281        let rips = VietorisRips::new(2, 2.0);
282
283        let filtration = rips.build(&cloud);
284
285        // Should have 3 vertices, 3 edges, 1 triangle
286        let values = filtration.filtration_values();
287        assert!(!values.is_empty());
288    }
289
290    #[test]
291    fn test_complex_at() {
292        let cloud = PointCloud::from_flat(&[0.0, 0.0, 1.0, 0.0, 2.0, 0.0], 2);
293        let rips = VietorisRips::new(1, 2.0);
294        let filtration = rips.build(&cloud);
295
296        // At scale 0.5, only vertices
297        let complex_0 = filtration.complex_at(0.5);
298        assert_eq!(complex_0.count_dim(0), 3);
299        assert_eq!(complex_0.count_dim(1), 0);
300
301        // At scale 1.5, vertices and adjacent edges
302        let complex_1 = filtration.complex_at(1.5);
303        assert_eq!(complex_1.count_dim(0), 3);
304        assert!(complex_1.count_dim(1) >= 2); // At least edges 0-1 and 1-2
305    }
306
307    #[test]
308    fn test_alpha_complex() {
309        let cloud = PointCloud::from_flat(&[0.0, 0.0, 1.0, 0.0, 0.0, 1.0], 2);
310        let alpha = AlphaComplex::new(2.0);
311
312        let filtration = alpha.build(&cloud);
313
314        assert!(filtration.len() >= 3); // At least vertices
315    }
316}