nrl-tracker 1.7.0__py3-none-any.whl → 1.7.3__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.7.0.dist-info → nrl_tracker-1.7.3.dist-info}/METADATA +43 -3
- {nrl_tracker-1.7.0.dist-info → nrl_tracker-1.7.3.dist-info}/RECORD +76 -76
- pytcl/__init__.py +2 -2
- pytcl/assignment_algorithms/__init__.py +15 -15
- pytcl/assignment_algorithms/gating.py +10 -10
- pytcl/assignment_algorithms/jpda.py +40 -40
- pytcl/assignment_algorithms/nd_assignment.py +5 -4
- pytcl/assignment_algorithms/network_flow.py +18 -8
- pytcl/assignment_algorithms/three_dimensional/assignment.py +3 -3
- pytcl/astronomical/__init__.py +9 -9
- pytcl/astronomical/ephemerides.py +14 -11
- pytcl/astronomical/reference_frames.py +8 -4
- pytcl/astronomical/relativity.py +6 -5
- pytcl/astronomical/special_orbits.py +9 -13
- pytcl/atmosphere/__init__.py +6 -6
- pytcl/atmosphere/nrlmsise00.py +153 -152
- pytcl/clustering/dbscan.py +2 -2
- pytcl/clustering/gaussian_mixture.py +3 -3
- pytcl/clustering/hierarchical.py +15 -15
- pytcl/clustering/kmeans.py +4 -4
- pytcl/containers/base.py +3 -3
- pytcl/containers/cluster_set.py +12 -2
- pytcl/containers/covertree.py +5 -3
- pytcl/containers/rtree.py +1 -1
- pytcl/containers/vptree.py +4 -2
- pytcl/coordinate_systems/conversions/geodetic.py +31 -7
- pytcl/coordinate_systems/jacobians/jacobians.py +2 -2
- pytcl/coordinate_systems/projections/__init__.py +1 -1
- pytcl/coordinate_systems/projections/projections.py +2 -2
- pytcl/coordinate_systems/rotations/rotations.py +10 -6
- pytcl/core/validation.py +3 -3
- pytcl/dynamic_estimation/__init__.py +16 -16
- pytcl/dynamic_estimation/gaussian_sum_filter.py +20 -38
- pytcl/dynamic_estimation/imm.py +14 -14
- pytcl/dynamic_estimation/kalman/__init__.py +1 -1
- pytcl/dynamic_estimation/kalman/constrained.py +35 -23
- pytcl/dynamic_estimation/kalman/extended.py +8 -8
- pytcl/dynamic_estimation/kalman/h_infinity.py +2 -2
- pytcl/dynamic_estimation/kalman/square_root.py +8 -2
- pytcl/dynamic_estimation/kalman/sr_ukf.py +3 -3
- pytcl/dynamic_estimation/kalman/ud_filter.py +11 -5
- pytcl/dynamic_estimation/kalman/unscented.py +8 -6
- pytcl/dynamic_estimation/particle_filters/bootstrap.py +15 -15
- pytcl/dynamic_estimation/rbpf.py +36 -40
- pytcl/gravity/spherical_harmonics.py +3 -3
- pytcl/gravity/tides.py +6 -6
- pytcl/logging_config.py +3 -3
- pytcl/magnetism/emm.py +10 -3
- pytcl/magnetism/wmm.py +4 -4
- pytcl/mathematical_functions/combinatorics/combinatorics.py +5 -5
- pytcl/mathematical_functions/geometry/geometry.py +5 -5
- pytcl/mathematical_functions/numerical_integration/quadrature.py +6 -6
- pytcl/mathematical_functions/signal_processing/detection.py +24 -24
- pytcl/mathematical_functions/signal_processing/filters.py +14 -14
- pytcl/mathematical_functions/signal_processing/matched_filter.py +12 -12
- pytcl/mathematical_functions/special_functions/bessel.py +15 -3
- pytcl/mathematical_functions/special_functions/debye.py +5 -1
- pytcl/mathematical_functions/special_functions/error_functions.py +3 -1
- pytcl/mathematical_functions/special_functions/gamma_functions.py +4 -4
- pytcl/mathematical_functions/special_functions/hypergeometric.py +6 -4
- pytcl/mathematical_functions/transforms/fourier.py +8 -8
- pytcl/mathematical_functions/transforms/stft.py +12 -12
- pytcl/mathematical_functions/transforms/wavelets.py +9 -9
- pytcl/navigation/geodesy.py +3 -3
- pytcl/navigation/great_circle.py +5 -5
- pytcl/plotting/coordinates.py +7 -7
- pytcl/plotting/tracks.py +2 -2
- pytcl/static_estimation/maximum_likelihood.py +16 -14
- pytcl/static_estimation/robust.py +5 -5
- pytcl/terrain/loaders.py +5 -5
- pytcl/trackers/hypothesis.py +1 -1
- pytcl/trackers/mht.py +9 -9
- pytcl/trackers/multi_target.py +1 -1
- {nrl_tracker-1.7.0.dist-info → nrl_tracker-1.7.3.dist-info}/LICENSE +0 -0
- {nrl_tracker-1.7.0.dist-info → nrl_tracker-1.7.3.dist-info}/WHEEL +0 -0
- {nrl_tracker-1.7.0.dist-info → nrl_tracker-1.7.3.dist-info}/top_level.txt +0 -0
|
@@ -9,7 +9,7 @@ This is more sophisticated than GNN which makes hard assignment decisions,
|
|
|
9
9
|
as JPDA can handle measurement origin uncertainty in cluttered environments.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
from typing import List, NamedTuple, Optional
|
|
12
|
+
from typing import Any, List, NamedTuple, Optional
|
|
13
13
|
|
|
14
14
|
import numpy as np
|
|
15
15
|
from numba import njit
|
|
@@ -24,7 +24,7 @@ class JPDAResult(NamedTuple):
|
|
|
24
24
|
|
|
25
25
|
Attributes
|
|
26
26
|
----------
|
|
27
|
-
association_probs : ndarray
|
|
27
|
+
association_probs : ndarray[Any]
|
|
28
28
|
Association probability matrix of shape (n_tracks, n_measurements + 1).
|
|
29
29
|
association_probs[i, j] is the probability that track i is associated
|
|
30
30
|
with measurement j. The last column (j = n_measurements) represents
|
|
@@ -32,9 +32,9 @@ class JPDAResult(NamedTuple):
|
|
|
32
32
|
marginal_probs : list of ndarray
|
|
33
33
|
List of marginal association probabilities for each track.
|
|
34
34
|
marginal_probs[i][j] = P(measurement j originated from track i).
|
|
35
|
-
likelihood_matrix : ndarray
|
|
35
|
+
likelihood_matrix : ndarray[Any]
|
|
36
36
|
Measurement likelihood matrix of shape (n_tracks, n_measurements).
|
|
37
|
-
gated : ndarray
|
|
37
|
+
gated : ndarray[Any]
|
|
38
38
|
Boolean matrix indicating which track-measurement pairs passed gating.
|
|
39
39
|
"""
|
|
40
40
|
|
|
@@ -53,7 +53,7 @@ class JPDAUpdate(NamedTuple):
|
|
|
53
53
|
Updated state estimates for each track.
|
|
54
54
|
covariances : list of ndarray
|
|
55
55
|
Updated covariances for each track (includes spread of means).
|
|
56
|
-
association_probs : ndarray
|
|
56
|
+
association_probs : ndarray[Any]
|
|
57
57
|
Association probability matrix.
|
|
58
58
|
innovations : list of ndarray
|
|
59
59
|
Combined weighted innovations for each track.
|
|
@@ -66,8 +66,8 @@ class JPDAUpdate(NamedTuple):
|
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
def compute_measurement_likelihood(
|
|
69
|
-
innovation: NDArray,
|
|
70
|
-
innovation_cov: NDArray,
|
|
69
|
+
innovation: NDArray[Any],
|
|
70
|
+
innovation_cov: NDArray[Any],
|
|
71
71
|
detection_prob: float = 1.0,
|
|
72
72
|
) -> float:
|
|
73
73
|
"""
|
|
@@ -75,9 +75,9 @@ def compute_measurement_likelihood(
|
|
|
75
75
|
|
|
76
76
|
Parameters
|
|
77
77
|
----------
|
|
78
|
-
innovation : ndarray
|
|
78
|
+
innovation : ndarray[Any]
|
|
79
79
|
Measurement innovation (residual), shape (m,).
|
|
80
|
-
innovation_cov : ndarray
|
|
80
|
+
innovation_cov : ndarray[Any]
|
|
81
81
|
Innovation covariance, shape (m, m).
|
|
82
82
|
detection_prob : float
|
|
83
83
|
Probability of detection (Pd).
|
|
@@ -102,14 +102,14 @@ def compute_measurement_likelihood(
|
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
def compute_likelihood_matrix(
|
|
105
|
-
track_states:
|
|
106
|
-
track_covariances:
|
|
107
|
-
measurements: NDArray,
|
|
108
|
-
H: NDArray,
|
|
109
|
-
R: NDArray,
|
|
105
|
+
track_states: list[NDArray[Any]],
|
|
106
|
+
track_covariances: list[NDArray[Any]],
|
|
107
|
+
measurements: NDArray[Any],
|
|
108
|
+
H: NDArray[Any],
|
|
109
|
+
R: NDArray[Any],
|
|
110
110
|
detection_prob: float = 1.0,
|
|
111
111
|
gate_threshold: Optional[float] = None,
|
|
112
|
-
) ->
|
|
112
|
+
) -> tuple[NDArray[Any], NDArray[Any]]:
|
|
113
113
|
"""
|
|
114
114
|
Compute likelihood matrix for all track-measurement pairs.
|
|
115
115
|
|
|
@@ -119,11 +119,11 @@ def compute_likelihood_matrix(
|
|
|
119
119
|
State estimates for each track.
|
|
120
120
|
track_covariances : list of ndarray
|
|
121
121
|
Covariances for each track.
|
|
122
|
-
measurements : ndarray
|
|
122
|
+
measurements : ndarray[Any]
|
|
123
123
|
Measurements, shape (n_meas, m).
|
|
124
|
-
H : ndarray
|
|
124
|
+
H : ndarray[Any]
|
|
125
125
|
Measurement matrix, shape (m, n).
|
|
126
|
-
R : ndarray
|
|
126
|
+
R : ndarray[Any]
|
|
127
127
|
Measurement noise covariance, shape (m, m).
|
|
128
128
|
detection_prob : float
|
|
129
129
|
Probability of detection.
|
|
@@ -132,9 +132,9 @@ def compute_likelihood_matrix(
|
|
|
132
132
|
|
|
133
133
|
Returns
|
|
134
134
|
-------
|
|
135
|
-
likelihood_matrix : ndarray
|
|
135
|
+
likelihood_matrix : ndarray[Any]
|
|
136
136
|
Likelihood values, shape (n_tracks, n_meas).
|
|
137
|
-
gated : ndarray
|
|
137
|
+
gated : ndarray[Any]
|
|
138
138
|
Boolean gating matrix, shape (n_tracks, n_meas).
|
|
139
139
|
"""
|
|
140
140
|
n_tracks = len(track_states)
|
|
@@ -163,11 +163,11 @@ def compute_likelihood_matrix(
|
|
|
163
163
|
|
|
164
164
|
|
|
165
165
|
def jpda_probabilities(
|
|
166
|
-
likelihood_matrix: NDArray,
|
|
167
|
-
gated: NDArray,
|
|
166
|
+
likelihood_matrix: NDArray[Any],
|
|
167
|
+
gated: NDArray[Any],
|
|
168
168
|
detection_prob: float = 1.0,
|
|
169
169
|
clutter_density: float = 1e-6,
|
|
170
|
-
) -> NDArray:
|
|
170
|
+
) -> NDArray[Any]:
|
|
171
171
|
"""
|
|
172
172
|
Compute JPDA association probabilities.
|
|
173
173
|
|
|
@@ -176,9 +176,9 @@ def jpda_probabilities(
|
|
|
176
176
|
|
|
177
177
|
Parameters
|
|
178
178
|
----------
|
|
179
|
-
likelihood_matrix : ndarray
|
|
179
|
+
likelihood_matrix : ndarray[Any]
|
|
180
180
|
Likelihood values, shape (n_tracks, n_meas).
|
|
181
|
-
gated : ndarray
|
|
181
|
+
gated : ndarray[Any]
|
|
182
182
|
Boolean gating matrix, shape (n_tracks, n_meas).
|
|
183
183
|
detection_prob : float
|
|
184
184
|
Probability of detection (Pd).
|
|
@@ -187,7 +187,7 @@ def jpda_probabilities(
|
|
|
187
187
|
|
|
188
188
|
Returns
|
|
189
189
|
-------
|
|
190
|
-
beta : ndarray
|
|
190
|
+
beta : ndarray[Any]
|
|
191
191
|
Association probability matrix, shape (n_tracks, n_meas + 1).
|
|
192
192
|
beta[i, j] = P(measurement j is from track i) for j < n_meas.
|
|
193
193
|
beta[i, n_meas] = P(track i has no measurement).
|
|
@@ -218,11 +218,11 @@ def jpda_probabilities(
|
|
|
218
218
|
|
|
219
219
|
|
|
220
220
|
def _jpda_exact(
|
|
221
|
-
likelihood_matrix: NDArray,
|
|
222
|
-
gated: NDArray,
|
|
221
|
+
likelihood_matrix: NDArray[Any],
|
|
222
|
+
gated: NDArray[Any],
|
|
223
223
|
detection_prob: float,
|
|
224
224
|
clutter_density: float,
|
|
225
|
-
) -> NDArray:
|
|
225
|
+
) -> NDArray[Any]:
|
|
226
226
|
"""
|
|
227
227
|
Exact JPDA computation via hypothesis enumeration.
|
|
228
228
|
|
|
@@ -241,8 +241,8 @@ def _jpda_exact(
|
|
|
241
241
|
def generate_hypotheses(
|
|
242
242
|
meas_idx: int,
|
|
243
243
|
current_assignment: List[int],
|
|
244
|
-
used_tracks: set,
|
|
245
|
-
):
|
|
244
|
+
used_tracks: set[Any],
|
|
245
|
+
) -> Any:
|
|
246
246
|
"""Recursively generate valid hypotheses."""
|
|
247
247
|
if meas_idx == n_meas:
|
|
248
248
|
yield current_assignment.copy()
|
|
@@ -268,11 +268,11 @@ def _jpda_exact(
|
|
|
268
268
|
hypothesis_probs = []
|
|
269
269
|
hypothesis_assignments = []
|
|
270
270
|
|
|
271
|
-
for assignment in generate_hypotheses(0, [], set()):
|
|
271
|
+
for assignment in generate_hypotheses(0, [], set[Any]()):
|
|
272
272
|
# Compute probability of this hypothesis
|
|
273
273
|
prob = 1.0
|
|
274
274
|
|
|
275
|
-
detected_tracks = set()
|
|
275
|
+
detected_tracks = set[Any]()
|
|
276
276
|
for j, track_idx in enumerate(assignment):
|
|
277
277
|
if track_idx == -1:
|
|
278
278
|
# Measurement j is clutter
|
|
@@ -301,7 +301,7 @@ def _jpda_exact(
|
|
|
301
301
|
for h_idx, (assignment, prob) in enumerate(
|
|
302
302
|
zip(hypothesis_assignments, hypothesis_probs)
|
|
303
303
|
):
|
|
304
|
-
detected_tracks = set()
|
|
304
|
+
detected_tracks = set[Any]()
|
|
305
305
|
for j, track_idx in enumerate(assignment):
|
|
306
306
|
if track_idx >= 0:
|
|
307
307
|
beta[track_idx, j] += prob
|
|
@@ -317,11 +317,11 @@ def _jpda_exact(
|
|
|
317
317
|
|
|
318
318
|
@njit(cache=True)
|
|
319
319
|
def _jpda_approximate_core(
|
|
320
|
-
likelihood_matrix: np.ndarray,
|
|
321
|
-
gated: np.ndarray,
|
|
320
|
+
likelihood_matrix: np.ndarray[Any, Any],
|
|
321
|
+
gated: np.ndarray[Any, Any],
|
|
322
322
|
detection_prob: float,
|
|
323
323
|
clutter_density: float,
|
|
324
|
-
) -> np.ndarray:
|
|
324
|
+
) -> np.ndarray[Any, Any]:
|
|
325
325
|
"""JIT-compiled core of approximate JPDA computation."""
|
|
326
326
|
n_tracks = likelihood_matrix.shape[0]
|
|
327
327
|
n_meas = likelihood_matrix.shape[1]
|
|
@@ -369,11 +369,11 @@ def _jpda_approximate_core(
|
|
|
369
369
|
|
|
370
370
|
|
|
371
371
|
def _jpda_approximate(
|
|
372
|
-
likelihood_matrix: NDArray,
|
|
373
|
-
gated: NDArray,
|
|
372
|
+
likelihood_matrix: NDArray[Any],
|
|
373
|
+
gated: NDArray[Any],
|
|
374
374
|
detection_prob: float,
|
|
375
375
|
clutter_density: float,
|
|
376
|
-
) -> NDArray:
|
|
376
|
+
) -> NDArray[Any]:
|
|
377
377
|
"""
|
|
378
378
|
Approximate JPDA using parametric approach.
|
|
379
379
|
|
|
@@ -212,14 +212,15 @@ def relaxation_assignment_nd(
|
|
|
212
212
|
result_relaxed = greedy_assignment_nd(relaxed_cost)
|
|
213
213
|
|
|
214
214
|
# Compute lower bound from relaxed solution
|
|
215
|
-
lower_bound = (
|
|
216
|
-
|
|
217
|
-
+ sum(np.sum(lambdas[d]) for d in range(n_dims))
|
|
215
|
+
lower_bound = result_relaxed.cost + sum(
|
|
216
|
+
np.sum(lambdas[d]) for d in range(n_dims)
|
|
218
217
|
)
|
|
219
218
|
|
|
220
219
|
# Extract solution from relaxed problem
|
|
221
220
|
if len(result_relaxed.assignments) > 0:
|
|
222
|
-
actual_cost = float(
|
|
221
|
+
actual_cost = float(
|
|
222
|
+
np.sum(cost_tensor[tuple(result_relaxed.assignments.T)])
|
|
223
|
+
)
|
|
223
224
|
|
|
224
225
|
if actual_cost < best_cost:
|
|
225
226
|
best_cost = actual_cost
|
|
@@ -20,7 +20,7 @@ References
|
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
from enum import Enum
|
|
23
|
-
from typing import NamedTuple, Tuple
|
|
23
|
+
from typing import Any, NamedTuple, Tuple
|
|
24
24
|
|
|
25
25
|
import numpy as np
|
|
26
26
|
from numpy.typing import NDArray
|
|
@@ -79,7 +79,7 @@ class FlowEdge(NamedTuple):
|
|
|
79
79
|
|
|
80
80
|
def assignment_to_flow_network(
|
|
81
81
|
cost_matrix: NDArray[np.float64],
|
|
82
|
-
) -> Tuple[list, NDArray, NDArray]:
|
|
82
|
+
) -> Tuple[list[FlowEdge], NDArray[np.floating], NDArray[Any]]:
|
|
83
83
|
"""
|
|
84
84
|
Convert 2D assignment problem to min-cost flow network.
|
|
85
85
|
|
|
@@ -140,7 +140,9 @@ def assignment_to_flow_network(
|
|
|
140
140
|
# Tasks to sink: capacity 1, cost 0
|
|
141
141
|
for j in range(1, n + 1):
|
|
142
142
|
task_node = m + j
|
|
143
|
-
edges.append(
|
|
143
|
+
edges.append(
|
|
144
|
+
FlowEdge(from_node=task_node, to_node=sink, capacity=1.0, cost=0.0)
|
|
145
|
+
)
|
|
144
146
|
|
|
145
147
|
# Supply/demand: source supplies m units, sink demands m units
|
|
146
148
|
supplies = np.zeros(n_nodes)
|
|
@@ -158,7 +160,7 @@ def assignment_to_flow_network(
|
|
|
158
160
|
|
|
159
161
|
|
|
160
162
|
def min_cost_flow_successive_shortest_paths(
|
|
161
|
-
edges: list,
|
|
163
|
+
edges: list[FlowEdge],
|
|
162
164
|
supplies: NDArray[np.float64],
|
|
163
165
|
max_iterations: int = 1000,
|
|
164
166
|
) -> MinCostFlowResult:
|
|
@@ -260,7 +262,9 @@ def min_cost_flow_successive_shortest_paths(
|
|
|
260
262
|
|
|
261
263
|
# Find minimum capacity along path
|
|
262
264
|
min_flow = min(residual_capacity[e] for e in path_edges)
|
|
263
|
-
min_flow = min(
|
|
265
|
+
min_flow = min(
|
|
266
|
+
min_flow, current_supplies[excess_node], -current_supplies[deficit_node]
|
|
267
|
+
)
|
|
264
268
|
|
|
265
269
|
# Push flow along path
|
|
266
270
|
total_cost = 0.0
|
|
@@ -295,7 +299,7 @@ def min_cost_flow_successive_shortest_paths(
|
|
|
295
299
|
|
|
296
300
|
def assignment_from_flow_solution(
|
|
297
301
|
flow: NDArray[np.float64],
|
|
298
|
-
edges: list,
|
|
302
|
+
edges: list[FlowEdge],
|
|
299
303
|
cost_matrix_shape: Tuple[int, int],
|
|
300
304
|
) -> Tuple[NDArray[np.intp], float]:
|
|
301
305
|
"""
|
|
@@ -331,7 +335,11 @@ def assignment_from_flow_solution(
|
|
|
331
335
|
assignment = np.array(assignment, dtype=np.intp)
|
|
332
336
|
cost = 0.0
|
|
333
337
|
if len(assignment) > 0:
|
|
334
|
-
cost = float(
|
|
338
|
+
cost = float(
|
|
339
|
+
np.sum(
|
|
340
|
+
flow[edge_idx] * edges[edge_idx].cost for edge_idx in range(len(edges))
|
|
341
|
+
)
|
|
342
|
+
)
|
|
335
343
|
|
|
336
344
|
return assignment, cost
|
|
337
345
|
|
|
@@ -356,6 +364,8 @@ def min_cost_assignment_via_flow(
|
|
|
356
364
|
"""
|
|
357
365
|
edges, supplies, _ = assignment_to_flow_network(cost_matrix)
|
|
358
366
|
result = min_cost_flow_successive_shortest_paths(edges, supplies)
|
|
359
|
-
assignment, cost = assignment_from_flow_solution(
|
|
367
|
+
assignment, cost = assignment_from_flow_solution(
|
|
368
|
+
result.flow, edges, cost_matrix.shape
|
|
369
|
+
)
|
|
360
370
|
|
|
361
371
|
return assignment, cost
|
|
@@ -11,7 +11,7 @@ cost subject to the constraint that each index appears in at most one
|
|
|
11
11
|
selected tuple.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
from typing import List, NamedTuple, Optional, Tuple
|
|
14
|
+
from typing import Any, List, NamedTuple, Optional, Tuple
|
|
15
15
|
|
|
16
16
|
import numpy as np
|
|
17
17
|
from numpy.typing import ArrayLike, NDArray
|
|
@@ -511,7 +511,7 @@ def assign3d_auction(
|
|
|
511
511
|
assign_i: List[Optional[Tuple[int, int]]] = [None] * n1
|
|
512
512
|
|
|
513
513
|
# Reverse: which i is assigned to (j, k)
|
|
514
|
-
reverse: dict = {}
|
|
514
|
+
reverse: dict[tuple[int, int], int] = {}
|
|
515
515
|
|
|
516
516
|
converged = False
|
|
517
517
|
|
|
@@ -585,7 +585,7 @@ def assign3d(
|
|
|
585
585
|
cost_tensor: ArrayLike,
|
|
586
586
|
method: str = "lagrangian",
|
|
587
587
|
maximize: bool = False,
|
|
588
|
-
**kwargs,
|
|
588
|
+
**kwargs: Any,
|
|
589
589
|
) -> Assignment3DResult:
|
|
590
590
|
"""
|
|
591
591
|
Solve 3D assignment problem.
|
pytcl/astronomical/__init__.py
CHANGED
|
@@ -77,10 +77,10 @@ from pytcl.astronomical.orbital_mechanics import (
|
|
|
77
77
|
vis_viva,
|
|
78
78
|
)
|
|
79
79
|
from pytcl.astronomical.reference_frames import (
|
|
80
|
-
earth_rotation_angle, # Time utilities; Precession; Nutation
|
|
80
|
+
earth_rotation_angle, # Time utilities; Precession; Nutation
|
|
81
81
|
)
|
|
82
|
+
from pytcl.astronomical.reference_frames import ecef_to_eci # Time utilities
|
|
82
83
|
from pytcl.astronomical.reference_frames import (
|
|
83
|
-
ecef_to_eci,
|
|
84
84
|
eci_to_ecef,
|
|
85
85
|
ecliptic_to_equatorial,
|
|
86
86
|
equation_of_equinoxes,
|
|
@@ -116,7 +116,7 @@ from pytcl.astronomical.reference_frames import (
|
|
|
116
116
|
true_obliquity,
|
|
117
117
|
)
|
|
118
118
|
from pytcl.astronomical.relativity import (
|
|
119
|
-
C_LIGHT, # Physical constants; Schwarzschild metric; Time dilation
|
|
119
|
+
C_LIGHT, # Physical constants; Schwarzschild metric; Time dilation
|
|
120
120
|
)
|
|
121
121
|
from pytcl.astronomical.relativity import (
|
|
122
122
|
G_GRAV,
|
|
@@ -130,6 +130,12 @@ from pytcl.astronomical.relativity import (
|
|
|
130
130
|
schwarzschild_radius,
|
|
131
131
|
shapiro_delay,
|
|
132
132
|
)
|
|
133
|
+
from pytcl.astronomical.sgp4 import (
|
|
134
|
+
SGP4Satellite,
|
|
135
|
+
SGP4State,
|
|
136
|
+
sgp4_propagate,
|
|
137
|
+
sgp4_propagate_batch,
|
|
138
|
+
)
|
|
133
139
|
from pytcl.astronomical.special_orbits import (
|
|
134
140
|
OrbitType,
|
|
135
141
|
classify_orbit,
|
|
@@ -148,12 +154,6 @@ from pytcl.astronomical.special_orbits import (
|
|
|
148
154
|
true_anomaly_to_parabolic_anomaly,
|
|
149
155
|
velocity_parabolic,
|
|
150
156
|
)
|
|
151
|
-
from pytcl.astronomical.sgp4 import (
|
|
152
|
-
SGP4Satellite,
|
|
153
|
-
SGP4State,
|
|
154
|
-
sgp4_propagate,
|
|
155
|
-
sgp4_propagate_batch,
|
|
156
|
-
)
|
|
157
157
|
from pytcl.astronomical.time_systems import (
|
|
158
158
|
JD_GPS_EPOCH, # Julian dates; Time scales; Unix time; GPS week; Sidereal time; Leap seconds; Constants
|
|
159
159
|
)
|
|
@@ -49,9 +49,10 @@ References
|
|
|
49
49
|
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
|
-
from typing import Literal, Optional, Tuple
|
|
52
|
+
from typing import Any, Literal, Optional, Tuple
|
|
53
53
|
|
|
54
54
|
import numpy as np
|
|
55
|
+
from numpy.typing import NDArray
|
|
55
56
|
|
|
56
57
|
# Constants for unit conversion
|
|
57
58
|
AU_PER_KM = 1.0 / 149597870.7 # 1 AU in km
|
|
@@ -152,10 +153,10 @@ class DEEphemeris:
|
|
|
152
153
|
self.version = version
|
|
153
154
|
self._jplephem = jplephem
|
|
154
155
|
self._kernel: Optional[object] = None
|
|
155
|
-
self._cache: dict = {}
|
|
156
|
+
self._cache: dict[str, Any] = {}
|
|
156
157
|
|
|
157
158
|
@property
|
|
158
|
-
def kernel(self):
|
|
159
|
+
def kernel(self) -> Optional[object]:
|
|
159
160
|
"""Lazy-load ephemeris kernel on first access.
|
|
160
161
|
|
|
161
162
|
Note: This requires jplephem to be installed and the kernel file
|
|
@@ -204,7 +205,7 @@ class DEEphemeris:
|
|
|
204
205
|
|
|
205
206
|
def sun_position(
|
|
206
207
|
self, jd: float, frame: Literal["icrf", "ecliptic"] = "icrf"
|
|
207
|
-
) -> Tuple[np.
|
|
208
|
+
) -> Tuple[NDArray[np.floating], NDArray[np.floating]]:
|
|
208
209
|
"""Compute Sun position and velocity.
|
|
209
210
|
|
|
210
211
|
Parameters
|
|
@@ -253,7 +254,7 @@ class DEEphemeris:
|
|
|
253
254
|
|
|
254
255
|
def moon_position(
|
|
255
256
|
self, jd: float, frame: Literal["icrf", "ecliptic", "earth_centered"] = "icrf"
|
|
256
|
-
) -> Tuple[np.
|
|
257
|
+
) -> Tuple[NDArray[np.floating], NDArray[np.floating]]:
|
|
257
258
|
"""Compute Moon position and velocity.
|
|
258
259
|
|
|
259
260
|
Parameters
|
|
@@ -323,7 +324,7 @@ class DEEphemeris:
|
|
|
323
324
|
],
|
|
324
325
|
jd: float,
|
|
325
326
|
frame: Literal["icrf", "ecliptic"] = "icrf",
|
|
326
|
-
) -> Tuple[np.ndarray, np.ndarray]:
|
|
327
|
+
) -> Tuple[np.ndarray[Any, Any], np.ndarray[Any, Any]]:
|
|
327
328
|
"""Compute planet position and velocity.
|
|
328
329
|
|
|
329
330
|
Parameters
|
|
@@ -379,7 +380,7 @@ class DEEphemeris:
|
|
|
379
380
|
|
|
380
381
|
def barycenter_position(
|
|
381
382
|
self, body: str, jd: float
|
|
382
|
-
) -> Tuple[np.
|
|
383
|
+
) -> Tuple[NDArray[np.floating], NDArray[np.floating]]:
|
|
383
384
|
"""Compute position of any body relative to Solar System Barycenter.
|
|
384
385
|
|
|
385
386
|
Parameters
|
|
@@ -424,7 +425,7 @@ def _get_default_ephemeris() -> DEEphemeris:
|
|
|
424
425
|
|
|
425
426
|
def sun_position(
|
|
426
427
|
jd: float, frame: Literal["icrf", "ecliptic"] = "icrf"
|
|
427
|
-
) -> Tuple[np.
|
|
428
|
+
) -> Tuple[NDArray[np.floating], NDArray[np.floating]]:
|
|
428
429
|
"""Convenience function: Compute Sun position and velocity.
|
|
429
430
|
|
|
430
431
|
Parameters
|
|
@@ -451,7 +452,7 @@ def sun_position(
|
|
|
451
452
|
|
|
452
453
|
def moon_position(
|
|
453
454
|
jd: float, frame: Literal["icrf", "ecliptic", "earth_centered"] = "icrf"
|
|
454
|
-
) -> Tuple[np.
|
|
455
|
+
) -> Tuple[NDArray[np.floating], NDArray[np.floating]]:
|
|
455
456
|
"""Convenience function: Compute Moon position and velocity.
|
|
456
457
|
|
|
457
458
|
Parameters
|
|
@@ -482,7 +483,7 @@ def planet_position(
|
|
|
482
483
|
],
|
|
483
484
|
jd: float,
|
|
484
485
|
frame: Literal["icrf", "ecliptic"] = "icrf",
|
|
485
|
-
) -> Tuple[np.ndarray, np.ndarray]:
|
|
486
|
+
) -> Tuple[np.ndarray[Any, Any], np.ndarray[Any, Any]]:
|
|
486
487
|
"""Convenience function: Compute planet position and velocity.
|
|
487
488
|
|
|
488
489
|
Parameters
|
|
@@ -509,7 +510,9 @@ def planet_position(
|
|
|
509
510
|
return _get_default_ephemeris().planet_position(planet, jd, frame=frame)
|
|
510
511
|
|
|
511
512
|
|
|
512
|
-
def barycenter_position(
|
|
513
|
+
def barycenter_position(
|
|
514
|
+
body: str, jd: float
|
|
515
|
+
) -> Tuple[NDArray[np.floating], NDArray[np.floating]]:
|
|
513
516
|
"""Convenience function: Position relative to Solar System Barycenter.
|
|
514
517
|
|
|
515
518
|
Parameters
|
|
@@ -23,7 +23,7 @@ References
|
|
|
23
23
|
|
|
24
24
|
import logging
|
|
25
25
|
from functools import lru_cache
|
|
26
|
-
from typing import Optional, Tuple
|
|
26
|
+
from typing import Any, Optional, Tuple
|
|
27
27
|
|
|
28
28
|
import numpy as np
|
|
29
29
|
from numpy.typing import NDArray
|
|
@@ -97,7 +97,9 @@ def precession_angles_iau76(T: float) -> Tuple[float, float, float]:
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
@lru_cache(maxsize=_CACHE_MAXSIZE)
|
|
100
|
-
def _precession_matrix_cached(
|
|
100
|
+
def _precession_matrix_cached(
|
|
101
|
+
jd_quantized: float,
|
|
102
|
+
) -> tuple[tuple[np.ndarray[Any, Any], ...], ...]:
|
|
101
103
|
"""Cached precession matrix computation (internal).
|
|
102
104
|
|
|
103
105
|
Returns tuple of tuples for hashability.
|
|
@@ -234,7 +236,9 @@ def mean_obliquity_iau80(jd: float) -> float:
|
|
|
234
236
|
|
|
235
237
|
|
|
236
238
|
@lru_cache(maxsize=_CACHE_MAXSIZE)
|
|
237
|
-
def _nutation_matrix_cached(
|
|
239
|
+
def _nutation_matrix_cached(
|
|
240
|
+
jd_quantized: float,
|
|
241
|
+
) -> tuple[tuple[np.ndarray[Any, Any], ...], ...]:
|
|
238
242
|
"""Cached nutation matrix computation (internal).
|
|
239
243
|
|
|
240
244
|
Returns tuple of tuples for hashability.
|
|
@@ -1414,7 +1418,7 @@ def clear_transformation_cache() -> None:
|
|
|
1414
1418
|
_logger.debug("Transformation matrix cache cleared")
|
|
1415
1419
|
|
|
1416
1420
|
|
|
1417
|
-
def get_cache_info() -> dict:
|
|
1421
|
+
def get_cache_info() -> dict[str, Any]:
|
|
1418
1422
|
"""Get cache statistics for transformation matrices.
|
|
1419
1423
|
|
|
1420
1424
|
Returns
|
pytcl/astronomical/relativity.py
CHANGED
|
@@ -20,6 +20,7 @@ References:
|
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
import numpy as np
|
|
23
|
+
from numpy.typing import NDArray
|
|
23
24
|
|
|
24
25
|
# Physical constants (CODATA 2018 values)
|
|
25
26
|
C_LIGHT = 299792458.0 # Speed of light (m/s)
|
|
@@ -154,9 +155,9 @@ def proper_time_rate(v: float, r: float, gm: float = GM_EARTH) -> float:
|
|
|
154
155
|
|
|
155
156
|
|
|
156
157
|
def shapiro_delay(
|
|
157
|
-
observer_pos: np.
|
|
158
|
-
light_source_pos: np.
|
|
159
|
-
gravitating_body_pos: np.
|
|
158
|
+
observer_pos: NDArray[np.floating],
|
|
159
|
+
light_source_pos: NDArray[np.floating],
|
|
160
|
+
gravitating_body_pos: NDArray[np.floating],
|
|
160
161
|
gm: float = GM_SUN,
|
|
161
162
|
) -> float:
|
|
162
163
|
"""Compute Shapiro time delay for light propagation through gravitational field.
|
|
@@ -263,8 +264,8 @@ def schwarzschild_precession_per_orbit(a: float, e: float, gm: float = GM_SUN) -
|
|
|
263
264
|
|
|
264
265
|
|
|
265
266
|
def post_newtonian_acceleration(
|
|
266
|
-
r_vec: np.
|
|
267
|
-
) -> np.
|
|
267
|
+
r_vec: NDArray[np.floating], v_vec: NDArray[np.floating], gm: float = GM_EARTH
|
|
268
|
+
) -> NDArray[np.floating]:
|
|
268
269
|
"""Compute post-Newtonian acceleration corrections (1PN order).
|
|
269
270
|
|
|
270
271
|
Extends Newtonian gravity with first-order post-Newtonian corrections.
|
|
@@ -26,10 +26,10 @@ from numpy.typing import NDArray
|
|
|
26
26
|
class OrbitType(Enum):
|
|
27
27
|
"""Classification of orbit types based on eccentricity."""
|
|
28
28
|
|
|
29
|
-
CIRCULAR = 0
|
|
30
|
-
ELLIPTICAL = 1
|
|
31
|
-
PARABOLIC = 2
|
|
32
|
-
HYPERBOLIC = 3
|
|
29
|
+
CIRCULAR = 0 # e = 0
|
|
30
|
+
ELLIPTICAL = 1 # 0 < e < 1
|
|
31
|
+
PARABOLIC = 2 # e = 1 (boundary case)
|
|
32
|
+
HYPERBOLIC = 3 # e > 1
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class ParabolicElements(NamedTuple):
|
|
@@ -303,9 +303,7 @@ def hyperbolic_anomaly_to_true_anomaly(H: float, e: float) -> float:
|
|
|
303
303
|
if e <= 1:
|
|
304
304
|
raise ValueError(f"Eccentricity must be > 1 for hyperbolic orbits, got {e}")
|
|
305
305
|
|
|
306
|
-
nu = 2.0 * np.arctan(
|
|
307
|
-
np.sqrt((e + 1.0) / (e - 1.0)) * np.tanh(H / 2.0)
|
|
308
|
-
)
|
|
306
|
+
nu = 2.0 * np.arctan(np.sqrt((e + 1.0) / (e - 1.0)) * np.tanh(H / 2.0))
|
|
309
307
|
|
|
310
308
|
return nu
|
|
311
309
|
|
|
@@ -334,9 +332,7 @@ def true_anomaly_to_hyperbolic_anomaly(nu: float, e: float) -> float:
|
|
|
334
332
|
if e <= 1:
|
|
335
333
|
raise ValueError(f"Eccentricity must be > 1 for hyperbolic orbits, got {e}")
|
|
336
334
|
|
|
337
|
-
H = 2.0 * np.arctanh(
|
|
338
|
-
np.sqrt((e - 1.0) / (e + 1.0)) * np.tan(nu / 2.0)
|
|
339
|
-
)
|
|
335
|
+
H = 2.0 * np.arctanh(np.sqrt((e - 1.0) / (e + 1.0)) * np.tan(nu / 2.0))
|
|
340
336
|
|
|
341
337
|
return H
|
|
342
338
|
|
|
@@ -501,10 +497,10 @@ def semi_major_axis_from_energy(mu: float, specific_energy: float) -> float:
|
|
|
501
497
|
|
|
502
498
|
|
|
503
499
|
def eccentricity_vector(
|
|
504
|
-
r: NDArray,
|
|
505
|
-
v: NDArray,
|
|
500
|
+
r: NDArray[np.floating],
|
|
501
|
+
v: NDArray[np.floating],
|
|
506
502
|
mu: float,
|
|
507
|
-
) -> NDArray:
|
|
503
|
+
) -> NDArray[np.floating]:
|
|
508
504
|
"""
|
|
509
505
|
Compute eccentricity vector from position and velocity.
|
|
510
506
|
|
pytcl/atmosphere/__init__.py
CHANGED
|
@@ -24,12 +24,6 @@ from pytcl.atmosphere.ionosphere import (
|
|
|
24
24
|
simple_iri,
|
|
25
25
|
)
|
|
26
26
|
from pytcl.atmosphere.models import G0 # Constants
|
|
27
|
-
from pytcl.atmosphere.nrlmsise00 import (
|
|
28
|
-
F107Index,
|
|
29
|
-
NRLMSISE00,
|
|
30
|
-
NRLMSISE00Output,
|
|
31
|
-
nrlmsise00,
|
|
32
|
-
)
|
|
33
27
|
from pytcl.atmosphere.models import (
|
|
34
28
|
GAMMA,
|
|
35
29
|
P0,
|
|
@@ -43,6 +37,12 @@ from pytcl.atmosphere.models import (
|
|
|
43
37
|
true_airspeed_from_mach,
|
|
44
38
|
us_standard_atmosphere_1976,
|
|
45
39
|
)
|
|
40
|
+
from pytcl.atmosphere.nrlmsise00 import (
|
|
41
|
+
NRLMSISE00,
|
|
42
|
+
F107Index,
|
|
43
|
+
NRLMSISE00Output,
|
|
44
|
+
nrlmsise00,
|
|
45
|
+
)
|
|
46
46
|
|
|
47
47
|
__all__ = [
|
|
48
48
|
# Atmosphere state and models
|