1#[derive(Debug, Clone, Copy)]
14pub struct SimplexEdge {
15 pub u: u32,
16 pub v: u32,
17 pub weight: f64,
18}
19
20#[derive(Debug, Clone)]
22pub struct PersistenceBar {
23 pub birth: f64,
24 pub death: f64,
25 pub dimension: usize,
26 pub persistence: f64,
28}
29
30impl PersistenceBar {
31 pub fn new(birth: f64, death: f64, dim: usize) -> Self {
32 Self {
33 birth,
34 death,
35 dimension: dim,
36 persistence: death - birth,
37 }
38 }
39
40 pub fn is_significant(&self, threshold: f64) -> bool {
41 self.persistence > threshold
42 }
43}
44
45pub struct ForwardPushPpr {
48 pub epsilon: f64,
50 pub alpha: f64,
52}
53
54impl ForwardPushPpr {
55 pub fn new(epsilon: f64) -> Self {
56 Self {
57 epsilon,
58 alpha: 0.15,
59 }
60 }
61
62 pub fn push_from(
65 &self,
66 source: u32,
67 adjacency: &[(u32, u32, f64)], n_nodes: u32,
69 ) -> Vec<(u32, f64)> {
70 let mut ppr = vec![0.0f64; n_nodes as usize];
71 let mut residual = vec![0.0f64; n_nodes as usize];
72 residual[source as usize] = 1.0;
73
74 let mut out_edges: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n_nodes as usize];
76 let mut out_weights: Vec<f64> = vec![0.0f64; n_nodes as usize];
77 for &(u, v, w) in adjacency {
78 out_edges[u as usize].push((v, w));
79 out_edges[v as usize].push((u, w)); out_weights[u as usize] += w;
81 out_weights[v as usize] += w;
82 }
83
84 let threshold = self.epsilon;
85 let mut queue: Vec<u32> = vec![source];
86
87 let max_iters = (1.0 / self.epsilon) as usize * 2;
89 let mut iter = 0;
90 while let Some(u) = queue.first().copied() {
91 queue.remove(0);
92 iter += 1;
93 if iter > max_iters {
94 break;
95 }
96
97 let d_u = out_weights[u as usize].max(1.0);
98 let r_u = residual[u as usize];
99 if r_u < threshold * d_u {
100 continue;
101 }
102
103 ppr[u as usize] += self.alpha * r_u;
105 let push_amount = (1.0 - self.alpha) * r_u;
106 residual[u as usize] = 0.0;
107
108 let neighbors: Vec<(u32, f64)> = out_edges[u as usize].clone();
109 for (v, w) in neighbors {
110 let contribution = push_amount * w / d_u;
111 residual[v as usize] += contribution;
112 if residual[v as usize] >= threshold * out_weights[v as usize].max(1.0) {
113 if !queue.contains(&v) {
114 queue.push(v);
115 }
116 }
117 }
118 }
119
120 ppr.into_iter()
122 .enumerate()
123 .filter(|(_, p)| *p > threshold)
124 .map(|(i, p)| (i as u32, p))
125 .collect()
126 }
127}
128
129pub struct SparseRipsComplex {
131 ppr: ForwardPushPpr,
132 pub max_radius: f64,
134 pub epsilon: f64,
136}
137
138impl SparseRipsComplex {
139 pub fn new(epsilon: f64, max_radius: f64) -> Self {
140 let ppr_epsilon = (epsilon * 0.01).max(1e-4);
143 Self {
144 ppr: ForwardPushPpr::new(ppr_epsilon),
145 max_radius,
146 epsilon,
147 }
148 }
149
150 pub fn sparse_1_skeleton(&self, points: &[Vec<f64>]) -> Vec<SimplexEdge> {
153 let n = points.len() as u32;
154 let mut all_edges = Vec::new();
158 for i in 0..n {
159 for j in (i + 1)..n {
160 let dist = euclidean_dist(&points[i as usize], &points[j as usize]);
161 if dist <= self.max_radius {
162 all_edges.push((i, j, 1.0f64));
163 }
164 }
165 }
166
167 let mut selected_edges = std::collections::HashSet::new();
169 for source in 0..n {
170 let neighbors = self.ppr.push_from(source, &all_edges, n);
171 for (nbr, _) in neighbors {
172 if nbr != source {
173 let key = (source.min(nbr), source.max(nbr));
174 selected_edges.insert(key);
175 }
176 }
177 }
178
179 selected_edges
181 .into_iter()
182 .filter_map(|(u, v)| {
183 let dist = euclidean_dist(&points[u as usize], &points[v as usize]);
184 if dist <= self.max_radius {
185 Some(SimplexEdge { u, v, weight: dist })
186 } else {
187 None
188 }
189 })
190 .collect()
191 }
192
193 pub fn compute_h0(&self, n_points: usize, edges: &[SimplexEdge]) -> Vec<PersistenceBar> {
195 let mut parent: Vec<usize> = (0..n_points).collect();
197 let birth: Vec<f64> = vec![0.0; n_points];
198 let mut bars = Vec::new();
199
200 fn find(parent: &mut Vec<usize>, x: usize) -> usize {
201 if parent[x] != x {
202 parent[x] = find(parent, parent[x]);
203 }
204 parent[x]
205 }
206
207 let mut sorted_edges: Vec<&SimplexEdge> = edges.iter().collect();
209 sorted_edges.sort_unstable_by(|a, b| {
210 a.weight
211 .partial_cmp(&b.weight)
212 .unwrap_or(std::cmp::Ordering::Equal)
213 });
214
215 for edge in sorted_edges {
216 let pu = find(&mut parent, edge.u as usize);
217 let pv = find(&mut parent, edge.v as usize);
218 if pu != pv {
219 let birth_young = birth[pu].max(birth[pv]);
221 bars.push(PersistenceBar::new(birth_young, edge.weight, 0));
222 let elder = if birth[pu] <= birth[pv] { pu } else { pv };
224 let younger = if elder == pu { pv } else { pu };
225 parent[younger] = elder;
226 }
227 }
228
229 bars
230 }
231
232 pub fn compute(&self, points: &[Vec<f64>]) -> PersistenceDiagram {
234 let edges = self.sparse_1_skeleton(points);
235 let h0_bars = self.compute_h0(points.len(), &edges);
236
237 let h1_count = edges.len().saturating_sub(points.len().saturating_sub(1));
240 let h1_bars: Vec<PersistenceBar> = edges
241 .iter()
242 .take(h1_count)
243 .filter_map(|e| {
244 if e.weight < self.max_radius * 0.8 {
245 Some(PersistenceBar::new(e.weight * 0.5, e.weight, 1))
246 } else {
247 None
248 }
249 })
250 .collect();
251
252 PersistenceDiagram {
253 h0: h0_bars,
254 h1: h1_bars,
255 n_points: points.len(),
256 }
257 }
258}
259
260fn euclidean_dist(a: &[f64], b: &[f64]) -> f64 {
261 a.iter()
262 .zip(b.iter())
263 .map(|(x, y)| (x - y).powi(2))
264 .sum::<f64>()
265 .sqrt()
266}
267
268#[derive(Debug)]
269pub struct PersistenceDiagram {
270 pub h0: Vec<PersistenceBar>,
272 pub h1: Vec<PersistenceBar>,
274 pub n_points: usize,
275}
276
277impl PersistenceDiagram {
278 pub fn significant_h0(&self, threshold: f64) -> Vec<&PersistenceBar> {
279 self.h0
280 .iter()
281 .filter(|b| b.is_significant(threshold))
282 .collect()
283 }
284
285 pub fn betti_0(&self) -> usize {
286 self.h0.iter().filter(|b| b.death >= 1e9).count() + 1
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn test_ppr_push_returns_neighbors() {
297 let ppr = ForwardPushPpr::new(0.01);
298 let edges = vec![(0u32, 1u32, 1.0), (1, 2, 1.0), (0, 2, 1.0)];
300 let result = ppr.push_from(0, &edges, 3);
301 assert!(!result.is_empty(), "PPR should find neighbors");
302 }
303
304 #[test]
305 fn test_sparse_rips_on_line() {
306 let rips = SparseRipsComplex::new(0.1, 2.0);
307 let points: Vec<Vec<f64>> = (0..10).map(|i| vec![i as f64 * 0.3]).collect();
308 let edges = rips.sparse_1_skeleton(&points);
309 assert!(!edges.is_empty(), "Nearby points should form edges");
310 }
311
312 #[test]
313 fn test_h0_detects_components() {
314 let rips = SparseRipsComplex::new(0.05, 1.0);
315 let mut points: Vec<Vec<f64>> = (0..5).map(|i| vec![i as f64 * 0.1]).collect();
317 points.extend((0..5).map(|i| vec![10.0 + i as f64 * 0.1]));
318 let diagram = rips.compute(&points);
319 assert!(
321 !diagram.h0.is_empty(),
322 "Should find connected component bars"
323 );
324 }
325
326 #[test]
327 fn test_persistence_bar_significance() {
328 let bar = PersistenceBar::new(0.1, 2.5, 0);
329 assert!(bar.is_significant(1.0));
330 assert!(!bar.is_significant(3.0));
331 }
332}