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}