use alloc::{vec, vec::Vec};
#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use num_traits::Float;
use super::forces::{apply_lin_repulsion_anti_collision_one_sided, apply_region_repulsion};
#[derive(Debug, Clone)]
struct Region {
mass: f64,
center: [f64; 2],
size: f64,
leaf: Option<usize>,
children: Vec<usize>,
}
#[derive(Debug, Clone)]
pub(super) struct QuadTree {
regions: Vec<Region>,
build_ids: Vec<Vec<usize>>,
}
impl QuadTree {
pub(super) fn build(positions: &[[f64; 2]], masses: &[f64]) -> Self {
let mut tree = Self { regions: Vec::new(), build_ids: Vec::new() };
let ids: Vec<usize> = (0..positions.len()).collect();
let root = tree.push_region(ids, positions, masses);
let mut pending = vec![root];
while let Some(index) = pending.pop() {
let Some((buckets, total)) = tree.split_ids(index, positions) else { continue };
let mut children = Vec::new();
for bucket in buckets {
if bucket.is_empty() {
continue;
}
if bucket.len() < total {
let child = tree.push_region(bucket, positions, masses);
pending.push(child);
children.push(child);
} else {
for id in bucket {
children.push(tree.push_region(vec![id], positions, masses));
}
}
}
tree.regions[index].children = children;
}
tree.build_ids.clear();
tree.build_ids.shrink_to_fit();
tree
}
#[allow(clippy::too_many_arguments)]
pub(super) fn apply_repulsion(
&self,
node: usize,
pos: [f64; 2],
node_mass: f64,
coefficient: f64,
theta: f64,
sizes: Option<&[f64]>,
force: &mut [f64; 2],
) {
let mut stack = vec![0_usize];
while let Some(region_index) = stack.pop() {
let region = &self.regions[region_index];
if let Some(leaf) = region.leaf {
if leaf != node {
if let Some(sizes) = sizes {
apply_lin_repulsion_anti_collision_one_sided(
pos,
region.center,
node_mass,
region.mass,
sizes[node],
sizes[leaf],
coefficient,
force,
);
} else {
apply_region_repulsion(
pos,
region.center,
node_mass,
region.mass,
coefficient,
force,
);
}
}
continue;
}
let x_dist = pos[0] - region.center[0];
let y_dist = pos[1] - region.center[1];
let distance = (x_dist * x_dist + y_dist * y_dist).sqrt();
if distance * theta > region.size {
apply_region_repulsion(
pos,
region.center,
node_mass,
region.mass,
coefficient,
force,
);
} else {
stack.extend(region.children.iter().rev());
}
}
}
fn split_ids(
&mut self,
index: usize,
positions: &[[f64; 2]],
) -> Option<([Vec<usize>; 4], usize)> {
if self.regions[index].leaf.is_some() {
return None;
}
let center = self.regions[index].center;
let ids = core::mem::take(&mut self.build_ids[index]);
let total = ids.len();
let mut buckets: [Vec<usize>; 4] = [Vec::new(), Vec::new(), Vec::new(), Vec::new()];
for id in ids {
let mut bucket = 0;
if positions[id][0] >= center[0] {
bucket |= 1;
}
if positions[id][1] >= center[1] {
bucket |= 2;
}
buckets[bucket].push(id);
}
Some((buckets, total))
}
fn push_region(&mut self, ids: Vec<usize>, positions: &[[f64; 2]], masses: &[f64]) -> usize {
let region = if ids.len() == 1 {
let id = ids[0];
Region {
mass: masses[id],
center: positions[id],
size: 0.0,
leaf: Some(id),
children: Vec::new(),
}
} else {
let mut mass = 0.0;
let mut weighted = [0.0_f64; 2];
for &id in &ids {
mass += masses[id];
weighted[0] += positions[id][0] * masses[id];
weighted[1] += positions[id][1] * masses[id];
}
let center = [weighted[0] / mass, weighted[1] / mass];
let mut size = 0.0_f64;
for &id in &ids {
let x_dist = positions[id][0] - center[0];
let y_dist = positions[id][1] - center[1];
size = size.max(2.0 * (x_dist * x_dist + y_dist * y_dist).sqrt());
}
Region { mass, center, size, leaf: None, children: Vec::new() }
};
self.regions.push(region);
self.build_ids.push(ids);
self.regions.len() - 1
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use alloc::vec::Vec;
use super::QuadTree;
#[test]
fn root_mass_center_and_size() {
let positions = [[0.0, 0.0], [3.0, 0.0]];
let masses = [1.0, 3.0];
let tree = QuadTree::build(&positions, &masses);
let root = &tree.regions[0];
assert_eq!(root.mass, 4.0);
assert_eq!(root.center, [2.25, 0.0]);
assert_eq!(root.size, 4.5);
}
#[test]
fn single_node_no_self_force() {
let positions = [[1.0, 2.0]];
let masses = [5.0];
let tree = QuadTree::build(&positions, &masses);
assert_eq!(tree.regions.len(), 1);
let mut force = [0.0; 2];
tree.apply_repulsion(0, positions[0], masses[0], 2.0, 1.2, None, &mut force);
assert_eq!(force, [0.0, 0.0]);
}
#[test]
fn coincident_nodes_build_finite_tree() {
let positions = [[1.0, 1.0]; 5];
let masses = [1.0; 5];
let tree = QuadTree::build(&positions, &masses);
assert_eq!(tree.regions.len(), 6);
assert_eq!(tree.regions[0].children.len(), 5);
let mut force = [0.0; 2];
tree.apply_repulsion(0, positions[0], masses[0], 2.0, 1.2, None, &mut force);
assert_eq!(force, [0.0, 0.0]);
}
#[test]
fn huge_theta_collapses_to_super_node() {
let positions = [[1.0, 0.0], [-1.0, 0.0], [0.0, 1.0], [0.0, -1.0], [100.0, 0.0]];
let masses = [1.0, 1.0, 1.0, 1.0, 2.0];
let tree = QuadTree::build(&positions, &masses);
let mut force = [0.0; 2];
tree.apply_repulsion(4, positions[4], masses[4], 2.0, 1e12, None, &mut force);
let root = &tree.regions[0];
let x_dist = positions[4][0] - root.center[0];
let expected = 2.0 * masses[4] * root.mass / (x_dist * x_dist) * x_dist;
assert!((force[0] - expected).abs() < 1e-12);
assert_eq!(force[1], 0.0);
}
#[test]
fn tiny_theta_matches_exact_sum() {
let positions =
[[0.1, 0.2], [-0.3, 0.4], [0.5, -0.1], [-0.2, -0.4], [0.7, 0.6], [-0.6, 0.1]];
let masses = [1.0, 2.0, 3.0, 1.5, 2.5, 1.0];
let tree = QuadTree::build(&positions, &masses);
for node in 0..positions.len() {
let mut bh_force = [0.0; 2];
tree.apply_repulsion(
node,
positions[node],
masses[node],
2.0,
1e-12,
None,
&mut bh_force,
);
let mut exact = [0.0; 2];
for other in 0..positions.len() {
if other == node {
continue;
}
let x_dist = positions[node][0] - positions[other][0];
let y_dist = positions[node][1] - positions[other][1];
let squared = x_dist * x_dist + y_dist * y_dist;
let factor = 2.0 * masses[node] * masses[other] / squared;
exact[0] += x_dist * factor;
exact[1] += y_dist * factor;
}
assert!((bh_force[0] - exact[0]).abs() < 1e-12, "node {node}");
assert!((bh_force[1] - exact[1]).abs() < 1e-12, "node {node}");
}
}
#[test]
fn partially_coincident_nodes_match_exact_sum() {
let positions = [[1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [-2.0, 0.5], [0.0, -3.0]];
let masses = [1.0, 2.0, 3.0, 1.5, 2.5];
let tree = QuadTree::build(&positions, &masses);
for node in 0..positions.len() {
let mut bh_force = [0.0; 2];
tree.apply_repulsion(
node,
positions[node],
masses[node],
2.0,
1e-12,
None,
&mut bh_force,
);
let mut exact = [0.0; 2];
for other in 0..positions.len() {
let x_dist = positions[node][0] - positions[other][0];
let y_dist = positions[node][1] - positions[other][1];
let squared = x_dist * x_dist + y_dist * y_dist;
if other == node || squared <= 0.0 {
continue;
}
let factor = 2.0 * masses[node] * masses[other] / squared;
exact[0] += x_dist * factor;
exact[1] += y_dist * factor;
}
assert!((bh_force[0] - exact[0]).abs() < 1e-12, "node {node}");
assert!((bh_force[1] - exact[1]).abs() < 1e-12, "node {node}");
}
}
#[test]
fn leaves_carry_node_data() {
let positions = [[0.0, 0.0], [4.0, 0.0], [0.0, 4.0], [4.0, 4.0]];
let masses = [1.0, 2.0, 3.0, 4.0];
let tree = QuadTree::build(&positions, &masses);
let leaves: Vec<_> = tree.regions.iter().filter(|region| region.leaf.is_some()).collect();
assert_eq!(leaves.len(), 4);
for leaf in leaves {
let node = leaf.leaf.unwrap();
assert_eq!(leaf.mass, masses[node]);
assert_eq!(leaf.center, positions[node]);
assert_eq!(leaf.size, 0.0);
}
}
}