gridpoints

Gridpoints

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).


TL;DR


The problem

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]).

What Gridpoints does

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.


Internal algorithm

To get the main ideas of the sorting algorithm, the core of gridpoints is:

  1. initialisation: a good starting assignement is important both for quick convergence, and to converge on an assignement with good neighborhood properties. The initialiser build a kdtree on the points, and convert the tree cuts to a grid structure
  2. iterative 1d sorts: once the initialiser built the starting assignement, gridpoints further optimise it iteratively by looking at 1d slices of the grid (axial slices, but also diagonal ones), and apply standard 1d sort on the points, projected along the direction of the slice.

For interested readers, the full sorting procedure and motivation behind is explained in more details here.

Installation

pip install gridpoints          # core only
pip install gridpoints[demo]    # adds what the demo notebook needs

Full demo: notebook.ipynb

Quickstart

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.


Choosing the parameters

Grid shape

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⋅…

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.

Neighborhood radius

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:

Padding with NaN and Inf

NaN and Inf values are supported in a consistent way by the package, so that padding is natural:

This lets you complete the N real points with ΔN fictitious points so that N + ΔN = I⋅J⋅K⋅… , using gridpoints.pad


Performance

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)

Running stencil operations efficiently

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:


Limits

Good fit

Poor fit

Should you use it?

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.