Skip to main content

Ultra-fast 2D and 3D Hilbert space-filling curve encode/decode kernels (NumPy + Numba).

Project description


HilbertSFC

License Documentation PyPI Python versions CI

Ultra-fast 2D & 3D Hilbert space-filling curve encode/decode kernels for Python.

2D Hilbert curves for nbits 1..5 3D Hilbert curves animation grid for nbits 1..4

2D Hilbert curves (nbits 1..5) and 3D Hilbert curves (nbits 1..4, animated).


This project is performance-first and implemented entirely in Python. The hot kernels are JIT-compiled with Numba and tuned for:

  • Branchless, fully unrolled inner loops
  • SIMD via LLVM vector intrinsics
  • Small, L1-cache-friendly lookup tables (LUTs)
  • Reduced dependency chains for better ILP and MLP
  • Optional multi-threading for batch operations

It provides both convenient Python APIs and kernel accessors designed to be embedded into other Numba kernels.

Performance

HilbertSFC is orders of magnitude faster than existing Python implementations. It also outperforms the Fast Hilbert implementation in Rust by a factor of ~7x. In fact, HilbertSFC takes only ~8 CPU cycles per point for 2D encode/decode of 32-bit coordinates.

2D Points - Random, nbits=32, n=5,000,000

Implementation ns/pt (enc) ns/pt (dec) Mpts/s (enc) Mpts/s (dec)
🔥hilbertsfc (multi-threaded) 0.53 0.57 1883.52 1742.08
🔥hilbertsfc (Python) 1.84 1.88 543.60 532.77
fast_hilbert (Rust) 13.71 13.47 72.92 74.23
hilbert_2d (Rust) 121.23 101.34 8.25 9.87
hilbert-bytes (Python) 2997.51 2642.86 0.334 0.378
numpy-hilbert-curve (Python) 7606.88 5075.58 0.131 0.197
hilbertcurve (Python) 14355.76 10411.20 0.0697 0.0961

System info: Intel Core Ultra 7 258v, Ubuntu 24.04.4, Python 3.12.12, Numba 0.63.1

Additional benchmarks and details are available in the benchmark.md.

For a deep dive into how the HilbertSFC kernels are derived and why the implementation maps well to modern CPUs (FSM/LUT formulation, dependency chains, ILP/MLP, unrolling, constant folding, vectorization, gathers), see the performance deep dive notebook.

Quickstart

Installation

With pip:

pip install hilbertsfc

Or with uv:

uv add hilbertsfc

Usage

Hilbert curves map multi-dimensional integer coordinates onto a single scalar index while preserving spatial locality. hilbertsfc provides an encode and decode API for 2D and 3D coordinates that support both scalar values and vectorized array inputs.

The nbits parameter specifies the number of bits per coordinate, defining the grid domain as [0, 2**nbits). If omitted, it's inferred from the input array dtype (for arrays) or defaults to the maximum (32 for 2D, 21 for 3D).

Scalar 2D

Encode a single (x, y) coordinate into a Hilbert index, and decode it back:

from hilbertsfc import hilbert_decode_2d, hilbert_encode_2d

index = hilbert_encode_2d(17, 23, nbits=10)  # index = 534
x, y = hilbert_decode_2d(index, nbits=10)    # x, y = (17, 23)

Batch 2D

The same functions operate elementwise on NumPy arrays, preserving shape and avoiding Python loops:

import numpy as np
from hilbertsfc import hilbert_decode_2d, hilbert_encode_2d

xs = np.arange(1024, dtype=np.uint16)
ys = xs[::-1]

indices = hilbert_encode_2d(xs, ys, nbits=10)    # shape (1024,), dtype uint32
xs2, ys2 = hilbert_decode_2d(indices, nbits=10)  # xs2 = xs, ys2 = ys

This is the preferred use for high-throughput workloads. It can be further accelerated with parallel=True.

Batch 3D

3D works identically, mapping (x, y, z) coordinates to a single Hilbert index:

import numpy as np
from hilbertsfc import hilbert_decode_3d, hilbert_encode_3d

nbits = 10
n = 10_000
rng = np.random.default_rng(0)

xs = rng.integers(0, 2**nbits, size=n, dtype=np.uint32)
ys = rng.integers(0, 2**nbits, size=n, dtype=np.uint32)
zs = rng.integers(0, 2**nbits, size=n, dtype=np.uint32)

indices = hilbert_encode_3d(xs, ys, zs, nbits=nbits)      # shape (10000,), dtype uint32
xs2, ys2, zs2 = hilbert_decode_3d(indices, nbits=nbits)   # xs2 = xs, ys2 = ys, zs2 = zs

This is can be useful for applications like 3D spatial indexing, volumetric data processing, compression, and more.

Embedding kernels in your own Numba code

While the main API is designed for ease of use, the package also provides kernel accessors that expose the scalar encode/decode kernels. This allows you to embed the Hilbert curve logic directly into your own Numba kernels, enabling further optimizations like loop fusion and reduced Python call overhead.

Example embedding the 2D encode kernel:

import numpy as np
import numba as nb

from hilbertsfc import get_hilbert_encode_2d_kernel

encode_2d_10 = get_hilbert_encode_2d_kernel(nbits=10)

@nb.njit
def encode_many(xs: np.ndarray, ys: np.ndarray) -> np.ndarray:
    out = np.empty(xs.shape, dtype=np.uint32)
    for i in range(xs.size):
        out[i] = encode_2d_10(xs[i], ys[i])
    return out

The same pattern works for decode and for 3D kernels.

Demo Notebook

For more examples, see the demo notebook which includes visualizations of the curves and embedding the kernels into custom Numba code.

API notes

  • nbits specifies the number of bits per coordinate. Coordinates must be in [0, 2**nbits). A tighter nbits improves performance and reduces output dtypes. Excess bits are ignored.
  • Hilbert indices obtained with a certain nbits are compatible with those from another nbits, given that the coordinates are within the valid range. This is because the kernels resolve the starting state parity to ensure compatibility.
  • The batched API accepts arbitrary shapes and preserves the input shape. The requirement is that inputs/outputs support a zero-copy 1D view. Most strided views are supported but they can reduce performance since the kernels are close to memory-bandwidth bound.
  • You can pass out=... buffers for batch encode, and out_xs/out_ys/out_zs for batch decode. This can for example be useful to write into memory-mapped arrays or to reuse buffers across multiple calls.
  • parallel=True dispatches the parallel version of the kernel (when available). The number of threads can be controlled with the environment variable NUMBA_NUM_THREADS or during runtime with numba.set_num_threads().

Documentation

Documentation is hosted online. It includes a quick start guide, and API reference.

To serve the docs locally:

uv run --no-dev --group docs mkdocs serve

Build a static site into site/:

uv run --no-dev --group docs mkdocs build

Development

The repo uses uv for environment management. CI and local development workflows (lint, tests, type checking, docs) are automated with nox.

Sync a local environment with dev dependencies:

uv sync

Run the full nox suite:

uvx nox

More details are in CONTRIBUTING.md.

Cache control

If you want to clear cached kernels and lookup tables (e.g., for benchmarking or testing), you can use the clear_all_caches() function:

from hilbertsfc import clear_all_caches

clear_all_caches()

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hilbertsfc-0.1.0.tar.gz (27.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hilbertsfc-0.1.0-py3-none-any.whl (37.3 kB view details)

Uploaded Python 3

File details

Details for the file hilbertsfc-0.1.0.tar.gz.

File metadata

  • Download URL: hilbertsfc-0.1.0.tar.gz
  • Upload date:
  • Size: 27.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for hilbertsfc-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f62749b4fa1da2a2cc44d95cdfe8bba07d6c3c925a3cc8bd4f09756fc0e8f249
MD5 d4299739df163d325f49a3a9753efacc
BLAKE2b-256 bfe47c59cc2c47d55194d5fd2ca2c2e4da37bdb33441ef7bb8d1ffd7ad42ea11

See more details on using hashes here.

File details

Details for the file hilbertsfc-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: hilbertsfc-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 37.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for hilbertsfc-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 364f4e6b98308e1a6b716116ffff7f1ddb5657afe0d53c5167a6b00021f621c7
MD5 a056dc8b089fd30ff95f16ec3c0e95e6
BLAKE2b-256 3ad0c97ab9897f8ba980873f23f0e9bd9a2a62c90929b41c2a7f474510f4ac86

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page