fastquadtree 0.7.0__cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl → 0.8.0__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.
fastquadtree/__init__.py CHANGED
@@ -1,417 +1,5 @@
1
- from __future__ import annotations
1
+ from ._item import Item, PointItem, RectItem
2
+ from .point_quadtree import QuadTree
3
+ from .rect_quadtree import RectQuadTree
2
4
 
3
- from typing import Any, Literal, Tuple, overload
4
-
5
- from ._bimap import BiMap # type: ignore[attr-defined]
6
- from ._item import Item
7
-
8
- # Compiled Rust module is provided by maturin (tool.maturin.module-name)
9
- from ._native import QuadTree as _RustQuadTree
10
-
11
- Bounds = Tuple[float, float, float, float]
12
- """Axis-aligned rectangle as (min_x, min_y, max_x, max_y)."""
13
-
14
- Point = Tuple[float, float]
15
- """2D point as (x, y)."""
16
-
17
- _IdCoord = Tuple[int, float, float]
18
- """Result tuple as (id, x, y)."""
19
-
20
-
21
- class QuadTree:
22
- """
23
- High-level Python wrapper over the Rust quadtree engine.
24
-
25
- The quadtree stores points with integer IDs. You may attach an arbitrary
26
- Python object per ID when object tracking is enabled.
27
-
28
- Performance characteristics:
29
- Inserts: average O(log n)
30
- Rect queries: average O(log n + k) where k is matches returned
31
- Nearest neighbor: average O(log n)
32
-
33
- Thread-safety:
34
- Instances are not thread-safe. Use external synchronization if you
35
- mutate the same tree from multiple threads.
36
-
37
- Args:
38
- bounds: World bounds as (min_x, min_y, max_x, max_y).
39
- capacity: Max number of points per node before splitting.
40
- max_depth: Optional max tree depth. If omitted, engine decides.
41
- track_objects: Enable id <-> object mapping inside Python.
42
- start_id: Starting auto-assigned id when you omit id on insert.
43
-
44
- Raises:
45
- ValueError: If parameters are invalid or inserts are out of bounds.
46
- """
47
-
48
- __slots__ = (
49
- "_bounds",
50
- "_capacity",
51
- "_count",
52
- "_items",
53
- "_max_depth",
54
- "_native",
55
- "_next_id",
56
- )
57
-
58
- def __init__(
59
- self,
60
- bounds: Bounds,
61
- capacity: int,
62
- *,
63
- max_depth: int | None = None,
64
- track_objects: bool = False,
65
- start_id: int = 1,
66
- ):
67
- self._bounds = bounds
68
- self._max_depth = max_depth # store for clear()
69
- self._capacity = capacity # store for clear()
70
- if max_depth is None:
71
- self._native = _RustQuadTree(self._bounds, self._capacity)
72
- else:
73
- self._native = _RustQuadTree(
74
- self._bounds, self._capacity, max_depth=max_depth
75
- )
76
- self._items: BiMap | None = BiMap() if track_objects else None
77
- self._next_id: int = int(start_id)
78
- self._count: int = 0
79
-
80
- # ---------- inserts ----------
81
-
82
- def insert(self, xy: Point, *, id_: int | None = None, obj: Any = None) -> int:
83
- """
84
- Insert a single point.
85
-
86
- Args:
87
- xy: Point (x, y).
88
- id: Optional integer id. If None, an auto id is assigned.
89
- obj: Optional Python object to associate with id. Stored only if
90
- object tracking is enabled.
91
-
92
- Returns:
93
- The id used for this insert.
94
-
95
- Raises:
96
- ValueError: If the point is outside tree bounds.
97
- """
98
- if id_ is None:
99
- id_ = self._next_id
100
- self._next_id += 1
101
- # ensure future auto-ids do not collide
102
- elif id_ >= self._next_id:
103
- self._next_id = id_ + 1
104
-
105
- if not self._native.insert(id_, xy):
106
- x, y = xy
107
- bx0, by0, bx1, by1 = self._bounds
108
- raise ValueError(
109
- f"Point ({x}, {y}) is outside bounds ({bx0}, {by0}, {bx1}, {by1})"
110
- )
111
-
112
- if self._items is not None:
113
- self._items.add(Item(id_, xy[0], xy[1], obj))
114
-
115
- self._count += 1
116
- return id_
117
-
118
- def insert_many_points(self, points: list[Point]) -> int:
119
- """
120
- Bulk insert points with auto-assigned ids.
121
-
122
- Args:
123
- points: List of (x, y) points.
124
-
125
- Returns:
126
- The number of points inserted
127
- """
128
- start_id = self._next_id
129
- last_id = self._native.insert_many_points(start_id, points)
130
-
131
- num_inserted = last_id - start_id + 1
132
-
133
- if num_inserted < len(points):
134
- raise ValueError("One or more points are outside tree bounds")
135
-
136
- self._next_id = last_id + 1
137
-
138
- # Update the item tracker if needed
139
- if self._items is not None:
140
- for i, id_ in enumerate(range(start_id, last_id + 1)):
141
- x, y = points[i]
142
- self._items.add(Item(id_, x, y, None))
143
-
144
- return num_inserted
145
-
146
- def attach(self, id_: int, obj: Any) -> None:
147
- """
148
- Attach or replace the Python object for an existing id.
149
- Tracking must be enabled.
150
-
151
- Args:
152
- id_: Target id.
153
- obj: Object to associate with id.
154
- """
155
- if self._items is None:
156
- raise ValueError("Cannot attach objects when track_objects=False")
157
-
158
- item = self._items.by_id(id_)
159
- if item is None:
160
- raise KeyError(f"Id {id_} not found in quadtree")
161
- self._items.add(Item(id_, item.x, item.y, obj))
162
-
163
- # ---------- deletes ----------
164
-
165
- def delete(self, id_: int, xy: Point) -> bool:
166
- """
167
- Delete an item by id and exact coordinates.
168
-
169
- Args:
170
- id_: Integer id to remove.
171
- xy: Coordinates (x, y) of the item.
172
-
173
- Returns:
174
- True if the item was found and deleted, else False.
175
- """
176
- deleted = self._native.delete(id_, xy)
177
- if deleted:
178
- self._count -= 1
179
- if self._items is not None:
180
- self._items.pop_id(id_) # ignore result
181
- return deleted
182
-
183
- def delete_by_object(self, obj: Any) -> bool:
184
- """
185
- Delete an item by Python object.
186
-
187
- Requires object tracking to be enabled. Performs an O(1) reverse
188
- lookup to get the id, then deletes that entry at the given location.
189
-
190
- Args:
191
- obj: The tracked Python object to remove.
192
-
193
- Returns:
194
- True if the item was found and deleted, else False.
195
-
196
- Raises:
197
- ValueError: If object tracking is disabled.
198
- """
199
- if self._items is None:
200
- raise ValueError(
201
- "Cannot delete by object when track_objects=False. Use delete(id, xy) instead."
202
- )
203
-
204
- item = self._items.by_obj(obj)
205
- if item is None:
206
- return False
207
-
208
- return self.delete(item.id_, (item.x, item.y))
209
-
210
- def clear(self, *, reset_ids: bool = False) -> None:
211
- """
212
- Empty the tree in place, preserving bounds/capacity/max_depth.
213
-
214
- Args:
215
- reset_ids: If True, restart auto-assigned ids from 1.
216
- """
217
- # swap in a fresh native instance
218
- if self._max_depth is None:
219
- self._native = _RustQuadTree(self._bounds, self._capacity)
220
- else:
221
- self._native = _RustQuadTree(
222
- self._bounds, self._capacity, max_depth=self._max_depth
223
- )
224
-
225
- # reset Python-side trackers
226
- self._count = 0
227
- if self._items is not None:
228
- self._items.clear()
229
- if reset_ids:
230
- self._next_id = 1
231
-
232
- # ---------- queries ----------
233
-
234
- @overload
235
- def query(
236
- self, rect: Bounds, *, as_items: Literal[False] = ...
237
- ) -> list[_IdCoord]: ...
238
-
239
- @overload
240
- def query(self, rect: Bounds, *, as_items: Literal[True]) -> list[Item]: ...
241
-
242
- def query(
243
- self, rect: Bounds, *, as_items: bool = False
244
- ) -> list[_IdCoord] | list[Item]:
245
- """
246
- Return all points inside an axis-aligned rectangle.
247
-
248
- Args:
249
- rect: Query rectangle as (min_x, min_y, max_x, max_y).
250
- as_items: If True, return Item wrappers. If False, return raw tuples.
251
-
252
- Returns:
253
- If as_items is False: list of (id, x, y) tuples.
254
- If as_items is True: list of Item objects.
255
- """
256
- raw = self._native.query(rect)
257
- if not as_items:
258
- return raw
259
-
260
- if self._items is None:
261
- raise ValueError("Cannot return results as items with track_objects=False")
262
- out: list[Item] = []
263
- for id_, _, _ in raw:
264
- item = self._items.by_id(id_)
265
- if item is None:
266
- raise RuntimeError(
267
- f"Internal error: id {id_} found in native tree but missing from object tracker. "
268
- f"Ensure all inserts/deletes are done via this wrapper."
269
- )
270
- out.append(item)
271
- return out
272
-
273
- @overload
274
- def nearest_neighbor(
275
- self, xy: Point, *, as_item: Literal[False] = ...
276
- ) -> _IdCoord | None: ...
277
-
278
- @overload
279
- def nearest_neighbor(self, xy: Point, *, as_item: Literal[True]) -> Item | None: ...
280
-
281
- def nearest_neighbor(self, xy: Point, *, as_item: bool = False):
282
- """
283
- Return the single nearest neighbor to the query point.
284
-
285
- Args:
286
- xy: Query point (x, y).
287
- as_item: If True, return Item. If False, return (id, x, y).
288
-
289
- Returns:
290
- The nearest neighbor or None if the tree is empty.
291
- """
292
- t = self._native.nearest_neighbor(xy)
293
- if t is None or not as_item:
294
- return t
295
-
296
- if self._items is None:
297
- raise ValueError("Cannot return result as item with track_objects=False")
298
- id_, _x, _y = t
299
- item = self._items.by_id(id_)
300
- if item is None:
301
- raise RuntimeError(
302
- f"Internal error: id {id_} found in native tree but missing from object tracker. "
303
- f"Ensure all inserts/deletes are done via this wrapper."
304
- )
305
- return item
306
-
307
- @overload
308
- def nearest_neighbors(
309
- self, xy: Point, k: int, *, as_items: Literal[False] = ...
310
- ) -> list[_IdCoord]: ...
311
-
312
- @overload
313
- def nearest_neighbors(
314
- self, xy: Point, k: int, *, as_items: Literal[True]
315
- ) -> list[Item]: ...
316
-
317
- def nearest_neighbors(self, xy: Point, k: int, *, as_items: bool = False):
318
- """
319
- Return the k nearest neighbors to the query point.
320
-
321
- Args:
322
- xy: Query point (x, y).
323
- k: Number of neighbors to return.
324
- as_items: If True, return Item wrappers. If False, return raw tuples.
325
-
326
- Returns:
327
- List of results in ascending distance order.
328
- """
329
- raw = self._native.nearest_neighbors(xy, k)
330
- if not as_items:
331
- return raw
332
- if self._items is None:
333
- raise ValueError("Cannot return results as items with track_objects=False")
334
-
335
- out: list[Item] = []
336
- for id_, _, _ in raw:
337
- item = self._items.by_id(id_)
338
- if item is None:
339
- raise RuntimeError(
340
- f"Internal error: id {id_} found in native tree but missing from object tracker. "
341
- f"Ensure all inserts/deletes are done via this wrapper."
342
- )
343
- out.append(item)
344
- return out
345
-
346
- # ---------- misc ----------
347
-
348
- def get(self, id_: int) -> Any | None:
349
- """
350
- Return the object associated with id.
351
-
352
- Returns:
353
- The tracked object if present and tracking is enabled, else None.
354
- """
355
- if self._items is None:
356
- raise ValueError("Cannot get objects when track_objects=False")
357
- item = self._items.by_id(id_)
358
- if item is None:
359
- return None
360
- return item.obj
361
-
362
- def get_all_rectangles(self) -> list[Bounds]:
363
- """
364
- Return all node rectangles in the current quadtree.
365
-
366
- Returns:
367
- List of (min_x, min_y, max_x, max_y) for each node in the tree.
368
- """
369
- return self._native.get_all_rectangles()
370
-
371
- def get_all_objects(self) -> list[Any]:
372
- """
373
- Return all tracked objects.
374
-
375
- Returns:
376
- List of objects if tracking is enabled, else an empty list.
377
- """
378
- if self._items is None:
379
- raise ValueError("Cannot get objects when track_objects=False")
380
- return [t.obj for t in self._items.items() if t.obj is not None]
381
-
382
- def get_all_items(self) -> list[Item]:
383
- """
384
- Return all tracked items.
385
-
386
- Returns:
387
- List of Item if tracking is enabled, else an empty list.
388
- """
389
- if self._items is None:
390
- raise ValueError("Cannot get items when track_objects=False")
391
- return list(self._items.items())
392
-
393
- def count_items(self) -> int:
394
- """
395
- Return the number of items stored in the native tree.
396
-
397
- Notes:
398
- This calls the native engine and may differ from len(self) if
399
- you create multiple wrappers around the same native structure.
400
- """
401
- return self._native.count_items()
402
-
403
- def __len__(self) -> int:
404
- """
405
- Return the number of successful inserts done via this wrapper.
406
-
407
- Notes:
408
- This is the Python-side counter that tracks calls that returned True.
409
- use count_items() to get the authoritative native-side count.
410
- """
411
- return self._count
412
-
413
- # Power users can access the raw class
414
- NativeQuadTree = _RustQuadTree
415
-
416
-
417
- __all__ = ["Bounds", "Item", "Point", "QuadTree"]
5
+ __all__ = ["Item", "PointItem", "QuadTree", "RectItem", "RectQuadTree"]
@@ -0,0 +1,263 @@
1
+ # _abc_quadtree.py
2
+ from __future__ import annotations
3
+
4
+ from abc import ABC, abstractmethod
5
+ from typing import Any, Generic, Tuple, TypeVar
6
+
7
+ from ._bimap import BiMap
8
+ from ._item import Item # base class for PointItem and RectItem
9
+
10
+ Bounds = Tuple[float, float, float, float]
11
+
12
+ # Generic parameters
13
+ G = TypeVar("G") # geometry type, e.g. Point or Bounds
14
+ HitT = TypeVar("HitT") # raw native tuple, e.g. (id,x,y) or (id,x0,y0,x1,y1)
15
+ ItemType = TypeVar(
16
+ "ItemType", bound=Item
17
+ ) # Python Item subtype, e.g. PointItem or RectItem
18
+
19
+
20
+ class _BaseQuadTree(Generic[G, HitT, ItemType], ABC):
21
+ """
22
+ Shared logic for Python QuadTree wrappers over native Rust engines.
23
+
24
+ Concrete subclasses must implement the few native hooks and item builders.
25
+ """
26
+
27
+ __slots__ = (
28
+ "_bounds",
29
+ "_capacity",
30
+ "_count",
31
+ "_items",
32
+ "_max_depth",
33
+ "_native",
34
+ "_next_id",
35
+ "_track_objects",
36
+ )
37
+
38
+ # ---- required native hooks ----
39
+
40
+ @abstractmethod
41
+ def _new_native(self, bounds: Bounds, capacity: int, max_depth: int | None) -> Any:
42
+ """Create the native engine instance."""
43
+
44
+ @abstractmethod
45
+ def _make_item(self, id_: int, geom: G, obj: Any | None) -> ItemType:
46
+ """Build an ItemType from id, geometry, and optional object."""
47
+
48
+ # ---- ctor ----
49
+
50
+ def __init__(
51
+ self,
52
+ bounds: Bounds,
53
+ capacity: int,
54
+ *,
55
+ max_depth: int | None = None,
56
+ track_objects: bool = False,
57
+ start_id: int = 1,
58
+ ):
59
+ self._bounds = bounds
60
+ self._max_depth = max_depth
61
+ self._capacity = capacity
62
+ self._native = self._new_native(bounds, capacity, max_depth)
63
+ # typed item map if tracking enabled
64
+ self._track_objects = bool(track_objects)
65
+ self._items: BiMap[ItemType] | None = BiMap() if track_objects else None
66
+ self._next_id = int(start_id)
67
+ self._count = 0
68
+
69
+ # ---- shared helpers ----
70
+
71
+ def _alloc_id(self, id_: int | None) -> int:
72
+ if id_ is None:
73
+ nid = self._next_id
74
+ self._next_id += 1
75
+ return nid
76
+ if id_ >= self._next_id:
77
+ self._next_id = id_ + 1
78
+ return id_
79
+
80
+ def insert(self, geom: G, *, id_: int | None = None, obj: Any = None) -> int:
81
+ """
82
+ Insert a single item
83
+
84
+ Args:
85
+ geom: Point (x, y) or Rect (x0, y0, x1, y1) depending on quadtree type.
86
+ id_: Optional integer id. If None, an auto id is assigned.
87
+ obj: Optional Python object to associate with id. Stored only if
88
+ object tracking is enabled.
89
+
90
+ Returns:
91
+ The id used for this insert.
92
+
93
+ Raises:
94
+ ValueError: If the point is outside tree bounds.
95
+ """
96
+ use_id = self._alloc_id(id_)
97
+ if not self._native.insert(use_id, geom):
98
+ bx0, by0, bx1, by1 = self._bounds
99
+ raise ValueError(
100
+ f"Geometry {geom!r} is outside bounds ({bx0}, {by0}, {bx1}, {by1})"
101
+ )
102
+
103
+ if self._items is not None:
104
+ self._items.add(self._make_item(use_id, geom, obj))
105
+
106
+ self._count += 1
107
+ return use_id
108
+
109
+ def insert_many(self, geoms: list[G]) -> int:
110
+ """
111
+ Bulk insert items with auto-assigned ids. Faster than inserting one at a time.
112
+
113
+ Args:
114
+ geoms: List of geometries. Either Points (x, y) or Rects (x0, y0, x1, y1) depending on quadtree type.
115
+
116
+ Returns:
117
+ The number of items inserted
118
+ """
119
+ start_id = self._next_id
120
+ last_id = self._native.insert_many(start_id, geoms)
121
+ num = last_id - start_id + 1
122
+ if num < len(geoms):
123
+ raise ValueError("One or more items are outside tree bounds")
124
+
125
+ self._next_id = last_id + 1
126
+ if self._items is not None:
127
+ for i, id_ in enumerate(range(start_id, last_id + 1)):
128
+ self._items.add(self._make_item(id_, geoms[i], None))
129
+ self._count += num
130
+ return num
131
+
132
+ def delete(self, id_: int, geom: G) -> bool:
133
+ """
134
+ Delete an item by id and exact geometry.
135
+
136
+ Args:
137
+ id_: Integer id to remove.
138
+ geom: Exact geometry to remove. Either Point (x, y) or Rect (x0, y0, x1, y1) depending on quadtree type.
139
+
140
+ Returns:
141
+ True if the item was found and deleted, else False.
142
+ """
143
+ deleted = self._native.delete(id_, geom)
144
+ if deleted:
145
+ self._count -= 1
146
+ if self._items is not None:
147
+ self._items.pop_id(id_)
148
+ return deleted
149
+
150
+ def attach(self, id_: int, obj: Any) -> None:
151
+ """
152
+ Attach or replace the Python object for an existing id.
153
+ Tracking must be enabled.
154
+
155
+ Args:
156
+ id_: Target id.
157
+ obj: Object to associate with id.
158
+ """
159
+ if self._items is None:
160
+ raise ValueError("Cannot attach objects when track_objects=False")
161
+ it = self._items.by_id(id_)
162
+ if it is None:
163
+ raise KeyError(f"Id {id_} not found in quadtree")
164
+ # Preserve geometry from existing item
165
+ self._items.add(self._make_item(id_, it.geom, obj)) # type: ignore[attr-defined]
166
+
167
+ def delete_by_object(self, obj: Any) -> bool:
168
+ """
169
+ Delete an item by Python object.
170
+
171
+ Requires object tracking to be enabled. Performs an O(1) reverse
172
+ lookup to get the id, then deletes that entry at the given location.
173
+
174
+ Args:
175
+ obj: The tracked Python object to remove.
176
+
177
+ Returns:
178
+ True if the item was found and deleted, else False.
179
+
180
+ Raises:
181
+ ValueError: If object tracking is disabled.
182
+ """
183
+ if self._items is None:
184
+ raise ValueError("Cannot delete by object when track_objects=False")
185
+ it = self._items.by_obj(obj)
186
+ if it is None:
187
+ return False
188
+ # type of geom is determined by concrete Item subtype
189
+ return self.delete(it.id_, it.geom) # type: ignore[arg-type]
190
+
191
+ def clear(self, *, reset_ids: bool = False) -> None:
192
+ """
193
+ Empty the tree in place, preserving bounds/capacity/max_depth.
194
+
195
+ Args:
196
+ reset_ids: If True, restart auto-assigned ids from 1.
197
+ """
198
+ self._native = self._new_native(self._bounds, self._capacity, self._max_depth)
199
+ self._count = 0
200
+ if self._items is not None:
201
+ self._items.clear()
202
+ if reset_ids:
203
+ self._next_id = 1
204
+
205
+ def get_all_objects(self) -> list[Any]:
206
+ """
207
+ Return all tracked Python objects in the tree.
208
+
209
+ Returns:
210
+ List of objects.
211
+ Raises:
212
+ ValueError: If object tracking is disabled.
213
+ """
214
+ if self._items is None:
215
+ raise ValueError("Cannot get objects when track_objects=False")
216
+ return [t.obj for t in self._items.items() if t.obj is not None]
217
+
218
+ def get_all_items(self) -> list[ItemType]:
219
+ """
220
+ Return all Item wrappers in the tree.
221
+
222
+ Returns:
223
+ List of Item objects.
224
+ Raises:
225
+ ValueError: If object tracking is disabled.
226
+ """
227
+ if self._items is None:
228
+ raise ValueError("Cannot get items when track_objects=False")
229
+ return list(self._items.items())
230
+
231
+ def get_all_node_boundaries(self) -> list[Bounds]:
232
+ """
233
+ Return all node boundaries in the tree. Great for visualizing the tree structure.
234
+
235
+ Returns:
236
+ List of (min_x, min_y, max_x, max_y) for each node in the tree.
237
+ """
238
+ return self._native.get_all_node_boundaries()
239
+
240
+ def get(self, id_: int) -> Any | None:
241
+ """
242
+ Return the object associated with id, if tracking is enabled.
243
+ """
244
+ if self._items is None:
245
+ raise ValueError("Cannot get objects when track_objects=False")
246
+ item = self._items.by_id(id_)
247
+ return None if item is None else item.obj
248
+
249
+ def count_items(self) -> int:
250
+ """
251
+ Return the number of items currently in the tree.
252
+
253
+ Note:
254
+ Performs a full scan of tree to count up every item.
255
+ Use the `len()` function or `len(tree)` for O(1) access.
256
+
257
+ Returns:
258
+ Number of items in the tree.
259
+ """
260
+ return self._native.count_items()
261
+
262
+ def __len__(self) -> int:
263
+ return self._count