Skip to main content

ruvector_math/homology/
mod.rs

1//! Persistent Homology and Topological Data Analysis
2//!
3//! Topological methods for analyzing shape and structure in data.
4//!
5//! ## Key Capabilities
6//!
7//! - **Persistent Homology**: Track topological features (components, loops, voids)
8//! - **Betti Numbers**: Count topological features at each scale
9//! - **Persistence Diagrams**: Visualize feature lifetimes
10//! - **Bottleneck/Wasserstein Distance**: Compare topological signatures
11//!
12//! ## Integration with Mincut
13//!
14//! TDA complements mincut by providing:
15//! - Long-term drift detection (shape changes over time)
16//! - Coherence monitoring (are attention patterns stable?)
17//! - Anomaly detection (topological outliers)
18//!
19//! ## Mathematical Background
20//!
21//! Given a filtration of simplicial complexes K_0 ⊆ K_1 ⊆ ... ⊆ K_n,
22//! persistent homology tracks when features are born and die.
23//!
24//! Birth-death pairs form the persistence diagram.
25
26mod distance;
27mod filtration;
28mod persistence;
29mod simplex;
30
31pub use distance::{BottleneckDistance, WassersteinDistance};
32pub use filtration::{AlphaComplex, Filtration, VietorisRips};
33pub use persistence::{BirthDeathPair, PersistenceDiagram, PersistentHomology};
34pub use simplex::{Simplex, SimplicialComplex};
35
36/// Betti numbers at a given scale
37#[derive(Debug, Clone, PartialEq)]
38pub struct BettiNumbers {
39    /// β_0: number of connected components
40    pub b0: usize,
41    /// β_1: number of 1-cycles (loops)
42    pub b1: usize,
43    /// β_2: number of 2-cycles (voids)
44    pub b2: usize,
45}
46
47impl BettiNumbers {
48    /// Create from values
49    pub fn new(b0: usize, b1: usize, b2: usize) -> Self {
50        Self { b0, b1, b2 }
51    }
52
53    /// Total number of features
54    pub fn total(&self) -> usize {
55        self.b0 + self.b1 + self.b2
56    }
57
58    /// Euler characteristic χ = β_0 - β_1 + β_2
59    pub fn euler_characteristic(&self) -> i64 {
60        self.b0 as i64 - self.b1 as i64 + self.b2 as i64
61    }
62}
63
64/// Point in Euclidean space
65#[derive(Debug, Clone)]
66pub struct Point {
67    pub coords: Vec<f64>,
68}
69
70impl Point {
71    /// Create point from coordinates
72    pub fn new(coords: Vec<f64>) -> Self {
73        Self { coords }
74    }
75
76    /// Dimension
77    pub fn dim(&self) -> usize {
78        self.coords.len()
79    }
80
81    /// Euclidean distance to another point
82    pub fn distance(&self, other: &Point) -> f64 {
83        self.coords
84            .iter()
85            .zip(other.coords.iter())
86            .map(|(a, b)| (a - b).powi(2))
87            .sum::<f64>()
88            .sqrt()
89    }
90
91    /// Squared distance (faster)
92    pub fn distance_sq(&self, other: &Point) -> f64 {
93        self.coords
94            .iter()
95            .zip(other.coords.iter())
96            .map(|(a, b)| (a - b).powi(2))
97            .sum()
98    }
99}
100
101/// Point cloud for TDA
102#[derive(Debug, Clone)]
103pub struct PointCloud {
104    /// Points
105    pub points: Vec<Point>,
106    /// Dimension of ambient space
107    pub ambient_dim: usize,
108}
109
110impl PointCloud {
111    /// Create from points
112    pub fn new(points: Vec<Point>) -> Self {
113        let ambient_dim = points.first().map(|p| p.dim()).unwrap_or(0);
114        Self {
115            points,
116            ambient_dim,
117        }
118    }
119
120    /// Create from flat array (row-major)
121    pub fn from_flat(data: &[f64], dim: usize) -> Self {
122        let points: Vec<Point> = data
123            .chunks(dim)
124            .map(|chunk| Point::new(chunk.to_vec()))
125            .collect();
126        Self {
127            points,
128            ambient_dim: dim,
129        }
130    }
131
132    /// Number of points
133    pub fn len(&self) -> usize {
134        self.points.len()
135    }
136
137    /// Is empty?
138    pub fn is_empty(&self) -> bool {
139        self.points.is_empty()
140    }
141
142    /// Compute all pairwise distances
143    pub fn distance_matrix(&self) -> Vec<f64> {
144        let n = self.points.len();
145        let mut dist = vec![0.0; n * n];
146
147        for i in 0..n {
148            for j in i + 1..n {
149                let d = self.points[i].distance(&self.points[j]);
150                dist[i * n + j] = d;
151                dist[j * n + i] = d;
152            }
153        }
154
155        dist
156    }
157
158    /// Get bounding box
159    pub fn bounding_box(&self) -> Option<(Point, Point)> {
160        if self.points.is_empty() {
161            return None;
162        }
163
164        let dim = self.ambient_dim;
165        let mut min_coords = vec![f64::INFINITY; dim];
166        let mut max_coords = vec![f64::NEG_INFINITY; dim];
167
168        for p in &self.points {
169            for (i, &c) in p.coords.iter().enumerate() {
170                min_coords[i] = min_coords[i].min(c);
171                max_coords[i] = max_coords[i].max(c);
172            }
173        }
174
175        Some((Point::new(min_coords), Point::new(max_coords)))
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_betti_numbers() {
185        let betti = BettiNumbers::new(1, 2, 0);
186
187        assert_eq!(betti.total(), 3);
188        assert_eq!(betti.euler_characteristic(), -1);
189    }
190
191    #[test]
192    fn test_point_distance() {
193        let p1 = Point::new(vec![0.0, 0.0]);
194        let p2 = Point::new(vec![3.0, 4.0]);
195
196        assert!((p1.distance(&p2) - 5.0).abs() < 1e-10);
197    }
198
199    #[test]
200    fn test_point_cloud() {
201        let cloud = PointCloud::from_flat(&[0.0, 0.0, 1.0, 0.0, 0.0, 1.0], 2);
202
203        assert_eq!(cloud.len(), 3);
204        assert_eq!(cloud.ambient_dim, 2);
205    }
206
207    #[test]
208    fn test_distance_matrix() {
209        let cloud = PointCloud::from_flat(&[0.0, 0.0, 1.0, 0.0, 0.0, 1.0], 2);
210        let dist = cloud.distance_matrix();
211
212        assert_eq!(dist.len(), 9);
213        assert!((dist[0 * 3 + 1] - 1.0).abs() < 1e-10); // (0,0) to (1,0)
214        assert!((dist[0 * 3 + 2] - 1.0).abs() < 1e-10); // (0,0) to (0,1)
215    }
216}