use super::errors::{validate_cutoff, validate_indices};
use crate::brute::finite;
use crate::{KdPeriodicOptions, NeighborPair, PeriodicBox, SpatialError};
use molframe_core::ExecutionContext;
use molframe_core::parallel::{BlockPlan, map_blocks_in};
use std::sync::Mutex;
mod batch;
mod heap;
mod periodic;
mod search;
mod stream;
pub use batch::{KnnOptions, KnnResult, NO_NEIGHBOR};
use periodic::{for_each_shift, periodic_image_limits, validate_image_budget};
pub(super) const LEAF_MAX: usize = 32;
#[derive(Clone, Copy, Debug)]
struct Entry {
position: [f32; 3],
atom: u32,
}
#[derive(Debug)]
pub struct KdTree<'a> {
positions: &'a [[f32; 3]],
xs: Vec<f32>,
ys: Vec<f32>,
zs: Vec<f32>,
ids: Vec<u32>,
split_value: Vec<f32>,
split_axis: Vec<u8>,
bounds: Option<([f32; 3], [f32; 3])>,
periodic: Option<PeriodicBox>,
periodic_options: KdPeriodicOptions,
reservation: Option<molframe_core::MemoryReservation>,
}
impl<'a> KdTree<'a> {
pub fn build(
positions: &'a [[f32; 3]],
targets: &[u32],
periodic: Option<PeriodicBox>,
) -> Result<Self, SpatialError> {
Self::build_with_options(positions, targets, periodic, KdPeriodicOptions::default())
}
pub fn build_with_options(
positions: &'a [[f32; 3]],
targets: &[u32],
periodic: Option<PeriodicBox>,
periodic_options: KdPeriodicOptions,
) -> Result<Self, SpatialError> {
Self::build_parallel(positions, targets, periodic, periodic_options, None)
}
pub(super) fn build_parallel(
positions: &'a [[f32; 3]],
targets: &[u32],
periodic: Option<PeriodicBox>,
periodic_options: KdPeriodicOptions,
context: Option<&ExecutionContext>,
) -> Result<Self, SpatialError> {
validate_indices(targets, positions.len())?;
periodic_options.validate()?;
let mut entries = finite_entries(positions, targets, periodic.as_ref());
let bounds = bounds_of(&entries);
let depth = depth_for(entries.len());
let node_count = (1_usize << depth) - 1;
let mut split_value = vec![0.0_f32; node_count];
let mut split_axis = vec![0_u8; node_count];
for (node, axis, value) in build_splits(&mut entries, depth, context)? {
split_value[node] = value;
split_axis[node] = axis;
}
let mut xs = Vec::with_capacity(entries.len());
let mut ys = Vec::with_capacity(entries.len());
let mut zs = Vec::with_capacity(entries.len());
let mut ids = Vec::with_capacity(entries.len());
for entry in &entries {
xs.push(entry.position[0]);
ys.push(entry.position[1]);
zs.push(entry.position[2]);
ids.push(entry.atom);
}
Ok(Self {
positions,
xs,
ys,
zs,
ids,
split_value,
split_axis,
bounds,
periodic,
periodic_options,
reservation: None,
})
}
#[must_use]
pub fn len(&self) -> usize {
self.ids.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.ids.is_empty()
}
pub(crate) fn retained_bytes(&self) -> usize {
self.ids.capacity() * (3 * size_of::<f32>() + size_of::<u32>())
+ self.split_value.capacity() * (size_of::<f32>() + size_of::<u8>())
}
pub(crate) fn build_bytes(targets: usize) -> Option<usize> {
let depth = depth_for(targets);
targets
.checked_mul(size_of::<Entry>() + 3 * size_of::<f32>() + size_of::<u32>())?
.checked_add(((1_usize << depth) - 1) * (size_of::<f32>() + size_of::<u8>()))
}
pub fn pairs(&self, query: &[u32], cutoff: f32) -> Result<Vec<NeighborPair>, SpatialError> {
validate_cutoff(cutoff)?;
validate_indices(query, self.positions.len())?;
let mut found = Vec::new();
let mut collector = self.radius_collector(cutoff)?;
for &atom in query {
collector.collect(self, atom, |target, squared| {
found.push(NeighborPair::new(atom, target, squared));
})?;
}
crate::brute::canonicalise(&mut found);
Ok(found)
}
pub fn k_nearest(&self, query: u32, count: usize) -> Result<Vec<(u32, f32)>, SpatialError> {
let index = usize::try_from(query).map_err(|_| SpatialError::NumericRangeExceeded)?;
let Some(point) = self.positions.get(index).copied() else {
return Err(SpatialError::AtomOutOfBounds(query));
};
if count == 0 || !finite(point) {
return Ok(Vec::new());
}
let mut heap = heap::KnnHeap::new(count);
self.knn_into(&mut heap, point, Some(query), f32::INFINITY)?;
Ok(heap
.sorted()
.into_iter()
.map(|(squared, atom)| (atom, squared))
.collect())
}
}
fn finite_entries(
positions: &[[f32; 3]],
targets: &[u32],
periodic: Option<&PeriodicBox>,
) -> Vec<Entry> {
let mut entries = Vec::with_capacity(targets.len());
entries.extend(targets.iter().filter_map(|&atom| {
let index = usize::try_from(atom).ok()?;
let position = positions.get(index).copied()?;
if !finite(position) {
return None;
}
let position = match periodic {
Some(periodic) => periodic.wrap(position),
None => position,
};
Some(Entry { position, atom })
}));
entries
}
fn bounds_of(entries: &[Entry]) -> Option<([f32; 3], [f32; 3])> {
let first = entries.first()?;
let mut low = first.position;
let mut high = first.position;
for entry in entries {
for axis in 0..3 {
low[axis] = low[axis].min(entry.position[axis]);
high[axis] = high[axis].max(entry.position[axis]);
}
}
Some((low, high))
}
fn depth_for(count: usize) -> usize {
let mut depth = 0;
while count.div_ceil(1 << depth) > LEAF_MAX {
depth += 1;
}
depth
}
type Split = (usize, u8, f32);
fn split_slice(slice: &mut [Entry]) -> (u8, f32, usize) {
let mut low = [f32::INFINITY; 3];
let mut high = [f32::NEG_INFINITY; 3];
for entry in slice.iter() {
for axis in 0..3 {
low[axis] = low[axis].min(entry.position[axis]);
high[axis] = high[axis].max(entry.position[axis]);
}
}
let mut axis = 0_u8;
for candidate in 1..3_u8 {
let (current, next) = (usize::from(axis), usize::from(candidate));
if high[next] - low[next] > high[current] - low[current] {
axis = candidate;
}
}
let middle = slice.len() / 2;
let key = usize::from(axis);
slice.select_nth_unstable_by(middle, |left, right| {
left.position[key].total_cmp(&right.position[key])
});
(axis, slice[middle].position[key], middle)
}
type Task<'s> = Mutex<Option<(usize, &'s mut [Entry])>>;
fn build_subtree(slice: &mut [Entry], node: usize, remaining: usize, out: &mut Vec<Split>) {
if remaining == 0 {
return;
}
let (axis, value, middle) = split_slice(slice);
out.push((node, axis, value));
let (left, right) = slice.split_at_mut(middle);
build_subtree(left, 2 * node + 1, remaining - 1, out);
build_subtree(right, 2 * node + 2, remaining - 1, out);
}
fn build_splits(
entries: &mut [Entry],
depth: usize,
context: Option<&ExecutionContext>,
) -> Result<Vec<Split>, SpatialError> {
let workers = context.map_or(1, ExecutionContext::worker_budget);
let parallel = workers > 1 && entries.len() >= 16_384 && depth > 0;
let mut splits = Vec::new();
if !parallel {
build_subtree(entries, 0, depth, &mut splits);
return Ok(splits);
}
let top = depth.min((workers * 4).next_power_of_two().trailing_zeros() as usize);
let mut frontier = vec![(0_usize, entries)];
for _ in 0..top {
let mut next = Vec::with_capacity(frontier.len() * 2);
for (node, slice) in frontier {
let (axis, value, middle) = split_slice(slice);
splits.push((node, axis, value));
let (left, right) = slice.split_at_mut(middle);
next.push((2 * node + 1, left));
next.push((2 * node + 2, right));
}
frontier = next;
}
let remaining = depth - top;
let tasks: Vec<Task> = frontier
.into_iter()
.map(|task| Mutex::new(Some(task)))
.collect();
let Some(context) = context else {
return Err(SpatialError::WorkerPanicked);
};
let produced = map_blocks_in(BlockPlan::new(tasks.len(), 1), context, |index, _| {
let mut out = Vec::new();
let task = tasks.get(index).and_then(|slot| slot.lock().ok()?.take());
if let Some((node, slice)) = task {
build_subtree(slice, node, remaining, &mut out);
}
out
})
.map_err(|_| SpatialError::WorkerPanicked)?;
for mut block in produced {
splits.append(&mut block);
}
Ok(splits)
}
#[cfg(test)]
#[path = "kd_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "kd_adversarial_tests.rs"]
mod adversarial_tests;