Rust-accelerated quadtree for Python with fast inserts, range queries, and k-NN search.
Project description
quadtree-rs
Rust-optimized quadtree with a simple Python API.
- Python package:
quadtree_rs - Python ≥ 3.8
- Import path:
from quadtree_rs import QuadTree
Install
pip install quadtree_rs
If you are developing locally:
# optimized dev install
maturin develop --release
Quickstart
from quadtree_rs import QuadTree
# Bounds are (min_x, min_y, max_x, max_y)
qt = QuadTree(bounds=(0, 0, 1000, 1000), capacity=20) # max_depth is optional
# Insert points with auto ids
id1 = qt.insert((10, 10))
id2 = qt.insert((200, 300))
id3 = qt.insert((999, 500), id=42) # you can supply your own id
# Axis-aligned rectangle query
hits = qt.query((0, 0, 250, 350)) # returns [(id, x, y), ...] by default
print(hits) # e.g. [(1, 10.0, 10.0), (2, 200.0, 300.0)]
# Nearest neighbor
best = qt.nearest_neighbor((210, 310)) # -> (id, x, y) or None
print(best)
# k-nearest neighbors
top3 = qt.nearest_neighbors((210, 310), 3)
print(top3) # list of up to 3 (id, x, y) tuples
Working with Python objects
You can keep the tree pure and manage your own id → object map, or let the wrapper manage it.
Option A: Manage your own map
from quadtree_rs import QuadTree
qt = QuadTree((0, 0, 1000, 1000), capacity=16)
objects: dict[int, object] = {}
def add(obj) -> int:
obj_id = qt.insert(obj.position) # auto id
objects[obj_id] = obj
return obj_id
# Later, resolve ids back to objects
ids = [obj_id for (obj_id, x, y) in qt.query((100, 100, 300, 300))]
selected = [objects[i] for i in ids]
Option B: Ask the wrapper to track objects
from quadtree_rs import QuadTree
qt = QuadTree((0, 0, 1000, 1000), capacity=16, track_objects=True)
# Store the object alongside the point
qt.insert((25, 40), obj={"name": "apple"})
# Ask for Item objects so you can access .obj lazily
items = qt.query((0, 0, 100, 100), as_items=True)
for it in items:
print(it.id, it.x, it.y, it.obj)
You can also attach or replace an object later:
qt.attach(123, my_object) # binds object to id 123
API
QuadTree(bounds, capacity, *, max_depth=None, track_objects=False, start_id=1)
bounds— tuple(min_x, min_y, max_x, max_y)covering all points you will insertcapacity— max number of points kept in a leaf before splittingmax_depth— optional depth cap. If omitted, the tree can keep splitting as neededtrack_objects— ifTrue, the wrapper maintains an id → object mapstart_id— starting value for auto-assigned ids
Methods
-
insert(xy: tuple[float, float], *, id: int | None = None, obj: object | None = None) -> intInsert a point. Returns the id used. RaisesValueErrorif the point is outsidebounds. Iftrack_objects=Trueandobjis provided, the object is stored under that id. -
insert_many_points(points: Iterable[tuple[float, float]]) -> intBulk insert points with auto ids. Returns count inserted. -
attach(id: int, obj: object) -> NoneAttach or replace an object for an existing id. Iftrack_objectswas false, a map is created on first use. -
query(rect: tuple[float, float, float, float], *, as_items: bool = False) -> list[(id, x, y)] | list[Item]Return all points whose coordinates lie inside the rectangle. Useas_items=Trueto getItemwrappers with lazy.obj. -
nearest_neighbor(xy: tuple[float, float], *, as_item: bool = False) -> (id, x, y) | Item | NoneReturn the closest point toxy, orNoneif empty. -
nearest_neighbors(xy: tuple[float, float], k: int, *, as_items: bool = False) -> list[(id, x, y)] | list[Item]Return up toknearest points. -
get(id: int) -> object | NoneGet the object associated withidif tracking is enabled. -
__len__() -> intNumber of successful inserts made through this wrapper. -
NativeQuadTreeReference to the underlying Rust classquadtree_rs._native.QuadTreefor power users.
Item (returned when as_items=True)
- Attributes:
id,x,y, and a lazyobjproperty - Accessing
objperforms a dictionary lookup only if tracking is enabled
Geometric conventions
- Rectangles are
(min_x, min_y, max_x, max_y). - Containment rule is open on the min edge and closed on the max edge
(x > min_x and x <= max_x and y > min_y and y <= max_y). This only matters for points exactly on edges.
Performance tips
- Choose
capacityso that leaves keep a small batch of points. Typical values are 8 to 64. - If your data is very skewed, set a
max_depthto prevent long chains. - For fastest local runs, use
maturin develop --release. - The wrapper keeps Python overhead low: raw tuple results by default,
Itemwrappers only when requested.
Benchmarks
quadtree-rs outperforms all other quadtree python packages (at least all the ones I could find and install via pip.)
Library comparison
Generated with benchmarks/benchmark_plotly.py in this repo.
- 100k points, 500 queries, capacity 20, max depth 10
- Median over 3 runs per size
Summary (largest dataset, PyQtree baseline)
- Points: 500,000, Queries: 500
- Fastest total: quadtree-rs at 2.288 s
- PyQtree total: 9.717 s
- quadtree-rs total: 2.288 s
- e-pyquadtree total: 13.504 s
- Brute force total: 20.450 s
| Library | Build (s) | Query (s) | Total (s) | Speed vs PyQtree |
|---|---|---|---|---|
| quadtree-rs | 0.330 | 1.958 | 2.288 | 4.25× |
| PyQtree | 4.479 | 5.238 | 9.717 | 1.00× |
| e-pyquadtree | 2.821 | 10.683 | 13.504 | 0.72× |
| Brute force | nan | 20.450 | 20.450 | 0.48× |
| nontree-QuadTree | 1.687 | 7.803 | 9.490 | 1.02× |
| quads | 3.977 | 9.070 | 13.046 | 0.74× |
| Rtree | 1.676 | 4.805 | 6.481 | 1.50× |
Native vs shim
Setup
- Points: 100,000
- Queries: 500
- Repeats: 5
Timing (seconds)
| Variant | Build | Query | Total |
|---|---|---|---|
| Native | 0.038 | 0.317 | 0.354 |
| Shim (no map) | 0.051 | 0.309 | 0.360 |
| Shim (track+objs) | 0.057 | 0.321 | 0.379 |
Overhead vs native
- No map: build 1.35x, query 0.98x, total 1.01x
- Track + objs: build 1.53x, query 1.01x, total 1.07x
FAQ
What happens if I insert the same id more than once? Allowed. For k-nearest, duplicates are de-duplicated by id. For range queries you will see every inserted point.
Can I store rectangles or circles? The core stores points. To index objects with extent, insert whatever representative point you choose. For rectangles you can insert centers or build an AABB tree separately.
Threading Use one tree per thread if you need heavy parallel inserts from Python.
License
MIT. See LICENSE.
Acknowledgments
- Python libraries compared: PyQtree, e-pyquadtree
- Built with PyO3 and maturin
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file quadtree_rs-0.3.0.tar.gz.
File metadata
- Download URL: quadtree_rs-0.3.0.tar.gz
- Upload date:
- Size: 613.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.9.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
34af2e1913c3909331d8dea358e026377987b71f72a50fff08173a3f932527cd
|
|
| MD5 |
10f176cc1d911a5a98c0771c9ccc7a24
|
|
| BLAKE2b-256 |
f62218c7c0a95d6eafa970082851c085ab769f802c2fb265c5fe3ccae3b210e3
|
File details
Details for the file quadtree_rs-0.3.0-cp38-abi3-win_amd64.whl.
File metadata
- Download URL: quadtree_rs-0.3.0-cp38-abi3-win_amd64.whl
- Upload date:
- Size: 144.9 kB
- Tags: CPython 3.8+, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.9.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1794dbc55d787245d3c16e26d81c3a45b5082c19e7e810ee670137529b56e049
|
|
| MD5 |
4259b57a179ea9770b24ca0a77757b3b
|
|
| BLAKE2b-256 |
cf54880e05c6cd08c8023dc642f3eebb0e810ac533a1e95db943d03e182e5943
|
File details
Details for the file quadtree_rs-0.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: quadtree_rs-0.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 240.7 kB
- Tags: CPython 3.8+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.9.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0961c5db7a39ed41791d76fe9e405f3ff7bea8540e99d99a00e47d0710468895
|
|
| MD5 |
a8493009bccaa9d152b73eda5bab3259
|
|
| BLAKE2b-256 |
f8109060bf838df347871de78b539500233a4d4cdb7c666b7d7c586c368d76da
|
File details
Details for the file quadtree_rs-0.3.0-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl.
File metadata
- Download URL: quadtree_rs-0.3.0-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
- Upload date:
- Size: 419.2 kB
- Tags: CPython 3.8+, macOS 10.12+ universal2 (ARM64, x86-64), macOS 10.12+ x86-64, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.9.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
96dc9551b52174c5ea306dc00d71e62181747c47cd9538af48a59f6e9b5e0a9f
|
|
| MD5 |
e24c7f40be7ffec7fdc3f495a7c645d5
|
|
| BLAKE2b-256 |
1e10e68318499c5dc432d2fc429a5bc775a3bb7ad23ebf1e84151126b1db43b8
|