#[derive(Debug, Clone, Copy)]
pub struct SimplexEdge {
pub u: u32,
pub v: u32,
pub weight: f64,
}
#[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,
}
}
pub fn is_significant(&self, threshold: f64) -> bool {
self.persistence > threshold
}
}
pub struct ForwardPushPpr {
pub epsilon: f64,
pub alpha: f64,
}
impl ForwardPushPpr {
pub fn new(epsilon: f64) -> Self {
Self {
epsilon,
alpha: 0.15,
}
}
pub fn push_from(
&self,
source: u32,
adjacency: &[(u32, u32, f64)], n_nodes: u32,
) -> Vec<(u32, f64)> {
let mut ppr = vec![0.0f64; n_nodes as usize];
let mut residual = vec![0.0f64; n_nodes as usize];
residual[source as usize] = 1.0;
let mut out_edges: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n_nodes as usize];
let mut out_weights: Vec<f64> = vec![0.0f64; n_nodes as usize];
for &(u, v, w) in adjacency {
out_edges[u as usize].push((v, w));
out_edges[v as usize].push((u, w)); out_weights[u as usize] += w;
out_weights[v as usize] += w;
}
let threshold = self.epsilon;
let mut queue: Vec<u32> = vec![source];
let max_iters = (1.0 / self.epsilon) as usize * 2;
let mut iter = 0;
while let Some(u) = queue.first().copied() {
queue.remove(0);
iter += 1;
if iter > max_iters {
break;
}
let d_u = out_weights[u as usize].max(1.0);
let r_u = residual[u as usize];
if r_u < threshold * d_u {
continue;
}
ppr[u as usize] += self.alpha * r_u;
let push_amount = (1.0 - self.alpha) * r_u;
residual[u as usize] = 0.0;
let neighbors: Vec<(u32, f64)> = out_edges[u as usize].clone();
for (v, w) in neighbors {
let contribution = push_amount * w / d_u;
residual[v as usize] += contribution;
if residual[v as usize] >= threshold * out_weights[v as usize].max(1.0) {
if !queue.contains(&v) {
queue.push(v);
}
}
}
}
ppr.into_iter()
.enumerate()
.filter(|(_, p)| *p > threshold)
.map(|(i, p)| (i as u32, p))
.collect()
}
}
pub struct SparseRipsComplex {
ppr: ForwardPushPpr,
pub max_radius: f64,
pub epsilon: f64,
}
impl SparseRipsComplex {
pub fn new(epsilon: f64, max_radius: f64) -> Self {
let ppr_epsilon = (epsilon * 0.01).max(1e-4);
Self {
ppr: ForwardPushPpr::new(ppr_epsilon),
max_radius,
epsilon,
}
}
pub fn sparse_1_skeleton(&self, points: &[Vec<f64>]) -> Vec<SimplexEdge> {
let n = points.len() as u32;
let mut all_edges = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
let dist = euclidean_dist(&points[i as usize], &points[j as usize]);
if dist <= self.max_radius {
all_edges.push((i, j, 1.0f64));
}
}
}
let mut selected_edges = std::collections::HashSet::new();
for source in 0..n {
let neighbors = self.ppr.push_from(source, &all_edges, n);
for (nbr, _) in neighbors {
if nbr != source {
let key = (source.min(nbr), source.max(nbr));
selected_edges.insert(key);
}
}
}
selected_edges
.into_iter()
.filter_map(|(u, v)| {
let dist = euclidean_dist(&points[u as usize], &points[v as usize]);
if dist <= self.max_radius {
Some(SimplexEdge { u, v, weight: dist })
} else {
None
}
})
.collect()
}
pub fn compute_h0(&self, n_points: usize, edges: &[SimplexEdge]) -> Vec<PersistenceBar> {
let mut parent: Vec<usize> = (0..n_points).collect();
let birth: Vec<f64> = vec![0.0; 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_or(std::cmp::Ordering::Equal)
});
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_bars = self.compute_h0(points.len(), &edges);
let h1_count = edges.len().saturating_sub(points.len().saturating_sub(1));
let h1_bars: 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: h0_bars,
h1: h1_bars,
n_points: points.len(),
}
}
}
fn euclidean_dist(a: &[f64], b: &[f64]) -> f64 {
a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).powi(2))
.sum::<f64>()
.sqrt()
}
#[derive(Debug)]
pub struct PersistenceDiagram {
pub h0: Vec<PersistenceBar>,
pub h1: Vec<PersistenceBar>,
pub n_points: usize,
}
impl PersistenceDiagram {
pub fn significant_h0(&self, threshold: f64) -> Vec<&PersistenceBar> {
self.h0
.iter()
.filter(|b| b.is_significant(threshold))
.collect()
}
pub fn betti_0(&self) -> usize {
self.h0.iter().filter(|b| b.death >= 1e9).count() + 1
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ppr_push_returns_neighbors() {
let ppr = ForwardPushPpr::new(0.01);
let edges = vec![(0u32, 1u32, 1.0), (1, 2, 1.0), (0, 2, 1.0)];
let result = ppr.push_from(0, &edges, 3);
assert!(!result.is_empty(), "PPR should find neighbors");
}
#[test]
fn test_sparse_rips_on_line() {
let rips = SparseRipsComplex::new(0.1, 2.0);
let points: Vec<Vec<f64>> = (0..10).map(|i| vec![i as f64 * 0.3]).collect();
let edges = rips.sparse_1_skeleton(&points);
assert!(!edges.is_empty(), "Nearby points should form edges");
}
#[test]
fn test_h0_detects_components() {
let rips = SparseRipsComplex::new(0.05, 1.0);
let mut points: Vec<Vec<f64>> = (0..5).map(|i| vec![i as f64 * 0.1]).collect();
points.extend((0..5).map(|i| vec![10.0 + i as f64 * 0.1]));
let diagram = rips.compute(&points);
assert!(
!diagram.h0.is_empty(),
"Should find connected component bars"
);
}
#[test]
fn test_persistence_bar_significance() {
let bar = PersistenceBar::new(0.1, 2.5, 0);
assert!(bar.is_significant(1.0));
assert!(!bar.is_significant(3.0));
}
}