nrl-tracker 1.2.0__py3-none-any.whl → 1.3.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.
- {nrl_tracker-1.2.0.dist-info → nrl_tracker-1.3.0.dist-info}/METADATA +1 -1
- {nrl_tracker-1.2.0.dist-info → nrl_tracker-1.3.0.dist-info}/RECORD +15 -12
- pytcl/__init__.py +1 -1
- pytcl/atmosphere/__init__.py +32 -1
- pytcl/atmosphere/ionosphere.py +512 -0
- pytcl/containers/rtree.py +199 -0
- pytcl/dynamic_estimation/kalman/square_root.py +52 -571
- pytcl/dynamic_estimation/kalman/sr_ukf.py +302 -0
- pytcl/dynamic_estimation/kalman/ud_filter.py +404 -0
- pytcl/magnetism/__init__.py +7 -0
- pytcl/magnetism/wmm.py +260 -23
- pytcl/mathematical_functions/special_functions/debye.py +132 -26
- {nrl_tracker-1.2.0.dist-info → nrl_tracker-1.3.0.dist-info}/LICENSE +0 -0
- {nrl_tracker-1.2.0.dist-info → nrl_tracker-1.3.0.dist-info}/WHEEL +0 -0
- {nrl_tracker-1.2.0.dist-info → nrl_tracker-1.3.0.dist-info}/top_level.txt +0 -0
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.
|