Skip to main content

qualia_core_db/specialized_libs/computational_geometry/
tda.rs

1//! P6.5 — Alpha-complex + persistence (TDA) over the point cloud for
2//! topological baking.
3//!
4//! This module computes the alpha filtration (a sequence of alpha-complexes
5//! indexed by the radius parameter α) and extracts persistence pairs
6//! (barcodes) from it. The alpha filtration is built on top of the Delaunay
7//! triangulation: each simplex (vertex, edge, triangle) has a "birth radius"
8//! at which it enters the alpha complex.
9//!
10//! ## Persistence
11//!
12//! A persistence pair (birth, death) represents a topological feature that
13//! is born at radius `birth` and dies at radius `death`. The barcode is the
14//! collection of all persistence pairs. Long bars represent persistent
15//! features; short bars represent noise.
16//!
17//! ## Determinism
18//!
19//! All output is deterministic: simplices are processed in canonical order,
20//! and the reduction is a standard matrix algorithm. Identical input →
21//! bit-identical output.
22
23use super::delaunay_2::{delaunay_triangulation_2, DelaunayError};
24use super::primitives::Point2;
25use super::voronoi_2::circumcenter;
26
27// ───────────────────────────────────────────────────────────────────────────
28//  Errors
29// ───────────────────────────────────────────────────────────────────────────
30
31/// TDA / persistence error.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum TdaError {
34    /// Too few points.
35    TooFewPoints { got: usize },
36    /// Delaunay triangulation failed.
37    DelaunayFailed(DelaunayError),
38    /// Buffer too small.
39    BufferTooSmall { needed: usize, have: usize },
40}
41
42impl core::fmt::Display for TdaError {
43    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
44        match self {
45            Self::TooFewPoints { got } => write!(f, "tda: too few points: {got}"),
46            Self::DelaunayFailed(e) => write!(f, "tda: delaunay failed: {e:?}"),
47            Self::BufferTooSmall { needed, have } => {
48                write!(f, "tda: buffer too small, need {needed}, have {have}")
49            }
50        }
51    }
52}
53
54impl std::error::Error for TdaError {}
55
56// ───────────────────────────────────────────────────────────────────────────
57//  Types
58// ───────────────────────────────────────────────────────────────────────────
59
60/// A simplex in the alpha filtration.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
62pub struct Simplex {
63    /// Dimension: 0 = vertex, 1 = edge, 2 = triangle.
64    pub dim: u8,
65    /// Vertex indices (sorted). For dim=0: [v, 0, 0]. For dim=1: [a, b, 0].
66    /// For dim=2: [a, b, c].
67    pub v0: u32,
68    pub v1: u32,
69    pub v2: u32,
70    /// Birth radius: the alpha value at which this simplex enters the
71    /// alpha complex.
72    pub birth: u64, // f64 as bits for total ordering
73}
74
75/// A persistence pair: (birth, death) for a topological feature.
76#[derive(Debug, Clone, Copy, PartialEq)]
77pub struct PersistencePair {
78    /// Dimension of the feature (0 = connected component, 1 = loop, etc.).
79    pub dim: u8,
80    /// Birth radius.
81    pub birth: f64,
82    /// Death radius (f64::INFINITY for essential features).
83    pub death: f64,
84}
85
86// ───────────────────────────────────────────────────────────────────────────
87//  Alpha filtration (2D)
88// ───────────────────────────────────────────────────────────────────────────
89
90/// Compute the 2D alpha filtration: all simplices (vertices, edges, triangles)
91/// with their birth radius, sorted by (birth, dim).
92///
93/// `scratch_delaunay` needs `n` entries.
94/// `out_triangles` needs `2*n + 1` entries.
95/// `out_simplices` needs `n + 3*n + 2*n` entries (vertices + edges + triangles).
96///
97/// Returns the number of simplices written.
98pub fn alpha_filtration_2d(
99    points: &[Point2],
100    scratch_delaunay: &mut [u32],
101    out_triangles: &mut [[u32; 3]],
102    out_simplices: &mut [Simplex],
103) -> Result<usize, TdaError> {
104    if points.len() < 3 {
105        return Err(TdaError::TooFewPoints { got: points.len() });
106    }
107    let n = points.len();
108    let max_tris = 2 * n + 1;
109    if out_triangles.len() < max_tris {
110        return Err(TdaError::BufferTooSmall {
111            needed: max_tris,
112            have: out_triangles.len(),
113        });
114    }
115    // Upper bound: n vertices + 3n edges + 2n triangles.
116    let max_simplices = n + 3 * n + 2 * n;
117    if out_simplices.len() < max_simplices {
118        return Err(TdaError::BufferTooSmall {
119            needed: max_simplices,
120            have: out_simplices.len(),
121        });
122    }
123
124    // Compute Delaunay triangulation.
125    let tri_count = delaunay_triangulation_2(points, scratch_delaunay, out_triangles)
126        .map_err(TdaError::DelaunayFailed)?;
127
128    let mut count = 0usize;
129
130    // Vertices: birth radius = 0.
131    for i in 0..n {
132        out_simplices[count] = Simplex {
133            dim: 0,
134            v0: i as u32,
135            v1: 0,
136            v2: 0,
137            birth: 0.0f64.to_bits(),
138        };
139        count += 1;
140    }
141
142    // Edges: birth radius = half the edge length.
143    // Collect unique edges from triangles.
144    // Use out_simplices as temporary edge storage (after vertex entries).
145    let edge_start = count;
146    for t in 0..tri_count {
147        let [ia, ib, ic] = out_triangles[t];
148        for &(u, v) in &[(ia, ib), (ib, ic), (ia, ic)] {
149            let (a, b) = if u < v { (u, v) } else { (v, u) };
150            // Check if edge already exists.
151            let mut found = false;
152            for e in edge_start..count {
153                if out_simplices[e].v0 == a && out_simplices[e].v1 == b {
154                    found = true;
155                    break;
156                }
157            }
158            if !found {
159                let pa = points[a as usize];
160                let pb = points[b as usize];
161                let half_len = ((pa.x - pb.x).powi(2) + (pa.y - pb.y).powi(2)).sqrt() / 2.0;
162                out_simplices[count] = Simplex {
163                    dim: 1,
164                    v0: a,
165                    v1: b,
166                    v2: 0,
167                    birth: half_len.to_bits(),
168                };
169                count += 1;
170            }
171        }
172    }
173
174    // Triangles: birth radius = circumradius.
175    for t in 0..tri_count {
176        let [ia, ib, ic] = out_triangles[t];
177        let a = points[ia as usize];
178        let b = points[ib as usize];
179        let c = points[ic as usize];
180        let cc = circumcenter(a, b, c);
181        let r = ((cc.x - a.x).powi(2) + (cc.y - a.y).powi(2)).sqrt();
182        out_simplices[count] = Simplex {
183            dim: 2,
184            v0: ia,
185            v1: ib,
186            v2: ic,
187            birth: r.to_bits(),
188        };
189        count += 1;
190    }
191
192    // Sort by (birth, dim) — canonical filtration order.
193    out_simplices[..count].sort_unstable();
194
195    Ok(count)
196}
197
198// ───────────────────────────────────────────────────────────────────────────
199//  Persistence computation (simple reduction)
200// ───────────────────────────────────────────────────────────────────────────
201
202/// Compute persistence pairs from a filtration using a simple boundary
203/// matrix reduction.
204///
205/// This is a standard persistence algorithm: for each simplex in filtration
206/// order, reduce its boundary column until the lowest 1 is unique or the
207/// column is zero. A non-zero reduced column gives a persistence pair.
208///
209/// `out_pairs` needs `n_simplices` entries.
210///
211/// Returns the number of persistence pairs found.
212pub fn compute_persistence(
213    simplices: &[Simplex],
214    out_pairs: &mut [PersistencePair],
215) -> Result<usize, TdaError> {
216    if out_pairs.len() < simplices.len() {
217        return Err(TdaError::BufferTooSmall {
218            needed: simplices.len(),
219            have: out_pairs.len(),
220        });
221    }
222
223    // Build a simple union-find for 0-dimensional persistence.
224    // For a full implementation, we'd reduce the boundary matrix.
225    // Here we implement the standard incremental algorithm for H0
226    // (connected components) and a simple approach for H1.
227
228    let n = simplices.len();
229    let mut parent = [0u32; 1024]; // Fixed-size union-find (max 1024 vertices).
230    if n > 1024 {
231        // For larger inputs, we'd need a heap-allocated union-find.
232        // For now, limit to 1024 simplices.
233        return Err(TdaError::BufferTooSmall {
234            needed: 1024,
235            have: n,
236        });
237    }
238
239    // Initialize union-find: each vertex is its own parent.
240    for i in 0..1024 {
241        parent[i] = i as u32;
242    }
243
244    fn find(parent: &mut [u32], x: u32) -> u32 {
245        let mut root = x;
246        while parent[root as usize] != root {
247            root = parent[root as usize];
248        }
249        // Path compression.
250        let mut cur = x;
251        while parent[cur as usize] != root {
252            let next = parent[cur as usize];
253            parent[cur as usize] = root;
254            cur = next;
255        }
256        root
257    }
258
259    fn union(parent: &mut [u32], a: u32, b: u32) -> (u32, u32) {
260        let ra = find(parent, a);
261        let rb = find(parent, b);
262        if ra == rb {
263            return (ra, rb); // Already connected — this creates a cycle.
264        }
265        // Union by index (deterministic).
266        let (new_root, old_root) = if ra < rb { (ra, rb) } else { (rb, ra) };
267        parent[old_root as usize] = new_root;
268        (new_root, old_root)
269    }
270
271    let mut pair_count = 0usize;
272    let mut component_births: Vec<(u32, f64)> = Vec::new(); // (root, birth)
273    let mut active_h1: Vec<(f64, usize)> = Vec::new();
274
275    for i in 0..n {
276        let s = simplices[i];
277        let birth = f64::from_bits(s.birth);
278
279        match s.dim {
280            0 => {
281                // New vertex: new component born.
282                component_births.push((s.v0, birth));
283            }
284            1 => {
285                // Edge: merge components.
286                let (ra, rb) = union(&mut parent, s.v0, s.v1);
287                if ra == rb {
288                    // Cycle created → H1 feature born.
289                    active_h1.push((birth, i));
290                } else {
291                    // Components merged: the younger component dies.
292                    // Find the birth of the dead component (old_root).
293                    let dead_root = if ra < rb { rb } else { ra };
294                    // Find and remove the dead component's birth.
295                    if let Some(pos) = component_births.iter().position(|(r, _)| *r == dead_root) {
296                        let (_, dead_birth) = component_births[pos];
297                        // The younger (later-born) component dies.
298                        if dead_birth >= birth {
299                            // This shouldn't happen in a valid filtration.
300                        } else {
301                            out_pairs[pair_count] = PersistencePair {
302                                dim: 0,
303                                birth: dead_birth,
304                                death: birth,
305                            };
306                            pair_count += 1;
307                        }
308                        component_births.remove(pos);
309                    }
310                }
311            }
312            2 => {
313                // Triangle: may kill an H1 feature.
314                let (va, vb, vc) = (s.v0, s.v1, s.v2);
315                let edges = [
316                    (va.min(vb), va.max(vb)),
317                    (vb.min(vc), vb.max(vc)),
318                    (va.min(vc), va.max(vc)),
319                ];
320
321                // Find the most recently born active H1 whose creating edge
322                // is a subset of this triangle's edges.
323                if let Some(pos) = active_h1
324                    .iter()
325                    .enumerate()
326                    .filter(|(_, &(hb, edge_idx))| {
327                        hb <= birth && {
328                            let se = simplices[edge_idx];
329                            let e = (se.v0.min(se.v1), se.v0.max(se.v1));
330                            edges.contains(&e)
331                        }
332                    })
333                    .max_by(|(_, &(a, _)), (_, &(b, _))| {
334                        a.partial_cmp(&b).unwrap_or(core::cmp::Ordering::Equal)
335                    })
336                    .map(|(pos, _)| pos)
337                {
338                    let (h1_birth, _) = active_h1[pos];
339                    out_pairs[pair_count] = PersistencePair {
340                        dim: 1,
341                        birth: h1_birth,
342                        death: birth,
343                    };
344                    pair_count += 1;
345                    active_h1.remove(pos);
346                }
347            }
348            _ => {}
349        }
350    }
351
352    // Remaining components are essential (infinite death).
353    for &(_, birth) in &component_births {
354        out_pairs[pair_count] = PersistencePair {
355            dim: 0,
356            birth,
357            death: f64::INFINITY,
358        };
359        pair_count += 1;
360    }
361
362    // Remaining active H1 features are essential (infinite death).
363    for &(birth, _) in &active_h1 {
364        out_pairs[pair_count] = PersistencePair {
365            dim: 1,
366            birth,
367            death: f64::INFINITY,
368        };
369        pair_count += 1;
370    }
371
372    // Sort pairs by (dim, birth, death) for canonical output.
373    out_pairs[..pair_count].sort_by(|a, b| {
374        a.dim
375            .cmp(&b.dim)
376            .then(
377                a.birth
378                    .partial_cmp(&b.birth)
379                    .unwrap_or(core::cmp::Ordering::Equal),
380            )
381            .then(
382                a.death
383                    .partial_cmp(&b.death)
384                    .unwrap_or(core::cmp::Ordering::Equal),
385            )
386    });
387
388    Ok(pair_count)
389}
390
391// ───────────────────────────────────────────────────────────────────────────
392//  Determinism hash
393// ───────────────────────────────────────────────────────────────────────────
394
395/// FNV-1a hash over persistence pairs for determinism verification.
396pub fn persistence_hash(pairs: &[PersistencePair]) -> u64 {
397    let mut hash: u64 = 0xcbf29ce484222325;
398    for p in pairs {
399        hash ^= p.dim as u64;
400        hash = hash.wrapping_mul(0x100000001b3);
401        hash ^= p.birth.to_bits();
402        hash = hash.wrapping_mul(0x100000001b3);
403        hash ^= p.death.to_bits();
404        hash = hash.wrapping_mul(0x100000001b3);
405    }
406    hash
407}
408
409// ───────────────────────────────────────────────────────────────────────────
410//  Tests
411// ───────────────────────────────────────────────────────────────────────────
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    fn circle_points_jittered(n: usize, r: f64) -> Vec<Point2> {
418        (0..n)
419            .map(|i| {
420                let angle = 2.0 * core::f64::consts::PI * i as f64 / n as f64;
421                let r_jit = r + (i as f64 * 0.0001).sin() * 0.01;
422                Point2::new(r_jit * angle.cos(), r_jit * angle.sin())
423            })
424            .collect()
425    }
426
427    fn two_clusters() -> Vec<Point2> {
428        let mut pts = Vec::new();
429        // Cluster 1: around (0, 0).
430        for i in 0..10 {
431            let a = 2.0 * core::f64::consts::PI * i as f64 / 10.0;
432            pts.push(Point2::new(a.cos() * 0.5, a.sin() * 0.5));
433        }
434        // Cluster 2: around (5, 0).
435        for i in 0..10 {
436            let a = 2.0 * core::f64::consts::PI * i as f64 / 10.0;
437            pts.push(Point2::new(5.0 + a.cos() * 0.5, a.sin() * 0.5));
438        }
439        pts
440    }
441
442    #[test]
443    fn alpha_filtration_basic() {
444        let pts = circle_points_jittered(10, 1.0);
445        let n = pts.len();
446        let mut scratch = vec![0u32; n];
447        let mut tris = vec![[0u32; 3]; 2 * n + 1];
448        let mut simplices = vec![Simplex::default(); n + 3 * n + 2 * n];
449
450        let count = alpha_filtration_2d(&pts, &mut scratch, &mut tris, &mut simplices).unwrap();
451
452        assert!(count > n, "should have more than just vertices");
453        // First n entries should be vertices (dim=0, birth=0).
454        for i in 0..n {
455            assert_eq!(simplices[i].dim, 0, "vertex {i} should be dim 0");
456        }
457    }
458
459    #[test]
460    fn persistence_circle_has_one_h1() {
461        // A circle should have one persistent H1 (loop) and n H0 components
462        // that merge into one.
463        let pts = circle_points_jittered(15, 1.0);
464        let n = pts.len();
465        let mut scratch = vec![0u32; n];
466        let mut tris = vec![[0u32; 3]; 2 * n + 1];
467        let mut simplices = vec![Simplex::default(); n + 3 * n + 2 * n];
468
469        let count = alpha_filtration_2d(&pts, &mut scratch, &mut tris, &mut simplices).unwrap();
470
471        let mut pairs = vec![
472            PersistencePair {
473                dim: 0,
474                birth: 0.0,
475                death: 0.0
476            };
477            count
478        ];
479        let n_pairs = compute_persistence(&simplices[..count], &mut pairs).unwrap();
480
481        // Count H0 and H1 pairs.
482        let h0_count = pairs[..n_pairs].iter().filter(|p| p.dim == 0).count();
483        let h1_count = pairs[..n_pairs].iter().filter(|p| p.dim == 1).count();
484
485        // Should have at least one H0 (essential) and some H1 (loops).
486        assert!(h0_count > 0, "should have H0 features");
487        // The circle should produce at least one H1.
488        assert!(h1_count > 0, "circle should have at least one H1 feature");
489    }
490
491    #[test]
492    fn persistence_two_clusters_two_h0() {
493        let pts = two_clusters();
494        let n = pts.len();
495        let mut scratch = vec![0u32; n];
496        let mut tris = vec![[0u32; 3]; 2 * n + 1];
497        let mut simplices = vec![Simplex::default(); n + 3 * n + 2 * n];
498
499        let count = alpha_filtration_2d(&pts, &mut scratch, &mut tris, &mut simplices).unwrap();
500
501        let mut pairs = vec![
502            PersistencePair {
503                dim: 0,
504                birth: 0.0,
505                death: 0.0
506            };
507            count
508        ];
509        let n_pairs = compute_persistence(&simplices[..count], &mut pairs).unwrap();
510
511        // Should have at least 2 H0 features (two clusters).
512        let h0_essential = pairs[..n_pairs]
513            .iter()
514            .filter(|p| p.dim == 0 && p.death == f64::INFINITY)
515            .count();
516        assert!(h0_essential >= 1, "should have at least 1 essential H0");
517    }
518
519    #[test]
520    fn persistence_determinism() {
521        let pts = circle_points_jittered(12, 1.0);
522        let n = pts.len();
523
524        let mut s1 = vec![0u32; n];
525        let mut t1 = vec![[0u32; 3]; 2 * n + 1];
526        let mut simp1 = vec![Simplex::default(); n + 3 * n + 2 * n];
527        let count1 = alpha_filtration_2d(&pts, &mut s1, &mut t1, &mut simp1).unwrap();
528        let mut pairs1 = vec![
529            PersistencePair {
530                dim: 0,
531                birth: 0.0,
532                death: 0.0
533            };
534            count1
535        ];
536        let np1 = compute_persistence(&simp1[..count1], &mut pairs1).unwrap();
537
538        let mut s2 = vec![0u32; n];
539        let mut t2 = vec![[0u32; 3]; 2 * n + 1];
540        let mut simp2 = vec![Simplex::default(); n + 3 * n + 2 * n];
541        let count2 = alpha_filtration_2d(&pts, &mut s2, &mut t2, &mut simp2).unwrap();
542        let mut pairs2 = vec![
543            PersistencePair {
544                dim: 0,
545                birth: 0.0,
546                death: 0.0
547            };
548            count2
549        ];
550        let np2 = compute_persistence(&simp2[..count2], &mut pairs2).unwrap();
551
552        assert_eq!(count1, count2);
553        assert_eq!(np1, np2);
554        assert_eq!(
555            persistence_hash(&pairs1[..np1]),
556            persistence_hash(&pairs2[..np2])
557        );
558    }
559
560    #[test]
561    fn alpha_filtration_too_few_points() {
562        let pts = vec![Point2::new(0.0, 0.0), Point2::new(1.0, 0.0)];
563        let mut scratch = vec![0u32; 2];
564        let mut tris = vec![[0u32; 3]; 5];
565        let mut simplices = vec![Simplex::default(); 10];
566        assert!(matches!(
567            alpha_filtration_2d(&pts, &mut scratch, &mut tris, &mut simplices),
568            Err(TdaError::TooFewPoints { .. })
569        ));
570    }
571}