nrl-tracker 0.22.5__py3-none-any.whl → 1.7.5__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.
Files changed (84) hide show
  1. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.7.5.dist-info}/METADATA +57 -10
  2. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.7.5.dist-info}/RECORD +84 -69
  3. pytcl/__init__.py +4 -3
  4. pytcl/assignment_algorithms/__init__.py +28 -0
  5. pytcl/assignment_algorithms/gating.py +10 -10
  6. pytcl/assignment_algorithms/jpda.py +40 -40
  7. pytcl/assignment_algorithms/nd_assignment.py +379 -0
  8. pytcl/assignment_algorithms/network_flow.py +371 -0
  9. pytcl/assignment_algorithms/three_dimensional/assignment.py +3 -3
  10. pytcl/astronomical/__init__.py +104 -3
  11. pytcl/astronomical/ephemerides.py +14 -11
  12. pytcl/astronomical/reference_frames.py +865 -56
  13. pytcl/astronomical/relativity.py +6 -5
  14. pytcl/astronomical/sgp4.py +710 -0
  15. pytcl/astronomical/special_orbits.py +532 -0
  16. pytcl/astronomical/tle.py +558 -0
  17. pytcl/atmosphere/__init__.py +43 -1
  18. pytcl/atmosphere/ionosphere.py +512 -0
  19. pytcl/atmosphere/nrlmsise00.py +809 -0
  20. pytcl/clustering/dbscan.py +2 -2
  21. pytcl/clustering/gaussian_mixture.py +3 -3
  22. pytcl/clustering/hierarchical.py +15 -15
  23. pytcl/clustering/kmeans.py +4 -4
  24. pytcl/containers/__init__.py +24 -0
  25. pytcl/containers/base.py +219 -0
  26. pytcl/containers/cluster_set.py +12 -2
  27. pytcl/containers/covertree.py +26 -29
  28. pytcl/containers/kd_tree.py +94 -29
  29. pytcl/containers/rtree.py +200 -1
  30. pytcl/containers/vptree.py +21 -28
  31. pytcl/coordinate_systems/conversions/geodetic.py +272 -5
  32. pytcl/coordinate_systems/jacobians/jacobians.py +2 -2
  33. pytcl/coordinate_systems/projections/__init__.py +1 -1
  34. pytcl/coordinate_systems/projections/projections.py +2 -2
  35. pytcl/coordinate_systems/rotations/rotations.py +10 -6
  36. pytcl/core/__init__.py +18 -0
  37. pytcl/core/validation.py +333 -2
  38. pytcl/dynamic_estimation/__init__.py +26 -0
  39. pytcl/dynamic_estimation/gaussian_sum_filter.py +434 -0
  40. pytcl/dynamic_estimation/imm.py +14 -14
  41. pytcl/dynamic_estimation/kalman/__init__.py +30 -0
  42. pytcl/dynamic_estimation/kalman/constrained.py +382 -0
  43. pytcl/dynamic_estimation/kalman/extended.py +8 -8
  44. pytcl/dynamic_estimation/kalman/h_infinity.py +613 -0
  45. pytcl/dynamic_estimation/kalman/square_root.py +60 -573
  46. pytcl/dynamic_estimation/kalman/sr_ukf.py +302 -0
  47. pytcl/dynamic_estimation/kalman/ud_filter.py +410 -0
  48. pytcl/dynamic_estimation/kalman/unscented.py +8 -6
  49. pytcl/dynamic_estimation/particle_filters/bootstrap.py +15 -15
  50. pytcl/dynamic_estimation/rbpf.py +589 -0
  51. pytcl/gravity/egm.py +13 -0
  52. pytcl/gravity/spherical_harmonics.py +98 -37
  53. pytcl/gravity/tides.py +6 -6
  54. pytcl/logging_config.py +328 -0
  55. pytcl/magnetism/__init__.py +7 -0
  56. pytcl/magnetism/emm.py +10 -3
  57. pytcl/magnetism/wmm.py +260 -23
  58. pytcl/mathematical_functions/combinatorics/combinatorics.py +5 -5
  59. pytcl/mathematical_functions/geometry/geometry.py +5 -5
  60. pytcl/mathematical_functions/numerical_integration/quadrature.py +6 -6
  61. pytcl/mathematical_functions/signal_processing/detection.py +24 -24
  62. pytcl/mathematical_functions/signal_processing/filters.py +14 -14
  63. pytcl/mathematical_functions/signal_processing/matched_filter.py +12 -12
  64. pytcl/mathematical_functions/special_functions/bessel.py +15 -3
  65. pytcl/mathematical_functions/special_functions/debye.py +136 -26
  66. pytcl/mathematical_functions/special_functions/error_functions.py +3 -1
  67. pytcl/mathematical_functions/special_functions/gamma_functions.py +4 -4
  68. pytcl/mathematical_functions/special_functions/hypergeometric.py +81 -15
  69. pytcl/mathematical_functions/transforms/fourier.py +8 -8
  70. pytcl/mathematical_functions/transforms/stft.py +12 -12
  71. pytcl/mathematical_functions/transforms/wavelets.py +9 -9
  72. pytcl/navigation/geodesy.py +246 -160
  73. pytcl/navigation/great_circle.py +101 -19
  74. pytcl/plotting/coordinates.py +7 -7
  75. pytcl/plotting/tracks.py +2 -2
  76. pytcl/static_estimation/maximum_likelihood.py +16 -14
  77. pytcl/static_estimation/robust.py +5 -5
  78. pytcl/terrain/loaders.py +5 -5
  79. pytcl/trackers/hypothesis.py +1 -1
  80. pytcl/trackers/mht.py +9 -9
  81. pytcl/trackers/multi_target.py +1 -1
  82. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.7.5.dist-info}/LICENSE +0 -0
  83. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.7.5.dist-info}/WHEEL +0 -0
  84. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.7.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,382 @@
1
+ """
2
+ Constrained Extended Kalman Filter (CEKF).
3
+
4
+ Extends the Extended Kalman Filter to enforce constraints on the state
5
+ estimate. Uses Lagrange multiplier method to project onto constraint manifold
6
+ while maintaining positive definite covariance.
7
+
8
+ References
9
+ ----------
10
+ .. [1] Simon, D. (2006). Optimal State Estimation: Kalman, H∞, and Nonlinear
11
+ Approaches. Wiley-Interscience.
12
+ .. [2] Simon, D. & Simon, D. L. (2010). Constrained Kalman filtering via
13
+ density function truncation. Journal of Guidance, Control, and Dynamics.
14
+ """
15
+
16
+ from typing import Any, Callable, Optional
17
+
18
+ import numpy as np
19
+ from numpy.typing import ArrayLike, NDArray
20
+
21
+ from pytcl.dynamic_estimation.kalman.extended import ekf_predict, ekf_update
22
+ from pytcl.dynamic_estimation.kalman.linear import KalmanPrediction, KalmanUpdate
23
+
24
+
25
+ class ConstraintFunction:
26
+ """Base class for state constraints."""
27
+
28
+ def __init__(
29
+ self,
30
+ g: Callable[[NDArray[Any]], NDArray[Any]],
31
+ G: Optional[Callable[[NDArray[Any]], NDArray[Any]]] = None,
32
+ constraint_type: str = "inequality",
33
+ ):
34
+ """
35
+ Define a constraint: g(x) ≤ 0 (inequality) or g(x) = 0 (equality).
36
+
37
+ Parameters
38
+ ----------
39
+ g : callable
40
+ Constraint function: g(x) -> scalar or array
41
+ Inequality: g(x) ≤ 0
42
+ Equality: g(x) = 0
43
+ G : callable, optional
44
+ Jacobian of g with respect to x: ∂g/∂x
45
+ If None, computed numerically.
46
+ constraint_type : {'inequality', 'equality'}
47
+ Constraint type.
48
+ """
49
+ self.g = g
50
+ self.G = G
51
+ self.constraint_type = constraint_type
52
+
53
+ def evaluate(self, x: NDArray[Any]) -> NDArray[Any]:
54
+ """Evaluate constraint at state x."""
55
+ return np.atleast_1d(np.asarray(self.g(x), dtype=np.float64))
56
+
57
+ def jacobian(self, x: NDArray[Any]) -> NDArray[Any]:
58
+ """Compute constraint Jacobian at x."""
59
+ if self.G is not None:
60
+ return np.atleast_2d(np.asarray(self.G(x), dtype=np.float64))
61
+ else:
62
+ # Numerical differentiation
63
+ eps = 1e-6
64
+ n = len(x)
65
+ g_x = self.evaluate(x)
66
+ m = len(g_x)
67
+ J = np.zeros((m, n))
68
+ for i in range(n):
69
+ x_plus = x.copy()
70
+ x_plus[i] += eps
71
+ g_plus = self.evaluate(x_plus)
72
+ J[:, i] = (g_plus - g_x) / eps
73
+ return J
74
+
75
+ def is_satisfied(self, x: NDArray[Any], tol: float = 1e-6) -> bool:
76
+ """Check if constraint is satisfied."""
77
+ g_val = self.evaluate(x)
78
+ if self.constraint_type == "inequality":
79
+ return np.all(g_val <= tol)
80
+ else: # equality
81
+ return np.allclose(g_val, 0, atol=tol)
82
+
83
+
84
+ class ConstrainedEKF:
85
+ """
86
+ Extended Kalman Filter with state constraints.
87
+
88
+ Enforces linear and/or nonlinear constraints on state estimate using
89
+ Lagrange multiplier method with covariance projection.
90
+
91
+ Attributes
92
+ ----------
93
+ constraints : list of ConstraintFunction
94
+ List of active constraints.
95
+ """
96
+
97
+ def __init__(self) -> None:
98
+ """Initialize Constrained EKF."""
99
+ self.constraints: list[ConstraintFunction] = []
100
+
101
+ def add_constraint(self, constraint: ConstraintFunction) -> None:
102
+ """
103
+ Add a constraint to the filter.
104
+
105
+ Parameters
106
+ ----------
107
+ constraint : ConstraintFunction
108
+ Constraint to enforce.
109
+ """
110
+ self.constraints.append(constraint)
111
+
112
+ def predict(
113
+ self,
114
+ x: ArrayLike,
115
+ P: ArrayLike,
116
+ f: Callable[[NDArray[Any]], NDArray[Any]],
117
+ F: ArrayLike,
118
+ Q: ArrayLike,
119
+ ) -> KalmanPrediction:
120
+ """
121
+ Constrained EKF prediction step.
122
+
123
+ Performs standard EKF prediction (constraints not enforced here,
124
+ only checked). Constraint enforcement happens in update step.
125
+
126
+ Parameters
127
+ ----------
128
+ x : array_like
129
+ Current state estimate, shape (n,).
130
+ P : array_like
131
+ Current state covariance, shape (n, n).
132
+ f : callable
133
+ Nonlinear state transition function.
134
+ F : array_like
135
+ Jacobian of f at current state.
136
+ Q : array_like
137
+ Process noise covariance, shape (n, n).
138
+
139
+ Returns
140
+ -------
141
+ result : KalmanPrediction
142
+ Predicted state and covariance.
143
+ """
144
+ return ekf_predict(x, P, f, F, Q)
145
+
146
+ def update(
147
+ self,
148
+ x: ArrayLike,
149
+ P: ArrayLike,
150
+ z: ArrayLike,
151
+ h: Callable[[NDArray[Any]], NDArray[Any]],
152
+ H: ArrayLike,
153
+ R: ArrayLike,
154
+ ) -> KalmanUpdate:
155
+ """
156
+ Constrained EKF update step.
157
+
158
+ Performs standard EKF update, then projects result onto constraint
159
+ manifold.
160
+
161
+ Parameters
162
+ ----------
163
+ x : array_like
164
+ Predicted state estimate, shape (n,).
165
+ P : array_like
166
+ Predicted state covariance, shape (n, n).
167
+ z : array_like
168
+ Measurement, shape (m,).
169
+ h : callable
170
+ Nonlinear measurement function.
171
+ H : array_like
172
+ Jacobian of h at current state.
173
+ R : array_like
174
+ Measurement noise covariance, shape (m, m).
175
+
176
+ Returns
177
+ -------
178
+ result : KalmanUpdate
179
+ Updated state and covariance (constrained).
180
+ """
181
+ # Standard EKF update
182
+ result = ekf_update(x, P, z, h, H, R)
183
+ x_upd = result.x
184
+ P_upd = result.P
185
+
186
+ # Apply constraint projection
187
+ if self.constraints:
188
+ x_upd, P_upd = self._project_onto_constraints(x_upd, P_upd)
189
+
190
+ return KalmanUpdate(
191
+ x=x_upd,
192
+ P=P_upd,
193
+ y=result.y,
194
+ S=result.S,
195
+ K=result.K,
196
+ likelihood=result.likelihood,
197
+ )
198
+
199
+ def _project_onto_constraints(
200
+ self,
201
+ x: NDArray[Any],
202
+ P: NDArray[Any],
203
+ max_iter: int = 10,
204
+ tol: float = 1e-6,
205
+ ) -> tuple[NDArray[Any], NDArray[Any]]:
206
+ """
207
+ Project state and covariance onto constraint manifold.
208
+
209
+ Uses iterative Lagrange multiplier method with covariance
210
+ projection to enforce constraints while maintaining positive
211
+ definiteness.
212
+
213
+ Parameters
214
+ ----------
215
+ x : ndarray
216
+ State estimate, shape (n,).
217
+ P : ndarray
218
+ Covariance matrix, shape (n, n).
219
+ max_iter : int
220
+ Maximum iterations for constraint projection.
221
+ tol : float
222
+ Convergence tolerance.
223
+
224
+ Returns
225
+ -------
226
+ x_proj : ndarray
227
+ Constrained state estimate.
228
+ P_proj : ndarray
229
+ Projected covariance.
230
+ """
231
+ x_proj = x.copy()
232
+ P_proj = P.copy()
233
+
234
+ # Check which constraints are violated
235
+ violated: list[ConstraintFunction] = [
236
+ c for c in self.constraints if not c.is_satisfied(x_proj)
237
+ ]
238
+
239
+ if not violated:
240
+ return x_proj, P_proj
241
+
242
+ # Iterative projection
243
+ for iteration in range(max_iter):
244
+ converged = True
245
+
246
+ for constraint in violated:
247
+ g_val = constraint.evaluate(x_proj)
248
+ G = constraint.jacobian(x_proj)
249
+
250
+ # Only process violated constraints
251
+ if constraint.constraint_type == "inequality":
252
+ mask = g_val > tol
253
+ else:
254
+ mask = np.abs(g_val) > tol
255
+
256
+ if not np.any(mask):
257
+ continue
258
+
259
+ converged = False
260
+
261
+ # Lagrange multiplier for this constraint
262
+ # λ = -(G P Gᵀ + μ)⁻¹ G (x - x_ref)
263
+ # where x_ref is desired state (we use x)
264
+
265
+ GP = G @ P_proj
266
+ GPGt = GP @ G.T
267
+
268
+ # Add small regularization for numerical stability
269
+ mu = np.eye(GPGt.shape[0]) * 1e-6
270
+
271
+ try:
272
+ GPGt_inv = np.linalg.inv(GPGt + mu)
273
+ lam = -GPGt_inv @ (G @ x_proj + g_val)
274
+
275
+ # State correction
276
+ x_corr = P_proj @ G.T @ lam
277
+ x_proj = x_proj + x_corr
278
+
279
+ # Covariance projection
280
+ # P_proj = P - P G^T (G P G^T)^{-1} G P
281
+ P_proj = P_proj - GP.T @ GPGt_inv @ GP
282
+
283
+ # Ensure symmetry
284
+ P_proj = (P_proj + P_proj.T) / 2
285
+
286
+ # Enforce positive definiteness
287
+ eigvals, eigvecs = np.linalg.eigh(P_proj)
288
+ if np.any(eigvals < 0):
289
+ eigvals[eigvals < 1e-10] = 1e-10
290
+ P_proj = eigvecs @ np.diag(eigvals) @ eigvecs.T
291
+
292
+ except np.linalg.LinAlgError:
293
+ # If inversion fails, use pseudoinverse
294
+ GPGt_pinv = np.linalg.pinv(GPGt)
295
+ lam = -GPGt_pinv @ (G @ x_proj + g_val)
296
+ x_proj = x_proj + P_proj @ G.T @ lam
297
+
298
+ if converged:
299
+ break
300
+
301
+ return x_proj, P_proj
302
+
303
+
304
+ def constrained_ekf_predict(
305
+ x: ArrayLike,
306
+ P: ArrayLike,
307
+ f: Callable[[NDArray[Any]], NDArray[Any]],
308
+ F: ArrayLike,
309
+ Q: ArrayLike,
310
+ ) -> KalmanPrediction:
311
+ """
312
+ Convenience function for constrained EKF prediction.
313
+
314
+ Parameters
315
+ ----------
316
+ x : array_like
317
+ Current state estimate.
318
+ P : array_like
319
+ Current covariance.
320
+ f : callable
321
+ Nonlinear dynamics function.
322
+ F : array_like
323
+ Jacobian of f.
324
+ Q : array_like
325
+ Process noise covariance.
326
+
327
+ Returns
328
+ -------
329
+ result : KalmanPrediction
330
+ Predicted state and covariance.
331
+ """
332
+ cekf = ConstrainedEKF()
333
+ return cekf.predict(x, P, f, F, Q)
334
+
335
+
336
+ def constrained_ekf_update(
337
+ x: ArrayLike,
338
+ P: ArrayLike,
339
+ z: ArrayLike,
340
+ h: Callable[[NDArray[Any]], NDArray[Any]],
341
+ H: ArrayLike,
342
+ R: ArrayLike,
343
+ constraints: Optional[list[ConstraintFunction]] = None,
344
+ ) -> KalmanUpdate:
345
+ """
346
+ Convenience function for constrained EKF update.
347
+
348
+ Parameters
349
+ ----------
350
+ x : array_like
351
+ Predicted state.
352
+ P : array_like
353
+ Predicted covariance.
354
+ z : array_like
355
+ Measurement.
356
+ h : callable
357
+ Nonlinear measurement function.
358
+ H : array_like
359
+ Jacobian of h.
360
+ R : array_like
361
+ Measurement noise covariance.
362
+ constraints : list, optional
363
+ List of ConstraintFunction objects.
364
+
365
+ Returns
366
+ -------
367
+ result : KalmanUpdate
368
+ Updated state and covariance.
369
+ """
370
+ cekf = ConstrainedEKF()
371
+ if constraints:
372
+ for c in constraints:
373
+ cekf.add_constraint(c)
374
+ return cekf.update(x, P, z, h, H, R)
375
+
376
+
377
+ __all__ = [
378
+ "ConstraintFunction",
379
+ "ConstrainedEKF",
380
+ "constrained_ekf_predict",
381
+ "constrained_ekf_update",
382
+ ]
@@ -5,7 +5,7 @@ The EKF handles nonlinear dynamics and/or measurements by linearizing
5
5
  around the current state estimate using Jacobians.
6
6
  """
7
7
 
8
- from typing import Callable
8
+ from typing import Any, Callable
9
9
 
10
10
  import numpy as np
11
11
  from numpy.typing import ArrayLike, NDArray
@@ -16,7 +16,7 @@ from pytcl.dynamic_estimation.kalman.linear import KalmanPrediction, KalmanUpdat
16
16
  def ekf_predict(
17
17
  x: ArrayLike,
18
18
  P: ArrayLike,
19
- f: Callable[[NDArray], NDArray],
19
+ f: Callable[[NDArray[Any]], NDArray[Any]],
20
20
  F: ArrayLike,
21
21
  Q: ArrayLike,
22
22
  ) -> KalmanPrediction:
@@ -86,7 +86,7 @@ def ekf_update(
86
86
  x: ArrayLike,
87
87
  P: ArrayLike,
88
88
  z: ArrayLike,
89
- h: Callable[[NDArray], NDArray],
89
+ h: Callable[[NDArray[Any]], NDArray[Any]],
90
90
  H: ArrayLike,
91
91
  R: ArrayLike,
92
92
  ) -> KalmanUpdate:
@@ -183,7 +183,7 @@ def ekf_update(
183
183
 
184
184
 
185
185
  def numerical_jacobian(
186
- f: Callable[[NDArray], NDArray],
186
+ f: Callable[[NDArray[Any]], NDArray[Any]],
187
187
  x: ArrayLike,
188
188
  dx: float = 1e-7,
189
189
  ) -> NDArray[np.floating]:
@@ -239,7 +239,7 @@ def numerical_jacobian(
239
239
  def ekf_predict_auto(
240
240
  x: ArrayLike,
241
241
  P: ArrayLike,
242
- f: Callable[[NDArray], NDArray],
242
+ f: Callable[[NDArray[Any]], NDArray[Any]],
243
243
  Q: ArrayLike,
244
244
  dx: float = 1e-7,
245
245
  ) -> KalmanPrediction:
@@ -273,7 +273,7 @@ def ekf_update_auto(
273
273
  x: ArrayLike,
274
274
  P: ArrayLike,
275
275
  z: ArrayLike,
276
- h: Callable[[NDArray], NDArray],
276
+ h: Callable[[NDArray[Any]], NDArray[Any]],
277
277
  R: ArrayLike,
278
278
  dx: float = 1e-7,
279
279
  ) -> KalmanUpdate:
@@ -309,8 +309,8 @@ def iterated_ekf_update(
309
309
  x: ArrayLike,
310
310
  P: ArrayLike,
311
311
  z: ArrayLike,
312
- h: Callable[[NDArray], NDArray],
313
- H_func: Callable[[NDArray], NDArray],
312
+ h: Callable[[NDArray[Any]], NDArray[Any]],
313
+ H_func: Callable[[NDArray[Any]], NDArray[Any]],
314
314
  R: ArrayLike,
315
315
  max_iter: int = 10,
316
316
  tol: float = 1e-6,