#![allow(clippy::needless_range_loop)]
#![allow(dead_code)]
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct Simplex {
pub vertices: Vec<usize>,
pub dimension: usize,
pub filtration: f64,
}
impl Simplex {
pub fn new(mut vertices: Vec<usize>, filtration: f64) -> Self {
vertices.sort_unstable();
let dimension = vertices.len().saturating_sub(1);
Self {
vertices,
dimension,
filtration,
}
}
pub fn faces(&self) -> Vec<Simplex> {
if self.dimension == 0 {
return vec![];
}
(0..self.vertices.len())
.map(|i| {
let verts: Vec<usize> = self
.vertices
.iter()
.enumerate()
.filter(|(j, _)| *j != i)
.map(|(_, &v)| v)
.collect();
Simplex::new(verts, self.filtration)
})
.collect()
}
pub fn contains(&self, other: &Simplex) -> bool {
other.vertices.iter().all(|v| self.vertices.contains(v))
}
}
#[derive(Debug, Clone)]
pub struct SimplicialComplex {
pub simplices: Vec<Vec<Simplex>>,
pub max_dimension: usize,
}
impl SimplicialComplex {
pub fn new() -> Self {
Self {
simplices: vec![],
max_dimension: 0,
}
}
pub fn add_simplex(&mut self, s: Simplex) {
let faces = s.faces();
for face in faces {
self.add_simplex(face);
}
let dim = s.dimension;
while self.simplices.len() <= dim {
self.simplices.push(vec![]);
}
if dim > self.max_dimension {
self.max_dimension = dim;
}
if !self.simplices[dim].iter().any(|x| x.vertices == s.vertices) {
self.simplices[dim].push(s);
}
}
pub fn count(&self, k: usize) -> usize {
self.simplices.get(k).map_or(0, |v| v.len())
}
pub fn euler_characteristic(&self) -> i64 {
self.simplices
.iter()
.enumerate()
.map(|(k, s)| {
if k % 2 == 0 {
s.len() as i64
} else {
-(s.len() as i64)
}
})
.sum()
}
pub fn boundary_matrix(&self, k: usize) -> Vec<Vec<i32>> {
if k == 0 || k > self.max_dimension {
return vec![];
}
let k_simplices = &self.simplices[k];
let km1_simplices = &self.simplices[k - 1];
let rows = k_simplices.len();
let cols = km1_simplices.len();
let mut mat = vec![vec![0i32; cols]; rows];
for (i, sigma) in k_simplices.iter().enumerate() {
for (j, tau) in km1_simplices.iter().enumerate() {
if sigma.contains(tau) {
mat[i][j] = 1;
}
}
}
mat
}
}
impl Default for SimplicialComplex {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct FilteredComplex {
pub ordered_simplices: Vec<Simplex>,
}
impl FilteredComplex {
pub fn new(mut simplices: Vec<Simplex>) -> Self {
simplices.sort_by(|a, b| {
a.filtration
.partial_cmp(&b.filtration)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.dimension.cmp(&b.dimension))
});
Self {
ordered_simplices: simplices,
}
}
pub fn len(&self) -> usize {
self.ordered_simplices.len()
}
pub fn is_empty(&self) -> bool {
self.ordered_simplices.is_empty()
}
pub fn boundary_column(&self, i: usize) -> Vec<usize> {
let sigma = &self.ordered_simplices[i];
if sigma.dimension == 0 {
return vec![];
}
let faces = sigma.faces();
let mut col = Vec::new();
for face in &faces {
if let Some(j) = self.ordered_simplices[..i]
.iter()
.rposition(|s| s.vertices == face.vertices)
{
col.push(j);
}
}
col.sort_unstable();
col
}
pub fn reduce(&self) -> Vec<(usize, usize)> {
let n = self.ordered_simplices.len();
let mut columns: Vec<Vec<usize>> = (0..n).map(|i| self.boundary_column(i)).collect();
let low = |col: &Vec<usize>| col.last().copied();
let mut pivot_col: HashMap<usize, usize> = HashMap::new();
let mut pairs = Vec::new();
for j in 0..n {
loop {
if columns[j].is_empty() {
break;
}
let l = low(&columns[j]).expect("column j is non-empty");
if let Some(&k) = pivot_col.get(&l) {
let col_k = columns[k].clone();
let col_j = std::mem::take(&mut columns[j]);
columns[j] = xor_sorted_vecs(col_j, col_k);
} else {
pivot_col.insert(l, j);
pairs.push((l, j));
break;
}
}
}
pairs
}
}
fn xor_sorted_vecs(mut a: Vec<usize>, mut b: Vec<usize>) -> Vec<usize> {
a.sort_unstable();
b.sort_unstable();
let mut result = Vec::new();
let (mut i, mut j) = (0, 0);
while i < a.len() && j < b.len() {
match a[i].cmp(&b[j]) {
std::cmp::Ordering::Less => {
result.push(a[i]);
i += 1;
}
std::cmp::Ordering::Greater => {
result.push(b[j]);
j += 1;
}
std::cmp::Ordering::Equal => {
i += 1;
j += 1;
}
}
}
result.extend_from_slice(&a[i..]);
result.extend_from_slice(&b[j..]);
result
}
#[derive(Debug, Clone)]
pub struct PersistenceDiagram {
pub pairs: Vec<(f64, f64, usize)>,
pub essential: Vec<(f64, usize)>,
}
impl PersistenceDiagram {
pub fn from_filtered_complex(fc: &FilteredComplex) -> Self {
let pairs_idx = fc.reduce();
let n = fc.ordered_simplices.len();
let mut paired = vec![false; n];
let mut pairs = Vec::new();
let mut essential = Vec::new();
for (birth_idx, death_idx) in &pairs_idx {
let b = fc.ordered_simplices[*birth_idx].filtration;
let d = fc.ordered_simplices[*death_idx].filtration;
let dim = fc.ordered_simplices[*birth_idx].dimension;
if (d - b).abs() > 1e-15 {
pairs.push((b, d, dim));
}
paired[*birth_idx] = true;
paired[*death_idx] = true;
}
for (i, s) in fc.ordered_simplices.iter().enumerate() {
if !paired[i] {
essential.push((s.filtration, s.dimension));
}
}
Self { pairs, essential }
}
pub fn betti_numbers(&self, t: f64, max_dim: usize) -> Vec<usize> {
let mut betti = vec![0usize; max_dim + 1];
for &(b, d, dim) in &self.pairs {
if dim <= max_dim && b <= t && d > t {
betti[dim] += 1;
}
}
for &(b, dim) in &self.essential {
if dim <= max_dim && b <= t {
betti[dim] += 1;
}
}
betti
}
pub fn total_persistence(&self) -> f64 {
self.pairs.iter().map(|(b, d, _)| d - b).sum()
}
pub fn pairs_for_dim(&self, k: usize) -> Vec<(f64, f64)> {
self.pairs
.iter()
.filter(|&&(_, _, d)| d == k)
.map(|&(b, d, _)| (b, d))
.collect()
}
}
#[derive(Debug, Clone)]
pub struct RipsFiltration {
pub distances: Vec<Vec<f64>>,
pub n_points: usize,
pub max_radius: f64,
pub max_dim: usize,
}
impl RipsFiltration {
pub fn from_point_cloud(points: &[[f64; 2]], max_radius: f64, max_dim: usize) -> Self {
let n = points.len();
let mut distances = vec![vec![0.0_f64; n]; n];
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];
let d = (dx * dx + dy * dy).sqrt();
distances[i][j] = d;
distances[j][i] = d;
}
}
Self {
distances,
n_points: n,
max_radius,
max_dim,
}
}
pub fn from_distance_matrix(distances: Vec<Vec<f64>>, max_radius: f64, max_dim: usize) -> Self {
let n = distances.len();
Self {
distances,
n_points: n,
max_radius,
max_dim,
}
}
pub fn build_complex(&self) -> FilteredComplex {
let mut simplices = Vec::new();
let n = self.n_points;
for i in 0..n {
simplices.push(Simplex::new(vec![i], 0.0));
}
for i in 0..n {
for j in (i + 1)..n {
let d = self.distances[i][j];
if d <= self.max_radius {
simplices.push(Simplex::new(vec![i, j], d));
}
}
}
if self.max_dim >= 2 {
for i in 0..n {
for j in (i + 1)..n {
for k in (j + 1)..n {
let d = self.distances[i][j]
.max(self.distances[i][k])
.max(self.distances[j][k]);
if d <= self.max_radius {
simplices.push(Simplex::new(vec![i, j, k], d));
}
}
}
}
}
if self.max_dim >= 3 && n >= 4 {
for i in 0..n {
for j in (i + 1)..n {
for k in (j + 1)..n {
for l in (k + 1)..n {
let d = self.distances[i][j]
.max(self.distances[i][k])
.max(self.distances[i][l])
.max(self.distances[j][k])
.max(self.distances[j][l])
.max(self.distances[k][l]);
if d <= self.max_radius {
simplices.push(Simplex::new(vec![i, j, k, l], d));
}
}
}
}
}
}
FilteredComplex::new(simplices)
}
pub fn persistence_diagram(&self) -> PersistenceDiagram {
let fc = self.build_complex();
PersistenceDiagram::from_filtered_complex(&fc)
}
}
#[derive(Debug, Clone)]
pub struct PersistenceImage {
pub pixels: Vec<Vec<f64>>,
pub n_rows: usize,
pub n_cols: usize,
pub birth_min: f64,
pub birth_max: f64,
pub pers_max: f64,
pub sigma: f64,
}
impl PersistenceImage {
pub fn from_diagram(
diagram: &PersistenceDiagram,
n_rows: usize,
n_cols: usize,
sigma: f64,
) -> Self {
let pts: Vec<(f64, f64)> = diagram
.pairs
.iter()
.filter(|&&(b, d, _)| d.is_finite() && d > b)
.map(|&(b, d, _)| (b, d - b))
.collect();
if pts.is_empty() {
return Self {
pixels: vec![vec![0.0; n_cols]; n_rows],
n_rows,
n_cols,
birth_min: 0.0,
birth_max: 1.0,
pers_max: 1.0,
sigma,
};
}
let birth_min = pts.iter().map(|(b, _)| *b).fold(f64::INFINITY, f64::min);
let birth_max = pts
.iter()
.map(|(b, _)| *b)
.fold(f64::NEG_INFINITY, f64::max);
let pers_max = pts
.iter()
.map(|(_, p)| *p)
.fold(f64::NEG_INFINITY, f64::max);
let birth_range = (birth_max - birth_min).max(1e-10);
let pers_range = pers_max.max(1e-10);
let mut pixels = vec![vec![0.0_f64; n_cols]; n_rows];
let two_sigma_sq = 2.0 * sigma * sigma;
for (bx, py) in &pts {
let weight = py / pers_range;
for row in 0..n_rows {
let p_grid =
pers_range * (n_rows - 1 - row) as f64 / (n_rows as f64 - 1.0).max(1.0);
for col in 0..n_cols {
let b_grid =
birth_min + birth_range * col as f64 / (n_cols as f64 - 1.0).max(1.0);
let db = bx - b_grid;
let dp = py - p_grid;
let gauss = (-(db * db + dp * dp) / two_sigma_sq).exp();
pixels[row][col] += weight * gauss;
}
}
}
Self {
pixels,
n_rows,
n_cols,
birth_min,
birth_max,
pers_max,
sigma,
}
}
pub fn to_vector(&self) -> Vec<f64> {
self.pixels
.iter()
.flat_map(|row| row.iter().copied())
.collect()
}
pub fn max_value(&self) -> f64 {
self.pixels
.iter()
.flat_map(|row| row.iter())
.cloned()
.fold(f64::NEG_INFINITY, f64::max)
}
}
#[derive(Debug, Clone)]
pub struct BarcodeStatistics {
pub total_persistence: f64,
pub mean_persistence: f64,
pub median_persistence: f64,
pub max_persistence: f64,
pub n_finite: usize,
pub n_essential: usize,
pub bottleneck_to_empty: f64,
}
impl BarcodeStatistics {
pub fn from_diagram(diagram: &PersistenceDiagram) -> Self {
let mut lifetimes: Vec<f64> = diagram
.pairs
.iter()
.filter(|&&(b, d, _)| d.is_finite() && d > b)
.map(|&(b, d, _)| d - b)
.collect();
lifetimes.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n_finite = lifetimes.len();
let total_persistence = lifetimes.iter().sum::<f64>();
let mean_persistence = if n_finite > 0 {
total_persistence / n_finite as f64
} else {
0.0
};
let median_persistence = if n_finite > 0 {
if n_finite % 2 == 1 {
lifetimes[n_finite / 2]
} else {
(lifetimes[n_finite / 2 - 1] + lifetimes[n_finite / 2]) / 2.0
}
} else {
0.0
};
let max_persistence = lifetimes.last().copied().unwrap_or(0.0);
let bottleneck_to_empty = max_persistence / 2.0;
let n_essential = diagram.essential.len();
Self {
total_persistence,
mean_persistence,
median_persistence,
max_persistence,
n_finite,
n_essential,
bottleneck_to_empty,
}
}
pub fn bottleneck_distance(a: &PersistenceDiagram, b: &PersistenceDiagram) -> f64 {
let pts_a: Vec<(f64, f64)> = a
.pairs
.iter()
.filter(|&&(b_, d, _)| d.is_finite() && d > b_)
.map(|&(b_, d, _)| (b_, d - b_))
.collect();
let pts_b: Vec<(f64, f64)> = b
.pairs
.iter()
.filter(|&&(b_, d, _)| d.is_finite() && d > b_)
.map(|&(b_, d, _)| (b_, d - b_))
.collect();
let linf = |pa: (f64, f64), pb: (f64, f64)| -> f64 {
(pa.0 - pb.0).abs().max((pa.1 - pb.1).abs())
};
let diag_dist = |p: (f64, f64)| -> f64 { p.1 / 2.0 };
let mut max_dist = 0.0_f64;
let mut used = vec![false; pts_b.len()];
for &pa in &pts_a {
let best = pts_b
.iter()
.enumerate()
.filter(|(idx, _)| !used[*idx])
.map(|(idx, &pb)| (idx, linf(pa, pb)))
.min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(std::cmp::Ordering::Equal));
match best {
Some((idx, d)) if d < diag_dist(pa) => {
used[idx] = true;
max_dist = max_dist.max(d);
}
_ => {
max_dist = max_dist.max(diag_dist(pa));
}
}
}
for (idx, &pb) in pts_b.iter().enumerate() {
if !used[idx] {
max_dist = max_dist.max(diag_dist(pb));
}
}
max_dist
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simplex_vertex() {
let s = Simplex::new(vec![3], 0.5);
assert_eq!(s.dimension, 0);
assert_eq!(s.vertices, vec![3]);
assert!((s.filtration - 0.5_f64).abs() < 1e-12);
}
#[test]
fn test_simplex_edge_dimension() {
let s = Simplex::new(vec![0, 1], 1.0);
assert_eq!(s.dimension, 1);
}
#[test]
fn test_simplex_triangle_dimension() {
let s = Simplex::new(vec![0, 1, 2], 2.0);
assert_eq!(s.dimension, 2);
}
#[test]
fn test_simplex_vertices_sorted() {
let s = Simplex::new(vec![3, 1, 0, 2], 0.0);
assert_eq!(s.vertices, vec![0, 1, 2, 3]);
}
#[test]
fn test_simplex_vertex_has_no_faces() {
let s = Simplex::new(vec![5], 0.0);
assert!(s.faces().is_empty());
}
#[test]
fn test_simplex_edge_has_two_faces() {
let s = Simplex::new(vec![0, 1], 1.0);
let faces = s.faces();
assert_eq!(faces.len(), 2);
assert!(faces.iter().all(|f| f.dimension == 0));
}
#[test]
fn test_simplex_triangle_has_three_faces() {
let s = Simplex::new(vec![0, 1, 2], 1.5);
let faces = s.faces();
assert_eq!(faces.len(), 3);
assert!(faces.iter().all(|f| f.dimension == 1));
}
#[test]
fn test_simplex_contains() {
let edge = Simplex::new(vec![0, 1], 1.0);
let v0 = Simplex::new(vec![0], 0.0);
let v2 = Simplex::new(vec![2], 0.0);
assert!(edge.contains(&v0));
assert!(!edge.contains(&v2));
}
#[test]
fn test_simplicial_complex_empty() {
let sc = SimplicialComplex::new();
assert_eq!(sc.count(0), 0);
assert_eq!(sc.euler_characteristic(), 0);
}
#[test]
fn test_simplicial_complex_add_triangle() {
let mut sc = SimplicialComplex::new();
sc.add_simplex(Simplex::new(vec![0, 1, 2], 1.0));
assert_eq!(sc.count(0), 3); assert_eq!(sc.count(1), 3); assert_eq!(sc.count(2), 1); }
#[test]
fn test_simplicial_complex_euler_triangle() {
let mut sc = SimplicialComplex::new();
sc.add_simplex(Simplex::new(vec![0, 1, 2], 1.0));
assert_eq!(sc.euler_characteristic(), 1);
}
#[test]
fn test_simplicial_complex_no_duplicates() {
let mut sc = SimplicialComplex::new();
sc.add_simplex(Simplex::new(vec![0, 1], 1.0));
sc.add_simplex(Simplex::new(vec![0, 1], 2.0)); assert_eq!(sc.count(1), 1);
}
#[test]
fn test_simplicial_complex_boundary_matrix_edge() {
let mut sc = SimplicialComplex::new();
sc.add_simplex(Simplex::new(vec![0, 1], 1.0));
sc.add_simplex(Simplex::new(vec![1, 2], 1.0));
let mat = sc.boundary_matrix(1);
assert_eq!(mat.len(), 2); }
#[test]
fn test_simplicial_complex_default() {
let sc = SimplicialComplex::default();
assert_eq!(sc.count(0), 0);
}
#[test]
fn test_filtered_complex_ordering() {
let simplices = vec![
Simplex::new(vec![0, 1], 2.0),
Simplex::new(vec![0], 0.0),
Simplex::new(vec![1], 0.0),
];
let fc = FilteredComplex::new(simplices);
assert_eq!(fc.ordered_simplices[0].dimension, 0);
assert_eq!(fc.ordered_simplices[2].dimension, 1);
}
#[test]
fn test_filtered_complex_len() {
let simplices = vec![
Simplex::new(vec![0], 0.0),
Simplex::new(vec![1], 0.0),
Simplex::new(vec![0, 1], 1.0),
];
let fc = FilteredComplex::new(simplices);
assert_eq!(fc.len(), 3);
assert!(!fc.is_empty());
}
#[test]
fn test_filtered_complex_empty() {
let fc = FilteredComplex::new(vec![]);
assert_eq!(fc.len(), 0);
assert!(fc.is_empty());
}
#[test]
fn test_filtered_complex_boundary_column_vertex() {
let simplices = vec![Simplex::new(vec![0], 0.0)];
let fc = FilteredComplex::new(simplices);
assert!(fc.boundary_column(0).is_empty());
}
#[test]
fn test_filtered_complex_reduce_two_points() {
let simplices = vec![
Simplex::new(vec![0], 0.0),
Simplex::new(vec![1], 0.0),
Simplex::new(vec![0, 1], 1.0),
];
let fc = FilteredComplex::new(simplices);
let pairs = fc.reduce();
assert!(!pairs.is_empty());
}
#[test]
fn test_persistence_diagram_two_points() {
let simplices = vec![
Simplex::new(vec![0], 0.0),
Simplex::new(vec![1], 0.0),
Simplex::new(vec![0, 1], 1.5),
];
let fc = FilteredComplex::new(simplices);
let diag = PersistenceDiagram::from_filtered_complex(&fc);
assert!(!diag.pairs.is_empty() || !diag.essential.is_empty());
}
#[test]
fn test_persistence_diagram_betti_numbers() {
let simplices = vec![
Simplex::new(vec![0], 0.0),
Simplex::new(vec![1], 0.0),
Simplex::new(vec![0, 1], 1.0),
];
let fc = FilteredComplex::new(simplices);
let diag = PersistenceDiagram::from_filtered_complex(&fc);
let b0 = diag.betti_numbers(2.0, 1);
assert_eq!(b0[0], 1);
}
#[test]
fn test_persistence_diagram_total_persistence() {
let simplices = vec![
Simplex::new(vec![0], 0.0),
Simplex::new(vec![1], 0.0),
Simplex::new(vec![0, 1], 1.0),
];
let fc = FilteredComplex::new(simplices);
let diag = PersistenceDiagram::from_filtered_complex(&fc);
assert!(diag.total_persistence() >= 0.0);
}
#[test]
fn test_rips_from_point_cloud_distances() {
let pts = [[0.0_f64, 0.0_f64], [1.0, 0.0], [0.0, 1.0]];
let rips = RipsFiltration::from_point_cloud(&pts, 2.0, 2);
assert_eq!(rips.n_points, 3);
assert!((rips.distances[0][1] - 1.0_f64).abs() < 1e-12);
}
#[test]
fn test_rips_build_complex_vertices() {
let pts = [[0.0_f64, 0.0_f64], [1.0, 0.0], [2.0, 0.0]];
let rips = RipsFiltration::from_point_cloud(&pts, 5.0, 1);
let fc = rips.build_complex();
let n_vertices = fc
.ordered_simplices
.iter()
.filter(|s| s.dimension == 0)
.count();
assert_eq!(n_vertices, 3);
}
#[test]
fn test_rips_build_complex_edges_within_radius() {
let pts = [[0.0_f64, 0.0_f64], [1.0, 0.0], [10.0, 0.0]];
let rips = RipsFiltration::from_point_cloud(&pts, 1.5, 1);
let fc = rips.build_complex();
let n_edges = fc
.ordered_simplices
.iter()
.filter(|s| s.dimension == 1)
.count();
assert_eq!(n_edges, 1);
}
#[test]
fn test_rips_from_distance_matrix() {
let d = vec![
vec![0.0_f64, 1.0, 2.0],
vec![1.0, 0.0, 1.5],
vec![2.0, 1.5, 0.0],
];
let rips = RipsFiltration::from_distance_matrix(d, 2.0, 2);
assert_eq!(rips.n_points, 3);
}
#[test]
fn test_rips_persistence_diagram_single_point() {
let pts = [[0.0_f64, 0.0_f64]];
let rips = RipsFiltration::from_point_cloud(&pts, 1.0, 0);
let diag = rips.persistence_diagram();
assert!(!diag.essential.is_empty());
}
#[test]
fn test_rips_triangle_has_triangles() {
let pts = [[0.0_f64, 0.0_f64], [1.0, 0.0], [0.5, 1.0]];
let rips = RipsFiltration::from_point_cloud(&pts, 3.0, 2);
let fc = rips.build_complex();
let n_tri = fc
.ordered_simplices
.iter()
.filter(|s| s.dimension == 2)
.count();
assert_eq!(n_tri, 1);
}
#[test]
fn test_persistence_image_shape() {
let diag = PersistenceDiagram {
pairs: vec![(0.0, 1.0, 0), (0.5, 2.0, 0)],
essential: vec![],
};
let img = PersistenceImage::from_diagram(&diag, 10, 10, 0.1);
assert_eq!(img.pixels.len(), 10);
assert_eq!(img.pixels[0].len(), 10);
}
#[test]
fn test_persistence_image_non_negative() {
let diag = PersistenceDiagram {
pairs: vec![(0.0, 1.0, 0), (0.5, 2.0, 1)],
essential: vec![],
};
let img = PersistenceImage::from_diagram(&diag, 8, 8, 0.2);
for row in &img.pixels {
for &v in row {
assert!(v >= 0.0);
}
}
}
#[test]
fn test_persistence_image_empty_diagram() {
let diag = PersistenceDiagram {
pairs: vec![],
essential: vec![],
};
let img = PersistenceImage::from_diagram(&diag, 5, 5, 0.1);
let total: f64 = img.to_vector().iter().sum();
assert!(total.abs() < 1e-12);
}
#[test]
fn test_persistence_image_to_vector_length() {
let diag = PersistenceDiagram {
pairs: vec![(0.0, 1.0, 0)],
essential: vec![],
};
let img = PersistenceImage::from_diagram(&diag, 6, 7, 0.1);
assert_eq!(img.to_vector().len(), 42);
}
#[test]
fn test_persistence_image_max_value_positive() {
let diag = PersistenceDiagram {
pairs: vec![(0.0, 1.0, 0)],
essential: vec![],
};
let img = PersistenceImage::from_diagram(&diag, 5, 5, 0.5);
assert!(img.max_value() > 0.0);
}
#[test]
fn test_barcode_stats_empty() {
let diag = PersistenceDiagram {
pairs: vec![],
essential: vec![],
};
let stats = BarcodeStatistics::from_diagram(&diag);
assert_eq!(stats.n_finite, 0);
assert!((stats.total_persistence).abs() < 1e-12);
}
#[test]
fn test_barcode_stats_single_bar() {
let diag = PersistenceDiagram {
pairs: vec![(0.0, 2.0, 0)],
essential: vec![],
};
let stats = BarcodeStatistics::from_diagram(&diag);
assert_eq!(stats.n_finite, 1);
assert!((stats.total_persistence - 2.0_f64).abs() < 1e-12);
assert!((stats.mean_persistence - 2.0_f64).abs() < 1e-12);
assert!((stats.max_persistence - 2.0_f64).abs() < 1e-12);
}
#[test]
fn test_barcode_stats_median_odd() {
let diag = PersistenceDiagram {
pairs: vec![(0.0, 1.0, 0), (0.0, 2.0, 0), (0.0, 3.0, 0)],
essential: vec![],
};
let stats = BarcodeStatistics::from_diagram(&diag);
assert!((stats.median_persistence - 2.0_f64).abs() < 1e-12);
}
#[test]
fn test_barcode_stats_median_even() {
let diag = PersistenceDiagram {
pairs: vec![(0.0, 1.0, 0), (0.0, 3.0, 0)],
essential: vec![],
};
let stats = BarcodeStatistics::from_diagram(&diag);
assert!((stats.median_persistence - 2.0_f64).abs() < 1e-12);
}
#[test]
fn test_barcode_stats_essential_counted() {
let diag = PersistenceDiagram {
pairs: vec![],
essential: vec![(0.0, 0), (0.0, 1)],
};
let stats = BarcodeStatistics::from_diagram(&diag);
assert_eq!(stats.n_essential, 2);
}
#[test]
fn test_barcode_bottleneck_identical() {
let diag = PersistenceDiagram {
pairs: vec![(0.0, 1.0, 0), (1.0, 3.0, 1)],
essential: vec![],
};
let d = BarcodeStatistics::bottleneck_distance(&diag, &diag);
assert!(d < 1e-12, "identical diagrams have bottleneck distance 0");
}
#[test]
fn test_barcode_bottleneck_nonneg() {
let a = PersistenceDiagram {
pairs: vec![(0.0, 1.0, 0)],
essential: vec![],
};
let b = PersistenceDiagram {
pairs: vec![(0.1, 1.2, 0)],
essential: vec![],
};
assert!(BarcodeStatistics::bottleneck_distance(&a, &b) >= 0.0);
}
#[test]
fn test_barcode_bottleneck_empty_vs_nonempty() {
let empty = PersistenceDiagram {
pairs: vec![],
essential: vec![],
};
let nonempty = PersistenceDiagram {
pairs: vec![(0.0, 2.0, 0)],
essential: vec![],
};
let d = BarcodeStatistics::bottleneck_distance(&empty, &nonempty);
assert!((d - 1.0_f64).abs() < 1e-12);
}
#[test]
fn test_xor_sorted_vecs_cancel() {
let a = vec![0, 1, 2];
let b = vec![1, 2, 3];
let result = xor_sorted_vecs(a, b);
assert_eq!(result, vec![0, 3]);
}
#[test]
fn test_xor_sorted_vecs_empty() {
let result = xor_sorted_vecs(vec![], vec![1, 2]);
assert_eq!(result, vec![1, 2]);
}
}