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 simplex;
27mod filtration;
28mod persistence;
29mod distance;
30
31pub use simplex::{Simplex, SimplicialComplex};
32pub use filtration::{Filtration, VietorisRips, AlphaComplex};
33pub use persistence::{PersistenceDiagram, PersistentHomology, BirthDeathPair};
34pub use distance::{BottleneckDistance, WassersteinDistance};
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 { points, ambient_dim }
115    }
116
117    /// Create from flat array (row-major)
118    pub fn from_flat(data: &[f64], dim: usize) -> Self {
119        let points: Vec<Point> = data
120            .chunks(dim)
121            .map(|chunk| Point::new(chunk.to_vec()))
122            .collect();
123        Self { points, ambient_dim: dim }
124    }
125
126    /// Number of points
127    pub fn len(&self) -> usize {
128        self.points.len()
129    }
130
131    /// Is empty?
132    pub fn is_empty(&self) -> bool {
133        self.points.is_empty()
134    }
135
136    /// Compute all pairwise distances
137    pub fn distance_matrix(&self) -> Vec<f64> {
138        let n = self.points.len();
139        let mut dist = vec![0.0; n * n];
140
141        for i in 0..n {
142            for j in i + 1..n {
143                let d = self.points[i].distance(&self.points[j]);
144                dist[i * n + j] = d;
145                dist[j * n + i] = d;
146            }
147        }
148
149        dist
150    }
151
152    /// Get bounding box
153    pub fn bounding_box(&self) -> Option<(Point, Point)> {
154        if self.points.is_empty() {
155            return None;
156        }
157
158        let dim = self.ambient_dim;
159        let mut min_coords = vec![f64::INFINITY; dim];
160        let mut max_coords = vec![f64::NEG_INFINITY; dim];
161
162        for p in &self.points {
163            for (i, &c) in p.coords.iter().enumerate() {
164                min_coords[i] = min_coords[i].min(c);
165                max_coords[i] = max_coords[i].max(c);
166            }
167        }
168
169        Some((Point::new(min_coords), Point::new(max_coords)))
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_betti_numbers() {
179        let betti = BettiNumbers::new(1, 2, 0);
180
181        assert_eq!(betti.total(), 3);
182        assert_eq!(betti.euler_characteristic(), -1);
183    }
184
185    #[test]
186    fn test_point_distance() {
187        let p1 = Point::new(vec![0.0, 0.0]);
188        let p2 = Point::new(vec![3.0, 4.0]);
189
190        assert!((p1.distance(&p2) - 5.0).abs() < 1e-10);
191    }
192
193    #[test]
194    fn test_point_cloud() {
195        let cloud = PointCloud::from_flat(&[0.0, 0.0, 1.0, 0.0, 0.0, 1.0], 2);
196
197        assert_eq!(cloud.len(), 3);
198        assert_eq!(cloud.ambient_dim, 2);
199    }
200
201    #[test]
202    fn test_distance_matrix() {
203        let cloud = PointCloud::from_flat(&[0.0, 0.0, 1.0, 0.0, 0.0, 1.0], 2);
204        let dist = cloud.distance_matrix();
205
206        assert_eq!(dist.len(), 9);
207        assert!((dist[0 * 3 + 1] - 1.0).abs() < 1e-10); // (0,0) to (1,0)
208        assert!((dist[0 * 3 + 2] - 1.0).abs() < 1e-10); // (0,0) to (0,1)
209    }
210}