#![allow(clippy::needless_range_loop)]
#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
#[derive(Debug, Clone, PartialEq)]
pub struct SimplexEdge {
pub v0: usize,
pub v1: usize,
pub weight: f64,
}
#[derive(Debug, Clone)]
pub struct UnionFind {
pub parent: Vec<usize>,
pub rank: Vec<usize>,
}
impl UnionFind {
pub fn new(n: usize) -> Self {
Self {
parent: (0..n).collect(),
rank: vec![0; n],
}
}
pub fn find(&mut self, i: usize) -> usize {
if self.parent[i] != i {
self.parent[i] = self.find(self.parent[i]);
}
self.parent[i]
}
pub fn union(&mut self, i: usize, j: usize) -> bool {
let ri = self.find(i);
let rj = self.find(j);
if ri == rj {
return false;
}
match self.rank[ri].cmp(&self.rank[rj]) {
std::cmp::Ordering::Less => self.parent[ri] = rj,
std::cmp::Ordering::Greater => self.parent[rj] = ri,
std::cmp::Ordering::Equal => {
self.parent[rj] = ri;
self.rank[ri] += 1;
}
}
true
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PersistentInterval {
pub birth: f64,
pub death: f64,
pub dimension: usize,
}
impl PersistentInterval {
pub fn persistence(&self) -> f64 {
self.death - self.birth
}
pub fn is_essential(&self) -> bool {
self.death.is_infinite()
}
}
fn all_edges(points: &[[f64; 2]]) -> Vec<SimplexEdge> {
let n = points.len();
let mut edges = Vec::with_capacity(n * (n.saturating_sub(1)) / 2);
for i in 0..n {
for j in (i + 1)..n {
let dx = points[i][0] - points[j][0];
let dy = points[i][1] - points[j][1];
edges.push(SimplexEdge {
v0: i,
v1: j,
weight: (dx * dx + dy * dy).sqrt(),
});
}
}
edges
}
pub fn vietoris_rips_h0(points: &[[f64; 2]], max_r: f64) -> Vec<PersistentInterval> {
let n = points.len();
if n == 0 {
return vec![];
}
let mut edges = all_edges(points);
edges.sort_by(|a, b| {
a.weight
.partial_cmp(&b.weight)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut uf = UnionFind::new(n);
let mut birth = vec![0.0_f64; n];
let mut intervals = Vec::new();
for e in &edges {
if e.weight > max_r {
break;
}
if uf.union(e.v0, e.v1) {
let r0 = uf.find(e.v0);
let r1 = uf.find(e.v1);
let root_after = uf.find(e.v0);
let dying = if root_after == r0 { r1 } else { r0 };
let b = birth[dying];
birth[root_after] = birth[root_after].min(b);
intervals.push(PersistentInterval {
birth: b,
death: e.weight,
dimension: 0,
});
}
}
intervals.push(PersistentInterval {
birth: 0.0,
death: f64::INFINITY,
dimension: 0,
});
intervals
}
pub fn vietoris_rips_h1_approx(
points: &[[f64; 2]],
max_r: f64,
n_edges: usize,
) -> Vec<PersistentInterval> {
let n = points.len();
if n == 0 {
return vec![];
}
let mut edges = all_edges(points);
edges.sort_by(|a, b| {
a.weight
.partial_cmp(&b.weight)
.unwrap_or(std::cmp::Ordering::Equal)
});
let edges: Vec<_> = edges
.into_iter()
.filter(|e| e.weight <= max_r)
.take(n_edges)
.collect();
let mut uf = UnionFind::new(n);
let mut intervals = Vec::new();
let mut edge_births: Vec<f64> = Vec::new();
for e in &edges {
if !uf.union(e.v0, e.v1) {
let birth = *edge_births.last().unwrap_or(&0.0);
intervals.push(PersistentInterval {
birth,
death: (e.weight + max_r) / 2.0,
dimension: 1,
});
} else {
edge_births.push(e.weight);
}
}
intervals
}
fn point_dist_inf(p: &PersistentInterval, q: &PersistentInterval) -> f64 {
let pb = p.birth.min(p.death);
let pd = p.birth.max(p.death);
let qb = q.birth.min(q.death);
let qd = q.birth.max(q.death);
f64::max((pb - qb).abs(), (pd - qd).abs())
}
fn dist_to_diagonal(p: &PersistentInterval) -> f64 {
(p.death - p.birth).abs() / 2.0
}
pub fn bottleneck_distance(a: &[PersistentInterval], b: &[PersistentInterval]) -> f64 {
let fa: Vec<&PersistentInterval> = a.iter().filter(|p| !p.is_essential()).collect();
let fb: Vec<&PersistentInterval> = b.iter().filter(|p| !p.is_essential()).collect();
let mut max_dist = 0.0_f64;
let mut used = vec![false; fb.len()];
for p in &fa {
let best = fb
.iter()
.enumerate()
.filter(|(idx, _)| !used[*idx])
.map(|(idx, q)| (idx, point_dist_inf(p, q)))
.min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(std::cmp::Ordering::Equal));
if let Some((idx, d)) = best {
used[idx] = true;
max_dist = max_dist.max(d);
} else {
max_dist = max_dist.max(dist_to_diagonal(p));
}
}
for (idx, q) in fb.iter().enumerate() {
if !used[idx] {
max_dist = max_dist.max(dist_to_diagonal(q));
}
}
max_dist
}
pub fn wasserstein_distance_1(a: &[PersistentInterval], b: &[PersistentInterval]) -> f64 {
let fa: Vec<&PersistentInterval> = a.iter().filter(|p| !p.is_essential()).collect();
let fb: Vec<&PersistentInterval> = b.iter().filter(|p| !p.is_essential()).collect();
let mut total = 0.0_f64;
let mut used = vec![false; fb.len()];
for p in &fa {
let best = fb
.iter()
.enumerate()
.filter(|(idx, _)| !used[*idx])
.map(|(idx, q)| (idx, point_dist_inf(p, q)))
.min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(std::cmp::Ordering::Equal));
if let Some((idx, d)) = best {
used[idx] = true;
total += d;
} else {
total += dist_to_diagonal(p);
}
}
for (idx, q) in fb.iter().enumerate() {
if !used[idx] {
total += dist_to_diagonal(q);
}
}
total
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BettiNumbers {
pub b0: usize,
pub b1: usize,
pub b2: usize,
}
pub fn euler_characteristic(betti: &BettiNumbers) -> i64 {
betti.b0 as i64 - betti.b1 as i64 + betti.b2 as i64
}
pub fn cubical_complex_1d(signal: &[f64], threshold: f64) -> BettiNumbers {
if signal.is_empty() {
return BettiNumbers {
b0: 0,
b1: 0,
b2: 0,
};
}
let below: Vec<bool> = signal.iter().map(|&v| v <= threshold).collect();
let mut b0 = 0usize;
let mut in_component = false;
for &b in &below {
if b && !in_component {
b0 += 1;
in_component = true;
} else if !b {
in_component = false;
}
}
BettiNumbers { b0, b1: 0, b2: 0 }
}
pub fn persistence_entropy(diagram: &[PersistentInterval]) -> f64 {
let lifetimes: Vec<f64> = diagram
.iter()
.filter(|p| !p.is_essential() && p.persistence() > 0.0)
.map(|p| p.persistence())
.collect();
if lifetimes.is_empty() {
return 0.0;
}
let total: f64 = lifetimes.iter().sum();
if total <= 0.0 {
return 0.0;
}
-lifetimes
.iter()
.map(|&l| {
let p = l / total;
if p > 0.0 { p * p.ln() } else { 0.0 }
})
.sum::<f64>()
}
#[derive(Debug, Clone)]
pub struct VietorisRips {
pub points: Vec<[f64; 2]>,
pub epsilon: f64,
}
impl VietorisRips {
pub fn new(points: Vec<[f64; 2]>, epsilon: f64) -> Self {
Self { points, epsilon }
}
fn dist(a: &[f64; 2], b: &[f64; 2]) -> f64 {
let dx = a[0] - b[0];
let dy = a[1] - b[1];
(dx * dx + dy * dy).sqrt()
}
pub fn edges(&self) -> Vec<(usize, usize, f64)> {
let n = self.points.len();
let mut result = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
let d = Self::dist(&self.points[i], &self.points[j]);
if d <= self.epsilon {
result.push((i, j, d));
}
}
}
result
}
pub fn triangles(&self) -> Vec<(usize, usize, usize)> {
let n = self.points.len();
let mut result = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
if Self::dist(&self.points[i], &self.points[j]) > self.epsilon {
continue;
}
for k in (j + 1)..n {
if Self::dist(&self.points[i], &self.points[k]) <= self.epsilon
&& Self::dist(&self.points[j], &self.points[k]) <= self.epsilon
{
result.push((i, j, k));
}
}
}
}
result
}
pub fn n_vertices(&self) -> usize {
self.points.len()
}
pub fn n_edges(&self) -> usize {
self.edges().len()
}
pub fn n_triangles(&self) -> usize {
self.triangles().len()
}
pub fn euler_characteristic(&self) -> i64 {
let v = self.n_vertices() as i64;
let e = self.n_edges() as i64;
let f = self.n_triangles() as i64;
v - e + f
}
pub fn betti_numbers(&self) -> BettiNumbers {
let n = self.points.len();
if n == 0 {
return BettiNumbers {
b0: 0,
b1: 0,
b2: 0,
};
}
let edges = self.edges();
let triangles = self.triangles();
let mut uf = UnionFind::new(n);
for (i, j, _) in &edges {
uf.union(*i, *j);
}
let b0 = (0..n).filter(|&i| uf.find(i) == i).count();
let b1 = (edges.len() as i64 - n as i64 + b0 as i64).max(0) as usize;
let chi = self.euler_characteristic();
let b2 = (chi - b0 as i64 + b1 as i64).max(0) as usize;
let _ = triangles; BettiNumbers { b0, b1, b2 }
}
pub fn persistent_h0(&self) -> Vec<PersistentInterval> {
vietoris_rips_h0(&self.points, self.epsilon)
}
}
#[derive(Debug, Clone)]
pub struct FilteredSimplex {
pub vertices: Vec<usize>,
pub filtration: f64,
}
impl FilteredSimplex {
pub fn dim(&self) -> usize {
self.vertices.len().saturating_sub(1)
}
}
#[derive(Debug, Clone, Default)]
pub struct SimplexTree {
pub simplices: Vec<FilteredSimplex>,
}
impl SimplexTree {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, mut vertices: Vec<usize>, f: f64) {
vertices.sort_unstable();
self.simplices.push(FilteredSimplex {
vertices,
filtration: f,
});
}
pub fn from_vietoris_rips(vr: &VietorisRips) -> Self {
let mut tree = SimplexTree::new();
for i in 0..vr.points.len() {
tree.insert(vec![i], 0.0);
}
for (i, j, d) in vr.edges() {
tree.insert(vec![i, j], d);
}
for (i, j, k) in vr.triangles() {
let d01 = VietorisRips::dist(&vr.points[i], &vr.points[j]);
let d02 = VietorisRips::dist(&vr.points[i], &vr.points[k]);
let d12 = VietorisRips::dist(&vr.points[j], &vr.points[k]);
let f = d01.max(d02).max(d12);
tree.insert(vec![i, j, k], f);
}
tree
}
pub fn sorted_simplices(&self) -> Vec<&FilteredSimplex> {
let mut v: Vec<&FilteredSimplex> = self.simplices.iter().collect();
v.sort_by(|a, b| {
a.filtration
.partial_cmp(&b.filtration)
.unwrap_or(std::cmp::Ordering::Equal)
});
v
}
pub fn count_dim(&self, d: usize) -> usize {
self.simplices.iter().filter(|s| s.dim() == d).count()
}
pub fn max_filtration(&self) -> f64 {
self.simplices
.iter()
.map(|s| s.filtration)
.fold(0.0_f64, f64::max)
}
}
#[derive(Debug, Clone, Default)]
pub struct PersistenceDiagram {
pub pairs: Vec<PersistentInterval>,
}
impl PersistenceDiagram {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, birth: f64, death: f64, dimension: usize) {
self.pairs.push(PersistentInterval {
birth,
death,
dimension,
});
}
pub fn from_vietoris_rips_h0(vr: &VietorisRips) -> Self {
let intervals = vr.persistent_h0();
PersistenceDiagram { pairs: intervals }
}
pub fn pairs_in_dim(&self, dim: usize) -> Vec<&PersistentInterval> {
self.pairs.iter().filter(|p| p.dimension == dim).collect()
}
pub fn max_persistence(&self) -> f64 {
self.pairs
.iter()
.filter(|p| !p.is_essential())
.map(|p| p.persistence())
.fold(0.0_f64, f64::max)
}
pub fn n_essential(&self) -> usize {
self.pairs.iter().filter(|p| p.is_essential()).count()
}
pub fn entropy(&self) -> f64 {
persistence_entropy(&self.pairs)
}
pub fn total_persistence(&self) -> f64 {
self.pairs
.iter()
.filter(|p| !p.is_essential())
.map(|p| p.persistence())
.sum()
}
}
#[derive(Debug, Clone)]
pub struct BarcodeSummary {
pub total_persistence: f64,
pub n_long_features: usize,
pub entropy: f64,
pub max_persistence: f64,
pub n_essential: usize,
pub mean_persistence: f64,
}
impl BarcodeSummary {
pub fn from_diagram(diagram: &PersistenceDiagram, long_threshold: f64) -> Self {
let finite: Vec<f64> = diagram
.pairs
.iter()
.filter(|p| !p.is_essential())
.map(|p| p.persistence())
.collect();
let total_persistence = finite.iter().sum::<f64>();
let n_long_features = finite.iter().filter(|&&p| p >= long_threshold).count();
let entropy = diagram.entropy();
let max_persistence = finite.iter().cloned().fold(0.0_f64, f64::max);
let n_essential = diagram.n_essential();
let mean_persistence = if finite.is_empty() {
0.0
} else {
total_persistence / finite.len() as f64
};
BarcodeSummary {
total_persistence,
n_long_features,
entropy,
max_persistence,
n_essential,
mean_persistence,
}
}
pub fn normalised_total(&self) -> f64 {
if self.max_persistence < 1e-15 {
0.0
} else {
self.total_persistence / self.max_persistence
}
}
}
#[derive(Debug, Clone)]
pub struct MapperNode {
pub point_indices: Vec<usize>,
pub cover_index: usize,
}
#[derive(Debug, Clone, Default)]
pub struct MapperGraph {
pub nodes: Vec<MapperNode>,
pub edges: Vec<(usize, usize)>,
}
impl MapperGraph {
pub fn build(
points: &[[f64; 2]],
filter_values: &[f64],
n_intervals: usize,
overlap: f64,
cluster_eps: f64,
) -> Self {
if points.is_empty() || n_intervals == 0 {
return Self::default();
}
let min_f = filter_values.iter().cloned().fold(f64::INFINITY, f64::min);
let max_f = filter_values
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max);
let range = (max_f - min_f).max(1e-15);
let step = range / n_intervals as f64;
let half_overlap = step * overlap.clamp(0.0, 0.99) / 2.0;
let mut graph = MapperGraph::default();
for k in 0..n_intervals {
let lo = min_f + k as f64 * step - half_overlap;
let hi = min_f + (k + 1) as f64 * step + half_overlap;
let indices_in: Vec<usize> = (0..points.len())
.filter(|&i| filter_values[i] >= lo && filter_values[i] <= hi)
.collect();
if indices_in.is_empty() {
continue;
}
let m = indices_in.len();
let mut uf = UnionFind::new(m);
for a in 0..m {
for b in (a + 1)..m {
let ia = indices_in[a];
let ib = indices_in[b];
let dx = points[ia][0] - points[ib][0];
let dy = points[ia][1] - points[ib][1];
let d = (dx * dx + dy * dy).sqrt();
if d <= cluster_eps {
uf.union(a, b);
}
}
}
let mut cluster_map: std::collections::HashMap<usize, Vec<usize>> =
std::collections::HashMap::new();
for a in 0..m {
let root = uf.find(a);
cluster_map.entry(root).or_default().push(indices_in[a]);
}
for (_root, members) in cluster_map {
graph.nodes.push(MapperNode {
point_indices: members,
cover_index: k,
});
}
}
let n_nodes = graph.nodes.len();
for i in 0..n_nodes {
for j in (i + 1)..n_nodes {
let set_i: std::collections::HashSet<usize> =
graph.nodes[i].point_indices.iter().copied().collect();
let shared = graph.nodes[j]
.point_indices
.iter()
.any(|p| set_i.contains(p));
if shared {
graph.edges.push((i, j));
}
}
}
graph
}
pub fn n_nodes(&self) -> usize {
self.nodes.len()
}
pub fn n_edges(&self) -> usize {
self.edges.len()
}
pub fn degree(&self, i: usize) -> usize {
self.edges
.iter()
.filter(|(a, b)| *a == i || *b == i)
.count()
}
pub fn total_point_count(&self) -> usize {
self.nodes.iter().map(|n| n.point_indices.len()).sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_uf_new_singletons() {
let mut uf = UnionFind::new(5);
for i in 0..5 {
assert_eq!(uf.find(i), i);
}
}
#[test]
fn test_uf_union_merges() {
let mut uf = UnionFind::new(4);
assert!(uf.union(0, 1));
assert_eq!(uf.find(0), uf.find(1));
}
#[test]
fn test_uf_union_idempotent() {
let mut uf = UnionFind::new(4);
uf.union(0, 1);
assert!(
!uf.union(0, 1),
"second union of same set should return false"
);
}
#[test]
fn test_uf_union_chain() {
let mut uf = UnionFind::new(4);
uf.union(0, 1);
uf.union(1, 2);
uf.union(2, 3);
let r0 = uf.find(0);
assert_eq!(uf.find(3), r0);
}
#[test]
fn test_uf_different_components() {
let mut uf = UnionFind::new(4);
uf.union(0, 1);
uf.union(2, 3);
assert_ne!(uf.find(0), uf.find(2));
}
#[test]
fn test_uf_path_compression() {
let mut uf = UnionFind::new(6);
for i in 0..5 {
uf.union(i, i + 1);
}
let root = uf.find(5);
assert_eq!(uf.find(0), root);
}
#[test]
fn test_uf_union_all_returns_true_n_minus_1_times() {
let n = 5;
let mut uf = UnionFind::new(n);
let mut merges = 0;
for i in 1..n {
if uf.union(0, i) {
merges += 1;
}
}
assert_eq!(merges, n - 1);
}
#[test]
fn test_simplex_edge_fields() {
let e = SimplexEdge {
v0: 1,
v1: 3,
weight: 2.5,
};
assert_eq!(e.v0, 1);
assert_eq!(e.v1, 3);
assert!((e.weight - 2.5).abs() < 1e-12);
}
#[test]
fn test_persistent_interval_persistence() {
let pi = PersistentInterval {
birth: 1.0,
death: 3.0,
dimension: 0,
};
assert!((pi.persistence() - 2.0).abs() < 1e-12);
}
#[test]
fn test_persistent_interval_essential() {
let pi = PersistentInterval {
birth: 0.0,
death: f64::INFINITY,
dimension: 0,
};
assert!(pi.is_essential());
}
#[test]
fn test_persistent_interval_not_essential() {
let pi = PersistentInterval {
birth: 0.0,
death: 1.0,
dimension: 0,
};
assert!(!pi.is_essential());
}
#[test]
fn test_h0_empty() {
let pts: [[f64; 2]; 0] = [];
let result = vietoris_rips_h0(&pts, 10.0);
assert!(result.is_empty());
}
#[test]
fn test_h0_single_point() {
let pts = [[0.0, 0.0]];
let result = vietoris_rips_h0(&pts, 10.0);
assert_eq!(result.len(), 1);
assert!(result[0].is_essential());
}
#[test]
fn test_h0_two_close_points() {
let pts = [[0.0, 0.0], [0.1, 0.0]];
let result = vietoris_rips_h0(&pts, 10.0);
assert_eq!(result.len(), 2);
let essential_count = result.iter().filter(|p| p.is_essential()).count();
assert_eq!(essential_count, 1);
}
#[test]
fn test_h0_all_dimension_zero() {
let pts = [[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]];
let result = vietoris_rips_h0(&pts, 10.0);
for pi in &result {
assert_eq!(pi.dimension, 0);
}
}
#[test]
fn test_h0_two_far_points_below_max_r() {
let pts = [[0.0, 0.0], [100.0, 0.0]];
let result = vietoris_rips_h0(&pts, 50.0);
let finite_count = result.iter().filter(|p| !p.is_essential()).count();
assert_eq!(finite_count, 0);
}
#[test]
fn test_h1_empty() {
let pts: [[f64; 2]; 0] = [];
let result = vietoris_rips_h1_approx(&pts, 10.0, 100);
assert!(result.is_empty());
}
#[test]
fn test_h1_dimension_one() {
let pts = [[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
let result = vietoris_rips_h1_approx(&pts, 10.0, 10);
for pi in &result {
assert_eq!(pi.dimension, 1);
}
}
#[test]
fn test_h1_triangle_has_loop() {
let pts = [[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
let result = vietoris_rips_h1_approx(&pts, 10.0, 3);
assert_eq!(result.len(), 1);
}
#[test]
fn test_bottleneck_identical_diagrams() {
let d = vec![
PersistentInterval {
birth: 0.0,
death: 1.0,
dimension: 0,
},
PersistentInterval {
birth: 0.5,
death: 2.0,
dimension: 0,
},
];
let dist = bottleneck_distance(&d, &d);
assert!(dist < 1e-12, "identical diagrams should have distance 0");
}
#[test]
fn test_bottleneck_empty_diagrams() {
assert!((bottleneck_distance(&[], &[])).abs() < 1e-12);
}
#[test]
fn test_bottleneck_non_negative() {
let a = vec![PersistentInterval {
birth: 0.0,
death: 1.0,
dimension: 0,
}];
let b = vec![PersistentInterval {
birth: 0.1,
death: 1.2,
dimension: 0,
}];
assert!(bottleneck_distance(&a, &b) >= 0.0);
}
#[test]
fn test_bottleneck_symmetry() {
let a = vec![PersistentInterval {
birth: 0.0,
death: 1.0,
dimension: 0,
}];
let b = vec![PersistentInterval {
birth: 0.5,
death: 2.0,
dimension: 0,
}];
let dab = bottleneck_distance(&a, &b);
let dba = bottleneck_distance(&b, &a);
assert!((dab - dba).abs() < 1e-12);
}
#[test]
fn test_wasserstein_identical() {
let d = vec![PersistentInterval {
birth: 0.0,
death: 2.0,
dimension: 0,
}];
assert!(wasserstein_distance_1(&d, &d) < 1e-12);
}
#[test]
fn test_wasserstein_empty() {
assert!(wasserstein_distance_1(&[], &[]).abs() < 1e-12);
}
#[test]
fn test_wasserstein_non_negative() {
let a = vec![PersistentInterval {
birth: 0.0,
death: 1.0,
dimension: 0,
}];
let b = vec![PersistentInterval {
birth: 0.2,
death: 1.5,
dimension: 0,
}];
assert!(wasserstein_distance_1(&a, &b) >= 0.0);
}
#[test]
fn test_euler_characteristic_sphere() {
let b = BettiNumbers {
b0: 1,
b1: 0,
b2: 1,
};
assert_eq!(euler_characteristic(&b), 2);
}
#[test]
fn test_euler_characteristic_torus() {
let b = BettiNumbers {
b0: 1,
b1: 2,
b2: 1,
};
assert_eq!(euler_characteristic(&b), 0);
}
#[test]
fn test_euler_characteristic_point() {
let b = BettiNumbers {
b0: 1,
b1: 0,
b2: 0,
};
assert_eq!(euler_characteristic(&b), 1);
}
#[test]
fn test_euler_characteristic_circle() {
let b = BettiNumbers {
b0: 1,
b1: 1,
b2: 0,
};
assert_eq!(euler_characteristic(&b), 0);
}
#[test]
fn test_cubical_1d_empty() {
let b = cubical_complex_1d(&[], 0.5);
assert_eq!(b.b0, 0);
}
#[test]
fn test_cubical_1d_all_below() {
let signal = [0.1, 0.2, 0.3];
let b = cubical_complex_1d(&signal, 1.0);
assert_eq!(b.b0, 1);
assert_eq!(b.b1, 0);
assert_eq!(b.b2, 0);
}
#[test]
fn test_cubical_1d_two_components() {
let signal = [0.1, 0.9, 0.1];
let b = cubical_complex_1d(&signal, 0.5);
assert_eq!(b.b0, 2);
}
#[test]
fn test_cubical_1d_none_below() {
let signal = [1.0, 2.0, 3.0];
let b = cubical_complex_1d(&signal, 0.5);
assert_eq!(b.b0, 0);
}
#[test]
fn test_cubical_1d_no_loops() {
let signal = [0.1, 0.2, 0.3, 0.4];
let b = cubical_complex_1d(&signal, 1.0);
assert_eq!(b.b1, 0);
assert_eq!(b.b2, 0);
}
#[test]
fn test_persistence_entropy_empty() {
assert_eq!(persistence_entropy(&[]), 0.0);
}
#[test]
fn test_persistence_entropy_single_bar() {
let d = vec![PersistentInterval {
birth: 0.0,
death: 1.0,
dimension: 0,
}];
assert!(persistence_entropy(&d).abs() < 1e-12);
}
#[test]
fn test_persistence_entropy_non_negative() {
let d = vec![
PersistentInterval {
birth: 0.0,
death: 1.0,
dimension: 0,
},
PersistentInterval {
birth: 0.5,
death: 2.0,
dimension: 0,
},
PersistentInterval {
birth: 1.0,
death: 3.0,
dimension: 0,
},
];
assert!(persistence_entropy(&d) >= 0.0);
}
#[test]
fn test_persistence_entropy_essential_excluded() {
let d = vec![
PersistentInterval {
birth: 0.0,
death: f64::INFINITY,
dimension: 0,
},
PersistentInterval {
birth: 0.0,
death: 1.0,
dimension: 0,
},
];
let h = persistence_entropy(&d);
assert!(h.is_finite());
}
#[test]
fn test_persistence_entropy_equal_bars_maximizes() {
let d = vec![
PersistentInterval {
birth: 0.0,
death: 1.0,
dimension: 0,
},
PersistentInterval {
birth: 0.0,
death: 1.0,
dimension: 0,
},
];
let h = persistence_entropy(&d);
assert!((h - 2_f64.ln()).abs() < 1e-10);
}
#[test]
fn test_vr_empty_cloud() {
let vr = VietorisRips::new(vec![], 1.0);
assert_eq!(vr.n_vertices(), 0);
assert_eq!(vr.n_edges(), 0);
}
#[test]
fn test_vr_single_point() {
let vr = VietorisRips::new(vec![[0.0, 0.0]], 1.0);
assert_eq!(vr.n_vertices(), 1);
assert_eq!(vr.n_edges(), 0);
}
#[test]
fn test_vr_two_close_points_have_edge() {
let vr = VietorisRips::new(vec![[0.0, 0.0], [0.5, 0.0]], 1.0);
assert_eq!(vr.n_edges(), 1);
}
#[test]
fn test_vr_two_far_points_no_edge() {
let vr = VietorisRips::new(vec![[0.0, 0.0], [10.0, 0.0]], 1.0);
assert_eq!(vr.n_edges(), 0);
}
#[test]
fn test_vr_triangle_has_one_triangle() {
let vr = VietorisRips::new(vec![[0.0, 0.0], [1.0, 0.0], [0.5, 0.866]], 2.0);
assert_eq!(vr.n_triangles(), 1);
}
#[test]
fn test_vr_betti_numbers_four_isolated() {
let pts = vec![[0.0, 0.0], [10.0, 0.0], [0.0, 10.0], [10.0, 10.0]];
let vr = VietorisRips::new(pts, 1.0);
let b = vr.betti_numbers();
assert_eq!(b.b0, 4);
assert_eq!(b.b1, 0);
}
#[test]
fn test_vr_betti_numbers_connected_pair() {
let vr = VietorisRips::new(vec![[0.0, 0.0], [0.5, 0.0]], 1.0);
let b = vr.betti_numbers();
assert_eq!(b.b0, 1);
}
#[test]
fn test_vr_euler_characteristic_points_only() {
let pts: Vec<[f64; 2]> = (0..5).map(|i| [i as f64 * 10.0, 0.0]).collect();
let vr = VietorisRips::new(pts, 1.0);
assert_eq!(vr.euler_characteristic(), 5);
}
#[test]
fn test_vr_persistent_h0_returns_correct_count() {
let pts = vec![[0.0, 0.0], [0.5, 0.0], [5.0, 0.0]];
let vr = VietorisRips::new(pts, 10.0);
let intervals = vr.persistent_h0();
let n_essential = intervals.iter().filter(|p| p.is_essential()).count();
assert_eq!(n_essential, 1);
}
#[test]
fn test_simplex_tree_empty() {
let st = SimplexTree::new();
assert_eq!(st.simplices.len(), 0);
}
#[test]
fn test_simplex_tree_insert_vertex() {
let mut st = SimplexTree::new();
st.insert(vec![0], 0.0);
assert_eq!(st.count_dim(0), 1);
}
#[test]
fn test_simplex_tree_insert_edge() {
let mut st = SimplexTree::new();
st.insert(vec![0, 1], 1.5);
assert_eq!(st.count_dim(1), 1);
}
#[test]
fn test_simplex_tree_max_filtration() {
let mut st = SimplexTree::new();
st.insert(vec![0], 0.0);
st.insert(vec![1], 0.0);
st.insert(vec![0, 1], 2.5);
assert!((st.max_filtration() - 2.5).abs() < 1e-12);
}
#[test]
fn test_simplex_tree_sorted_simplices_order() {
let mut st = SimplexTree::new();
st.insert(vec![0, 1], 2.0);
st.insert(vec![0], 0.0);
st.insert(vec![1], 0.0);
let sorted = st.sorted_simplices();
for w in sorted.windows(2) {
assert!(w[0].filtration <= w[1].filtration);
}
}
#[test]
fn test_simplex_tree_from_vr() {
let vr = VietorisRips::new(vec![[0.0, 0.0], [0.5, 0.0], [0.25, 0.5]], 2.0);
let st = SimplexTree::from_vietoris_rips(&vr);
assert_eq!(st.count_dim(0), 3);
assert_eq!(st.count_dim(1), 3);
assert_eq!(st.count_dim(2), 1);
}
#[test]
fn test_persistence_diagram_add_and_count() {
let mut pd = PersistenceDiagram::new();
pd.add(0.0, 1.0, 0);
pd.add(0.5, 2.0, 1);
assert_eq!(pd.pairs.len(), 2);
}
#[test]
fn test_persistence_diagram_pairs_in_dim() {
let mut pd = PersistenceDiagram::new();
pd.add(0.0, 1.0, 0);
pd.add(0.5, 2.0, 1);
pd.add(1.0, 3.0, 0);
assert_eq!(pd.pairs_in_dim(0).len(), 2);
assert_eq!(pd.pairs_in_dim(1).len(), 1);
}
#[test]
fn test_persistence_diagram_max_persistence() {
let mut pd = PersistenceDiagram::new();
pd.add(0.0, 1.0, 0);
pd.add(0.0, 3.0, 0);
assert!((pd.max_persistence() - 3.0).abs() < 1e-12);
}
#[test]
fn test_persistence_diagram_n_essential() {
let mut pd = PersistenceDiagram::new();
pd.add(0.0, f64::INFINITY, 0);
pd.add(0.5, 1.5, 0);
assert_eq!(pd.n_essential(), 1);
}
#[test]
fn test_persistence_diagram_total_persistence() {
let mut pd = PersistenceDiagram::new();
pd.add(0.0, 1.0, 0);
pd.add(0.0, 2.0, 0);
assert!((pd.total_persistence() - 3.0).abs() < 1e-12);
}
#[test]
fn test_persistence_diagram_from_vr() {
let vr = VietorisRips::new(vec![[0.0, 0.0], [1.0, 0.0]], 5.0);
let pd = PersistenceDiagram::from_vietoris_rips_h0(&vr);
assert!(!pd.pairs.is_empty());
assert_eq!(pd.n_essential(), 1);
}
#[test]
fn test_barcode_summary_total_persistence() {
let mut pd = PersistenceDiagram::new();
pd.add(0.0, 1.0, 0);
pd.add(0.0, 2.0, 0);
let bs = BarcodeSummary::from_diagram(&pd, 0.5);
assert!((bs.total_persistence - 3.0).abs() < 1e-12);
}
#[test]
fn test_barcode_summary_long_features() {
let mut pd = PersistenceDiagram::new();
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);
assert_eq!(bs.n_long_features, 2);
}
#[test]
fn test_barcode_summary_entropy_non_negative() {
let mut pd = PersistenceDiagram::new();
pd.add(0.0, 1.0, 0);
pd.add(0.5, 2.0, 0);
let bs = BarcodeSummary::from_diagram(&pd, 0.5);
assert!(bs.entropy >= 0.0);
}
#[test]
fn test_barcode_summary_normalised_total() {
let mut pd = PersistenceDiagram::new();
pd.add(0.0, 1.0, 0);
pd.add(0.0, 2.0, 0);
let bs = BarcodeSummary::from_diagram(&pd, 0.5);
assert!((bs.normalised_total() - 1.5).abs() < 1e-10);
}
#[test]
fn test_barcode_summary_mean_persistence() {
let mut pd = PersistenceDiagram::new();
pd.add(0.0, 2.0, 0);
pd.add(0.0, 4.0, 0);
let bs = BarcodeSummary::from_diagram(&pd, 1.0);
assert!((bs.mean_persistence - 3.0).abs() < 1e-12);
}
#[test]
fn test_mapper_empty_cloud() {
let mg = MapperGraph::build(&[], &[], 3, 0.3, 0.5);
assert_eq!(mg.n_nodes(), 0);
assert_eq!(mg.n_edges(), 0);
}
#[test]
fn test_mapper_single_point() {
let pts = [[0.0, 0.0]];
let filter = [0.0];
let mg = MapperGraph::build(&pts, &filter, 2, 0.2, 1.0);
assert!(mg.n_nodes() >= 1);
}
#[test]
fn test_mapper_line_of_points() {
let pts: Vec<[f64; 2]> = (0..10).map(|i| [i as f64, 0.0]).collect();
let filter: Vec<f64> = pts.iter().map(|p| p[0]).collect();
let mg = MapperGraph::build(&pts, &filter, 3, 0.4, 1.5);
assert!(mg.n_nodes() >= 1);
}
#[test]
fn test_mapper_degree_non_negative() {
let pts: Vec<[f64; 2]> = (0..6).map(|i| [i as f64, 0.0]).collect();
let filter: Vec<f64> = pts.iter().map(|p| p[0]).collect();
let mg = MapperGraph::build(&pts, &filter, 3, 0.5, 1.5);
for i in 0..mg.n_nodes() {
assert!(mg.degree(i) <= mg.n_edges());
}
}
#[test]
fn test_mapper_total_point_count_ge_input() {
let pts: Vec<[f64; 2]> = (0..5).map(|i| [i as f64, 0.0]).collect();
let filter: Vec<f64> = pts.iter().map(|p| p[0]).collect();
let mg = MapperGraph::build(&pts, &filter, 2, 0.5, 2.0);
assert!(mg.total_point_count() >= 1);
}
#[test]
fn test_mapper_no_intervals_returns_empty() {
let pts = [[0.0, 0.0], [1.0, 0.0]];
let filter = [0.0, 1.0];
let mg = MapperGraph::build(&pts, &filter, 0, 0.2, 0.5);
assert_eq!(mg.n_nodes(), 0);
}
}