nrl-tracker 1.1.3__py3-none-any.whl → 1.2.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.2.0.dist-info}/METADATA +1 -1
- {nrl_tracker-1.1.3.dist-info → nrl_tracker-1.2.0.dist-info}/RECORD +19 -18
- pytcl/__init__.py +1 -1
- pytcl/astronomical/reference_frames.py +127 -55
- 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/vptree.py +17 -26
- pytcl/core/__init__.py +18 -0
- pytcl/core/validation.py +331 -0
- pytcl/gravity/egm.py +13 -0
- pytcl/gravity/spherical_harmonics.py +97 -36
- 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.2.0.dist-info}/LICENSE +0 -0
- {nrl_tracker-1.1.3.dist-info → nrl_tracker-1.2.0.dist-info}/WHEEL +0 -0
- {nrl_tracker-1.1.3.dist-info → nrl_tracker-1.2.0.dist-info}/top_level.txt +0 -0
pytcl/containers/covertree.py
CHANGED
|
@@ -11,11 +11,17 @@ References
|
|
|
11
11
|
neighbor," ICML 2006.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
import logging
|
|
14
15
|
from typing import Callable, List, NamedTuple, Optional, Set, 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.covertree")
|
|
24
|
+
|
|
19
25
|
|
|
20
26
|
class CoverTreeResult(NamedTuple):
|
|
21
27
|
"""Result of Cover tree query.
|
|
@@ -60,7 +66,7 @@ class CoverTreeNode:
|
|
|
60
66
|
self.children[level].append(child)
|
|
61
67
|
|
|
62
68
|
|
|
63
|
-
class CoverTree:
|
|
69
|
+
class CoverTree(MetricSpatialIndex):
|
|
64
70
|
"""
|
|
65
71
|
Cover Tree for metric space nearest neighbor search.
|
|
66
72
|
|
|
@@ -93,6 +99,11 @@ class CoverTree:
|
|
|
93
99
|
|
|
94
100
|
The implementation uses a simplified version of the original
|
|
95
101
|
algorithm for clarity.
|
|
102
|
+
|
|
103
|
+
See Also
|
|
104
|
+
--------
|
|
105
|
+
MetricSpatialIndex : Abstract base class for metric-based spatial indices.
|
|
106
|
+
VPTree : Alternative metric space index using vantage points.
|
|
96
107
|
"""
|
|
97
108
|
|
|
98
109
|
def __init__(
|
|
@@ -101,19 +112,9 @@ class CoverTree:
|
|
|
101
112
|
metric: Optional[Callable[[NDArray, NDArray], float]] = None,
|
|
102
113
|
base: float = 2.0,
|
|
103
114
|
):
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if self.data.ndim != 2:
|
|
107
|
-
raise ValueError("Data must be 2-dimensional")
|
|
108
|
-
|
|
109
|
-
self.n_samples, self.n_features = self.data.shape
|
|
115
|
+
super().__init__(data, metric)
|
|
110
116
|
self.base = base
|
|
111
117
|
|
|
112
|
-
if metric is None:
|
|
113
|
-
self.metric = self._euclidean_distance
|
|
114
|
-
else:
|
|
115
|
-
self.metric = metric
|
|
116
|
-
|
|
117
118
|
# Compute distance cache for small datasets
|
|
118
119
|
self._distance_cache: dict[Tuple[int, int], float] = {}
|
|
119
120
|
|
|
@@ -124,10 +125,12 @@ class CoverTree:
|
|
|
124
125
|
|
|
125
126
|
if self.n_samples > 0:
|
|
126
127
|
self._build_tree()
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
_logger.debug(
|
|
129
|
+
"CoverTree built with base=%.1f, levels=%d to %d",
|
|
130
|
+
base,
|
|
131
|
+
self.min_level,
|
|
132
|
+
self.max_level,
|
|
133
|
+
)
|
|
131
134
|
|
|
132
135
|
def _distance(self, i: int, j: int) -> float:
|
|
133
136
|
"""Get distance between points i and j (with caching)."""
|
|
@@ -245,11 +248,7 @@ class CoverTree:
|
|
|
245
248
|
result : CoverTreeResult
|
|
246
249
|
Indices and distances of k nearest neighbors.
|
|
247
250
|
"""
|
|
248
|
-
X =
|
|
249
|
-
|
|
250
|
-
if X.ndim == 1:
|
|
251
|
-
X = X.reshape(1, -1)
|
|
252
|
-
|
|
251
|
+
X = validate_query_input(X, self.n_features)
|
|
253
252
|
n_queries = X.shape[0]
|
|
254
253
|
|
|
255
254
|
all_indices = np.zeros((n_queries, k), dtype=np.intp)
|
|
@@ -368,11 +367,7 @@ class CoverTree:
|
|
|
368
367
|
indices : list of lists
|
|
369
368
|
For each query, list of indices within radius.
|
|
370
369
|
"""
|
|
371
|
-
X =
|
|
372
|
-
|
|
373
|
-
if X.ndim == 1:
|
|
374
|
-
X = X.reshape(1, -1)
|
|
375
|
-
|
|
370
|
+
X = validate_query_input(X, self.n_features)
|
|
376
371
|
n_queries = X.shape[0]
|
|
377
372
|
results: List[List[int]] = []
|
|
378
373
|
|
pytcl/containers/kd_tree.py
CHANGED
|
@@ -13,11 +13,17 @@ References
|
|
|
13
13
|
Finding Best Matches in Logarithmic Expected Time," ACM TOMS, 1977.
|
|
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 BaseSpatialIndex, validate_query_input
|
|
23
|
+
|
|
24
|
+
# Module logger
|
|
25
|
+
_logger = logging.getLogger("pytcl.containers.kd_tree")
|
|
26
|
+
|
|
21
27
|
|
|
22
28
|
class KDNode:
|
|
23
29
|
"""A node in the k-d tree.
|
|
@@ -66,7 +72,7 @@ class NearestNeighborResult(NamedTuple):
|
|
|
66
72
|
distances: NDArray[np.floating]
|
|
67
73
|
|
|
68
74
|
|
|
69
|
-
class KDTree:
|
|
75
|
+
class KDTree(BaseSpatialIndex):
|
|
70
76
|
"""
|
|
71
77
|
K-D Tree for efficient spatial queries.
|
|
72
78
|
|
|
@@ -97,6 +103,11 @@ class KDTree:
|
|
|
97
103
|
|
|
98
104
|
Query complexity is O(log n) on average for nearest neighbor search,
|
|
99
105
|
though worst case is O(n) for highly unbalanced queries.
|
|
106
|
+
|
|
107
|
+
See Also
|
|
108
|
+
--------
|
|
109
|
+
BaseSpatialIndex : Abstract base class defining the spatial index interface.
|
|
110
|
+
BallTree : Alternative spatial index using hyperspheres.
|
|
100
111
|
"""
|
|
101
112
|
|
|
102
113
|
def __init__(
|
|
@@ -104,17 +115,13 @@ class KDTree:
|
|
|
104
115
|
data: ArrayLike,
|
|
105
116
|
leaf_size: int = 10,
|
|
106
117
|
):
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if self.data.ndim != 2:
|
|
110
|
-
raise ValueError("Data must be 2-dimensional (n_samples, n_features)")
|
|
111
|
-
|
|
112
|
-
self.n_samples, self.n_features = self.data.shape
|
|
118
|
+
super().__init__(data)
|
|
113
119
|
self.leaf_size = leaf_size
|
|
114
120
|
|
|
115
121
|
# Build the tree
|
|
116
122
|
indices = np.arange(self.n_samples)
|
|
117
123
|
self.root = self._build_tree(indices, depth=0)
|
|
124
|
+
_logger.debug("KDTree built with leaf_size=%d", leaf_size)
|
|
118
125
|
|
|
119
126
|
def _build_tree(
|
|
120
127
|
self,
|
|
@@ -173,12 +180,9 @@ class KDTree:
|
|
|
173
180
|
>>> result.indices
|
|
174
181
|
array([[0, 1]])
|
|
175
182
|
"""
|
|
176
|
-
X =
|
|
177
|
-
|
|
178
|
-
if X.ndim == 1:
|
|
179
|
-
X = X.reshape(1, -1)
|
|
180
|
-
|
|
183
|
+
X = validate_query_input(X, self.n_features)
|
|
181
184
|
n_queries = X.shape[0]
|
|
185
|
+
_logger.debug("KDTree.query: %d queries, k=%d", n_queries, k)
|
|
182
186
|
|
|
183
187
|
all_indices = np.zeros((n_queries, k), dtype=np.intp)
|
|
184
188
|
all_distances = np.full((n_queries, k), np.inf)
|
|
@@ -263,11 +267,7 @@ class KDTree:
|
|
|
263
267
|
>>> tree.query_radius([[0, 0]], r=1.5)
|
|
264
268
|
[[0, 1, 2]]
|
|
265
269
|
"""
|
|
266
|
-
X =
|
|
267
|
-
|
|
268
|
-
if X.ndim == 1:
|
|
269
|
-
X = X.reshape(1, -1)
|
|
270
|
-
|
|
270
|
+
X = validate_query_input(X, self.n_features)
|
|
271
271
|
n_queries = X.shape[0]
|
|
272
272
|
results: List[List[int]] = []
|
|
273
273
|
|
|
@@ -331,7 +331,7 @@ class KDTree:
|
|
|
331
331
|
return self.query_radius(X, r)
|
|
332
332
|
|
|
333
333
|
|
|
334
|
-
class BallTree:
|
|
334
|
+
class BallTree(BaseSpatialIndex):
|
|
335
335
|
"""
|
|
336
336
|
Ball Tree for efficient spatial queries.
|
|
337
337
|
|
|
@@ -357,6 +357,11 @@ class BallTree:
|
|
|
357
357
|
-----
|
|
358
358
|
Ball trees have O(n log n) construction and O(log n) average-case
|
|
359
359
|
query time. They can outperform k-d trees in high dimensions.
|
|
360
|
+
|
|
361
|
+
See Also
|
|
362
|
+
--------
|
|
363
|
+
BaseSpatialIndex : Abstract base class defining the spatial index interface.
|
|
364
|
+
KDTree : Alternative spatial index using axis-aligned splits.
|
|
360
365
|
"""
|
|
361
366
|
|
|
362
367
|
def __init__(
|
|
@@ -364,12 +369,7 @@ class BallTree:
|
|
|
364
369
|
data: ArrayLike,
|
|
365
370
|
leaf_size: int = 10,
|
|
366
371
|
):
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if self.data.ndim != 2:
|
|
370
|
-
raise ValueError("Data must be 2-dimensional")
|
|
371
|
-
|
|
372
|
-
self.n_samples, self.n_features = self.data.shape
|
|
372
|
+
super().__init__(data)
|
|
373
373
|
self.leaf_size = leaf_size
|
|
374
374
|
|
|
375
375
|
# Build tree using indices
|
|
@@ -382,6 +382,7 @@ class BallTree:
|
|
|
382
382
|
self._leaf_indices: List[Optional[NDArray[np.intp]]] = []
|
|
383
383
|
|
|
384
384
|
self._build_tree(self._indices)
|
|
385
|
+
_logger.debug("BallTree built with leaf_size=%d", leaf_size)
|
|
385
386
|
|
|
386
387
|
def _build_tree(
|
|
387
388
|
self,
|
|
@@ -459,11 +460,7 @@ class BallTree:
|
|
|
459
460
|
result : NearestNeighborResult
|
|
460
461
|
Indices and distances of k nearest neighbors.
|
|
461
462
|
"""
|
|
462
|
-
X =
|
|
463
|
-
|
|
464
|
-
if X.ndim == 1:
|
|
465
|
-
X = X.reshape(1, -1)
|
|
466
|
-
|
|
463
|
+
X = validate_query_input(X, self.n_features)
|
|
467
464
|
n_queries = X.shape[0]
|
|
468
465
|
all_indices = np.zeros((n_queries, k), dtype=np.intp)
|
|
469
466
|
all_distances = np.full((n_queries, k), np.inf)
|
|
@@ -478,6 +475,74 @@ class BallTree:
|
|
|
478
475
|
|
|
479
476
|
return NearestNeighborResult(indices=all_indices, distances=all_distances)
|
|
480
477
|
|
|
478
|
+
def query_radius(
|
|
479
|
+
self,
|
|
480
|
+
X: ArrayLike,
|
|
481
|
+
r: float,
|
|
482
|
+
) -> List[List[int]]:
|
|
483
|
+
"""
|
|
484
|
+
Query the tree for all points within radius r.
|
|
485
|
+
|
|
486
|
+
Parameters
|
|
487
|
+
----------
|
|
488
|
+
X : array_like
|
|
489
|
+
Query points of shape (n_queries, n_features) or (n_features,).
|
|
490
|
+
r : float
|
|
491
|
+
Query radius.
|
|
492
|
+
|
|
493
|
+
Returns
|
|
494
|
+
-------
|
|
495
|
+
indices : list of lists
|
|
496
|
+
For each query, a list of indices of points within radius r.
|
|
497
|
+
"""
|
|
498
|
+
X = validate_query_input(X, self.n_features)
|
|
499
|
+
n_queries = X.shape[0]
|
|
500
|
+
results: List[List[int]] = []
|
|
501
|
+
|
|
502
|
+
for i in range(n_queries):
|
|
503
|
+
indices = self._query_radius_single(X[i], r)
|
|
504
|
+
results.append(indices)
|
|
505
|
+
|
|
506
|
+
return results
|
|
507
|
+
|
|
508
|
+
def _query_radius_single(
|
|
509
|
+
self,
|
|
510
|
+
query: NDArray[np.floating],
|
|
511
|
+
r: float,
|
|
512
|
+
) -> List[int]:
|
|
513
|
+
"""Find all points within radius r of query point."""
|
|
514
|
+
indices: List[int] = []
|
|
515
|
+
|
|
516
|
+
def _search(node_id: int) -> None:
|
|
517
|
+
if node_id < 0:
|
|
518
|
+
return
|
|
519
|
+
|
|
520
|
+
centroid = self._centroids[node_id]
|
|
521
|
+
radius = self._radii[node_id]
|
|
522
|
+
|
|
523
|
+
# Distance to ball surface
|
|
524
|
+
dist_to_center = np.sqrt(np.sum((query - centroid) ** 2))
|
|
525
|
+
|
|
526
|
+
# Prune if ball is farther than radius
|
|
527
|
+
if dist_to_center - radius > r:
|
|
528
|
+
return
|
|
529
|
+
|
|
530
|
+
if self._is_leaf[node_id]:
|
|
531
|
+
# Check all points in leaf
|
|
532
|
+
leaf_indices = self._leaf_indices[node_id]
|
|
533
|
+
if leaf_indices is not None:
|
|
534
|
+
for idx in leaf_indices:
|
|
535
|
+
dist = np.sqrt(np.sum((query - self.data[idx]) ** 2))
|
|
536
|
+
if dist <= r:
|
|
537
|
+
indices.append(idx)
|
|
538
|
+
else:
|
|
539
|
+
# Visit both children
|
|
540
|
+
_search(self._left[node_id])
|
|
541
|
+
_search(self._right[node_id])
|
|
542
|
+
|
|
543
|
+
_search(0)
|
|
544
|
+
return indices
|
|
545
|
+
|
|
481
546
|
def _query_single(
|
|
482
547
|
self,
|
|
483
548
|
query: NDArray[np.floating],
|
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",
|