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/_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
- class BiMap:
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__(self, items: Iterable[Item] | None = None) -> None:
24
- self._id_to_item: dict[int, Item] = {}
25
- self._objid_to_item: dict[int, Item] = {}
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: Item) -> None:
37
+ def add(self, item: TItem) -> None:
33
38
  """
34
39
  Insert or replace mapping for this Item.
35
- Handles conflicts so that both id and obj point to this exact Item.
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) -> Item | None:
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) -> Item | None:
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) -> Item | None:
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) -> Item | None:
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: Item) -> Item | None:
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, Item]]:
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[Item]:
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
- x: X coordinate.
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__ = ("id_", "obj", "x", "y")
23
+ __slots__ = ("geom", "id_", "obj")
19
24
 
20
- def __init__(self, id_: int, x: float, y: float, obj: Any | None = None):
25
+ def __init__(self, id_: int, geom: Point | Bounds, obj: Any | None = None):
21
26
  self.id_ = id_
22
- self.x = x
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)