#[derive(Debug, Clone)]
pub struct SimplicialComplex {
pub vertices: Vec<[f64; 3]>,
pub edges: Vec<[usize; 2]>,
pub triangles: Vec<[usize; 3]>,
pub tetrahedra: Vec<[usize; 4]>,
}
impl SimplicialComplex {
#[allow(dead_code)]
pub fn new() -> Self {
Self {
vertices: Vec::new(),
edges: Vec::new(),
triangles: Vec::new(),
tetrahedra: Vec::new(),
}
}
#[allow(dead_code)]
pub fn add_vertex(&mut self, pos: [f64; 3]) -> usize {
let idx = self.vertices.len();
self.vertices.push(pos);
idx
}
#[allow(dead_code)]
pub fn add_edge(&mut self, i: usize, j: usize) {
let e = if i < j { [i, j] } else { [j, i] };
if !self.edges.contains(&e) {
self.edges.push(e);
}
}
#[allow(dead_code)]
pub fn add_triangle(&mut self, i: usize, j: usize, k: usize) {
let mut t = [i, j, k];
t.sort_unstable();
if !self.triangles.contains(&t) {
self.triangles.push(t);
}
}
#[allow(dead_code)]
pub fn add_tetrahedron(&mut self, i: usize, j: usize, k: usize, l: usize) {
let mut t = [i, j, k, l];
t.sort_unstable();
if !self.tetrahedra.contains(&t) {
self.tetrahedra.push(t);
}
}
#[allow(dead_code)]
pub fn euler_characteristic(&self) -> i64 {
self.vertices.len() as i64 - self.edges.len() as i64 + self.triangles.len() as i64
- self.tetrahedra.len() as i64
}
#[allow(dead_code)]
pub fn n_vertices(&self) -> usize {
self.vertices.len()
}
#[allow(dead_code)]
pub fn n_edges(&self) -> usize {
self.edges.len()
}
#[allow(dead_code)]
pub fn n_triangles(&self) -> usize {
self.triangles.len()
}
#[allow(dead_code)]
pub fn n_tetrahedra(&self) -> usize {
self.tetrahedra.len()
}
#[allow(dead_code)]
pub fn boundary_1(&self) -> Vec<i32> {
let nv = self.vertices.len();
let ne = self.edges.len();
let mut b = vec![0_i32; nv * ne];
for (j, &[i0, i1]) in self.edges.iter().enumerate() {
b[i0 * ne + j] = -1;
b[i1 * ne + j] = 1;
}
b
}
#[allow(dead_code)]
pub fn boundary_2(&self) -> Vec<i32> {
let ne = self.edges.len();
let nf = self.triangles.len();
let mut b = vec![0_i32; ne * nf];
for (j, &[a, b_idx, c]) in self.triangles.iter().enumerate() {
let tri_edges = [[a, b_idx], [b_idx, c], [a, c]];
let signs = [1_i32, 1, -1];
for (k, &e) in tri_edges.iter().enumerate() {
let es = if e[0] < e[1] {
[e[0], e[1]]
} else {
[e[1], e[0]]
};
if let Some(ei) = self.edges.iter().position(|&x| x == es) {
b[ei * nf + j] = signs[k];
}
}
}
b
}
#[allow(dead_code)]
pub fn octahedral_sphere() -> Self {
let mut sc = Self::new();
sc.add_vertex([1.0, 0.0, 0.0]);
sc.add_vertex([-1.0, 0.0, 0.0]);
sc.add_vertex([0.0, 1.0, 0.0]);
sc.add_vertex([0.0, -1.0, 0.0]);
sc.add_vertex([0.0, 0.0, 1.0]);
sc.add_vertex([0.0, 0.0, -1.0]);
let edges = [
[0, 2],
[0, 3],
[0, 4],
[0, 5],
[1, 2],
[1, 3],
[1, 4],
[1, 5],
[2, 4],
[2, 5],
[3, 4],
[3, 5],
];
for [i, j] in edges {
sc.add_edge(i, j);
}
sc.add_triangle(0, 2, 4);
sc.add_triangle(0, 2, 5);
sc.add_triangle(0, 3, 4);
sc.add_triangle(0, 3, 5);
sc.add_triangle(1, 2, 4);
sc.add_triangle(1, 2, 5);
sc.add_triangle(1, 3, 4);
sc.add_triangle(1, 3, 5);
sc
}
#[allow(dead_code)]
pub fn minimal_torus() -> Self {
let mut sc = Self::new();
for i in 0..7 {
let angle = 2.0 * std::f64::consts::PI * i as f64 / 7.0;
sc.add_vertex([angle.cos(), angle.sin(), 0.0]);
}
let tris: [[usize; 3]; 14] = [
[0, 1, 2],
[0, 2, 3],
[0, 3, 4],
[0, 4, 5],
[0, 5, 6],
[0, 6, 1],
[1, 3, 2],
[1, 4, 3],
[1, 5, 4],
[1, 6, 5],
[2, 6, 3],
[3, 5, 6],
[2, 4, 6],
[2, 5, 4],
];
for [a, b, c] in tris {
sc.add_edge(a, b);
sc.add_edge(b, c);
sc.add_edge(a, c);
sc.add_triangle(a, b, c);
}
sc
}
}
impl Default for SimplicialComplex {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BirthDeathPair {
pub birth: f64,
pub death: f64,
pub dim: usize,
}
impl BirthDeathPair {
#[allow(dead_code)]
pub fn persistence(&self) -> f64 {
self.death - self.birth
}
#[allow(dead_code)]
pub fn is_essential(&self) -> bool {
self.death.is_infinite()
}
}
#[derive(Debug, Clone)]
pub struct PersistentHomology {
pub pairs: Vec<BirthDeathPair>,
}
impl PersistentHomology {
#[allow(dead_code)]
pub fn new() -> Self {
Self { pairs: Vec::new() }
}
#[allow(dead_code)]
pub fn compute_0d(points: &[[f64; 3]]) -> Self {
let n = points.len();
if n == 0 {
return Self::new();
}
let mut edges: Vec<(f64, usize, usize)> = Vec::new();
for i in 0..n {
for j in i + 1..n {
let d = dist3(&points[i], &points[j]);
edges.push((d, i, j));
}
}
edges.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut uf = UnionFind::new(n);
let mut pairs = Vec::new();
for _ in 0..n {
}
for (dist, i, j) in edges {
let ri = uf.find(i);
let rj = uf.find(j);
if ri != rj {
let born_i = 0.0_f64;
let born_j = 0.0_f64;
let dying = if born_i >= born_j { ri } else { rj };
let _ = dying; pairs.push(BirthDeathPair {
birth: 0.0,
death: dist,
dim: 0,
});
uf.union(i, j);
}
}
pairs.push(BirthDeathPair {
birth: 0.0,
death: f64::INFINITY,
dim: 0,
});
Self { pairs }
}
#[allow(dead_code)]
pub fn diagram(&self) -> &[BirthDeathPair] {
&self.pairs
}
#[allow(dead_code)]
pub fn significant_features(&self, threshold: f64) -> usize {
self.pairs
.iter()
.filter(|p| p.persistence() > threshold)
.count()
}
#[allow(dead_code)]
pub fn bottleneck_distance(&self, other: &Self) -> f64 {
let d1: Vec<_> = self.pairs.iter().filter(|p| p.death.is_finite()).collect();
let d2: Vec<_> = other.pairs.iter().filter(|p| p.death.is_finite()).collect();
if d1.is_empty() && d2.is_empty() {
return 0.0;
}
let mut max_cost = 0.0_f64;
for p1 in &d1 {
let diag_dist = (p1.death - p1.birth) / 2.0;
let min_d2 = d2
.iter()
.map(|p2| (p1.birth - p2.birth).abs().max((p1.death - p2.death).abs()))
.fold(f64::INFINITY, f64::min);
max_cost = max_cost.max(min_d2.min(diag_dist));
}
for p2 in &d2 {
let diag_dist = (p2.death - p2.birth) / 2.0;
let min_d1 = d1
.iter()
.map(|p1| (p1.birth - p2.birth).abs().max((p1.death - p2.death).abs()))
.fold(f64::INFINITY, f64::min);
max_cost = max_cost.max(min_d1.min(diag_dist));
}
max_cost
}
}
impl Default for PersistentHomology {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
struct UnionFind {
parent: Vec<usize>,
rank: Vec<usize>,
}
impl UnionFind {
fn new(n: usize) -> Self {
Self {
parent: (0..n).collect(),
rank: vec![0; n],
}
}
fn find(&mut self, x: usize) -> usize {
if self.parent[x] != x {
self.parent[x] = self.find(self.parent[x]);
}
self.parent[x]
}
fn union(&mut self, x: usize, y: usize) {
let rx = self.find(x);
let ry = self.find(y);
if rx == ry {
return;
}
if self.rank[rx] < self.rank[ry] {
self.parent[rx] = ry;
} else if self.rank[rx] > self.rank[ry] {
self.parent[ry] = rx;
} else {
self.parent[ry] = rx;
self.rank[rx] += 1;
}
}
fn n_components(&mut self) -> usize {
let n = self.parent.len();
let roots: std::collections::HashSet<usize> = (0..n).map(|i| self.find(i)).collect();
roots.len()
}
}
#[derive(Debug, Clone)]
pub struct ReebGraph {
pub nodes: Vec<ReebNode>,
pub arcs: Vec<ReebArc>,
}
#[derive(Debug, Clone)]
pub struct ReebNode {
pub value: f64,
pub kind: CriticalPointKind,
pub vertex_idx: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CriticalPointKind {
Minimum,
Saddle,
Maximum,
}
#[derive(Debug, Clone)]
pub struct ReebArc {
pub source: usize,
pub target: usize,
}
impl ReebGraph {
#[allow(dead_code)]
pub fn new() -> Self {
Self {
nodes: Vec::new(),
arcs: Vec::new(),
}
}
#[allow(dead_code)]
pub fn compute(values: &[f64], edges: &[[usize; 2]]) -> Self {
let n = values.len();
if n == 0 {
return Self::new();
}
let mut sorted_idx: Vec<usize> = (0..n).collect();
sorted_idx.sort_by(|&a, &b| {
values[a]
.partial_cmp(&values[b])
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut nodes = Vec::new();
let mut arcs = Vec::new();
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
for &[i, j] in edges {
adj[i].push(j);
adj[j].push(i);
}
for &v in &sorted_idx {
let fv = values[v];
let lower_nbrs: Vec<usize> = adj[v]
.iter()
.filter(|&&u| values[u] < fv)
.cloned()
.collect();
let upper_nbrs: Vec<usize> = adj[v]
.iter()
.filter(|&&u| values[u] > fv)
.cloned()
.collect();
let kind = if lower_nbrs.is_empty() {
CriticalPointKind::Minimum
} else if upper_nbrs.is_empty() {
CriticalPointKind::Maximum
} else {
CriticalPointKind::Saddle
};
if matches!(
kind,
CriticalPointKind::Minimum | CriticalPointKind::Maximum | CriticalPointKind::Saddle
) {
if matches!(
kind,
CriticalPointKind::Minimum | CriticalPointKind::Maximum
) {
let node_idx = nodes.len();
nodes.push(ReebNode {
value: fv,
kind,
vertex_idx: v,
});
if node_idx > 0 {
arcs.push(ReebArc {
source: node_idx - 1,
target: node_idx,
});
}
}
}
}
Self { nodes, arcs }
}
#[allow(dead_code)]
pub fn n_critical_points(&self) -> usize {
self.nodes.len()
}
#[allow(dead_code)]
pub fn n_minima(&self) -> usize {
self.nodes
.iter()
.filter(|n| n.kind == CriticalPointKind::Minimum)
.count()
}
#[allow(dead_code)]
pub fn n_maxima(&self) -> usize {
self.nodes
.iter()
.filter(|n| n.kind == CriticalPointKind::Maximum)
.count()
}
#[allow(dead_code)]
pub fn n_saddles(&self) -> usize {
self.nodes
.iter()
.filter(|n| n.kind == CriticalPointKind::Saddle)
.count()
}
}
impl Default for ReebGraph {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct MorseComplex {
pub critical_points: Vec<MorseCriticalPoint>,
pub connections: Vec<(usize, usize)>,
}
#[derive(Debug, Clone)]
pub struct MorseCriticalPoint {
pub vertex_idx: usize,
pub value: f64,
pub morse_index: usize,
}
impl MorseComplex {
#[allow(dead_code)]
pub fn new() -> Self {
Self {
critical_points: Vec::new(),
connections: Vec::new(),
}
}
#[allow(dead_code)]
pub fn compute(values: &[f64], edges: &[[usize; 2]]) -> Self {
let n = values.len();
if n == 0 {
return Self::new();
}
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
for &[i, j] in edges {
adj[i].push(j);
adj[j].push(i);
}
let mut critical_points = Vec::new();
for v in 0..n {
let fv = values[v];
let n_lower = adj[v].iter().filter(|&&u| values[u] < fv).count();
let n_upper = adj[v].iter().filter(|&&u| values[u] > fv).count();
let morse_index = if n_lower == 0 && n_upper > 0 {
Some(0) } else if n_upper == 0 && n_lower > 0 {
Some(2) } else if n_lower > 0 && n_upper > 0 {
Some(1)
} else {
None
};
if let Some(idx) = morse_index {
critical_points.push(MorseCriticalPoint {
vertex_idx: v,
value: fv,
morse_index: idx,
});
}
}
critical_points.sort_by(|a, b| {
a.value
.partial_cmp(&b.value)
.unwrap_or(std::cmp::Ordering::Equal)
});
let connections: Vec<(usize, usize)> = (0..critical_points.len().saturating_sub(1))
.map(|i| (i, i + 1))
.collect();
Self {
critical_points,
connections,
}
}
#[allow(dead_code)]
pub fn n_minima(&self) -> usize {
self.critical_points
.iter()
.filter(|c| c.morse_index == 0)
.count()
}
#[allow(dead_code)]
pub fn n_saddles(&self) -> usize {
self.critical_points
.iter()
.filter(|c| c.morse_index == 1)
.count()
}
#[allow(dead_code)]
pub fn n_maxima(&self) -> usize {
self.critical_points
.iter()
.filter(|c| c.morse_index == 2)
.count()
}
#[allow(dead_code)]
pub fn morse_relation_holds(&self) -> bool {
let nm = self.n_minima();
let ns = self.n_saddles();
let nmax = self.n_maxima();
nm as i64 - ns as i64 + nmax as i64 >= 0
}
}
impl Default for MorseComplex {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct AlphaComplex {
pub alpha: f64,
pub points: Vec<[f64; 3]>,
pub edges: Vec<[usize; 2]>,
pub triangles: Vec<[usize; 3]>,
}
impl AlphaComplex {
#[allow(dead_code)]
pub fn new(points: Vec<[f64; 3]>, alpha: f64) -> Self {
let n = points.len();
let mut edges = Vec::new();
let mut triangles = Vec::new();
for i in 0..n {
for j in i + 1..n {
let d = dist3(&points[i], &points[j]);
if d / 2.0 <= alpha {
edges.push([i, j]);
}
}
}
for i in 0..n {
for j in i + 1..n {
for k in j + 1..n {
let r = circumradius_3pts(&points[i], &points[j], &points[k]);
if r <= alpha {
triangles.push([i, j, k]);
}
}
}
}
Self {
alpha,
points,
edges,
triangles,
}
}
#[allow(dead_code)]
pub fn n_components(&self) -> usize {
let n = self.points.len();
if n == 0 {
return 0;
}
let mut uf = UnionFind::new(n);
for &[i, j] in &self.edges {
uf.union(i, j);
}
uf.n_components()
}
#[allow(dead_code)]
pub fn to_simplicial_complex(&self) -> SimplicialComplex {
let mut sc = SimplicialComplex::new();
for &p in &self.points {
sc.add_vertex(p);
}
for &[i, j] in &self.edges {
sc.add_edge(i, j);
}
for &[i, j, k] in &self.triangles {
sc.add_triangle(i, j, k);
}
sc
}
}
#[allow(dead_code)]
fn circumradius_3pts(a: &[f64; 3], b: &[f64; 3], c: &[f64; 3]) -> f64 {
let ab = dist3(a, b);
let bc = dist3(b, c);
let ca = dist3(c, a);
let s = (ab + bc + ca) / 2.0;
let area_sq = s * (s - ab) * (s - bc) * (s - ca);
if area_sq <= 0.0 {
return f64::INFINITY;
}
let area = area_sq.sqrt();
ab * bc * ca / (4.0 * area)
}
#[allow(dead_code)]
fn dist3(a: &[f64; 3], b: &[f64; 3]) -> f64 {
let dx = a[0] - b[0];
let dy = a[1] - b[1];
let dz = a[2] - b[2];
(dx * dx + dy * dy + dz * dz).sqrt()
}
#[derive(Debug, Clone)]
pub struct CheckerboardComplex {
pub dims: [usize; 2],
pub data: Vec<bool>,
}
impl CheckerboardComplex {
#[allow(dead_code)]
pub fn new(dims: [usize; 2], data: Vec<bool>) -> Self {
Self { dims, data }
}
#[allow(dead_code)]
pub fn filled_disk(size: usize, radius: f64) -> Self {
let cx = size as f64 / 2.0;
let cy = size as f64 / 2.0;
let data: Vec<bool> = (0..size * size)
.map(|idx| {
let i = (idx / size) as f64;
let j = (idx % size) as f64;
((i - cx).powi(2) + (j - cy).powi(2)).sqrt() <= radius
})
.collect();
Self::new([size, size], data)
}
#[allow(dead_code)]
pub fn annulus(size: usize, r_inner: f64, r_outer: f64) -> Self {
let cx = size as f64 / 2.0;
let cy = size as f64 / 2.0;
let data: Vec<bool> = (0..size * size)
.map(|idx| {
let i = (idx / size) as f64;
let j = (idx % size) as f64;
let r = ((i - cx).powi(2) + (j - cy).powi(2)).sqrt();
r >= r_inner && r <= r_outer
})
.collect();
Self::new([size, size], data)
}
#[allow(dead_code)]
pub fn volume(&self) -> usize {
self.data.iter().filter(|&&b| b).count()
}
#[allow(dead_code)]
pub fn euler_characteristic_2d(&self) -> i64 {
let nx = self.dims[0];
let ny = self.dims[1];
let mut v = 0_i64; let mut e = 0_i64; let mut f = 0_i64;
for i in 0..nx {
for j in 0..ny {
let filled = self.data[i * ny + j];
if filled {
v += 1;
if i + 1 < nx && self.data[(i + 1) * ny + j] {
e += 1;
}
if j + 1 < ny && self.data[i * ny + j + 1] {
e += 1;
}
if i + 1 < nx
&& j + 1 < ny
&& self.data[(i + 1) * ny + j]
&& self.data[i * ny + j + 1]
&& self.data[(i + 1) * ny + j + 1]
{
f += 1;
}
}
}
}
v - e + f
}
}
#[derive(Debug, Clone, Copy)]
pub struct BettiNumbers {
pub beta_0: usize,
pub beta_1: usize,
pub beta_2: usize,
}
impl BettiNumbers {
#[allow(dead_code)]
pub fn from_simplicial_complex(
sc: &SimplicialComplex,
n_components: usize,
n_voids: usize,
) -> Self {
let chi = sc.euler_characteristic();
let beta_1 = (n_components as i64 + n_voids as i64 - chi).max(0) as usize;
Self {
beta_0: n_components,
beta_1,
beta_2: n_voids,
}
}
#[allow(dead_code)]
pub fn euler_characteristic(&self) -> i64 {
self.beta_0 as i64 - self.beta_1 as i64 + self.beta_2 as i64
}
#[allow(dead_code)]
pub fn new(beta_0: usize, beta_1: usize, beta_2: usize) -> Self {
Self {
beta_0,
beta_1,
beta_2,
}
}
}
#[derive(Debug, Clone)]
pub struct TopologicalNoise {
pub threshold: f64,
}
impl TopologicalNoise {
#[allow(dead_code)]
pub fn new(threshold: f64) -> Self {
Self { threshold }
}
#[allow(dead_code)]
pub fn filter(&self, pairs: &[BirthDeathPair]) -> Vec<BirthDeathPair> {
pairs
.iter()
.filter(|p| p.persistence() > self.threshold || p.is_essential())
.cloned()
.collect()
}
#[allow(dead_code)]
pub fn count_features(&self, pairs: &[BirthDeathPair], dim: usize) -> usize {
pairs
.iter()
.filter(|p| p.dim == dim && (p.persistence() > self.threshold || p.is_essential()))
.count()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_euler_characteristic_sphere() {
let sc = SimplicialComplex::octahedral_sphere();
assert_eq!(sc.euler_characteristic(), 2);
}
#[test]
fn test_octahedral_sphere_vertex_count() {
let sc = SimplicialComplex::octahedral_sphere();
assert_eq!(sc.n_vertices(), 6);
}
#[test]
fn test_octahedral_sphere_edge_count() {
let sc = SimplicialComplex::octahedral_sphere();
assert_eq!(sc.n_edges(), 12);
}
#[test]
fn test_octahedral_sphere_triangle_count() {
let sc = SimplicialComplex::octahedral_sphere();
assert_eq!(sc.n_triangles(), 8);
}
#[test]
fn test_euler_characteristic_torus() {
let sc = SimplicialComplex::minimal_torus();
assert_eq!(sc.euler_characteristic(), 0);
}
#[test]
fn test_single_vertex_euler() {
let mut sc = SimplicialComplex::new();
sc.add_vertex([0.0, 0.0, 0.0]);
assert_eq!(sc.euler_characteristic(), 1);
}
#[test]
fn test_add_duplicate_edge_ignored() {
let mut sc = SimplicialComplex::new();
sc.add_vertex([0.0, 0.0, 0.0]);
sc.add_vertex([1.0, 0.0, 0.0]);
sc.add_edge(0, 1);
sc.add_edge(0, 1); assert_eq!(sc.n_edges(), 1);
}
#[test]
fn test_tetrahedron_euler_characteristic() {
let mut sc = SimplicialComplex::new();
for i in 0..4 {
sc.add_vertex([i as f64, 0.0, 0.0]);
}
sc.add_edge(0, 1);
sc.add_edge(0, 2);
sc.add_edge(0, 3);
sc.add_edge(1, 2);
sc.add_edge(1, 3);
sc.add_edge(2, 3);
sc.add_triangle(0, 1, 2);
sc.add_triangle(0, 1, 3);
sc.add_triangle(0, 2, 3);
sc.add_triangle(1, 2, 3);
assert_eq!(sc.euler_characteristic(), 2);
}
#[test]
fn test_boundary_1_dimensions() {
let sc = SimplicialComplex::octahedral_sphere();
let b1 = sc.boundary_1();
assert_eq!(b1.len(), sc.n_vertices() * sc.n_edges());
}
#[test]
fn test_boundary_2_dimensions() {
let sc = SimplicialComplex::octahedral_sphere();
let b2 = sc.boundary_2();
assert_eq!(b2.len(), sc.n_edges() * sc.n_triangles());
}
#[test]
fn test_persistent_homology_single_point() {
let pts = [[0.0, 0.0, 0.0]];
let ph = PersistentHomology::compute_0d(&pts);
assert_eq!(ph.significant_features(0.0), 1);
}
#[test]
fn test_persistent_homology_two_points() {
let pts = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
let ph = PersistentHomology::compute_0d(&pts);
assert_eq!(ph.pairs.len(), 2);
}
#[test]
fn test_persistent_homology_one_blob() {
let pts: Vec<[f64; 3]> = (0..5).map(|i| [i as f64 * 0.1, 0.0, 0.0]).collect();
let ph = PersistentHomology::compute_0d(&pts);
let essential = ph.pairs.iter().filter(|p| p.is_essential()).count();
assert_eq!(essential, 1);
}
#[test]
fn test_persistent_homology_birth_death_order() {
let pts = [[0.0, 0.0, 0.0], [0.1, 0.0, 0.0], [5.0, 0.0, 0.0]];
let ph = PersistentHomology::compute_0d(&pts);
for p in ph.pairs.iter().filter(|p| p.death.is_finite()) {
assert!(p.birth <= p.death);
}
}
#[test]
fn test_bottleneck_distance_nonneg() {
let pts1 = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
let pts2 = [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
let ph1 = PersistentHomology::compute_0d(&pts1);
let ph2 = PersistentHomology::compute_0d(&pts2);
let d = ph1.bottleneck_distance(&ph2);
assert!(d >= 0.0);
}
#[test]
fn test_bottleneck_distance_self_zero() {
let pts = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
let ph = PersistentHomology::compute_0d(&pts);
let d = ph.bottleneck_distance(&ph);
assert!(d < 1e-10);
}
#[test]
fn test_persistence_positive() {
let p = BirthDeathPair {
birth: 0.5,
death: 1.5,
dim: 0,
};
assert!((p.persistence() - 1.0).abs() < 1e-10);
}
#[test]
fn test_reeb_graph_height_sphere_two_critical_points() {
let values = vec![0.0, 0.5, 1.0, 0.7, 0.3];
let edges = [[0usize, 1], [1, 2], [2, 3], [3, 4]];
let rg = ReebGraph::compute(&values, &edges);
assert!(rg.n_minima() >= 1);
assert!(rg.n_maxima() >= 1);
}
#[test]
fn test_reeb_graph_monotone_no_saddle() {
let values = vec![0.0, 1.0, 2.0, 3.0, 4.0];
let edges = [[0usize, 1], [1, 2], [2, 3], [3, 4]];
let rg = ReebGraph::compute(&values, &edges);
assert_eq!(rg.n_saddles(), 0);
}
#[test]
fn test_reeb_graph_empty() {
let rg = ReebGraph::compute(&[], &[]);
assert_eq!(rg.n_critical_points(), 0);
}
#[test]
fn test_morse_complex_line() {
let values = vec![1.0, 0.5, 0.8, 0.3, 0.7];
let edges = [[0usize, 1], [1, 2], [2, 3], [3, 4]];
let mc = MorseComplex::compute(&values, &edges);
assert!(mc.n_minima() >= 1);
assert!(mc.n_maxima() >= 1);
}
#[test]
fn test_morse_relation_holds() {
let values = vec![1.0, 3.0, 0.5, 2.0, 0.3];
let edges = [[0usize, 1], [1, 2], [2, 3], [3, 4], [0, 4]];
let mc = MorseComplex::compute(&values, &edges);
assert!(mc.morse_relation_holds());
}
#[test]
fn test_morse_empty() {
let mc = MorseComplex::compute(&[], &[]);
assert_eq!(mc.n_minima() + mc.n_saddles() + mc.n_maxima(), 0);
}
#[test]
fn test_morse_single_vertex() {
let values = vec![1.0];
let mc = MorseComplex::compute(&values, &[]);
assert_eq!(mc.critical_points.len(), 0);
}
#[test]
fn test_alpha_complex_large_alpha_all_edges() {
let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
let ac = AlphaComplex::new(pts, 1000.0);
assert_eq!(ac.edges.len(), 3);
}
#[test]
fn test_alpha_complex_zero_alpha_no_edges() {
let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
let ac = AlphaComplex::new(pts, 0.0);
assert_eq!(ac.edges.len(), 0);
}
#[test]
fn test_alpha_complex_components() {
let pts = vec![[0.0, 0.0, 0.0], [0.1, 0.0, 0.0], [10.0, 0.0, 0.0]];
let ac = AlphaComplex::new(pts, 0.1);
assert_eq!(ac.n_components(), 2);
}
#[test]
fn test_alpha_complex_single_component_large_alpha() {
let pts: Vec<[f64; 3]> = (0..4).map(|i| [i as f64 * 0.5, 0.0, 0.0]).collect();
let ac = AlphaComplex::new(pts, 1000.0);
assert_eq!(ac.n_components(), 1);
}
#[test]
fn test_checkerboard_filled_disk_volume() {
let cc = CheckerboardComplex::filled_disk(10, 4.0);
assert!(cc.volume() > 0);
}
#[test]
fn test_checkerboard_annulus_volume() {
let cc = CheckerboardComplex::annulus(20, 3.0, 7.0);
assert!(cc.volume() > 0);
}
#[test]
fn test_checkerboard_euler_single_square() {
let cc = CheckerboardComplex::new([1, 1], vec![true]);
let chi = cc.euler_characteristic_2d();
assert_eq!(chi, 1); }
#[test]
fn test_betti_euler_consistency() {
let beta = BettiNumbers::new(1, 2, 1);
assert_eq!(beta.euler_characteristic(), 0);
}
#[test]
fn test_betti_sphere_euler() {
let beta = BettiNumbers::new(1, 0, 1);
assert_eq!(beta.euler_characteristic(), 2);
}
#[test]
fn test_betti_from_simplicial_sphere() {
let sc = SimplicialComplex::octahedral_sphere();
let beta = BettiNumbers::from_simplicial_complex(&sc, 1, 0);
assert_eq!(beta.beta_0, 1);
}
#[test]
fn test_topological_noise_removes_small_features() {
let noise = TopologicalNoise::new(0.5);
let pairs = vec![
BirthDeathPair {
birth: 0.0,
death: 0.1,
dim: 0,
},
BirthDeathPair {
birth: 0.0,
death: 1.0,
dim: 0,
},
];
let filtered = noise.filter(&pairs);
assert_eq!(filtered.len(), 1);
}
#[test]
fn test_topological_noise_keeps_essential() {
let noise = TopologicalNoise::new(100.0);
let pairs = vec![BirthDeathPair {
birth: 0.0,
death: f64::INFINITY,
dim: 0,
}];
let filtered = noise.filter(&pairs);
assert_eq!(filtered.len(), 1);
}
#[test]
fn test_topological_noise_count_features() {
let noise = TopologicalNoise::new(0.3);
let pairs = vec![
BirthDeathPair {
birth: 0.0,
death: 0.1,
dim: 0,
},
BirthDeathPair {
birth: 0.0,
death: 0.5,
dim: 0,
},
BirthDeathPair {
birth: 0.0,
death: f64::INFINITY,
dim: 0,
},
];
assert_eq!(noise.count_features(&pairs, 0), 2);
}
#[test]
fn test_topological_noise_zero_threshold() {
let noise = TopologicalNoise::new(0.0);
let pairs = vec![
BirthDeathPair {
birth: 0.0,
death: 0.001,
dim: 1,
},
BirthDeathPair {
birth: 0.0,
death: 1.0,
dim: 1,
},
];
let filtered = noise.filter(&pairs);
assert_eq!(filtered.len(), 2);
}
#[test]
fn test_circumradius_equilateral() {
let a = [0.0, 0.0, 0.0];
let b = [1.0, 0.0, 0.0];
let c = [0.5, (3.0_f64).sqrt() / 2.0, 0.0];
let r = circumradius_3pts(&a, &b, &c);
assert!((r - 1.0 / 3.0_f64.sqrt()).abs() < 1e-6);
}
}