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