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.DARE;
008import edu.wpi.first.math.MathSharedStore;
009import edu.wpi.first.math.MathUsageId;
010import edu.wpi.first.math.Matrix;
011import edu.wpi.first.math.Nat;
012import edu.wpi.first.math.Num;
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.LinearSystem;
017
018/**
019 * A Kalman filter combines predictions from a model and measurements to give an estimate of the
020 * true system state. This is useful because many states cannot be measured directly as a result of
021 * sensor noise, or because the state is "hidden".
022 *
023 * <p>Kalman filters use a K gain matrix to determine whether to trust the model or measurements
024 * more. Kalman filter theory uses statistics to compute an optimal K gain which minimizes the sum
025 * of squares error in the state estimate. This K gain is used to correct the state estimate by some
026 * amount of the difference between the actual measurements and the measurements predicted by the
027 * model.
028 *
029 * <p>For more on the underlying math, read <a
030 * href="https://file.tavsys.net/control/controls-engineering-in-frc.pdf">https://file.tavsys.net/control/controls-engineering-in-frc.pdf</a>
031 * chapter 9 "Stochastic control theory".
032 *
033 * @param <States> Number of states.
034 * @param <Inputs> Number of inputs.
035 * @param <Outputs> Number of outputs.
036 */
037public class KalmanFilter<States extends Num, Inputs extends Num, Outputs extends Num>
038    implements KalmanTypeFilter<States, Inputs, Outputs> {
039  private final Nat<States> m_states;
040
041  private final LinearSystem<States, Inputs, Outputs> m_plant;
042  private Matrix<States, N1> m_xHat;
043  private Matrix<States, States> m_P;
044  private final Matrix<States, States> m_contQ;
045  private final Matrix<Outputs, Outputs> m_contR;
046  private double m_dtSeconds;
047
048  private final Matrix<States, States> m_initP;
049
050  /**
051   * Constructs a Kalman filter with the given plant.
052   *
053   * <p>See <a
054   * 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>
055   * for how to select the standard deviations.
056   *
057   * @param states A Nat representing the states of the system.
058   * @param outputs A Nat representing the outputs of the system.
059   * @param plant The plant used for the prediction step.
060   * @param stateStdDevs Standard deviations of model states.
061   * @param measurementStdDevs Standard deviations of measurements.
062   * @param dtSeconds Nominal discretization timestep.
063   * @throws IllegalArgumentException If the system is undetectable.
064   */
065  public KalmanFilter(
066      Nat<States> states,
067      Nat<Outputs> outputs,
068      LinearSystem<States, Inputs, Outputs> plant,
069      Matrix<States, N1> stateStdDevs,
070      Matrix<Outputs, N1> measurementStdDevs,
071      double dtSeconds) {
072    this.m_states = states;
073
074    this.m_plant = plant;
075
076    m_contQ = StateSpaceUtil.makeCovarianceMatrix(states, stateStdDevs);
077    m_contR = StateSpaceUtil.makeCovarianceMatrix(outputs, measurementStdDevs);
078    m_dtSeconds = dtSeconds;
079
080    // Find discrete A and Q
081    var pair = Discretization.discretizeAQ(plant.getA(), m_contQ, dtSeconds);
082    var discA = pair.getFirst();
083    var discQ = pair.getSecond();
084
085    var discR = Discretization.discretizeR(m_contR, dtSeconds);
086
087    var C = plant.getC();
088
089    m_initP = new Matrix<>(DARE.dare(discA.transpose(), C.transpose(), discQ, discR));
090
091    reset();
092
093    MathSharedStore.getMathShared().reportUsage(MathUsageId.kEstimator_KalmanFilter, 1);
094  }
095
096  /**
097   * Returns the error covariance matrix P.
098   *
099   * @return the error covariance matrix P.
100   */
101  @Override
102  public Matrix<States, States> getP() {
103    return m_P;
104  }
105
106  /**
107   * Returns an element of the error covariance matrix P.
108   *
109   * @param row Row of P.
110   * @param col Column of P.
111   * @return the value of the error covariance matrix P at (i, j).
112   */
113  @Override
114  public double getP(int row, int col) {
115    return m_P.get(row, col);
116  }
117
118  /**
119   * Sets the entire error covariance matrix P.
120   *
121   * @param newP The new value of P to use.
122   */
123  @Override
124  public void setP(Matrix<States, States> newP) {
125    m_P = newP;
126  }
127
128  /**
129   * Returns the state estimate x-hat.
130   *
131   * @return the state estimate x-hat.
132   */
133  @Override
134  public Matrix<States, N1> getXhat() {
135    return m_xHat;
136  }
137
138  /**
139   * Returns an element of the state estimate x-hat.
140   *
141   * @param row Row of x-hat.
142   * @return the value of the state estimate x-hat at that row.
143   */
144  @Override
145  public double getXhat(int row) {
146    return m_xHat.get(row, 0);
147  }
148
149  /**
150   * Set initial state estimate x-hat.
151   *
152   * @param xHat The state estimate x-hat.
153   */
154  @Override
155  public void setXhat(Matrix<States, N1> xHat) {
156    m_xHat = xHat;
157  }
158
159  /**
160   * Set an element of the initial state estimate x-hat.
161   *
162   * @param row Row of x-hat.
163   * @param value Value for element of x-hat.
164   */
165  @Override
166  public void setXhat(int row, double value) {
167    m_xHat.set(row, 0, value);
168  }
169
170  @Override
171  public final void reset() {
172    m_xHat = new Matrix<>(m_states, Nat.N1());
173    m_P = m_initP;
174  }
175
176  /**
177   * Project the model into the future with a new control input u.
178   *
179   * @param u New control input from controller.
180   * @param dtSeconds Timestep for prediction.
181   */
182  @Override
183  public void predict(Matrix<Inputs, N1> u, double dtSeconds) {
184    // Find discrete A and Q
185    final var discPair = Discretization.discretizeAQ(m_plant.getA(), m_contQ, dtSeconds);
186    final var discA = discPair.getFirst();
187    final var discQ = discPair.getSecond();
188
189    m_xHat = m_plant.calculateX(m_xHat, u, dtSeconds);
190
191    // Pₖ₊₁⁻ = APₖ⁻Aᵀ + Q
192    m_P = discA.times(m_P).times(discA.transpose()).plus(discQ);
193
194    m_dtSeconds = dtSeconds;
195  }
196
197  /**
198   * Correct the state estimate x-hat using the measurements in y.
199   *
200   * @param u Same control input used in the predict step.
201   * @param y Measurement vector.
202   */
203  @Override
204  public void correct(Matrix<Inputs, N1> u, Matrix<Outputs, N1> y) {
205    correct(u, y, m_contR);
206  }
207
208  /**
209   * Correct the state estimate x-hat using the measurements in y.
210   *
211   * <p>This is useful for when the measurement noise covariances vary.
212   *
213   * @param u Same control input used in the predict step.
214   * @param y Measurement vector.
215   * @param R Continuous measurement noise covariance matrix.
216   */
217  public void correct(Matrix<Inputs, N1> u, Matrix<Outputs, N1> y, Matrix<Outputs, Outputs> R) {
218    final var C = m_plant.getC();
219    final var D = m_plant.getD();
220
221    final var discR = Discretization.discretizeR(R, m_dtSeconds);
222
223    final var S = C.times(m_P).times(C.transpose()).plus(discR);
224
225    // We want to put K = PCᵀS⁻¹ into Ax = b form so we can solve it more
226    // efficiently.
227    //
228    // K = PCᵀS⁻¹
229    // KS = PCᵀ
230    // (KS)ᵀ = (PCᵀ)ᵀ
231    // SᵀKᵀ = CPᵀ
232    //
233    // The solution of Ax = b can be found via x = A.solve(b).
234    //
235    // Kᵀ = Sᵀ.solve(CPᵀ)
236    // K = (Sᵀ.solve(CPᵀ))ᵀ
237    //
238    // Drop the transposes on symmetric matrices S and P.
239    //
240    // K = (S.solve(CP))ᵀ
241    final Matrix<States, Outputs> K = S.solve(C.times(m_P)).transpose();
242
243    // x̂ₖ₊₁⁺ = x̂ₖ₊₁⁻ + K(y − (Cx̂ₖ₊₁⁻ + Duₖ₊₁))
244    m_xHat = m_xHat.plus(K.times(y.minus(C.times(m_xHat).plus(D.times(u)))));
245
246    // Pₖ₊₁⁺ = (I−Kₖ₊₁C)Pₖ₊₁⁻(I−Kₖ₊₁C)ᵀ + Kₖ₊₁RKₖ₊₁ᵀ
247    // Use Joseph form for numerical stability
248    m_P =
249        Matrix.eye(m_states)
250            .minus(K.times(C))
251            .times(m_P)
252            .times(Matrix.eye(m_states).minus(K.times(C)).transpose())
253            .plus(K.times(discR).times(K.transpose()));
254  }
255}