diff --git a/README.md b/README.md index 14973d3..a7c3bbc 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,13 @@ Lower theta values are more accurate; higher values are faster. Large graphs still want multilevel coarsening, because even good approximation does not make every visual decision worth rendering individually. +Additional scale knobs split local and global behavior: + +- `barnes_hut_near_radius` forces Barnes-Hut to recurse near the target instead + of aggregating local neighborhoods. +- `near_repulsion_scale` controls exact pairwise/local repulsion. +- `far_repulsion_scale` controls Barnes-Hut aggregate far-field repulsion. + ## Accuracy Sweeps Use the repulsion accuracy sweep to compare approximation settings against exact @@ -131,6 +138,7 @@ let reports = repulsion_accuracy_sweep( RepulsionAccuracyCandidate::barnes_hut(0.4), RepulsionAccuracyCandidate::barnes_hut(0.7), RepulsionAccuracyCandidate::barnes_hut(1.0), + RepulsionAccuracyCandidate::barnes_hut_tuned(1.0, 240.0, 0.85), RepulsionAccuracyCandidate::spatial_grid(180.0, 1), ], ); diff --git a/RESEARCH.md b/RESEARCH.md index f8ffd3f..4899e31 100644 --- a/RESEARCH.md +++ b/RESEARCH.md @@ -87,6 +87,8 @@ Current state: absolute/relative error and elapsed time for tuning passes. - `TUNING_DOCTRINE.md` records preliminary supervised sweeps and the current policy for exact, Barnes-Hut, grid, and future multilevel force categories. +- Local/global repulsion knobs now expose Barnes-Hut near radius, local pair + scale, and aggregate far-field scale. Source: https://doi.org/10.1371/journal.pone.0098679 diff --git a/TUNING_DOCTRINE.md b/TUNING_DOCTRINE.md index 69c5ba7..8ba02f8 100644 --- a/TUNING_DOCTRINE.md +++ b/TUNING_DOCTRINE.md @@ -210,3 +210,87 @@ Supervised tuning recommendation: The doctrine for now is simple: exact is the truth, Barnes-Hut is the large scale field, grid is local blunt force, and fold groups are the path to making large graphs readable without making every node audition for camera time. + +## Second Sweep: More Knobs + +Date: 2026-05-07 + +New knobs: + +- `barnes_hut_near_radius`: prevents Barnes-Hut aggregation inside a local + radius, forcing recursion toward exact local behavior. +- `near_repulsion_scale`: scales exact/local pairwise repulsion. +- `far_repulsion_scale`: scales aggregate Barnes-Hut far-field repulsion. + +New candidates added: + +- Barnes-Hut theta `0.8`, `1.0`, `1.2` +- near radius `120` or `240` +- far scale `0.85` or `1.0` + +Representative second-pass results: + +| Dataset | Candidate | Time | Mean Rel Error | RMS Rel Error | Note | +|---|---:|---:|---:|---:|---| +| layered_dag_256 | exact | 178 us | 0.000 | 0.000 | Exact still wins small. | +| layered_dag_256 | Barnes-Hut theta 1.0, near 180, far 1.0 | 556 us | 0.560 | 0.676 | Best plain Barnes-Hut balance here. | +| layered_dag_256 | Barnes-Hut theta 1.0, near 240, far 1.0 | 644 us | 0.555 | 0.673 | Tiny accuracy gain, more cost. | +| layered_dag_256 | grid 180/r2 | 452 us | 0.419 | 0.492 | Still better local approximation than Barnes-Hut. | +| clustered_fold_512 | exact | 821 us | 0.000 | 0.000 | Exact still cheap. | +| clustered_fold_512 | Barnes-Hut theta 1.2, near 120, far 0.85 | 1910 us | 0.916 | 0.946 | Damping far field helps a little, still poor. | +| clustered_fold_512 | grid 240/r1 | 568 us | 0.087 | 0.110 | Best practical local folded-body candidate. | +| uniform_cloud_1024 | exact | 3433 us | 0.000 | 0.000 | Exact remains viable. | +| uniform_cloud_1024 | Barnes-Hut theta 1.2, near 240, far 1.0 | 2633 us | 0.476 | 0.658 | Speed win, high error. | +| uniform_cloud_1024 | Barnes-Hut theta 1.2, near 120, far 0.85 | 3270 us | 0.475 | 0.686 | Far damping did not clearly help. | +| uniform_cloud_4096 | exact | 70970 us | 0.000 | 0.000 | Too slow per frame. | +| uniform_cloud_4096 | Barnes-Hut theta 1.2, near 180, far 1.0 | 18528 us | 0.343 | 0.486 | Best broad large graph speed/accuracy point here. | +| uniform_cloud_4096 | Barnes-Hut theta 1.0, near 180, far 1.0 | 28078 us | 0.336 | 0.472 | More time, modest accuracy gain. | +| uniform_cloud_4096 | Barnes-Hut theta 1.0, near 120, far 0.85 | 20274 us | 0.366 | 0.517 | Far damping worsened force accuracy. | +| uniform_cloud_4096 | Barnes-Hut theta 1.0, near 240, far 1.0 | 36039 us | 0.350 | 0.483 | Larger near radius cost more without enough gain. | + +### Hypothesis 5: larger local exact radius improves Barnes-Hut enough to pay + +Result: mostly rejected. + +Increasing `barnes_hut_near_radius` from `180` to `240` often increased runtime +with only tiny accuracy improvement, and sometimes worsened mean relative error. +The current implementation already recurses for target-containing octants, so a +large near radius is not a free lunch. It is lunch with a service charge and +slightly cold fries. + +Doctrine: + +- Keep `barnes_hut_near_radius = 180.0` as the default. +- Sweep near radius on real dense graphs, but do not raise it globally. + +### Hypothesis 6: reducing far-field scale preserves local accuracy + +Result: rejected for force-ground-truth accuracy. + +`far_repulsion_scale = 0.85` sometimes reduced absolute force magnitude, but it +did not improve relative error against exact all-pairs ground truth. On broad +4096-node clouds it worsened mean/RMS relative error compared to far scale `1.0`. + +Doctrine: + +- Keep `far_repulsion_scale = 1.0` for physical force approximation. +- Use far-field scale as an artistic/readability control later, not as an + accuracy knob. + +### Hypothesis 7: exact/grid/Barnes-Hut should be selected per force category + +Result: strengthened. + +The second sweep made the split more obvious: + +- exact wins small graphs. +- grid wins dense local folded bodies. +- Barnes-Hut wins large broad far-field layouts. +- far-field damping and larger near radius do not replace category separation. + +Doctrine: + +- Add a hybrid repulsion architecture next: + local exact/grid inside fold groups and near neighborhoods; + Barnes-Hut between distant bodies; + coarse fold-group repulsion for global structure. diff --git a/examples/tuning_sweep.rs b/examples/tuning_sweep.rs index 3e082db..ca7dc06 100644 --- a/examples/tuning_sweep.rs +++ b/examples/tuning_sweep.rs @@ -16,6 +16,12 @@ fn main() { RepulsionAccuracyCandidate::barnes_hut(0.80), RepulsionAccuracyCandidate::barnes_hut(1.00), RepulsionAccuracyCandidate::barnes_hut(1.20), + RepulsionAccuracyCandidate::barnes_hut_tuned(0.80, 120.0, 0.85), + RepulsionAccuracyCandidate::barnes_hut_tuned(0.80, 240.0, 1.00), + RepulsionAccuracyCandidate::barnes_hut_tuned(1.00, 120.0, 0.85), + RepulsionAccuracyCandidate::barnes_hut_tuned(1.00, 240.0, 1.00), + RepulsionAccuracyCandidate::barnes_hut_tuned(1.20, 120.0, 0.85), + RepulsionAccuracyCandidate::barnes_hut_tuned(1.20, 240.0, 1.00), RepulsionAccuracyCandidate::spatial_grid(120.0, 1), RepulsionAccuracyCandidate::spatial_grid(180.0, 1), RepulsionAccuracyCandidate::spatial_grid(240.0, 1), @@ -23,7 +29,7 @@ fn main() { ]; println!( - "dataset,n,mode,theta,cell,radius,elapsed_us,mean_abs,max_abs,mean_rel,rms_rel,max_rel" + "dataset,n,mode,theta,near_radius,far_scale,cell,radius,elapsed_us,mean_abs,max_abs,mean_rel,rms_rel,max_rel" ); for (name, positions) in [ @@ -35,11 +41,13 @@ fn main() { let reports = repulsion_accuracy_sweep(&positions, &config, &candidates); for report in reports { println!( - "{},{},{},{:.2},{:.1},{},{},{:.6},{:.6},{:.6},{:.6},{:.6}", + "{},{},{},{:.2},{:.1},{:.2},{:.1},{},{},{:.6},{:.6},{:.6},{:.6},{:.6}", name, report.node_count, mode_name(report.candidate.repulsion_mode), report.candidate.barnes_hut_theta, + report.candidate.barnes_hut_near_radius, + report.candidate.far_repulsion_scale, report.candidate.grid_cell_size, report.candidate.grid_radius, report.elapsed.as_micros(), diff --git a/src/lib.rs b/src/lib.rs index fa34eaf..c1615a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -158,6 +158,12 @@ pub struct Layout3dConfig { pub barnes_hut_theta: f32, /// Maximum Barnes-Hut octree depth. pub barnes_hut_max_depth: usize, + /// Radius inside which Barnes-Hut must recurse instead of aggregating. + pub barnes_hut_near_radius: f32, + /// Scale for exact pairwise/local repulsion. + pub near_repulsion_scale: f32, + /// Scale for Barnes-Hut aggregate far-field repulsion. + pub far_repulsion_scale: f32, } impl Default for Layout3dConfig { @@ -182,6 +188,9 @@ impl Default for Layout3dConfig { grid_radius: 1, barnes_hut_theta: 0.65, barnes_hut_max_depth: 24, + barnes_hut_near_radius: 180.0, + near_repulsion_scale: 1.0, + far_repulsion_scale: 1.0, } } } @@ -204,6 +213,9 @@ pub struct RepulsionAccuracyCandidate { pub barnes_hut_theta: f32, pub grid_cell_size: f32, pub grid_radius: i32, + pub barnes_hut_near_radius: f32, + pub near_repulsion_scale: f32, + pub far_repulsion_scale: f32, } impl RepulsionAccuracyCandidate { @@ -213,6 +225,21 @@ impl RepulsionAccuracyCandidate { barnes_hut_theta: theta, grid_cell_size: Layout3dConfig::default().grid_cell_size, grid_radius: Layout3dConfig::default().grid_radius, + barnes_hut_near_radius: Layout3dConfig::default().barnes_hut_near_radius, + near_repulsion_scale: Layout3dConfig::default().near_repulsion_scale, + far_repulsion_scale: Layout3dConfig::default().far_repulsion_scale, + } + } + + pub fn barnes_hut_tuned(theta: f32, near_radius: f32, far_scale: f32) -> Self { + Self { + repulsion_mode: RepulsionMode::BarnesHut, + barnes_hut_theta: theta, + grid_cell_size: Layout3dConfig::default().grid_cell_size, + grid_radius: Layout3dConfig::default().grid_radius, + barnes_hut_near_radius: near_radius, + near_repulsion_scale: Layout3dConfig::default().near_repulsion_scale, + far_repulsion_scale: far_scale, } } @@ -222,6 +249,9 @@ impl RepulsionAccuracyCandidate { barnes_hut_theta: Layout3dConfig::default().barnes_hut_theta, grid_cell_size: cell_size, grid_radius: radius, + barnes_hut_near_radius: Layout3dConfig::default().barnes_hut_near_radius, + near_repulsion_scale: Layout3dConfig::default().near_repulsion_scale, + far_repulsion_scale: Layout3dConfig::default().far_repulsion_scale, } } @@ -231,6 +261,9 @@ impl RepulsionAccuracyCandidate { barnes_hut_theta: Layout3dConfig::default().barnes_hut_theta, grid_cell_size: Layout3dConfig::default().grid_cell_size, grid_radius: Layout3dConfig::default().grid_radius, + barnes_hut_near_radius: Layout3dConfig::default().barnes_hut_near_radius, + near_repulsion_scale: Layout3dConfig::default().near_repulsion_scale, + far_repulsion_scale: Layout3dConfig::default().far_repulsion_scale, } } } @@ -619,6 +652,9 @@ fn repulsion_accuracy_sweep_tuples( candidate_config.barnes_hut_theta = candidate.barnes_hut_theta; candidate_config.grid_cell_size = candidate.grid_cell_size; candidate_config.grid_radius = candidate.grid_radius; + candidate_config.barnes_hut_near_radius = candidate.barnes_hut_near_radius; + candidate_config.near_repulsion_scale = candidate.near_repulsion_scale; + candidate_config.far_repulsion_scale = candidate.far_repulsion_scale; let started = Instant::now(); let forces = compute_repulsion_forces(positions, &candidate_config); @@ -1992,7 +2028,10 @@ impl BarnesHutTree { let distance = (dx * dx + dy * dy + dz * dz).sqrt().max(0.01); let width = node.half_size * 2.0; - if !node.contains(target_position) && width / distance < theta { + let outside_local_radius = + config.barnes_hut_near_radius <= 0.0 || distance > config.barnes_hut_near_radius; + + if !node.contains(target_position) && outside_local_radius && width / distance < theta { apply_aggregate_repulsion_3d(target, node, positions, forces, config); return; } @@ -2128,7 +2167,8 @@ fn apply_aggregate_repulsion_3d( let dz = positions[target].2 - node.center_of_mass.2; let dist2 = (dx * dx + dy * dy + dz * dz).max(0.01); let dist = dist2.sqrt(); - let magnitude = config.base.repulsion_strength * node.mass as f32 / dist2; + let magnitude = + config.base.repulsion_strength * node.mass as f32 * config.far_repulsion_scale / dist2; forces[target].0 += dx / dist * magnitude * config.horizontal_repulsion; forces[target].1 += dy / dist * magnitude * config.vertical_repulsion; forces[target].2 += dz / dist * magnitude * config.horizontal_repulsion; @@ -2146,7 +2186,7 @@ fn apply_pair_repulsion_3d( let dz = positions[a].2 - positions[b].2; let dist2 = (dx * dx + dy * dy + dz * dz).max(0.01); let dist = dist2.sqrt(); - let magnitude = config.base.repulsion_strength / dist2; + let magnitude = config.base.repulsion_strength * config.near_repulsion_scale / dist2; let fx = dx / dist * magnitude * config.horizontal_repulsion; let fy = dy / dist * magnitude * config.vertical_repulsion; let fz = dz / dist * magnitude * config.horizontal_repulsion; @@ -2606,6 +2646,16 @@ mod tests { ); } + #[test] + fn tuned_barnes_hut_candidate_preserves_knobs() { + let candidate = RepulsionAccuracyCandidate::barnes_hut_tuned(1.0, 240.0, 0.85); + + assert_eq!(candidate.repulsion_mode, RepulsionMode::BarnesHut); + assert_eq!(candidate.barnes_hut_theta, 1.0); + assert_eq!(candidate.barnes_hut_near_radius, 240.0); + assert_eq!(candidate.far_repulsion_scale, 0.85); + } + #[test] fn solver_repulsion_accuracy_sweep_uses_live_positions() { let mut graph = Graph::new();