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