1use crate::brute::{canonicalise, distance_squared, finite};
4use crate::{KdPeriodicOptions, NeighborPair, PeriodicBox, SpatialError};
5use std::cmp::Ordering;
6use std::collections::{BTreeMap, BinaryHeap};
7
8#[path = "kd_periodic.rs"]
9mod periodic_helpers;
10#[path = "kd_stream.rs"]
11mod stream;
12use periodic_helpers::{for_each_shift, periodic_image_limits, validate_image_budget};
13
14#[derive(Clone, Copy, Debug)]
15struct Node {
16 atom: u32,
17 position: [f32; 3],
18 axis: usize,
19 left: Option<usize>,
20 right: Option<usize>,
21}
22
23#[derive(Debug)]
28pub struct KdTree<'a> {
29 positions: &'a [[f32; 3]],
30 nodes: Vec<Node>,
31 root: Option<usize>,
32 periodic: Option<PeriodicBox>,
33 periodic_options: KdPeriodicOptions,
34 reservation: Option<molframe_core::MemoryReservation>,
35}
36
37impl<'a> KdTree<'a> {
38 pub fn build(
44 positions: &'a [[f32; 3]],
45 targets: &[u32],
46 periodic: Option<PeriodicBox>,
47 ) -> Result<Self, SpatialError> {
48 Self::build_with_options(positions, targets, periodic, KdPeriodicOptions::default())
49 }
50
51 pub fn build_with_options(
57 positions: &'a [[f32; 3]],
58 targets: &[u32],
59 periodic: Option<PeriodicBox>,
60 periodic_options: KdPeriodicOptions,
61 ) -> Result<Self, SpatialError> {
62 validate_indices(targets, positions.len())?;
63 periodic_options.validate()?;
64
65 let mut entries = finite_entries(positions, targets, periodic.as_ref());
66 let mut nodes = Vec::with_capacity(entries.len());
67 let root = build_nodes(&mut entries, 0, &mut nodes);
68
69 Ok(Self {
70 positions,
71 nodes,
72 root,
73 periodic,
74 periodic_options,
75 reservation: None,
76 })
77 }
78
79 pub fn pairs(&self, query: &[u32], cutoff: f32) -> Result<Vec<NeighborPair>, SpatialError> {
85 validate_cutoff(cutoff)?;
86 validate_indices(query, self.positions.len())?;
87
88 let cutoff_squared = cutoff * cutoff;
89 let mut found = Vec::new();
90
91 for &atom in query {
92 let index = usize::try_from(atom).map_err(|_| SpatialError::NumericRangeExceeded)?;
93 let Some(position) = self.positions.get(index).copied() else {
94 continue;
95 };
96
97 if !finite(position) {
98 continue;
99 }
100
101 self.append_radius_pairs(atom, position, cutoff, cutoff_squared, &mut found)?;
102 }
103
104 canonicalise(&mut found);
105 Ok(found)
106 }
107
108 pub fn k_nearest(&self, query: u32, count: usize) -> Result<Vec<(u32, f32)>, SpatialError> {
114 let index = usize::try_from(query).map_err(|_| SpatialError::NumericRangeExceeded)?;
115 let Some(point) = self.positions.get(index).copied() else {
116 return Err(SpatialError::AtomOutOfBounds(query));
117 };
118
119 if count == 0 || !finite(point) {
120 return Ok(Vec::new());
121 }
122
123 let Some(root) = self.root else {
124 return Ok(Vec::new());
125 };
126
127 if let Some(periodic) = self.periodic {
128 return self.periodic_nearest(root, query, point, count, &periodic);
129 }
130
131 let capacity = count
132 .checked_add(1)
133 .ok_or(SpatialError::NumericRangeExceeded)?;
134 let mut heap = BinaryHeap::with_capacity(capacity);
135
136 self.nearest_search(root, query, point, count, &mut heap);
137
138 Ok(sorted_candidates(heap))
139 }
140
141 fn append_radius_pairs(
142 &self,
143 atom: u32,
144 position: [f32; 3],
145 cutoff: f32,
146 cutoff_squared: f32,
147 found: &mut Vec<NeighborPair>,
148 ) -> Result<(), SpatialError> {
149 let Some(root) = self.root else {
150 return Ok(());
151 };
152 let Some(periodic) = self.periodic else {
153 self.radius_search(root, position, cutoff_squared, &mut |_, target, squared| {
154 if atom != target {
155 found.push(NeighborPair::new(atom, target, squared));
156 }
157 });
158 return Ok(());
159 };
160
161 let limits = periodic_image_limits(&periodic, cutoff)?;
162 validate_image_budget(limits, self.periodic_options)?;
163 let wrapped = periodic.wrap(position);
164 for_each_shift(limits, |shift| {
165 let image = periodic.translated(wrapped, shift)?;
166 self.radius_search(root, image, cutoff_squared, &mut |_, target, _| {
167 if atom == target {
168 return;
169 }
170 let Ok(index) = usize::try_from(target) else {
171 return;
172 };
173 let Some(target_position) = self.positions.get(index).copied() else {
174 return;
175 };
176 let squared = periodic.distance_squared(position, target_position);
177 if squared <= cutoff_squared {
178 found.push(NeighborPair::new(atom, target, squared));
179 }
180 });
181 Ok(())
182 })?;
183 Ok(())
184 }
185
186 fn radius_search(
191 &self,
192 node_index: usize,
193 point: [f32; 3],
194 cutoff_squared: f32,
195 visit: &mut impl FnMut(usize, u32, f32),
196 ) {
197 let Some(node) = self.nodes.get(node_index).copied() else {
198 return;
199 };
200
201 let position = node.position;
202
203 let squared = distance_squared(point, position, None);
204
205 if squared <= cutoff_squared {
206 visit(node_index, node.atom, squared);
207 }
208
209 let delta = point[node.axis] - position[node.axis];
210 let (near, far) = ordered_children(node, delta);
211
212 if let Some(near) = near {
213 self.radius_search(near, point, cutoff_squared, visit);
214 }
215
216 if delta * delta <= cutoff_squared
217 && let Some(far) = far
218 {
219 self.radius_search(far, point, cutoff_squared, visit);
220 }
221 }
222
223 fn nearest_search(
228 &self,
229 node_index: usize,
230 query: u32,
231 point: [f32; 3],
232 count: usize,
233 heap: &mut BinaryHeap<Candidate>,
234 ) {
235 let Some(node) = self.nodes.get(node_index).copied() else {
236 return;
237 };
238
239 let position = node.position;
240
241 let squared = distance_squared(point, position, None);
242
243 if node.atom != query {
244 push_candidate(heap, Candidate::new(node.atom, squared), count);
245 }
246
247 let axis = node.axis;
248 let delta = point[axis] - position[axis];
249 let (near, far) = ordered_children(node, delta);
250
251 if let Some(near) = near {
252 self.nearest_search(near, query, point, count, heap);
253 }
254
255 if should_visit_far(heap, count, delta)
256 && let Some(far) = far
257 {
258 self.nearest_search(far, query, point, count, heap);
259 }
260 }
261
262 fn periodic_nearest(
263 &self,
264 root: usize,
265 query: u32,
266 point: [f32; 3],
267 count: usize,
268 periodic: &PeriodicBox,
269 ) -> Result<Vec<(u32, f32)>, SpatialError> {
270 const NEAREST_IMAGE_LIMITS: [i64; 3] = [2; 3];
271 validate_image_budget(NEAREST_IMAGE_LIMITS, self.periodic_options)?;
272 let wrapped = periodic.wrap(point);
273 let mut best_by_atom = BTreeMap::new();
274
275 for_each_shift(NEAREST_IMAGE_LIMITS, |shift| {
276 let image = periodic.translated(wrapped, shift)?;
277 let capacity = count
278 .checked_add(1)
279 .ok_or(SpatialError::NumericRangeExceeded)?;
280 let mut image_heap = BinaryHeap::with_capacity(capacity);
281 self.nearest_search(root, query, image, count, &mut image_heap);
282 for (atom, _) in sorted_candidates(image_heap) {
283 let index =
284 usize::try_from(atom).map_err(|_| SpatialError::NumericRangeExceeded)?;
285 let Some(target) = self.positions.get(index).copied() else {
286 continue;
287 };
288 let squared = periodic.distance_squared(point, target);
289 best_by_atom
290 .entry(atom)
291 .and_modify(|best: &mut f32| *best = best.min(squared))
292 .or_insert(squared);
293 }
294 Ok(())
295 })?;
296
297 let capacity = count
298 .checked_add(1)
299 .ok_or(SpatialError::NumericRangeExceeded)?;
300 let mut heap = BinaryHeap::with_capacity(capacity);
301 for (atom, squared) in best_by_atom {
302 push_candidate(&mut heap, Candidate::new(atom, squared), count);
303 }
304 Ok(sorted_candidates(heap))
305 }
306}
307
308#[derive(Clone, Copy, Debug)]
309struct Entry {
310 atom: u32,
311 position: [f32; 3],
312}
313
314fn finite_entries(
318 positions: &[[f32; 3]],
319 targets: &[u32],
320 periodic: Option<&PeriodicBox>,
321) -> Vec<Entry> {
322 let mut entries = Vec::with_capacity(targets.len());
323 entries.extend(targets.iter().filter_map(|&atom| {
324 let index = usize::try_from(atom).ok()?;
325 let position = positions.get(index).copied()?;
326 if !finite(position) {
327 return None;
328 }
329 let position = match periodic {
330 Some(periodic) => periodic.wrap(position),
331 None => position,
332 };
333 Some(Entry { atom, position })
334 }));
335 entries
336}
337
338fn build_nodes(entries: &mut [Entry], depth: usize, nodes: &mut Vec<Node>) -> Option<usize> {
343 if entries.is_empty() {
344 return None;
345 }
346
347 let axis = depth % 3;
348 let middle = entries.len() / 2;
349
350 entries.select_nth_unstable_by(middle, |left, right| {
351 left.position[axis]
352 .total_cmp(&right.position[axis])
353 .then_with(|| left.atom.cmp(&right.atom))
354 });
355
356 let (left, rest) = entries.split_at_mut(middle);
357 let (median, right) = rest.split_first_mut()?;
358 let entry = *median;
359
360 let node_index = nodes.len();
361 nodes.push(Node {
362 atom: entry.atom,
363 position: entry.position,
364 axis,
365 left: None,
366 right: None,
367 });
368
369 let left_node = build_nodes(left, depth + 1, nodes);
370 let right_node = build_nodes(right, depth + 1, nodes);
371
372 if let Some(node) = nodes.get_mut(node_index) {
373 node.left = left_node;
374 node.right = right_node;
375 }
376
377 Some(node_index)
378}
379
380#[inline]
384fn ordered_children(node: Node, delta: f32) -> (Option<usize>, Option<usize>) {
385 if delta <= 0.0 {
386 (node.left, node.right)
387 } else {
388 (node.right, node.left)
389 }
390}
391
392#[inline]
396fn should_visit_far(heap: &BinaryHeap<Candidate>, count: usize, delta: f32) -> bool {
397 if heap.len() < count {
398 return true;
399 }
400
401 heap.peek()
402 .is_some_and(|candidate| delta * delta <= candidate.squared)
403}
404
405#[derive(Clone, Copy, Debug)]
406struct Candidate {
407 atom: u32,
408 squared: f32,
409}
410
411impl Candidate {
412 const fn new(atom: u32, squared: f32) -> Self {
416 Self { atom, squared }
417 }
418}
419
420impl PartialEq for Candidate {
421 fn eq(&self, other: &Self) -> bool {
423 self.atom == other.atom && self.squared.to_bits() == other.squared.to_bits()
424 }
425}
426
427impl Eq for Candidate {}
428
429impl PartialOrd for Candidate {
430 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
432 Some(self.cmp(other))
433 }
434}
435
436impl Ord for Candidate {
437 fn cmp(&self, other: &Self) -> Ordering {
439 self.squared
440 .total_cmp(&other.squared)
441 .then_with(|| self.atom.cmp(&other.atom))
442 }
443}
444
445fn push_candidate(heap: &mut BinaryHeap<Candidate>, candidate: Candidate, count: usize) {
449 heap.push(candidate);
450
451 if heap.len() > count {
452 let _discarded = heap.pop();
453 }
454}
455
456fn sorted_candidates(heap: BinaryHeap<Candidate>) -> Vec<(u32, f32)> {
460 heap.into_sorted_vec()
461 .into_iter()
462 .map(|candidate| (candidate.atom, candidate.squared))
463 .collect()
464}
465
466fn validate_cutoff(cutoff: f32) -> Result<(), SpatialError> {
470 if cutoff.is_finite() && cutoff >= 0.0 {
471 Ok(())
472 } else {
473 Err(SpatialError::InvalidCutoff)
474 }
475}
476
477fn validate_indices(atoms: &[u32], position_count: usize) -> Result<(), SpatialError> {
481 for &atom in atoms {
482 let index = usize::try_from(atom).map_err(|_| SpatialError::NumericRangeExceeded)?;
483 if index >= position_count {
484 return Err(SpatialError::AtomOutOfBounds(atom));
485 }
486 }
487
488 Ok(())
489}
490
491#[cfg(test)]
492#[path = "kd_tests.rs"]
493mod tests;