use core::{array, ops::AddAssign};
use alloc::vec::Vec;
use num_traits::float::FloatCore;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use crate::{
ForceKernel,
morton::{Dim, Morton, MortonWord, quantize},
};
#[cfg(feature = "parallel")]
const ARENA_MIN_CHUNK: usize = 4096;
const SENTINEL: u32 = u32::MAX;
const SLACK_FRACTION: f64 = 0.03;
#[derive(Debug)]
struct Node<T, const D: usize> {
center_of_mass: [T; D],
mass: T,
count: u32,
first_child: u32,
child_count: u8,
level: u8,
}
#[derive(Debug)]
pub struct BarnesHutTree<T, W, const D: usize>
where
W: MortonWord,
{
nodes: Vec<Node<T, D>>,
ranges: Vec<(u32, u32)>,
sorted: Vec<(W, u32)>,
level_half_width_sq: Vec<T>,
}
fn bounding_box<T, const D: usize>(y_chunks: &[[T; D]]) -> ([T; D], [T; D])
where
T: FloatCore + Send + Sync,
{
let identity = || ([T::max_value(); D], [-T::max_value(); D]);
let widen = |(mut min, mut max): ([T; D], [T; D]), point: &[T; D]| {
for axis in 0..D {
min[axis] = min[axis].min(point[axis]);
max[axis] = max[axis].max(point[axis]);
}
(min, max)
};
#[cfg(feature = "parallel")]
{
y_chunks
.par_iter()
.with_min_len(ARENA_MIN_CHUNK)
.fold(identity, widen)
.reduce(identity, |(mut min_a, mut max_a), (min_b, max_b)| {
for axis in 0..D {
min_a[axis] = min_a[axis].min(min_b[axis]);
max_a[axis] = max_a[axis].max(max_b[axis]);
}
(min_a, max_a)
})
}
#[cfg(not(feature = "parallel"))]
{
y_chunks.iter().fold(identity(), widen)
}
}
impl<T, W, const D: usize> BarnesHutTree<T, W, D>
where
T: FloatCore + Send + Sync + AddAssign,
W: MortonWord,
{
pub fn empty() -> Self {
Self {
nodes: Vec::new(),
ranges: Vec::new(),
sorted: Vec::new(),
level_half_width_sq: Vec::new(),
}
}
pub fn root_count(&self) -> usize {
self.nodes.first().map_or(0, |node| node.count as usize)
}
pub fn new(points: &[T], masses: &[T]) -> Self
where
Dim<D>: Morton<D, Word = W>,
{
let mut tree = Self::empty();
tree.rebuild(points, masses);
tree
}
pub fn new_uniform(points: &[T]) -> Self
where
Dim<D>: Morton<D, Word = W>,
{
let mut tree = Self::empty();
tree.rebuild_uniform(points);
tree
}
pub fn rebuild(&mut self, points: &[T], masses: &[T])
where
Dim<D>: Morton<D, Word = W>,
{
assert_eq!(
points.len(),
masses.len() * D,
"points must hold masses.len() points of D components each"
);
self.rebuild_impl(points, masses.len(), |index| masses[index]);
}
pub fn rebuild_uniform(&mut self, points: &[T])
where
Dim<D>: Morton<D, Word = W>,
{
assert_eq!(
points.len() % D,
0,
"points must hold a whole number of D-component points"
);
self.rebuild_impl(points, points.len() / D, |_| T::one());
}
fn rebuild_impl<F>(&mut self, points: &[T], n_samples: usize, mass_of: F)
where
Dim<D>: Morton<D, Word = W>,
F: Fn(usize) -> T + Sync,
{
let bits = <Dim<D> as Morton<D>>::BITS;
self.nodes.clear();
self.ranges.clear();
self.sorted.clear();
if n_samples == 0 {
self.level_half_width_sq.clear();
return;
}
let (y_chunks, _) = points.as_chunks::<D>();
let (min, max) = bounding_box::<T, D>(y_chunks);
let extent: [T; D] = array::from_fn(|axis| max[axis] - min[axis]);
let scale = T::from(1u64 << bits).unwrap();
let max_bucket = ((1u64 << bits) - 1) as u32;
let inv_scale: [T; D] = array::from_fn(|axis| {
if extent[axis] > T::zero() {
scale / extent[axis]
} else {
T::zero()
}
});
let max_extent = extent.iter().copied().fold(T::zero(), T::max);
self.level_half_width_sq.clear();
for level in 0..=bits {
let half_width = max_extent / T::from(1u64 << (level + 1)).unwrap();
self.level_half_width_sq.push(half_width * half_width);
}
let encode_at = |i: usize| {
let point = &y_chunks[i];
let code = <Dim<D> as Morton<D>>::encode(quantize::<T, D>(
point, &min, &inv_scale, max_bucket,
));
(code, i as u32)
};
#[cfg(feature = "parallel")]
{
self.sorted.par_extend(
(0..n_samples)
.into_par_iter()
.with_min_len(ARENA_MIN_CHUNK)
.map(&encode_at),
);
self.sorted.par_sort_unstable_by_key(|&(code, _)| code);
}
#[cfg(not(feature = "parallel"))]
{
self.sorted.extend((0..n_samples).map(&encode_at));
self.sorted.sort_unstable_by_key(|&(code, _)| code);
}
let sorted = &self.sorted;
let nodes = &mut self.nodes;
let ranges = &mut self.ranges;
nodes.reserve(2 * n_samples - 1);
ranges.reserve(2 * n_samples - 1);
nodes.push(Node {
center_of_mass: [T::zero(); D],
mass: T::zero(),
count: n_samples as u32,
first_child: SENTINEL,
child_count: 0,
level: bits as u8,
});
ranges.push((0, n_samples as u32));
let mask = W::d_bit_mask(D as u32);
let mut node = 0usize;
while node < nodes.len() {
let (start, end) = ranges[node];
if end - start <= 1 || sorted[start as usize].0 == sorted[(end - 1) as usize].0 {
node += 1;
continue;
}
let xor = sorted[start as usize].0 ^ sorted[(end - 1) as usize].0;
let highest_diff = xor.msb_position();
let level = (bits - 1) - highest_diff / D as u32;
let shift = D as u32 * (bits - 1 - level);
nodes[node].level = level as u8;
let first_child = nodes.len() as u32;
let mut child_count: u8 = 0;
let mut child_start = start;
while child_start < end {
let group = (sorted[child_start as usize].0 >> shift) & mask;
let mut child_end = child_start + 1;
while child_end < end && (sorted[child_end as usize].0 >> shift) & mask == group {
child_end += 1;
}
nodes.push(Node {
center_of_mass: [T::zero(); D],
mass: T::zero(),
count: (child_end - child_start),
first_child: SENTINEL,
child_count: 0,
level: bits as u8,
});
ranges.push((child_start, child_end));
child_count += 1;
child_start = child_end;
}
debug_assert!(child_count as usize <= <Dim<D> as Morton<D>>::CHILDREN);
nodes[node].first_child = first_child;
nodes[node].child_count = child_count;
node += 1;
}
let mass_ref = &mass_of;
let aggregate_leaf = |(node, &(start, end)): (&mut Node<T, D>, &(u32, u32))| {
let mut center = [T::zero(); D];
let mut total = T::zero();
for slot in start..end {
let index = sorted[slot as usize].1 as usize;
let weight = mass_ref(index);
total += weight;
let point = &y_chunks[index];
center
.iter_mut()
.zip(point.iter())
.for_each(|(ci, pi)| *ci += weight * *pi);
}
let inverse = total.recip();
center
.iter_mut()
.for_each(|value| *value = *value * inverse);
node.mass = total;
node.center_of_mass = center;
};
#[cfg(feature = "parallel")]
nodes
.par_iter_mut()
.zip(ranges.par_iter())
.with_min_len(ARENA_MIN_CHUNK)
.filter(|(node, _)| node.first_child == SENTINEL)
.for_each(aggregate_leaf);
#[cfg(not(feature = "parallel"))]
nodes
.iter_mut()
.zip(ranges.iter())
.filter(|(node, _)| node.first_child == SENTINEL)
.for_each(aggregate_leaf);
for node in (0..nodes.len()).rev() {
if nodes[node].first_child == SENTINEL {
continue;
}
let first_child = nodes[node].first_child as usize;
let child_count = nodes[node].child_count as usize;
let mut center = [T::zero(); D];
let mut total = T::zero();
for child in &nodes[first_child..first_child + child_count] {
total += child.mass;
center
.iter_mut()
.zip(child.center_of_mass.iter())
.for_each(|(value, &component)| *value += component * child.mass);
}
let inverse = total.recip();
center
.iter_mut()
.for_each(|value| *value = *value * inverse);
nodes[node].mass = total;
nodes[node].center_of_mass = center;
}
debug_assert_eq!(
nodes
.iter()
.filter(|node| node.first_child == SENTINEL)
.map(|node| node.count as u64)
.sum::<u64>() as usize,
n_samples,
"arena lost or invented points"
);
debug_assert!(check_coms_within_cells::<T, W, D>(
nodes, ranges, sorted, &min, &extent, bits
));
}
pub fn compute_non_edge_forces(
&self,
index: usize,
theta_sq: T,
y: &[T],
negative_forces_row: &mut [T; D],
q_sum: &mut T,
stack: &mut [u32],
) {
if self.nodes.is_empty() {
return;
}
let (y_chunks, _) = y.as_chunks::<D>();
let query = &y_chunks[index];
let mut top = 0usize;
stack[top] = 0;
top += 1;
while top > 0 {
top -= 1;
let node = &self.nodes[stack[top] as usize];
let mut displacement = [T::zero(); D];
let mut distance = T::zero();
for axis in 0..D {
let delta = query[axis] - node.center_of_mass[axis];
displacement[axis] = delta;
distance += delta * delta;
}
if node.first_child == SENTINEL {
if distance == T::zero() {
continue;
}
} else if self.level_half_width_sq[node.level as usize] >= theta_sq * distance {
for child in 0..node.child_count as u32 {
stack[top] = node.first_child + child;
top += 1;
}
continue;
}
let inverse = (T::one() + distance).recip();
let mut magnitude = node.mass * inverse;
*q_sum += magnitude;
magnitude = magnitude * inverse;
for axis in 0..D {
negative_forces_row[axis] += magnitude * displacement[axis];
}
}
}
pub fn accumulate_forces<K>(
&self,
query: &[T],
theta: T,
kernel: &K,
force: &mut [T; D],
stack: &mut [u32],
) where
K: ForceKernel<T, D>,
{
if self.nodes.is_empty() {
return;
}
assert!(query.len() >= D, "query must hold at least D components");
let theta_sq = theta * theta;
let mut top = 0usize;
stack[top] = 0;
top += 1;
while top > 0 {
top -= 1;
let node = &self.nodes[stack[top] as usize];
let mut displacement = [T::zero(); D];
let mut distance_sq = T::zero();
for axis in 0..D {
let delta = query[axis] - node.center_of_mass[axis];
displacement[axis] = delta;
distance_sq += delta * delta;
}
let is_leaf = node.first_child == SENTINEL;
if is_leaf {
if distance_sq <= T::zero() {
continue;
}
} else if self.level_half_width_sq[node.level as usize] >= theta_sq * distance_sq {
for child in 0..node.child_count as u32 {
stack[top] = node.first_child + child;
top += 1;
}
continue;
}
kernel.accumulate(&displacement, distance_sq, node.mass, is_leaf, force);
}
}
pub fn accumulate_all<K>(&self, queries: &[T], theta: T, kernel: &K, out: &mut [T])
where
K: ForceKernel<T, D> + Sync,
Dim<D>: Morton<D, Word = W>,
{
assert_eq!(
queries.len() % D,
0,
"queries must hold a whole number of D-component points"
);
assert_eq!(queries.len(), out.len(), "out must match queries in length");
let compute_row = |stack: &mut [u32], (row, query): (&mut [T], &[T])| {
let mut force = [T::zero(); D];
self.accumulate_forces(query, theta, kernel, &mut force, stack);
row.copy_from_slice(&force);
};
#[cfg(feature = "parallel")]
out.par_chunks_mut(D)
.zip(queries.par_chunks(D))
.with_min_len(ARENA_MIN_CHUNK)
.for_each_init(<Dim<D> as Morton<D>>::empty_stack, |stack, item| {
compute_row(stack.as_mut(), item)
});
#[cfg(not(feature = "parallel"))]
{
let mut stack = <Dim<D> as Morton<D>>::empty_stack();
out.chunks_mut(D)
.zip(queries.chunks(D))
.for_each(|item| compute_row(stack.as_mut(), item));
}
}
}
fn check_coms_within_cells<T, W, const D: usize>(
nodes: &[Node<T, D>],
ranges: &[(u32, u32)],
sorted: &[(W, u32)],
min: &[T; D],
extent: &[T; D],
bits: u32,
) -> bool
where
T: FloatCore,
W: MortonWord,
Dim<D>: Morton<D, Word = W>,
{
let slack_fraction = T::from(SLACK_FRACTION).unwrap();
nodes.iter().zip(ranges.iter()).all(|(node, &(start, _))| {
let level = node.level as u32;
let coords = <Dim<D> as Morton<D>>::decode(sorted[start as usize].0);
let cells = T::from(1u64 << level).unwrap();
(0..D).all(|axis| {
let width = extent[axis] / cells;
let cell_index = T::from((coords[axis] as u64) >> (bits - level)).unwrap();
let low = min[axis] + cell_index * width;
let high = low + width;
let magnitude = low.abs().max(high.abs());
let slack = slack_fraction * (extent[axis] + magnitude) + T::min_positive_value();
node.center_of_mass[axis] >= low - slack && node.center_of_mass[axis] <= high + slack
})
})
}
#[cfg(test)]
impl<T, W, const D: usize> BarnesHutTree<T, W, D>
where
W: MortonWord,
{
pub(crate) fn root_center_of_mass(&self) -> &[T] {
self.nodes.first().map_or(&[], |node| &node.center_of_mass)
}
pub(crate) fn node_count(&self) -> usize {
self.nodes.len()
}
pub(crate) fn root_first_child(&self) -> u32 {
self.nodes.first().map_or(SENTINEL, |node| node.first_child)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn lcg_cloud(n: usize, dim: usize, mut state: u64) -> Vec<f32> {
let mut data = Vec::with_capacity(n * dim);
for _ in 0..n * dim {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
data.push((state >> 40) as f32 / (1u64 << 24) as f32);
}
data
}
fn mean<const D: usize>(y: &[f32], n: usize) -> [f32; D] {
let mut sum = [0.0f32; D];
let (points, _) = y.as_chunks::<D>();
for point in points {
for axis in 0..D {
sum[axis] += point[axis];
}
}
sum.iter_mut().for_each(|value| *value /= n as f32);
sum
}
#[test]
fn build_conserves_points_and_keeps_coms_in_cells_2d() {
const N: usize = 2_000;
let mut data = lcg_cloud(N, 2, 17);
for value in data.iter_mut() {
*value += 100.0;
}
let arena = BarnesHutTree::<f32, u64, 2>::new_uniform(&data);
assert_eq!(arena.root_count(), N);
}
#[test]
fn build_conserves_points_3d() {
const N: usize = 1_500;
let data = lcg_cloud(N, 3, 23);
let arena = BarnesHutTree::<f32, u64, 3>::new_uniform(&data);
assert_eq!(arena.root_count(), N);
}
#[test]
fn root_center_of_mass_equals_the_mean() {
const N: usize = 1_000;
let data = lcg_cloud(N, 2, 5);
let arena = BarnesHutTree::<f32, u64, 2>::new_uniform(&data);
let expected = mean::<2>(&data, N);
let root = arena.root_center_of_mass();
assert!((root[0] - expected[0]).abs() < 1e-3);
assert!((root[1] - expected[1]).abs() < 1e-3);
}
#[test]
fn duplicate_points_collapse_to_one_leaf() {
const N: usize = 500;
let data = vec![3.5f32; N * 2];
let arena = BarnesHutTree::<f32, u64, 2>::new_uniform(&data);
assert_eq!(arena.root_count(), N);
assert_eq!(arena.node_count(), 1);
assert_eq!(arena.root_first_child(), SENTINEL);
}
#[test]
fn single_point_builds_a_leaf_root() {
let data = [2.0f32, -1.0];
let arena = BarnesHutTree::<f32, u64, 2>::new_uniform(&data);
assert_eq!(arena.root_count(), 1);
assert_eq!(arena.node_count(), 1);
let com = arena.root_center_of_mass();
assert_eq!(com, &[2.0, -1.0]);
}
#[test]
fn empty_input_builds_empty_arena() {
let arena = BarnesHutTree::<f32, u64, 2>::new_uniform(&[]);
assert_eq!(arena.root_count(), 0);
let mut forces = [0.0f32; 2];
let mut q_sum = 0.0f32;
let mut stack = <Dim<2> as Morton<2>>::empty_stack();
arena.compute_non_edge_forces(0, 0.25, &[], &mut forces, &mut q_sum, &mut stack);
assert_eq!(forces, [0.0, 0.0]);
assert_eq!(q_sum, 0.0);
}
#[test]
fn build_conserves_points_4d() {
const N: usize = 1_000;
let data = lcg_cloud(N, 4, 31);
let arena = BarnesHutTree::<f32, u64, 4>::new_uniform(&data);
assert_eq!(arena.root_count(), N);
}
#[test]
fn repulsive_forces_point_away_from_each_other() {
let data = [-5.0f32, 0.0, 5.0, 0.0];
let arena = BarnesHutTree::<f32, u64, 2>::new_uniform(&data);
assert_eq!(arena.root_count(), 2);
let theta_sq = 0.25;
let mut stack = <Dim<2> as Morton<2>>::empty_stack();
let mut forces0 = [0.0f32; 2];
let mut q_sum0 = 0.0f32;
arena.compute_non_edge_forces(0, theta_sq, &data, &mut forces0, &mut q_sum0, &mut stack);
assert!(
forces0[0] < 0.0,
"force on point 0 should point away from point 1"
);
assert!(q_sum0 > 0.0, "q_sum must be positive");
let mut forces1 = [0.0f32; 2];
let mut q_sum1 = 0.0f32;
arena.compute_non_edge_forces(1, theta_sq, &data, &mut forces1, &mut q_sum1, &mut stack);
assert!(
forces1[0] > 0.0,
"force on point 1 should point away from point 0"
);
assert!(q_sum1 > 0.0, "q_sum must be positive");
}
#[test]
fn repulsive_forces_with_multiple_points() {
let data = [-1.0f32, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0];
let arena = BarnesHutTree::<f32, u64, 2>::new_uniform(&data);
assert_eq!(arena.root_count(), 4);
let theta_sq = 0.25;
let mut stack = <Dim<2> as Morton<2>>::empty_stack();
let mut forces = [0.0f32; 2];
let mut q_sum = 0.0f32;
arena.compute_non_edge_forces(0, theta_sq, &data, &mut forces, &mut q_sum, &mut stack);
assert!(forces[0] < 0.0, "x-force should be negative");
assert!(forces[1] < 0.0, "y-force should be negative");
assert!(q_sum > 0.0, "q_sum must be positive");
}
#[test]
fn build_conserves_points_5d() {
const N: usize = 500;
let data = lcg_cloud(N, 5, 37);
let arena = BarnesHutTree::<f32, u128, 5>::new_uniform(&data);
assert_eq!(arena.root_count(), N);
}
#[test]
fn build_conserves_points_6d() {
const N: usize = 400;
let data = lcg_cloud(N, 6, 39);
let arena = BarnesHutTree::<f32, u128, 6>::new_uniform(&data);
assert_eq!(arena.root_count(), N);
}
#[test]
fn build_conserves_points_7d() {
const N: usize = 300;
let data = lcg_cloud(N, 7, 41);
let arena = BarnesHutTree::<f32, u128, 7>::new_uniform(&data);
assert_eq!(arena.root_count(), N);
}
#[test]
fn rebuild_reuses_buffers() {
let mut arena = BarnesHutTree::<f32, u64, 2>::empty();
let data1 = lcg_cloud(500, 2, 10);
arena.rebuild_uniform(&data1);
assert_eq!(arena.root_count(), 500);
let com1: Vec<f32> = arena.root_center_of_mass().to_vec();
let data2 = lcg_cloud(500, 2, 99);
arena.rebuild_uniform(&data2);
assert_eq!(arena.root_count(), 500);
let com2: Vec<f32> = arena.root_center_of_mass().to_vec();
assert!(com1[0] != com2[0] || com1[1] != com2[1]);
}
#[test]
fn repulsive_forces_in_3d() {
let data = [0.0f32, 0.0, 0.0, 10.0, 0.0, 0.0, 5.0, 8.66, 0.0];
let arena = BarnesHutTree::<f32, u64, 3>::new_uniform(&data);
assert_eq!(arena.root_count(), 3);
let theta_sq = 0.25;
let mut stack = <Dim<3> as Morton<3>>::empty_stack();
let mut forces = [0.0f32; 3];
let mut q_sum = 0.0f32;
arena.compute_non_edge_forces(0, theta_sq, &data, &mut forces, &mut q_sum, &mut stack);
assert!(q_sum > 0.0, "q_sum must be positive");
let mag = (forces[0] * forces[0] + forces[1] * forces[1] + forces[2] * forces[2]).sqrt();
assert!(mag > 1e-6, "force magnitude should be nonzero");
}
fn repulse<const D: usize>(
displacement: &[f32; D],
distance_squared: f32,
mass: f32,
_is_leaf: bool,
force: &mut [f32; D],
) {
let scale = mass / (1.0 + distance_squared);
for axis in 0..D {
force[axis] += scale * displacement[axis];
}
}
fn direct_forces<const D: usize>(y: &[f32], n: usize) -> Vec<f32> {
let mut out = vec![0.0f32; n * D];
for i in 0..n {
let mut force = [0.0f32; D];
for j in 0..n {
if i == j {
continue;
}
let mut displacement = [0.0f32; D];
let mut distance_squared = 0.0f32;
for axis in 0..D {
let delta = y[i * D + axis] - y[j * D + axis];
displacement[axis] = delta;
distance_squared += delta * delta;
}
if distance_squared <= 0.0 {
continue;
}
repulse::<D>(&displacement, distance_squared, 1.0, true, &mut force);
}
out[i * D..i * D + D].copy_from_slice(&force);
}
out
}
#[test]
fn accumulate_all_matches_direct_sum_at_theta_zero() {
const N: usize = 400;
const D: usize = 3;
let data = lcg_cloud(N, D, 71);
let tree = BarnesHutTree::<f32, u64, D>::new_uniform(&data);
let mut got = vec![0.0f32; N * D];
tree.accumulate_all(&data, 0.0, &repulse::<D>, &mut got);
let expected = direct_forces::<D>(&data, N);
for (component, (&g, &e)) in got.iter().zip(expected.iter()).enumerate() {
let tolerance = 1e-3 * e.abs().max(1.0);
assert!(
(g - e).abs() <= tolerance,
"theta=0 force diverged at component {component}: got {g}, expected {e}"
);
}
}
#[test]
fn accumulate_all_agrees_with_per_row_forces() {
const N: usize = 300;
const D: usize = 2;
let data = lcg_cloud(N, D, 13);
let tree = BarnesHutTree::<f32, u64, D>::new_uniform(&data);
for &theta in &[0.0f32, 0.5] {
let mut driver = vec![0.0f32; N * D];
tree.accumulate_all(&data, theta, &repulse::<D>, &mut driver);
let mut per_row = vec![0.0f32; N * D];
let mut stack = <Dim<D> as Morton<D>>::empty_stack();
for i in 0..N {
let mut force = [0.0f32; D];
tree.accumulate_forces(
&data[i * D..],
theta,
&repulse::<D>,
&mut force,
stack.as_mut(),
);
per_row[i * D..i * D + D].copy_from_slice(&force);
}
assert_eq!(
driver, per_row,
"driver diverged from per-row traversal at theta {theta}"
);
}
}
}