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.1.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.1-cp311-abi3-win_amd64.whl (316.6 kB view details)

Uploaded CPython 3.11+Windows x86-64

torann-0.5.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (433.9 kB view details)

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

torann-0.5.1-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (423.9 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.17+ ARM64

torann-0.5.1-cp311-abi3-macosx_11_0_arm64.whl (388.1 kB view details)

Uploaded CPython 3.11+macOS 11.0+ ARM64

torann-0.5.1-cp311-abi3-macosx_10_12_x86_64.whl (402.7 kB view details)

Uploaded CPython 3.11+macOS 10.12+ x86-64

File details

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

File metadata

  • Download URL: torann-0.5.1.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.1.tar.gz
Algorithm Hash digest
SHA256 c089251b1328c313f71ea6f5ab527893395f13b832f7cc049b32957c8137b891
MD5 cb359a41fc5b46623d4d64f5ebb0a397
BLAKE2b-256 d3a30b941ad95de7039d71986809393f2640912f3e6c99e69955b4c0b4636f27

See more details on using hashes here.

File details

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

File metadata

  • Download URL: torann-0.5.1-cp311-abi3-win_amd64.whl
  • Upload date:
  • Size: 316.6 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.1-cp311-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 14950bd50716e7abc33db6e6f25bbf01ac265d9c82d4606d5ab1c66c41115a73
MD5 05a13fe24a0d58e897fca5150cb9f5b1
BLAKE2b-256 afb8455a7a2b94eb695b08fb93ad00cb92c6e7554a9c0701000e0bc7d9851b65

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for torann-0.5.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 0d7e8707954322b5fcdafac06d0d0724f95d0d580b56ebdd17316f1b5f8f02e4
MD5 9141d2aa98681f87da056462d56e90d9
BLAKE2b-256 222e37d2d7f1beb0d99ee44f5f956020c5c18f09142113acace67f449fb8b74b

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for torann-0.5.1-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 974ce24a1c1ab47cb54a314b4d58808956e6a0ece5a60a5d75146ff60fcfaca5
MD5 5bb2b779bc58d114ba5690307dc5ca91
BLAKE2b-256 fe5df224969e7e8609f31317cf39df6b604a5d3dc68a90152fec7b0b0d253c60

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for torann-0.5.1-cp311-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 ce52bc209c325cf4cd31f97ea5b5b07d157e72071c9febff97c641ebc53353f6
MD5 810075a076da95ec2355b589b7f3cf3b
BLAKE2b-256 b83fc8cad4bde78d1d763bb41f67317468909a3eea8d2934b064cc815c303212

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for torann-0.5.1-cp311-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 19b8ea2d4d79e7dd59f88a7e7039d7f32fc001b75a973a3a761985796d16d800
MD5 635b15ab1d76b8f01466849e0272461b
BLAKE2b-256 8387eea27e4076d38ffb7cae7759adca27fa7aa8aefc0b43364145473975dede

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.5.1 This release

6 files

0.5.0

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