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}