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