Skip to main content

manifold_knn/
lib.rs

1//! Dynamic-programming k-nearest-neighbor queries for birth-ordered point clouds.
2//!
3//! This crate implements the query-side data structure described by Wang et al.,
4//! *Manifold k-NN: Accelerated k-NN Queries for Manifold Point Clouds*.
5//!
6//! The paper separates the method into two concerns:
7//!
8//! 1. Maintain a **successor table**. When a point `j` is inserted, append `j` to
9//!    every earlier point whose Voronoi cell is pruned by the insertion. In a
10//!    Delaunay implementation this means appending `j` to the Delaunay neighbors
11//!    of `j` in the prefix triangulation.
12//! 2. Answer k-NN queries by first following the dynamic-programming 1-NN path,
13//!    then expanding only the successor lists of the best candidates.
14//!
15//! This crate implements item 2, plus safe helpers for constructing and updating
16//! successor tables. With the optional `delaunay-3d` feature it can also build
17//! insertion-time successor tables directly from the `delaunay` crate's 3D
18//! incremental triangulation API.
19//!
20//! # Example
21//!
22//! ```
23//! use manifold_knn::ManifoldKnn;
24//!
25//! let points = vec![
26//!     [0.0, 0.0, 0.0],
27//!     [1.0, 0.0, 0.0],
28//!     [0.0, 1.0, 0.0],
29//!     [0.0, 0.0, 1.0],
30//! ];
31//!
32//! // Exact but quadratic fallback: every later point is a successor of every
33//! // earlier point. This is useful for tests and small point sets.
34//! let index = ManifoldKnn::<3>::from_complete_successors(points)?;
35//! let nn = index.knn(&[0.2, 0.1, 0.0], 2)?;
36//!
37//! assert_eq!(nn.len(), 2);
38//! assert_eq!(nn[0].index, 0);
39//! # Ok::<(), manifold_knn::Error>(())
40//! ```
41
42#![cfg_attr(feature = "simd", feature(portable_simd))]
43#![deny(unsafe_code)]
44#![warn(missing_docs)]
45
46mod bounded;
47mod error;
48mod table;
49
50#[cfg(feature = "delaunay-3d")]
51pub mod delaunay3d;
52
53#[cfg(feature = "delaunay-3d")]
54pub use delaunay3d::{Delaunay3dKernel, DelaunayManifoldKnn3, DelaunayTriangulation3};
55
56pub use error::Error;
57pub use table::SuccessorTable;
58
59use bounded::{BoundedNeighbors, neighbor_cmp};
60
61/// A nearest-neighbor result.
62#[derive(Clone, Copy, Debug, PartialEq)]
63pub struct Neighbor {
64    /// Index of the point in the birth-time-ordered point array.
65    pub index: usize,
66    /// Squared Euclidean distance from the query to the point.
67    pub squared_distance: f64,
68}
69
70impl Neighbor {
71    /// Returns the Euclidean distance from the query to this neighbor.
72    #[must_use]
73    pub fn distance(self) -> f64 {
74        self.squared_distance.sqrt()
75    }
76}
77
78/// Summary returned by deletion/update operations.
79#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
80pub struct DeleteReport {
81    /// Number of references to the deleted point that were removed from lists.
82    pub removed_references: usize,
83    /// Number of new successor edges inserted.
84    pub inserted_successors: usize,
85    /// Number of requested successor edges that were already present.
86    pub already_present: usize,
87}
88
89/// Dynamic-programming k-NN index over a fixed-dimensional Euclidean point set.
90///
91/// `D` is the ambient dimension. For the paper's common point-cloud case use
92/// `ManifoldKnn<3>`.
93#[derive(Clone, Debug)]
94pub struct ManifoldKnn<const D: usize> {
95    points: Vec<[f64; D]>,
96    successors: SuccessorTable,
97    active: Vec<bool>,
98}
99
100impl<const D: usize> ManifoldKnn<D> {
101    /// Creates an index from points and a successor table.
102    ///
103    /// The successor table must have one list per point; every entry in list `i`
104    /// must be a strictly later birth index `j > i`. Lists must be sorted and
105    /// duplicate-free. Use [`SuccessorTable::from_lists_normalized`] when input
106    /// lists may be unsorted.
107    pub fn new(points: Vec<[f64; D]>, successors: SuccessorTable) -> Result<Self, Error> {
108        validate_points(&points)?;
109        successors.validate_for_len(points.len())?;
110        let active = vec![true; points.len()];
111        Ok(Self {
112            points,
113            successors,
114            active,
115        })
116    }
117
118    /// Builds an exact but quadratic successor table.
119    ///
120    /// This stores every pair `i < j` as a successor edge `i -> j`. It is useful
121    /// as a correctness oracle, for small inputs, and for tests. It does **not**
122    /// provide the acceleration of the manifold/Delaunay successor table.
123    pub fn from_complete_successors(points: Vec<[f64; D]>) -> Result<Self, Error> {
124        let n = points.len();
125        Self::new(points, SuccessorTable::complete(n))
126    }
127
128    /// Builds an index from insertion-time neighbor lists.
129    ///
130    /// `neighbors_at_insertion[j]` must contain earlier birth indices `i < j`
131    /// that were adjacent to `j` when `j` was inserted. In the paper, these are
132    /// the Delaunay neighbors of `p_j` in the prefix triangulation. For every
133    /// such neighbor, this constructor appends `j` to the successor list of `i`.
134    pub fn from_insertion_neighbors(
135        points: Vec<[f64; D]>,
136        neighbors_at_insertion: Vec<Vec<usize>>,
137    ) -> Result<Self, Error> {
138        let successors =
139            SuccessorTable::from_insertion_neighbors(points.len(), neighbors_at_insertion)?;
140        Self::new(points, successors)
141    }
142
143    /// Returns all points in birth order.
144    #[must_use]
145    pub fn points(&self) -> &[[f64; D]] {
146        &self.points
147    }
148
149    /// Returns the successor table.
150    #[must_use]
151    pub fn successors(&self) -> &SuccessorTable {
152        &self.successors
153    }
154
155    /// Returns a mutable successor table.
156    ///
157    /// This is exposed for integration with external Delaunay maintenance code.
158    /// Call [`SuccessorTable::validate_for_len`] after substantial edits.
159    #[must_use]
160    pub fn successors_mut(&mut self) -> &mut SuccessorTable {
161        &mut self.successors
162    }
163
164    /// Number of stored points, including inactive points left by deletion APIs.
165    #[must_use]
166    #[inline]
167    pub fn len(&self) -> usize {
168        self.points.len()
169    }
170
171    /// Returns `true` if the index contains no stored points.
172    #[must_use]
173    #[inline]
174    pub fn is_empty(&self) -> bool {
175        self.points.is_empty()
176    }
177
178    /// Number of active points.
179    #[must_use]
180    #[inline]
181    pub fn active_len(&self) -> usize {
182        self.active.iter().filter(|&&is_active| is_active).count()
183    }
184
185    /// Returns whether the point at `index` is active.
186    #[must_use]
187    #[inline]
188    pub fn is_active(&self, index: usize) -> bool {
189        self.active.get(index).copied().unwrap_or(false)
190    }
191
192    /// Iterator over active point indices in birth order.
193    #[inline]
194    pub fn active_indices(&self) -> impl Iterator<Item = usize> + '_ {
195        self.active
196            .iter()
197            .enumerate()
198            .filter_map(|(index, &is_active)| is_active.then_some(index))
199    }
200
201    /// Inserts a new point and updates successor lists from externally supplied
202    /// insertion-time neighbors.
203    ///
204    /// `prior_neighbors` must contain active earlier indices that are adjacent to
205    /// the new point at insertion time. In a faithful implementation of the
206    /// paper, these come from incremental Delaunay construction.
207    ///
208    /// Returns the birth index assigned to the inserted point.
209    #[inline]
210    pub fn insert_with_neighbors<I>(
211        &mut self,
212        point: [f64; D],
213        prior_neighbors: I,
214    ) -> Result<usize, Error>
215    where
216        I: IntoIterator<Item = usize>,
217    {
218        validate_point(self.points.len(), &point)?;
219
220        let new_index = self.points.len();
221        let mut neighbors: Vec<usize> = prior_neighbors.into_iter().collect();
222        neighbors.sort_unstable();
223        neighbors.dedup();
224
225        for &neighbor in &neighbors {
226            if neighbor >= new_index {
227                return Err(Error::InvalidInsertionNeighbor {
228                    inserted: new_index,
229                    neighbor,
230                });
231            }
232            if !self.active[neighbor] {
233                return Err(Error::InactivePoint { index: neighbor });
234            }
235        }
236
237        self.points.push(point);
238        self.active.push(true);
239        self.successors.push_empty_list();
240
241        for neighbor in neighbors {
242            self.successors.insert_successor(neighbor, new_index)?;
243        }
244
245        Ok(new_index)
246    }
247
248    /// Returns the nearest active point in the full index.
249    #[inline]
250    pub fn nearest(&self, query: &[f64; D]) -> Result<Option<Neighbor>, Error> {
251        WORKSPACE.with(|ws| {
252            let mut workspace = ws.borrow_mut();
253            self.nearest_with_workspace(query, &mut workspace)
254        })
255    }
256
257    /// Returns the nearest active point in the full index using a workspace to avoid allocations.
258    #[inline]
259    pub fn nearest_with_workspace(
260        &self,
261        query: &[f64; D],
262        workspace: &mut QueryWorkspace,
263    ) -> Result<Option<Neighbor>, Error> {
264        let slice = self.knn_prefix_with_workspace(query, 1, self.points.len(), workspace)?;
265        Ok(slice.first().copied())
266    }
267
268    /// Returns up to `k` nearest active points in the full index.
269    ///
270    /// Results are sorted by squared distance, with birth index used as a stable
271    /// tie-breaker.
272    #[inline]
273    pub fn knn(&self, query: &[f64; D], k: usize) -> Result<Vec<Neighbor>, Error> {
274        self.knn_prefix(query, k, self.points.len())
275    }
276
277    /// Returns up to `k` nearest active points in the full index using a workspace.
278    #[inline]
279    pub fn knn_with_workspace<'a>(
280        &self,
281        query: &[f64; D],
282        k: usize,
283        workspace: &'a mut QueryWorkspace,
284    ) -> Result<&'a [Neighbor], Error> {
285        self.knn_prefix_with_workspace(query, k, self.points.len(), workspace)
286    }
287
288    /// Returns up to `k` nearest active points whose birth index is `< prefix_len`.
289    ///
290    /// This is the paper's zero-overhead prefix-subset query. The same successor
291    /// table is reused; candidates outside the prefix are ignored.
292    #[inline]
293    pub fn knn_prefix(
294        &self,
295        query: &[f64; D],
296        k: usize,
297        prefix_len: usize,
298    ) -> Result<Vec<Neighbor>, Error> {
299        WORKSPACE.with(|ws| {
300            let mut workspace = ws.borrow_mut();
301            let slice = self.knn_prefix_with_workspace(query, k, prefix_len, &mut workspace)?;
302            Ok(slice.to_vec())
303        })
304    }
305
306    /// Returns up to `k` nearest active points whose birth index is `< prefix_len` using a workspace.
307    #[inline]
308    pub fn knn_prefix_with_workspace<'a>(
309        &self,
310        query: &[f64; D],
311        k: usize,
312        prefix_len: usize,
313        workspace: &'a mut QueryWorkspace,
314    ) -> Result<&'a [Neighbor], Error> {
315        self.validate_query_and_prefix(query, prefix_len)?;
316        if k == 0 {
317            workspace.candidates.reset(0);
318            return Ok(&[]);
319        }
320
321        let Some(first_active) = self.first_active_before(prefix_len) else {
322            workspace.candidates.reset(0);
323            return Ok(&[]);
324        };
325
326        self.transition_sites_internal_from_index_with_workspace(
327            query,
328            k,
329            prefix_len,
330            first_active,
331            workspace,
332        )?;
333
334        if workspace.processed.len() < self.points.len() {
335            workspace.processed.resize(self.points.len(), false);
336        }
337        if workspace.discovered.len() < self.points.len() {
338            workspace.discovered.resize(self.points.len(), false);
339        }
340
341        for candidate in workspace.candidates.as_slice() {
342            let idx = candidate.index;
343            if !workspace.discovered[idx] {
344                workspace.discovered[idx] = true;
345                workspace.visited_indices.push(idx);
346            }
347        }
348
349        while let Some(index) = workspace
350            .candidates
351            .as_slice()
352            .iter()
353            .find(|candidate| !workspace.processed[candidate.index])
354            .map(|candidate| candidate.index)
355        {
356            workspace.processed[index] = true;
357
358            for &successor in self.successors.list(index) {
359                if successor >= prefix_len {
360                    break;
361                }
362                if workspace.discovered[successor] {
363                    continue;
364                }
365                if !self.active[successor] {
366                    continue;
367                }
368                workspace.discovered[successor] = true;
369                workspace.visited_indices.push(successor);
370
371                workspace.candidates.insert(Neighbor {
372                    index: successor,
373                    squared_distance: squared_distance(&self.points[successor], query),
374                });
375            }
376        }
377
378        for &index in &workspace.visited_indices {
379            workspace.processed[index] = false;
380            workspace.discovered[index] = false;
381        }
382        workspace.visited_indices.clear();
383
384        Ok(workspace.candidates.as_slice())
385    }
386
387    /// Collects transition sites from the dynamic-programming 1-NN path.
388    ///
389    /// Transition sites are the successive points that become the nearest
390    /// neighbor during the birth-ordered insertion history. The returned list is
391    /// sorted by query distance and capped to `capacity` entries.
392    #[inline]
393    pub fn transition_sites(
394        &self,
395        query: &[f64; D],
396        capacity: usize,
397    ) -> Result<Vec<Neighbor>, Error> {
398        self.transition_sites_prefix(query, capacity, self.points.len())
399    }
400
401    /// Collects transition sites using a workspace.
402    #[inline]
403    pub fn transition_sites_with_workspace<'a>(
404        &self,
405        query: &[f64; D],
406        capacity: usize,
407        workspace: &'a mut QueryWorkspace,
408    ) -> Result<&'a [Neighbor], Error> {
409        self.transition_sites_prefix_with_workspace(query, capacity, self.points.len(), workspace)
410    }
411
412    /// Prefix-restricted variant of [`Self::transition_sites`].
413    #[inline]
414    pub fn transition_sites_prefix(
415        &self,
416        query: &[f64; D],
417        capacity: usize,
418        prefix_len: usize,
419    ) -> Result<Vec<Neighbor>, Error> {
420        WORKSPACE.with(|ws| {
421            let mut workspace = ws.borrow_mut();
422            let slice = self.transition_sites_prefix_with_workspace(
423                query,
424                capacity,
425                prefix_len,
426                &mut workspace,
427            )?;
428            Ok(slice.to_vec())
429        })
430    }
431
432    /// Prefix-restricted variant of [`Self::transition_sites`] using a workspace.
433    #[inline]
434    pub fn transition_sites_prefix_with_workspace<'a>(
435        &self,
436        query: &[f64; D],
437        capacity: usize,
438        prefix_len: usize,
439        workspace: &'a mut QueryWorkspace,
440    ) -> Result<&'a [Neighbor], Error> {
441        self.validate_query_and_prefix(query, prefix_len)?;
442        self.transition_sites_internal_with_workspace(query, capacity, prefix_len, workspace)?;
443        Ok(workspace.candidates.as_slice())
444    }
445
446    /// Brute-force k-NN over the active full index.
447    ///
448    /// This is intended for validation and benchmarking, not production queries.
449    #[inline]
450    pub fn brute_force_knn(&self, query: &[f64; D], k: usize) -> Result<Vec<Neighbor>, Error> {
451        self.brute_force_knn_prefix(query, k, self.points.len())
452    }
453
454    /// Brute-force k-NN over active points with birth index `< prefix_len`.
455    #[inline]
456    pub fn brute_force_knn_prefix(
457        &self,
458        query: &[f64; D],
459        k: usize,
460        prefix_len: usize,
461    ) -> Result<Vec<Neighbor>, Error> {
462        self.validate_query_and_prefix(query, prefix_len)?;
463        if k == 0 {
464            return Ok(Vec::new());
465        }
466
467        let mut neighbors = Vec::new();
468        for index in 0..prefix_len {
469            if self.active[index] {
470                neighbors.push(Neighbor {
471                    index,
472                    squared_distance: squared_distance(&self.points[index], query),
473                });
474            }
475        }
476        neighbors.sort_by(neighbor_cmp);
477        neighbors.truncate(k);
478        Ok(neighbors)
479    }
480
481    /// Marks `index` as deleted and applies externally computed local successor
482    /// edges that replace adjacencies created by the deletion.
483    ///
484    /// This is the safe hook for the paper's local Delaunay deletion algorithm.
485    /// The crate validates and applies the successor-table mutations, while the
486    /// caller supplies the geometric result of the local retriangulation.
487    ///
488    /// Each pair `(i, j)` in `new_successors` means "insert successor edge
489    /// `i -> j`" and must satisfy `i < j`; both endpoints must be active and must
490    /// not be the deleted point.
491    #[inline]
492    pub fn delete_with_new_successors<I>(
493        &mut self,
494        index: usize,
495        new_successors: I,
496    ) -> Result<DeleteReport, Error>
497    where
498        I: IntoIterator<Item = (usize, usize)>,
499    {
500        self.ensure_active_index(index)?;
501
502        let mut edges: Vec<(usize, usize)> = new_successors.into_iter().collect();
503        edges.sort_unstable();
504        edges.dedup();
505
506        for &(owner, successor) in &edges {
507            self.validate_new_successor_edge(index, owner, successor)?;
508        }
509
510        let removed_references = self.successors.remove_references_to(index);
511        self.successors.clear_list(index)?;
512        self.active[index] = false;
513
514        let mut inserted_successors = 0;
515        let mut already_present = 0;
516        for (owner, successor) in edges {
517            if self.successors.insert_successor(owner, successor)? {
518                inserted_successors += 1;
519            } else {
520                already_present += 1;
521            }
522        }
523
524        Ok(DeleteReport {
525            removed_references,
526            inserted_successors,
527            already_present,
528        })
529    }
530
531    /// Deletes a point and rebuilds a complete quadratic successor table over the
532    /// remaining active points.
533    ///
534    /// This is an exact fallback for applications that need correctness after a
535    /// deletion but do not yet have local Delaunay-update integration. It discards
536    /// the accelerated successor table and should only be used for small or test
537    /// data.
538    #[inline]
539    pub fn delete_rebuild_complete(&mut self, index: usize) -> Result<DeleteReport, Error> {
540        self.ensure_active_index(index)?;
541        let removed_references = self.successors.remove_references_to(index);
542        self.successors.clear_all();
543        self.active[index] = false;
544
545        let mut inserted_successors = 0;
546        for owner in 0..self.points.len() {
547            if !self.active[owner] {
548                continue;
549            }
550            for successor in (owner + 1)..self.points.len() {
551                if self.active[successor] {
552                    self.successors.insert_successor(owner, successor)?;
553                    inserted_successors += 1;
554                }
555            }
556        }
557
558        Ok(DeleteReport {
559            removed_references,
560            inserted_successors,
561            already_present: 0,
562        })
563    }
564
565    #[inline]
566    fn transition_sites_internal_with_workspace(
567        &self,
568        query: &[f64; D],
569        capacity: usize,
570        prefix_len: usize,
571        workspace: &mut QueryWorkspace,
572    ) -> Result<(), Error> {
573        if capacity == 0 {
574            workspace.candidates.reset(0);
575            return Ok(());
576        }
577        let Some(first_active) = self.first_active_before(prefix_len) else {
578            workspace.candidates.reset(capacity);
579            return Ok(());
580        };
581        self.transition_sites_internal_from_index_with_workspace(
582            query,
583            capacity,
584            prefix_len,
585            first_active,
586            workspace,
587        )
588    }
589
590    #[inline]
591    fn transition_sites_internal_from_index_with_workspace(
592        &self,
593        query: &[f64; D],
594        capacity: usize,
595        prefix_len: usize,
596        mut current: usize,
597        workspace: &mut QueryWorkspace,
598    ) -> Result<(), Error> {
599        workspace.candidates.reset(capacity);
600        if capacity == 0 {
601            return Ok(());
602        }
603
604        workspace.candidates.insert(Neighbor {
605            index: current,
606            squared_distance: squared_distance(&self.points[current], query),
607        });
608
609        loop {
610            let current_distance = squared_distance(&self.points[current], query);
611            let mut next = None;
612
613            for &successor in self.successors.list(current) {
614                if successor >= prefix_len {
615                    break;
616                }
617                if !self.active[successor] {
618                    continue;
619                }
620
621                let successor_distance = squared_distance(&self.points[successor], query);
622                if is_strictly_better(successor_distance, successor, current_distance, current) {
623                    next = Some((successor, successor_distance));
624                    break;
625                }
626            }
627
628            let Some((successor, successor_distance)) = next else {
629                break;
630            };
631
632            current = successor;
633            workspace.candidates.insert(Neighbor {
634                index: current,
635                squared_distance: successor_distance,
636            });
637        }
638
639        Ok(())
640    }
641
642    #[inline]
643    fn validate_query_and_prefix(&self, query: &[f64; D], prefix_len: usize) -> Result<(), Error> {
644        validate_query(query)?;
645        if prefix_len > self.points.len() {
646            return Err(Error::InvalidPrefix {
647                prefix_len,
648                len: self.points.len(),
649            });
650        }
651        Ok(())
652    }
653
654    #[inline]
655    fn first_active_before(&self, prefix_len: usize) -> Option<usize> {
656        self.active[..prefix_len]
657            .iter()
658            .position(|&is_active| is_active)
659    }
660
661    #[inline]
662    fn ensure_active_index(&self, index: usize) -> Result<(), Error> {
663        if index >= self.points.len() {
664            return Err(Error::InvalidIndex {
665                index,
666                len: self.points.len(),
667            });
668        }
669        if !self.active[index] {
670            return Err(Error::InactivePoint { index });
671        }
672        Ok(())
673    }
674
675    #[inline]
676    fn validate_new_successor_edge(
677        &self,
678        deleted: usize,
679        owner: usize,
680        successor: usize,
681    ) -> Result<(), Error> {
682        if owner >= self.points.len() {
683            return Err(Error::InvalidIndex {
684                index: owner,
685                len: self.points.len(),
686            });
687        }
688        if successor >= self.points.len() {
689            return Err(Error::InvalidIndex {
690                index: successor,
691                len: self.points.len(),
692            });
693        }
694        if owner == deleted || successor == deleted || owner >= successor {
695            return Err(Error::InvalidSuccessor {
696                owner,
697                successor,
698                len: self.points.len(),
699            });
700        }
701        if !self.active[owner] {
702            return Err(Error::InactivePoint { index: owner });
703        }
704        if !self.active[successor] {
705            return Err(Error::InactivePoint { index: successor });
706        }
707        Ok(())
708    }
709}
710
711/// Reusable query workspace to avoid allocations during nearest-neighbor queries.
712#[derive(Clone, Debug, Default)]
713pub struct QueryWorkspace {
714    processed: Vec<bool>,
715    discovered: Vec<bool>,
716    visited_indices: Vec<usize>,
717    candidates: BoundedNeighbors,
718}
719
720impl QueryWorkspace {
721    /// Creates a new empty query workspace.
722    #[must_use]
723    #[inline]
724    pub const fn new() -> Self {
725        Self {
726            processed: Vec::new(),
727            discovered: Vec::new(),
728            visited_indices: Vec::new(),
729            candidates: BoundedNeighbors::new_empty(),
730        }
731    }
732}
733
734thread_local! {
735    static WORKSPACE: std::cell::RefCell<QueryWorkspace> = const { std::cell::RefCell::new(QueryWorkspace::new()) };
736}
737
738#[inline]
739pub(crate) fn validate_points<const D: usize>(points: &[[f64; D]]) -> Result<(), Error> {
740    #[cfg(feature = "parallel")]
741    {
742        use rayon::prelude::*;
743        points
744            .par_iter()
745            .enumerate()
746            .try_for_each(|(index, point)| validate_point(index, point))
747    }
748    #[cfg(not(feature = "parallel"))]
749    {
750        for (index, point) in points.iter().enumerate() {
751            validate_point(index, point)?;
752        }
753        Ok(())
754    }
755}
756
757#[inline]
758pub(crate) fn validate_point<const D: usize>(index: usize, point: &[f64; D]) -> Result<(), Error> {
759    for (coordinate, &value) in point.iter().enumerate() {
760        if !value.is_finite() {
761            return Err(Error::NonFiniteCoordinate {
762                point: index,
763                coordinate,
764                value,
765            });
766        }
767    }
768    Ok(())
769}
770
771#[inline]
772pub(crate) fn validate_query<const D: usize>(query: &[f64; D]) -> Result<(), Error> {
773    for (coordinate, &value) in query.iter().enumerate() {
774        if !value.is_finite() {
775            return Err(Error::NonFiniteQuery { coordinate, value });
776        }
777    }
778    Ok(())
779}
780
781#[cfg(feature = "simd")]
782#[inline]
783pub(crate) fn squared_distance<const D: usize>(point: &[f64; D], query: &[f64; D]) -> f64 {
784    use std::simd::{f64x4, num::SimdFloat};
785
786    if D == 0 {
787        return 0.0;
788    }
789
790    // Fast path for the most common small dimensions (D=2, D=3, D=4)
791    // This covers the vast majority of point-cloud use cases.
792    if D <= 4 {
793        let mut p = [0.0_f64; 4];
794        let mut q = [0.0_f64; 4];
795        p[..D].copy_from_slice(&point[..D]);
796        q[..D].copy_from_slice(&query[..D]);
797        let pv = f64x4::from_array(p);
798        let qv = f64x4::from_array(q);
799        let diff = pv - qv;
800        return (diff * diff).reduce_sum();
801    }
802
803    // General case: process 4 elements at a time using SIMD
804    let mut sum = 0.0_f64;
805    let mut i = 0;
806
807    while i + 4 <= D {
808        let pv = f64x4::from_array([point[i], point[i + 1], point[i + 2], point[i + 3]]);
809        let qv = f64x4::from_array([query[i], query[i + 1], query[i + 2], query[i + 3]]);
810        let diff = pv - qv;
811        sum += (diff * diff).reduce_sum();
812        i += 4;
813    }
814
815    // Handle remaining elements (0-3)
816    for j in i..D {
817        let delta = point[j] - query[j];
818        sum += delta * delta;
819    }
820
821    sum
822}
823
824#[cfg(not(feature = "simd"))]
825#[inline]
826pub(crate) fn squared_distance<const D: usize>(point: &[f64; D], query: &[f64; D]) -> f64 {
827    let mut sum = 0.0;
828    for i in 0..D {
829        let delta = point[i] - query[i];
830        sum += delta * delta;
831    }
832    sum
833}
834
835#[inline]
836pub(crate) fn is_strictly_better(
837    new_distance: f64,
838    new_index: usize,
839    old_distance: f64,
840    old_index: usize,
841) -> bool {
842    new_distance < old_distance || (new_distance == old_distance && new_index < old_index)
843}