fastquadtree 1.1.2__cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl → 1.2.1__cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl

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.

@@ -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
 
fastquadtree/_item.py CHANGED
@@ -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
  """
Binary file
@@ -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] = ...
@@ -68,6 +80,13 @@ class QuadTree(_BaseQuadTree[Point, _IdCoord, PointItem]):
68
80
  Returns:
69
81
  If as_items is False: list of (id, x, y) tuples.
70
82
  If as_items is True: list of Item objects.
83
+
84
+ Example:
85
+ ```python
86
+ results = qt.query((10.0, 10.0, 20.0, 20.0), as_items=True)
87
+ for item in results:
88
+ print(f"Found point id={item.id_} at {item.geom} with obj={item.obj}")
89
+ ```
71
90
  """
72
91
  if not as_items:
73
92
  return self._native.query(rect)
@@ -145,5 +164,11 @@ class QuadTree(_BaseQuadTree[Point, _IdCoord, PointItem]):
145
164
  return _RustQuadTree(bounds, capacity)
146
165
  return _RustQuadTree(bounds, capacity, max_depth=max_depth)
147
166
 
148
- def _make_item(self, id_: int, geom: Point, obj: Any | None) -> PointItem:
167
+ @classmethod
168
+ def _new_native_from_bytes(cls, data: bytes) -> Any:
169
+ """Create a new native engine instance from serialized bytes."""
170
+ return _RustQuadTree.from_bytes(data)
171
+
172
+ @staticmethod
173
+ def _make_item(id_: int, geom: Point, obj: Any | None) -> PointItem:
149
174
  return PointItem(id_, geom, obj)
@@ -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] = ...
@@ -69,6 +81,13 @@ class RectQuadTree(_BaseQuadTree[Bounds, _IdRect, RectItem]):
69
81
  Returns:
70
82
  If as_items is False: list of (id, x0, y0, x1, y1) tuples.
71
83
  If as_items is True: list of Item objects.
84
+
85
+ Example:
86
+ ```python
87
+ results = rqt.query((10.0, 10.0, 20.0, 20.0), as_items=True)
88
+ for item in results:
89
+ print(f"Found rect id={item.id_} at {item.geom} with obj={item.obj}")
90
+ ```
72
91
  """
73
92
  if not as_items:
74
93
  return self._native.query(rect)
@@ -81,5 +100,11 @@ class RectQuadTree(_BaseQuadTree[Bounds, _IdRect, RectItem]):
81
100
  return _RustRectQuadTree(bounds, capacity)
82
101
  return _RustRectQuadTree(bounds, capacity, max_depth=max_depth)
83
102
 
84
- def _make_item(self, id_: int, geom: Bounds, obj: Any | None) -> RectItem:
103
+ @classmethod
104
+ def _new_native_from_bytes(cls, data: bytes) -> Any:
105
+ """Create a new native engine instance from serialized bytes."""
106
+ return _RustRectQuadTree.from_bytes(data)
107
+
108
+ @staticmethod
109
+ def _make_item(id_: int, geom: Bounds, obj: Any | None) -> RectItem:
85
110
  return RectItem(id_, geom, obj)
@@ -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
 
@@ -0,0 +1,13 @@
1
+ fastquadtree-1.2.1.dist-info/METADATA,sha256=IYzvJNOV07j4qtOFQqryt15YjrnuWrSjJpK0NUDe-1E,9614
2
+ fastquadtree-1.2.1.dist-info/WHEEL,sha256=xBqaWU3Z-cfW-EFy0ENuEqxgXZXkIHUBkA1cT6FlqGM,127
3
+ fastquadtree-1.2.1.dist-info/licenses/LICENSE,sha256=pRuvcuqIMtEUBMgvP1Bc4fOHydzeuA61c6DQoQ1pb1w,1071
4
+ fastquadtree/__init__.py,sha256=rtkveNz7rScRasTRGu1yEqzeoJfLfreJNxg21orPL-U,195
5
+ fastquadtree/_base_quadtree.py,sha256=NhUkMulVa2ItCtx5rEnE_asHY3aGN0XxG6m-AYJk7CE,15091
6
+ fastquadtree/_item.py,sha256=EDS3nJHdVtjDsuTqTZKGTZH8iWJIQ-TKxLXqvMScNPA,2405
7
+ fastquadtree/_native.abi3.so,sha256=MkRrcW6ZJwhRQzl7OXguY7nk2T_CsycgLC22C2G-p0I,556604
8
+ fastquadtree/_obj_store.py,sha256=HeYFGUPYhvxBzL7Js0g0jsIxflpZS6RsXNk50rGeNlA,6522
9
+ fastquadtree/point_quadtree.py,sha256=BM6mxw2pSmAKkOgXnosREfC6WOXCgEYNeQDlvnlEbKQ,5945
10
+ fastquadtree/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ fastquadtree/pyqtree.py,sha256=2Khh1gCPalD4z0gb3EmqtzMoga08E9BkB0j5bwkiRPU,6076
12
+ fastquadtree/rect_quadtree.py,sha256=82IygiEmJ4JdA6LBDXq288cs9VfS3LpdJdkjCPcYCKM,3807
13
+ fastquadtree-1.2.1.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- fastquadtree-1.1.2.dist-info/METADATA,sha256=Xuiwr9cpXZhCSt6P3e_kr6t4YWHD0PTszufwxLgmq74,9501
2
- fastquadtree-1.1.2.dist-info/WHEEL,sha256=xBqaWU3Z-cfW-EFy0ENuEqxgXZXkIHUBkA1cT6FlqGM,127
3
- fastquadtree-1.1.2.dist-info/licenses/LICENSE,sha256=pRuvcuqIMtEUBMgvP1Bc4fOHydzeuA61c6DQoQ1pb1w,1071
4
- fastquadtree/__init__.py,sha256=rtkveNz7rScRasTRGu1yEqzeoJfLfreJNxg21orPL-U,195
5
- fastquadtree/_base_quadtree.py,sha256=o5T0xqDM9i_JjpYF4U7VmLM0qUHkw0wr0bbRqb-CGt4,9901
6
- fastquadtree/_item.py,sha256=LoTDr7zlsZyUrrKK6Ketzl5fxTcFF8YbcxEXbQ65st4,1679
7
- fastquadtree/_native.abi3.so,sha256=Z5UGIeTn4Un5PwQm4v5dPnip3BWlQeS2JljdY3s7YS0,499724
8
- fastquadtree/_obj_store.py,sha256=vmhZGdzEoTQHvRbFjTne_0X2Z1l48SXyB6I9SAjjbiM,5267
9
- fastquadtree/point_quadtree.py,sha256=Pz8ZS7N3kYSYJJYGa3ghKzy7d3JCA1dbi9nfEwwpF_k,5178
10
- fastquadtree/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- fastquadtree/pyqtree.py,sha256=2Khh1gCPalD4z0gb3EmqtzMoga08E9BkB0j5bwkiRPU,6076
12
- fastquadtree/rect_quadtree.py,sha256=7F-JceCHn5RLhztxSYCIJEZ_e2TV-NeobobbrdauJQA,3024
13
- fastquadtree-1.1.2.dist-info/RECORD,,