Skip to main content

Rust-accelerated quadtree for Python with fast inserts, range queries, and k-NN search.

Project description

fastquadtree

PyPI version Python versions Wheels License: MIT

Downloads total Downloads month

Build Codecov

Rust core via PyO3 Built with maturin Code style: Black Type checking: mypy

Interactive_V2_Screenshot

Rust-optimized quadtree with a simple Python API.

  • Python package: fastquadtree
  • Python ≥ 3.8
  • Import path: from fastquadtree import QuadTree

Install

pip install fastquadtree

If you are developing locally:

# optimized dev install
maturin develop --release

Quickstart

from fastquadtree 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

# Delete items by ID and location
deleted = qt.delete(id2, (200, 300))  # True if found and deleted
print(f"Deleted: {deleted}")
print(f"Remaining items: {qt.count_items()}")

# For object tracking with track_objects=True
qt_tracked = QuadTree((0, 0, 1000, 1000), capacity=4, track_objects=True)
player1 = {"name": "Alice", "score": 100}
player2 = {"name": "Bob", "score": 200}

id1 = qt_tracked.insert((50, 50), obj=player1)
id2 = qt_tracked.insert((150, 150), obj=player2)

# Delete by object reference (O(1) lookup!)
deleted = qt_tracked.delete_by_object(player1)
print(f"Deleted player: {deleted}")  # True

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 fastquadtree 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 fastquadtree 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) defines the 2D area covered by the quadtree
  • capacity — max number of points kept in a leaf before splitting
  • max_depth — optional depth cap. If omitted, the tree can keep splitting as needed
  • track_objects — if True, the wrapper maintains an id → object map for convenience.
  • start_id — starting value for auto-assigned ids

Core Methods

Full docs are in the docstrings of the Python Shim

  • insert(xy, *, id=None, obj=None) -> int

  • insert_many_points(points) -> int

  • query(rect, *, as_items=False) -> list

  • nearest_neighbor(xy, *, as_item=False) -> (id, x, y) | Item | None

  • nearest_neighbors(xy, k, *, as_items=False) -> list

  • delete(id, xy) -> bool

  • delete_by_object(obj) -> bool (requires track_objects=True)

  • attach(id, obj) -> None (requires track_objects=True)

  • count_items() -> int

  • get(id) -> object | None

  • get_all_rectangles() -> list[tuple] (for visualization)

Item (returned when as_items=True)

  • Attributes: id, x, y, and a lazy obj property
  • Accessing obj performs a dictionary lookup only if tracking is enabled

Geometric conventions

  • Rectangles are (min_x, min_y, max_x, max_y).
  • Containment rule is closed on the min edge and open 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 capacity so that leaves keep a small batch of points. Typical values are 8 to 64.
  • If your data is very skewed, set a max_depth to prevent long chains.
  • For fastest local runs, use maturin develop --release.
  • The wrapper keeps Python overhead low: raw tuple results by default, Item wrappers only when requested.

Benchmarks

fastquadtree outperforms all other quadtree python packages (at least all the ones I could find and install via pip.)

Library comparison

Total time Throughput

Summary (largest dataset, PyQtree baseline)

  • Points: 500,000, Queries: 500
  • Fastest total: fastquadtree at 2.207 s
Library Build (s) Query (s) Total (s) Speed vs PyQtree
fastquadtree 0.321 1.885 2.207 4.27×
Rtree 1.718 4.376 6.095 1.55×
nontree-QuadTree 1.617 7.643 9.260 1.02×
PyQtree 4.349 5.082 9.431 1.00×
quads 3.874 9.058 12.932 0.73×
e-pyquadtree 2.732 10.598 13.330 0.71×
Brute force 0.019 19.986 20.005 0.47×

Native vs Shim

Setup

  • Points: 500,000
  • Queries: 500
  • Repeats: 5

Timing (seconds)

Variant Build Query Total
Native 0.483 4.380 4.863
Shim (no map) 0.668 4.167 4.835
Shim (track+objs) 1.153 4.458 5.610

Overhead vs Native

  • No map: build 1.38x, query 0.95x, total 0.99x
  • Track + objs: build 2.39x, query 1.02x, total 1.15x

Run benchmarks

To run the benchmarks yourself, first install the dependencies:

pip install -r benchmarks/requirements.txt

Then run:

python benchmarks/cross_library_bench.py
python benchmarks/benchmark_native_vs_shim.py 

Run Visualizer

A visualizer is included to help you understand how the quadtree subdivides space.

pip install -r interactive/requirements.txt
python interactive/interactive_v2.py

Check the CLI arguments for the cross-library benchmark in benchmarks/quadtree_bench/main.py.

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 delete items from the quadtree? Yes! Use delete(id, xy) to remove specific items. You must provide both the ID and exact location for precise deletion. This handles cases where multiple items exist at the same location. If you're using track_objects=True, you can also use delete_by_object(obj, xy) for convenient object-based deletion with O(1) lookup. The tree automatically merges nodes when item counts drop below capacity.

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

Project details


Download files

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

Source Distribution

fastquadtree-0.4.2.tar.gz (644.7 kB view details)

Uploaded Source

Built Distributions

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

fastquadtree-0.4.2-cp38-abi3-win_amd64.whl (149.4 kB view details)

Uploaded CPython 3.8+Windows x86-64

fastquadtree-0.4.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (246.7 kB view details)

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

fastquadtree-0.4.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl (425.3 kB view details)

Uploaded CPython 3.8+macOS 10.12+ universal2 (ARM64, x86-64)macOS 10.12+ x86-64macOS 11.0+ ARM64

File details

Details for the file fastquadtree-0.4.2.tar.gz.

File metadata

  • Download URL: fastquadtree-0.4.2.tar.gz
  • Upload date:
  • Size: 644.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: maturin/1.9.4

File hashes

Hashes for fastquadtree-0.4.2.tar.gz
Algorithm Hash digest
SHA256 bfdc7148ebddc4577233c175aad45736393da562988fb54c980ead28673257da
MD5 18539a179ec18c03a60ca88969a8fa85
BLAKE2b-256 7ea632f538574d0964b4714e57e88bb63c8da1e3fe94ccfc13e3310ba2df5aef

See more details on using hashes here.

File details

Details for the file fastquadtree-0.4.2-cp38-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for fastquadtree-0.4.2-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 be7b2ff810001f1c80974698c16c86668a957fef70d59e83473ebbb0d30563da
MD5 d245208922630e692e1a09071a30ef6a
BLAKE2b-256 f57472d77b809957f20018fe42339486224800a8971c6eb56bbab840d19a0800

See more details on using hashes here.

File details

Details for the file fastquadtree-0.4.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for fastquadtree-0.4.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 1e051b99c59fe064fbb645fb54eb4666e708f0cdce95c930c749eb363373873a
MD5 617a1bff6a5c61bb9fd9e22ccc1527f9
BLAKE2b-256 7296171b9cfca20d562f950f335af4a7a8b2f172010dfde53e71ef548c604bd3

See more details on using hashes here.

File details

Details for the file fastquadtree-0.4.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl.

File metadata

File hashes

Hashes for fastquadtree-0.4.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
Algorithm Hash digest
SHA256 b96805bd9d66309497f63c4c8473e7d0260f032fef720c334872ebe428e7b377
MD5 0a65005a6726bbdb3338adf328b588a2
BLAKE2b-256 d97b3a65bd8d101eca7783e5e0c043f97a467b7aa87761970f8cb7ddad4662f8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page