Repository navigation
Math Spatial Hashing
How do you find the neighbours of a point among ten thousand others without comparing it with all ten
thousand? Cut space into equal cells, file each point under its cell, and look only in the cells the question
can reach. This page works out the cost of that idea (how many cells, how many distance tests, what cell size
to choose), proves that the shell search for the SpatialHash3::nearest is exact, says
where it degrades, and compares it with trees. Every operation count in it is a count, not a timing, and the
counts were obtained two ways: by instrumenting a copy of the shipped Rust and by an independent Python port
that gave identical numbers.
Distance tests (solid) and cell lookups (dashed) per query against cell size. Left: radius queries of 3 units in 4 000 uniform points. Middle: the same radius in 4 000 points in six Gaussian clumps. Right: the 22 nearest of 8 000 flock points in 3-D. Brute force is the horizontal line. The model curves of section 2 sit on the measured points.
The idea of binning points into equal cells is older than the word "hash" in this sense. Molecular dynamics met the problem first: every one of
Hashing the cell, rather than allocating a dense array of them, belongs to computer graphics. Matthias Teschner, Bruno Heidelberger, Matthias Mueller, Danat Pomeranets and Markus Gross, at ETH Zurich, presented "Optimized spatial hashing for collision detection of deformable objects" at Vision, Modeling, and Visualization 2003 in Munich (proceedings pp. 47-54). They compressed a potentially infinite regular grid into a finite table with a hash function that does not map cells uniquely, and reported real-time collision and self-collision detection for scenes of up to 20 000 tetrahedra. Their hash multiplies the three integer cell coordinates by the primes 73 856 093, 19 349 663 and 83 492 791 and takes the exclusive-or; the three numbers have since been copied into so many codebases that they behave as folk constants. Ericson's Real-Time Collision Detection (2005) put grids and hashed grids into the standard textbook form.
isotop does not use that hash. It keys cells on the exact integer triple, so distant cells never share a key (section 5). The k-nearest shell search of section 3 is a plain expanding-ring search with an exactness proof, not a result from this literature.
Given
-
radius query: all
$i$ with$\lVert p_i-x\rVert<r$ ; -
$k$ -nearest: the$k$ indices with the smallest$\lVert p_i-x\rVert$ .
Brute force is
Choose a cell width
-
Build: one sort of
$n$ indices by(cell key, index),$O(n\log n)$ (a counting sort over a bounded domain would give$O(n)$ ; the code sorts, so that a cell lists its points in ascending index order, which makes results deterministic). The map is then filled in one pass over the runs of equal keys. -
Rebuild each step: the same buffers are reused, so a steady point count allocates nothing (test
rebuild_at_a_steady_point_count_keeps_its_buffers). There is no incremental update: a moving population is simply rebuilt.
A radius query for
When
With uniform density
of which the true neighbours are
Cost has two parts that pull in opposite directions: lookups are cheap in operations but cost a hash probe
(weight
Setting
So the best cell is $s^=r,(2a/bm)^{1/(d+1)}$: finer when the data are denser ($m$ large) and coarser when lookups are dear. It is a weak dependence (the $d+1$-th root), so a cell width of the order of the query radius is within a small factor of optimal over a wide range of densities, and the cost near the optimum is flat. With $a=b$, $d=2$ and $\rho=0.4$, $r=3$ ($m=3.6$): $u^=(2/3.6)^{1/3}=0.82$.
Counts per query for Rng::new(7) (SpatialHash (instrumented copy of simulation.rs) and my independent port:
| lookups, model |
lookups, measured | tests, model |
tests, measured | hits | ||
|---|---|---|---|---|---|---|
| 0.3 | 0.1 | 441 | 441.00 | 15.9 | 16.50 | 12.12 |
| 0.75 | 0.25 | 81 | 81.00 | 18.3 | 18.75 | 12.12 |
| 1.5 | 0.5 | 25 | 25.00 | 22.5 | 22.79 | 12.12 |
| 3 | 1 | 9 | 9.00 | 32.4 | 32.00 | 12.12 |
| 6 | 2 | 4 | 4.00 | 57.6 | 55.23 | 12.12 |
| 12 | 4 | 2.25 | 2.33 | 129.6 | 122.19 | 12.12 |
| 24 | 8 | 1.56 | 1.65 | 360 | 333.00 | 12.12 |
Brute force is 4 000 tests per query. At SpatialHash::new(NEIGHBOURHOOD) with NEIGHBOURHOOD = 4.0, which is
A SpatialHash3::nearest(point, k, out) finds one by visiting
shells of cells around the query cell
Let
be the distance from
Claim. After examining all points in
Proof sketch. A point not in
Termination, and the cost bound. Because the ball of radius
visiting at most
Fallback. If the cube of the next shell would hold more cells than there are points
(
Within a shell most cells are far from visit_shell computes, for a cell offset
f32 the code widens every comparison
by
The flock is the one of the unit test timing_nearest_22_of_8000_flock_points_and_fft_1024: 11 Gaussian clumps
(centres Rng::new(51). Per
query, over all 8 000 queries (each query point is itself stored, so the query is "the point plus its 21
nearest"):
| cell |
distance tests | cell lookups | tests + lookups |
|---|---|---|---|
| 2.5 | 79.27 | 318.69 | 398.0 |
| 4 | 78.66 | 102.44 | 181.1 |
| 5 | 107.61 | 62.87 | 170.5 |
| 6.5 (the test's choice) | 190.32 | 40.79 | 231.1 |
| 8 | 265.85 | 30.22 | 296.1 |
| 10 | 327.94 | 21.29 | 349.2 |
| 13 | 421.80 | 13.83 | 435.6 |
| 16 | 534.95 | 9.67 | 544.6 |
| 24 | 961.79 | 5.43 | 967.2 |
Brute force would be 8 000. At 6.5 the grid does a forty-second of the distance work. All nine rows return the
same neighbours (the checksum of the last-neighbour index over the 8 000 queries is identical,
Two lessons. First, equal weights put the optimum at 5; a lookup is a hash probe and a test is three
multiplies, so the true optimum sits to the right of this table's, where the author put it. Second, the
per-query cost is heavy-tailed: at
-
Coincident points. All
$n$ points in one cell: a query visits one cell and tests all$n$ (measured: 1 lookup, 2 000 tests for 2 000 coincident points). It is brute force plus one probe. For radius queries the work is output-sensitive (all$n$ really are neighbours); for$k$ -nearest it is the worst case. The testspatial_hash_handles_empty_and_coincident_pointscovers correctness, and the nearest test with 30 coincident points checks that the index breaks the tie. -
Clustered data. Density varies by orders of magnitude, so any single
$s$ is wrong somewhere: too coarse in the core (many tests per hit), too fine in the halo (many empty lookups). The middle panel of the figure: six Gaussian clumps,$\sigma=4$ , radius 3, give 111 hits per query (against 12 for uniform); tests + lookups is smallest at$s=1.5$ ($u=0.5$ : 205 + 25 = 230) rather than at$s=3$ (276 + 9 = 285). This is the$u^*\propto m^{-1/(d+1)}$ of section 2.3 with a larger effective$m$ . -
Query radius much larger than the cell. The visited box has
$(2r/s+1)^d$ cells, which can exceed$n$ ;neighboursthen scans all points (cells > points.len()), so the cost is$\min(\text{cells},n)$ -ish. The product of spans can overflow, so it is computed saturating ini64. -
Extreme or non-finite coordinates. Cell coordinates are
i32, produced by a float-to-int cast that saturates and maps NaN to 0, so every input yields a cell.SpatialHash3additionally recordsgridded = falseif any point is non-finite or has a cell coordinate beyond$\pm2^{30}$ (CELL_LIMIT). A grid withgridded = false, a query point with a non-finite coordinate, or a query cell beyond the limit answers by scanning every point, which is correct and slow (testsnearest_scans_every_point_for_extreme_or_non_finite_coordinates,a_radius_beyond_the_cell_range_scans_every_point_without_overflowing). -
Flat data. If all
$z$ are equal the 3-D grid is a slab: occupied cells are a 2-D set, but the shell loops still enumerate all$(2R+1)^3$ offsets; the per-cell pruning skips most of them by their$z$ gap, at the price of one comparison each. -
Radius query in
f32. The proof of a radius query is two lines in real arithmetic: a point at distance$<r$ has every coordinate within$r$ of$x$ , so its cell index is between the cells of$x\pm r$ by monotonicity of$\lfloor\cdot\rfloor$ . Inf32the bounds$x\pm r$ are rounded, so a point whose distance rounds to just under$r$ in the test could sit one ulp outside the rounded box.neighboursdoes not widen its box (the shell search does, as above); the tests compare with brute force on seeded sets and find no difference, and the band is of relative width$10^{-7}$ .
| build | radius query |
|
adapts to density | updates | |
|---|---|---|---|---|---|
| uniform grid (here) |
|
|
no: one scale | rebuild | |
|
|
|
|
yes: splits at the median | rebuild or lazy | |
| octree / quadtree (Meagher 1982) |
|
like |
yes: subdivides where dense | local, cheap | |
| BVH / R-tree | similar | yes | refit | ||
| hierarchical grids | one grid per scale | per scale | partly | rebuild |
A grid wins when (a) the query scale is known and roughly uniform (BIRD_CAP)) sit squarely in
the grid's regime. The HashMap from cell to range (instead of a dense 3-D array) is what lets the domain be
unbounded: only occupied cells exist, and a key is the cell's integer triple (SpatialHash packs two i32
into one u64; SpatialHash3 keys on [i32; 3] with its own multiply-and-fold hasher, because the default
SipHash "cost a k-nearest query about as much as the distances it computes"). For the same reason a Gaussian flock
never needs the empty space between clumps, which a dense grid would allocate.
Teschner et al. (2003) analyse the same structure as a hash table of cells for collision detection (they hash cell coordinates into a table smaller than the domain and accept collisions between distant cells); isotop avoids false sharing by keying on the exact cell triple instead.
-
SpatialHash(2-D, radius only) has one production user: Coop's Vicsek flocks (SpatialHash::new(NEIGHBOURHOOD),NEIGHBOURHOOD = 4.0world units, rebuilt per fixed step; see Flocking). -
SpatialHash3(3-D, radius and exact$k$ -nearest) has one production user, the Murmuration view (src/murmuration.rs):Flockcallsnearestonce per bird per fixed step, asking forFAR_NEIGHBOURS + 1= 22 points (the bird and its 21 nearest), and takes the seven nearest of them for alignment. ItsGrid::adaptrebuilds the hash with a cell as wide as the mean distance of a bird to its 21st neighbour whenever that is more than a quarter off the cell it has, clamped toCELL_RANGE = (1.0, 8.0)from a start ofCELL = 2.0; the sizing rule of section 3.1 is therefore applied at run time. The 3-D radius queryneighboursis still used only by tests. - Results are deterministic (sorted by index, or by distance then index) and independent of hash order, which keeps the frames reproducible (Determinism).
- The measured timing (not a count): the ignored test asserts that 22-nearest queries for 8 000 points take under 10 ms in release.
The counts above were produced twice. (1) A copy of src/simulation.rs outside the repository, with atomic
counters added to the distance and lookup sites, driven by a small program that reads a point file; (2) a
Python port of the algorithm (docs/figures/hashing-op-counts.py carries the version that regenerates the
figure; it contains the port of Rng and of both queries). On the same point files the two agree to the printed
digits in every row of the tables of sections 2.4 and 3.3. To regenerate the figure:
python3 docs/figures/hashing-op-counts.py-
Adaptive cell width, beyond the mean. Murmuration already adapts
$s$ to the mean 21st-neighbour distance (Grid::adapt). The heavy tail of section 3.3 argues for a two-level grid (coarse cells, subdivided where occupied beyond a threshold). -
Counting sort. Replace the
$O(n\log n)$ sort by a two-pass counting sort over occupied cells. The determinism requirement (ascending index inside a cell) is met by a stable scatter. - Morton order. Sort by the interleaved bits of the cell coordinates so that spatially near cells are near in memory; the shell loops then stream.
-
Exact count of the sphere-pruned cells. Derive the expected number of cells that survive the
squared_gaptest as a function of$s/d_k$ and compare with the 40.8 lookups at 6.5. -
A radius-widening
neighbours. Mirror the shell search's$10^{-6}$ slack in the radius box so that the two APIs share one rounding guarantee.
-
src/simulation.rs:SpatialHash(new,rebuild,neighbours),SpatialHash3(rebuild,neighbours,nearest,visit_shell,face_distance,width),Cell,CellHasher,CELL_LIMIT,rank,closer; the testsspatial_hash_finds_exactly_the_points_brute_force_finds,nearest_finds_exactly_what_brute_force_finds,neighbours_in_3d_finds_exactly_what_brute_force_finds,timing_nearest_22_of_8000_flock_points_and_fft_1024. -
src/coop/sim.rs: the one user ofSpatialHash. -
src/murmuration.rs: the one user ofSpatialHash3(Flock,Grid::adapt). -
docs/mockups/code/murmuration.py: the flock the tests are scaled from.
- Bentley, J. L. 1975. "Multidimensional binary search trees used for associative searching." Communications of the ACM 18(9): 509-517.
- Bentley, J. L. and Friedman, J. H. 1979. "Data structures for range searching." ACM Computing Surveys 11(4): 397-409.
- Friedman, J. H., Bentley, J. L. and Finkel, R. A. 1977. "An algorithm for finding best matches in logarithmic expected time." ACM Transactions on Mathematical Software 3(3): 209-226.
- Meagher, D. 1982. "Geometric modeling using octree encoding." Computer Graphics and Image Processing 19(2): 129-147.
- Teschner, M., Heidelberger, B., Mueller, M., Pomeranets, D. and Gross, M. 2003. "Optimized spatial hashing for collision detection of deformable objects." Vision, Modeling, and Visualization (VMV) 2003.
- Verlet, L. 1967. "Computer 'experiments' on classical fluids. I. Thermodynamical properties of Lennard-Jones molecules." Physical Review 159(1): 98-103.
- Ericson, C. 2005. Real-Time Collision Detection. Morgan Kaufmann.
- Samet, H. 2006. Foundations of Multidimensional and Metric Data Structures. Morgan Kaufmann.
isotop — maximalism for its own sake. Source: tfpickard/isotop. This wiki is published from wiki/ in the repository; edit it there. This footer is decorative.
Philosophy · Gallery · Glossary
Theory
Overview
Sampling: Linux
Sampling: macOS
Background sampler
Model and rates
Life events
History
Journal
Scene and display list
Camera and projection
Rasterization
GPU and parity
Picking
Terminal graphics
Stable layout
Determinism
Simulation toolkit
Colour
Text safety
Mathematics
Fourier epicycles
Wave equation
Flocking
Spatial hashing
Stellar photometry
Lorentz and Schlegel
Reaction-diffusion
Spiking neurons
Schwarzschild
Views
City · Orbit · Ripple
Flow · Cores · Cells
Strata · Globe · Reef
Coop · Matrix
Ptolemy · Tesseract
Necropolis · Cortex
Horizon · Murmuration
Arbor · HR
Hive · Switchboard
Armorial · Lights
Planned
Mold · Chamber
Anthill · Plumbing
Morph · Ouija
Canyon · Die
Guides
Writing a view
Recipes
Headless renders
Contributing to the wiki