nrl-tracker 1.2.0__py3-none-any.whl → 1.4.0__py3-none-any.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.
pytcl/containers/rtree.py CHANGED
@@ -13,11 +13,17 @@ References
13
13
  Method for Points and Rectangles," ACM SIGMOD, 1990.
14
14
  """
15
15
 
16
+ import logging
16
17
  from typing import List, NamedTuple, Optional, Tuple
17
18
 
18
19
  import numpy as np
19
20
  from numpy.typing import ArrayLike, NDArray
20
21
 
22
+ from pytcl.containers.base import SpatialQueryResult, validate_query_input
23
+
24
+ # Module logger
25
+ _logger = logging.getLogger("pytcl.containers.rtree")
26
+
21
27
 
22
28
  class BoundingBox(NamedTuple):
23
29
  """Axis-aligned bounding box.
@@ -160,6 +166,9 @@ class RTree:
160
166
  An R-tree groups nearby objects and represents them with their
161
167
  minimum bounding rectangle. This allows efficient spatial queries.
162
168
 
169
+ Unlike KDTree and BallTree which only index points, RTree can index
170
+ bounding boxes of arbitrary size. It also supports dynamic insertion.
171
+
163
172
  Parameters
164
173
  ----------
165
174
  max_entries : int, optional
@@ -167,6 +176,13 @@ class RTree:
167
176
  min_entries : int, optional
168
177
  Minimum entries per node (except root). Default max_entries // 2.
169
178
 
179
+ Attributes
180
+ ----------
181
+ n_entries : int
182
+ Number of entries in the tree.
183
+ n_features : int
184
+ Dimensionality of the data (set after first insertion).
185
+
170
186
  Examples
171
187
  --------
172
188
  >>> tree = RTree()
@@ -179,6 +195,11 @@ class RTree:
179
195
  -----
180
196
  This implementation uses a simplified insertion algorithm.
181
197
  For production use, consider using R*-tree or packed R-tree variants.
198
+
199
+ See Also
200
+ --------
201
+ KDTree : Point-based spatial index using axis-aligned splits.
202
+ BallTree : Point-based spatial index using hyperspheres.
182
203
  """
183
204
 
184
205
  def __init__(
@@ -190,11 +211,70 @@ class RTree:
190
211
  self.min_entries = min_entries or max_entries // 2
191
212
  self.root = RTreeNode(is_leaf=True)
192
213
  self.n_entries = 0
214
+ self.n_features: Optional[int] = None
193
215
  self._data: List[BoundingBox] = []
216
+ self._points: Optional[NDArray[np.floating]] = None
217
+ _logger.debug("RTree initialized with max_entries=%d", max_entries)
218
+
219
+ @classmethod
220
+ def from_points(
221
+ cls,
222
+ data: ArrayLike,
223
+ max_entries: int = 10,
224
+ min_entries: Optional[int] = None,
225
+ ) -> "RTree":
226
+ """
227
+ Create an RTree from point data.
228
+
229
+ This factory method provides an interface similar to KDTree and BallTree,
230
+ allowing RTree to be used interchangeably for point queries.
231
+
232
+ Parameters
233
+ ----------
234
+ data : array_like
235
+ Data points of shape (n_samples, n_features).
236
+ max_entries : int, optional
237
+ Maximum entries per node. Default 10.
238
+ min_entries : int, optional
239
+ Minimum entries per node. Default max_entries // 2.
240
+
241
+ Returns
242
+ -------
243
+ tree : RTree
244
+ RTree with all points inserted.
245
+
246
+ Examples
247
+ --------
248
+ >>> points = np.array([[0, 0], [1, 0], [0, 1], [1, 1]])
249
+ >>> tree = RTree.from_points(points)
250
+ >>> result = tree.query([[0.1, 0.1]], k=2)
251
+ """
252
+ data = np.asarray(data, dtype=np.float64)
253
+ if data.ndim != 2:
254
+ raise ValueError(
255
+ f"Data must be 2-dimensional (n_samples, n_features), "
256
+ f"got shape {data.shape}"
257
+ )
258
+
259
+ tree = cls(max_entries=max_entries, min_entries=min_entries)
260
+ tree._points = data
261
+ tree.n_features = data.shape[1]
262
+ tree.insert_points(data)
263
+ _logger.debug(
264
+ "RTree.from_points: indexed %d points in %d dimensions",
265
+ data.shape[0],
266
+ data.shape[1],
267
+ )
268
+ return tree
194
269
 
195
270
  def __len__(self) -> int:
196
271
  return self.n_entries
197
272
 
273
+ def __repr__(self) -> str:
274
+ if self.n_features is not None:
275
+ return f"RTree(n_entries={self.n_entries}, n_features={self.n_features})"
276
+ return f"RTree(n_entries={self.n_entries})"
277
+
198
278
  def insert(self, bbox: BoundingBox, data_index: Optional[int] = None) -> int:
199
279
  """
200
280
  Insert a bounding box into the tree.
@@ -214,6 +294,10 @@ class RTree:
214
294
  if data_index is None:
215
295
  data_index = self.n_entries
216
296
 
297
+ # Track dimensionality
298
+ if self.n_features is None:
299
+ self.n_features = len(bbox.min_coords)
300
+
217
301
  self._data.append(bbox)
218
302
 
219
303
  # Find leaf to insert into
@@ -330,6 +414,121 @@ class RTree:
330
414
  current.update_bbox()
331
415
  current = current.parent
332
416
 
417
+ def query(
418
+ self,
419
+ X: ArrayLike,
420
+ k: int = 1,
421
+ ) -> SpatialQueryResult:
422
+ """
423
+ Query the tree for k nearest neighbors.
424
+
425
+ This method provides API compatibility with KDTree and BallTree.
426
+
427
+ Parameters
428
+ ----------
429
+ X : array_like
430
+ Query points of shape (n_queries, n_features) or (n_features,).
431
+ k : int, optional
432
+ Number of nearest neighbors. Default 1.
433
+
434
+ Returns
435
+ -------
436
+ result : SpatialQueryResult
437
+ Indices and distances of k nearest neighbors for each query.
438
+
439
+ Examples
440
+ --------
441
+ >>> tree = RTree.from_points(np.array([[0, 0], [1, 1], [2, 2]]))
442
+ >>> result = tree.query([[0.5, 0.5]], k=2)
443
+ >>> result.indices
444
+ array([[0, 1]])
445
+ """
446
+ if self.n_features is None:
447
+ raise ValueError("Cannot query empty RTree")
448
+
449
+ X = validate_query_input(X, self.n_features)
450
+ n_queries = X.shape[0]
451
+ _logger.debug("RTree.query: %d queries, k=%d", n_queries, k)
452
+
453
+ all_indices = np.zeros((n_queries, k), dtype=np.intp)
454
+ all_distances = np.full((n_queries, k), np.inf)
455
+
456
+ for i in range(n_queries):
457
+ indices, distances = self.nearest(X[i], k=k)
458
+ n_found = len(indices)
459
+ if n_found > 0:
460
+ all_indices[i, :n_found] = indices
461
+ all_distances[i, :n_found] = distances
462
+
463
+ return SpatialQueryResult(indices=all_indices, distances=all_distances)
464
+
465
+ def query_radius(
466
+ self,
467
+ X: ArrayLike,
468
+ r: float,
469
+ ) -> List[List[int]]:
470
+ """
471
+ Query the tree for all points within radius r.
472
+
473
+ This method provides API compatibility with KDTree and BallTree.
474
+
475
+ Parameters
476
+ ----------
477
+ X : array_like
478
+ Query points of shape (n_queries, n_features) or (n_features,).
479
+ r : float
480
+ Query radius.
481
+
482
+ Returns
483
+ -------
484
+ indices : list of lists
485
+ For each query, a list of indices of points within radius r.
486
+
487
+ Examples
488
+ --------
489
+ >>> tree = RTree.from_points(np.array([[0, 0], [1, 0], [0, 1], [5, 5]]))
490
+ >>> tree.query_radius([[0, 0]], r=1.5)
491
+ [[0, 1, 2]]
492
+ """
493
+ if self.n_features is None:
494
+ raise ValueError("Cannot query empty RTree")
495
+
496
+ X = validate_query_input(X, self.n_features)
497
+ n_queries = X.shape[0]
498
+ results: List[List[int]] = []
499
+
500
+ for i in range(n_queries):
501
+ query = X[i]
502
+ indices: List[int] = []
503
+
504
+ def search(node: RTreeNode) -> None:
505
+ if node.bbox is None:
506
+ return
507
+
508
+ # Minimum distance from query point to node's bounding box
509
+ clamped = np.clip(query, node.bbox.min_coords, node.bbox.max_coords)
510
+ min_dist = float(np.sqrt(np.sum((query - clamped) ** 2)))
511
+
512
+ # Prune if node is entirely outside radius
513
+ if min_dist > r:
514
+ return
515
+
516
+ if node.is_leaf:
517
+ for bbox, idx in node.entries:
518
+ # Distance to point (center of zero-volume box)
519
+ clamped_pt = np.clip(query, bbox.min_coords, bbox.max_coords)
520
+ dist = float(np.sqrt(np.sum((query - clamped_pt) ** 2)))
521
+ if dist <= r:
522
+ indices.append(idx)
523
+ else:
524
+ for child in node.children:
525
+ search(child)
526
+
527
+ search(self.root)
528
+ results.append(indices)
529
+
530
+ return results
531
+
333
532
  def query_intersect(self, query_bbox: BoundingBox) -> RTreeResult:
334
533
  """
335
534
  Find all entries intersecting a query box.