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.

Metadata

Release files for torann 0.5.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for torann 0.5.1
File Size Uploaded Provenance
torann-0.5.1.tar.gz 2.8 MB Without provenance Details

Built distributions (wheels)

Table of built distributions (wheels) for torann 0.5.1
File
torann-0.5.1-cp311-abi3-win_amd64.whl CPython 3.11 abi3 Windows x86-64 Details
torann-0.5.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.11 abi3 Linux glibc 2.17+ x86-64 Details
torann-0.5.1-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.11 abi3 Linux glibc 2.17+ ARM64 Details
torann-0.5.1-cp311-abi3-macosx_11_0_arm64.whl CPython 3.11 abi3 macOS 11.0+ ARM64 Details
torann-0.5.1-cp311-abi3-macosx_10_12_x86_64.whl CPython 3.11 abi3 macOS 10.12+ x86-64 Details

Total release size: 4.8 MB

Release files / torann-0.5.1.tar.gz

Download URL torann-0.5.1.tar.gz
Size 2.8 MB
Tags Source
SHA-256 checksum
How to use checksums
c089251b1328c313f71ea6f5ab527893395f13b832f7cc049b32957c8137b891
BLAKE2b-256 checksum
How to use checksums
d3a30b941ad95de7039d71986809393f2640912f3e6c99e69955b4c0b4636f27
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / torann-0.5.1-cp311-abi3-win_amd64.whl

Download URL torann-0.5.1-cp311-abi3-win_amd64.whl
Size 316.6 kB
Tags CPython 3.11 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
14950bd50716e7abc33db6e6f25bbf01ac265d9c82d4606d5ab1c66c41115a73
BLAKE2b-256 checksum
How to use checksums
afb8455a7a2b94eb695b08fb93ad00cb92c6e7554a9c0701000e0bc7d9851b65
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / torann-0.5.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL torann-0.5.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 433.9 kB
Tags CPython 3.11 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
0d7e8707954322b5fcdafac06d0d0724f95d0d580b56ebdd17316f1b5f8f02e4
BLAKE2b-256 checksum
How to use checksums
222e37d2d7f1beb0d99ee44f5f956020c5c18f09142113acace67f449fb8b74b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / torann-0.5.1-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL torann-0.5.1-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 423.9 kB
Tags CPython 3.11 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
974ce24a1c1ab47cb54a314b4d58808956e6a0ece5a60a5d75146ff60fcfaca5
BLAKE2b-256 checksum
How to use checksums
fe5df224969e7e8609f31317cf39df6b604a5d3dc68a90152fec7b0b0d253c60
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / torann-0.5.1-cp311-abi3-macosx_11_0_arm64.whl

Download URL torann-0.5.1-cp311-abi3-macosx_11_0_arm64.whl
Size 388.1 kB
Tags CPython 3.11 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
ce52bc209c325cf4cd31f97ea5b5b07d157e72071c9febff97c641ebc53353f6
BLAKE2b-256 checksum
How to use checksums
b83fc8cad4bde78d1d763bb41f67317468909a3eea8d2934b064cc815c303212
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / torann-0.5.1-cp311-abi3-macosx_10_12_x86_64.whl

Download URL torann-0.5.1-cp311-abi3-macosx_10_12_x86_64.whl
Size 402.7 kB
Tags CPython 3.11 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
19b8ea2d4d79e7dd59f88a7e7039d7f32fc001b75a973a3a761985796d16d800
BLAKE2b-256 checksum
How to use checksums
8387eea27e4076d38ffb7cae7759adca27fa7aa8aefc0b43364145473975dede
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Security for torann 0.5.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

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