Files
wifi-densepose/crates/prime-radiant/tests/integration/coherence_tests.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

773 lines
22 KiB
Rust

//! Integration tests for Coherence Computation
//!
//! Tests the Coherence Computation bounded context, verifying:
//! - Full energy computation from graph state
//! - Incremental updates when nodes change
//! - Spectral drift detection
//! - Hotspot identification
//! - Caching and fingerprint-based staleness
use std::collections::HashMap;
// ============================================================================
// TEST INFRASTRUCTURE
// ============================================================================
/// Simple restriction map for testing
struct RestrictionMap {
matrix: Vec<Vec<f32>>,
bias: Vec<f32>,
}
impl RestrictionMap {
fn new(rows: usize, cols: usize) -> Self {
// Identity-like (truncated or padded)
let matrix: Vec<Vec<f32>> = (0..rows)
.map(|i| (0..cols).map(|j| if i == j { 1.0 } else { 0.0 }).collect())
.collect();
let bias = vec![0.0; rows];
Self { matrix, bias }
}
fn apply(&self, input: &[f32]) -> Vec<f32> {
self.matrix
.iter()
.zip(&self.bias)
.map(|(row, b)| row.iter().zip(input).map(|(a, x)| a * x).sum::<f32>() + b)
.collect()
}
fn output_dim(&self) -> usize {
self.matrix.len()
}
}
/// Simple edge for testing
struct TestEdge {
source: u64,
target: u64,
weight: f32,
rho_source: RestrictionMap,
rho_target: RestrictionMap,
}
impl TestEdge {
fn compute_residual(&self, states: &HashMap<u64, Vec<f32>>) -> Option<Vec<f32>> {
let source_state = states.get(&self.source)?;
let target_state = states.get(&self.target)?;
let projected_source = self.rho_source.apply(source_state);
let projected_target = self.rho_target.apply(target_state);
Some(
projected_source
.iter()
.zip(&projected_target)
.map(|(a, b)| a - b)
.collect(),
)
}
fn compute_energy(&self, states: &HashMap<u64, Vec<f32>>) -> Option<f32> {
let residual = self.compute_residual(states)?;
let norm_sq: f32 = residual.iter().map(|x| x * x).sum();
Some(self.weight * norm_sq)
}
}
/// Simple coherence energy computation
fn compute_total_energy(
states: &HashMap<u64, Vec<f32>>,
edges: &[TestEdge],
) -> (f32, HashMap<usize, f32>) {
let mut total = 0.0;
let mut edge_energies = HashMap::new();
for (i, edge) in edges.iter().enumerate() {
if let Some(energy) = edge.compute_energy(states) {
total += energy;
edge_energies.insert(i, energy);
}
}
(total, edge_energies)
}
// ============================================================================
// ENERGY COMPUTATION TESTS
// ============================================================================
#[test]
fn test_energy_computation_consistent_section() {
// A consistent section (all nodes agree) should have zero energy
let mut states = HashMap::new();
states.insert(1, vec![1.0, 0.5, 0.3]);
states.insert(2, vec![1.0, 0.5, 0.3]); // Same state
let edges = vec![TestEdge {
source: 1,
target: 2,
weight: 1.0,
rho_source: RestrictionMap::new(3, 3),
rho_target: RestrictionMap::new(3, 3),
}];
let (total, _) = compute_total_energy(&states, &edges);
// Energy should be zero (or very close) for consistent section
assert!(total < 1e-10, "Expected near-zero energy, got {}", total);
}
#[test]
fn test_energy_computation_inconsistent_section() {
// Inconsistent states should produce positive energy
let mut states = HashMap::new();
states.insert(1, vec![1.0, 0.5, 0.3]);
states.insert(2, vec![0.5, 0.8, 0.1]); // Different state
let edges = vec![TestEdge {
source: 1,
target: 2,
weight: 1.0,
rho_source: RestrictionMap::new(3, 3),
rho_target: RestrictionMap::new(3, 3),
}];
let (total, _) = compute_total_energy(&states, &edges);
// Compute expected energy manually
let residual = vec![1.0 - 0.5, 0.5 - 0.8, 0.3 - 0.1]; // [0.5, -0.3, 0.2]
let expected: f32 = residual.iter().map(|x| x * x).sum(); // 0.25 + 0.09 + 0.04 = 0.38
assert!(
(total - expected).abs() < 1e-6,
"Expected energy {}, got {}",
expected,
total
);
}
#[test]
fn test_energy_computation_weighted_edges() {
// Edge weight should scale energy proportionally
let mut states = HashMap::new();
states.insert(1, vec![1.0, 0.0]);
states.insert(2, vec![0.0, 0.0]);
let weight1 = 1.0;
let weight10 = 10.0;
let edges_w1 = vec![TestEdge {
source: 1,
target: 2,
weight: weight1,
rho_source: RestrictionMap::new(2, 2),
rho_target: RestrictionMap::new(2, 2),
}];
let edges_w10 = vec![TestEdge {
source: 1,
target: 2,
weight: weight10,
rho_source: RestrictionMap::new(2, 2),
rho_target: RestrictionMap::new(2, 2),
}];
let (energy_w1, _) = compute_total_energy(&states, &edges_w1);
let (energy_w10, _) = compute_total_energy(&states, &edges_w10);
assert!(
(energy_w10 / energy_w1 - 10.0).abs() < 1e-6,
"Expected 10x energy scaling"
);
}
#[test]
fn test_energy_is_nonnegative() {
// Energy should always be non-negative (sum of squared terms)
use rand::Rng;
let mut rng = rand::thread_rng();
for _ in 0..100 {
let mut states = HashMap::new();
states.insert(1, (0..4).map(|_| rng.gen_range(-10.0..10.0)).collect());
states.insert(2, (0..4).map(|_| rng.gen_range(-10.0..10.0)).collect());
let edges = vec![TestEdge {
source: 1,
target: 2,
weight: rng.gen_range(0.0..10.0),
rho_source: RestrictionMap::new(4, 4),
rho_target: RestrictionMap::new(4, 4),
}];
let (total, _) = compute_total_energy(&states, &edges);
assert!(total >= 0.0, "Energy must be non-negative, got {}", total);
}
}
#[test]
fn test_energy_with_multiple_edges() {
// Total energy should be sum of individual edge energies
let mut states = HashMap::new();
states.insert(1, vec![1.0, 0.0]);
states.insert(2, vec![0.5, 0.0]);
states.insert(3, vec![0.0, 0.0]);
let edges = vec![
TestEdge {
source: 1,
target: 2,
weight: 1.0,
rho_source: RestrictionMap::new(2, 2),
rho_target: RestrictionMap::new(2, 2),
},
TestEdge {
source: 2,
target: 3,
weight: 1.0,
rho_source: RestrictionMap::new(2, 2),
rho_target: RestrictionMap::new(2, 2),
},
];
let (total, edge_energies) = compute_total_energy(&states, &edges);
let sum_of_parts: f32 = edge_energies.values().sum();
assert!(
(total - sum_of_parts).abs() < 1e-10,
"Total should equal sum of parts"
);
// Verify individual energies
// Edge 1-2: residual = [0.5, 0.0], energy = 0.25
// Edge 2-3: residual = [0.5, 0.0], energy = 0.25
assert!((total - 0.5).abs() < 1e-6);
}
// ============================================================================
// INCREMENTAL UPDATE TESTS
// ============================================================================
#[test]
fn test_incremental_update_single_node() {
// Updating a single node should only affect incident edges
let mut states = HashMap::new();
states.insert(1, vec![1.0, 0.0]);
states.insert(2, vec![0.5, 0.0]);
states.insert(3, vec![0.0, 0.0]);
// Edges: 1-2, 3 is isolated
let edges = vec![TestEdge {
source: 1,
target: 2,
weight: 1.0,
rho_source: RestrictionMap::new(2, 2),
rho_target: RestrictionMap::new(2, 2),
}];
let (energy_before, _) = compute_total_energy(&states, &edges);
// Update node 1
states.insert(1, vec![0.8, 0.0]);
let (energy_after, _) = compute_total_energy(&states, &edges);
// Energy should change because node 1 is incident to an edge
assert_ne!(
energy_before, energy_after,
"Energy should change when incident node updates"
);
// Update isolated node 3
states.insert(3, vec![0.5, 0.5]);
let (energy_isolated, _) = compute_total_energy(&states, &edges);
// Energy should NOT change because node 3 is isolated
assert!(
(energy_after - energy_isolated).abs() < 1e-10,
"Energy should not change when isolated node updates"
);
}
#[test]
fn test_incremental_update_affected_edges() {
// Helper to find edges affected by a node update
fn affected_edges(node_id: u64, edges: &[TestEdge]) -> Vec<usize> {
edges
.iter()
.enumerate()
.filter(|(_, e)| e.source == node_id || e.target == node_id)
.map(|(i, _)| i)
.collect()
}
let edges = vec![
TestEdge {
source: 1,
target: 2,
weight: 1.0,
rho_source: RestrictionMap::new(2, 2),
rho_target: RestrictionMap::new(2, 2),
},
TestEdge {
source: 2,
target: 3,
weight: 1.0,
rho_source: RestrictionMap::new(2, 2),
rho_target: RestrictionMap::new(2, 2),
},
TestEdge {
source: 3,
target: 4,
weight: 1.0,
rho_source: RestrictionMap::new(2, 2),
rho_target: RestrictionMap::new(2, 2),
},
];
// Node 2 is incident to edges 0 and 1
let affected = affected_edges(2, &edges);
assert_eq!(affected, vec![0, 1]);
// Node 1 is incident to edge 0 only
let affected = affected_edges(1, &edges);
assert_eq!(affected, vec![0]);
// Node 4 is incident to edge 2 only
let affected = affected_edges(4, &edges);
assert_eq!(affected, vec![2]);
}
#[test]
fn test_incremental_vs_full_recomputation() {
// Incremental and full recomputation should produce the same result
let mut states = HashMap::new();
states.insert(1, vec![1.0, 0.5, 0.3]);
states.insert(2, vec![0.8, 0.6, 0.4]);
states.insert(3, vec![0.6, 0.7, 0.5]);
let edges = vec![
TestEdge {
source: 1,
target: 2,
weight: 1.0,
rho_source: RestrictionMap::new(3, 3),
rho_target: RestrictionMap::new(3, 3),
},
TestEdge {
source: 2,
target: 3,
weight: 1.0,
rho_source: RestrictionMap::new(3, 3),
rho_target: RestrictionMap::new(3, 3),
},
];
// Full computation
let (energy_full, _) = compute_total_energy(&states, &edges);
// Simulate incremental by computing only affected edges
let affected_by_node2: Vec<usize> = edges
.iter()
.enumerate()
.filter(|(_, e)| e.source == 2 || e.target == 2)
.map(|(i, _)| i)
.collect();
let mut incremental_sum = 0.0;
for i in 0..edges.len() {
if let Some(energy) = edges[i].compute_energy(&states) {
incremental_sum += energy;
}
}
assert!(
(energy_full - incremental_sum).abs() < 1e-10,
"Incremental and full should match"
);
}
// ============================================================================
// RESIDUAL COMPUTATION TESTS
// ============================================================================
#[test]
fn test_residual_symmetry() {
// r_e for edge (u,v) should be negation of r_e for edge (v,u)
// when restriction maps are the same
let mut states = HashMap::new();
states.insert(1, vec![1.0, 0.5]);
states.insert(2, vec![0.8, 0.6]);
let rho = RestrictionMap::new(2, 2);
let edge_uv = TestEdge {
source: 1,
target: 2,
weight: 1.0,
rho_source: RestrictionMap::new(2, 2),
rho_target: RestrictionMap::new(2, 2),
};
let edge_vu = TestEdge {
source: 2,
target: 1,
weight: 1.0,
rho_source: RestrictionMap::new(2, 2),
rho_target: RestrictionMap::new(2, 2),
};
let r_uv = edge_uv.compute_residual(&states).unwrap();
let r_vu = edge_vu.compute_residual(&states).unwrap();
// Check that r_uv = -r_vu
for (a, b) in r_uv.iter().zip(&r_vu) {
assert!(
(a + b).abs() < 1e-10,
"Residuals should be negations of each other"
);
}
}
#[test]
fn test_residual_dimension() {
// Residual dimension should match restriction map output dimension
let mut states = HashMap::new();
states.insert(1, vec![1.0, 0.5, 0.3, 0.2]);
states.insert(2, vec![0.8, 0.6, 0.4, 0.3]);
let edge = TestEdge {
source: 1,
target: 2,
weight: 1.0,
rho_source: RestrictionMap::new(2, 4), // 4D -> 2D
rho_target: RestrictionMap::new(2, 4),
};
let residual = edge.compute_residual(&states).unwrap();
assert_eq!(
residual.len(),
edge.rho_source.output_dim(),
"Residual dimension should match restriction map output"
);
}
// ============================================================================
// HOTSPOT IDENTIFICATION TESTS
// ============================================================================
#[test]
fn test_hotspot_identification() {
// Find edges with highest energy
fn find_hotspots(edge_energies: &HashMap<usize, f32>, k: usize) -> Vec<(usize, f32)> {
let mut sorted: Vec<_> = edge_energies.iter().collect();
sorted.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap());
sorted.into_iter().take(k).map(|(i, e)| (*i, *e)).collect()
}
let mut states = HashMap::new();
states.insert(1, vec![1.0, 0.0]);
states.insert(2, vec![0.1, 0.0]); // Large difference with 1
states.insert(3, vec![0.05, 0.0]); // Small difference with 2
let edges = vec![
TestEdge {
source: 1,
target: 2,
weight: 1.0,
rho_source: RestrictionMap::new(2, 2),
rho_target: RestrictionMap::new(2, 2),
},
TestEdge {
source: 2,
target: 3,
weight: 1.0,
rho_source: RestrictionMap::new(2, 2),
rho_target: RestrictionMap::new(2, 2),
},
];
let (_, edge_energies) = compute_total_energy(&states, &edges);
let hotspots = find_hotspots(&edge_energies, 1);
// Edge 0 (1-2) should have higher energy
assert_eq!(hotspots[0].0, 0, "Edge 1-2 should be the hotspot");
assert!(
edge_energies.get(&0).unwrap() > edge_energies.get(&1).unwrap(),
"Edge 1-2 should have higher energy than edge 2-3"
);
}
// ============================================================================
// SCOPE-BASED ENERGY TESTS
// ============================================================================
#[test]
fn test_energy_by_scope() {
// Energy can be aggregated by scope (namespace)
let mut states = HashMap::new();
let mut node_scopes: HashMap<u64, String> = HashMap::new();
// Finance nodes
states.insert(1, vec![1.0, 0.5]);
states.insert(2, vec![0.8, 0.6]);
node_scopes.insert(1, "finance".to_string());
node_scopes.insert(2, "finance".to_string());
// Medical nodes
states.insert(3, vec![0.5, 0.3]);
states.insert(4, vec![0.2, 0.1]);
node_scopes.insert(3, "medical".to_string());
node_scopes.insert(4, "medical".to_string());
let edges = vec![
TestEdge {
source: 1,
target: 2,
weight: 1.0,
rho_source: RestrictionMap::new(2, 2),
rho_target: RestrictionMap::new(2, 2),
},
TestEdge {
source: 3,
target: 4,
weight: 1.0,
rho_source: RestrictionMap::new(2, 2),
rho_target: RestrictionMap::new(2, 2),
},
];
fn energy_by_scope(
edges: &[TestEdge],
states: &HashMap<u64, Vec<f32>>,
node_scopes: &HashMap<u64, String>,
) -> HashMap<String, f32> {
let mut scope_energy: HashMap<String, f32> = HashMap::new();
for edge in edges {
if let Some(energy) = edge.compute_energy(states) {
let source_scope = node_scopes.get(&edge.source).cloned().unwrap_or_default();
*scope_energy.entry(source_scope).or_insert(0.0) += energy;
}
}
scope_energy
}
let by_scope = energy_by_scope(&edges, &states, &node_scopes);
assert!(by_scope.contains_key("finance"));
assert!(by_scope.contains_key("medical"));
assert!(by_scope.get("finance").unwrap() > &0.0);
}
// ============================================================================
// FINGERPRINT AND CACHING TESTS
// ============================================================================
#[test]
fn test_cache_invalidation_on_state_change() {
// Cached energy should be invalidated when state changes
struct CachedEnergy {
value: Option<f32>,
fingerprint: u64,
}
impl CachedEnergy {
fn new() -> Self {
Self {
value: None,
fingerprint: 0,
}
}
fn get_or_compute(
&mut self,
current_fingerprint: u64,
compute_fn: impl FnOnce() -> f32,
) -> f32 {
if self.fingerprint == current_fingerprint {
if let Some(v) = self.value {
return v;
}
}
let value = compute_fn();
self.value = Some(value);
self.fingerprint = current_fingerprint;
value
}
fn invalidate(&mut self) {
self.value = None;
}
}
let mut cache = CachedEnergy::new();
let mut compute_count = 0;
// First computation
let v1 = cache.get_or_compute(1, || {
compute_count += 1;
10.0
});
assert_eq!(v1, 10.0);
assert_eq!(compute_count, 1);
// Cached retrieval (same fingerprint)
let v2 = cache.get_or_compute(1, || {
compute_count += 1;
10.0
});
assert_eq!(v2, 10.0);
assert_eq!(compute_count, 1); // Not recomputed
// Fingerprint changed - should recompute
let v3 = cache.get_or_compute(2, || {
compute_count += 1;
20.0
});
assert_eq!(v3, 20.0);
assert_eq!(compute_count, 2);
}
// ============================================================================
// PARALLEL COMPUTATION TESTS
// ============================================================================
#[test]
fn test_parallel_energy_computation() {
// Energy computation should be parallelizable across edges
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
let mut states = HashMap::new();
for i in 0..100 {
states.insert(i as u64, vec![i as f32 / 100.0, 0.5]);
}
let mut edges = Vec::new();
for i in 0..99 {
edges.push(TestEdge {
source: i,
target: i + 1,
weight: 1.0,
rho_source: RestrictionMap::new(2, 2),
rho_target: RestrictionMap::new(2, 2),
});
}
// Simulate parallel computation
let states = Arc::new(states);
let edges = Arc::new(edges);
let total = Arc::new(std::sync::Mutex::new(0.0f32));
let num_threads = 4;
let edges_per_thread = edges.len() / num_threads;
let handles: Vec<_> = (0..num_threads)
.map(|t| {
let states = Arc::clone(&states);
let edges = Arc::clone(&edges);
let total = Arc::clone(&total);
thread::spawn(move || {
let start = t * edges_per_thread;
let end = if t == num_threads - 1 {
edges.len()
} else {
(t + 1) * edges_per_thread
};
let mut local_sum = 0.0;
for i in start..end {
if let Some(energy) = edges[i].compute_energy(&states) {
local_sum += energy;
}
}
let mut total = total.lock().unwrap();
*total += local_sum;
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let parallel_total = *total.lock().unwrap();
// Verify against sequential
let (sequential_total, _) = compute_total_energy(&states, &edges);
assert!(
(parallel_total - sequential_total).abs() < 1e-6,
"Parallel and sequential computation should match"
);
}
// ============================================================================
// SPECTRAL DRIFT DETECTION TESTS
// ============================================================================
#[test]
fn test_spectral_drift_detection() {
// Spectral drift should be detected when eigenvalue distribution changes significantly
/// Simple eigenvalue snapshot
struct EigenvalueSnapshot {
eigenvalues: Vec<f32>,
}
/// Wasserstein-like distance between eigenvalue distributions
fn eigenvalue_distance(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() {
return f32::MAX;
}
let mut a_sorted = a.to_vec();
let mut b_sorted = b.to_vec();
a_sorted.sort_by(|x, y| x.partial_cmp(y).unwrap());
b_sorted.sort_by(|x, y| x.partial_cmp(y).unwrap());
a_sorted
.iter()
.zip(&b_sorted)
.map(|(x, y)| (x - y).abs())
.sum::<f32>()
/ a.len() as f32
}
let snapshot1 = EigenvalueSnapshot {
eigenvalues: vec![0.1, 0.3, 0.5, 0.8, 1.0],
};
// Small change - no drift
let snapshot2 = EigenvalueSnapshot {
eigenvalues: vec![0.11, 0.31, 0.49, 0.79, 1.01],
};
// Large change - drift detected
let snapshot3 = EigenvalueSnapshot {
eigenvalues: vec![0.5, 0.6, 0.7, 0.9, 2.0],
};
let dist_small = eigenvalue_distance(&snapshot1.eigenvalues, &snapshot2.eigenvalues);
let dist_large = eigenvalue_distance(&snapshot1.eigenvalues, &snapshot3.eigenvalues);
let drift_threshold = 0.1;
assert!(
dist_small < drift_threshold,
"Small change should not trigger drift"
);
assert!(
dist_large > drift_threshold,
"Large change should trigger drift"
);
}