nrl-tracker 1.9.2__py3-none-any.whl → 1.10.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.
pytcl/gpu/kalman.py ADDED
@@ -0,0 +1,543 @@
1
+ """
2
+ GPU-accelerated Linear Kalman Filter using CuPy.
3
+
4
+ This module provides GPU-accelerated implementations of the linear Kalman filter
5
+ for batch processing of multiple tracks. The implementations achieve 5-10x
6
+ speedup compared to CPU for batch sizes > 100.
7
+
8
+ Key Features
9
+ ------------
10
+ - Batch processing of multiple tracks in parallel
11
+ - Memory-efficient operations using CuPy's memory pool
12
+ - Compatible API with CPU implementations
13
+ - Automatic fallback to CPU if GPU unavailable
14
+
15
+ Examples
16
+ --------
17
+ Batch predict for 1000 tracks:
18
+
19
+ >>> from pytcl.gpu.kalman import batch_kf_predict
20
+ >>> import numpy as np
21
+ >>> n_tracks = 1000
22
+ >>> state_dim = 4
23
+ >>> x = np.random.randn(n_tracks, state_dim)
24
+ >>> P = np.tile(np.eye(state_dim), (n_tracks, 1, 1))
25
+ >>> F = np.array([[1, 1, 0, 0], [0, 1, 0, 0], [0, 0, 1, 1], [0, 0, 0, 1]])
26
+ >>> Q = np.eye(state_dim) * 0.1
27
+ >>> x_pred, P_pred = batch_kf_predict(x, P, F, Q)
28
+
29
+ See Also
30
+ --------
31
+ pytcl.dynamic_estimation.kalman.linear : CPU Kalman filter
32
+ pytcl.gpu.ekf : GPU Extended Kalman filter
33
+ """
34
+
35
+ from typing import NamedTuple, Optional, Tuple
36
+
37
+ import numpy as np
38
+ from numpy.typing import ArrayLike, NDArray
39
+
40
+ from pytcl.core.optional_deps import import_optional, requires
41
+ from pytcl.gpu.utils import ensure_gpu_array
42
+
43
+
44
+ class BatchKalmanPrediction(NamedTuple):
45
+ """Result of batch Kalman filter prediction.
46
+
47
+ Attributes
48
+ ----------
49
+ x : ndarray
50
+ Predicted state estimates, shape (n_tracks, state_dim).
51
+ P : ndarray
52
+ Predicted covariances, shape (n_tracks, state_dim, state_dim).
53
+ """
54
+
55
+ x: NDArray[np.floating]
56
+ P: NDArray[np.floating]
57
+
58
+
59
+ class BatchKalmanUpdate(NamedTuple):
60
+ """Result of batch Kalman filter update.
61
+
62
+ Attributes
63
+ ----------
64
+ x : ndarray
65
+ Updated state estimates, shape (n_tracks, state_dim).
66
+ P : ndarray
67
+ Updated covariances, shape (n_tracks, state_dim, state_dim).
68
+ y : ndarray
69
+ Innovations, shape (n_tracks, meas_dim).
70
+ S : ndarray
71
+ Innovation covariances, shape (n_tracks, meas_dim, meas_dim).
72
+ K : ndarray
73
+ Kalman gains, shape (n_tracks, state_dim, meas_dim).
74
+ likelihood : ndarray
75
+ Measurement likelihoods, shape (n_tracks,).
76
+ """
77
+
78
+ x: NDArray[np.floating]
79
+ P: NDArray[np.floating]
80
+ y: NDArray[np.floating]
81
+ S: NDArray[np.floating]
82
+ K: NDArray[np.floating]
83
+ likelihood: NDArray[np.floating]
84
+
85
+
86
+ @requires("cupy", extra="gpu", feature="GPU Kalman filter")
87
+ def batch_kf_predict(
88
+ x: ArrayLike,
89
+ P: ArrayLike,
90
+ F: ArrayLike,
91
+ Q: ArrayLike,
92
+ B: Optional[ArrayLike] = None,
93
+ u: Optional[ArrayLike] = None,
94
+ ) -> BatchKalmanPrediction:
95
+ """
96
+ Batch Kalman filter prediction for multiple tracks.
97
+
98
+ Performs the prediction step for N tracks in parallel on GPU:
99
+ x_pred[i] = F @ x[i] + B @ u[i] (if B, u provided)
100
+ P_pred[i] = F @ P[i] @ F' + Q
101
+
102
+ Parameters
103
+ ----------
104
+ x : array_like
105
+ Current state estimates, shape (n_tracks, state_dim).
106
+ P : array_like
107
+ Current covariances, shape (n_tracks, state_dim, state_dim).
108
+ F : array_like
109
+ State transition matrix, shape (state_dim, state_dim).
110
+ Can also be (n_tracks, state_dim, state_dim) for track-specific matrices.
111
+ Q : array_like
112
+ Process noise covariance, shape (state_dim, state_dim).
113
+ Can also be (n_tracks, state_dim, state_dim) for track-specific noise.
114
+ B : array_like, optional
115
+ Control input matrix, shape (state_dim, control_dim).
116
+ u : array_like, optional
117
+ Control inputs, shape (n_tracks, control_dim).
118
+
119
+ Returns
120
+ -------
121
+ result : BatchKalmanPrediction
122
+ Named tuple with predicted states and covariances.
123
+
124
+ Examples
125
+ --------
126
+ >>> import numpy as np
127
+ >>> from pytcl.gpu.kalman import batch_kf_predict
128
+ >>> n_tracks = 100
129
+ >>> x = np.random.randn(n_tracks, 4)
130
+ >>> P = np.tile(np.eye(4) * 0.1, (n_tracks, 1, 1))
131
+ >>> F = np.array([[1, 1, 0, 0], [0, 1, 0, 0],
132
+ ... [0, 0, 1, 1], [0, 0, 0, 1]])
133
+ >>> Q = np.eye(4) * 0.01
134
+ >>> pred = batch_kf_predict(x, P, F, Q)
135
+ >>> pred.x.shape
136
+ (100, 4)
137
+ """
138
+ cp = import_optional("cupy", extra="gpu", feature="GPU Kalman filter")
139
+
140
+ # Move arrays to GPU
141
+ x_gpu = ensure_gpu_array(x, dtype=cp.float64)
142
+ P_gpu = ensure_gpu_array(P, dtype=cp.float64)
143
+ F_gpu = ensure_gpu_array(F, dtype=cp.float64)
144
+ Q_gpu = ensure_gpu_array(Q, dtype=cp.float64)
145
+
146
+ n_tracks = x_gpu.shape[0]
147
+ state_dim = x_gpu.shape[1]
148
+
149
+ # Handle F matrix dimensions
150
+ if F_gpu.ndim == 2:
151
+ # Broadcast F to all tracks: (n, n) -> (n_tracks, n, n)
152
+ F_batch = cp.broadcast_to(F_gpu, (n_tracks, state_dim, state_dim))
153
+ else:
154
+ F_batch = F_gpu
155
+
156
+ # Handle Q matrix dimensions
157
+ if Q_gpu.ndim == 2:
158
+ Q_batch = cp.broadcast_to(Q_gpu, (n_tracks, state_dim, state_dim))
159
+ else:
160
+ Q_batch = Q_gpu
161
+
162
+ # Batch prediction: x_pred = F @ x
163
+ # Use einsum for batched matrix-vector multiplication
164
+ x_pred = cp.einsum("nij,nj->ni", F_batch, x_gpu)
165
+
166
+ # Add control input if provided
167
+ if B is not None and u is not None:
168
+ B_gpu = ensure_gpu_array(B, dtype=cp.float64)
169
+ u_gpu = ensure_gpu_array(u, dtype=cp.float64)
170
+ if B_gpu.ndim == 2:
171
+ # Broadcast B
172
+ x_pred += cp.einsum("ij,nj->ni", B_gpu, u_gpu)
173
+ else:
174
+ x_pred += cp.einsum("nij,nj->ni", B_gpu, u_gpu)
175
+
176
+ # Batch covariance prediction: P_pred = F @ P @ F' + Q
177
+ # Step 1: FP = F @ P
178
+ FP = cp.einsum("nij,njk->nik", F_batch, P_gpu)
179
+ # Step 2: P_pred = FP @ F' + Q
180
+ P_pred = cp.einsum("nij,nkj->nik", FP, F_batch) + Q_batch
181
+
182
+ # Ensure symmetry
183
+ P_pred = (P_pred + cp.swapaxes(P_pred, -2, -1)) / 2
184
+
185
+ return BatchKalmanPrediction(x=x_pred, P=P_pred)
186
+
187
+
188
+ @requires("cupy", extra="gpu", feature="GPU Kalman filter")
189
+ def batch_kf_update(
190
+ x: ArrayLike,
191
+ P: ArrayLike,
192
+ z: ArrayLike,
193
+ H: ArrayLike,
194
+ R: ArrayLike,
195
+ ) -> BatchKalmanUpdate:
196
+ """
197
+ Batch Kalman filter update for multiple tracks.
198
+
199
+ Performs the update step for N tracks in parallel on GPU:
200
+ y[i] = z[i] - H @ x[i] (innovation)
201
+ S[i] = H @ P[i] @ H' + R (innovation covariance)
202
+ K[i] = P[i] @ H' @ S[i]^{-1} (Kalman gain)
203
+ x_upd[i] = x[i] + K[i] @ y[i] (updated state)
204
+ P_upd[i] = (I - K[i] @ H) @ P[i] (updated covariance)
205
+
206
+ Parameters
207
+ ----------
208
+ x : array_like
209
+ Predicted state estimates, shape (n_tracks, state_dim).
210
+ P : array_like
211
+ Predicted covariances, shape (n_tracks, state_dim, state_dim).
212
+ z : array_like
213
+ Measurements, shape (n_tracks, meas_dim).
214
+ H : array_like
215
+ Measurement matrix, shape (meas_dim, state_dim).
216
+ Can also be (n_tracks, meas_dim, state_dim).
217
+ R : array_like
218
+ Measurement noise covariance, shape (meas_dim, meas_dim).
219
+ Can also be (n_tracks, meas_dim, meas_dim).
220
+
221
+ Returns
222
+ -------
223
+ result : BatchKalmanUpdate
224
+ Named tuple with updated states, covariances, and statistics.
225
+
226
+ Examples
227
+ --------
228
+ >>> import numpy as np
229
+ >>> from pytcl.gpu.kalman import batch_kf_update
230
+ >>> n_tracks = 100
231
+ >>> x = np.random.randn(n_tracks, 4)
232
+ >>> P = np.tile(np.eye(4) * 0.1, (n_tracks, 1, 1))
233
+ >>> z = np.random.randn(n_tracks, 2) # position measurements
234
+ >>> H = np.array([[1, 0, 0, 0], [0, 0, 1, 0]])
235
+ >>> R = np.eye(2) * 0.5
236
+ >>> upd = batch_kf_update(x, P, z, H, R)
237
+ >>> upd.x.shape
238
+ (100, 4)
239
+ """
240
+ cp = import_optional("cupy", extra="gpu", feature="GPU Kalman filter")
241
+
242
+ # Move arrays to GPU
243
+ x_gpu = ensure_gpu_array(x, dtype=cp.float64)
244
+ P_gpu = ensure_gpu_array(P, dtype=cp.float64)
245
+ z_gpu = ensure_gpu_array(z, dtype=cp.float64)
246
+ H_gpu = ensure_gpu_array(H, dtype=cp.float64)
247
+ R_gpu = ensure_gpu_array(R, dtype=cp.float64)
248
+
249
+ n_tracks = x_gpu.shape[0]
250
+ state_dim = x_gpu.shape[1]
251
+ meas_dim = z_gpu.shape[1]
252
+
253
+ # Handle H matrix dimensions
254
+ if H_gpu.ndim == 2:
255
+ H_batch = cp.broadcast_to(H_gpu, (n_tracks, meas_dim, state_dim))
256
+ else:
257
+ H_batch = H_gpu
258
+
259
+ # Handle R matrix dimensions
260
+ if R_gpu.ndim == 2:
261
+ R_batch = cp.broadcast_to(R_gpu, (n_tracks, meas_dim, meas_dim))
262
+ else:
263
+ R_batch = R_gpu
264
+
265
+ # Innovation: y = z - H @ x
266
+ z_pred = cp.einsum("nij,nj->ni", H_batch, x_gpu)
267
+ y = z_gpu - z_pred
268
+
269
+ # Innovation covariance: S = H @ P @ H' + R
270
+ HP = cp.einsum("nij,njk->nik", H_batch, P_gpu)
271
+ S = cp.einsum("nij,nkj->nik", HP, H_batch) + R_batch
272
+
273
+ # Kalman gain: K = P @ H' @ S^{-1}
274
+ # First compute P @ H'
275
+ PHT = cp.einsum("nij,nkj->nik", P_gpu, H_batch)
276
+
277
+ # Batch matrix inverse using batched solve
278
+ # K = PHT @ S^{-1} is equivalent to solving S @ K' = PHT' for K
279
+ # But for efficiency, we solve S @ X = I for S^{-1}, then compute K = PHT @ S^{-1}
280
+ S_inv = cp.linalg.inv(S)
281
+ K = cp.einsum("nij,njk->nik", PHT, S_inv)
282
+
283
+ # Updated state: x_upd = x + K @ y
284
+ x_upd = x_gpu + cp.einsum("nij,nj->ni", K, y)
285
+
286
+ # Updated covariance using Joseph form: P_upd = (I - K @ H) @ P @ (I - K @ H)' + K @ R @ K'
287
+ eye = cp.eye(state_dim, dtype=cp.float64)
288
+ I_KH = eye - cp.einsum("nij,njk->nik", K, H_batch)
289
+
290
+ # Joseph form for numerical stability
291
+ P_upd = cp.einsum("nij,njk->nik", I_KH, P_gpu)
292
+ P_upd = cp.einsum("nij,nkj->nik", P_upd, I_KH)
293
+ KRK = cp.einsum("nij,njk,nlk->nil", K, R_batch, K)
294
+ P_upd = P_upd + KRK
295
+
296
+ # Ensure symmetry
297
+ P_upd = (P_upd + cp.swapaxes(P_upd, -2, -1)) / 2
298
+
299
+ # Compute likelihoods
300
+ # log(L) = -0.5 * (y' @ S^{-1} @ y + log(det(S)) + m*log(2*pi))
301
+ mahal_sq = cp.einsum("ni,nij,nj->n", y, S_inv, y)
302
+ sign, logdet = cp.linalg.slogdet(S)
303
+ log_likelihood = -0.5 * (mahal_sq + logdet + meas_dim * np.log(2 * np.pi))
304
+ likelihood = cp.exp(log_likelihood)
305
+
306
+ return BatchKalmanUpdate(
307
+ x=x_upd,
308
+ P=P_upd,
309
+ y=y,
310
+ S=S,
311
+ K=K,
312
+ likelihood=likelihood,
313
+ )
314
+
315
+
316
+ @requires("cupy", extra="gpu", feature="GPU Kalman filter")
317
+ def batch_kf_predict_update(
318
+ x: ArrayLike,
319
+ P: ArrayLike,
320
+ z: ArrayLike,
321
+ F: ArrayLike,
322
+ Q: ArrayLike,
323
+ H: ArrayLike,
324
+ R: ArrayLike,
325
+ B: Optional[ArrayLike] = None,
326
+ u: Optional[ArrayLike] = None,
327
+ ) -> BatchKalmanUpdate:
328
+ """
329
+ Combined batch Kalman filter prediction and update.
330
+
331
+ Parameters
332
+ ----------
333
+ x : array_like
334
+ Current state estimates, shape (n_tracks, state_dim).
335
+ P : array_like
336
+ Current covariances, shape (n_tracks, state_dim, state_dim).
337
+ z : array_like
338
+ Measurements, shape (n_tracks, meas_dim).
339
+ F : array_like
340
+ State transition matrix.
341
+ Q : array_like
342
+ Process noise covariance.
343
+ H : array_like
344
+ Measurement matrix.
345
+ R : array_like
346
+ Measurement noise covariance.
347
+ B : array_like, optional
348
+ Control input matrix.
349
+ u : array_like, optional
350
+ Control inputs.
351
+
352
+ Returns
353
+ -------
354
+ result : BatchKalmanUpdate
355
+ Named tuple with updated states, covariances, and statistics.
356
+ """
357
+ pred = batch_kf_predict(x, P, F, Q, B, u)
358
+ return batch_kf_update(pred.x, pred.P, z, H, R)
359
+
360
+
361
+ class CuPyKalmanFilter:
362
+ """
363
+ GPU-accelerated Linear Kalman Filter for batch processing.
364
+
365
+ This class provides a stateful interface for processing multiple tracks
366
+ in parallel on the GPU. It maintains the filter matrices and provides
367
+ methods for prediction and update.
368
+
369
+ Parameters
370
+ ----------
371
+ state_dim : int
372
+ Dimension of the state vector.
373
+ meas_dim : int
374
+ Dimension of the measurement vector.
375
+ F : array_like, optional
376
+ State transition matrix. If None, uses identity.
377
+ H : array_like, optional
378
+ Measurement matrix. If None, measures first meas_dim states.
379
+ Q : array_like, optional
380
+ Process noise covariance. If None, uses 0.01 * I.
381
+ R : array_like, optional
382
+ Measurement noise covariance. If None, uses 1.0 * I.
383
+
384
+ Examples
385
+ --------
386
+ >>> import numpy as np
387
+ >>> from pytcl.gpu.kalman import CuPyKalmanFilter
388
+ >>>
389
+ >>> # Create filter for 2D constant velocity model
390
+ >>> kf = CuPyKalmanFilter(
391
+ ... state_dim=4, # [x, vx, y, vy]
392
+ ... meas_dim=2, # [x, y]
393
+ ... F=np.array([[1, 1, 0, 0], [0, 1, 0, 0],
394
+ ... [0, 0, 1, 1], [0, 0, 0, 1]]),
395
+ ... H=np.array([[1, 0, 0, 0], [0, 0, 1, 0]]),
396
+ ... Q=np.eye(4) * 0.1,
397
+ ... R=np.eye(2) * 1.0,
398
+ ... )
399
+ >>>
400
+ >>> # Process batch of tracks
401
+ >>> n_tracks = 1000
402
+ >>> x = np.random.randn(n_tracks, 4)
403
+ >>> P = np.tile(np.eye(4), (n_tracks, 1, 1))
404
+ >>> z = np.random.randn(n_tracks, 2)
405
+ >>>
406
+ >>> # Predict and update
407
+ >>> x_pred, P_pred = kf.predict(x, P)
408
+ >>> result = kf.update(x_pred, P_pred, z)
409
+ """
410
+
411
+ @requires("cupy", extra="gpu", feature="GPU Kalman filter")
412
+ def __init__(
413
+ self,
414
+ state_dim: int,
415
+ meas_dim: int,
416
+ F: Optional[ArrayLike] = None,
417
+ H: Optional[ArrayLike] = None,
418
+ Q: Optional[ArrayLike] = None,
419
+ R: Optional[ArrayLike] = None,
420
+ ):
421
+ cp = import_optional("cupy", extra="gpu", feature="GPU Kalman filter")
422
+
423
+ self.state_dim = state_dim
424
+ self.meas_dim = meas_dim
425
+
426
+ # Initialize matrices on GPU
427
+ if F is None:
428
+ self.F = cp.eye(state_dim, dtype=cp.float64)
429
+ else:
430
+ self.F = ensure_gpu_array(F, dtype=cp.float64)
431
+
432
+ if H is None:
433
+ self.H = cp.zeros((meas_dim, state_dim), dtype=cp.float64)
434
+ self.H[:meas_dim, :meas_dim] = cp.eye(meas_dim, dtype=cp.float64)
435
+ else:
436
+ self.H = ensure_gpu_array(H, dtype=cp.float64)
437
+
438
+ if Q is None:
439
+ self.Q = cp.eye(state_dim, dtype=cp.float64) * 0.01
440
+ else:
441
+ self.Q = ensure_gpu_array(Q, dtype=cp.float64)
442
+
443
+ if R is None:
444
+ self.R = cp.eye(meas_dim, dtype=cp.float64)
445
+ else:
446
+ self.R = ensure_gpu_array(R, dtype=cp.float64)
447
+
448
+ def predict(
449
+ self,
450
+ x: ArrayLike,
451
+ P: ArrayLike,
452
+ B: Optional[ArrayLike] = None,
453
+ u: Optional[ArrayLike] = None,
454
+ ) -> Tuple[NDArray[np.floating], NDArray[np.floating]]:
455
+ """
456
+ Perform batch prediction.
457
+
458
+ Parameters
459
+ ----------
460
+ x : array_like
461
+ State estimates, shape (n_tracks, state_dim).
462
+ P : array_like
463
+ Covariances, shape (n_tracks, state_dim, state_dim).
464
+ B : array_like, optional
465
+ Control input matrix.
466
+ u : array_like, optional
467
+ Control inputs.
468
+
469
+ Returns
470
+ -------
471
+ x_pred : ndarray
472
+ Predicted states.
473
+ P_pred : ndarray
474
+ Predicted covariances.
475
+ """
476
+ result = batch_kf_predict(x, P, self.F, self.Q, B, u)
477
+ return result.x, result.P
478
+
479
+ def update(
480
+ self,
481
+ x: ArrayLike,
482
+ P: ArrayLike,
483
+ z: ArrayLike,
484
+ ) -> BatchKalmanUpdate:
485
+ """
486
+ Perform batch update.
487
+
488
+ Parameters
489
+ ----------
490
+ x : array_like
491
+ Predicted state estimates.
492
+ P : array_like
493
+ Predicted covariances.
494
+ z : array_like
495
+ Measurements.
496
+
497
+ Returns
498
+ -------
499
+ result : BatchKalmanUpdate
500
+ Update results including states, covariances, and statistics.
501
+ """
502
+ return batch_kf_update(x, P, z, self.H, self.R)
503
+
504
+ def predict_update(
505
+ self,
506
+ x: ArrayLike,
507
+ P: ArrayLike,
508
+ z: ArrayLike,
509
+ B: Optional[ArrayLike] = None,
510
+ u: Optional[ArrayLike] = None,
511
+ ) -> BatchKalmanUpdate:
512
+ """
513
+ Combined batch prediction and update.
514
+
515
+ Parameters
516
+ ----------
517
+ x : array_like
518
+ Current state estimates.
519
+ P : array_like
520
+ Current covariances.
521
+ z : array_like
522
+ Measurements.
523
+ B : array_like, optional
524
+ Control input matrix.
525
+ u : array_like, optional
526
+ Control inputs.
527
+
528
+ Returns
529
+ -------
530
+ result : BatchKalmanUpdate
531
+ Update results.
532
+ """
533
+ return batch_kf_predict_update(x, P, z, self.F, self.Q, self.H, self.R, B, u)
534
+
535
+
536
+ __all__ = [
537
+ "BatchKalmanPrediction",
538
+ "BatchKalmanUpdate",
539
+ "batch_kf_predict",
540
+ "batch_kf_update",
541
+ "batch_kf_predict_update",
542
+ "CuPyKalmanFilter",
543
+ ]