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/_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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastquadtree
3
- Version: 0.7.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>=7.0 ; extra == 'dev'
16
- Requires-Dist: pytest-cov>=4.1 ; extra == 'dev'
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
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/fastquadtree?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=BLUE&left_text=Total+Downloads)](https://pepy.tech/projects/fastquadtree)
47
48
 
48
- [![Build](https://github.com/Elan456/fastquadtree/actions/workflows/release.yml/badge.svg)](https://github.com/Elan456/fastquadtree/actions/workflows/ci.yml)
49
+ [![Build](https://github.com/Elan456/fastquadtree/actions/workflows/release.yml/badge.svg)](https://github.com/Elan456/fastquadtree/actions/workflows/release.yml)
49
50
  [![Codecov](https://codecov.io/gh/Elan456/fastquadtree/branch/main/graph/badge.svg)](https://codecov.io/gh/Elan456/fastquadtree)
50
51
 
51
52
  [![Rust core via PyO3](https://img.shields.io/badge/Rust-core%20via%20PyO3-orange)](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
- #### Benchmark Configuration
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
- [Full api for QuadTree](https://elan456.github.io/fastquadtree/api/quadtree/)
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
- ### Core Methods
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
- - `delete_by_object(obj) -> bool (requires track_objects=True)`
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 only maintains an ID -> Obj map only if the quadtree was constructed with `track_objects=True`. If you don't need it, leave it off for best performance. Look at the [Native vs Shim Benchmark](#native-vs-shim-benchmark) below for details.
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
- ```bash
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
- 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.
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