Skip to main content

oxiphysics_geometry/
topology_geometry.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3//! Computational topology for geometry: persistent homology, Reeb graphs.
4//!
5//! This module provides tools for analyzing the topological structure
6//! of geometric data, including:
7//!
8//! - Simplicial complexes ([`SimplicialComplex`])
9//! - Persistent homology ([`PersistentHomology`])
10//! - Reeb graphs ([`ReebGraph`])
11//! - Morse complexes ([`MorseComplex`])
12//! - Alpha shapes ([`AlphaComplex`])
13//! - Cubical complexes ([`CheckerboardComplex`])
14//! - Betti numbers ([`BettiNumbers`])
15//! - Topological noise removal ([`TopologicalNoise`])
16
17/// Simplicial complex: vertices, edges, triangles, tetrahedra.
18///
19/// A simplicial complex is a set of simplices that satisfies the condition
20/// that every face of a simplex is also a simplex in the complex.
21#[derive(Debug, Clone)]
22pub struct SimplicialComplex {
23    /// Vertices (0-simplices) stored as \[x, y, z\]
24    pub vertices: Vec<[f64; 3]>,
25    /// Edges (1-simplices) as pairs of vertex indices
26    pub edges: Vec<[usize; 2]>,
27    /// Triangles (2-simplices) as triples of vertex indices
28    pub triangles: Vec<[usize; 3]>,
29    /// Tetrahedra (3-simplices) as quadruples of vertex indices
30    pub tetrahedra: Vec<[usize; 4]>,
31}
32
33impl SimplicialComplex {
34    /// Creates an empty simplicial complex.
35    pub fn new() -> Self {
36        Self {
37            vertices: Vec::new(),
38            edges: Vec::new(),
39            triangles: Vec::new(),
40            tetrahedra: Vec::new(),
41        }
42    }
43
44    /// Adds a vertex and returns its index.
45    pub fn add_vertex(&mut self, pos: [f64; 3]) -> usize {
46        let idx = self.vertices.len();
47        self.vertices.push(pos);
48        idx
49    }
50
51    /// Adds an edge if it is not already present.
52    pub fn add_edge(&mut self, i: usize, j: usize) {
53        let e = if i < j { [i, j] } else { [j, i] };
54        if !self.edges.contains(&e) {
55            self.edges.push(e);
56        }
57    }
58
59    /// Adds a triangle.
60    pub fn add_triangle(&mut self, i: usize, j: usize, k: usize) {
61        let mut t = [i, j, k];
62        t.sort_unstable();
63        if !self.triangles.contains(&t) {
64            self.triangles.push(t);
65        }
66    }
67
68    /// Adds a tetrahedron.
69    pub fn add_tetrahedron(&mut self, i: usize, j: usize, k: usize, l: usize) {
70        let mut t = [i, j, k, l];
71        t.sort_unstable();
72        if !self.tetrahedra.contains(&t) {
73            self.tetrahedra.push(t);
74        }
75    }
76
77    /// Computes the Euler characteristic χ = V - E + F - T.
78    pub fn euler_characteristic(&self) -> i64 {
79        self.vertices.len() as i64 - self.edges.len() as i64 + self.triangles.len() as i64
80            - self.tetrahedra.len() as i64
81    }
82
83    /// Returns the number of vertices V.
84    pub fn n_vertices(&self) -> usize {
85        self.vertices.len()
86    }
87
88    /// Returns the number of edges E.
89    pub fn n_edges(&self) -> usize {
90        self.edges.len()
91    }
92
93    /// Returns the number of triangles F.
94    pub fn n_triangles(&self) -> usize {
95        self.triangles.len()
96    }
97
98    /// Returns the number of tetrahedra T.
99    pub fn n_tetrahedra(&self) -> usize {
100        self.tetrahedra.len()
101    }
102
103    /// Builds the boundary operator ∂_1: edges → vertices as incidence matrix.
104    ///
105    /// Returns a |V| × |E| matrix (flat row-major).
106    pub fn boundary_1(&self) -> Vec<i32> {
107        let nv = self.vertices.len();
108        let ne = self.edges.len();
109        let mut b = vec![0_i32; nv * ne];
110        for (j, &[i0, i1]) in self.edges.iter().enumerate() {
111            b[i0 * ne + j] = -1;
112            b[i1 * ne + j] = 1;
113        }
114        b
115    }
116
117    /// Builds the boundary operator ∂_2: triangles → edges.
118    ///
119    /// Returns a |E| × |F| matrix (flat row-major).
120    pub fn boundary_2(&self) -> Vec<i32> {
121        let ne = self.edges.len();
122        let nf = self.triangles.len();
123        let mut b = vec![0_i32; ne * nf];
124        for (j, &[a, b_idx, c]) in self.triangles.iter().enumerate() {
125            // Edges of triangle: (a,b), (b,c), (a,c)
126            let tri_edges = [[a, b_idx], [b_idx, c], [a, c]];
127            let signs = [1_i32, 1, -1];
128            for (k, &e) in tri_edges.iter().enumerate() {
129                let es = if e[0] < e[1] {
130                    [e[0], e[1]]
131                } else {
132                    [e[1], e[0]]
133                };
134                if let Some(ei) = self.edges.iter().position(|&x| x == es) {
135                    b[ei * nf + j] = signs[k];
136                }
137            }
138        }
139        b
140    }
141
142    /// Creates the simplicial complex for a triangulated sphere (icosphere-like).
143    ///
144    /// Uses an octahedral approximation: 6 vertices, 12 edges, 8 triangles.
145    /// Euler characteristic = 6 - 12 + 8 = 2.
146    pub fn octahedral_sphere() -> Self {
147        let mut sc = Self::new();
148        // 6 vertices of octahedron
149        sc.add_vertex([1.0, 0.0, 0.0]);
150        sc.add_vertex([-1.0, 0.0, 0.0]);
151        sc.add_vertex([0.0, 1.0, 0.0]);
152        sc.add_vertex([0.0, -1.0, 0.0]);
153        sc.add_vertex([0.0, 0.0, 1.0]);
154        sc.add_vertex([0.0, 0.0, -1.0]);
155        // 12 edges
156        let edges = [
157            [0, 2],
158            [0, 3],
159            [0, 4],
160            [0, 5],
161            [1, 2],
162            [1, 3],
163            [1, 4],
164            [1, 5],
165            [2, 4],
166            [2, 5],
167            [3, 4],
168            [3, 5],
169        ];
170        for [i, j] in edges {
171            sc.add_edge(i, j);
172        }
173        // 8 triangular faces
174        sc.add_triangle(0, 2, 4);
175        sc.add_triangle(0, 2, 5);
176        sc.add_triangle(0, 3, 4);
177        sc.add_triangle(0, 3, 5);
178        sc.add_triangle(1, 2, 4);
179        sc.add_triangle(1, 2, 5);
180        sc.add_triangle(1, 3, 4);
181        sc.add_triangle(1, 3, 5);
182        sc
183    }
184
185    /// Creates a torus triangulation with Euler characteristic 0.
186    ///
187    /// Minimal triangulation: V=7, E=21, F=14, χ=0.
188    /// Uses the complete graph K_7 triangulation of the torus.
189    pub fn minimal_torus() -> Self {
190        let mut sc = Self::new();
191        for i in 0..7 {
192            let angle = 2.0 * std::f64::consts::PI * i as f64 / 7.0;
193            sc.add_vertex([angle.cos(), angle.sin(), 0.0]);
194        }
195        // Minimal 7-vertex triangulation of torus (K_7 embedding):
196        // V=7, E=21, F=14, χ=0
197        let tris: [[usize; 3]; 14] = [
198            [0, 1, 2],
199            [0, 2, 3],
200            [0, 3, 4],
201            [0, 4, 5],
202            [0, 5, 6],
203            [0, 6, 1],
204            [1, 3, 2],
205            [1, 4, 3],
206            [1, 5, 4],
207            [1, 6, 5],
208            [2, 6, 3],
209            [3, 5, 6],
210            [2, 4, 6],
211            [2, 5, 4],
212        ];
213        for [a, b, c] in tris {
214            sc.add_edge(a, b);
215            sc.add_edge(b, c);
216            sc.add_edge(a, c);
217            sc.add_triangle(a, b, c);
218        }
219        sc
220    }
221}
222
223impl Default for SimplicialComplex {
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229/// A birth-death pair in a persistence diagram.
230#[derive(Debug, Clone, Copy, PartialEq)]
231pub struct BirthDeathPair {
232    /// Birth value (filtration parameter)
233    pub birth: f64,
234    /// Death value (filtration parameter), f64::INFINITY for essential classes
235    pub death: f64,
236    /// Homological dimension
237    pub dim: usize,
238}
239
240impl BirthDeathPair {
241    /// Computes persistence = death - birth.
242    pub fn persistence(&self) -> f64 {
243        self.death - self.birth
244    }
245
246    /// Returns true if this is an essential class (death = ∞).
247    pub fn is_essential(&self) -> bool {
248        self.death.is_infinite()
249    }
250}
251
252/// Persistent homology computation for filtrations.
253///
254/// Computes birth-death pairs (persistence diagram) for 0-dim and 1-dim
255/// topological features (connected components and loops).
256#[derive(Debug, Clone)]
257pub struct PersistentHomology {
258    /// Computed birth-death pairs
259    pub pairs: Vec<BirthDeathPair>,
260}
261
262impl PersistentHomology {
263    /// Creates an empty persistent homology.
264    pub fn new() -> Self {
265        Self { pairs: Vec::new() }
266    }
267
268    /// Computes 0-dimensional persistent homology from a point cloud.
269    ///
270    /// Uses union-find (single-linkage clustering) to track connected components.
271    /// Each merge event creates a birth-death pair.
272    ///
273    /// # Arguments
274    /// * `points` - Point cloud as \[x,y,z\] array
275    /// * `filtration_values` - Per-edge filtration values (distances)
276    pub fn compute_0d(points: &[[f64; 3]]) -> Self {
277        let n = points.len();
278        if n == 0 {
279            return Self::new();
280        }
281        // Build all pairwise distances and sort
282        let mut edges: Vec<(f64, usize, usize)> = Vec::new();
283        for i in 0..n {
284            for j in i + 1..n {
285                let d = dist3(&points[i], &points[j]);
286                edges.push((d, i, j));
287            }
288        }
289        edges.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
290
291        let mut uf = UnionFind::new(n);
292        let mut pairs = Vec::new();
293
294        // Initially all components born at 0
295        for _ in 0..n {
296            // Each point is born at 0
297        }
298
299        for (dist, i, j) in edges {
300            let ri = uf.find(i);
301            let rj = uf.find(j);
302            if ri != rj {
303                // Merge: the younger component dies
304                let born_i = 0.0_f64;
305                let born_j = 0.0_f64;
306                let dying = if born_i >= born_j { ri } else { rj };
307                let _ = dying; // used for tracking
308                pairs.push(BirthDeathPair {
309                    birth: 0.0,
310                    death: dist,
311                    dim: 0,
312                });
313                uf.union(i, j);
314            }
315        }
316        // Essential class: the last surviving component
317        pairs.push(BirthDeathPair {
318            birth: 0.0,
319            death: f64::INFINITY,
320            dim: 0,
321        });
322
323        Self { pairs }
324    }
325
326    /// Returns all pairs in the persistence diagram.
327    pub fn diagram(&self) -> &[BirthDeathPair] {
328        &self.pairs
329    }
330
331    /// Returns the number of features with persistence > threshold.
332    ///
333    /// # Arguments
334    /// * `threshold` - Minimum persistence to count as a feature
335    pub fn significant_features(&self, threshold: f64) -> usize {
336        self.pairs
337            .iter()
338            .filter(|p| p.persistence() > threshold)
339            .count()
340    }
341
342    /// Computes the bottleneck distance between two persistence diagrams.
343    ///
344    /// d_B = inf_γ sup_{x ∈ D1} ||x - γ(x)||_∞
345    ///
346    /// Uses a greedy approximation.
347    ///
348    /// # Arguments
349    /// * `other` - Other persistence diagram
350    pub fn bottleneck_distance(&self, other: &Self) -> f64 {
351        // Filter to finite pairs
352        let d1: Vec<_> = self.pairs.iter().filter(|p| p.death.is_finite()).collect();
353        let d2: Vec<_> = other.pairs.iter().filter(|p| p.death.is_finite()).collect();
354
355        if d1.is_empty() && d2.is_empty() {
356            return 0.0;
357        }
358
359        let mut max_cost = 0.0_f64;
360
361        // For each point in d1, find closest in d2 or diagonal
362        for p1 in &d1 {
363            let diag_dist = (p1.death - p1.birth) / 2.0;
364            let min_d2 = d2
365                .iter()
366                .map(|p2| (p1.birth - p2.birth).abs().max((p1.death - p2.death).abs()))
367                .fold(f64::INFINITY, f64::min);
368            max_cost = max_cost.max(min_d2.min(diag_dist));
369        }
370
371        for p2 in &d2 {
372            let diag_dist = (p2.death - p2.birth) / 2.0;
373            let min_d1 = d1
374                .iter()
375                .map(|p1| (p1.birth - p2.birth).abs().max((p1.death - p2.death).abs()))
376                .fold(f64::INFINITY, f64::min);
377            max_cost = max_cost.max(min_d1.min(diag_dist));
378        }
379
380        max_cost
381    }
382}
383
384impl Default for PersistentHomology {
385    fn default() -> Self {
386        Self::new()
387    }
388}
389
390/// Union-Find data structure for persistent homology.
391#[derive(Debug, Clone)]
392struct UnionFind {
393    parent: Vec<usize>,
394    rank: Vec<usize>,
395}
396
397impl UnionFind {
398    fn new(n: usize) -> Self {
399        Self {
400            parent: (0..n).collect(),
401            rank: vec![0; n],
402        }
403    }
404
405    fn find(&mut self, x: usize) -> usize {
406        if self.parent[x] != x {
407            self.parent[x] = self.find(self.parent[x]);
408        }
409        self.parent[x]
410    }
411
412    fn union(&mut self, x: usize, y: usize) {
413        let rx = self.find(x);
414        let ry = self.find(y);
415        if rx == ry {
416            return;
417        }
418        if self.rank[rx] < self.rank[ry] {
419            self.parent[rx] = ry;
420        } else if self.rank[rx] > self.rank[ry] {
421            self.parent[ry] = rx;
422        } else {
423            self.parent[ry] = rx;
424            self.rank[rx] += 1;
425        }
426    }
427
428    fn n_components(&mut self) -> usize {
429        let n = self.parent.len();
430        let roots: std::collections::HashSet<usize> = (0..n).map(|i| self.find(i)).collect();
431        roots.len()
432    }
433}
434
435/// Reeb graph of a scalar function on a mesh.
436///
437/// The Reeb graph tracks the evolution of connected components
438/// of level sets f^{-1}(c) as c varies.
439#[derive(Debug, Clone)]
440pub struct ReebGraph {
441    /// Nodes of the Reeb graph (critical points)
442    pub nodes: Vec<ReebNode>,
443    /// Arcs connecting nodes
444    pub arcs: Vec<ReebArc>,
445}
446
447/// A node in the Reeb graph corresponding to a topological event.
448#[derive(Debug, Clone)]
449pub struct ReebNode {
450    /// Function value at this critical point
451    pub value: f64,
452    /// Type of critical point
453    pub kind: CriticalPointKind,
454    /// Index of the corresponding vertex in the mesh
455    pub vertex_idx: usize,
456}
457
458/// Type of critical point in the Reeb graph.
459#[derive(Debug, Clone, PartialEq)]
460pub enum CriticalPointKind {
461    /// Local minimum (birth of component)
462    Minimum,
463    /// Saddle (merge or split)
464    Saddle,
465    /// Local maximum (death of component)
466    Maximum,
467}
468
469/// An arc in the Reeb graph connecting two nodes.
470#[derive(Debug, Clone)]
471pub struct ReebArc {
472    /// Source node index
473    pub source: usize,
474    /// Target node index
475    pub target: usize,
476}
477
478impl ReebGraph {
479    /// Creates an empty Reeb graph.
480    pub fn new() -> Self {
481        Self {
482            nodes: Vec::new(),
483            arcs: Vec::new(),
484        }
485    }
486
487    /// Computes the Reeb graph of a scalar function on a set of vertices.
488    ///
489    /// # Arguments
490    /// * `values` - Scalar function values at each vertex
491    /// * `edges` - Edge connectivity of the mesh
492    pub fn compute(values: &[f64], edges: &[[usize; 2]]) -> Self {
493        let n = values.len();
494        if n == 0 {
495            return Self::new();
496        }
497
498        // Sort vertices by function value
499        let mut sorted_idx: Vec<usize> = (0..n).collect();
500        sorted_idx.sort_by(|&a, &b| {
501            values[a]
502                .partial_cmp(&values[b])
503                .unwrap_or(std::cmp::Ordering::Equal)
504        });
505
506        let mut nodes = Vec::new();
507        let mut arcs = Vec::new();
508
509        // Build adjacency
510        let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
511        for &[i, j] in edges {
512            adj[i].push(j);
513            adj[j].push(i);
514        }
515
516        // Classify critical points
517        for &v in &sorted_idx {
518            let fv = values[v];
519            let lower_nbrs: Vec<usize> = adj[v]
520                .iter()
521                .filter(|&&u| values[u] < fv)
522                .cloned()
523                .collect();
524            let upper_nbrs: Vec<usize> = adj[v]
525                .iter()
526                .filter(|&&u| values[u] > fv)
527                .cloned()
528                .collect();
529
530            let kind = if lower_nbrs.is_empty() {
531                CriticalPointKind::Minimum
532            } else if upper_nbrs.is_empty() {
533                CriticalPointKind::Maximum
534            } else {
535                CriticalPointKind::Saddle
536            };
537
538            if matches!(
539                kind,
540                CriticalPointKind::Minimum | CriticalPointKind::Maximum | CriticalPointKind::Saddle
541            ) {
542                // Only add if truly critical (for simplicity, add min/max always)
543                if matches!(
544                    kind,
545                    CriticalPointKind::Minimum | CriticalPointKind::Maximum
546                ) {
547                    let node_idx = nodes.len();
548                    nodes.push(ReebNode {
549                        value: fv,
550                        kind,
551                        vertex_idx: v,
552                    });
553                    if node_idx > 0 {
554                        arcs.push(ReebArc {
555                            source: node_idx - 1,
556                            target: node_idx,
557                        });
558                    }
559                }
560            }
561        }
562
563        Self { nodes, arcs }
564    }
565
566    /// Returns the number of critical points.
567    pub fn n_critical_points(&self) -> usize {
568        self.nodes.len()
569    }
570
571    /// Returns the number of minima.
572    pub fn n_minima(&self) -> usize {
573        self.nodes
574            .iter()
575            .filter(|n| n.kind == CriticalPointKind::Minimum)
576            .count()
577    }
578
579    /// Returns the number of maxima.
580    pub fn n_maxima(&self) -> usize {
581        self.nodes
582            .iter()
583            .filter(|n| n.kind == CriticalPointKind::Maximum)
584            .count()
585    }
586
587    /// Returns the number of saddles.
588    pub fn n_saddles(&self) -> usize {
589        self.nodes
590            .iter()
591            .filter(|n| n.kind == CriticalPointKind::Saddle)
592            .count()
593    }
594}
595
596impl Default for ReebGraph {
597    fn default() -> Self {
598        Self::new()
599    }
600}
601
602/// Morse-Smale complex for topological data analysis.
603///
604/// Decomposes a manifold based on gradient flow between critical points.
605#[derive(Debug, Clone)]
606pub struct MorseComplex {
607    /// List of critical points
608    pub critical_points: Vec<MorseCriticalPoint>,
609    /// Gradient flow connections (unstable/stable manifolds)
610    pub connections: Vec<(usize, usize)>,
611}
612
613/// A critical point in the Morse complex.
614#[derive(Debug, Clone)]
615pub struct MorseCriticalPoint {
616    /// Index of the vertex
617    pub vertex_idx: usize,
618    /// Function value at this critical point
619    pub value: f64,
620    /// Morse index (0=minimum, 1=saddle, 2=maximum for 2-manifold)
621    pub morse_index: usize,
622}
623
624impl MorseComplex {
625    /// Creates an empty Morse complex.
626    pub fn new() -> Self {
627        Self {
628            critical_points: Vec::new(),
629            connections: Vec::new(),
630        }
631    }
632
633    /// Computes the Morse complex of a scalar function on a mesh.
634    ///
635    /// # Arguments
636    /// * `values` - Scalar function values at each vertex
637    /// * `edges` - Edge connectivity
638    pub fn compute(values: &[f64], edges: &[[usize; 2]]) -> Self {
639        let n = values.len();
640        if n == 0 {
641            return Self::new();
642        }
643        let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
644        for &[i, j] in edges {
645            adj[i].push(j);
646            adj[j].push(i);
647        }
648
649        let mut critical_points = Vec::new();
650
651        for v in 0..n {
652            let fv = values[v];
653            let n_lower = adj[v].iter().filter(|&&u| values[u] < fv).count();
654            let n_upper = adj[v].iter().filter(|&&u| values[u] > fv).count();
655
656            let morse_index = if n_lower == 0 && n_upper > 0 {
657                Some(0) // minimum
658            } else if n_upper == 0 && n_lower > 0 {
659                Some(2) // maximum
660            } else if n_lower > 0 && n_upper > 0 {
661                // Check if lower link is disconnected (saddle)
662                Some(1)
663            } else {
664                None
665            };
666
667            if let Some(idx) = morse_index {
668                critical_points.push(MorseCriticalPoint {
669                    vertex_idx: v,
670                    value: fv,
671                    morse_index: idx,
672                });
673            }
674        }
675
676        // Build connections between consecutive critical points
677        critical_points.sort_by(|a, b| {
678            a.value
679                .partial_cmp(&b.value)
680                .unwrap_or(std::cmp::Ordering::Equal)
681        });
682        let connections: Vec<(usize, usize)> = (0..critical_points.len().saturating_sub(1))
683            .map(|i| (i, i + 1))
684            .collect();
685
686        Self {
687            critical_points,
688            connections,
689        }
690    }
691
692    /// Returns the number of minima.
693    pub fn n_minima(&self) -> usize {
694        self.critical_points
695            .iter()
696            .filter(|c| c.morse_index == 0)
697            .count()
698    }
699
700    /// Returns the number of saddles.
701    pub fn n_saddles(&self) -> usize {
702        self.critical_points
703            .iter()
704            .filter(|c| c.morse_index == 1)
705            .count()
706    }
707
708    /// Returns the number of maxima.
709    pub fn n_maxima(&self) -> usize {
710        self.critical_points
711            .iter()
712            .filter(|c| c.morse_index == 2)
713            .count()
714    }
715
716    /// Verifies Morse inequalities: saddles + 2 ≤ maxima + minima (approx).
717    pub fn morse_relation_holds(&self) -> bool {
718        let nm = self.n_minima();
719        let ns = self.n_saddles();
720        let nmax = self.n_maxima();
721        // Morse inequality: m_0 - m_1 + m_2 ≥ χ (here approximate)
722        nm as i64 - ns as i64 + nmax as i64 >= 0
723    }
724}
725
726impl Default for MorseComplex {
727    fn default() -> Self {
728        Self::new()
729    }
730}
731
732/// Alpha complex / alpha shapes for point clouds.
733///
734/// An alpha complex is a subset of the Delaunay triangulation
735/// based on a circumradius threshold α.
736#[derive(Debug, Clone)]
737pub struct AlphaComplex {
738    /// Alpha parameter (circumradius threshold)
739    pub alpha: f64,
740    /// Points in the cloud
741    pub points: Vec<[f64; 3]>,
742    /// Edges included at this alpha value
743    pub edges: Vec<[usize; 2]>,
744    /// Triangles included at this alpha value
745    pub triangles: Vec<[usize; 3]>,
746}
747
748impl AlphaComplex {
749    /// Creates an alpha complex from a point cloud.
750    ///
751    /// # Arguments
752    /// * `points` - Point cloud
753    /// * `alpha` - Circumradius threshold (larger = more simplices)
754    pub fn new(points: Vec<[f64; 3]>, alpha: f64) -> Self {
755        let n = points.len();
756        let mut edges = Vec::new();
757        let mut triangles = Vec::new();
758
759        // Add edges where half-distance ≤ alpha
760        for i in 0..n {
761            for j in i + 1..n {
762                let d = dist3(&points[i], &points[j]);
763                if d / 2.0 <= alpha {
764                    edges.push([i, j]);
765                }
766            }
767        }
768
769        // Add triangles where circumradius ≤ alpha
770        for i in 0..n {
771            for j in i + 1..n {
772                for k in j + 1..n {
773                    let r = circumradius_3pts(&points[i], &points[j], &points[k]);
774                    if r <= alpha {
775                        triangles.push([i, j, k]);
776                        // Ensure edges exist
777                    }
778                }
779            }
780        }
781
782        Self {
783            alpha,
784            points,
785            edges,
786            triangles,
787        }
788    }
789
790    /// Returns the number of connected components at this alpha.
791    pub fn n_components(&self) -> usize {
792        let n = self.points.len();
793        if n == 0 {
794            return 0;
795        }
796        let mut uf = UnionFind::new(n);
797        for &[i, j] in &self.edges {
798            uf.union(i, j);
799        }
800        uf.n_components()
801    }
802
803    /// Returns the simplicial complex at this alpha level.
804    pub fn to_simplicial_complex(&self) -> SimplicialComplex {
805        let mut sc = SimplicialComplex::new();
806        for &p in &self.points {
807            sc.add_vertex(p);
808        }
809        for &[i, j] in &self.edges {
810            sc.add_edge(i, j);
811        }
812        for &[i, j, k] in &self.triangles {
813            sc.add_triangle(i, j, k);
814        }
815        sc
816    }
817}
818
819/// Circumradius of a triangle defined by three 3D points.
820fn circumradius_3pts(a: &[f64; 3], b: &[f64; 3], c: &[f64; 3]) -> f64 {
821    let ab = dist3(a, b);
822    let bc = dist3(b, c);
823    let ca = dist3(c, a);
824    let s = (ab + bc + ca) / 2.0;
825    let area_sq = s * (s - ab) * (s - bc) * (s - ca);
826    if area_sq <= 0.0 {
827        return f64::INFINITY;
828    }
829    let area = area_sq.sqrt();
830    ab * bc * ca / (4.0 * area)
831}
832
833/// 3D Euclidean distance between two points.
834fn dist3(a: &[f64; 3], b: &[f64; 3]) -> f64 {
835    let dx = a[0] - b[0];
836    let dy = a[1] - b[1];
837    let dz = a[2] - b[2];
838    (dx * dx + dy * dy + dz * dz).sqrt()
839}
840
841/// Cubical complex for image topology (checkerboard-like).
842///
843/// Represents a binary image as a cubical complex and computes
844/// its topological features.
845#[derive(Debug, Clone)]
846pub struct CheckerboardComplex {
847    /// Grid dimensions \[nx, ny\]
848    pub dims: [usize; 2],
849    /// Binary image data (true = filled voxel)
850    pub data: Vec<bool>,
851}
852
853impl CheckerboardComplex {
854    /// Creates a cubical complex from a binary grid.
855    ///
856    /// # Arguments
857    /// * `dims` - Grid dimensions \[nx, ny\]
858    /// * `data` - Binary values row-major
859    pub fn new(dims: [usize; 2], data: Vec<bool>) -> Self {
860        Self { dims, data }
861    }
862
863    /// Creates a filled disk of given radius in a grid.
864    pub fn filled_disk(size: usize, radius: f64) -> Self {
865        let cx = size as f64 / 2.0;
866        let cy = size as f64 / 2.0;
867        let data: Vec<bool> = (0..size * size)
868            .map(|idx| {
869                let i = (idx / size) as f64;
870                let j = (idx % size) as f64;
871                ((i - cx).powi(2) + (j - cy).powi(2)).sqrt() <= radius
872            })
873            .collect();
874        Self::new([size, size], data)
875    }
876
877    /// Creates an annulus (ring) in a grid.
878    pub fn annulus(size: usize, r_inner: f64, r_outer: f64) -> Self {
879        let cx = size as f64 / 2.0;
880        let cy = size as f64 / 2.0;
881        let data: Vec<bool> = (0..size * size)
882            .map(|idx| {
883                let i = (idx / size) as f64;
884                let j = (idx % size) as f64;
885                let r = ((i - cx).powi(2) + (j - cy).powi(2)).sqrt();
886                r >= r_inner && r <= r_outer
887            })
888            .collect();
889        Self::new([size, size], data)
890    }
891
892    /// Counts the number of filled voxels.
893    pub fn volume(&self) -> usize {
894        self.data.iter().filter(|&&b| b).count()
895    }
896
897    /// Computes the Euler characteristic via the cubical formula.
898    ///
899    /// χ = V - E + F for 2D cubical complex
900    pub fn euler_characteristic_2d(&self) -> i64 {
901        let nx = self.dims[0];
902        let ny = self.dims[1];
903        let mut v = 0_i64; // vertices
904        let mut e = 0_i64; // edges
905        let mut f = 0_i64; // faces
906
907        for i in 0..nx {
908            for j in 0..ny {
909                let filled = self.data[i * ny + j];
910                if filled {
911                    v += 1;
912                    if i + 1 < nx && self.data[(i + 1) * ny + j] {
913                        e += 1;
914                    }
915                    if j + 1 < ny && self.data[i * ny + j + 1] {
916                        e += 1;
917                    }
918                    if i + 1 < nx
919                        && j + 1 < ny
920                        && self.data[(i + 1) * ny + j]
921                        && self.data[i * ny + j + 1]
922                        && self.data[(i + 1) * ny + j + 1]
923                    {
924                        f += 1;
925                    }
926                }
927            }
928        }
929        v - e + f
930    }
931}
932
933/// Betti numbers for topological characterization.
934///
935/// β_0 = number of connected components
936/// β_1 = number of independent loops
937/// β_2 = number of enclosed voids
938#[derive(Debug, Clone, Copy)]
939pub struct BettiNumbers {
940    /// β_0: connected components
941    pub beta_0: usize,
942    /// β_1: loops (1-cycles)
943    pub beta_1: usize,
944    /// β_2: voids (2-cycles)
945    pub beta_2: usize,
946}
947
948impl BettiNumbers {
949    /// Computes Betti numbers from a simplicial complex using Euler relation.
950    ///
951    /// For a closed orientable surface: χ = β_0 - β_1 + β_2
952    ///
953    /// # Arguments
954    /// * `sc` - Simplicial complex
955    /// * `n_components` - Number of connected components (β_0)
956    /// * `n_voids` - Number of enclosed voids (β_2, 0 for surfaces)
957    pub fn from_simplicial_complex(
958        sc: &SimplicialComplex,
959        n_components: usize,
960        n_voids: usize,
961    ) -> Self {
962        let chi = sc.euler_characteristic();
963        // χ = β_0 - β_1 + β_2
964        // => β_1 = β_0 + β_2 - χ
965        let beta_1 = (n_components as i64 + n_voids as i64 - chi).max(0) as usize;
966        Self {
967            beta_0: n_components,
968            beta_1,
969            beta_2: n_voids,
970        }
971    }
972
973    /// Returns the Euler characteristic χ = β_0 - β_1 + β_2.
974    pub fn euler_characteristic(&self) -> i64 {
975        self.beta_0 as i64 - self.beta_1 as i64 + self.beta_2 as i64
976    }
977
978    /// Creates Betti numbers directly.
979    pub fn new(beta_0: usize, beta_1: usize, beta_2: usize) -> Self {
980        Self {
981            beta_0,
982            beta_1,
983            beta_2,
984        }
985    }
986}
987
988/// Topological noise removal via persistence thresholding.
989///
990/// Features with persistence below the threshold are considered noise.
991#[derive(Debug, Clone)]
992pub struct TopologicalNoise {
993    /// Persistence threshold
994    pub threshold: f64,
995}
996
997impl TopologicalNoise {
998    /// Creates a noise removal filter with given persistence threshold.
999    ///
1000    /// # Arguments
1001    /// * `threshold` - Minimum persistence to keep a feature
1002    pub fn new(threshold: f64) -> Self {
1003        Self { threshold }
1004    }
1005
1006    /// Filters birth-death pairs, keeping only significant features.
1007    ///
1008    /// # Arguments
1009    /// * `pairs` - Input persistence diagram
1010    pub fn filter(&self, pairs: &[BirthDeathPair]) -> Vec<BirthDeathPair> {
1011        pairs
1012            .iter()
1013            .filter(|p| p.persistence() > self.threshold || p.is_essential())
1014            .cloned()
1015            .collect()
1016    }
1017
1018    /// Counts significant features at given dimension.
1019    ///
1020    /// # Arguments
1021    /// * `pairs` - Persistence diagram
1022    /// * `dim` - Homological dimension
1023    pub fn count_features(&self, pairs: &[BirthDeathPair], dim: usize) -> usize {
1024        pairs
1025            .iter()
1026            .filter(|p| p.dim == dim && (p.persistence() > self.threshold || p.is_essential()))
1027            .count()
1028    }
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033    use super::*;
1034
1035    // ---- SimplicialComplex ----
1036
1037    #[test]
1038    fn test_euler_characteristic_sphere() {
1039        // Octahedral sphere: V=6, E=12, F=8 => χ=2
1040        let sc = SimplicialComplex::octahedral_sphere();
1041        assert_eq!(sc.euler_characteristic(), 2);
1042    }
1043
1044    #[test]
1045    fn test_octahedral_sphere_vertex_count() {
1046        let sc = SimplicialComplex::octahedral_sphere();
1047        assert_eq!(sc.n_vertices(), 6);
1048    }
1049
1050    #[test]
1051    fn test_octahedral_sphere_edge_count() {
1052        let sc = SimplicialComplex::octahedral_sphere();
1053        assert_eq!(sc.n_edges(), 12);
1054    }
1055
1056    #[test]
1057    fn test_octahedral_sphere_triangle_count() {
1058        let sc = SimplicialComplex::octahedral_sphere();
1059        assert_eq!(sc.n_triangles(), 8);
1060    }
1061
1062    #[test]
1063    fn test_euler_characteristic_torus() {
1064        // Minimal torus: χ=0
1065        let sc = SimplicialComplex::minimal_torus();
1066        assert_eq!(sc.euler_characteristic(), 0);
1067    }
1068
1069    #[test]
1070    fn test_single_vertex_euler() {
1071        let mut sc = SimplicialComplex::new();
1072        sc.add_vertex([0.0, 0.0, 0.0]);
1073        assert_eq!(sc.euler_characteristic(), 1);
1074    }
1075
1076    #[test]
1077    fn test_add_duplicate_edge_ignored() {
1078        let mut sc = SimplicialComplex::new();
1079        sc.add_vertex([0.0, 0.0, 0.0]);
1080        sc.add_vertex([1.0, 0.0, 0.0]);
1081        sc.add_edge(0, 1);
1082        sc.add_edge(0, 1); // duplicate
1083        assert_eq!(sc.n_edges(), 1);
1084    }
1085
1086    #[test]
1087    fn test_tetrahedron_euler_characteristic() {
1088        let mut sc = SimplicialComplex::new();
1089        for i in 0..4 {
1090            sc.add_vertex([i as f64, 0.0, 0.0]);
1091        }
1092        sc.add_edge(0, 1);
1093        sc.add_edge(0, 2);
1094        sc.add_edge(0, 3);
1095        sc.add_edge(1, 2);
1096        sc.add_edge(1, 3);
1097        sc.add_edge(2, 3);
1098        sc.add_triangle(0, 1, 2);
1099        sc.add_triangle(0, 1, 3);
1100        sc.add_triangle(0, 2, 3);
1101        sc.add_triangle(1, 2, 3);
1102        // Tetrahedron surface: V=4, E=6, F=4 => χ=2
1103        assert_eq!(sc.euler_characteristic(), 2);
1104    }
1105
1106    #[test]
1107    fn test_boundary_1_dimensions() {
1108        let sc = SimplicialComplex::octahedral_sphere();
1109        let b1 = sc.boundary_1();
1110        assert_eq!(b1.len(), sc.n_vertices() * sc.n_edges());
1111    }
1112
1113    #[test]
1114    fn test_boundary_2_dimensions() {
1115        let sc = SimplicialComplex::octahedral_sphere();
1116        let b2 = sc.boundary_2();
1117        assert_eq!(b2.len(), sc.n_edges() * sc.n_triangles());
1118    }
1119
1120    // ---- PersistentHomology ----
1121
1122    #[test]
1123    fn test_persistent_homology_single_point() {
1124        let pts = [[0.0, 0.0, 0.0]];
1125        let ph = PersistentHomology::compute_0d(&pts);
1126        // One essential class
1127        assert_eq!(ph.significant_features(0.0), 1);
1128    }
1129
1130    #[test]
1131    fn test_persistent_homology_two_points() {
1132        let pts = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
1133        let ph = PersistentHomology::compute_0d(&pts);
1134        // One finite pair + one essential
1135        assert_eq!(ph.pairs.len(), 2);
1136    }
1137
1138    #[test]
1139    fn test_persistent_homology_one_blob() {
1140        let pts: Vec<[f64; 3]> = (0..5).map(|i| [i as f64 * 0.1, 0.0, 0.0]).collect();
1141        let ph = PersistentHomology::compute_0d(&pts);
1142        // Should have exactly 1 essential component
1143        let essential = ph.pairs.iter().filter(|p| p.is_essential()).count();
1144        assert_eq!(essential, 1);
1145    }
1146
1147    #[test]
1148    fn test_persistent_homology_birth_death_order() {
1149        let pts = [[0.0, 0.0, 0.0], [0.1, 0.0, 0.0], [5.0, 0.0, 0.0]];
1150        let ph = PersistentHomology::compute_0d(&pts);
1151        for p in ph.pairs.iter().filter(|p| p.death.is_finite()) {
1152            assert!(p.birth <= p.death);
1153        }
1154    }
1155
1156    #[test]
1157    fn test_bottleneck_distance_nonneg() {
1158        let pts1 = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
1159        let pts2 = [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
1160        let ph1 = PersistentHomology::compute_0d(&pts1);
1161        let ph2 = PersistentHomology::compute_0d(&pts2);
1162        let d = ph1.bottleneck_distance(&ph2);
1163        assert!(d >= 0.0);
1164    }
1165
1166    #[test]
1167    fn test_bottleneck_distance_self_zero() {
1168        let pts = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
1169        let ph = PersistentHomology::compute_0d(&pts);
1170        let d = ph.bottleneck_distance(&ph);
1171        assert!(d < 1e-10);
1172    }
1173
1174    #[test]
1175    fn test_persistence_positive() {
1176        let p = BirthDeathPair {
1177            birth: 0.5,
1178            death: 1.5,
1179            dim: 0,
1180        };
1181        assert!((p.persistence() - 1.0).abs() < 1e-10);
1182    }
1183
1184    // ---- ReebGraph ----
1185
1186    #[test]
1187    fn test_reeb_graph_height_sphere_two_critical_points() {
1188        // Height function on line: min, max
1189        let values = vec![0.0, 0.5, 1.0, 0.7, 0.3];
1190        let edges = [[0usize, 1], [1, 2], [2, 3], [3, 4]];
1191        let rg = ReebGraph::compute(&values, &edges);
1192        assert!(rg.n_minima() >= 1);
1193        assert!(rg.n_maxima() >= 1);
1194    }
1195
1196    #[test]
1197    fn test_reeb_graph_monotone_no_saddle() {
1198        let values = vec![0.0, 1.0, 2.0, 3.0, 4.0];
1199        let edges = [[0usize, 1], [1, 2], [2, 3], [3, 4]];
1200        let rg = ReebGraph::compute(&values, &edges);
1201        // Monotone function: only min and max
1202        assert_eq!(rg.n_saddles(), 0);
1203    }
1204
1205    #[test]
1206    fn test_reeb_graph_empty() {
1207        let rg = ReebGraph::compute(&[], &[]);
1208        assert_eq!(rg.n_critical_points(), 0);
1209    }
1210
1211    // ---- MorseComplex ----
1212
1213    #[test]
1214    fn test_morse_complex_line() {
1215        let values = vec![1.0, 0.5, 0.8, 0.3, 0.7];
1216        let edges = [[0usize, 1], [1, 2], [2, 3], [3, 4]];
1217        let mc = MorseComplex::compute(&values, &edges);
1218        assert!(mc.n_minima() >= 1);
1219        assert!(mc.n_maxima() >= 1);
1220    }
1221
1222    #[test]
1223    fn test_morse_relation_holds() {
1224        let values = vec![1.0, 3.0, 0.5, 2.0, 0.3];
1225        let edges = [[0usize, 1], [1, 2], [2, 3], [3, 4], [0, 4]];
1226        let mc = MorseComplex::compute(&values, &edges);
1227        assert!(mc.morse_relation_holds());
1228    }
1229
1230    #[test]
1231    fn test_morse_empty() {
1232        let mc = MorseComplex::compute(&[], &[]);
1233        assert_eq!(mc.n_minima() + mc.n_saddles() + mc.n_maxima(), 0);
1234    }
1235
1236    #[test]
1237    fn test_morse_single_vertex() {
1238        let values = vec![1.0];
1239        let mc = MorseComplex::compute(&values, &[]);
1240        // Isolated point: no neighbors -> neither min nor max classified
1241        assert_eq!(mc.critical_points.len(), 0);
1242    }
1243
1244    // ---- AlphaComplex ----
1245
1246    #[test]
1247    fn test_alpha_complex_large_alpha_all_edges() {
1248        let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]];
1249        let ac = AlphaComplex::new(pts, 1000.0);
1250        // At very large alpha, all pairwise edges should be included
1251        assert_eq!(ac.edges.len(), 3);
1252    }
1253
1254    #[test]
1255    fn test_alpha_complex_zero_alpha_no_edges() {
1256        let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
1257        let ac = AlphaComplex::new(pts, 0.0);
1258        assert_eq!(ac.edges.len(), 0);
1259    }
1260
1261    #[test]
1262    fn test_alpha_complex_components() {
1263        let pts = vec![[0.0, 0.0, 0.0], [0.1, 0.0, 0.0], [10.0, 0.0, 0.0]];
1264        let ac = AlphaComplex::new(pts, 0.1);
1265        // First two close, third far
1266        assert_eq!(ac.n_components(), 2);
1267    }
1268
1269    #[test]
1270    fn test_alpha_complex_single_component_large_alpha() {
1271        let pts: Vec<[f64; 3]> = (0..4).map(|i| [i as f64 * 0.5, 0.0, 0.0]).collect();
1272        let ac = AlphaComplex::new(pts, 1000.0);
1273        assert_eq!(ac.n_components(), 1);
1274    }
1275
1276    // ---- CheckerboardComplex ----
1277
1278    #[test]
1279    fn test_checkerboard_filled_disk_volume() {
1280        let cc = CheckerboardComplex::filled_disk(10, 4.0);
1281        assert!(cc.volume() > 0);
1282    }
1283
1284    #[test]
1285    fn test_checkerboard_annulus_volume() {
1286        let cc = CheckerboardComplex::annulus(20, 3.0, 7.0);
1287        assert!(cc.volume() > 0);
1288    }
1289
1290    #[test]
1291    fn test_checkerboard_euler_single_square() {
1292        let cc = CheckerboardComplex::new([1, 1], vec![true]);
1293        let chi = cc.euler_characteristic_2d();
1294        assert_eq!(chi, 1); // single vertex
1295    }
1296
1297    // ---- BettiNumbers ----
1298
1299    #[test]
1300    fn test_betti_euler_consistency() {
1301        let beta = BettiNumbers::new(1, 2, 1);
1302        // χ = 1 - 2 + 1 = 0 (torus)
1303        assert_eq!(beta.euler_characteristic(), 0);
1304    }
1305
1306    #[test]
1307    fn test_betti_sphere_euler() {
1308        let beta = BettiNumbers::new(1, 0, 1);
1309        // χ = 1 - 0 + 1 = 2 (sphere)
1310        assert_eq!(beta.euler_characteristic(), 2);
1311    }
1312
1313    #[test]
1314    fn test_betti_from_simplicial_sphere() {
1315        let sc = SimplicialComplex::octahedral_sphere();
1316        let beta = BettiNumbers::from_simplicial_complex(&sc, 1, 0);
1317        // χ = 2, β_0=1, β_2=0 => β_1 = 1 + 0 - 2 = -1 -> 0
1318        assert_eq!(beta.beta_0, 1);
1319    }
1320
1321    // ---- TopologicalNoise ----
1322
1323    #[test]
1324    fn test_topological_noise_removes_small_features() {
1325        let noise = TopologicalNoise::new(0.5);
1326        let pairs = vec![
1327            BirthDeathPair {
1328                birth: 0.0,
1329                death: 0.1,
1330                dim: 0,
1331            },
1332            BirthDeathPair {
1333                birth: 0.0,
1334                death: 1.0,
1335                dim: 0,
1336            },
1337        ];
1338        let filtered = noise.filter(&pairs);
1339        assert_eq!(filtered.len(), 1);
1340    }
1341
1342    #[test]
1343    fn test_topological_noise_keeps_essential() {
1344        let noise = TopologicalNoise::new(100.0);
1345        let pairs = vec![BirthDeathPair {
1346            birth: 0.0,
1347            death: f64::INFINITY,
1348            dim: 0,
1349        }];
1350        let filtered = noise.filter(&pairs);
1351        assert_eq!(filtered.len(), 1);
1352    }
1353
1354    #[test]
1355    fn test_topological_noise_count_features() {
1356        let noise = TopologicalNoise::new(0.3);
1357        let pairs = vec![
1358            BirthDeathPair {
1359                birth: 0.0,
1360                death: 0.1,
1361                dim: 0,
1362            },
1363            BirthDeathPair {
1364                birth: 0.0,
1365                death: 0.5,
1366                dim: 0,
1367            },
1368            BirthDeathPair {
1369                birth: 0.0,
1370                death: f64::INFINITY,
1371                dim: 0,
1372            },
1373        ];
1374        assert_eq!(noise.count_features(&pairs, 0), 2);
1375    }
1376
1377    #[test]
1378    fn test_topological_noise_zero_threshold() {
1379        let noise = TopologicalNoise::new(0.0);
1380        let pairs = vec![
1381            BirthDeathPair {
1382                birth: 0.0,
1383                death: 0.001,
1384                dim: 1,
1385            },
1386            BirthDeathPair {
1387                birth: 0.0,
1388                death: 1.0,
1389                dim: 1,
1390            },
1391        ];
1392        let filtered = noise.filter(&pairs);
1393        assert_eq!(filtered.len(), 2);
1394    }
1395
1396    #[test]
1397    fn test_circumradius_equilateral() {
1398        let a = [0.0, 0.0, 0.0];
1399        let b = [1.0, 0.0, 0.0];
1400        let c = [0.5, (3.0_f64).sqrt() / 2.0, 0.0];
1401        let r = circumradius_3pts(&a, &b, &c);
1402        // Circumradius of equilateral triangle with side 1 = 1/sqrt(3)
1403        assert!((r - 1.0 / 3.0_f64.sqrt()).abs() < 1e-6);
1404    }
1405}