fastquadtree 0.5.1__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/Cargo.lock +2 -1
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/Cargo.toml +2 -1
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/PKG-INFO +23 -13
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/README.md +22 -13
- fastquadtree-0.6.0/assets/quadtree_bench_throughput.png +0 -0
- fastquadtree-0.6.0/assets/quadtree_bench_time.png +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/benchmarks/quadtree_bench/engines.py +39 -2
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/benchmarks/quadtree_bench/main.py +3 -3
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/benchmarks/quadtree_bench/runner.py +15 -6
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/benchmarks/requirements.txt +2 -1
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/pyproject.toml +2 -1
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/pysrc/fastquadtree/__init__.py +22 -26
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/pysrc/fastquadtree/__init__.pyi +0 -1
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/src/lib.rs +20 -6
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/src/quadtree.rs +72 -14
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/tests/query.rs +2 -2
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/tests/test_delete.rs +2 -2
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/tests/unconventional_bounds.rs +2 -2
- fastquadtree-0.5.1/assets/quadtree_bench_throughput.png +0 -0
- fastquadtree-0.5.1/assets/quadtree_bench_time.png +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/.github/workflows/release.yml +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/.gitignore +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/.pre-commit-config.yaml +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/LICENSE +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/assets/interactive_v2_screenshot.png +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/benchmarks/benchmark_native_vs_shim.py +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/benchmarks/cross_library_bench.py +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/benchmarks/quadtree_bench/__init__.py +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/benchmarks/quadtree_bench/plotting.py +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/benchmarks/runner.py +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/interactive/interactive.py +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/interactive/interactive_v2.py +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/interactive/requirements.txt +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/pysrc/fastquadtree/_bimap.py +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/pysrc/fastquadtree/_item.py +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/pysrc/fastquadtree/py.typed +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/src/geom.rs +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/tests/insertions.rs +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/tests/nearest_neighbor.rs +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/tests/rectangle_traversal.rs +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/tests/test_bimap.py +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/tests/test_delete_by_object.py +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/tests/test_delete_python.py +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/tests/test_python.py +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/tests/test_unconventional_bounds.py +0 -0
- {fastquadtree-0.5.1 → fastquadtree-0.6.0}/tests/test_wrapper_edges.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
[package]
|
2
2
|
name = "fastquadtree"
|
3
|
-
version = "0.
|
3
|
+
version = "0.6.0"
|
4
4
|
edition = "2021"
|
5
5
|
readme = "README.md"
|
6
6
|
|
@@ -9,6 +9,7 @@ crate-type = ["rlib", "cdylib"]
|
|
9
9
|
|
10
10
|
[dependencies]
|
11
11
|
pyo3 = { version = "0.21", features = ["extension-module", "abi3-py38"] }
|
12
|
+
smallvec = "1.15.1"
|
12
13
|
|
13
14
|
[profile.release]
|
14
15
|
opt-level = 3
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: fastquadtree
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.6.0
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
5
5
|
Classifier: Programming Language :: Python :: 3 :: Only
|
6
6
|
Classifier: Programming Language :: Rust
|
@@ -35,15 +35,14 @@ Project-URL: Issues, https://github.com/Elan456/fastquadtree/issues
|
|
35
35
|
[](https://pypi.org/project/fastquadtree/#files)
|
36
36
|
[](LICENSE)
|
37
37
|
|
38
|
-
[](https://pepy.tech/projects/fastquadtree)
|
38
|
+
[](https://pepy.tech/projects/fastquadtree)
|
40
39
|
|
41
40
|
[](https://github.com/Elan456/fastquadtree/actions/workflows/ci.yml)
|
42
41
|
[](https://codecov.io/gh/Elan456/fastquadtree)
|
43
42
|
|
44
43
|
[](https://pyo3.rs/)
|
45
44
|
[](https://www.maturin.rs/)
|
46
|
-
[](https://github.com/astral-sh/ruff)
|
47
46
|
|
48
47
|
|
49
48
|
|
@@ -66,18 +65,27 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
66
65
|

|
67
66
|
|
68
67
|
### Summary (largest dataset, PyQtree baseline)
|
69
|
-
- Points: **
|
68
|
+
- Points: **250,000**, Queries: **500**
|
70
69
|
--------------------
|
71
|
-
- Fastest total: **fastquadtree** at **
|
70
|
+
- Fastest total: **fastquadtree** at **0.120 s**
|
72
71
|
|
73
72
|
| Library | Build (s) | Query (s) | Total (s) | Speed vs PyQtree |
|
74
73
|
|---|---:|---:|---:|---:|
|
75
|
-
| fastquadtree | 0.
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
| fastquadtree | 0.031 | 0.089 | 0.120 | 14.64× |
|
75
|
+
| Shapely STRtree | 0.179 | 0.100 | 0.279 | 6.29× |
|
76
|
+
| nontree-QuadTree | 0.595 | 0.605 | 1.200 | 1.46× |
|
77
|
+
| Rtree | 0.961 | 0.300 | 1.261 | 1.39× |
|
78
|
+
| e-pyquadtree | 1.005 | 0.660 | 1.665 | 1.05× |
|
79
|
+
| PyQtree | 1.492 | 0.263 | 1.755 | 1.00× |
|
80
|
+
| quads | 1.407 | 0.484 | 1.890 | 0.93× |
|
81
|
+
|
82
|
+
#### Benchmark Configuration
|
83
|
+
| Parameter | Value |
|
84
|
+
|---|---:|
|
85
|
+
| Bounds | (0, 0, 1000, 1000) |
|
86
|
+
| Max points per node | 128 |
|
87
|
+
| Max depth | 16 |
|
88
|
+
| Queries per experiment | 500 |
|
81
89
|
|
82
90
|
## Install
|
83
91
|
|
@@ -281,7 +289,7 @@ MIT. See `LICENSE`.
|
|
281
289
|
|
282
290
|
## Acknowledgments
|
283
291
|
|
284
|
-
* Python libraries compared: [PyQtree], [e-pyquadtree], [Rtree], [nontree], [quads]
|
292
|
+
* Python libraries compared: [PyQtree], [e-pyquadtree], [Rtree], [nontree], [quads], [Shapely]
|
285
293
|
* Built with [PyO3] and [maturin]
|
286
294
|
|
287
295
|
[PyQtree]: https://pypi.org/project/pyqtree/
|
@@ -291,3 +299,5 @@ MIT. See `LICENSE`.
|
|
291
299
|
[Rtree]: https://pypi.org/project/Rtree/
|
292
300
|
[nontree]: https://pypi.org/project/nontree/
|
293
301
|
[quads]: https://pypi.org/project/quads/
|
302
|
+
[Shapely]: https://pypi.org/project/Shapely/
|
303
|
+
|
@@ -5,15 +5,14 @@
|
|
5
5
|
[](https://pypi.org/project/fastquadtree/#files)
|
6
6
|
[](LICENSE)
|
7
7
|
|
8
|
-
[](https://pepy.tech/projects/fastquadtree)
|
8
|
+
[](https://pepy.tech/projects/fastquadtree)
|
10
9
|
|
11
10
|
[](https://github.com/Elan456/fastquadtree/actions/workflows/ci.yml)
|
12
11
|
[](https://codecov.io/gh/Elan456/fastquadtree)
|
13
12
|
|
14
13
|
[](https://pyo3.rs/)
|
15
14
|
[](https://www.maturin.rs/)
|
16
|
-
[](https://github.com/astral-sh/ruff)
|
17
16
|
|
18
17
|
|
19
18
|
|
@@ -36,18 +35,27 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
36
35
|

|
37
36
|
|
38
37
|
### Summary (largest dataset, PyQtree baseline)
|
39
|
-
- Points: **
|
38
|
+
- Points: **250,000**, Queries: **500**
|
40
39
|
--------------------
|
41
|
-
- Fastest total: **fastquadtree** at **
|
40
|
+
- Fastest total: **fastquadtree** at **0.120 s**
|
42
41
|
|
43
42
|
| Library | Build (s) | Query (s) | Total (s) | Speed vs PyQtree |
|
44
43
|
|---|---:|---:|---:|---:|
|
45
|
-
| fastquadtree | 0.
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
| fastquadtree | 0.031 | 0.089 | 0.120 | 14.64× |
|
45
|
+
| Shapely STRtree | 0.179 | 0.100 | 0.279 | 6.29× |
|
46
|
+
| nontree-QuadTree | 0.595 | 0.605 | 1.200 | 1.46× |
|
47
|
+
| Rtree | 0.961 | 0.300 | 1.261 | 1.39× |
|
48
|
+
| e-pyquadtree | 1.005 | 0.660 | 1.665 | 1.05× |
|
49
|
+
| PyQtree | 1.492 | 0.263 | 1.755 | 1.00× |
|
50
|
+
| quads | 1.407 | 0.484 | 1.890 | 0.93× |
|
51
|
+
|
52
|
+
#### Benchmark Configuration
|
53
|
+
| Parameter | Value |
|
54
|
+
|---|---:|
|
55
|
+
| Bounds | (0, 0, 1000, 1000) |
|
56
|
+
| Max points per node | 128 |
|
57
|
+
| Max depth | 16 |
|
58
|
+
| Queries per experiment | 500 |
|
51
59
|
|
52
60
|
## Install
|
53
61
|
|
@@ -251,7 +259,7 @@ MIT. See `LICENSE`.
|
|
251
259
|
|
252
260
|
## Acknowledgments
|
253
261
|
|
254
|
-
* Python libraries compared: [PyQtree], [e-pyquadtree], [Rtree], [nontree], [quads]
|
262
|
+
* Python libraries compared: [PyQtree], [e-pyquadtree], [Rtree], [nontree], [quads], [Shapely]
|
255
263
|
* Built with [PyO3] and [maturin]
|
256
264
|
|
257
265
|
[PyQtree]: https://pypi.org/project/pyqtree/
|
@@ -260,4 +268,5 @@ MIT. See `LICENSE`.
|
|
260
268
|
[maturin]: https://www.maturin.rs/
|
261
269
|
[Rtree]: https://pypi.org/project/Rtree/
|
262
270
|
[nontree]: https://pypi.org/project/nontree/
|
263
|
-
[quads]: https://pypi.org/project/quads/
|
271
|
+
[quads]: https://pypi.org/project/quads/
|
272
|
+
[Shapely]: https://pypi.org/project/Shapely/
|
Binary file
|
Binary file
|
@@ -7,10 +7,13 @@ allowing fair comparison of their performance characteristics.
|
|
7
7
|
|
8
8
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
9
9
|
|
10
|
+
import numpy as np
|
10
11
|
from pyqtree import Index as PyQTree # Pyqtree
|
11
12
|
|
12
13
|
# Built-in engines (always available in this repo)
|
13
14
|
from pyquadtree.quadtree import QuadTree as EPyQuadTree # e-pyquadtree
|
15
|
+
from shapely import box as shp_box, points # Shapely 2.x
|
16
|
+
from shapely.strtree import STRtree
|
14
17
|
|
15
18
|
from fastquadtree import QuadTree as RustQuadTree # fastquadtree
|
16
19
|
|
@@ -101,8 +104,7 @@ def _create_fastquadtree_engine(
|
|
101
104
|
|
102
105
|
def build(points):
|
103
106
|
qt = RustQuadTree(bounds, max_points, max_depth=max_depth)
|
104
|
-
|
105
|
-
qt.insert(p)
|
107
|
+
qt.insert_many_points(points)
|
106
108
|
return qt
|
107
109
|
|
108
110
|
def query(qt, queries):
|
@@ -239,6 +241,40 @@ def _create_rtree_engine(
|
|
239
241
|
return Engine("Rtree", "#e377c2", build, query)
|
240
242
|
|
241
243
|
|
244
|
+
def _create_strtree_engine(
|
245
|
+
bounds: Tuple[int, int, int, int], max_points: int, max_depth: int
|
246
|
+
) -> Optional[Engine]:
|
247
|
+
"""Create engine adapter for Shapely STRtree (optional)."""
|
248
|
+
|
249
|
+
def build(points_list: List[Tuple[int, int]]):
|
250
|
+
# Build geometries efficiently
|
251
|
+
|
252
|
+
xs = np.fromiter(
|
253
|
+
(x for x, _ in points_list), dtype="float32", count=len(points_list)
|
254
|
+
)
|
255
|
+
ys = np.fromiter(
|
256
|
+
(y for _, y in points_list), dtype="float32", count=len(points_list)
|
257
|
+
)
|
258
|
+
geoms = points(xs, ys) # vectorized Point creation
|
259
|
+
assert type(geoms) is np.ndarray
|
260
|
+
tree = STRtree(geoms, node_capacity=max_points)
|
261
|
+
# Keep geoms alive next to the tree
|
262
|
+
return (tree, geoms)
|
263
|
+
|
264
|
+
def query(built, queries: List[Tuple[int, int, int, int]]):
|
265
|
+
tree, _geoms = built
|
266
|
+
for xmin, ymin, xmax, ymax in queries:
|
267
|
+
window = shp_box(xmin, ymin, xmax, ymax)
|
268
|
+
# Shapely 2.x returns ndarray of indices for a single geometry
|
269
|
+
res = tree.query(window)
|
270
|
+
# Consume results without materializing to keep parity with other engines
|
271
|
+
if hasattr(res, "__iter__"):
|
272
|
+
for _ in res:
|
273
|
+
pass
|
274
|
+
|
275
|
+
return Engine("Shapely STRtree", "#7f7f7f", build, query)
|
276
|
+
|
277
|
+
|
242
278
|
def get_engines(
|
243
279
|
bounds: Tuple[int, int, int, int] = (0, 0, 1000, 1000),
|
244
280
|
max_points: int = 20,
|
@@ -268,6 +304,7 @@ def get_engines(
|
|
268
304
|
_create_quads_engine,
|
269
305
|
_create_nontree_engine,
|
270
306
|
_create_rtree_engine,
|
307
|
+
_create_strtree_engine,
|
271
308
|
]
|
272
309
|
|
273
310
|
for engine_creator in optional_engines:
|
@@ -25,10 +25,10 @@ def main():
|
|
25
25
|
parser.add_argument(
|
26
26
|
"--max-points",
|
27
27
|
type=int,
|
28
|
-
default=
|
28
|
+
default=128,
|
29
29
|
help="Maximum points per node before splitting",
|
30
30
|
)
|
31
|
-
parser.add_argument("--max-depth", type=int, default=
|
31
|
+
parser.add_argument("--max-depth", type=int, default=16, help="Maximum tree depth")
|
32
32
|
parser.add_argument(
|
33
33
|
"--n-queries", type=int, default=500, help="Number of queries per experiment"
|
34
34
|
)
|
@@ -41,7 +41,7 @@ def main():
|
|
41
41
|
parser.add_argument(
|
42
42
|
"--max-experiment-points",
|
43
43
|
type=int,
|
44
|
-
default=
|
44
|
+
default=250_000,
|
45
45
|
help="Maximum number of points in largest experiment",
|
46
46
|
)
|
47
47
|
parser.add_argument(
|
@@ -23,12 +23,12 @@ class BenchmarkConfig:
|
|
23
23
|
"""Configuration for benchmark runs."""
|
24
24
|
|
25
25
|
bounds: Tuple[int, int, int, int] = (0, 0, 1000, 1000)
|
26
|
-
max_points: int =
|
27
|
-
max_depth: int =
|
28
|
-
n_queries: int =
|
26
|
+
max_points: int = 64 # node capacity where supported
|
27
|
+
max_depth: int = 1_000 # depth cap for fairness where supported
|
28
|
+
n_queries: int = 100 # queries per experiment
|
29
29
|
repeats: int = 3 # median over repeats
|
30
30
|
rng_seed: int = 42 # random seed for reproducibility
|
31
|
-
max_experiment_points: int =
|
31
|
+
max_experiment_points: int = 100_000
|
32
32
|
|
33
33
|
def __post_init__(self):
|
34
34
|
"""Generate experiment point sizes."""
|
@@ -69,8 +69,8 @@ class BenchmarkRunner:
|
|
69
69
|
for _ in range(m):
|
70
70
|
x = rng.randint(x_min, x_max)
|
71
71
|
y = rng.randint(y_min, y_max)
|
72
|
-
w = rng.randint(0, x_max - x)
|
73
|
-
h = rng.randint(0, y_max - y)
|
72
|
+
w = rng.randint(0, x_max - x) // rng.randint(1, 8)
|
73
|
+
h = rng.randint(0, y_max - y) // rng.randint(1, 8)
|
74
74
|
queries.append((x, y, x + w, y + h))
|
75
75
|
return queries
|
76
76
|
|
@@ -306,3 +306,12 @@ class BenchmarkRunner:
|
|
306
306
|
print(f"| {name:12} | {fmt(b)} | {fmt(q)} | {fmt(t)} | {rel_speed(name)} |")
|
307
307
|
|
308
308
|
print("")
|
309
|
+
|
310
|
+
# Config table
|
311
|
+
print("#### Benchmark Configuration")
|
312
|
+
print("| Parameter | Value |")
|
313
|
+
print("|---|---:|")
|
314
|
+
print(f"| Bounds | {config.bounds} |")
|
315
|
+
print(f"| Max points per node | {config.max_points} |")
|
316
|
+
print(f"| Max depth | {config.max_depth} |")
|
317
|
+
print(f"| Queries per experiment | {config.n_queries} |")
|
@@ -11,4 +11,5 @@ fastquadtree>=0.1.0
|
|
11
11
|
# Added libraries
|
12
12
|
quads>=1.1.0 # pure-Python quadtree
|
13
13
|
nontree>=1.0.5 # pure-Python PR quadtree (TreeMap mode=4)
|
14
|
-
rtree>=1.3.0 # R-tree comparator with wheels for Win/macOS/Linux
|
14
|
+
rtree>=1.3.0 # R-tree comparator with wheels for Win/macOS/Linux
|
15
|
+
shapely>=2.0.0 # STRtree comparator
|
@@ -41,7 +41,7 @@ module-name = "fastquadtree._native"
|
|
41
41
|
compatibility = "manylinux2014"
|
42
42
|
|
43
43
|
[tool.pytest.ini_options]
|
44
|
-
addopts = "--cov=fastquadtree --cov-branch --cov-report=xml --cov-fail-under=
|
44
|
+
addopts = "--cov=fastquadtree --cov-branch --cov-report=xml --cov-fail-under=100"
|
45
45
|
testpaths = ["tests"] # still run tests
|
46
46
|
|
47
47
|
[tool.coverage.run]
|
@@ -110,6 +110,7 @@ ignore = [
|
|
110
110
|
"PLR0915",
|
111
111
|
"PLR0913",
|
112
112
|
"PLR0912",
|
113
|
+
"PLC0415",
|
113
114
|
]
|
114
115
|
|
115
116
|
# Make pytest files less strict where asserts and fixtures are common.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from typing import Any,
|
3
|
+
from typing import Any, Literal, Tuple, overload
|
4
4
|
|
5
5
|
from ._bimap import BiMap # type: ignore[attr-defined]
|
6
6
|
from ._item import Item
|
@@ -103,37 +103,33 @@ class QuadTree:
|
|
103
103
|
self._count += 1
|
104
104
|
return id_
|
105
105
|
|
106
|
-
def insert_many_points(self, points:
|
106
|
+
def insert_many_points(self, points: list[Point]) -> int:
|
107
107
|
"""
|
108
108
|
Bulk insert points with auto-assigned ids.
|
109
109
|
|
110
110
|
Args:
|
111
|
-
points:
|
111
|
+
points: List of (x, y) points.
|
112
112
|
|
113
113
|
Returns:
|
114
|
-
|
115
|
-
|
116
|
-
Raises:
|
117
|
-
ValueError: If any point is outside tree bounds.
|
114
|
+
The number of points inserted
|
118
115
|
"""
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
self._items.add(Item(id_,
|
134
|
-
|
135
|
-
|
136
|
-
return inserted
|
116
|
+
start_id = self._next_id
|
117
|
+
last_id = self._native.insert_many_points(start_id, points)
|
118
|
+
|
119
|
+
num_inserted = last_id - start_id + 1
|
120
|
+
|
121
|
+
if num_inserted < len(points):
|
122
|
+
raise ValueError("One or more points are outside tree bounds")
|
123
|
+
|
124
|
+
self._next_id = last_id + 1
|
125
|
+
|
126
|
+
# Update the item tracker if needed
|
127
|
+
if self._items is not None:
|
128
|
+
for i, id_ in enumerate(range(start_id, last_id + 1)):
|
129
|
+
x, y = points[i]
|
130
|
+
self._items.add(Item(id_, x, y, None))
|
131
|
+
|
132
|
+
return num_inserted
|
137
133
|
|
138
134
|
def attach(self, id_: int, obj: Any) -> None:
|
139
135
|
"""
|
@@ -263,7 +259,7 @@ class QuadTree:
|
|
263
259
|
|
264
260
|
if self._items is None:
|
265
261
|
raise ValueError("Cannot return result as item with track_objects=False")
|
266
|
-
id_,
|
262
|
+
id_, _x, _y = t
|
267
263
|
item = self._items.by_id(id_)
|
268
264
|
if item is None:
|
269
265
|
raise RuntimeError(
|
@@ -31,7 +31,6 @@ class QuadTree:
|
|
31
31
|
# Inserts
|
32
32
|
def insert(self, xy: Point, *, id_: int | None = ..., obj: Any = ...) -> int: ...
|
33
33
|
def insert_many_points(self, points: Iterable[Point]) -> int: ...
|
34
|
-
def insert_many(self, items: Iterable[tuple[Point, Any]]) -> int: ...
|
35
34
|
def attach(self, id_: int, obj: Any) -> None: ...
|
36
35
|
|
37
36
|
# Deletions
|
@@ -7,6 +7,7 @@ pub use crate::geom::{Point, Rect, dist_sq_point_to_rect, dist_sq_points};
|
|
7
7
|
pub use crate::quadtree::{Item, QuadTree};
|
8
8
|
|
9
9
|
use pyo3::prelude::*;
|
10
|
+
use pyo3::types::{PyList};
|
10
11
|
|
11
12
|
fn item_to_tuple(it: Item) -> (u64, f32, f32) {
|
12
13
|
(it.id, it.point.x, it.point.y)
|
@@ -35,20 +36,33 @@ impl PyQuadTree {
|
|
35
36
|
self.inner.insert(Item { id, point: Point { x, y } })
|
36
37
|
}
|
37
38
|
|
39
|
+
// Insert many points with auto ids starting at `start_id`: [(x, y), ...]
|
40
|
+
// Returns the last id used
|
41
|
+
pub fn insert_many_points(&mut self, start_id: u64, points: Vec<(f32, f32)>) -> u64 {
|
42
|
+
let mut id = start_id;
|
43
|
+
for (x, y) in points {
|
44
|
+
if self.inner.insert(Item { id, point: Point { x, y } }) {
|
45
|
+
id += 1;
|
46
|
+
}
|
47
|
+
}
|
48
|
+
id - 1 // -1 because id was incremented after last successful insert
|
49
|
+
}
|
50
|
+
|
38
51
|
pub fn delete(&mut self, id: u64, xy: (f32, f32)) -> bool {
|
39
52
|
let (x, y) = xy;
|
40
53
|
self.inner.delete(id, Point { x, y })
|
41
54
|
}
|
42
55
|
|
43
|
-
|
56
|
+
// Build the Python list of (id, x, y) directly from the Vec<Item>.
|
57
|
+
// Public behavior is unchanged: returns list[(id, x, y)].
|
58
|
+
pub fn query<'py>(&self, py: Python<'py>, rect: (f32, f32, f32, f32)) -> Bound<'py, PyList> {
|
44
59
|
let (min_x, min_y, max_x, max_y) = rect;
|
45
|
-
self.inner
|
46
|
-
|
47
|
-
|
48
|
-
.map(item_to_tuple)
|
49
|
-
.collect()
|
60
|
+
let tuples = self.inner.query(Rect { min_x, min_y, max_x, max_y });
|
61
|
+
// PyO3 will turn Vec<(u64,f32,f32)> into a Python list of tuples
|
62
|
+
PyList::new_bound(py, &tuples)
|
50
63
|
}
|
51
64
|
|
65
|
+
|
52
66
|
pub fn nearest_neighbor(&self, xy: (f32, f32)) -> Option<(u64, f32, f32)> {
|
53
67
|
let (x, y) = xy;
|
54
68
|
self.inner.nearest_neighbor(Point { x, y }).map(item_to_tuple)
|
@@ -1,5 +1,6 @@
|
|
1
1
|
use std::collections::HashSet;
|
2
2
|
use crate::geom::{Point, Rect, dist_sq_point_to_rect, dist_sq_points};
|
3
|
+
use smallvec::SmallVec;
|
3
4
|
|
4
5
|
#[derive(Copy, Clone, Debug, PartialEq, Default)]
|
5
6
|
pub struct Item {
|
@@ -120,26 +121,83 @@ impl QuadTree {
|
|
120
121
|
self.children = Some(Box::new(kids));
|
121
122
|
}
|
122
123
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
124
|
+
#[inline(always)]
|
125
|
+
fn rect_contains_rect(a: &Rect, b: &Rect) -> bool {
|
126
|
+
a.min_x <= b.min_x && a.min_y <= b.min_y &&
|
127
|
+
a.max_x >= b.max_x && a.max_y >= b.max_y
|
128
|
+
}
|
127
129
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
130
|
+
pub fn query(&self, range: Rect) -> Vec<(u64, f32, f32)> {
|
131
|
+
#[derive(Copy, Clone)]
|
132
|
+
enum Mode { Filter, ReportAll }
|
133
|
+
|
134
|
+
// Hoist bounds for tight leaf checks
|
135
|
+
let rx0 = range.min_x;
|
136
|
+
let ry0 = range.min_y;
|
137
|
+
let rx1 = range.max_x;
|
138
|
+
let ry1 = range.max_y;
|
139
|
+
|
140
|
+
let mut out: Vec<(u64, f32, f32)> = Vec::with_capacity(128);
|
141
|
+
let mut stack: SmallVec<[(&QuadTree, Mode); 64]> = SmallVec::new();
|
142
|
+
stack.push((self, Mode::Filter));
|
143
|
+
|
144
|
+
while let Some((node, mode)) = stack.pop() {
|
145
|
+
match mode {
|
146
|
+
Mode::ReportAll => {
|
147
|
+
if let Some(children) = node.children.as_ref() {
|
148
|
+
// Entire subtree is inside the query.
|
149
|
+
// No filtering, just recurse in ReportAll.
|
150
|
+
stack.push((&children[0], Mode::ReportAll));
|
151
|
+
stack.push((&children[1], Mode::ReportAll));
|
152
|
+
stack.push((&children[2], Mode::ReportAll));
|
153
|
+
stack.push((&children[3], Mode::ReportAll));
|
154
|
+
} else {
|
155
|
+
// Leaf: append all items, no per-point test
|
156
|
+
let items = &node.items;
|
157
|
+
out.reserve(items.len());
|
158
|
+
out.extend(items.iter().map(|it| (it.id, it.point.x, it.point.y)));
|
159
|
+
}
|
132
160
|
}
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
161
|
+
|
162
|
+
Mode::Filter => {
|
163
|
+
// Node cull
|
164
|
+
if !range.intersects(&node.boundary) {
|
165
|
+
continue;
|
166
|
+
}
|
167
|
+
|
168
|
+
// Full cover: switch to ReportAll
|
169
|
+
if Self::rect_contains_rect(&range, &node.boundary) {
|
170
|
+
stack.push((node, Mode::ReportAll));
|
171
|
+
continue;
|
172
|
+
}
|
173
|
+
|
174
|
+
// Partial overlap
|
175
|
+
if let Some(children) = node.children.as_ref() {
|
176
|
+
// Only push intersecting children
|
177
|
+
let c0 = &children[0];
|
178
|
+
if range.intersects(&c0.boundary) { stack.push((c0, Mode::Filter)); }
|
179
|
+
let c1 = &children[1];
|
180
|
+
if range.intersects(&c1.boundary) { stack.push((c1, Mode::Filter)); }
|
181
|
+
let c2 = &children[2];
|
182
|
+
if range.intersects(&c2.boundary) { stack.push((c2, Mode::Filter)); }
|
183
|
+
let c3 = &children[3];
|
184
|
+
if range.intersects(&c3.boundary) { stack.push((c3, Mode::Filter)); }
|
185
|
+
} else {
|
186
|
+
// Leaf scan with tight predicate
|
187
|
+
let items = &node.items;
|
188
|
+
// Reserve a little to reduce reallocs if many will pass
|
189
|
+
out.reserve(items.len().min(64));
|
190
|
+
for it in items {
|
191
|
+
let p = &it.point;
|
192
|
+
if p.x >= rx0 && p.x < rx1 && p.y >= ry0 && p.y < ry1 {
|
193
|
+
out.push((it.id, p.x, p.y));
|
194
|
+
}
|
195
|
+
}
|
139
196
|
}
|
140
197
|
}
|
141
198
|
}
|
142
199
|
}
|
200
|
+
|
143
201
|
out
|
144
202
|
}
|
145
203
|
|
@@ -4,8 +4,8 @@ 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
|
fn pt(x: f32, y: f32) -> Point { Point { x, y } }
|
7
|
-
fn ids(v: &
|
8
|
-
let mut out: Vec<u64> = v.iter().map(|it| it.
|
7
|
+
fn ids(v: &Vec<(u64, f32, f32)>) -> Vec<u64> {
|
8
|
+
let mut out: Vec<u64> = v.iter().map(|it| it.0).collect();
|
9
9
|
out.sort_unstable();
|
10
10
|
out
|
11
11
|
}
|
@@ -166,7 +166,7 @@ fn test_delete_preserves_other_operations() {
|
|
166
166
|
let query_rect = Rect { min_x: 5.0, min_y: 5.0, max_x: 25.0, max_y: 25.0 };
|
167
167
|
let results = tree.query(query_rect);
|
168
168
|
assert_eq!(results.len(), 1); // Should only find point (10,10)
|
169
|
-
assert_eq!(results[0].
|
169
|
+
assert_eq!(results[0].0, 1);
|
170
170
|
|
171
171
|
// Test nearest neighbor
|
172
172
|
let nearest = tree.nearest_neighbor(Point { x: 15.0, y: 15.0 });
|
@@ -230,7 +230,7 @@ fn test_delete_multiple_items_same_location() {
|
|
230
230
|
assert_eq!(results.len(), 2);
|
231
231
|
|
232
232
|
// Verify we can find both remaining items
|
233
|
-
let ids: Vec<u64> = results.iter().map(|item| item.
|
233
|
+
let ids: Vec<u64> = results.iter().map(|item| item.0).collect();
|
234
234
|
assert!(ids.contains(&10));
|
235
235
|
assert!(ids.contains(&30));
|
236
236
|
assert!(!ids.contains(&20)); // Should be deleted
|
@@ -12,8 +12,8 @@ fn item(id: u64, x: f32, y: f32) -> Item {
|
|
12
12
|
Item { id, point: pt(x, y) }
|
13
13
|
}
|
14
14
|
|
15
|
-
fn ids(v: &
|
16
|
-
let mut out: Vec<u64> = v.iter().map(|it| it.
|
15
|
+
fn ids(v: &Vec<(u64, f32, f32)>) -> Vec<u64> {
|
16
|
+
let mut out: Vec<u64> = v.iter().map(|it| it.0).collect();
|
17
17
|
out.sort_unstable();
|
18
18
|
out
|
19
19
|
}
|
Binary file
|
Binary file
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|