Spatial algorithms: KDTree, distances and Delaunay
Answer nearest-neighbour queries quickly, compute distance matrices without loops, triangulate point clouds, and align two sets of vectors.
Nearest neighbours with cKDTree
import numpy as np
from scipy.spatial import cKDTree, distance
rng = np.random.default_rng(0)
points = rng.random((20_000, 3))
queries = rng.random((500, 3))
tree = cKDTree(points) # build once, query many times
d, idx = tree.query(queries, k=5) # 5 nearest per query
print(d.shape, idx.shape) # (500, 5) for both
within = tree.query_ball_point(queries[0], r=0.1)
counts = tree.query_ball_tree(cKDTree(queries), r=0.05)
# pairs closer than a threshold without building a full matrix
pairs = tree.query_pairs(r=0.02, output_type="ndarray")
print(within[:5], len(pairs))- Building the tree costs O(n log n) and pays off as soon as you run more than a handful of queries.
querywithk=1is the common case;workers=-1uses all cores and can halve the wall time.- Set
distance_upper_boundto skip distant matches and speed up large queries. - For a periodic domain, use the minimum-image convention on the distances or replicating points; a plain KDTree does not wrap around edges.
Distance matrices
a = rng.random((1000, 4))
b = rng.random((800, 4))
compact = distance.pdist(a, metric="euclidean") # 1D, n*(n-1)/2 entries
square = distance.squareform(compact) # (1000, 1000) dense
cross = distance.cdist(a, b, metric="cosine") # (1000, 800)
print(compact.shape, square.shape, cross.shape)⚠️
A dense distance matrix grows quadratically: 100,000 points means 80 GB as float64. For anything large, keep the compact form from
cdist and pdist or query a tree rather than materialising the full matrix.Triangulations and alignment
from scipy.spatial import Delaunay, Voronoi, ConvexHull, Rotation, procrustes
pts = np.random.default_rng(1).random((60, 2))
tri = Delaunay(pts)
print(tri.simplices.shape) # triangles as index triples
mask = tri.find_simplex(np.array([[0.3, 0.4], [0.9, 0.1]]))
print(mask)
hull = ConvexHull(pts)
print(hull.volume, hull.area if hasattr(hull, "area") else hull.area)
# rigid alignment of two sets of vectors (Kabsch algorithm)
A = np.random.default_rng(2).random((10, 3))
B = A @ np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]], dtype=float) + 0.1
rot, rssd = Rotation.align_vectors(A, B)
print(rot.as_matrix().round(3), rssd)
result = procrustes(A, B) # rotation, scale and translation
print(result.scale)These are the primitives behind collision detection, mesh generation, geographic clustering and molecular alignment. Each takes a point array of shape (n_points, n_dimensions) and returns indices into it, so your original data stays the source of truth.
FAQ
How do I find the nearest neighbour inside a specific radius only?
Use
query_ball_point for a list of candidates, or query with distance_upper_bound and set k to the most neighbours you would ever want. Both avoid scanning the whole set.Can I update a KDTree after building it?
Not in place. Rebuild the tree, or collect the new points and rebuild once. Inserting a point at a time into a fresh tree is O(n log n) each time, so batch your updates.
Related
Image processing with scipy.ndimage Linear algebra with scipy.linalg
Last refreshed 2026-09-18.