fastquadtree 1.1.2__tar.gz → 1.2.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.

Potentially problematic release.


This version of fastquadtree might be problematic. Click here for more details.

Files changed (77) hide show
  1. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/.gitignore +2 -1
  2. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/Cargo.lock +65 -1
  3. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/Cargo.toml +3 -1
  4. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/PKG-INFO +2 -1
  5. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/README.md +1 -0
  6. fastquadtree-1.2.1/benchmarks/benchmark_serialization_vs_rebuild.py +149 -0
  7. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/docs/benchmark.md +26 -0
  8. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/docs/future_features.md +1 -1
  9. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/docs/index.md +1 -0
  10. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/interactive/interactive_v2.py +18 -0
  11. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/pysrc/fastquadtree/_base_quadtree.py +180 -1
  12. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/pysrc/fastquadtree/_item.py +29 -0
  13. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/pysrc/fastquadtree/_obj_store.py +39 -5
  14. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/pysrc/fastquadtree/point_quadtree.py +26 -1
  15. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/pysrc/fastquadtree/rect_quadtree.py +26 -1
  16. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/src/geom.rs +4 -2
  17. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/src/lib.rs +35 -0
  18. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/src/quadtree.rs +13 -2
  19. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/src/rect_quadtree.rs +13 -1
  20. fastquadtree-1.2.1/tests/serialization.rs +61 -0
  21. fastquadtree-1.2.1/tests/test_serialization.py +114 -0
  22. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/.github/workflows/docs.yml +0 -0
  23. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/.github/workflows/release.yml +0 -0
  24. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/.pre-commit-config.yaml +0 -0
  25. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/LICENSE +0 -0
  26. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/assets/ballpit.png +0 -0
  27. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/assets/interactive_v2_rect_screenshot.png +0 -0
  28. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/assets/interactive_v2_screenshot.png +0 -0
  29. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/assets/quadtree_bench_throughput.png +0 -0
  30. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/assets/quadtree_bench_time.png +0 -0
  31. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/benchmarks/benchmark_native_vs_shim.py +0 -0
  32. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/benchmarks/benchmark_np_vs_list.py +0 -0
  33. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/benchmarks/cross_library_bench.py +0 -0
  34. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/benchmarks/quadtree_bench/__init__.py +0 -0
  35. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/benchmarks/quadtree_bench/engines.py +0 -0
  36. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/benchmarks/quadtree_bench/main.py +0 -0
  37. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/benchmarks/quadtree_bench/plotting.py +0 -0
  38. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/benchmarks/quadtree_bench/runner.py +0 -0
  39. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/benchmarks/requirements.txt +0 -0
  40. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/benchmarks/runner.py +0 -0
  41. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/benchmarks/system_info_collector.py +0 -0
  42. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/docs/api/point_item.md +0 -0
  43. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/docs/api/pyqtree.md +0 -0
  44. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/docs/api/quadtree.md +0 -0
  45. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/docs/api/rect_item.md +0 -0
  46. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/docs/api/rect_quadtree.md +0 -0
  47. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/docs/quickstart.md +0 -0
  48. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/docs/runnables.md +0 -0
  49. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/docs/styles/overrides.css +0 -0
  50. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/interactive/ballpit.py +0 -0
  51. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/interactive/interactive.py +0 -0
  52. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/interactive/interactive_v2_rect.py +0 -0
  53. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/interactive/requirements.txt +0 -0
  54. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/mkdocs.yml +0 -0
  55. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/pyproject.toml +0 -0
  56. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/pysrc/fastquadtree/__init__.py +0 -0
  57. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/pysrc/fastquadtree/py.typed +0 -0
  58. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/pysrc/fastquadtree/pyqtree.py +0 -0
  59. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/insertions.rs +0 -0
  60. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/nearest_neighbor.rs +0 -0
  61. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/query.rs +0 -0
  62. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/rect_quadtree.rs +0 -0
  63. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/rectangle_traversal.rs +0 -0
  64. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/test_base_quadtree.py +0 -0
  65. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/test_clear.py +0 -0
  66. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/test_delete.rs +0 -0
  67. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/test_delete_by_object.py +0 -0
  68. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/test_delete_python.py +0 -0
  69. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/test_insert_many_numpy.py +0 -0
  70. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/test_obj_store.py +0 -0
  71. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/test_point_quadtree_nn_runtime.py +0 -0
  72. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/test_pyqtree_shim_compat.py +0 -0
  73. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/test_python.py +0 -0
  74. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/test_rect_quadtree.py +0 -0
  75. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/test_unconventional_bounds.py +0 -0
  76. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/test_wrapper_edges.py +0 -0
  77. {fastquadtree-1.1.2 → fastquadtree-1.2.1}/tests/unconventional_bounds.rs +0 -0
@@ -210,4 +210,5 @@ __marimo__/
210
210
 
211
211
 
212
212
  *.exe
213
- *.pdb
213
+ *.pdb
214
+ *.bin
@@ -8,12 +8,34 @@ version = "1.5.0"
8
8
  source = "registry+https://github.com/rust-lang/crates.io-index"
9
9
  checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
10
10
 
11
+ [[package]]
12
+ name = "bincode"
13
+ version = "2.0.1"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
16
+ dependencies = [
17
+ "bincode_derive",
18
+ "serde",
19
+ "unty",
20
+ ]
21
+
22
+ [[package]]
23
+ name = "bincode_derive"
24
+ version = "2.0.1"
25
+ source = "registry+https://github.com/rust-lang/crates.io-index"
26
+ checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
27
+ dependencies = [
28
+ "virtue",
29
+ ]
30
+
11
31
  [[package]]
12
32
  name = "fastquadtree"
13
- version = "1.1.2"
33
+ version = "1.2.1"
14
34
  dependencies = [
35
+ "bincode",
15
36
  "numpy",
16
37
  "pyo3",
38
+ "serde",
17
39
  "smallvec",
18
40
  ]
19
41
 
@@ -224,6 +246,36 @@ version = "2.1.1"
224
246
  source = "registry+https://github.com/rust-lang/crates.io-index"
225
247
  checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
226
248
 
249
+ [[package]]
250
+ name = "serde"
251
+ version = "1.0.228"
252
+ source = "registry+https://github.com/rust-lang/crates.io-index"
253
+ checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
254
+ dependencies = [
255
+ "serde_core",
256
+ "serde_derive",
257
+ ]
258
+
259
+ [[package]]
260
+ name = "serde_core"
261
+ version = "1.0.228"
262
+ source = "registry+https://github.com/rust-lang/crates.io-index"
263
+ checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
264
+ dependencies = [
265
+ "serde_derive",
266
+ ]
267
+
268
+ [[package]]
269
+ name = "serde_derive"
270
+ version = "1.0.228"
271
+ source = "registry+https://github.com/rust-lang/crates.io-index"
272
+ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
273
+ dependencies = [
274
+ "proc-macro2",
275
+ "quote",
276
+ "syn",
277
+ ]
278
+
227
279
  [[package]]
228
280
  name = "smallvec"
229
281
  version = "1.15.1"
@@ -258,3 +310,15 @@ name = "unindent"
258
310
  version = "0.2.4"
259
311
  source = "registry+https://github.com/rust-lang/crates.io-index"
260
312
  checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
313
+
314
+ [[package]]
315
+ name = "unty"
316
+ version = "0.0.4"
317
+ source = "registry+https://github.com/rust-lang/crates.io-index"
318
+ checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
319
+
320
+ [[package]]
321
+ name = "virtue"
322
+ version = "0.0.18"
323
+ source = "registry+https://github.com/rust-lang/crates.io-index"
324
+ checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "fastquadtree"
3
- version = "1.1.2"
3
+ version = "1.2.1"
4
4
  edition = "2021"
5
5
  readme = "README.md"
6
6
 
@@ -11,6 +11,8 @@ crate-type = ["rlib", "cdylib"]
11
11
  pyo3 = { version = "0.26", features = ["extension-module", "abi3-py38"] }
12
12
  smallvec = "1.15.1"
13
13
  numpy = "0.26"
14
+ serde = { version = "1.0", features = ["derive"] }
15
+ bincode = {version = "2.0.1", features = ["serde"]}
14
16
 
15
17
  [profile.release]
16
18
  opt-level = 3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastquadtree
3
- Version: 1.1.2
3
+ Version: 1.2.1
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3 :: Only
6
6
  Classifier: Programming Language :: Rust
@@ -70,6 +70,7 @@ Rust-optimized quadtree with a clean Python API
70
70
  - Support for [inserting bounding boxes](https://elan456.github.io/fastquadtree/api/rect_quadtree/) or points
71
71
  - Fast KNN and range queries
72
72
  - Optional object tracking for id ↔ object mapping
73
+ - Fast [serialization](https://elan456.github.io/fastquadtree/benchmark/#serialization-vs-rebuild) to/from bytes
73
74
  - [100% test coverage](https://codecov.io/gh/Elan456/fastquadtree) and CI on GitHub Actions
74
75
  - Offers a drop-in [pyqtree shim](https://elan456.github.io/fastquadtree/benchmark/#pyqtree-drop-in-shim-performance-gains) that is 6.567x faster while keeping the same API
75
76
 
@@ -30,6 +30,7 @@ Rust-optimized quadtree with a clean Python API
30
30
  - Support for [inserting bounding boxes](https://elan456.github.io/fastquadtree/api/rect_quadtree/) or points
31
31
  - Fast KNN and range queries
32
32
  - Optional object tracking for id ↔ object mapping
33
+ - Fast [serialization](https://elan456.github.io/fastquadtree/benchmark/#serialization-vs-rebuild) to/from bytes
33
34
  - [100% test coverage](https://codecov.io/gh/Elan456/fastquadtree) and CI on GitHub Actions
34
35
  - Offers a drop-in [pyqtree shim](https://elan456.github.io/fastquadtree/benchmark/#pyqtree-drop-in-shim-performance-gains) that is 6.567x faster while keeping the same API
35
36
 
@@ -0,0 +1,149 @@
1
+ """
2
+ Benchmark: serialization/deserialization vs rebuild from NumPy array
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import gc
8
+ import statistics as stats
9
+ from pathlib import Path
10
+ from time import perf_counter as pc
11
+
12
+ import numpy as np
13
+
14
+ from fastquadtree import QuadTree
15
+
16
+ CAPACITY = 64
17
+ MAX_DEPTH = 10
18
+ N = 1_000_000
19
+ REPEATS = 7
20
+
21
+
22
+ def timeit(fn, repeat=REPEATS):
23
+ # warmup
24
+ fn()
25
+ times = []
26
+ gc.disable()
27
+ try:
28
+ for _ in range(repeat):
29
+ t0 = pc()
30
+ fn()
31
+ times.append(pc() - t0)
32
+ finally:
33
+ gc.enable()
34
+ return times
35
+
36
+
37
+ def make_points():
38
+ rng = np.random.default_rng(42) # seed
39
+ # shape (N, 2), float32 to match native expectations
40
+ return rng.uniform(0.0, 1000.0, size=(N, 2)).astype(np.float32)
41
+
42
+
43
+ def build_original(pts: np.ndarray) -> QuadTree:
44
+ qt = QuadTree((0, 0, 1000, 1000), capacity=CAPACITY, max_depth=MAX_DEPTH)
45
+ qt.insert_many(pts)
46
+ return qt
47
+
48
+
49
+ def main():
50
+ pts = make_points()
51
+ original_qt = build_original(pts)
52
+
53
+ # correctness baseline
54
+ base_count = original_qt.count_items()
55
+ assert base_count == N
56
+
57
+ # serialize timing
58
+ ser_times = timeit(lambda: original_qt.to_bytes())
59
+ qt_bytes = original_qt.to_bytes()
60
+ print(f"Serialized size: {len(qt_bytes):,} bytes")
61
+
62
+ # write once for the file path
63
+ fname = "quadtree_serialization.bin"
64
+ with Path(fname).open("wb") as f:
65
+ f.write(qt_bytes)
66
+
67
+ def rebuild_points():
68
+ qt = QuadTree((0, 0, 1000, 1000), capacity=CAPACITY, max_depth=MAX_DEPTH)
69
+ qt.insert_many(pts)
70
+ assert qt.count_items() == base_count
71
+ _ = qt.query((100, 100, 200, 200))
72
+ return qt
73
+
74
+ def rebuild_from_mem():
75
+ qt = QuadTree.from_bytes(qt_bytes)
76
+ assert qt.count_items() == base_count
77
+ _ = qt.query((100, 100, 200, 200))
78
+ return qt
79
+
80
+ def rebuild_from_file():
81
+ with Path(fname).open("rb") as f:
82
+ data = f.read()
83
+ qt = QuadTree.from_bytes(data)
84
+ assert qt.count_items() == base_count
85
+ _ = qt.query((100, 100, 200, 200))
86
+ return qt
87
+
88
+ t_points = timeit(rebuild_points)
89
+ t_mem = timeit(rebuild_from_mem)
90
+ t_file = timeit(rebuild_from_file)
91
+
92
+ Path(fname).unlink(missing_ok=True)
93
+
94
+ def show(label, arr):
95
+ print(
96
+ f"{label:<28} mean={stats.mean(arr):.6f}s stdev={stats.pstdev(arr):.6f}s "
97
+ )
98
+
99
+ show("serialize to bytes", ser_times)
100
+ show("rebuild from points", t_points)
101
+ show("rebuild from bytes", t_mem)
102
+ show("rebuild from file", t_file)
103
+
104
+ # ----- Markdown summary -----
105
+ def fmt(x: float) -> str:
106
+ return f"{x:.6f}"
107
+
108
+ m_ser = stats.mean(ser_times)
109
+ m_pts = stats.mean(t_points)
110
+ m_mem = stats.mean(t_mem)
111
+ m_file = stats.mean(t_file)
112
+
113
+ s_ser = stats.pstdev(ser_times)
114
+ s_pts = stats.pstdev(t_points)
115
+ s_mem = stats.pstdev(t_mem)
116
+ s_file = stats.pstdev(t_file)
117
+
118
+ speedup_mem = m_pts / m_mem if m_mem > 0 else float("inf")
119
+ speedup_file = m_pts / m_file if m_file > 0 else float("inf")
120
+
121
+ md = f"""
122
+ ## Serialization vs Rebuild
123
+
124
+ ### Configuration
125
+ - Points: {N:,}
126
+ - Capacity: {CAPACITY}
127
+ - Max depth: {MAX_DEPTH}
128
+ - Repeats: {REPEATS}
129
+
130
+ ### Results
131
+
132
+ | Variant | Mean (s) | Stdev (s) |
133
+ |---|---:|---:|
134
+ | Serialize to bytes | {fmt(m_ser)} | {fmt(s_ser)} |
135
+ | Rebuild from points | {fmt(m_pts)} | {fmt(s_pts)} |
136
+ | Rebuild from bytes | {fmt(m_mem)} | {fmt(s_mem)} |
137
+ | Rebuild from file | {fmt(m_file)} | {fmt(s_file)} |
138
+
139
+ ### Summary
140
+
141
+ - Rebuild from bytes is **{fmt(speedup_mem)}x** faster than reinserting points.
142
+ - Rebuild from file is **{fmt(speedup_file)}x** faster than reinserting points.
143
+ - Serialized blob size is **{len(qt_bytes):,} bytes**.
144
+ """
145
+ print(md.strip())
146
+
147
+
148
+ if __name__ == "__main__":
149
+ main()
@@ -107,6 +107,31 @@ If your data is already in a NumPy array, using the `insert_many` method directl
107
107
 
108
108
  ---------
109
109
 
110
+ ## Serialization vs Rebuild
111
+
112
+ ### Configuration
113
+ - Points: 1,000,000
114
+ - Capacity: 64
115
+ - Max depth: 10
116
+ - Repeats: 7
117
+
118
+ ### Results
119
+
120
+ | Variant | Mean (s) | Stdev (s) |
121
+ |---|---:|---:|
122
+ | Serialize to bytes | 0.021356 | 0.000937 |
123
+ | Rebuild from points | 0.106783 | 0.011430 |
124
+ | Rebuild from bytes | 0.021754 | 0.001687 |
125
+ | Rebuild from file | 0.024887 | 0.001846 |
126
+
127
+ ### Summary
128
+
129
+ - Rebuild from bytes is **4.908747x** faster than reinserting points.
130
+ - Rebuild from file is **4.290712x** faster than reinserting points.
131
+ - Serialized blob size is **13,770,328 bytes**.
132
+
133
+ ----------------
134
+
110
135
  ## System Info
111
136
  - **OS**: Windows 11 AMD64
112
137
  - **Python**: CPython 3.12.2
@@ -127,6 +152,7 @@ Then run:
127
152
  python benchmarks/cross_library_bench.py
128
153
  python benchmarks/benchmark_native_vs_shim.py
129
154
  python benchmarks/benchmark_np_vs_list.py
155
+ python benchmarks/benchmark_serialization_vs_rebuild.py
130
156
  ```
131
157
 
132
158
  Check the CLI arguments for the cross-library benchmark in `benchmarks/quadtree_bench/main.py`.
@@ -7,7 +7,7 @@ If you have any suggestions or would like to contribute, please feel free to ope
7
7
 
8
8
  ## Planned Features
9
9
 
10
- ### 1. Quadtree serialization
10
+ ### 1. [COMPLETE] Quadtree serialization
11
11
 
12
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
13
 
@@ -41,6 +41,7 @@
41
41
  - Support for [inserting bounding boxes](api/rect_quadtree.md) or points
42
42
  - Fast KNN and range queries
43
43
  - Optional object tracking for id ↔ object mapping
44
+ - Fast [serialization](benchmark.md#serialization-vs-rebuild) to/from bytes
44
45
  - [100% test coverage](https://codecov.io/gh/Elan456/fastquadtree) and CI on GitHub Actions
45
46
 
46
47
  ## Examples
@@ -1,6 +1,7 @@
1
1
  import math
2
2
  import random
3
3
  from collections import deque
4
+ from pathlib import Path
4
5
 
5
6
  import pygame
6
7
 
@@ -240,6 +241,7 @@ def hud(text_lines):
240
241
  # ------------------------------
241
242
  def handle_events(running, paused, show_nodes, show_nn, show_trails, zoom_target):
242
243
  """Handle discrete events like key presses and mouse clicks."""
244
+ global qtree
243
245
  for ev in pygame.event.get():
244
246
  if ev.type == pygame.QUIT:
245
247
  running = False
@@ -258,6 +260,21 @@ def handle_events(running, paused, show_nodes, show_nn, show_trails, zoom_target
258
260
  zoom_target = clamp(zoom_target * ZOOM_FACTOR, ZOOM_MIN, ZOOM_MAX)
259
261
  elif ev.key in (pygame.K_MINUS, pygame.K_KP_MINUS):
260
262
  zoom_target = clamp(zoom_target / ZOOM_FACTOR, ZOOM_MIN, ZOOM_MAX)
263
+ elif ev.key == pygame.K_c:
264
+ # Save quadtree state
265
+ data = qtree.to_bytes()
266
+ with Path("quadtree_state.bin").open("wb") as f:
267
+ f.write(data)
268
+ print("Quadtree state saved to quadtree_state.bin")
269
+ elif ev.key == pygame.K_v:
270
+ # Load quadtree state
271
+ try:
272
+ with Path("quadtree_state.bin").open("rb") as f:
273
+ data = f.read()
274
+ qtree = QuadTree.from_bytes(data)
275
+ print("Quadtree state loaded from quadtree_state.bin")
276
+ except Exception as e: # noqa: BLE001
277
+ print(f"Failed to load quadtree state: {e}")
261
278
 
262
279
  elif ev.type == pygame.MOUSEWHEEL:
263
280
  if ev.y != 0:
@@ -436,6 +453,7 @@ def main():
436
453
  f"zoom: {zoom:.2f} target: {zoom_target:.2f}",
437
454
  "WASD to pan. Mouse wheel or +/- to zoom.",
438
455
  "arrow keys to resize rectangle.",
456
+ "c to save current state, v to load.",
439
457
  "1 nodes. 2 NN rays. 3 trails. SPACE pause. L-click add. R-click remove (shift to repeat)",
440
458
  ]
441
459
  )
@@ -1,6 +1,7 @@
1
1
  # _abc_quadtree.py
2
2
  from __future__ import annotations
3
3
 
4
+ import pickle
4
5
  from abc import ABC, abstractmethod
5
6
  from typing import (
6
7
  TYPE_CHECKING,
@@ -58,8 +59,9 @@ class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
58
59
  def _new_native(self, bounds: Bounds, capacity: int, max_depth: int | None) -> Any:
59
60
  """Create the native engine instance."""
60
61
 
62
+ @staticmethod
61
63
  @abstractmethod
62
- def _make_item(self, id_: int, geom: G, obj: Any | None) -> ItemType:
64
+ def _make_item(id_: int, geom: G, obj: Any | None) -> ItemType:
63
65
  """Build an ItemType from id, geometry, and optional object."""
64
66
 
65
67
  # ---- ctor ----
@@ -84,6 +86,91 @@ class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
84
86
  self._next_id = 0
85
87
  self._count = 0
86
88
 
89
+ # ---- serialization ----
90
+
91
+ def to_dict(self) -> dict[str, Any]:
92
+ """
93
+ Serialize the quadtree to a dict suitable for JSON or other serialization.
94
+
95
+ Returns:
96
+ Includes a binary 'core' key for the native engine state, plus other metadata such as bounds and capacity and the obj store if tracking is enabled.
97
+
98
+ Example:
99
+ ```python
100
+ state = qt.to_dict()
101
+ assert "core" in state and "bounds" in state
102
+ ```
103
+ """
104
+
105
+ core_bytes = self._native.to_bytes()
106
+
107
+ return {
108
+ "core": core_bytes,
109
+ "store": self._store.to_dict() if self._store is not None else None,
110
+ "bounds": self._bounds,
111
+ "capacity": self._capacity,
112
+ "max_depth": self._max_depth,
113
+ "track_objects": self._track_objects,
114
+ "next_id": self._next_id,
115
+ "count": self._count,
116
+ }
117
+
118
+ def to_bytes(self) -> bytes:
119
+ """
120
+ Serialize the quadtree to bytes.
121
+
122
+ Returns:
123
+ Bytes representing the serialized quadtree. Can be saved as a file or loaded with `from_bytes()`.
124
+
125
+ Example:
126
+ ```python
127
+ blob = qt.to_bytes()
128
+ with open("tree.fqt", "wb") as f:
129
+ f.write(blob)
130
+ ```
131
+ """
132
+ return pickle.dumps(self.to_dict())
133
+
134
+ @classmethod
135
+ def from_bytes(cls, data: bytes) -> _BaseQuadTree[G, HitT, ItemType]:
136
+ """
137
+ Deserialize a quadtree from bytes.
138
+
139
+ Args:
140
+ data: Bytes representing the serialized quadtree from `to_bytes()`.
141
+
142
+ Returns:
143
+ A new quadtree instance with the same state as when serialized.
144
+
145
+ Example:
146
+ ```python
147
+ blob = qt.to_bytes()
148
+ qt2 = type(qt).from_bytes(blob)
149
+ assert qt2.count_items() == qt.count_items()
150
+ ```
151
+ """
152
+ in_dict = pickle.loads(data)
153
+ core_bytes = in_dict["core"]
154
+ store_dict = in_dict["store"]
155
+
156
+ qt = cls.__new__(cls) # type: ignore[call-arg]
157
+ qt._native = cls._new_native_from_bytes(core_bytes)
158
+
159
+ if store_dict is not None:
160
+ qt._store = ObjStore.from_dict(store_dict, qt._make_item)
161
+ else:
162
+ qt._store = None
163
+
164
+ # Extract bounds, capacity, max_depth from native
165
+ qt._bounds = in_dict["bounds"]
166
+ qt._capacity = in_dict["capacity"]
167
+ qt._max_depth = in_dict["max_depth"]
168
+ qt._next_id = in_dict["next_id"]
169
+ qt._count = in_dict["count"]
170
+ qt._track_objects = in_dict["track_objects"]
171
+
172
+ return qt
173
+
87
174
  # ---- internal helper ----
88
175
 
89
176
  def _ids_to_objects(self, ids: Iterable[int]) -> list[Any]:
@@ -107,6 +194,13 @@ class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
107
194
 
108
195
  Raises:
109
196
  ValueError: If geometry is outside the tree bounds.
197
+
198
+ Example:
199
+ ```python
200
+ id0 = point_qt.insert((10.0, 20.0)) # for point trees
201
+ id1 = rect_qt.insert((0.0, 0.0, 5.0, 5.0), obj="box") # for rect trees
202
+ assert isinstance(id0, int) and isinstance(id1, int)
203
+ ```
110
204
  """
111
205
  if self._store is not None:
112
206
  # Reuse a dense free slot if available, else append
@@ -152,6 +246,17 @@ class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
152
246
 
153
247
  Raises:
154
248
  ValueError: If any geometry is outside bounds.
249
+
250
+ Example:
251
+ ```python
252
+ n = qt.insert_many([(1.0, 1.0), (2.0, 2.0)])
253
+ assert n == 2
254
+
255
+ import numpy as np
256
+ arr = np.array([[3.0, 3.0], [4.0, 4.0]], dtype=np.float32)
257
+ n2 = qt.insert_many(arr)
258
+ assert n2 == 2
259
+ ```
155
260
  """
156
261
  if type(geoms) is list and len(geoms) == 0:
157
262
  return 0
@@ -216,8 +321,19 @@ class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
216
321
  """
217
322
  Delete an item by id and exact geometry.
218
323
 
324
+ Args:
325
+ id_: The id of the item to delete.
326
+ geom: The geometry of the item to delete.
327
+
219
328
  Returns:
220
329
  True if the item was found and deleted.
330
+
331
+ Example:
332
+ ```python
333
+ i = qt.insert((1.0, 2.0))
334
+ ok = qt.delete(i, (1.0, 2.0))
335
+ assert ok is True
336
+ ```
221
337
  """
222
338
  deleted = self._native.delete(id_, geom)
223
339
  if deleted:
@@ -230,6 +346,17 @@ class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
230
346
  """
231
347
  Attach or replace the Python object for an existing id.
232
348
  Tracking must be enabled.
349
+
350
+ Args:
351
+ id_: The id of the item to attach the object to.
352
+ obj: The Python object to attach.
353
+
354
+ Example:
355
+ ```python
356
+ i = qt.insert((2.0, 3.0), obj=None)
357
+ qt.attach(i, {"meta": 123})
358
+ assert qt.get(i) == {"meta": 123}
359
+ ```
233
360
  """
234
361
  if self._store is None:
235
362
  raise ValueError("Cannot attach objects when track_objects=False")
@@ -242,6 +369,16 @@ class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
242
369
  def delete_by_object(self, obj: Any) -> bool:
243
370
  """
244
371
  Delete an item by Python object identity. Tracking must be enabled.
372
+
373
+ Args:
374
+ obj: The Python object to delete.
375
+
376
+ Example:
377
+ ```python
378
+ i = qt.insert((3.0, 4.0), obj="tag")
379
+ ok = qt.delete_by_object("tag")
380
+ assert ok is True
381
+ ```
245
382
  """
246
383
  if self._store is None:
247
384
  raise ValueError("Cannot delete by object when track_objects=False")
@@ -256,6 +393,13 @@ class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
256
393
 
257
394
  If tracking is enabled, the id -> object mapping is also cleared.
258
395
  The ids are reset to start at zero again.
396
+
397
+ Example:
398
+ ```python
399
+ _ = qt.insert((5.0, 6.0))
400
+ qt.clear()
401
+ assert qt.count_items() == 0 and len(qt) == 0
402
+ ```
259
403
  """
260
404
  self._native = self._new_native(self._bounds, self._capacity, self._max_depth)
261
405
  self._count = 0
@@ -266,6 +410,14 @@ class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
266
410
  def get_all_objects(self) -> list[Any]:
267
411
  """
268
412
  Return all tracked Python objects in the tree.
413
+
414
+ Example:
415
+ ```python
416
+ _ = qt.insert((7.0, 8.0), obj="a")
417
+ _ = qt.insert((9.0, 1.0), obj="b")
418
+ objs = qt.get_all_objects()
419
+ assert set(objs) == {"a", "b"}
420
+ ```
269
421
  """
270
422
  if self._store is None:
271
423
  raise ValueError("Cannot get objects when track_objects=False")
@@ -274,6 +426,13 @@ class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
274
426
  def get_all_items(self) -> list[ItemType]:
275
427
  """
276
428
  Return all Item wrappers in the tree.
429
+
430
+ Example:
431
+ ```python
432
+ _ = qt.insert((1.0, 1.0), obj=None)
433
+ items = qt.get_all_items()
434
+ assert hasattr(items[0], "id_") and hasattr(items[0], "geom")
435
+ ```
277
436
  """
278
437
  if self._store is None:
279
438
  raise ValueError("Cannot get items when track_objects=False")
@@ -282,12 +441,25 @@ class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
282
441
  def get_all_node_boundaries(self) -> list[Bounds]:
283
442
  """
284
443
  Return all node boundaries in the tree. Useful for visualization.
444
+
445
+ Example:
446
+ ```python
447
+ bounds = qt.get_all_node_boundaries()
448
+ assert isinstance(bounds, list)
449
+ ```
285
450
  """
286
451
  return self._native.get_all_node_boundaries()
287
452
 
288
453
  def get(self, id_: int) -> Any | None:
289
454
  """
290
455
  Return the object associated with id, if tracking is enabled.
456
+
457
+ Example:
458
+ ```python
459
+ i = qt.insert((1.0, 2.0), obj={"k": "v"})
460
+ obj = qt.get(i)
461
+ assert obj == {"k": "v"}
462
+ ```
291
463
  """
292
464
  if self._store is None:
293
465
  raise ValueError("Cannot get objects when track_objects=False")
@@ -297,6 +469,13 @@ class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
297
469
  def count_items(self) -> int:
298
470
  """
299
471
  Return the number of items currently in the tree (native count).
472
+
473
+ Example:
474
+ ```python
475
+ before = qt.count_items()
476
+ _ = qt.insert((2.0, 2.0))
477
+ assert qt.count_items() == before + 1
478
+ ```
300
479
  """
301
480
  return self._native.count_items()
302
481