Expand description
Approximate all-nearest-neighbour search over neighbourhood graphs.
Given two 3D point clouds, each equipped with a neighbourhood graph in CSR
form (e.g. the vertex adjacency of a Delaunay triangulation, see
graph_from_simplices), this crate finds for every point of the query
cloud its (approximate) nearest neighbour(s) in the target cloud via a
warm-started greedy graph descent. Distances are SIMD-accelerated via the
wide crate, so the crate builds on stable Rust.
The Python bindings live behind the non-default python cargo feature;
with default features this is a pure-Rust library.
use aann::{graph_from_simplices, PreparedF64};
use aann::ndarray::array;
// Target cloud: the 4 corners of the unit tetrahedron, fully connected
// by a single Delaunay simplex.
let points = array![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let (indptr, indices) = graph_from_simplices(array![[0u64, 1, 2, 3]].view(), 4);
let target = PreparedF64::new(points.view(), indptr.view(), indices.view());
// Query cloud with its own neighbourhood graph (here: two points linked
// to each other).
let queries = array![[0.1, 0.0, 0.0], [0.9, 0.1, 0.0]];
let (qptr, qidx) = (array![0u32, 1, 2], array![1u32, 0]);
let (dists, idxs) = target.query(queries.view(), qptr.view(), qidx.view(), None);
assert_eq!(idxs.to_vec(), vec![0, 1]);
assert!((dists[0] - 0.1).abs() < 1e-12);Re-exports§
pub use ndarray;
Structs§
- Grouped
Queries F32 - A reusable, Morton-sorted concatenation of many query clouds’ points,
built ONCE for a whole all-by-all and descended against each target with
$prepared::query_grouped. The concat + Morton sort are target- independent, so hoisting them here (instead of redoing them per target) is what lets the grouped descent keep its ~2x single-thread / ~4x multi- thread gain rather than spending ~half its time re-sorting per target. - Grouped
Queries F64 - A reusable, Morton-sorted concatenation of many query clouds’ points,
built ONCE for a whole all-by-all and descended against each target with
$prepared::query_grouped. The concat + Morton sort are target- independent, so hoisting them here (instead of redoing them per target) is what lets the grouped descent keep its ~2x single-thread / ~4x multi- thread gain rather than spending ~half its time re-sorting per target. - Neighborhood
F32 - A neighborhood graph over
$tcoordinates. - Neighborhood
F64 - A neighborhood graph over
$tcoordinates. - Prepared
F32 - A prepared neighbourhood graph that owns its SIMD-packed points
and CSR adjacency, so the packing is done once at construction and
reused across every
query. This is the persistent counterpart to the per-call$search: buildinglet tree = $prepared::new(...)once and callingtree.query(...)many times (e.g. one target vs many query clouds, or an all-by-all) avoids repacking the target on each call. - Prepared
F64 - A prepared neighbourhood graph that owns its SIMD-packed points
and CSR adjacency, so the packing is done once at construction and
reused across every
query. This is the persistent counterpart to the per-call$search: buildinglet tree = $prepared::new(...)once and callingtree.query(...)many times (e.g. one target vs many query clouds, or an all-by-all) avoids repacking the target on each call. - Workspace
- Reusable per-thread scratch for the k=1 all-nearest-neighbour descent
(
search_into_f64/search_into_f32and thequery_*_intomethods).
Functions§
- box_
box_ dist2_ f32 - Squared euclidean distance between two axis-aligned boxes (0 when they
overlap). A lower bound on the distance between any point of box A
and any point of box B, so
$box_box_dist2(..) > ub²proves no pair is withinub– lets a whole separated query cloud be skipped in one test. - box_
box_ dist2_ f64 - Squared euclidean distance between two axis-aligned boxes (0 when they
overlap). A lower bound on the distance between any point of box A
and any point of box B, so
$box_box_dist2(..) > ub²proves no pair is withinub– lets a whole separated query cloud be skipped in one test. - box_
dist2_ f32 - Squared euclidean distance from packed point
pto the axis-aligned box[bmin, bmax](0 whenplies inside). Because the box contains every target point, this is a lower bound onp’s distance to any of them, so$box_dist2(..) > ub²proves no target lies withinub– the geometric hook the greedy descent otherwise lacks. The packed pad lane is 0 inp,bminandbmax, so it contributes nothing. - box_
dist2_ f64 - Squared euclidean distance from packed point
pto the axis-aligned box[bmin, bmax](0 whenplies inside). Because the box contains every target point, this is a lower bound onp’s distance to any of them, so$box_dist2(..) > ub²proves no target lies withinub– the geometric hook the greedy descent otherwise lacks. The packed pad lane is 0 inp,bminandbmax, so it contributes nothing. - euclidean_
distance_ f32 - Squared euclidean distance between two packed points.
- euclidean_
distance_ f64 - Squared euclidean distance between two packed points.
- find_
nn_ f32 - Approximate nearest neighbour of
pamong the points iny, via a greedy descent that starts atstart. The descent moves to a strictly-closer node, or – on an exact distance tie – to the one with the lower vertex id. Without that tie-break the incumbent always wins, so which of several equidistant points is returned depends on the start vertex, and the start vertex depends on how the caller batched its queries (seemorton_perm). Ties are common on grid-quantised clouds (e.g. resampled skeletons), where the unstable index picks a different tangent vector downstream. - find_
nn_ f64 - Approximate nearest neighbour of
pamong the points iny, via a greedy descent that starts atstart. The descent moves to a strictly-closer node, or – on an exact distance tie – to the one with the lower vertex id. Without that tie-break the incumbent always wins, so which of several equidistant points is returned depends on the start vertex, and the start vertex depends on how the caller batched its queries (seemorton_perm). Ties are common on grid-quantised clouds (e.g. resampled skeletons), where the unstable index picks a different tangent vector downstream. - graph_
from_ simplices - Build a vertex-adjacency CSR graph from Delaunay tetrahedra.
- morton_
perm - Argsort points into Morton (Z-order) so spatially-near points are adjacent.
Returns
permwithperm[k]= original index of the k-th point in Z-order. Used by the per-target grouped descent to interleave many query clouds so co-located points (from different clouds) share a block – and thus one gather. - pack_
points_ f32 - Pack an
(N, 3)point array into SIMD[x, y, z, 0.0]lanes. - pack_
points_ f64 - Pack an
(N, 3)point array into SIMD[x, y, z, 0.0]lanes. - search_
blocked_ f32 - PROTOTYPE – blocked/batched k=1 all-nearest-neighbour descent.
- search_
blocked_ f64 - PROTOTYPE – blocked/batched k=1 all-nearest-neighbour descent.
- search_
f32 - The full all-nearest-neighbours search over two graphs: for each
point in
x, find its nearest neighbour among the points iny. Allocates the output arrays and scratch fresh; for an all-by-all where buffers can be recycled, prefer$search_into/$prepared::query_prepared_into. - search_
f64 - The full all-nearest-neighbours search over two graphs: for each
point in
x, find its nearest neighbour among the points iny. Allocates the output arrays and scratch fresh; for an all-by-all where buffers can be recycled, prefer$search_into/$prepared::query_prepared_into. - search_
into_ f32 - Buffer-reuse form of
$search: writes each query point’s nearest neighbour into the caller-owneddists/idx(both resized to the query cloud size) and recycles the DFS scratch held inws. With a warmwsand warm output buffers this performs no heap allocation – the hot path for an all-by-all inner loop (see$prepared::query_prepared_into). Behaviour is identical to$search; only the buffers’ provenance differs. - search_
into_ f64 - Buffer-reuse form of
$search: writes each query point’s nearest neighbour into the caller-owneddists/idx(both resized to the query cloud size) and recycles the DFS scratch held inws. With a warmwsand warm output buffers this performs no heap allocation – the hot path for an all-by-all inner loop (see$prepared::query_prepared_into). Behaviour is identical to$search; only the buffers’ provenance differs. - search_
k_ f32 - The k>1 all-nearest-neighbours search: the same DFS walk and
warm-start chaining as the k=1
$search, but each query runs the best-first$findkand is seeded with the previous query’s best hit. Rows are (inf, |y|)-padded when the search exhausts with fewer thankresults (disconnected graph or k > |y|); the outer loop restarts the walk on every unvisitedxvertex, so disconnected query graphs are covered too. - search_
k_ f64 - The k>1 all-nearest-neighbours search: the same DFS walk and
warm-start chaining as the k=1
$search, but each query runs the best-first$findkand is seeded with the previous query’s best hit. Rows are (inf, |y|)-padded when the search exhausts with fewer thankresults (disconnected graph or k > |y|); the outer loop restarts the walk on every unvisitedxvertex, so disconnected query graphs are covered too. - search_
pruned_ f32 - Bound-aware
$search:prune = Some((target_min, target_max, ub, query_box?))reports the miss marker (inf, |y|) for every query point with noyneighbour withinub, using the target box to skip the descent where it provably can’t help (see$search_into/$resolve_prune).Noneis plain$search. - search_
pruned_ f64 - Bound-aware
$search:prune = Some((target_min, target_max, ub, query_box?))reports the miss marker (inf, |y|) for every query point with noyneighbour withinub, using the target box to skip the descent where it provably can’t help (see$search_into/$resolve_prune).Noneis plain$search.