nrl-tracker 1.7.5__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.
Files changed (33) hide show
  1. {nrl_tracker-1.7.5.dist-info → nrl_tracker-1.9.0.dist-info}/METADATA +2 -2
  2. {nrl_tracker-1.7.5.dist-info → nrl_tracker-1.9.0.dist-info}/RECORD +33 -27
  3. pytcl/__init__.py +3 -3
  4. pytcl/assignment_algorithms/dijkstra_min_cost.py +183 -0
  5. pytcl/assignment_algorithms/network_flow.py +94 -1
  6. pytcl/assignment_algorithms/network_simplex.py +165 -0
  7. pytcl/astronomical/ephemerides.py +8 -4
  8. pytcl/astronomical/relativity.py +20 -0
  9. pytcl/containers/__init__.py +19 -8
  10. pytcl/containers/base.py +82 -9
  11. pytcl/containers/covertree.py +14 -21
  12. pytcl/containers/kd_tree.py +18 -45
  13. pytcl/containers/rtree.py +43 -4
  14. pytcl/containers/vptree.py +14 -21
  15. pytcl/core/__init__.py +59 -2
  16. pytcl/core/constants.py +59 -0
  17. pytcl/core/exceptions.py +865 -0
  18. pytcl/core/optional_deps.py +531 -0
  19. pytcl/core/validation.py +4 -6
  20. pytcl/dynamic_estimation/kalman/matrix_utils.py +427 -0
  21. pytcl/dynamic_estimation/kalman/square_root.py +20 -213
  22. pytcl/dynamic_estimation/kalman/sr_ukf.py +5 -5
  23. pytcl/dynamic_estimation/kalman/types.py +98 -0
  24. pytcl/mathematical_functions/signal_processing/detection.py +19 -0
  25. pytcl/mathematical_functions/transforms/wavelets.py +7 -6
  26. pytcl/plotting/coordinates.py +25 -27
  27. pytcl/plotting/ellipses.py +14 -16
  28. pytcl/plotting/metrics.py +7 -5
  29. pytcl/plotting/tracks.py +8 -7
  30. pytcl/terrain/loaders.py +10 -6
  31. {nrl_tracker-1.7.5.dist-info → nrl_tracker-1.9.0.dist-info}/LICENSE +0 -0
  32. {nrl_tracker-1.7.5.dist-info → nrl_tracker-1.9.0.dist-info}/WHEEL +0 -0
  33. {nrl_tracker-1.7.5.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 NamedTuple, Optional
16
+ from typing import Optional
17
17
 
18
18
  import numpy as np
19
19
  import scipy.linalg
20
- from numpy.typing import ArrayLike, NDArray
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
- class SRKalmanState(NamedTuple):
24
- """State of a square-root Kalman filter.
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
- Attributes
27
- ----------
28
- x : ndarray
29
- State estimate.
30
- S : ndarray
31
- Lower triangular Cholesky factor of covariance (P = S @ S.T).
32
- """
33
-
34
- x: NDArray[np.floating]
35
- S: NDArray[np.floating]
36
-
37
-
38
- class SRKalmanPrediction(NamedTuple):
39
- """Result of square-root Kalman filter prediction step.
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 pytcl.dynamic_estimation.kalman.square_root import (
24
- SRKalmanPrediction,
25
- SRKalmanUpdate,
26
- cholesky_update,
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(