fastquadtree 0.4.1__tar.gz → 0.5.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.
Files changed (47) hide show
  1. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/.github/workflows/release.yml +29 -11
  2. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/.pre-commit-config.yaml +2 -11
  3. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/Cargo.lock +1 -1
  4. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/Cargo.toml +7 -1
  5. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/PKG-INFO +29 -47
  6. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/README.md +28 -46
  7. fastquadtree-0.5.0/assets/quadtree_bench_throughput.png +0 -0
  8. fastquadtree-0.5.0/assets/quadtree_bench_time.png +0 -0
  9. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/benchmarks/quadtree_bench/engines.py +1 -1
  10. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/benchmarks/quadtree_bench/runner.py +1 -1
  11. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/pyproject.toml +15 -0
  12. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/pysrc/fastquadtree/_bimap.py +3 -3
  13. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/src/geom.rs +8 -8
  14. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/src/lib.rs +8 -8
  15. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/src/quadtree.rs +24 -27
  16. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/tests/insertions.rs +3 -3
  17. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/tests/nearest_neighbor.rs +6 -6
  18. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/tests/query.rs +4 -4
  19. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/tests/test_wrapper_edges.py +17 -0
  20. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/tests/unconventional_bounds.rs +5 -5
  21. fastquadtree-0.4.1/assets/quadtree_bench_throughput.png +0 -0
  22. fastquadtree-0.4.1/assets/quadtree_bench_time.png +0 -0
  23. fastquadtree-0.4.1/examples/delete_demo.rs +0 -63
  24. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/.gitignore +0 -0
  25. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/LICENSE +0 -0
  26. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/assets/interactive_v2_screenshot.png +0 -0
  27. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/benchmarks/benchmark_native_vs_shim.py +0 -0
  28. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/benchmarks/cross_library_bench.py +0 -0
  29. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/benchmarks/quadtree_bench/__init__.py +0 -0
  30. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/benchmarks/quadtree_bench/main.py +0 -0
  31. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/benchmarks/quadtree_bench/plotting.py +0 -0
  32. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/benchmarks/requirements.txt +0 -0
  33. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/benchmarks/runner.py +0 -0
  34. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/interactive/interactive.py +0 -0
  35. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/interactive/interactive_v2.py +0 -0
  36. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/interactive/requirements.txt +0 -0
  37. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/pysrc/fastquadtree/__init__.py +0 -0
  38. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/pysrc/fastquadtree/__init__.pyi +0 -0
  39. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/pysrc/fastquadtree/_item.py +0 -0
  40. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/pysrc/fastquadtree/py.typed +0 -0
  41. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/tests/rectangle_traversal.rs +0 -0
  42. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/tests/test_bimap.py +0 -0
  43. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/tests/test_delete.rs +0 -0
  44. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/tests/test_delete_by_object.py +0 -0
  45. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/tests/test_delete_python.py +0 -0
  46. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/tests/test_python.py +0 -0
  47. {fastquadtree-0.4.1 → fastquadtree-0.5.0}/tests/test_unconventional_bounds.py +0 -0
@@ -2,8 +2,7 @@ name: Publish to PyPI
2
2
 
3
3
  on:
4
4
  push:
5
- tags:
6
- - "v*"
5
+ tags: ["v*"]
7
6
  release:
8
7
  types: [published]
9
8
  workflow_dispatch:
@@ -18,21 +17,41 @@ jobs:
18
17
  uses: actions/setup-python@v5
19
18
  with:
20
19
  python-version: "3.10"
21
- - name: Build with maturin
20
+
21
+ - name: Create venv for maturin develop
22
+ run: python -m venv .venv
23
+
24
+ - name: Build with maturin & install into .venv
22
25
  uses: PyO3/maturin-action@v1
23
26
  with:
24
- command: build
27
+ command: develop
28
+ args: --release
25
29
  manylinux: manylinux2014
26
- - name: Install Dependencies
27
- run: pip install pytest pytest-cov
28
- - name: Test with pytest
29
- run: pytest --cov --cov-branch --cov-report=xml
30
+
31
+ - name: Install Python test deps into .venv
32
+ run: |
33
+ . .venv/bin/activate
34
+ pip install -U pip pytest pytest-cov
35
+
36
+ - name: Run Python tests
37
+ run: |
38
+ . .venv/bin/activate
39
+ pytest
40
+
41
+ - name: Install Rust toolchain
42
+ uses: dtolnay/rust-toolchain@stable
43
+
44
+ - name: Run Rust tests
45
+ run: cargo test
46
+
30
47
  - name: Upload coverage to Codecov
31
48
  uses: codecov/codecov-action@v5
32
49
  with:
33
50
  token: ${{ secrets.CODECOV_TOKEN }}
51
+
34
52
  build:
35
53
  name: Build wheels
54
+ needs: test
36
55
  runs-on: ${{ matrix.os }}
37
56
  strategy:
38
57
  matrix:
@@ -45,8 +64,7 @@ jobs:
45
64
  args: "--skip-existing"
46
65
  steps:
47
66
  - uses: actions/checkout@v4
48
- - name: Set up Python
49
- uses: actions/setup-python@v5
67
+ - uses: actions/setup-python@v5
50
68
  with:
51
69
  python-version: "3.10"
52
70
  - name: Build and publish with maturin
@@ -56,4 +74,4 @@ jobs:
56
74
  with:
57
75
  command: publish
58
76
  args: ${{ matrix.args }}
59
- manylinux: manylinux2014
77
+ manylinux: manylinux2014
@@ -31,16 +31,7 @@ repos:
31
31
  # This both runs pytest and generates .coverage data
32
32
  - id: pytest
33
33
  name: pytest (under coverage)
34
- entry: bash -c "coverage run -m pytest -q"
34
+ entry: pytest
35
35
  language: system
36
36
  pass_filenames: false
37
- always_run: true
38
-
39
- # 4) Coverage threshold check
40
- # Change FAIL_UNDER to your target percent
41
- - id: coverage-report
42
- name: coverage report
43
- entry: bash -c 'coverage report --omit="tests/*" --fail-under=85'
44
- language: system
45
- pass_filenames: false
46
- always_run: true
37
+ always_run: true
@@ -22,7 +22,7 @@ checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
22
22
 
23
23
  [[package]]
24
24
  name = "fastquadtree"
25
- version = "0.4.1"
25
+ version = "0.5.0"
26
26
  dependencies = [
27
27
  "pyo3",
28
28
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "fastquadtree"
3
- version = "0.4.1"
3
+ version = "0.5.0"
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.1
3
+ Version: 0.5.0
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3 :: Only
6
6
  Classifier: Programming Language :: Rust
@@ -37,7 +37,6 @@ Project-URL: Issues, https://github.com/Elan456/fastquadtree/issues
37
37
  [![Rust core via PyO3](https://img.shields.io/badge/Rust-core%20via%20PyO3-orange)](https://pyo3.rs/)
38
38
  [![Built with maturin](https://img.shields.io/badge/Built%20with-maturin-1f6feb)](https://www.maturin.rs/)
39
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/)
41
40
 
42
41
 
43
42
  ![Interactive_V2_Screenshot](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/interactive_v2_screenshot.png)
@@ -49,6 +48,29 @@ Rust-optimized quadtree with a simple Python API.
49
48
  - Python ≥ 3.8
50
49
  - Import path: `from fastquadtree import QuadTree`
51
50
 
51
+ ## Benchmarks
52
+
53
+ fastquadtree **outperforms** all other quadtree Python packages, including the Rtree spatial index.
54
+
55
+ ### Library comparison
56
+
57
+ ![Total time](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/quadtree_bench_time.png)
58
+ ![Throughput](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/quadtree_bench_throughput.png)
59
+
60
+ ### Summary (largest dataset, PyQtree baseline)
61
+ - Points: **500,000**, Queries: **500**
62
+ --------------------
63
+ - Fastest total: **fastquadtree** at **1.591 s**
64
+
65
+ | Library | Build (s) | Query (s) | Total (s) | Speed vs PyQtree |
66
+ |---|---:|---:|---:|---:|
67
+ | fastquadtree | 0.165 | 1.427 | 1.591 | 5.09× |
68
+ | Rtree | 1.320 | 2.369 | 3.688 | 2.20× |
69
+ | PyQtree | 2.687 | 5.415 | 8.102 | 1.00× |
70
+ | nontree-QuadTree | 1.284 | 9.891 | 11.175 | 0.73× |
71
+ | quads | 2.346 | 10.129 | 12.475 | 0.65× |
72
+ | e-pyquadtree | 1.795 | 11.855 | 13.650 | 0.59× |
73
+
52
74
  ## Install
53
75
 
54
76
  ```bash
@@ -109,25 +131,7 @@ print(f"Deleted player: {deleted}") # True
109
131
 
110
132
  You can keep the tree pure and manage your own id → object map, or let the wrapper manage it.
111
133
 
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**
134
+ **Wrapper Managed Objects**
131
135
 
132
136
  ```python
133
137
  from fastquadtree import QuadTree
@@ -137,7 +141,7 @@ qt = QuadTree((0, 0, 1000, 1000), capacity=16, track_objects=True)
137
141
  # Store the object alongside the point
138
142
  qt.insert((25, 40), obj={"name": "apple"})
139
143
 
140
- # Ask for Item objects so you can access .obj lazily
144
+ # Ask for Item objects within a bounding box
141
145
  items = qt.query((0, 0, 100, 100), as_items=True)
142
146
  for it in items:
143
147
  print(it.id, it.x, it.y, it.obj)
@@ -151,7 +155,7 @@ qt.attach(123, my_object) # binds object to id 123
151
155
 
152
156
  ## API
153
157
 
154
- ### `QuadTree(bounds, capacity, *, max_depth=None, track_objects=False, start_id=1)`
158
+ ### `QuadTree(bounds, capacity, max_depth=None, track_objects=False, start_id=1)`
155
159
 
156
160
  * `bounds` — tuple `(min_x, min_y, max_x, max_y)` defines the 2D area covered by the quadtree
157
161
  * `capacity` — max number of points kept in a leaf before splitting
@@ -204,30 +208,8 @@ Full docs are in the docstrings of the [Python Shim](pysrc/fastquadtree/__init__
204
208
  * For fastest local runs, use `maturin develop --release`.
205
209
  * The wrapper keeps Python overhead low: raw tuple results by default, `Item` wrappers only when requested.
206
210
 
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
211
 
230
- ### Native vs Shim
212
+ ### Native vs Shim Benchmark
231
213
 
232
214
  **Setup**
233
215
  - Points: 500,000
@@ -277,7 +259,7 @@ Check the CLI arguments for the cross-library benchmark in `benchmarks/quadtree_
277
259
  Allowed. For k-nearest, duplicates are de-duplicated by id. For range queries you will see every inserted point.
278
260
 
279
261
  **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.
262
+ 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
263
 
282
264
  **Can I store rectangles or circles?**
283
265
  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.
@@ -14,7 +14,6 @@
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
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/)
18
17
 
19
18
 
20
19
  ![Interactive_V2_Screenshot](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/interactive_v2_screenshot.png)
@@ -26,6 +25,29 @@ Rust-optimized quadtree with a simple Python API.
26
25
  - Python ≥ 3.8
27
26
  - Import path: `from fastquadtree import QuadTree`
28
27
 
28
+ ## Benchmarks
29
+
30
+ fastquadtree **outperforms** all other quadtree Python packages, including the Rtree spatial index.
31
+
32
+ ### Library comparison
33
+
34
+ ![Total time](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/quadtree_bench_time.png)
35
+ ![Throughput](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/quadtree_bench_throughput.png)
36
+
37
+ ### Summary (largest dataset, PyQtree baseline)
38
+ - Points: **500,000**, Queries: **500**
39
+ --------------------
40
+ - Fastest total: **fastquadtree** at **1.591 s**
41
+
42
+ | Library | Build (s) | Query (s) | Total (s) | Speed vs PyQtree |
43
+ |---|---:|---:|---:|---:|
44
+ | fastquadtree | 0.165 | 1.427 | 1.591 | 5.09× |
45
+ | Rtree | 1.320 | 2.369 | 3.688 | 2.20× |
46
+ | PyQtree | 2.687 | 5.415 | 8.102 | 1.00× |
47
+ | nontree-QuadTree | 1.284 | 9.891 | 11.175 | 0.73× |
48
+ | quads | 2.346 | 10.129 | 12.475 | 0.65× |
49
+ | e-pyquadtree | 1.795 | 11.855 | 13.650 | 0.59× |
50
+
29
51
  ## Install
30
52
 
31
53
  ```bash
@@ -86,25 +108,7 @@ print(f"Deleted player: {deleted}") # True
86
108
 
87
109
  You can keep the tree pure and manage your own id → object map, or let the wrapper manage it.
88
110
 
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**
111
+ **Wrapper Managed Objects**
108
112
 
109
113
  ```python
110
114
  from fastquadtree import QuadTree
@@ -114,7 +118,7 @@ qt = QuadTree((0, 0, 1000, 1000), capacity=16, track_objects=True)
114
118
  # Store the object alongside the point
115
119
  qt.insert((25, 40), obj={"name": "apple"})
116
120
 
117
- # Ask for Item objects so you can access .obj lazily
121
+ # Ask for Item objects within a bounding box
118
122
  items = qt.query((0, 0, 100, 100), as_items=True)
119
123
  for it in items:
120
124
  print(it.id, it.x, it.y, it.obj)
@@ -128,7 +132,7 @@ qt.attach(123, my_object) # binds object to id 123
128
132
 
129
133
  ## API
130
134
 
131
- ### `QuadTree(bounds, capacity, *, max_depth=None, track_objects=False, start_id=1)`
135
+ ### `QuadTree(bounds, capacity, max_depth=None, track_objects=False, start_id=1)`
132
136
 
133
137
  * `bounds` — tuple `(min_x, min_y, max_x, max_y)` defines the 2D area covered by the quadtree
134
138
  * `capacity` — max number of points kept in a leaf before splitting
@@ -181,30 +185,8 @@ Full docs are in the docstrings of the [Python Shim](pysrc/fastquadtree/__init__
181
185
  * For fastest local runs, use `maturin develop --release`.
182
186
  * The wrapper keeps Python overhead low: raw tuple results by default, `Item` wrappers only when requested.
183
187
 
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
188
 
207
- ### Native vs Shim
189
+ ### Native vs Shim Benchmark
208
190
 
209
191
  **Setup**
210
192
  - Points: 500,000
@@ -254,7 +236,7 @@ Check the CLI arguments for the cross-library benchmark in `benchmarks/quadtree_
254
236
  Allowed. For k-nearest, duplicates are de-duplicated by id. For range queries you will see every inserted point.
255
237
 
256
238
  **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.
239
+ 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
240
 
259
241
  **Can I store rectangles or circles?**
260
242
  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.
@@ -247,7 +247,7 @@ def get_engines(
247
247
  "fastquadtree": _create_fastquadtree_engine(bounds, max_points, max_depth),
248
248
  "e-pyquadtree": _create_e_pyquadtree_engine(bounds, max_points, max_depth),
249
249
  "PyQtree": _create_pyqtree_engine(bounds, max_points, max_depth),
250
- # "Brute force": _create_brute_force_engine(bounds, max_points, max_depth), # Brute force doesn't scale well on the graphs so omit it from the main set
250
+ # "Brute force": _create_brute_force_engine(bounds, max_points, max_depth), # Brute force doesn't scale well on the graphs so omit it from the main set
251
251
  }
252
252
 
253
253
  # Optional engines (only include if import succeeded)
@@ -34,7 +34,7 @@ class BenchmarkConfig:
34
34
  """Generate experiment point sizes."""
35
35
  self.experiments = [2, 4, 8, 16]
36
36
  while self.experiments[-1] < self.max_experiment_points:
37
- self.experiments.append(int(self.experiments[-1] * 1.5))
37
+ self.experiments.append(int(self.experiments[-1] * 2))
38
38
  if self.experiments[-1] > self.max_experiment_points:
39
39
  self.experiments[-1] = self.max_experiment_points
40
40
 
@@ -39,3 +39,18 @@ module-name = "fastquadtree._native"
39
39
  # Choose a wide wheel tag for Linux. Adjust if your build images are newer.
40
40
  # Options include: manylinux2014, manylinux_2_28, musllinux_1_2, or "off".
41
41
  compatibility = "manylinux2014"
42
+
43
+ [tool.pytest.ini_options]
44
+ addopts = "--cov=fastquadtree --cov-branch --cov-report=xml --cov-fail-under=95"
45
+ testpaths = ["tests"] # still run tests
46
+
47
+ [tool.coverage.run]
48
+ source = ["fastquadtree"]
49
+ omit = [
50
+ "tests/*",
51
+ ]
52
+
53
+ [tool.coverage.report]
54
+ omit = [
55
+ "tests/*",
56
+ ]
@@ -80,11 +80,11 @@ class BiMap:
80
80
  """
81
81
  removed = None
82
82
  # Remove by id first
83
- if self._id_to_item.get(item.id) is item:
84
- removed = self._id_to_item.pop(item.id)
83
+ removed = self._id_to_item.pop(item.id)
84
+
85
85
  # Remove by obj side
86
86
  obj = item.obj
87
- if obj is not None and self._objid_to_item.get(id(obj)) is item:
87
+ if obj is not None:
88
88
  self._objid_to_item.pop(id(obj), None)
89
89
  removed = removed or item
90
90
  return removed
@@ -1,15 +1,15 @@
1
1
  #[derive(Copy, Clone, Debug, PartialEq, Default)]
2
2
  pub struct Point {
3
- pub x: f64,
4
- pub y: f64,
3
+ pub x: f32,
4
+ pub y: f32,
5
5
  }
6
6
 
7
7
  #[derive(Copy, Clone, Debug, PartialEq, Default)]
8
8
  pub struct Rect {
9
- pub min_x: f64,
10
- pub min_y: f64,
11
- pub max_x: f64,
12
- pub max_y: f64,
9
+ pub min_x: f32,
10
+ pub min_y: f32,
11
+ pub max_x: f32,
12
+ pub max_y: f32,
13
13
  }
14
14
 
15
15
  impl Rect {
@@ -23,7 +23,7 @@ impl Rect {
23
23
  }
24
24
  }
25
25
 
26
- pub fn dist_sq_point_to_rect(p: &Point, r: &Rect) -> f64 {
26
+ pub fn dist_sq_point_to_rect(p: &Point, r: &Rect) -> f32 {
27
27
  let dx = if p.x < r.min_x {
28
28
  r.min_x - p.x
29
29
  } else if p.x > r.max_x {
@@ -43,7 +43,7 @@ pub fn dist_sq_point_to_rect(p: &Point, r: &Rect) -> f64 {
43
43
  dx * dx + dy * dy
44
44
  }
45
45
 
46
- pub fn dist_sq_points(a: &Point, b: &Point) -> f64 {
46
+ pub fn dist_sq_points(a: &Point, b: &Point) -> f32 {
47
47
  let dx = a.x - b.x;
48
48
  let dy = a.y - b.y;
49
49
  dx * dx + dy * dy
@@ -8,7 +8,7 @@ pub use crate::quadtree::{Item, QuadTree};
8
8
 
9
9
  use pyo3::prelude::*;
10
10
 
11
- fn item_to_tuple(it: Item) -> (u64, f64, f64) {
11
+ fn item_to_tuple(it: Item) -> (u64, f32, f32) {
12
12
  (it.id, it.point.x, it.point.y)
13
13
  }
14
14
 
@@ -20,7 +20,7 @@ pub struct PyQuadTree {
20
20
  #[pymethods]
21
21
  impl PyQuadTree {
22
22
  #[new]
23
- pub fn new(bounds: (f64, f64, f64, f64), capacity: usize, max_depth: Option<usize>) -> Self {
23
+ pub fn new(bounds: (f32, f32, f32, f32), capacity: usize, max_depth: Option<usize>) -> Self {
24
24
  let (min_x, min_y, max_x, max_y) = bounds;
25
25
  let rect = Rect { min_x, min_y, max_x, max_y };
26
26
  let inner = match max_depth {
@@ -30,17 +30,17 @@ impl PyQuadTree {
30
30
  Self { inner }
31
31
  }
32
32
 
33
- pub fn insert(&mut self, id: u64, xy: (f64, f64)) -> bool {
33
+ pub fn insert(&mut self, id: u64, xy: (f32, f32)) -> bool {
34
34
  let (x, y) = xy;
35
35
  self.inner.insert(Item { id, point: Point { x, y } })
36
36
  }
37
37
 
38
- pub fn delete(&mut self, id: u64, xy: (f64, f64)) -> bool {
38
+ pub fn delete(&mut self, id: u64, xy: (f32, f32)) -> bool {
39
39
  let (x, y) = xy;
40
40
  self.inner.delete(id, Point { x, y })
41
41
  }
42
42
 
43
- pub fn query(&self, rect: (f64, f64, f64, f64)) -> Vec<(u64, f64, f64)> {
43
+ pub fn query(&self, rect: (f32, f32, f32, f32)) -> Vec<(u64, f32, f32)> {
44
44
  let (min_x, min_y, max_x, max_y) = rect;
45
45
  self.inner
46
46
  .query(Rect { min_x, min_y, max_x, max_y })
@@ -49,12 +49,12 @@ impl PyQuadTree {
49
49
  .collect()
50
50
  }
51
51
 
52
- pub fn nearest_neighbor(&self, xy: (f64, f64)) -> Option<(u64, f64, f64)> {
52
+ pub fn nearest_neighbor(&self, xy: (f32, f32)) -> Option<(u64, f32, f32)> {
53
53
  let (x, y) = xy;
54
54
  self.inner.nearest_neighbor(Point { x, y }).map(item_to_tuple)
55
55
  }
56
56
 
57
- pub fn nearest_neighbors(&self, xy: (f64, f64), k: usize) -> Vec<(u64, f64, f64)> {
57
+ pub fn nearest_neighbors(&self, xy: (f32, f32), k: usize) -> Vec<(u64, f32, f32)> {
58
58
  let (x, y) = xy;
59
59
  self.inner
60
60
  .nearest_neighbors(Point { x, y }, k)
@@ -64,7 +64,7 @@ impl PyQuadTree {
64
64
  }
65
65
 
66
66
  /// Returns all rectangle boundaries in the quadtree for visualization
67
- pub fn get_all_rectangles(&self) -> Vec<(f64, f64, f64, f64)> {
67
+ pub fn get_all_rectangles(&self) -> Vec<(f32, f32, f32, f32)> {
68
68
  self.inner
69
69
  .get_all_rectangles()
70
70
  .into_iter()
@@ -22,6 +22,7 @@ pub struct QuadTree {
22
22
  // 1: (x >= cx, y < cy)
23
23
  // 2: (x < cx, y >= cy)
24
24
  // 3: (x >= cx, y >= cy)
25
+ #[inline(always)]
25
26
  fn child_index_for_point(b: &Rect, p: &Point) -> usize {
26
27
  let cx = 0.5 * (b.min_x + b.max_x);
27
28
  let cy = 0.5 * (b.min_y + b.max_y);
@@ -34,7 +35,7 @@ impl QuadTree {
34
35
  pub fn new(boundary: Rect, capacity: usize) -> Self {
35
36
  QuadTree {
36
37
  boundary,
37
- items: Vec::new(),
38
+ items: Vec::with_capacity(capacity),
38
39
  capacity,
39
40
  children: None,
40
41
  depth: 0,
@@ -45,7 +46,7 @@ impl QuadTree {
45
46
  pub fn new_with_max_depth(boundary: Rect, capacity: usize, max_depth: usize) -> Self {
46
47
  QuadTree {
47
48
  boundary,
48
- items: Vec::new(),
49
+ items: Vec::with_capacity(capacity),
49
50
  capacity,
50
51
  children: None,
51
52
  depth: 0,
@@ -56,7 +57,7 @@ impl QuadTree {
56
57
  pub fn new_child(boundary: Rect, capacity: usize, depth: usize, max_depth: usize) -> Self {
57
58
  QuadTree {
58
59
  boundary,
59
- items: Vec::new(),
60
+ items: Vec::with_capacity(capacity),
60
61
  capacity,
61
62
  children: None,
62
63
  depth: depth,
@@ -121,42 +122,38 @@ impl QuadTree {
121
122
 
122
123
  pub fn query(&self, range: Rect) -> Vec<Item> {
123
124
  let mut out = Vec::new();
124
- self.query_into(&range, &mut out);
125
- out
126
- }
125
+ let mut stack: Vec<&QuadTree> = Vec::new();
126
+ stack.push(self);
127
127
 
128
- fn query_into(&self, range: &Rect, out: &mut Vec<Item>) {
129
- // prune if this node does not intersect the query
130
- if !self.boundary.intersects(range) {
131
- return;
132
- }
133
-
134
- // check items stored at this node
135
- for it in &self.items {
136
- if range.contains(&it.point) {
137
- out.push(*it); // Item is Copy
128
+ while let Some(node) = stack.pop() {
129
+ for it in &node.items {
130
+ if range.contains(&it.point) {
131
+ out.push(*it);
132
+ }
138
133
  }
139
- }
140
-
141
- // recurse to children
142
- if let Some(children) = self.children.as_ref() {
143
- for child in children.iter() {
144
- child.query_into(range, out);
134
+ if let Some(children) = node.children.as_ref() {
135
+ // Push children that intersect the query range
136
+ for child in children.iter() {
137
+ if range.intersects(&child.boundary) {
138
+ stack.push(child);
139
+ }
140
+ }
145
141
  }
146
142
  }
143
+ out
147
144
  }
148
145
 
149
146
  pub fn nearest_neighbor(&self, point: Point) -> Option<Item> {
150
- self.nearest_neighbors_within(point, 1, f64::INFINITY)
147
+ self.nearest_neighbors_within(point, 1, f32::INFINITY)
151
148
  .into_iter()
152
149
  .next()
153
150
  }
154
151
 
155
152
  pub fn nearest_neighbors(&self, point: Point, k: usize) -> Vec<Item> {
156
- self.nearest_neighbors_within(point, k, f64::INFINITY)
153
+ self.nearest_neighbors_within(point, k, f32::INFINITY)
157
154
  }
158
155
 
159
- pub fn nearest_neighbors_within(&self, point: Point, k: usize, max_distance: f64) -> Vec<Item> {
156
+ pub fn nearest_neighbors_within(&self, point: Point, k: usize, max_distance: f32) -> Vec<Item> {
160
157
  if k == 0 {
161
158
  return Vec::new();
162
159
  }
@@ -167,7 +164,7 @@ impl QuadTree {
167
164
 
168
165
  for _ in 0..k {
169
166
  // stack holds (node_ref, bbox_distance_sq)
170
- let mut stack: Vec<(&QuadTree, f64)> = Vec::new();
167
+ let mut stack: Vec<(&QuadTree, f32)> = Vec::new();
171
168
  stack.push((self, dist_sq_point_to_rect(&point, &self.boundary)));
172
169
 
173
170
  let mut best: Option<Item> = None;
@@ -181,7 +178,7 @@ impl QuadTree {
181
178
 
182
179
  if let Some(children) = node.children.as_ref() {
183
180
  // compute and sort children by bbox distance, push farthest first
184
- let mut kids: Vec<(&QuadTree, f64)> = children
181
+ let mut kids: Vec<(&QuadTree, f32)> = children
185
182
  .iter()
186
183
  .map(|c| (c, dist_sq_point_to_rect(&point, &c.boundary)))
187
184
  .filter(|(_, d2)| *d2 < best_d2)
@@ -1,10 +1,10 @@
1
1
  use fastquadtree::{Point, Rect, Item, QuadTree};
2
2
 
3
- fn r(x0: f64, y0: f64, x1: f64, y1: f64) -> Rect {
3
+ fn r(x0: f32, y0: f32, x1: f32, y1: f32) -> Rect {
4
4
  Rect { min_x: x0, min_y: y0, max_x: x1, max_y: y1 }
5
5
  }
6
6
 
7
- fn pt(x: f64, y: f64) -> Point { Point { x, y } }
7
+ fn pt(x: f32, y: f32) -> Point { Point { x, y } }
8
8
 
9
9
  #[test]
10
10
  fn rect_contains_half_open() {
@@ -73,7 +73,7 @@ fn many_inserts_all_succeed_when_inside() {
73
73
  let mut id = 1u64;
74
74
  for x in (10..1000).step_by(100) {
75
75
  for y in (10..1000).step_by(100) {
76
- if qt.insert(Item { id, point: pt(x as f64, y as f64) }) {
76
+ if qt.insert(Item { id, point: pt(x as f32, y as f32) }) {
77
77
  ok += 1;
78
78
  }
79
79
  id += 1;
@@ -1,15 +1,15 @@
1
1
  use fastquadtree::{Point, Rect, Item, QuadTree};
2
2
 
3
- fn r(x0: f64, y0: f64, x1: f64, y1: f64) -> Rect {
3
+ fn r(x0: f32, y0: f32, x1: f32, y1: f32) -> Rect {
4
4
  Rect { min_x: x0, min_y: y0, max_x: x1, max_y: y1 }
5
5
  }
6
- fn pt(x: f64, y: f64) -> Point { Point { x, y } }
6
+ fn pt(x: f32, y: f32) -> Point { Point { x, y } }
7
7
  fn ids(v: &[Item]) -> Vec<u64> {
8
8
  let mut out: Vec<u64> = v.iter().map(|it| it.id).collect();
9
9
  out.sort_unstable();
10
10
  out
11
11
  }
12
- fn dist2(a: Point, b: Point) -> f64 {
12
+ fn dist2(a: Point, b: Point) -> f32 {
13
13
  let dx = a.x - b.x;
14
14
  let dy = a.y - b.y;
15
15
  dx * dx + dy * dy
@@ -69,7 +69,7 @@ fn knn_basic_ordering_no_split() {
69
69
  assert_eq!(order, vec![2, 1, 4, 3, 5]);
70
70
 
71
71
  // distances should be nondecreasing
72
- let d: Vec<f64> = res.iter().map(|it| dist2(q, it.point)).collect();
72
+ let d: Vec<f32> = res.iter().map(|it| dist2(q, it.point)).collect();
73
73
  for w in d.windows(2) {
74
74
  assert!(w[0] <= w[1] + 1e-12);
75
75
  }
@@ -176,7 +176,7 @@ fn ordering_is_by_distance_even_after_splits() {
176
176
  let mut id = 1u64;
177
177
  for y in (4..=60).step_by(8) {
178
178
  for x in (4..=60).step_by(8) {
179
- qt.insert(Item { id, point: pt(x as f64, y as f64) });
179
+ qt.insert(Item { id, point: pt(x as f32, y as f32) });
180
180
  id += 1;
181
181
  }
182
182
  }
@@ -185,7 +185,7 @@ fn ordering_is_by_distance_even_after_splits() {
185
185
  assert!(!res.is_empty());
186
186
 
187
187
  // distances should be nondecreasing
188
- let d: Vec<f64> = res.iter().map(|it| dist2(q, it.point)).collect();
188
+ let d: Vec<f32> = res.iter().map(|it| dist2(q, it.point)).collect();
189
189
  for w in d.windows(2) {
190
190
  assert!(w[0] <= w[1] + 1e-12);
191
191
  }
@@ -1,9 +1,9 @@
1
1
  use fastquadtree::{Point, Rect, Item, QuadTree};
2
2
 
3
- fn r(x0: f64, y0: f64, x1: f64, y1: f64) -> Rect {
3
+ fn r(x0: f32, y0: f32, x1: f32, y1: f32) -> Rect {
4
4
  Rect { min_x: x0, min_y: y0, max_x: x1, max_y: y1 }
5
5
  }
6
- fn pt(x: f64, y: f64) -> Point { Point { x, y } }
6
+ fn pt(x: f32, y: f32) -> Point { Point { x, y } }
7
7
  fn ids(v: &[Item]) -> Vec<u64> {
8
8
  let mut out: Vec<u64> = v.iter().map(|it| it.id).collect();
9
9
  out.sort_unstable();
@@ -101,8 +101,8 @@ fn query_range_covering_multiple_children_returns_union() {
101
101
  fn full_range_query_returns_all_items() {
102
102
  let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 3);
103
103
  for i in 0..9 {
104
- let x = 10.0 + 10.0 * (i as f64);
105
- let y = 20.0 + 5.0 * (i as f64);
104
+ let x = 10.0 + 10.0 * (i as f32);
105
+ let y = 20.0 + 5.0 * (i as f32);
106
106
  assert!(qt.insert(Item { id: i + 1, point: pt(x, y) }));
107
107
  }
108
108
  let hits = qt.query(r(0.0, 0.0, 100.0, 100.0));
@@ -207,3 +207,20 @@ def test_insert_many_points_exception_for_out_of_bounds():
207
207
 
208
208
  with pytest.raises(ValueError):
209
209
  qt.insert_many_points(points)
210
+
211
+
212
+ def test_auto_id_collision_prevention():
213
+ qt = QuadTree(BOUNDS, capacity=8, track_objects=True)
214
+ id1 = qt.insert((10, 10)) # auto ID
215
+
216
+ id2 = qt.insert((20, 20), id=200) # Large ID
217
+
218
+ # Next auto ID should be 201
219
+ id3 = qt.insert((30, 30)) # auto ID
220
+
221
+ assert id3 == 201
222
+ assert len(qt) == 3
223
+ assert id1 != id2 != id3
224
+
225
+ id4 = qt.insert((40, 40), id=150) # Manual ID lower than current auto ID
226
+ assert id4 == 150
@@ -1,14 +1,14 @@
1
1
  use fastquadtree::{Point, Rect, Item, QuadTree};
2
2
 
3
- fn r(x0: f64, y0: f64, x1: f64, y1: f64) -> Rect {
3
+ fn r(x0: f32, y0: f32, x1: f32, y1: f32) -> Rect {
4
4
  Rect { min_x: x0, min_y: y0, max_x: x1, max_y: y1 }
5
5
  }
6
6
 
7
- fn pt(x: f64, y: f64) -> Point {
7
+ fn pt(x: f32, y: f32) -> Point {
8
8
  Point { x, y }
9
9
  }
10
10
 
11
- fn item(id: u64, x: f64, y: f64) -> Item {
11
+ fn item(id: u64, x: f32, y: f32) -> Item {
12
12
  Item { id, point: pt(x, y) }
13
13
  }
14
14
 
@@ -226,8 +226,8 @@ fn stress_test_negative_coordinates() {
226
226
  let mut inserted_count = 0;
227
227
  for i in 0..50 {
228
228
  for j in 0..50 {
229
- let x = -20.0 * (i as f64) - 10.0; // Range from -10 to -990
230
- let y = -20.0 * (j as f64) - 10.0; // Range from -10 to -990
229
+ let x = -20.0 * (i as f32) - 10.0; // Range from -10 to -990
230
+ let y = -20.0 * (j as f32) - 10.0; // Range from -10 to -990
231
231
  let id = (i * 50 + j) as u64 + 1;
232
232
  if x >= -1000.0 && y >= -1000.0 && x < 0.0 && y < 0.0 {
233
233
  assert!(qt.insert(item(id, x, y)));
@@ -1,63 +0,0 @@
1
- use fastquadtree::{QuadTree, Item, Point, Rect};
2
-
3
- fn main() {
4
- println!("=== QuadTree Delete by ID+Location Demo ===\n");
5
-
6
- // Create a quadtree
7
- let mut tree = QuadTree::new(Rect { min_x: 0.0, min_y: 0.0, max_x: 100.0, max_y: 100.0 }, 4);
8
-
9
- // Insert multiple items at the same location
10
- let shared_location = Point { x: 50.0, y: 50.0 };
11
- tree.insert(Item { id: 1, point: shared_location });
12
- tree.insert(Item { id: 2, point: shared_location });
13
- tree.insert(Item { id: 3, point: shared_location });
14
-
15
- // Insert items at different locations
16
- tree.insert(Item { id: 4, point: Point { x: 25.0, y: 25.0 } });
17
- tree.insert(Item { id: 5, point: Point { x: 75.0, y: 75.0 } });
18
-
19
- println!("Initial tree with {} items", tree.count_items());
20
-
21
- // Query the area around the shared location
22
- let query_rect = Rect { min_x: 45.0, min_y: 45.0, max_x: 55.0, max_y: 55.0 };
23
- let items_at_location = tree.query(query_rect);
24
- println!("Items at (50,50): {:?}", items_at_location.iter().map(|i| i.id).collect::<Vec<_>>());
25
-
26
- // Delete specific item by ID+location - only removes that specific item
27
- println!("\nDeleting item with ID=2 at (50,50)...");
28
- let deleted = tree.delete(2, shared_location);
29
- println!("Delete successful: {}", deleted);
30
- println!("Tree now has {} items", tree.count_items());
31
-
32
- // Verify the other items at the same location are still there
33
- let remaining_items = tree.query(query_rect);
34
- println!("Remaining items at (50,50): {:?}", remaining_items.iter().map(|i| i.id).collect::<Vec<_>>());
35
-
36
- // Try to delete the same item again - should fail
37
- println!("\nTrying to delete ID=2 again...");
38
- let deleted_again = tree.delete(2, shared_location);
39
- println!("Delete successful: {}", deleted_again);
40
-
41
- // Try to delete with wrong ID - should fail
42
- println!("\nTrying to delete ID=999 at (50,50)...");
43
- let wrong_id = tree.delete(999, shared_location);
44
- println!("Delete successful: {}", wrong_id);
45
-
46
- // Try to delete with wrong location - should fail
47
- println!("\nTrying to delete ID=1 at wrong location (60,60)...");
48
- let wrong_location = tree.delete(1, Point { x: 60.0, y: 60.0 });
49
- println!("Delete successful: {}", wrong_location);
50
-
51
- // Delete the remaining items at the shared location
52
- println!("\nDeleting remaining items at (50,50)...");
53
- tree.delete(1, shared_location);
54
- tree.delete(3, shared_location);
55
-
56
- println!("Final tree has {} items", tree.count_items());
57
-
58
- // Verify the items at other locations are still there
59
- let all_items = tree.query(Rect { min_x: 0.0, min_y: 0.0, max_x: 100.0, max_y: 100.0 });
60
- println!("All remaining items: {:?}", all_items.iter().map(|i| (i.id, i.point.x, i.point.y)).collect::<Vec<_>>());
61
-
62
- println!("\n=== Demo Complete ===");
63
- }
File without changes
File without changes