fastquadtree 0.4.2__cp38-abi3-win_amd64.whl → 0.5.1__cp38-abi3-win_amd64.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.
- fastquadtree/__init__.py +42 -45
- fastquadtree/__init__.pyi +25 -21
- fastquadtree/_bimap.py +12 -11
- fastquadtree/_item.py +5 -4
- fastquadtree/_native.pyd +0 -0
- {fastquadtree-0.4.2.dist-info → fastquadtree-0.5.1.dist-info}/METADATA +38 -48
- fastquadtree-0.5.1.dist-info/RECORD +10 -0
- fastquadtree-0.4.2.dist-info/RECORD +0 -10
- {fastquadtree-0.4.2.dist-info → fastquadtree-0.5.1.dist-info}/WHEEL +0 -0
- {fastquadtree-0.4.2.dist-info → fastquadtree-0.5.1.dist-info}/licenses/LICENSE +0 -0
fastquadtree/__init__.py
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from typing import Any, Iterable,
|
4
|
-
from typing import Literal
|
3
|
+
from typing import Any, Iterable, Literal, Tuple, overload
|
5
4
|
|
6
|
-
# Compiled Rust module is provided by maturin (tool.maturin.module-name)
|
7
|
-
from ._native import QuadTree as _RustQuadTree
|
8
5
|
from ._bimap import BiMap # type: ignore[attr-defined]
|
9
6
|
from ._item import Item
|
10
7
|
|
8
|
+
# Compiled Rust module is provided by maturin (tool.maturin.module-name)
|
9
|
+
from ._native import QuadTree as _RustQuadTree
|
10
|
+
|
11
11
|
Bounds = Tuple[float, float, float, float]
|
12
12
|
"""Axis-aligned rectangle as (min_x, min_y, max_x, max_y)."""
|
13
13
|
|
@@ -45,14 +45,14 @@ class QuadTree:
|
|
45
45
|
ValueError: If parameters are invalid or inserts are out of bounds.
|
46
46
|
"""
|
47
47
|
|
48
|
-
__slots__ = ("
|
48
|
+
__slots__ = ("_bounds", "_count", "_items", "_native", "_next_id")
|
49
49
|
|
50
50
|
def __init__(
|
51
51
|
self,
|
52
52
|
bounds: Bounds,
|
53
53
|
capacity: int,
|
54
54
|
*,
|
55
|
-
max_depth:
|
55
|
+
max_depth: int | None = None,
|
56
56
|
track_objects: bool = False,
|
57
57
|
start_id: int = 1,
|
58
58
|
):
|
@@ -60,14 +60,14 @@ class QuadTree:
|
|
60
60
|
self._native = _RustQuadTree(bounds, capacity)
|
61
61
|
else:
|
62
62
|
self._native = _RustQuadTree(bounds, capacity, max_depth=max_depth)
|
63
|
-
self._items:
|
63
|
+
self._items: BiMap | None = BiMap() if track_objects else None
|
64
64
|
self._next_id: int = int(start_id)
|
65
65
|
self._count: int = 0
|
66
66
|
self._bounds = bounds
|
67
67
|
|
68
68
|
# ---------- inserts ----------
|
69
69
|
|
70
|
-
def insert(self, xy: Point, *,
|
70
|
+
def insert(self, xy: Point, *, id_: int | None = None, obj: Any = None) -> int:
|
71
71
|
"""
|
72
72
|
Insert a single point.
|
73
73
|
|
@@ -83,15 +83,14 @@ class QuadTree:
|
|
83
83
|
Raises:
|
84
84
|
ValueError: If the point is outside tree bounds.
|
85
85
|
"""
|
86
|
-
if
|
87
|
-
|
86
|
+
if id_ is None:
|
87
|
+
id_ = self._next_id
|
88
88
|
self._next_id += 1
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
self._next_id = id + 1
|
89
|
+
# ensure future auto-ids do not collide
|
90
|
+
elif id_ >= self._next_id:
|
91
|
+
self._next_id = id_ + 1
|
93
92
|
|
94
|
-
if not self._native.insert(
|
93
|
+
if not self._native.insert(id_, xy):
|
95
94
|
x, y = xy
|
96
95
|
bx0, by0, bx1, by1 = self._bounds
|
97
96
|
raise ValueError(
|
@@ -99,10 +98,10 @@ class QuadTree:
|
|
99
98
|
)
|
100
99
|
|
101
100
|
if self._items is not None:
|
102
|
-
self._items.add(Item(
|
101
|
+
self._items.add(Item(id_, xy[0], xy[1], obj))
|
103
102
|
|
104
103
|
self._count += 1
|
105
|
-
return
|
104
|
+
return id_
|
106
105
|
|
107
106
|
def insert_many_points(self, points: Iterable[Point]) -> int:
|
108
107
|
"""
|
@@ -136,39 +135,39 @@ class QuadTree:
|
|
136
135
|
self._count += inserted
|
137
136
|
return inserted
|
138
137
|
|
139
|
-
def attach(self,
|
138
|
+
def attach(self, id_: int, obj: Any) -> None:
|
140
139
|
"""
|
141
140
|
Attach or replace the Python object for an existing id.
|
142
141
|
Tracking must be enabled.
|
143
142
|
|
144
143
|
Args:
|
145
|
-
|
144
|
+
id_: Target id.
|
146
145
|
obj: Object to associate with id.
|
147
146
|
"""
|
148
147
|
if self._items is None:
|
149
148
|
raise ValueError("Cannot attach objects when track_objects=False")
|
150
149
|
|
151
|
-
item = self._items.by_id(
|
150
|
+
item = self._items.by_id(id_)
|
152
151
|
if item is None:
|
153
|
-
raise KeyError(f"Id {
|
154
|
-
self._items.add(Item(
|
152
|
+
raise KeyError(f"Id {id_} not found in quadtree")
|
153
|
+
self._items.add(Item(id_, item.x, item.y, obj))
|
155
154
|
|
156
|
-
def delete(self,
|
155
|
+
def delete(self, id_: int, xy: Point) -> bool:
|
157
156
|
"""
|
158
157
|
Delete an item by id and exact coordinates.
|
159
158
|
|
160
159
|
Args:
|
161
|
-
|
160
|
+
id_: Integer id to remove.
|
162
161
|
xy: Coordinates (x, y) of the item.
|
163
162
|
|
164
163
|
Returns:
|
165
164
|
True if the item was found and deleted, else False.
|
166
165
|
"""
|
167
|
-
deleted = self._native.delete(
|
166
|
+
deleted = self._native.delete(id_, xy)
|
168
167
|
if deleted:
|
169
168
|
self._count -= 1
|
170
169
|
if self._items is not None:
|
171
|
-
self._items.pop_id(
|
170
|
+
self._items.pop_id(id_) # ignore result
|
172
171
|
return deleted
|
173
172
|
|
174
173
|
def delete_by_object(self, obj: Any) -> bool:
|
@@ -196,21 +195,21 @@ class QuadTree:
|
|
196
195
|
if item is None:
|
197
196
|
return False
|
198
197
|
|
199
|
-
return self.delete(item.
|
198
|
+
return self.delete(item.id_, (item.x, item.y))
|
200
199
|
|
201
200
|
# ---------- queries ----------
|
202
201
|
|
203
202
|
@overload
|
204
203
|
def query(
|
205
204
|
self, rect: Bounds, *, as_items: Literal[False] = ...
|
206
|
-
) ->
|
205
|
+
) -> list[_IdCoord]: ...
|
207
206
|
|
208
207
|
@overload
|
209
|
-
def query(self, rect: Bounds, *, as_items: Literal[True]) ->
|
208
|
+
def query(self, rect: Bounds, *, as_items: Literal[True]) -> list[Item]: ...
|
210
209
|
|
211
210
|
def query(
|
212
211
|
self, rect: Bounds, *, as_items: bool = False
|
213
|
-
) ->
|
212
|
+
) -> list[_IdCoord] | list[Item]:
|
214
213
|
"""
|
215
214
|
Return all points inside an axis-aligned rectangle.
|
216
215
|
|
@@ -228,8 +227,8 @@ class QuadTree:
|
|
228
227
|
|
229
228
|
if self._items is None:
|
230
229
|
raise ValueError("Cannot return results as items with track_objects=False")
|
231
|
-
out:
|
232
|
-
for id_,
|
230
|
+
out: list[Item] = []
|
231
|
+
for id_, _, _ in raw:
|
233
232
|
item = self._items.by_id(id_)
|
234
233
|
if item is None:
|
235
234
|
raise RuntimeError(
|
@@ -242,12 +241,10 @@ class QuadTree:
|
|
242
241
|
@overload
|
243
242
|
def nearest_neighbor(
|
244
243
|
self, xy: Point, *, as_item: Literal[False] = ...
|
245
|
-
) ->
|
244
|
+
) -> _IdCoord | None: ...
|
246
245
|
|
247
246
|
@overload
|
248
|
-
def nearest_neighbor(
|
249
|
-
self, xy: Point, *, as_item: Literal[True]
|
250
|
-
) -> Optional[Item]: ...
|
247
|
+
def nearest_neighbor(self, xy: Point, *, as_item: Literal[True]) -> Item | None: ...
|
251
248
|
|
252
249
|
def nearest_neighbor(self, xy: Point, *, as_item: bool = False):
|
253
250
|
"""
|
@@ -278,12 +275,12 @@ class QuadTree:
|
|
278
275
|
@overload
|
279
276
|
def nearest_neighbors(
|
280
277
|
self, xy: Point, k: int, *, as_items: Literal[False] = ...
|
281
|
-
) ->
|
278
|
+
) -> list[_IdCoord]: ...
|
282
279
|
|
283
280
|
@overload
|
284
281
|
def nearest_neighbors(
|
285
282
|
self, xy: Point, k: int, *, as_items: Literal[True]
|
286
|
-
) ->
|
283
|
+
) -> list[Item]: ...
|
287
284
|
|
288
285
|
def nearest_neighbors(self, xy: Point, k: int, *, as_items: bool = False):
|
289
286
|
"""
|
@@ -303,7 +300,7 @@ class QuadTree:
|
|
303
300
|
if self._items is None:
|
304
301
|
raise ValueError("Cannot return results as items with track_objects=False")
|
305
302
|
|
306
|
-
out:
|
303
|
+
out: list[Item] = []
|
307
304
|
for id_, _, _ in raw:
|
308
305
|
item = self._items.by_id(id_)
|
309
306
|
if item is None:
|
@@ -316,7 +313,7 @@ class QuadTree:
|
|
316
313
|
|
317
314
|
# ---------- misc ----------
|
318
315
|
|
319
|
-
def get(self,
|
316
|
+
def get(self, id_: int) -> Any | None:
|
320
317
|
"""
|
321
318
|
Return the object associated with id.
|
322
319
|
|
@@ -325,12 +322,12 @@ class QuadTree:
|
|
325
322
|
"""
|
326
323
|
if self._items is None:
|
327
324
|
raise ValueError("Cannot get objects when track_objects=False")
|
328
|
-
item = self._items.by_id(
|
325
|
+
item = self._items.by_id(id_)
|
329
326
|
if item is None:
|
330
327
|
return None
|
331
328
|
return item.obj
|
332
329
|
|
333
|
-
def get_all_rectangles(self) ->
|
330
|
+
def get_all_rectangles(self) -> list[Bounds]:
|
334
331
|
"""
|
335
332
|
Return all node rectangles in the current quadtree.
|
336
333
|
|
@@ -339,7 +336,7 @@ class QuadTree:
|
|
339
336
|
"""
|
340
337
|
return self._native.get_all_rectangles()
|
341
338
|
|
342
|
-
def get_all_objects(self) ->
|
339
|
+
def get_all_objects(self) -> list[Any]:
|
343
340
|
"""
|
344
341
|
Return all tracked objects.
|
345
342
|
|
@@ -350,7 +347,7 @@ class QuadTree:
|
|
350
347
|
raise ValueError("Cannot get objects when track_objects=False")
|
351
348
|
return [t.obj for t in self._items.items() if t.obj is not None]
|
352
349
|
|
353
|
-
def get_all_items(self) ->
|
350
|
+
def get_all_items(self) -> list[Item]:
|
354
351
|
"""
|
355
352
|
Return all tracked items.
|
356
353
|
|
@@ -385,4 +382,4 @@ class QuadTree:
|
|
385
382
|
NativeQuadTree = _RustQuadTree
|
386
383
|
|
387
384
|
|
388
|
-
__all__ = ["
|
385
|
+
__all__ = ["Bounds", "Item", "Point", "QuadTree"]
|
fastquadtree/__init__.pyi
CHANGED
@@ -1,67 +1,71 @@
|
|
1
|
-
from typing import
|
2
|
-
|
1
|
+
from typing import (
|
2
|
+
Any,
|
3
|
+
Iterable,
|
4
|
+
Literal as _Literal, # avoid polluting public namespace
|
5
|
+
overload,
|
6
|
+
)
|
3
7
|
|
4
|
-
Bounds =
|
5
|
-
Point =
|
8
|
+
Bounds = tuple[float, float, float, float]
|
9
|
+
Point = tuple[float, float]
|
6
10
|
|
7
11
|
class Item:
|
8
|
-
|
12
|
+
id_: int
|
9
13
|
x: float
|
10
14
|
y: float
|
11
15
|
obj: Any | None
|
12
16
|
|
13
17
|
class QuadTree:
|
14
18
|
# Expose the raw native class for power users
|
15
|
-
NativeQuadTree:
|
19
|
+
NativeQuadTree: type
|
16
20
|
|
17
21
|
def __init__(
|
18
22
|
self,
|
19
23
|
bounds: Bounds,
|
20
24
|
capacity: int,
|
21
25
|
*,
|
22
|
-
max_depth:
|
26
|
+
max_depth: int | None = None,
|
23
27
|
track_objects: bool = False,
|
24
28
|
start_id: int = 1,
|
25
29
|
) -> None: ...
|
26
30
|
|
27
31
|
# Inserts
|
28
|
-
def insert(self, xy: Point, *,
|
32
|
+
def insert(self, xy: Point, *, id_: int | None = ..., obj: Any = ...) -> int: ...
|
29
33
|
def insert_many_points(self, points: Iterable[Point]) -> int: ...
|
30
|
-
def insert_many(self, items: Iterable[
|
31
|
-
def attach(self,
|
34
|
+
def insert_many(self, items: Iterable[tuple[Point, Any]]) -> int: ...
|
35
|
+
def attach(self, id_: int, obj: Any) -> None: ...
|
32
36
|
|
33
37
|
# Deletions
|
34
|
-
def delete(self,
|
38
|
+
def delete(self, id_: int, xy: Point) -> bool: ...
|
35
39
|
def delete_by_object(self, obj: Any) -> bool: ...
|
36
40
|
|
37
41
|
# Queries
|
38
42
|
@overload
|
39
43
|
def query(
|
40
44
|
self, rect: Bounds, *, as_items: _Literal[False] = ...
|
41
|
-
) ->
|
45
|
+
) -> list[tuple[int, float, float]]: ...
|
42
46
|
@overload
|
43
|
-
def query(self, rect: Bounds, *, as_items: _Literal[True]) ->
|
47
|
+
def query(self, rect: Bounds, *, as_items: _Literal[True]) -> list[Item]: ...
|
44
48
|
@overload
|
45
49
|
def nearest_neighbor(
|
46
50
|
self, xy: Point, *, as_item: _Literal[False] = ...
|
47
|
-
) ->
|
51
|
+
) -> tuple[int, float, float] | None: ...
|
48
52
|
@overload
|
49
53
|
def nearest_neighbor(
|
50
54
|
self, xy: Point, *, as_item: _Literal[True]
|
51
|
-
) ->
|
55
|
+
) -> Item | None: ...
|
52
56
|
@overload
|
53
57
|
def nearest_neighbors(
|
54
58
|
self, xy: Point, k: int, *, as_items: _Literal[False] = ...
|
55
|
-
) ->
|
59
|
+
) -> list[tuple[int, float, float]]: ...
|
56
60
|
@overload
|
57
61
|
def nearest_neighbors(
|
58
62
|
self, xy: Point, k: int, *, as_items: _Literal[True]
|
59
|
-
) ->
|
63
|
+
) -> list[Item]: ...
|
60
64
|
|
61
65
|
# Misc
|
62
|
-
def get(self,
|
63
|
-
def get_all_rectangles(self) ->
|
64
|
-
def get_all_objects(self) ->
|
65
|
-
def get_all_items(self) ->
|
66
|
+
def get(self, id_: int) -> Any | None: ...
|
67
|
+
def get_all_rectangles(self) -> list[Bounds]: ...
|
68
|
+
def get_all_objects(self) -> list[Any]: ...
|
69
|
+
def get_all_items(self) -> list[Item]: ...
|
66
70
|
def count_items(self) -> int: ...
|
67
71
|
def __len__(self) -> int: ...
|
fastquadtree/_bimap.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# _bimap.py
|
2
2
|
from __future__ import annotations
|
3
|
-
|
3
|
+
|
4
|
+
from typing import Any, Iterable, Iterator
|
4
5
|
|
5
6
|
from ._item import Item
|
6
7
|
|
@@ -33,7 +34,7 @@ class BiMap:
|
|
33
34
|
Insert or replace mapping for this Item.
|
34
35
|
Handles conflicts so that both id and obj point to this exact Item.
|
35
36
|
"""
|
36
|
-
id_ = item.
|
37
|
+
id_ = item.id_
|
37
38
|
obj = item.obj
|
38
39
|
|
39
40
|
# Unlink any old item currently bound to this id
|
@@ -47,20 +48,20 @@ class BiMap:
|
|
47
48
|
if obj is not None:
|
48
49
|
prev = self._objid_to_item.get(id(obj))
|
49
50
|
if prev is not None and prev is not item:
|
50
|
-
self._id_to_item.pop(prev.
|
51
|
+
self._id_to_item.pop(prev.id_, None)
|
51
52
|
|
52
53
|
# Link new
|
53
54
|
self._id_to_item[id_] = item
|
54
55
|
if obj is not None:
|
55
56
|
self._objid_to_item[id(obj)] = item
|
56
57
|
|
57
|
-
def by_id(self, id_: int) ->
|
58
|
+
def by_id(self, id_: int) -> Item | None:
|
58
59
|
return self._id_to_item.get(id_)
|
59
60
|
|
60
|
-
def by_obj(self, obj: Any) ->
|
61
|
+
def by_obj(self, obj: Any) -> Item | None:
|
61
62
|
return self._objid_to_item.get(id(obj))
|
62
63
|
|
63
|
-
def pop_id(self, id_: int) ->
|
64
|
+
def pop_id(self, id_: int) -> Item | None:
|
64
65
|
it = self._id_to_item.pop(id_, None)
|
65
66
|
if it is not None:
|
66
67
|
obj = it.obj
|
@@ -68,19 +69,19 @@ class BiMap:
|
|
68
69
|
self._objid_to_item.pop(id(obj), None)
|
69
70
|
return it
|
70
71
|
|
71
|
-
def pop_obj(self, obj: Any) ->
|
72
|
+
def pop_obj(self, obj: Any) -> Item | None:
|
72
73
|
it = self._objid_to_item.pop(id(obj), None)
|
73
74
|
if it is not None:
|
74
|
-
self._id_to_item.pop(it.
|
75
|
+
self._id_to_item.pop(it.id_, None)
|
75
76
|
return it
|
76
77
|
|
77
|
-
def pop_item(self, item: Item) ->
|
78
|
+
def pop_item(self, item: Item) -> Item | None:
|
78
79
|
"""
|
79
80
|
Remove this exact Item if present on either side.
|
80
81
|
"""
|
81
82
|
removed = None
|
82
83
|
# Remove by id first
|
83
|
-
removed = self._id_to_item.pop(item.
|
84
|
+
removed = self._id_to_item.pop(item.id_)
|
84
85
|
|
85
86
|
# Remove by obj side
|
86
87
|
obj = item.obj
|
@@ -104,7 +105,7 @@ class BiMap:
|
|
104
105
|
def contains_obj(self, obj: Any) -> bool:
|
105
106
|
return id(obj) in self._objid_to_item
|
106
107
|
|
107
|
-
def items_by_id(self) -> Iterator[
|
108
|
+
def items_by_id(self) -> Iterator[tuple[int, Item]]:
|
108
109
|
return iter(self._id_to_item.items())
|
109
110
|
|
110
111
|
def items(self) -> Iterator[Item]:
|
fastquadtree/_item.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# item.py
|
2
2
|
from __future__ import annotations
|
3
|
+
|
3
4
|
from typing import Any
|
4
5
|
|
5
6
|
|
@@ -8,16 +9,16 @@ class Item:
|
|
8
9
|
Lightweight view of an index entry.
|
9
10
|
|
10
11
|
Attributes:
|
11
|
-
|
12
|
+
id_: Integer identifier.
|
12
13
|
x: X coordinate.
|
13
14
|
y: Y coordinate.
|
14
15
|
obj: The attached Python object if available, else None.
|
15
16
|
"""
|
16
17
|
|
17
|
-
__slots__ = ("
|
18
|
+
__slots__ = ("id_", "obj", "x", "y")
|
18
19
|
|
19
|
-
def __init__(self,
|
20
|
-
self.
|
20
|
+
def __init__(self, id_: int, x: float, y: float, obj: Any | None = None):
|
21
|
+
self.id_ = id_
|
21
22
|
self.x = x
|
22
23
|
self.y = y
|
23
24
|
self.obj = obj
|
fastquadtree/_native.pyd
CHANGED
Binary file
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: fastquadtree
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.5.1
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
5
5
|
Classifier: Programming Language :: Python :: 3 :: Only
|
6
6
|
Classifier: Programming Language :: Rust
|
@@ -11,6 +11,13 @@ Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
11
11
|
Classifier: Topic :: Software Development :: Libraries
|
12
12
|
Classifier: Typing :: Typed
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
14
|
+
Requires-Dist: ruff>=0.6.0 ; extra == 'dev'
|
15
|
+
Requires-Dist: pytest>=7.0 ; extra == 'dev'
|
16
|
+
Requires-Dist: pytest-cov>=4.1 ; extra == 'dev'
|
17
|
+
Requires-Dist: coverage>=7.5 ; extra == 'dev'
|
18
|
+
Requires-Dist: mypy>=1.10 ; extra == 'dev'
|
19
|
+
Requires-Dist: build>=1.2.1 ; extra == 'dev'
|
20
|
+
Provides-Extra: dev
|
14
21
|
License-File: LICENSE
|
15
22
|
Summary: Rust-accelerated quadtree for Python with fast inserts, range queries, and k-NN search.
|
16
23
|
Keywords: quadtree,spatial-index,geometry,rust,pyo3,nearest-neighbor,k-nn
|
@@ -36,8 +43,8 @@ Project-URL: Issues, https://github.com/Elan456/fastquadtree/issues
|
|
36
43
|
|
37
44
|
[](https://pyo3.rs/)
|
38
45
|
[](https://www.maturin.rs/)
|
39
|
-
[](https://docs.astral.sh/ruff/)
|
47
|
+
|
41
48
|
|
42
49
|
|
43
50
|

|
@@ -49,6 +56,29 @@ Rust-optimized quadtree with a simple Python API.
|
|
49
56
|
- Python ≥ 3.8
|
50
57
|
- Import path: `from fastquadtree import QuadTree`
|
51
58
|
|
59
|
+
## Benchmarks
|
60
|
+
|
61
|
+
fastquadtree **outperforms** all other quadtree Python packages, including the Rtree spatial index.
|
62
|
+
|
63
|
+
### Library comparison
|
64
|
+
|
65
|
+

|
66
|
+

|
67
|
+
|
68
|
+
### Summary (largest dataset, PyQtree baseline)
|
69
|
+
- Points: **500,000**, Queries: **500**
|
70
|
+
--------------------
|
71
|
+
- Fastest total: **fastquadtree** at **1.591 s**
|
72
|
+
|
73
|
+
| Library | Build (s) | Query (s) | Total (s) | Speed vs PyQtree |
|
74
|
+
|---|---:|---:|---:|---:|
|
75
|
+
| fastquadtree | 0.165 | 1.427 | 1.591 | 5.09× |
|
76
|
+
| Rtree | 1.320 | 2.369 | 3.688 | 2.20× |
|
77
|
+
| PyQtree | 2.687 | 5.415 | 8.102 | 1.00× |
|
78
|
+
| nontree-QuadTree | 1.284 | 9.891 | 11.175 | 0.73× |
|
79
|
+
| quads | 2.346 | 10.129 | 12.475 | 0.65× |
|
80
|
+
| e-pyquadtree | 1.795 | 11.855 | 13.650 | 0.59× |
|
81
|
+
|
52
82
|
## Install
|
53
83
|
|
54
84
|
```bash
|
@@ -109,25 +139,7 @@ print(f"Deleted player: {deleted}") # True
|
|
109
139
|
|
110
140
|
You can keep the tree pure and manage your own id → object map, or let the wrapper manage it.
|
111
141
|
|
112
|
-
**
|
113
|
-
|
114
|
-
```python
|
115
|
-
from fastquadtree import QuadTree
|
116
|
-
|
117
|
-
qt = QuadTree((0, 0, 1000, 1000), capacity=16)
|
118
|
-
objects: dict[int, object] = {}
|
119
|
-
|
120
|
-
def add(obj) -> int:
|
121
|
-
obj_id = qt.insert(obj.position) # auto id
|
122
|
-
objects[obj_id] = obj
|
123
|
-
return obj_id
|
124
|
-
|
125
|
-
# Later, resolve ids back to objects
|
126
|
-
ids = [obj_id for (obj_id, x, y) in qt.query((100, 100, 300, 300))]
|
127
|
-
selected = [objects[i] for i in ids]
|
128
|
-
```
|
129
|
-
|
130
|
-
**Option B: Ask the wrapper to track objects**
|
142
|
+
**Wrapper Managed Objects**
|
131
143
|
|
132
144
|
```python
|
133
145
|
from fastquadtree import QuadTree
|
@@ -137,7 +149,7 @@ qt = QuadTree((0, 0, 1000, 1000), capacity=16, track_objects=True)
|
|
137
149
|
# Store the object alongside the point
|
138
150
|
qt.insert((25, 40), obj={"name": "apple"})
|
139
151
|
|
140
|
-
# Ask for Item objects
|
152
|
+
# Ask for Item objects within a bounding box
|
141
153
|
items = qt.query((0, 0, 100, 100), as_items=True)
|
142
154
|
for it in items:
|
143
155
|
print(it.id, it.x, it.y, it.obj)
|
@@ -151,7 +163,7 @@ qt.attach(123, my_object) # binds object to id 123
|
|
151
163
|
|
152
164
|
## API
|
153
165
|
|
154
|
-
### `QuadTree(bounds, capacity,
|
166
|
+
### `QuadTree(bounds, capacity, max_depth=None, track_objects=False, start_id=1)`
|
155
167
|
|
156
168
|
* `bounds` — tuple `(min_x, min_y, max_x, max_y)` defines the 2D area covered by the quadtree
|
157
169
|
* `capacity` — max number of points kept in a leaf before splitting
|
@@ -204,30 +216,8 @@ Full docs are in the docstrings of the [Python Shim](pysrc/fastquadtree/__init__
|
|
204
216
|
* For fastest local runs, use `maturin develop --release`.
|
205
217
|
* The wrapper keeps Python overhead low: raw tuple results by default, `Item` wrappers only when requested.
|
206
218
|
|
207
|
-
## Benchmarks
|
208
|
-
|
209
|
-
fastquadtree outperforms all other quadtree python packages (at least all the ones I could find and install via pip.)
|
210
|
-
|
211
|
-
### Library comparison
|
212
|
-
|
213
|
-

|
214
|
-

|
215
|
-
|
216
|
-
### Summary (largest dataset, PyQtree baseline)
|
217
|
-
- Points: **500,000**, Queries: **500**
|
218
|
-
- Fastest total: **fastquadtree** at **2.207 s**
|
219
|
-
|
220
|
-
| Library | Build (s) | Query (s) | Total (s) | Speed vs PyQtree |
|
221
|
-
|---|---:|---:|---:|---:|
|
222
|
-
| fastquadtree | 0.321 | 1.885 | 2.207 | 4.27× |
|
223
|
-
| Rtree | 1.718 | 4.376 | 6.095 | 1.55× |
|
224
|
-
| nontree-QuadTree | 1.617 | 7.643 | 9.260 | 1.02× |
|
225
|
-
| PyQtree | 4.349 | 5.082 | 9.431 | 1.00× |
|
226
|
-
| quads | 3.874 | 9.058 | 12.932 | 0.73× |
|
227
|
-
| e-pyquadtree | 2.732 | 10.598 | 13.330 | 0.71× |
|
228
|
-
| Brute force | 0.019 | 19.986 | 20.005 | 0.47× |
|
229
219
|
|
230
|
-
### Native vs Shim
|
220
|
+
### Native vs Shim Benchmark
|
231
221
|
|
232
222
|
**Setup**
|
233
223
|
- Points: 500,000
|
@@ -277,7 +267,7 @@ Check the CLI arguments for the cross-library benchmark in `benchmarks/quadtree_
|
|
277
267
|
Allowed. For k-nearest, duplicates are de-duplicated by id. For range queries you will see every inserted point.
|
278
268
|
|
279
269
|
**Can I delete items from the quadtree?**
|
280
|
-
Yes! Use `delete(id, xy)` to remove specific items. You must provide both the ID and exact location for precise deletion. This handles cases where multiple items exist at the same location. If you're using `track_objects=True`, you can also use `delete_by_object(obj
|
270
|
+
Yes! Use `delete(id, xy)` to remove specific items. You must provide both the ID and exact location for precise deletion. This handles cases where multiple items exist at the same location. If you're using `track_objects=True`, you can also use `delete_by_object(obj)` for convenient object-based deletion with O(1) lookup. The tree automatically merges nodes when item counts drop below capacity.
|
281
271
|
|
282
272
|
**Can I store rectangles or circles?**
|
283
273
|
The core stores points. To index objects with extent, insert whatever representative point you choose. For rectangles you can insert centers or build an AABB tree separately.
|
@@ -0,0 +1,10 @@
|
|
1
|
+
fastquadtree-0.5.1.dist-info/METADATA,sha256=3tYPbCeIMK_f0U0lR1_oeKPagw3M00sPO2GYq6us-xs,10443
|
2
|
+
fastquadtree-0.5.1.dist-info/WHEEL,sha256=7bfl5v0wbVhXZba613g0x-n2obNNfpQuN8I1cQ4oaU8,94
|
3
|
+
fastquadtree-0.5.1.dist-info/licenses/LICENSE,sha256=46IVFhoCIwMo-ocq4olyEB1eBvvtaKic5yGLeKXnDuc,1092
|
4
|
+
fastquadtree/__init__.py,sha256=NhKAMKyc9N6UV1BKpyF4ssGamLiZlwi-gqwx1znhDEw,12583
|
5
|
+
fastquadtree/__init__.pyi,sha256=hoX8-dibrH3Q9kv8M-h0Tac0BsJAoh1VYMd84vayhDE,2137
|
6
|
+
fastquadtree/_bimap.py,sha256=SPFFw9hWAVRKZVXyDGYFFNw4MLV8qUlOq8gDbLcTqkg,3402
|
7
|
+
fastquadtree/_item.py,sha256=p-ymnE9S-c8Lc00KhNKN9Pt4bd05nZ6g-DB8q8D9zYk,533
|
8
|
+
fastquadtree/_native.pyd,sha256=Wz9Z_lSFp6JCgOY5gCYeHl4RwzgCyoyB-ZAgvoWVVhA,247808
|
9
|
+
fastquadtree/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
fastquadtree-0.5.1.dist-info/RECORD,,
|
@@ -1,10 +0,0 @@
|
|
1
|
-
fastquadtree-0.4.2.dist-info/METADATA,sha256=EMQa7CtuH5DoKCsGXFzxZJvyYjNVjOmz0zCnMnCtVSA,10735
|
2
|
-
fastquadtree-0.4.2.dist-info/WHEEL,sha256=7bfl5v0wbVhXZba613g0x-n2obNNfpQuN8I1cQ4oaU8,94
|
3
|
-
fastquadtree-0.4.2.dist-info/licenses/LICENSE,sha256=46IVFhoCIwMo-ocq4olyEB1eBvvtaKic5yGLeKXnDuc,1092
|
4
|
-
fastquadtree/__init__.py,sha256=rqttCrFBaqMjD7BDZlcwkU9eeOQh2gzRi81xKHHLwLk,12652
|
5
|
-
fastquadtree/__init__.pyi,sha256=4lKaqtnvylRzhj322ue_y_DO_xxrkzOzjpYnH5SN1Fg,2166
|
6
|
-
fastquadtree/_bimap.py,sha256=zMp20NJkvVuO3jn2OeVApf7j4siBI4r2kU6SHyb7EZE,3428
|
7
|
-
fastquadtree/_item.py,sha256=imyiwnbmY21mHsOvNqB4j-TsOPpAmsvb38LKxEL8q4o,526
|
8
|
-
fastquadtree/_native.pyd,sha256=DwOyfT3BhgOCOV7_vq6EclT0XtYA-7-2_uf9oZPgMSs,309248
|
9
|
-
fastquadtree/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
fastquadtree-0.4.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|