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 pub fn new() -> Self {
36 Self {
37 vertices: Vec::new(),
38 edges: Vec::new(),
39 triangles: Vec::new(),
40 tetrahedra: Vec::new(),
41 }
42 }
43
44 pub fn add_vertex(&mut self, pos: [f64; 3]) -> usize {
46 let idx = self.vertices.len();
47 self.vertices.push(pos);
48 idx
49 }
50
51 pub fn add_edge(&mut self, i: usize, j: usize) {
53 let e = if i < j { [i, j] } else { [j, i] };
54 if !self.edges.contains(&e) {
55 self.edges.push(e);
56 }
57 }
58
59 pub fn add_triangle(&mut self, i: usize, j: usize, k: usize) {
61 let mut t = [i, j, k];
62 t.sort_unstable();
63 if !self.triangles.contains(&t) {
64 self.triangles.push(t);
65 }
66 }
67
68 pub fn add_tetrahedron(&mut self, i: usize, j: usize, k: usize, l: usize) {
70 let mut t = [i, j, k, l];
71 t.sort_unstable();
72 if !self.tetrahedra.contains(&t) {
73 self.tetrahedra.push(t);
74 }
75 }
76
77 pub fn euler_characteristic(&self) -> i64 {
79 self.vertices.len() as i64 - self.edges.len() as i64 + self.triangles.len() as i64
80 - self.tetrahedra.len() as i64
81 }
82
83 pub fn n_vertices(&self) -> usize {
85 self.vertices.len()
86 }
87
88 pub fn n_edges(&self) -> usize {
90 self.edges.len()
91 }
92
93 pub fn n_triangles(&self) -> usize {
95 self.triangles.len()
96 }
97
98 pub fn n_tetrahedra(&self) -> usize {
100 self.tetrahedra.len()
101 }
102
103 pub fn boundary_1(&self) -> Vec<i32> {
107 let nv = self.vertices.len();
108 let ne = self.edges.len();
109 let mut b = vec![0_i32; nv * ne];
110 for (j, &[i0, i1]) in self.edges.iter().enumerate() {
111 b[i0 * ne + j] = -1;
112 b[i1 * ne + j] = 1;
113 }
114 b
115 }
116
117 pub fn boundary_2(&self) -> Vec<i32> {
121 let ne = self.edges.len();
122 let nf = self.triangles.len();
123 let mut b = vec![0_i32; ne * nf];
124 for (j, &[a, b_idx, c]) in self.triangles.iter().enumerate() {
125 let tri_edges = [[a, b_idx], [b_idx, c], [a, c]];
127 let signs = [1_i32, 1, -1];
128 for (k, &e) in tri_edges.iter().enumerate() {
129 let es = if e[0] < e[1] {
130 [e[0], e[1]]
131 } else {
132 [e[1], e[0]]
133 };
134 if let Some(ei) = self.edges.iter().position(|&x| x == es) {
135 b[ei * nf + j] = signs[k];
136 }
137 }
138 }
139 b
140 }
141
142 pub fn octahedral_sphere() -> Self {
147 let mut sc = Self::new();
148 sc.add_vertex([1.0, 0.0, 0.0]);
150 sc.add_vertex([-1.0, 0.0, 0.0]);
151 sc.add_vertex([0.0, 1.0, 0.0]);
152 sc.add_vertex([0.0, -1.0, 0.0]);
153 sc.add_vertex([0.0, 0.0, 1.0]);
154 sc.add_vertex([0.0, 0.0, -1.0]);
155 let edges = [
157 [0, 2],
158 [0, 3],
159 [0, 4],
160 [0, 5],
161 [1, 2],
162 [1, 3],
163 [1, 4],
164 [1, 5],
165 [2, 4],
166 [2, 5],
167 [3, 4],
168 [3, 5],
169 ];
170 for [i, j] in edges {
171 sc.add_edge(i, j);
172 }
173 sc.add_triangle(0, 2, 4);
175 sc.add_triangle(0, 2, 5);
176 sc.add_triangle(0, 3, 4);
177 sc.add_triangle(0, 3, 5);
178 sc.add_triangle(1, 2, 4);
179 sc.add_triangle(1, 2, 5);
180 sc.add_triangle(1, 3, 4);
181 sc.add_triangle(1, 3, 5);
182 sc
183 }
184
185 pub fn minimal_torus() -> Self {
190 let mut sc = Self::new();
191 for i in 0..7 {
192 let angle = 2.0 * std::f64::consts::PI * i as f64 / 7.0;
193 sc.add_vertex([angle.cos(), angle.sin(), 0.0]);
194 }
195 let tris: [[usize; 3]; 14] = [
198 [0, 1, 2],
199 [0, 2, 3],
200 [0, 3, 4],
201 [0, 4, 5],
202 [0, 5, 6],
203 [0, 6, 1],
204 [1, 3, 2],
205 [1, 4, 3],
206 [1, 5, 4],
207 [1, 6, 5],
208 [2, 6, 3],
209 [3, 5, 6],
210 [2, 4, 6],
211 [2, 5, 4],
212 ];
213 for [a, b, c] in tris {
214 sc.add_edge(a, b);
215 sc.add_edge(b, c);
216 sc.add_edge(a, c);
217 sc.add_triangle(a, b, c);
218 }
219 sc
220 }
221}
222
223impl Default for SimplicialComplex {
224 fn default() -> Self {
225 Self::new()
226 }
227}
228
229#[derive(Debug, Clone, Copy, PartialEq)]
231pub struct BirthDeathPair {
232 pub birth: f64,
234 pub death: f64,
236 pub dim: usize,
238}
239
240impl BirthDeathPair {
241 pub fn persistence(&self) -> f64 {
243 self.death - self.birth
244 }
245
246 pub fn is_essential(&self) -> bool {
248 self.death.is_infinite()
249 }
250}
251
252#[derive(Debug, Clone)]
257pub struct PersistentHomology {
258 pub pairs: Vec<BirthDeathPair>,
260}
261
262impl PersistentHomology {
263 pub fn new() -> Self {
265 Self { pairs: Vec::new() }
266 }
267
268 pub fn compute_0d(points: &[[f64; 3]]) -> Self {
277 let n = points.len();
278 if n == 0 {
279 return Self::new();
280 }
281 let mut edges: Vec<(f64, usize, usize)> = Vec::new();
283 for i in 0..n {
284 for j in i + 1..n {
285 let d = dist3(&points[i], &points[j]);
286 edges.push((d, i, j));
287 }
288 }
289 edges.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
290
291 let mut uf = UnionFind::new(n);
292 let mut pairs = Vec::new();
293
294 for _ in 0..n {
296 }
298
299 for (dist, i, j) in edges {
300 let ri = uf.find(i);
301 let rj = uf.find(j);
302 if ri != rj {
303 let born_i = 0.0_f64;
305 let born_j = 0.0_f64;
306 let dying = if born_i >= born_j { ri } else { rj };
307 let _ = dying; pairs.push(BirthDeathPair {
309 birth: 0.0,
310 death: dist,
311 dim: 0,
312 });
313 uf.union(i, j);
314 }
315 }
316 pairs.push(BirthDeathPair {
318 birth: 0.0,
319 death: f64::INFINITY,
320 dim: 0,
321 });
322
323 Self { pairs }
324 }
325
326 pub fn diagram(&self) -> &[BirthDeathPair] {
328 &self.pairs
329 }
330
331 pub fn significant_features(&self, threshold: f64) -> usize {
336 self.pairs
337 .iter()
338 .filter(|p| p.persistence() > threshold)
339 .count()
340 }
341
342 pub fn bottleneck_distance(&self, other: &Self) -> f64 {
351 let d1: Vec<_> = self.pairs.iter().filter(|p| p.death.is_finite()).collect();
353 let d2: Vec<_> = other.pairs.iter().filter(|p| p.death.is_finite()).collect();
354
355 if d1.is_empty() && d2.is_empty() {
356 return 0.0;
357 }
358
359 let mut max_cost = 0.0_f64;
360
361 for p1 in &d1 {
363 let diag_dist = (p1.death - p1.birth) / 2.0;
364 let min_d2 = d2
365 .iter()
366 .map(|p2| (p1.birth - p2.birth).abs().max((p1.death - p2.death).abs()))
367 .fold(f64::INFINITY, f64::min);
368 max_cost = max_cost.max(min_d2.min(diag_dist));
369 }
370
371 for p2 in &d2 {
372 let diag_dist = (p2.death - p2.birth) / 2.0;
373 let min_d1 = d1
374 .iter()
375 .map(|p1| (p1.birth - p2.birth).abs().max((p1.death - p2.death).abs()))
376 .fold(f64::INFINITY, f64::min);
377 max_cost = max_cost.max(min_d1.min(diag_dist));
378 }
379
380 max_cost
381 }
382}
383
384impl Default for PersistentHomology {
385 fn default() -> Self {
386 Self::new()
387 }
388}
389
390#[derive(Debug, Clone)]
392struct UnionFind {
393 parent: Vec<usize>,
394 rank: Vec<usize>,
395}
396
397impl UnionFind {
398 fn new(n: usize) -> Self {
399 Self {
400 parent: (0..n).collect(),
401 rank: vec![0; n],
402 }
403 }
404
405 fn find(&mut self, x: usize) -> usize {
406 if self.parent[x] != x {
407 self.parent[x] = self.find(self.parent[x]);
408 }
409 self.parent[x]
410 }
411
412 fn union(&mut self, x: usize, y: usize) {
413 let rx = self.find(x);
414 let ry = self.find(y);
415 if rx == ry {
416 return;
417 }
418 if self.rank[rx] < self.rank[ry] {
419 self.parent[rx] = ry;
420 } else if self.rank[rx] > self.rank[ry] {
421 self.parent[ry] = rx;
422 } else {
423 self.parent[ry] = rx;
424 self.rank[rx] += 1;
425 }
426 }
427
428 fn n_components(&mut self) -> usize {
429 let n = self.parent.len();
430 let roots: std::collections::HashSet<usize> = (0..n).map(|i| self.find(i)).collect();
431 roots.len()
432 }
433}
434
435#[derive(Debug, Clone)]
440pub struct ReebGraph {
441 pub nodes: Vec<ReebNode>,
443 pub arcs: Vec<ReebArc>,
445}
446
447#[derive(Debug, Clone)]
449pub struct ReebNode {
450 pub value: f64,
452 pub kind: CriticalPointKind,
454 pub vertex_idx: usize,
456}
457
458#[derive(Debug, Clone, PartialEq)]
460pub enum CriticalPointKind {
461 Minimum,
463 Saddle,
465 Maximum,
467}
468
469#[derive(Debug, Clone)]
471pub struct ReebArc {
472 pub source: usize,
474 pub target: usize,
476}
477
478impl ReebGraph {
479 pub fn new() -> Self {
481 Self {
482 nodes: Vec::new(),
483 arcs: Vec::new(),
484 }
485 }
486
487 pub fn compute(values: &[f64], edges: &[[usize; 2]]) -> Self {
493 let n = values.len();
494 if n == 0 {
495 return Self::new();
496 }
497
498 let mut sorted_idx: Vec<usize> = (0..n).collect();
500 sorted_idx.sort_by(|&a, &b| {
501 values[a]
502 .partial_cmp(&values[b])
503 .unwrap_or(std::cmp::Ordering::Equal)
504 });
505
506 let mut nodes = Vec::new();
507 let mut arcs = Vec::new();
508
509 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
511 for &[i, j] in edges {
512 adj[i].push(j);
513 adj[j].push(i);
514 }
515
516 for &v in &sorted_idx {
518 let fv = values[v];
519 let lower_nbrs: Vec<usize> = adj[v]
520 .iter()
521 .filter(|&&u| values[u] < fv)
522 .cloned()
523 .collect();
524 let upper_nbrs: Vec<usize> = adj[v]
525 .iter()
526 .filter(|&&u| values[u] > fv)
527 .cloned()
528 .collect();
529
530 let kind = if lower_nbrs.is_empty() {
531 CriticalPointKind::Minimum
532 } else if upper_nbrs.is_empty() {
533 CriticalPointKind::Maximum
534 } else {
535 CriticalPointKind::Saddle
536 };
537
538 if matches!(
539 kind,
540 CriticalPointKind::Minimum | CriticalPointKind::Maximum | CriticalPointKind::Saddle
541 ) {
542 if matches!(
544 kind,
545 CriticalPointKind::Minimum | CriticalPointKind::Maximum
546 ) {
547 let node_idx = nodes.len();
548 nodes.push(ReebNode {
549 value: fv,
550 kind,
551 vertex_idx: v,
552 });
553 if node_idx > 0 {
554 arcs.push(ReebArc {
555 source: node_idx - 1,
556 target: node_idx,
557 });
558 }
559 }
560 }
561 }
562
563 Self { nodes, arcs }
564 }
565
566 pub fn n_critical_points(&self) -> usize {
568 self.nodes.len()
569 }
570
571 pub fn n_minima(&self) -> usize {
573 self.nodes
574 .iter()
575 .filter(|n| n.kind == CriticalPointKind::Minimum)
576 .count()
577 }
578
579 pub fn n_maxima(&self) -> usize {
581 self.nodes
582 .iter()
583 .filter(|n| n.kind == CriticalPointKind::Maximum)
584 .count()
585 }
586
587 pub fn n_saddles(&self) -> usize {
589 self.nodes
590 .iter()
591 .filter(|n| n.kind == CriticalPointKind::Saddle)
592 .count()
593 }
594}
595
596impl Default for ReebGraph {
597 fn default() -> Self {
598 Self::new()
599 }
600}
601
602#[derive(Debug, Clone)]
606pub struct MorseComplex {
607 pub critical_points: Vec<MorseCriticalPoint>,
609 pub connections: Vec<(usize, usize)>,
611}
612
613#[derive(Debug, Clone)]
615pub struct MorseCriticalPoint {
616 pub vertex_idx: usize,
618 pub value: f64,
620 pub morse_index: usize,
622}
623
624impl MorseComplex {
625 pub fn new() -> Self {
627 Self {
628 critical_points: Vec::new(),
629 connections: Vec::new(),
630 }
631 }
632
633 pub fn compute(values: &[f64], edges: &[[usize; 2]]) -> Self {
639 let n = values.len();
640 if n == 0 {
641 return Self::new();
642 }
643 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
644 for &[i, j] in edges {
645 adj[i].push(j);
646 adj[j].push(i);
647 }
648
649 let mut critical_points = Vec::new();
650
651 for v in 0..n {
652 let fv = values[v];
653 let n_lower = adj[v].iter().filter(|&&u| values[u] < fv).count();
654 let n_upper = adj[v].iter().filter(|&&u| values[u] > fv).count();
655
656 let morse_index = if n_lower == 0 && n_upper > 0 {
657 Some(0) } else if n_upper == 0 && n_lower > 0 {
659 Some(2) } else if n_lower > 0 && n_upper > 0 {
661 Some(1)
663 } else {
664 None
665 };
666
667 if let Some(idx) = morse_index {
668 critical_points.push(MorseCriticalPoint {
669 vertex_idx: v,
670 value: fv,
671 morse_index: idx,
672 });
673 }
674 }
675
676 critical_points.sort_by(|a, b| {
678 a.value
679 .partial_cmp(&b.value)
680 .unwrap_or(std::cmp::Ordering::Equal)
681 });
682 let connections: Vec<(usize, usize)> = (0..critical_points.len().saturating_sub(1))
683 .map(|i| (i, i + 1))
684 .collect();
685
686 Self {
687 critical_points,
688 connections,
689 }
690 }
691
692 pub fn n_minima(&self) -> usize {
694 self.critical_points
695 .iter()
696 .filter(|c| c.morse_index == 0)
697 .count()
698 }
699
700 pub fn n_saddles(&self) -> usize {
702 self.critical_points
703 .iter()
704 .filter(|c| c.morse_index == 1)
705 .count()
706 }
707
708 pub fn n_maxima(&self) -> usize {
710 self.critical_points
711 .iter()
712 .filter(|c| c.morse_index == 2)
713 .count()
714 }
715
716 pub fn morse_relation_holds(&self) -> bool {
718 let nm = self.n_minima();
719 let ns = self.n_saddles();
720 let nmax = self.n_maxima();
721 nm as i64 - ns as i64 + nmax as i64 >= 0
723 }
724}
725
726impl Default for MorseComplex {
727 fn default() -> Self {
728 Self::new()
729 }
730}
731
732#[derive(Debug, Clone)]
737pub struct AlphaComplex {
738 pub alpha: f64,
740 pub points: Vec<[f64; 3]>,
742 pub edges: Vec<[usize; 2]>,
744 pub triangles: Vec<[usize; 3]>,
746}
747
748impl AlphaComplex {
749 pub fn new(points: Vec<[f64; 3]>, alpha: f64) -> Self {
755 let n = points.len();
756 let mut edges = Vec::new();
757 let mut triangles = Vec::new();
758
759 for i in 0..n {
761 for j in i + 1..n {
762 let d = dist3(&points[i], &points[j]);
763 if d / 2.0 <= alpha {
764 edges.push([i, j]);
765 }
766 }
767 }
768
769 for i in 0..n {
771 for j in i + 1..n {
772 for k in j + 1..n {
773 let r = circumradius_3pts(&points[i], &points[j], &points[k]);
774 if r <= alpha {
775 triangles.push([i, j, k]);
776 }
778 }
779 }
780 }
781
782 Self {
783 alpha,
784 points,
785 edges,
786 triangles,
787 }
788 }
789
790 pub fn n_components(&self) -> usize {
792 let n = self.points.len();
793 if n == 0 {
794 return 0;
795 }
796 let mut uf = UnionFind::new(n);
797 for &[i, j] in &self.edges {
798 uf.union(i, j);
799 }
800 uf.n_components()
801 }
802
803 pub fn to_simplicial_complex(&self) -> SimplicialComplex {
805 let mut sc = SimplicialComplex::new();
806 for &p in &self.points {
807 sc.add_vertex(p);
808 }
809 for &[i, j] in &self.edges {
810 sc.add_edge(i, j);
811 }
812 for &[i, j, k] in &self.triangles {
813 sc.add_triangle(i, j, k);
814 }
815 sc
816 }
817}
818
819fn circumradius_3pts(a: &[f64; 3], b: &[f64; 3], c: &[f64; 3]) -> f64 {
821 let ab = dist3(a, b);
822 let bc = dist3(b, c);
823 let ca = dist3(c, a);
824 let s = (ab + bc + ca) / 2.0;
825 let area_sq = s * (s - ab) * (s - bc) * (s - ca);
826 if area_sq <= 0.0 {
827 return f64::INFINITY;
828 }
829 let area = area_sq.sqrt();
830 ab * bc * ca / (4.0 * area)
831}
832
833fn dist3(a: &[f64; 3], b: &[f64; 3]) -> f64 {
835 let dx = a[0] - b[0];
836 let dy = a[1] - b[1];
837 let dz = a[2] - b[2];
838 (dx * dx + dy * dy + dz * dz).sqrt()
839}
840
841#[derive(Debug, Clone)]
846pub struct CheckerboardComplex {
847 pub dims: [usize; 2],
849 pub data: Vec<bool>,
851}
852
853impl CheckerboardComplex {
854 pub fn new(dims: [usize; 2], data: Vec<bool>) -> Self {
860 Self { dims, data }
861 }
862
863 pub fn filled_disk(size: usize, radius: f64) -> Self {
865 let cx = size as f64 / 2.0;
866 let cy = size as f64 / 2.0;
867 let data: Vec<bool> = (0..size * size)
868 .map(|idx| {
869 let i = (idx / size) as f64;
870 let j = (idx % size) as f64;
871 ((i - cx).powi(2) + (j - cy).powi(2)).sqrt() <= radius
872 })
873 .collect();
874 Self::new([size, size], data)
875 }
876
877 pub fn annulus(size: usize, r_inner: f64, r_outer: f64) -> Self {
879 let cx = size as f64 / 2.0;
880 let cy = size as f64 / 2.0;
881 let data: Vec<bool> = (0..size * size)
882 .map(|idx| {
883 let i = (idx / size) as f64;
884 let j = (idx % size) as f64;
885 let r = ((i - cx).powi(2) + (j - cy).powi(2)).sqrt();
886 r >= r_inner && r <= r_outer
887 })
888 .collect();
889 Self::new([size, size], data)
890 }
891
892 pub fn volume(&self) -> usize {
894 self.data.iter().filter(|&&b| b).count()
895 }
896
897 pub fn euler_characteristic_2d(&self) -> i64 {
901 let nx = self.dims[0];
902 let ny = self.dims[1];
903 let mut v = 0_i64; let mut e = 0_i64; let mut f = 0_i64; for i in 0..nx {
908 for j in 0..ny {
909 let filled = self.data[i * ny + j];
910 if filled {
911 v += 1;
912 if i + 1 < nx && self.data[(i + 1) * ny + j] {
913 e += 1;
914 }
915 if j + 1 < ny && self.data[i * ny + j + 1] {
916 e += 1;
917 }
918 if i + 1 < nx
919 && j + 1 < ny
920 && self.data[(i + 1) * ny + j]
921 && self.data[i * ny + j + 1]
922 && self.data[(i + 1) * ny + j + 1]
923 {
924 f += 1;
925 }
926 }
927 }
928 }
929 v - e + f
930 }
931}
932
933#[derive(Debug, Clone, Copy)]
939pub struct BettiNumbers {
940 pub beta_0: usize,
942 pub beta_1: usize,
944 pub beta_2: usize,
946}
947
948impl BettiNumbers {
949 pub fn from_simplicial_complex(
958 sc: &SimplicialComplex,
959 n_components: usize,
960 n_voids: usize,
961 ) -> Self {
962 let chi = sc.euler_characteristic();
963 let beta_1 = (n_components as i64 + n_voids as i64 - chi).max(0) as usize;
966 Self {
967 beta_0: n_components,
968 beta_1,
969 beta_2: n_voids,
970 }
971 }
972
973 pub fn euler_characteristic(&self) -> i64 {
975 self.beta_0 as i64 - self.beta_1 as i64 + self.beta_2 as i64
976 }
977
978 pub fn new(beta_0: usize, beta_1: usize, beta_2: usize) -> Self {
980 Self {
981 beta_0,
982 beta_1,
983 beta_2,
984 }
985 }
986}
987
988#[derive(Debug, Clone)]
992pub struct TopologicalNoise {
993 pub threshold: f64,
995}
996
997impl TopologicalNoise {
998 pub fn new(threshold: f64) -> Self {
1003 Self { threshold }
1004 }
1005
1006 pub fn filter(&self, pairs: &[BirthDeathPair]) -> Vec<BirthDeathPair> {
1011 pairs
1012 .iter()
1013 .filter(|p| p.persistence() > self.threshold || p.is_essential())
1014 .cloned()
1015 .collect()
1016 }
1017
1018 pub fn count_features(&self, pairs: &[BirthDeathPair], dim: usize) -> usize {
1024 pairs
1025 .iter()
1026 .filter(|p| p.dim == dim && (p.persistence() > self.threshold || p.is_essential()))
1027 .count()
1028 }
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033 use super::*;
1034
1035 #[test]
1038 fn test_euler_characteristic_sphere() {
1039 let sc = SimplicialComplex::octahedral_sphere();
1041 assert_eq!(sc.euler_characteristic(), 2);
1042 }
1043
1044 #[test]
1045 fn test_octahedral_sphere_vertex_count() {
1046 let sc = SimplicialComplex::octahedral_sphere();
1047 assert_eq!(sc.n_vertices(), 6);
1048 }
1049
1050 #[test]
1051 fn test_octahedral_sphere_edge_count() {
1052 let sc = SimplicialComplex::octahedral_sphere();
1053 assert_eq!(sc.n_edges(), 12);
1054 }
1055
1056 #[test]
1057 fn test_octahedral_sphere_triangle_count() {
1058 let sc = SimplicialComplex::octahedral_sphere();
1059 assert_eq!(sc.n_triangles(), 8);
1060 }
1061
1062 #[test]
1063 fn test_euler_characteristic_torus() {
1064 let sc = SimplicialComplex::minimal_torus();
1066 assert_eq!(sc.euler_characteristic(), 0);
1067 }
1068
1069 #[test]
1070 fn test_single_vertex_euler() {
1071 let mut sc = SimplicialComplex::new();
1072 sc.add_vertex([0.0, 0.0, 0.0]);
1073 assert_eq!(sc.euler_characteristic(), 1);
1074 }
1075
1076 #[test]
1077 fn test_add_duplicate_edge_ignored() {
1078 let mut sc = SimplicialComplex::new();
1079 sc.add_vertex([0.0, 0.0, 0.0]);
1080 sc.add_vertex([1.0, 0.0, 0.0]);
1081 sc.add_edge(0, 1);
1082 sc.add_edge(0, 1); assert_eq!(sc.n_edges(), 1);
1084 }
1085
1086 #[test]
1087 fn test_tetrahedron_euler_characteristic() {
1088 let mut sc = SimplicialComplex::new();
1089 for i in 0..4 {
1090 sc.add_vertex([i as f64, 0.0, 0.0]);
1091 }
1092 sc.add_edge(0, 1);
1093 sc.add_edge(0, 2);
1094 sc.add_edge(0, 3);
1095 sc.add_edge(1, 2);
1096 sc.add_edge(1, 3);
1097 sc.add_edge(2, 3);
1098 sc.add_triangle(0, 1, 2);
1099 sc.add_triangle(0, 1, 3);
1100 sc.add_triangle(0, 2, 3);
1101 sc.add_triangle(1, 2, 3);
1102 assert_eq!(sc.euler_characteristic(), 2);
1104 }
1105
1106 #[test]
1107 fn test_boundary_1_dimensions() {
1108 let sc = SimplicialComplex::octahedral_sphere();
1109 let b1 = sc.boundary_1();
1110 assert_eq!(b1.len(), sc.n_vertices() * sc.n_edges());
1111 }
1112
1113 #[test]
1114 fn test_boundary_2_dimensions() {
1115 let sc = SimplicialComplex::octahedral_sphere();
1116 let b2 = sc.boundary_2();
1117 assert_eq!(b2.len(), sc.n_edges() * sc.n_triangles());
1118 }
1119
1120 #[test]
1123 fn test_persistent_homology_single_point() {
1124 let pts = [[0.0, 0.0, 0.0]];
1125 let ph = PersistentHomology::compute_0d(&pts);
1126 assert_eq!(ph.significant_features(0.0), 1);
1128 }
1129
1130 #[test]
1131 fn test_persistent_homology_two_points() {
1132 let pts = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
1133 let ph = PersistentHomology::compute_0d(&pts);
1134 assert_eq!(ph.pairs.len(), 2);
1136 }
1137
1138 #[test]
1139 fn test_persistent_homology_one_blob() {
1140 let pts: Vec<[f64; 3]> = (0..5).map(|i| [i as f64 * 0.1, 0.0, 0.0]).collect();
1141 let ph = PersistentHomology::compute_0d(&pts);
1142 let essential = ph.pairs.iter().filter(|p| p.is_essential()).count();
1144 assert_eq!(essential, 1);
1145 }
1146
1147 #[test]
1148 fn test_persistent_homology_birth_death_order() {
1149 let pts = [[0.0, 0.0, 0.0], [0.1, 0.0, 0.0], [5.0, 0.0, 0.0]];
1150 let ph = PersistentHomology::compute_0d(&pts);
1151 for p in ph.pairs.iter().filter(|p| p.death.is_finite()) {
1152 assert!(p.birth <= p.death);
1153 }
1154 }
1155
1156 #[test]
1157 fn test_bottleneck_distance_nonneg() {
1158 let pts1 = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
1159 let pts2 = [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
1160 let ph1 = PersistentHomology::compute_0d(&pts1);
1161 let ph2 = PersistentHomology::compute_0d(&pts2);
1162 let d = ph1.bottleneck_distance(&ph2);
1163 assert!(d >= 0.0);
1164 }
1165
1166 #[test]
1167 fn test_bottleneck_distance_self_zero() {
1168 let pts = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
1169 let ph = PersistentHomology::compute_0d(&pts);
1170 let d = ph.bottleneck_distance(&ph);
1171 assert!(d < 1e-10);
1172 }
1173
1174 #[test]
1175 fn test_persistence_positive() {
1176 let p = BirthDeathPair {
1177 birth: 0.5,
1178 death: 1.5,
1179 dim: 0,
1180 };
1181 assert!((p.persistence() - 1.0).abs() < 1e-10);
1182 }
1183
1184 #[test]
1187 fn test_reeb_graph_height_sphere_two_critical_points() {
1188 let values = vec![0.0, 0.5, 1.0, 0.7, 0.3];
1190 let edges = [[0usize, 1], [1, 2], [2, 3], [3, 4]];
1191 let rg = ReebGraph::compute(&values, &edges);
1192 assert!(rg.n_minima() >= 1);
1193 assert!(rg.n_maxima() >= 1);
1194 }
1195
1196 #[test]
1197 fn test_reeb_graph_monotone_no_saddle() {
1198 let values = vec![0.0, 1.0, 2.0, 3.0, 4.0];
1199 let edges = [[0usize, 1], [1, 2], [2, 3], [3, 4]];
1200 let rg = ReebGraph::compute(&values, &edges);
1201 assert_eq!(rg.n_saddles(), 0);
1203 }
1204
1205 #[test]
1206 fn test_reeb_graph_empty() {
1207 let rg = ReebGraph::compute(&[], &[]);
1208 assert_eq!(rg.n_critical_points(), 0);
1209 }
1210
1211 #[test]
1214 fn test_morse_complex_line() {
1215 let values = vec![1.0, 0.5, 0.8, 0.3, 0.7];
1216 let edges = [[0usize, 1], [1, 2], [2, 3], [3, 4]];
1217 let mc = MorseComplex::compute(&values, &edges);
1218 assert!(mc.n_minima() >= 1);
1219 assert!(mc.n_maxima() >= 1);
1220 }
1221
1222 #[test]
1223 fn test_morse_relation_holds() {
1224 let values = vec![1.0, 3.0, 0.5, 2.0, 0.3];
1225 let edges = [[0usize, 1], [1, 2], [2, 3], [3, 4], [0, 4]];
1226 let mc = MorseComplex::compute(&values, &edges);
1227 assert!(mc.morse_relation_holds());
1228 }
1229
1230 #[test]
1231 fn test_morse_empty() {
1232 let mc = MorseComplex::compute(&[], &[]);
1233 assert_eq!(mc.n_minima() + mc.n_saddles() + mc.n_maxima(), 0);
1234 }
1235
1236 #[test]
1237 fn test_morse_single_vertex() {
1238 let values = vec![1.0];
1239 let mc = MorseComplex::compute(&values, &[]);
1240 assert_eq!(mc.critical_points.len(), 0);
1242 }
1243
1244 #[test]
1247 fn test_alpha_complex_large_alpha_all_edges() {
1248 let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
1249 let ac = AlphaComplex::new(pts, 1000.0);
1250 assert_eq!(ac.edges.len(), 3);
1252 }
1253
1254 #[test]
1255 fn test_alpha_complex_zero_alpha_no_edges() {
1256 let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
1257 let ac = AlphaComplex::new(pts, 0.0);
1258 assert_eq!(ac.edges.len(), 0);
1259 }
1260
1261 #[test]
1262 fn test_alpha_complex_components() {
1263 let pts = vec![[0.0, 0.0, 0.0], [0.1, 0.0, 0.0], [10.0, 0.0, 0.0]];
1264 let ac = AlphaComplex::new(pts, 0.1);
1265 assert_eq!(ac.n_components(), 2);
1267 }
1268
1269 #[test]
1270 fn test_alpha_complex_single_component_large_alpha() {
1271 let pts: Vec<[f64; 3]> = (0..4).map(|i| [i as f64 * 0.5, 0.0, 0.0]).collect();
1272 let ac = AlphaComplex::new(pts, 1000.0);
1273 assert_eq!(ac.n_components(), 1);
1274 }
1275
1276 #[test]
1279 fn test_checkerboard_filled_disk_volume() {
1280 let cc = CheckerboardComplex::filled_disk(10, 4.0);
1281 assert!(cc.volume() > 0);
1282 }
1283
1284 #[test]
1285 fn test_checkerboard_annulus_volume() {
1286 let cc = CheckerboardComplex::annulus(20, 3.0, 7.0);
1287 assert!(cc.volume() > 0);
1288 }
1289
1290 #[test]
1291 fn test_checkerboard_euler_single_square() {
1292 let cc = CheckerboardComplex::new([1, 1], vec![true]);
1293 let chi = cc.euler_characteristic_2d();
1294 assert_eq!(chi, 1); }
1296
1297 #[test]
1300 fn test_betti_euler_consistency() {
1301 let beta = BettiNumbers::new(1, 2, 1);
1302 assert_eq!(beta.euler_characteristic(), 0);
1304 }
1305
1306 #[test]
1307 fn test_betti_sphere_euler() {
1308 let beta = BettiNumbers::new(1, 0, 1);
1309 assert_eq!(beta.euler_characteristic(), 2);
1311 }
1312
1313 #[test]
1314 fn test_betti_from_simplicial_sphere() {
1315 let sc = SimplicialComplex::octahedral_sphere();
1316 let beta = BettiNumbers::from_simplicial_complex(&sc, 1, 0);
1317 assert_eq!(beta.beta_0, 1);
1319 }
1320
1321 #[test]
1324 fn test_topological_noise_removes_small_features() {
1325 let noise = TopologicalNoise::new(0.5);
1326 let pairs = vec![
1327 BirthDeathPair {
1328 birth: 0.0,
1329 death: 0.1,
1330 dim: 0,
1331 },
1332 BirthDeathPair {
1333 birth: 0.0,
1334 death: 1.0,
1335 dim: 0,
1336 },
1337 ];
1338 let filtered = noise.filter(&pairs);
1339 assert_eq!(filtered.len(), 1);
1340 }
1341
1342 #[test]
1343 fn test_topological_noise_keeps_essential() {
1344 let noise = TopologicalNoise::new(100.0);
1345 let pairs = vec![BirthDeathPair {
1346 birth: 0.0,
1347 death: f64::INFINITY,
1348 dim: 0,
1349 }];
1350 let filtered = noise.filter(&pairs);
1351 assert_eq!(filtered.len(), 1);
1352 }
1353
1354 #[test]
1355 fn test_topological_noise_count_features() {
1356 let noise = TopologicalNoise::new(0.3);
1357 let pairs = vec![
1358 BirthDeathPair {
1359 birth: 0.0,
1360 death: 0.1,
1361 dim: 0,
1362 },
1363 BirthDeathPair {
1364 birth: 0.0,
1365 death: 0.5,
1366 dim: 0,
1367 },
1368 BirthDeathPair {
1369 birth: 0.0,
1370 death: f64::INFINITY,
1371 dim: 0,
1372 },
1373 ];
1374 assert_eq!(noise.count_features(&pairs, 0), 2);
1375 }
1376
1377 #[test]
1378 fn test_topological_noise_zero_threshold() {
1379 let noise = TopologicalNoise::new(0.0);
1380 let pairs = vec![
1381 BirthDeathPair {
1382 birth: 0.0,
1383 death: 0.001,
1384 dim: 1,
1385 },
1386 BirthDeathPair {
1387 birth: 0.0,
1388 death: 1.0,
1389 dim: 1,
1390 },
1391 ];
1392 let filtered = noise.filter(&pairs);
1393 assert_eq!(filtered.len(), 2);
1394 }
1395
1396 #[test]
1397 fn test_circumradius_equilateral() {
1398 let a = [0.0, 0.0, 0.0];
1399 let b = [1.0, 0.0, 0.0];
1400 let c = [0.5, (3.0_f64).sqrt() / 2.0, 0.0];
1401 let r = circumradius_3pts(&a, &b, &c);
1402 assert!((r - 1.0 / 3.0_f64.sqrt()).abs() < 1e-6);
1404 }
1405}