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.
Files changed (74) hide show
  1. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/.pre-commit-config.yaml +2 -0
  2. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/Cargo.lock +91 -1
  3. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/Cargo.toml +2 -1
  4. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/PKG-INFO +4 -2
  5. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/README.md +2 -1
  6. fastquadtree-1.1.0/benchmarks/benchmark_np_vs_list.py +178 -0
  7. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/requirements.txt +4 -1
  8. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/benchmark.md +30 -0
  9. fastquadtree-1.1.0/docs/future_features.md +30 -0
  10. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/index.md +1 -1
  11. fastquadtree-1.1.0/docs/styles/overrides.css +38 -0
  12. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/mkdocs.yml +14 -1
  13. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pyproject.toml +2 -0
  14. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pysrc/fastquadtree/_base_quadtree.py +51 -5
  15. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pysrc/fastquadtree/_item.py +15 -5
  16. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/src/lib.rs +60 -0
  17. fastquadtree-1.1.0/tests/test_insert_many_numpy.py +183 -0
  18. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_pyqtree_shim_compat.py +97 -0
  19. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/.github/workflows/docs.yml +0 -0
  20. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/.github/workflows/release.yml +0 -0
  21. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/.gitignore +0 -0
  22. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/LICENSE +0 -0
  23. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/assets/ballpit.png +0 -0
  24. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/assets/interactive_v2_rect_screenshot.png +0 -0
  25. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/assets/interactive_v2_screenshot.png +0 -0
  26. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/assets/quadtree_bench_throughput.png +0 -0
  27. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/assets/quadtree_bench_time.png +0 -0
  28. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/benchmark_native_vs_shim.py +0 -0
  29. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/cross_library_bench.py +0 -0
  30. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/quadtree_bench/__init__.py +0 -0
  31. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/quadtree_bench/engines.py +0 -0
  32. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/quadtree_bench/main.py +0 -0
  33. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/quadtree_bench/plotting.py +0 -0
  34. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/quadtree_bench/runner.py +0 -0
  35. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/runner.py +0 -0
  36. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/benchmarks/system_info_collector.py +0 -0
  37. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/api/point_item.md +0 -0
  38. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/api/pyqtree.md +0 -0
  39. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/api/quadtree.md +0 -0
  40. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/api/rect_item.md +0 -0
  41. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/api/rect_quadtree.md +0 -0
  42. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/quickstart.md +0 -0
  43. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/docs/runnables.md +0 -0
  44. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/interactive/ballpit.py +0 -0
  45. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/interactive/interactive.py +0 -0
  46. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/interactive/interactive_v2.py +0 -0
  47. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/interactive/interactive_v2_rect.py +0 -0
  48. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/interactive/requirements.txt +0 -0
  49. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pysrc/fastquadtree/__init__.py +0 -0
  50. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pysrc/fastquadtree/_obj_store.py +0 -0
  51. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pysrc/fastquadtree/point_quadtree.py +0 -0
  52. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pysrc/fastquadtree/py.typed +0 -0
  53. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pysrc/fastquadtree/pyqtree.py +0 -0
  54. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/pysrc/fastquadtree/rect_quadtree.py +0 -0
  55. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/src/geom.rs +0 -0
  56. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/src/quadtree.rs +0 -0
  57. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/src/rect_quadtree.rs +0 -0
  58. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/insertions.rs +0 -0
  59. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/nearest_neighbor.rs +0 -0
  60. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/query.rs +0 -0
  61. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/rect_quadtree.rs +0 -0
  62. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/rectangle_traversal.rs +0 -0
  63. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_base_quadtree.py +0 -0
  64. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_clear.py +0 -0
  65. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_delete.rs +0 -0
  66. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_delete_by_object.py +0 -0
  67. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_delete_python.py +0 -0
  68. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_obj_store.py +0 -0
  69. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_point_quadtree_nn_runtime.py +0 -0
  70. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_python.py +0 -0
  71. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_rect_quadtree.py +0 -0
  72. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_unconventional_bounds.py +0 -0
  73. {fastquadtree-1.0.1 → fastquadtree-1.1.0}/tests/test_wrapper_edges.py +0 -0
  74. {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.1"
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.1"
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.1
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
  [![Python versions](https://img.shields.io/pypi/pyversions/fastquadtree.svg)](https://pypi.org/project/fastquadtree/)
49
50
  [![Downloads](https://static.pepy.tech/personalized-badge/fastquadtree?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=BLUE&left_text=Total%20Downloads)](https://pepy.tech/projects/fastquadtree)
50
51
  [![Build](https://github.com/Elan456/fastquadtree/actions/workflows/release.yml/badge.svg)](https://github.com/Elan456/fastquadtree/actions/workflows/release.yml)
52
+ ![No runtime deps](https://img.shields.io/badge/deps-none-success)
51
53
 
52
54
  [![PyO3](https://img.shields.io/badge/Rust-core%20via%20PyO3-orange)](https://pyo3.rs/)
53
55
  [![maturin](https://img.shields.io/badge/Built%20with-maturin-1f6feb)](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
  [![Python versions](https://img.shields.io/pypi/pyversions/fastquadtree.svg)](https://pypi.org/project/fastquadtree/)
10
10
  [![Downloads](https://static.pepy.tech/personalized-badge/fastquadtree?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=BLUE&left_text=Total%20Downloads)](https://pepy.tech/projects/fastquadtree)
11
11
  [![Build](https://github.com/Elan456/fastquadtree/actions/workflows/release.yml/badge.svg)](https://github.com/Elan456/fastquadtree/actions/workflows/release.yml)
12
+ ![No runtime deps](https://img.shields.io/badge/deps-none-success)
12
13
 
13
14
  [![PyO3](https://img.shields.io/badge/Rust-core%20via%20PyO3-orange)](https://pyo3.rs/)
14
15
  [![maturin](https://img.shields.io/badge/Built%20with-maturin-1f6feb)](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()
@@ -17,4 +17,7 @@ shapely>=2.0.0 # STRtree comparator
17
17
  # For getting system info
18
18
  psutil
19
19
  py-cpuinfo
20
- distro
20
+ distro
21
+
22
+ # For benchmarking NumPy bulk insert vs list insert
23
+ numpy
@@ -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 Any, Generic, Iterable, Tuple, TypeVar
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
- def insert_many(self, geoms: list[G], objs: list[Any] | None = None) -> int:
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 not geoms:
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
- last_id = self._native.insert_many(start_id, geoms)
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
- last_id = self._native.insert_many(start_id, geoms)
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")