fastquadtree 0.7.0__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.7.0 → fastquadtree-0.8.0}/Cargo.lock +1 -1
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/Cargo.toml +1 -1
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/PKG-INFO +17 -149
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/README.md +13 -146
- fastquadtree-0.8.0/assets/ballpit.png +0 -0
- {fastquadtree-0.7.0 → 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.7.0 → fastquadtree-0.8.0}/benchmarks/quadtree_bench/engines.py +1 -1
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/benchmarks/quadtree_bench/runner.py +2 -11
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/benchmarks/requirements.txt +1 -2
- fastquadtree-0.8.0/benchmarks/system_info_collector.py +259 -0
- fastquadtree-0.8.0/docs/api/point_item.md +4 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/docs/api/quadtree.md +2 -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.7.0 → fastquadtree-0.8.0}/docs/quickstart.md +4 -14
- fastquadtree-0.8.0/docs/runnables.md +33 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/interactive/interactive.py +1 -1
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/interactive/interactive_v2.py +1 -1
- fastquadtree-0.8.0/interactive/interactive_v2_rect.py +432 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/interactive/requirements.txt +1 -1
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/mkdocs.yml +5 -1
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/pyproject.toml +11 -4
- fastquadtree-0.8.0/pysrc/fastquadtree/__init__.py +5 -0
- fastquadtree-0.8.0/pysrc/fastquadtree/_base_quadtree.py +263 -0
- {fastquadtree-0.7.0 → 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.7.0 → 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.7.0 → fastquadtree-0.8.0}/tests/rectangle_traversal.rs +8 -8
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/tests/test_bimap.py +1 -1
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/tests/test_delete.rs +12 -12
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/tests/test_delete_python.py +4 -4
- fastquadtree-0.8.0/tests/test_rect_quadtree.py +199 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/tests/test_unconventional_bounds.py +2 -2
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/tests/test_wrapper_edges.py +6 -6
- fastquadtree-0.7.0/assets/ballpit.png +0 -0
- fastquadtree-0.7.0/benchmarks/cross_library_bench.py +0 -3
- fastquadtree-0.7.0/benchmarks/quadtree_bench/system_info_collector.py +0 -140
- fastquadtree-0.7.0/docs/api/item.md +0 -2
- fastquadtree-0.7.0/docs/index.md +0 -4
- fastquadtree-0.7.0/pysrc/fastquadtree/__init__.py +0 -417
- fastquadtree-0.7.0/pysrc/fastquadtree/__init__.pyi +0 -71
- fastquadtree-0.7.0/pysrc/fastquadtree/_item.py +0 -24
- fastquadtree-0.7.0/src/lib.rs +0 -99
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/.github/workflows/docs.yml +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/.github/workflows/release.yml +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/.gitignore +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/.pre-commit-config.yaml +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/LICENSE +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/assets/interactive_v2_screenshot.png +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/assets/quadtree_bench_throughput.png +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/assets/quadtree_bench_time.png +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/benchmarks/quadtree_bench/__init__.py +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/benchmarks/quadtree_bench/main.py +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/benchmarks/quadtree_bench/plotting.py +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/benchmarks/runner.py +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/interactive/ballpit.py +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/pysrc/fastquadtree/py.typed +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/src/geom.rs +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/tests/insertions.rs +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/tests/nearest_neighbor.rs +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/tests/query.rs +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/tests/test_clear.py +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/tests/test_delete_by_object.py +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/tests/test_python.py +0 -0
- {fastquadtree-0.7.0 → fastquadtree-0.8.0}/tests/unconventional_bounds.rs +0 -0
@@ -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,8 +12,8 @@ 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'
|
@@ -23,6 +23,7 @@ Requires-Dist: mkdocstrings[python] ; extra == 'dev'
|
|
23
23
|
Requires-Dist: mkdocs-autorefs ; extra == 'dev'
|
24
24
|
Requires-Dist: mkdocs-git-revision-date-localized-plugin ; extra == 'dev'
|
25
25
|
Requires-Dist: mkdocs-minify-plugin ; extra == 'dev'
|
26
|
+
Requires-Dist: maturin>=1.5 ; extra == 'dev'
|
26
27
|
Provides-Extra: dev
|
27
28
|
License-File: LICENSE
|
28
29
|
Summary: Rust-accelerated quadtree for Python with fast inserts, range queries, and k-NN search.
|
@@ -45,7 +46,7 @@ Project-URL: Issues, https://github.com/Elan456/fastquadtree/issues
|
|
45
46
|
|
46
47
|
[](https://pepy.tech/projects/fastquadtree)
|
47
48
|
|
48
|
-
[](https://github.com/Elan456/fastquadtree/actions/workflows/
|
49
|
+
[](https://github.com/Elan456/fastquadtree/actions/workflows/release.yml)
|
49
50
|
[](https://codecov.io/gh/Elan456/fastquadtree)
|
50
51
|
|
51
52
|
[](https://pyo3.rs/)
|
@@ -76,7 +77,6 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
76
77
|
|
77
78
|
### Summary (largest dataset, PyQtree baseline)
|
78
79
|
- Points: **250,000**, Queries: **500**
|
79
|
-
--------------------
|
80
80
|
- Fastest total: **fastquadtree** at **0.120 s**
|
81
81
|
|
82
82
|
| Library | Build (s) | Query (s) | Total (s) | Speed vs PyQtree |
|
@@ -89,7 +89,7 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
89
89
|
| PyQtree | 1.492 | 0.263 | 1.755 | 1.00× |
|
90
90
|
| quads | 1.407 | 0.484 | 1.890 | 0.93× |
|
91
91
|
|
92
|
-
|
92
|
+
### Benchmark Configuration
|
93
93
|
| Parameter | Value |
|
94
94
|
|---|---:|
|
95
95
|
| Bounds | (0, 0, 1000, 1000) |
|
@@ -97,11 +97,13 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
97
97
|
| Max depth | 16 |
|
98
98
|
| Queries per experiment | 500 |
|
99
99
|
|
100
|
+
See the [benchmark section](https://elan456.github.io/fastquadtree/benchmark/) for details.
|
101
|
+
|
100
102
|
## Install
|
101
103
|
|
102
104
|
```bash
|
103
105
|
pip install fastquadtree
|
104
|
-
|
106
|
+
```
|
105
107
|
|
106
108
|
If you are developing locally:
|
107
109
|
|
@@ -111,77 +113,11 @@ maturin develop --release
|
|
111
113
|
```
|
112
114
|
|
113
115
|
## Quickstart
|
114
|
-
|
115
|
-
```python
|
116
|
-
from fastquadtree import QuadTree
|
117
|
-
|
118
|
-
# Bounds are (min_x, min_y, max_x, max_y)
|
119
|
-
qt = QuadTree(bounds=(0, 0, 1000, 1000), capacity=20) # max_depth is optional
|
120
|
-
|
121
|
-
# Insert points with auto ids
|
122
|
-
id1 = qt.insert((10, 10))
|
123
|
-
id2 = qt.insert((200, 300))
|
124
|
-
id3 = qt.insert((999, 500), id=42) # you can supply your own id
|
125
|
-
|
126
|
-
# Axis-aligned rectangle query
|
127
|
-
hits = qt.query((0, 0, 250, 350)) # returns [(id, x, y), ...] by default
|
128
|
-
print(hits) # e.g. [(1, 10.0, 10.0), (2, 200.0, 300.0)]
|
129
|
-
|
130
|
-
# Nearest neighbor
|
131
|
-
best = qt.nearest_neighbor((210, 310)) # -> (id, x, y) or None
|
132
|
-
print(best)
|
133
|
-
|
134
|
-
# k-nearest neighbors
|
135
|
-
top3 = qt.nearest_neighbors((210, 310), 3)
|
136
|
-
print(top3) # list of up to 3 (id, x, y) tuples
|
137
|
-
|
138
|
-
# Delete items by ID and location
|
139
|
-
deleted = qt.delete(id2, (200, 300)) # True if found and deleted
|
140
|
-
print(f"Deleted: {deleted}")
|
141
|
-
print(f"Remaining items: {qt.count_items()}")
|
142
|
-
|
143
|
-
# For object tracking with track_objects=True
|
144
|
-
qt_tracked = QuadTree((0, 0, 1000, 1000), capacity=4, track_objects=True)
|
145
|
-
player1 = {"name": "Alice", "score": 100}
|
146
|
-
player2 = {"name": "Bob", "score": 200}
|
147
|
-
|
148
|
-
id1 = qt_tracked.insert((50, 50), obj=player1)
|
149
|
-
id2 = qt_tracked.insert((150, 150), obj=player2)
|
150
|
-
|
151
|
-
# Delete by object reference (O(1) lookup!)
|
152
|
-
deleted = qt_tracked.delete_by_object(player1)
|
153
|
-
print(f"Deleted player: {deleted}") # True
|
154
|
-
```
|
155
|
-
|
156
|
-
### Working with Python objects
|
157
|
-
|
158
|
-
You can keep the tree pure and manage your own id → object map, or let the wrapper manage it.
|
159
|
-
|
160
|
-
**Wrapper Managed Objects**
|
161
|
-
|
162
|
-
```python
|
163
|
-
from fastquadtree import QuadTree
|
164
|
-
|
165
|
-
qt = QuadTree((0, 0, 1000, 1000), capacity=16, track_objects=True)
|
166
|
-
|
167
|
-
# Store the object alongside the point
|
168
|
-
qt.insert((25, 40), obj={"name": "apple"})
|
169
|
-
|
170
|
-
# Ask for Item objects within a bounding box
|
171
|
-
items = qt.query((0, 0, 100, 100), as_items=True)
|
172
|
-
for it in items:
|
173
|
-
print(it.id, it.x, it.y, it.obj)
|
174
|
-
```
|
175
|
-
|
176
|
-
You can also attach or replace an object later:
|
177
|
-
|
178
|
-
```python
|
179
|
-
qt.attach(123, my_object) # binds object to id 123
|
180
|
-
```
|
116
|
+
[See the quickstart guide](https://elan456.github.io/fastquadtree/quickstart/)
|
181
117
|
|
182
118
|
## API
|
183
119
|
|
184
|
-
[
|
120
|
+
[See the full API](https://elan456.github.io/fastquadtree/api/quadtree/)
|
185
121
|
|
186
122
|
### `QuadTree(bounds, capacity, max_depth=None, track_objects=False, start_id=1)`
|
187
123
|
|
@@ -191,38 +127,17 @@ qt.attach(123, my_object) # binds object to id 123
|
|
191
127
|
* `track_objects` — if `True`, the wrapper maintains an id → object map for convenience.
|
192
128
|
* `start_id` — starting value for auto-assigned ids
|
193
129
|
|
194
|
-
###
|
130
|
+
### Key Methods
|
195
131
|
|
196
132
|
- `insert(xy, *, id=None, obj=None) -> int`
|
197
133
|
|
198
|
-
- `insert_many_points(points) -> int`
|
199
|
-
|
200
134
|
- `query(rect, *, as_items=False) -> list`
|
201
135
|
|
202
136
|
- `nearest_neighbor(xy, *, as_item=False) -> (id, x, y) | Item | None`
|
203
137
|
|
204
|
-
- `nearest_neighbors(xy, k, *, as_items=False) -> list`
|
205
|
-
|
206
138
|
- `delete(id, xy) -> bool`
|
207
139
|
|
208
|
-
|
209
|
-
|
210
|
-
- `clear(*, reset_ids=False) -> None`
|
211
|
-
|
212
|
-
- `attach(id, obj) -> None (requires track_objects=True)`
|
213
|
-
|
214
|
-
- `count_items() -> int`
|
215
|
-
|
216
|
-
- `get(id) -> object | None`
|
217
|
-
|
218
|
-
- `get_all_rectangles() -> list[tuple] (for visualization)`
|
219
|
-
|
220
|
-
- `get_all_objects() -> list[object] (requires track_objects=True)`
|
221
|
-
|
222
|
-
### `Item` (returned when `as_items=True`)
|
223
|
-
|
224
|
-
* Attributes: `id`, `x`, `y`, and a lazy `obj` property
|
225
|
-
* 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/).
|
226
141
|
|
227
142
|
### Geometric conventions
|
228
143
|
|
@@ -236,52 +151,8 @@ qt.attach(123, my_object) # binds object to id 123
|
|
236
151
|
* Choose `capacity` so that leaves keep a small batch of points. Typical values are 8 to 64.
|
237
152
|
* If your data is very skewed, set a `max_depth` to prevent long chains.
|
238
153
|
* For fastest local runs, use `maturin develop --release`.
|
239
|
-
* The wrapper
|
240
|
-
|
241
|
-
|
242
|
-
### Native vs Shim Benchmark
|
243
|
-
|
244
|
-
**Setup**
|
245
|
-
- Points: 500,000
|
246
|
-
- Queries: 500
|
247
|
-
- Repeats: 5
|
248
|
-
|
249
|
-
**Timing (seconds)**
|
250
|
-
|
251
|
-
| Variant | Build | Query | Total |
|
252
|
-
|---|---:|---:|---:|
|
253
|
-
| Native | 0.483 | 4.380 | 4.863 |
|
254
|
-
| Shim (no map) | 0.668 | 4.167 | 4.835 |
|
255
|
-
| Shim (track+objs) | 1.153 | 4.458 | 5.610 |
|
256
|
-
|
257
|
-
**Overhead vs Native**
|
258
|
-
|
259
|
-
- No map: build 1.38x, query 0.95x, total 0.99x
|
260
|
-
- Track + objs: build 2.39x, query 1.02x, total 1.15x
|
261
|
-
|
262
|
-
### Run benchmarks
|
263
|
-
To run the benchmarks yourself, first install the dependencies:
|
264
|
-
|
265
|
-
```bash
|
266
|
-
pip install -r benchmarks/requirements.txt
|
267
|
-
```
|
268
|
-
|
269
|
-
Then run:
|
270
|
-
|
271
|
-
```bash
|
272
|
-
python benchmarks/cross_library_bench.py
|
273
|
-
python benchmarks/benchmark_native_vs_shim.py
|
274
|
-
```
|
275
|
-
|
276
|
-
Check the CLI arguments for the cross-library benchmark in `benchmarks/quadtree_bench/main.py`.
|
277
|
-
|
278
|
-
## Run Visualizer
|
279
|
-
A visualizer is included to help you understand how the quadtree subdivides space.
|
280
|
-
|
281
|
-
```bash
|
282
|
-
pip install -r interactive/requirements.txt
|
283
|
-
python interactive/interactive_v2.py
|
284
|
-
```
|
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.
|
285
156
|
|
286
157
|
### Pygame Ball Pit Demo
|
287
158
|
|
@@ -290,10 +161,7 @@ python interactive/interactive_v2.py
|
|
290
161
|
A simple demo of moving objects with collision detection using **fastquadtree**.
|
291
162
|
You can toggle between quadtree mode and brute-force mode to see the performance difference.
|
292
163
|
|
293
|
-
|
294
|
-
pip install -r interactive/requirements.txt
|
295
|
-
python interactive/ball_pit.py
|
296
|
-
```
|
164
|
+
See the [runnables guide](https://elan456.github.io/fastquadtree/runnables/) for setup instructions.
|
297
165
|
|
298
166
|
## FAQ
|
299
167
|
|
@@ -304,7 +172,7 @@ Allowed. For k-nearest, duplicates are de-duplicated by id. For range queries yo
|
|
304
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.
|
305
173
|
|
306
174
|
**Can I store rectangles or circles?**
|
307
|
-
|
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.
|
308
176
|
|
309
177
|
## License
|
310
178
|
|
@@ -8,7 +8,7 @@
|
|
8
8
|
|
9
9
|
[](https://pepy.tech/projects/fastquadtree)
|
10
10
|
|
11
|
-
[](https://github.com/Elan456/fastquadtree/actions/workflows/
|
11
|
+
[](https://github.com/Elan456/fastquadtree/actions/workflows/release.yml)
|
12
12
|
[](https://codecov.io/gh/Elan456/fastquadtree)
|
13
13
|
|
14
14
|
[](https://pyo3.rs/)
|
@@ -39,7 +39,6 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
39
39
|
|
40
40
|
### Summary (largest dataset, PyQtree baseline)
|
41
41
|
- Points: **250,000**, Queries: **500**
|
42
|
-
--------------------
|
43
42
|
- Fastest total: **fastquadtree** at **0.120 s**
|
44
43
|
|
45
44
|
| Library | Build (s) | Query (s) | Total (s) | Speed vs PyQtree |
|
@@ -52,7 +51,7 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
52
51
|
| PyQtree | 1.492 | 0.263 | 1.755 | 1.00× |
|
53
52
|
| quads | 1.407 | 0.484 | 1.890 | 0.93× |
|
54
53
|
|
55
|
-
|
54
|
+
### Benchmark Configuration
|
56
55
|
| Parameter | Value |
|
57
56
|
|---|---:|
|
58
57
|
| Bounds | (0, 0, 1000, 1000) |
|
@@ -60,11 +59,13 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
60
59
|
| Max depth | 16 |
|
61
60
|
| Queries per experiment | 500 |
|
62
61
|
|
62
|
+
See the [benchmark section](https://elan456.github.io/fastquadtree/benchmark/) for details.
|
63
|
+
|
63
64
|
## Install
|
64
65
|
|
65
66
|
```bash
|
66
67
|
pip install fastquadtree
|
67
|
-
|
68
|
+
```
|
68
69
|
|
69
70
|
If you are developing locally:
|
70
71
|
|
@@ -74,77 +75,11 @@ maturin develop --release
|
|
74
75
|
```
|
75
76
|
|
76
77
|
## Quickstart
|
77
|
-
|
78
|
-
```python
|
79
|
-
from fastquadtree import QuadTree
|
80
|
-
|
81
|
-
# Bounds are (min_x, min_y, max_x, max_y)
|
82
|
-
qt = QuadTree(bounds=(0, 0, 1000, 1000), capacity=20) # max_depth is optional
|
83
|
-
|
84
|
-
# Insert points with auto ids
|
85
|
-
id1 = qt.insert((10, 10))
|
86
|
-
id2 = qt.insert((200, 300))
|
87
|
-
id3 = qt.insert((999, 500), id=42) # you can supply your own id
|
88
|
-
|
89
|
-
# Axis-aligned rectangle query
|
90
|
-
hits = qt.query((0, 0, 250, 350)) # returns [(id, x, y), ...] by default
|
91
|
-
print(hits) # e.g. [(1, 10.0, 10.0), (2, 200.0, 300.0)]
|
92
|
-
|
93
|
-
# Nearest neighbor
|
94
|
-
best = qt.nearest_neighbor((210, 310)) # -> (id, x, y) or None
|
95
|
-
print(best)
|
96
|
-
|
97
|
-
# k-nearest neighbors
|
98
|
-
top3 = qt.nearest_neighbors((210, 310), 3)
|
99
|
-
print(top3) # list of up to 3 (id, x, y) tuples
|
100
|
-
|
101
|
-
# Delete items by ID and location
|
102
|
-
deleted = qt.delete(id2, (200, 300)) # True if found and deleted
|
103
|
-
print(f"Deleted: {deleted}")
|
104
|
-
print(f"Remaining items: {qt.count_items()}")
|
105
|
-
|
106
|
-
# For object tracking with track_objects=True
|
107
|
-
qt_tracked = QuadTree((0, 0, 1000, 1000), capacity=4, track_objects=True)
|
108
|
-
player1 = {"name": "Alice", "score": 100}
|
109
|
-
player2 = {"name": "Bob", "score": 200}
|
110
|
-
|
111
|
-
id1 = qt_tracked.insert((50, 50), obj=player1)
|
112
|
-
id2 = qt_tracked.insert((150, 150), obj=player2)
|
113
|
-
|
114
|
-
# Delete by object reference (O(1) lookup!)
|
115
|
-
deleted = qt_tracked.delete_by_object(player1)
|
116
|
-
print(f"Deleted player: {deleted}") # True
|
117
|
-
```
|
118
|
-
|
119
|
-
### Working with Python objects
|
120
|
-
|
121
|
-
You can keep the tree pure and manage your own id → object map, or let the wrapper manage it.
|
122
|
-
|
123
|
-
**Wrapper Managed Objects**
|
124
|
-
|
125
|
-
```python
|
126
|
-
from fastquadtree import QuadTree
|
127
|
-
|
128
|
-
qt = QuadTree((0, 0, 1000, 1000), capacity=16, track_objects=True)
|
129
|
-
|
130
|
-
# Store the object alongside the point
|
131
|
-
qt.insert((25, 40), obj={"name": "apple"})
|
132
|
-
|
133
|
-
# Ask for Item objects within a bounding box
|
134
|
-
items = qt.query((0, 0, 100, 100), as_items=True)
|
135
|
-
for it in items:
|
136
|
-
print(it.id, it.x, it.y, it.obj)
|
137
|
-
```
|
138
|
-
|
139
|
-
You can also attach or replace an object later:
|
140
|
-
|
141
|
-
```python
|
142
|
-
qt.attach(123, my_object) # binds object to id 123
|
143
|
-
```
|
78
|
+
[See the quickstart guide](https://elan456.github.io/fastquadtree/quickstart/)
|
144
79
|
|
145
80
|
## API
|
146
81
|
|
147
|
-
[
|
82
|
+
[See the full API](https://elan456.github.io/fastquadtree/api/quadtree/)
|
148
83
|
|
149
84
|
### `QuadTree(bounds, capacity, max_depth=None, track_objects=False, start_id=1)`
|
150
85
|
|
@@ -154,38 +89,17 @@ qt.attach(123, my_object) # binds object to id 123
|
|
154
89
|
* `track_objects` — if `True`, the wrapper maintains an id → object map for convenience.
|
155
90
|
* `start_id` — starting value for auto-assigned ids
|
156
91
|
|
157
|
-
###
|
92
|
+
### Key Methods
|
158
93
|
|
159
94
|
- `insert(xy, *, id=None, obj=None) -> int`
|
160
95
|
|
161
|
-
- `insert_many_points(points) -> int`
|
162
|
-
|
163
96
|
- `query(rect, *, as_items=False) -> list`
|
164
97
|
|
165
98
|
- `nearest_neighbor(xy, *, as_item=False) -> (id, x, y) | Item | None`
|
166
99
|
|
167
|
-
- `nearest_neighbors(xy, k, *, as_items=False) -> list`
|
168
|
-
|
169
100
|
- `delete(id, xy) -> bool`
|
170
101
|
|
171
|
-
|
172
|
-
|
173
|
-
- `clear(*, reset_ids=False) -> None`
|
174
|
-
|
175
|
-
- `attach(id, obj) -> None (requires track_objects=True)`
|
176
|
-
|
177
|
-
- `count_items() -> int`
|
178
|
-
|
179
|
-
- `get(id) -> object | None`
|
180
|
-
|
181
|
-
- `get_all_rectangles() -> list[tuple] (for visualization)`
|
182
|
-
|
183
|
-
- `get_all_objects() -> list[object] (requires track_objects=True)`
|
184
|
-
|
185
|
-
### `Item` (returned when `as_items=True`)
|
186
|
-
|
187
|
-
* Attributes: `id`, `x`, `y`, and a lazy `obj` property
|
188
|
-
* 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/).
|
189
103
|
|
190
104
|
### Geometric conventions
|
191
105
|
|
@@ -199,52 +113,8 @@ qt.attach(123, my_object) # binds object to id 123
|
|
199
113
|
* Choose `capacity` so that leaves keep a small batch of points. Typical values are 8 to 64.
|
200
114
|
* If your data is very skewed, set a `max_depth` to prevent long chains.
|
201
115
|
* For fastest local runs, use `maturin develop --release`.
|
202
|
-
* The wrapper
|
203
|
-
|
204
|
-
|
205
|
-
### Native vs Shim Benchmark
|
206
|
-
|
207
|
-
**Setup**
|
208
|
-
- Points: 500,000
|
209
|
-
- Queries: 500
|
210
|
-
- Repeats: 5
|
211
|
-
|
212
|
-
**Timing (seconds)**
|
213
|
-
|
214
|
-
| Variant | Build | Query | Total |
|
215
|
-
|---|---:|---:|---:|
|
216
|
-
| Native | 0.483 | 4.380 | 4.863 |
|
217
|
-
| Shim (no map) | 0.668 | 4.167 | 4.835 |
|
218
|
-
| Shim (track+objs) | 1.153 | 4.458 | 5.610 |
|
219
|
-
|
220
|
-
**Overhead vs Native**
|
221
|
-
|
222
|
-
- No map: build 1.38x, query 0.95x, total 0.99x
|
223
|
-
- Track + objs: build 2.39x, query 1.02x, total 1.15x
|
224
|
-
|
225
|
-
### Run benchmarks
|
226
|
-
To run the benchmarks yourself, first install the dependencies:
|
227
|
-
|
228
|
-
```bash
|
229
|
-
pip install -r benchmarks/requirements.txt
|
230
|
-
```
|
231
|
-
|
232
|
-
Then run:
|
233
|
-
|
234
|
-
```bash
|
235
|
-
python benchmarks/cross_library_bench.py
|
236
|
-
python benchmarks/benchmark_native_vs_shim.py
|
237
|
-
```
|
238
|
-
|
239
|
-
Check the CLI arguments for the cross-library benchmark in `benchmarks/quadtree_bench/main.py`.
|
240
|
-
|
241
|
-
## Run Visualizer
|
242
|
-
A visualizer is included to help you understand how the quadtree subdivides space.
|
243
|
-
|
244
|
-
```bash
|
245
|
-
pip install -r interactive/requirements.txt
|
246
|
-
python interactive/interactive_v2.py
|
247
|
-
```
|
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.
|
248
118
|
|
249
119
|
### Pygame Ball Pit Demo
|
250
120
|
|
@@ -253,10 +123,7 @@ python interactive/interactive_v2.py
|
|
253
123
|
A simple demo of moving objects with collision detection using **fastquadtree**.
|
254
124
|
You can toggle between quadtree mode and brute-force mode to see the performance difference.
|
255
125
|
|
256
|
-
|
257
|
-
pip install -r interactive/requirements.txt
|
258
|
-
python interactive/ball_pit.py
|
259
|
-
```
|
126
|
+
See the [runnables guide](https://elan456.github.io/fastquadtree/runnables/) for setup instructions.
|
260
127
|
|
261
128
|
## FAQ
|
262
129
|
|
@@ -267,7 +134,7 @@ Allowed. For k-nearest, duplicates are de-duplicated by id. For range queries yo
|
|
267
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.
|
268
135
|
|
269
136
|
**Can I store rectangles or circles?**
|
270
|
-
|
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.
|
271
138
|
|
272
139
|
## License
|
273
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()
|
@@ -6,7 +6,6 @@ and result collection for performance analysis.
|
|
6
6
|
"""
|
7
7
|
|
8
8
|
import gc
|
9
|
-
import json
|
10
9
|
import math
|
11
10
|
import random
|
12
11
|
import statistics as stats
|
@@ -17,7 +16,6 @@ from typing import Any, Dict, List, Tuple
|
|
17
16
|
from tqdm import tqdm
|
18
17
|
|
19
18
|
from .engines import Engine
|
20
|
-
from .system_info_collector import collect_system_info
|
21
19
|
|
22
20
|
|
23
21
|
@dataclass
|
@@ -257,8 +255,8 @@ class BenchmarkRunner:
|
|
257
255
|
experiment_bar.close()
|
258
256
|
|
259
257
|
# Add metadata to results
|
260
|
-
results["engines"] = engines
|
261
|
-
results["config"] = self.config
|
258
|
+
results["engines"] = engines # pyright: ignore[reportArgumentType]
|
259
|
+
results["config"] = self.config # pyright: ignore[reportArgumentType]
|
262
260
|
|
263
261
|
return results
|
264
262
|
|
@@ -279,7 +277,6 @@ class BenchmarkRunner:
|
|
279
277
|
print(
|
280
278
|
f"- Points: **{config.experiments[i]:,}**, Queries: **{config.n_queries}**"
|
281
279
|
)
|
282
|
-
print("--------------------")
|
283
280
|
|
284
281
|
# Find fastest and show key results
|
285
282
|
ranked = sorted(
|
@@ -317,9 +314,3 @@ class BenchmarkRunner:
|
|
317
314
|
print(f"| Max points per node | {config.max_points} |")
|
318
315
|
print(f"| Max depth | {config.max_depth} |")
|
319
316
|
print(f"| Queries per experiment | {config.n_queries} |")
|
320
|
-
|
321
|
-
system_info = collect_system_info()
|
322
|
-
print("#### System Info")
|
323
|
-
print("```json")
|
324
|
-
print(json.dumps(system_info, indent=2))
|
325
|
-
print("```")
|