Files
wifi-densepose/examples/exo-ai-2025/research/08-meta-simulation-consciousness/src/closed_form_phi.rs
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

549 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Closed-Form Φ Computation via Eigenvalue Methods
//!
//! This module implements the breakthrough: O(N³) integrated information
//! computation for ergodic cognitive systems, reducing from O(Bell(N)).
//!
//! # Theoretical Foundation
//!
//! For ergodic systems with unique stationary distribution π:
//! 1. Steady-state Φ = H(π) - H(MIP)
//! 2. π = eigenvector with eigenvalue λ = 1
//! 3. MIP found via SCC decomposition + eigenvalue analysis
//!
//! Total complexity: O(N³) eigendecomposition + O(V+E) graph analysis
use std::collections::HashSet;
/// Eigenvalue-based Φ calculator for ergodic systems
pub struct ClosedFormPhi {
/// Tolerance for eigenvalue ≈ 1
tolerance: f64,
/// Number of power iterations for eigenvalue refinement
power_iterations: usize,
}
impl Default for ClosedFormPhi {
fn default() -> Self {
Self {
tolerance: 1e-6,
power_iterations: 100,
}
}
}
impl ClosedFormPhi {
/// Create new calculator with custom tolerance
pub fn new(tolerance: f64) -> Self {
Self {
tolerance,
power_iterations: 100,
}
}
/// Compute Φ for ergodic system via eigenvalue decomposition
///
/// # Complexity
/// O(N³) for eigendecomposition + O(V+E) for SCC + O(N) for entropy
/// = O(N³) total (vs O(Bell(N) × 2^N) brute force)
pub fn compute_phi_ergodic(
&self,
adjacency: &[Vec<f64>],
node_ids: &[u64],
) -> ErgodicPhiResult {
let n = adjacency.len();
if n == 0 {
return ErgodicPhiResult::empty();
}
// Step 1: Check for cycles (required for Φ > 0)
let has_cycles = self.detect_cycles(adjacency);
if !has_cycles {
return ErgodicPhiResult {
phi: 0.0,
stationary_distribution: vec![1.0 / n as f64; n],
dominant_eigenvalue: 0.0,
is_ergodic: false,
computation_time_us: 0,
method: "feedforward_skip".to_string(),
};
}
let start = std::time::Instant::now();
// Step 2: Compute stationary distribution via power iteration
// (More stable than full eigendecomposition for stochastic matrices)
let stationary = self.compute_stationary_distribution(adjacency);
// Step 3: Compute dominant eigenvalue (should be ≈ 1 for ergodic)
let dominant_eigenvalue = self.estimate_dominant_eigenvalue(adjacency);
// Step 4: Check ergodicity (λ₁ ≈ 1)
let is_ergodic = (dominant_eigenvalue - 1.0).abs() < self.tolerance;
if !is_ergodic {
return ErgodicPhiResult {
phi: 0.0,
stationary_distribution: stationary,
dominant_eigenvalue,
is_ergodic: false,
computation_time_us: start.elapsed().as_micros(),
method: "non_ergodic".to_string(),
};
}
// Step 5: Compute whole-system effective information (entropy)
let whole_ei = shannon_entropy(&stationary);
// Step 6: Find MIP via SCC decomposition
let sccs = self.find_strongly_connected_components(adjacency, node_ids);
let mip_ei = self.compute_mip_ei(&sccs, adjacency, &stationary);
// Step 7: Φ = whole - parts
let phi = (whole_ei - mip_ei).max(0.0);
ErgodicPhiResult {
phi,
stationary_distribution: stationary,
dominant_eigenvalue,
is_ergodic: true,
computation_time_us: start.elapsed().as_micros(),
method: "eigenvalue_analytical".to_string(),
}
}
/// Detect cycles using DFS (O(V+E))
fn detect_cycles(&self, adjacency: &[Vec<f64>]) -> bool {
let n = adjacency.len();
let mut color = vec![0u8; n]; // 0=white, 1=gray, 2=black
for start in 0..n {
if color[start] != 0 {
continue;
}
let mut stack = vec![(start, 0)];
color[start] = 1;
while let Some((node, edge_idx)) = stack.last_mut() {
let neighbors: Vec<usize> = adjacency[*node]
.iter()
.enumerate()
.filter(|(_, &w)| w > 1e-10)
.map(|(i, _)| i)
.collect();
if *edge_idx < neighbors.len() {
let neighbor = neighbors[*edge_idx];
*edge_idx += 1;
match color[neighbor] {
1 => return true, // Back edge = cycle
0 => {
color[neighbor] = 1;
stack.push((neighbor, 0));
}
_ => {} // Already processed
}
} else {
color[*node] = 2;
stack.pop();
}
}
}
false
}
/// Compute stationary distribution via power iteration (O(kN²))
/// More numerically stable than direct eigendecomposition
fn compute_stationary_distribution(&self, adjacency: &[Vec<f64>]) -> Vec<f64> {
let n = adjacency.len();
// Normalize adjacency to transition matrix
let transition = self.normalize_to_stochastic(adjacency);
// Start with uniform distribution
let mut dist = vec![1.0 / n as f64; n];
// Power iteration: v_{k+1} = P^T v_k
for _ in 0..self.power_iterations {
let mut next_dist = vec![0.0; n];
for i in 0..n {
for j in 0..n {
next_dist[i] += transition[j][i] * dist[j];
}
}
// Normalize (maintain probability)
let sum: f64 = next_dist.iter().sum();
if sum > 1e-10 {
for x in &mut next_dist {
*x /= sum;
}
}
// Check convergence
let diff: f64 = dist
.iter()
.zip(next_dist.iter())
.map(|(a, b)| (a - b).abs())
.sum();
dist = next_dist;
if diff < self.tolerance {
break;
}
}
dist
}
/// Normalize adjacency matrix to row-stochastic (each row sums to 1)
fn normalize_to_stochastic(&self, adjacency: &[Vec<f64>]) -> Vec<Vec<f64>> {
let n = adjacency.len();
let mut stochastic = vec![vec![0.0; n]; n];
for i in 0..n {
let row_sum: f64 = adjacency[i].iter().sum();
if row_sum > 1e-10 {
for j in 0..n {
stochastic[i][j] = adjacency[i][j] / row_sum;
}
} else {
// Uniform if no outgoing edges
for j in 0..n {
stochastic[i][j] = 1.0 / n as f64;
}
}
}
stochastic
}
/// Estimate dominant eigenvalue via power method (O(kN²))
fn estimate_dominant_eigenvalue(&self, adjacency: &[Vec<f64>]) -> f64 {
let n = adjacency.len();
let transition = self.normalize_to_stochastic(adjacency);
// Random initial vector
let mut v = vec![1.0; n];
let mut eigenvalue = 0.0;
for _ in 0..self.power_iterations {
let mut next_v = vec![0.0; n];
// Matrix-vector multiply
for i in 0..n {
for j in 0..n {
next_v[i] += transition[i][j] * v[j];
}
}
// Compute eigenvalue estimate
let norm: f64 = next_v.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm > 1e-10 {
eigenvalue = norm / v.iter().map(|x| x * x).sum::<f64>().sqrt();
// Normalize
for x in &mut next_v {
*x /= norm;
}
}
v = next_v;
}
eigenvalue
}
/// Find strongly connected components via Tarjan's algorithm (O(V+E))
fn find_strongly_connected_components(
&self,
adjacency: &[Vec<f64>],
node_ids: &[u64],
) -> Vec<HashSet<u64>> {
let n = adjacency.len();
let mut index = 0;
let mut stack = Vec::new();
let mut indices = vec![None; n];
let mut lowlinks = vec![0; n];
let mut on_stack = vec![false; n];
let mut sccs = Vec::new();
fn strongconnect(
v: usize,
adjacency: &[Vec<f64>],
node_ids: &[u64],
index: &mut usize,
stack: &mut Vec<usize>,
indices: &mut Vec<Option<usize>>,
lowlinks: &mut Vec<usize>,
on_stack: &mut Vec<bool>,
sccs: &mut Vec<HashSet<u64>>,
) {
indices[v] = Some(*index);
lowlinks[v] = *index;
*index += 1;
stack.push(v);
on_stack[v] = true;
// Consider successors
for (w, &weight) in adjacency[v].iter().enumerate() {
if weight <= 1e-10 {
continue;
}
if indices[w].is_none() {
strongconnect(
w, adjacency, node_ids, index, stack, indices, lowlinks, on_stack, sccs,
);
lowlinks[v] = lowlinks[v].min(lowlinks[w]);
} else if on_stack[w] {
lowlinks[v] = lowlinks[v].min(indices[w].unwrap());
}
}
// Root of SCC
if lowlinks[v] == indices[v].unwrap() {
let mut scc = HashSet::new();
loop {
let w = stack.pop().unwrap();
on_stack[w] = false;
scc.insert(node_ids[w]);
if w == v {
break;
}
}
sccs.push(scc);
}
}
for v in 0..n {
if indices[v].is_none() {
strongconnect(
v,
adjacency,
node_ids,
&mut index,
&mut stack,
&mut indices,
&mut lowlinks,
&mut on_stack,
&mut sccs,
);
}
}
sccs
}
/// Compute MIP effective information (sum of parts)
fn compute_mip_ei(
&self,
sccs: &[HashSet<u64>],
_adjacency: &[Vec<f64>],
stationary: &[f64],
) -> f64 {
if sccs.is_empty() {
return 0.0;
}
// For MIP: sum entropy of each SCC's marginal distribution
let mut total_ei = 0.0;
for scc in sccs {
if scc.is_empty() {
continue;
}
// Marginal distribution for this SCC
let mut marginal_prob = 0.0;
for (i, &prob) in stationary.iter().enumerate() {
if scc.contains(&(i as u64)) {
marginal_prob += prob;
}
}
if marginal_prob > 1e-10 {
// Entropy of this partition
total_ei += -marginal_prob * marginal_prob.log2();
}
}
total_ei
}
/// Compute Consciousness Eigenvalue Index (CEI)
/// CEI = |λ₁ - 1| + α × H(λ₂, ..., λₙ)
pub fn compute_cei(&self, adjacency: &[Vec<f64>], alpha: f64) -> f64 {
let n = adjacency.len();
if n == 0 {
return f64::INFINITY;
}
// Estimate dominant eigenvalue
let lambda_1 = self.estimate_dominant_eigenvalue(adjacency);
// For full CEI, would need all eigenvalues (O(N³))
// Approximation: use stationary distribution entropy as proxy
let stationary = self.compute_stationary_distribution(adjacency);
let spectral_entropy = shannon_entropy(&stationary);
(lambda_1 - 1.0).abs() + alpha * (1.0 - spectral_entropy / (n as f64).log2())
}
}
/// Result of ergodic Φ computation
#[derive(Debug, Clone)]
pub struct ErgodicPhiResult {
/// Integrated information value
pub phi: f64,
/// Stationary distribution (eigenvector with λ=1)
pub stationary_distribution: Vec<f64>,
/// Dominant eigenvalue (should be ≈ 1)
pub dominant_eigenvalue: f64,
/// Whether system is ergodic
pub is_ergodic: bool,
/// Computation time in microseconds
pub computation_time_us: u128,
/// Method used
pub method: String,
}
impl ErgodicPhiResult {
fn empty() -> Self {
Self {
phi: 0.0,
stationary_distribution: Vec::new(),
dominant_eigenvalue: 0.0,
is_ergodic: false,
computation_time_us: 0,
method: "empty".to_string(),
}
}
/// Speedup over brute force (approximate)
pub fn speedup_vs_bruteforce(&self, n: usize) -> f64 {
if n <= 1 {
return 1.0;
}
// Bell numbers grow as: B(n) ≈ (n/e)^n × e^(e^n/n)
// Rough approximation: B(n) ≈ e^(n log n)
let bruteforce_complexity = (n as f64).powi(2) * (n as f64 * (n as f64).ln()).exp();
// Our method: O(N³)
let our_complexity = (n as f64).powi(3);
bruteforce_complexity / our_complexity
}
}
/// Shannon entropy of probability distribution
pub fn shannon_entropy(dist: &[f64]) -> f64 {
dist.iter()
.filter(|&&p| p > 1e-10)
.map(|&p| -p * p.log2())
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_symmetric_cycle() {
let calc = ClosedFormPhi::default();
// 4-node cycle: 0→1→2→3→0
let mut adj = vec![vec![0.0; 4]; 4];
adj[0][1] = 1.0;
adj[1][2] = 1.0;
adj[2][3] = 1.0;
adj[3][0] = 1.0;
let nodes = vec![0, 1, 2, 3];
let result = calc.compute_phi_ergodic(&adj, &nodes);
assert!(result.is_ergodic);
assert!((result.dominant_eigenvalue - 1.0).abs() < 0.1);
assert!(result.phi >= 0.0);
// Stationary should be uniform for symmetric cycle
for &p in &result.stationary_distribution {
assert!((p - 0.25).abs() < 0.1);
}
}
#[test]
fn test_feedforward_zero_phi() {
let calc = ClosedFormPhi::default();
// Feedforward: 0→1→2→3 (no cycles)
let mut adj = vec![vec![0.0; 4]; 4];
adj[0][1] = 1.0;
adj[1][2] = 1.0;
adj[2][3] = 1.0;
let nodes = vec![0, 1, 2, 3];
let result = calc.compute_phi_ergodic(&adj, &nodes);
// Should detect no cycles → Φ = 0
assert_eq!(result.phi, 0.0);
}
#[test]
fn test_cei_computation() {
let calc = ClosedFormPhi::default();
// Cycle (should have low CEI, near critical)
let mut cycle = vec![vec![0.0; 4]; 4];
cycle[0][1] = 1.0;
cycle[1][2] = 1.0;
cycle[2][3] = 1.0;
cycle[3][0] = 1.0;
let cei_cycle = calc.compute_cei(&cycle, 1.0);
// Fully connected (degenerate, high CEI)
let mut full = vec![vec![1.0; 4]; 4];
for i in 0..4 {
full[i][i] = 0.0;
}
let cei_full = calc.compute_cei(&full, 1.0);
// Both should be non-negative and finite
assert!(cei_cycle >= 0.0 && cei_cycle.is_finite());
assert!(cei_full >= 0.0 && cei_full.is_finite());
// CEI values should be in reasonable range
assert!(cei_cycle < 10.0);
assert!(cei_full < 10.0);
}
#[test]
fn test_speedup_estimate() {
let result = ErgodicPhiResult {
phi: 1.0,
stationary_distribution: vec![0.1; 10],
dominant_eigenvalue: 1.0,
is_ergodic: true,
computation_time_us: 100,
method: "test".to_string(),
};
let speedup_10 = result.speedup_vs_bruteforce(10);
let speedup_12 = result.speedup_vs_bruteforce(12);
// Speedup should increase with system size
assert!(speedup_12 > speedup_10);
assert!(speedup_10 > 1000.0); // At least 1000x for n=10
}
}