Skip to main content

logo torann

TORoidal Approximate Nearest Neighbours

PyPI - Version PyPI - Python Version GitHub License GitHub Actions Workflow Status GitHub last commit

torann is exact + approximate k-NN and range search on the unit torus $[0,1)^d$ under toroidal L1 — a metric mainstream ANN libraries do not offer, chosen deliberately: L1 degrades more gracefully than L2/cosine in high dimensions, and on the torus the LSH guarantee is exact. Built for ESS-style epoch workloads: static anchors, a moving candidate tier, selective updates, batch promotion.

Features

  • The metric is the contract: toroidal L1, exact distances everywhere — the LSH only filters candidates, never approximates a distance.
  • An LSH family that is exactly L1-sensitive on the torus: randomly rotated integer grids with a closed-form, seam-free collision law (see How it works).
  • ESS-shaped lifecycle: two tiers (anchors + candidates), selective update() that re-places only points whose hash cell changed, promote() as a linear merge — never a re-sort, exact after every step.
  • No brute-force fallback: under-filled queries widen buckets by prefix relaxation (contiguous sorted-key ranges), so k results are structurally guaranteed.
  • Self-tuning: fit(..., k=...) or radius=... derives the hash parameters (B, K, L) from the workload; explicit arguments always win.
  • Three interchangeable implementations of one interface (torann/base.py): exact NumPy brute force, a pure-Python LSH reference, and a Rust core (PyO3 + rayon) that produces byte-identical hash tables at 60–120× the speed. Without the compiled module — or on a CPU below the AVX2 floor — the package still runs, on the reference implementation.
  • No unsafe: the SIMD kernel is wide, a safe stable-Rust wrapper. Hand-written core::arch intrinsics were measured at 21% faster and declined; that trade is deliberate and stays open.

Installation

From PyPI

pip install torann

From source

The project is a maturin mixed Rust/Python package — a Rust toolchain is required to build the native core:

python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install .

Requirements: Python ≥ 3.12, numpy.

Rust ≥ 1.98 is required to build the native core: the distance kernel uses the algebraic floating-point methods stabilized in that release.

CPU: published x86-64 wheels are built with an AVX2 + FMA floor — Intel Haswell (2013) and AMD Excavator (2015) onward. That is not gratuitous: the distance kernel is a plain scalar loop that the compiler auto-vectorizes, and without AVX it has no 256-bit register to lower to, costing 17% (340 ms → 408 ms on the reference shape). AVX-512 is not used: it is a further 4% at d = 32 but nearly 2× worse at d = 8. On a CPU without AVX2 the compiled backend is not loaded and the pure-Python implementation is used instead, with a warning; installing from the sdist builds a native module for whatever the machine has. arm64 needs no floor — NEON is baseline there, and the kernel names no vector type, so it follows the target rather than pinning a width.

Quick Start

import numpy as np
from torann import ToroidalNN

d = 16
static = np.random.rand(15_000, d)      # anchors: never move
batch = np.random.rand(3_000, d)        # candidates: move each epoch

nn = ToroidalNN(seed=42)
nn.fit(static, batch, k=2*d)            # build + tune from zero

for epoch in range(32):
    idx, dist = nn.query()              # each candidate vs everything
    new = force_step(nn.candidates, idx, dist)   # your physics here
    nn.update(new)                      # selective refresh

nn.promote(next_batch)                  # candidates freeze into anchors

nn.query_radius(0.25)                   # range query as a post-filter
nn.query(k=8, queries=Q)                # arbitrary external queries

Knobs (all optional — tuning fills them in): num_tables, resolution, dims_per_table, target_bucket_size, probes, brute_threshold, backend ("auto" prefers the fastest installed of rust, python).

How it works

The metric

On the torus, distance wraps: per dimension it is $\min(|a_i-b_i|,, 1-|a_i-b_i|)$, and the metric is the sum. Near an edge the nearest region of a query is not where a seam-blind index looks — it wraps around every boundary it touches. The teal points are the true 12-NN of the star:

the toroidal nearest region

The region follows the query around the torus:

the nearest region wraps

At d=16, distance concentration makes wrapping the common case: almost every true neighbour pair wraps in at least one dimension, which is why a seam-blind exact search misses ~74 % of the true toroidal neighbours (examples/compare_faiss.py).

The hash

One hashed dimension, with integer resolution $B \ge 2$ and a random offset $u \sim U[0,1)$:

$$c(x) = \lfloor B,((x+u) \bmod 1) \rfloor \qquad P[c(x){=}c(y)] = \max(0,, 1 - B\delta)$$

Because $B$ is an integer, the $B$ arcs tile the circle exactly — the grid has no seam, and the collision law is exact, not approximate (dots are measured frequencies):

the offset integer grid the collision law is exact

A table concatenates $K$ sampled dimensions into a base-$B$ key, so collisions decay as $\prod_j \max(0, 1-B\delta_j) \approx e^{-B \cdot L1}$ — inherently an L1 guarantee, which is why L1 is the public contract and no other metric is offered. A uniformly random pair collides per dimension with probability exactly $1/B$ (closed-form bucket load $n/B^K$), and a point that moves by $s$ changes its cell with probability $B s$ — churn is proportional to movement, which is what makes selective updates cheap. The rejected alternatives (p-stable projections cannot wrap; integer projections alias far points onto near ones) are measured in exploration/.

The index

index representation

Sorted key arrays make a bucket a contiguous range (an $O(1)$ direct-address offset table serves the static tier), keys are digit concatenations so prefix relaxation — dropping low-order digits — widens a bucket into a wider contiguous range without any distance scan, and every gathered candidate is refined with the exact toroidal L1 before the top-k. Full details: torann/lsh.py — the reference implementation, normative for the L1 hash.

Scope: what torann answers, and what it does not

torann answers geometric questions about points on the torus — which points are near which, and how far apart they are. That is the index, and it is also torann.metrics: a metric like toroidal_separation is one exact k-NN scan, so it belongs beside the scan.

from torann import toroidal_separation

toroidal_separation(design)            # maximin separation of a point set
toroidal_separation(batch, anchors)    # ...of a batch added to existing points

torann does not decide what a "good" point set is. Ranking one design against another depends on the purpose the points serve, not on the geometry, so that judgement belongs to the caller. torann/metrics.py documents which competing metrics were measured and how each behaves — including the ones that stop discriminating in high dimension — so a caller can choose with numbers in front of it. It does not choose.

That line is drawn deliberately. When a metric's definition lived in one project and the choice of metric in another, a rename in one silently outlived the other: scripts asked for keys that no longer existed and died in their reporting after completing every run, and one printed a lower-is-better number under a higher-is-better heading for weeks without ever failing. Definitions here, choices in the caller.

Benchmarks

Measured on an AMD Ryzen AI 7 PRO 350 (16 threads), d=16, k=32, on a build carrying the AVX2 floor described under Installation — which is what the published wheels now ship, so these are numbers you get rather than numbers only the developer got. Regenerate the full grids, crossover and complexity tables with python examples/benchmark.py:

These figures predate the Rust 1.98 distance kernel and are now conservative. That rewrite is worth 8–50% on the query path depending on dimensionality, and d = 16 — the dimension charted here — is near the top of that range at ~27%. The plots are regenerated by the command above; the numbers below have not been re-measured since.

queries vs n

On the workload this library exists for — the ESS main loop, simulated end to end (examples/ess_sim.py) — torann is 1.7× faster than FAISS Flat rebuilt per epoch and correct, where FAISS's exact seam-blind L1 delivers 0.25–0.28 recall against the true toroidal neighbours:

the ESS main loop

Queries run 11–148 µs at 16 threads across n ∈ [20k, 1M] on torus data — 60–120× the NumPy pipeline — with 0.9–2.1 ms selective updates and ~1 s builds at n = 1M (HNSW: 24–32 s). On wrap-free data (FAISS's best case) torann sits within 1.06–1.56× of FAISS's exact SIMD Flat scan at equal ≈ 1.0 recall. Python and Rust implementations produce byte-identical hash tables, so their recall is identical by construction.

Running Unit Tests

The conformance suite runs once per installed backend and checks byte-identical tables plus equivalent query results across the whole lifecycle, using the standard unittest framework:

python -m unittest discover -s test

Documentation

The library uses Google-style docstrings; the API documentation is generated with pdoc by a GitHub Action and published here. To preview locally:

pip install pdoc
pdoc --math -d google torann torann.wrapper torann.base torann.brute torann.lsh torann.rust

Development

The checks in CI also run at commit time:

pip install pre-commit
pre-commit install

That gates each commit on ruff, basedpyright, vulture, the unit tests, and -- because the native core is held to the same standard -- cargo fmt --check and cargo clippy --release -- -D warnings. pre-commit run --all-files checks the tree without committing.

python examples/ess_sim.py             # the ESS main loop end to end, vs FAISS
python examples/bench_backends.py      # per-op grid over (n, d, backend)
python examples/crossover.py           # brute vs LSH crossover n*(d, backend)
python examples/compare_faiss_flat.py  # non-toroidal throughput vs FAISS
python examples/figures.py             # regenerate the README method figures
python examples/plot_benchmarks.py     # regenerate the benchmark figures
python exploration/exp_1d.py           # regenerate the concept experiments

maturin build --release produces the complete wheel (Cargo.toml + src/lib.rs are the native core; torann/ is the Python package). The C contender from the phase-6 bake-off is preserved at tag archive/backend-c.

Authors

License

This project is licensed under the MIT License - see the LICENSE file for details.

Download files

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

Source Distribution

torann-0.5.0.tar.gz (2.8 MB view details)

Uploaded Source

Built Distributions

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

torann-0.5.0-cp311-abi3-win_amd64.whl (316.1 kB view details)

Uploaded CPython 3.11+Windows x86-64

torann-0.5.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (433.3 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.17+ x86-64

torann-0.5.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (423.2 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.17+ ARM64

torann-0.5.0-cp311-abi3-macosx_11_0_arm64.whl (387.6 kB view details)

Uploaded CPython 3.11+macOS 11.0+ ARM64

torann-0.5.0-cp311-abi3-macosx_10_12_x86_64.whl (402.1 kB view details)

Uploaded CPython 3.11+macOS 10.12+ x86-64

File details

Details for the file torann-0.5.0.tar.gz.

File metadata

  • Download URL: torann-0.5.0.tar.gz
  • Upload date:
  • Size: 2.8 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for torann-0.5.0.tar.gz
Algorithm Hash digest
SHA256 6de2fec135553c9ad7aa241951ddbacbd5c282e5c0d0f81c129f68a45126d37f
MD5 8c08f6522e6497c78e644007ee82bb46
BLAKE2b-256 cc015e96a53bdc324ed97d74ebdc21776792235338cb901f3555392484cb58fb

See more details on using hashes here.

File details

Details for the file torann-0.5.0-cp311-abi3-win_amd64.whl.

File metadata

  • Download URL: torann-0.5.0-cp311-abi3-win_amd64.whl
  • Upload date:
  • Size: 316.1 kB
  • Tags: CPython 3.11+, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for torann-0.5.0-cp311-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 fe7e449059b0dbe5ef58ec036503bd0498959fd9e1adbb99f2826039f84ca5f5
MD5 803d0ea86a1e7b6ae44c572ad73fee5e
BLAKE2b-256 a8251723c29475ebcce8bd71d6dde36bfe9f0b8ecd1f24fad989604b4aad5b89

See more details on using hashes here.

File details

Details for the file torann-0.5.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for torann-0.5.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 2b781927499d81f5524d308132a7dbd30ba4806f2cfcb70d237c8c513450a404
MD5 d3e33d367a42c96af37b7c5e99653497
BLAKE2b-256 b1a9def6b82aca2b1e8b3af3bfc24b94e858ccf263b7e33ee42f37c532efc864

See more details on using hashes here.

File details

Details for the file torann-0.5.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for torann-0.5.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 6bdf86eec7310e11ad3169457a7e07fcc25cf3291c357f9a8135c099650c7918
MD5 11ab02f2c5d7aefffefccd6c6fef8cca
BLAKE2b-256 39474742235fe6d815cbb422e094eaa778f5bea666390dd9b3247e39dda97357

See more details on using hashes here.

File details

Details for the file torann-0.5.0-cp311-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for torann-0.5.0-cp311-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 0e0248071bbf4b0c76a52207332c6b3a73025a0b31d8aefbb89259d4ad7d8aca
MD5 0df7cb9d16d1a3d2e86e57ac0dfa1eb4
BLAKE2b-256 c414da47f701baed41fa7f2f0ad262da7202e467fd45f3d677a512a30b117068

See more details on using hashes here.

File details

Details for the file torann-0.5.0-cp311-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for torann-0.5.0-cp311-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 524d88f2d3538808a6952c51671263d73b09071e9048c5b8db223d1f9c7fe21b
MD5 2180b86fe516011776feb781ae59cab7
BLAKE2b-256 1f80aecef5c174461a2028b003a47255abef2036fd916bab5db7023d700ddc60

See more details on using hashes here.

Release history Release notifications | RSS feed

0.5.1

6 files

This release

0.5.0 This release

6 files

0.4.0

16 files

0.3.0

16 files

0.2.2

26 files

0.2.1

26 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page