1use super::vr_filtration::VrSimplex;
27
28#[derive(Debug, Clone, Copy, PartialEq)]
34pub struct PersistencePair {
35 pub dim: u8,
37 pub birth: f64,
39 pub death: f64,
41}
42
43#[derive(Debug, Clone)]
45pub struct Barcode {
46 pub pairs: Vec<PersistencePair>,
47}
48
49impl Barcode {
50 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 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 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
76pub 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 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; }
132 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 let mut component_births: Vec<(u32, f64)> = Vec::new();
146 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 if (s.v0 as usize) < MAX_VERTICES {
159 component_births.push((s.v0, birth));
160 }
161 }
162 1 => {
163 let merged = union(&mut parent, &mut rank, s.v0, s.v1);
165 if !merged {
166 active_h1.push((birth, i));
168 } else {
169 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 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 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 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 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 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 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 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#[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
310pub 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#[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 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 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 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 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 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 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 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 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 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}