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.
Files changed (48) hide show
  1. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/.pre-commit-config.yaml +11 -9
  2. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/Cargo.lock +1 -1
  3. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/Cargo.toml +7 -1
  4. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/PKG-INFO +38 -48
  5. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/README.md +30 -47
  6. fastquadtree-0.5.1/assets/quadtree_bench_throughput.png +0 -0
  7. fastquadtree-0.5.1/assets/quadtree_bench_time.png +0 -0
  8. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/benchmark_native_vs_shim.py +25 -6
  9. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/quadtree_bench/__init__.py +2 -2
  10. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/quadtree_bench/engines.py +21 -8
  11. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/quadtree_bench/main.py +1 -1
  12. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/quadtree_bench/plotting.py +24 -24
  13. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/quadtree_bench/runner.py +83 -13
  14. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/runner.py +0 -7
  15. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/interactive/interactive.py +12 -6
  16. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/interactive/interactive_v2.py +123 -95
  17. fastquadtree-0.5.1/pyproject.toml +130 -0
  18. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/pysrc/fastquadtree/__init__.py +42 -45
  19. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/pysrc/fastquadtree/__init__.pyi +25 -21
  20. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/pysrc/fastquadtree/_bimap.py +12 -11
  21. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/pysrc/fastquadtree/_item.py +5 -4
  22. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/src/geom.rs +8 -8
  23. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/src/lib.rs +8 -8
  24. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/src/quadtree.rs +24 -27
  25. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/insertions.rs +3 -3
  26. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/nearest_neighbor.rs +6 -6
  27. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/query.rs +4 -4
  28. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/test_bimap.py +7 -6
  29. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/test_delete_by_object.py +1 -0
  30. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/test_delete_python.py +0 -1
  31. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/test_python.py +7 -4
  32. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/test_unconventional_bounds.py +4 -2
  33. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/test_wrapper_edges.py +14 -15
  34. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/unconventional_bounds.rs +5 -5
  35. fastquadtree-0.4.2/assets/quadtree_bench_throughput.png +0 -0
  36. fastquadtree-0.4.2/assets/quadtree_bench_time.png +0 -0
  37. fastquadtree-0.4.2/examples/delete_demo.rs +0 -63
  38. fastquadtree-0.4.2/pyproject.toml +0 -56
  39. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/.github/workflows/release.yml +0 -0
  40. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/.gitignore +0 -0
  41. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/LICENSE +0 -0
  42. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/assets/interactive_v2_screenshot.png +0 -0
  43. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/cross_library_bench.py +0 -0
  44. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/benchmarks/requirements.txt +0 -0
  45. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/interactive/requirements.txt +0 -0
  46. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/pysrc/fastquadtree/py.typed +0 -0
  47. {fastquadtree-0.4.2 → fastquadtree-0.5.1}/tests/rectangle_traversal.rs +0 -0
  48. {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: black
3
- - repo: https://github.com/psf/black
4
- rev: 24.8.0
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
- - id: black
7
- name: black
8
- language_version: python3
9
- args: ["--line-length", "88"]
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
@@ -22,7 +22,7 @@ checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
22
22
 
23
23
  [[package]]
24
24
  name = "fastquadtree"
25
- version = "0.4.2"
25
+ version = "0.5.1"
26
26
  dependencies = [
27
27
  "pyo3",
28
28
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "fastquadtree"
3
- version = "0.4.2"
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.4.2
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
  [![Rust core via PyO3](https://img.shields.io/badge/Rust-core%20via%20PyO3-orange)](https://pyo3.rs/)
38
45
  [![Built with maturin](https://img.shields.io/badge/Built%20with-maturin-1f6feb)](https://www.maturin.rs/)
39
- [![Code style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
40
- [![Type checking: mypy](https://img.shields.io/badge/type%20checking-mypy-2a6db2)](http://mypy-lang.org/)
46
+ [![Lint and format: Ruff](https://img.shields.io/badge/Lint%20and%20format-Ruff-46a758?logo=ruff&logoColor=white)](https://docs.astral.sh/ruff/)
47
+
41
48
 
42
49
 
43
50
  ![Interactive_V2_Screenshot](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/interactive_v2_screenshot.png)
@@ -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
+ ![Total time](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/quadtree_bench_time.png)
66
+ ![Throughput](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/quadtree_bench_throughput.png)
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
- **Option A: Manage your own map**
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 so you can access .obj lazily
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, *, max_depth=None, track_objects=False, start_id=1)`
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
- ![Total time](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/quadtree_bench_time.png)
214
- ![Throughput](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/quadtree_bench_throughput.png)
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, xy)` for convenient object-based deletion with O(1) lookup. The tree automatically merges nodes when item counts drop below capacity.
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
  [![Rust core via PyO3](https://img.shields.io/badge/Rust-core%20via%20PyO3-orange)](https://pyo3.rs/)
15
15
  [![Built with maturin](https://img.shields.io/badge/Built%20with-maturin-1f6feb)](https://www.maturin.rs/)
16
- [![Code style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
17
- [![Type checking: mypy](https://img.shields.io/badge/type%20checking-mypy-2a6db2)](http://mypy-lang.org/)
16
+ [![Lint and format: Ruff](https://img.shields.io/badge/Lint%20and%20format-Ruff-46a758?logo=ruff&logoColor=white)](https://docs.astral.sh/ruff/)
17
+
18
18
 
19
19
 
20
20
  ![Interactive_V2_Screenshot](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/interactive_v2_screenshot.png)
@@ -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
+ ![Total time](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/quadtree_bench_time.png)
36
+ ![Throughput](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/quadtree_bench_throughput.png)
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
- **Option A: Manage your own map**
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 so you can access .obj lazily
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, *, max_depth=None, track_objects=False, start_id=1)`
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
- ![Total time](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/quadtree_bench_time.png)
191
- ![Throughput](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/quadtree_bench_throughput.png)
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, xy)` for convenient object-based deletion with O(1) lookup. The tree automatically merges nodes when item counts drop below capacity.
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.
@@ -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), points, queries, args.repeats
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__ = ["Engine", "get_engines", "BenchmarkRunner", "BenchmarkConfig", "PlotManager"]
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
- from pyqtree import Index as PyQTree # Pyqtree
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", "#1f77b4", build, query # display name # color (blue)
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", "#ff7f0e", build, query # display name # color (orange)
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", "#17becf", build, query # display name # color (cyan)
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", "#9467bd", build, query # display name # color (purple)
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
- idx = rindex.Index(stream, properties=p)
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
@@ -10,8 +10,8 @@ import argparse
10
10
  from pathlib import Path
11
11
 
12
12
  from .engines import get_engines
13
- from .runner import BenchmarkRunner, BenchmarkConfig
14
13
  from .plotting import PlotManager
14
+ from .runner import BenchmarkConfig, BenchmarkRunner
15
15
 
16
16
 
17
17
  def main():
@@ -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 Dict, Any, Tuple
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=dict(color=color, width=3),
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=dict(
73
- orientation="v",
74
- traceorder="normal",
75
- xanchor="left",
76
- x=0,
77
- yanchor="top",
78
- y=1,
79
- ),
80
- margin=dict(l=40, r=20, t=80, b=40),
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=dict(color=color, width=3),
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=dict(color=color, width=3),
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=dict(
142
- orientation="h",
143
- x=0,
144
- xanchor="left",
145
- y=1.08, # above the subplots
146
- yanchor="bottom",
147
- ),
148
- margin=dict(l=60, r=40, t=120, b=40),
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=dict(color=color, width=3),
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=dict(orientation="v"),
240
+ legend={"orientation": "v"},
241
241
  height=600,
242
242
  )
243
243