use std::collections::HashMap; use std::fs; use std::path::Path; use epiphany_graph_rs::{ Graph, Layout3dConfig, Layout3dSolver, RepulsionAccuracyCandidate, RepulsionMode, evaluate_layout_quality, solver_structural_accuracy_sweep, }; fn main() { let candidates = [ RepulsionAccuracyCandidate::exact(), RepulsionAccuracyCandidate::barnes_hut(1.0), RepulsionAccuracyCandidate::barnes_hut(1.2), RepulsionAccuracyCandidate::spatial_grid(240.0, 1), RepulsionAccuracyCandidate::structured(RepulsionMode::Exact, 0.20, 0.50, 256), RepulsionAccuracyCandidate::structured(RepulsionMode::SpatialGrid, 0.25, 0.75, 512), ]; println!( "dataset,nodes,edges,communities,cores,mode,theta,body_mode,body_scale,body_far,elapsed_us,mean_abs,max_abs,mean_rel,rms_rel,max_rel,fold_compactness,group_separation,rank_error,edge_mean,edge_var,bridge_mean,bridge_var" ); for fixture in fixtures() { let base_graph = fixture.graph.clone(); let mut solver = Layout3dSolver::new(base_graph.clone(), Layout3dConfig::default()); solver.tick(8); let reports = solver_structural_accuracy_sweep(&solver, &candidates); for report in reports { let quality = candidate_quality(&base_graph, &report.candidate); println!( "{},{},{},{},{},{},{:.2},{},{:.2},{:.2},{},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6}", fixture.name, solver.positions().len(), fixture.edge_count, solver.analysis().communities.len(), solver.analysis().core_shells.len(), mode_name(report.candidate.repulsion_mode), report.candidate.barnes_hut_theta, mode_name(report.candidate.body_repulsion_mode), report.candidate.body_repulsion_scale, report.candidate.body_far_repulsion_scale, report.elapsed.as_micros(), report.mean_absolute_error, report.max_absolute_error, report.mean_relative_error, report.rms_relative_error, report.max_relative_error, quality.mean_fold_compactness, quality.mean_group_separation, quality.mean_rank_error, quality.edge_length_mean, quality.edge_length_variance, quality.bridge_length_mean, quality.bridge_length_variance ); } } } struct Fixture { name: &'static str, graph: Graph, edge_count: usize, } fn candidate_quality( graph: &Graph, candidate: &RepulsionAccuracyCandidate, ) -> epiphany_graph_rs::LayoutQualityReport { let config = Layout3dConfig { repulsion_mode: candidate.repulsion_mode, barnes_hut_theta: candidate.barnes_hut_theta, grid_cell_size: candidate.grid_cell_size, grid_radius: candidate.grid_radius, barnes_hut_near_radius: candidate.barnes_hut_near_radius, near_repulsion_scale: candidate.near_repulsion_scale, far_repulsion_scale: candidate.far_repulsion_scale, body_repulsion_mode: candidate.body_repulsion_mode, body_repulsion_scale: candidate.body_repulsion_scale, body_exact_limit: candidate.body_exact_limit, body_far_repulsion_scale: candidate.body_far_repulsion_scale, ..Layout3dConfig::default() }; let mut solver = Layout3dSolver::new(graph.clone(), config); solver.tick(32); let layout = solver.snapshot(); evaluate_layout_quality(graph, &layout) } fn fixtures() -> Vec { let mut fixtures = vec![ Fixture { name: "synthetic_layered_512", graph: synthetic_layered(512), edge_count: 0, }, Fixture { name: "synthetic_clustered_1024", graph: synthetic_clustered(1024), edge_count: 0, }, ]; for (name, path, limit) in [ ( "snap_email_eu_core", "data/tuning/email-Eu-core.txt", 1500usize, ), ( "snap_p2p_gnutella08", "data/tuning/p2p-Gnutella08.txt", 3000usize, ), ] { if let Some((graph, edge_count)) = load_edge_list(path, limit) { fixtures.push(Fixture { name, graph, edge_count, }); } } fixtures } fn load_edge_list(path: &str, node_limit: usize) -> Option<(Graph, usize)> { let path = Path::new(path); let contents = fs::read_to_string(path).ok()?; let mut ids = HashMap::::new(); let mut edges = Vec::<(usize, usize)>::new(); for line in contents.lines() { let line = line.trim(); if line.is_empty() || line.starts_with('#') { continue; } let mut parts = line.split_whitespace(); let Some(source) = parts.next().and_then(|part| part.parse::().ok()) else { continue; }; let Some(target) = parts.next().and_then(|part| part.parse::().ok()) else { continue; }; if !ids.contains_key(&source) { if ids.len() >= node_limit { continue; } ids.insert(source, ids.len()); } if !ids.contains_key(&target) { if ids.len() >= node_limit { continue; } ids.insert(target, ids.len()); } edges.push((ids[&source], ids[&target])); } let mut graph = Graph::with_capacity(ids.len(), edges.len()); let nodes = (0..ids.len()) .map(|_| graph.add_node(1.0)) .collect::>(); for (source, target) in &edges { graph.add_edge(nodes[*source], nodes[*target]); } Some((graph, edges.len())) } fn synthetic_layered(count: usize) -> Graph { let mut graph = Graph::with_capacity(count, count * 3); let nodes = (0..count).map(|_| graph.add_node(1.0)).collect::>(); let width = 32usize; for idx in 0..count { let rank = idx / width; let next_rank = rank + 1; for offset in [0usize, 3, 11] { let target = next_rank * width + ((idx + offset) % width); if target < count { graph.add_edge(nodes[idx], nodes[target]); } } } graph } fn synthetic_clustered(count: usize) -> Graph { let mut graph = Graph::with_capacity(count, count * 4); let nodes = (0..count).map(|_| graph.add_node(1.0)).collect::>(); let clusters = 8usize; let cluster_size = count / clusters; for cluster in 0..clusters { let start = cluster * cluster_size; let end = if cluster == clusters - 1 { count } else { start + cluster_size }; for idx in start..end { for step in [1usize, 2, 5] { let target = start + ((idx - start + step) % (end - start)); graph.add_edge(nodes[idx], nodes[target]); } } if cluster + 1 < clusters { graph.add_edge(nodes[start], nodes[(cluster + 1) * cluster_size]); } } graph } fn mode_name(mode: RepulsionMode) -> &'static str { match mode { RepulsionMode::Exact => "exact", RepulsionMode::BarnesHut => "barnes_hut", RepulsionMode::SpatialGrid => "spatial_grid", } }