use super::KdTree;
use crate::backends::errors::{block_error, validate_cutoff, validate_indices};
use crate::brute::canonicalise;
use crate::{NeighborPair, SpatialError};
use molframe_core::ExecutionContext;
use molframe_core::parallel::{BlockPlan, try_for_each_block_in};
const QUERY_BLOCK: usize = 1024;
pub const NO_NEIGHBOR: u32 = u32::MAX;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct KnnOptions {
pub max_distance: Option<f32>,
pub exclude_self: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct KnnResult {
pub k: usize,
pub indices: Vec<u32>,
pub distances_squared: Vec<f32>,
}
impl KnnOptions {
#[must_use]
pub fn for_queries(
max_distance: Option<f32>,
exclude_self: Option<bool>,
separate_queries: bool,
) -> Self {
let exclude_self = match exclude_self {
Some(choice) => choice,
None => !separate_queries,
};
Self {
max_distance,
exclude_self,
}
}
}
impl KnnResult {
#[must_use]
pub fn into_columns(self) -> (Vec<u32>, Vec<f32>) {
let mut distances = self.distances_squared;
for value in &mut distances {
*value = value.sqrt();
}
(self.indices, distances)
}
#[must_use]
pub fn rows(&self) -> usize {
match self.indices.len().checked_div(self.k) {
Some(rows) => rows,
None => 0,
}
}
}
impl KdTree<'_> {
pub fn knn(
&self,
query: &[u32],
k: usize,
options: KnnOptions,
context: &ExecutionContext,
) -> Result<KnnResult, SpatialError> {
if let Some(distance) = options.max_distance {
validate_cutoff(distance)?;
}
validate_indices(query, self.positions.len())?;
let slots = query
.len()
.checked_mul(k)
.ok_or(SpatialError::NumericRangeExceeded)?;
let bytes = slots
.checked_mul(size_of::<u32>() + size_of::<f32>())
.ok_or(SpatialError::NumericRangeExceeded)?;
let _reservation = context.try_reserve(bytes)?;
let mut indices = Vec::new();
let mut distances = Vec::new();
indices
.try_reserve_exact(slots)
.map_err(|_| SpatialError::NumericRangeExceeded)?;
distances
.try_reserve_exact(slots)
.map_err(|_| SpatialError::NumericRangeExceeded)?;
if k == 0 || query.is_empty() {
return Ok(KnnResult {
k,
indices,
distances_squared: distances,
});
}
self.searcher_check(k, options)?;
let plan = BlockPlan::new(query.len(), QUERY_BLOCK);
try_for_each_block_in(
plan,
context,
QUERY_BLOCK * k * (size_of::<u32>() + size_of::<f32>()),
|_, range| {
let mut searcher = self.knn_searcher(k, options.max_distance)?;
let chunk = match query.get(range) {
Some(chunk) => chunk,
None => &[],
};
let mut ids = Vec::with_capacity(chunk.len() * k);
let mut squared = Vec::with_capacity(chunk.len() * k);
for &atom in chunk {
searcher.search(self, atom, options.exclude_self, options.max_distance);
let found = searcher.heap.sort_in_place();
let limit = options.max_distance.map_or(f32::INFINITY, |d| d * d);
let mut filled = 0;
for &(distance, id) in found.iter().take(k) {
if distance > limit {
break;
}
ids.push(id);
squared.push(distance);
filled += 1;
}
for _ in filled..k {
ids.push(NO_NEIGHBOR);
squared.push(f32::INFINITY);
}
}
Ok((ids, squared))
},
|(ids, squared): (Vec<u32>, Vec<f32>)| {
indices.extend_from_slice(&ids);
distances.extend_from_slice(&squared);
Ok(())
},
)
.map_err(|error| block_error(&error))?;
Ok(KnnResult {
k,
indices,
distances_squared: distances,
})
}
fn searcher_check(&self, k: usize, options: KnnOptions) -> Result<(), SpatialError> {
self.knn_searcher(k, options.max_distance).map(|_| ())
}
pub(crate) fn pairs_in(
&self,
query: &[u32],
cutoff: f32,
same_selection: bool,
sorted: bool,
context: &ExecutionContext,
) -> Result<Vec<NeighborPair>, SpatialError> {
validate_cutoff(cutoff)?;
validate_indices(query, self.positions.len())?;
self.radius_collector(cutoff)?;
let block = if self.periodic.is_some() { 256 } else { 2048 };
let plan = BlockPlan::new(query.len(), block);
let expected = 32 * size_of::<NeighborPair>();
let mut found: Vec<NeighborPair> = Vec::new();
try_for_each_block_in(
plan,
context,
block.saturating_mul(expected),
|_, range| {
let mut collector = self.radius_collector(cutoff)?;
let mut out = Vec::new();
let mut hits: Vec<(u32, f32)> = Vec::new();
let chunk = match query.get(range) {
Some(chunk) => chunk,
None => &[],
};
for &atom in chunk {
hits.clear();
collector.collect(self, atom, |target, squared| {
if !same_selection || target > atom {
hits.push((target, squared));
}
})?;
if sorted || same_selection {
hits.sort_unstable_by_key(|hit| hit.0);
}
out.extend(
hits.iter()
.map(|&(target, squared)| NeighborPair::new(atom, target, squared)),
);
}
Ok(out)
},
|mut block: Vec<NeighborPair>| {
found.append(&mut block);
Ok(())
},
)
.map_err(|error| block_error(&error))?;
let ascending = query.windows(2).all(|pair| pair[0] < pair[1]);
if !same_selection || (sorted && !ascending) {
canonicalise(&mut found);
}
Ok(found)
}
}