Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
479
vendor/ruvector/examples/exo-ai-2025/research/07-causal-emergence/src/causal_hierarchy.rs
vendored
Normal file
479
vendor/ruvector/examples/exo-ai-2025/research/07-causal-emergence/src/causal_hierarchy.rs
vendored
Normal file
@@ -0,0 +1,479 @@
|
||||
// Hierarchical Causal Structure Management
|
||||
// Implements transfer entropy and consciousness metrics for HCC framework
|
||||
|
||||
use crate::coarse_graining::{ScaleHierarchy, ScaleLevel};
|
||||
use crate::effective_information::compute_ei_simd;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Represents the complete hierarchical causal structure with all metrics
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CausalHierarchy {
|
||||
pub hierarchy: ScaleHierarchy,
|
||||
pub metrics: HierarchyMetrics,
|
||||
}
|
||||
|
||||
/// Metrics computed at each scale of the hierarchy
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HierarchyMetrics {
|
||||
/// Effective information at each scale
|
||||
pub ei: Vec<f32>,
|
||||
/// Integrated information (Φ) at each scale
|
||||
pub phi: Vec<f32>,
|
||||
/// Upward transfer entropy (micro → macro)
|
||||
pub te_up: Vec<f32>,
|
||||
/// Downward transfer entropy (macro → micro)
|
||||
pub te_down: Vec<f32>,
|
||||
/// Consciousness metric Ψ at each scale
|
||||
pub psi: Vec<f32>,
|
||||
/// Optimal scale (argmax Ψ)
|
||||
pub optimal_scale: usize,
|
||||
/// Consciousness score at optimal scale
|
||||
pub consciousness_score: f32,
|
||||
}
|
||||
|
||||
impl CausalHierarchy {
|
||||
/// Builds hierarchical causal structure from time-series data
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - Time-series of neural states
|
||||
/// * `branching_factor` - k for k-way coarse-graining
|
||||
/// * `use_optimal` - Whether to use optimal partitioning (slower but better)
|
||||
///
|
||||
/// # Returns
|
||||
/// Complete causal hierarchy with all metrics computed
|
||||
pub fn from_time_series(data: &[f32], branching_factor: usize, use_optimal: bool) -> Self {
|
||||
// Estimate transition matrix from data
|
||||
let transition_matrix = estimate_transition_matrix(data, 256); // 256 bins
|
||||
|
||||
// Build scale hierarchy
|
||||
let hierarchy = if use_optimal {
|
||||
ScaleHierarchy::build_optimal(transition_matrix, branching_factor)
|
||||
} else {
|
||||
ScaleHierarchy::build_sequential(transition_matrix, branching_factor)
|
||||
};
|
||||
|
||||
// Compute all metrics
|
||||
let metrics = compute_hierarchy_metrics(&hierarchy, data);
|
||||
|
||||
Self { hierarchy, metrics }
|
||||
}
|
||||
|
||||
/// Checks if system is conscious according to HCC criterion
|
||||
pub fn is_conscious(&self, threshold: f32) -> bool {
|
||||
self.metrics.consciousness_score > threshold
|
||||
}
|
||||
|
||||
/// Returns consciousness level classification
|
||||
pub fn consciousness_level(&self) -> ConsciousnessLevel {
|
||||
match self.metrics.consciousness_score {
|
||||
x if x > 10.0 => ConsciousnessLevel::FullyConscious,
|
||||
x if x > 5.0 => ConsciousnessLevel::MinimallyConscious,
|
||||
x if x > 1.0 => ConsciousnessLevel::Borderline,
|
||||
_ => ConsciousnessLevel::Unconscious,
|
||||
}
|
||||
}
|
||||
|
||||
/// Detects causal emergence across scales
|
||||
pub fn causal_emergence(&self) -> Option<(usize, f32)> {
|
||||
if self.metrics.ei.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let micro_ei = self.metrics.ei[0];
|
||||
let (max_scale, &max_ei) = self
|
||||
.metrics
|
||||
.ei
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())?;
|
||||
|
||||
let emergence_strength = max_ei - micro_ei;
|
||||
|
||||
if emergence_strength > 0.1 {
|
||||
Some((max_scale, emergence_strength))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for circular causation at optimal scale
|
||||
pub fn has_circular_causation(&self) -> bool {
|
||||
let s = self.metrics.optimal_scale;
|
||||
|
||||
// Need both upward and downward TE > threshold
|
||||
if s >= self.metrics.te_up.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
const TE_THRESHOLD: f32 = 0.01; // Minimum TE to count as causal
|
||||
self.metrics.te_up[s] > TE_THRESHOLD && self.metrics.te_down[s] > TE_THRESHOLD
|
||||
}
|
||||
}
|
||||
|
||||
/// Consciousness level classifications
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConsciousnessLevel {
|
||||
Unconscious,
|
||||
Borderline,
|
||||
MinimallyConscious,
|
||||
FullyConscious,
|
||||
}
|
||||
|
||||
/// Computes all hierarchical metrics (EI, Φ, TE, Ψ)
|
||||
fn compute_hierarchy_metrics(hierarchy: &ScaleHierarchy, data: &[f32]) -> HierarchyMetrics {
|
||||
let num_scales = hierarchy.num_scales();
|
||||
|
||||
// Compute EI at each scale
|
||||
let mut ei = Vec::with_capacity(num_scales);
|
||||
for level in &hierarchy.levels {
|
||||
let ei_val = compute_ei_simd(&level.transition_matrix, level.num_states);
|
||||
ei.push(ei_val);
|
||||
}
|
||||
|
||||
// Compute Φ at each scale (approximate)
|
||||
let mut phi = Vec::with_capacity(num_scales);
|
||||
for level in &hierarchy.levels {
|
||||
let phi_val = approximate_phi(&level.transition_matrix, level.num_states);
|
||||
phi.push(phi_val);
|
||||
}
|
||||
|
||||
// Compute transfer entropy between adjacent scales
|
||||
let mut te_up = Vec::with_capacity(num_scales - 1);
|
||||
let mut te_down = Vec::with_capacity(num_scales - 1);
|
||||
|
||||
for i in 0..(num_scales - 1) {
|
||||
// Project data to each scale
|
||||
let data_micro = project_to_scale(data, &hierarchy.levels[i]);
|
||||
let data_macro = project_to_scale(data, &hierarchy.levels[i + 1]);
|
||||
|
||||
// Compute bidirectional transfer entropy
|
||||
te_up.push(transfer_entropy(&data_micro, &data_macro, 1, 1));
|
||||
te_down.push(transfer_entropy(&data_macro, &data_micro, 1, 1));
|
||||
}
|
||||
|
||||
// Compute consciousness metric Ψ at each scale
|
||||
let mut psi = vec![0.0; num_scales];
|
||||
for i in 0..(num_scales - 1) {
|
||||
// Ψ = EI · Φ · √(TE_up · TE_down)
|
||||
psi[i] = ei[i] * phi[i] * (te_up[i] * te_down[i]).sqrt();
|
||||
}
|
||||
|
||||
// Find optimal scale (max Ψ)
|
||||
let (optimal_scale, &consciousness_score) = psi
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
|
||||
.unwrap_or((0, &0.0));
|
||||
|
||||
HierarchyMetrics {
|
||||
ei,
|
||||
phi,
|
||||
te_up,
|
||||
te_down,
|
||||
psi,
|
||||
optimal_scale,
|
||||
consciousness_score,
|
||||
}
|
||||
}
|
||||
|
||||
/// Estimates transition probability matrix from time-series data
|
||||
/// Uses binning/discretization
|
||||
fn estimate_transition_matrix(data: &[f32], num_bins: usize) -> Vec<f32> {
|
||||
if data.len() < 2 {
|
||||
return vec![1.0]; // Trivial 1×1 matrix
|
||||
}
|
||||
|
||||
// Discretize data into bins
|
||||
let binned = discretize_data(data, num_bins);
|
||||
|
||||
// Count transitions
|
||||
let mut counts = vec![0u32; num_bins * num_bins];
|
||||
for i in 0..(binned.len() - 1) {
|
||||
let from = binned[i];
|
||||
let to = binned[i + 1];
|
||||
counts[from * num_bins + to] += 1;
|
||||
}
|
||||
|
||||
// Normalize to probabilities
|
||||
let mut matrix = vec![0.0f32; num_bins * num_bins];
|
||||
for i in 0..num_bins {
|
||||
let row_sum: u32 = (0..num_bins).map(|j| counts[i * num_bins + j]).sum();
|
||||
|
||||
if row_sum > 0 {
|
||||
for j in 0..num_bins {
|
||||
matrix[i * num_bins + j] = counts[i * num_bins + j] as f32 / row_sum as f32;
|
||||
}
|
||||
} else {
|
||||
// Uniform distribution if no data
|
||||
for j in 0..num_bins {
|
||||
matrix[i * num_bins + j] = 1.0 / num_bins as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matrix
|
||||
}
|
||||
|
||||
/// Discretizes continuous data into bins
|
||||
fn discretize_data(data: &[f32], num_bins: usize) -> Vec<usize> {
|
||||
if data.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let min = data.iter().copied().fold(f32::INFINITY, f32::min);
|
||||
let max = data.iter().copied().fold(f32::NEG_INFINITY, f32::max);
|
||||
let range = max - min;
|
||||
|
||||
if range < 1e-6 {
|
||||
// All values same, put in middle bin
|
||||
return vec![num_bins / 2; data.len()];
|
||||
}
|
||||
|
||||
data.iter()
|
||||
.map(|&x| {
|
||||
let normalized = (x - min) / range;
|
||||
let bin = (normalized * num_bins as f32).floor() as usize;
|
||||
bin.min(num_bins - 1)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Projects time-series data to a specific scale level
|
||||
fn project_to_scale(data: &[f32], level: &ScaleLevel) -> Vec<usize> {
|
||||
let binned = discretize_data(data, level.partition.num_micro_states());
|
||||
|
||||
// Map micro-states to macro-states
|
||||
let micro_to_macro: HashMap<usize, usize> = level
|
||||
.partition
|
||||
.groups
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(macro_idx, micro_group)| {
|
||||
micro_group
|
||||
.iter()
|
||||
.map(move |µ_idx| (micro_idx, macro_idx))
|
||||
})
|
||||
.collect();
|
||||
|
||||
binned
|
||||
.iter()
|
||||
.map(|µ| *micro_to_macro.get(µ).unwrap_or(&0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Computes transfer entropy between two time series
|
||||
///
|
||||
/// TE(X→Y) = I(Y_t+1; X_t | Y_t)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `x` - Source time series (discretized)
|
||||
/// * `y` - Target time series (discretized)
|
||||
/// * `k` - History length for X
|
||||
/// * `l` - History length for Y
|
||||
pub fn transfer_entropy(x: &[usize], y: &[usize], k: usize, l: usize) -> f32 {
|
||||
if x.len() != y.len() || x.len() < k.max(l) + 1 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let t_max = x.len() - 1;
|
||||
let lag = k.max(l);
|
||||
|
||||
// Count joint occurrences
|
||||
let mut counts = HashMap::new();
|
||||
for t in lag..t_max {
|
||||
let x_past: Vec<_> = x[t - k..t].to_vec();
|
||||
let y_past: Vec<_> = y[t - l..t].to_vec();
|
||||
let y_future = y[t + 1];
|
||||
|
||||
*counts
|
||||
.entry((y_future, x_past.clone(), y_past.clone()))
|
||||
.or_insert(0) += 1;
|
||||
}
|
||||
|
||||
let total = (t_max - lag) as f32;
|
||||
|
||||
// Compute marginals
|
||||
let mut p_y_future: HashMap<usize, f32> = HashMap::new();
|
||||
let mut p_x_past: HashMap<Vec<usize>, f32> = HashMap::new();
|
||||
let mut p_y_past: HashMap<Vec<usize>, f32> = HashMap::new();
|
||||
let mut p_y_xy: HashMap<(usize, Vec<usize>, Vec<usize>), f32> = HashMap::new();
|
||||
let mut p_xy: HashMap<(Vec<usize>, Vec<usize>), f32> = HashMap::new();
|
||||
let mut p_y: HashMap<Vec<usize>, f32> = HashMap::new();
|
||||
|
||||
for ((y_fut, x_p, y_p), &count) in &counts {
|
||||
let prob = count as f32 / total;
|
||||
*p_y_future.entry(*y_fut).or_insert(0.0) += prob;
|
||||
*p_x_past.entry(x_p.clone()).or_insert(0.0) += prob;
|
||||
*p_y_past.entry(y_p.clone()).or_insert(0.0) += prob;
|
||||
*p_y_xy
|
||||
.entry((*y_fut, x_p.clone(), y_p.clone()))
|
||||
.or_insert(0.0) += prob;
|
||||
*p_xy.entry((x_p.clone(), y_p.clone())).or_insert(0.0) += prob;
|
||||
*p_y.entry(y_p.clone()).or_insert(0.0) += prob;
|
||||
}
|
||||
|
||||
// Compute TE = I(Y_future; X_past | Y_past)
|
||||
let mut te = 0.0;
|
||||
for ((_y_fut, x_p, y_p), &p_joint) in &counts {
|
||||
let p = p_joint as f32 / total;
|
||||
let p_y_given_xy = p / p_xy[&(x_p.clone(), y_p.clone())];
|
||||
let p_y_given_y = p / p_y[y_p];
|
||||
|
||||
te += p * (p_y_given_xy / p_y_given_y).log2();
|
||||
}
|
||||
|
||||
te.max(0.0) // Ensure non-negative
|
||||
}
|
||||
|
||||
/// Approximates integrated information Φ using spectral method
|
||||
fn approximate_phi(transition_matrix: &[f32], n: usize) -> f32 {
|
||||
if n <= 1 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Find bipartition that minimizes KL divergence
|
||||
// For simplicity, try a few random partitions and take minimum
|
||||
|
||||
let mut min_kl = f32::MAX;
|
||||
|
||||
// Try half-split partition
|
||||
let mid = n / 2;
|
||||
let partition_a: Vec<_> = (0..mid).collect();
|
||||
let partition_b: Vec<_> = (mid..n).collect();
|
||||
|
||||
let kl = compute_kl_partition(transition_matrix, &partition_a, &partition_b, n);
|
||||
min_kl = min_kl.min(kl);
|
||||
|
||||
// Try a few random partitions
|
||||
for _ in 0..5 {
|
||||
let size_a = (n / 2).max(1);
|
||||
let mut partition_a: Vec<_> = (0..size_a).collect();
|
||||
let partition_b: Vec<_> = (size_a..n).collect();
|
||||
|
||||
// Randomize (simple shuffle)
|
||||
partition_a.rotate_left(size_a / 3);
|
||||
|
||||
let kl = compute_kl_partition(transition_matrix, &partition_a, &partition_b, n);
|
||||
min_kl = min_kl.min(kl);
|
||||
}
|
||||
|
||||
min_kl
|
||||
}
|
||||
|
||||
/// Computes KL divergence between full and partitioned systems
|
||||
fn compute_kl_partition(
|
||||
_matrix: &[f32],
|
||||
partition_a: &[usize],
|
||||
partition_b: &[usize],
|
||||
n: usize,
|
||||
) -> f32 {
|
||||
// Compute stationary distribution (simplified: uniform)
|
||||
let p_full = vec![1.0 / n as f32; n];
|
||||
|
||||
// Compute partitioned distribution (independent subsystems)
|
||||
let mut p_cut = vec![0.0; n];
|
||||
|
||||
let prob_a = partition_a.len() as f32 / n as f32;
|
||||
let prob_b = partition_b.len() as f32 / n as f32;
|
||||
|
||||
for &i in partition_a {
|
||||
p_cut[i] = prob_a / partition_a.len() as f32;
|
||||
}
|
||||
for &i in partition_b {
|
||||
p_cut[i] = prob_b / partition_b.len() as f32;
|
||||
}
|
||||
|
||||
// KL divergence
|
||||
let mut kl = 0.0;
|
||||
for i in 0..n {
|
||||
if p_full[i] > 1e-10 && p_cut[i] > 1e-10 {
|
||||
kl += p_full[i] * (p_full[i] / p_cut[i]).log2();
|
||||
}
|
||||
}
|
||||
|
||||
kl
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_discretization() {
|
||||
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
let binned = discretize_data(&data, 5);
|
||||
|
||||
assert_eq!(binned.len(), 5);
|
||||
// Should map to different bins
|
||||
assert_eq!(binned[0], 0);
|
||||
assert_eq!(binned[4], 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_entropy_independent() {
|
||||
// Two independent random sequences
|
||||
let x = vec![0, 1, 0, 1, 0, 1, 0, 1];
|
||||
let y = vec![1, 0, 1, 0, 1, 0, 1, 0];
|
||||
|
||||
let te = transfer_entropy(&x, &y, 1, 1);
|
||||
|
||||
// Should be low for independent sequences
|
||||
assert!(te < 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transfer_entropy_deterministic() {
|
||||
// Y follows X with 1-step delay: Y[t+1] = X[t]
|
||||
let x = vec![0, 1, 1, 0, 1, 0, 0, 1];
|
||||
let y = vec![0, 0, 1, 1, 0, 1, 0, 0]; // Shifted version
|
||||
|
||||
let te = transfer_entropy(&x, &y, 1, 1);
|
||||
|
||||
// Should be high
|
||||
assert!(te > 0.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_causal_hierarchy_construction() {
|
||||
// Synthetic oscillating data
|
||||
let data: Vec<f32> = (0..1000)
|
||||
.map(|t| (t as f32 * 0.1).sin() + 0.5 * (t as f32 * 0.3).cos())
|
||||
.collect();
|
||||
|
||||
let hierarchy = CausalHierarchy::from_time_series(&data, 2, false);
|
||||
|
||||
// Should have multiple scales
|
||||
assert!(hierarchy.hierarchy.num_scales() > 1);
|
||||
|
||||
// Metrics should be computed
|
||||
assert_eq!(hierarchy.metrics.ei.len(), hierarchy.hierarchy.num_scales());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_consciousness_detection() {
|
||||
// Create data with strong multi-scale structure
|
||||
let data: Vec<f32> = (0..1000)
|
||||
.map(|t| {
|
||||
// Multiple frequencies -> multi-scale structure
|
||||
(t as f32 * 0.05).sin()
|
||||
+ 0.5 * (t as f32 * 0.2).cos()
|
||||
+ 0.25 * (t as f32 * 0.8).sin()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let hierarchy = CausalHierarchy::from_time_series(&data, 2, false);
|
||||
|
||||
// Check consciousness score is positive
|
||||
assert!(hierarchy.metrics.consciousness_score >= 0.0);
|
||||
|
||||
// Check level classification works
|
||||
let level = hierarchy.consciousness_level();
|
||||
assert!(matches!(
|
||||
level,
|
||||
ConsciousnessLevel::Unconscious
|
||||
| ConsciousnessLevel::Borderline
|
||||
| ConsciousnessLevel::MinimallyConscious
|
||||
| ConsciousnessLevel::FullyConscious
|
||||
));
|
||||
}
|
||||
}
|
||||
418
vendor/ruvector/examples/exo-ai-2025/research/07-causal-emergence/src/coarse_graining.rs
vendored
Normal file
418
vendor/ruvector/examples/exo-ai-2025/research/07-causal-emergence/src/coarse_graining.rs
vendored
Normal file
@@ -0,0 +1,418 @@
|
||||
// Multi-Scale Coarse-Graining for Hierarchical Causal Analysis
|
||||
// Implements k-way aggregation with O(log n) depth
|
||||
|
||||
/// Represents a partition of states into groups
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Partition {
|
||||
/// groups[i] contains indices of micro-states in macro-state i
|
||||
pub groups: Vec<Vec<usize>>,
|
||||
}
|
||||
|
||||
impl Partition {
|
||||
pub fn num_macro_states(&self) -> usize {
|
||||
self.groups.len()
|
||||
}
|
||||
|
||||
pub fn num_micro_states(&self) -> usize {
|
||||
self.groups.iter().map(|g| g.len()).sum()
|
||||
}
|
||||
|
||||
/// Creates sequential k-way partition
|
||||
/// Groups: [0..k), [k..2k), [2k..3k), ...
|
||||
pub fn sequential(n: usize, k: usize) -> Self {
|
||||
let num_groups = (n + k - 1) / k; // Ceiling division
|
||||
let mut groups = Vec::with_capacity(num_groups);
|
||||
|
||||
for i in 0..num_groups {
|
||||
let start = i * k;
|
||||
let end = (start + k).min(n);
|
||||
groups.push((start..end).collect());
|
||||
}
|
||||
|
||||
Self { groups }
|
||||
}
|
||||
|
||||
/// Creates partition by clustering based on transition similarity
|
||||
pub fn from_clustering(labels: Vec<usize>) -> Self {
|
||||
let num_clusters = labels.iter().max().map(|&x| x + 1).unwrap_or(0);
|
||||
let mut groups = vec![Vec::new(); num_clusters];
|
||||
|
||||
for (state, &label) in labels.iter().enumerate() {
|
||||
groups[label].push(state);
|
||||
}
|
||||
|
||||
Self { groups }
|
||||
}
|
||||
}
|
||||
|
||||
/// Coarse-grains a transition matrix according to a partition
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `micro_matrix` - n×n transition matrix
|
||||
/// * `partition` - How to group micro-states into macro-states
|
||||
///
|
||||
/// # Returns
|
||||
/// m×m coarse-grained transition matrix where m = number of groups
|
||||
///
|
||||
/// # Algorithm
|
||||
/// T'[I,J] = (1/|group_I|) Σᵢ∈group_I Σⱼ∈group_J T[i,j]
|
||||
pub fn coarse_grain_transition_matrix(micro_matrix: &[f32], partition: &Partition) -> Vec<f32> {
|
||||
let n = (micro_matrix.len() as f32).sqrt() as usize;
|
||||
let m = partition.num_macro_states();
|
||||
|
||||
let mut macro_matrix = vec![0.0; m * m];
|
||||
|
||||
for (i_macro, group_i) in partition.groups.iter().enumerate() {
|
||||
for (j_macro, group_j) in partition.groups.iter().enumerate() {
|
||||
let mut sum = 0.0;
|
||||
|
||||
// Sum transitions from group I to group J
|
||||
for &i_micro in group_i {
|
||||
for &j_micro in group_j {
|
||||
sum += micro_matrix[i_micro * n + j_micro];
|
||||
}
|
||||
}
|
||||
|
||||
// Average over source group size
|
||||
macro_matrix[i_macro * m + j_macro] = sum / (group_i.len() as f32);
|
||||
}
|
||||
}
|
||||
|
||||
macro_matrix
|
||||
}
|
||||
|
||||
/// Represents a hierarchical scale structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScaleLevel {
|
||||
pub num_states: usize,
|
||||
pub transition_matrix: Vec<f32>,
|
||||
/// Partition mapping to original micro-states (level 0)
|
||||
pub partition: Partition,
|
||||
}
|
||||
|
||||
/// Complete hierarchical decomposition
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScaleHierarchy {
|
||||
pub levels: Vec<ScaleLevel>,
|
||||
}
|
||||
|
||||
impl ScaleHierarchy {
|
||||
/// Builds hierarchy using sequential k-way coarse-graining
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `micro_matrix` - Base-level n×n transition matrix
|
||||
/// * `branching_factor` - k (typically 2-8)
|
||||
///
|
||||
/// # Returns
|
||||
/// Hierarchy with O(log_k n) levels
|
||||
pub fn build_sequential(micro_matrix: Vec<f32>, branching_factor: usize) -> Self {
|
||||
let n = (micro_matrix.len() as f32).sqrt() as usize;
|
||||
let mut levels = Vec::new();
|
||||
|
||||
// Level 0: micro-level
|
||||
levels.push(ScaleLevel {
|
||||
num_states: n,
|
||||
transition_matrix: micro_matrix.clone(),
|
||||
partition: Partition {
|
||||
groups: (0..n).map(|i| vec![i]).collect(),
|
||||
},
|
||||
});
|
||||
|
||||
let mut current_matrix = micro_matrix;
|
||||
let mut current_partition = Partition {
|
||||
groups: (0..n).map(|i| vec![i]).collect(),
|
||||
};
|
||||
|
||||
// Build hierarchy bottom-up
|
||||
while levels.last().unwrap().num_states > branching_factor {
|
||||
let current_n = levels.last().unwrap().num_states;
|
||||
|
||||
// Create k-way partition
|
||||
let new_partition = Partition::sequential(current_n, branching_factor);
|
||||
|
||||
// Coarse-grain matrix
|
||||
current_matrix = coarse_grain_transition_matrix(¤t_matrix, &new_partition);
|
||||
|
||||
// Update partition relative to original micro-states
|
||||
current_partition = merge_partitions(¤t_partition, &new_partition);
|
||||
|
||||
levels.push(ScaleLevel {
|
||||
num_states: new_partition.num_macro_states(),
|
||||
transition_matrix: current_matrix.clone(),
|
||||
partition: current_partition.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Self { levels }
|
||||
}
|
||||
|
||||
/// Builds hierarchy using optimal coarse-graining (minimizes redundancy)
|
||||
/// More expensive but finds better emergence
|
||||
pub fn build_optimal(micro_matrix: Vec<f32>, branching_factor: usize) -> Self {
|
||||
let n = (micro_matrix.len() as f32).sqrt() as usize;
|
||||
let mut levels = Vec::new();
|
||||
|
||||
// Level 0
|
||||
levels.push(ScaleLevel {
|
||||
num_states: n,
|
||||
transition_matrix: micro_matrix.clone(),
|
||||
partition: Partition {
|
||||
groups: (0..n).map(|i| vec![i]).collect(),
|
||||
},
|
||||
});
|
||||
|
||||
let mut current_matrix = micro_matrix;
|
||||
let mut current_partition = Partition {
|
||||
groups: (0..n).map(|i| vec![i]).collect(),
|
||||
};
|
||||
|
||||
while levels.last().unwrap().num_states > branching_factor {
|
||||
let current_n = levels.last().unwrap().num_states;
|
||||
|
||||
// Find optimal partition using similarity clustering
|
||||
let new_partition =
|
||||
find_optimal_partition(¤t_matrix, current_n, branching_factor);
|
||||
|
||||
current_matrix = coarse_grain_transition_matrix(¤t_matrix, &new_partition);
|
||||
|
||||
current_partition = merge_partitions(¤t_partition, &new_partition);
|
||||
|
||||
levels.push(ScaleLevel {
|
||||
num_states: new_partition.num_macro_states(),
|
||||
transition_matrix: current_matrix.clone(),
|
||||
partition: current_partition.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Self { levels }
|
||||
}
|
||||
|
||||
pub fn num_scales(&self) -> usize {
|
||||
self.levels.len()
|
||||
}
|
||||
|
||||
pub fn scale(&self, index: usize) -> Option<&ScaleLevel> {
|
||||
self.levels.get(index)
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges two partitions: applies new_partition to current_partition
|
||||
///
|
||||
/// Example:
|
||||
/// current: [[0,1], [2,3], [4,5]]
|
||||
/// new: [[0,1], [2]] (groups 0&1 together, group 2 alone)
|
||||
/// result: [[0,1,2,3], [4,5]]
|
||||
fn merge_partitions(current: &Partition, new: &Partition) -> Partition {
|
||||
let mut merged_groups = Vec::new();
|
||||
|
||||
for new_group in &new.groups {
|
||||
let mut merged_group = Vec::new();
|
||||
|
||||
for ¯o_state in new_group {
|
||||
// Add all micro-states from this macro-state
|
||||
if let Some(micro_states) = current.groups.get(macro_state) {
|
||||
merged_group.extend(micro_states);
|
||||
}
|
||||
}
|
||||
|
||||
merged_groups.push(merged_group);
|
||||
}
|
||||
|
||||
Partition {
|
||||
groups: merged_groups,
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds optimal k-way partition by minimizing within-group variance
|
||||
/// Uses k-means-like clustering on transition probability vectors
|
||||
fn find_optimal_partition(matrix: &[f32], n: usize, k: usize) -> Partition {
|
||||
if n <= k {
|
||||
// Can't cluster into more groups than states
|
||||
return Partition::sequential(n, k);
|
||||
}
|
||||
|
||||
// Extract row vectors (outgoing transition probabilities)
|
||||
let mut rows: Vec<Vec<f32>> = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
rows.push(matrix[i * n..(i + 1) * n].to_vec());
|
||||
}
|
||||
|
||||
// Simple k-means clustering
|
||||
let labels = kmeans_cluster(&rows, k);
|
||||
|
||||
Partition::from_clustering(labels)
|
||||
}
|
||||
|
||||
/// Simple k-means clustering for transition probability vectors
|
||||
/// Returns cluster labels for each state
|
||||
fn kmeans_cluster(data: &[Vec<f32>], k: usize) -> Vec<usize> {
|
||||
let n = data.len();
|
||||
if n <= k {
|
||||
return (0..n).collect();
|
||||
}
|
||||
|
||||
let dim = data[0].len();
|
||||
|
||||
// Initialize centroids (first k data points)
|
||||
let mut centroids: Vec<Vec<f32>> = data[..k].to_vec();
|
||||
let mut labels = vec![0; n];
|
||||
|
||||
// Iterate until convergence (max 20 iterations)
|
||||
for _ in 0..20 {
|
||||
let old_labels = labels.clone();
|
||||
|
||||
// Assign each point to nearest centroid
|
||||
for (i, point) in data.iter().enumerate() {
|
||||
let mut min_dist = f32::MAX;
|
||||
let mut best_cluster = 0;
|
||||
|
||||
for (c, centroid) in centroids.iter().enumerate() {
|
||||
let dist = euclidean_distance(point, centroid);
|
||||
if dist < min_dist {
|
||||
min_dist = dist;
|
||||
best_cluster = c;
|
||||
}
|
||||
}
|
||||
|
||||
labels[i] = best_cluster;
|
||||
}
|
||||
|
||||
// Update centroids
|
||||
for c in 0..k {
|
||||
let cluster_points: Vec<_> = data
|
||||
.iter()
|
||||
.zip(&labels)
|
||||
.filter(|(_, &label)| label == c)
|
||||
.map(|(point, _)| point)
|
||||
.collect();
|
||||
|
||||
if !cluster_points.is_empty() {
|
||||
centroids[c] = compute_centroid(&cluster_points, dim);
|
||||
}
|
||||
}
|
||||
|
||||
// Check convergence
|
||||
if labels == old_labels {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
labels
|
||||
}
|
||||
|
||||
fn euclidean_distance(a: &[f32], b: &[f32]) -> f32 {
|
||||
a.iter()
|
||||
.zip(b.iter())
|
||||
.map(|(x, y)| (x - y).powi(2))
|
||||
.sum::<f32>()
|
||||
.sqrt()
|
||||
}
|
||||
|
||||
fn compute_centroid(points: &[&Vec<f32>], dim: usize) -> Vec<f32> {
|
||||
let n = points.len() as f32;
|
||||
let mut centroid = vec![0.0; dim];
|
||||
|
||||
for point in points {
|
||||
for (i, &val) in point.iter().enumerate() {
|
||||
centroid[i] += val / n;
|
||||
}
|
||||
}
|
||||
|
||||
centroid
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sequential_partition() {
|
||||
let partition = Partition::sequential(10, 3);
|
||||
|
||||
assert_eq!(partition.num_macro_states(), 4); // [0-2], [3-5], [6-8], [9]
|
||||
assert_eq!(partition.groups[0], vec![0, 1, 2]);
|
||||
assert_eq!(partition.groups[3], vec![9]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coarse_grain_deterministic() {
|
||||
// 4-state cycle: 0→1→2→3→0
|
||||
let mut micro = vec![0.0; 16];
|
||||
micro[0 * 4 + 1] = 1.0;
|
||||
micro[1 * 4 + 2] = 1.0;
|
||||
micro[2 * 4 + 3] = 1.0;
|
||||
micro[3 * 4 + 0] = 1.0;
|
||||
|
||||
// Partition into 2 groups: [0,1] and [2,3]
|
||||
let partition = Partition {
|
||||
groups: vec![vec![0, 1], vec![2, 3]],
|
||||
};
|
||||
|
||||
let macro_matrix = coarse_grain_transition_matrix(µ, &partition);
|
||||
|
||||
// Should get 2×2 matrix
|
||||
assert_eq!(macro_matrix.len(), 4);
|
||||
|
||||
// Group 0 transitions to group 1 with prob 0.5 (state 0→1 or 1→2)
|
||||
assert!((macro_matrix[0 * 2 + 1] - 0.5).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hierarchy_construction() {
|
||||
// 16-state random system
|
||||
let mut matrix = vec![0.0; 256];
|
||||
for i in 0..16 {
|
||||
for j in 0..16 {
|
||||
matrix[i * 16 + j] = ((i + j) % 10) as f32 / 10.0;
|
||||
}
|
||||
// Normalize row
|
||||
let row_sum: f32 = matrix[i * 16..(i + 1) * 16].iter().sum();
|
||||
for j in 0..16 {
|
||||
matrix[i * 16 + j] /= row_sum;
|
||||
}
|
||||
}
|
||||
|
||||
let hierarchy = ScaleHierarchy::build_sequential(matrix, 2);
|
||||
|
||||
// Should have 4 levels (16, 8, 4, 2) - stops at branching_factor
|
||||
assert_eq!(hierarchy.num_scales(), 4);
|
||||
assert_eq!(hierarchy.levels[0].num_states, 16);
|
||||
assert_eq!(hierarchy.levels[1].num_states, 8);
|
||||
assert_eq!(hierarchy.levels[2].num_states, 4);
|
||||
assert_eq!(hierarchy.levels[3].num_states, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partition_merging() {
|
||||
let partition1 = Partition {
|
||||
groups: vec![vec![0, 1], vec![2, 3], vec![4, 5]],
|
||||
};
|
||||
|
||||
let partition2 = Partition {
|
||||
groups: vec![vec![0, 1], vec![2]], // Merge groups 0&1, keep group 2
|
||||
};
|
||||
|
||||
let merged = merge_partitions(&partition1, &partition2);
|
||||
|
||||
assert_eq!(merged.num_macro_states(), 2);
|
||||
assert_eq!(merged.groups[0], vec![0, 1, 2, 3]);
|
||||
assert_eq!(merged.groups[1], vec![4, 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kmeans_clustering() {
|
||||
let data = vec![
|
||||
vec![1.0, 0.0],
|
||||
vec![0.9, 0.1],
|
||||
vec![0.0, 1.0],
|
||||
vec![0.1, 0.9],
|
||||
];
|
||||
|
||||
let labels = kmeans_cluster(&data, 2);
|
||||
|
||||
// Points 0&1 should cluster together, 2&3 together
|
||||
assert_eq!(labels[0], labels[1]);
|
||||
assert_eq!(labels[2], labels[3]);
|
||||
assert_ne!(labels[0], labels[2]);
|
||||
}
|
||||
}
|
||||
359
vendor/ruvector/examples/exo-ai-2025/research/07-causal-emergence/src/effective_information.rs
vendored
Normal file
359
vendor/ruvector/examples/exo-ai-2025/research/07-causal-emergence/src/effective_information.rs
vendored
Normal file
@@ -0,0 +1,359 @@
|
||||
// Effective Information (EI) Calculation with SIMD Acceleration
|
||||
// Implements Hoel's causal emergence framework for O(log n) hierarchical analysis
|
||||
|
||||
use std::simd::prelude::*;
|
||||
use std::simd::StdFloat;
|
||||
|
||||
/// Computes effective information using SIMD acceleration
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `transition_matrix` - Flattened n×n matrix where T[i*n + j] = P(j|i)
|
||||
/// * `n` - Number of states (matrix is n×n)
|
||||
///
|
||||
/// # Returns
|
||||
/// Effective information in bits
|
||||
///
|
||||
/// # Complexity
|
||||
/// O(n²) with SIMD vectorization (8-16× faster than scalar)
|
||||
pub fn compute_ei_simd(transition_matrix: &[f32], n: usize) -> f32 {
|
||||
assert_eq!(transition_matrix.len(), n * n, "Matrix must be n×n");
|
||||
|
||||
// Step 1: Compute marginal output distribution under uniform input
|
||||
// p(j) = (1/n) Σᵢ T[i,j]
|
||||
let p_out = compute_column_means_simd(transition_matrix, n);
|
||||
|
||||
// Step 2: Compute output entropy H(out)
|
||||
let h_out = entropy_simd(&p_out);
|
||||
|
||||
// Step 3: Compute conditional entropy H(out|in)
|
||||
// H(out|in) = (1/n) Σᵢ Σⱼ T[i,j] log₂ T[i,j]
|
||||
let h_cond = conditional_entropy_simd(transition_matrix, n);
|
||||
|
||||
// Step 4: Effective information = H(out) - H(out|in)
|
||||
let ei = h_out - h_cond;
|
||||
|
||||
// Ensure non-negative (numerical errors can cause tiny negatives)
|
||||
ei.max(0.0)
|
||||
}
|
||||
|
||||
/// Computes column means of transition matrix (SIMD accelerated)
|
||||
/// Returns p(j) = mean of column j
|
||||
fn compute_column_means_simd(matrix: &[f32], n: usize) -> Vec<f32> {
|
||||
let mut means = vec![0.0f32; n];
|
||||
|
||||
for j in 0..n {
|
||||
let mut sum = f32x16::splat(0.0);
|
||||
|
||||
// Process 16 rows at a time
|
||||
let full_chunks = (n / 16) * 16;
|
||||
for i in (0..full_chunks).step_by(16) {
|
||||
// Load 16 elements from column j
|
||||
let mut chunk = [0.0f32; 16];
|
||||
for k in 0..16 {
|
||||
chunk[k] = matrix[(i + k) * n + j];
|
||||
}
|
||||
sum += f32x16::from_array(chunk);
|
||||
}
|
||||
|
||||
// Handle remaining rows (scalar)
|
||||
let mut scalar_sum = sum.reduce_sum();
|
||||
for i in full_chunks..n {
|
||||
scalar_sum += matrix[i * n + j];
|
||||
}
|
||||
|
||||
means[j] = scalar_sum / (n as f32);
|
||||
}
|
||||
|
||||
means
|
||||
}
|
||||
|
||||
/// Computes Shannon entropy with SIMD acceleration
|
||||
/// H(X) = -Σ p(x) log₂ p(x)
|
||||
pub fn entropy_simd(probs: &[f32]) -> f32 {
|
||||
let n = probs.len();
|
||||
let mut entropy = f32x16::splat(0.0);
|
||||
|
||||
const EPSILON: f32 = 1e-10;
|
||||
let eps_vec = f32x16::splat(EPSILON);
|
||||
let log2_e = f32x16::splat(std::f32::consts::LOG2_E);
|
||||
|
||||
// Process 16 elements at a time
|
||||
let full_chunks = (n / 16) * 16;
|
||||
for i in (0..full_chunks).step_by(16) {
|
||||
let p = f32x16::from_slice(&probs[i..i + 16]);
|
||||
|
||||
// Clip to avoid log(0)
|
||||
let p_safe = p.simd_max(eps_vec);
|
||||
|
||||
// -p * log₂(p) = -p * ln(p) / ln(2)
|
||||
let log_p = p_safe.ln() * log2_e;
|
||||
entropy -= p * log_p;
|
||||
}
|
||||
|
||||
// Handle remaining elements (scalar)
|
||||
let mut scalar_entropy = entropy.reduce_sum();
|
||||
for i in full_chunks..n {
|
||||
let p = probs[i];
|
||||
if p > EPSILON {
|
||||
scalar_entropy -= p * p.log2();
|
||||
}
|
||||
}
|
||||
|
||||
scalar_entropy
|
||||
}
|
||||
|
||||
/// Computes conditional entropy H(out|in) for transition matrix
|
||||
/// H(out|in) = (1/n) Σᵢ Σⱼ T[i,j] log₂ T[i,j]
|
||||
fn conditional_entropy_simd(matrix: &[f32], n: usize) -> f32 {
|
||||
let mut h_cond = f32x16::splat(0.0);
|
||||
|
||||
const EPSILON: f32 = 1e-10;
|
||||
let eps_vec = f32x16::splat(EPSILON);
|
||||
let log2_e = f32x16::splat(std::f32::consts::LOG2_E);
|
||||
|
||||
// Process matrix in 16-element chunks
|
||||
let total_elements = n * n;
|
||||
let full_chunks = (total_elements / 16) * 16;
|
||||
|
||||
for i in (0..full_chunks).step_by(16) {
|
||||
let t = f32x16::from_slice(&matrix[i..i + 16]);
|
||||
|
||||
// Clip to avoid log(0)
|
||||
let t_safe = t.simd_max(eps_vec);
|
||||
|
||||
// -T[i,j] * log₂(T[i,j])
|
||||
let log_t = t_safe.ln() * log2_e;
|
||||
h_cond -= t * log_t;
|
||||
}
|
||||
|
||||
// Handle remaining elements
|
||||
let mut scalar_sum = h_cond.reduce_sum();
|
||||
for i in full_chunks..total_elements {
|
||||
let t = matrix[i];
|
||||
if t > EPSILON {
|
||||
scalar_sum -= t * t.log2();
|
||||
}
|
||||
}
|
||||
|
||||
// Divide by n (average over uniform input distribution)
|
||||
scalar_sum / (n as f32)
|
||||
}
|
||||
|
||||
/// Computes effective information for multiple scales in parallel
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `transition_matrices` - Vector of transition matrices at different scales
|
||||
/// * `state_counts` - Number of states at each scale
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of EI values, one per scale
|
||||
pub fn compute_ei_multi_scale(
|
||||
transition_matrices: &[Vec<f32>],
|
||||
state_counts: &[usize],
|
||||
) -> Vec<f32> {
|
||||
assert_eq!(transition_matrices.len(), state_counts.len());
|
||||
|
||||
transition_matrices
|
||||
.iter()
|
||||
.zip(state_counts.iter())
|
||||
.map(|(matrix, &n)| compute_ei_simd(matrix, n))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Detects causal emergence by comparing EI across scales
|
||||
///
|
||||
/// # Returns
|
||||
/// (emergent_scale, ei_gain) where:
|
||||
/// - emergent_scale: Index of scale with maximum EI
|
||||
/// - ei_gain: EI(emergent) - EI(micro)
|
||||
pub fn detect_causal_emergence(ei_per_scale: &[f32]) -> Option<(usize, f32)> {
|
||||
if ei_per_scale.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (max_scale, &max_ei) = ei_per_scale
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))?;
|
||||
|
||||
let micro_ei = ei_per_scale[0];
|
||||
let ei_gain = max_ei - micro_ei;
|
||||
|
||||
Some((max_scale, ei_gain))
|
||||
}
|
||||
|
||||
/// Normalized effective information (0 to 1 scale)
|
||||
///
|
||||
/// EI_normalized = EI / log₂(n)
|
||||
/// where log₂(n) is the maximum possible EI
|
||||
pub fn normalized_ei(ei: f32, num_states: usize) -> f32 {
|
||||
if num_states <= 1 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let max_ei = (num_states as f32).log2();
|
||||
(ei / max_ei).min(1.0).max(0.0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ei_deterministic_cycle() {
|
||||
// Deterministic cyclic transition: i → (i+1) mod n
|
||||
let n = 16;
|
||||
let mut matrix = vec![0.0; n * n];
|
||||
for i in 0..n {
|
||||
matrix[i * n + ((i + 1) % n)] = 1.0;
|
||||
}
|
||||
|
||||
let ei = compute_ei_simd(&matrix, n);
|
||||
let expected = (n as f32).log2(); // Should be maximal
|
||||
|
||||
assert!(
|
||||
(ei - expected).abs() < 0.1,
|
||||
"Deterministic system should have EI ≈ log₂(n), got {}, expected {}",
|
||||
ei,
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ei_random() {
|
||||
// Uniform random transitions
|
||||
let n = 16;
|
||||
let matrix = vec![1.0 / (n as f32); n * n];
|
||||
|
||||
let ei = compute_ei_simd(&matrix, n);
|
||||
|
||||
assert!(ei < 0.1, "Random system should have EI ≈ 0, got {}", ei);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ei_identity() {
|
||||
// Identity transition (state doesn't change)
|
||||
let n = 8;
|
||||
let mut matrix = vec![0.0; n * n];
|
||||
for i in 0..n {
|
||||
matrix[i * n + i] = 1.0;
|
||||
}
|
||||
|
||||
let ei = compute_ei_simd(&matrix, n);
|
||||
let expected = (n as f32).log2();
|
||||
|
||||
assert!(
|
||||
(ei - expected).abs() < 0.1,
|
||||
"Identity should have maximal EI, got {}, expected {}",
|
||||
ei,
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_entropy_uniform() {
|
||||
let probs = vec![0.25, 0.25, 0.25, 0.25];
|
||||
let h = entropy_simd(&probs);
|
||||
assert!(
|
||||
(h - 2.0).abs() < 0.01,
|
||||
"Uniform 4-state should have H=2 bits"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_entropy_deterministic() {
|
||||
let probs = vec![1.0, 0.0, 0.0, 0.0];
|
||||
let h = entropy_simd(&probs);
|
||||
assert!(h < 0.01, "Deterministic should have H≈0, got {}", h);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_causal_emergence_detection() {
|
||||
// Create hierarchy where middle scale has highest EI
|
||||
let ei_scales = vec![2.0, 3.5, 4.2, 3.8, 2.5];
|
||||
|
||||
let (emergent_scale, gain) = detect_causal_emergence(&ei_scales).unwrap();
|
||||
|
||||
assert_eq!(emergent_scale, 2, "Should detect scale 2 as emergent");
|
||||
assert!(
|
||||
(gain - 2.2).abs() < 0.01,
|
||||
"EI gain should be 4.2 - 2.0 = 2.2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalized_ei() {
|
||||
let ei = 3.0;
|
||||
let n = 8; // log₂(8) = 3.0
|
||||
|
||||
let norm = normalized_ei(ei, n);
|
||||
assert!((norm - 1.0).abs() < 0.01, "Should normalize to 1.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_scale_computation() {
|
||||
// Test computing EI for multiple scales
|
||||
let matrix1 = vec![0.25; 16]; // 4×4 matrix
|
||||
let matrix2 = vec![0.5; 4]; // 2×2 matrix (correctly sized)
|
||||
|
||||
let matrices = vec![matrix1, matrix2];
|
||||
let counts = vec![4, 2]; // Correct sizes
|
||||
|
||||
// Compute EI for both scales
|
||||
let results = compute_ei_multi_scale(&matrices, &counts);
|
||||
assert_eq!(results.len(), 2);
|
||||
|
||||
// Results should be non-negative
|
||||
for ei in &results {
|
||||
assert!(*ei >= 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmarking utilities
|
||||
#[cfg(feature = "bench")]
|
||||
pub mod bench {
|
||||
use super::*;
|
||||
use std::time::Instant;
|
||||
|
||||
pub fn benchmark_ei(n: usize, iterations: usize) -> f64 {
|
||||
// Create random transition matrix
|
||||
let mut matrix = vec![0.0; n * n];
|
||||
for i in 0..n {
|
||||
let mut row_sum = 0.0;
|
||||
for j in 0..n {
|
||||
let val = (i * n + j) as f32 % 100.0 / 100.0;
|
||||
matrix[i * n + j] = val;
|
||||
row_sum += val;
|
||||
}
|
||||
// Normalize row to sum to 1
|
||||
for j in 0..n {
|
||||
matrix[i * n + j] /= row_sum;
|
||||
}
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
for _ in 0..iterations {
|
||||
compute_ei_simd(&matrix, n);
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
elapsed.as_secs_f64() / iterations as f64
|
||||
}
|
||||
|
||||
pub fn print_benchmark_results() {
|
||||
println!("Effective Information SIMD Benchmarks:");
|
||||
println!("----------------------------------------");
|
||||
|
||||
for n in [16, 64, 256, 1024] {
|
||||
let time = benchmark_ei(n, 100);
|
||||
let states_per_sec = (n * n) as f64 / time;
|
||||
println!(
|
||||
"n={:4}: {:.3}ms ({:.0} states/sec)",
|
||||
n,
|
||||
time * 1000.0,
|
||||
states_per_sec
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
548
vendor/ruvector/examples/exo-ai-2025/research/07-causal-emergence/src/emergence_detection.rs
vendored
Normal file
548
vendor/ruvector/examples/exo-ai-2025/research/07-causal-emergence/src/emergence_detection.rs
vendored
Normal file
@@ -0,0 +1,548 @@
|
||||
// Automatic Emergence Detection and Scale Selection
|
||||
// Implements NeuralRG-inspired methods for optimal coarse-graining
|
||||
|
||||
use crate::causal_hierarchy::{CausalHierarchy, ConsciousnessLevel};
|
||||
use crate::coarse_graining::Partition;
|
||||
|
||||
/// Result of emergence detection analysis
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EmergenceReport {
|
||||
/// Whether emergence was detected
|
||||
pub emergence_detected: bool,
|
||||
/// Scale where emergence is strongest
|
||||
pub emergent_scale: usize,
|
||||
/// EI gain from micro to emergent scale
|
||||
pub ei_gain: f32,
|
||||
/// Percentage increase in causal power
|
||||
pub ei_gain_percent: f32,
|
||||
/// Scale-by-scale EI progression
|
||||
pub ei_progression: Vec<f32>,
|
||||
/// Optimal partition at emergent scale
|
||||
pub optimal_partition: Option<Partition>,
|
||||
}
|
||||
|
||||
/// Comprehensive consciousness assessment report
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConsciousnessReport {
|
||||
/// Whether system meets consciousness criteria
|
||||
pub is_conscious: bool,
|
||||
/// Consciousness level classification
|
||||
pub level: ConsciousnessLevel,
|
||||
/// Quantitative consciousness score (Ψ)
|
||||
pub score: f32,
|
||||
/// Scale at which consciousness emerges
|
||||
pub conscious_scale: usize,
|
||||
/// Whether circular causation is present
|
||||
pub has_circular_causation: bool,
|
||||
/// Effective information at conscious scale
|
||||
pub ei: f32,
|
||||
/// Integrated information at conscious scale
|
||||
pub phi: f32,
|
||||
/// Upward transfer entropy
|
||||
pub te_up: f32,
|
||||
/// Downward transfer entropy
|
||||
pub te_down: f32,
|
||||
/// Emergence analysis
|
||||
pub emergence: EmergenceReport,
|
||||
}
|
||||
|
||||
/// Automatically detects causal emergence in time-series data
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - Time-series of neural or system states
|
||||
/// * `branching_factor` - k for hierarchical coarse-graining
|
||||
/// * `min_ei_gain` - Minimum EI increase to count as emergence
|
||||
///
|
||||
/// # Returns
|
||||
/// Comprehensive emergence report
|
||||
pub fn detect_emergence(
|
||||
data: &[f32],
|
||||
branching_factor: usize,
|
||||
min_ei_gain: f32,
|
||||
) -> EmergenceReport {
|
||||
// Build hierarchical structure
|
||||
let hierarchy = CausalHierarchy::from_time_series(data, branching_factor, false);
|
||||
|
||||
let ei_progression = hierarchy.metrics.ei.clone();
|
||||
|
||||
if ei_progression.is_empty() {
|
||||
return EmergenceReport {
|
||||
emergence_detected: false,
|
||||
emergent_scale: 0,
|
||||
ei_gain: 0.0,
|
||||
ei_gain_percent: 0.0,
|
||||
ei_progression,
|
||||
optimal_partition: None,
|
||||
};
|
||||
}
|
||||
|
||||
// Find scale with maximum EI
|
||||
let micro_ei = ei_progression[0];
|
||||
let (emergent_scale, &max_ei) = ei_progression
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
|
||||
.unwrap();
|
||||
|
||||
let ei_gain = max_ei - micro_ei;
|
||||
let ei_gain_percent = if micro_ei > 1e-6 {
|
||||
(ei_gain / micro_ei) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let emergence_detected = ei_gain > min_ei_gain;
|
||||
|
||||
let optimal_partition = if emergent_scale < hierarchy.hierarchy.levels.len() {
|
||||
Some(hierarchy.hierarchy.levels[emergent_scale].partition.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
EmergenceReport {
|
||||
emergence_detected,
|
||||
emergent_scale,
|
||||
ei_gain,
|
||||
ei_gain_percent,
|
||||
ei_progression,
|
||||
optimal_partition,
|
||||
}
|
||||
}
|
||||
|
||||
/// Comprehensively assesses consciousness using HCC criteria
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - Time-series neural data
|
||||
/// * `branching_factor` - Coarse-graining factor (typically 2-4)
|
||||
/// * `use_optimal_partition` - Whether to use optimal partitioning (slower)
|
||||
/// * `threshold` - Consciousness score threshold (default 5.0)
|
||||
///
|
||||
/// # Returns
|
||||
/// Full consciousness assessment report
|
||||
pub fn assess_consciousness(
|
||||
data: &[f32],
|
||||
branching_factor: usize,
|
||||
use_optimal_partition: bool,
|
||||
threshold: f32,
|
||||
) -> ConsciousnessReport {
|
||||
// Build causal hierarchy with full metrics
|
||||
let hierarchy =
|
||||
CausalHierarchy::from_time_series(data, branching_factor, use_optimal_partition);
|
||||
|
||||
// Extract key metrics at conscious scale
|
||||
let conscious_scale = hierarchy.metrics.optimal_scale;
|
||||
let score = hierarchy.metrics.consciousness_score;
|
||||
let level = hierarchy.consciousness_level();
|
||||
let is_conscious = hierarchy.is_conscious(threshold);
|
||||
let has_circular_causation = hierarchy.has_circular_causation();
|
||||
|
||||
let ei = hierarchy
|
||||
.metrics
|
||||
.ei
|
||||
.get(conscious_scale)
|
||||
.copied()
|
||||
.unwrap_or(0.0);
|
||||
let phi = hierarchy
|
||||
.metrics
|
||||
.phi
|
||||
.get(conscious_scale)
|
||||
.copied()
|
||||
.unwrap_or(0.0);
|
||||
let te_up = hierarchy
|
||||
.metrics
|
||||
.te_up
|
||||
.get(conscious_scale)
|
||||
.copied()
|
||||
.unwrap_or(0.0);
|
||||
let te_down = hierarchy
|
||||
.metrics
|
||||
.te_down
|
||||
.get(conscious_scale)
|
||||
.copied()
|
||||
.unwrap_or(0.0);
|
||||
|
||||
// Run emergence detection
|
||||
let emergence = detect_emergence(data, branching_factor, 0.5);
|
||||
|
||||
ConsciousnessReport {
|
||||
is_conscious,
|
||||
level,
|
||||
score,
|
||||
conscious_scale,
|
||||
has_circular_causation,
|
||||
ei,
|
||||
phi,
|
||||
te_up,
|
||||
te_down,
|
||||
emergence,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares consciousness across multiple datasets (e.g., different states)
|
||||
///
|
||||
/// # Use Cases
|
||||
/// - Awake vs anesthesia
|
||||
/// - Different sleep stages
|
||||
/// - Healthy vs disorder of consciousness
|
||||
///
|
||||
/// # Returns
|
||||
/// Vector of reports, one per dataset
|
||||
pub fn compare_consciousness_states(
|
||||
datasets: &[Vec<f32>],
|
||||
branching_factor: usize,
|
||||
threshold: f32,
|
||||
) -> Vec<ConsciousnessReport> {
|
||||
datasets
|
||||
.iter()
|
||||
.map(|data| assess_consciousness(data, branching_factor, false, threshold))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Finds optimal scale for a given optimization criterion
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ScaleOptimizationCriterion {
|
||||
/// Maximize effective information
|
||||
MaxEI,
|
||||
/// Maximize integrated information
|
||||
MaxPhi,
|
||||
/// Maximize consciousness score
|
||||
MaxPsi,
|
||||
/// Maximize causal emergence (EI gain)
|
||||
MaxEmergence,
|
||||
}
|
||||
|
||||
pub fn find_optimal_scale(
|
||||
hierarchy: &CausalHierarchy,
|
||||
criterion: ScaleOptimizationCriterion,
|
||||
) -> (usize, f32) {
|
||||
let values = match criterion {
|
||||
ScaleOptimizationCriterion::MaxEI => &hierarchy.metrics.ei,
|
||||
ScaleOptimizationCriterion::MaxPhi => &hierarchy.metrics.phi,
|
||||
ScaleOptimizationCriterion::MaxPsi => &hierarchy.metrics.psi,
|
||||
ScaleOptimizationCriterion::MaxEmergence => {
|
||||
// Compute EI gain relative to micro-level
|
||||
let micro_ei = hierarchy.metrics.ei.first().copied().unwrap_or(0.0);
|
||||
let gains: Vec<f32> = hierarchy
|
||||
.metrics
|
||||
.ei
|
||||
.iter()
|
||||
.map(|&ei| ei - micro_ei)
|
||||
.collect();
|
||||
let (scale, &max_gain) = gains
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
|
||||
.unwrap_or((0, &0.0));
|
||||
return (scale, max_gain);
|
||||
}
|
||||
};
|
||||
|
||||
values
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
|
||||
.map(|(idx, &val)| (idx, val))
|
||||
.unwrap_or((0, 0.0))
|
||||
}
|
||||
|
||||
/// Real-time consciousness monitor (streaming data)
|
||||
pub struct ConsciousnessMonitor {
|
||||
window_size: usize,
|
||||
branching_factor: usize,
|
||||
threshold: f32,
|
||||
buffer: Vec<f32>,
|
||||
last_report: Option<ConsciousnessReport>,
|
||||
}
|
||||
|
||||
impl ConsciousnessMonitor {
|
||||
pub fn new(window_size: usize, branching_factor: usize, threshold: f32) -> Self {
|
||||
Self {
|
||||
window_size,
|
||||
branching_factor,
|
||||
threshold,
|
||||
buffer: Vec::with_capacity(window_size),
|
||||
last_report: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add new data point and update consciousness estimate
|
||||
pub fn update(&mut self, value: f32) -> Option<ConsciousnessReport> {
|
||||
self.buffer.push(value);
|
||||
|
||||
// Keep only most recent window
|
||||
if self.buffer.len() > self.window_size {
|
||||
self.buffer.drain(0..(self.buffer.len() - self.window_size));
|
||||
}
|
||||
|
||||
// Need minimum data for analysis
|
||||
if self.buffer.len() < 100 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Compute consciousness assessment
|
||||
let report = assess_consciousness(
|
||||
&self.buffer,
|
||||
self.branching_factor,
|
||||
false, // Use fast sequential partitioning for real-time
|
||||
self.threshold,
|
||||
);
|
||||
|
||||
self.last_report = Some(report.clone());
|
||||
Some(report)
|
||||
}
|
||||
|
||||
pub fn current_score(&self) -> Option<f32> {
|
||||
self.last_report.as_ref().map(|r| r.score)
|
||||
}
|
||||
|
||||
pub fn is_conscious(&self) -> bool {
|
||||
self.last_report
|
||||
.as_ref()
|
||||
.map(|r| r.is_conscious)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Visualizes consciousness metrics over time
|
||||
pub fn consciousness_time_series(
|
||||
data: &[f32],
|
||||
window_size: usize,
|
||||
step_size: usize,
|
||||
branching_factor: usize,
|
||||
threshold: f32,
|
||||
) -> Vec<(usize, ConsciousnessReport)> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
let mut t = 0;
|
||||
while t + window_size <= data.len() {
|
||||
let window = &data[t..t + window_size];
|
||||
|
||||
let report = assess_consciousness(window, branching_factor, false, threshold);
|
||||
results.push((t, report));
|
||||
|
||||
t += step_size;
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Detects transitions in consciousness state
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConsciousnessTransition {
|
||||
pub time: usize,
|
||||
pub from_level: ConsciousnessLevel,
|
||||
pub to_level: ConsciousnessLevel,
|
||||
pub score_change: f32,
|
||||
}
|
||||
|
||||
pub fn detect_consciousness_transitions(
|
||||
time_series_reports: &[(usize, ConsciousnessReport)],
|
||||
min_score_change: f32,
|
||||
) -> Vec<ConsciousnessTransition> {
|
||||
let mut transitions = Vec::new();
|
||||
|
||||
for i in 1..time_series_reports.len() {
|
||||
let (_t_prev, report_prev) = &time_series_reports[i - 1];
|
||||
let (t_curr, report_curr) = &time_series_reports[i];
|
||||
|
||||
let score_change = report_curr.score - report_prev.score;
|
||||
|
||||
if report_prev.level != report_curr.level || score_change.abs() > min_score_change {
|
||||
transitions.push(ConsciousnessTransition {
|
||||
time: *t_curr,
|
||||
from_level: report_prev.level,
|
||||
to_level: report_curr.level,
|
||||
score_change,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
transitions
|
||||
}
|
||||
|
||||
/// Export utilities for visualization and analysis
|
||||
pub mod export {
|
||||
use super::*;
|
||||
|
||||
/// Exports consciousness report as JSON-compatible string
|
||||
pub fn report_to_json(report: &ConsciousnessReport) -> String {
|
||||
format!(
|
||||
r#"{{
|
||||
"is_conscious": {},
|
||||
"level": "{:?}",
|
||||
"score": {},
|
||||
"conscious_scale": {},
|
||||
"has_circular_causation": {},
|
||||
"ei": {},
|
||||
"phi": {},
|
||||
"te_up": {},
|
||||
"te_down": {},
|
||||
"emergence_detected": {},
|
||||
"emergent_scale": {},
|
||||
"ei_gain": {},
|
||||
"ei_gain_percent": {}
|
||||
}}"#,
|
||||
report.is_conscious,
|
||||
report.level,
|
||||
report.score,
|
||||
report.conscious_scale,
|
||||
report.has_circular_causation,
|
||||
report.ei,
|
||||
report.phi,
|
||||
report.te_up,
|
||||
report.te_down,
|
||||
report.emergence.emergence_detected,
|
||||
report.emergence.emergent_scale,
|
||||
report.emergence.ei_gain,
|
||||
report.emergence.ei_gain_percent
|
||||
)
|
||||
}
|
||||
|
||||
/// Exports time series as CSV
|
||||
pub fn time_series_to_csv(results: &[(usize, ConsciousnessReport)]) -> String {
|
||||
let mut csv = String::from("time,score,level,ei,phi,te_up,te_down,emergent_scale\n");
|
||||
|
||||
for (t, report) in results {
|
||||
csv.push_str(&format!(
|
||||
"{},{},{:?},{},{},{},{},{}\n",
|
||||
t,
|
||||
report.score,
|
||||
report.level,
|
||||
report.ei,
|
||||
report.phi,
|
||||
report.te_up,
|
||||
report.te_down,
|
||||
report.emergence.emergent_scale
|
||||
));
|
||||
}
|
||||
|
||||
csv
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn generate_synthetic_conscious_data(n: usize) -> Vec<f32> {
|
||||
// Multi-scale oscillations simulate hierarchical structure
|
||||
(0..n)
|
||||
.map(|t| {
|
||||
let t_f = t as f32;
|
||||
// Low frequency (macro)
|
||||
0.5 * (t_f * 0.01).sin() +
|
||||
// Medium frequency
|
||||
0.3 * (t_f * 0.05).cos() +
|
||||
// High frequency (micro)
|
||||
0.2 * (t_f * 0.2).sin()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_synthetic_unconscious_data(n: usize) -> Vec<f32> {
|
||||
// Random noise - no hierarchical structure
|
||||
(0..n)
|
||||
.map(|t| ((t * 12345 + 67890) % 1000) as f32 / 1000.0)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_emergence_detection() {
|
||||
let data = generate_synthetic_conscious_data(500);
|
||||
let report = detect_emergence(&data, 2, 0.1);
|
||||
|
||||
assert!(!report.ei_progression.is_empty());
|
||||
// Multi-scale data should show some emergence
|
||||
assert!(report.ei_gain >= 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_consciousness_assessment() {
|
||||
let conscious_data = generate_synthetic_conscious_data(500);
|
||||
let report = assess_consciousness(&conscious_data, 2, false, 1.0);
|
||||
|
||||
// Should detect some level of organization
|
||||
assert!(report.score >= 0.0);
|
||||
assert_eq!(
|
||||
report.emergence.ei_progression.len(),
|
||||
report.ei_progression_len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_consciousness_comparison() {
|
||||
let data1 = generate_synthetic_conscious_data(300);
|
||||
let data2 = generate_synthetic_unconscious_data(300);
|
||||
|
||||
let datasets = vec![data1, data2];
|
||||
let reports = compare_consciousness_states(&datasets, 2, 2.0);
|
||||
|
||||
assert_eq!(reports.len(), 2);
|
||||
// First should have higher score than second (multi-scale vs noise)
|
||||
assert!(reports[0].score >= reports[1].score);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_consciousness_monitor() {
|
||||
let mut monitor = ConsciousnessMonitor::new(200, 2, 2.0);
|
||||
|
||||
let data = generate_synthetic_conscious_data(500);
|
||||
|
||||
let mut reports = Vec::new();
|
||||
for &value in &data {
|
||||
if let Some(report) = monitor.update(value) {
|
||||
reports.push(report);
|
||||
}
|
||||
}
|
||||
|
||||
// Should generate multiple reports as buffer fills
|
||||
assert!(!reports.is_empty());
|
||||
|
||||
// Current score should be available
|
||||
assert!(monitor.current_score().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transition_detection() {
|
||||
// Create data with clear transition
|
||||
let mut data = generate_synthetic_conscious_data(250);
|
||||
data.extend(generate_synthetic_unconscious_data(250));
|
||||
|
||||
let time_series = consciousness_time_series(&data, 100, 50, 2, 1.0);
|
||||
let transitions = detect_consciousness_transitions(&time_series, 0.1);
|
||||
|
||||
// Should generate multiple time windows
|
||||
assert!(!time_series.is_empty());
|
||||
|
||||
// Transitions might be detected depending on threshold
|
||||
// Just ensure function works correctly
|
||||
assert!(transitions.len() <= time_series.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_export() {
|
||||
let data = generate_synthetic_conscious_data(200);
|
||||
let report = assess_consciousness(&data, 2, false, 2.0);
|
||||
|
||||
let json = export::report_to_json(&report);
|
||||
assert!(json.contains("is_conscious"));
|
||||
assert!(json.contains("score"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_csv_export() {
|
||||
let data = generate_synthetic_conscious_data(300);
|
||||
let time_series = consciousness_time_series(&data, 100, 50, 2, 2.0);
|
||||
|
||||
let csv = export::time_series_to_csv(&time_series);
|
||||
assert!(csv.contains("time,score"));
|
||||
assert!(csv.contains("\n")); // Has multiple lines
|
||||
}
|
||||
|
||||
// Helper method for test
|
||||
impl ConsciousnessReport {
|
||||
fn ei_progression_len(&self) -> usize {
|
||||
self.emergence.ei_progression.len()
|
||||
}
|
||||
}
|
||||
}
|
||||
130
vendor/ruvector/examples/exo-ai-2025/research/07-causal-emergence/src/lib.rs
vendored
Normal file
130
vendor/ruvector/examples/exo-ai-2025/research/07-causal-emergence/src/lib.rs
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
//! # Causal Emergence: Hierarchical Causal Consciousness (HCC) Framework
|
||||
//!
|
||||
//! This library implements Erik Hoel's causal emergence theory with SIMD acceleration
|
||||
//! for O(log n) consciousness detection through multi-scale information-theoretic analysis.
|
||||
//!
|
||||
//! ## Key Concepts
|
||||
//!
|
||||
//! - **Effective Information (EI)**: Measures causal power at each scale
|
||||
//! - **Integrated Information (Φ)**: Measures irreducibility of causal structure
|
||||
//! - **Transfer Entropy (TE)**: Measures directed information flow between scales
|
||||
//! - **Consciousness Score (Ψ)**: Combines all metrics into unified measure
|
||||
//!
|
||||
//! ## Quick Start
|
||||
//!
|
||||
//! ```rust
|
||||
//! use causal_emergence::*;
|
||||
//!
|
||||
//! // Generate synthetic neural data
|
||||
//! let neural_data: Vec<f32> = (0..1000)
|
||||
//! .map(|t| (t as f32 * 0.1).sin())
|
||||
//! .collect();
|
||||
//!
|
||||
//! // Assess consciousness
|
||||
//! let report = assess_consciousness(
|
||||
//! &neural_data,
|
||||
//! 2, // branching factor
|
||||
//! false, // use fast partitioning
|
||||
//! 5.0 // consciousness threshold
|
||||
//! );
|
||||
//!
|
||||
//! // Check results
|
||||
//! if report.is_conscious {
|
||||
//! println!("Consciousness detected!");
|
||||
//! println!("Level: {:?}", report.level);
|
||||
//! println!("Score: {}", report.score);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Modules
|
||||
//!
|
||||
//! - `effective_information`: SIMD-accelerated EI calculation
|
||||
//! - `coarse_graining`: Multi-scale hierarchical coarse-graining
|
||||
//! - `causal_hierarchy`: Transfer entropy and consciousness metrics
|
||||
//! - `emergence_detection`: Automatic scale selection and consciousness assessment
|
||||
|
||||
// Feature gate for SIMD (stable in Rust 1.80+)
|
||||
#![feature(portable_simd)]
|
||||
|
||||
pub mod causal_hierarchy;
|
||||
pub mod coarse_graining;
|
||||
pub mod effective_information;
|
||||
pub mod emergence_detection;
|
||||
|
||||
// Re-export key types and functions for convenience
|
||||
pub use effective_information::{
|
||||
compute_ei_multi_scale, compute_ei_simd, detect_causal_emergence, entropy_simd, normalized_ei,
|
||||
};
|
||||
|
||||
pub use coarse_graining::{coarse_grain_transition_matrix, Partition, ScaleHierarchy, ScaleLevel};
|
||||
|
||||
pub use causal_hierarchy::{
|
||||
transfer_entropy, CausalHierarchy, ConsciousnessLevel, HierarchyMetrics,
|
||||
};
|
||||
|
||||
pub use emergence_detection::{
|
||||
assess_consciousness, compare_consciousness_states, consciousness_time_series,
|
||||
detect_consciousness_transitions, detect_emergence, find_optimal_scale, ConsciousnessMonitor,
|
||||
ConsciousnessReport, ConsciousnessTransition, EmergenceReport, ScaleOptimizationCriterion,
|
||||
};
|
||||
|
||||
/// Library version
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Recommended branching factor for most use cases
|
||||
pub const DEFAULT_BRANCHING_FACTOR: usize = 2;
|
||||
|
||||
/// Default consciousness threshold (Ψ > 5.0 indicates consciousness)
|
||||
pub const DEFAULT_CONSCIOUSNESS_THRESHOLD: f32 = 5.0;
|
||||
|
||||
/// Minimum data points required for reliable analysis
|
||||
pub const MIN_DATA_POINTS: usize = 100;
|
||||
|
||||
#[cfg(test)]
|
||||
mod integration_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_full_pipeline() {
|
||||
// Generate multi-scale data
|
||||
let data: Vec<f32> = (0..500)
|
||||
.map(|t| {
|
||||
let t_f = t as f32;
|
||||
0.5 * (t_f * 0.05).sin() + 0.3 * (t_f * 0.15).cos() + 0.2 * (t_f * 0.5).sin()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Run full consciousness assessment
|
||||
let report = assess_consciousness(&data, DEFAULT_BRANCHING_FACTOR, false, 1.0);
|
||||
|
||||
// Basic sanity checks
|
||||
assert!(report.score >= 0.0);
|
||||
assert!(report.ei >= 0.0);
|
||||
assert!(report.phi >= 0.0);
|
||||
assert!(!report.emergence.ei_progression.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_emergence_detection_pipeline() {
|
||||
let data: Vec<f32> = (0..300).map(|t| (t as f32 * 0.1).sin()).collect();
|
||||
|
||||
let emergence_report = detect_emergence(&data, 2, 0.1);
|
||||
|
||||
assert!(!emergence_report.ei_progression.is_empty());
|
||||
assert!(emergence_report.ei_gain >= 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_real_time_monitoring() {
|
||||
let mut monitor = ConsciousnessMonitor::new(200, 2, 5.0);
|
||||
|
||||
// Stream data
|
||||
for t in 0..300 {
|
||||
let value = (t as f32 * 0.1).sin();
|
||||
monitor.update(value);
|
||||
}
|
||||
|
||||
// Should have a score by now
|
||||
assert!(monitor.current_score().is_some());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user