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