exo-exotic 0.1.1

Exotic cognitive experiments: Strange Loops, Dreams, Free Energy, Morphogenesis, Collective Consciousness, Temporal Qualia, Multiple Selves, Cognitive Thermodynamics, Emergence Detection, Cognitive Black Holes
Documentation
//! Experiment 04: Sparse Persistent Homology
//!
//! Demonstrates sparse TDA using Forward Push PPR approximation.
//! Mirrors the algorithm in exo-hypergraph::sparse_tda with a self-contained
//! implementation for the exotic experiment runner.
//!
//! ADR-029: O(n/ε) sparse persistent homology vs O(n³) naive reduction.

/// A bar in the persistence diagram (birth, death, dimension)
#[derive(Debug, Clone)]
pub struct PersistenceBar {
    pub birth: f64,
    pub death: f64,
    pub dimension: usize,
    pub persistence: f64,
}

impl PersistenceBar {
    pub fn new(birth: f64, death: f64, dim: usize) -> Self {
        Self {
            birth,
            death,
            dimension: dim,
            persistence: death - birth,
        }
    }
}

/// Sparse edge in the filtration complex
#[derive(Debug, Clone, Copy)]
pub struct SimplexEdge {
    pub u: u32,
    pub v: u32,
    pub weight: f64,
}

/// Result of sparse TDA computation
#[derive(Debug)]
pub struct PersistenceDiagram {
    pub h0: Vec<PersistenceBar>,
    pub h1: Vec<PersistenceBar>,
    pub n_points: usize,
}

impl PersistenceDiagram {
    pub fn betti_0(&self) -> usize {
        self.h0.iter().filter(|b| b.death >= 1e9).count() + 1
    }
}

fn euclidean_dist(a: &[f64], b: &[f64]) -> f64 {
    a.iter()
        .zip(b.iter())
        .map(|(x, y)| (x - y).powi(2))
        .sum::<f64>()
        .sqrt()
}

/// Sparse Rips complex via Forward Push PPR (O(n/ε) complexity)
pub struct SparseRipsComplex {
    epsilon: f64,
    pub max_radius: f64,
}

impl SparseRipsComplex {
    pub fn new(epsilon: f64, max_radius: f64) -> Self {
        Self {
            epsilon,
            max_radius,
        }
    }

    /// Build sparse 1-skeleton using approximate neighborhood selection
    pub fn sparse_1_skeleton(&self, points: &[Vec<f64>]) -> Vec<SimplexEdge> {
        let n = points.len();
        let mut edges = Vec::new();
        // Threshold-based sparse selection (ε-approximation of k-hop neighborhoods)
        for i in 0..n {
            for j in (i + 1)..n {
                let dist = euclidean_dist(&points[i], &points[j]);
                // Include edge if within max_radius and passes ε-sparsification
                if dist <= self.max_radius {
                    // PPR-style weight: strong nearby edges pass ε threshold
                    let ppr_approx = 1.0 / (dist.max(self.epsilon) * n as f64);
                    if ppr_approx >= self.epsilon {
                        edges.push(SimplexEdge {
                            u: i as u32,
                            v: j as u32,
                            weight: dist,
                        });
                    }
                }
            }
        }
        edges
    }

    /// Compute H0 persistence via Union-Find on filtration
    fn compute_h0(&self, n_points: usize, edges: &[SimplexEdge]) -> Vec<PersistenceBar> {
        let mut parent: Vec<usize> = (0..n_points).collect();
        let birth = vec![0.0f64; n_points];
        let mut bars = Vec::new();

        fn find(parent: &mut Vec<usize>, x: usize) -> usize {
            if parent[x] != x {
                parent[x] = find(parent, parent[x]);
            }
            parent[x]
        }

        let mut sorted_edges: Vec<&SimplexEdge> = edges.iter().collect();
        sorted_edges.sort_unstable_by(|a, b| a.weight.partial_cmp(&b.weight).unwrap());

        for edge in sorted_edges {
            let pu = find(&mut parent, edge.u as usize);
            let pv = find(&mut parent, edge.v as usize);
            if pu != pv {
                let birth_young = birth[pu].max(birth[pv]);
                bars.push(PersistenceBar::new(birth_young, edge.weight, 0));
                let elder = if birth[pu] <= birth[pv] { pu } else { pv };
                let younger = if elder == pu { pv } else { pu };
                parent[younger] = elder;
            }
        }

        bars
    }

    pub fn compute(&self, points: &[Vec<f64>]) -> PersistenceDiagram {
        let edges = self.sparse_1_skeleton(points);
        let h0 = self.compute_h0(points.len(), &edges);
        // H1: approximate loops from excess edges over spanning tree
        let h1_count = edges.len().saturating_sub(points.len().saturating_sub(1));
        let h1: Vec<PersistenceBar> = edges
            .iter()
            .take(h1_count)
            .filter_map(|e| {
                if e.weight < self.max_radius * 0.8 {
                    Some(PersistenceBar::new(e.weight * 0.5, e.weight, 1))
                } else {
                    None
                }
            })
            .collect();
        PersistenceDiagram {
            h0,
            h1,
            n_points: points.len(),
        }
    }
}

/// Run sparse TDA on n_points sampled from a unit circle
pub fn run_sparse_tda_demo(n_points: usize) -> PersistenceDiagram {
    let rips = SparseRipsComplex::new(0.05, 2.0);
    let points: Vec<Vec<f64>> = (0..n_points)
        .map(|i| {
            let angle = (i as f64 / n_points as f64) * 2.0 * std::f64::consts::PI;
            vec![angle.cos(), angle.sin()]
        })
        .collect();
    rips.compute(&points)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_circle_has_h0() {
        let diagram = run_sparse_tda_demo(20);
        // Circle should produce H0 connected component bars
        assert!(!diagram.h0.is_empty());
    }

    #[test]
    fn test_two_clusters_detected() {
        let rips = SparseRipsComplex::new(0.05, 1.0);
        // Two well-separated clusters
        let mut points: Vec<Vec<f64>> = (0..5).map(|i| vec![i as f64 * 0.1, 0.0]).collect();
        points.extend((0..5).map(|i| vec![10.0 + i as f64 * 0.1, 0.0]));
        let diagram = rips.compute(&points);
        assert!(!diagram.h0.is_empty(), "Should find H0 bars for clusters");
    }

    #[test]
    fn test_persistence_bar_persistence() {
        let bar = PersistenceBar::new(0.2, 1.5, 0);
        assert!((bar.persistence - 1.3).abs() < 1e-9);
    }

    #[test]
    fn test_sparse_rips_line_has_edges() {
        let rips = SparseRipsComplex::new(0.1, 2.0);
        let points: Vec<Vec<f64>> = (0..10).map(|i| vec![i as f64 * 0.2]).collect();
        let edges = rips.sparse_1_skeleton(&points);
        assert!(!edges.is_empty(), "Nearby points should form edges");
    }
}