//! Benchmarks for full graph energy computation //! //! ADR-014 Performance Target: < 10ms for 10K nodes //! //! Global coherence energy: E(S) = sum(w_e * |r_e|^2) //! This is the aggregate measure of system incoherence. use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use std::collections::HashMap; // ============================================================================ // Graph Types (Simulated for benchmarking) // ============================================================================ /// Simplified restriction map for energy benchmarks #[derive(Clone)] pub struct RestrictionMap { pub matrix: Vec, pub bias: Vec, pub input_dim: usize, pub output_dim: usize, } impl RestrictionMap { pub fn identity(dim: usize) -> Self { let mut matrix = vec![0.0f32; dim * dim]; for i in 0..dim { matrix[i * dim + i] = 1.0; } Self { matrix, bias: vec![0.0; dim], input_dim: dim, output_dim: dim, } } #[inline] pub fn apply_into(&self, input: &[f32], output: &mut [f32]) { output.copy_from_slice(&self.bias); for i in 0..self.output_dim { let row_start = i * self.input_dim; for j in 0..self.input_dim { output[i] += self.matrix[row_start + j] * input[j]; } } } } /// Node in sheaf graph #[derive(Clone)] pub struct SheafNode { pub id: u64, pub state: Vec, } /// Edge with restriction maps #[derive(Clone)] pub struct SheafEdge { pub source: u64, pub target: u64, pub weight: f32, pub rho_source: RestrictionMap, pub rho_target: RestrictionMap, } impl SheafEdge { #[inline] pub fn weighted_residual_energy_into( &self, source: &[f32], target: &[f32], source_buf: &mut [f32], target_buf: &mut [f32], ) -> f32 { self.rho_source.apply_into(source, source_buf); self.rho_target.apply_into(target, target_buf); let mut norm_sq = 0.0f32; for i in 0..source_buf.len() { let diff = source_buf[i] - target_buf[i]; norm_sq += diff * diff; } self.weight * norm_sq } } /// Full sheaf graph for coherence computation pub struct SheafGraph { pub nodes: HashMap, pub edges: Vec, pub state_dim: usize, } /// Result of energy computation pub struct CoherenceEnergy { pub total_energy: f32, pub edge_energies: Vec, } impl SheafGraph { /// Generate a random graph for benchmarking pub fn random(num_nodes: usize, avg_degree: usize, state_dim: usize, seed: u64) -> Self { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; let mut hasher = || { let mut h = DefaultHasher::new(); seed.hash(&mut h); h }; // Generate nodes let nodes: HashMap = (0..num_nodes as u64) .map(|id| { let state: Vec = (0..state_dim) .map(|i| { let mut h = hasher(); (id, i).hash(&mut h); (h.finish() % 1000) as f32 / 1000.0 - 0.5 }) .collect(); (id, SheafNode { id, state }) }) .collect(); // Generate edges (random graph with target average degree) let num_edges = (num_nodes * avg_degree) / 2; let mut edges = Vec::with_capacity(num_edges); for i in 0..num_edges { let mut h = hasher(); (seed, i, "edge").hash(&mut h); let source = (h.finish() % num_nodes as u64) as u64; let mut h = hasher(); (seed, i, "target").hash(&mut h); let target = (h.finish() % num_nodes as u64) as u64; if source != target { edges.push(SheafEdge { source, target, weight: 1.0, rho_source: RestrictionMap::identity(state_dim), rho_target: RestrictionMap::identity(state_dim), }); } } Self { nodes, edges, state_dim, } } /// Generate a chain graph (linear topology) pub fn chain(num_nodes: usize, state_dim: usize, seed: u64) -> Self { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; let nodes: HashMap = (0..num_nodes as u64) .map(|id| { let state: Vec = (0..state_dim) .map(|i| { let mut h = DefaultHasher::new(); (seed, id, i).hash(&mut h); (h.finish() % 1000) as f32 / 1000.0 - 0.5 }) .collect(); (id, SheafNode { id, state }) }) .collect(); let edges: Vec = (0..num_nodes - 1) .map(|i| SheafEdge { source: i as u64, target: (i + 1) as u64, weight: 1.0, rho_source: RestrictionMap::identity(state_dim), rho_target: RestrictionMap::identity(state_dim), }) .collect(); Self { nodes, edges, state_dim, } } /// Generate a dense graph (high connectivity) pub fn dense(num_nodes: usize, state_dim: usize, seed: u64) -> Self { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; let nodes: HashMap = (0..num_nodes as u64) .map(|id| { let state: Vec = (0..state_dim) .map(|i| { let mut h = DefaultHasher::new(); (seed, id, i).hash(&mut h); (h.finish() % 1000) as f32 / 1000.0 - 0.5 }) .collect(); (id, SheafNode { id, state }) }) .collect(); // Dense: ~30% of possible edges let mut edges = Vec::new(); for i in 0..num_nodes as u64 { for j in (i + 1)..num_nodes as u64 { let mut h = DefaultHasher::new(); (seed, i, j).hash(&mut h); if h.finish() % 10 < 3 { // 30% probability edges.push(SheafEdge { source: i, target: j, weight: 1.0, rho_source: RestrictionMap::identity(state_dim), rho_target: RestrictionMap::identity(state_dim), }); } } } Self { nodes, edges, state_dim, } } /// Compute global coherence energy (sequential) pub fn compute_energy_sequential(&self) -> CoherenceEnergy { let mut source_buf = vec![0.0f32; self.state_dim]; let mut target_buf = vec![0.0f32; self.state_dim]; let edge_energies: Vec = self .edges .iter() .map(|edge| { let source_state = &self.nodes[&edge.source].state; let target_state = &self.nodes[&edge.target].state; edge.weighted_residual_energy_into( source_state, target_state, &mut source_buf, &mut target_buf, ) }) .collect(); let total_energy: f32 = edge_energies.iter().sum(); CoherenceEnergy { total_energy, edge_energies, } } /// Compute global coherence energy (parallel with rayon) #[cfg(feature = "parallel")] pub fn compute_energy_parallel(&self) -> CoherenceEnergy { use rayon::prelude::*; let edge_energies: Vec = self .edges .par_iter() .map(|edge| { let mut source_buf = vec![0.0f32; self.state_dim]; let mut target_buf = vec![0.0f32; self.state_dim]; let source_state = &self.nodes[&edge.source].state; let target_state = &self.nodes[&edge.target].state; edge.weighted_residual_energy_into( source_state, target_state, &mut source_buf, &mut target_buf, ) }) .collect(); let total_energy: f32 = edge_energies.par_iter().sum(); CoherenceEnergy { total_energy, edge_energies, } } /// Compute just total energy (no per-edge tracking) pub fn compute_total_energy(&self) -> f32 { let mut source_buf = vec![0.0f32; self.state_dim]; let mut target_buf = vec![0.0f32; self.state_dim]; let mut total = 0.0f32; for edge in &self.edges { let source_state = &self.nodes[&edge.source].state; let target_state = &self.nodes[&edge.target].state; total += edge.weighted_residual_energy_into( source_state, target_state, &mut source_buf, &mut target_buf, ); } total } } // ============================================================================ // Benchmarks // ============================================================================ /// Benchmark full graph energy at various sizes fn bench_full_graph_energy(c: &mut Criterion) { let mut group = c.benchmark_group("energy_full_graph"); // ADR-014 target: 10K nodes in <10ms // Test progression: 100, 1K, 10K, 100K for num_nodes in [100, 1_000, 10_000] { let avg_degree = 4; let state_dim = 64; let graph = SheafGraph::random(num_nodes, avg_degree, state_dim, 42); group.throughput(Throughput::Elements(graph.edges.len() as u64)); group.bench_with_input( BenchmarkId::new("sequential", format!("{}nodes", num_nodes)), &num_nodes, |b, _| b.iter(|| black_box(graph.compute_energy_sequential())), ); // Total energy only (no per-edge allocation) group.bench_with_input( BenchmarkId::new("total_only", format!("{}nodes", num_nodes)), &num_nodes, |b, _| b.iter(|| black_box(graph.compute_total_energy())), ); } group.finish(); } /// Benchmark with 100K nodes (reduced sample size due to runtime) fn bench_large_graph_energy(c: &mut Criterion) { let mut group = c.benchmark_group("energy_large_graph"); group.sample_size(10); let num_nodes = 100_000; let avg_degree = 4; let state_dim = 64; let graph = SheafGraph::random(num_nodes, avg_degree, state_dim, 42); group.throughput(Throughput::Elements(graph.edges.len() as u64)); group.bench_function("100K_nodes_total_energy", |b| { b.iter(|| black_box(graph.compute_total_energy())) }); group.finish(); } /// Benchmark energy computation for different graph topologies fn bench_topology_impact(c: &mut Criterion) { let mut group = c.benchmark_group("energy_topology"); let num_nodes = 1000; let state_dim = 64; // Chain topology (sparse, n-1 edges) let chain = SheafGraph::chain(num_nodes, state_dim, 42); group.throughput(Throughput::Elements(chain.edges.len() as u64)); group.bench_function("chain_1000", |b| { b.iter(|| black_box(chain.compute_total_energy())) }); // Random topology (avg degree 4) let random = SheafGraph::random(num_nodes, 4, state_dim, 42); group.throughput(Throughput::Elements(random.edges.len() as u64)); group.bench_function("random_1000_deg4", |b| { b.iter(|| black_box(random.compute_total_energy())) }); // Dense topology (~30% edges) let dense = SheafGraph::dense(100, state_dim, 42); // Smaller for dense group.throughput(Throughput::Elements(dense.edges.len() as u64)); group.bench_function("dense_100", |b| { b.iter(|| black_box(dense.compute_total_energy())) }); group.finish(); } /// Benchmark impact of state dimension on energy computation fn bench_state_dimension(c: &mut Criterion) { let mut group = c.benchmark_group("energy_state_dim"); let num_nodes = 1000; let avg_degree = 4; for state_dim in [8, 32, 64, 128, 256] { let graph = SheafGraph::random(num_nodes, avg_degree, state_dim, 42); group.throughput(Throughput::Elements(graph.edges.len() as u64)); group.bench_with_input(BenchmarkId::new("dim", state_dim), &state_dim, |b, _| { b.iter(|| black_box(graph.compute_total_energy())) }); } group.finish(); } /// Benchmark edge density scaling fn bench_edge_density(c: &mut Criterion) { let mut group = c.benchmark_group("energy_edge_density"); let num_nodes = 1000; let state_dim = 64; // Varying average degree for avg_degree in [2, 4, 8, 16, 32] { let graph = SheafGraph::random(num_nodes, avg_degree, state_dim, 42); group.throughput(Throughput::Elements(graph.edges.len() as u64)); group.bench_with_input( BenchmarkId::new("avg_degree", avg_degree), &avg_degree, |b, _| b.iter(|| black_box(graph.compute_total_energy())), ); } group.finish(); } /// Benchmark scope-based energy aggregation fn bench_scoped_energy(c: &mut Criterion) { let mut group = c.benchmark_group("energy_scoped"); let num_nodes = 10_000; let avg_degree = 4; let state_dim = 64; let graph = SheafGraph::random(num_nodes, avg_degree, state_dim, 42); // Simulate scope-based aggregation (e.g., by namespace) let num_scopes = 10; let scope_assignments: Vec = graph .edges .iter() .enumerate() .map(|(i, _)| i % num_scopes) .collect(); group.bench_function("aggregate_by_scope", |b| { b.iter(|| { let mut source_buf = vec![0.0f32; state_dim]; let mut target_buf = vec![0.0f32; state_dim]; let mut scope_energies = vec![0.0f32; num_scopes]; for (i, edge) in graph.edges.iter().enumerate() { let source_state = &graph.nodes[&edge.source].state; let target_state = &graph.nodes[&edge.target].state; let energy = edge.weighted_residual_energy_into( source_state, target_state, &mut source_buf, &mut target_buf, ); scope_energies[scope_assignments[i]] += energy; } black_box(scope_energies) }) }); group.finish(); } /// Benchmark energy fingerprint computation fn bench_energy_fingerprint(c: &mut Criterion) { let mut group = c.benchmark_group("energy_fingerprint"); let num_nodes = 1000; let avg_degree = 4; let state_dim = 64; let graph = SheafGraph::random(num_nodes, avg_degree, state_dim, 42); group.bench_function("compute_with_fingerprint", |b| { b.iter(|| { let energy = graph.compute_energy_sequential(); // Compute fingerprint from edge energies let mut fingerprint = 0u64; for e in &energy.edge_energies { fingerprint ^= e.to_bits() as u64; fingerprint = fingerprint.rotate_left(7); } black_box((energy.total_energy, fingerprint)) }) }); group.finish(); } /// Benchmark memory access patterns for energy computation fn bench_memory_patterns(c: &mut Criterion) { let mut group = c.benchmark_group("energy_memory"); let num_nodes = 10_000; let state_dim = 64; // Sequential node access (chain) let chain = SheafGraph::chain(num_nodes, state_dim, 42); group.bench_function("sequential_access", |b| { b.iter(|| black_box(chain.compute_total_energy())) }); // Random node access let random = SheafGraph::random(num_nodes, 4, state_dim, 42); group.bench_function("random_access", |b| { b.iter(|| black_box(random.compute_total_energy())) }); group.finish(); } criterion_group!( benches, bench_full_graph_energy, bench_large_graph_energy, bench_topology_impact, bench_state_dimension, bench_edge_density, bench_scoped_energy, bench_energy_fingerprint, bench_memory_patterns, ); criterion_main!(benches);