nrl-tracker 1.8.0__py3-none-any.whl → 1.9.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.8.0.dist-info → nrl_tracker-1.9.0.dist-info}/METADATA +2 -2
- {nrl_tracker-1.8.0.dist-info → nrl_tracker-1.9.0.dist-info}/RECORD +32 -28
- pytcl/__init__.py +3 -3
- pytcl/assignment_algorithms/dijkstra_min_cost.py +0 -1
- pytcl/assignment_algorithms/network_simplex.py +0 -2
- pytcl/astronomical/ephemerides.py +8 -4
- pytcl/astronomical/relativity.py +20 -0
- pytcl/containers/__init__.py +19 -8
- pytcl/containers/base.py +82 -9
- pytcl/containers/covertree.py +14 -21
- pytcl/containers/kd_tree.py +18 -45
- pytcl/containers/rtree.py +43 -4
- pytcl/containers/vptree.py +14 -21
- pytcl/core/__init__.py +59 -2
- pytcl/core/constants.py +59 -0
- pytcl/core/exceptions.py +865 -0
- pytcl/core/optional_deps.py +531 -0
- pytcl/core/validation.py +4 -6
- pytcl/dynamic_estimation/kalman/matrix_utils.py +427 -0
- pytcl/dynamic_estimation/kalman/square_root.py +20 -213
- pytcl/dynamic_estimation/kalman/sr_ukf.py +5 -5
- pytcl/dynamic_estimation/kalman/types.py +98 -0
- pytcl/mathematical_functions/signal_processing/detection.py +19 -0
- pytcl/mathematical_functions/transforms/wavelets.py +7 -6
- pytcl/plotting/coordinates.py +25 -27
- pytcl/plotting/ellipses.py +14 -16
- pytcl/plotting/metrics.py +7 -5
- pytcl/plotting/tracks.py +8 -7
- pytcl/terrain/loaders.py +10 -6
- {nrl_tracker-1.8.0.dist-info → nrl_tracker-1.9.0.dist-info}/LICENSE +0 -0
- {nrl_tracker-1.8.0.dist-info → nrl_tracker-1.9.0.dist-info}/WHEEL +0 -0
- {nrl_tracker-1.8.0.dist-info → nrl_tracker-1.9.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Matrix utility functions for Kalman filter implementations.
|
|
3
|
+
|
|
4
|
+
This module provides numerically stable matrix operations used across
|
|
5
|
+
multiple Kalman filter implementations. Separating these utilities prevents
|
|
6
|
+
circular imports between filter implementations.
|
|
7
|
+
|
|
8
|
+
Functions include:
|
|
9
|
+
- Cholesky factor update/downdate
|
|
10
|
+
- QR-based covariance propagation
|
|
11
|
+
- Matrix symmetry enforcement
|
|
12
|
+
- Matrix square root computation
|
|
13
|
+
- Innovation likelihood computation
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import Optional, Tuple
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
from numpy.typing import NDArray
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def cholesky_update(
|
|
23
|
+
S: NDArray[np.floating], v: NDArray[np.floating], sign: float = 1.0
|
|
24
|
+
) -> NDArray[np.floating]:
|
|
25
|
+
"""
|
|
26
|
+
Rank-1 Cholesky update/downdate.
|
|
27
|
+
|
|
28
|
+
Computes the Cholesky factor of P ± v @ v.T given S where P = S @ S.T.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
S : ndarray
|
|
33
|
+
Lower triangular Cholesky factor, shape (n, n).
|
|
34
|
+
v : ndarray
|
|
35
|
+
Vector for rank-1 update, shape (n,).
|
|
36
|
+
sign : float
|
|
37
|
+
+1 for update (addition), -1 for downdate (subtraction).
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
S_new : ndarray
|
|
42
|
+
Updated lower triangular Cholesky factor.
|
|
43
|
+
|
|
44
|
+
Notes
|
|
45
|
+
-----
|
|
46
|
+
Uses the efficient O(n²) algorithm from [1].
|
|
47
|
+
|
|
48
|
+
References
|
|
49
|
+
----------
|
|
50
|
+
.. [1] P. E. Gill, G. H. Golub, W. Murray, and M. A. Saunders,
|
|
51
|
+
"Methods for modifying matrix factorizations,"
|
|
52
|
+
Mathematics of Computation, vol. 28, pp. 505-535, 1974.
|
|
53
|
+
|
|
54
|
+
Examples
|
|
55
|
+
--------
|
|
56
|
+
>>> import numpy as np
|
|
57
|
+
>>> S = np.linalg.cholesky(np.eye(2))
|
|
58
|
+
>>> v = np.array([0.5, 0.5])
|
|
59
|
+
>>> S_updated = cholesky_update(S, v, sign=1.0)
|
|
60
|
+
>>> P_updated = S_updated @ S_updated.T
|
|
61
|
+
>>> np.allclose(P_updated, np.eye(2) + np.outer(v, v))
|
|
62
|
+
True
|
|
63
|
+
"""
|
|
64
|
+
S = np.asarray(S, dtype=np.float64).copy()
|
|
65
|
+
v = np.asarray(v, dtype=np.float64).flatten().copy()
|
|
66
|
+
n = len(v)
|
|
67
|
+
|
|
68
|
+
if sign > 0:
|
|
69
|
+
# Cholesky update
|
|
70
|
+
for k in range(n):
|
|
71
|
+
r = np.sqrt(S[k, k] ** 2 + v[k] ** 2)
|
|
72
|
+
c = r / S[k, k]
|
|
73
|
+
s = v[k] / S[k, k]
|
|
74
|
+
S[k, k] = r
|
|
75
|
+
if k < n - 1:
|
|
76
|
+
S[k + 1 :, k] = (S[k + 1 :, k] + s * v[k + 1 :]) / c
|
|
77
|
+
v[k + 1 :] = c * v[k + 1 :] - s * S[k + 1 :, k]
|
|
78
|
+
else:
|
|
79
|
+
# Cholesky downdate
|
|
80
|
+
for k in range(n):
|
|
81
|
+
r_sq = S[k, k] ** 2 - v[k] ** 2
|
|
82
|
+
if r_sq < 0:
|
|
83
|
+
raise ValueError("Downdate would make matrix non-positive definite")
|
|
84
|
+
r = np.sqrt(r_sq)
|
|
85
|
+
c = r / S[k, k]
|
|
86
|
+
s = v[k] / S[k, k]
|
|
87
|
+
S[k, k] = r
|
|
88
|
+
if k < n - 1:
|
|
89
|
+
S[k + 1 :, k] = (S[k + 1 :, k] - s * v[k + 1 :]) / c
|
|
90
|
+
v[k + 1 :] = c * v[k + 1 :] - s * S[k + 1 :, k]
|
|
91
|
+
|
|
92
|
+
return S
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def qr_update(
|
|
96
|
+
S_x: NDArray[np.floating],
|
|
97
|
+
S_noise: NDArray[np.floating],
|
|
98
|
+
F: Optional[NDArray[np.floating]] = None,
|
|
99
|
+
) -> NDArray[np.floating]:
|
|
100
|
+
"""
|
|
101
|
+
QR-based covariance square root update.
|
|
102
|
+
|
|
103
|
+
Computes the Cholesky factor of F @ P @ F.T + Q given S_x (where P = S_x @ S_x.T)
|
|
104
|
+
and S_noise (where Q = S_noise @ S_noise.T).
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
S_x : ndarray
|
|
109
|
+
Lower triangular Cholesky factor of state covariance, shape (n, n).
|
|
110
|
+
S_noise : ndarray
|
|
111
|
+
Lower triangular Cholesky factor of noise covariance, shape (n, n).
|
|
112
|
+
F : ndarray, optional
|
|
113
|
+
State transition matrix, shape (n, n). If None, uses identity.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
S_new : ndarray
|
|
118
|
+
Lower triangular Cholesky factor of the updated covariance.
|
|
119
|
+
|
|
120
|
+
Notes
|
|
121
|
+
-----
|
|
122
|
+
Uses QR decomposition for numerical stability. The compound matrix
|
|
123
|
+
[F @ S_x, S_noise].T is QR decomposed, and R.T gives the new Cholesky factor.
|
|
124
|
+
|
|
125
|
+
Examples
|
|
126
|
+
--------
|
|
127
|
+
>>> import numpy as np
|
|
128
|
+
>>> S_x = np.linalg.cholesky(np.eye(2) * 0.1)
|
|
129
|
+
>>> S_noise = np.linalg.cholesky(np.eye(2) * 0.01)
|
|
130
|
+
>>> F = np.array([[1, 1], [0, 1]])
|
|
131
|
+
>>> S_new = qr_update(S_x, S_noise, F)
|
|
132
|
+
"""
|
|
133
|
+
S_x = np.asarray(S_x, dtype=np.float64)
|
|
134
|
+
S_noise = np.asarray(S_noise, dtype=np.float64)
|
|
135
|
+
n = S_x.shape[0]
|
|
136
|
+
|
|
137
|
+
if F is not None:
|
|
138
|
+
F = np.asarray(F, dtype=np.float64)
|
|
139
|
+
FS = F @ S_x
|
|
140
|
+
else:
|
|
141
|
+
FS = S_x
|
|
142
|
+
|
|
143
|
+
# Stack the matrices: [F @ S_x; S_noise]
|
|
144
|
+
compound = np.vstack([FS.T, S_noise.T])
|
|
145
|
+
|
|
146
|
+
# QR decomposition
|
|
147
|
+
_, R = np.linalg.qr(compound)
|
|
148
|
+
|
|
149
|
+
# The upper triangular R gives us the new Cholesky factor
|
|
150
|
+
# Take absolute values on diagonal to ensure positive
|
|
151
|
+
S_new = R[:n, :n].T
|
|
152
|
+
for i in range(n):
|
|
153
|
+
if S_new[i, i] < 0:
|
|
154
|
+
S_new[i:, i] = -S_new[i:, i]
|
|
155
|
+
|
|
156
|
+
return S_new
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def ensure_symmetric(P: NDArray[np.floating]) -> NDArray[np.floating]:
|
|
160
|
+
"""
|
|
161
|
+
Enforce symmetry of a covariance matrix.
|
|
162
|
+
|
|
163
|
+
Computes (P + P.T) / 2 to ensure the matrix is exactly symmetric,
|
|
164
|
+
which can be lost due to numerical precision issues in matrix operations.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
P : ndarray
|
|
169
|
+
Square matrix to symmetrize, shape (n, n).
|
|
170
|
+
|
|
171
|
+
Returns
|
|
172
|
+
-------
|
|
173
|
+
P_sym : ndarray
|
|
174
|
+
Symmetric matrix.
|
|
175
|
+
|
|
176
|
+
Examples
|
|
177
|
+
--------
|
|
178
|
+
>>> import numpy as np
|
|
179
|
+
>>> P = np.array([[1.0, 0.5 + 1e-15], [0.5, 1.0]])
|
|
180
|
+
>>> P_sym = ensure_symmetric(P)
|
|
181
|
+
>>> np.allclose(P_sym, P_sym.T)
|
|
182
|
+
True
|
|
183
|
+
"""
|
|
184
|
+
P = np.asarray(P, dtype=np.float64)
|
|
185
|
+
return (P + P.T) / 2
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def compute_matrix_sqrt(
|
|
189
|
+
P: NDArray[np.floating],
|
|
190
|
+
scale: float = 1.0,
|
|
191
|
+
use_eigh_fallback: bool = True,
|
|
192
|
+
) -> NDArray[np.floating]:
|
|
193
|
+
"""
|
|
194
|
+
Compute the matrix square root using Cholesky or eigendecomposition.
|
|
195
|
+
|
|
196
|
+
Attempts Cholesky decomposition first (faster, but requires positive definiteness).
|
|
197
|
+
If that fails and use_eigh_fallback is True, falls back to eigendecomposition
|
|
198
|
+
which is more robust for nearly singular matrices.
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
P : ndarray
|
|
203
|
+
Symmetric positive semi-definite matrix, shape (n, n).
|
|
204
|
+
scale : float, optional
|
|
205
|
+
Scale factor to multiply P by before taking square root. Default is 1.0.
|
|
206
|
+
use_eigh_fallback : bool, optional
|
|
207
|
+
If True, fall back to eigendecomposition if Cholesky fails. Default is True.
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
sqrt_P : ndarray
|
|
212
|
+
Lower triangular matrix such that sqrt_P @ sqrt_P.T ≈ scale * P.
|
|
213
|
+
|
|
214
|
+
Raises
|
|
215
|
+
------
|
|
216
|
+
np.linalg.LinAlgError
|
|
217
|
+
If Cholesky fails and use_eigh_fallback is False.
|
|
218
|
+
|
|
219
|
+
Examples
|
|
220
|
+
--------
|
|
221
|
+
>>> import numpy as np
|
|
222
|
+
>>> P = np.array([[4.0, 2.0], [2.0, 3.0]])
|
|
223
|
+
>>> sqrt_P = compute_matrix_sqrt(P)
|
|
224
|
+
>>> np.allclose(sqrt_P @ sqrt_P.T, P)
|
|
225
|
+
True
|
|
226
|
+
"""
|
|
227
|
+
P = np.asarray(P, dtype=np.float64)
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
sqrt_P = np.linalg.cholesky(scale * P)
|
|
231
|
+
except np.linalg.LinAlgError:
|
|
232
|
+
if not use_eigh_fallback:
|
|
233
|
+
raise
|
|
234
|
+
# Eigendecomposition fallback for near-singular matrices
|
|
235
|
+
eigvals, eigvecs = np.linalg.eigh(P)
|
|
236
|
+
# Clamp negative eigenvalues to small positive value
|
|
237
|
+
eigvals = np.maximum(eigvals, 1e-10)
|
|
238
|
+
sqrt_P = eigvecs @ np.diag(np.sqrt(scale * eigvals))
|
|
239
|
+
|
|
240
|
+
return sqrt_P
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def compute_innovation_likelihood(
|
|
244
|
+
innovation: NDArray[np.floating],
|
|
245
|
+
S: NDArray[np.floating],
|
|
246
|
+
S_is_cholesky: bool = False,
|
|
247
|
+
) -> float:
|
|
248
|
+
"""
|
|
249
|
+
Compute the likelihood of an innovation (measurement residual).
|
|
250
|
+
|
|
251
|
+
Computes the multivariate Gaussian probability density for the innovation,
|
|
252
|
+
which is used for track scoring and association in multi-target tracking.
|
|
253
|
+
|
|
254
|
+
Parameters
|
|
255
|
+
----------
|
|
256
|
+
innovation : ndarray
|
|
257
|
+
Innovation (measurement residual) vector, shape (m,).
|
|
258
|
+
S : ndarray
|
|
259
|
+
Innovation covariance matrix, shape (m, m), or its lower triangular
|
|
260
|
+
Cholesky factor if S_is_cholesky is True.
|
|
261
|
+
S_is_cholesky : bool, optional
|
|
262
|
+
If True, S is treated as the lower triangular Cholesky factor.
|
|
263
|
+
Default is False.
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
likelihood : float
|
|
268
|
+
Probability density value. Returns 0.0 if covariance is singular.
|
|
269
|
+
|
|
270
|
+
Examples
|
|
271
|
+
--------
|
|
272
|
+
>>> import numpy as np
|
|
273
|
+
>>> y = np.array([0.1, -0.2])
|
|
274
|
+
>>> S = np.array([[0.5, 0.1], [0.1, 0.4]])
|
|
275
|
+
>>> likelihood = compute_innovation_likelihood(y, S)
|
|
276
|
+
>>> likelihood > 0
|
|
277
|
+
True
|
|
278
|
+
"""
|
|
279
|
+
innovation = np.asarray(innovation, dtype=np.float64).flatten()
|
|
280
|
+
S = np.asarray(S, dtype=np.float64)
|
|
281
|
+
m = len(innovation)
|
|
282
|
+
|
|
283
|
+
if S_is_cholesky:
|
|
284
|
+
# S is already the lower triangular Cholesky factor
|
|
285
|
+
S_chol = S
|
|
286
|
+
# det(S @ S.T) = det(S)^2
|
|
287
|
+
det_S = np.prod(np.diag(S_chol)) ** 2
|
|
288
|
+
if det_S <= 0:
|
|
289
|
+
return 0.0
|
|
290
|
+
# Solve S @ x = innovation for x, then compute x.T @ x
|
|
291
|
+
import scipy.linalg
|
|
292
|
+
|
|
293
|
+
y_normalized = scipy.linalg.solve_triangular(S_chol, innovation, lower=True)
|
|
294
|
+
mahal_sq = np.sum(y_normalized**2)
|
|
295
|
+
else:
|
|
296
|
+
# Compute Cholesky factorization
|
|
297
|
+
try:
|
|
298
|
+
S_chol = np.linalg.cholesky(S)
|
|
299
|
+
det_S = np.prod(np.diag(S_chol)) ** 2
|
|
300
|
+
if det_S <= 0:
|
|
301
|
+
return 0.0
|
|
302
|
+
import scipy.linalg
|
|
303
|
+
|
|
304
|
+
y_normalized = scipy.linalg.solve_triangular(S_chol, innovation, lower=True)
|
|
305
|
+
mahal_sq = np.sum(y_normalized**2)
|
|
306
|
+
except np.linalg.LinAlgError:
|
|
307
|
+
# Fallback to direct determinant and solve
|
|
308
|
+
det_S = np.linalg.det(S)
|
|
309
|
+
if det_S <= 0:
|
|
310
|
+
return 0.0
|
|
311
|
+
mahal_sq = innovation @ np.linalg.solve(S, innovation)
|
|
312
|
+
|
|
313
|
+
likelihood = np.exp(-0.5 * mahal_sq) / np.sqrt((2 * np.pi) ** m * det_S)
|
|
314
|
+
return float(likelihood)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def compute_mahalanobis_distance(
|
|
318
|
+
innovation: NDArray[np.floating],
|
|
319
|
+
S: NDArray[np.floating],
|
|
320
|
+
S_is_cholesky: bool = False,
|
|
321
|
+
) -> float:
|
|
322
|
+
"""
|
|
323
|
+
Compute the Mahalanobis distance of an innovation.
|
|
324
|
+
|
|
325
|
+
The Mahalanobis distance is sqrt(y.T @ S^{-1} @ y), which measures
|
|
326
|
+
how many standard deviations the innovation is from zero.
|
|
327
|
+
|
|
328
|
+
Parameters
|
|
329
|
+
----------
|
|
330
|
+
innovation : ndarray
|
|
331
|
+
Innovation (measurement residual) vector, shape (m,).
|
|
332
|
+
S : ndarray
|
|
333
|
+
Innovation covariance matrix, shape (m, m), or its lower triangular
|
|
334
|
+
Cholesky factor if S_is_cholesky is True.
|
|
335
|
+
S_is_cholesky : bool, optional
|
|
336
|
+
If True, S is treated as the lower triangular Cholesky factor.
|
|
337
|
+
Default is False.
|
|
338
|
+
|
|
339
|
+
Returns
|
|
340
|
+
-------
|
|
341
|
+
distance : float
|
|
342
|
+
Mahalanobis distance.
|
|
343
|
+
|
|
344
|
+
Examples
|
|
345
|
+
--------
|
|
346
|
+
>>> import numpy as np
|
|
347
|
+
>>> y = np.array([1.0, 0.0])
|
|
348
|
+
>>> S = np.eye(2)
|
|
349
|
+
>>> dist = compute_mahalanobis_distance(y, S)
|
|
350
|
+
>>> np.isclose(dist, 1.0)
|
|
351
|
+
True
|
|
352
|
+
"""
|
|
353
|
+
innovation = np.asarray(innovation, dtype=np.float64).flatten()
|
|
354
|
+
S = np.asarray(S, dtype=np.float64)
|
|
355
|
+
|
|
356
|
+
if S_is_cholesky:
|
|
357
|
+
import scipy.linalg
|
|
358
|
+
|
|
359
|
+
y_normalized = scipy.linalg.solve_triangular(S, innovation, lower=True)
|
|
360
|
+
mahal_sq = np.sum(y_normalized**2)
|
|
361
|
+
else:
|
|
362
|
+
try:
|
|
363
|
+
S_chol = np.linalg.cholesky(S)
|
|
364
|
+
import scipy.linalg
|
|
365
|
+
|
|
366
|
+
y_normalized = scipy.linalg.solve_triangular(S_chol, innovation, lower=True)
|
|
367
|
+
mahal_sq = np.sum(y_normalized**2)
|
|
368
|
+
except np.linalg.LinAlgError:
|
|
369
|
+
mahal_sq = innovation @ np.linalg.solve(S, innovation)
|
|
370
|
+
|
|
371
|
+
return float(np.sqrt(mahal_sq))
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def compute_merwe_weights(
|
|
375
|
+
n: int, alpha: float = 1e-3, beta: float = 2.0, kappa: float = 0.0
|
|
376
|
+
) -> Tuple[NDArray[np.floating], NDArray[np.floating]]:
|
|
377
|
+
"""
|
|
378
|
+
Compute sigma point weights for the Van der Merwe scaled UKF.
|
|
379
|
+
|
|
380
|
+
Parameters
|
|
381
|
+
----------
|
|
382
|
+
n : int
|
|
383
|
+
State dimension.
|
|
384
|
+
alpha : float, optional
|
|
385
|
+
Spread of sigma points around mean. Default is 1e-3.
|
|
386
|
+
beta : float, optional
|
|
387
|
+
Prior knowledge about distribution. Default is 2.0 (Gaussian).
|
|
388
|
+
kappa : float, optional
|
|
389
|
+
Secondary scaling parameter. Default is 0.0.
|
|
390
|
+
|
|
391
|
+
Returns
|
|
392
|
+
-------
|
|
393
|
+
W_m : ndarray
|
|
394
|
+
Mean weights, shape (2n+1,).
|
|
395
|
+
W_c : ndarray
|
|
396
|
+
Covariance weights, shape (2n+1,).
|
|
397
|
+
|
|
398
|
+
Examples
|
|
399
|
+
--------
|
|
400
|
+
>>> W_m, W_c = compute_merwe_weights(4, alpha=1e-3, beta=2.0, kappa=0.0)
|
|
401
|
+
>>> np.isclose(W_m.sum(), 1.0)
|
|
402
|
+
True
|
|
403
|
+
"""
|
|
404
|
+
lam = alpha**2 * (n + kappa) - n
|
|
405
|
+
|
|
406
|
+
W_m = np.zeros(2 * n + 1)
|
|
407
|
+
W_c = np.zeros(2 * n + 1)
|
|
408
|
+
|
|
409
|
+
W_m[0] = lam / (n + lam)
|
|
410
|
+
W_c[0] = lam / (n + lam) + (1 - alpha**2 + beta)
|
|
411
|
+
|
|
412
|
+
weight = 1 / (2 * (n + lam))
|
|
413
|
+
W_m[1:] = weight
|
|
414
|
+
W_c[1:] = weight
|
|
415
|
+
|
|
416
|
+
return W_m, W_c
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
__all__ = [
|
|
420
|
+
"cholesky_update",
|
|
421
|
+
"qr_update",
|
|
422
|
+
"ensure_symmetric",
|
|
423
|
+
"compute_matrix_sqrt",
|
|
424
|
+
"compute_innovation_likelihood",
|
|
425
|
+
"compute_mahalanobis_distance",
|
|
426
|
+
"compute_merwe_weights",
|
|
427
|
+
]
|
|
@@ -13,205 +13,32 @@ For U-D factorization filters, see :mod:`pytcl.dynamic_estimation.kalman.ud_filt
|
|
|
13
13
|
For square-root UKF, see :mod:`pytcl.dynamic_estimation.kalman.sr_ukf`.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
from typing import
|
|
16
|
+
from typing import Optional
|
|
17
17
|
|
|
18
18
|
import numpy as np
|
|
19
19
|
import scipy.linalg
|
|
20
|
-
from numpy.typing import ArrayLike
|
|
20
|
+
from numpy.typing import ArrayLike
|
|
21
21
|
|
|
22
|
+
# Import matrix utilities from centralized module to avoid circular imports
|
|
23
|
+
from pytcl.dynamic_estimation.kalman.matrix_utils import cholesky_update, qr_update
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
# Import from submodules for backward compatibility (now at top level, no circular import)
|
|
26
|
+
from pytcl.dynamic_estimation.kalman.sr_ukf import sr_ukf_predict, sr_ukf_update
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Attributes
|
|
42
|
-
----------
|
|
43
|
-
x : ndarray
|
|
44
|
-
Predicted state estimate.
|
|
45
|
-
S : ndarray
|
|
46
|
-
Lower triangular Cholesky factor of predicted covariance.
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
x: NDArray[np.floating]
|
|
50
|
-
S: NDArray[np.floating]
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class SRKalmanUpdate(NamedTuple):
|
|
54
|
-
"""Result of square-root Kalman filter update step.
|
|
55
|
-
|
|
56
|
-
Attributes
|
|
57
|
-
----------
|
|
58
|
-
x : ndarray
|
|
59
|
-
Updated state estimate.
|
|
60
|
-
S : ndarray
|
|
61
|
-
Lower triangular Cholesky factor of updated covariance.
|
|
62
|
-
y : ndarray
|
|
63
|
-
Innovation (measurement residual).
|
|
64
|
-
S_y : ndarray
|
|
65
|
-
Lower triangular Cholesky factor of innovation covariance.
|
|
66
|
-
K : ndarray
|
|
67
|
-
Kalman gain.
|
|
68
|
-
likelihood : float
|
|
69
|
-
Measurement likelihood (for association).
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
x: NDArray[np.floating]
|
|
73
|
-
S: NDArray[np.floating]
|
|
74
|
-
y: NDArray[np.floating]
|
|
75
|
-
S_y: NDArray[np.floating]
|
|
76
|
-
K: NDArray[np.floating]
|
|
77
|
-
likelihood: float
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def cholesky_update(
|
|
81
|
-
S: NDArray[np.floating], v: NDArray[np.floating], sign: float = 1.0
|
|
82
|
-
) -> NDArray[np.floating]:
|
|
83
|
-
"""
|
|
84
|
-
Rank-1 Cholesky update/downdate.
|
|
85
|
-
|
|
86
|
-
Computes the Cholesky factor of P ± v @ v.T given S where P = S @ S.T.
|
|
87
|
-
|
|
88
|
-
Parameters
|
|
89
|
-
----------
|
|
90
|
-
S : ndarray
|
|
91
|
-
Lower triangular Cholesky factor, shape (n, n).
|
|
92
|
-
v : ndarray
|
|
93
|
-
Vector for rank-1 update, shape (n,).
|
|
94
|
-
sign : float
|
|
95
|
-
+1 for update (addition), -1 for downdate (subtraction).
|
|
96
|
-
|
|
97
|
-
Returns
|
|
98
|
-
-------
|
|
99
|
-
S_new : ndarray
|
|
100
|
-
Updated lower triangular Cholesky factor.
|
|
101
|
-
|
|
102
|
-
Notes
|
|
103
|
-
-----
|
|
104
|
-
Uses the efficient O(n²) algorithm from [1].
|
|
105
|
-
|
|
106
|
-
References
|
|
107
|
-
----------
|
|
108
|
-
.. [1] P. E. Gill, G. H. Golub, W. Murray, and M. A. Saunders,
|
|
109
|
-
"Methods for modifying matrix factorizations,"
|
|
110
|
-
Mathematics of Computation, vol. 28, pp. 505-535, 1974.
|
|
111
|
-
|
|
112
|
-
Examples
|
|
113
|
-
--------
|
|
114
|
-
>>> import numpy as np
|
|
115
|
-
>>> S = np.linalg.cholesky(np.eye(2))
|
|
116
|
-
>>> v = np.array([0.5, 0.5])
|
|
117
|
-
>>> S_updated = cholesky_update(S, v, sign=1.0)
|
|
118
|
-
>>> P_updated = S_updated @ S_updated.T
|
|
119
|
-
>>> np.allclose(P_updated, np.eye(2) + np.outer(v, v))
|
|
120
|
-
True
|
|
121
|
-
"""
|
|
122
|
-
S = np.asarray(S, dtype=np.float64).copy()
|
|
123
|
-
v = np.asarray(v, dtype=np.float64).flatten().copy()
|
|
124
|
-
n = len(v)
|
|
125
|
-
|
|
126
|
-
if sign > 0:
|
|
127
|
-
# Cholesky update
|
|
128
|
-
for k in range(n):
|
|
129
|
-
r = np.sqrt(S[k, k] ** 2 + v[k] ** 2)
|
|
130
|
-
c = r / S[k, k]
|
|
131
|
-
s = v[k] / S[k, k]
|
|
132
|
-
S[k, k] = r
|
|
133
|
-
if k < n - 1:
|
|
134
|
-
S[k + 1 :, k] = (S[k + 1 :, k] + s * v[k + 1 :]) / c
|
|
135
|
-
v[k + 1 :] = c * v[k + 1 :] - s * S[k + 1 :, k]
|
|
136
|
-
else:
|
|
137
|
-
# Cholesky downdate
|
|
138
|
-
for k in range(n):
|
|
139
|
-
r_sq = S[k, k] ** 2 - v[k] ** 2
|
|
140
|
-
if r_sq < 0:
|
|
141
|
-
raise ValueError("Downdate would make matrix non-positive definite")
|
|
142
|
-
r = np.sqrt(r_sq)
|
|
143
|
-
c = r / S[k, k]
|
|
144
|
-
s = v[k] / S[k, k]
|
|
145
|
-
S[k, k] = r
|
|
146
|
-
if k < n - 1:
|
|
147
|
-
S[k + 1 :, k] = (S[k + 1 :, k] - s * v[k + 1 :]) / c
|
|
148
|
-
v[k + 1 :] = c * v[k + 1 :] - s * S[k + 1 :, k]
|
|
149
|
-
|
|
150
|
-
return S
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def qr_update(
|
|
154
|
-
S_x: NDArray[np.floating],
|
|
155
|
-
S_noise: NDArray[np.floating],
|
|
156
|
-
F: Optional[NDArray[np.floating]] = None,
|
|
157
|
-
) -> NDArray[np.floating]:
|
|
158
|
-
"""
|
|
159
|
-
QR-based covariance square root update.
|
|
160
|
-
|
|
161
|
-
Computes the Cholesky factor of F @ P @ F.T + Q given S_x (where P = S_x @ S_x.T)
|
|
162
|
-
and S_noise (where Q = S_noise @ S_noise.T).
|
|
163
|
-
|
|
164
|
-
Parameters
|
|
165
|
-
----------
|
|
166
|
-
S_x : ndarray
|
|
167
|
-
Lower triangular Cholesky factor of state covariance, shape (n, n).
|
|
168
|
-
S_noise : ndarray
|
|
169
|
-
Lower triangular Cholesky factor of noise covariance, shape (n, n).
|
|
170
|
-
F : ndarray, optional
|
|
171
|
-
State transition matrix, shape (n, n). If None, uses identity.
|
|
172
|
-
|
|
173
|
-
Returns
|
|
174
|
-
-------
|
|
175
|
-
S_new : ndarray
|
|
176
|
-
Lower triangular Cholesky factor of the updated covariance.
|
|
177
|
-
|
|
178
|
-
Notes
|
|
179
|
-
-----
|
|
180
|
-
Uses QR decomposition for numerical stability. The compound matrix
|
|
181
|
-
[F @ S_x, S_noise].T is QR decomposed, and R.T gives the new Cholesky factor.
|
|
182
|
-
|
|
183
|
-
Examples
|
|
184
|
-
--------
|
|
185
|
-
>>> import numpy as np
|
|
186
|
-
>>> S_x = np.linalg.cholesky(np.eye(2) * 0.1)
|
|
187
|
-
>>> S_noise = np.linalg.cholesky(np.eye(2) * 0.01)
|
|
188
|
-
>>> F = np.array([[1, 1], [0, 1]])
|
|
189
|
-
>>> S_new = qr_update(S_x, S_noise, F)
|
|
190
|
-
"""
|
|
191
|
-
S_x = np.asarray(S_x, dtype=np.float64)
|
|
192
|
-
S_noise = np.asarray(S_noise, dtype=np.float64)
|
|
193
|
-
n = S_x.shape[0]
|
|
194
|
-
|
|
195
|
-
if F is not None:
|
|
196
|
-
F = np.asarray(F, dtype=np.float64)
|
|
197
|
-
FS = F @ S_x
|
|
198
|
-
else:
|
|
199
|
-
FS = S_x
|
|
200
|
-
|
|
201
|
-
# Stack the matrices: [F @ S_x; S_noise]
|
|
202
|
-
compound = np.vstack([FS.T, S_noise.T])
|
|
203
|
-
|
|
204
|
-
# QR decomposition
|
|
205
|
-
_, R = np.linalg.qr(compound)
|
|
206
|
-
|
|
207
|
-
# The upper triangular R gives us the new Cholesky factor
|
|
208
|
-
# Take absolute values on diagonal to ensure positive
|
|
209
|
-
S_new = R[:n, :n].T
|
|
210
|
-
for i in range(n):
|
|
211
|
-
if S_new[i, i] < 0:
|
|
212
|
-
S_new[i:, i] = -S_new[i:, i]
|
|
213
|
-
|
|
214
|
-
return S_new
|
|
28
|
+
# Import types from centralized types module
|
|
29
|
+
from pytcl.dynamic_estimation.kalman.types import (
|
|
30
|
+
SRKalmanPrediction,
|
|
31
|
+
SRKalmanState,
|
|
32
|
+
SRKalmanUpdate,
|
|
33
|
+
)
|
|
34
|
+
from pytcl.dynamic_estimation.kalman.ud_filter import (
|
|
35
|
+
UDState,
|
|
36
|
+
ud_factorize,
|
|
37
|
+
ud_predict,
|
|
38
|
+
ud_reconstruct,
|
|
39
|
+
ud_update,
|
|
40
|
+
ud_update_scalar,
|
|
41
|
+
)
|
|
215
42
|
|
|
216
43
|
|
|
217
44
|
def srkf_predict(
|
|
@@ -445,26 +272,6 @@ def srkf_predict_update(
|
|
|
445
272
|
return srkf_update(pred.x, pred.S, z, H, S_R)
|
|
446
273
|
|
|
447
274
|
|
|
448
|
-
# =============================================================================
|
|
449
|
-
# Backward compatibility: Re-export from submodules
|
|
450
|
-
# =============================================================================
|
|
451
|
-
|
|
452
|
-
# Square-root UKF (now in sr_ukf.py)
|
|
453
|
-
from pytcl.dynamic_estimation.kalman.sr_ukf import ( # noqa: E402
|
|
454
|
-
sr_ukf_predict,
|
|
455
|
-
sr_ukf_update,
|
|
456
|
-
)
|
|
457
|
-
|
|
458
|
-
# U-D factorization filter (now in ud_filter.py)
|
|
459
|
-
from pytcl.dynamic_estimation.kalman.ud_filter import ( # noqa: E402
|
|
460
|
-
UDState,
|
|
461
|
-
ud_factorize,
|
|
462
|
-
ud_predict,
|
|
463
|
-
ud_reconstruct,
|
|
464
|
-
ud_update,
|
|
465
|
-
ud_update_scalar,
|
|
466
|
-
)
|
|
467
|
-
|
|
468
275
|
__all__ = [
|
|
469
276
|
# Square-root KF types
|
|
470
277
|
"SRKalmanState",
|
|
@@ -20,11 +20,11 @@ import numpy as np
|
|
|
20
20
|
import scipy.linalg
|
|
21
21
|
from numpy.typing import ArrayLike
|
|
22
22
|
|
|
23
|
-
from
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
# Import utility function from matrix_utils to avoid circular imports
|
|
24
|
+
from pytcl.dynamic_estimation.kalman.matrix_utils import cholesky_update
|
|
25
|
+
|
|
26
|
+
# Import types from centralized types module to avoid circular imports
|
|
27
|
+
from pytcl.dynamic_estimation.kalman.types import SRKalmanPrediction, SRKalmanUpdate
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def sr_ukf_predict(
|