fastquadtree 0.5.1__tar.gz → 0.6.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 (46) hide show
  1. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/.github/workflows/release.yml +8 -0
  2. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/Cargo.lock +2 -1
  3. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/Cargo.toml +2 -1
  4. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/PKG-INFO +23 -13
  5. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/README.md +22 -13
  6. fastquadtree-0.6.1/assets/quadtree_bench_throughput.png +0 -0
  7. fastquadtree-0.6.1/assets/quadtree_bench_time.png +0 -0
  8. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/benchmarks/quadtree_bench/engines.py +39 -2
  9. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/benchmarks/quadtree_bench/main.py +3 -3
  10. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/benchmarks/quadtree_bench/runner.py +15 -6
  11. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/benchmarks/requirements.txt +2 -1
  12. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/pyproject.toml +2 -1
  13. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/pysrc/fastquadtree/__init__.py +22 -26
  14. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/pysrc/fastquadtree/__init__.pyi +0 -1
  15. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/src/lib.rs +20 -6
  16. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/src/quadtree.rs +72 -14
  17. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/tests/query.rs +2 -2
  18. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/tests/test_delete.rs +2 -2
  19. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/tests/unconventional_bounds.rs +2 -2
  20. fastquadtree-0.5.1/assets/quadtree_bench_throughput.png +0 -0
  21. fastquadtree-0.5.1/assets/quadtree_bench_time.png +0 -0
  22. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/.gitignore +0 -0
  23. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/.pre-commit-config.yaml +0 -0
  24. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/LICENSE +0 -0
  25. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/assets/interactive_v2_screenshot.png +0 -0
  26. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/benchmarks/benchmark_native_vs_shim.py +0 -0
  27. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/benchmarks/cross_library_bench.py +0 -0
  28. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/benchmarks/quadtree_bench/__init__.py +0 -0
  29. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/benchmarks/quadtree_bench/plotting.py +0 -0
  30. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/benchmarks/runner.py +0 -0
  31. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/interactive/interactive.py +0 -0
  32. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/interactive/interactive_v2.py +0 -0
  33. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/interactive/requirements.txt +0 -0
  34. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/pysrc/fastquadtree/_bimap.py +0 -0
  35. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/pysrc/fastquadtree/_item.py +0 -0
  36. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/pysrc/fastquadtree/py.typed +0 -0
  37. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/src/geom.rs +0 -0
  38. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/tests/insertions.rs +0 -0
  39. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/tests/nearest_neighbor.rs +0 -0
  40. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/tests/rectangle_traversal.rs +0 -0
  41. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/tests/test_bimap.py +0 -0
  42. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/tests/test_delete_by_object.py +0 -0
  43. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/tests/test_delete_python.py +0 -0
  44. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/tests/test_python.py +0 -0
  45. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/tests/test_unconventional_bounds.py +0 -0
  46. {fastquadtree-0.5.1 → fastquadtree-0.6.1}/tests/test_wrapper_edges.py +0 -0
@@ -62,6 +62,14 @@ jobs:
62
62
  args: "--skip-existing --target universal2-apple-darwin"
63
63
  - os: windows-latest
64
64
  args: "--skip-existing"
65
+ # Linux aarch64 abi3
66
+ - os: ubuntu-latest
67
+ target: "aarch64-unknown-linux-gnu"
68
+ args: "--skip-existing --target aarch64-unknown-linux-gnu --zig"
69
+ # Linux armv7 abi3
70
+ - os: ubuntu-latest
71
+ target: "armv7-unknown-linux-gnueabihf"
72
+ args: "--skip-existing --target armv7-unknown-linux-gnueabihf --zig"
65
73
  steps:
66
74
  - uses: actions/checkout@v4
67
75
  - uses: actions/setup-python@v5
@@ -22,9 +22,10 @@ checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
22
22
 
23
23
  [[package]]
24
24
  name = "fastquadtree"
25
- version = "0.5.1"
25
+ version = "0.6.1"
26
26
  dependencies = [
27
27
  "pyo3",
28
+ "smallvec",
28
29
  ]
29
30
 
30
31
  [[package]]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "fastquadtree"
3
- version = "0.5.1"
3
+ version = "0.6.1"
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.5.1
3
+ Version: 0.6.1
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
  [![Wheels](https://img.shields.io/pypi/wheel/fastquadtree.svg)](https://pypi.org/project/fastquadtree/#files)
36
36
  [![License: MIT](https://img.shields.io/pypi/l/fastquadtree.svg)](LICENSE)
37
37
 
38
- [![Downloads total](https://static.pepy.tech/badge/fastquadtree)](https://pepy.tech/projects/fastquadtree)
39
- [![Downloads month](https://static.pepy.tech/badge/fastquadtree/month)](https://pepy.tech/projects/fastquadtree)
38
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/fastquadtree?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=BLUE&left_text=Total+Downloads)](https://pepy.tech/projects/fastquadtree)
40
39
 
41
40
  [![Build](https://github.com/Elan456/fastquadtree/actions/workflows/release.yml/badge.svg)](https://github.com/Elan456/fastquadtree/actions/workflows/ci.yml)
42
41
  [![Codecov](https://codecov.io/gh/Elan456/fastquadtree/branch/main/graph/badge.svg)](https://codecov.io/gh/Elan456/fastquadtree)
43
42
 
44
43
  [![Rust core via PyO3](https://img.shields.io/badge/Rust-core%20via%20PyO3-orange)](https://pyo3.rs/)
45
44
  [![Built with maturin](https://img.shields.io/badge/Built%20with-maturin-1f6feb)](https://www.maturin.rs/)
46
- [![Lint and format: Ruff](https://img.shields.io/badge/Lint%20and%20format-Ruff-46a758?logo=ruff&logoColor=white)](https://docs.astral.sh/ruff/)
45
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](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
  ![Throughput](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/quadtree_bench_throughput.png)
67
66
 
68
67
  ### Summary (largest dataset, PyQtree baseline)
69
- - Points: **500,000**, Queries: **500**
68
+ - Points: **250,000**, Queries: **500**
70
69
  --------------------
71
- - Fastest total: **fastquadtree** at **1.591 s**
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.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× |
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
  [![Wheels](https://img.shields.io/pypi/wheel/fastquadtree.svg)](https://pypi.org/project/fastquadtree/#files)
6
6
  [![License: MIT](https://img.shields.io/pypi/l/fastquadtree.svg)](LICENSE)
7
7
 
8
- [![Downloads total](https://static.pepy.tech/badge/fastquadtree)](https://pepy.tech/projects/fastquadtree)
9
- [![Downloads month](https://static.pepy.tech/badge/fastquadtree/month)](https://pepy.tech/projects/fastquadtree)
8
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/fastquadtree?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=BLUE&left_text=Total+Downloads)](https://pepy.tech/projects/fastquadtree)
10
9
 
11
10
  [![Build](https://github.com/Elan456/fastquadtree/actions/workflows/release.yml/badge.svg)](https://github.com/Elan456/fastquadtree/actions/workflows/ci.yml)
12
11
  [![Codecov](https://codecov.io/gh/Elan456/fastquadtree/branch/main/graph/badge.svg)](https://codecov.io/gh/Elan456/fastquadtree)
13
12
 
14
13
  [![Rust core via PyO3](https://img.shields.io/badge/Rust-core%20via%20PyO3-orange)](https://pyo3.rs/)
15
14
  [![Built with maturin](https://img.shields.io/badge/Built%20with-maturin-1f6feb)](https://www.maturin.rs/)
16
- [![Lint and format: Ruff](https://img.shields.io/badge/Lint%20and%20format-Ruff-46a758?logo=ruff&logoColor=white)](https://docs.astral.sh/ruff/)
15
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](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
  ![Throughput](https://raw.githubusercontent.com/Elan456/fastquadtree/main/assets/quadtree_bench_throughput.png)
37
36
 
38
37
  ### Summary (largest dataset, PyQtree baseline)
39
- - Points: **500,000**, Queries: **500**
38
+ - Points: **250,000**, Queries: **500**
40
39
  --------------------
41
- - Fastest total: **fastquadtree** at **1.591 s**
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.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× |
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/
@@ -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
- for p in points:
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=20,
28
+ default=128,
29
29
  help="Maximum points per node before splitting",
30
30
  )
31
- parser.add_argument("--max-depth", type=int, default=10, help="Maximum tree depth")
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=500_000,
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 = 20 # node capacity where supported
27
- max_depth: int = 10 # depth cap for fairness where supported
28
- n_queries: int = 500 # queries per experiment
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 = 500_000
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=95"
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, Iterable, Literal, Tuple, overload
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: Iterable[Point]) -> int:
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: Iterable of (x, y) points.
111
+ points: List of (x, y) points.
112
112
 
113
113
  Returns:
114
- Number of points successfully inserted.
115
-
116
- Raises:
117
- ValueError: If any point is outside tree bounds.
114
+ The number of points inserted
118
115
  """
119
- ins = self._native.insert
120
- nid = self._next_id
121
- inserted = 0
122
- bx0, by0, bx1, by1 = self._bounds
123
- for xy in points:
124
- id_ = nid
125
- nid += 1
126
- if not ins(id_, xy):
127
- x, y = xy
128
- raise ValueError(
129
- f"Point ({x}, {y}) is outside bounds ({bx0}, {by0}, {bx1}, {by1})"
130
- )
131
- inserted += 1
132
- if self._items is not None:
133
- self._items.add(Item(id_, xy[0], xy[1], None))
134
- self._next_id = nid
135
- self._count += inserted
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_, x, y = t
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
- pub fn query(&self, rect: (f32, f32, f32, f32)) -> Vec<(u64, f32, f32)> {
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
- .query(Rect { min_x, min_y, max_x, max_y })
47
- .into_iter()
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
- pub fn query(&self, range: Rect) -> Vec<Item> {
124
- let mut out = Vec::new();
125
- let mut stack: Vec<&QuadTree> = Vec::new();
126
- stack.push(self);
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
- while let Some(node) = stack.pop() {
129
- for it in &node.items {
130
- if range.contains(&it.point) {
131
- out.push(*it);
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
- 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);
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: &[Item]) -> Vec<u64> {
8
- let mut out: Vec<u64> = v.iter().map(|it| it.id).collect();
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].id, 1);
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.id).collect();
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: &[Item]) -> Vec<u64> {
16
- let mut out: Vec<u64> = v.iter().map(|it| it.id).collect();
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
  }
File without changes
File without changes
File without changes