use crate::epoch::eigensolver::SpectralCoords;
pub const TAU_SCALES: [f32; 4] = [1.0, 10.0, 100.0, 1000.0];
#[repr(C)]
#[derive(Default, Clone, Copy)]
pub struct BvhNode {
pub aabb_min: [f32; 3], pub focus_sum: f32, pub aabb_max: [f32; 3], pub child_start: u32, pub child_count: u32, pub is_leaf: u32, pub leaf_start: u32, pub leaf_count: u32, }
#[derive(Clone)]
pub struct Bvh {
pub nodes: Vec<BvhNode>,
pub cluster_ids: Vec<[u32; 4]>,
}
fn grid_cluster(coords: &SpectralCoords, tau: f32) -> Vec<u32> {
let n = coords.n;
if n == 0 {
return Vec::new();
}
let cell_size = 100.0_f32 * tau.sqrt();
let mut cell_to_id: std::collections::HashMap<(i32, i32, i32), u32> =
std::collections::HashMap::new();
let mut ids = vec![0u32; n];
let mut next_id = 0u32;
for p in 0..n {
let pos = coords.position(p);
let ix = (pos[0] / cell_size).floor() as i32;
let iy = (pos[1] / cell_size).floor() as i32;
let iz = (pos[2] / cell_size).floor() as i32;
let entry = cell_to_id.entry((ix, iy, iz)).or_insert_with(|| {
let id = next_id;
next_id += 1;
id
});
ids[p] = *entry;
}
ids
}
fn aabb_expand(mn: &mut [f32; 3], mx: &mut [f32; 3], pos: [f32; 3]) {
for d in 0..3 {
mn[d] = mn[d].min(pos[d]);
mx[d] = mx[d].max(pos[d]);
}
}
fn aabb_empty() -> ([f32; 3], [f32; 3]) {
([f32::INFINITY; 3], [f32::NEG_INFINITY; 3])
}
fn canonicalize_ids(ids: &mut Vec<u32>, focus: &[f32]) -> u32 {
let max_id = ids.iter().copied().max().unwrap_or(0);
let n_clusters = (max_id as usize) + 1;
let mut cluster_focus = vec![0.0f32; n_clusters];
let mut cluster_min_p = vec![usize::MAX; n_clusters];
for (p, &cid) in ids.iter().enumerate() {
let c = cid as usize;
if c < n_clusters {
cluster_focus[c] += focus.get(p).copied().unwrap_or(0.0);
cluster_min_p[c] = cluster_min_p[c].min(p);
}
}
let mut order: Vec<usize> = (0..n_clusters).collect();
order.sort_by(|&a, &b| {
cluster_focus[b].partial_cmp(&cluster_focus[a])
.unwrap_or(std::cmp::Ordering::Equal)
.then(cluster_min_p[a].cmp(&cluster_min_p[b]))
});
let mut remap = vec![0u32; n_clusters];
for (new_id, &old_id) in order.iter().enumerate() {
remap[old_id] = new_id as u32;
}
for cid in ids.iter_mut() {
*cid = remap[*cid as usize];
}
max_id
}
pub fn build(coords: &SpectralCoords, focus: &[f32]) -> Bvh {
let n = coords.n;
let mut cluster_ids = vec![[0u32; 4]; n];
let mut all_level_ids: [Vec<u32>; 4] = std::array::from_fn(๏ฟฟ_๏ฟฟ Vec::new());
for (level, &tau) in TAU_SCALES.iter().enumerate() {
let mut ids = grid_cluster(coords, tau);
canonicalize_ids(&mut ids, focus);
for p in 0..n {
cluster_ids[p][level] = ids[p];
}
all_level_ids[level] = ids;
}
let mut nodes: Vec<BvhNode> = Vec::new();
let ids0 = &all_level_ids[0];
let n_c0 = ids0.iter().copied().max().map(๏ฟฟm๏ฟฟ m + 1).unwrap_or(0) as usize;
let offset0 = 0usize;
let mut l0_nodes: Vec<BvhNode> = (0..n_c0)
.map(๏ฟฟ_๏ฟฟ {
let (mn, mx) = aabb_empty();
BvhNode {
aabb_min: mn, focus_sum: 0.0, aabb_max: mx,
child_start: 0, child_count: 0,
is_leaf: 1, leaf_start: 0, leaf_count: 0,
}
})
.collect();
for p in 0..n {
let cid = ids0[p] as usize;
if cid < n_c0 {
let pos = coords.position(p);
let foc = focus.get(p).copied().unwrap_or(0.0);
let node = &mut l0_nodes[cid];
aabb_expand(&mut node.aabb_min, &mut node.aabb_max, pos);
node.focus_sum += foc;
}
}
nodes.extend_from_slice(&l0_nodes);
let mut prev_offset = offset0;
let mut prev_ids = ids0.clone();
for level in 1..4 {
let cur_ids = &all_level_ids[level];
let n_cur = cur_ids.iter().copied().max().map(๏ฟฟm๏ฟฟ m + 1).unwrap_or(0) as usize;
let cur_offset = nodes.len();
let mut cur_nodes: Vec<BvhNode> = (0..n_cur)
.map(๏ฟฟ_๏ฟฟ {
let (mn, mx) = aabb_empty();
BvhNode {
aabb_min: mn,
focus_sum: 0.0,
aabb_max: mx,
child_start: 0,
child_count: 0,
is_leaf: 0,
leaf_start: 0,
leaf_count: 0,
}
})
.collect();
for p in 0..n {
let cid = cur_ids[p] as usize;
if cid < n_cur {
let pos = coords.position(p);
let foc = focus.get(p).copied().unwrap_or(0.0);
let node = &mut cur_nodes[cid];
aabb_expand(&mut node.aabb_min, &mut node.aabb_max, pos);
node.focus_sum += foc;
}
}
let prev_count = nodes.len() - prev_offset;
let mut prev_to_cur = vec![u32::MAX; prev_count];
for p in 0..n {
let pc = prev_ids[p] as usize;
let cc = cur_ids[p] as usize;
if pc < prev_count && cc < n_cur && prev_to_cur[pc] == u32::MAX {
prev_to_cur[pc] = cc as u32;
}
}
let mut children_map: Vec<Vec<u32>> = vec![Vec::new(); n_cur];
for (prev_c, &cur_c) in prev_to_cur.iter().enumerate() {
if cur_c != u32::MAX {
children_map[cur_c as usize].push((prev_offset + prev_c) as u32);
}
}
for cid in 0..n_cur {
let children = &children_map[cid];
if !children.is_empty() {
cur_nodes[cid].child_start = children[0];
cur_nodes[cid].child_count = children.len() as u32;
}
}
nodes.extend_from_slice(&cur_nodes);
prev_offset = cur_offset;
prev_ids = cur_ids.clone();
}
Bvh { nodes, cluster_ids }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::epoch::eigensolver::SpectralCoords;
fn make_coords(pts: &span>; 3) -> SpectralCoords {
let n = pts.len();
let mut coords = Vec::with_capacity(n * 3);
for p in pts {
coords.extend_from_slice(p);
}
SpectralCoords { n, coords, extra: vec![0.0f32; n * 2] }
}
#[test]
fn bvh_100_particles_all_assigned() {
let n = 100usize;
let pts: Vec<[f32; 3]> = (0..n)
.map(|i| {
let t = i as f32 / n as f32;
let x = (t * 17.3).sin() * 500.0;
let y = (t * 13.7).cos() * 500.0;
let z = (t * 11.1).sin() * 300.0;
[x, y, z]
})
.collect();
let coords = make_coords(&pts);
let focus = vec![1.0f32 / n as f32; n];
let bvh = build(&coords, &focus);
assert_eq!(bvh.cluster_ids.len(), n, "cluster_ids length mismatch");
for (p, ids) in bvh.cluster_ids.iter().enumerate() {
for level in 0..4 {
assert_ne!(ids[level], u32::MAX, "particle {p} has no cluster at level {level}");
}
}
assert!(!bvh.nodes.is_empty(), "BVH has no nodes");
let ids0: Vec<u32> = bvh.cluster_ids.iter().map(|ids| ids[0]).collect();
let n_c0 = ids0.iter().copied().max().map(|m| m + 1).unwrap_or(0) as usize;
for p in 0..n {
let cid = ids0[p] as usize;
assert!(cid < n_c0, "cluster id {cid} out of range {n_c0}");
let node = &bvh.nodes[cid];
let pos = pts[p];
for d in 0..3 {
assert!(
node.aabb_min[d] <= pos[d] + 1e-3,
"particle {p} dim {d}: pos {} < aabb_min {}",
pos[d],
node.aabb_min[d]
);
assert!(
node.aabb_max[d] >= pos[d] - 1e-3,
"particle {p} dim {d}: pos {} > aabb_max {}",
pos[d],
node.aabb_max[d]
);
}
}
}
}