fastquadtree 0.6.1__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastquadtree-0.8.0/.github/workflows/docs.yml +20 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/Cargo.lock +1 -1
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/Cargo.toml +1 -1
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/PKG-INFO +32 -141
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/README.md +21 -138
- fastquadtree-0.8.0/assets/ballpit.png +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/benchmarks/benchmark_native_vs_shim.py +14 -8
- fastquadtree-0.8.0/benchmarks/cross_library_bench.py +6 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/benchmarks/quadtree_bench/engines.py +1 -1
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/benchmarks/quadtree_bench/runner.py +2 -3
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/benchmarks/requirements.txt +6 -1
- fastquadtree-0.8.0/benchmarks/system_info_collector.py +259 -0
- fastquadtree-0.8.0/docs/api/point_item.md +4 -0
- fastquadtree-0.8.0/docs/api/quadtree.md +4 -0
- fastquadtree-0.8.0/docs/api/rect_item.md +4 -0
- fastquadtree-0.8.0/docs/api/rect_quadtree.md +4 -0
- fastquadtree-0.8.0/docs/benchmark.md +80 -0
- fastquadtree-0.8.0/docs/index.md +48 -0
- fastquadtree-0.8.0/docs/quickstart.md +135 -0
- fastquadtree-0.8.0/docs/runnables.md +33 -0
- fastquadtree-0.8.0/interactive/ballpit.py +283 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/interactive/interactive.py +1 -1
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/interactive/interactive_v2.py +2 -2
- fastquadtree-0.8.0/interactive/interactive_v2_rect.py +432 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/interactive/requirements.txt +1 -1
- fastquadtree-0.8.0/mkdocs.yml +86 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/pyproject.toml +18 -4
- fastquadtree-0.8.0/pysrc/fastquadtree/__init__.py +5 -0
- fastquadtree-0.8.0/pysrc/fastquadtree/_base_quadtree.py +263 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/pysrc/fastquadtree/_bimap.py +22 -22
- fastquadtree-0.8.0/pysrc/fastquadtree/_item.py +52 -0
- fastquadtree-0.8.0/pysrc/fastquadtree/point_quadtree.py +161 -0
- fastquadtree-0.8.0/pysrc/fastquadtree/rect_quadtree.py +98 -0
- fastquadtree-0.8.0/src/lib.rs +169 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/src/quadtree.rs +1 -1
- fastquadtree-0.8.0/src/rect_quadtree.rs +295 -0
- fastquadtree-0.8.0/tests/rect_quadtree.rs +221 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/tests/rectangle_traversal.rs +8 -8
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/tests/test_bimap.py +1 -1
- fastquadtree-0.8.0/tests/test_clear.py +130 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/tests/test_delete.rs +12 -12
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/tests/test_delete_python.py +4 -4
- fastquadtree-0.8.0/tests/test_rect_quadtree.py +199 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/tests/test_unconventional_bounds.py +2 -2
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/tests/test_wrapper_edges.py +6 -6
- fastquadtree-0.6.1/benchmarks/cross_library_bench.py +0 -3
- fastquadtree-0.6.1/pysrc/fastquadtree/__init__.py +0 -381
- fastquadtree-0.6.1/pysrc/fastquadtree/__init__.pyi +0 -70
- fastquadtree-0.6.1/pysrc/fastquadtree/_item.py +0 -24
- fastquadtree-0.6.1/src/lib.rs +0 -99
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/.github/workflows/release.yml +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/.gitignore +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/.pre-commit-config.yaml +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/LICENSE +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/assets/interactive_v2_screenshot.png +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/assets/quadtree_bench_throughput.png +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/assets/quadtree_bench_time.png +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/benchmarks/quadtree_bench/__init__.py +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/benchmarks/quadtree_bench/main.py +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/benchmarks/quadtree_bench/plotting.py +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/benchmarks/runner.py +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/pysrc/fastquadtree/py.typed +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/src/geom.rs +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/tests/insertions.rs +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/tests/nearest_neighbor.rs +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/tests/query.rs +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/tests/test_delete_by_object.py +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/tests/test_python.py +0 -0
- {fastquadtree-0.6.1 → fastquadtree-0.8.0}/tests/unconventional_bounds.rs +0 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
# .github/workflows/docs.yml
|
2
|
+
name: Docs
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ main ]
|
6
|
+
|
7
|
+
permissions:
|
8
|
+
contents: write
|
9
|
+
pages: write
|
10
|
+
id-token: write
|
11
|
+
|
12
|
+
jobs:
|
13
|
+
build:
|
14
|
+
runs-on: ubuntu-latest
|
15
|
+
steps:
|
16
|
+
- uses: actions/checkout@v4
|
17
|
+
- uses: actions/setup-python@v5
|
18
|
+
with: { python-version: '3.11' }
|
19
|
+
- run: pip install -e '.[dev]'
|
20
|
+
- run: mkdocs gh-deploy --force
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: fastquadtree
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.8.0
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
5
5
|
Classifier: Programming Language :: Python :: 3 :: Only
|
6
6
|
Classifier: Programming Language :: Rust
|
@@ -12,11 +12,18 @@ Classifier: Topic :: Software Development :: Libraries
|
|
12
12
|
Classifier: Typing :: Typed
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
14
14
|
Requires-Dist: ruff>=0.6.0 ; extra == 'dev'
|
15
|
-
Requires-Dist: pytest>=
|
16
|
-
Requires-Dist: pytest-cov>=
|
15
|
+
Requires-Dist: pytest>=8.4.2 ; extra == 'dev'
|
16
|
+
Requires-Dist: pytest-cov>=7.0.0 ; extra == 'dev'
|
17
17
|
Requires-Dist: coverage>=7.5 ; extra == 'dev'
|
18
18
|
Requires-Dist: mypy>=1.10 ; extra == 'dev'
|
19
19
|
Requires-Dist: build>=1.2.1 ; extra == 'dev'
|
20
|
+
Requires-Dist: mkdocs>=1.6 ; extra == 'dev'
|
21
|
+
Requires-Dist: mkdocs-material ; extra == 'dev'
|
22
|
+
Requires-Dist: mkdocstrings[python] ; extra == 'dev'
|
23
|
+
Requires-Dist: mkdocs-autorefs ; extra == 'dev'
|
24
|
+
Requires-Dist: mkdocs-git-revision-date-localized-plugin ; extra == 'dev'
|
25
|
+
Requires-Dist: mkdocs-minify-plugin ; extra == 'dev'
|
26
|
+
Requires-Dist: maturin>=1.5 ; extra == 'dev'
|
20
27
|
Provides-Extra: dev
|
21
28
|
License-File: LICENSE
|
22
29
|
Summary: Rust-accelerated quadtree for Python with fast inserts, range queries, and k-NN search.
|
@@ -26,10 +33,12 @@ Requires-Python: >=3.8
|
|
26
33
|
Description-Content-Type: text/markdown
|
27
34
|
Project-URL: Homepage, https://github.com/Elan456/fastquadtree
|
28
35
|
Project-URL: Repository, https://github.com/Elan456/fastquadtree
|
36
|
+
Project-URL: Documentation, https://elan456.github.io/fastquadtree/
|
29
37
|
Project-URL: Issues, https://github.com/Elan456/fastquadtree/issues
|
30
38
|
|
31
39
|
# fastquadtree
|
32
40
|
|
41
|
+
[](https://elan456.github.io/fastquadtree/)
|
33
42
|
[](https://pypi.org/project/fastquadtree/)
|
34
43
|
[](https://pypi.org/project/fastquadtree/)
|
35
44
|
[](https://pypi.org/project/fastquadtree/#files)
|
@@ -37,7 +46,7 @@ Project-URL: Issues, https://github.com/Elan456/fastquadtree/issues
|
|
37
46
|
|
38
47
|
[](https://pepy.tech/projects/fastquadtree)
|
39
48
|
|
40
|
-
[](https://github.com/Elan456/fastquadtree/actions/workflows/
|
49
|
+
[](https://github.com/Elan456/fastquadtree/actions/workflows/release.yml)
|
41
50
|
[](https://codecov.io/gh/Elan456/fastquadtree)
|
42
51
|
|
43
52
|
[](https://pyo3.rs/)
|
@@ -51,6 +60,8 @@ Project-URL: Issues, https://github.com/Elan456/fastquadtree/issues
|
|
51
60
|
|
52
61
|
Rust-optimized quadtree with a simple Python API.
|
53
62
|
|
63
|
+
👉 **Docs:** https://elan456.github.io/fastquadtree/
|
64
|
+
|
54
65
|
- Python package: **`fastquadtree`**
|
55
66
|
- Python ≥ 3.8
|
56
67
|
- Import path: `from fastquadtree import QuadTree`
|
@@ -66,7 +77,6 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
66
77
|
|
67
78
|
### Summary (largest dataset, PyQtree baseline)
|
68
79
|
- Points: **250,000**, Queries: **500**
|
69
|
-
--------------------
|
70
80
|
- Fastest total: **fastquadtree** at **0.120 s**
|
71
81
|
|
72
82
|
| Library | Build (s) | Query (s) | Total (s) | Speed vs PyQtree |
|
@@ -79,7 +89,7 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
79
89
|
| PyQtree | 1.492 | 0.263 | 1.755 | 1.00× |
|
80
90
|
| quads | 1.407 | 0.484 | 1.890 | 0.93× |
|
81
91
|
|
82
|
-
|
92
|
+
### Benchmark Configuration
|
83
93
|
| Parameter | Value |
|
84
94
|
|---|---:|
|
85
95
|
| Bounds | (0, 0, 1000, 1000) |
|
@@ -87,11 +97,13 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
87
97
|
| Max depth | 16 |
|
88
98
|
| Queries per experiment | 500 |
|
89
99
|
|
100
|
+
See the [benchmark section](https://elan456.github.io/fastquadtree/benchmark/) for details.
|
101
|
+
|
90
102
|
## Install
|
91
103
|
|
92
104
|
```bash
|
93
105
|
pip install fastquadtree
|
94
|
-
|
106
|
+
```
|
95
107
|
|
96
108
|
If you are developing locally:
|
97
109
|
|
@@ -101,76 +113,12 @@ maturin develop --release
|
|
101
113
|
```
|
102
114
|
|
103
115
|
## Quickstart
|
104
|
-
|
105
|
-
```python
|
106
|
-
from fastquadtree import QuadTree
|
107
|
-
|
108
|
-
# Bounds are (min_x, min_y, max_x, max_y)
|
109
|
-
qt = QuadTree(bounds=(0, 0, 1000, 1000), capacity=20) # max_depth is optional
|
110
|
-
|
111
|
-
# Insert points with auto ids
|
112
|
-
id1 = qt.insert((10, 10))
|
113
|
-
id2 = qt.insert((200, 300))
|
114
|
-
id3 = qt.insert((999, 500), id=42) # you can supply your own id
|
115
|
-
|
116
|
-
# Axis-aligned rectangle query
|
117
|
-
hits = qt.query((0, 0, 250, 350)) # returns [(id, x, y), ...] by default
|
118
|
-
print(hits) # e.g. [(1, 10.0, 10.0), (2, 200.0, 300.0)]
|
119
|
-
|
120
|
-
# Nearest neighbor
|
121
|
-
best = qt.nearest_neighbor((210, 310)) # -> (id, x, y) or None
|
122
|
-
print(best)
|
123
|
-
|
124
|
-
# k-nearest neighbors
|
125
|
-
top3 = qt.nearest_neighbors((210, 310), 3)
|
126
|
-
print(top3) # list of up to 3 (id, x, y) tuples
|
127
|
-
|
128
|
-
# Delete items by ID and location
|
129
|
-
deleted = qt.delete(id2, (200, 300)) # True if found and deleted
|
130
|
-
print(f"Deleted: {deleted}")
|
131
|
-
print(f"Remaining items: {qt.count_items()}")
|
132
|
-
|
133
|
-
# For object tracking with track_objects=True
|
134
|
-
qt_tracked = QuadTree((0, 0, 1000, 1000), capacity=4, track_objects=True)
|
135
|
-
player1 = {"name": "Alice", "score": 100}
|
136
|
-
player2 = {"name": "Bob", "score": 200}
|
137
|
-
|
138
|
-
id1 = qt_tracked.insert((50, 50), obj=player1)
|
139
|
-
id2 = qt_tracked.insert((150, 150), obj=player2)
|
140
|
-
|
141
|
-
# Delete by object reference (O(1) lookup!)
|
142
|
-
deleted = qt_tracked.delete_by_object(player1)
|
143
|
-
print(f"Deleted player: {deleted}") # True
|
144
|
-
```
|
145
|
-
|
146
|
-
### Working with Python objects
|
147
|
-
|
148
|
-
You can keep the tree pure and manage your own id → object map, or let the wrapper manage it.
|
149
|
-
|
150
|
-
**Wrapper Managed Objects**
|
151
|
-
|
152
|
-
```python
|
153
|
-
from fastquadtree import QuadTree
|
154
|
-
|
155
|
-
qt = QuadTree((0, 0, 1000, 1000), capacity=16, track_objects=True)
|
156
|
-
|
157
|
-
# Store the object alongside the point
|
158
|
-
qt.insert((25, 40), obj={"name": "apple"})
|
159
|
-
|
160
|
-
# Ask for Item objects within a bounding box
|
161
|
-
items = qt.query((0, 0, 100, 100), as_items=True)
|
162
|
-
for it in items:
|
163
|
-
print(it.id, it.x, it.y, it.obj)
|
164
|
-
```
|
165
|
-
|
166
|
-
You can also attach or replace an object later:
|
167
|
-
|
168
|
-
```python
|
169
|
-
qt.attach(123, my_object) # binds object to id 123
|
170
|
-
```
|
116
|
+
[See the quickstart guide](https://elan456.github.io/fastquadtree/quickstart/)
|
171
117
|
|
172
118
|
## API
|
173
119
|
|
120
|
+
[See the full API](https://elan456.github.io/fastquadtree/api/quadtree/)
|
121
|
+
|
174
122
|
### `QuadTree(bounds, capacity, max_depth=None, track_objects=False, start_id=1)`
|
175
123
|
|
176
124
|
* `bounds` — tuple `(min_x, min_y, max_x, max_y)` defines the 2D area covered by the quadtree
|
@@ -179,36 +127,17 @@ qt.attach(123, my_object) # binds object to id 123
|
|
179
127
|
* `track_objects` — if `True`, the wrapper maintains an id → object map for convenience.
|
180
128
|
* `start_id` — starting value for auto-assigned ids
|
181
129
|
|
182
|
-
###
|
183
|
-
|
184
|
-
Full docs are in the docstrings of the [Python Shim](pysrc/fastquadtree/__init__.py)
|
130
|
+
### Key Methods
|
185
131
|
|
186
132
|
- `insert(xy, *, id=None, obj=None) -> int`
|
187
133
|
|
188
|
-
- `insert_many_points(points) -> int`
|
189
|
-
|
190
134
|
- `query(rect, *, as_items=False) -> list`
|
191
135
|
|
192
136
|
- `nearest_neighbor(xy, *, as_item=False) -> (id, x, y) | Item | None`
|
193
137
|
|
194
|
-
- `nearest_neighbors(xy, k, *, as_items=False) -> list`
|
195
|
-
|
196
138
|
- `delete(id, xy) -> bool`
|
197
139
|
|
198
|
-
|
199
|
-
|
200
|
-
- `attach(id, obj) -> None (requires track_objects=True)`
|
201
|
-
|
202
|
-
- `count_items() -> int`
|
203
|
-
|
204
|
-
- `get(id) -> object | None`
|
205
|
-
|
206
|
-
- `get_all_rectangles() -> list[tuple] (for visualization)`
|
207
|
-
|
208
|
-
### `Item` (returned when `as_items=True`)
|
209
|
-
|
210
|
-
* Attributes: `id`, `x`, `y`, and a lazy `obj` property
|
211
|
-
* Accessing `obj` performs a dictionary lookup only if tracking is enabled
|
140
|
+
There are more methods and object tracking versions in the [docs](https://elan456.github.io/fastquadtree/api/quadtree/).
|
212
141
|
|
213
142
|
### Geometric conventions
|
214
143
|
|
@@ -222,52 +151,17 @@ Full docs are in the docstrings of the [Python Shim](pysrc/fastquadtree/__init__
|
|
222
151
|
* Choose `capacity` so that leaves keep a small batch of points. Typical values are 8 to 64.
|
223
152
|
* If your data is very skewed, set a `max_depth` to prevent long chains.
|
224
153
|
* For fastest local runs, use `maturin develop --release`.
|
225
|
-
* The wrapper
|
226
|
-
|
227
|
-
|
228
|
-
### Native vs Shim Benchmark
|
229
|
-
|
230
|
-
**Setup**
|
231
|
-
- Points: 500,000
|
232
|
-
- Queries: 500
|
233
|
-
- Repeats: 5
|
234
|
-
|
235
|
-
**Timing (seconds)**
|
154
|
+
* The wrapper maintains an object map only if the quadtree was constructed with `track_objects=True`. If you don't need it, leave it off for best performance.
|
155
|
+
* Refer to the [Native vs Shim Benchmark](https://elan456.github.io/fastquadtree/benchmark/#native-vs-shim-benchmark) for overhead details.
|
236
156
|
|
237
|
-
|
238
|
-
|---|---:|---:|---:|
|
239
|
-
| Native | 0.483 | 4.380 | 4.863 |
|
240
|
-
| Shim (no map) | 0.668 | 4.167 | 4.835 |
|
241
|
-
| Shim (track+objs) | 1.153 | 4.458 | 5.610 |
|
157
|
+
### Pygame Ball Pit Demo
|
242
158
|
|
243
|
-
|
159
|
+

|
244
160
|
|
245
|
-
|
246
|
-
|
161
|
+
A simple demo of moving objects with collision detection using **fastquadtree**.
|
162
|
+
You can toggle between quadtree mode and brute-force mode to see the performance difference.
|
247
163
|
|
248
|
-
|
249
|
-
To run the benchmarks yourself, first install the dependencies:
|
250
|
-
|
251
|
-
```bash
|
252
|
-
pip install -r benchmarks/requirements.txt
|
253
|
-
```
|
254
|
-
|
255
|
-
Then run:
|
256
|
-
|
257
|
-
```bash
|
258
|
-
python benchmarks/cross_library_bench.py
|
259
|
-
python benchmarks/benchmark_native_vs_shim.py
|
260
|
-
```
|
261
|
-
|
262
|
-
## Run Visualizer
|
263
|
-
A visualizer is included to help you understand how the quadtree subdivides space.
|
264
|
-
|
265
|
-
```bash
|
266
|
-
pip install -r interactive/requirements.txt
|
267
|
-
python interactive/interactive_v2.py
|
268
|
-
```
|
269
|
-
|
270
|
-
Check the CLI arguments for the cross-library benchmark in `benchmarks/quadtree_bench/main.py`.
|
164
|
+
See the [runnables guide](https://elan456.github.io/fastquadtree/runnables/) for setup instructions.
|
271
165
|
|
272
166
|
## FAQ
|
273
167
|
|
@@ -278,10 +172,7 @@ Allowed. For k-nearest, duplicates are de-duplicated by id. For range queries yo
|
|
278
172
|
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)` for convenient object-based deletion with O(1) lookup. The tree automatically merges nodes when item counts drop below capacity.
|
279
173
|
|
280
174
|
**Can I store rectangles or circles?**
|
281
|
-
|
282
|
-
|
283
|
-
**Threading**
|
284
|
-
Use one tree per thread if you need heavy parallel inserts from Python.
|
175
|
+
Yes, you can store rectangles using the `RectQuadTree` class. Circles can be approximated with bounding boxes. See the [RectQuadTree docs](https://elan456.github.io/fastquadtree/api/rect_quadtree/) for details.
|
285
176
|
|
286
177
|
## License
|
287
178
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# fastquadtree
|
2
2
|
|
3
|
+
[](https://elan456.github.io/fastquadtree/)
|
3
4
|
[](https://pypi.org/project/fastquadtree/)
|
4
5
|
[](https://pypi.org/project/fastquadtree/)
|
5
6
|
[](https://pypi.org/project/fastquadtree/#files)
|
@@ -7,7 +8,7 @@
|
|
7
8
|
|
8
9
|
[](https://pepy.tech/projects/fastquadtree)
|
9
10
|
|
10
|
-
[](https://github.com/Elan456/fastquadtree/actions/workflows/
|
11
|
+
[](https://github.com/Elan456/fastquadtree/actions/workflows/release.yml)
|
11
12
|
[](https://codecov.io/gh/Elan456/fastquadtree)
|
12
13
|
|
13
14
|
[](https://pyo3.rs/)
|
@@ -21,6 +22,8 @@
|
|
21
22
|
|
22
23
|
Rust-optimized quadtree with a simple Python API.
|
23
24
|
|
25
|
+
👉 **Docs:** https://elan456.github.io/fastquadtree/
|
26
|
+
|
24
27
|
- Python package: **`fastquadtree`**
|
25
28
|
- Python ≥ 3.8
|
26
29
|
- Import path: `from fastquadtree import QuadTree`
|
@@ -36,7 +39,6 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
36
39
|
|
37
40
|
### Summary (largest dataset, PyQtree baseline)
|
38
41
|
- Points: **250,000**, Queries: **500**
|
39
|
-
--------------------
|
40
42
|
- Fastest total: **fastquadtree** at **0.120 s**
|
41
43
|
|
42
44
|
| Library | Build (s) | Query (s) | Total (s) | Speed vs PyQtree |
|
@@ -49,7 +51,7 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
49
51
|
| PyQtree | 1.492 | 0.263 | 1.755 | 1.00× |
|
50
52
|
| quads | 1.407 | 0.484 | 1.890 | 0.93× |
|
51
53
|
|
52
|
-
|
54
|
+
### Benchmark Configuration
|
53
55
|
| Parameter | Value |
|
54
56
|
|---|---:|
|
55
57
|
| Bounds | (0, 0, 1000, 1000) |
|
@@ -57,11 +59,13 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
57
59
|
| Max depth | 16 |
|
58
60
|
| Queries per experiment | 500 |
|
59
61
|
|
62
|
+
See the [benchmark section](https://elan456.github.io/fastquadtree/benchmark/) for details.
|
63
|
+
|
60
64
|
## Install
|
61
65
|
|
62
66
|
```bash
|
63
67
|
pip install fastquadtree
|
64
|
-
|
68
|
+
```
|
65
69
|
|
66
70
|
If you are developing locally:
|
67
71
|
|
@@ -71,76 +75,12 @@ maturin develop --release
|
|
71
75
|
```
|
72
76
|
|
73
77
|
## Quickstart
|
74
|
-
|
75
|
-
```python
|
76
|
-
from fastquadtree import QuadTree
|
77
|
-
|
78
|
-
# Bounds are (min_x, min_y, max_x, max_y)
|
79
|
-
qt = QuadTree(bounds=(0, 0, 1000, 1000), capacity=20) # max_depth is optional
|
80
|
-
|
81
|
-
# Insert points with auto ids
|
82
|
-
id1 = qt.insert((10, 10))
|
83
|
-
id2 = qt.insert((200, 300))
|
84
|
-
id3 = qt.insert((999, 500), id=42) # you can supply your own id
|
85
|
-
|
86
|
-
# Axis-aligned rectangle query
|
87
|
-
hits = qt.query((0, 0, 250, 350)) # returns [(id, x, y), ...] by default
|
88
|
-
print(hits) # e.g. [(1, 10.0, 10.0), (2, 200.0, 300.0)]
|
89
|
-
|
90
|
-
# Nearest neighbor
|
91
|
-
best = qt.nearest_neighbor((210, 310)) # -> (id, x, y) or None
|
92
|
-
print(best)
|
93
|
-
|
94
|
-
# k-nearest neighbors
|
95
|
-
top3 = qt.nearest_neighbors((210, 310), 3)
|
96
|
-
print(top3) # list of up to 3 (id, x, y) tuples
|
97
|
-
|
98
|
-
# Delete items by ID and location
|
99
|
-
deleted = qt.delete(id2, (200, 300)) # True if found and deleted
|
100
|
-
print(f"Deleted: {deleted}")
|
101
|
-
print(f"Remaining items: {qt.count_items()}")
|
102
|
-
|
103
|
-
# For object tracking with track_objects=True
|
104
|
-
qt_tracked = QuadTree((0, 0, 1000, 1000), capacity=4, track_objects=True)
|
105
|
-
player1 = {"name": "Alice", "score": 100}
|
106
|
-
player2 = {"name": "Bob", "score": 200}
|
107
|
-
|
108
|
-
id1 = qt_tracked.insert((50, 50), obj=player1)
|
109
|
-
id2 = qt_tracked.insert((150, 150), obj=player2)
|
110
|
-
|
111
|
-
# Delete by object reference (O(1) lookup!)
|
112
|
-
deleted = qt_tracked.delete_by_object(player1)
|
113
|
-
print(f"Deleted player: {deleted}") # True
|
114
|
-
```
|
115
|
-
|
116
|
-
### Working with Python objects
|
117
|
-
|
118
|
-
You can keep the tree pure and manage your own id → object map, or let the wrapper manage it.
|
119
|
-
|
120
|
-
**Wrapper Managed Objects**
|
121
|
-
|
122
|
-
```python
|
123
|
-
from fastquadtree import QuadTree
|
124
|
-
|
125
|
-
qt = QuadTree((0, 0, 1000, 1000), capacity=16, track_objects=True)
|
126
|
-
|
127
|
-
# Store the object alongside the point
|
128
|
-
qt.insert((25, 40), obj={"name": "apple"})
|
129
|
-
|
130
|
-
# Ask for Item objects within a bounding box
|
131
|
-
items = qt.query((0, 0, 100, 100), as_items=True)
|
132
|
-
for it in items:
|
133
|
-
print(it.id, it.x, it.y, it.obj)
|
134
|
-
```
|
135
|
-
|
136
|
-
You can also attach or replace an object later:
|
137
|
-
|
138
|
-
```python
|
139
|
-
qt.attach(123, my_object) # binds object to id 123
|
140
|
-
```
|
78
|
+
[See the quickstart guide](https://elan456.github.io/fastquadtree/quickstart/)
|
141
79
|
|
142
80
|
## API
|
143
81
|
|
82
|
+
[See the full API](https://elan456.github.io/fastquadtree/api/quadtree/)
|
83
|
+
|
144
84
|
### `QuadTree(bounds, capacity, max_depth=None, track_objects=False, start_id=1)`
|
145
85
|
|
146
86
|
* `bounds` — tuple `(min_x, min_y, max_x, max_y)` defines the 2D area covered by the quadtree
|
@@ -149,36 +89,17 @@ qt.attach(123, my_object) # binds object to id 123
|
|
149
89
|
* `track_objects` — if `True`, the wrapper maintains an id → object map for convenience.
|
150
90
|
* `start_id` — starting value for auto-assigned ids
|
151
91
|
|
152
|
-
###
|
153
|
-
|
154
|
-
Full docs are in the docstrings of the [Python Shim](pysrc/fastquadtree/__init__.py)
|
92
|
+
### Key Methods
|
155
93
|
|
156
94
|
- `insert(xy, *, id=None, obj=None) -> int`
|
157
95
|
|
158
|
-
- `insert_many_points(points) -> int`
|
159
|
-
|
160
96
|
- `query(rect, *, as_items=False) -> list`
|
161
97
|
|
162
98
|
- `nearest_neighbor(xy, *, as_item=False) -> (id, x, y) | Item | None`
|
163
99
|
|
164
|
-
- `nearest_neighbors(xy, k, *, as_items=False) -> list`
|
165
|
-
|
166
100
|
- `delete(id, xy) -> bool`
|
167
101
|
|
168
|
-
|
169
|
-
|
170
|
-
- `attach(id, obj) -> None (requires track_objects=True)`
|
171
|
-
|
172
|
-
- `count_items() -> int`
|
173
|
-
|
174
|
-
- `get(id) -> object | None`
|
175
|
-
|
176
|
-
- `get_all_rectangles() -> list[tuple] (for visualization)`
|
177
|
-
|
178
|
-
### `Item` (returned when `as_items=True`)
|
179
|
-
|
180
|
-
* Attributes: `id`, `x`, `y`, and a lazy `obj` property
|
181
|
-
* Accessing `obj` performs a dictionary lookup only if tracking is enabled
|
102
|
+
There are more methods and object tracking versions in the [docs](https://elan456.github.io/fastquadtree/api/quadtree/).
|
182
103
|
|
183
104
|
### Geometric conventions
|
184
105
|
|
@@ -192,52 +113,17 @@ Full docs are in the docstrings of the [Python Shim](pysrc/fastquadtree/__init__
|
|
192
113
|
* Choose `capacity` so that leaves keep a small batch of points. Typical values are 8 to 64.
|
193
114
|
* If your data is very skewed, set a `max_depth` to prevent long chains.
|
194
115
|
* For fastest local runs, use `maturin develop --release`.
|
195
|
-
* The wrapper
|
196
|
-
|
197
|
-
|
198
|
-
### Native vs Shim Benchmark
|
199
|
-
|
200
|
-
**Setup**
|
201
|
-
- Points: 500,000
|
202
|
-
- Queries: 500
|
203
|
-
- Repeats: 5
|
204
|
-
|
205
|
-
**Timing (seconds)**
|
116
|
+
* The wrapper maintains an object map only if the quadtree was constructed with `track_objects=True`. If you don't need it, leave it off for best performance.
|
117
|
+
* Refer to the [Native vs Shim Benchmark](https://elan456.github.io/fastquadtree/benchmark/#native-vs-shim-benchmark) for overhead details.
|
206
118
|
|
207
|
-
|
208
|
-
|---|---:|---:|---:|
|
209
|
-
| Native | 0.483 | 4.380 | 4.863 |
|
210
|
-
| Shim (no map) | 0.668 | 4.167 | 4.835 |
|
211
|
-
| Shim (track+objs) | 1.153 | 4.458 | 5.610 |
|
119
|
+
### Pygame Ball Pit Demo
|
212
120
|
|
213
|
-
|
121
|
+

|
214
122
|
|
215
|
-
|
216
|
-
|
123
|
+
A simple demo of moving objects with collision detection using **fastquadtree**.
|
124
|
+
You can toggle between quadtree mode and brute-force mode to see the performance difference.
|
217
125
|
|
218
|
-
|
219
|
-
To run the benchmarks yourself, first install the dependencies:
|
220
|
-
|
221
|
-
```bash
|
222
|
-
pip install -r benchmarks/requirements.txt
|
223
|
-
```
|
224
|
-
|
225
|
-
Then run:
|
226
|
-
|
227
|
-
```bash
|
228
|
-
python benchmarks/cross_library_bench.py
|
229
|
-
python benchmarks/benchmark_native_vs_shim.py
|
230
|
-
```
|
231
|
-
|
232
|
-
## Run Visualizer
|
233
|
-
A visualizer is included to help you understand how the quadtree subdivides space.
|
234
|
-
|
235
|
-
```bash
|
236
|
-
pip install -r interactive/requirements.txt
|
237
|
-
python interactive/interactive_v2.py
|
238
|
-
```
|
239
|
-
|
240
|
-
Check the CLI arguments for the cross-library benchmark in `benchmarks/quadtree_bench/main.py`.
|
126
|
+
See the [runnables guide](https://elan456.github.io/fastquadtree/runnables/) for setup instructions.
|
241
127
|
|
242
128
|
## FAQ
|
243
129
|
|
@@ -248,10 +134,7 @@ Allowed. For k-nearest, duplicates are de-duplicated by id. For range queries yo
|
|
248
134
|
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)` for convenient object-based deletion with O(1) lookup. The tree automatically merges nodes when item counts drop below capacity.
|
249
135
|
|
250
136
|
**Can I store rectangles or circles?**
|
251
|
-
|
252
|
-
|
253
|
-
**Threading**
|
254
|
-
Use one tree per thread if you need heavy parallel inserts from Python.
|
137
|
+
Yes, you can store rectangles using the `RectQuadTree` class. Circles can be approximated with bounding boxes. See the [RectQuadTree docs](https://elan456.github.io/fastquadtree/api/rect_quadtree/) for details.
|
255
138
|
|
256
139
|
## License
|
257
140
|
|
Binary file
|
@@ -7,6 +7,7 @@ import random
|
|
7
7
|
import statistics as stats
|
8
8
|
from time import perf_counter as now
|
9
9
|
|
10
|
+
from system_info_collector import collect_system_info, format_system_info_markdown_lite
|
10
11
|
from tqdm import tqdm
|
11
12
|
|
12
13
|
from fastquadtree import QuadTree as ShimQuadTree
|
@@ -55,10 +56,10 @@ def bench_shim(points, queries, *, track_objects: bool, with_objs: bool):
|
|
55
56
|
)
|
56
57
|
if with_objs:
|
57
58
|
for i, p in enumerate(points):
|
58
|
-
qt.insert(p,
|
59
|
+
qt.insert(p, id_=i, obj=i) # store a tiny object
|
59
60
|
else:
|
60
61
|
for i, p in enumerate(points):
|
61
|
-
qt.insert(p,
|
62
|
+
qt.insert(p, id_=i)
|
62
63
|
t_build = now() - t0
|
63
64
|
|
64
65
|
t0 = now()
|
@@ -133,14 +134,14 @@ def main():
|
|
133
134
|
return f"{x:.3f}"
|
134
135
|
|
135
136
|
md = f"""
|
136
|
-
|
137
|
+
## Native vs Shim
|
137
138
|
|
138
|
-
|
139
|
+
### Configuration
|
139
140
|
- Points: {args.points:,}
|
140
141
|
- Queries: {args.queries}
|
141
142
|
- Repeats: {args.repeats}
|
142
143
|
|
143
|
-
|
144
|
+
### Results
|
144
145
|
|
145
146
|
| Variant | Build | Query | Total |
|
146
147
|
|---|---:|---:|---:|
|
@@ -148,13 +149,18 @@ def main():
|
|
148
149
|
| Shim (no map) | {fmt(s_build_no_map)} | {fmt(s_query_no_map)} | {fmt(s_build_no_map + s_query_no_map)} |
|
149
150
|
| Shim (track+objs) | {fmt(s_build_map)} | {fmt(s_query_map)} | {fmt(s_build_map + s_query_map)} |
|
150
151
|
|
151
|
-
|
152
|
+
### Summary
|
152
153
|
|
153
|
-
|
154
|
-
|
154
|
+
Using the shim with object tracking increases build time by {fmt(s_build_map / n_build)}x and query time by {fmt(s_query_map / n_query)}x.
|
155
|
+
**Total slowdown = {fmt((s_build_map + s_query_map) / (n_build + n_query))}x.**
|
156
|
+
|
157
|
+
Adding the object map only impacts the build time, not the query time.
|
155
158
|
"""
|
156
159
|
print(md.strip())
|
157
160
|
|
161
|
+
info = collect_system_info()
|
162
|
+
print(format_system_info_markdown_lite(info))
|
163
|
+
|
158
164
|
|
159
165
|
if __name__ == "__main__":
|
160
166
|
main()
|
@@ -255,8 +255,8 @@ class BenchmarkRunner:
|
|
255
255
|
experiment_bar.close()
|
256
256
|
|
257
257
|
# Add metadata to results
|
258
|
-
results["engines"] = engines
|
259
|
-
results["config"] = self.config
|
258
|
+
results["engines"] = engines # pyright: ignore[reportArgumentType]
|
259
|
+
results["config"] = self.config # pyright: ignore[reportArgumentType]
|
260
260
|
|
261
261
|
return results
|
262
262
|
|
@@ -277,7 +277,6 @@ class BenchmarkRunner:
|
|
277
277
|
print(
|
278
278
|
f"- Points: **{config.experiments[i]:,}**, Queries: **{config.n_queries}**"
|
279
279
|
)
|
280
|
-
print("--------------------")
|
281
280
|
|
282
281
|
# Find fastest and show key results
|
283
282
|
ranked = sorted(
|
@@ -12,4 +12,9 @@ fastquadtree>=0.1.0
|
|
12
12
|
quads>=1.1.0 # pure-Python quadtree
|
13
13
|
nontree>=1.0.5 # pure-Python PR quadtree (TreeMap mode=4)
|
14
14
|
rtree>=1.3.0 # R-tree comparator with wheels for Win/macOS/Linux
|
15
|
-
shapely>=2.0.0 # STRtree comparator
|
15
|
+
shapely>=2.0.0 # STRtree comparator
|
16
|
+
|
17
|
+
# For getting system info
|
18
|
+
psutil
|
19
|
+
py-cpuinfo
|
20
|
+
distro
|