nrl-tracker 1.9.1__py3-none-any.whl → 1.9.2__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.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/METADATA +4 -4
- {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/RECORD +60 -59
- pytcl/__init__.py +2 -2
- pytcl/assignment_algorithms/gating.py +18 -0
- pytcl/assignment_algorithms/jpda.py +56 -0
- pytcl/assignment_algorithms/nd_assignment.py +65 -0
- pytcl/assignment_algorithms/network_flow.py +40 -0
- pytcl/astronomical/ephemerides.py +18 -0
- pytcl/astronomical/orbital_mechanics.py +131 -0
- pytcl/atmosphere/ionosphere.py +44 -0
- pytcl/atmosphere/models.py +29 -0
- pytcl/clustering/dbscan.py +9 -0
- pytcl/clustering/gaussian_mixture.py +20 -0
- pytcl/clustering/hierarchical.py +29 -0
- pytcl/clustering/kmeans.py +9 -0
- pytcl/coordinate_systems/conversions/geodetic.py +46 -0
- pytcl/coordinate_systems/conversions/spherical.py +35 -0
- pytcl/coordinate_systems/rotations/rotations.py +147 -0
- pytcl/core/__init__.py +16 -0
- pytcl/core/maturity.py +346 -0
- pytcl/dynamic_estimation/gaussian_sum_filter.py +55 -0
- pytcl/dynamic_estimation/imm.py +29 -0
- pytcl/dynamic_estimation/information_filter.py +64 -0
- pytcl/dynamic_estimation/kalman/extended.py +56 -0
- pytcl/dynamic_estimation/kalman/linear.py +69 -0
- pytcl/dynamic_estimation/kalman/unscented.py +81 -0
- pytcl/dynamic_estimation/particle_filters/bootstrap.py +146 -0
- pytcl/dynamic_estimation/rbpf.py +51 -0
- pytcl/dynamic_estimation/smoothers.py +58 -0
- pytcl/dynamic_models/continuous_time/dynamics.py +104 -0
- pytcl/dynamic_models/discrete_time/coordinated_turn.py +6 -0
- pytcl/dynamic_models/discrete_time/singer.py +12 -0
- pytcl/dynamic_models/process_noise/coordinated_turn.py +46 -0
- pytcl/dynamic_models/process_noise/polynomial.py +6 -0
- pytcl/dynamic_models/process_noise/singer.py +52 -0
- pytcl/gravity/clenshaw.py +60 -0
- pytcl/gravity/egm.py +47 -0
- pytcl/gravity/models.py +34 -0
- pytcl/gravity/spherical_harmonics.py +73 -0
- pytcl/gravity/tides.py +34 -0
- pytcl/mathematical_functions/numerical_integration/quadrature.py +85 -0
- pytcl/mathematical_functions/special_functions/bessel.py +55 -0
- pytcl/mathematical_functions/special_functions/elliptic.py +42 -0
- pytcl/mathematical_functions/special_functions/error_functions.py +49 -0
- pytcl/mathematical_functions/special_functions/gamma_functions.py +43 -0
- pytcl/mathematical_functions/special_functions/lambert_w.py +5 -0
- pytcl/mathematical_functions/special_functions/marcum_q.py +16 -0
- pytcl/navigation/geodesy.py +101 -2
- pytcl/navigation/great_circle.py +71 -0
- pytcl/navigation/rhumb.py +74 -0
- pytcl/performance_evaluation/estimation_metrics.py +70 -0
- pytcl/performance_evaluation/track_metrics.py +30 -0
- pytcl/static_estimation/maximum_likelihood.py +54 -0
- pytcl/static_estimation/robust.py +57 -0
- pytcl/terrain/dem.py +69 -0
- pytcl/terrain/visibility.py +65 -0
- pytcl/trackers/hypothesis.py +65 -0
- {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/LICENSE +0 -0
- {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/WHEEL +0 -0
- {nrl_tracker-1.9.1.dist-info → nrl_tracker-1.9.2.dist-info}/top_level.txt +0 -0
pytcl/dynamic_estimation/imm.py
CHANGED
|
@@ -468,6 +468,35 @@ def imm_predict_update(
|
|
|
468
468
|
-------
|
|
469
469
|
result : IMMUpdate
|
|
470
470
|
Updated states, covariances, and mode probabilities.
|
|
471
|
+
|
|
472
|
+
Examples
|
|
473
|
+
--------
|
|
474
|
+
Track a target with 2 motion modes (CV and CA):
|
|
475
|
+
|
|
476
|
+
>>> import numpy as np
|
|
477
|
+
>>> # Two modes: constant velocity and constant acceleration
|
|
478
|
+
>>> states = [np.array([0, 1, 0, 1]), np.array([0, 1, 0, 1])]
|
|
479
|
+
>>> covs = [np.eye(4) * 0.1, np.eye(4) * 0.1]
|
|
480
|
+
>>> probs = np.array([0.9, 0.1]) # likely CV mode
|
|
481
|
+
>>> # Mode transition matrix (90% stay, 10% switch)
|
|
482
|
+
>>> trans = np.array([[0.9, 0.1], [0.1, 0.9]])
|
|
483
|
+
>>> # Dynamics and measurement matrices for each mode
|
|
484
|
+
>>> F_cv = np.array([[1, 1, 0, 0], [0, 1, 0, 0], [0, 0, 1, 1], [0, 0, 0, 1]])
|
|
485
|
+
>>> F_ca = F_cv.copy() # simplified
|
|
486
|
+
>>> H = np.array([[1, 0, 0, 0], [0, 0, 1, 0]])
|
|
487
|
+
>>> Q = np.eye(4) * 0.01
|
|
488
|
+
>>> R = np.eye(2) * 0.1
|
|
489
|
+
>>> z = np.array([1.0, 1.0])
|
|
490
|
+
>>> result = imm_predict_update(states, covs, probs, trans, z,
|
|
491
|
+
... [F_cv, F_ca], [Q, Q], [H, H], [R, R])
|
|
492
|
+
>>> len(result.mode_probs)
|
|
493
|
+
2
|
|
494
|
+
|
|
495
|
+
See Also
|
|
496
|
+
--------
|
|
497
|
+
imm_predict : IMM prediction step only.
|
|
498
|
+
imm_update : IMM update step only.
|
|
499
|
+
IMMEstimator : Object-oriented interface.
|
|
471
500
|
"""
|
|
472
501
|
pred = imm_predict(
|
|
473
502
|
mode_states, mode_covs, mode_probs, transition_matrix, F_list, Q_list
|
|
@@ -119,6 +119,20 @@ def information_to_state(
|
|
|
119
119
|
State estimate.
|
|
120
120
|
P : ndarray
|
|
121
121
|
State covariance.
|
|
122
|
+
|
|
123
|
+
Examples
|
|
124
|
+
--------
|
|
125
|
+
>>> import numpy as np
|
|
126
|
+
>>> # Information form: Y = inv(P), y = Y @ x
|
|
127
|
+
>>> x_true = np.array([1.0, 2.0])
|
|
128
|
+
>>> P_true = np.eye(2) * 0.5
|
|
129
|
+
>>> Y = np.linalg.inv(P_true) # Information matrix
|
|
130
|
+
>>> y = Y @ x_true # Information vector
|
|
131
|
+
>>> x_recovered, P_recovered = information_to_state(y, Y)
|
|
132
|
+
>>> np.allclose(x_recovered, x_true)
|
|
133
|
+
True
|
|
134
|
+
>>> np.allclose(P_recovered, P_true)
|
|
135
|
+
True
|
|
122
136
|
"""
|
|
123
137
|
y = np.asarray(y, dtype=np.float64).flatten()
|
|
124
138
|
Y = np.asarray(Y, dtype=np.float64)
|
|
@@ -149,6 +163,18 @@ def state_to_information(
|
|
|
149
163
|
Information vector.
|
|
150
164
|
Y : ndarray
|
|
151
165
|
Information matrix.
|
|
166
|
+
|
|
167
|
+
Examples
|
|
168
|
+
--------
|
|
169
|
+
>>> import numpy as np
|
|
170
|
+
>>> x = np.array([1.0, 2.0])
|
|
171
|
+
>>> P = np.eye(2) * 0.5 # Covariance
|
|
172
|
+
>>> y, Y = state_to_information(x, P)
|
|
173
|
+
>>> Y # Should be inv(P) = 2*I
|
|
174
|
+
array([[2., 0.],
|
|
175
|
+
[0., 2.]])
|
|
176
|
+
>>> y # Should be Y @ x = [2, 4]
|
|
177
|
+
array([2., 4.])
|
|
152
178
|
"""
|
|
153
179
|
x = np.asarray(x, dtype=np.float64).flatten()
|
|
154
180
|
P = np.asarray(P, dtype=np.float64)
|
|
@@ -323,6 +349,24 @@ def srif_predict(
|
|
|
323
349
|
R_pred : ndarray
|
|
324
350
|
Predicted square root information matrix.
|
|
325
351
|
|
|
352
|
+
Examples
|
|
353
|
+
--------
|
|
354
|
+
>>> import numpy as np
|
|
355
|
+
>>> n = 2
|
|
356
|
+
>>> # Initialize with known state
|
|
357
|
+
>>> P0 = np.eye(n)
|
|
358
|
+
>>> R0 = np.linalg.inv(np.linalg.cholesky(P0)).T
|
|
359
|
+
>>> x0 = np.array([1.0, 0.5])
|
|
360
|
+
>>> r0 = R0 @ x0
|
|
361
|
+
>>> # Prediction step
|
|
362
|
+
>>> F = np.array([[1, 1], [0, 1]])
|
|
363
|
+
>>> Q = np.eye(2) * 0.1
|
|
364
|
+
>>> r_pred, R_pred = srif_predict(r0, R0, F, Q)
|
|
365
|
+
>>> r_pred.shape
|
|
366
|
+
(2,)
|
|
367
|
+
>>> R_pred.shape
|
|
368
|
+
(2, 2)
|
|
369
|
+
|
|
326
370
|
Notes
|
|
327
371
|
-----
|
|
328
372
|
The SRIF prediction uses:
|
|
@@ -400,6 +444,26 @@ def srif_update(
|
|
|
400
444
|
R_upd : ndarray
|
|
401
445
|
Updated square root information matrix.
|
|
402
446
|
|
|
447
|
+
Examples
|
|
448
|
+
--------
|
|
449
|
+
>>> import numpy as np
|
|
450
|
+
>>> n = 2
|
|
451
|
+
>>> # Prior in SRIF form
|
|
452
|
+
>>> P_prior = np.eye(n) * 2.0
|
|
453
|
+
>>> R_prior = np.linalg.inv(np.linalg.cholesky(P_prior)).T
|
|
454
|
+
>>> x_prior = np.array([1.0, 0.5])
|
|
455
|
+
>>> r_prior = R_prior @ x_prior
|
|
456
|
+
>>> # Measurement update
|
|
457
|
+
>>> z = np.array([1.2]) # Position measurement
|
|
458
|
+
>>> H = np.array([[1, 0]])
|
|
459
|
+
>>> R_meas = np.array([[0.5]]) # Measurement noise covariance
|
|
460
|
+
>>> r_upd, R_upd = srif_update(r_prior, R_prior, z, H, R_meas)
|
|
461
|
+
>>> r_upd.shape
|
|
462
|
+
(2,)
|
|
463
|
+
>>> # Updated information matrix has more information (higher values)
|
|
464
|
+
>>> np.linalg.det(R_upd.T @ R_upd) > np.linalg.det(R_prior.T @ R_prior)
|
|
465
|
+
True
|
|
466
|
+
|
|
403
467
|
Notes
|
|
404
468
|
-----
|
|
405
469
|
The update uses QR decomposition to combine the prior information
|
|
@@ -263,6 +263,23 @@ def ekf_predict_auto(
|
|
|
263
263
|
-------
|
|
264
264
|
result : KalmanPrediction
|
|
265
265
|
Predicted state and covariance.
|
|
266
|
+
|
|
267
|
+
Examples
|
|
268
|
+
--------
|
|
269
|
+
>>> import numpy as np
|
|
270
|
+
>>> # Simple nonlinear dynamics
|
|
271
|
+
>>> def f(x):
|
|
272
|
+
... return np.array([x[0] + x[1], 0.9 * x[1]])
|
|
273
|
+
>>> x = np.array([0.0, 1.0])
|
|
274
|
+
>>> P = np.eye(2) * 0.1
|
|
275
|
+
>>> Q = np.eye(2) * 0.01
|
|
276
|
+
>>> pred = ekf_predict_auto(x, P, f, Q)
|
|
277
|
+
>>> pred.x
|
|
278
|
+
array([1., 0.9])
|
|
279
|
+
|
|
280
|
+
See Also
|
|
281
|
+
--------
|
|
282
|
+
ekf_predict : EKF prediction with explicit Jacobian.
|
|
266
283
|
"""
|
|
267
284
|
x = np.asarray(x, dtype=np.float64).flatten()
|
|
268
285
|
F = numerical_jacobian(f, x, dx)
|
|
@@ -299,6 +316,24 @@ def ekf_update_auto(
|
|
|
299
316
|
-------
|
|
300
317
|
result : KalmanUpdate
|
|
301
318
|
Updated state and covariance.
|
|
319
|
+
|
|
320
|
+
Examples
|
|
321
|
+
--------
|
|
322
|
+
>>> import numpy as np
|
|
323
|
+
>>> # Range measurement h(x) = sqrt(x[0]^2 + x[1]^2)
|
|
324
|
+
>>> def h(x):
|
|
325
|
+
... return np.array([np.sqrt(x[0]**2 + x[1]**2)])
|
|
326
|
+
>>> x = np.array([3.0, 4.0]) # predicted position
|
|
327
|
+
>>> P = np.eye(2) * 0.5
|
|
328
|
+
>>> z = np.array([5.1]) # measured range
|
|
329
|
+
>>> R = np.array([[0.1]])
|
|
330
|
+
>>> upd = ekf_update_auto(x, P, z, h, R)
|
|
331
|
+
>>> np.linalg.norm(upd.x) # estimate moves toward measurement
|
|
332
|
+
5.0...
|
|
333
|
+
|
|
334
|
+
See Also
|
|
335
|
+
--------
|
|
336
|
+
ekf_update : EKF update with explicit Jacobian.
|
|
302
337
|
"""
|
|
303
338
|
x = np.asarray(x, dtype=np.float64).flatten()
|
|
304
339
|
H = numerical_jacobian(h, x, dx)
|
|
@@ -344,6 +379,27 @@ def iterated_ekf_update(
|
|
|
344
379
|
-------
|
|
345
380
|
result : KalmanUpdate
|
|
346
381
|
Updated state and covariance after convergence.
|
|
382
|
+
|
|
383
|
+
Examples
|
|
384
|
+
--------
|
|
385
|
+
>>> import numpy as np
|
|
386
|
+
>>> # Bearing-only measurement (highly nonlinear)
|
|
387
|
+
>>> def h(x):
|
|
388
|
+
... return np.array([np.arctan2(x[1], x[0])])
|
|
389
|
+
>>> def H_func(x):
|
|
390
|
+
... r2 = x[0]**2 + x[1]**2
|
|
391
|
+
... return np.array([[-x[1]/r2, x[0]/r2]])
|
|
392
|
+
>>> x = np.array([10.0, 10.0])
|
|
393
|
+
>>> P = np.eye(2) * 5.0
|
|
394
|
+
>>> z = np.array([np.radians(50)]) # measured bearing
|
|
395
|
+
>>> R = np.array([[0.01]])
|
|
396
|
+
>>> upd = iterated_ekf_update(x, P, z, h, H_func, R)
|
|
397
|
+
>>> upd.x.shape
|
|
398
|
+
(2,)
|
|
399
|
+
|
|
400
|
+
See Also
|
|
401
|
+
--------
|
|
402
|
+
ekf_update : Standard (non-iterated) EKF update.
|
|
347
403
|
"""
|
|
348
404
|
x = np.asarray(x, dtype=np.float64).flatten()
|
|
349
405
|
P = np.asarray(P, dtype=np.float64)
|
|
@@ -282,6 +282,22 @@ def kf_predict_update(
|
|
|
282
282
|
result : KalmanUpdate
|
|
283
283
|
Updated state and covariance with innovation statistics.
|
|
284
284
|
|
|
285
|
+
Examples
|
|
286
|
+
--------
|
|
287
|
+
Track a 1D constant-velocity target with position measurements:
|
|
288
|
+
|
|
289
|
+
>>> import numpy as np
|
|
290
|
+
>>> x = np.array([0.0, 1.0]) # [position, velocity]
|
|
291
|
+
>>> P = np.eye(2) * 0.1
|
|
292
|
+
>>> F = np.array([[1, 1], [0, 1]]) # CV model, T=1s
|
|
293
|
+
>>> Q = np.array([[0.25, 0.5], [0.5, 1.0]]) # process noise
|
|
294
|
+
>>> H = np.array([[1, 0]]) # measure position only
|
|
295
|
+
>>> R = np.array([[0.5]]) # measurement noise variance
|
|
296
|
+
>>> z = np.array([1.1]) # measured position
|
|
297
|
+
>>> result = kf_predict_update(x, P, z, F, Q, H, R)
|
|
298
|
+
>>> result.x # updated state
|
|
299
|
+
array([1.0..., 1.0...])
|
|
300
|
+
|
|
285
301
|
See Also
|
|
286
302
|
--------
|
|
287
303
|
kf_predict : Prediction step only.
|
|
@@ -336,6 +352,30 @@ def kf_smooth(
|
|
|
336
352
|
G = P_filt @ F' @ P_pred^{-1}
|
|
337
353
|
x_smooth = x_filt + G @ (x_smooth_next - x_pred)
|
|
338
354
|
P_smooth = P_filt + G @ (P_smooth_next - P_pred) @ G'
|
|
355
|
+
|
|
356
|
+
Examples
|
|
357
|
+
--------
|
|
358
|
+
Apply RTS smoothing to improve a filtered estimate:
|
|
359
|
+
|
|
360
|
+
>>> import numpy as np
|
|
361
|
+
>>> # Filtered estimate at time k
|
|
362
|
+
>>> x_filt = np.array([1.0, 0.9])
|
|
363
|
+
>>> P_filt = np.array([[0.1, 0.05], [0.05, 0.2]])
|
|
364
|
+
>>> # Predicted estimate at time k+1 (from forward pass)
|
|
365
|
+
>>> x_pred = np.array([1.9, 0.9])
|
|
366
|
+
>>> P_pred = np.array([[0.35, 0.25], [0.25, 0.4]])
|
|
367
|
+
>>> # Smoothed estimate at time k+1 (already computed)
|
|
368
|
+
>>> x_smooth_next = np.array([2.0, 1.0])
|
|
369
|
+
>>> P_smooth_next = np.array([[0.08, 0.03], [0.03, 0.15]])
|
|
370
|
+
>>> F = np.array([[1, 1], [0, 1]]) # CV model
|
|
371
|
+
>>> x_s, P_s = kf_smooth(x_filt, P_filt, x_pred, P_pred,
|
|
372
|
+
... x_smooth_next, P_smooth_next, F)
|
|
373
|
+
>>> x_s # smoothed state at time k
|
|
374
|
+
array([1.0..., 0.9...])
|
|
375
|
+
|
|
376
|
+
See Also
|
|
377
|
+
--------
|
|
378
|
+
kf_predict_update : Forward filtering step.
|
|
339
379
|
"""
|
|
340
380
|
x_filt = np.asarray(x_filt, dtype=np.float64).flatten()
|
|
341
381
|
P_filt = np.asarray(P_filt, dtype=np.float64)
|
|
@@ -390,6 +430,21 @@ def information_filter_predict(
|
|
|
390
430
|
Y_pred : ndarray
|
|
391
431
|
Predicted information matrix.
|
|
392
432
|
|
|
433
|
+
Examples
|
|
434
|
+
--------
|
|
435
|
+
>>> import numpy as np
|
|
436
|
+
>>> # Convert from state form to information form
|
|
437
|
+
>>> x = np.array([1.0, 0.5])
|
|
438
|
+
>>> P = np.eye(2) * 0.1
|
|
439
|
+
>>> Y = np.linalg.inv(P)
|
|
440
|
+
>>> y = Y @ x
|
|
441
|
+
>>> # Predict with constant velocity model
|
|
442
|
+
>>> F = np.array([[1, 1], [0, 1]])
|
|
443
|
+
>>> Q = np.eye(2) * 0.01
|
|
444
|
+
>>> y_pred, Y_pred = information_filter_predict(y, Y, F, Q)
|
|
445
|
+
>>> y_pred.shape
|
|
446
|
+
(2,)
|
|
447
|
+
|
|
393
448
|
Notes
|
|
394
449
|
-----
|
|
395
450
|
The prediction requires converting to state space, predicting,
|
|
@@ -445,6 +500,20 @@ def information_filter_update(
|
|
|
445
500
|
Y_upd : ndarray
|
|
446
501
|
Updated information matrix.
|
|
447
502
|
|
|
503
|
+
Examples
|
|
504
|
+
--------
|
|
505
|
+
>>> import numpy as np
|
|
506
|
+
>>> # Initial information form (from prior)
|
|
507
|
+
>>> Y = np.eye(2) * 10.0 # High prior information
|
|
508
|
+
>>> y = np.array([10.0, 5.0]) # Corresponds to state [1.0, 0.5]
|
|
509
|
+
>>> # Measurement z = H @ x + noise
|
|
510
|
+
>>> z = np.array([1.1])
|
|
511
|
+
>>> H = np.array([[1, 0]]) # Measure position only
|
|
512
|
+
>>> R = np.array([[0.1]])
|
|
513
|
+
>>> y_upd, Y_upd = information_filter_update(y, Y, z, H, R)
|
|
514
|
+
>>> Y_upd[0, 0] > Y[0, 0] # Information increased
|
|
515
|
+
True
|
|
516
|
+
|
|
448
517
|
Notes
|
|
449
518
|
-----
|
|
450
519
|
The update in information form is additive:
|
|
@@ -138,6 +138,17 @@ def sigma_points_julier(
|
|
|
138
138
|
result : SigmaPoints
|
|
139
139
|
Sigma points and weights.
|
|
140
140
|
|
|
141
|
+
Examples
|
|
142
|
+
--------
|
|
143
|
+
>>> import numpy as np
|
|
144
|
+
>>> x = np.array([1.0, 2.0])
|
|
145
|
+
>>> P = np.eye(2) * 0.1
|
|
146
|
+
>>> sp = sigma_points_julier(x, P, kappa=1.0)
|
|
147
|
+
>>> sp.points.shape # 2*n+1 = 5 points for n=2
|
|
148
|
+
(5, 2)
|
|
149
|
+
>>> np.allclose(sp.Wm.sum(), 1.0)
|
|
150
|
+
True
|
|
151
|
+
|
|
141
152
|
Notes
|
|
142
153
|
-----
|
|
143
154
|
Julier's method is a special case of Merwe's with alpha=1, beta=0.
|
|
@@ -196,6 +207,21 @@ def unscented_transform(
|
|
|
196
207
|
Weighted mean, shape (m,).
|
|
197
208
|
cov : ndarray
|
|
198
209
|
Weighted covariance, shape (m, m).
|
|
210
|
+
|
|
211
|
+
Examples
|
|
212
|
+
--------
|
|
213
|
+
>>> import numpy as np
|
|
214
|
+
>>> # Generate sigma points
|
|
215
|
+
>>> x = np.array([0.0, 0.0])
|
|
216
|
+
>>> P = np.eye(2)
|
|
217
|
+
>>> sp = sigma_points_merwe(x, P)
|
|
218
|
+
>>> # Pass through nonlinear function
|
|
219
|
+
>>> def f(x):
|
|
220
|
+
... return np.array([x[0]**2, x[1]])
|
|
221
|
+
>>> sigmas_f = np.array([f(sp.points[i]) for i in range(len(sp.points))])
|
|
222
|
+
>>> mean, cov = unscented_transform(sigmas_f, sp.Wm, sp.Wc)
|
|
223
|
+
>>> mean.shape
|
|
224
|
+
(2,)
|
|
199
225
|
"""
|
|
200
226
|
# Weighted mean
|
|
201
227
|
mean = np.sum(Wm[:, np.newaxis] * sigmas, axis=0)
|
|
@@ -325,6 +351,24 @@ def ukf_update(
|
|
|
325
351
|
result : KalmanUpdate
|
|
326
352
|
Updated state, covariance, and innovation statistics.
|
|
327
353
|
|
|
354
|
+
Examples
|
|
355
|
+
--------
|
|
356
|
+
Update with a range-bearing measurement:
|
|
357
|
+
|
|
358
|
+
>>> import numpy as np
|
|
359
|
+
>>> # State: [x, y], range-bearing measurement
|
|
360
|
+
>>> def h_rb(x):
|
|
361
|
+
... r = np.sqrt(x[0]**2 + x[1]**2)
|
|
362
|
+
... theta = np.arctan2(x[1], x[0])
|
|
363
|
+
... return np.array([r, theta])
|
|
364
|
+
>>> x = np.array([100.0, 50.0])
|
|
365
|
+
>>> P = np.eye(2) * 10.0
|
|
366
|
+
>>> z = np.array([112.0, 0.46]) # measured range and bearing
|
|
367
|
+
>>> R = np.diag([1.0, 0.01]) # measurement noise
|
|
368
|
+
>>> upd = ukf_update(x, P, z, h_rb, R)
|
|
369
|
+
>>> upd.x.shape
|
|
370
|
+
(2,)
|
|
371
|
+
|
|
328
372
|
See Also
|
|
329
373
|
--------
|
|
330
374
|
ukf_predict : UKF prediction step.
|
|
@@ -400,6 +444,15 @@ def ckf_spherical_cubature_points(
|
|
|
400
444
|
weights : ndarray
|
|
401
445
|
Cubature weights.
|
|
402
446
|
|
|
447
|
+
Examples
|
|
448
|
+
--------
|
|
449
|
+
>>> import numpy as np
|
|
450
|
+
>>> points, weights = ckf_spherical_cubature_points(3)
|
|
451
|
+
>>> points.shape # 2n = 6 points for n=3
|
|
452
|
+
(6, 3)
|
|
453
|
+
>>> np.allclose(weights.sum(), 1.0)
|
|
454
|
+
True
|
|
455
|
+
|
|
403
456
|
Notes
|
|
404
457
|
-----
|
|
405
458
|
The CKF uses a third-degree spherical-radial cubature rule with
|
|
@@ -445,6 +498,20 @@ def ckf_predict(
|
|
|
445
498
|
result : KalmanPrediction
|
|
446
499
|
Predicted state and covariance.
|
|
447
500
|
|
|
501
|
+
Examples
|
|
502
|
+
--------
|
|
503
|
+
>>> import numpy as np
|
|
504
|
+
>>> # Linear dynamics: x_k+1 = F @ x_k
|
|
505
|
+
>>> def f(x):
|
|
506
|
+
... F = np.array([[1, 1], [0, 1]])
|
|
507
|
+
... return F @ x
|
|
508
|
+
>>> x = np.array([0.0, 1.0])
|
|
509
|
+
>>> P = np.eye(2) * 0.1
|
|
510
|
+
>>> Q = np.eye(2) * 0.01
|
|
511
|
+
>>> pred = ckf_predict(x, P, f, Q)
|
|
512
|
+
>>> pred.x # Should be approximately [1, 1]
|
|
513
|
+
array([1., 1.])
|
|
514
|
+
|
|
448
515
|
References
|
|
449
516
|
----------
|
|
450
517
|
.. [1] Arasaratnam, I. and Haykin, S., "Cubature Kalman Filters",
|
|
@@ -512,6 +579,20 @@ def ckf_update(
|
|
|
512
579
|
-------
|
|
513
580
|
result : KalmanUpdate
|
|
514
581
|
Updated state and covariance.
|
|
582
|
+
|
|
583
|
+
Examples
|
|
584
|
+
--------
|
|
585
|
+
>>> import numpy as np
|
|
586
|
+
>>> # Position measurement h(x) = x[0]
|
|
587
|
+
>>> def h(x):
|
|
588
|
+
... return np.array([x[0]])
|
|
589
|
+
>>> x = np.array([3.0, 1.0]) # predicted [position, velocity]
|
|
590
|
+
>>> P = np.eye(2) * 0.5
|
|
591
|
+
>>> z = np.array([3.1]) # measurement
|
|
592
|
+
>>> R = np.array([[0.1]])
|
|
593
|
+
>>> upd = ckf_update(x, P, z, h, R)
|
|
594
|
+
>>> upd.x.shape
|
|
595
|
+
(2,)
|
|
515
596
|
"""
|
|
516
597
|
x = np.asarray(x, dtype=np.float64).flatten()
|
|
517
598
|
P = np.asarray(P, dtype=np.float64)
|
|
@@ -48,6 +48,15 @@ def resample_multinomial(
|
|
|
48
48
|
-------
|
|
49
49
|
resampled : ndarray
|
|
50
50
|
Resampled particles with uniform weights.
|
|
51
|
+
|
|
52
|
+
Examples
|
|
53
|
+
--------
|
|
54
|
+
>>> rng = np.random.default_rng(42)
|
|
55
|
+
>>> particles = np.array([[1.0], [2.0], [3.0], [4.0]])
|
|
56
|
+
>>> weights = np.array([0.1, 0.1, 0.1, 0.7]) # particle 4 dominant
|
|
57
|
+
>>> resampled = resample_multinomial(particles, weights, rng)
|
|
58
|
+
>>> resampled.shape
|
|
59
|
+
(4, 1)
|
|
51
60
|
"""
|
|
52
61
|
if rng is None:
|
|
53
62
|
rng = np.random.default_rng()
|
|
@@ -80,6 +89,15 @@ def resample_systematic(
|
|
|
80
89
|
-------
|
|
81
90
|
resampled : ndarray
|
|
82
91
|
Resampled particles.
|
|
92
|
+
|
|
93
|
+
Examples
|
|
94
|
+
--------
|
|
95
|
+
>>> rng = np.random.default_rng(42)
|
|
96
|
+
>>> particles = np.array([[0.0], [1.0], [2.0], [3.0]])
|
|
97
|
+
>>> weights = np.array([0.25, 0.25, 0.25, 0.25])
|
|
98
|
+
>>> resampled = resample_systematic(particles, weights, rng)
|
|
99
|
+
>>> resampled.shape
|
|
100
|
+
(4, 1)
|
|
83
101
|
"""
|
|
84
102
|
if rng is None:
|
|
85
103
|
rng = np.random.default_rng()
|
|
@@ -145,6 +163,18 @@ def resample_residual(
|
|
|
145
163
|
-------
|
|
146
164
|
resampled : ndarray
|
|
147
165
|
Resampled particles.
|
|
166
|
+
|
|
167
|
+
Examples
|
|
168
|
+
--------
|
|
169
|
+
>>> rng = np.random.default_rng(42)
|
|
170
|
+
>>> particles = np.array([[1.0], [2.0], [3.0], [4.0]])
|
|
171
|
+
>>> weights = np.array([0.1, 0.2, 0.3, 0.4])
|
|
172
|
+
>>> resampled = resample_residual(particles, weights, rng)
|
|
173
|
+
>>> resampled.shape
|
|
174
|
+
(4, 1)
|
|
175
|
+
>>> # High-weight particles are copied deterministically
|
|
176
|
+
>>> np.sum(resampled == 4.0) >= 1
|
|
177
|
+
True
|
|
148
178
|
"""
|
|
149
179
|
if rng is None:
|
|
150
180
|
rng = np.random.default_rng()
|
|
@@ -184,6 +214,17 @@ def effective_sample_size(weights: NDArray[np.floating]) -> float:
|
|
|
184
214
|
ess : float
|
|
185
215
|
Effective sample size, in range [1, N].
|
|
186
216
|
|
|
217
|
+
Examples
|
|
218
|
+
--------
|
|
219
|
+
>>> # Uniform weights -> ESS = N
|
|
220
|
+
>>> weights = np.array([0.25, 0.25, 0.25, 0.25])
|
|
221
|
+
>>> effective_sample_size(weights)
|
|
222
|
+
4.0
|
|
223
|
+
>>> # Degenerate weights -> ESS approaches 1
|
|
224
|
+
>>> weights = np.array([0.97, 0.01, 0.01, 0.01])
|
|
225
|
+
>>> effective_sample_size(weights)
|
|
226
|
+
1.06...
|
|
227
|
+
|
|
187
228
|
Notes
|
|
188
229
|
-----
|
|
189
230
|
ESS = 1 / sum(w_i^2)
|
|
@@ -220,6 +261,29 @@ def bootstrap_pf_predict(
|
|
|
220
261
|
-------
|
|
221
262
|
particles_pred : ndarray
|
|
222
263
|
Predicted particles.
|
|
264
|
+
|
|
265
|
+
Examples
|
|
266
|
+
--------
|
|
267
|
+
Predict particles through constant velocity dynamics:
|
|
268
|
+
|
|
269
|
+
>>> import numpy as np
|
|
270
|
+
>>> rng = np.random.default_rng(42)
|
|
271
|
+
>>> # 50 particles for 2D state [position, velocity]
|
|
272
|
+
>>> particles = np.column_stack([
|
|
273
|
+
... np.zeros(50), # position
|
|
274
|
+
... np.ones(50) # velocity = 1
|
|
275
|
+
... ])
|
|
276
|
+
>>> dt = 0.1
|
|
277
|
+
>>> # x_next = [pos + vel*dt, vel]
|
|
278
|
+
>>> f = lambda x: np.array([x[0] + x[1] * dt, x[1]])
|
|
279
|
+
>>> # Process noise sampler
|
|
280
|
+
>>> Q_sample = lambda n, r: r.normal(0, 0.01, (n, 2))
|
|
281
|
+
>>> pred = bootstrap_pf_predict(particles, f, Q_sample, rng)
|
|
282
|
+
>>> pred.shape
|
|
283
|
+
(50, 2)
|
|
284
|
+
>>> # Positions moved forward by ~0.1
|
|
285
|
+
>>> np.mean(pred[:, 0]) # doctest: +ELLIPSIS
|
|
286
|
+
0.1...
|
|
223
287
|
"""
|
|
224
288
|
if rng is None:
|
|
225
289
|
rng = np.random.default_rng()
|
|
@@ -266,6 +330,25 @@ def bootstrap_pf_update(
|
|
|
266
330
|
Updated normalized weights.
|
|
267
331
|
log_likelihood : float
|
|
268
332
|
Log marginal likelihood of measurement.
|
|
333
|
+
|
|
334
|
+
Examples
|
|
335
|
+
--------
|
|
336
|
+
Update particle weights with a position measurement:
|
|
337
|
+
|
|
338
|
+
>>> import numpy as np
|
|
339
|
+
>>> # 4 particles at different positions
|
|
340
|
+
>>> particles = np.array([[0.0], [1.0], [2.0], [3.0]])
|
|
341
|
+
>>> weights = np.ones(4) / 4 # uniform initial weights
|
|
342
|
+
>>> z = np.array([2.0]) # measured position
|
|
343
|
+
>>> # Gaussian likelihood centered on measurement
|
|
344
|
+
>>> def likelihood(z, x):
|
|
345
|
+
... return np.exp(-0.5 * (z[0] - x[0])**2)
|
|
346
|
+
>>> new_weights, log_lik = bootstrap_pf_update(particles, weights, z, likelihood)
|
|
347
|
+
>>> # Particle at x=2 should have highest weight
|
|
348
|
+
>>> np.argmax(new_weights)
|
|
349
|
+
2
|
|
350
|
+
>>> np.sum(new_weights) # weights sum to 1
|
|
351
|
+
1.0
|
|
269
352
|
"""
|
|
270
353
|
z = np.asarray(z, dtype=np.float64)
|
|
271
354
|
N = len(particles)
|
|
@@ -312,6 +395,19 @@ def gaussian_likelihood(
|
|
|
312
395
|
-------
|
|
313
396
|
likelihood : float
|
|
314
397
|
p(z|x) = N(z; z_pred, R).
|
|
398
|
+
|
|
399
|
+
Examples
|
|
400
|
+
--------
|
|
401
|
+
>>> z = np.array([1.0, 2.0]) # actual measurement
|
|
402
|
+
>>> z_pred = np.array([1.0, 2.0]) # perfect prediction
|
|
403
|
+
>>> R = np.eye(2) * 0.1 # measurement noise covariance
|
|
404
|
+
>>> lik = gaussian_likelihood(z, z_pred, R)
|
|
405
|
+
>>> lik > 0
|
|
406
|
+
True
|
|
407
|
+
>>> # Zero innovation -> maximum likelihood
|
|
408
|
+
>>> z_off = np.array([10.0, 10.0])
|
|
409
|
+
>>> gaussian_likelihood(z_off, z_pred, R) < lik
|
|
410
|
+
True
|
|
315
411
|
"""
|
|
316
412
|
m = len(z)
|
|
317
413
|
innovation = z - z_pred
|
|
@@ -366,6 +462,30 @@ def bootstrap_pf_step(
|
|
|
366
462
|
-------
|
|
367
463
|
state : ParticleState
|
|
368
464
|
Updated particles and weights.
|
|
465
|
+
|
|
466
|
+
Examples
|
|
467
|
+
--------
|
|
468
|
+
Track a 1D random walk with position measurements:
|
|
469
|
+
|
|
470
|
+
>>> import numpy as np
|
|
471
|
+
>>> rng = np.random.default_rng(42)
|
|
472
|
+
>>> # 100 particles for 1D state
|
|
473
|
+
>>> particles = rng.normal(0, 1, (100, 1))
|
|
474
|
+
>>> weights = np.ones(100) / 100
|
|
475
|
+
>>> # Simple dynamics: x_k+1 = x_k + w
|
|
476
|
+
>>> f = lambda x: x
|
|
477
|
+
>>> h = lambda x: x # measure position directly
|
|
478
|
+
>>> Q_sample = lambda n, r: r.normal(0, 0.1, (n, 1))
|
|
479
|
+
>>> R = np.array([[0.5]]) # measurement noise
|
|
480
|
+
>>> z = np.array([0.5]) # measurement
|
|
481
|
+
>>> state = bootstrap_pf_step(particles, weights, z, f, h, Q_sample, R, rng=rng)
|
|
482
|
+
>>> state.particles.shape
|
|
483
|
+
(100, 1)
|
|
484
|
+
|
|
485
|
+
See Also
|
|
486
|
+
--------
|
|
487
|
+
bootstrap_pf_predict : Prediction step only.
|
|
488
|
+
bootstrap_pf_update : Update step only.
|
|
369
489
|
"""
|
|
370
490
|
if rng is None:
|
|
371
491
|
rng = np.random.default_rng()
|
|
@@ -420,6 +540,13 @@ def particle_mean(
|
|
|
420
540
|
-------
|
|
421
541
|
mean : ndarray
|
|
422
542
|
Weighted mean estimate.
|
|
543
|
+
|
|
544
|
+
Examples
|
|
545
|
+
--------
|
|
546
|
+
>>> particles = np.array([[0.0], [1.0], [2.0], [3.0]])
|
|
547
|
+
>>> weights = np.array([0.1, 0.2, 0.3, 0.4])
|
|
548
|
+
>>> particle_mean(particles, weights)
|
|
549
|
+
array([2.])
|
|
423
550
|
"""
|
|
424
551
|
return np.sum(weights[:, np.newaxis] * particles, axis=0)
|
|
425
552
|
|
|
@@ -470,6 +597,14 @@ def particle_covariance(
|
|
|
470
597
|
-------
|
|
471
598
|
cov : ndarray
|
|
472
599
|
Weighted covariance.
|
|
600
|
+
|
|
601
|
+
Examples
|
|
602
|
+
--------
|
|
603
|
+
>>> particles = np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])
|
|
604
|
+
>>> weights = np.array([0.25, 0.25, 0.25, 0.25])
|
|
605
|
+
>>> cov = particle_covariance(particles, weights)
|
|
606
|
+
>>> cov.shape
|
|
607
|
+
(2, 2)
|
|
473
608
|
"""
|
|
474
609
|
if mean is None:
|
|
475
610
|
mean = particle_mean(particles, weights)
|
|
@@ -505,6 +640,17 @@ def initialize_particles(
|
|
|
505
640
|
-------
|
|
506
641
|
state : ParticleState
|
|
507
642
|
Initial particles with uniform weights.
|
|
643
|
+
|
|
644
|
+
Examples
|
|
645
|
+
--------
|
|
646
|
+
>>> rng = np.random.default_rng(42)
|
|
647
|
+
>>> x0 = np.array([0.0, 0.0])
|
|
648
|
+
>>> P0 = np.eye(2) * 0.1
|
|
649
|
+
>>> state = initialize_particles(x0, P0, N=100, rng=rng)
|
|
650
|
+
>>> state.particles.shape
|
|
651
|
+
(100, 2)
|
|
652
|
+
>>> np.allclose(state.weights, 0.01) # uniform 1/N
|
|
653
|
+
True
|
|
508
654
|
"""
|
|
509
655
|
if rng is None:
|
|
510
656
|
rng = np.random.default_rng()
|
pytcl/dynamic_estimation/rbpf.py
CHANGED
|
@@ -501,6 +501,32 @@ def rbpf_predict(
|
|
|
501
501
|
-------
|
|
502
502
|
list[RBPFParticle]
|
|
503
503
|
Predicted particles
|
|
504
|
+
|
|
505
|
+
Examples
|
|
506
|
+
--------
|
|
507
|
+
>>> import numpy as np
|
|
508
|
+
>>> from pytcl.dynamic_estimation.rbpf import RBPFParticle
|
|
509
|
+
>>> np.random.seed(42)
|
|
510
|
+
>>> # 3 particles with nonlinear bearing and linear position
|
|
511
|
+
>>> particles = [
|
|
512
|
+
... RBPFParticle(y=np.array([0.1]), x=np.array([0.0, 1.0]),
|
|
513
|
+
... P=np.eye(2) * 0.5, w=1/3),
|
|
514
|
+
... RBPFParticle(y=np.array([0.0]), x=np.array([0.0, 1.0]),
|
|
515
|
+
... P=np.eye(2) * 0.5, w=1/3),
|
|
516
|
+
... RBPFParticle(y=np.array([-0.1]), x=np.array([0.0, 1.0]),
|
|
517
|
+
... P=np.eye(2) * 0.5, w=1/3),
|
|
518
|
+
... ]
|
|
519
|
+
>>> # Nonlinear dynamics for bearing
|
|
520
|
+
>>> g = lambda y: y # bearing stays constant
|
|
521
|
+
>>> G = np.eye(1)
|
|
522
|
+
>>> Qy = np.eye(1) * 0.01
|
|
523
|
+
>>> # Linear dynamics for position
|
|
524
|
+
>>> f = lambda x, y: np.array([x[0] + x[1] * 0.1, x[1]])
|
|
525
|
+
>>> F = np.array([[1, 0.1], [0, 1]])
|
|
526
|
+
>>> Qx = np.eye(2) * 0.01
|
|
527
|
+
>>> predicted = rbpf_predict(particles, g, G, Qy, f, F, Qx)
|
|
528
|
+
>>> len(predicted)
|
|
529
|
+
3
|
|
504
530
|
"""
|
|
505
531
|
new_particles = []
|
|
506
532
|
|
|
@@ -553,6 +579,31 @@ def rbpf_update(
|
|
|
553
579
|
-------
|
|
554
580
|
list[RBPFParticle]
|
|
555
581
|
Updated particles with adapted weights
|
|
582
|
+
|
|
583
|
+
Examples
|
|
584
|
+
--------
|
|
585
|
+
>>> import numpy as np
|
|
586
|
+
>>> from pytcl.dynamic_estimation.rbpf import RBPFParticle
|
|
587
|
+
>>> # 3 particles at different bearings
|
|
588
|
+
>>> particles = [
|
|
589
|
+
... RBPFParticle(y=np.array([0.5]), x=np.array([1.0, 0.0]),
|
|
590
|
+
... P=np.eye(2) * 0.5, w=1/3),
|
|
591
|
+
... RBPFParticle(y=np.array([0.0]), x=np.array([1.0, 0.0]),
|
|
592
|
+
... P=np.eye(2) * 0.5, w=1/3),
|
|
593
|
+
... RBPFParticle(y=np.array([-0.5]), x=np.array([1.0, 0.0]),
|
|
594
|
+
... P=np.eye(2) * 0.5, w=1/3),
|
|
595
|
+
... ]
|
|
596
|
+
>>> # Position measurement
|
|
597
|
+
>>> z = np.array([1.1])
|
|
598
|
+
>>> h = lambda x, y: np.array([x[0]]) # measure position
|
|
599
|
+
>>> H = np.array([[1, 0]])
|
|
600
|
+
>>> R = np.array([[0.1]])
|
|
601
|
+
>>> updated = rbpf_update(particles, z, h, H, R)
|
|
602
|
+
>>> len(updated)
|
|
603
|
+
3
|
|
604
|
+
>>> # Weights should sum to 1
|
|
605
|
+
>>> abs(sum(p.w for p in updated) - 1.0) < 1e-10
|
|
606
|
+
True
|
|
556
607
|
"""
|
|
557
608
|
weights = np.zeros(len(particles))
|
|
558
609
|
new_particles = []
|