Skip to main content

oxiphysics_core/
topological_data.rs

1#![allow(clippy::needless_range_loop)]
2// Copyright 2026 COOLJAPAN OU (Team KitaSan)
3// SPDX-License-Identifier: Apache-2.0
4
5//! Topological Data Analysis (TDA) — persistent homology utilities.
6//!
7//! Provides Vietoris–Rips filtration for 0- and 1-dimensional persistent
8//! homology, bottleneck and Wasserstein distances between persistence
9//! diagrams, Betti numbers, Euler characteristic, cubical complexes,
10//! and persistence entropy.
11//!
12//! Structured types: [`VietorisRips`], [`SimplexTree`], [`PersistenceDiagram`],
13//! [`BarcodeSummary`], and [`MapperGraph`].
14
15#![allow(dead_code)]
16#![allow(clippy::too_many_arguments)]
17
18// ─────────────────────────────────────────────────────────────────────────────
19// Primitive types
20// ─────────────────────────────────────────────────────────────────────────────
21
22/// A weighted undirected edge in a simplicial filtration.
23#[derive(Debug, Clone, PartialEq)]
24pub struct SimplexEdge {
25    /// Index of the first vertex.
26    pub v0: usize,
27    /// Index of the second vertex.
28    pub v1: usize,
29    /// Filtration weight (e.g., Euclidean distance).
30    pub weight: f64,
31}
32
33/// Union–Find (disjoint-set) data structure for Kruskal's algorithm.
34#[derive(Debug, Clone)]
35pub struct UnionFind {
36    /// Parent pointers; `parent[i] == i` for root nodes.
37    pub parent: Vec<usize>,
38    /// Union-by-rank array.
39    pub rank: Vec<usize>,
40}
41
42impl UnionFind {
43    /// Create a new `UnionFind` with `n` singleton sets.
44    pub fn new(n: usize) -> Self {
45        Self {
46            parent: (0..n).collect(),
47            rank: vec![0; n],
48        }
49    }
50
51    /// Find the representative of the set containing `i` (path compression).
52    pub fn find(&mut self, i: usize) -> usize {
53        if self.parent[i] != i {
54            self.parent[i] = self.find(self.parent[i]);
55        }
56        self.parent[i]
57    }
58
59    /// Union the sets containing `i` and `j`.  Returns `true` if they were
60    /// in different sets (a merge happened).
61    pub fn union(&mut self, i: usize, j: usize) -> bool {
62        let ri = self.find(i);
63        let rj = self.find(j);
64        if ri == rj {
65            return false;
66        }
67        match self.rank[ri].cmp(&self.rank[rj]) {
68            std::cmp::Ordering::Less => self.parent[ri] = rj,
69            std::cmp::Ordering::Greater => self.parent[rj] = ri,
70            std::cmp::Ordering::Equal => {
71                self.parent[rj] = ri;
72                self.rank[ri] += 1;
73            }
74        }
75        true
76    }
77}
78
79/// A bar in a persistence diagram: `[birth, death)` for homology class H_k.
80///
81/// An *essential* class (never destroyed) has `death == f64::INFINITY`.
82#[derive(Debug, Clone, PartialEq)]
83pub struct PersistentInterval {
84    /// Filtration value at which this class was born.
85    pub birth: f64,
86    /// Filtration value at which this class died; `INFINITY` if essential.
87    pub death: f64,
88    /// Homological dimension (0 = connected component, 1 = loop, …).
89    pub dimension: usize,
90}
91
92impl PersistentInterval {
93    /// Persistence (lifetime) of this interval.  Returns `INFINITY` for
94    /// essential classes.
95    pub fn persistence(&self) -> f64 {
96        self.death - self.birth
97    }
98
99    /// Returns `true` if this class is essential (never destroyed).
100    pub fn is_essential(&self) -> bool {
101        self.death.is_infinite()
102    }
103}
104
105// ─────────────────────────────────────────────────────────────────────────────
106// Vietoris–Rips H₀ via Kruskal
107// ─────────────────────────────────────────────────────────────────────────────
108
109/// Compute all pairwise edges from a 2-D point cloud.
110fn all_edges(points: &[[f64; 2]]) -> Vec<SimplexEdge> {
111    let n = points.len();
112    let mut edges = Vec::with_capacity(n * (n.saturating_sub(1)) / 2);
113    for i in 0..n {
114        for j in (i + 1)..n {
115            let dx = points[i][0] - points[j][0];
116            let dy = points[i][1] - points[j][1];
117            edges.push(SimplexEdge {
118                v0: i,
119                v1: j,
120                weight: (dx * dx + dy * dy).sqrt(),
121            });
122        }
123    }
124    edges
125}
126
127/// Compute 0-dimensional persistent homology of the Vietoris–Rips filtration
128/// up to `max_r` using Kruskal's algorithm.
129///
130/// Each returned interval represents a connected component: it is born at
131/// `r = 0` and dies when it merges with an older component.  The last
132/// surviving component has `death = INFINITY`.
133pub fn vietoris_rips_h0(points: &[[f64; 2]], max_r: f64) -> Vec<PersistentInterval> {
134    let n = points.len();
135    if n == 0 {
136        return vec![];
137    }
138    let mut edges = all_edges(points);
139    edges.sort_by(|a, b| {
140        a.weight
141            .partial_cmp(&b.weight)
142            .unwrap_or(std::cmp::Ordering::Equal)
143    });
144
145    let mut uf = UnionFind::new(n);
146    // birth times: all components born at filtration value 0
147    let mut birth = vec![0.0_f64; n];
148    let mut intervals = Vec::new();
149
150    for e in &edges {
151        if e.weight > max_r {
152            break;
153        }
154        if uf.union(e.v0, e.v1) {
155            // The *younger* component (higher birth) dies
156            let r0 = uf.find(e.v0);
157            let r1 = uf.find(e.v1);
158            // After union, one root absorbed the other; the non-root died.
159            // We compare the original roots before the merge.
160            let root_after = uf.find(e.v0);
161            let dying = if root_after == r0 { r1 } else { r0 };
162            let b = birth[dying];
163            birth[root_after] = birth[root_after].min(b);
164            intervals.push(PersistentInterval {
165                birth: b,
166                death: e.weight,
167                dimension: 0,
168            });
169        }
170    }
171
172    // Essential class: the one component that survives
173    intervals.push(PersistentInterval {
174        birth: 0.0,
175        death: f64::INFINITY,
176        dimension: 0,
177    });
178    intervals
179}
180
181// ─────────────────────────────────────────────────────────────────────────────
182// Vietoris–Rips H₁ approximation via cycle detection
183// ─────────────────────────────────────────────────────────────────────────────
184
185/// Approximate 1-dimensional persistent homology (loops) of the
186/// Vietoris–Rips filtration.
187///
188/// Uses the first `n_edges` edges (sorted by weight).  A 1-cycle is created
189/// whenever an edge closes a loop in the current spanning forest.
190pub fn vietoris_rips_h1_approx(
191    points: &[[f64; 2]],
192    max_r: f64,
193    n_edges: usize,
194) -> Vec<PersistentInterval> {
195    let n = points.len();
196    if n == 0 {
197        return vec![];
198    }
199    let mut edges = all_edges(points);
200    edges.sort_by(|a, b| {
201        a.weight
202            .partial_cmp(&b.weight)
203            .unwrap_or(std::cmp::Ordering::Equal)
204    });
205    let edges: Vec<_> = edges
206        .into_iter()
207        .filter(|e| e.weight <= max_r)
208        .take(n_edges)
209        .collect();
210
211    let mut uf = UnionFind::new(n);
212    let mut intervals = Vec::new();
213    let mut edge_births: Vec<f64> = Vec::new(); // birth of spanning tree edges
214
215    for e in &edges {
216        if !uf.union(e.v0, e.v1) {
217            // This edge closes a loop — approximate death as max_r
218            let birth = *edge_births.last().unwrap_or(&0.0);
219            intervals.push(PersistentInterval {
220                birth,
221                death: (e.weight + max_r) / 2.0,
222                dimension: 1,
223            });
224        } else {
225            edge_births.push(e.weight);
226        }
227    }
228    intervals
229}
230
231// ─────────────────────────────────────────────────────────────────────────────
232// Diagram distances
233// ─────────────────────────────────────────────────────────────────────────────
234
235/// L∞ distance between two points in a persistence diagram,
236/// projected to the diagonal if needed.
237fn point_dist_inf(p: &PersistentInterval, q: &PersistentInterval) -> f64 {
238    let pb = p.birth.min(p.death);
239    let pd = p.birth.max(p.death);
240    let qb = q.birth.min(q.death);
241    let qd = q.birth.max(q.death);
242    f64::max((pb - qb).abs(), (pd - qd).abs())
243}
244
245/// Distance from a point to the diagonal (half its persistence).
246fn dist_to_diagonal(p: &PersistentInterval) -> f64 {
247    (p.death - p.birth).abs() / 2.0
248}
249
250/// Bottleneck distance between two persistence diagrams.
251///
252/// Computed via a greedy approximation: match each point in the smaller
253/// diagram to the closest point in the larger, then take the maximum
254/// residual (including diagonal projections).
255pub fn bottleneck_distance(a: &[PersistentInterval], b: &[PersistentInterval]) -> f64 {
256    // Filter to finite intervals only
257    let fa: Vec<&PersistentInterval> = a.iter().filter(|p| !p.is_essential()).collect();
258    let fb: Vec<&PersistentInterval> = b.iter().filter(|p| !p.is_essential()).collect();
259
260    let mut max_dist = 0.0_f64;
261
262    // For each point in fa find closest in fb
263    let mut used = vec![false; fb.len()];
264    for p in &fa {
265        let best = fb
266            .iter()
267            .enumerate()
268            .filter(|(idx, _)| !used[*idx])
269            .map(|(idx, q)| (idx, point_dist_inf(p, q)))
270            .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(std::cmp::Ordering::Equal));
271        if let Some((idx, d)) = best {
272            used[idx] = true;
273            max_dist = max_dist.max(d);
274        } else {
275            max_dist = max_dist.max(dist_to_diagonal(p));
276        }
277    }
278    // Unmatched points in fb go to diagonal
279    for (idx, q) in fb.iter().enumerate() {
280        if !used[idx] {
281            max_dist = max_dist.max(dist_to_diagonal(q));
282        }
283    }
284    max_dist
285}
286
287/// 1-Wasserstein (Earth Mover's) distance between two persistence diagrams.
288///
289/// Uses a greedy nearest-neighbor matching (not optimal for large diagrams,
290/// but exact for the 1-Wasserstein cost under the L∞ ground metric).
291pub fn wasserstein_distance_1(a: &[PersistentInterval], b: &[PersistentInterval]) -> f64 {
292    let fa: Vec<&PersistentInterval> = a.iter().filter(|p| !p.is_essential()).collect();
293    let fb: Vec<&PersistentInterval> = b.iter().filter(|p| !p.is_essential()).collect();
294
295    let mut total = 0.0_f64;
296    let mut used = vec![false; fb.len()];
297
298    for p in &fa {
299        let best = fb
300            .iter()
301            .enumerate()
302            .filter(|(idx, _)| !used[*idx])
303            .map(|(idx, q)| (idx, point_dist_inf(p, q)))
304            .min_by(|x, y| x.1.partial_cmp(&y.1).unwrap_or(std::cmp::Ordering::Equal));
305        if let Some((idx, d)) = best {
306            used[idx] = true;
307            total += d;
308        } else {
309            total += dist_to_diagonal(p);
310        }
311    }
312    for (idx, q) in fb.iter().enumerate() {
313        if !used[idx] {
314            total += dist_to_diagonal(q);
315        }
316    }
317    total
318}
319
320// ─────────────────────────────────────────────────────────────────────────────
321// Betti numbers & Euler characteristic
322// ─────────────────────────────────────────────────────────────────────────────
323
324/// Betti numbers β₀, β₁, β₂ of a topological space.
325#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct BettiNumbers {
327    /// β₀: number of connected components.
328    pub b0: usize,
329    /// β₁: number of independent loops.
330    pub b1: usize,
331    /// β₂: number of voids (2-spheres).
332    pub b2: usize,
333}
334
335/// Euler characteristic χ = β₀ − β₁ + β₂.
336pub fn euler_characteristic(betti: &BettiNumbers) -> i64 {
337    betti.b0 as i64 - betti.b1 as i64 + betti.b2 as i64
338}
339
340// ─────────────────────────────────────────────────────────────────────────────
341// 1-D cubical complex (sublevel-set filtration)
342// ─────────────────────────────────────────────────────────────────────────────
343
344/// Compute Betti numbers of the sublevel set `{x : signal[i] ≤ threshold}`.
345///
346/// Only β₀ and β₁ are non-trivial in 1-D; β₂ is always 0.
347pub fn cubical_complex_1d(signal: &[f64], threshold: f64) -> BettiNumbers {
348    if signal.is_empty() {
349        return BettiNumbers {
350            b0: 0,
351            b1: 0,
352            b2: 0,
353        };
354    }
355    // Connected components = contiguous intervals where signal ≤ threshold
356    let below: Vec<bool> = signal.iter().map(|&v| v <= threshold).collect();
357    let mut b0 = 0usize;
358    let mut in_component = false;
359    for &b in &below {
360        if b && !in_component {
361            b0 += 1;
362            in_component = true;
363        } else if !b {
364            in_component = false;
365        }
366    }
367    // In 1-D there are no loops
368    BettiNumbers { b0, b1: 0, b2: 0 }
369}
370
371// ─────────────────────────────────────────────────────────────────────────────
372// Persistence entropy
373// ─────────────────────────────────────────────────────────────────────────────
374
375/// Entropy of a persistence diagram.
376///
377/// H = −Σ (lᵢ/L) log(lᵢ/L) where lᵢ = persistence of interval i and
378/// L = Σ lᵢ.  Essential (infinite) intervals are excluded.
379pub fn persistence_entropy(diagram: &[PersistentInterval]) -> f64 {
380    let lifetimes: Vec<f64> = diagram
381        .iter()
382        .filter(|p| !p.is_essential() && p.persistence() > 0.0)
383        .map(|p| p.persistence())
384        .collect();
385    if lifetimes.is_empty() {
386        return 0.0;
387    }
388    let total: f64 = lifetimes.iter().sum();
389    if total <= 0.0 {
390        return 0.0;
391    }
392    -lifetimes
393        .iter()
394        .map(|&l| {
395            let p = l / total;
396            if p > 0.0 { p * p.ln() } else { 0.0 }
397        })
398        .sum::<f64>()
399}
400
401// ─────────────────────────────────────────────────────────────────────────────
402// VietorisRips struct
403// ─────────────────────────────────────────────────────────────────────────────
404
405/// Vietoris–Rips complex construction from a 2-D point cloud.
406///
407/// The complex includes all simplices (vertices, edges, triangles) whose
408/// maximum pairwise distance is at most `epsilon`.
409#[derive(Debug, Clone)]
410pub struct VietorisRips {
411    /// The 2-D point cloud.
412    pub points: Vec<[f64; 2]>,
413    /// Maximum edge length (epsilon radius).
414    pub epsilon: f64,
415}
416
417impl VietorisRips {
418    /// Create a new `VietorisRips` complex from a point cloud with radius `epsilon`.
419    pub fn new(points: Vec<[f64; 2]>, epsilon: f64) -> Self {
420        Self { points, epsilon }
421    }
422
423    /// Euclidean distance between two 2-D points.
424    fn dist(a: &[f64; 2], b: &[f64; 2]) -> f64 {
425        let dx = a[0] - b[0];
426        let dy = a[1] - b[1];
427        (dx * dx + dy * dy).sqrt()
428    }
429
430    /// All edges (pairs) within `epsilon` distance.
431    ///
432    /// Returns a list of `(i, j, distance)` tuples.
433    pub fn edges(&self) -> Vec<(usize, usize, f64)> {
434        let n = self.points.len();
435        let mut result = Vec::new();
436        for i in 0..n {
437            for j in (i + 1)..n {
438                let d = Self::dist(&self.points[i], &self.points[j]);
439                if d <= self.epsilon {
440                    result.push((i, j, d));
441                }
442            }
443        }
444        result
445    }
446
447    /// All triangles (triples) where every pair is within `epsilon`.
448    ///
449    /// Returns a list of `(i, j, k)` index triples.
450    pub fn triangles(&self) -> Vec<(usize, usize, usize)> {
451        let n = self.points.len();
452        let mut result = Vec::new();
453        for i in 0..n {
454            for j in (i + 1)..n {
455                if Self::dist(&self.points[i], &self.points[j]) > self.epsilon {
456                    continue;
457                }
458                for k in (j + 1)..n {
459                    if Self::dist(&self.points[i], &self.points[k]) <= self.epsilon
460                        && Self::dist(&self.points[j], &self.points[k]) <= self.epsilon
461                    {
462                        result.push((i, j, k));
463                    }
464                }
465            }
466        }
467        result
468    }
469
470    /// Number of vertices (all points are 0-simplices).
471    pub fn n_vertices(&self) -> usize {
472        self.points.len()
473    }
474
475    /// Number of edges within `epsilon`.
476    pub fn n_edges(&self) -> usize {
477        self.edges().len()
478    }
479
480    /// Number of triangles (2-simplices) within `epsilon`.
481    pub fn n_triangles(&self) -> usize {
482        self.triangles().len()
483    }
484
485    /// Euler characteristic χ = V − E + F.
486    pub fn euler_characteristic(&self) -> i64 {
487        let v = self.n_vertices() as i64;
488        let e = self.n_edges() as i64;
489        let f = self.n_triangles() as i64;
490        v - e + f
491    }
492
493    /// Betti numbers via the Euler formula and a union-find for β₀.
494    ///
495    /// β₀ is the number of connected components.
496    /// β₁ is estimated as β₁ = E − V + β₀ (first Betti number from spanning tree).
497    /// β₂ is estimated from the Euler characteristic.
498    pub fn betti_numbers(&self) -> BettiNumbers {
499        let n = self.points.len();
500        if n == 0 {
501            return BettiNumbers {
502                b0: 0,
503                b1: 0,
504                b2: 0,
505            };
506        }
507        let edges = self.edges();
508        let triangles = self.triangles();
509        let mut uf = UnionFind::new(n);
510        for (i, j, _) in &edges {
511            uf.union(*i, *j);
512        }
513        // β₀: count distinct roots
514        let b0 = (0..n).filter(|&i| uf.find(i) == i).count();
515        // β₁ = E − V + β₀ (number of independent cycles)
516        let b1 = (edges.len() as i64 - n as i64 + b0 as i64).max(0) as usize;
517        // β₂ estimated from Euler characteristic: χ = β₀ − β₁ + β₂
518        let chi = self.euler_characteristic();
519        let b2 = (chi - b0 as i64 + b1 as i64).max(0) as usize;
520        let _ = triangles; // used indirectly via euler_characteristic
521        BettiNumbers { b0, b1, b2 }
522    }
523
524    /// Compute the 0-dimensional persistent homology (H₀) via Kruskal.
525    pub fn persistent_h0(&self) -> Vec<PersistentInterval> {
526        vietoris_rips_h0(&self.points, self.epsilon)
527    }
528}
529
530// ─────────────────────────────────────────────────────────────────────────────
531// SimplexTree struct
532// ─────────────────────────────────────────────────────────────────────────────
533
534/// A simplex with its filtration value.
535#[derive(Debug, Clone)]
536pub struct FilteredSimplex {
537    /// Sorted vertex indices.
538    pub vertices: Vec<usize>,
539    /// Filtration value at which this simplex enters.
540    pub filtration: f64,
541}
542
543impl FilteredSimplex {
544    /// Dimension of this simplex (0=vertex, 1=edge, 2=triangle, …).
545    pub fn dim(&self) -> usize {
546        self.vertices.len().saturating_sub(1)
547    }
548}
549
550/// Simplex tree data structure for filtration-based TDA.
551///
552/// Stores all simplices sorted by filtration value, supporting
553/// insertion, lookup, and filtration-order iteration.
554#[derive(Debug, Clone, Default)]
555pub struct SimplexTree {
556    /// All simplices, in arbitrary insertion order.
557    pub simplices: Vec<FilteredSimplex>,
558}
559
560impl SimplexTree {
561    /// Create an empty `SimplexTree`.
562    pub fn new() -> Self {
563        Self::default()
564    }
565
566    /// Insert a simplex defined by `vertices` at filtration value `f`.
567    ///
568    /// Vertices are sorted before insertion.
569    pub fn insert(&mut self, mut vertices: Vec<usize>, f: f64) {
570        vertices.sort_unstable();
571        self.simplices.push(FilteredSimplex {
572            vertices,
573            filtration: f,
574        });
575    }
576
577    /// Build a `SimplexTree` from a `VietorisRips` complex.
578    ///
579    /// Inserts all vertices (at f=0), edges, and triangles at their
580    /// respective filtration values (maximum edge length).
581    pub fn from_vietoris_rips(vr: &VietorisRips) -> Self {
582        let mut tree = SimplexTree::new();
583        // Vertices at filtration 0
584        for i in 0..vr.points.len() {
585            tree.insert(vec![i], 0.0);
586        }
587        // Edges
588        for (i, j, d) in vr.edges() {
589            tree.insert(vec![i, j], d);
590        }
591        // Triangles (filtration = max edge)
592        for (i, j, k) in vr.triangles() {
593            let d01 = VietorisRips::dist(&vr.points[i], &vr.points[j]);
594            let d02 = VietorisRips::dist(&vr.points[i], &vr.points[k]);
595            let d12 = VietorisRips::dist(&vr.points[j], &vr.points[k]);
596            let f = d01.max(d02).max(d12);
597            tree.insert(vec![i, j, k], f);
598        }
599        tree
600    }
601
602    /// Return all simplices sorted by filtration value (ascending).
603    pub fn sorted_simplices(&self) -> Vec<&FilteredSimplex> {
604        let mut v: Vec<&FilteredSimplex> = self.simplices.iter().collect();
605        v.sort_by(|a, b| {
606            a.filtration
607                .partial_cmp(&b.filtration)
608                .unwrap_or(std::cmp::Ordering::Equal)
609        });
610        v
611    }
612
613    /// Number of simplices of dimension `d`.
614    pub fn count_dim(&self, d: usize) -> usize {
615        self.simplices.iter().filter(|s| s.dim() == d).count()
616    }
617
618    /// Filtration threshold that includes all simplices (maximum filtration value).
619    pub fn max_filtration(&self) -> f64 {
620        self.simplices
621            .iter()
622            .map(|s| s.filtration)
623            .fold(0.0_f64, f64::max)
624    }
625}
626
627// ─────────────────────────────────────────────────────────────────────────────
628// PersistenceDiagram struct
629// ─────────────────────────────────────────────────────────────────────────────
630
631/// A persistence diagram: a collection of birth-death pairs.
632///
633/// Each pair represents a topological feature that appears at `birth` and
634/// disappears at `death` in a filtration.
635#[derive(Debug, Clone, Default)]
636pub struct PersistenceDiagram {
637    /// The birth-death pairs (birth, death, dimension).
638    pub pairs: Vec<PersistentInterval>,
639}
640
641impl PersistenceDiagram {
642    /// Create an empty `PersistenceDiagram`.
643    pub fn new() -> Self {
644        Self::default()
645    }
646
647    /// Add a birth-death pair to the diagram.
648    pub fn add(&mut self, birth: f64, death: f64, dimension: usize) {
649        self.pairs.push(PersistentInterval {
650            birth,
651            death,
652            dimension,
653        });
654    }
655
656    /// Build a persistence diagram from the 0-dimensional VR filtration.
657    pub fn from_vietoris_rips_h0(vr: &VietorisRips) -> Self {
658        let intervals = vr.persistent_h0();
659        PersistenceDiagram { pairs: intervals }
660    }
661
662    /// Filter pairs by homological dimension.
663    pub fn pairs_in_dim(&self, dim: usize) -> Vec<&PersistentInterval> {
664        self.pairs.iter().filter(|p| p.dimension == dim).collect()
665    }
666
667    /// Maximum persistence (lifetime) over all finite pairs.
668    pub fn max_persistence(&self) -> f64 {
669        self.pairs
670            .iter()
671            .filter(|p| !p.is_essential())
672            .map(|p| p.persistence())
673            .fold(0.0_f64, f64::max)
674    }
675
676    /// Number of essential (infinite) bars.
677    pub fn n_essential(&self) -> usize {
678        self.pairs.iter().filter(|p| p.is_essential()).count()
679    }
680
681    /// Persistence entropy of the diagram (in nats).
682    pub fn entropy(&self) -> f64 {
683        persistence_entropy(&self.pairs)
684    }
685
686    /// Total persistence Σ (death − birth) for finite pairs.
687    pub fn total_persistence(&self) -> f64 {
688        self.pairs
689            .iter()
690            .filter(|p| !p.is_essential())
691            .map(|p| p.persistence())
692            .sum()
693    }
694}
695
696// ─────────────────────────────────────────────────────────────────────────────
697// BarcodeSummary struct
698// ─────────────────────────────────────────────────────────────────────────────
699
700/// Summary statistics derived from a persistence barcode.
701#[derive(Debug, Clone)]
702pub struct BarcodeSummary {
703    /// Total persistence: Σ (death − birth) for finite pairs.
704    pub total_persistence: f64,
705    /// Number of features with persistence ≥ `threshold`.
706    pub n_long_features: usize,
707    /// Persistence entropy.
708    pub entropy: f64,
709    /// Maximum persistence over all finite pairs.
710    pub max_persistence: f64,
711    /// Number of essential (infinite) features.
712    pub n_essential: usize,
713    /// Mean persistence of finite pairs; 0 if none.
714    pub mean_persistence: f64,
715}
716
717impl BarcodeSummary {
718    /// Compute a `BarcodeSummary` from a persistence diagram.
719    ///
720    /// `long_threshold` determines the cutoff for "long" features.
721    pub fn from_diagram(diagram: &PersistenceDiagram, long_threshold: f64) -> Self {
722        let finite: Vec<f64> = diagram
723            .pairs
724            .iter()
725            .filter(|p| !p.is_essential())
726            .map(|p| p.persistence())
727            .collect();
728        let total_persistence = finite.iter().sum::<f64>();
729        let n_long_features = finite.iter().filter(|&&p| p >= long_threshold).count();
730        let entropy = diagram.entropy();
731        let max_persistence = finite.iter().cloned().fold(0.0_f64, f64::max);
732        let n_essential = diagram.n_essential();
733        let mean_persistence = if finite.is_empty() {
734            0.0
735        } else {
736            total_persistence / finite.len() as f64
737        };
738        BarcodeSummary {
739            total_persistence,
740            n_long_features,
741            entropy,
742            max_persistence,
743            n_essential,
744            mean_persistence,
745        }
746    }
747
748    /// Normalised total persistence: total / max (0 if max is 0).
749    pub fn normalised_total(&self) -> f64 {
750        if self.max_persistence < 1e-15 {
751            0.0
752        } else {
753            self.total_persistence / self.max_persistence
754        }
755    }
756}
757
758// ─────────────────────────────────────────────────────────────────────────────
759// MapperGraph struct
760// ─────────────────────────────────────────────────────────────────────────────
761
762/// A node in a Mapper graph.
763///
764/// Each node corresponds to a cluster of points from one cover set.
765#[derive(Debug, Clone)]
766pub struct MapperNode {
767    /// Indices of points in the original point cloud assigned to this node.
768    pub point_indices: Vec<usize>,
769    /// Cover interval index that generated this node.
770    pub cover_index: usize,
771}
772
773/// Mapper graph: nodes (clusters) connected when adjacent cover intervals
774/// share common points.
775///
776/// Implements a simplified version of the Mapper algorithm:
777/// 1. Cover the filter function range with overlapping intervals.
778/// 2. Restrict the point cloud to each interval.
779/// 3. Cluster each restriction (here: single-linkage at `cluster_eps`).
780/// 4. Connect clusters from adjacent intervals that share points.
781#[derive(Debug, Clone, Default)]
782pub struct MapperGraph {
783    /// Nodes of the graph.
784    pub nodes: Vec<MapperNode>,
785    /// Edges between nodes: `(node_i, node_j)`.
786    pub edges: Vec<(usize, usize)>,
787}
788
789impl MapperGraph {
790    /// Build a `MapperGraph` from a point cloud using a 1-D filter function.
791    ///
792    /// # Parameters
793    /// - `points`: 2-D point cloud.
794    /// - `filter_values`: scalar filter value for each point (e.g., x-coordinate).
795    /// - `n_intervals`: number of cover intervals.
796    /// - `overlap`: fractional overlap between adjacent intervals (in \[0, 1)).
797    /// - `cluster_eps`: distance threshold for single-linkage clustering within
798    ///   each cover interval.
799    pub fn build(
800        points: &[[f64; 2]],
801        filter_values: &[f64],
802        n_intervals: usize,
803        overlap: f64,
804        cluster_eps: f64,
805    ) -> Self {
806        if points.is_empty() || n_intervals == 0 {
807            return Self::default();
808        }
809        let min_f = filter_values.iter().cloned().fold(f64::INFINITY, f64::min);
810        let max_f = filter_values
811            .iter()
812            .cloned()
813            .fold(f64::NEG_INFINITY, f64::max);
814        let range = (max_f - min_f).max(1e-15);
815        let step = range / n_intervals as f64;
816        let half_overlap = step * overlap.clamp(0.0, 0.99) / 2.0;
817
818        let mut graph = MapperGraph::default();
819
820        // For each cover interval, find the points inside, then cluster them
821        for k in 0..n_intervals {
822            let lo = min_f + k as f64 * step - half_overlap;
823            let hi = min_f + (k + 1) as f64 * step + half_overlap;
824
825            // Points in this interval
826            let indices_in: Vec<usize> = (0..points.len())
827                .filter(|&i| filter_values[i] >= lo && filter_values[i] <= hi)
828                .collect();
829
830            if indices_in.is_empty() {
831                continue;
832            }
833
834            // Single-linkage clustering: union-find on the sub-cloud
835            let m = indices_in.len();
836            let mut uf = UnionFind::new(m);
837            for a in 0..m {
838                for b in (a + 1)..m {
839                    let ia = indices_in[a];
840                    let ib = indices_in[b];
841                    let dx = points[ia][0] - points[ib][0];
842                    let dy = points[ia][1] - points[ib][1];
843                    let d = (dx * dx + dy * dy).sqrt();
844                    if d <= cluster_eps {
845                        uf.union(a, b);
846                    }
847                }
848            }
849
850            // Group by cluster root
851            let mut cluster_map: std::collections::HashMap<usize, Vec<usize>> =
852                std::collections::HashMap::new();
853            for a in 0..m {
854                let root = uf.find(a);
855                cluster_map.entry(root).or_default().push(indices_in[a]);
856            }
857
858            // Add one node per cluster
859            for (_root, members) in cluster_map {
860                graph.nodes.push(MapperNode {
861                    point_indices: members,
862                    cover_index: k,
863                });
864            }
865        }
866
867        // Connect nodes that share at least one point index
868        let n_nodes = graph.nodes.len();
869        for i in 0..n_nodes {
870            for j in (i + 1)..n_nodes {
871                let set_i: std::collections::HashSet<usize> =
872                    graph.nodes[i].point_indices.iter().copied().collect();
873                let shared = graph.nodes[j]
874                    .point_indices
875                    .iter()
876                    .any(|p| set_i.contains(p));
877                if shared {
878                    graph.edges.push((i, j));
879                }
880            }
881        }
882
883        graph
884    }
885
886    /// Number of nodes in the graph.
887    pub fn n_nodes(&self) -> usize {
888        self.nodes.len()
889    }
890
891    /// Number of edges in the graph.
892    pub fn n_edges(&self) -> usize {
893        self.edges.len()
894    }
895
896    /// Degree of node `i` (number of edges incident to it).
897    pub fn degree(&self, i: usize) -> usize {
898        self.edges
899            .iter()
900            .filter(|(a, b)| *a == i || *b == i)
901            .count()
902    }
903
904    /// Total number of points across all nodes (with possible duplicates from overlap).
905    pub fn total_point_count(&self) -> usize {
906        self.nodes.iter().map(|n| n.point_indices.len()).sum()
907    }
908}
909
910// ─────────────────────────────────────────────────────────────────────────────
911// Tests
912// ─────────────────────────────────────────────────────────────────────────────
913
914#[cfg(test)]
915mod tests {
916    use super::*;
917
918    // ── UnionFind ────────────────────────────────────────────────────────────
919
920    #[test]
921    fn test_uf_new_singletons() {
922        let mut uf = UnionFind::new(5);
923        for i in 0..5 {
924            assert_eq!(uf.find(i), i);
925        }
926    }
927
928    #[test]
929    fn test_uf_union_merges() {
930        let mut uf = UnionFind::new(4);
931        assert!(uf.union(0, 1));
932        assert_eq!(uf.find(0), uf.find(1));
933    }
934
935    #[test]
936    fn test_uf_union_idempotent() {
937        let mut uf = UnionFind::new(4);
938        uf.union(0, 1);
939        assert!(
940            !uf.union(0, 1),
941            "second union of same set should return false"
942        );
943    }
944
945    #[test]
946    fn test_uf_union_chain() {
947        let mut uf = UnionFind::new(4);
948        uf.union(0, 1);
949        uf.union(1, 2);
950        uf.union(2, 3);
951        let r0 = uf.find(0);
952        assert_eq!(uf.find(3), r0);
953    }
954
955    #[test]
956    fn test_uf_different_components() {
957        let mut uf = UnionFind::new(4);
958        uf.union(0, 1);
959        uf.union(2, 3);
960        assert_ne!(uf.find(0), uf.find(2));
961    }
962
963    #[test]
964    fn test_uf_path_compression() {
965        let mut uf = UnionFind::new(6);
966        for i in 0..5 {
967            uf.union(i, i + 1);
968        }
969        let root = uf.find(5);
970        assert_eq!(uf.find(0), root);
971    }
972
973    #[test]
974    fn test_uf_union_all_returns_true_n_minus_1_times() {
975        let n = 5;
976        let mut uf = UnionFind::new(n);
977        let mut merges = 0;
978        for i in 1..n {
979            if uf.union(0, i) {
980                merges += 1;
981            }
982        }
983        assert_eq!(merges, n - 1);
984    }
985
986    // ── SimplexEdge ──────────────────────────────────────────────────────────
987
988    #[test]
989    fn test_simplex_edge_fields() {
990        let e = SimplexEdge {
991            v0: 1,
992            v1: 3,
993            weight: 2.5,
994        };
995        assert_eq!(e.v0, 1);
996        assert_eq!(e.v1, 3);
997        assert!((e.weight - 2.5).abs() < 1e-12);
998    }
999
1000    // ── PersistentInterval ───────────────────────────────────────────────────
1001
1002    #[test]
1003    fn test_persistent_interval_persistence() {
1004        let pi = PersistentInterval {
1005            birth: 1.0,
1006            death: 3.0,
1007            dimension: 0,
1008        };
1009        assert!((pi.persistence() - 2.0).abs() < 1e-12);
1010    }
1011
1012    #[test]
1013    fn test_persistent_interval_essential() {
1014        let pi = PersistentInterval {
1015            birth: 0.0,
1016            death: f64::INFINITY,
1017            dimension: 0,
1018        };
1019        assert!(pi.is_essential());
1020    }
1021
1022    #[test]
1023    fn test_persistent_interval_not_essential() {
1024        let pi = PersistentInterval {
1025            birth: 0.0,
1026            death: 1.0,
1027            dimension: 0,
1028        };
1029        assert!(!pi.is_essential());
1030    }
1031
1032    // ── vietoris_rips_h0 ─────────────────────────────────────────────────────
1033
1034    #[test]
1035    fn test_h0_empty() {
1036        let pts: [[f64; 2]; 0] = [];
1037        let result = vietoris_rips_h0(&pts, 10.0);
1038        assert!(result.is_empty());
1039    }
1040
1041    #[test]
1042    fn test_h0_single_point() {
1043        let pts = [[0.0, 0.0]];
1044        let result = vietoris_rips_h0(&pts, 10.0);
1045        assert_eq!(result.len(), 1);
1046        assert!(result[0].is_essential());
1047    }
1048
1049    #[test]
1050    fn test_h0_two_close_points() {
1051        let pts = [[0.0, 0.0], [0.1, 0.0]];
1052        let result = vietoris_rips_h0(&pts, 10.0);
1053        // One interval dies, one essential
1054        assert_eq!(result.len(), 2);
1055        let essential_count = result.iter().filter(|p| p.is_essential()).count();
1056        assert_eq!(essential_count, 1);
1057    }
1058
1059    #[test]
1060    fn test_h0_all_dimension_zero() {
1061        let pts = [[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]];
1062        let result = vietoris_rips_h0(&pts, 10.0);
1063        for pi in &result {
1064            assert_eq!(pi.dimension, 0);
1065        }
1066    }
1067
1068    #[test]
1069    fn test_h0_two_far_points_below_max_r() {
1070        let pts = [[0.0, 0.0], [100.0, 0.0]];
1071        let result = vietoris_rips_h0(&pts, 50.0);
1072        // They don't merge within max_r=50, so both stay alive
1073        let finite_count = result.iter().filter(|p| !p.is_essential()).count();
1074        // No merge happened within max_r
1075        assert_eq!(finite_count, 0);
1076    }
1077
1078    // ── vietoris_rips_h1_approx ───────────────────────────────────────────────
1079
1080    #[test]
1081    fn test_h1_empty() {
1082        let pts: [[f64; 2]; 0] = [];
1083        let result = vietoris_rips_h1_approx(&pts, 10.0, 100);
1084        assert!(result.is_empty());
1085    }
1086
1087    #[test]
1088    fn test_h1_dimension_one() {
1089        let pts = [[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
1090        let result = vietoris_rips_h1_approx(&pts, 10.0, 10);
1091        for pi in &result {
1092            assert_eq!(pi.dimension, 1);
1093        }
1094    }
1095
1096    #[test]
1097    fn test_h1_triangle_has_loop() {
1098        // Triangle: 3 edges, spanning tree = 2, so 1 cycle
1099        let pts = [[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
1100        let result = vietoris_rips_h1_approx(&pts, 10.0, 3);
1101        assert_eq!(result.len(), 1);
1102    }
1103
1104    // ── bottleneck_distance ───────────────────────────────────────────────────
1105
1106    #[test]
1107    fn test_bottleneck_identical_diagrams() {
1108        let d = vec![
1109            PersistentInterval {
1110                birth: 0.0,
1111                death: 1.0,
1112                dimension: 0,
1113            },
1114            PersistentInterval {
1115                birth: 0.5,
1116                death: 2.0,
1117                dimension: 0,
1118            },
1119        ];
1120        let dist = bottleneck_distance(&d, &d);
1121        assert!(dist < 1e-12, "identical diagrams should have distance 0");
1122    }
1123
1124    #[test]
1125    fn test_bottleneck_empty_diagrams() {
1126        assert!((bottleneck_distance(&[], &[])).abs() < 1e-12);
1127    }
1128
1129    #[test]
1130    fn test_bottleneck_non_negative() {
1131        let a = vec![PersistentInterval {
1132            birth: 0.0,
1133            death: 1.0,
1134            dimension: 0,
1135        }];
1136        let b = vec![PersistentInterval {
1137            birth: 0.1,
1138            death: 1.2,
1139            dimension: 0,
1140        }];
1141        assert!(bottleneck_distance(&a, &b) >= 0.0);
1142    }
1143
1144    #[test]
1145    fn test_bottleneck_symmetry() {
1146        let a = vec![PersistentInterval {
1147            birth: 0.0,
1148            death: 1.0,
1149            dimension: 0,
1150        }];
1151        let b = vec![PersistentInterval {
1152            birth: 0.5,
1153            death: 2.0,
1154            dimension: 0,
1155        }];
1156        let dab = bottleneck_distance(&a, &b);
1157        let dba = bottleneck_distance(&b, &a);
1158        assert!((dab - dba).abs() < 1e-12);
1159    }
1160
1161    // ── wasserstein_distance_1 ────────────────────────────────────────────────
1162
1163    #[test]
1164    fn test_wasserstein_identical() {
1165        let d = vec![PersistentInterval {
1166            birth: 0.0,
1167            death: 2.0,
1168            dimension: 0,
1169        }];
1170        assert!(wasserstein_distance_1(&d, &d) < 1e-12);
1171    }
1172
1173    #[test]
1174    fn test_wasserstein_empty() {
1175        assert!(wasserstein_distance_1(&[], &[]).abs() < 1e-12);
1176    }
1177
1178    #[test]
1179    fn test_wasserstein_non_negative() {
1180        let a = vec![PersistentInterval {
1181            birth: 0.0,
1182            death: 1.0,
1183            dimension: 0,
1184        }];
1185        let b = vec![PersistentInterval {
1186            birth: 0.2,
1187            death: 1.5,
1188            dimension: 0,
1189        }];
1190        assert!(wasserstein_distance_1(&a, &b) >= 0.0);
1191    }
1192
1193    // ── BettiNumbers & euler_characteristic ─────────────────────────────────
1194
1195    #[test]
1196    fn test_euler_characteristic_sphere() {
1197        // Topological 2-sphere: β0=1, β1=0, β2=1 → χ=2
1198        let b = BettiNumbers {
1199            b0: 1,
1200            b1: 0,
1201            b2: 1,
1202        };
1203        assert_eq!(euler_characteristic(&b), 2);
1204    }
1205
1206    #[test]
1207    fn test_euler_characteristic_torus() {
1208        // Torus: β0=1, β1=2, β2=1 → χ=0
1209        let b = BettiNumbers {
1210            b0: 1,
1211            b1: 2,
1212            b2: 1,
1213        };
1214        assert_eq!(euler_characteristic(&b), 0);
1215    }
1216
1217    #[test]
1218    fn test_euler_characteristic_point() {
1219        let b = BettiNumbers {
1220            b0: 1,
1221            b1: 0,
1222            b2: 0,
1223        };
1224        assert_eq!(euler_characteristic(&b), 1);
1225    }
1226
1227    #[test]
1228    fn test_euler_characteristic_circle() {
1229        // Circle: β0=1, β1=1, β2=0 → χ=0
1230        let b = BettiNumbers {
1231            b0: 1,
1232            b1: 1,
1233            b2: 0,
1234        };
1235        assert_eq!(euler_characteristic(&b), 0);
1236    }
1237
1238    // ── cubical_complex_1d ────────────────────────────────────────────────────
1239
1240    #[test]
1241    fn test_cubical_1d_empty() {
1242        let b = cubical_complex_1d(&[], 0.5);
1243        assert_eq!(b.b0, 0);
1244    }
1245
1246    #[test]
1247    fn test_cubical_1d_all_below() {
1248        let signal = [0.1, 0.2, 0.3];
1249        let b = cubical_complex_1d(&signal, 1.0);
1250        assert_eq!(b.b0, 1);
1251        assert_eq!(b.b1, 0);
1252        assert_eq!(b.b2, 0);
1253    }
1254
1255    #[test]
1256    fn test_cubical_1d_two_components() {
1257        let signal = [0.1, 0.9, 0.1];
1258        let b = cubical_complex_1d(&signal, 0.5);
1259        assert_eq!(b.b0, 2);
1260    }
1261
1262    #[test]
1263    fn test_cubical_1d_none_below() {
1264        let signal = [1.0, 2.0, 3.0];
1265        let b = cubical_complex_1d(&signal, 0.5);
1266        assert_eq!(b.b0, 0);
1267    }
1268
1269    #[test]
1270    fn test_cubical_1d_no_loops() {
1271        let signal = [0.1, 0.2, 0.3, 0.4];
1272        let b = cubical_complex_1d(&signal, 1.0);
1273        assert_eq!(b.b1, 0);
1274        assert_eq!(b.b2, 0);
1275    }
1276
1277    // ── persistence_entropy ───────────────────────────────────────────────────
1278
1279    #[test]
1280    fn test_persistence_entropy_empty() {
1281        assert_eq!(persistence_entropy(&[]), 0.0);
1282    }
1283
1284    #[test]
1285    fn test_persistence_entropy_single_bar() {
1286        let d = vec![PersistentInterval {
1287            birth: 0.0,
1288            death: 1.0,
1289            dimension: 0,
1290        }];
1291        // Single bar: p=1, so -1*ln(1) = 0
1292        assert!(persistence_entropy(&d).abs() < 1e-12);
1293    }
1294
1295    #[test]
1296    fn test_persistence_entropy_non_negative() {
1297        let d = vec![
1298            PersistentInterval {
1299                birth: 0.0,
1300                death: 1.0,
1301                dimension: 0,
1302            },
1303            PersistentInterval {
1304                birth: 0.5,
1305                death: 2.0,
1306                dimension: 0,
1307            },
1308            PersistentInterval {
1309                birth: 1.0,
1310                death: 3.0,
1311                dimension: 0,
1312            },
1313        ];
1314        assert!(persistence_entropy(&d) >= 0.0);
1315    }
1316
1317    #[test]
1318    fn test_persistence_entropy_essential_excluded() {
1319        let d = vec![
1320            PersistentInterval {
1321                birth: 0.0,
1322                death: f64::INFINITY,
1323                dimension: 0,
1324            },
1325            PersistentInterval {
1326                birth: 0.0,
1327                death: 1.0,
1328                dimension: 0,
1329            },
1330        ];
1331        // Should not panic or give NaN
1332        let h = persistence_entropy(&d);
1333        assert!(h.is_finite());
1334    }
1335
1336    #[test]
1337    fn test_persistence_entropy_equal_bars_maximizes() {
1338        // Two equal-length bars → maximum entropy for 2 events = ln(2)
1339        let d = vec![
1340            PersistentInterval {
1341                birth: 0.0,
1342                death: 1.0,
1343                dimension: 0,
1344            },
1345            PersistentInterval {
1346                birth: 0.0,
1347                death: 1.0,
1348                dimension: 0,
1349            },
1350        ];
1351        let h = persistence_entropy(&d);
1352        assert!((h - 2_f64.ln()).abs() < 1e-10);
1353    }
1354
1355    // ── VietorisRips struct ───────────────────────────────────────────────────
1356
1357    #[test]
1358    fn test_vr_empty_cloud() {
1359        let vr = VietorisRips::new(vec![], 1.0);
1360        assert_eq!(vr.n_vertices(), 0);
1361        assert_eq!(vr.n_edges(), 0);
1362    }
1363
1364    #[test]
1365    fn test_vr_single_point() {
1366        let vr = VietorisRips::new(vec![[0.0, 0.0]], 1.0);
1367        assert_eq!(vr.n_vertices(), 1);
1368        assert_eq!(vr.n_edges(), 0);
1369    }
1370
1371    #[test]
1372    fn test_vr_two_close_points_have_edge() {
1373        let vr = VietorisRips::new(vec![[0.0, 0.0], [0.5, 0.0]], 1.0);
1374        assert_eq!(vr.n_edges(), 1);
1375    }
1376
1377    #[test]
1378    fn test_vr_two_far_points_no_edge() {
1379        let vr = VietorisRips::new(vec![[0.0, 0.0], [10.0, 0.0]], 1.0);
1380        assert_eq!(vr.n_edges(), 0);
1381    }
1382
1383    #[test]
1384    fn test_vr_triangle_has_one_triangle() {
1385        let vr = VietorisRips::new(vec![[0.0, 0.0], [1.0, 0.0], [0.5, 0.866]], 2.0);
1386        assert_eq!(vr.n_triangles(), 1);
1387    }
1388
1389    #[test]
1390    fn test_vr_betti_numbers_four_isolated() {
1391        // 4 isolated points: β₀=4, β₁=0
1392        let pts = vec![[0.0, 0.0], [10.0, 0.0], [0.0, 10.0], [10.0, 10.0]];
1393        let vr = VietorisRips::new(pts, 1.0);
1394        let b = vr.betti_numbers();
1395        assert_eq!(b.b0, 4);
1396        assert_eq!(b.b1, 0);
1397    }
1398
1399    #[test]
1400    fn test_vr_betti_numbers_connected_pair() {
1401        let vr = VietorisRips::new(vec![[0.0, 0.0], [0.5, 0.0]], 1.0);
1402        let b = vr.betti_numbers();
1403        assert_eq!(b.b0, 1);
1404    }
1405
1406    #[test]
1407    fn test_vr_euler_characteristic_points_only() {
1408        // 5 points, no edges: χ = 5
1409        let pts: Vec<[f64; 2]> = (0..5).map(|i| [i as f64 * 10.0, 0.0]).collect();
1410        let vr = VietorisRips::new(pts, 1.0);
1411        assert_eq!(vr.euler_characteristic(), 5);
1412    }
1413
1414    #[test]
1415    fn test_vr_persistent_h0_returns_correct_count() {
1416        let pts = vec![[0.0, 0.0], [0.5, 0.0], [5.0, 0.0]];
1417        let vr = VietorisRips::new(pts, 10.0);
1418        let intervals = vr.persistent_h0();
1419        // Should have essential count = 1
1420        let n_essential = intervals.iter().filter(|p| p.is_essential()).count();
1421        assert_eq!(n_essential, 1);
1422    }
1423
1424    // ── SimplexTree struct ────────────────────────────────────────────────────
1425
1426    #[test]
1427    fn test_simplex_tree_empty() {
1428        let st = SimplexTree::new();
1429        assert_eq!(st.simplices.len(), 0);
1430    }
1431
1432    #[test]
1433    fn test_simplex_tree_insert_vertex() {
1434        let mut st = SimplexTree::new();
1435        st.insert(vec![0], 0.0);
1436        assert_eq!(st.count_dim(0), 1);
1437    }
1438
1439    #[test]
1440    fn test_simplex_tree_insert_edge() {
1441        let mut st = SimplexTree::new();
1442        st.insert(vec![0, 1], 1.5);
1443        assert_eq!(st.count_dim(1), 1);
1444    }
1445
1446    #[test]
1447    fn test_simplex_tree_max_filtration() {
1448        let mut st = SimplexTree::new();
1449        st.insert(vec![0], 0.0);
1450        st.insert(vec![1], 0.0);
1451        st.insert(vec![0, 1], 2.5);
1452        assert!((st.max_filtration() - 2.5).abs() < 1e-12);
1453    }
1454
1455    #[test]
1456    fn test_simplex_tree_sorted_simplices_order() {
1457        let mut st = SimplexTree::new();
1458        st.insert(vec![0, 1], 2.0);
1459        st.insert(vec![0], 0.0);
1460        st.insert(vec![1], 0.0);
1461        let sorted = st.sorted_simplices();
1462        for w in sorted.windows(2) {
1463            assert!(w[0].filtration <= w[1].filtration);
1464        }
1465    }
1466
1467    #[test]
1468    fn test_simplex_tree_from_vr() {
1469        let vr = VietorisRips::new(vec![[0.0, 0.0], [0.5, 0.0], [0.25, 0.5]], 2.0);
1470        let st = SimplexTree::from_vietoris_rips(&vr);
1471        // 3 vertices, 3 edges, 1 triangle
1472        assert_eq!(st.count_dim(0), 3);
1473        assert_eq!(st.count_dim(1), 3);
1474        assert_eq!(st.count_dim(2), 1);
1475    }
1476
1477    // ── PersistenceDiagram struct ─────────────────────────────────────────────
1478
1479    #[test]
1480    fn test_persistence_diagram_add_and_count() {
1481        let mut pd = PersistenceDiagram::new();
1482        pd.add(0.0, 1.0, 0);
1483        pd.add(0.5, 2.0, 1);
1484        assert_eq!(pd.pairs.len(), 2);
1485    }
1486
1487    #[test]
1488    fn test_persistence_diagram_pairs_in_dim() {
1489        let mut pd = PersistenceDiagram::new();
1490        pd.add(0.0, 1.0, 0);
1491        pd.add(0.5, 2.0, 1);
1492        pd.add(1.0, 3.0, 0);
1493        assert_eq!(pd.pairs_in_dim(0).len(), 2);
1494        assert_eq!(pd.pairs_in_dim(1).len(), 1);
1495    }
1496
1497    #[test]
1498    fn test_persistence_diagram_max_persistence() {
1499        let mut pd = PersistenceDiagram::new();
1500        pd.add(0.0, 1.0, 0);
1501        pd.add(0.0, 3.0, 0);
1502        assert!((pd.max_persistence() - 3.0).abs() < 1e-12);
1503    }
1504
1505    #[test]
1506    fn test_persistence_diagram_n_essential() {
1507        let mut pd = PersistenceDiagram::new();
1508        pd.add(0.0, f64::INFINITY, 0);
1509        pd.add(0.5, 1.5, 0);
1510        assert_eq!(pd.n_essential(), 1);
1511    }
1512
1513    #[test]
1514    fn test_persistence_diagram_total_persistence() {
1515        let mut pd = PersistenceDiagram::new();
1516        pd.add(0.0, 1.0, 0);
1517        pd.add(0.0, 2.0, 0);
1518        assert!((pd.total_persistence() - 3.0).abs() < 1e-12);
1519    }
1520
1521    #[test]
1522    fn test_persistence_diagram_from_vr() {
1523        let vr = VietorisRips::new(vec![[0.0, 0.0], [1.0, 0.0]], 5.0);
1524        let pd = PersistenceDiagram::from_vietoris_rips_h0(&vr);
1525        assert!(!pd.pairs.is_empty());
1526        assert_eq!(pd.n_essential(), 1);
1527    }
1528
1529    // ── BarcodeSummary struct ─────────────────────────────────────────────────
1530
1531    #[test]
1532    fn test_barcode_summary_total_persistence() {
1533        let mut pd = PersistenceDiagram::new();
1534        pd.add(0.0, 1.0, 0);
1535        pd.add(0.0, 2.0, 0);
1536        let bs = BarcodeSummary::from_diagram(&pd, 0.5);
1537        assert!((bs.total_persistence - 3.0).abs() < 1e-12);
1538    }
1539
1540    #[test]
1541    fn test_barcode_summary_long_features() {
1542        let mut pd = PersistenceDiagram::new();
1543        pd.add(0.0, 0.3, 0); // short
1544        pd.add(0.0, 1.5, 0); // long
1545        pd.add(0.0, 2.0, 0); // long
1546        let bs = BarcodeSummary::from_diagram(&pd, 1.0);
1547        assert_eq!(bs.n_long_features, 2);
1548    }
1549
1550    #[test]
1551    fn test_barcode_summary_entropy_non_negative() {
1552        let mut pd = PersistenceDiagram::new();
1553        pd.add(0.0, 1.0, 0);
1554        pd.add(0.5, 2.0, 0);
1555        let bs = BarcodeSummary::from_diagram(&pd, 0.5);
1556        assert!(bs.entropy >= 0.0);
1557    }
1558
1559    #[test]
1560    fn test_barcode_summary_normalised_total() {
1561        let mut pd = PersistenceDiagram::new();
1562        pd.add(0.0, 1.0, 0);
1563        pd.add(0.0, 2.0, 0);
1564        let bs = BarcodeSummary::from_diagram(&pd, 0.5);
1565        // normalised_total = 3.0 / 2.0 = 1.5
1566        assert!((bs.normalised_total() - 1.5).abs() < 1e-10);
1567    }
1568
1569    #[test]
1570    fn test_barcode_summary_mean_persistence() {
1571        let mut pd = PersistenceDiagram::new();
1572        pd.add(0.0, 2.0, 0);
1573        pd.add(0.0, 4.0, 0);
1574        let bs = BarcodeSummary::from_diagram(&pd, 1.0);
1575        assert!((bs.mean_persistence - 3.0).abs() < 1e-12);
1576    }
1577
1578    // ── MapperGraph struct ────────────────────────────────────────────────────
1579
1580    #[test]
1581    fn test_mapper_empty_cloud() {
1582        let mg = MapperGraph::build(&[], &[], 3, 0.3, 0.5);
1583        assert_eq!(mg.n_nodes(), 0);
1584        assert_eq!(mg.n_edges(), 0);
1585    }
1586
1587    #[test]
1588    fn test_mapper_single_point() {
1589        let pts = [[0.0, 0.0]];
1590        let filter = [0.0];
1591        let mg = MapperGraph::build(&pts, &filter, 2, 0.2, 1.0);
1592        assert!(mg.n_nodes() >= 1);
1593    }
1594
1595    #[test]
1596    fn test_mapper_line_of_points() {
1597        // 10 collinear points
1598        let pts: Vec<[f64; 2]> = (0..10).map(|i| [i as f64, 0.0]).collect();
1599        let filter: Vec<f64> = pts.iter().map(|p| p[0]).collect();
1600        let mg = MapperGraph::build(&pts, &filter, 3, 0.4, 1.5);
1601        assert!(mg.n_nodes() >= 1);
1602    }
1603
1604    #[test]
1605    fn test_mapper_degree_non_negative() {
1606        let pts: Vec<[f64; 2]> = (0..6).map(|i| [i as f64, 0.0]).collect();
1607        let filter: Vec<f64> = pts.iter().map(|p| p[0]).collect();
1608        let mg = MapperGraph::build(&pts, &filter, 3, 0.5, 1.5);
1609        for i in 0..mg.n_nodes() {
1610            assert!(mg.degree(i) <= mg.n_edges());
1611        }
1612    }
1613
1614    #[test]
1615    fn test_mapper_total_point_count_ge_input() {
1616        let pts: Vec<[f64; 2]> = (0..5).map(|i| [i as f64, 0.0]).collect();
1617        let filter: Vec<f64> = pts.iter().map(|p| p[0]).collect();
1618        let mg = MapperGraph::build(&pts, &filter, 2, 0.5, 2.0);
1619        // With overlap, total can exceed n
1620        assert!(mg.total_point_count() >= 1);
1621    }
1622
1623    #[test]
1624    fn test_mapper_no_intervals_returns_empty() {
1625        let pts = [[0.0, 0.0], [1.0, 0.0]];
1626        let filter = [0.0, 1.0];
1627        let mg = MapperGraph::build(&pts, &filter, 0, 0.2, 0.5);
1628        assert_eq!(mg.n_nodes(), 0);
1629    }
1630}