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}