35
35
* href="https://file.tavsys.net/control/controls-engineering-in-frc.pdf">https://file.tavsys.net/control/controls-engineering-in-frc.pdf</a>
36
36
* chapter 9 "Stochastic control theory".
37
37
*
38
- * <p>This class implements a square-root-form unscented Kalman filter (SR-UKF). For more
39
- * information about the SR-UKF, see <a
40
- * href=" https://www.researchgate.net/publication/3908304">https://www.researchgate.net/publication/3908304</a> .
38
+ * <p>This class implements a square-root-form unscented Kalman filter (SR-UKF). The main reason for
39
+ * this is to guarantee that the covariance matrix remains positive definite. For more information
40
+ * about the SR-UKF, see https://www.researchgate.net/publication/3908304.
41
41
*
42
42
* @param <States> Number of states.
43
43
* @param <Inputs> Number of inputs.
@@ -105,7 +105,7 @@ public UnscentedKalmanFilter(
105
105
}
106
106
107
107
/**
108
- * Constructs an unscented Kalman filter with custom mean, residual, and addition functions. Using
108
+ * Constructs an Unscented Kalman filter with custom mean, residual, and addition functions. Using
109
109
* custom functions for arithmetic can be useful if you have angles in the state or measurements,
110
110
* because they allow you to correctly account for the modular nature of angle arithmetic.
111
111
*
@@ -193,12 +193,21 @@ Pair<Matrix<C, N1>, Matrix<C, C>> squareRootUnscentedTransform(
193
193
"Wc must be 2 * states + 1 by 1! Got " + Wc .getNumRows () + " by " + Wc .getNumCols ());
194
194
}
195
195
196
- // New mean is usually just the sum of the sigmas * weight:
197
- // n
198
- // dot = Σ W[k] Xᵢ[k]
199
- // k=1
196
+ // New mean is usually just the sum of the sigmas * weights:
197
+ //
198
+ // 2n
199
+ // x̂ = Σ Wᵢ⁽ᵐ⁾𝒳ᵢ
200
+ // i=0
201
+ //
202
+ // equations (19) and (23) in the paper show this,
203
+ // but we allow a custom function, usually for angle wrapping
200
204
Matrix <C , N1 > x = meanFunc .apply (sigmas , Wm );
201
205
206
+ // Form an intermediate matrix S⁻ as:
207
+ //
208
+ // [√{W₁⁽ᶜ⁾}(𝒳_{1:2L} - x̂) √{Rᵛ}]
209
+ //
210
+ // the part of equations (20) and (24) within the "qr{}"
202
211
Matrix <C , ?> Sbar = new Matrix <>(new SimpleMatrix (dim .getNum (), 2 * s .getNum () + dim .getNum ()));
203
212
for (int i = 0 ; i < 2 * s .getNum (); i ++) {
204
213
Sbar .setColumn (
@@ -214,8 +223,24 @@ Pair<Matrix<C, N1>, Matrix<C, C>> squareRootUnscentedTransform(
214
223
throw new RuntimeException ("QR decomposition failed! Input matrix:\n " + qrStorage );
215
224
}
216
225
217
- Matrix <C , C > newS = new Matrix <>(new SimpleMatrix (qr .getR (null , true )));
218
- newS .rankUpdate (residualFunc .apply (sigmas .extractColumnVector (0 ), x ), Wc .get (0 , 0 ), false );
226
+ // Compute the square-root covariance of the sigma points
227
+ //
228
+ // We transpose S⁻ first because we formed it by horizontally
229
+ // concatenating each part; it should be vertical so we can take
230
+ // the QR decomposition as defined in the "QR Decomposition" passage
231
+ // of section 3. "EFFICIENT SQUARE-ROOT IMPLEMENTATION"
232
+ //
233
+ // The resulting matrix R is the square-root covariance S, but it
234
+ // is upper triangular, so we need to transpose it.
235
+ //
236
+ // equations (20) and (24)
237
+ Matrix <C , C > newS = new Matrix <>(new SimpleMatrix (qr .getR (null , true )).transpose ());
238
+
239
+ // Update or downdate the square-root covariance with (𝒳₀-x̂)
240
+ // depending on whether its weight (W₀⁽ᶜ⁾) is positive or negative.
241
+ //
242
+ // equations (21) and (25)
243
+ newS .rankUpdate (residualFunc .apply (sigmas .extractColumnVector (0 ), x ), Wc .get (0 , 0 ), true );
219
244
220
245
return new Pair <>(x , newS );
221
246
}
@@ -256,7 +281,7 @@ public void setS(Matrix<States, States> newS) {
256
281
*/
257
282
@ Override
258
283
public Matrix <States , States > getP () {
259
- return m_S .transpose (). times (m_S );
284
+ return m_S .times (m_S . transpose () );
260
285
}
261
286
262
287
/**
@@ -280,7 +305,7 @@ public double getP(int row, int col) {
280
305
*/
281
306
@ Override
282
307
public void setP (Matrix <States , States > newP ) {
283
- m_S = newP .lltDecompose (false );
308
+ m_S = newP .lltDecompose (true );
284
309
}
285
310
286
311
/**
@@ -347,14 +372,28 @@ public void predict(Matrix<Inputs, N1> u, double dtSeconds) {
347
372
var discQ = Discretization .discretizeAQ (contA , m_contQ , dtSeconds ).getSecond ();
348
373
var squareRootDiscQ = discQ .lltDecompose (true );
349
374
375
+ // Generate sigma points around the state mean
376
+ //
377
+ // equation (17)
350
378
var sigmas = m_pts .squareRootSigmaPoints (m_xHat , m_S );
351
379
380
+ // Project each sigma point forward in time according to the
381
+ // dynamics f(x, u)
382
+ //
383
+ // sigmas = 𝒳ₖ₋₁
384
+ // sigmasF = 𝒳ₖ,ₖ₋₁ or just 𝒳 for readability
385
+ //
386
+ // equation (18)
352
387
for (int i = 0 ; i < m_pts .getNumSigmas (); ++i ) {
353
388
Matrix <States , N1 > x = sigmas .extractColumnVector (i );
354
389
355
390
m_sigmasF .setColumn (i , NumericalIntegration .rk4 (m_f , x , u , dtSeconds ));
356
391
}
357
392
393
+ // Pass the predicted sigmas (𝒳) through the Unscented Transform
394
+ // to compute the prior state mean and covariance
395
+ //
396
+ // equations (18) (19) and (20)
358
397
var ret =
359
398
squareRootUnscentedTransform (
360
399
m_states ,
@@ -459,15 +498,27 @@ public <R extends Num> void correct(
459
498
final var discR = Discretization .discretizeR (R , m_dtSeconds );
460
499
final var squareRootDiscR = discR .lltDecompose (true );
461
500
462
- // Transform sigma points into measurement space
501
+ // Generate new sigma points from the prior mean and covariance
502
+ // and transform them into measurement space using h(x, u)
503
+ //
504
+ // sigmas = 𝒳
505
+ // sigmasH = 𝒴
506
+ //
507
+ // This differs from equation (22) which uses
508
+ // the prior sigma points, regenerating them allows
509
+ // multiple measurement updates per time update
463
510
Matrix <R , ?> sigmasH = new Matrix <>(new SimpleMatrix (rows .getNum (), 2 * m_states .getNum () + 1 ));
464
511
var sigmas = m_pts .squareRootSigmaPoints (m_xHat , m_S );
465
512
for (int i = 0 ; i < m_pts .getNumSigmas (); i ++) {
466
513
Matrix <R , N1 > hRet = h .apply (sigmas .extractColumnVector (i ), u );
467
514
sigmasH .setColumn (i , hRet );
468
515
}
469
516
470
- // Mean and covariance of prediction passed through unscented transform
517
+ // Pass the predicted measurement sigmas through the Unscented Transform
518
+ // to compute the mean predicted measurement and square-root innovation
519
+ // covariance.
520
+ //
521
+ // equations (23) (24) and (25)
471
522
var transRet =
472
523
squareRootUnscentedTransform (
473
524
m_states ,
@@ -481,30 +532,54 @@ public <R extends Num> void correct(
481
532
var yHat = transRet .getFirst ();
482
533
var Sy = transRet .getSecond ();
483
534
484
- // Compute cross covariance of the state and the measurements
535
+ // Compute cross covariance of the predicted state and measurement sigma
536
+ // points given as:
537
+ //
538
+ // 2n
539
+ // P_{xy} = Σ Wᵢ⁽ᶜ⁾[𝒳ᵢ - x̂][𝒴ᵢ - ŷ⁻]ᵀ
540
+ // i=0
541
+ //
542
+ // equation (26)
485
543
Matrix <States , R > Pxy = new Matrix <>(m_states , rows );
486
544
for (int i = 0 ; i < m_pts .getNumSigmas (); i ++) {
487
- // Pxy += (sigmas_f[:, i] - x̂)(sigmas_h[:, i] - ŷ)ᵀ W_c[i]
488
545
var dx = residualFuncX .apply (m_sigmasF .extractColumnVector (i ), m_xHat );
489
546
var dy = residualFuncY .apply (sigmasH .extractColumnVector (i ), yHat ).transpose ();
490
547
491
548
Pxy = Pxy .plus (dx .times (dy ).times (m_pts .getWc (i )));
492
549
}
493
550
494
- // K = (P_{xy} / S_yᵀ) / S_y
495
- // K = (S_y \ P_{xy}ᵀ)ᵀ / S_y
496
- // K = (S_yᵀ \ (S_y \ P_{xy}ᵀ))ᵀ
551
+ // Compute the Kalman gain. We use Eigen's QR decomposition to solve. This
552
+ // is equivalent to MATLAB's \ operator, so we need to rearrange to use
553
+ // that.
554
+ //
555
+ // K = (P_{xy} / S_{y}ᵀ) / S_{y}
556
+ // K = (S_{y} \ P_{xy})ᵀ / S_{y}
557
+ // K = (S_{y}ᵀ \ (S_{y} \ P_{xy}ᵀ))ᵀ
558
+ //
559
+ // equation (27)
497
560
Matrix <States , R > K =
498
561
Sy .transpose ()
499
562
.solveFullPivHouseholderQr (Sy .solveFullPivHouseholderQr (Pxy .transpose ()))
500
563
.transpose ();
501
564
502
- // x̂ₖ₊₁⁺ = x̂ₖ₊₁⁻ + K(y − ŷ)
565
+ // Compute the posterior state mean
566
+ //
567
+ // x̂ = x̂⁻ + K(y − ŷ⁻)
568
+ //
569
+ // second part of equation (27)
503
570
m_xHat = addFuncX .apply (m_xHat , K .times (residualFuncY .apply (y , yHat )));
504
571
572
+ // Compute the intermediate matrix U for downdating
573
+ // the square-root covariance
574
+ //
575
+ // equation (28)
505
576
Matrix <States , R > U = K .times (Sy );
577
+
578
+ // Downdate the posterior square-root state covariance
579
+ //
580
+ // equation (29)
506
581
for (int i = 0 ; i < rows .getNum (); i ++) {
507
- m_S .rankUpdate (U .extractColumnVector (i ), -1 , false );
582
+ m_S .rankUpdate (U .extractColumnVector (i ), -1 , true );
508
583
}
509
584
}
510
585
}
0 commit comments