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>This class assumes predict() and correct() are called in pairs, so the Kalman gain converges
030 * to a steady-state value. If they aren't, use {@link KalmanFilter} instead.
031 *
032 * <p>For more on the underlying math, read <a
033 * href="https://file.tavsys.net/control/controls-engineering-in-frc.pdf">https://file.tavsys.net/control/controls-engineering-in-frc.pdf</a>
034 * chapter 9 "Stochastic control theory".
035 *
036 * @param <States> Number of states.
037 * @param <Inputs> Number of inputs.
038 * @param <Outputs> Number of outputs.
039 */
040public class SteadyStateKalmanFilter<States extends Num, Inputs extends Num, Outputs extends Num> {
041  private final Nat<States> m_states;
042
043  private final LinearSystem<States, Inputs, Outputs> m_plant;
044
045  /** The steady-state Kalman gain matrix. */
046  private final Matrix<States, Outputs> m_K;
047
048  /** The state estimate. */
049  private Matrix<States, N1> m_xHat;
050
051  /**
052   * Constructs a steady-state Kalman filter with the given plant.
053   *
054   * <p>See <a
055   * 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>
056   * for how to select the standard deviations.
057   *
058   * @param states A Nat representing the states of the system.
059   * @param outputs A Nat representing the outputs of the system.
060   * @param plant The plant used for the prediction step.
061   * @param stateStdDevs Standard deviations of model states.
062   * @param measurementStdDevs Standard deviations of measurements.
063   * @param dtSeconds Nominal discretization timestep.
064   * @throws IllegalArgumentException If the system is undetectable.
065   */
066  public SteadyStateKalmanFilter(
067      Nat<States> states,
068      Nat<Outputs> outputs,
069      LinearSystem<States, Inputs, Outputs> plant,
070      Matrix<States, N1> stateStdDevs,
071      Matrix<Outputs, N1> measurementStdDevs,
072      double dtSeconds) {
073    this.m_states = states;
074
075    this.m_plant = plant;
076
077    var contQ = StateSpaceUtil.makeCovarianceMatrix(states, stateStdDevs);
078    var contR = StateSpaceUtil.makeCovarianceMatrix(outputs, measurementStdDevs);
079
080    var pair = Discretization.discretizeAQ(plant.getA(), contQ, dtSeconds);
081    var discA = pair.getFirst();
082    var discQ = pair.getSecond();
083
084    var discR = Discretization.discretizeR(contR, dtSeconds);
085
086    var C = plant.getC();
087
088    var P = new Matrix<>(DARE.dare(discA.transpose(), C.transpose(), discQ, discR));
089
090    // S = CPCᵀ + R
091    var S = C.times(P).times(C.transpose()).plus(discR);
092
093    // We want to put K = PCᵀS⁻¹ into Ax = b form so we can solve it more
094    // efficiently.
095    //
096    // K = PCᵀS⁻¹
097    // KS = PCᵀ
098    // (KS)ᵀ = (PCᵀ)ᵀ
099    // SᵀKᵀ = CPᵀ
100    //
101    // The solution of Ax = b can be found via x = A.solve(b).
102    //
103    // Kᵀ = Sᵀ.solve(CPᵀ)
104    // K = (Sᵀ.solve(CPᵀ))ᵀ
105    //
106    // Drop the transposes on symmetric matrices S and P.
107    //
108    // K = (S.solve(CP))ᵀ
109    m_K = new Matrix<>(S.getStorage().solve(C.times(P).getStorage()).transpose());
110
111    reset();
112    MathSharedStore.getMathShared().reportUsage(MathUsageId.kEstimator_KalmanFilter, 4);
113  }
114
115  /** Resets the observer. */
116  public final void reset() {
117    m_xHat = new Matrix<>(m_states, Nat.N1());
118  }
119
120  /**
121   * Returns the steady-state Kalman gain matrix K.
122   *
123   * @return The steady-state Kalman gain matrix K.
124   */
125  public Matrix<States, Outputs> getK() {
126    return m_K;
127  }
128
129  /**
130   * Returns an element of the steady-state Kalman gain matrix K.
131   *
132   * @param row Row of K.
133   * @param col Column of K.
134   * @return the element (i, j) of the steady-state Kalman gain matrix K.
135   */
136  public double getK(int row, int col) {
137    return m_K.get(row, col);
138  }
139
140  /**
141   * Set initial state estimate x-hat.
142   *
143   * @param xhat The state estimate x-hat.
144   */
145  public void setXhat(Matrix<States, N1> xhat) {
146    this.m_xHat = xhat;
147  }
148
149  /**
150   * Set an element of the initial state estimate x-hat.
151   *
152   * @param row Row of x-hat.
153   * @param value Value for element of x-hat.
154   */
155  public void setXhat(int row, double value) {
156    m_xHat.set(row, 0, value);
157  }
158
159  /**
160   * Returns the state estimate x-hat.
161   *
162   * @return The state estimate x-hat.
163   */
164  public Matrix<States, N1> getXhat() {
165    return m_xHat;
166  }
167
168  /**
169   * Returns an element of the state estimate x-hat.
170   *
171   * @param row Row of x-hat.
172   * @return the state estimate x-hat at that row.
173   */
174  public double getXhat(int row) {
175    return m_xHat.get(row, 0);
176  }
177
178  /**
179   * Project the model into the future with a new control input u.
180   *
181   * @param u New control input from controller.
182   * @param dtSeconds Timestep for prediction.
183   */
184  public void predict(Matrix<Inputs, N1> u, double dtSeconds) {
185    this.m_xHat = m_plant.calculateX(m_xHat, u, dtSeconds);
186  }
187
188  /**
189   * Correct the state estimate x-hat using the measurements in y.
190   *
191   * @param u Same control input used in the last predict step.
192   * @param y Measurement vector.
193   */
194  public void correct(Matrix<Inputs, N1> u, Matrix<Outputs, N1> y) {
195    final var C = m_plant.getC();
196    final var D = m_plant.getD();
197
198    // x̂ₖ₊₁⁺ = x̂ₖ₊₁⁻ + K(y − (Cx̂ₖ₊₁⁻ + Duₖ₊₁))
199    m_xHat = m_xHat.plus(m_K.times(y.minus(C.times(m_xHat).plus(D.times(u)))));
200  }
201}