1#[derive(Debug, Clone, PartialEq)]
30pub struct PersistenceInterval {
31 pub dimension: usize,
33 pub birth: f64,
35 pub death: f64,
37}
38
39impl PersistenceInterval {
40 pub fn persistence(&self) -> f64 {
42 if self.death.is_infinite() {
43 f64::INFINITY
44 } else {
45 self.death - self.birth
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
52pub struct PersistenceDiagram {
53 pub intervals: Vec<PersistenceInterval>,
55 pub n_points: usize,
57}
58
59impl PersistenceDiagram {
60 pub fn betti(&self, dimension: usize, radius: f64) -> usize {
62 self.intervals
63 .iter()
64 .filter(|iv| iv.dimension == dimension && iv.birth <= radius && iv.death > radius)
65 .count()
66 }
67
68 pub fn betti_curve(&self, dimension: usize, radii: &[f64]) -> Vec<usize> {
70 radii.iter().map(|&r| self.betti(dimension, r)).collect()
71 }
72
73 pub fn total_persistence(&self, dimension: usize) -> f64 {
75 self.intervals
76 .iter()
77 .filter(|iv| iv.dimension == dimension && iv.death.is_finite())
78 .map(|iv| iv.persistence())
79 .sum()
80 }
81
82 pub fn n_significant(&self, dimension: usize, min_persistence: f64) -> usize {
84 self.intervals
85 .iter()
86 .filter(|iv| iv.dimension == dimension && iv.persistence() > min_persistence)
87 .count()
88 }
89}
90
91pub fn vietoris_rips_h0(points: &[&[f32]]) -> PersistenceDiagram {
109 let n = points.len();
110 if n == 0 {
111 return PersistenceDiagram {
112 intervals: vec![],
113 n_points: 0,
114 };
115 }
116
117 let mut birth_times = vec![0.0f64; n];
119
120 let mut edges: Vec<(f64, usize, usize)> = Vec::with_capacity(n * (n - 1) / 2);
122 for i in 0..n {
123 for j in (i + 1)..n {
124 edges.push((l2_dist(points[i], points[j]), i, j));
125 }
126 }
127 edges.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
128
129 let mut parent: Vec<usize> = (0..n).collect();
131 let mut rank = vec![0usize; n];
132
133 let find = |parent: &mut Vec<usize>, mut x: usize| -> usize {
134 while parent[x] != x {
135 parent[x] = parent[parent[x]]; x = parent[x];
137 }
138 x
139 };
140
141 let mut intervals = Vec::new();
142
143 for (dist, i, j) in edges {
144 let ri = find(&mut parent, i);
145 let rj = find(&mut parent, j);
146 if ri == rj {
147 continue; }
149
150 let (survivor, dying) = if birth_times[ri] <= birth_times[rj] {
152 (ri, rj)
153 } else {
154 (rj, ri)
155 };
156
157 intervals.push(PersistenceInterval {
158 dimension: 0,
159 birth: birth_times[dying],
160 death: dist,
161 });
162
163 if rank[survivor] < rank[dying] {
165 parent[survivor] = dying;
166 birth_times[dying] = birth_times[dying].min(birth_times[survivor]);
167 } else {
168 parent[dying] = survivor;
169 if rank[survivor] == rank[dying] {
170 rank[survivor] += 1;
171 }
172 }
173 }
174
175 let final_root = find(&mut parent, 0);
177 intervals.push(PersistenceInterval {
178 dimension: 0,
179 birth: birth_times[final_root],
180 death: f64::INFINITY,
181 });
182
183 PersistenceDiagram {
184 intervals,
185 n_points: n,
186 }
187}
188
189#[derive(Debug, Clone)]
191pub struct TopologicalFeatures {
192 pub n_components: usize,
194 pub total_persistence_h0: f64,
196 pub max_persistence: f64,
198 pub mean_persistence: f64,
200 pub persistence_entropy: f64,
202 pub betti_curve: Vec<usize>,
204 pub radii: Vec<f64>,
206}
207
208pub fn topological_summary(
218 points: &[&[f32]],
219 n_radii: usize,
220 persistence_threshold: f64,
221) -> TopologicalFeatures {
222 let diagram = vietoris_rips_h0(points);
223
224 let finite: Vec<f64> = diagram
225 .intervals
226 .iter()
227 .filter(|iv| iv.death.is_finite())
228 .map(|iv| iv.persistence())
229 .collect();
230
231 let max_persistence = finite.iter().cloned().fold(0.0f64, f64::max);
232 let total = finite.iter().sum::<f64>();
233 let mean_persistence = if finite.is_empty() {
234 0.0
235 } else {
236 total / finite.len() as f64
237 };
238
239 let persistence_entropy = if total > 0.0 {
241 finite
242 .iter()
243 .map(|&p| {
244 let q = p / total;
245 if q > 0.0 { -q * q.ln() } else { 0.0 }
246 })
247 .sum()
248 } else {
249 0.0
250 };
251
252 let max_death = finite.iter().cloned().fold(0.0f64, f64::max);
254 let radii: Vec<f64> = (0..n_radii)
255 .map(|i| max_death * (i as f64 + 0.5) / n_radii as f64)
256 .collect();
257 let betti_curve = diagram.betti_curve(0, &radii);
258
259 let n_components = diagram.n_significant(0, persistence_threshold);
260
261 TopologicalFeatures {
262 n_components,
263 total_persistence_h0: total,
264 max_persistence,
265 mean_persistence,
266 persistence_entropy,
267 betti_curve,
268 radii,
269 }
270}
271
272#[inline]
274fn l2_dist(a: &[f32], b: &[f32]) -> f64 {
275 a.iter()
276 .zip(b.iter())
277 .map(|(x, y)| {
278 let d = *x as f64 - *y as f64;
279 d * d
280 })
281 .sum::<f64>()
282 .sqrt()
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn single_point_one_component() {
291 let points: Vec<&[f32]> = vec![&[0.0f32, 0.0]];
292 let d = vietoris_rips_h0(&points);
293 assert_eq!(d.intervals.len(), 1);
294 assert!(d.intervals[0].death.is_infinite()); assert_eq!(d.betti(0, 0.0), 1);
296 }
297
298 #[test]
299 fn two_distant_clusters() {
300 let points: Vec<&[f32]> = vec![
302 &[0.0f32, 0.0],
303 &[0.1, 0.0],
304 &[0.0, 0.1], &[10.0, 10.0],
306 &[10.1, 10.0],
307 &[10.0, 10.1], ];
309 let d = vietoris_rips_h0(&points);
310
311 assert_eq!(d.betti(0, 0.5), 2, "should see 2 clusters at r=0.5");
313
314 assert_eq!(d.betti(0, 20.0), 1, "should see 1 component at r=20");
316 }
317
318 #[test]
319 fn three_equidistant_points() {
320 let s = 3.0f32.sqrt() / 2.0;
322 let p0 = [0.0f32, 0.0];
323 let p1 = [1.0f32, 0.0];
324 let p2 = [0.5f32, s];
325 let points: Vec<&[f32]> = vec![&p0, &p1, &p2];
326 let d = vietoris_rips_h0(&points);
327
328 assert_eq!(d.intervals.len(), 3);
330 assert_eq!(d.betti(0, 0.5), 3); assert_eq!(d.betti(0, 1.5), 1); }
333
334 #[test]
335 fn betti_curve_monotone_decreasing() {
336 let points: Vec<&[f32]> = vec![&[0.0f32], &[1.0], &[3.0], &[6.0], &[10.0]];
337 let d = vietoris_rips_h0(&points);
338 let radii: Vec<f64> = (0..20).map(|i| i as f64 * 0.6).collect();
339 let curve = d.betti_curve(0, &radii);
340
341 for i in 1..curve.len() {
343 assert!(
344 curve[i] <= curve[i - 1],
345 "β₀ should be non-increasing: β₀[{}]={} > β₀[{}]={}",
346 i,
347 curve[i],
348 i - 1,
349 curve[i - 1]
350 );
351 }
352 }
353
354 #[test]
355 fn topological_summary_two_clusters() {
356 let points: Vec<&[f32]> = vec![&[0.0f32, 0.0], &[0.1, 0.0], &[10.0, 0.0], &[10.1, 0.0]];
357 let feat = topological_summary(&points, 20, 1.0);
358
359 assert!(feat.n_components >= 1, "should detect cluster structure");
361 assert!(
362 feat.max_persistence > 5.0,
363 "max persistence should reflect cluster gap"
364 );
365 }
366
367 #[test]
368 fn persistence_entropy_uniform() {
369 let pts: Vec<[f32; 1]> = (0..10).map(|i| [i as f32]).collect();
371 let point_refs: Vec<&[f32]> = pts.iter().map(|p| p.as_slice()).collect();
372 let feat = topological_summary(&point_refs, 10, 0.01);
373
374 assert!(
376 feat.persistence_entropy > 1.0,
377 "uniform should have high entropy, got {}",
378 feat.persistence_entropy
379 );
380 }
381
382 #[test]
383 fn empty_points() {
384 let points: Vec<&[f32]> = vec![];
385 let d = vietoris_rips_h0(&points);
386 assert_eq!(d.intervals.len(), 0);
387 assert_eq!(d.n_points, 0);
388 }
389}