nrl-tracker 1.1.3__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.1.3.dist-info → nrl_tracker-1.3.0.dist-info}/METADATA +1 -1
- {nrl_tracker-1.1.3.dist-info → nrl_tracker-1.3.0.dist-info}/RECORD +28 -24
- pytcl/__init__.py +1 -1
- pytcl/astronomical/reference_frames.py +127 -55
- pytcl/atmosphere/__init__.py +32 -1
- pytcl/atmosphere/ionosphere.py +512 -0
- pytcl/containers/__init__.py +24 -0
- pytcl/containers/base.py +219 -0
- pytcl/containers/covertree.py +21 -26
- pytcl/containers/kd_tree.py +94 -29
- pytcl/containers/rtree.py +199 -0
- pytcl/containers/vptree.py +17 -26
- pytcl/core/__init__.py +18 -0
- pytcl/core/validation.py +331 -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/gravity/egm.py +13 -0
- pytcl/gravity/spherical_harmonics.py +97 -36
- pytcl/magnetism/__init__.py +7 -0
- pytcl/magnetism/wmm.py +260 -23
- pytcl/mathematical_functions/special_functions/debye.py +132 -26
- pytcl/mathematical_functions/special_functions/hypergeometric.py +79 -15
- pytcl/navigation/geodesy.py +245 -159
- pytcl/navigation/great_circle.py +98 -16
- {nrl_tracker-1.1.3.dist-info → nrl_tracker-1.3.0.dist-info}/LICENSE +0 -0
- {nrl_tracker-1.1.3.dist-info → nrl_tracker-1.3.0.dist-info}/WHEEL +0 -0
- {nrl_tracker-1.1.3.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.
|
pytcl/containers/vptree.py
CHANGED
|
@@ -11,11 +11,17 @@ References
|
|
|
11
11
|
neighbor search in general metric spaces," SODA 1993.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
import logging
|
|
14
15
|
from typing import Callable, List, NamedTuple, Optional, Tuple
|
|
15
16
|
|
|
16
17
|
import numpy as np
|
|
17
18
|
from numpy.typing import ArrayLike, NDArray
|
|
18
19
|
|
|
20
|
+
from pytcl.containers.base import MetricSpatialIndex, validate_query_input
|
|
21
|
+
|
|
22
|
+
# Module logger
|
|
23
|
+
_logger = logging.getLogger("pytcl.containers.vptree")
|
|
24
|
+
|
|
19
25
|
|
|
20
26
|
class VPTreeResult(NamedTuple):
|
|
21
27
|
"""Result of VP-tree query.
|
|
@@ -56,7 +62,7 @@ class VPNode:
|
|
|
56
62
|
self.right: Optional["VPNode"] = None
|
|
57
63
|
|
|
58
64
|
|
|
59
|
-
class VPTree:
|
|
65
|
+
class VPTree(MetricSpatialIndex):
|
|
60
66
|
"""
|
|
61
67
|
Vantage Point Tree for metric space nearest neighbor search.
|
|
62
68
|
|
|
@@ -89,6 +95,11 @@ class VPTree:
|
|
|
89
95
|
|
|
90
96
|
Query complexity is O(log n) on average but can degrade to O(n)
|
|
91
97
|
for pathological distance distributions.
|
|
98
|
+
|
|
99
|
+
See Also
|
|
100
|
+
--------
|
|
101
|
+
MetricSpatialIndex : Abstract base class for metric-based spatial indices.
|
|
102
|
+
CoverTree : Alternative metric space index with theoretical guarantees.
|
|
92
103
|
"""
|
|
93
104
|
|
|
94
105
|
def __init__(
|
|
@@ -96,25 +107,13 @@ class VPTree:
|
|
|
96
107
|
data: ArrayLike,
|
|
97
108
|
metric: Optional[Callable[[NDArray, NDArray], float]] = None,
|
|
98
109
|
):
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if self.data.ndim != 2:
|
|
102
|
-
raise ValueError("Data must be 2-dimensional")
|
|
103
|
-
|
|
104
|
-
self.n_samples, self.n_features = self.data.shape
|
|
105
|
-
|
|
106
|
-
if metric is None:
|
|
107
|
-
self.metric = self._euclidean_distance
|
|
108
|
-
else:
|
|
109
|
-
self.metric = metric
|
|
110
|
+
super().__init__(data, metric)
|
|
110
111
|
|
|
111
112
|
# Build tree
|
|
112
113
|
indices = np.arange(self.n_samples)
|
|
113
114
|
self.root = self._build_tree(indices)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"""Default Euclidean distance metric."""
|
|
117
|
-
return float(np.sqrt(np.sum((x - y) ** 2)))
|
|
115
|
+
metric_name = metric.__name__ if metric else "euclidean"
|
|
116
|
+
_logger.debug("VPTree built with metric=%s", metric_name)
|
|
118
117
|
|
|
119
118
|
def _build_tree(self, indices: NDArray[np.intp]) -> Optional[VPNode]:
|
|
120
119
|
"""Recursively build the VP-tree."""
|
|
@@ -169,11 +168,7 @@ class VPTree:
|
|
|
169
168
|
result : VPTreeResult
|
|
170
169
|
Indices and distances of k nearest neighbors.
|
|
171
170
|
"""
|
|
172
|
-
X =
|
|
173
|
-
|
|
174
|
-
if X.ndim == 1:
|
|
175
|
-
X = X.reshape(1, -1)
|
|
176
|
-
|
|
171
|
+
X = validate_query_input(X, self.n_features)
|
|
177
172
|
n_queries = X.shape[0]
|
|
178
173
|
|
|
179
174
|
all_indices = np.zeros((n_queries, k), dtype=np.intp)
|
|
@@ -259,11 +254,7 @@ class VPTree:
|
|
|
259
254
|
indices : list of lists
|
|
260
255
|
For each query, list of indices within radius.
|
|
261
256
|
"""
|
|
262
|
-
X =
|
|
263
|
-
|
|
264
|
-
if X.ndim == 1:
|
|
265
|
-
X = X.reshape(1, -1)
|
|
266
|
-
|
|
257
|
+
X = validate_query_input(X, self.n_features)
|
|
267
258
|
n_queries = X.shape[0]
|
|
268
259
|
results: List[List[int]] = []
|
|
269
260
|
|
pytcl/core/__init__.py
CHANGED
|
@@ -24,10 +24,19 @@ from pytcl.core.constants import (
|
|
|
24
24
|
PhysicalConstants,
|
|
25
25
|
)
|
|
26
26
|
from pytcl.core.validation import (
|
|
27
|
+
ArraySpec,
|
|
28
|
+
ScalarSpec,
|
|
29
|
+
ValidationError,
|
|
30
|
+
check_compatible_shapes,
|
|
27
31
|
ensure_2d,
|
|
28
32
|
ensure_column_vector,
|
|
33
|
+
ensure_positive_definite,
|
|
29
34
|
ensure_row_vector,
|
|
35
|
+
ensure_square_matrix,
|
|
36
|
+
ensure_symmetric,
|
|
30
37
|
validate_array,
|
|
38
|
+
validate_inputs,
|
|
39
|
+
validate_same_shape,
|
|
31
40
|
)
|
|
32
41
|
|
|
33
42
|
__all__ = [
|
|
@@ -40,10 +49,19 @@ __all__ = [
|
|
|
40
49
|
"WGS84",
|
|
41
50
|
"PhysicalConstants",
|
|
42
51
|
# Validation
|
|
52
|
+
"ValidationError",
|
|
43
53
|
"validate_array",
|
|
54
|
+
"validate_inputs",
|
|
55
|
+
"validate_same_shape",
|
|
56
|
+
"check_compatible_shapes",
|
|
57
|
+
"ArraySpec",
|
|
58
|
+
"ScalarSpec",
|
|
44
59
|
"ensure_2d",
|
|
45
60
|
"ensure_column_vector",
|
|
46
61
|
"ensure_row_vector",
|
|
62
|
+
"ensure_square_matrix",
|
|
63
|
+
"ensure_symmetric",
|
|
64
|
+
"ensure_positive_definite",
|
|
47
65
|
# Array utilities
|
|
48
66
|
"wrap_to_pi",
|
|
49
67
|
"wrap_to_2pi",
|