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

Potentially problematic release.


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

Files changed (77) hide show
  1. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/.gitignore +2 -1
  2. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/Cargo.lock +65 -1
  3. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/Cargo.toml +3 -1
  4. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/PKG-INFO +2 -1
  5. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/README.md +1 -0
  6. fastquadtree-1.2.0/benchmarks/benchmark_serialization_vs_rebuild.py +149 -0
  7. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/docs/benchmark.md +26 -0
  8. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/docs/future_features.md +1 -1
  9. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/docs/index.md +1 -0
  10. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/interactive/interactive_v2.py +18 -0
  11. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/pysrc/fastquadtree/_base_quadtree.py +68 -1
  12. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/pysrc/fastquadtree/_item.py +29 -0
  13. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/pysrc/fastquadtree/_obj_store.py +39 -5
  14. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/pysrc/fastquadtree/point_quadtree.py +19 -1
  15. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/pysrc/fastquadtree/pyqtree.py +6 -0
  16. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/pysrc/fastquadtree/rect_quadtree.py +19 -1
  17. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/src/geom.rs +4 -2
  18. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/src/lib.rs +35 -0
  19. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/src/quadtree.rs +13 -2
  20. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/src/rect_quadtree.rs +13 -1
  21. fastquadtree-1.2.0/tests/serialization.rs +61 -0
  22. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/test_pyqtree_shim_compat.py +39 -0
  23. fastquadtree-1.2.0/tests/test_serialization.py +114 -0
  24. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/.github/workflows/docs.yml +0 -0
  25. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/.github/workflows/release.yml +0 -0
  26. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/.pre-commit-config.yaml +0 -0
  27. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/LICENSE +0 -0
  28. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/assets/ballpit.png +0 -0
  29. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/assets/interactive_v2_rect_screenshot.png +0 -0
  30. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/assets/interactive_v2_screenshot.png +0 -0
  31. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/assets/quadtree_bench_throughput.png +0 -0
  32. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/assets/quadtree_bench_time.png +0 -0
  33. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/benchmarks/benchmark_native_vs_shim.py +0 -0
  34. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/benchmarks/benchmark_np_vs_list.py +0 -0
  35. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/benchmarks/cross_library_bench.py +0 -0
  36. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/benchmarks/quadtree_bench/__init__.py +0 -0
  37. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/benchmarks/quadtree_bench/engines.py +0 -0
  38. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/benchmarks/quadtree_bench/main.py +0 -0
  39. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/benchmarks/quadtree_bench/plotting.py +0 -0
  40. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/benchmarks/quadtree_bench/runner.py +0 -0
  41. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/benchmarks/requirements.txt +0 -0
  42. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/benchmarks/runner.py +0 -0
  43. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/benchmarks/system_info_collector.py +0 -0
  44. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/docs/api/point_item.md +0 -0
  45. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/docs/api/pyqtree.md +0 -0
  46. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/docs/api/quadtree.md +0 -0
  47. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/docs/api/rect_item.md +0 -0
  48. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/docs/api/rect_quadtree.md +0 -0
  49. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/docs/quickstart.md +0 -0
  50. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/docs/runnables.md +0 -0
  51. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/docs/styles/overrides.css +0 -0
  52. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/interactive/ballpit.py +0 -0
  53. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/interactive/interactive.py +0 -0
  54. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/interactive/interactive_v2_rect.py +0 -0
  55. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/interactive/requirements.txt +0 -0
  56. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/mkdocs.yml +0 -0
  57. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/pyproject.toml +0 -0
  58. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/pysrc/fastquadtree/__init__.py +0 -0
  59. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/pysrc/fastquadtree/py.typed +0 -0
  60. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/insertions.rs +0 -0
  61. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/nearest_neighbor.rs +0 -0
  62. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/query.rs +0 -0
  63. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/rect_quadtree.rs +0 -0
  64. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/rectangle_traversal.rs +0 -0
  65. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/test_base_quadtree.py +0 -0
  66. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/test_clear.py +0 -0
  67. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/test_delete.rs +0 -0
  68. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/test_delete_by_object.py +0 -0
  69. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/test_delete_python.py +0 -0
  70. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/test_insert_many_numpy.py +0 -0
  71. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/test_obj_store.py +0 -0
  72. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/test_point_quadtree_nn_runtime.py +0 -0
  73. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/test_python.py +0 -0
  74. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/test_rect_quadtree.py +0 -0
  75. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/test_unconventional_bounds.py +0 -0
  76. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/tests/test_wrapper_edges.py +0 -0
  77. {fastquadtree-1.1.1 → fastquadtree-1.2.0}/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.1"
33
+ version = "1.2.0"
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.1"
3
+ version = "1.2.0"
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.1
3
+ Version: 1.2.0
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,71 @@ 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
+
99
+ core_bytes = self._native.to_bytes()
100
+
101
+ return {
102
+ "core": core_bytes,
103
+ "store": self._store.to_dict() if self._store is not None else None,
104
+ "bounds": self._bounds,
105
+ "capacity": self._capacity,
106
+ "max_depth": self._max_depth,
107
+ "track_objects": self._track_objects,
108
+ "next_id": self._next_id,
109
+ "count": self._count,
110
+ }
111
+
112
+ def to_bytes(self) -> bytes:
113
+ """
114
+ Serialize the quadtree to bytes.
115
+
116
+ Returns:
117
+ Bytes representing the serialized quadtree. Can be saved as a file or loaded with `from_bytes()`.
118
+ """
119
+ return pickle.dumps(self.to_dict())
120
+
121
+ @classmethod
122
+ def from_bytes(cls, data: bytes) -> _BaseQuadTree[G, HitT, ItemType]:
123
+ """
124
+ Deserialize a quadtree from bytes.
125
+
126
+ Args:
127
+ data: Bytes representing the serialized quadtree from `to_bytes()`.
128
+
129
+ Returns:
130
+ A new quadtree instance with the same state as when serialized.
131
+ """
132
+ in_dict = pickle.loads(data)
133
+ core_bytes = in_dict["core"]
134
+ store_dict = in_dict["store"]
135
+
136
+ qt = cls.__new__(cls) # type: ignore[call-arg]
137
+ qt._native = cls._new_native_from_bytes(core_bytes)
138
+
139
+ if store_dict is not None:
140
+ qt._store = ObjStore.from_dict(store_dict, qt._make_item)
141
+ else:
142
+ qt._store = None
143
+
144
+ # Extract bounds, capacity, max_depth from native
145
+ qt._bounds = in_dict["bounds"]
146
+ qt._capacity = in_dict["capacity"]
147
+ qt._max_depth = in_dict["max_depth"]
148
+ qt._next_id = in_dict["next_id"]
149
+ qt._count = in_dict["count"]
150
+ qt._track_objects = in_dict["track_objects"]
151
+
152
+ return qt
153
+
87
154
  # ---- internal helper ----
88
155
 
89
156
  def _ids_to_objects(self, ids: Iterable[int]) -> list[Any]:
@@ -27,6 +27,35 @@ class Item:
27
27
  self.geom: Point | Bounds = geom
28
28
  self.obj: Any | None = obj
29
29
 
30
+ def to_dict(self) -> dict[str, Any]:
31
+ """
32
+ Serialize the item to a dictionary.
33
+
34
+ Returns:
35
+ A dictionary with 'id', 'geom', and 'obj' keys.
36
+ """
37
+ return {
38
+ "id": self.id_,
39
+ "geom": self.geom,
40
+ "obj": self.obj,
41
+ }
42
+
43
+ @classmethod
44
+ def from_dict(cls, data: dict[str, Any]) -> Item:
45
+ """
46
+ Deserialize an item from a dictionary.
47
+
48
+ Args:
49
+ data: A dictionary with 'id', 'geom', and 'obj' keys.
50
+
51
+ Returns:
52
+ An Item instance populated with the deserialized data.
53
+ """
54
+ id_ = data["id"]
55
+ geom = data["geom"]
56
+ obj = data["obj"]
57
+ return cls(id_, geom, obj)
58
+
30
59
 
31
60
  class PointItem(Item):
32
61
  """
@@ -35,11 +35,40 @@ class ObjStore(Generic[TItem]):
35
35
 
36
36
  if items:
37
37
  for it in items:
38
- self.add(it)
38
+ self.add(it, handle_out_of_order=True)
39
+
40
+ # ---- Serialization ----
41
+ def to_dict(self) -> dict[str, Any]:
42
+ """
43
+ Serialize to a dict suitable for JSON or other serialization.
44
+
45
+ Returns:
46
+ A dict with 'items' key containing list of serialized items.
47
+ """
48
+ items = [it.to_dict() for it in self._arr if it is not None]
49
+ return {"items": items}
50
+
51
+ @classmethod
52
+ def from_dict(cls, data: dict[str, Any], item_factory: Any) -> ObjStore[TItem]:
53
+ """
54
+ Deserialize from a dict.
55
+
56
+ Args:
57
+ data: A dict with 'items' key containing list of serialized items.
58
+ item_factory: A callable that takes (id, obj) and returns an Item.
59
+
60
+ Returns:
61
+ An ObjStore instance populated with the deserialized items.
62
+ """
63
+ items = []
64
+ for item_data in data.get("items", []):
65
+ item = Item.from_dict(item_data)
66
+ items.append(item_factory(item.id_, item.geom, item.obj))
67
+ return cls(items)
39
68
 
40
69
  # -------- core --------
41
70
 
42
- def add(self, item: TItem) -> None:
71
+ def add(self, item: TItem, handle_out_of_order: bool = False) -> None:
43
72
  """
44
73
  Insert or replace the mapping at item.id_. Reverse map updated so obj points to id.
45
74
  """
@@ -48,9 +77,14 @@ class ObjStore(Generic[TItem]):
48
77
 
49
78
  # ids must be dense and assigned by the caller
50
79
  if id_ > len(self._arr):
51
- raise AssertionError(
52
- "ObjStore.add received an out-of-order id, use alloc_id() to get the next available id"
53
- )
80
+ if not handle_out_of_order:
81
+ raise AssertionError(
82
+ "ObjStore.add received an out-of-order id, use alloc_id() to get the next available id"
83
+ )
84
+ # fill holes with None
85
+ while len(self._arr) < id_:
86
+ self._arr.append(None)
87
+ self._objs.append(None)
54
88
 
55
89
  if id_ == len(self._arr):
56
90
  # append
@@ -49,6 +49,18 @@ class QuadTree(_BaseQuadTree[Point, _IdCoord, PointItem]):
49
49
  track_objects=track_objects,
50
50
  )
51
51
 
52
+ @classmethod
53
+ def from_bytes(cls, data: bytes) -> QuadTree:
54
+ """
55
+ Create a QuadTree instance from serialized bytes.
56
+
57
+ Args:
58
+ data: Serialized byte data from `to_bytes()`.
59
+ Returns:
60
+ A QuadTree instance.
61
+ """
62
+ return super().from_bytes(data)
63
+
52
64
  @overload
53
65
  def query(
54
66
  self, rect: Bounds, *, as_items: Literal[False] = ...
@@ -145,5 +157,11 @@ class QuadTree(_BaseQuadTree[Point, _IdCoord, PointItem]):
145
157
  return _RustQuadTree(bounds, capacity)
146
158
  return _RustQuadTree(bounds, capacity, max_depth=max_depth)
147
159
 
148
- def _make_item(self, id_: int, geom: Point, obj: Any | None) -> PointItem:
160
+ @classmethod
161
+ def _new_native_from_bytes(cls, data: bytes) -> Any:
162
+ """Create a new native engine instance from serialized bytes."""
163
+ return _RustQuadTree.from_bytes(data)
164
+
165
+ @staticmethod
166
+ def _make_item(id_: int, geom: Point, obj: Any | None) -> PointItem:
149
167
  return PointItem(id_, geom, obj)
@@ -122,6 +122,9 @@ class Index:
122
122
  - **item**: The item to insert into the index, which will be returned by the intersection method
123
123
  - **bbox**: The spatial bounding box tuple of the item, with four members (xmin,ymin,xmax,ymax)
124
124
  """
125
+ if type(bbox) is list: # Handle list input
126
+ bbox = tuple(bbox)
127
+
125
128
  if self._free:
126
129
  rid = self._free.pop()
127
130
  self._objects[rid] = item
@@ -141,6 +144,9 @@ class Index:
141
144
 
142
145
  Both parameters need to exactly match the parameters provided to the insert method.
143
146
  """
147
+ if type(bbox) is list: # Handle list input
148
+ bbox = tuple(bbox)
149
+
144
150
  rid = self._item_to_id.pop(id(item))
145
151
  self._qt.delete(rid, bbox)
146
152
  self._objects[rid] = None
@@ -50,6 +50,18 @@ class RectQuadTree(_BaseQuadTree[Bounds, _IdRect, RectItem]):
50
50
  track_objects=track_objects,
51
51
  )
52
52
 
53
+ @classmethod
54
+ def from_bytes(cls, data: bytes) -> RectQuadTree:
55
+ """
56
+ Create a RectQuadTree instance from serialized bytes.
57
+
58
+ Args:
59
+ data: Serialized byte data from `to_bytes()`.
60
+ Returns:
61
+ A RectQuadTree instance.
62
+ """
63
+ return super().from_bytes(data)
64
+
53
65
  @overload
54
66
  def query(
55
67
  self, rect: Bounds, *, as_items: Literal[False] = ...
@@ -81,5 +93,11 @@ class RectQuadTree(_BaseQuadTree[Bounds, _IdRect, RectItem]):
81
93
  return _RustRectQuadTree(bounds, capacity)
82
94
  return _RustRectQuadTree(bounds, capacity, max_depth=max_depth)
83
95
 
84
- def _make_item(self, id_: int, geom: Bounds, obj: Any | None) -> RectItem:
96
+ @classmethod
97
+ def _new_native_from_bytes(cls, data: bytes) -> Any:
98
+ """Create a new native engine instance from serialized bytes."""
99
+ return _RustRectQuadTree.from_bytes(data)
100
+
101
+ @staticmethod
102
+ def _make_item(id_: int, geom: Bounds, obj: Any | None) -> RectItem:
85
103
  return RectItem(id_, geom, obj)
@@ -1,10 +1,12 @@
1
- #[derive(Copy, Clone, Debug, PartialEq, Default)]
1
+ use serde::{Serialize, Deserialize};
2
+
3
+ #[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
2
4
  pub struct Point {
3
5
  pub x: f32,
4
6
  pub y: f32,
5
7
  }
6
8
 
7
- #[derive(Copy, Clone, Debug, PartialEq, Default)]
9
+ #[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
8
10
  pub struct Rect {
9
11
  pub min_x: f32,
10
12
  pub min_y: f32,
@@ -8,6 +8,7 @@ pub use crate::rect_quadtree::{RectItem, RectQuadTree};
8
8
 
9
9
  use pyo3::prelude::*;
10
10
  use pyo3::types::PyList;
11
+ use pyo3::types::PyBytes;
11
12
  use pyo3::exceptions::PyValueError;
12
13
  use numpy::PyReadonlyArray2;
13
14
 
@@ -38,6 +39,23 @@ impl PyQuadTree {
38
39
  Self { inner }
39
40
  }
40
41
 
42
+ // Return Python bytes
43
+ pub fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
44
+ let buf = self
45
+ .inner
46
+ .to_bytes()
47
+ .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("serialize failed: {e}")))?;
48
+ Ok(PyBytes::new(py, &buf))
49
+ }
50
+
51
+ // Construct from Python bytes
52
+ #[staticmethod]
53
+ pub fn from_bytes(bytes: &Bound<PyBytes>) -> PyResult<Self> {
54
+ let inner = QuadTree::from_bytes(bytes.as_bytes())
55
+ .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("deserialize failed: {e}")))?;
56
+ Ok(Self { inner })
57
+ }
58
+
41
59
  pub fn insert(&mut self, id: u64, xy: (f32, f32)) -> bool {
42
60
  let (x, y) = xy;
43
61
  self.inner.insert(Item { id, point: Point { x, y } })
@@ -152,6 +170,23 @@ impl PyRectQuadTree {
152
170
  Self { inner }
153
171
  }
154
172
 
173
+ // Return Python bytes
174
+ pub fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
175
+ let buf = self
176
+ .inner
177
+ .to_bytes()
178
+ .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("serialize failed: {e}")))?;
179
+ Ok(PyBytes::new(py, &buf))
180
+ }
181
+
182
+ // Construct from Python bytes
183
+ #[staticmethod]
184
+ pub fn from_bytes(bytes: &Bound<PyBytes>) -> PyResult<Self> {
185
+ let inner = RectQuadTree::from_bytes(bytes.as_bytes())
186
+ .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("deserialize failed: {e}")))?;
187
+ Ok(Self { inner })
188
+ }
189
+
155
190
  /// Insert one AABB by id.
156
191
  pub fn insert(&mut self, id: u64, rect: (f32, f32, f32, f32)) -> bool {
157
192
  let (min_x, min_y, max_x, max_y) = rect;
@@ -1,14 +1,17 @@
1
1
  use std::collections::HashSet;
2
2
  use crate::geom::{Point, Rect, dist_sq_point_to_rect, dist_sq_points};
3
3
  use smallvec::SmallVec;
4
+ use serde::{Serialize, Deserialize};
5
+ use bincode::config::standard;
6
+ use bincode::serde::{encode_to_vec, decode_from_slice};
4
7
 
5
- #[derive(Copy, Clone, Debug, PartialEq, Default)]
8
+ #[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
6
9
  pub struct Item {
7
10
  pub id: u64,
8
11
  pub point: Point,
9
12
  }
10
13
 
11
-
14
+ #[derive(Serialize, Deserialize)]
12
15
  pub struct QuadTree {
13
16
  pub boundary: Rect,
14
17
  pub items: Vec<Item>,
@@ -55,6 +58,14 @@ impl QuadTree {
55
58
  }
56
59
  }
57
60
 
61
+ pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::error::EncodeError> {
62
+ encode_to_vec(self, standard())
63
+ }
64
+ pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::error::DecodeError> {
65
+ let (qt, _len): (Self, usize) = decode_from_slice(bytes, standard())?;
66
+ Ok(qt)
67
+ }
68
+
58
69
  pub fn new_child(boundary: Rect, capacity: usize, depth: usize, max_depth: usize) -> Self {
59
70
  QuadTree {
60
71
  boundary,
@@ -1,12 +1,16 @@
1
1
  use smallvec::SmallVec;
2
2
  use crate::geom::Rect;
3
+ use serde::{Serialize, Deserialize};
4
+ use bincode::config::standard;
5
+ use bincode::serde::{encode_to_vec, decode_from_slice};
3
6
 
4
- #[derive(Copy, Clone, Debug, PartialEq, Default)]
7
+ #[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
5
8
  pub struct RectItem {
6
9
  pub id: u64,
7
10
  pub rect: Rect,
8
11
  }
9
12
 
13
+ #[derive(Serialize, Deserialize)]
10
14
  pub struct RectQuadTree {
11
15
  pub boundary: Rect,
12
16
  pub items: Vec<RectItem>,
@@ -68,6 +72,14 @@ impl RectQuadTree {
68
72
  }
69
73
  }
70
74
 
75
+ pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::error::EncodeError> {
76
+ encode_to_vec(self, standard())
77
+ }
78
+ pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::error::DecodeError> {
79
+ let (qt, _len): (Self, usize) = decode_from_slice(bytes, standard())?;
80
+ Ok(qt)
81
+ }
82
+
71
83
  fn new_child(boundary: Rect, capacity: usize, depth: usize, max_depth: usize) -> Self {
72
84
  RectQuadTree {
73
85
  boundary,
@@ -0,0 +1,61 @@
1
+ use fastquadtree::{RectQuadTree, QuadTree, Item, RectItem, Point, Rect};
2
+
3
+ #[test]
4
+ fn quadtree_roundtrip_bytes() {
5
+ // Build a small tree
6
+ let mut qt = QuadTree::new(
7
+ Rect { min_x: 0.0, min_y: 0.0, max_x: 10.0, max_y: 10.0 },
8
+ 4,
9
+ );
10
+ for (i, (x, y)) in [(1.0, 1.0), (2.0, 3.0), (7.5, 8.5), (9.0, 0.5)].into_iter().enumerate() {
11
+ qt.insert(Item { id: i as u64 + 1, point: Point { x, y } });
12
+ }
13
+
14
+ // Serialize
15
+ let bytes = qt.to_bytes().expect("serialize quadtree");
16
+
17
+ // Deserialize
18
+ let qt2 = QuadTree::from_bytes(&bytes).expect("deserialize quadtree");
19
+
20
+ // Basic invariants
21
+ assert_eq!(qt.count_items(), qt2.count_items());
22
+
23
+ // Query equality for a region
24
+ let rect = Rect { min_x: 0.0, min_y: 0.0, max_x: 5.0, max_y: 5.0 };
25
+ let a: Vec<_> = qt.query(rect).into_iter().map(|(id, _, _)| id).collect();
26
+ let b: Vec<_> = qt2.query(rect).into_iter().map(|(id, _, _)| id).collect();
27
+ assert_eq!(a, b);
28
+
29
+ // Nearest neighbor equality
30
+ let nn1 = qt.nearest_neighbor(Point { x: 1.2, y: 1.1 }).map(|it| it.id);
31
+ let nn2 = qt2.nearest_neighbor(Point { x: 1.2, y: 1.1 }).map(|it| it.id);
32
+ assert_eq!(nn1, nn2);
33
+ }
34
+
35
+
36
+ #[test]
37
+ fn rectquadtree_roundtrip_bytes() {
38
+ // Build a small tree
39
+ let mut qt = RectQuadTree::new(
40
+ Rect { min_x: 0.0, min_y: 0.0, max_x: 10.0, max_y: 10.0 },
41
+ 4,
42
+ );
43
+ for (i, (x, y)) in [(1.0, 1.0), (2.0, 3.0), (7.5, 8.5), (9.0, 0.5)].into_iter().enumerate() {
44
+ qt.insert(RectItem { id: i as u64 + 1, rect: Rect { min_x: x, min_y: y, max_x: x + 1.0, max_y: y + 1.0 } });
45
+ }
46
+
47
+ // Serialize
48
+ let bytes = qt.to_bytes().expect("serialize quadtree");
49
+
50
+ // Deserialize
51
+ let qt2 = RectQuadTree::from_bytes(&bytes).expect("deserialize quadtree");
52
+
53
+ // Basic invariants
54
+ assert_eq!(qt.count_items(), qt2.count_items());
55
+
56
+ // Query equality for a region
57
+ let rect = Rect { min_x: 0.0, min_y: 0.0, max_x: 5.0, max_y: 5.0 };
58
+ let a: Vec<_> = qt.query(rect).into_iter().map(|it| it.0).collect();
59
+ let b: Vec<_> = qt2.query(rect).into_iter().map(|it| it.0).collect();
60
+ assert_eq!(a, b);
61
+ }
@@ -362,3 +362,42 @@ def test_query_list_and_tuple_equivalence():
362
362
  results_from_list = idx.intersect(query_list)
363
363
 
364
364
  assert results_from_tuple == results_from_list == [obj1]
365
+
366
+
367
+ def test_remove_list_and_tuple_equivalence():
368
+ """Test that both list and tuple inputs for bbox work the same in remove."""
369
+ idx = FQTIndex(bbox=WORLD)
370
+
371
+ obj1, box1 = "obj1", (10.0, 10.0, 20.0, 20.0)
372
+ obj2, box2 = "obj2", (30.0, 30.0, 40.0, 40.0)
373
+
374
+ idx.insert(obj1, box1)
375
+ idx.insert(obj2, box2)
376
+
377
+ # Remove using tuple
378
+ idx.remove(obj1, box1)
379
+
380
+ # Remove using list
381
+ box2_list = [30.0, 30.0, 40.0, 40.0]
382
+ idx.remove(obj2, box2_list)
383
+
384
+ # Both objects should be removed
385
+ assert idx.intersect((0.0, 0.0, 100.0, 100.0)) == []
386
+
387
+
388
+ def test_insert_list_and_tuple_equivalence():
389
+ """Test that both list and tuple inputs for bbox work the same in insert."""
390
+ idx = FQTIndex(bbox=WORLD)
391
+
392
+ obj1, box1 = "obj1", (10.0, 10.0, 20.0, 20.0)
393
+ obj2, box2 = "obj2", [30.0, 30.0, 40.0, 40.0] # box2 as list
394
+
395
+ # Insert using tuple
396
+ idx.insert(obj1, box1)
397
+
398
+ # Insert using list
399
+ idx.insert(obj2, box2)
400
+
401
+ # Both objects should be present
402
+ results = idx.intersect((0.0, 0.0, 100.0, 100.0))
403
+ assert set(results) == {obj1, obj2}
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env python3
2
+ """Test unconventional quadtree bounding boxes including negative regions."""
3
+
4
+ from fastquadtree import QuadTree, RectQuadTree
5
+
6
+
7
+ def test_serialization_point_quadtree():
8
+ """Test serialization and deserialization of PointQuadTree."""
9
+ qt = QuadTree((0, 0, 100, 100), capacity=4)
10
+ points = [(10, 10), (20, 20), (30, 30), (40, 40)]
11
+ for pt in points:
12
+ qt.insert(pt)
13
+
14
+ data = qt.to_bytes()
15
+ qt2 = QuadTree.from_bytes(data)
16
+
17
+ assert qt.count_items() == qt2.count_items()
18
+ for rect in [(0, 0, 50, 50), (15, 15, 35, 35)]:
19
+ res1 = qt.query(rect)
20
+ res2 = qt2.query(rect)
21
+ assert sorted(res1) == sorted(res2)
22
+
23
+
24
+ def test_serialization_rect_quadtree():
25
+ """Test serialization and deserialization of RectQuadTree."""
26
+ rqt = RectQuadTree((0, 0, 100, 100), capacity=4)
27
+ rects = [(5, 5, 15, 15), (20, 20, 30, 30), (35, 35, 45, 45), (50, 50, 60, 60)]
28
+ for rect in rects:
29
+ rqt.insert(rect)
30
+
31
+ data = rqt.to_bytes()
32
+ rqt2 = RectQuadTree.from_bytes(data)
33
+
34
+ assert rqt.count_items() == rqt2.count_items()
35
+ for query_rect in [(0, 0, 25, 25), (30, 30, 55, 55)]:
36
+ res1 = rqt.query(query_rect)
37
+ res2 = rqt2.query(query_rect)
38
+ assert sorted(res1) == sorted(res2)
39
+
40
+
41
+ def test_serialization_with_objects_point():
42
+ """Test serialization of quadtree with associated objects."""
43
+ qt = QuadTree((0, 0, 100, 100), capacity=4, track_objects=True)
44
+ items = [((10, 10), "A"), ((20, 20), "B"), ((30, 30), "C")]
45
+ for pt, obj in items:
46
+ qt.insert(pt, obj=obj)
47
+
48
+ data = qt.to_bytes()
49
+ qt2 = QuadTree.from_bytes(data)
50
+
51
+ assert qt.count_items() == qt2.count_items()
52
+
53
+ # Check that the objects in the object store are preserved
54
+ all_qt2_items = [item.to_dict() for item in qt2.get_all_items()]
55
+ all_qt_items = [item.to_dict() for item in qt.get_all_items()]
56
+
57
+ for item in all_qt_items:
58
+ assert item in all_qt2_items
59
+
60
+ assert type(qt2.get_all_items()[0]) is type(qt.get_all_items()[0])
61
+ assert type(qt2.get_all_items()[0].obj) is type(qt.get_all_items()[0].obj)
62
+
63
+
64
+ def test_serialization_with_objects_rect():
65
+ """Test serialization of rect quadtree with associated objects."""
66
+ rqt = RectQuadTree((0, 0, 100, 100), capacity=4, track_objects=True)
67
+ items = [
68
+ ((5, 5, 15, 15), "RectA"),
69
+ ((20, 20, 30, 30), "RectB"),
70
+ ((35, 35, 45, 45), "RectC"),
71
+ ]
72
+ for rect, obj in items:
73
+ rqt.insert(rect, obj=obj)
74
+
75
+ data = rqt.to_bytes()
76
+ rqt2 = RectQuadTree.from_bytes(data)
77
+
78
+ assert rqt.count_items() == rqt2.count_items()
79
+
80
+ # Check that the objects in the object store are preserved
81
+ all_rqt2_items = [item.to_dict() for item in rqt2.get_all_items()]
82
+ all_rqt_items = [item.to_dict() for item in rqt.get_all_items()]
83
+
84
+ for item in all_rqt_items:
85
+ assert item in all_rqt2_items
86
+
87
+ assert type(rqt2.get_all_items()[0]) is type(rqt.get_all_items()[0])
88
+ assert type(rqt2.get_all_items()[0].obj) is type(rqt.get_all_items()[0].obj)
89
+
90
+
91
+ def test_serialization_perserves_ids():
92
+ """Test that serialization and deserialization preserves item ids."""
93
+ qt = QuadTree((0, 0, 100, 100), capacity=4, track_objects=True)
94
+ items = [((10, 10), "A"), ((20, 20), "B"), ((30, 30), "C")]
95
+ for pt, obj in items:
96
+ qt.insert(pt, obj=obj)
97
+
98
+ original_ids = [item.id_ for item in qt.get_all_items()]
99
+
100
+ data = qt.to_bytes()
101
+ qt2 = QuadTree.from_bytes(data)
102
+
103
+ deserialized_ids = [item.id_ for item in qt2.get_all_items()]
104
+
105
+ assert sorted(original_ids) == sorted(deserialized_ids)
106
+
107
+ # Delete id 1
108
+ qt.delete(1, (20, 20))
109
+ ids_after_delete = [item.id_ for item in qt.get_all_items()]
110
+
111
+ qt3 = QuadTree.from_bytes(qt.to_bytes())
112
+ ids_after_delete_deserialized = [item.id_ for item in qt3.get_all_items()]
113
+ assert sorted(ids_after_delete) == [0, 2]
114
+ assert sorted(ids_after_delete_deserialized) == [0, 2]
File without changes
File without changes