fastquadtree 0.4.2__tar.gz → 0.5.1__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.4.2 → fastquadtree-0.5.1}/.pre-commit-config.yaml +11 -9
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/Cargo.lock +1 -1
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/Cargo.toml +7 -1
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/PKG-INFO +38 -48
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/README.md +30 -47
- fastquadtree-0.5.1/assets/quadtree_bench_throughput.png +0 -0
- fastquadtree-0.5.1/assets/quadtree_bench_time.png +0 -0
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/benchmark_native_vs_shim.py +25 -6
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/quadtree_bench/__init__.py +2 -2
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/quadtree_bench/engines.py +21 -8
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/quadtree_bench/main.py +1 -1
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/quadtree_bench/plotting.py +24 -24
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/quadtree_bench/runner.py +83 -13
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/runner.py +0 -7
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/interactive/interactive.py +12 -6
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/interactive/interactive_v2.py +123 -95
- fastquadtree-0.5.1/pyproject.toml +130 -0
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/pysrc/fastquadtree/__init__.py +42 -45
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/pysrc/fastquadtree/__init__.pyi +25 -21
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/pysrc/fastquadtree/_bimap.py +12 -11
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/pysrc/fastquadtree/_item.py +5 -4
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/src/geom.rs +8 -8
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/src/lib.rs +8 -8
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/src/quadtree.rs +24 -27
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/insertions.rs +3 -3
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/nearest_neighbor.rs +6 -6
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/query.rs +4 -4
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/test_bimap.py +7 -6
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/test_delete_by_object.py +1 -0
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/test_delete_python.py +0 -1
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/test_python.py +7 -4
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/test_unconventional_bounds.py +4 -2
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/test_wrapper_edges.py +14 -15
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/unconventional_bounds.rs +5 -5
- fastquadtree-0.4.2/assets/quadtree_bench_throughput.png +0 -0
- fastquadtree-0.4.2/assets/quadtree_bench_time.png +0 -0
- fastquadtree-0.4.2/examples/delete_demo.rs +0 -63
- fastquadtree-0.4.2/pyproject.toml +0 -56
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/.github/workflows/release.yml +0 -0
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/.gitignore +0 -0
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/LICENSE +0 -0
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/assets/interactive_v2_screenshot.png +0 -0
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/cross_library_bench.py +0 -0
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/requirements.txt +0 -0
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/interactive/requirements.txt +0 -0
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/pysrc/fastquadtree/py.typed +0 -0
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/rectangle_traversal.rs +0 -0
- {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/test_delete.rs +0 -0
@@ -1,12 +1,15 @@
|
|
1
1
|
repos:
|
2
|
-
# 0) Python formatter:
|
3
|
-
- repo: https://github.com/
|
4
|
-
rev:
|
2
|
+
# 0) Python linter and formatter: Ruff
|
3
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
4
|
+
rev: v0.6.9
|
5
5
|
hooks:
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
args: ["--
|
6
|
+
# Lint + autofix. If Ruff fixes something, the hook exits nonzero to force a re-run.
|
7
|
+
- id: ruff
|
8
|
+
name: ruff (lint + fix)
|
9
|
+
args: ["--fix", "--exit-non-zero-on-fix"]
|
10
|
+
# Code formatter (Black replacement)
|
11
|
+
- id: ruff-format
|
12
|
+
name: ruff (format)
|
10
13
|
|
11
14
|
# Local hooks that run in sequence and do not receive file args
|
12
15
|
- repo: local
|
@@ -28,10 +31,9 @@ repos:
|
|
28
31
|
always_run: true
|
29
32
|
|
30
33
|
# 3) Python tests under coverage
|
31
|
-
# This both runs pytest and generates .coverage data
|
32
34
|
- id: pytest
|
33
35
|
name: pytest (under coverage)
|
34
36
|
entry: pytest
|
35
37
|
language: system
|
36
38
|
pass_filenames: false
|
37
|
-
always_run: true
|
39
|
+
always_run: true
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[package]
|
2
2
|
name = "fastquadtree"
|
3
|
-
version = "0.
|
3
|
+
version = "0.5.1"
|
4
4
|
edition = "2021"
|
5
5
|
readme = "README.md"
|
6
6
|
|
@@ -9,3 +9,9 @@ crate-type = ["rlib", "cdylib"]
|
|
9
9
|
|
10
10
|
[dependencies]
|
11
11
|
pyo3 = { version = "0.21", features = ["extension-module", "abi3-py38"] }
|
12
|
+
|
13
|
+
[profile.release]
|
14
|
+
opt-level = 3
|
15
|
+
lto = "thin"
|
16
|
+
codegen-units = 1
|
17
|
+
panic = "abort"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: fastquadtree
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.5.1
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
5
5
|
Classifier: Programming Language :: Python :: 3 :: Only
|
6
6
|
Classifier: Programming Language :: Rust
|
@@ -11,6 +11,13 @@ Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
11
11
|
Classifier: Topic :: Software Development :: Libraries
|
12
12
|
Classifier: Typing :: Typed
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
14
|
+
Requires-Dist: ruff>=0.6.0 ; extra == 'dev'
|
15
|
+
Requires-Dist: pytest>=7.0 ; extra == 'dev'
|
16
|
+
Requires-Dist: pytest-cov>=4.1 ; extra == 'dev'
|
17
|
+
Requires-Dist: coverage>=7.5 ; extra == 'dev'
|
18
|
+
Requires-Dist: mypy>=1.10 ; extra == 'dev'
|
19
|
+
Requires-Dist: build>=1.2.1 ; extra == 'dev'
|
20
|
+
Provides-Extra: dev
|
14
21
|
License-File: LICENSE
|
15
22
|
Summary: Rust-accelerated quadtree for Python with fast inserts, range queries, and k-NN search.
|
16
23
|
Keywords: quadtree,spatial-index,geometry,rust,pyo3,nearest-neighbor,k-nn
|
@@ -36,8 +43,8 @@ Project-URL: Issues, https://github.com/Elan456/fastquadtree/issues
|
|
36
43
|
|
37
44
|
[](https://pyo3.rs/)
|
38
45
|
[](https://www.maturin.rs/)
|
39
|
-
[](https://docs.astral.sh/ruff/)
|
47
|
+
|
41
48
|
|
42
49
|
|
43
50
|

|
@@ -49,6 +56,29 @@ Rust-optimized quadtree with a simple Python API.
|
|
49
56
|
- Python ≥ 3.8
|
50
57
|
- Import path: `from fastquadtree import QuadTree`
|
51
58
|
|
59
|
+
## Benchmarks
|
60
|
+
|
61
|
+
fastquadtree **outperforms** all other quadtree Python packages, including the Rtree spatial index.
|
62
|
+
|
63
|
+
### Library comparison
|
64
|
+
|
65
|
+

|
66
|
+

|
67
|
+
|
68
|
+
### Summary (largest dataset, PyQtree baseline)
|
69
|
+
- Points: **500,000**, Queries: **500**
|
70
|
+
--------------------
|
71
|
+
- Fastest total: **fastquadtree** at **1.591 s**
|
72
|
+
|
73
|
+
| Library | Build (s) | Query (s) | Total (s) | Speed vs PyQtree |
|
74
|
+
|---|---:|---:|---:|---:|
|
75
|
+
| fastquadtree | 0.165 | 1.427 | 1.591 | 5.09× |
|
76
|
+
| Rtree | 1.320 | 2.369 | 3.688 | 2.20× |
|
77
|
+
| PyQtree | 2.687 | 5.415 | 8.102 | 1.00× |
|
78
|
+
| nontree-QuadTree | 1.284 | 9.891 | 11.175 | 0.73× |
|
79
|
+
| quads | 2.346 | 10.129 | 12.475 | 0.65× |
|
80
|
+
| e-pyquadtree | 1.795 | 11.855 | 13.650 | 0.59× |
|
81
|
+
|
52
82
|
## Install
|
53
83
|
|
54
84
|
```bash
|
@@ -109,25 +139,7 @@ print(f"Deleted player: {deleted}") # True
|
|
109
139
|
|
110
140
|
You can keep the tree pure and manage your own id → object map, or let the wrapper manage it.
|
111
141
|
|
112
|
-
**
|
113
|
-
|
114
|
-
```python
|
115
|
-
from fastquadtree import QuadTree
|
116
|
-
|
117
|
-
qt = QuadTree((0, 0, 1000, 1000), capacity=16)
|
118
|
-
objects: dict[int, object] = {}
|
119
|
-
|
120
|
-
def add(obj) -> int:
|
121
|
-
obj_id = qt.insert(obj.position) # auto id
|
122
|
-
objects[obj_id] = obj
|
123
|
-
return obj_id
|
124
|
-
|
125
|
-
# Later, resolve ids back to objects
|
126
|
-
ids = [obj_id for (obj_id, x, y) in qt.query((100, 100, 300, 300))]
|
127
|
-
selected = [objects[i] for i in ids]
|
128
|
-
```
|
129
|
-
|
130
|
-
**Option B: Ask the wrapper to track objects**
|
142
|
+
**Wrapper Managed Objects**
|
131
143
|
|
132
144
|
```python
|
133
145
|
from fastquadtree import QuadTree
|
@@ -137,7 +149,7 @@ qt = QuadTree((0, 0, 1000, 1000), capacity=16, track_objects=True)
|
|
137
149
|
# Store the object alongside the point
|
138
150
|
qt.insert((25, 40), obj={"name": "apple"})
|
139
151
|
|
140
|
-
# Ask for Item objects
|
152
|
+
# Ask for Item objects within a bounding box
|
141
153
|
items = qt.query((0, 0, 100, 100), as_items=True)
|
142
154
|
for it in items:
|
143
155
|
print(it.id, it.x, it.y, it.obj)
|
@@ -151,7 +163,7 @@ qt.attach(123, my_object) # binds object to id 123
|
|
151
163
|
|
152
164
|
## API
|
153
165
|
|
154
|
-
### `QuadTree(bounds, capacity,
|
166
|
+
### `QuadTree(bounds, capacity, max_depth=None, track_objects=False, start_id=1)`
|
155
167
|
|
156
168
|
* `bounds` — tuple `(min_x, min_y, max_x, max_y)` defines the 2D area covered by the quadtree
|
157
169
|
* `capacity` — max number of points kept in a leaf before splitting
|
@@ -204,30 +216,8 @@ Full docs are in the docstrings of the [Python Shim](pysrc/fastquadtree/__init__
|
|
204
216
|
* For fastest local runs, use `maturin develop --release`.
|
205
217
|
* The wrapper keeps Python overhead low: raw tuple results by default, `Item` wrappers only when requested.
|
206
218
|
|
207
|
-
## Benchmarks
|
208
|
-
|
209
|
-
fastquadtree outperforms all other quadtree python packages (at least all the ones I could find and install via pip.)
|
210
|
-
|
211
|
-
### Library comparison
|
212
|
-
|
213
|
-

|
214
|
-

|
215
|
-
|
216
|
-
### Summary (largest dataset, PyQtree baseline)
|
217
|
-
- Points: **500,000**, Queries: **500**
|
218
|
-
- Fastest total: **fastquadtree** at **2.207 s**
|
219
|
-
|
220
|
-
| Library | Build (s) | Query (s) | Total (s) | Speed vs PyQtree |
|
221
|
-
|---|---:|---:|---:|---:|
|
222
|
-
| fastquadtree | 0.321 | 1.885 | 2.207 | 4.27× |
|
223
|
-
| Rtree | 1.718 | 4.376 | 6.095 | 1.55× |
|
224
|
-
| nontree-QuadTree | 1.617 | 7.643 | 9.260 | 1.02× |
|
225
|
-
| PyQtree | 4.349 | 5.082 | 9.431 | 1.00× |
|
226
|
-
| quads | 3.874 | 9.058 | 12.932 | 0.73× |
|
227
|
-
| e-pyquadtree | 2.732 | 10.598 | 13.330 | 0.71× |
|
228
|
-
| Brute force | 0.019 | 19.986 | 20.005 | 0.47× |
|
229
219
|
|
230
|
-
### Native vs Shim
|
220
|
+
### Native vs Shim Benchmark
|
231
221
|
|
232
222
|
**Setup**
|
233
223
|
- Points: 500,000
|
@@ -277,7 +267,7 @@ Check the CLI arguments for the cross-library benchmark in `benchmarks/quadtree_
|
|
277
267
|
Allowed. For k-nearest, duplicates are de-duplicated by id. For range queries you will see every inserted point.
|
278
268
|
|
279
269
|
**Can I delete items from the quadtree?**
|
280
|
-
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
|
270
|
+
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.
|
281
271
|
|
282
272
|
**Can I store rectangles or circles?**
|
283
273
|
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.
|
@@ -13,8 +13,8 @@
|
|
13
13
|
|
14
14
|
[](https://pyo3.rs/)
|
15
15
|
[](https://www.maturin.rs/)
|
16
|
-
[](https://docs.astral.sh/ruff/)
|
17
|
+
|
18
18
|
|
19
19
|
|
20
20
|

|
@@ -26,6 +26,29 @@ Rust-optimized quadtree with a simple Python API.
|
|
26
26
|
- Python ≥ 3.8
|
27
27
|
- Import path: `from fastquadtree import QuadTree`
|
28
28
|
|
29
|
+
## Benchmarks
|
30
|
+
|
31
|
+
fastquadtree **outperforms** all other quadtree Python packages, including the Rtree spatial index.
|
32
|
+
|
33
|
+
### Library comparison
|
34
|
+
|
35
|
+

|
36
|
+

|
37
|
+
|
38
|
+
### Summary (largest dataset, PyQtree baseline)
|
39
|
+
- Points: **500,000**, Queries: **500**
|
40
|
+
--------------------
|
41
|
+
- Fastest total: **fastquadtree** at **1.591 s**
|
42
|
+
|
43
|
+
| Library | Build (s) | Query (s) | Total (s) | Speed vs PyQtree |
|
44
|
+
|---|---:|---:|---:|---:|
|
45
|
+
| fastquadtree | 0.165 | 1.427 | 1.591 | 5.09× |
|
46
|
+
| Rtree | 1.320 | 2.369 | 3.688 | 2.20× |
|
47
|
+
| PyQtree | 2.687 | 5.415 | 8.102 | 1.00× |
|
48
|
+
| nontree-QuadTree | 1.284 | 9.891 | 11.175 | 0.73× |
|
49
|
+
| quads | 2.346 | 10.129 | 12.475 | 0.65× |
|
50
|
+
| e-pyquadtree | 1.795 | 11.855 | 13.650 | 0.59× |
|
51
|
+
|
29
52
|
## Install
|
30
53
|
|
31
54
|
```bash
|
@@ -86,25 +109,7 @@ print(f"Deleted player: {deleted}") # True
|
|
86
109
|
|
87
110
|
You can keep the tree pure and manage your own id → object map, or let the wrapper manage it.
|
88
111
|
|
89
|
-
**
|
90
|
-
|
91
|
-
```python
|
92
|
-
from fastquadtree import QuadTree
|
93
|
-
|
94
|
-
qt = QuadTree((0, 0, 1000, 1000), capacity=16)
|
95
|
-
objects: dict[int, object] = {}
|
96
|
-
|
97
|
-
def add(obj) -> int:
|
98
|
-
obj_id = qt.insert(obj.position) # auto id
|
99
|
-
objects[obj_id] = obj
|
100
|
-
return obj_id
|
101
|
-
|
102
|
-
# Later, resolve ids back to objects
|
103
|
-
ids = [obj_id for (obj_id, x, y) in qt.query((100, 100, 300, 300))]
|
104
|
-
selected = [objects[i] for i in ids]
|
105
|
-
```
|
106
|
-
|
107
|
-
**Option B: Ask the wrapper to track objects**
|
112
|
+
**Wrapper Managed Objects**
|
108
113
|
|
109
114
|
```python
|
110
115
|
from fastquadtree import QuadTree
|
@@ -114,7 +119,7 @@ qt = QuadTree((0, 0, 1000, 1000), capacity=16, track_objects=True)
|
|
114
119
|
# Store the object alongside the point
|
115
120
|
qt.insert((25, 40), obj={"name": "apple"})
|
116
121
|
|
117
|
-
# Ask for Item objects
|
122
|
+
# Ask for Item objects within a bounding box
|
118
123
|
items = qt.query((0, 0, 100, 100), as_items=True)
|
119
124
|
for it in items:
|
120
125
|
print(it.id, it.x, it.y, it.obj)
|
@@ -128,7 +133,7 @@ qt.attach(123, my_object) # binds object to id 123
|
|
128
133
|
|
129
134
|
## API
|
130
135
|
|
131
|
-
### `QuadTree(bounds, capacity,
|
136
|
+
### `QuadTree(bounds, capacity, max_depth=None, track_objects=False, start_id=1)`
|
132
137
|
|
133
138
|
* `bounds` — tuple `(min_x, min_y, max_x, max_y)` defines the 2D area covered by the quadtree
|
134
139
|
* `capacity` — max number of points kept in a leaf before splitting
|
@@ -181,30 +186,8 @@ Full docs are in the docstrings of the [Python Shim](pysrc/fastquadtree/__init__
|
|
181
186
|
* For fastest local runs, use `maturin develop --release`.
|
182
187
|
* The wrapper keeps Python overhead low: raw tuple results by default, `Item` wrappers only when requested.
|
183
188
|
|
184
|
-
## Benchmarks
|
185
|
-
|
186
|
-
fastquadtree outperforms all other quadtree python packages (at least all the ones I could find and install via pip.)
|
187
|
-
|
188
|
-
### Library comparison
|
189
|
-
|
190
|
-

|
191
|
-

|
192
|
-
|
193
|
-
### Summary (largest dataset, PyQtree baseline)
|
194
|
-
- Points: **500,000**, Queries: **500**
|
195
|
-
- Fastest total: **fastquadtree** at **2.207 s**
|
196
|
-
|
197
|
-
| Library | Build (s) | Query (s) | Total (s) | Speed vs PyQtree |
|
198
|
-
|---|---:|---:|---:|---:|
|
199
|
-
| fastquadtree | 0.321 | 1.885 | 2.207 | 4.27× |
|
200
|
-
| Rtree | 1.718 | 4.376 | 6.095 | 1.55× |
|
201
|
-
| nontree-QuadTree | 1.617 | 7.643 | 9.260 | 1.02× |
|
202
|
-
| PyQtree | 4.349 | 5.082 | 9.431 | 1.00× |
|
203
|
-
| quads | 3.874 | 9.058 | 12.932 | 0.73× |
|
204
|
-
| e-pyquadtree | 2.732 | 10.598 | 13.330 | 0.71× |
|
205
|
-
| Brute force | 0.019 | 19.986 | 20.005 | 0.47× |
|
206
189
|
|
207
|
-
### Native vs Shim
|
190
|
+
### Native vs Shim Benchmark
|
208
191
|
|
209
192
|
**Setup**
|
210
193
|
- Points: 500,000
|
@@ -254,7 +237,7 @@ Check the CLI arguments for the cross-library benchmark in `benchmarks/quadtree_
|
|
254
237
|
Allowed. For k-nearest, duplicates are de-duplicated by id. For range queries you will see every inserted point.
|
255
238
|
|
256
239
|
**Can I delete items from the quadtree?**
|
257
|
-
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
|
240
|
+
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.
|
258
241
|
|
259
242
|
**Can I store rectangles or circles?**
|
260
243
|
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.
|
Binary file
|
Binary file
|
@@ -6,11 +6,11 @@ import gc
|
|
6
6
|
import random
|
7
7
|
import statistics as stats
|
8
8
|
from time import perf_counter as now
|
9
|
+
|
9
10
|
from tqdm import tqdm
|
10
11
|
|
11
|
-
from fastquadtree._native import QuadTree as NativeQuadTree
|
12
12
|
from fastquadtree import QuadTree as ShimQuadTree
|
13
|
-
|
13
|
+
from fastquadtree._native import QuadTree as NativeQuadTree
|
14
14
|
|
15
15
|
BOUNDS = (0.0, 0.0, 1000.0, 1000.0)
|
16
16
|
CAPACITY = 20
|
@@ -68,9 +68,10 @@ def bench_shim(points, queries, *, track_objects: bool, with_objs: bool):
|
|
68
68
|
return t_build, t_query
|
69
69
|
|
70
70
|
|
71
|
-
def median_times(fn, points, queries, repeats: int):
|
71
|
+
def median_times(fn, points, queries, repeats: int, desc: str = "Running"):
|
72
|
+
"""Run benchmark multiple times and return median times."""
|
72
73
|
builds, queries_t = [], []
|
73
|
-
for _ in tqdm(range(repeats)):
|
74
|
+
for _ in tqdm(range(repeats), desc=desc, unit="run"):
|
74
75
|
gc.disable()
|
75
76
|
b, q = fn(points, queries)
|
76
77
|
gc.enable()
|
@@ -86,29 +87,47 @@ def main():
|
|
86
87
|
ap.add_argument("--repeats", type=int, default=5)
|
87
88
|
args = ap.parse_args()
|
88
89
|
|
90
|
+
print("Native vs Shim Benchmark")
|
91
|
+
print("=" * 50)
|
92
|
+
print("Configuration:")
|
93
|
+
print(f" Points: {args.points:,}")
|
94
|
+
print(f" Queries: {args.queries}")
|
95
|
+
print(f" Repeats: {args.repeats}")
|
96
|
+
print()
|
97
|
+
|
89
98
|
rng = random.Random(SEED)
|
90
99
|
points = gen_points(args.points, rng)
|
91
100
|
queries = gen_queries(args.queries, rng)
|
92
101
|
|
93
102
|
# Warmup to load modules
|
103
|
+
print("Warming up...")
|
94
104
|
_ = bench_native(points[:1000], queries[:50])
|
95
105
|
_ = bench_shim(points[:1000], queries[:50], track_objects=False, with_objs=False)
|
106
|
+
print()
|
96
107
|
|
108
|
+
print("Running benchmarks...")
|
97
109
|
n_build, n_query = median_times(
|
98
|
-
lambda pts, qs: bench_native(pts, qs),
|
110
|
+
lambda pts, qs: bench_native(pts, qs),
|
111
|
+
points,
|
112
|
+
queries,
|
113
|
+
args.repeats,
|
114
|
+
desc="Native",
|
99
115
|
)
|
100
116
|
s_build_no_map, s_query_no_map = median_times(
|
101
117
|
lambda pts, qs: bench_shim(pts, qs, track_objects=False, with_objs=False),
|
102
118
|
points,
|
103
119
|
queries,
|
104
120
|
args.repeats,
|
121
|
+
desc="Shim (no map)",
|
105
122
|
)
|
106
123
|
s_build_map, s_query_map = median_times(
|
107
124
|
lambda pts, qs: bench_shim(pts, qs, track_objects=True, with_objs=True),
|
108
125
|
points,
|
109
126
|
queries,
|
110
127
|
args.repeats,
|
128
|
+
desc="Shim (track+objs)",
|
111
129
|
)
|
130
|
+
print()
|
112
131
|
|
113
132
|
def fmt(x):
|
114
133
|
return f"{x:.3f}"
|
@@ -131,7 +150,7 @@ def main():
|
|
131
150
|
|
132
151
|
**Overhead vs Native**
|
133
152
|
|
134
|
-
- No map: build {s_build_no_map / n_build:.2f}x, query {s_query_no_map / n_query:.2f}x, total {(s_build_no_map + s_query_no_map) / (n_build + n_query):.2f}x
|
153
|
+
- No map: build {s_build_no_map / n_build:.2f}x, query {s_query_no_map / n_query:.2f}x, total {(s_build_no_map + s_query_no_map) / (n_build + n_query):.2f}x
|
135
154
|
- Track + objs: build {s_build_map / n_build:.2f}x, query {s_query_map / n_query:.2f}x, total {(s_build_map + s_query_map) / (n_build + n_query):.2f}x
|
136
155
|
"""
|
137
156
|
print(md.strip())
|
@@ -6,8 +6,8 @@ implementations, including performance comparison, visualization, and analysis.
|
|
6
6
|
"""
|
7
7
|
|
8
8
|
from .engines import Engine, get_engines
|
9
|
-
from .runner import BenchmarkRunner, BenchmarkConfig
|
10
9
|
from .plotting import PlotManager
|
10
|
+
from .runner import BenchmarkConfig, BenchmarkRunner
|
11
11
|
|
12
12
|
__version__ = "1.0.0"
|
13
|
-
__all__ = ["
|
13
|
+
__all__ = ["BenchmarkConfig", "BenchmarkRunner", "Engine", "PlotManager", "get_engines"]
|
@@ -7,9 +7,11 @@ allowing fair comparison of their performance characteristics.
|
|
7
7
|
|
8
8
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
9
9
|
|
10
|
+
from pyqtree import Index as PyQTree # Pyqtree
|
11
|
+
|
10
12
|
# Built-in engines (always available in this repo)
|
11
13
|
from pyquadtree.quadtree import QuadTree as EPyQuadTree # e-pyquadtree
|
12
|
-
|
14
|
+
|
13
15
|
from fastquadtree import QuadTree as RustQuadTree # fastquadtree
|
14
16
|
|
15
17
|
|
@@ -67,7 +69,10 @@ def _create_e_pyquadtree_engine(
|
|
67
69
|
_ = qt.query(q)
|
68
70
|
|
69
71
|
return Engine(
|
70
|
-
"e-pyquadtree",
|
72
|
+
"e-pyquadtree",
|
73
|
+
"#1f77b4",
|
74
|
+
build,
|
75
|
+
query, # display name # color (blue)
|
71
76
|
)
|
72
77
|
|
73
78
|
|
@@ -105,7 +110,10 @@ def _create_fastquadtree_engine(
|
|
105
110
|
_ = qt.query(q)
|
106
111
|
|
107
112
|
return Engine(
|
108
|
-
"fastquadtree",
|
113
|
+
"fastquadtree",
|
114
|
+
"#ff7f0e",
|
115
|
+
build,
|
116
|
+
query, # display name # color (orange)
|
109
117
|
)
|
110
118
|
|
111
119
|
|
@@ -165,7 +173,10 @@ def _create_nontree_engine(
|
|
165
173
|
_ = tm.get_rect((xmin, ymin, xmax - xmin, ymax - ymin))
|
166
174
|
|
167
175
|
return Engine(
|
168
|
-
"nontree-QuadTree",
|
176
|
+
"nontree-QuadTree",
|
177
|
+
"#17becf",
|
178
|
+
build,
|
179
|
+
query, # display name # color (cyan)
|
169
180
|
)
|
170
181
|
|
171
182
|
|
@@ -178,7 +189,7 @@ def _create_brute_force_engine(
|
|
178
189
|
# Append each item as if they were being added separately
|
179
190
|
out = []
|
180
191
|
for p in points:
|
181
|
-
out.append(p)
|
192
|
+
out.append(p) # noqa: PERF402
|
182
193
|
return out
|
183
194
|
|
184
195
|
def query(points, queries):
|
@@ -187,7 +198,10 @@ def _create_brute_force_engine(
|
|
187
198
|
_ = [p for p in points if q[0] <= p[0] <= q[2] and q[1] <= p[1] <= q[3]]
|
188
199
|
|
189
200
|
return Engine(
|
190
|
-
"Brute force",
|
201
|
+
"Brute force",
|
202
|
+
"#9467bd",
|
203
|
+
build,
|
204
|
+
query, # display name # color (purple)
|
191
205
|
)
|
192
206
|
|
193
207
|
|
@@ -213,8 +227,7 @@ def _create_rtree_engine(
|
|
213
227
|
# Bulk stream loading is the fastest way to build
|
214
228
|
# Keep the same 1x1 bbox convention used elsewhere for fairness
|
215
229
|
stream = ((i, (x, y, x + 1, y + 1), None) for i, (x, y) in enumerate(points))
|
216
|
-
|
217
|
-
return idx
|
230
|
+
return rindex.Index(stream, properties=p)
|
218
231
|
|
219
232
|
def query(idx, queries):
|
220
233
|
# Do not materialize results into a list, just consume the generator
|
@@ -5,7 +5,7 @@ This module handles creation of performance charts, graphs, and visualizations
|
|
5
5
|
for benchmark results analysis.
|
6
6
|
"""
|
7
7
|
|
8
|
-
from typing import
|
8
|
+
from typing import Any, Dict, Tuple
|
9
9
|
|
10
10
|
import plotly.graph_objects as go
|
11
11
|
from plotly.subplots import make_subplots
|
@@ -43,7 +43,7 @@ class PlotManager:
|
|
43
43
|
name=name,
|
44
44
|
legendgroup=name,
|
45
45
|
showlegend=show_legend,
|
46
|
-
line=
|
46
|
+
line={"color": color, "width": 3},
|
47
47
|
),
|
48
48
|
row=1,
|
49
49
|
col=col,
|
@@ -69,15 +69,15 @@ class PlotManager:
|
|
69
69
|
f"{self.config.n_queries} queries)"
|
70
70
|
),
|
71
71
|
template="plotly_dark",
|
72
|
-
legend=
|
73
|
-
orientation
|
74
|
-
traceorder
|
75
|
-
xanchor
|
76
|
-
x
|
77
|
-
yanchor
|
78
|
-
y
|
79
|
-
|
80
|
-
margin=
|
72
|
+
legend={
|
73
|
+
"orientation": "v",
|
74
|
+
"traceorder": "normal",
|
75
|
+
"xanchor": "left",
|
76
|
+
"x": 0,
|
77
|
+
"yanchor": "top",
|
78
|
+
"y": 1,
|
79
|
+
},
|
80
|
+
margin={"l": 40, "r": 20, "t": 80, "b": 40},
|
81
81
|
height=520,
|
82
82
|
)
|
83
83
|
|
@@ -108,7 +108,7 @@ class PlotManager:
|
|
108
108
|
name=name,
|
109
109
|
legendgroup=name,
|
110
110
|
showlegend=False,
|
111
|
-
line=
|
111
|
+
line={"color": color, "width": 3},
|
112
112
|
),
|
113
113
|
row=1,
|
114
114
|
col=1,
|
@@ -122,7 +122,7 @@ class PlotManager:
|
|
122
122
|
name=name,
|
123
123
|
legendgroup=name,
|
124
124
|
showlegend=True,
|
125
|
-
line=
|
125
|
+
line={"color": color, "width": 3},
|
126
126
|
),
|
127
127
|
row=1,
|
128
128
|
col=2,
|
@@ -138,14 +138,14 @@ class PlotManager:
|
|
138
138
|
fig.update_layout(
|
139
139
|
title="Throughput",
|
140
140
|
template="plotly_dark",
|
141
|
-
legend=
|
142
|
-
orientation
|
143
|
-
x
|
144
|
-
xanchor
|
145
|
-
y
|
146
|
-
yanchor
|
147
|
-
|
148
|
-
margin=
|
141
|
+
legend={
|
142
|
+
"orientation": "h",
|
143
|
+
"x": 0,
|
144
|
+
"xanchor": "left",
|
145
|
+
"y": 1.08, # above the subplots
|
146
|
+
"yanchor": "bottom",
|
147
|
+
},
|
148
|
+
margin={"l": 60, "r": 40, "t": 120, "b": 40},
|
149
149
|
height=480,
|
150
150
|
)
|
151
151
|
|
@@ -187,7 +187,7 @@ class PlotManager:
|
|
187
187
|
height=480,
|
188
188
|
)
|
189
189
|
print(f"Saved PNG images to {output_dir}/ with prefix '{output_prefix}'")
|
190
|
-
except Exception as e:
|
190
|
+
except Exception as e: # noqa: BLE001
|
191
191
|
print(
|
192
192
|
f"Failed to save PNG images. Install kaleido to enable PNG export: {e}"
|
193
193
|
)
|
@@ -227,7 +227,7 @@ class PlotManager:
|
|
227
227
|
x=config.experiments,
|
228
228
|
y=values,
|
229
229
|
name=f"{label} - {engine_name}",
|
230
|
-
line=
|
230
|
+
line={"color": color, "width": 3},
|
231
231
|
mode="lines+markers",
|
232
232
|
)
|
233
233
|
)
|
@@ -237,7 +237,7 @@ class PlotManager:
|
|
237
237
|
xaxis_title="Number of points",
|
238
238
|
yaxis_title="Time (s)" if "rate" not in metric else "Ops/sec",
|
239
239
|
template="plotly_dark",
|
240
|
-
legend=
|
240
|
+
legend={"orientation": "v"},
|
241
241
|
height=600,
|
242
242
|
)
|
243
243
|
|