Reorder an unstructured point cloud into a regular grid, so that neighbor-based operations become simple grid processing.
import gridpoints as grid
points = your_dataset_of_points(1000000, 3) # standard (N, D) array
order = grid.argsort(points, gridshape=(100, 100, 100)) # set gridshape: (I, J, K, ...) = (100, 100, 100)
Pgrid = points[order].reshape(100, 100, 100, 3) # now an (I, J, K, ..., D) version of your dataset
# Ready for convolution, neighbor look-ups or geometrical analysis.
# Note that grid.argsort also supports a batch mode (B, N, D) -> (B, I, J, K, ..., D)
Gridpoints supports NumPy, PyTorch and CuPy arrays.
The argsort() reordering is a one-to-one assignment of your points to the cells of a grid with the given gridshape. This is related to Monge optimal transport, but argsort() trades optimality for scalability, by combining greedy axial operations which exploit the specific structure of grids. One million 2D/3D points are sorted in about 0.5 s on a GPU (PyTorch) or 10 s on a CPU (NumPy).

[P1, ..., PN]. Finding the neighbors of a point usually needs a tree or a graph (KD-tree, radius graph…), which is hard to use with standard tensor-based machine learning, especially on GPU.points -> grid. To recover the result on the points, one just applies the inverse assignment: grid -> points.Points are almost always stored in a one-dimensional order. You would like points that are close in space to also be close in memory, so that the nearest neighbor of P[n] is often P[n±1], P[n±2], or a bit further. This is a well-studied problem, and space-filling curves such as the Morton curve are a common solution.
Gridpoints takes a different route. Instead of a 1D order, it gives each point a multi-index [i, j, ...] in a D-dimensional grid. This matters because a grid supports stencil operations (“look at the cells within a few steps in each direction”, [i±di, j±dj, k±dk]).
Input: a point cloud P of shape (N, D) and a target grid shape (I, J, ...).
Output: a permutation order of the indices such that
Pgrid = P[order].reshape(I, J, ..., D)
is a bijection between points and grid cells (one point, one cell), and is sorted along every axis. In 3D, with xyz notation P = (x, y, z):
x[i+1, j, k] >= x[i, j, k] # x increases along axis 0
y[i, j+1, k] >= y[i, j, k] # y increases along axis 1
z[i, j, k+1] >= z[i, j, k] # z increases along axis 2
This property (grid monotonicity) is the only thing guaranteed. See Neighborhood radius for what it does and does not imply.
On Pgrid, a neighbor query becomes a stencil lookup:
neighborhood(Pgrid[i, j, k]) ≈ { Pgrid[i±di, j±dj, k±dk] : (di, dj, dk) within radius R }
where R is a cutoff you choose for your task. Operations built on this run in O(N) on the grid view.
To get the main ideas of the sorting algorithm, the core of gridpoints is:
For interested readers, the full sorting procedure and motivation behind is explained in more details here.
pip install gridpoints # core only
pip install gridpoints[demo] # adds what the demo notebook needs
Full demo: notebook.ipynb
import gridpoints as grid
import numpy as np
N = 1_000_000
A = np.random.rand(N, 3) # raw point cloud (NumPy, PyTorch or CuPy)
# 1. Compute the permutation and view the points as a grid
order = grid.argsort(A, gridshape=(100, 100, 100))
Bflat = A[order] # sorted, flat (C-order)
Bgrid = Bflat.reshape(100, 100, 100, 3) # grid view
# 2. Work on the grid
Cgrid = apply_something(Bgrid) # e.g. a convolution; shape (100, 100, 100, *)
# 3. Go back to the original point order
Cflat = Cgrid.reshape(N, -1)
orderinv = grid.invert_permutation(order)
C = Cflat[orderinv] # back to the original point indexing
apply_something stands for whatever you want to compute on the grid.
gridpoints.sort() is also available.
The grid shape must be roughly tuned to the kind of data, with the constraint that the number of points N has to be factorized as I⋅J⋅K⋅…
(18, 20, 16) and (16, 15, 24) behave similarly.(18, 20, 16) and (36, 40, 4) can give substantially different results.(128, 128, 2). A naive (32, 32, 32) grid on the same data produces a poor representation of the geometry.Padding: if the number of cells I⋅J⋅K⋅… exceeds the number of points N, the point cloud can be padded with special values (see Padding). This lets you sort a prime or variable number of points, and removes the integer-factorization constraint on the grid shape.
There is no theoretical guarantee on the radius R needed for a given task. Nearest neighbors are not guaranteed to land in exactly adjacent cells.
Empirically, in 2D, R = 5 captures about 99% of nearest neighbors. Some outliers might sit further away, mostly on complex geometries with sharp peaks, holes or other non-smooth features.
If you need stricter neighborhoods, or work in higher dimension, there are two options:
NaN and Inf values are supported in a consistent way by the package, so that padding is natural:
NaN → placed at a random position±Inf → placed on the border of the gridThis lets you complete the N real points with ΔN fictitious points so that N + ΔN = I⋅J⋅K⋅… , using gridpoints.pad
Sorting 1 million points (2D/3D), measured on Google Colab:
| Backend | Time |
|---|---|
| NumPy (CPU) | ~10 s |
| PyTorch (T4 GPU) | ~0.5 s |
| CuPy (T4 GPU) | ~1 s (the first, cold call takes ~10 s because of compilation) |
A typical use looks like:
output(i, j, k) = f( Pgrid[i±di, j±dj, k±dk] ) for (di, dj, dk) in a local window
To compute this kind of kernel efficiently, avoid pure Python loops, which are slow. Prefer:
pystencils or taichi for complex or non-linear kernels.triton kernel can be particularly efficient.Good fit
Poor fit
Try it if you process 2D/3D point clouds with local operations and you want regular array layouts (especially on GPU). Approximate neighborhoods work out of the box, and exact ones are available through tiling.
Stay with a KD-tree or a graph method if you work in high dimension or have only a few hundred points. If you need exact nearest neighbors, Gridpoints can still work, via tiling, but expect some extra implementation effort.
A quick way to decide is to run the demo notebook on a sample of your own data and check how much of your true nearest neighbors fall within your radius R.