terminals-core 0.1.0

Core runtime primitives for Terminals OS: phase dynamics, AXON wire protocol, substrate engine, and sematonic types
Documentation
//! ConvergenceWitness โ€” Closed-Loop Synchronization Proof.
//!
//! The witness captures the substrate's convergence state AFTER a step.
//! This is the critical closure: witness uses current R, not stale.
//! Paper 6 ยง9.2: open loops break the Convergence-Realizability Identity.

use serde::{Deserialize, Serialize};

use super::atom::ComputeAtom;
use super::kuramoto::KuramotoProjection;
use crate::phase::kuramoto::order_parameter;

/// Convergence witness โ€” snapshot of substrate coherence at a step.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ConvergenceWitness {
    /// Kuramoto order parameter R in [0, 1].
    pub r: f32,
    /// Shannon entropy of substrate activation distribution.
    pub entropy: f32,
    /// Whether convergence criteria are met (R >= threshold).
    pub converged: bool,
    /// Step count at which this witness was taken.
    pub step: u32,
}

impl Default for ConvergenceWitness {
    fn default() -> Self {
        Self {
            r: 0.0,
            entropy: 0.0,
            converged: false,
            step: 0,
        }
    }
}

/// Default convergence threshold for R.
pub const CONVERGENCE_THRESHOLD: f32 = 0.9;

/// Compute a witness from the current atom states.
/// This MUST be called AFTER a coupling step โ€” it reads post-step phases.
pub fn compute_witness(atoms: &[ComputeAtom], step: u32) -> ConvergenceWitness {
    if atoms.is_empty() {
        return ConvergenceWitness::default();
    }

    // Extract phases from Kuramoto projections
    let phases: Vec<f32> = atoms
        .iter()
        .filter_map(|a| a.read_projection::<KuramotoProjection>())
        .map(|k| k.theta)
        .collect();

    if phases.is_empty() {
        return ConvergenceWitness {
            r: 0.0,
            entropy: 0.0,
            converged: false,
            step,
        };
    }

    // Compute R from the real phase::kuramoto module
    let op = order_parameter(&phases);

    // Compute activation entropy (Shannon) from expert gates if available
    let entropy = compute_activation_entropy(atoms);

    ConvergenceWitness {
        r: op.r,
        entropy,
        converged: op.r >= CONVERGENCE_THRESHOLD,
        step,
    }
}

/// Shannon entropy of activation distribution across atoms.
/// Uses expert activation values if present, else Kuramoto coupling as proxy.
fn compute_activation_entropy(atoms: &[ComputeAtom]) -> f32 {
    use super::expert::ExpertProjection;

    let activations: Vec<f32> = atoms
        .iter()
        .filter_map(|a| {
            a.read_projection::<ExpertProjection>()
                .map(|e| e.activation.max(0.0))
        })
        .collect();

    if activations.is_empty() {
        // Fallback: use Kuramoto coupling values
        let couplings: Vec<f32> = atoms
            .iter()
            .filter_map(|a| {
                a.read_projection::<KuramotoProjection>()
                    .map(|k| k.coupling.max(0.0))
            })
            .collect();
        return shannon_entropy(&couplings);
    }

    shannon_entropy(&activations)
}

/// Normalized Shannon entropy of a distribution.
fn shannon_entropy(values: &[f32]) -> f32 {
    if values.is_empty() {
        return 0.0;
    }
    let total: f32 = values.iter().sum();
    if total <= 0.0 {
        return 0.0;
    }

    let mut h = 0.0f32;
    for &v in values {
        let p = v / total;
        if p > 0.0 {
            h -= p * p.log2();
        }
    }

    let max_h = (values.len() as f32).log2();
    if max_h > 0.0 {
        h / max_h
    } else {
        0.0
    }
}

/// Verify that a sequence of witnesses maintains monotonic R convergence
/// (within tolerance). Returns the index of the first violation, or None.
pub fn verify_monotonic(witnesses: &[ConvergenceWitness], tolerance: f32) -> Option<usize> {
    for i in 1..witnesses.len() {
        if witnesses[i].r < witnesses[i - 1].r - tolerance {
            return Some(i);
        }
    }
    None
}

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

    #[test]
    fn test_witness_empty_atoms() {
        let w = compute_witness(&[], 0);
        assert_eq!(w.r, 0.0);
        assert!(!w.converged);
    }

    #[test]
    fn test_witness_synchronized_atoms() {
        let layout = ProjectionLayout::full();
        let mut atoms = ComputeAtom::create_n(&layout, 10);

        // Set all phases to 0 (perfectly synchronized)
        for atom in &mut atoms {
            let k = KuramotoProjection {
                theta: 0.0,
                omega: 0.0,
                coupling: 1.0,
            };
            atom.write_projection(&k);
        }

        let w = compute_witness(&atoms, 1);
        assert!((w.r - 1.0).abs() < 1e-6, "R = {}", w.r);
        assert!(w.converged);
        assert_eq!(w.step, 1);
    }

    #[test]
    fn test_witness_desynchronized_atoms() {
        let layout = ProjectionLayout::full();
        let mut atoms = ComputeAtom::create_n(&layout, 100);

        // Spread phases uniformly
        for (i, atom) in atoms.iter_mut().enumerate() {
            let theta = 2.0 * std::f32::consts::PI * i as f32 / 100.0;
            let k = KuramotoProjection {
                theta,
                omega: 0.0,
                coupling: 1.0,
            };
            atom.write_projection(&k);
        }

        let w = compute_witness(&atoms, 5);
        assert!(w.r < 0.1, "R = {} (expected < 0.1)", w.r);
        assert!(!w.converged);
    }

    #[test]
    fn test_verify_monotonic_clean() {
        let witnesses = vec![
            ConvergenceWitness { r: 0.3, entropy: 0.5, converged: false, step: 0 },
            ConvergenceWitness { r: 0.5, entropy: 0.4, converged: false, step: 1 },
            ConvergenceWitness { r: 0.9, entropy: 0.2, converged: true, step: 2 },
        ];
        assert_eq!(verify_monotonic(&witnesses, 0.02), None);
    }

    #[test]
    fn test_verify_monotonic_violation() {
        let witnesses = vec![
            ConvergenceWitness { r: 0.8, entropy: 0.3, converged: false, step: 0 },
            ConvergenceWitness { r: 0.5, entropy: 0.5, converged: false, step: 1 },
        ];
        assert_eq!(verify_monotonic(&witnesses, 0.02), Some(1));
    }

    #[test]
    fn test_shannon_entropy_uniform() {
        let values = vec![1.0f32; 8];
        let h = shannon_entropy(&values);
        assert!((h - 1.0).abs() < 1e-6, "H = {}", h);
    }

    #[test]
    fn test_shannon_entropy_single() {
        let values = vec![1.0, 0.0, 0.0, 0.0];
        let h = shannon_entropy(&values);
        assert!(h < 0.01, "H = {}", h);
    }
}