#[derive(Debug, Clone)]
pub struct PersistenceBar {
pub birth: f64,
pub death: f64,
pub dimension: usize,
pub persistence: f64,
}
impl PersistenceBar {
pub fn new(birth: f64, death: f64, dim: usize) -> Self {
Self {
birth,
death,
dimension: dim,
persistence: death - birth,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SimplexEdge {
pub u: u32,
pub v: u32,
pub weight: f64,
}
#[derive(Debug)]
pub struct PersistenceDiagram {
pub h0: Vec<PersistenceBar>,
pub h1: Vec<PersistenceBar>,
pub n_points: usize,
}
impl PersistenceDiagram {
pub fn betti_0(&self) -> usize {
self.h0.iter().filter(|b| b.death >= 1e9).count() + 1
}
}
fn euclidean_dist(a: &[f64], b: &[f64]) -> f64 {
a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).powi(2))
.sum::<f64>()
.sqrt()
}
pub struct SparseRipsComplex {
epsilon: f64,
pub max_radius: f64,
}
impl SparseRipsComplex {
pub fn new(epsilon: f64, max_radius: f64) -> Self {
Self {
epsilon,
max_radius,
}
}
pub fn sparse_1_skeleton(&self, points: &[Vec<f64>]) -> Vec<SimplexEdge> {
let n = points.len();
let mut edges = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
let dist = euclidean_dist(&points[i], &points[j]);
if dist <= self.max_radius {
let ppr_approx = 1.0 / (dist.max(self.epsilon) * n as f64);
if ppr_approx >= self.epsilon {
edges.push(SimplexEdge {
u: i as u32,
v: j as u32,
weight: dist,
});
}
}
}
}
edges
}
fn compute_h0(&self, n_points: usize, edges: &[SimplexEdge]) -> Vec<PersistenceBar> {
let mut parent: Vec<usize> = (0..n_points).collect();
let birth = vec![0.0f64; n_points];
let mut bars = Vec::new();
fn find(parent: &mut Vec<usize>, x: usize) -> usize {
if parent[x] != x {
parent[x] = find(parent, parent[x]);
}
parent[x]
}
let mut sorted_edges: Vec<&SimplexEdge> = edges.iter().collect();
sorted_edges.sort_unstable_by(|a, b| a.weight.partial_cmp(&b.weight).unwrap());
for edge in sorted_edges {
let pu = find(&mut parent, edge.u as usize);
let pv = find(&mut parent, edge.v as usize);
if pu != pv {
let birth_young = birth[pu].max(birth[pv]);
bars.push(PersistenceBar::new(birth_young, edge.weight, 0));
let elder = if birth[pu] <= birth[pv] { pu } else { pv };
let younger = if elder == pu { pv } else { pu };
parent[younger] = elder;
}
}
bars
}
pub fn compute(&self, points: &[Vec<f64>]) -> PersistenceDiagram {
let edges = self.sparse_1_skeleton(points);
let h0 = self.compute_h0(points.len(), &edges);
let h1_count = edges.len().saturating_sub(points.len().saturating_sub(1));
let h1: Vec<PersistenceBar> = edges
.iter()
.take(h1_count)
.filter_map(|e| {
if e.weight < self.max_radius * 0.8 {
Some(PersistenceBar::new(e.weight * 0.5, e.weight, 1))
} else {
None
}
})
.collect();
PersistenceDiagram {
h0,
h1,
n_points: points.len(),
}
}
}
pub fn run_sparse_tda_demo(n_points: usize) -> PersistenceDiagram {
let rips = SparseRipsComplex::new(0.05, 2.0);
let points: Vec<Vec<f64>> = (0..n_points)
.map(|i| {
let angle = (i as f64 / n_points as f64) * 2.0 * std::f64::consts::PI;
vec![angle.cos(), angle.sin()]
})
.collect();
rips.compute(&points)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circle_has_h0() {
let diagram = run_sparse_tda_demo(20);
assert!(!diagram.h0.is_empty());
}
#[test]
fn test_two_clusters_detected() {
let rips = SparseRipsComplex::new(0.05, 1.0);
let mut points: Vec<Vec<f64>> = (0..5).map(|i| vec![i as f64 * 0.1, 0.0]).collect();
points.extend((0..5).map(|i| vec![10.0 + i as f64 * 0.1, 0.0]));
let diagram = rips.compute(&points);
assert!(!diagram.h0.is_empty(), "Should find H0 bars for clusters");
}
#[test]
fn test_persistence_bar_persistence() {
let bar = PersistenceBar::new(0.2, 1.5, 0);
assert!((bar.persistence - 1.3).abs() < 1e-9);
}
#[test]
fn test_sparse_rips_line_has_edges() {
let rips = SparseRipsComplex::new(0.1, 2.0);
let points: Vec<Vec<f64>> = (0..10).map(|i| vec![i as f64 * 0.2]).collect();
let edges = rips.sparse_1_skeleton(&points);
assert!(!edges.is_empty(), "Nearby points should form edges");
}
}