1#[derive(Debug, Clone)]
22pub struct SimplicialComplex {
23 pub vertices: Vec<[f64; 3]>,
25 pub edges: Vec<[usize; 2]>,
27 pub triangles: Vec<[usize; 3]>,
29 pub tetrahedra: Vec<[usize; 4]>,
31}
32
33impl SimplicialComplex {
34 #[allow(dead_code)]
36 pub fn new() -> Self {
37 Self {
38 vertices: Vec::new(),
39 edges: Vec::new(),
40 triangles: Vec::new(),
41 tetrahedra: Vec::new(),
42 }
43 }
44
45 #[allow(dead_code)]
47 pub fn add_vertex(&mut self, pos: [f64; 3]) -> usize {
48 let idx = self.vertices.len();
49 self.vertices.push(pos);
50 idx
51 }
52
53 #[allow(dead_code)]
55 pub fn add_edge(&mut self, i: usize, j: usize) {
56 let e = if i < j { [i, j] } else { [j, i] };
57 if !self.edges.contains(&e) {
58 self.edges.push(e);
59 }
60 }
61
62 #[allow(dead_code)]
64 pub fn add_triangle(&mut self, i: usize, j: usize, k: usize) {
65 let mut t = [i, j, k];
66 t.sort_unstable();
67 if !self.triangles.contains(&t) {
68 self.triangles.push(t);
69 }
70 }
71
72 #[allow(dead_code)]
74 pub fn add_tetrahedron(&mut self, i: usize, j: usize, k: usize, l: usize) {
75 let mut t = [i, j, k, l];
76 t.sort_unstable();
77 if !self.tetrahedra.contains(&t) {
78 self.tetrahedra.push(t);
79 }
80 }
81
82 #[allow(dead_code)]
84 pub fn euler_characteristic(&self) -> i64 {
85 self.vertices.len() as i64 - self.edges.len() as i64 + self.triangles.len() as i64
86 - self.tetrahedra.len() as i64
87 }
88
89 #[allow(dead_code)]
91 pub fn n_vertices(&self) -> usize {
92 self.vertices.len()
93 }
94
95 #[allow(dead_code)]
97 pub fn n_edges(&self) -> usize {
98 self.edges.len()
99 }
100
101 #[allow(dead_code)]
103 pub fn n_triangles(&self) -> usize {
104 self.triangles.len()
105 }
106
107 #[allow(dead_code)]
109 pub fn n_tetrahedra(&self) -> usize {
110 self.tetrahedra.len()
111 }
112
113 #[allow(dead_code)]
117 pub fn boundary_1(&self) -> Vec<i32> {
118 let nv = self.vertices.len();
119 let ne = self.edges.len();
120 let mut b = vec![0_i32; nv * ne];
121 for (j, &[i0, i1]) in self.edges.iter().enumerate() {
122 b[i0 * ne + j] = -1;
123 b[i1 * ne + j] = 1;
124 }
125 b
126 }
127
128 #[allow(dead_code)]
132 pub fn boundary_2(&self) -> Vec<i32> {
133 let ne = self.edges.len();
134 let nf = self.triangles.len();
135 let mut b = vec![0_i32; ne * nf];
136 for (j, &[a, b_idx, c]) in self.triangles.iter().enumerate() {
137 let tri_edges = [[a, b_idx], [b_idx, c], [a, c]];
139 let signs = [1_i32, 1, -1];
140 for (k, &e) in tri_edges.iter().enumerate() {
141 let es = if e[0] < e[1] {
142 [e[0], e[1]]
143 } else {
144 [e[1], e[0]]
145 };
146 if let Some(ei) = self.edges.iter().position(|&x| x == es) {
147 b[ei * nf + j] = signs[k];
148 }
149 }
150 }
151 b
152 }
153
154 #[allow(dead_code)]
159 pub fn octahedral_sphere() -> Self {
160 let mut sc = Self::new();
161 sc.add_vertex([1.0, 0.0, 0.0]);
163 sc.add_vertex([-1.0, 0.0, 0.0]);
164 sc.add_vertex([0.0, 1.0, 0.0]);
165 sc.add_vertex([0.0, -1.0, 0.0]);
166 sc.add_vertex([0.0, 0.0, 1.0]);
167 sc.add_vertex([0.0, 0.0, -1.0]);
168 let edges = [
170 [0, 2],
171 [0, 3],
172 [0, 4],
173 [0, 5],
174 [1, 2],
175 [1, 3],
176 [1, 4],
177 [1, 5],
178 [2, 4],
179 [2, 5],
180 [3, 4],
181 [3, 5],
182 ];
183 for [i, j] in edges {
184 sc.add_edge(i, j);
185 }
186 sc.add_triangle(0, 2, 4);
188 sc.add_triangle(0, 2, 5);
189 sc.add_triangle(0, 3, 4);
190 sc.add_triangle(0, 3, 5);
191 sc.add_triangle(1, 2, 4);
192 sc.add_triangle(1, 2, 5);
193 sc.add_triangle(1, 3, 4);
194 sc.add_triangle(1, 3, 5);
195 sc
196 }
197
198 #[allow(dead_code)]
203 pub fn minimal_torus() -> Self {
204 let mut sc = Self::new();
205 for i in 0..7 {
206 let angle = 2.0 * std::f64::consts::PI * i as f64 / 7.0;
207 sc.add_vertex([angle.cos(), angle.sin(), 0.0]);
208 }
209 let tris: [[usize; 3]; 14] = [
212 [0, 1, 2],
213 [0, 2, 3],
214 [0, 3, 4],
215 [0, 4, 5],
216 [0, 5, 6],
217 [0, 6, 1],
218 [1, 3, 2],
219 [1, 4, 3],
220 [1, 5, 4],
221 [1, 6, 5],
222 [2, 6, 3],
223 [3, 5, 6],
224 [2, 4, 6],
225 [2, 5, 4],
226 ];
227 for [a, b, c] in tris {
228 sc.add_edge(a, b);
229 sc.add_edge(b, c);
230 sc.add_edge(a, c);
231 sc.add_triangle(a, b, c);
232 }
233 sc
234 }
235}
236
237impl Default for SimplicialComplex {
238 fn default() -> Self {
239 Self::new()
240 }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq)]
245pub struct BirthDeathPair {
246 pub birth: f64,
248 pub death: f64,
250 pub dim: usize,
252}
253
254impl BirthDeathPair {
255 #[allow(dead_code)]
257 pub fn persistence(&self) -> f64 {
258 self.death - self.birth
259 }
260
261 #[allow(dead_code)]
263 pub fn is_essential(&self) -> bool {
264 self.death.is_infinite()
265 }
266}
267
268#[derive(Debug, Clone)]
273pub struct PersistentHomology {
274 pub pairs: Vec<BirthDeathPair>,
276}
277
278impl PersistentHomology {
279 #[allow(dead_code)]
281 pub fn new() -> Self {
282 Self { pairs: Vec::new() }
283 }
284
285 #[allow(dead_code)]
294 pub fn compute_0d(points: &[[f64; 3]]) -> Self {
295 let n = points.len();
296 if n == 0 {
297 return Self::new();
298 }
299 let mut edges: Vec<(f64, usize, usize)> = Vec::new();
301 for i in 0..n {
302 for j in i + 1..n {
303 let d = dist3(&points[i], &points[j]);
304 edges.push((d, i, j));
305 }
306 }
307 edges.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
308
309 let mut uf = UnionFind::new(n);
310 let mut pairs = Vec::new();
311
312 for _ in 0..n {
314 }
316
317 for (dist, i, j) in edges {
318 let ri = uf.find(i);
319 let rj = uf.find(j);
320 if ri != rj {
321 let born_i = 0.0_f64;
323 let born_j = 0.0_f64;
324 let dying = if born_i >= born_j { ri } else { rj };
325 let _ = dying; pairs.push(BirthDeathPair {
327 birth: 0.0,
328 death: dist,
329 dim: 0,
330 });
331 uf.union(i, j);
332 }
333 }
334 pairs.push(BirthDeathPair {
336 birth: 0.0,
337 death: f64::INFINITY,
338 dim: 0,
339 });
340
341 Self { pairs }
342 }
343
344 #[allow(dead_code)]
346 pub fn diagram(&self) -> &[BirthDeathPair] {
347 &self.pairs
348 }
349
350 #[allow(dead_code)]
355 pub fn significant_features(&self, threshold: f64) -> usize {
356 self.pairs
357 .iter()
358 .filter(|p| p.persistence() > threshold)
359 .count()
360 }
361
362 #[allow(dead_code)]
371 pub fn bottleneck_distance(&self, other: &Self) -> f64 {
372 let d1: Vec<_> = self.pairs.iter().filter(|p| p.death.is_finite()).collect();
374 let d2: Vec<_> = other.pairs.iter().filter(|p| p.death.is_finite()).collect();
375
376 if d1.is_empty() && d2.is_empty() {
377 return 0.0;
378 }
379
380 let mut max_cost = 0.0_f64;
381
382 for p1 in &d1 {
384 let diag_dist = (p1.death - p1.birth) / 2.0;
385 let min_d2 = d2
386 .iter()
387 .map(|p2| (p1.birth - p2.birth).abs().max((p1.death - p2.death).abs()))
388 .fold(f64::INFINITY, f64::min);
389 max_cost = max_cost.max(min_d2.min(diag_dist));
390 }
391
392 for p2 in &d2 {
393 let diag_dist = (p2.death - p2.birth) / 2.0;
394 let min_d1 = d1
395 .iter()
396 .map(|p1| (p1.birth - p2.birth).abs().max((p1.death - p2.death).abs()))
397 .fold(f64::INFINITY, f64::min);
398 max_cost = max_cost.max(min_d1.min(diag_dist));
399 }
400
401 max_cost
402 }
403}
404
405impl Default for PersistentHomology {
406 fn default() -> Self {
407 Self::new()
408 }
409}
410
411#[derive(Debug, Clone)]
413struct UnionFind {
414 parent: Vec<usize>,
415 rank: Vec<usize>,
416}
417
418impl UnionFind {
419 fn new(n: usize) -> Self {
420 Self {
421 parent: (0..n).collect(),
422 rank: vec![0; n],
423 }
424 }
425
426 fn find(&mut self, x: usize) -> usize {
427 if self.parent[x] != x {
428 self.parent[x] = self.find(self.parent[x]);
429 }
430 self.parent[x]
431 }
432
433 fn union(&mut self, x: usize, y: usize) {
434 let rx = self.find(x);
435 let ry = self.find(y);
436 if rx == ry {
437 return;
438 }
439 if self.rank[rx] < self.rank[ry] {
440 self.parent[rx] = ry;
441 } else if self.rank[rx] > self.rank[ry] {
442 self.parent[ry] = rx;
443 } else {
444 self.parent[ry] = rx;
445 self.rank[rx] += 1;
446 }
447 }
448
449 fn n_components(&mut self) -> usize {
450 let n = self.parent.len();
451 let roots: std::collections::HashSet<usize> = (0..n).map(|i| self.find(i)).collect();
452 roots.len()
453 }
454}
455
456#[derive(Debug, Clone)]
461pub struct ReebGraph {
462 pub nodes: Vec<ReebNode>,
464 pub arcs: Vec<ReebArc>,
466}
467
468#[derive(Debug, Clone)]
470pub struct ReebNode {
471 pub value: f64,
473 pub kind: CriticalPointKind,
475 pub vertex_idx: usize,
477}
478
479#[derive(Debug, Clone, PartialEq)]
481pub enum CriticalPointKind {
482 Minimum,
484 Saddle,
486 Maximum,
488}
489
490#[derive(Debug, Clone)]
492pub struct ReebArc {
493 pub source: usize,
495 pub target: usize,
497}
498
499impl ReebGraph {
500 #[allow(dead_code)]
502 pub fn new() -> Self {
503 Self {
504 nodes: Vec::new(),
505 arcs: Vec::new(),
506 }
507 }
508
509 #[allow(dead_code)]
515 pub fn compute(values: &[f64], edges: &[[usize; 2]]) -> Self {
516 let n = values.len();
517 if n == 0 {
518 return Self::new();
519 }
520
521 let mut sorted_idx: Vec<usize> = (0..n).collect();
523 sorted_idx.sort_by(|&a, &b| {
524 values[a]
525 .partial_cmp(&values[b])
526 .unwrap_or(std::cmp::Ordering::Equal)
527 });
528
529 let mut nodes = Vec::new();
530 let mut arcs = Vec::new();
531
532 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
534 for &[i, j] in edges {
535 adj[i].push(j);
536 adj[j].push(i);
537 }
538
539 for &v in &sorted_idx {
541 let fv = values[v];
542 let lower_nbrs: Vec<usize> = adj[v]
543 .iter()
544 .filter(|&&u| values[u] < fv)
545 .cloned()
546 .collect();
547 let upper_nbrs: Vec<usize> = adj[v]
548 .iter()
549 .filter(|&&u| values[u] > fv)
550 .cloned()
551 .collect();
552
553 let kind = if lower_nbrs.is_empty() {
554 CriticalPointKind::Minimum
555 } else if upper_nbrs.is_empty() {
556 CriticalPointKind::Maximum
557 } else {
558 CriticalPointKind::Saddle
559 };
560
561 if matches!(
562 kind,
563 CriticalPointKind::Minimum | CriticalPointKind::Maximum | CriticalPointKind::Saddle
564 ) {
565 if matches!(
567 kind,
568 CriticalPointKind::Minimum | CriticalPointKind::Maximum
569 ) {
570 let node_idx = nodes.len();
571 nodes.push(ReebNode {
572 value: fv,
573 kind,
574 vertex_idx: v,
575 });
576 if node_idx > 0 {
577 arcs.push(ReebArc {
578 source: node_idx - 1,
579 target: node_idx,
580 });
581 }
582 }
583 }
584 }
585
586 Self { nodes, arcs }
587 }
588
589 #[allow(dead_code)]
591 pub fn n_critical_points(&self) -> usize {
592 self.nodes.len()
593 }
594
595 #[allow(dead_code)]
597 pub fn n_minima(&self) -> usize {
598 self.nodes
599 .iter()
600 .filter(|n| n.kind == CriticalPointKind::Minimum)
601 .count()
602 }
603
604 #[allow(dead_code)]
606 pub fn n_maxima(&self) -> usize {
607 self.nodes
608 .iter()
609 .filter(|n| n.kind == CriticalPointKind::Maximum)
610 .count()
611 }
612
613 #[allow(dead_code)]
615 pub fn n_saddles(&self) -> usize {
616 self.nodes
617 .iter()
618 .filter(|n| n.kind == CriticalPointKind::Saddle)
619 .count()
620 }
621}
622
623impl Default for ReebGraph {
624 fn default() -> Self {
625 Self::new()
626 }
627}
628
629#[derive(Debug, Clone)]
633pub struct MorseComplex {
634 pub critical_points: Vec<MorseCriticalPoint>,
636 pub connections: Vec<(usize, usize)>,
638}
639
640#[derive(Debug, Clone)]
642pub struct MorseCriticalPoint {
643 pub vertex_idx: usize,
645 pub value: f64,
647 pub morse_index: usize,
649}
650
651impl MorseComplex {
652 #[allow(dead_code)]
654 pub fn new() -> Self {
655 Self {
656 critical_points: Vec::new(),
657 connections: Vec::new(),
658 }
659 }
660
661 #[allow(dead_code)]
667 pub fn compute(values: &[f64], edges: &[[usize; 2]]) -> Self {
668 let n = values.len();
669 if n == 0 {
670 return Self::new();
671 }
672 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
673 for &[i, j] in edges {
674 adj[i].push(j);
675 adj[j].push(i);
676 }
677
678 let mut critical_points = Vec::new();
679
680 for v in 0..n {
681 let fv = values[v];
682 let n_lower = adj[v].iter().filter(|&&u| values[u] < fv).count();
683 let n_upper = adj[v].iter().filter(|&&u| values[u] > fv).count();
684
685 let morse_index = if n_lower == 0 && n_upper > 0 {
686 Some(0) } else if n_upper == 0 && n_lower > 0 {
688 Some(2) } else if n_lower > 0 && n_upper > 0 {
690 Some(1)
692 } else {
693 None
694 };
695
696 if let Some(idx) = morse_index {
697 critical_points.push(MorseCriticalPoint {
698 vertex_idx: v,
699 value: fv,
700 morse_index: idx,
701 });
702 }
703 }
704
705 critical_points.sort_by(|a, b| {
707 a.value
708 .partial_cmp(&b.value)
709 .unwrap_or(std::cmp::Ordering::Equal)
710 });
711 let connections: Vec<(usize, usize)> = (0..critical_points.len().saturating_sub(1))
712 .map(|i| (i, i + 1))
713 .collect();
714
715 Self {
716 critical_points,
717 connections,
718 }
719 }
720
721 #[allow(dead_code)]
723 pub fn n_minima(&self) -> usize {
724 self.critical_points
725 .iter()
726 .filter(|c| c.morse_index == 0)
727 .count()
728 }
729
730 #[allow(dead_code)]
732 pub fn n_saddles(&self) -> usize {
733 self.critical_points
734 .iter()
735 .filter(|c| c.morse_index == 1)
736 .count()
737 }
738
739 #[allow(dead_code)]
741 pub fn n_maxima(&self) -> usize {
742 self.critical_points
743 .iter()
744 .filter(|c| c.morse_index == 2)
745 .count()
746 }
747
748 #[allow(dead_code)]
750 pub fn morse_relation_holds(&self) -> bool {
751 let nm = self.n_minima();
752 let ns = self.n_saddles();
753 let nmax = self.n_maxima();
754 nm as i64 - ns as i64 + nmax as i64 >= 0
756 }
757}
758
759impl Default for MorseComplex {
760 fn default() -> Self {
761 Self::new()
762 }
763}
764
765#[derive(Debug, Clone)]
770pub struct AlphaComplex {
771 pub alpha: f64,
773 pub points: Vec<[f64; 3]>,
775 pub edges: Vec<[usize; 2]>,
777 pub triangles: Vec<[usize; 3]>,
779}
780
781impl AlphaComplex {
782 #[allow(dead_code)]
788 pub fn new(points: Vec<[f64; 3]>, alpha: f64) -> Self {
789 let n = points.len();
790 let mut edges = Vec::new();
791 let mut triangles = Vec::new();
792
793 for i in 0..n {
795 for j in i + 1..n {
796 let d = dist3(&points[i], &points[j]);
797 if d / 2.0 <= alpha {
798 edges.push([i, j]);
799 }
800 }
801 }
802
803 for i in 0..n {
805 for j in i + 1..n {
806 for k in j + 1..n {
807 let r = circumradius_3pts(&points[i], &points[j], &points[k]);
808 if r <= alpha {
809 triangles.push([i, j, k]);
810 }
812 }
813 }
814 }
815
816 Self {
817 alpha,
818 points,
819 edges,
820 triangles,
821 }
822 }
823
824 #[allow(dead_code)]
826 pub fn n_components(&self) -> usize {
827 let n = self.points.len();
828 if n == 0 {
829 return 0;
830 }
831 let mut uf = UnionFind::new(n);
832 for &[i, j] in &self.edges {
833 uf.union(i, j);
834 }
835 uf.n_components()
836 }
837
838 #[allow(dead_code)]
840 pub fn to_simplicial_complex(&self) -> SimplicialComplex {
841 let mut sc = SimplicialComplex::new();
842 for &p in &self.points {
843 sc.add_vertex(p);
844 }
845 for &[i, j] in &self.edges {
846 sc.add_edge(i, j);
847 }
848 for &[i, j, k] in &self.triangles {
849 sc.add_triangle(i, j, k);
850 }
851 sc
852 }
853}
854
855#[allow(dead_code)]
857fn circumradius_3pts(a: &[f64; 3], b: &[f64; 3], c: &[f64; 3]) -> f64 {
858 let ab = dist3(a, b);
859 let bc = dist3(b, c);
860 let ca = dist3(c, a);
861 let s = (ab + bc + ca) / 2.0;
862 let area_sq = s * (s - ab) * (s - bc) * (s - ca);
863 if area_sq <= 0.0 {
864 return f64::INFINITY;
865 }
866 let area = area_sq.sqrt();
867 ab * bc * ca / (4.0 * area)
868}
869
870#[allow(dead_code)]
872fn dist3(a: &[f64; 3], b: &[f64; 3]) -> f64 {
873 let dx = a[0] - b[0];
874 let dy = a[1] - b[1];
875 let dz = a[2] - b[2];
876 (dx * dx + dy * dy + dz * dz).sqrt()
877}
878
879#[derive(Debug, Clone)]
884pub struct CheckerboardComplex {
885 pub dims: [usize; 2],
887 pub data: Vec<bool>,
889}
890
891impl CheckerboardComplex {
892 #[allow(dead_code)]
898 pub fn new(dims: [usize; 2], data: Vec<bool>) -> Self {
899 Self { dims, data }
900 }
901
902 #[allow(dead_code)]
904 pub fn filled_disk(size: usize, radius: f64) -> Self {
905 let cx = size as f64 / 2.0;
906 let cy = size as f64 / 2.0;
907 let data: Vec<bool> = (0..size * size)
908 .map(|idx| {
909 let i = (idx / size) as f64;
910 let j = (idx % size) as f64;
911 ((i - cx).powi(2) + (j - cy).powi(2)).sqrt() <= radius
912 })
913 .collect();
914 Self::new([size, size], data)
915 }
916
917 #[allow(dead_code)]
919 pub fn annulus(size: usize, r_inner: f64, r_outer: f64) -> Self {
920 let cx = size as f64 / 2.0;
921 let cy = size as f64 / 2.0;
922 let data: Vec<bool> = (0..size * size)
923 .map(|idx| {
924 let i = (idx / size) as f64;
925 let j = (idx % size) as f64;
926 let r = ((i - cx).powi(2) + (j - cy).powi(2)).sqrt();
927 r >= r_inner && r <= r_outer
928 })
929 .collect();
930 Self::new([size, size], data)
931 }
932
933 #[allow(dead_code)]
935 pub fn volume(&self) -> usize {
936 self.data.iter().filter(|&&b| b).count()
937 }
938
939 #[allow(dead_code)]
943 pub fn euler_characteristic_2d(&self) -> i64 {
944 let nx = self.dims[0];
945 let ny = self.dims[1];
946 let mut v = 0_i64; let mut e = 0_i64; let mut f = 0_i64; for i in 0..nx {
951 for j in 0..ny {
952 let filled = self.data[i * ny + j];
953 if filled {
954 v += 1;
955 if i + 1 < nx && self.data[(i + 1) * ny + j] {
956 e += 1;
957 }
958 if j + 1 < ny && self.data[i * ny + j + 1] {
959 e += 1;
960 }
961 if i + 1 < nx
962 && j + 1 < ny
963 && self.data[(i + 1) * ny + j]
964 && self.data[i * ny + j + 1]
965 && self.data[(i + 1) * ny + j + 1]
966 {
967 f += 1;
968 }
969 }
970 }
971 }
972 v - e + f
973 }
974}
975
976#[derive(Debug, Clone, Copy)]
982pub struct BettiNumbers {
983 pub beta_0: usize,
985 pub beta_1: usize,
987 pub beta_2: usize,
989}
990
991impl BettiNumbers {
992 #[allow(dead_code)]
1001 pub fn from_simplicial_complex(
1002 sc: &SimplicialComplex,
1003 n_components: usize,
1004 n_voids: usize,
1005 ) -> Self {
1006 let chi = sc.euler_characteristic();
1007 let beta_1 = (n_components as i64 + n_voids as i64 - chi).max(0) as usize;
1010 Self {
1011 beta_0: n_components,
1012 beta_1,
1013 beta_2: n_voids,
1014 }
1015 }
1016
1017 #[allow(dead_code)]
1019 pub fn euler_characteristic(&self) -> i64 {
1020 self.beta_0 as i64 - self.beta_1 as i64 + self.beta_2 as i64
1021 }
1022
1023 #[allow(dead_code)]
1025 pub fn new(beta_0: usize, beta_1: usize, beta_2: usize) -> Self {
1026 Self {
1027 beta_0,
1028 beta_1,
1029 beta_2,
1030 }
1031 }
1032}
1033
1034#[derive(Debug, Clone)]
1038pub struct TopologicalNoise {
1039 pub threshold: f64,
1041}
1042
1043impl TopologicalNoise {
1044 #[allow(dead_code)]
1049 pub fn new(threshold: f64) -> Self {
1050 Self { threshold }
1051 }
1052
1053 #[allow(dead_code)]
1058 pub fn filter(&self, pairs: &[BirthDeathPair]) -> Vec<BirthDeathPair> {
1059 pairs
1060 .iter()
1061 .filter(|p| p.persistence() > self.threshold || p.is_essential())
1062 .cloned()
1063 .collect()
1064 }
1065
1066 #[allow(dead_code)]
1072 pub fn count_features(&self, pairs: &[BirthDeathPair], dim: usize) -> usize {
1073 pairs
1074 .iter()
1075 .filter(|p| p.dim == dim && (p.persistence() > self.threshold || p.is_essential()))
1076 .count()
1077 }
1078}
1079
1080#[cfg(test)]
1081mod tests {
1082 use super::*;
1083
1084 #[test]
1087 fn test_euler_characteristic_sphere() {
1088 let sc = SimplicialComplex::octahedral_sphere();
1090 assert_eq!(sc.euler_characteristic(), 2);
1091 }
1092
1093 #[test]
1094 fn test_octahedral_sphere_vertex_count() {
1095 let sc = SimplicialComplex::octahedral_sphere();
1096 assert_eq!(sc.n_vertices(), 6);
1097 }
1098
1099 #[test]
1100 fn test_octahedral_sphere_edge_count() {
1101 let sc = SimplicialComplex::octahedral_sphere();
1102 assert_eq!(sc.n_edges(), 12);
1103 }
1104
1105 #[test]
1106 fn test_octahedral_sphere_triangle_count() {
1107 let sc = SimplicialComplex::octahedral_sphere();
1108 assert_eq!(sc.n_triangles(), 8);
1109 }
1110
1111 #[test]
1112 fn test_euler_characteristic_torus() {
1113 let sc = SimplicialComplex::minimal_torus();
1115 assert_eq!(sc.euler_characteristic(), 0);
1116 }
1117
1118 #[test]
1119 fn test_single_vertex_euler() {
1120 let mut sc = SimplicialComplex::new();
1121 sc.add_vertex([0.0, 0.0, 0.0]);
1122 assert_eq!(sc.euler_characteristic(), 1);
1123 }
1124
1125 #[test]
1126 fn test_add_duplicate_edge_ignored() {
1127 let mut sc = SimplicialComplex::new();
1128 sc.add_vertex([0.0, 0.0, 0.0]);
1129 sc.add_vertex([1.0, 0.0, 0.0]);
1130 sc.add_edge(0, 1);
1131 sc.add_edge(0, 1); assert_eq!(sc.n_edges(), 1);
1133 }
1134
1135 #[test]
1136 fn test_tetrahedron_euler_characteristic() {
1137 let mut sc = SimplicialComplex::new();
1138 for i in 0..4 {
1139 sc.add_vertex([i as f64, 0.0, 0.0]);
1140 }
1141 sc.add_edge(0, 1);
1142 sc.add_edge(0, 2);
1143 sc.add_edge(0, 3);
1144 sc.add_edge(1, 2);
1145 sc.add_edge(1, 3);
1146 sc.add_edge(2, 3);
1147 sc.add_triangle(0, 1, 2);
1148 sc.add_triangle(0, 1, 3);
1149 sc.add_triangle(0, 2, 3);
1150 sc.add_triangle(1, 2, 3);
1151 assert_eq!(sc.euler_characteristic(), 2);
1153 }
1154
1155 #[test]
1156 fn test_boundary_1_dimensions() {
1157 let sc = SimplicialComplex::octahedral_sphere();
1158 let b1 = sc.boundary_1();
1159 assert_eq!(b1.len(), sc.n_vertices() * sc.n_edges());
1160 }
1161
1162 #[test]
1163 fn test_boundary_2_dimensions() {
1164 let sc = SimplicialComplex::octahedral_sphere();
1165 let b2 = sc.boundary_2();
1166 assert_eq!(b2.len(), sc.n_edges() * sc.n_triangles());
1167 }
1168
1169 #[test]
1172 fn test_persistent_homology_single_point() {
1173 let pts = [[0.0, 0.0, 0.0]];
1174 let ph = PersistentHomology::compute_0d(&pts);
1175 assert_eq!(ph.significant_features(0.0), 1);
1177 }
1178
1179 #[test]
1180 fn test_persistent_homology_two_points() {
1181 let pts = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
1182 let ph = PersistentHomology::compute_0d(&pts);
1183 assert_eq!(ph.pairs.len(), 2);
1185 }
1186
1187 #[test]
1188 fn test_persistent_homology_one_blob() {
1189 let pts: Vec<[f64; 3]> = (0..5).map(|i| [i as f64 * 0.1, 0.0, 0.0]).collect();
1190 let ph = PersistentHomology::compute_0d(&pts);
1191 let essential = ph.pairs.iter().filter(|p| p.is_essential()).count();
1193 assert_eq!(essential, 1);
1194 }
1195
1196 #[test]
1197 fn test_persistent_homology_birth_death_order() {
1198 let pts = [[0.0, 0.0, 0.0], [0.1, 0.0, 0.0], [5.0, 0.0, 0.0]];
1199 let ph = PersistentHomology::compute_0d(&pts);
1200 for p in ph.pairs.iter().filter(|p| p.death.is_finite()) {
1201 assert!(p.birth <= p.death);
1202 }
1203 }
1204
1205 #[test]
1206 fn test_bottleneck_distance_nonneg() {
1207 let pts1 = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
1208 let pts2 = [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
1209 let ph1 = PersistentHomology::compute_0d(&pts1);
1210 let ph2 = PersistentHomology::compute_0d(&pts2);
1211 let d = ph1.bottleneck_distance(&ph2);
1212 assert!(d >= 0.0);
1213 }
1214
1215 #[test]
1216 fn test_bottleneck_distance_self_zero() {
1217 let pts = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
1218 let ph = PersistentHomology::compute_0d(&pts);
1219 let d = ph.bottleneck_distance(&ph);
1220 assert!(d < 1e-10);
1221 }
1222
1223 #[test]
1224 fn test_persistence_positive() {
1225 let p = BirthDeathPair {
1226 birth: 0.5,
1227 death: 1.5,
1228 dim: 0,
1229 };
1230 assert!((p.persistence() - 1.0).abs() < 1e-10);
1231 }
1232
1233 #[test]
1236 fn test_reeb_graph_height_sphere_two_critical_points() {
1237 let values = vec![0.0, 0.5, 1.0, 0.7, 0.3];
1239 let edges = [[0usize, 1], [1, 2], [2, 3], [3, 4]];
1240 let rg = ReebGraph::compute(&values, &edges);
1241 assert!(rg.n_minima() >= 1);
1242 assert!(rg.n_maxima() >= 1);
1243 }
1244
1245 #[test]
1246 fn test_reeb_graph_monotone_no_saddle() {
1247 let values = vec![0.0, 1.0, 2.0, 3.0, 4.0];
1248 let edges = [[0usize, 1], [1, 2], [2, 3], [3, 4]];
1249 let rg = ReebGraph::compute(&values, &edges);
1250 assert_eq!(rg.n_saddles(), 0);
1252 }
1253
1254 #[test]
1255 fn test_reeb_graph_empty() {
1256 let rg = ReebGraph::compute(&[], &[]);
1257 assert_eq!(rg.n_critical_points(), 0);
1258 }
1259
1260 #[test]
1263 fn test_morse_complex_line() {
1264 let values = vec![1.0, 0.5, 0.8, 0.3, 0.7];
1265 let edges = [[0usize, 1], [1, 2], [2, 3], [3, 4]];
1266 let mc = MorseComplex::compute(&values, &edges);
1267 assert!(mc.n_minima() >= 1);
1268 assert!(mc.n_maxima() >= 1);
1269 }
1270
1271 #[test]
1272 fn test_morse_relation_holds() {
1273 let values = vec![1.0, 3.0, 0.5, 2.0, 0.3];
1274 let edges = [[0usize, 1], [1, 2], [2, 3], [3, 4], [0, 4]];
1275 let mc = MorseComplex::compute(&values, &edges);
1276 assert!(mc.morse_relation_holds());
1277 }
1278
1279 #[test]
1280 fn test_morse_empty() {
1281 let mc = MorseComplex::compute(&[], &[]);
1282 assert_eq!(mc.n_minima() + mc.n_saddles() + mc.n_maxima(), 0);
1283 }
1284
1285 #[test]
1286 fn test_morse_single_vertex() {
1287 let values = vec![1.0];
1288 let mc = MorseComplex::compute(&values, &[]);
1289 assert_eq!(mc.critical_points.len(), 0);
1291 }
1292
1293 #[test]
1296 fn test_alpha_complex_large_alpha_all_edges() {
1297 let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
1298 let ac = AlphaComplex::new(pts, 1000.0);
1299 assert_eq!(ac.edges.len(), 3);
1301 }
1302
1303 #[test]
1304 fn test_alpha_complex_zero_alpha_no_edges() {
1305 let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
1306 let ac = AlphaComplex::new(pts, 0.0);
1307 assert_eq!(ac.edges.len(), 0);
1308 }
1309
1310 #[test]
1311 fn test_alpha_complex_components() {
1312 let pts = vec![[0.0, 0.0, 0.0], [0.1, 0.0, 0.0], [10.0, 0.0, 0.0]];
1313 let ac = AlphaComplex::new(pts, 0.1);
1314 assert_eq!(ac.n_components(), 2);
1316 }
1317
1318 #[test]
1319 fn test_alpha_complex_single_component_large_alpha() {
1320 let pts: Vec<[f64; 3]> = (0..4).map(|i| [i as f64 * 0.5, 0.0, 0.0]).collect();
1321 let ac = AlphaComplex::new(pts, 1000.0);
1322 assert_eq!(ac.n_components(), 1);
1323 }
1324
1325 #[test]
1328 fn test_checkerboard_filled_disk_volume() {
1329 let cc = CheckerboardComplex::filled_disk(10, 4.0);
1330 assert!(cc.volume() > 0);
1331 }
1332
1333 #[test]
1334 fn test_checkerboard_annulus_volume() {
1335 let cc = CheckerboardComplex::annulus(20, 3.0, 7.0);
1336 assert!(cc.volume() > 0);
1337 }
1338
1339 #[test]
1340 fn test_checkerboard_euler_single_square() {
1341 let cc = CheckerboardComplex::new([1, 1], vec![true]);
1342 let chi = cc.euler_characteristic_2d();
1343 assert_eq!(chi, 1); }
1345
1346 #[test]
1349 fn test_betti_euler_consistency() {
1350 let beta = BettiNumbers::new(1, 2, 1);
1351 assert_eq!(beta.euler_characteristic(), 0);
1353 }
1354
1355 #[test]
1356 fn test_betti_sphere_euler() {
1357 let beta = BettiNumbers::new(1, 0, 1);
1358 assert_eq!(beta.euler_characteristic(), 2);
1360 }
1361
1362 #[test]
1363 fn test_betti_from_simplicial_sphere() {
1364 let sc = SimplicialComplex::octahedral_sphere();
1365 let beta = BettiNumbers::from_simplicial_complex(&sc, 1, 0);
1366 assert_eq!(beta.beta_0, 1);
1368 }
1369
1370 #[test]
1373 fn test_topological_noise_removes_small_features() {
1374 let noise = TopologicalNoise::new(0.5);
1375 let pairs = vec![
1376 BirthDeathPair {
1377 birth: 0.0,
1378 death: 0.1,
1379 dim: 0,
1380 },
1381 BirthDeathPair {
1382 birth: 0.0,
1383 death: 1.0,
1384 dim: 0,
1385 },
1386 ];
1387 let filtered = noise.filter(&pairs);
1388 assert_eq!(filtered.len(), 1);
1389 }
1390
1391 #[test]
1392 fn test_topological_noise_keeps_essential() {
1393 let noise = TopologicalNoise::new(100.0);
1394 let pairs = vec![BirthDeathPair {
1395 birth: 0.0,
1396 death: f64::INFINITY,
1397 dim: 0,
1398 }];
1399 let filtered = noise.filter(&pairs);
1400 assert_eq!(filtered.len(), 1);
1401 }
1402
1403 #[test]
1404 fn test_topological_noise_count_features() {
1405 let noise = TopologicalNoise::new(0.3);
1406 let pairs = vec![
1407 BirthDeathPair {
1408 birth: 0.0,
1409 death: 0.1,
1410 dim: 0,
1411 },
1412 BirthDeathPair {
1413 birth: 0.0,
1414 death: 0.5,
1415 dim: 0,
1416 },
1417 BirthDeathPair {
1418 birth: 0.0,
1419 death: f64::INFINITY,
1420 dim: 0,
1421 },
1422 ];
1423 assert_eq!(noise.count_features(&pairs, 0), 2);
1424 }
1425
1426 #[test]
1427 fn test_topological_noise_zero_threshold() {
1428 let noise = TopologicalNoise::new(0.0);
1429 let pairs = vec![
1430 BirthDeathPair {
1431 birth: 0.0,
1432 death: 0.001,
1433 dim: 1,
1434 },
1435 BirthDeathPair {
1436 birth: 0.0,
1437 death: 1.0,
1438 dim: 1,
1439 },
1440 ];
1441 let filtered = noise.filter(&pairs);
1442 assert_eq!(filtered.len(), 2);
1443 }
1444
1445 #[test]
1446 fn test_circumradius_equilateral() {
1447 let a = [0.0, 0.0, 0.0];
1448 let b = [1.0, 0.0, 0.0];
1449 let c = [0.5, (3.0_f64).sqrt() / 2.0, 0.0];
1450 let r = circumradius_3pts(&a, &b, &c);
1451 assert!((r - 1.0 / 3.0_f64.sqrt()).abs() < 1e-6);
1453 }
1454}