1#![allow(clippy::needless_range_loop)]
2#![allow(dead_code)]
16#![allow(clippy::too_many_arguments)]
17
18#[derive(Debug, Clone, PartialEq)]
24pub struct SimplexEdge {
25 pub v0: usize,
27 pub v1: usize,
29 pub weight: f64,
31}
32
33#[derive(Debug, Clone)]
35pub struct UnionFind {
36 pub parent: Vec<usize>,
38 pub rank: Vec<usize>,
40}
41
42impl UnionFind {
43 pub fn new(n: usize) -> Self {
45 Self {
46 parent: (0..n).collect(),
47 rank: vec![0; n],
48 }
49 }
50
51 pub fn find(&mut self, i: usize) -> usize {
53 if self.parent[i] != i {
54 self.parent[i] = self.find(self.parent[i]);
55 }
56 self.parent[i]
57 }
58
59 pub fn union(&mut self, i: usize, j: usize) -> bool {
62 let ri = self.find(i);
63 let rj = self.find(j);
64 if ri == rj {
65 return false;
66 }
67 match self.rank[ri].cmp(&self.rank[rj]) {
68 std::cmp::Ordering::Less => self.parent[ri] = rj,
69 std::cmp::Ordering::Greater => self.parent[rj] = ri,
70 std::cmp::Ordering::Equal => {
71 self.parent[rj] = ri;
72 self.rank[ri] += 1;
73 }
74 }
75 true
76 }
77}
78
79#[derive(Debug, Clone, PartialEq)]
83pub struct PersistentInterval {
84 pub birth: f64,
86 pub death: f64,
88 pub dimension: usize,
90}
91
92impl PersistentInterval {
93 pub fn persistence(&self) -> f64 {
96 self.death - self.birth
97 }
98
99 pub fn is_essential(&self) -> bool {
101 self.death.is_infinite()
102 }
103}
104
105fn all_edges(points: &[[f64; 2]]) -> Vec<SimplexEdge> {
111 let n = points.len();
112 let mut edges = Vec::with_capacity(n * (n.saturating_sub(1)) / 2);
113 for i in 0..n {
114 for j in (i + 1)..n {
115 let dx = points[i][0] - points[j][0];
116 let dy = points[i][1] - points[j][1];
117 edges.push(SimplexEdge {
118 v0: i,
119 v1: j,
120 weight: (dx * dx + dy * dy).sqrt(),
121 });
122 }
123 }
124 edges
125}
126
127pub fn vietoris_rips_h0(points: &[[f64; 2]], max_r: f64) -> Vec<PersistentInterval> {
134 let n = points.len();
135 if n == 0 {
136 return vec![];
137 }
138 let mut edges = all_edges(points);
139 edges.sort_by(|a, b| {
140 a.weight
141 .partial_cmp(&b.weight)
142 .unwrap_or(std::cmp::Ordering::Equal)
143 });
144
145 let mut uf = UnionFind::new(n);
146 let mut birth = vec![0.0_f64; n];
148 let mut intervals = Vec::new();
149
150 for e in &edges {
151 if e.weight > max_r {
152 break;
153 }
154 if uf.union(e.v0, e.v1) {
155 let r0 = uf.find(e.v0);
157 let r1 = uf.find(e.v1);
158 let root_after = uf.find(e.v0);
161 let dying = if root_after == r0 { r1 } else { r0 };
162 let b = birth[dying];
163 birth[root_after] = birth[root_after].min(b);
164 intervals.push(PersistentInterval {
165 birth: b,
166 death: e.weight,
167 dimension: 0,
168 });
169 }
170 }
171
172 intervals.push(PersistentInterval {
174 birth: 0.0,
175 death: f64::INFINITY,
176 dimension: 0,
177 });
178 intervals
179}
180
181pub fn vietoris_rips_h1_approx(
191 points: &[[f64; 2]],
192 max_r: f64,
193 n_edges: usize,
194) -> Vec<PersistentInterval> {
195 let n = points.len();
196 if n == 0 {
197 return vec![];
198 }
199 let mut edges = all_edges(points);
200 edges.sort_by(|a, b| {
201 a.weight
202 .partial_cmp(&b.weight)
203 .unwrap_or(std::cmp::Ordering::Equal)
204 });
205 let edges: Vec<_> = edges
206 .into_iter()
207 .filter(|e| e.weight <= max_r)
208 .take(n_edges)
209 .collect();
210
211 let mut uf = UnionFind::new(n);
212 let mut intervals = Vec::new();
213 let mut edge_births: Vec<f64> = Vec::new(); for e in &edges {
216 if !uf.union(e.v0, e.v1) {
217 let birth = *edge_births.last().unwrap_or(&0.0);
219 intervals.push(PersistentInterval {
220 birth,
221 death: (e.weight + max_r) / 2.0,
222 dimension: 1,
223 });
224 } else {
225 edge_births.push(e.weight);
226 }
227 }
228 intervals
229}
230
231fn point_dist_inf(p: &PersistentInterval, q: &PersistentInterval) -> f64 {
238 let pb = p.birth.min(p.death);
239 let pd = p.birth.max(p.death);
240 let qb = q.birth.min(q.death);
241 let qd = q.birth.max(q.death);
242 f64::max((pb - qb).abs(), (pd - qd).abs())
243}
244
245fn dist_to_diagonal(p: &PersistentInterval) -> f64 {
247 (p.death - p.birth).abs() / 2.0
248}
249
250pub fn bottleneck_distance(a: &[PersistentInterval], b: &[PersistentInterval]) -> f64 {
256 let fa: Vec<&PersistentInterval> = a.iter().filter(|p| !p.is_essential()).collect();
258 let fb: Vec<&PersistentInterval> = b.iter().filter(|p| !p.is_essential()).collect();
259
260 let mut max_dist = 0.0_f64;
261
262 let mut used = vec![false; fb.len()];
264 for p in &fa {
265 let best = fb
266 .iter()
267 .enumerate()
268 .filter(|(idx, _)| !used[*idx])
269 .map(|(idx, q)| (idx, point_dist_inf(p, q)))
270 .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(std::cmp::Ordering::Equal));
271 if let Some((idx, d)) = best {
272 used[idx] = true;
273 max_dist = max_dist.max(d);
274 } else {
275 max_dist = max_dist.max(dist_to_diagonal(p));
276 }
277 }
278 for (idx, q) in fb.iter().enumerate() {
280 if !used[idx] {
281 max_dist = max_dist.max(dist_to_diagonal(q));
282 }
283 }
284 max_dist
285}
286
287pub fn wasserstein_distance_1(a: &[PersistentInterval], b: &[PersistentInterval]) -> f64 {
292 let fa: Vec<&PersistentInterval> = a.iter().filter(|p| !p.is_essential()).collect();
293 let fb: Vec<&PersistentInterval> = b.iter().filter(|p| !p.is_essential()).collect();
294
295 let mut total = 0.0_f64;
296 let mut used = vec![false; fb.len()];
297
298 for p in &fa {
299 let best = fb
300 .iter()
301 .enumerate()
302 .filter(|(idx, _)| !used[*idx])
303 .map(|(idx, q)| (idx, point_dist_inf(p, q)))
304 .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(std::cmp::Ordering::Equal));
305 if let Some((idx, d)) = best {
306 used[idx] = true;
307 total += d;
308 } else {
309 total += dist_to_diagonal(p);
310 }
311 }
312 for (idx, q) in fb.iter().enumerate() {
313 if !used[idx] {
314 total += dist_to_diagonal(q);
315 }
316 }
317 total
318}
319
320#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct BettiNumbers {
327 pub b0: usize,
329 pub b1: usize,
331 pub b2: usize,
333}
334
335pub fn euler_characteristic(betti: &BettiNumbers) -> i64 {
337 betti.b0 as i64 - betti.b1 as i64 + betti.b2 as i64
338}
339
340pub fn cubical_complex_1d(signal: &[f64], threshold: f64) -> BettiNumbers {
348 if signal.is_empty() {
349 return BettiNumbers {
350 b0: 0,
351 b1: 0,
352 b2: 0,
353 };
354 }
355 let below: Vec<bool> = signal.iter().map(|&v| v <= threshold).collect();
357 let mut b0 = 0usize;
358 let mut in_component = false;
359 for &b in &below {
360 if b && !in_component {
361 b0 += 1;
362 in_component = true;
363 } else if !b {
364 in_component = false;
365 }
366 }
367 BettiNumbers { b0, b1: 0, b2: 0 }
369}
370
371pub fn persistence_entropy(diagram: &[PersistentInterval]) -> f64 {
380 let lifetimes: Vec<f64> = diagram
381 .iter()
382 .filter(|p| !p.is_essential() && p.persistence() > 0.0)
383 .map(|p| p.persistence())
384 .collect();
385 if lifetimes.is_empty() {
386 return 0.0;
387 }
388 let total: f64 = lifetimes.iter().sum();
389 if total <= 0.0 {
390 return 0.0;
391 }
392 -lifetimes
393 .iter()
394 .map(|&l| {
395 let p = l / total;
396 if p > 0.0 { p * p.ln() } else { 0.0 }
397 })
398 .sum::<f64>()
399}
400
401#[derive(Debug, Clone)]
410pub struct VietorisRips {
411 pub points: Vec<[f64; 2]>,
413 pub epsilon: f64,
415}
416
417impl VietorisRips {
418 pub fn new(points: Vec<[f64; 2]>, epsilon: f64) -> Self {
420 Self { points, epsilon }
421 }
422
423 fn dist(a: &[f64; 2], b: &[f64; 2]) -> f64 {
425 let dx = a[0] - b[0];
426 let dy = a[1] - b[1];
427 (dx * dx + dy * dy).sqrt()
428 }
429
430 pub fn edges(&self) -> Vec<(usize, usize, f64)> {
434 let n = self.points.len();
435 let mut result = Vec::new();
436 for i in 0..n {
437 for j in (i + 1)..n {
438 let d = Self::dist(&self.points[i], &self.points[j]);
439 if d <= self.epsilon {
440 result.push((i, j, d));
441 }
442 }
443 }
444 result
445 }
446
447 pub fn triangles(&self) -> Vec<(usize, usize, usize)> {
451 let n = self.points.len();
452 let mut result = Vec::new();
453 for i in 0..n {
454 for j in (i + 1)..n {
455 if Self::dist(&self.points[i], &self.points[j]) > self.epsilon {
456 continue;
457 }
458 for k in (j + 1)..n {
459 if Self::dist(&self.points[i], &self.points[k]) <= self.epsilon
460 && Self::dist(&self.points[j], &self.points[k]) <= self.epsilon
461 {
462 result.push((i, j, k));
463 }
464 }
465 }
466 }
467 result
468 }
469
470 pub fn n_vertices(&self) -> usize {
472 self.points.len()
473 }
474
475 pub fn n_edges(&self) -> usize {
477 self.edges().len()
478 }
479
480 pub fn n_triangles(&self) -> usize {
482 self.triangles().len()
483 }
484
485 pub fn euler_characteristic(&self) -> i64 {
487 let v = self.n_vertices() as i64;
488 let e = self.n_edges() as i64;
489 let f = self.n_triangles() as i64;
490 v - e + f
491 }
492
493 pub fn betti_numbers(&self) -> BettiNumbers {
499 let n = self.points.len();
500 if n == 0 {
501 return BettiNumbers {
502 b0: 0,
503 b1: 0,
504 b2: 0,
505 };
506 }
507 let edges = self.edges();
508 let triangles = self.triangles();
509 let mut uf = UnionFind::new(n);
510 for (i, j, _) in &edges {
511 uf.union(*i, *j);
512 }
513 let b0 = (0..n).filter(|&i| uf.find(i) == i).count();
515 let b1 = (edges.len() as i64 - n as i64 + b0 as i64).max(0) as usize;
517 let chi = self.euler_characteristic();
519 let b2 = (chi - b0 as i64 + b1 as i64).max(0) as usize;
520 let _ = triangles; BettiNumbers { b0, b1, b2 }
522 }
523
524 pub fn persistent_h0(&self) -> Vec<PersistentInterval> {
526 vietoris_rips_h0(&self.points, self.epsilon)
527 }
528}
529
530#[derive(Debug, Clone)]
536pub struct FilteredSimplex {
537 pub vertices: Vec<usize>,
539 pub filtration: f64,
541}
542
543impl FilteredSimplex {
544 pub fn dim(&self) -> usize {
546 self.vertices.len().saturating_sub(1)
547 }
548}
549
550#[derive(Debug, Clone, Default)]
555pub struct SimplexTree {
556 pub simplices: Vec<FilteredSimplex>,
558}
559
560impl SimplexTree {
561 pub fn new() -> Self {
563 Self::default()
564 }
565
566 pub fn insert(&mut self, mut vertices: Vec<usize>, f: f64) {
570 vertices.sort_unstable();
571 self.simplices.push(FilteredSimplex {
572 vertices,
573 filtration: f,
574 });
575 }
576
577 pub fn from_vietoris_rips(vr: &VietorisRips) -> Self {
582 let mut tree = SimplexTree::new();
583 for i in 0..vr.points.len() {
585 tree.insert(vec![i], 0.0);
586 }
587 for (i, j, d) in vr.edges() {
589 tree.insert(vec![i, j], d);
590 }
591 for (i, j, k) in vr.triangles() {
593 let d01 = VietorisRips::dist(&vr.points[i], &vr.points[j]);
594 let d02 = VietorisRips::dist(&vr.points[i], &vr.points[k]);
595 let d12 = VietorisRips::dist(&vr.points[j], &vr.points[k]);
596 let f = d01.max(d02).max(d12);
597 tree.insert(vec![i, j, k], f);
598 }
599 tree
600 }
601
602 pub fn sorted_simplices(&self) -> Vec<&FilteredSimplex> {
604 let mut v: Vec<&FilteredSimplex> = self.simplices.iter().collect();
605 v.sort_by(|a, b| {
606 a.filtration
607 .partial_cmp(&b.filtration)
608 .unwrap_or(std::cmp::Ordering::Equal)
609 });
610 v
611 }
612
613 pub fn count_dim(&self, d: usize) -> usize {
615 self.simplices.iter().filter(|s| s.dim() == d).count()
616 }
617
618 pub fn max_filtration(&self) -> f64 {
620 self.simplices
621 .iter()
622 .map(|s| s.filtration)
623 .fold(0.0_f64, f64::max)
624 }
625}
626
627#[derive(Debug, Clone, Default)]
636pub struct PersistenceDiagram {
637 pub pairs: Vec<PersistentInterval>,
639}
640
641impl PersistenceDiagram {
642 pub fn new() -> Self {
644 Self::default()
645 }
646
647 pub fn add(&mut self, birth: f64, death: f64, dimension: usize) {
649 self.pairs.push(PersistentInterval {
650 birth,
651 death,
652 dimension,
653 });
654 }
655
656 pub fn from_vietoris_rips_h0(vr: &VietorisRips) -> Self {
658 let intervals = vr.persistent_h0();
659 PersistenceDiagram { pairs: intervals }
660 }
661
662 pub fn pairs_in_dim(&self, dim: usize) -> Vec<&PersistentInterval> {
664 self.pairs.iter().filter(|p| p.dimension == dim).collect()
665 }
666
667 pub fn max_persistence(&self) -> f64 {
669 self.pairs
670 .iter()
671 .filter(|p| !p.is_essential())
672 .map(|p| p.persistence())
673 .fold(0.0_f64, f64::max)
674 }
675
676 pub fn n_essential(&self) -> usize {
678 self.pairs.iter().filter(|p| p.is_essential()).count()
679 }
680
681 pub fn entropy(&self) -> f64 {
683 persistence_entropy(&self.pairs)
684 }
685
686 pub fn total_persistence(&self) -> f64 {
688 self.pairs
689 .iter()
690 .filter(|p| !p.is_essential())
691 .map(|p| p.persistence())
692 .sum()
693 }
694}
695
696#[derive(Debug, Clone)]
702pub struct BarcodeSummary {
703 pub total_persistence: f64,
705 pub n_long_features: usize,
707 pub entropy: f64,
709 pub max_persistence: f64,
711 pub n_essential: usize,
713 pub mean_persistence: f64,
715}
716
717impl BarcodeSummary {
718 pub fn from_diagram(diagram: &PersistenceDiagram, long_threshold: f64) -> Self {
722 let finite: Vec<f64> = diagram
723 .pairs
724 .iter()
725 .filter(|p| !p.is_essential())
726 .map(|p| p.persistence())
727 .collect();
728 let total_persistence = finite.iter().sum::<f64>();
729 let n_long_features = finite.iter().filter(|&&p| p >= long_threshold).count();
730 let entropy = diagram.entropy();
731 let max_persistence = finite.iter().cloned().fold(0.0_f64, f64::max);
732 let n_essential = diagram.n_essential();
733 let mean_persistence = if finite.is_empty() {
734 0.0
735 } else {
736 total_persistence / finite.len() as f64
737 };
738 BarcodeSummary {
739 total_persistence,
740 n_long_features,
741 entropy,
742 max_persistence,
743 n_essential,
744 mean_persistence,
745 }
746 }
747
748 pub fn normalised_total(&self) -> f64 {
750 if self.max_persistence < 1e-15 {
751 0.0
752 } else {
753 self.total_persistence / self.max_persistence
754 }
755 }
756}
757
758#[derive(Debug, Clone)]
766pub struct MapperNode {
767 pub point_indices: Vec<usize>,
769 pub cover_index: usize,
771}
772
773#[derive(Debug, Clone, Default)]
782pub struct MapperGraph {
783 pub nodes: Vec<MapperNode>,
785 pub edges: Vec<(usize, usize)>,
787}
788
789impl MapperGraph {
790 pub fn build(
800 points: &[[f64; 2]],
801 filter_values: &[f64],
802 n_intervals: usize,
803 overlap: f64,
804 cluster_eps: f64,
805 ) -> Self {
806 if points.is_empty() || n_intervals == 0 {
807 return Self::default();
808 }
809 let min_f = filter_values.iter().cloned().fold(f64::INFINITY, f64::min);
810 let max_f = filter_values
811 .iter()
812 .cloned()
813 .fold(f64::NEG_INFINITY, f64::max);
814 let range = (max_f - min_f).max(1e-15);
815 let step = range / n_intervals as f64;
816 let half_overlap = step * overlap.clamp(0.0, 0.99) / 2.0;
817
818 let mut graph = MapperGraph::default();
819
820 for k in 0..n_intervals {
822 let lo = min_f + k as f64 * step - half_overlap;
823 let hi = min_f + (k + 1) as f64 * step + half_overlap;
824
825 let indices_in: Vec<usize> = (0..points.len())
827 .filter(|&i| filter_values[i] >= lo && filter_values[i] <= hi)
828 .collect();
829
830 if indices_in.is_empty() {
831 continue;
832 }
833
834 let m = indices_in.len();
836 let mut uf = UnionFind::new(m);
837 for a in 0..m {
838 for b in (a + 1)..m {
839 let ia = indices_in[a];
840 let ib = indices_in[b];
841 let dx = points[ia][0] - points[ib][0];
842 let dy = points[ia][1] - points[ib][1];
843 let d = (dx * dx + dy * dy).sqrt();
844 if d <= cluster_eps {
845 uf.union(a, b);
846 }
847 }
848 }
849
850 let mut cluster_map: std::collections::HashMap<usize, Vec<usize>> =
852 std::collections::HashMap::new();
853 for a in 0..m {
854 let root = uf.find(a);
855 cluster_map.entry(root).or_default().push(indices_in[a]);
856 }
857
858 for (_root, members) in cluster_map {
860 graph.nodes.push(MapperNode {
861 point_indices: members,
862 cover_index: k,
863 });
864 }
865 }
866
867 let n_nodes = graph.nodes.len();
869 for i in 0..n_nodes {
870 for j in (i + 1)..n_nodes {
871 let set_i: std::collections::HashSet<usize> =
872 graph.nodes[i].point_indices.iter().copied().collect();
873 let shared = graph.nodes[j]
874 .point_indices
875 .iter()
876 .any(|p| set_i.contains(p));
877 if shared {
878 graph.edges.push((i, j));
879 }
880 }
881 }
882
883 graph
884 }
885
886 pub fn n_nodes(&self) -> usize {
888 self.nodes.len()
889 }
890
891 pub fn n_edges(&self) -> usize {
893 self.edges.len()
894 }
895
896 pub fn degree(&self, i: usize) -> usize {
898 self.edges
899 .iter()
900 .filter(|(a, b)| *a == i || *b == i)
901 .count()
902 }
903
904 pub fn total_point_count(&self) -> usize {
906 self.nodes.iter().map(|n| n.point_indices.len()).sum()
907 }
908}
909
910#[cfg(test)]
915mod tests {
916 use super::*;
917
918 #[test]
921 fn test_uf_new_singletons() {
922 let mut uf = UnionFind::new(5);
923 for i in 0..5 {
924 assert_eq!(uf.find(i), i);
925 }
926 }
927
928 #[test]
929 fn test_uf_union_merges() {
930 let mut uf = UnionFind::new(4);
931 assert!(uf.union(0, 1));
932 assert_eq!(uf.find(0), uf.find(1));
933 }
934
935 #[test]
936 fn test_uf_union_idempotent() {
937 let mut uf = UnionFind::new(4);
938 uf.union(0, 1);
939 assert!(
940 !uf.union(0, 1),
941 "second union of same set should return false"
942 );
943 }
944
945 #[test]
946 fn test_uf_union_chain() {
947 let mut uf = UnionFind::new(4);
948 uf.union(0, 1);
949 uf.union(1, 2);
950 uf.union(2, 3);
951 let r0 = uf.find(0);
952 assert_eq!(uf.find(3), r0);
953 }
954
955 #[test]
956 fn test_uf_different_components() {
957 let mut uf = UnionFind::new(4);
958 uf.union(0, 1);
959 uf.union(2, 3);
960 assert_ne!(uf.find(0), uf.find(2));
961 }
962
963 #[test]
964 fn test_uf_path_compression() {
965 let mut uf = UnionFind::new(6);
966 for i in 0..5 {
967 uf.union(i, i + 1);
968 }
969 let root = uf.find(5);
970 assert_eq!(uf.find(0), root);
971 }
972
973 #[test]
974 fn test_uf_union_all_returns_true_n_minus_1_times() {
975 let n = 5;
976 let mut uf = UnionFind::new(n);
977 let mut merges = 0;
978 for i in 1..n {
979 if uf.union(0, i) {
980 merges += 1;
981 }
982 }
983 assert_eq!(merges, n - 1);
984 }
985
986 #[test]
989 fn test_simplex_edge_fields() {
990 let e = SimplexEdge {
991 v0: 1,
992 v1: 3,
993 weight: 2.5,
994 };
995 assert_eq!(e.v0, 1);
996 assert_eq!(e.v1, 3);
997 assert!((e.weight - 2.5).abs() < 1e-12);
998 }
999
1000 #[test]
1003 fn test_persistent_interval_persistence() {
1004 let pi = PersistentInterval {
1005 birth: 1.0,
1006 death: 3.0,
1007 dimension: 0,
1008 };
1009 assert!((pi.persistence() - 2.0).abs() < 1e-12);
1010 }
1011
1012 #[test]
1013 fn test_persistent_interval_essential() {
1014 let pi = PersistentInterval {
1015 birth: 0.0,
1016 death: f64::INFINITY,
1017 dimension: 0,
1018 };
1019 assert!(pi.is_essential());
1020 }
1021
1022 #[test]
1023 fn test_persistent_interval_not_essential() {
1024 let pi = PersistentInterval {
1025 birth: 0.0,
1026 death: 1.0,
1027 dimension: 0,
1028 };
1029 assert!(!pi.is_essential());
1030 }
1031
1032 #[test]
1035 fn test_h0_empty() {
1036 let pts: [[f64; 2]; 0] = [];
1037 let result = vietoris_rips_h0(&pts, 10.0);
1038 assert!(result.is_empty());
1039 }
1040
1041 #[test]
1042 fn test_h0_single_point() {
1043 let pts = [[0.0, 0.0]];
1044 let result = vietoris_rips_h0(&pts, 10.0);
1045 assert_eq!(result.len(), 1);
1046 assert!(result[0].is_essential());
1047 }
1048
1049 #[test]
1050 fn test_h0_two_close_points() {
1051 let pts = [[0.0, 0.0], [0.1, 0.0]];
1052 let result = vietoris_rips_h0(&pts, 10.0);
1053 assert_eq!(result.len(), 2);
1055 let essential_count = result.iter().filter(|p| p.is_essential()).count();
1056 assert_eq!(essential_count, 1);
1057 }
1058
1059 #[test]
1060 fn test_h0_all_dimension_zero() {
1061 let pts = [[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]];
1062 let result = vietoris_rips_h0(&pts, 10.0);
1063 for pi in &result {
1064 assert_eq!(pi.dimension, 0);
1065 }
1066 }
1067
1068 #[test]
1069 fn test_h0_two_far_points_below_max_r() {
1070 let pts = [[0.0, 0.0], [100.0, 0.0]];
1071 let result = vietoris_rips_h0(&pts, 50.0);
1072 let finite_count = result.iter().filter(|p| !p.is_essential()).count();
1074 assert_eq!(finite_count, 0);
1076 }
1077
1078 #[test]
1081 fn test_h1_empty() {
1082 let pts: [[f64; 2]; 0] = [];
1083 let result = vietoris_rips_h1_approx(&pts, 10.0, 100);
1084 assert!(result.is_empty());
1085 }
1086
1087 #[test]
1088 fn test_h1_dimension_one() {
1089 let pts = [[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
1090 let result = vietoris_rips_h1_approx(&pts, 10.0, 10);
1091 for pi in &result {
1092 assert_eq!(pi.dimension, 1);
1093 }
1094 }
1095
1096 #[test]
1097 fn test_h1_triangle_has_loop() {
1098 let pts = [[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
1100 let result = vietoris_rips_h1_approx(&pts, 10.0, 3);
1101 assert_eq!(result.len(), 1);
1102 }
1103
1104 #[test]
1107 fn test_bottleneck_identical_diagrams() {
1108 let d = vec![
1109 PersistentInterval {
1110 birth: 0.0,
1111 death: 1.0,
1112 dimension: 0,
1113 },
1114 PersistentInterval {
1115 birth: 0.5,
1116 death: 2.0,
1117 dimension: 0,
1118 },
1119 ];
1120 let dist = bottleneck_distance(&d, &d);
1121 assert!(dist < 1e-12, "identical diagrams should have distance 0");
1122 }
1123
1124 #[test]
1125 fn test_bottleneck_empty_diagrams() {
1126 assert!((bottleneck_distance(&[], &[])).abs() < 1e-12);
1127 }
1128
1129 #[test]
1130 fn test_bottleneck_non_negative() {
1131 let a = vec![PersistentInterval {
1132 birth: 0.0,
1133 death: 1.0,
1134 dimension: 0,
1135 }];
1136 let b = vec![PersistentInterval {
1137 birth: 0.1,
1138 death: 1.2,
1139 dimension: 0,
1140 }];
1141 assert!(bottleneck_distance(&a, &b) >= 0.0);
1142 }
1143
1144 #[test]
1145 fn test_bottleneck_symmetry() {
1146 let a = vec![PersistentInterval {
1147 birth: 0.0,
1148 death: 1.0,
1149 dimension: 0,
1150 }];
1151 let b = vec![PersistentInterval {
1152 birth: 0.5,
1153 death: 2.0,
1154 dimension: 0,
1155 }];
1156 let dab = bottleneck_distance(&a, &b);
1157 let dba = bottleneck_distance(&b, &a);
1158 assert!((dab - dba).abs() < 1e-12);
1159 }
1160
1161 #[test]
1164 fn test_wasserstein_identical() {
1165 let d = vec![PersistentInterval {
1166 birth: 0.0,
1167 death: 2.0,
1168 dimension: 0,
1169 }];
1170 assert!(wasserstein_distance_1(&d, &d) < 1e-12);
1171 }
1172
1173 #[test]
1174 fn test_wasserstein_empty() {
1175 assert!(wasserstein_distance_1(&[], &[]).abs() < 1e-12);
1176 }
1177
1178 #[test]
1179 fn test_wasserstein_non_negative() {
1180 let a = vec![PersistentInterval {
1181 birth: 0.0,
1182 death: 1.0,
1183 dimension: 0,
1184 }];
1185 let b = vec![PersistentInterval {
1186 birth: 0.2,
1187 death: 1.5,
1188 dimension: 0,
1189 }];
1190 assert!(wasserstein_distance_1(&a, &b) >= 0.0);
1191 }
1192
1193 #[test]
1196 fn test_euler_characteristic_sphere() {
1197 let b = BettiNumbers {
1199 b0: 1,
1200 b1: 0,
1201 b2: 1,
1202 };
1203 assert_eq!(euler_characteristic(&b), 2);
1204 }
1205
1206 #[test]
1207 fn test_euler_characteristic_torus() {
1208 let b = BettiNumbers {
1210 b0: 1,
1211 b1: 2,
1212 b2: 1,
1213 };
1214 assert_eq!(euler_characteristic(&b), 0);
1215 }
1216
1217 #[test]
1218 fn test_euler_characteristic_point() {
1219 let b = BettiNumbers {
1220 b0: 1,
1221 b1: 0,
1222 b2: 0,
1223 };
1224 assert_eq!(euler_characteristic(&b), 1);
1225 }
1226
1227 #[test]
1228 fn test_euler_characteristic_circle() {
1229 let b = BettiNumbers {
1231 b0: 1,
1232 b1: 1,
1233 b2: 0,
1234 };
1235 assert_eq!(euler_characteristic(&b), 0);
1236 }
1237
1238 #[test]
1241 fn test_cubical_1d_empty() {
1242 let b = cubical_complex_1d(&[], 0.5);
1243 assert_eq!(b.b0, 0);
1244 }
1245
1246 #[test]
1247 fn test_cubical_1d_all_below() {
1248 let signal = [0.1, 0.2, 0.3];
1249 let b = cubical_complex_1d(&signal, 1.0);
1250 assert_eq!(b.b0, 1);
1251 assert_eq!(b.b1, 0);
1252 assert_eq!(b.b2, 0);
1253 }
1254
1255 #[test]
1256 fn test_cubical_1d_two_components() {
1257 let signal = [0.1, 0.9, 0.1];
1258 let b = cubical_complex_1d(&signal, 0.5);
1259 assert_eq!(b.b0, 2);
1260 }
1261
1262 #[test]
1263 fn test_cubical_1d_none_below() {
1264 let signal = [1.0, 2.0, 3.0];
1265 let b = cubical_complex_1d(&signal, 0.5);
1266 assert_eq!(b.b0, 0);
1267 }
1268
1269 #[test]
1270 fn test_cubical_1d_no_loops() {
1271 let signal = [0.1, 0.2, 0.3, 0.4];
1272 let b = cubical_complex_1d(&signal, 1.0);
1273 assert_eq!(b.b1, 0);
1274 assert_eq!(b.b2, 0);
1275 }
1276
1277 #[test]
1280 fn test_persistence_entropy_empty() {
1281 assert_eq!(persistence_entropy(&[]), 0.0);
1282 }
1283
1284 #[test]
1285 fn test_persistence_entropy_single_bar() {
1286 let d = vec![PersistentInterval {
1287 birth: 0.0,
1288 death: 1.0,
1289 dimension: 0,
1290 }];
1291 assert!(persistence_entropy(&d).abs() < 1e-12);
1293 }
1294
1295 #[test]
1296 fn test_persistence_entropy_non_negative() {
1297 let d = vec![
1298 PersistentInterval {
1299 birth: 0.0,
1300 death: 1.0,
1301 dimension: 0,
1302 },
1303 PersistentInterval {
1304 birth: 0.5,
1305 death: 2.0,
1306 dimension: 0,
1307 },
1308 PersistentInterval {
1309 birth: 1.0,
1310 death: 3.0,
1311 dimension: 0,
1312 },
1313 ];
1314 assert!(persistence_entropy(&d) >= 0.0);
1315 }
1316
1317 #[test]
1318 fn test_persistence_entropy_essential_excluded() {
1319 let d = vec![
1320 PersistentInterval {
1321 birth: 0.0,
1322 death: f64::INFINITY,
1323 dimension: 0,
1324 },
1325 PersistentInterval {
1326 birth: 0.0,
1327 death: 1.0,
1328 dimension: 0,
1329 },
1330 ];
1331 let h = persistence_entropy(&d);
1333 assert!(h.is_finite());
1334 }
1335
1336 #[test]
1337 fn test_persistence_entropy_equal_bars_maximizes() {
1338 let d = vec![
1340 PersistentInterval {
1341 birth: 0.0,
1342 death: 1.0,
1343 dimension: 0,
1344 },
1345 PersistentInterval {
1346 birth: 0.0,
1347 death: 1.0,
1348 dimension: 0,
1349 },
1350 ];
1351 let h = persistence_entropy(&d);
1352 assert!((h - 2_f64.ln()).abs() < 1e-10);
1353 }
1354
1355 #[test]
1358 fn test_vr_empty_cloud() {
1359 let vr = VietorisRips::new(vec![], 1.0);
1360 assert_eq!(vr.n_vertices(), 0);
1361 assert_eq!(vr.n_edges(), 0);
1362 }
1363
1364 #[test]
1365 fn test_vr_single_point() {
1366 let vr = VietorisRips::new(vec![[0.0, 0.0]], 1.0);
1367 assert_eq!(vr.n_vertices(), 1);
1368 assert_eq!(vr.n_edges(), 0);
1369 }
1370
1371 #[test]
1372 fn test_vr_two_close_points_have_edge() {
1373 let vr = VietorisRips::new(vec![[0.0, 0.0], [0.5, 0.0]], 1.0);
1374 assert_eq!(vr.n_edges(), 1);
1375 }
1376
1377 #[test]
1378 fn test_vr_two_far_points_no_edge() {
1379 let vr = VietorisRips::new(vec![[0.0, 0.0], [10.0, 0.0]], 1.0);
1380 assert_eq!(vr.n_edges(), 0);
1381 }
1382
1383 #[test]
1384 fn test_vr_triangle_has_one_triangle() {
1385 let vr = VietorisRips::new(vec![[0.0, 0.0], [1.0, 0.0], [0.5, 0.866]], 2.0);
1386 assert_eq!(vr.n_triangles(), 1);
1387 }
1388
1389 #[test]
1390 fn test_vr_betti_numbers_four_isolated() {
1391 let pts = vec![[0.0, 0.0], [10.0, 0.0], [0.0, 10.0], [10.0, 10.0]];
1393 let vr = VietorisRips::new(pts, 1.0);
1394 let b = vr.betti_numbers();
1395 assert_eq!(b.b0, 4);
1396 assert_eq!(b.b1, 0);
1397 }
1398
1399 #[test]
1400 fn test_vr_betti_numbers_connected_pair() {
1401 let vr = VietorisRips::new(vec![[0.0, 0.0], [0.5, 0.0]], 1.0);
1402 let b = vr.betti_numbers();
1403 assert_eq!(b.b0, 1);
1404 }
1405
1406 #[test]
1407 fn test_vr_euler_characteristic_points_only() {
1408 let pts: Vec<[f64; 2]> = (0..5).map(|i| [i as f64 * 10.0, 0.0]).collect();
1410 let vr = VietorisRips::new(pts, 1.0);
1411 assert_eq!(vr.euler_characteristic(), 5);
1412 }
1413
1414 #[test]
1415 fn test_vr_persistent_h0_returns_correct_count() {
1416 let pts = vec![[0.0, 0.0], [0.5, 0.0], [5.0, 0.0]];
1417 let vr = VietorisRips::new(pts, 10.0);
1418 let intervals = vr.persistent_h0();
1419 let n_essential = intervals.iter().filter(|p| p.is_essential()).count();
1421 assert_eq!(n_essential, 1);
1422 }
1423
1424 #[test]
1427 fn test_simplex_tree_empty() {
1428 let st = SimplexTree::new();
1429 assert_eq!(st.simplices.len(), 0);
1430 }
1431
1432 #[test]
1433 fn test_simplex_tree_insert_vertex() {
1434 let mut st = SimplexTree::new();
1435 st.insert(vec![0], 0.0);
1436 assert_eq!(st.count_dim(0), 1);
1437 }
1438
1439 #[test]
1440 fn test_simplex_tree_insert_edge() {
1441 let mut st = SimplexTree::new();
1442 st.insert(vec![0, 1], 1.5);
1443 assert_eq!(st.count_dim(1), 1);
1444 }
1445
1446 #[test]
1447 fn test_simplex_tree_max_filtration() {
1448 let mut st = SimplexTree::new();
1449 st.insert(vec![0], 0.0);
1450 st.insert(vec![1], 0.0);
1451 st.insert(vec![0, 1], 2.5);
1452 assert!((st.max_filtration() - 2.5).abs() < 1e-12);
1453 }
1454
1455 #[test]
1456 fn test_simplex_tree_sorted_simplices_order() {
1457 let mut st = SimplexTree::new();
1458 st.insert(vec![0, 1], 2.0);
1459 st.insert(vec![0], 0.0);
1460 st.insert(vec![1], 0.0);
1461 let sorted = st.sorted_simplices();
1462 for w in sorted.windows(2) {
1463 assert!(w[0].filtration <= w[1].filtration);
1464 }
1465 }
1466
1467 #[test]
1468 fn test_simplex_tree_from_vr() {
1469 let vr = VietorisRips::new(vec![[0.0, 0.0], [0.5, 0.0], [0.25, 0.5]], 2.0);
1470 let st = SimplexTree::from_vietoris_rips(&vr);
1471 assert_eq!(st.count_dim(0), 3);
1473 assert_eq!(st.count_dim(1), 3);
1474 assert_eq!(st.count_dim(2), 1);
1475 }
1476
1477 #[test]
1480 fn test_persistence_diagram_add_and_count() {
1481 let mut pd = PersistenceDiagram::new();
1482 pd.add(0.0, 1.0, 0);
1483 pd.add(0.5, 2.0, 1);
1484 assert_eq!(pd.pairs.len(), 2);
1485 }
1486
1487 #[test]
1488 fn test_persistence_diagram_pairs_in_dim() {
1489 let mut pd = PersistenceDiagram::new();
1490 pd.add(0.0, 1.0, 0);
1491 pd.add(0.5, 2.0, 1);
1492 pd.add(1.0, 3.0, 0);
1493 assert_eq!(pd.pairs_in_dim(0).len(), 2);
1494 assert_eq!(pd.pairs_in_dim(1).len(), 1);
1495 }
1496
1497 #[test]
1498 fn test_persistence_diagram_max_persistence() {
1499 let mut pd = PersistenceDiagram::new();
1500 pd.add(0.0, 1.0, 0);
1501 pd.add(0.0, 3.0, 0);
1502 assert!((pd.max_persistence() - 3.0).abs() < 1e-12);
1503 }
1504
1505 #[test]
1506 fn test_persistence_diagram_n_essential() {
1507 let mut pd = PersistenceDiagram::new();
1508 pd.add(0.0, f64::INFINITY, 0);
1509 pd.add(0.5, 1.5, 0);
1510 assert_eq!(pd.n_essential(), 1);
1511 }
1512
1513 #[test]
1514 fn test_persistence_diagram_total_persistence() {
1515 let mut pd = PersistenceDiagram::new();
1516 pd.add(0.0, 1.0, 0);
1517 pd.add(0.0, 2.0, 0);
1518 assert!((pd.total_persistence() - 3.0).abs() < 1e-12);
1519 }
1520
1521 #[test]
1522 fn test_persistence_diagram_from_vr() {
1523 let vr = VietorisRips::new(vec![[0.0, 0.0], [1.0, 0.0]], 5.0);
1524 let pd = PersistenceDiagram::from_vietoris_rips_h0(&vr);
1525 assert!(!pd.pairs.is_empty());
1526 assert_eq!(pd.n_essential(), 1);
1527 }
1528
1529 #[test]
1532 fn test_barcode_summary_total_persistence() {
1533 let mut pd = PersistenceDiagram::new();
1534 pd.add(0.0, 1.0, 0);
1535 pd.add(0.0, 2.0, 0);
1536 let bs = BarcodeSummary::from_diagram(&pd, 0.5);
1537 assert!((bs.total_persistence - 3.0).abs() < 1e-12);
1538 }
1539
1540 #[test]
1541 fn test_barcode_summary_long_features() {
1542 let mut pd = PersistenceDiagram::new();
1543 pd.add(0.0, 0.3, 0); pd.add(0.0, 1.5, 0); pd.add(0.0, 2.0, 0); let bs = BarcodeSummary::from_diagram(&pd, 1.0);
1547 assert_eq!(bs.n_long_features, 2);
1548 }
1549
1550 #[test]
1551 fn test_barcode_summary_entropy_non_negative() {
1552 let mut pd = PersistenceDiagram::new();
1553 pd.add(0.0, 1.0, 0);
1554 pd.add(0.5, 2.0, 0);
1555 let bs = BarcodeSummary::from_diagram(&pd, 0.5);
1556 assert!(bs.entropy >= 0.0);
1557 }
1558
1559 #[test]
1560 fn test_barcode_summary_normalised_total() {
1561 let mut pd = PersistenceDiagram::new();
1562 pd.add(0.0, 1.0, 0);
1563 pd.add(0.0, 2.0, 0);
1564 let bs = BarcodeSummary::from_diagram(&pd, 0.5);
1565 assert!((bs.normalised_total() - 1.5).abs() < 1e-10);
1567 }
1568
1569 #[test]
1570 fn test_barcode_summary_mean_persistence() {
1571 let mut pd = PersistenceDiagram::new();
1572 pd.add(0.0, 2.0, 0);
1573 pd.add(0.0, 4.0, 0);
1574 let bs = BarcodeSummary::from_diagram(&pd, 1.0);
1575 assert!((bs.mean_persistence - 3.0).abs() < 1e-12);
1576 }
1577
1578 #[test]
1581 fn test_mapper_empty_cloud() {
1582 let mg = MapperGraph::build(&[], &[], 3, 0.3, 0.5);
1583 assert_eq!(mg.n_nodes(), 0);
1584 assert_eq!(mg.n_edges(), 0);
1585 }
1586
1587 #[test]
1588 fn test_mapper_single_point() {
1589 let pts = [[0.0, 0.0]];
1590 let filter = [0.0];
1591 let mg = MapperGraph::build(&pts, &filter, 2, 0.2, 1.0);
1592 assert!(mg.n_nodes() >= 1);
1593 }
1594
1595 #[test]
1596 fn test_mapper_line_of_points() {
1597 let pts: Vec<[f64; 2]> = (0..10).map(|i| [i as f64, 0.0]).collect();
1599 let filter: Vec<f64> = pts.iter().map(|p| p[0]).collect();
1600 let mg = MapperGraph::build(&pts, &filter, 3, 0.4, 1.5);
1601 assert!(mg.n_nodes() >= 1);
1602 }
1603
1604 #[test]
1605 fn test_mapper_degree_non_negative() {
1606 let pts: Vec<[f64; 2]> = (0..6).map(|i| [i as f64, 0.0]).collect();
1607 let filter: Vec<f64> = pts.iter().map(|p| p[0]).collect();
1608 let mg = MapperGraph::build(&pts, &filter, 3, 0.5, 1.5);
1609 for i in 0..mg.n_nodes() {
1610 assert!(mg.degree(i) <= mg.n_edges());
1611 }
1612 }
1613
1614 #[test]
1615 fn test_mapper_total_point_count_ge_input() {
1616 let pts: Vec<[f64; 2]> = (0..5).map(|i| [i as f64, 0.0]).collect();
1617 let filter: Vec<f64> = pts.iter().map(|p| p[0]).collect();
1618 let mg = MapperGraph::build(&pts, &filter, 2, 0.5, 2.0);
1619 assert!(mg.total_point_count() >= 1);
1621 }
1622
1623 #[test]
1624 fn test_mapper_no_intervals_returns_empty() {
1625 let pts = [[0.0, 0.0], [1.0, 0.0]];
1626 let filter = [0.0, 1.0];
1627 let mg = MapperGraph::build(&pts, &filter, 0, 0.2, 0.5);
1628 assert_eq!(mg.n_nodes(), 0);
1629 }
1630}