fastquadtree 1.0.1__tar.gz → 1.1.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-1.0.1 → fastquadtree-1.1.0}/.pre-commit-config.yaml +2 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/Cargo.lock +91 -1
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/Cargo.toml +2 -1
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/PKG-INFO +4 -2
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/README.md +2 -1
- fastquadtree-1.1.0/benchmarks/benchmark_np_vs_list.py +178 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/requirements.txt +4 -1
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/benchmark.md +30 -0
- fastquadtree-1.1.0/docs/future_features.md +30 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/index.md +1 -1
- fastquadtree-1.1.0/docs/styles/overrides.css +38 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/mkdocs.yml +14 -1
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pyproject.toml +2 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pysrc/fastquadtree/_base_quadtree.py +51 -5
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pysrc/fastquadtree/_item.py +15 -5
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/src/lib.rs +60 -0
- fastquadtree-1.1.0/tests/test_insert_many_numpy.py +183 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_pyqtree_shim_compat.py +97 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/.github/workflows/docs.yml +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/.github/workflows/release.yml +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/.gitignore +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/LICENSE +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/assets/ballpit.png +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/assets/interactive_v2_rect_screenshot.png +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/assets/interactive_v2_screenshot.png +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/assets/quadtree_bench_throughput.png +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/assets/quadtree_bench_time.png +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/benchmark_native_vs_shim.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/cross_library_bench.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/quadtree_bench/__init__.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/quadtree_bench/engines.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/quadtree_bench/main.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/quadtree_bench/plotting.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/quadtree_bench/runner.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/runner.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/system_info_collector.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/api/point_item.md +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/api/pyqtree.md +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/api/quadtree.md +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/api/rect_item.md +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/api/rect_quadtree.md +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/quickstart.md +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/runnables.md +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/interactive/ballpit.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/interactive/interactive.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/interactive/interactive_v2.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/interactive/interactive_v2_rect.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/interactive/requirements.txt +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pysrc/fastquadtree/__init__.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pysrc/fastquadtree/_obj_store.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pysrc/fastquadtree/point_quadtree.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pysrc/fastquadtree/py.typed +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pysrc/fastquadtree/pyqtree.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pysrc/fastquadtree/rect_quadtree.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/src/geom.rs +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/src/quadtree.rs +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/src/rect_quadtree.rs +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/insertions.rs +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/nearest_neighbor.rs +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/query.rs +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/rect_quadtree.rs +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/rectangle_traversal.rs +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_base_quadtree.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_clear.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_delete.rs +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_delete_by_object.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_delete_python.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_obj_store.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_point_quadtree_nn_runtime.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_python.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_rect_quadtree.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_unconventional_bounds.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_wrapper_edges.py +0 -0
- {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/unconventional_bounds.rs +0 -0
@@ -7,9 +7,11 @@ repos:
|
|
7
7
|
- id: ruff
|
8
8
|
name: ruff (lint + fix)
|
9
9
|
args: ["--fix", "--exit-non-zero-on-fix"]
|
10
|
+
fail_fast: true
|
10
11
|
# Code formatter (Black replacement)
|
11
12
|
- id: ruff-format
|
12
13
|
name: ruff (format)
|
14
|
+
fail_fast: true
|
13
15
|
|
14
16
|
# Local hooks that run in sequence and do not receive file args
|
15
17
|
- repo: local
|
@@ -10,8 +10,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
|
10
10
|
|
11
11
|
[[package]]
|
12
12
|
name = "fastquadtree"
|
13
|
-
version = "1.0
|
13
|
+
version = "1.1.0"
|
14
14
|
dependencies = [
|
15
|
+
"numpy",
|
15
16
|
"pyo3",
|
16
17
|
"smallvec",
|
17
18
|
]
|
@@ -34,6 +35,16 @@ version = "0.2.175"
|
|
34
35
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
35
36
|
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
36
37
|
|
38
|
+
[[package]]
|
39
|
+
name = "matrixmultiply"
|
40
|
+
version = "0.3.10"
|
41
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
42
|
+
checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
|
43
|
+
dependencies = [
|
44
|
+
"autocfg",
|
45
|
+
"rawpointer",
|
46
|
+
]
|
47
|
+
|
37
48
|
[[package]]
|
38
49
|
name = "memoffset"
|
39
50
|
version = "0.9.1"
|
@@ -43,6 +54,64 @@ dependencies = [
|
|
43
54
|
"autocfg",
|
44
55
|
]
|
45
56
|
|
57
|
+
[[package]]
|
58
|
+
name = "ndarray"
|
59
|
+
version = "0.16.1"
|
60
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
61
|
+
checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841"
|
62
|
+
dependencies = [
|
63
|
+
"matrixmultiply",
|
64
|
+
"num-complex",
|
65
|
+
"num-integer",
|
66
|
+
"num-traits",
|
67
|
+
"portable-atomic",
|
68
|
+
"portable-atomic-util",
|
69
|
+
"rawpointer",
|
70
|
+
]
|
71
|
+
|
72
|
+
[[package]]
|
73
|
+
name = "num-complex"
|
74
|
+
version = "0.4.6"
|
75
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
76
|
+
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
77
|
+
dependencies = [
|
78
|
+
"num-traits",
|
79
|
+
]
|
80
|
+
|
81
|
+
[[package]]
|
82
|
+
name = "num-integer"
|
83
|
+
version = "0.1.46"
|
84
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
85
|
+
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
86
|
+
dependencies = [
|
87
|
+
"num-traits",
|
88
|
+
]
|
89
|
+
|
90
|
+
[[package]]
|
91
|
+
name = "num-traits"
|
92
|
+
version = "0.2.19"
|
93
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
94
|
+
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
95
|
+
dependencies = [
|
96
|
+
"autocfg",
|
97
|
+
]
|
98
|
+
|
99
|
+
[[package]]
|
100
|
+
name = "numpy"
|
101
|
+
version = "0.26.0"
|
102
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
103
|
+
checksum = "9b2dba356160b54f5371b550575b78130a54718b4c6e46b3f33a6da74a27e78b"
|
104
|
+
dependencies = [
|
105
|
+
"libc",
|
106
|
+
"ndarray",
|
107
|
+
"num-complex",
|
108
|
+
"num-integer",
|
109
|
+
"num-traits",
|
110
|
+
"pyo3",
|
111
|
+
"pyo3-build-config",
|
112
|
+
"rustc-hash",
|
113
|
+
]
|
114
|
+
|
46
115
|
[[package]]
|
47
116
|
name = "once_cell"
|
48
117
|
version = "1.21.3"
|
@@ -55,6 +124,15 @@ version = "1.11.1"
|
|
55
124
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
56
125
|
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
57
126
|
|
127
|
+
[[package]]
|
128
|
+
name = "portable-atomic-util"
|
129
|
+
version = "0.2.4"
|
130
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
131
|
+
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
132
|
+
dependencies = [
|
133
|
+
"portable-atomic",
|
134
|
+
]
|
135
|
+
|
58
136
|
[[package]]
|
59
137
|
name = "proc-macro2"
|
60
138
|
version = "1.0.101"
|
@@ -134,6 +212,18 @@ dependencies = [
|
|
134
212
|
"proc-macro2",
|
135
213
|
]
|
136
214
|
|
215
|
+
[[package]]
|
216
|
+
name = "rawpointer"
|
217
|
+
version = "0.2.1"
|
218
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
219
|
+
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
220
|
+
|
221
|
+
[[package]]
|
222
|
+
name = "rustc-hash"
|
223
|
+
version = "2.1.1"
|
224
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
225
|
+
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
226
|
+
|
137
227
|
[[package]]
|
138
228
|
name = "smallvec"
|
139
229
|
version = "1.15.1"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[package]
|
2
2
|
name = "fastquadtree"
|
3
|
-
version = "1.0
|
3
|
+
version = "1.1.0"
|
4
4
|
edition = "2021"
|
5
5
|
readme = "README.md"
|
6
6
|
|
@@ -10,6 +10,7 @@ crate-type = ["rlib", "cdylib"]
|
|
10
10
|
[dependencies]
|
11
11
|
pyo3 = { version = "0.26", features = ["extension-module", "abi3-py38"] }
|
12
12
|
smallvec = "1.15.1"
|
13
|
+
numpy = "0.26"
|
13
14
|
|
14
15
|
[profile.release]
|
15
16
|
opt-level = 3
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: fastquadtree
|
3
|
-
Version: 1.0
|
3
|
+
Version: 1.1.0
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
5
5
|
Classifier: Programming Language :: Python :: 3 :: Only
|
6
6
|
Classifier: Programming Language :: Rust
|
@@ -25,6 +25,7 @@ Requires-Dist: mkdocs-git-revision-date-localized-plugin ; extra == 'dev'
|
|
25
25
|
Requires-Dist: mkdocs-minify-plugin ; extra == 'dev'
|
26
26
|
Requires-Dist: maturin>=1.5 ; extra == 'dev'
|
27
27
|
Requires-Dist: pyqtree==1.0.0 ; extra == 'dev'
|
28
|
+
Requires-Dist: numpy ; extra == 'dev'
|
28
29
|
Provides-Extra: dev
|
29
30
|
License-File: LICENSE
|
30
31
|
Summary: Rust-accelerated quadtree for Python with fast inserts, range queries, and k-NN search.
|
@@ -48,6 +49,7 @@ Rust-optimized quadtree with a clean Python API
|
|
48
49
|
[](https://pypi.org/project/fastquadtree/)
|
49
50
|
[](https://pepy.tech/projects/fastquadtree)
|
50
51
|
[](https://github.com/Elan456/fastquadtree/actions/workflows/release.yml)
|
52
|
+

|
51
53
|
|
52
54
|
[](https://pyo3.rs/)
|
53
55
|
[](https://www.maturin.rs/)
|
@@ -62,7 +64,7 @@ Rust-optimized quadtree with a clean Python API
|
|
62
64
|
|
63
65
|
## Why use fastquadtree
|
64
66
|
|
65
|
-
- Clean [Python API](https://elan456.github.io/fastquadtree/api/quadtree/) with modern typing hints
|
67
|
+
- Clean [Python API](https://elan456.github.io/fastquadtree/api/quadtree/) with no external dependencies and modern typing hints
|
66
68
|
- The fastest quadtree Python package ([>10x faster](https://elan456.github.io/fastquadtree/benchmark/) than pyqtree)
|
67
69
|
- Prebuilt wheels for Windows, macOS, and Linux
|
68
70
|
- Support for [inserting bounding boxes](https://elan456.github.io/fastquadtree/api/rect_quadtree/) or points
|
@@ -9,6 +9,7 @@ Rust-optimized quadtree with a clean Python API
|
|
9
9
|
[](https://pypi.org/project/fastquadtree/)
|
10
10
|
[](https://pepy.tech/projects/fastquadtree)
|
11
11
|
[](https://github.com/Elan456/fastquadtree/actions/workflows/release.yml)
|
12
|
+

|
12
13
|
|
13
14
|
[](https://pyo3.rs/)
|
14
15
|
[](https://www.maturin.rs/)
|
@@ -23,7 +24,7 @@ Rust-optimized quadtree with a clean Python API
|
|
23
24
|
|
24
25
|
## Why use fastquadtree
|
25
26
|
|
26
|
-
- Clean [Python API](https://elan456.github.io/fastquadtree/api/quadtree/) with modern typing hints
|
27
|
+
- Clean [Python API](https://elan456.github.io/fastquadtree/api/quadtree/) with no external dependencies and modern typing hints
|
27
28
|
- The fastest quadtree Python package ([>10x faster](https://elan456.github.io/fastquadtree/benchmark/) than pyqtree)
|
28
29
|
- Prebuilt wheels for Windows, macOS, and Linux
|
29
30
|
- Support for [inserting bounding boxes](https://elan456.github.io/fastquadtree/api/rect_quadtree/) or points
|
@@ -0,0 +1,178 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
import argparse
|
5
|
+
import gc
|
6
|
+
import statistics as stats
|
7
|
+
from time import perf_counter as now
|
8
|
+
|
9
|
+
import numpy as np
|
10
|
+
from system_info_collector import (
|
11
|
+
collect_system_info,
|
12
|
+
format_system_info_markdown_lite,
|
13
|
+
)
|
14
|
+
|
15
|
+
from fastquadtree import QuadTree as ShimQuadTree # high level wrapper with insert_many
|
16
|
+
|
17
|
+
BOUNDS: tuple[float, float, float, float] = (0.0, 0.0, 1000.0, 1000.0)
|
18
|
+
CAPACITY = 64
|
19
|
+
MAX_DEPTH = 10
|
20
|
+
SEED = 12345
|
21
|
+
|
22
|
+
|
23
|
+
def gen_np_points(n: int, dtype: np.dtype) -> np.ndarray:
|
24
|
+
rng = np.random.default_rng(SEED)
|
25
|
+
# Generate within bounds. Use dtype the user requested.
|
26
|
+
if np.issubdtype(dtype, np.integer):
|
27
|
+
arr = rng.integers(low=0, high=1000, size=(n, 2), dtype=dtype)
|
28
|
+
else:
|
29
|
+
arr = rng.random(size=(n, 2), dtype=dtype) * 1000.0
|
30
|
+
return arr
|
31
|
+
|
32
|
+
|
33
|
+
def _build_tree_np(points_np: np.ndarray, track_objects: bool) -> float:
|
34
|
+
t0 = now()
|
35
|
+
qt = ShimQuadTree(
|
36
|
+
BOUNDS, CAPACITY, max_depth=MAX_DEPTH, track_objects=track_objects
|
37
|
+
)
|
38
|
+
# Direct NumPy path
|
39
|
+
inserted = qt.insert_many(points_np)
|
40
|
+
dt = now() - t0
|
41
|
+
assert (
|
42
|
+
inserted == points_np.shape[0]
|
43
|
+
), f"Inserted {inserted} != {points_np.shape[0]}"
|
44
|
+
return dt
|
45
|
+
|
46
|
+
|
47
|
+
def _build_tree_list(
|
48
|
+
points_list: list[tuple[float, float]], track_objects: bool
|
49
|
+
) -> float:
|
50
|
+
t0 = now()
|
51
|
+
qt = ShimQuadTree(
|
52
|
+
BOUNDS, CAPACITY, max_depth=MAX_DEPTH, track_objects=track_objects
|
53
|
+
)
|
54
|
+
inserted = qt.insert_many(points_list)
|
55
|
+
dt = now() - t0
|
56
|
+
assert inserted == len(points_list), f"Inserted {inserted} != {len(points_list)}"
|
57
|
+
return dt
|
58
|
+
|
59
|
+
|
60
|
+
def bench_np_direct(points_np: np.ndarray, repeats: int, track_objects: bool) -> float:
|
61
|
+
times = []
|
62
|
+
for _ in range(repeats):
|
63
|
+
gc.disable()
|
64
|
+
times.append(_build_tree_np(points_np, track_objects))
|
65
|
+
gc.enable()
|
66
|
+
return stats.median(times)
|
67
|
+
|
68
|
+
|
69
|
+
def bench_list_from_np(
|
70
|
+
points_np: np.ndarray, repeats: int, track_objects: bool, include_conversion: bool
|
71
|
+
) -> float:
|
72
|
+
times = []
|
73
|
+
if not include_conversion:
|
74
|
+
# Convert once up front so measured time is insert only
|
75
|
+
points_list = [tuple(map(float, row)) for row in points_np]
|
76
|
+
for _ in range(repeats):
|
77
|
+
gc.disable()
|
78
|
+
if include_conversion:
|
79
|
+
# Count conversion cost
|
80
|
+
t0 = now()
|
81
|
+
points_list = [tuple(map(float, row)) for row in points_np]
|
82
|
+
convert_time = now() - t0
|
83
|
+
build_time = _build_tree_list(points_list, track_objects) # type: ignore
|
84
|
+
times.append(convert_time + build_time)
|
85
|
+
else:
|
86
|
+
# Insert only
|
87
|
+
times.append(_build_tree_list(points_list, track_objects)) # pyright: ignore[reportPossiblyUnboundVariable, reportArgumentType]
|
88
|
+
gc.enable()
|
89
|
+
return stats.median(times)
|
90
|
+
|
91
|
+
|
92
|
+
def main():
|
93
|
+
ap = argparse.ArgumentParser(
|
94
|
+
description="Benchmark: NumPy insert vs Python list insert"
|
95
|
+
)
|
96
|
+
ap.add_argument("--points", type=int, default=500_000)
|
97
|
+
ap.add_argument("--repeats", type=int, default=5)
|
98
|
+
ap.add_argument(
|
99
|
+
"--dtype",
|
100
|
+
type=str,
|
101
|
+
default="float32",
|
102
|
+
choices=["float32", "float64", "int32", "int64"],
|
103
|
+
help="Dtype for generated NumPy points",
|
104
|
+
)
|
105
|
+
ap.add_argument(
|
106
|
+
"--track-objects",
|
107
|
+
action="store_true",
|
108
|
+
help="Enable object tracking in the shim to include store overhead",
|
109
|
+
)
|
110
|
+
args = ap.parse_args()
|
111
|
+
|
112
|
+
dtype_map = {
|
113
|
+
"float32": np.float32,
|
114
|
+
"float64": np.float64,
|
115
|
+
"int32": np.int32,
|
116
|
+
"int64": np.int64,
|
117
|
+
}
|
118
|
+
dtype = dtype_map[args.dtype]
|
119
|
+
|
120
|
+
print("NumPy vs list insert benchmark")
|
121
|
+
print("=" * 50)
|
122
|
+
print("Configuration:")
|
123
|
+
print(f" Points: {args.points:,}")
|
124
|
+
print(f" Repeats: {args.repeats}")
|
125
|
+
print(f" Dtype: {args.dtype}")
|
126
|
+
print(f" Track objects: {args.track_objects}")
|
127
|
+
print()
|
128
|
+
|
129
|
+
# Data
|
130
|
+
pts_np = gen_np_points(args.points, dtype=dtype)
|
131
|
+
|
132
|
+
# Warmup
|
133
|
+
_ = bench_np_direct(pts_np[:10_000], repeats=1, track_objects=args.track_objects)
|
134
|
+
_ = bench_list_from_np(
|
135
|
+
pts_np[:10_000],
|
136
|
+
repeats=1,
|
137
|
+
track_objects=args.track_objects,
|
138
|
+
include_conversion=False,
|
139
|
+
)
|
140
|
+
_ = bench_list_from_np(
|
141
|
+
pts_np[:10_000],
|
142
|
+
repeats=1,
|
143
|
+
track_objects=args.track_objects,
|
144
|
+
include_conversion=True,
|
145
|
+
)
|
146
|
+
|
147
|
+
# Actual runs
|
148
|
+
t_np = bench_np_direct(pts_np, args.repeats, args.track_objects)
|
149
|
+
t_list_insert_only = bench_list_from_np(
|
150
|
+
pts_np, args.repeats, args.track_objects, include_conversion=False
|
151
|
+
)
|
152
|
+
t_list_with_convert = bench_list_from_np(
|
153
|
+
pts_np, args.repeats, args.track_objects, include_conversion=True
|
154
|
+
)
|
155
|
+
|
156
|
+
def fmt(x: float) -> str:
|
157
|
+
if x < 1e-3:
|
158
|
+
return f"{x * 1e6:.1f} µs"
|
159
|
+
if x < 1:
|
160
|
+
return f"{x * 1e3:.1f} ms"
|
161
|
+
return f"{x:.3f} s"
|
162
|
+
|
163
|
+
print("Results (median of repeats)")
|
164
|
+
print()
|
165
|
+
print("| Variant | Build time |")
|
166
|
+
print("|---|---:|")
|
167
|
+
print(f"| NumPy array direct | {fmt(t_np)} |")
|
168
|
+
print(f"| Python list insert only | {fmt(t_list_insert_only)} |")
|
169
|
+
print(f"| Python list including conversion | {fmt(t_list_with_convert)} |")
|
170
|
+
|
171
|
+
if collect_system_info and format_system_info_markdown_lite:
|
172
|
+
info = collect_system_info()
|
173
|
+
print()
|
174
|
+
print(format_system_info_markdown_lite(info))
|
175
|
+
|
176
|
+
|
177
|
+
if __name__ == "__main__":
|
178
|
+
main()
|
@@ -57,6 +57,8 @@ Using the shim with object tracking increases build time by 5.345x and query tim
|
|
57
57
|
|
58
58
|
Adding the object map tends to impact the build time more than query time.
|
59
59
|
|
60
|
+
-----------
|
61
|
+
|
60
62
|
## pyqtree drop-in shim performance gains
|
61
63
|
|
62
64
|
### Configuration
|
@@ -78,6 +80,33 @@ This is a **total speedup of 6.567x** compared to the original pyqtree and requi
|
|
78
80
|
|
79
81
|
---------
|
80
82
|
|
83
|
+
## NumPy Bulk Insert vs Python List Insert
|
84
|
+
### Configuration
|
85
|
+
|
86
|
+
- Points: 500,000
|
87
|
+
- Repeats: 5
|
88
|
+
- Dtype: float32
|
89
|
+
- Track objects: False
|
90
|
+
|
91
|
+
### Results (median of repeats)
|
92
|
+
|
93
|
+
| Variant | Build time |
|
94
|
+
|---|---:|
|
95
|
+
| NumPy array direct | 42.8 ms |
|
96
|
+
| Python list insert only | 51.1 ms |
|
97
|
+
| Python list including conversion | 540.2 ms |
|
98
|
+
|
99
|
+
Key:
|
100
|
+
|
101
|
+
- *NumPy array direct*: Using the `insert_many` method with a NumPy array of shape (N, 2).
|
102
|
+
- *Python list insert only*: Using the `insert_many` method with a Python list of tuples.
|
103
|
+
- *Python list including conversion*: Time taken to convert a NumPy array to a Python list of tuples, then inserting.
|
104
|
+
|
105
|
+
### Summary
|
106
|
+
If your data is already in a NumPy array, using the `insert_many` method directly with the array is significantly faster than converting to a Python list first.
|
107
|
+
|
108
|
+
---------
|
109
|
+
|
81
110
|
## System Info
|
82
111
|
- **OS**: Windows 11 AMD64
|
83
112
|
- **Python**: CPython 3.12.2
|
@@ -97,6 +126,7 @@ Then run:
|
|
97
126
|
```bash
|
98
127
|
python benchmarks/cross_library_bench.py
|
99
128
|
python benchmarks/benchmark_native_vs_shim.py
|
129
|
+
python benchmarks/benchmark_np_vs_list.py
|
100
130
|
```
|
101
131
|
|
102
132
|
Check the CLI arguments for the cross-library benchmark in `benchmarks/quadtree_bench/main.py`.
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Future Features
|
2
|
+
|
3
|
+
Below are a list of features that may be added to future versions of this project.
|
4
|
+
If you really want any of these features, please let us know by opening an issue.
|
5
|
+
|
6
|
+
If you have any suggestions or would like to contribute, please feel free to open an issue or a pull request.
|
7
|
+
|
8
|
+
## Planned Features
|
9
|
+
|
10
|
+
### 1. Quadtree serialization
|
11
|
+
|
12
|
+
By serializing the quadtree, we can save its state to a file and load it later. This will allow us to persist the quadtree structure and data across sessions. For example, you could pre build a quadtree with all the walls in your video game level, serialize it to a file, and then load it when the game starts. This will heavily reduce the game load time since you won't have to rebuild the quadtree from scratch every time.
|
13
|
+
|
14
|
+
### 2. Circle support
|
15
|
+
|
16
|
+
Currently, we support points and rectangles in two separate quadtrees.
|
17
|
+
For example, in the ball-pit demo, we use a point quadtree, but then query a larger area to account for the radius of the balls.
|
18
|
+
With a circle quadtree, we could directly insert circles and perform circle-circle collision detection.
|
19
|
+
|
20
|
+
### 3. KNN with criteria function
|
21
|
+
|
22
|
+
Currently, KNN only supports finding the nearest neighbors based on euclidean distance.
|
23
|
+
By adding a criteria function, we could allow users to define custom criteria for finding neighbors by passing a function that
|
24
|
+
takes in a point and returns a score. The KNN algorithm would then use this score to determine the nearest neighbors.
|
25
|
+
|
26
|
+
### 4. KNN in rectangle quadtree
|
27
|
+
|
28
|
+
Currently, KNN is only supported in the point quadtree. By adding KNN support to the rectangle quadtree, we could allow users to find the nearest rectangles to a given point. This would be to the nearest edge of the rectangle, adding complexity to the algorithm.
|
29
|
+
However, it will allow for really quick collision detection between a point and a set of rectangles as the point can just do
|
30
|
+
robust-collision handling with the nearest rectangles.
|
@@ -35,7 +35,7 @@
|
|
35
35
|
|
36
36
|
## Why use fastquadtree
|
37
37
|
|
38
|
-
- Clean [Python API](api/quadtree.md) with modern typing hints
|
38
|
+
- Clean [Python API](api/quadtree.md) with no external dependencies and modern typing hints
|
39
39
|
- The fastest quadtree Python package ([>10x faster](benchmark.md) than pyqtree)
|
40
40
|
- Prebuilt wheels for Windows, macOS, and Linux
|
41
41
|
- Support for [inserting bounding boxes](api/rect_quadtree.md) or points
|
@@ -0,0 +1,38 @@
|
|
1
|
+
/* Global typography tuning */
|
2
|
+
:root {
|
3
|
+
/* Larger base size for body copy */
|
4
|
+
--md-typeset-font-size: 0.98rem; /* default ~0.9-0.95 */
|
5
|
+
}
|
6
|
+
|
7
|
+
.md-typeset {
|
8
|
+
line-height: 1.65; /* more relaxed, better for focus */
|
9
|
+
letter-spacing: 0.001em;
|
10
|
+
}
|
11
|
+
|
12
|
+
/* Crisper headings without looking heavy */
|
13
|
+
.md-typeset h1, .md-typeset h2, .md-typeset h3 {
|
14
|
+
font-weight: 700;
|
15
|
+
line-height: 1.25;
|
16
|
+
}
|
17
|
+
|
18
|
+
/* Improve contrast on body text and links */
|
19
|
+
:root {
|
20
|
+
--md-default-fg-color: rgba(0,0,0,0.86);
|
21
|
+
--md-default-fg-color--light: rgba(0,0,0,0.64);
|
22
|
+
--md-typeset-a-color: #1e40af; /* indigo-800 like */
|
23
|
+
}
|
24
|
+
[data-md-color-scheme="slate"] {
|
25
|
+
--md-default-fg-color: rgba(255,255,255,0.92);
|
26
|
+
--md-default-fg-color--light: rgba(255,255,255,0.72);
|
27
|
+
--md-typeset-a-color: #93c5fd; /* lighter link on dark */
|
28
|
+
}
|
29
|
+
|
30
|
+
/* Make code a tad larger for readability */
|
31
|
+
.md-typeset code, .md-typeset pre code {
|
32
|
+
font-size: 0.93em;
|
33
|
+
}
|
34
|
+
|
35
|
+
/* Slightly stronger nav text */
|
36
|
+
.md-nav__item .md-nav__link {
|
37
|
+
font-weight: 500;
|
38
|
+
}
|
@@ -15,10 +15,18 @@ theme:
|
|
15
15
|
- toc.integrate
|
16
16
|
- search.suggest
|
17
17
|
- search.highlight
|
18
|
+
- content.tooltips
|
18
19
|
palette:
|
19
20
|
- scheme: slate
|
20
21
|
primary: indigo
|
21
22
|
accent: deep orange
|
23
|
+
font:
|
24
|
+
text: Inter
|
25
|
+
code: JetBrains Mono
|
26
|
+
|
27
|
+
extra_css:
|
28
|
+
- styles/overrides.css
|
29
|
+
|
22
30
|
|
23
31
|
plugins:
|
24
32
|
- search
|
@@ -36,6 +44,11 @@ plugins:
|
|
36
44
|
separate_signature: true
|
37
45
|
line_length: 88
|
38
46
|
heading_level: 2
|
47
|
+
filters: # Exclude __slots__ and __len__ and anything with a single underscore prefix (don't filter insert_many even though it has a single underscore)
|
48
|
+
- "!__slots__"
|
49
|
+
- "!__len__"
|
50
|
+
- "!^_[^_]"
|
51
|
+
|
39
52
|
- autorefs
|
40
53
|
- git-revision-date-localized:
|
41
54
|
fallback_to_build_date: true
|
@@ -68,6 +81,7 @@ nav:
|
|
68
81
|
- Quickstart: quickstart.md
|
69
82
|
- Runnables: runnables.md
|
70
83
|
- Benchmark: benchmark.md
|
84
|
+
- Future Features: future_features.md
|
71
85
|
- API:
|
72
86
|
- QuadTree: api/quadtree.md
|
73
87
|
- RectQuadTree: api/rect_quadtree.md
|
@@ -83,5 +97,4 @@ extra:
|
|
83
97
|
link: https://pypi.org/project/fastquadtree/
|
84
98
|
|
85
99
|
# Build options
|
86
|
-
extra_css: []
|
87
100
|
extra_javascript: []
|
@@ -9,6 +9,7 @@ dynamic = ["version"]
|
|
9
9
|
description = "Rust-accelerated quadtree for Python with fast inserts, range queries, and k-NN search."
|
10
10
|
readme = { file = "README.md", content-type = "text/markdown" }
|
11
11
|
requires-python = ">=3.8"
|
12
|
+
dependencies = [] # No runtime dependencies
|
12
13
|
license = { file = "LICENSE" }
|
13
14
|
authors = [{ name = "Ethan Anderson" }]
|
14
15
|
keywords = ["quadtree", "spatial-index", "geometry", "rust", "pyo3", "nearest-neighbor", "k-nn"]
|
@@ -78,6 +79,7 @@ dev = [
|
|
78
79
|
"mkdocs-minify-plugin",
|
79
80
|
"maturin>=1.5", # build Rust wheels
|
80
81
|
"pyqtree==1.0.0", # for comparison in tests
|
82
|
+
"numpy",
|
81
83
|
]
|
82
84
|
|
83
85
|
[tool.ruff]
|
@@ -2,11 +2,23 @@
|
|
2
2
|
from __future__ import annotations
|
3
3
|
|
4
4
|
from abc import ABC, abstractmethod
|
5
|
-
from typing import
|
5
|
+
from typing import (
|
6
|
+
TYPE_CHECKING,
|
7
|
+
Any,
|
8
|
+
Generic,
|
9
|
+
Iterable,
|
10
|
+
Sequence,
|
11
|
+
Tuple,
|
12
|
+
TypeVar,
|
13
|
+
overload,
|
14
|
+
)
|
6
15
|
|
7
16
|
from ._item import Item # base class for PointItem and RectItem
|
8
17
|
from ._obj_store import ObjStore
|
9
18
|
|
19
|
+
if TYPE_CHECKING:
|
20
|
+
from numpy.typing import NDArray
|
21
|
+
|
10
22
|
Bounds = Tuple[float, float, float, float]
|
11
23
|
|
12
24
|
# Generic parameters
|
@@ -15,6 +27,11 @@ HitT = TypeVar("HitT") # raw native tuple, e.g. (id,x,y) or (id,x0,y0,x1,y1)
|
|
15
27
|
ItemType = TypeVar("ItemType", bound=Item) # e.g. PointItem or RectItem
|
16
28
|
|
17
29
|
|
30
|
+
def _is_np_array(x: Any) -> bool:
|
31
|
+
mod = getattr(x.__class__, "__module__", "")
|
32
|
+
return mod.startswith("numpy") and hasattr(x, "ndim") and hasattr(x, "shape")
|
33
|
+
|
34
|
+
|
18
35
|
class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
|
19
36
|
"""
|
20
37
|
Shared logic for Python QuadTree wrappers over native Rust engines.
|
@@ -110,9 +127,18 @@ class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
|
|
110
127
|
self._count += 1
|
111
128
|
return rid
|
112
129
|
|
113
|
-
|
130
|
+
@overload
|
131
|
+
def insert_many(self, geoms: Sequence[G], objs: list[Any] | None = None) -> int: ...
|
132
|
+
@overload
|
133
|
+
def insert_many(
|
134
|
+
self, geoms: NDArray[Any], objs: list[Any] | None = None
|
135
|
+
) -> int: ...
|
136
|
+
def insert_many(
|
137
|
+
self, geoms: NDArray[Any] | Sequence[G], objs: list[Any] | None = None
|
138
|
+
) -> int:
|
114
139
|
"""
|
115
140
|
Bulk insert with auto-assigned contiguous ids. Faster than inserting one-by-one.<br>
|
141
|
+
Can accept either a Python sequence of geometries or a NumPy array of shape (N,2) or (N,4) with dtype float32.
|
116
142
|
|
117
143
|
If tracking is enabled, the objects will be bulk stored internally.
|
118
144
|
If no objects are provided, the items will have obj=None (if tracking).
|
@@ -127,13 +153,30 @@ class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
|
|
127
153
|
Raises:
|
128
154
|
ValueError: If any geometry is outside bounds.
|
129
155
|
"""
|
130
|
-
if
|
156
|
+
if type(geoms) is list and len(geoms) == 0:
|
131
157
|
return 0
|
132
158
|
|
159
|
+
if _is_np_array(geoms):
|
160
|
+
import numpy as _np
|
161
|
+
else:
|
162
|
+
_np = None
|
163
|
+
|
164
|
+
# Early return if the numpy array is empty
|
165
|
+
if _np is not None and isinstance(geoms, _np.ndarray):
|
166
|
+
if geoms.size == 0:
|
167
|
+
return 0
|
168
|
+
|
169
|
+
if geoms.dtype != _np.float32:
|
170
|
+
raise TypeError("Numpy array must use dtype float32")
|
171
|
+
|
133
172
|
if self._store is None:
|
134
173
|
# Simple contiguous path with native bulk insert
|
135
174
|
start_id = self._next_id
|
136
|
-
|
175
|
+
|
176
|
+
if _np is not None:
|
177
|
+
last_id = self._native.insert_many_np(start_id, geoms)
|
178
|
+
else:
|
179
|
+
last_id = self._native.insert_many(start_id, geoms)
|
137
180
|
num = last_id - start_id + 1
|
138
181
|
if num < len(geoms):
|
139
182
|
raise ValueError("One or more items are outside tree bounds")
|
@@ -143,7 +186,10 @@ class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
|
|
143
186
|
|
144
187
|
# With tracking enabled:
|
145
188
|
start_id = len(self._store._arr) # contiguous tail position
|
146
|
-
|
189
|
+
if _np is not None:
|
190
|
+
last_id = self._native.insert_many_np(start_id, geoms)
|
191
|
+
else:
|
192
|
+
last_id = self._native.insert_many(start_id, geoms)
|
147
193
|
num = last_id - start_id + 1
|
148
194
|
if num < len(geoms):
|
149
195
|
raise ValueError("One or more items are outside tree bounds")
|