fastquadtree 0.7.0__cp38-abi3-win_amd64.whl → 0.8.0__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 +4 -416
- fastquadtree/_base_quadtree.py +263 -0
- fastquadtree/_bimap.py +22 -22
- fastquadtree/_item.py +35 -7
- fastquadtree/_native.pyd +0 -0
- fastquadtree/point_quadtree.py +161 -0
- fastquadtree/rect_quadtree.py +98 -0
- {fastquadtree-0.7.0.dist-info → fastquadtree-0.8.0.dist-info}/METADATA +17 -149
- fastquadtree-0.8.0.dist-info/RECORD +12 -0
- fastquadtree/__init__.pyi +0 -71
- fastquadtree-0.7.0.dist-info/RECORD +0 -10
- {fastquadtree-0.7.0.dist-info → fastquadtree-0.8.0.dist-info}/WHEEL +0 -0
- {fastquadtree-0.7.0.dist-info → fastquadtree-0.8.0.dist-info}/licenses/LICENSE +0 -0
fastquadtree/_bimap.py
CHANGED
@@ -1,14 +1,16 @@
|
|
1
1
|
# _bimap.py
|
2
2
|
from __future__ import annotations
|
3
3
|
|
4
|
-
from typing import Any, Iterable, Iterator
|
4
|
+
from typing import Any, Generic, Iterable, Iterator, TypeVar
|
5
5
|
|
6
|
-
from ._item import Item
|
6
|
+
from ._item import Item # base class for PointItem and RectItem
|
7
7
|
|
8
|
+
TItem = TypeVar("TItem", bound=Item)
|
8
9
|
|
9
|
-
|
10
|
+
|
11
|
+
class BiMap(Generic[TItem]):
|
10
12
|
"""
|
11
|
-
Bidirectional map to the same Item:
|
13
|
+
Bidirectional map to the same Item subtype:
|
12
14
|
id -> Item
|
13
15
|
obj -> Item (uses object identity)
|
14
16
|
|
@@ -20,19 +22,22 @@ class BiMap:
|
|
20
22
|
|
21
23
|
__slots__ = ("_id_to_item", "_objid_to_item")
|
22
24
|
|
23
|
-
def __init__(
|
24
|
-
self
|
25
|
-
|
25
|
+
def __init__(
|
26
|
+
self,
|
27
|
+
items: Iterable[TItem] | None = None,
|
28
|
+
) -> None:
|
29
|
+
self._id_to_item: dict[int, TItem] = {}
|
30
|
+
self._objid_to_item: dict[int, TItem] = {}
|
26
31
|
if items:
|
27
32
|
for it in items:
|
28
33
|
self.add(it)
|
29
34
|
|
30
35
|
# - core -
|
31
36
|
|
32
|
-
def add(self, item:
|
37
|
+
def add(self, item: TItem) -> None:
|
33
38
|
"""
|
34
39
|
Insert or replace mapping for this Item.
|
35
|
-
Handles conflicts so
|
40
|
+
Handles conflicts so both id and obj point to this exact Item.
|
36
41
|
"""
|
37
42
|
id_ = item.id_
|
38
43
|
obj = item.obj
|
@@ -55,13 +60,13 @@ class BiMap:
|
|
55
60
|
if obj is not None:
|
56
61
|
self._objid_to_item[id(obj)] = item
|
57
62
|
|
58
|
-
def by_id(self, id_: int) ->
|
63
|
+
def by_id(self, id_: int) -> TItem | None:
|
59
64
|
return self._id_to_item.get(id_)
|
60
65
|
|
61
|
-
def by_obj(self, obj: Any) ->
|
66
|
+
def by_obj(self, obj: Any) -> TItem | None:
|
62
67
|
return self._objid_to_item.get(id(obj))
|
63
68
|
|
64
|
-
def pop_id(self, id_: int) ->
|
69
|
+
def pop_id(self, id_: int) -> TItem | None:
|
65
70
|
it = self._id_to_item.pop(id_, None)
|
66
71
|
if it is not None:
|
67
72
|
obj = it.obj
|
@@ -69,25 +74,20 @@ class BiMap:
|
|
69
74
|
self._objid_to_item.pop(id(obj), None)
|
70
75
|
return it
|
71
76
|
|
72
|
-
def pop_obj(self, obj: Any) ->
|
77
|
+
def pop_obj(self, obj: Any) -> TItem | None:
|
73
78
|
it = self._objid_to_item.pop(id(obj), None)
|
74
79
|
if it is not None:
|
75
80
|
self._id_to_item.pop(it.id_, None)
|
76
81
|
return it
|
77
82
|
|
78
|
-
def pop_item(self, item:
|
83
|
+
def pop_item(self, item: TItem) -> TItem | None:
|
79
84
|
"""
|
80
85
|
Remove this exact Item if present on either side.
|
81
86
|
"""
|
82
|
-
removed = None
|
83
|
-
# Remove by id first
|
84
|
-
removed = self._id_to_item.pop(item.id_)
|
85
|
-
|
86
|
-
# Remove by obj side
|
87
|
+
removed = self._id_to_item.pop(item.id_, None)
|
87
88
|
obj = item.obj
|
88
89
|
if obj is not None:
|
89
90
|
self._objid_to_item.pop(id(obj), None)
|
90
|
-
removed = removed or item
|
91
91
|
return removed
|
92
92
|
|
93
93
|
# - convenience -
|
@@ -105,8 +105,8 @@ class BiMap:
|
|
105
105
|
def contains_obj(self, obj: Any) -> bool:
|
106
106
|
return id(obj) in self._objid_to_item
|
107
107
|
|
108
|
-
def items_by_id(self) -> Iterator[tuple[int,
|
108
|
+
def items_by_id(self) -> Iterator[tuple[int, TItem]]:
|
109
109
|
return iter(self._id_to_item.items())
|
110
110
|
|
111
|
-
def items(self) -> Iterator[
|
111
|
+
def items(self) -> Iterator[TItem]:
|
112
112
|
return iter(self._id_to_item.values())
|
fastquadtree/_item.py
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
# item.py
|
2
2
|
from __future__ import annotations
|
3
3
|
|
4
|
-
from typing import Any
|
4
|
+
from typing import Any, Tuple
|
5
|
+
|
6
|
+
Bounds = Tuple[float, float, float, float]
|
7
|
+
"""Axis-aligned rectangle as (min_x, min_y, max_x, max_y)."""
|
8
|
+
|
9
|
+
Point = Tuple[float, float]
|
10
|
+
"""2D point as (x, y)."""
|
5
11
|
|
6
12
|
|
7
13
|
class Item:
|
@@ -10,15 +16,37 @@ class Item:
|
|
10
16
|
|
11
17
|
Attributes:
|
12
18
|
id_: Integer identifier.
|
13
|
-
|
14
|
-
y: Y coordinate.
|
19
|
+
geom: The geometry, either a Point or Rectangle Bounds.
|
15
20
|
obj: The attached Python object if available, else None.
|
16
21
|
"""
|
17
22
|
|
18
|
-
__slots__ = ("
|
23
|
+
__slots__ = ("geom", "id_", "obj")
|
19
24
|
|
20
|
-
def __init__(self, id_: int,
|
25
|
+
def __init__(self, id_: int, geom: Point | Bounds, obj: Any | None = None):
|
21
26
|
self.id_ = id_
|
22
|
-
self.
|
23
|
-
self.y = y
|
27
|
+
self.geom = geom
|
24
28
|
self.obj = obj
|
29
|
+
|
30
|
+
|
31
|
+
class PointItem(Item):
|
32
|
+
"""
|
33
|
+
Lightweight point item wrapper for tracking and as_items results.
|
34
|
+
"""
|
35
|
+
|
36
|
+
__slots__ = ("geom", "id_", "obj", "x", "y")
|
37
|
+
|
38
|
+
def __init__(self, id_: int, geom: Point, obj: Any | None = None):
|
39
|
+
super().__init__(id_, geom, obj)
|
40
|
+
self.x, self.y = geom
|
41
|
+
|
42
|
+
|
43
|
+
class RectItem(Item):
|
44
|
+
"""
|
45
|
+
Lightweight rectangle item wrapper for tracking and as_items results.
|
46
|
+
"""
|
47
|
+
|
48
|
+
__slots__ = ("geom", "id_", "max_x", "max_y", "min_x", "min_y", "obj")
|
49
|
+
|
50
|
+
def __init__(self, id_: int, geom: Bounds, obj: Any | None = None):
|
51
|
+
super().__init__(id_, geom, obj)
|
52
|
+
self.min_x, self.min_y, self.max_x, self.max_y = geom
|
fastquadtree/_native.pyd
CHANGED
Binary file
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# point_quadtree.py
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
from typing import Any, Literal, Tuple, overload
|
5
|
+
|
6
|
+
from ._base_quadtree import Bounds, _BaseQuadTree
|
7
|
+
from ._item import Point, PointItem
|
8
|
+
from ._native import QuadTree as _RustQuadTree # native point tree
|
9
|
+
|
10
|
+
_IdCoord = Tuple[int, float, float]
|
11
|
+
|
12
|
+
|
13
|
+
class QuadTree(_BaseQuadTree[Point, _IdCoord, PointItem]):
|
14
|
+
"""
|
15
|
+
Point version of the quadtree. All geometries are 2D points (x, y).
|
16
|
+
High-level Python wrapper over the Rust quadtree engine.
|
17
|
+
|
18
|
+
Performance characteristics:
|
19
|
+
Inserts: average O(log n) <br>
|
20
|
+
Rect queries: average O(log n + k) where k is matches returned <br>
|
21
|
+
Nearest neighbor: average O(log n) <br>
|
22
|
+
|
23
|
+
Thread-safety:
|
24
|
+
Instances are not thread-safe. Use external synchronization if you
|
25
|
+
mutate the same tree from multiple threads.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
bounds: World bounds as (min_x, min_y, max_x, max_y).
|
29
|
+
capacity: Max number of points per node before splitting.
|
30
|
+
max_depth: Optional max tree depth. If omitted, engine decides.
|
31
|
+
track_objects: Enable id <-> object mapping inside Python.
|
32
|
+
start_id: Starting auto-assigned id when you omit id on insert.
|
33
|
+
|
34
|
+
Raises:
|
35
|
+
ValueError: If parameters are invalid or inserts are out of bounds.
|
36
|
+
"""
|
37
|
+
|
38
|
+
def __init__(
|
39
|
+
self,
|
40
|
+
bounds: Bounds,
|
41
|
+
capacity: int,
|
42
|
+
*,
|
43
|
+
max_depth: int | None = None,
|
44
|
+
track_objects: bool = False,
|
45
|
+
start_id: int = 1,
|
46
|
+
):
|
47
|
+
super().__init__(
|
48
|
+
bounds,
|
49
|
+
capacity,
|
50
|
+
max_depth=max_depth,
|
51
|
+
track_objects=track_objects,
|
52
|
+
start_id=start_id,
|
53
|
+
)
|
54
|
+
|
55
|
+
@overload
|
56
|
+
def query(
|
57
|
+
self, rect: Bounds, *, as_items: Literal[False] = ...
|
58
|
+
) -> list[_IdCoord]: ...
|
59
|
+
@overload
|
60
|
+
def query(self, rect: Bounds, *, as_items: Literal[True]) -> list[PointItem]: ...
|
61
|
+
def query(
|
62
|
+
self, rect: Bounds, *, as_items: bool = False
|
63
|
+
) -> list[PointItem] | list[_IdCoord]:
|
64
|
+
"""
|
65
|
+
Return all points inside an axis-aligned rectangle.
|
66
|
+
|
67
|
+
Args:
|
68
|
+
rect: Query rectangle as (min_x, min_y, max_x, max_y).
|
69
|
+
as_items: If True, return Item wrappers. If False, return raw tuples.
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
If as_items is False: list of (id, x, y) tuples.
|
73
|
+
If as_items is True: list of Item objects.
|
74
|
+
"""
|
75
|
+
raw = self._native.query(rect)
|
76
|
+
if not as_items:
|
77
|
+
return raw
|
78
|
+
if self._items is None:
|
79
|
+
raise ValueError("Cannot return results as items with track_objects=False")
|
80
|
+
out: list[PointItem] = []
|
81
|
+
for id_, _x, _y in raw:
|
82
|
+
it = self._items.by_id(id_)
|
83
|
+
if it is None:
|
84
|
+
raise RuntimeError(
|
85
|
+
f"Internal error: id {id_} present in native tree but missing from tracker."
|
86
|
+
)
|
87
|
+
out.append(it)
|
88
|
+
return out
|
89
|
+
|
90
|
+
@overload
|
91
|
+
def nearest_neighbor(
|
92
|
+
self, xy: Point, *, as_item: Literal[False] = ...
|
93
|
+
) -> _IdCoord | None: ...
|
94
|
+
@overload
|
95
|
+
def nearest_neighbor(
|
96
|
+
self, xy: Point, *, as_item: Literal[True]
|
97
|
+
) -> PointItem | None: ...
|
98
|
+
def nearest_neighbor(
|
99
|
+
self, xy: Point, *, as_item: bool = False
|
100
|
+
) -> PointItem | _IdCoord | None:
|
101
|
+
"""
|
102
|
+
Return the single nearest neighbor to the query point.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
xy: Query point (x, y).
|
106
|
+
as_item: If True, return Item. If False, return (id, x, y).
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
The nearest neighbor or None if the tree is empty.
|
110
|
+
"""
|
111
|
+
t = self._native.nearest_neighbor(xy)
|
112
|
+
if t is None or not as_item:
|
113
|
+
return t
|
114
|
+
if self._items is None:
|
115
|
+
raise ValueError("Cannot return result as item with track_objects=False")
|
116
|
+
id_, _x, _y = t
|
117
|
+
it = self._items.by_id(id_)
|
118
|
+
if it is None:
|
119
|
+
raise RuntimeError("Internal error: missing tracked item")
|
120
|
+
return it
|
121
|
+
|
122
|
+
@overload
|
123
|
+
def nearest_neighbors(
|
124
|
+
self, xy: Point, k: int, *, as_items: Literal[False] = ...
|
125
|
+
) -> list[_IdCoord]: ...
|
126
|
+
@overload
|
127
|
+
def nearest_neighbors(
|
128
|
+
self, xy: Point, k: int, *, as_items: Literal[True]
|
129
|
+
) -> list[PointItem]: ...
|
130
|
+
def nearest_neighbors(self, xy: Point, k: int, *, as_items: bool = False):
|
131
|
+
"""
|
132
|
+
Return the k nearest neighbors to the query point in order of increasing distance.
|
133
|
+
|
134
|
+
Args:
|
135
|
+
xy: Query point (x, y).
|
136
|
+
k: Number of neighbors to return.
|
137
|
+
as_items: If True, return Item wrappers. If False, return raw tuples.
|
138
|
+
Returns:
|
139
|
+
If as_items is False: list of (id, x, y) tuples.
|
140
|
+
If as_items is True: list of Item objects.
|
141
|
+
"""
|
142
|
+
raw = self._native.nearest_neighbors(xy, k)
|
143
|
+
if not as_items:
|
144
|
+
return raw
|
145
|
+
if self._items is None:
|
146
|
+
raise ValueError("Cannot return results as items with track_objects=False")
|
147
|
+
out: list[PointItem] = []
|
148
|
+
for id_, _x, _y in raw:
|
149
|
+
it = self._items.by_id(id_)
|
150
|
+
if it is None:
|
151
|
+
raise RuntimeError("Internal error: missing tracked item")
|
152
|
+
out.append(it)
|
153
|
+
return out
|
154
|
+
|
155
|
+
def _new_native(self, bounds: Bounds, capacity: int, max_depth: int | None) -> Any:
|
156
|
+
if max_depth is None:
|
157
|
+
return _RustQuadTree(bounds, capacity)
|
158
|
+
return _RustQuadTree(bounds, capacity, max_depth=max_depth)
|
159
|
+
|
160
|
+
def _make_item(self, id_: int, geom: Point, obj: Any | None) -> PointItem:
|
161
|
+
return PointItem(id_, geom, obj)
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# rect_quadtree.py
|
2
|
+
from __future__ import annotations
|
3
|
+
|
4
|
+
from typing import Any, Literal, Tuple, overload
|
5
|
+
|
6
|
+
from ._base_quadtree import Bounds, _BaseQuadTree
|
7
|
+
from ._item import RectItem
|
8
|
+
from ._native import RectQuadTree as _RustRectQuadTree # native rect tree
|
9
|
+
|
10
|
+
_IdRect = Tuple[int, float, float, float, float]
|
11
|
+
Point = Tuple[float, float] # only for type hints in docstrings
|
12
|
+
|
13
|
+
|
14
|
+
class RectQuadTree(_BaseQuadTree[Bounds, _IdRect, RectItem]):
|
15
|
+
"""
|
16
|
+
Rectangle version of the quadtree. All geometries are axis-aligned rectangles. (min_x, min_y, max_x, max_y)
|
17
|
+
High-level Python wrapper over the Rust quadtree engine.
|
18
|
+
|
19
|
+
Performance characteristics:
|
20
|
+
Inserts: average O(log n) <br>
|
21
|
+
Rect queries: average O(log n + k) where k is matches returned <br>
|
22
|
+
Nearest neighbor: average O(log n) <br>
|
23
|
+
|
24
|
+
Thread-safety:
|
25
|
+
Instances are not thread-safe. Use external synchronization if you
|
26
|
+
mutate the same tree from multiple threads.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
bounds: World bounds as (min_x, min_y, max_x, max_y).
|
30
|
+
capacity: Max number of points per node before splitting.
|
31
|
+
max_depth: Optional max tree depth. If omitted, engine decides.
|
32
|
+
track_objects: Enable id <-> object mapping inside Python.
|
33
|
+
start_id: Starting auto-assigned id when you omit id on insert.
|
34
|
+
|
35
|
+
Raises:
|
36
|
+
ValueError: If parameters are invalid or inserts are out of bounds.
|
37
|
+
"""
|
38
|
+
|
39
|
+
def __init__(
|
40
|
+
self,
|
41
|
+
bounds: Bounds,
|
42
|
+
capacity: int,
|
43
|
+
*,
|
44
|
+
max_depth: int | None = None,
|
45
|
+
track_objects: bool = False,
|
46
|
+
start_id: int = 1,
|
47
|
+
):
|
48
|
+
super().__init__(
|
49
|
+
bounds,
|
50
|
+
capacity,
|
51
|
+
max_depth=max_depth,
|
52
|
+
track_objects=track_objects,
|
53
|
+
start_id=start_id,
|
54
|
+
)
|
55
|
+
|
56
|
+
@overload
|
57
|
+
def query(
|
58
|
+
self, rect: Bounds, *, as_items: Literal[False] = ...
|
59
|
+
) -> list[_IdRect]: ...
|
60
|
+
@overload
|
61
|
+
def query(self, rect: Bounds, *, as_items: Literal[True]) -> list[RectItem]: ...
|
62
|
+
def query(self, rect: Bounds, *, as_items: bool = False):
|
63
|
+
"""
|
64
|
+
Query the tree for all items that intersect the given rectangle.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
rect: Query rectangle as (min_x, min_y, max_x, max_y).
|
68
|
+
as_items: If True, return Item wrappers. If False, return raw tuples.
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
If as_items is False: list of (id, x0, y0, x1, y1) tuples.
|
72
|
+
If as_items is True: list of Item objects.
|
73
|
+
"""
|
74
|
+
raw = self._native.query(rect)
|
75
|
+
if not as_items:
|
76
|
+
return raw
|
77
|
+
if self._items is None:
|
78
|
+
# Build RectItem without objects
|
79
|
+
return [
|
80
|
+
RectItem(id_, (x0, y0, x1, y1), None) for (id_, x0, y0, x1, y1) in raw
|
81
|
+
]
|
82
|
+
out: list[RectItem] = []
|
83
|
+
for id_, _x0, _y0, _x1, _y1 in raw:
|
84
|
+
it = self._items.by_id(id_)
|
85
|
+
if it is None:
|
86
|
+
raise RuntimeError(
|
87
|
+
f"Internal error: id {id_} present in native tree but missing from tracker."
|
88
|
+
)
|
89
|
+
out.append(it)
|
90
|
+
return out
|
91
|
+
|
92
|
+
def _new_native(self, bounds: Bounds, capacity: int, max_depth: int | None) -> Any:
|
93
|
+
if max_depth is None:
|
94
|
+
return _RustRectQuadTree(bounds, capacity)
|
95
|
+
return _RustRectQuadTree(bounds, capacity, max_depth=max_depth)
|
96
|
+
|
97
|
+
def _make_item(self, id_: int, geom: Bounds, obj: Any | None) -> RectItem:
|
98
|
+
return RectItem(id_, geom, obj)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: fastquadtree
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.8.0
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
5
5
|
Classifier: Programming Language :: Python :: 3 :: Only
|
6
6
|
Classifier: Programming Language :: Rust
|
@@ -12,8 +12,8 @@ Classifier: Topic :: Software Development :: Libraries
|
|
12
12
|
Classifier: Typing :: Typed
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
14
14
|
Requires-Dist: ruff>=0.6.0 ; extra == 'dev'
|
15
|
-
Requires-Dist: pytest>=
|
16
|
-
Requires-Dist: pytest-cov>=
|
15
|
+
Requires-Dist: pytest>=8.4.2 ; extra == 'dev'
|
16
|
+
Requires-Dist: pytest-cov>=7.0.0 ; extra == 'dev'
|
17
17
|
Requires-Dist: coverage>=7.5 ; extra == 'dev'
|
18
18
|
Requires-Dist: mypy>=1.10 ; extra == 'dev'
|
19
19
|
Requires-Dist: build>=1.2.1 ; extra == 'dev'
|
@@ -23,6 +23,7 @@ Requires-Dist: mkdocstrings[python] ; extra == 'dev'
|
|
23
23
|
Requires-Dist: mkdocs-autorefs ; extra == 'dev'
|
24
24
|
Requires-Dist: mkdocs-git-revision-date-localized-plugin ; extra == 'dev'
|
25
25
|
Requires-Dist: mkdocs-minify-plugin ; extra == 'dev'
|
26
|
+
Requires-Dist: maturin>=1.5 ; extra == 'dev'
|
26
27
|
Provides-Extra: dev
|
27
28
|
License-File: LICENSE
|
28
29
|
Summary: Rust-accelerated quadtree for Python with fast inserts, range queries, and k-NN search.
|
@@ -45,7 +46,7 @@ Project-URL: Issues, https://github.com/Elan456/fastquadtree/issues
|
|
45
46
|
|
46
47
|
[](https://pepy.tech/projects/fastquadtree)
|
47
48
|
|
48
|
-
[](https://github.com/Elan456/fastquadtree/actions/workflows/
|
49
|
+
[](https://github.com/Elan456/fastquadtree/actions/workflows/release.yml)
|
49
50
|
[](https://codecov.io/gh/Elan456/fastquadtree)
|
50
51
|
|
51
52
|
[](https://pyo3.rs/)
|
@@ -76,7 +77,6 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
76
77
|
|
77
78
|
### Summary (largest dataset, PyQtree baseline)
|
78
79
|
- Points: **250,000**, Queries: **500**
|
79
|
-
--------------------
|
80
80
|
- Fastest total: **fastquadtree** at **0.120 s**
|
81
81
|
|
82
82
|
| Library | Build (s) | Query (s) | Total (s) | Speed vs PyQtree |
|
@@ -89,7 +89,7 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
89
89
|
| PyQtree | 1.492 | 0.263 | 1.755 | 1.00× |
|
90
90
|
| quads | 1.407 | 0.484 | 1.890 | 0.93× |
|
91
91
|
|
92
|
-
|
92
|
+
### Benchmark Configuration
|
93
93
|
| Parameter | Value |
|
94
94
|
|---|---:|
|
95
95
|
| Bounds | (0, 0, 1000, 1000) |
|
@@ -97,11 +97,13 @@ fastquadtree **outperforms** all other quadtree Python packages, including the R
|
|
97
97
|
| Max depth | 16 |
|
98
98
|
| Queries per experiment | 500 |
|
99
99
|
|
100
|
+
See the [benchmark section](https://elan456.github.io/fastquadtree/benchmark/) for details.
|
101
|
+
|
100
102
|
## Install
|
101
103
|
|
102
104
|
```bash
|
103
105
|
pip install fastquadtree
|
104
|
-
|
106
|
+
```
|
105
107
|
|
106
108
|
If you are developing locally:
|
107
109
|
|
@@ -111,77 +113,11 @@ maturin develop --release
|
|
111
113
|
```
|
112
114
|
|
113
115
|
## Quickstart
|
114
|
-
|
115
|
-
```python
|
116
|
-
from fastquadtree import QuadTree
|
117
|
-
|
118
|
-
# Bounds are (min_x, min_y, max_x, max_y)
|
119
|
-
qt = QuadTree(bounds=(0, 0, 1000, 1000), capacity=20) # max_depth is optional
|
120
|
-
|
121
|
-
# Insert points with auto ids
|
122
|
-
id1 = qt.insert((10, 10))
|
123
|
-
id2 = qt.insert((200, 300))
|
124
|
-
id3 = qt.insert((999, 500), id=42) # you can supply your own id
|
125
|
-
|
126
|
-
# Axis-aligned rectangle query
|
127
|
-
hits = qt.query((0, 0, 250, 350)) # returns [(id, x, y), ...] by default
|
128
|
-
print(hits) # e.g. [(1, 10.0, 10.0), (2, 200.0, 300.0)]
|
129
|
-
|
130
|
-
# Nearest neighbor
|
131
|
-
best = qt.nearest_neighbor((210, 310)) # -> (id, x, y) or None
|
132
|
-
print(best)
|
133
|
-
|
134
|
-
# k-nearest neighbors
|
135
|
-
top3 = qt.nearest_neighbors((210, 310), 3)
|
136
|
-
print(top3) # list of up to 3 (id, x, y) tuples
|
137
|
-
|
138
|
-
# Delete items by ID and location
|
139
|
-
deleted = qt.delete(id2, (200, 300)) # True if found and deleted
|
140
|
-
print(f"Deleted: {deleted}")
|
141
|
-
print(f"Remaining items: {qt.count_items()}")
|
142
|
-
|
143
|
-
# For object tracking with track_objects=True
|
144
|
-
qt_tracked = QuadTree((0, 0, 1000, 1000), capacity=4, track_objects=True)
|
145
|
-
player1 = {"name": "Alice", "score": 100}
|
146
|
-
player2 = {"name": "Bob", "score": 200}
|
147
|
-
|
148
|
-
id1 = qt_tracked.insert((50, 50), obj=player1)
|
149
|
-
id2 = qt_tracked.insert((150, 150), obj=player2)
|
150
|
-
|
151
|
-
# Delete by object reference (O(1) lookup!)
|
152
|
-
deleted = qt_tracked.delete_by_object(player1)
|
153
|
-
print(f"Deleted player: {deleted}") # True
|
154
|
-
```
|
155
|
-
|
156
|
-
### Working with Python objects
|
157
|
-
|
158
|
-
You can keep the tree pure and manage your own id → object map, or let the wrapper manage it.
|
159
|
-
|
160
|
-
**Wrapper Managed Objects**
|
161
|
-
|
162
|
-
```python
|
163
|
-
from fastquadtree import QuadTree
|
164
|
-
|
165
|
-
qt = QuadTree((0, 0, 1000, 1000), capacity=16, track_objects=True)
|
166
|
-
|
167
|
-
# Store the object alongside the point
|
168
|
-
qt.insert((25, 40), obj={"name": "apple"})
|
169
|
-
|
170
|
-
# Ask for Item objects within a bounding box
|
171
|
-
items = qt.query((0, 0, 100, 100), as_items=True)
|
172
|
-
for it in items:
|
173
|
-
print(it.id, it.x, it.y, it.obj)
|
174
|
-
```
|
175
|
-
|
176
|
-
You can also attach or replace an object later:
|
177
|
-
|
178
|
-
```python
|
179
|
-
qt.attach(123, my_object) # binds object to id 123
|
180
|
-
```
|
116
|
+
[See the quickstart guide](https://elan456.github.io/fastquadtree/quickstart/)
|
181
117
|
|
182
118
|
## API
|
183
119
|
|
184
|
-
[
|
120
|
+
[See the full API](https://elan456.github.io/fastquadtree/api/quadtree/)
|
185
121
|
|
186
122
|
### `QuadTree(bounds, capacity, max_depth=None, track_objects=False, start_id=1)`
|
187
123
|
|
@@ -191,38 +127,17 @@ qt.attach(123, my_object) # binds object to id 123
|
|
191
127
|
* `track_objects` — if `True`, the wrapper maintains an id → object map for convenience.
|
192
128
|
* `start_id` — starting value for auto-assigned ids
|
193
129
|
|
194
|
-
###
|
130
|
+
### Key Methods
|
195
131
|
|
196
132
|
- `insert(xy, *, id=None, obj=None) -> int`
|
197
133
|
|
198
|
-
- `insert_many_points(points) -> int`
|
199
|
-
|
200
134
|
- `query(rect, *, as_items=False) -> list`
|
201
135
|
|
202
136
|
- `nearest_neighbor(xy, *, as_item=False) -> (id, x, y) | Item | None`
|
203
137
|
|
204
|
-
- `nearest_neighbors(xy, k, *, as_items=False) -> list`
|
205
|
-
|
206
138
|
- `delete(id, xy) -> bool`
|
207
139
|
|
208
|
-
|
209
|
-
|
210
|
-
- `clear(*, reset_ids=False) -> None`
|
211
|
-
|
212
|
-
- `attach(id, obj) -> None (requires track_objects=True)`
|
213
|
-
|
214
|
-
- `count_items() -> int`
|
215
|
-
|
216
|
-
- `get(id) -> object | None`
|
217
|
-
|
218
|
-
- `get_all_rectangles() -> list[tuple] (for visualization)`
|
219
|
-
|
220
|
-
- `get_all_objects() -> list[object] (requires track_objects=True)`
|
221
|
-
|
222
|
-
### `Item` (returned when `as_items=True`)
|
223
|
-
|
224
|
-
* Attributes: `id`, `x`, `y`, and a lazy `obj` property
|
225
|
-
* Accessing `obj` performs a dictionary lookup only if tracking is enabled
|
140
|
+
There are more methods and object tracking versions in the [docs](https://elan456.github.io/fastquadtree/api/quadtree/).
|
226
141
|
|
227
142
|
### Geometric conventions
|
228
143
|
|
@@ -236,52 +151,8 @@ qt.attach(123, my_object) # binds object to id 123
|
|
236
151
|
* Choose `capacity` so that leaves keep a small batch of points. Typical values are 8 to 64.
|
237
152
|
* If your data is very skewed, set a `max_depth` to prevent long chains.
|
238
153
|
* For fastest local runs, use `maturin develop --release`.
|
239
|
-
* The wrapper
|
240
|
-
|
241
|
-
|
242
|
-
### Native vs Shim Benchmark
|
243
|
-
|
244
|
-
**Setup**
|
245
|
-
- Points: 500,000
|
246
|
-
- Queries: 500
|
247
|
-
- Repeats: 5
|
248
|
-
|
249
|
-
**Timing (seconds)**
|
250
|
-
|
251
|
-
| Variant | Build | Query | Total |
|
252
|
-
|---|---:|---:|---:|
|
253
|
-
| Native | 0.483 | 4.380 | 4.863 |
|
254
|
-
| Shim (no map) | 0.668 | 4.167 | 4.835 |
|
255
|
-
| Shim (track+objs) | 1.153 | 4.458 | 5.610 |
|
256
|
-
|
257
|
-
**Overhead vs Native**
|
258
|
-
|
259
|
-
- No map: build 1.38x, query 0.95x, total 0.99x
|
260
|
-
- Track + objs: build 2.39x, query 1.02x, total 1.15x
|
261
|
-
|
262
|
-
### Run benchmarks
|
263
|
-
To run the benchmarks yourself, first install the dependencies:
|
264
|
-
|
265
|
-
```bash
|
266
|
-
pip install -r benchmarks/requirements.txt
|
267
|
-
```
|
268
|
-
|
269
|
-
Then run:
|
270
|
-
|
271
|
-
```bash
|
272
|
-
python benchmarks/cross_library_bench.py
|
273
|
-
python benchmarks/benchmark_native_vs_shim.py
|
274
|
-
```
|
275
|
-
|
276
|
-
Check the CLI arguments for the cross-library benchmark in `benchmarks/quadtree_bench/main.py`.
|
277
|
-
|
278
|
-
## Run Visualizer
|
279
|
-
A visualizer is included to help you understand how the quadtree subdivides space.
|
280
|
-
|
281
|
-
```bash
|
282
|
-
pip install -r interactive/requirements.txt
|
283
|
-
python interactive/interactive_v2.py
|
284
|
-
```
|
154
|
+
* The wrapper maintains an object map only if the quadtree was constructed with `track_objects=True`. If you don't need it, leave it off for best performance.
|
155
|
+
* Refer to the [Native vs Shim Benchmark](https://elan456.github.io/fastquadtree/benchmark/#native-vs-shim-benchmark) for overhead details.
|
285
156
|
|
286
157
|
### Pygame Ball Pit Demo
|
287
158
|
|
@@ -290,10 +161,7 @@ python interactive/interactive_v2.py
|
|
290
161
|
A simple demo of moving objects with collision detection using **fastquadtree**.
|
291
162
|
You can toggle between quadtree mode and brute-force mode to see the performance difference.
|
292
163
|
|
293
|
-
|
294
|
-
pip install -r interactive/requirements.txt
|
295
|
-
python interactive/ball_pit.py
|
296
|
-
```
|
164
|
+
See the [runnables guide](https://elan456.github.io/fastquadtree/runnables/) for setup instructions.
|
297
165
|
|
298
166
|
## FAQ
|
299
167
|
|
@@ -304,7 +172,7 @@ Allowed. For k-nearest, duplicates are de-duplicated by id. For range queries yo
|
|
304
172
|
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.
|
305
173
|
|
306
174
|
**Can I store rectangles or circles?**
|
307
|
-
|
175
|
+
Yes, you can store rectangles using the `RectQuadTree` class. Circles can be approximated with bounding boxes. See the [RectQuadTree docs](https://elan456.github.io/fastquadtree/api/rect_quadtree/) for details.
|
308
176
|
|
309
177
|
## License
|
310
178
|
|