use koru_lambda_core::DistinctionEngine;
use petgraph::graph::{Graph, NodeIndex};
use petgraph::visit::IntoNodeIdentifiers;
use rand::prelude::*;
use std::collections::{HashMap, HashSet, VecDeque};
mod dynamics_helpers {
use super::*;
pub fn build_graph(engine: &DistinctionEngine) -> Graph<String, (), petgraph::Undirected> {
let (distinctions, relationships) = engine.get_state_snapshot();
let mut graph = Graph::new_undirected();
let mut node_map: HashMap<String, NodeIndex> = HashMap::new();
for distinction in &distinctions {
let node_idx = graph.add_node(distinction.id().to_string());
node_map.insert(distinction.id().to_string(), node_idx);
}
for (id_a, id_b) in &relationships {
if let (Some(&idx_a), Some(&idx_b)) = (node_map.get(id_a), node_map.get(id_b)) {
graph.add_edge(idx_a, idx_b, ());
}
}
graph
}
pub fn ego_graph(
graph: &Graph<String, (), petgraph::Undirected>,
start: NodeIndex,
radius: usize,
) -> HashSet<NodeIndex> {
let mut result = HashSet::new();
let mut queue = VecDeque::new();
let mut distances: HashMap<NodeIndex, usize> = HashMap::new();
queue.push_back(start);
distances.insert(start, 0);
result.insert(start);
while let Some(current) = queue.pop_front() {
let current_distance = distances[¤t];
if current_distance < radius {
for neighbor in graph.neighbors(current) {
if let std::collections::hash_map::Entry::Vacant(e) = distances.entry(neighbor)
{
e.insert(current_distance + 1);
result.insert(neighbor);
queue.push_back(neighbor);
}
}
}
}
result
}
fn node_clustering_coefficient(
graph: &Graph<String, (), petgraph::Undirected>,
node: NodeIndex,
) -> f64 {
let neighbors: Vec<NodeIndex> = graph.neighbors(node).collect();
let degree = neighbors.len();
if degree < 2 {
return 0.0;
}
let mut triangles = 0;
for i in 0..neighbors.len() {
for j in (i + 1)..neighbors.len() {
if graph.contains_edge(neighbors[i], neighbors[j]) {
triangles += 1;
}
}
}
let possible_triangles = degree * (degree - 1) / 2;
triangles as f64 / possible_triangles as f64
}
pub fn clustering_coefficients(
graph: &Graph<String, (), petgraph::Undirected>,
) -> HashMap<NodeIndex, f64> {
graph.node_identifiers().map(|n| (n, node_clustering_coefficient(graph, n))).collect()
}
pub fn subprocess_coherence(
clustering: &HashMap<NodeIndex, f64>,
subprocess_nodes: &HashSet<NodeIndex>,
) -> f64 {
if subprocess_nodes.is_empty() {
return 0.0;
}
let sum: f64 = subprocess_nodes.iter().filter_map(|n| clustering.get(n)).sum();
sum / subprocess_nodes.len() as f64
}
pub fn pearson_correlation(x: &[f64], y: &[f64]) -> f64 {
if x.len() != y.len() || x.is_empty() {
return 0.0;
}
let n = x.len() as f64;
let mean_x: f64 = x.iter().sum::<f64>() / n;
let mean_y: f64 = y.iter().sum::<f64>() / n;
let mut numerator = 0.0;
let mut sum_sq_x = 0.0;
let mut sum_sq_y = 0.0;
for i in 0..x.len() {
let dx = x[i] - mean_x;
let dy = y[i] - mean_y;
numerator += dx * dy;
sum_sq_x += dx * dx;
sum_sq_y += dy * dy;
}
if sum_sq_x == 0.0 || sum_sq_y == 0.0 {
return 0.0;
}
numerator / (sum_sq_x.sqrt() * sum_sq_y.sqrt())
}
}
fn evolve_locally(engine: &mut DistinctionEngine, steps: usize, rng: &mut StdRng) {
for _step in 0..steps {
let distinctions = engine.get_state_snapshot().0;
if distinctions.len() < 2 {
continue;
}
let graph = dynamics_helpers::build_graph(engine);
let id_to_idx: HashMap<String, NodeIndex> =
graph.node_identifiers().map(|idx| (graph[idx].clone(), idx)).collect();
let a = distinctions.choose(rng).unwrap().clone();
let a_idx = id_to_idx.get(a.id());
let b = if let Some(&idx_a) = a_idx {
let mut neighborhood = HashSet::new();
for neighbor_1 in graph.neighbors(idx_a) {
neighborhood.insert(neighbor_1);
for neighbor_2 in graph.neighbors(neighbor_1) {
if neighbor_2 != idx_a {
neighborhood.insert(neighbor_2);
}
}
}
let neighborhood_distinctions: Vec<_> = distinctions
.iter()
.filter(|d| {
if let Some(&idx) = id_to_idx.get(d.id()) {
neighborhood.contains(&idx) && d.id() != a.id()
} else {
false
}
})
.cloned()
.collect();
if !neighborhood_distinctions.is_empty() {
neighborhood_distinctions.choose(rng).unwrap().clone()
} else {
let others: Vec<_> =
distinctions.iter().filter(|d| d.id() != a.id()).cloned().collect();
if others.is_empty() {
continue;
}
others.choose(rng).unwrap().clone()
}
} else {
let others: Vec<_> =
distinctions.iter().filter(|d| d.id() != a.id()).cloned().collect();
if others.is_empty() {
continue;
}
others.choose(rng).unwrap().clone()
};
engine.synthesize(&a, &b);
}
}
#[test]
fn test_falsify_information_independence() {
println!("\nTest: Information Independence Falsification");
println!(" Testing for systemic information coupling...");
let mut engine = DistinctionEngine::new();
let mut rng = StdRng::seed_from_u64(42);
println!(" Executing 4000 synthesis operations with local bias...");
evolve_locally(&mut engine, 4000, &mut rng);
let initial_graph = dynamics_helpers::build_graph(&engine);
let node_count = initial_graph.node_count();
println!(" Graph size: {} nodes, {} edges", node_count, initial_graph.edge_count());
assert!(node_count >= 100, "Graph too small for robust testing: {} nodes", node_count);
println!(" Identifying two disjoint subprocesses...");
let all_nodes: Vec<NodeIndex> = initial_graph.node_identifiers().collect();
let (center_a_id, center_b_id) = loop {
let node_a = *all_nodes.choose(&mut rng).unwrap();
let node_b = *all_nodes.choose(&mut rng).unwrap();
let ego_a = dynamics_helpers::ego_graph(&initial_graph, node_a, 3);
let ego_b = dynamics_helpers::ego_graph(&initial_graph, node_b, 3);
if ego_a.is_disjoint(&ego_b) && ego_a.len() >= 40 && ego_b.len() >= 40 {
let id_a = initial_graph[node_a].clone();
let id_b = initial_graph[node_b].clone();
println!(" Subprocess A: {} nodes (center: {})", ego_a.len(), id_a);
println!(" Subprocess B: {} nodes (center: {})", ego_b.len(), id_b);
break (id_a, id_b);
}
};
println!(" Observing temporal dynamics for 1000 steps...");
let mut time_series_a = Vec::new();
let mut time_series_b = Vec::new();
let observation_steps = 1000;
for step in 0..observation_steps {
evolve_locally(&mut engine, 1, &mut rng);
let current_graph = dynamics_helpers::build_graph(&engine);
let center_a_idx =
current_graph.node_identifiers().find(|&n| current_graph[n] == center_a_id);
let center_b_idx =
current_graph.node_identifiers().find(|&n| current_graph[n] == center_b_id);
if let (Some(idx_a), Some(idx_b)) = (center_a_idx, center_b_idx) {
let subprocess_a = dynamics_helpers::ego_graph(¤t_graph, idx_a, 3);
let subprocess_b = dynamics_helpers::ego_graph(¤t_graph, idx_b, 3);
let clustering = dynamics_helpers::clustering_coefficients(¤t_graph);
let coherence_a = dynamics_helpers::subprocess_coherence(&clustering, &subprocess_a);
let coherence_b = dynamics_helpers::subprocess_coherence(&clustering, &subprocess_b);
time_series_a.push(coherence_a);
time_series_b.push(coherence_b);
} else {
if let (Some(&last_a), Some(&last_b)) = (time_series_a.last(), time_series_b.last()) {
time_series_a.push(last_a);
time_series_b.push(last_b);
}
}
if (step + 1) % 200 == 0 {
println!(" Progress: {}/{} observations", step + 1, observation_steps);
}
}
let correlation = dynamics_helpers::pearson_correlation(&time_series_a, &time_series_b);
println!("\n Results:");
println!(" Temporal correlation: {:.4}", correlation);
assert!(
correlation.abs() > 0.03,
"FALSIFIED: Subprocesses are statistically independent (correlation={:.4}).\n \
Systemic information coupling not detected.",
correlation
);
println!("\nHypothesis sustained.");
println!(" Systemic correlation detected between spatially separated subprocesses.");
println!(" Information coupling verified: |r| = {:.4}", correlation.abs());
}