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 org.wpilib.math.system; 006 007import java.util.function.Function; 008import org.ejml.MatrixDimensionException; 009import org.ejml.simple.SimpleMatrix; 010import org.wpilib.math.controller.LinearPlantInversionFeedforward; 011import org.wpilib.math.controller.LinearQuadraticRegulator; 012import org.wpilib.math.estimator.KalmanFilter; 013import org.wpilib.math.linalg.Matrix; 014import org.wpilib.math.numbers.N1; 015import org.wpilib.math.util.MathSharedStore; 016import org.wpilib.math.util.Num; 017import org.wpilib.math.util.StateSpaceUtil; 018 019/** 020 * Combines a controller, feedforward, and observer for controlling a mechanism with full state 021 * feedback. 022 * 023 * <p>For everything in this file, "inputs" and "outputs" are defined from the perspective of the 024 * plant. This means U is an input and Y is an output (because you give the plant U (powers) and it 025 * gives you back a Y (sensor values)). This is the opposite of what they mean from the perspective 026 * of the controller (U is an output because that's what goes to the motors and Y is an input 027 * because that's what comes back from the sensors). 028 * 029 * <p>For more on the underlying math, read <a 030 * href="https://file.tavsys.net/control/controls-engineering-in-frc.pdf">https://file.tavsys.net/control/controls-engineering-in-frc.pdf</a>. 031 * 032 * @param <States> Number of states. 033 * @param <Inputs> Number of inputs. 034 * @param <Outputs> Number of outputs. 035 */ 036public class LinearSystemLoop<States extends Num, Inputs extends Num, Outputs extends Num> { 037 private final LinearQuadraticRegulator<States, Inputs, Outputs> m_controller; 038 private final LinearPlantInversionFeedforward<States, Inputs, Outputs> m_feedforward; 039 private final KalmanFilter<States, Inputs, Outputs> m_observer; 040 private Matrix<States, N1> m_nextR; 041 private Function<Matrix<Inputs, N1>, Matrix<Inputs, N1>> m_clampFunction; 042 043 /** 044 * Constructs a state-space loop with the given plant, controller, and observer. By default, the 045 * initial reference is all zeros. Users should call reset with the initial system state before 046 * enabling the loop. This constructor assumes that the input(s) to this system are voltage. 047 * 048 * @param plant State-space plant. 049 * @param controller State-space controller. 050 * @param observer State-space observer. 051 * @param maxVoltageVolts The maximum voltage that can be applied. Commonly 12. 052 * @param dt The nominal timestep in seconds. 053 */ 054 public LinearSystemLoop( 055 LinearSystem<States, Inputs, Outputs> plant, 056 LinearQuadraticRegulator<States, Inputs, Outputs> controller, 057 KalmanFilter<States, Inputs, Outputs> observer, 058 double maxVoltageVolts, 059 double dt) { 060 this( 061 controller, 062 new LinearPlantInversionFeedforward<>(plant, dt), 063 observer, 064 u -> StateSpaceUtil.desaturateInputVector(u, maxVoltageVolts)); 065 } 066 067 /** 068 * Constructs a state-space loop with the given plant, controller, and observer. By default, the 069 * initial reference is all zeros. Users should call reset with the initial system state before 070 * enabling the loop. 071 * 072 * @param plant State-space plant. 073 * @param controller State-space controller. 074 * @param observer State-space observer. 075 * @param clampFunction The function used to clamp the input U. 076 * @param dt The nominal timestep in seconds. 077 */ 078 public LinearSystemLoop( 079 LinearSystem<States, Inputs, Outputs> plant, 080 LinearQuadraticRegulator<States, Inputs, Outputs> controller, 081 KalmanFilter<States, Inputs, Outputs> observer, 082 Function<Matrix<Inputs, N1>, Matrix<Inputs, N1>> clampFunction, 083 double dt) { 084 this(controller, new LinearPlantInversionFeedforward<>(plant, dt), observer, clampFunction); 085 } 086 087 /** 088 * Constructs a state-space loop with the given controller, feedforward and observer. By default, 089 * the initial reference is all zeros. Users should call reset with the initial system state 090 * before enabling the loop. 091 * 092 * @param controller State-space controller. 093 * @param feedforward Plant inversion feedforward. 094 * @param observer State-space observer. 095 * @param maxVoltageVolts The maximum voltage that can be applied. Assumes that the inputs are 096 * voltages. 097 */ 098 public LinearSystemLoop( 099 LinearQuadraticRegulator<States, Inputs, Outputs> controller, 100 LinearPlantInversionFeedforward<States, Inputs, Outputs> feedforward, 101 KalmanFilter<States, Inputs, Outputs> observer, 102 double maxVoltageVolts) { 103 this( 104 controller, 105 feedforward, 106 observer, 107 u -> StateSpaceUtil.desaturateInputVector(u, maxVoltageVolts)); 108 } 109 110 /** 111 * Constructs a state-space loop with the given controller, feedforward, and observer. By default, 112 * the initial reference is all zeros. Users should call reset with the initial system state 113 * before enabling the loop. 114 * 115 * @param controller State-space controller. 116 * @param feedforward Plant inversion feedforward. 117 * @param observer State-space observer. 118 * @param clampFunction The function used to clamp the input U. 119 */ 120 public LinearSystemLoop( 121 LinearQuadraticRegulator<States, Inputs, Outputs> controller, 122 LinearPlantInversionFeedforward<States, Inputs, Outputs> feedforward, 123 KalmanFilter<States, Inputs, Outputs> observer, 124 Function<Matrix<Inputs, N1>, Matrix<Inputs, N1>> clampFunction) { 125 this.m_controller = controller; 126 this.m_feedforward = feedforward; 127 this.m_observer = observer; 128 this.m_clampFunction = clampFunction; 129 130 m_nextR = new Matrix<>(new SimpleMatrix(controller.getK().getNumCols(), 1)); 131 reset(m_nextR); 132 MathSharedStore.getMathShared().reportUsage("LinearSystemLoop", ""); 133 } 134 135 /** 136 * Returns the observer's state estimate x-hat. 137 * 138 * @return the observer's state estimate x-hat. 139 */ 140 public Matrix<States, N1> getXHat() { 141 return getObserver().getXhat(); 142 } 143 144 /** 145 * Returns an element of the observer's state estimate x-hat. 146 * 147 * @param row Row of x-hat. 148 * @return the i-th element of the observer's state estimate x-hat. 149 */ 150 public double getXHat(int row) { 151 return getObserver().getXhat(row); 152 } 153 154 /** 155 * Set the initial state estimate x-hat. 156 * 157 * @param xhat The initial state estimate x-hat. 158 */ 159 public void setXHat(Matrix<States, N1> xhat) { 160 getObserver().setXhat(xhat); 161 } 162 163 /** 164 * Set an element of the initial state estimate x-hat. 165 * 166 * @param row Row of x-hat. 167 * @param value Value for element of x-hat. 168 */ 169 public void setXHat(int row, double value) { 170 getObserver().setXhat(row, value); 171 } 172 173 /** 174 * Returns an element of the controller's next reference r. 175 * 176 * @param row Row of r. 177 * @return the element i of the controller's next reference r. 178 */ 179 public double getNextR(int row) { 180 return getNextR().get(row, 0); 181 } 182 183 /** 184 * Returns the controller's next reference r. 185 * 186 * @return the controller's next reference r. 187 */ 188 public Matrix<States, N1> getNextR() { 189 return m_nextR; 190 } 191 192 /** 193 * Set the next reference r. 194 * 195 * @param nextR Next reference. 196 */ 197 public void setNextR(Matrix<States, N1> nextR) { 198 m_nextR = nextR; 199 } 200 201 /** 202 * Set the next reference r. 203 * 204 * @param nextR Next reference. 205 */ 206 public void setNextR(double... nextR) { 207 if (nextR.length != m_nextR.getNumRows()) { 208 throw new MatrixDimensionException( 209 String.format( 210 "The next reference does not have the " 211 + "correct number of entries! Expected %s, but got %s.", 212 m_nextR.getNumRows(), nextR.length)); 213 } 214 m_nextR = new Matrix<>(new SimpleMatrix(m_nextR.getNumRows(), 1, true, nextR)); 215 } 216 217 /** 218 * Returns the controller's calculated control input u plus the calculated feedforward u_ff. 219 * 220 * @return the calculated control input u. 221 */ 222 public Matrix<Inputs, N1> getU() { 223 return clampInput(m_controller.getU().plus(m_feedforward.getUff())); 224 } 225 226 /** 227 * Returns an element of the controller's calculated control input u. 228 * 229 * @param row Row of u. 230 * @return the calculated control input u at the row i. 231 */ 232 public double getU(int row) { 233 return getU().get(row, 0); 234 } 235 236 /** 237 * Return the controller used internally. 238 * 239 * @return the controller used internally. 240 */ 241 public LinearQuadraticRegulator<States, Inputs, Outputs> getController() { 242 return m_controller; 243 } 244 245 /** 246 * Return the feedforward used internally. 247 * 248 * @return the feedforward used internally. 249 */ 250 public LinearPlantInversionFeedforward<States, Inputs, Outputs> getFeedforward() { 251 return m_feedforward; 252 } 253 254 /** 255 * Return the observer used internally. 256 * 257 * @return the observer used internally. 258 */ 259 public KalmanFilter<States, Inputs, Outputs> getObserver() { 260 return m_observer; 261 } 262 263 /** 264 * Zeroes reference r and controller output u. The previous reference of the 265 * PlantInversionFeedforward and the initial state estimate of the KalmanFilter are set to the 266 * initial state provided. 267 * 268 * @param initialState The initial state. 269 */ 270 public final void reset(Matrix<States, N1> initialState) { 271 m_nextR.fill(0.0); 272 m_controller.reset(); 273 m_feedforward.reset(initialState); 274 m_observer.setXhat(initialState); 275 } 276 277 /** 278 * Returns difference between reference r and current state x-hat. 279 * 280 * @return The state error matrix. 281 */ 282 public Matrix<States, N1> getError() { 283 return getController().getR().minus(m_observer.getXhat()); 284 } 285 286 /** 287 * Returns difference between reference r and current state x-hat. 288 * 289 * @param index The index of the error matrix to return. 290 * @return The error at that index. 291 */ 292 public double getError(int index) { 293 return getController().getR().minus(m_observer.getXhat()).get(index, 0); 294 } 295 296 /** 297 * Get the function used to clamp the input u. 298 * 299 * @return The clamping function. 300 */ 301 public Function<Matrix<Inputs, N1>, Matrix<Inputs, N1>> getClampFunction() { 302 return m_clampFunction; 303 } 304 305 /** 306 * Set the clamping function used to clamp inputs. 307 * 308 * @param clampFunction The clamping function. 309 */ 310 public void setClampFunction(Function<Matrix<Inputs, N1>, Matrix<Inputs, N1>> clampFunction) { 311 this.m_clampFunction = clampFunction; 312 } 313 314 /** 315 * Correct the state estimate x-hat using the measurements in y. 316 * 317 * @param y Measurement vector. 318 */ 319 public void correct(Matrix<Outputs, N1> y) { 320 getObserver().correct(getU(), y); 321 } 322 323 /** 324 * Sets new controller output, projects model forward, and runs observer prediction. 325 * 326 * <p>After calling this, the user should send the elements of u to the actuators. 327 * 328 * @param dt Timestep for model update in seconds. 329 */ 330 public void predict(double dt) { 331 var u = 332 clampInput( 333 m_controller 334 .calculate(getObserver().getXhat(), m_nextR) 335 .plus(m_feedforward.calculate(m_nextR))); 336 getObserver().predict(u, dt); 337 } 338 339 /** 340 * Clamp the input u to the min and max. 341 * 342 * @param unclampedU The input to clamp. 343 * @return The clamped input. 344 */ 345 public Matrix<Inputs, N1> clampInput(Matrix<Inputs, N1> unclampedU) { 346 return m_clampFunction.apply(unclampedU); 347 } 348}