fastquadtree 0.6.1__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 -380
- 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.6.1.dist-info → fastquadtree-0.8.0.dist-info}/METADATA +32 -141
- fastquadtree-0.8.0.dist-info/RECORD +12 -0
- {fastquadtree-0.6.1.dist-info → fastquadtree-0.8.0.dist-info}/WHEEL +1 -1
- fastquadtree/__init__.pyi +0 -70
- fastquadtree-0.6.1.dist-info/RECORD +0 -10
- {fastquadtree-0.6.1.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)
|