001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.math.estimator;
006
007import edu.wpi.first.math.MathSharedStore;
008import edu.wpi.first.math.MathUsageId;
009import edu.wpi.first.math.Matrix;
010import edu.wpi.first.math.Nat;
011import edu.wpi.first.math.Num;
012import edu.wpi.first.math.Pair;
013import edu.wpi.first.math.StateSpaceUtil;
014import edu.wpi.first.math.numbers.N1;
015import edu.wpi.first.math.system.Discretization;
016import edu.wpi.first.math.system.NumericalIntegration;
017import edu.wpi.first.math.system.NumericalJacobian;
018import java.util.function.BiFunction;
019import org.ejml.dense.row.decomposition.qr.QRDecompositionHouseholder_DDRM;
020import org.ejml.simple.SimpleMatrix;
021
022/**
023 * A Kalman filter combines predictions from a model and measurements to give an estimate of the
024 * true system state. This is useful because many states cannot be measured directly as a result of
025 * sensor noise, or because the state is "hidden".
026 *
027 * <p>Kalman filters use a K gain matrix to determine whether to trust the model or measurements
028 * more. Kalman filter theory uses statistics to compute an optimal K gain which minimizes the sum
029 * of squares error in the state estimate. This K gain is used to correct the state estimate by some
030 * amount of the difference between the actual measurements and the measurements predicted by the
031 * model.
032 *
033 * <p>An unscented Kalman filter uses nonlinear state and measurement models. It propagates the
034 * error covariance using sigma points chosen to approximate the true probability distribution.
035 *
036 * <p>For more on the underlying math, read <a
037 * href="https://file.tavsys.net/control/controls-engineering-in-frc.pdf">https://file.tavsys.net/control/controls-engineering-in-frc.pdf</a>
038 * chapter 9 "Stochastic control theory".
039 *
040 * <p>This class implements a square-root-form unscented Kalman filter (SR-UKF). The main reason for
041 * this is to guarantee that the covariance matrix remains positive definite. For more information
042 * about the SR-UKF, see https://www.researchgate.net/publication/3908304.
043 *
044 * @param <States> Number of states.
045 * @param <Inputs> Number of inputs.
046 * @param <Outputs> Number of outputs.
047 */
048public class UnscentedKalmanFilter<States extends Num, Inputs extends Num, Outputs extends Num>
049    implements KalmanTypeFilter<States, Inputs, Outputs> {
050  private final Nat<States> m_states;
051  private final Nat<Outputs> m_outputs;
052
053  private final BiFunction<Matrix<States, N1>, Matrix<Inputs, N1>, Matrix<States, N1>> m_f;
054  private final BiFunction<Matrix<States, N1>, Matrix<Inputs, N1>, Matrix<Outputs, N1>> m_h;
055
056  private BiFunction<Matrix<States, ?>, Matrix<?, N1>, Matrix<States, N1>> m_meanFuncX;
057  private BiFunction<Matrix<Outputs, ?>, Matrix<?, N1>, Matrix<Outputs, N1>> m_meanFuncY;
058  private BiFunction<Matrix<States, N1>, Matrix<States, N1>, Matrix<States, N1>> m_residualFuncX;
059  private BiFunction<Matrix<Outputs, N1>, Matrix<Outputs, N1>, Matrix<Outputs, N1>> m_residualFuncY;
060  private BiFunction<Matrix<States, N1>, Matrix<States, N1>, Matrix<States, N1>> m_addFuncX;
061
062  private Matrix<States, N1> m_xHat;
063  private Matrix<States, States> m_S;
064  private final Matrix<States, States> m_contQ;
065  private final Matrix<Outputs, Outputs> m_contR;
066  private Matrix<States, ?> m_sigmasF;
067  private double m_dtSeconds;
068
069  private final MerweScaledSigmaPoints<States> m_pts;
070
071  /**
072   * Constructs an Unscented Kalman Filter.
073   *
074   * <p>See <a
075   * href="https://docs.wpilib.org/en/stable/docs/software/advanced-controls/state-space/state-space-observers.html#process-and-measurement-noise-covariance-matrices">https://docs.wpilib.org/en/stable/docs/software/advanced-controls/state-space/state-space-observers.html#process-and-measurement-noise-covariance-matrices</a>
076   * for how to select the standard deviations.
077   *
078   * @param states A Nat representing the number of states.
079   * @param outputs A Nat representing the number of outputs.
080   * @param f A vector-valued function of x and u that returns the derivative of the state vector.
081   * @param h A vector-valued function of x and u that returns the measurement vector.
082   * @param stateStdDevs Standard deviations of model states.
083   * @param measurementStdDevs Standard deviations of measurements.
084   * @param nominalDtSeconds Nominal discretization timestep.
085   */
086  public UnscentedKalmanFilter(
087      Nat<States> states,
088      Nat<Outputs> outputs,
089      BiFunction<Matrix<States, N1>, Matrix<Inputs, N1>, Matrix<States, N1>> f,
090      BiFunction<Matrix<States, N1>, Matrix<Inputs, N1>, Matrix<Outputs, N1>> h,
091      Matrix<States, N1> stateStdDevs,
092      Matrix<Outputs, N1> measurementStdDevs,
093      double nominalDtSeconds) {
094    this(
095        states,
096        outputs,
097        f,
098        h,
099        stateStdDevs,
100        measurementStdDevs,
101        (sigmas, Wm) -> sigmas.times(Matrix.changeBoundsUnchecked(Wm)),
102        (sigmas, Wm) -> sigmas.times(Matrix.changeBoundsUnchecked(Wm)),
103        Matrix::minus,
104        Matrix::minus,
105        Matrix::plus,
106        nominalDtSeconds);
107  }
108
109  /**
110   * Constructs an Unscented Kalman filter with custom mean, residual, and addition functions. Using
111   * custom functions for arithmetic can be useful if you have angles in the state or measurements,
112   * because they allow you to correctly account for the modular nature of angle arithmetic.
113   *
114   * <p>See <a
115   * href="https://docs.wpilib.org/en/stable/docs/software/advanced-controls/state-space/state-space-observers.html#process-and-measurement-noise-covariance-matrices">https://docs.wpilib.org/en/stable/docs/software/advanced-controls/state-space/state-space-observers.html#process-and-measurement-noise-covariance-matrices</a>
116   * for how to select the standard deviations.
117   *
118   * @param states A Nat representing the number of states.
119   * @param outputs A Nat representing the number of outputs.
120   * @param f A vector-valued function of x and u that returns the derivative of the state vector.
121   * @param h A vector-valued function of x and u that returns the measurement vector.
122   * @param stateStdDevs Standard deviations of model states.
123   * @param measurementStdDevs Standard deviations of measurements.
124   * @param meanFuncX A function that computes the mean of 2 * States + 1 state vectors using a
125   *     given set of weights.
126   * @param meanFuncY A function that computes the mean of 2 * States + 1 measurement vectors using
127   *     a given set of weights.
128   * @param residualFuncX A function that computes the residual of two state vectors (i.e. it
129   *     subtracts them.)
130   * @param residualFuncY A function that computes the residual of two measurement vectors (i.e. it
131   *     subtracts them.)
132   * @param addFuncX A function that adds two state vectors.
133   * @param nominalDtSeconds Nominal discretization timestep.
134   */
135  public UnscentedKalmanFilter(
136      Nat<States> states,
137      Nat<Outputs> outputs,
138      BiFunction<Matrix<States, N1>, Matrix<Inputs, N1>, Matrix<States, N1>> f,
139      BiFunction<Matrix<States, N1>, Matrix<Inputs, N1>, Matrix<Outputs, N1>> h,
140      Matrix<States, N1> stateStdDevs,
141      Matrix<Outputs, N1> measurementStdDevs,
142      BiFunction<Matrix<States, ?>, Matrix<?, N1>, Matrix<States, N1>> meanFuncX,
143      BiFunction<Matrix<Outputs, ?>, Matrix<?, N1>, Matrix<Outputs, N1>> meanFuncY,
144      BiFunction<Matrix<States, N1>, Matrix<States, N1>, Matrix<States, N1>> residualFuncX,
145      BiFunction<Matrix<Outputs, N1>, Matrix<Outputs, N1>, Matrix<Outputs, N1>> residualFuncY,
146      BiFunction<Matrix<States, N1>, Matrix<States, N1>, Matrix<States, N1>> addFuncX,
147      double nominalDtSeconds) {
148    this.m_states = states;
149    this.m_outputs = outputs;
150
151    m_f = f;
152    m_h = h;
153
154    m_meanFuncX = meanFuncX;
155    m_meanFuncY = meanFuncY;
156    m_residualFuncX = residualFuncX;
157    m_residualFuncY = residualFuncY;
158    m_addFuncX = addFuncX;
159
160    m_dtSeconds = nominalDtSeconds;
161
162    m_contQ = StateSpaceUtil.makeCovarianceMatrix(states, stateStdDevs);
163    m_contR = StateSpaceUtil.makeCovarianceMatrix(outputs, measurementStdDevs);
164
165    m_pts = new MerweScaledSigmaPoints<>(states);
166
167    reset();
168    MathSharedStore.getMathShared().reportUsage(MathUsageId.kEstimator_KalmanFilter, 3);
169  }
170
171  static <S extends Num, C extends Num>
172      Pair<Matrix<C, N1>, Matrix<C, C>> squareRootUnscentedTransform(
173          Nat<S> s,
174          Nat<C> dim,
175          Matrix<C, ?> sigmas,
176          Matrix<?, N1> Wm,
177          Matrix<?, N1> Wc,
178          BiFunction<Matrix<C, ?>, Matrix<?, N1>, Matrix<C, N1>> meanFunc,
179          BiFunction<Matrix<C, N1>, Matrix<C, N1>, Matrix<C, N1>> residualFunc,
180          Matrix<C, C> squareRootR) {
181    if (sigmas.getNumRows() != dim.getNum() || sigmas.getNumCols() != 2 * s.getNum() + 1) {
182      throw new IllegalArgumentException(
183          "Sigmas must be covDim by 2 * states + 1! Got "
184              + sigmas.getNumRows()
185              + " by "
186              + sigmas.getNumCols());
187    }
188
189    if (Wm.getNumRows() != 2 * s.getNum() + 1 || Wm.getNumCols() != 1) {
190      throw new IllegalArgumentException(
191          "Wm must be 2 * states + 1 by 1! Got " + Wm.getNumRows() + " by " + Wm.getNumCols());
192    }
193
194    if (Wc.getNumRows() != 2 * s.getNum() + 1 || Wc.getNumCols() != 1) {
195      throw new IllegalArgumentException(
196          "Wc must be 2 * states + 1 by 1! Got " + Wc.getNumRows() + " by " + Wc.getNumCols());
197    }
198
199    // New mean is usually just the sum of the sigmas * weights:
200    //
201    //      2n
202    //   x̂ = Σ Wᵢ⁽ᵐ⁾𝒳ᵢ
203    //      i=0
204    //
205    // equations (19) and (23) in the paper show this,
206    // but we allow a custom function, usually for angle wrapping
207    Matrix<C, N1> x = meanFunc.apply(sigmas, Wm);
208
209    // Form an intermediate matrix S⁻ as:
210    //
211    //   [√{W₁⁽ᶜ⁾}(𝒳_{1:2L} - x̂) √{Rᵛ}]
212    //
213    // the part of equations (20) and (24) within the "qr{}"
214    Matrix<C, ?> Sbar = new Matrix<>(new SimpleMatrix(dim.getNum(), 2 * s.getNum() + dim.getNum()));
215    for (int i = 0; i < 2 * s.getNum(); i++) {
216      Sbar.setColumn(
217          i,
218          residualFunc.apply(sigmas.extractColumnVector(1 + i), x).times(Math.sqrt(Wc.get(1, 0))));
219    }
220    Sbar.assignBlock(0, 2 * s.getNum(), squareRootR);
221
222    QRDecompositionHouseholder_DDRM qr = new QRDecompositionHouseholder_DDRM();
223    var qrStorage = Sbar.transpose().getStorage();
224
225    if (!qr.decompose(qrStorage.getDDRM())) {
226      throw new RuntimeException("QR decomposition failed! Input matrix:\n" + qrStorage);
227    }
228
229    // Compute the square-root covariance of the sigma points
230    //
231    // We transpose S⁻ first because we formed it by horizontally
232    // concatenating each part; it should be vertical so we can take
233    // the QR decomposition as defined in the "QR Decomposition" passage
234    // of section 3. "EFFICIENT SQUARE-ROOT IMPLEMENTATION"
235    //
236    // The resulting matrix R is the square-root covariance S, but it
237    // is upper triangular, so we need to transpose it.
238    //
239    // equations (20) and (24)
240    Matrix<C, C> newS = new Matrix<>(new SimpleMatrix(qr.getR(null, true)).transpose());
241
242    // Update or downdate the square-root covariance with (𝒳₀-x̂)
243    // depending on whether its weight (W₀⁽ᶜ⁾) is positive or negative.
244    //
245    // equations (21) and (25)
246    newS.rankUpdate(residualFunc.apply(sigmas.extractColumnVector(0), x), Wc.get(0, 0), true);
247
248    return new Pair<>(x, newS);
249  }
250
251  /**
252   * Returns the square-root error covariance matrix S.
253   *
254   * @return the square-root error covariance matrix S.
255   */
256  public Matrix<States, States> getS() {
257    return m_S;
258  }
259
260  /**
261   * Returns an element of the square-root error covariance matrix S.
262   *
263   * @param row Row of S.
264   * @param col Column of S.
265   * @return the value of the square-root error covariance matrix S at (i, j).
266   */
267  public double getS(int row, int col) {
268    return m_S.get(row, col);
269  }
270
271  /**
272   * Sets the entire square-root error covariance matrix S.
273   *
274   * @param newS The new value of S to use.
275   */
276  public void setS(Matrix<States, States> newS) {
277    m_S = newS;
278  }
279
280  /**
281   * Returns the reconstructed error covariance matrix P.
282   *
283   * @return the error covariance matrix P.
284   */
285  @Override
286  public Matrix<States, States> getP() {
287    return m_S.times(m_S.transpose());
288  }
289
290  /**
291   * Returns an element of the error covariance matrix P.
292   *
293   * @param row Row of P.
294   * @param col Column of P.
295   * @return the value of the error covariance matrix P at (i, j).
296   * @throws UnsupportedOperationException indexing into the reconstructed P matrix is not supported
297   */
298  @Override
299  public double getP(int row, int col) {
300    throw new UnsupportedOperationException(
301        "indexing into the reconstructed P matrix is not supported");
302  }
303
304  /**
305   * Sets the entire error covariance matrix P.
306   *
307   * @param newP The new value of P to use.
308   */
309  @Override
310  public void setP(Matrix<States, States> newP) {
311    m_S = newP.lltDecompose(true);
312  }
313
314  /**
315   * Returns the state estimate x-hat.
316   *
317   * @return the state estimate x-hat.
318   */
319  @Override
320  public Matrix<States, N1> getXhat() {
321    return m_xHat;
322  }
323
324  /**
325   * Returns an element of the state estimate x-hat.
326   *
327   * @param row Row of x-hat.
328   * @return the value of the state estimate x-hat at 'i'.
329   */
330  @Override
331  public double getXhat(int row) {
332    return m_xHat.get(row, 0);
333  }
334
335  /**
336   * Set initial state estimate x-hat.
337   *
338   * @param xHat The state estimate x-hat.
339   */
340  @Override
341  public void setXhat(Matrix<States, N1> xHat) {
342    m_xHat = xHat;
343  }
344
345  /**
346   * Set an element of the initial state estimate x-hat.
347   *
348   * @param row Row of x-hat.
349   * @param value Value for element of x-hat.
350   */
351  @Override
352  public void setXhat(int row, double value) {
353    m_xHat.set(row, 0, value);
354  }
355
356  /** Resets the observer. */
357  @Override
358  public final void reset() {
359    m_xHat = new Matrix<>(m_states, Nat.N1());
360    m_S = new Matrix<>(m_states, m_states);
361    m_sigmasF = new Matrix<>(new SimpleMatrix(m_states.getNum(), 2 * m_states.getNum() + 1));
362  }
363
364  /**
365   * Project the model into the future with a new control input u.
366   *
367   * @param u New control input from controller.
368   * @param dtSeconds Timestep for prediction.
369   */
370  @Override
371  public void predict(Matrix<Inputs, N1> u, double dtSeconds) {
372    // Discretize Q before projecting mean and covariance forward
373    Matrix<States, States> contA =
374        NumericalJacobian.numericalJacobianX(m_states, m_states, m_f, m_xHat, u);
375    var discQ = Discretization.discretizeAQ(contA, m_contQ, dtSeconds).getSecond();
376    var squareRootDiscQ = discQ.lltDecompose(true);
377
378    // Generate sigma points around the state mean
379    //
380    // equation (17)
381    var sigmas = m_pts.squareRootSigmaPoints(m_xHat, m_S);
382
383    // Project each sigma point forward in time according to the
384    // dynamics f(x, u)
385    //
386    //   sigmas  = 𝒳ₖ₋₁
387    //   sigmasF = 𝒳ₖ,ₖ₋₁ or just 𝒳 for readability
388    //
389    // equation (18)
390    for (int i = 0; i < m_pts.getNumSigmas(); ++i) {
391      Matrix<States, N1> x = sigmas.extractColumnVector(i);
392
393      m_sigmasF.setColumn(i, NumericalIntegration.rk4(m_f, x, u, dtSeconds));
394    }
395
396    // Pass the predicted sigmas (𝒳) through the Unscented Transform
397    // to compute the prior state mean and covariance
398    //
399    // equations (18) (19) and (20)
400    var ret =
401        squareRootUnscentedTransform(
402            m_states,
403            m_states,
404            m_sigmasF,
405            m_pts.getWm(),
406            m_pts.getWc(),
407            m_meanFuncX,
408            m_residualFuncX,
409            squareRootDiscQ);
410
411    m_xHat = ret.getFirst();
412    m_S = ret.getSecond();
413    m_dtSeconds = dtSeconds;
414  }
415
416  /**
417   * Correct the state estimate x-hat using the measurements in y.
418   *
419   * @param u Same control input used in the predict step.
420   * @param y Measurement vector.
421   */
422  @Override
423  public void correct(Matrix<Inputs, N1> u, Matrix<Outputs, N1> y) {
424    correct(
425        m_outputs, u, y, m_h, m_contR, m_meanFuncY, m_residualFuncY, m_residualFuncX, m_addFuncX);
426  }
427
428  /**
429   * Correct the state estimate x-hat using the measurements in y.
430   *
431   * <p>This is useful for when the measurement noise covariances vary.
432   *
433   * @param u Same control input used in the predict step.
434   * @param y Measurement vector.
435   * @param R Continuous measurement noise covariance matrix.
436   */
437  public void correct(Matrix<Inputs, N1> u, Matrix<Outputs, N1> y, Matrix<Outputs, Outputs> R) {
438    correct(m_outputs, u, y, m_h, R, m_meanFuncY, m_residualFuncY, m_residualFuncX, m_addFuncX);
439  }
440
441  /**
442   * Correct the state estimate x-hat using the measurements in y.
443   *
444   * <p>This is useful for when the measurements available during a timestep's Correct() call vary.
445   * The h(x, u) passed to the constructor is used if one is not provided (the two-argument version
446   * of this function).
447   *
448   * @param <R> Number of measurements in y.
449   * @param rows Number of rows in y.
450   * @param u Same control input used in the predict step.
451   * @param y Measurement vector.
452   * @param h A vector-valued function of x and u that returns the measurement vector.
453   * @param R Continuous measurement noise covariance matrix.
454   */
455  public <R extends Num> void correct(
456      Nat<R> rows,
457      Matrix<Inputs, N1> u,
458      Matrix<R, N1> y,
459      BiFunction<Matrix<States, N1>, Matrix<Inputs, N1>, Matrix<R, N1>> h,
460      Matrix<R, R> R) {
461    BiFunction<Matrix<R, ?>, Matrix<?, N1>, Matrix<R, N1>> meanFuncY =
462        (sigmas, Wm) -> sigmas.times(Matrix.changeBoundsUnchecked(Wm));
463    BiFunction<Matrix<States, N1>, Matrix<States, N1>, Matrix<States, N1>> residualFuncX =
464        Matrix::minus;
465    BiFunction<Matrix<R, N1>, Matrix<R, N1>, Matrix<R, N1>> residualFuncY = Matrix::minus;
466    BiFunction<Matrix<States, N1>, Matrix<States, N1>, Matrix<States, N1>> addFuncX = Matrix::plus;
467    correct(rows, u, y, h, R, meanFuncY, residualFuncY, residualFuncX, addFuncX);
468  }
469
470  /**
471   * Correct the state estimate x-hat using the measurements in y.
472   *
473   * <p>This is useful for when the measurements available during a timestep's Correct() call vary.
474   * The h(x, u) passed to the constructor is used if one is not provided (the two-argument version
475   * of this function).
476   *
477   * @param <R> Number of measurements in y.
478   * @param rows Number of rows in y.
479   * @param u Same control input used in the predict step.
480   * @param y Measurement vector.
481   * @param h A vector-valued function of x and u that returns the measurement vector.
482   * @param R Continuous measurement noise covariance matrix.
483   * @param meanFuncY A function that computes the mean of 2 * States + 1 measurement vectors using
484   *     a given set of weights.
485   * @param residualFuncY A function that computes the residual of two measurement vectors (i.e. it
486   *     subtracts them.)
487   * @param residualFuncX A function that computes the residual of two state vectors (i.e. it
488   *     subtracts them.)
489   * @param addFuncX A function that adds two state vectors.
490   */
491  public <R extends Num> void correct(
492      Nat<R> rows,
493      Matrix<Inputs, N1> u,
494      Matrix<R, N1> y,
495      BiFunction<Matrix<States, N1>, Matrix<Inputs, N1>, Matrix<R, N1>> h,
496      Matrix<R, R> R,
497      BiFunction<Matrix<R, ?>, Matrix<?, N1>, Matrix<R, N1>> meanFuncY,
498      BiFunction<Matrix<R, N1>, Matrix<R, N1>, Matrix<R, N1>> residualFuncY,
499      BiFunction<Matrix<States, N1>, Matrix<States, N1>, Matrix<States, N1>> residualFuncX,
500      BiFunction<Matrix<States, N1>, Matrix<States, N1>, Matrix<States, N1>> addFuncX) {
501    final var discR = Discretization.discretizeR(R, m_dtSeconds);
502    final var squareRootDiscR = discR.lltDecompose(true);
503
504    // Generate new sigma points from the prior mean and covariance
505    // and transform them into measurement space using h(x, u)
506    //
507    //   sigmas  = 𝒳
508    //   sigmasH = 𝒴
509    //
510    // This differs from equation (22) which uses
511    // the prior sigma points, regenerating them allows
512    // multiple measurement updates per time update
513    Matrix<R, ?> sigmasH = new Matrix<>(new SimpleMatrix(rows.getNum(), 2 * m_states.getNum() + 1));
514    var sigmas = m_pts.squareRootSigmaPoints(m_xHat, m_S);
515    for (int i = 0; i < m_pts.getNumSigmas(); i++) {
516      Matrix<R, N1> hRet = h.apply(sigmas.extractColumnVector(i), u);
517      sigmasH.setColumn(i, hRet);
518    }
519
520    // Pass the predicted measurement sigmas through the Unscented Transform
521    // to compute the mean predicted measurement and square-root innovation
522    // covariance.
523    //
524    // equations (23) (24) and (25)
525    var transRet =
526        squareRootUnscentedTransform(
527            m_states,
528            rows,
529            sigmasH,
530            m_pts.getWm(),
531            m_pts.getWc(),
532            meanFuncY,
533            residualFuncY,
534            squareRootDiscR);
535    var yHat = transRet.getFirst();
536    var Sy = transRet.getSecond();
537
538    // Compute cross covariance of the predicted state and measurement sigma
539    // points given as:
540    //
541    //           2n
542    //   P_{xy} = Σ Wᵢ⁽ᶜ⁾[𝒳ᵢ - x̂][𝒴ᵢ - ŷ⁻]ᵀ
543    //           i=0
544    //
545    // equation (26)
546    Matrix<States, R> Pxy = new Matrix<>(m_states, rows);
547    for (int i = 0; i < m_pts.getNumSigmas(); i++) {
548      var dx = residualFuncX.apply(m_sigmasF.extractColumnVector(i), m_xHat);
549      var dy = residualFuncY.apply(sigmasH.extractColumnVector(i), yHat).transpose();
550
551      Pxy = Pxy.plus(dx.times(dy).times(m_pts.getWc(i)));
552    }
553
554    // Compute the Kalman gain. We use Eigen's QR decomposition to solve. This
555    // is equivalent to MATLAB's \ operator, so we need to rearrange to use
556    // that.
557    //
558    //   K = (P_{xy} / S_{y}ᵀ) / S_{y}
559    //   K = (S_{y} \ P_{xy})ᵀ / S_{y}
560    //   K = (S_{y}ᵀ \ (S_{y} \ P_{xy}ᵀ))ᵀ
561    //
562    // equation (27)
563    Matrix<States, R> K =
564        Sy.transpose()
565            .solveFullPivHouseholderQr(Sy.solveFullPivHouseholderQr(Pxy.transpose()))
566            .transpose();
567
568    // Compute the posterior state mean
569    //
570    // x̂ = x̂⁻ + K(y − ŷ⁻)
571    //
572    // second part of equation (27)
573    m_xHat = addFuncX.apply(m_xHat, K.times(residualFuncY.apply(y, yHat)));
574
575    // Compute the intermediate matrix U for downdating
576    // the square-root covariance
577    //
578    // equation (28)
579    Matrix<States, R> U = K.times(Sy);
580
581    // Downdate the posterior square-root state covariance
582    //
583    // equation (29)
584    for (int i = 0; i < rows.getNum(); i++) {
585      m_S.rankUpdate(U.extractColumnVector(i), -1, true);
586    }
587  }
588}