{"id":32362938,"url":"https://github.com/schlegelp/aann","last_synced_at":"2026-07-14T03:31:31.124Z","repository":{"id":219502379,"uuid":"749072601","full_name":"schlegelp/aann","owner":"schlegelp","description":"All nearest-neighbor search using neighborhood graphs","archived":false,"fork":false,"pushed_at":"2026-07-08T14:46:26.000Z","size":231,"stargazers_count":1,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-07-08T15:13:06.712Z","etag":null,"topics":["ann","approximate-nearest-neighbor","delaunay","kdtree","nblast","nearest-neighbor","neighborhood-graph","spatial"],"latest_commit_sha":null,"homepage":"","language":"Jupyter Notebook","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/schlegelp.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-01-27T14:07:48.000Z","updated_at":"2026-07-08T14:31:21.000Z","dependencies_parsed_at":"2024-02-01T16:20:48.752Z","dependency_job_id":null,"html_url":"https://github.com/schlegelp/aann","commit_stats":null,"previous_names":["schlegelp/aann"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/schlegelp/aann","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/schlegelp%2Faann","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/schlegelp%2Faann/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/schlegelp%2Faann/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/schlegelp%2Faann/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/schlegelp","download_url":"https://codeload.github.com/schlegelp/aann/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/schlegelp%2Faann/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35445233,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-14T02:00:06.603Z","response_time":114,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ann","approximate-nearest-neighbor","delaunay","kdtree","nblast","nearest-neighbor","neighborhood-graph","spatial"],"created_at":"2025-10-24T16:00:46.701Z","updated_at":"2026-07-14T03:31:31.111Z","avatar_url":"https://github.com/schlegelp.png","language":"Jupyter Notebook","funding_links":[],"categories":[],"sub_categories":[],"readme":"# `AANN`\n**A**pproximate **A**ll **N**earest-**N**eighbor (\"_aann_\") search using neighborhood graphs. Implemented in Rust with Python bindings. Based on [Soudani \u0026 Karami (2018)](https://arxiv.org/abs/1802.09594).\n\nIt is optimised for **many nearest-neighbour joins that reuse the same clouds** —\nbuild each cloud's index once, then query it again and again (e.g. an all-by-all\njoin). See [Usage](#usage).\n\n### Problem\n\nGiven two point clouds `Q` and `P`, for each point `q` in `Q` find its nearest neighbor `p` among the points in `P`.\n\n### Solution\n1. Calculate neighborhood (e.g. Delaunay) graphs for both point clouds.\n2. Start with a random vertex `q` in `Q` and traverse `P` using an A* search to find its nearest neighbor `p`.\n3. Move to a vertex adjacent to `q` and search `P` for its nearest neighbor using `p` as the start. Since we start the search where we have already established spatial proximity the A* search should finish quickly.\n4. Rinse-repeat until we found nearest neighbors for all points in `Q`.\n\n![schematic](./_static/aann_schematic.png)\n\n### Limitations\nWe currently only support 3D point clouds and Euclidean distances but may extend this to N-dimensions and other metrics in the future.\n\n## Install\n\nWe provide prebuilt wheels for Linux, macOS, and Windows on PyPI:\n\n```bash\npip install aann\n```\n\n## Usage\n\n`aann` is built for **repeated** nearest-neighbour queries against prepared\nindices. Building an `AANN` index does the expensive work up front — triangulate\nthe cloud into a neighbourhood graph, SIMD-pack its coordinates, and (by default)\nreorder them for cache locality — so that every subsequent `.query` is cheap. The\npayoff grows the more you reuse an index; for a single one-off search the build\ncost dominates and a plain KD-tree is simpler.\n\n### Build an index once, query it many times\n\n```python\nimport aann\nimport numpy as np\n\ntarget = np.random.rand(10_000, 3)\nindex = aann.AANN(target)              # pay the build cost once...\n\nfor query in query_clouds:             # ...then amortise it over many queries\n    distances, indices = index.query(query)\n\n# k nearest neighbours per point -\u003e (N, k) arrays, rows sorted by distance\n# (k\u003e1 is approximate; raise `ef` to trade search breadth for recall):\ndistances, indices = index.query(query, k=4)\n\n# Ignore matches beyond a cutoff (scipy convention: misses get\n# distance=inf and neighbour index=len(target)):\ndistances, indices = index.query(query, distance_upper_bound=0.05)\n```\n\nThe query cloud can be a raw `(N, 3)` array, a `scipy`/`shull` `Delaunay`, or\nanother `AANN` — passing a prepared `AANN` skips re-triangulating it (the fast\npath the all-by-all below uses). `k` counts *returned* neighbours (as in\n`scipy.spatial.cKDTree.query`); the degree of the optional `graph=\"knn\"`\nneighbourhood graph is `graph_k`.\n\n### All-by-all: every cloud against every other\n\nThis is where `aann` pulls ahead. Build one index per cloud with `prepare_many`\n(parallel), then join them with `all_by_all`: every index is built and packed\n**once** and reused across every pair it appears in, and the Rust search releases\nthe GIL so the pairs run concurrently across cores.\n\n```python\nindexes = aann.prepare_many(clouds)                 # list[AANN], built in parallel\nresults = aann.all_by_all(indexes)                  # every ordered i != j pair\nresults = aann.all_by_all(indexes, pairs=[(0, 1)])  # ...or a specific subset\n# results[m] is the (distances, indices) for pairs[m] (query = i, target = j)\n```\n\nBecause a cloud that appears in many pairs is triangulated and packed only once,\na full all-by-all over *n* clouds does *O(n)* index builds rather than *O(n²)* —\nthe reason `aann` suits workloads like NBLAST neuron-vs-neuron matrices.\n\n### `aann` vs a KD-tree\n\nUnlike a KD-tree, `aann` is a **cloud-vs-cloud** method: the query points are\nthemselves triangulated into a graph, so the warm-started descent starts each\nquery near the previous answer. That is what makes it fast on coherent clouds\n(neurons, meshes, space-filling data) — but pass a whole cloud, not a handful of\nscattered points, and expect *approximate* results for `k\u003e1` (`k=1` is exact on a\nDelaunay graph).\n\n## Using from Rust\n\nThe core search is a plain Rust library — the Python bindings sit behind the\nnon-default `python` cargo feature. It builds on **stable** Rust (SIMD comes\nfrom the [`wide`](https://crates.io/crates/wide) crate):\n\n```toml\n[dependencies]\naann-graph = { git = \"https://github.com/schlegelp/aann\" }\n```\n\nThe package is named `aann-graph` (plain `aann` is taken on crates.io by an\nunrelated project) but its library target is `aann`, so in code you import it\nas `aann`:\n\n```rust\nuse aann::{graph_from_simplices, PreparedF64};\nuse aann::ndarray::array; // re-exported ndarray\n\n// Target cloud + its Delaunay simplices (rows of 4 vertex ids):\nlet points = array![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];\nlet (indptr, indices) = graph_from_simplices(array![[0u64, 1, 2, 3]].view(), 4);\nlet target = PreparedF64::new(points.view(), indptr.view(), indices.view());\n\n// Query cloud with its own CSR neighbourhood graph:\nlet queries = array![[0.1, 0.0, 0.0], [0.9, 0.1, 0.0]];\nlet (qptr, qidx) = (array![0usize, 1, 2], array![1usize, 0]);\nlet (dists, idxs) = target.query(queries.view(), qptr.view(), qidx.view());\n```\n\n`PreparedF64::query_k(..., k, ef)` gives k nearest neighbours,\n`query_prepared(\u0026other)` is the pack-free prepared-vs-prepared fast path, and\neverything exists in an `F32` flavour too (see the crate docs for the full\nAPI, including the lower-level `Neighborhood*`/`search_*` functions).\n\n## Benchmark\n\n`bench.py` contrasts `aann` with `scipy.spatial.KDTree` on uniform random 3D\nclouds, single-threaded (so it compares the algorithms, not the thread pools).\nIt makes the trade-off concrete — an `aann` index is expensive to build but cheap\nto query, so it only pays off once reused. Representative run (N = 5000\npoints/cloud, float64; numbers are indicative and machine-dependent):\n\n|              | build  | query  |\n| ------------ | ------ | ------ |\n| scipy KDTree | 0.6 ms | 2.5 ms |\n| aann index   | 19 ms  | 0.5 ms |\n\nThe index costs ~30× more to build but answers each query ~5× faster, so it\nbreaks even after ~9 reuses. In a full all-by-all — where every cloud's index is\nbuilt once and reused across all its pairs — `aann`'s **total** wall-clock\n(build + every pair) overtakes `scipy` at roughly a dozen clouds and keeps\npulling ahead (≈1.6× faster at n = 20). Recall vs the exact KDTree is 100% on\nthis uniform data. Reproduce with `python bench.py`.\n\n## TODOs\n- [x] use SIMD (singe instruction multiple data) for distance calculations\n- [x] implement k-all-nearest neighbors (`k\u003e1` uses an approximate best-first search; recall tunable via `ef`)\n- [x] benchmarks\n- [ ] test other neighborhood graphs (e.g. Gabriel, relative neighborhood, etc.) and compare performance/recall\n- [ ] implement `query_radius` (analagous to `scipy.spatial.cKDTree.query_ball_tree`)\n- [ ] add alternative distance metrics (currently only Euclidean)\n- [ ] generalize to N-dimensions (currently only 3D)\n\n## Build\nRequires **Python ≥ 3.10**. The extension is built against the stable ABI\n(`abi3-py310`), so a single wheel works across all supported CPython versions.\n\n1. `cd` into directory\n2. Activate virtual environment: `source .venv/bin/activate`\n3. Run `maturin build --release` to build a wheel or use `maturin develop` to compile and install in development mode\n\n### SIMD\n`aann` uses the [`wide`](https://crates.io/crates/wide) crate for SIMD, which\nworks on stable Rust. One thing to be aware of:\n\n1. By default, only the oldest SIMD extension `sse2` is enabled during compilation (on x86-64). It is very likely that your processor supports newer extensions such as `avx2` or even `avx512f`. To check what's supported run:\n    ```bash\n    $ cargo install cargo-simd-detect --force\n    $ cargo simd-detect\n    extension       width                   available       enabled\n    sse2            128-bit/16-bytes        true            true\n    avx2            256-bit/32-bytes        true            false\n    avx512f         512-bit/64-bytes        true            false\n    ```\n    You can tell the compiler to use newer extensions by setting rust flags:\n    ```bash\n    # To activate a specific extension\n    export RUSTFLAGS=\"-C target-feature=+avx2\"\n\n    # Alternatively to activate all available extensions\n    export RUSTFLAGS=\"-C target-cpu=native\"\n    ```\n\n## Test\nFirst make sure `pytest` and `pandas` are installed:\n```\npip install pytest -U\n```\n\nThen run the test-suite like so:\n```\npytest --verbose -s\n```\n\nNote that unless you compiled with `maturin develop --release` the timings will\nbe much slower (up to 10x) than in a release build.\n\n\n## References\n\nSoudani, N. M., \u0026 Karami, A. (2018). All nearest neighbor calculation based on Delaunay graphs (Version 1). arXiv. https://doi.org/10.48550/ARXIV.1802.09594\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fschlegelp%2Faann","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fschlegelp%2Faann","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fschlegelp%2Faann/lists"}