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.controller;
006
007import edu.wpi.first.math.DARE;
008import edu.wpi.first.math.InterpolatingMatrixTreeMap;
009import edu.wpi.first.math.MatBuilder;
010import edu.wpi.first.math.Matrix;
011import edu.wpi.first.math.Nat;
012import edu.wpi.first.math.StateSpaceUtil;
013import edu.wpi.first.math.VecBuilder;
014import edu.wpi.first.math.Vector;
015import edu.wpi.first.math.geometry.Pose2d;
016import edu.wpi.first.math.kinematics.ChassisSpeeds;
017import edu.wpi.first.math.numbers.N2;
018import edu.wpi.first.math.numbers.N3;
019import edu.wpi.first.math.system.Discretization;
020import edu.wpi.first.math.trajectory.Trajectory;
021
022/**
023 * The linear time-varying unicycle controller has a similar form to the LQR, but the model used to
024 * compute the controller gain is the nonlinear unicycle model linearized around the drivetrain's
025 * current state.
026 *
027 * <p>This controller is a roughly drop-in replacement for {@link RamseteController} with more
028 * optimal feedback gains in the "least-squares error" sense.
029 *
030 * <p>See section 8.9 in Controls Engineering in FRC for a derivation of the control law we used
031 * shown in theorem 8.9.1.
032 */
033public class LTVUnicycleController {
034  // LUT from drivetrain linear velocity to LQR gain
035  private final InterpolatingMatrixTreeMap<Double, N2, N3> m_table =
036      new InterpolatingMatrixTreeMap<>();
037
038  private Pose2d m_poseError;
039  private Pose2d m_poseTolerance;
040  private boolean m_enabled = true;
041
042  /** States of the drivetrain system. */
043  private enum State {
044    kX(0),
045    kY(1),
046    kHeading(2);
047
048    public final int value;
049
050    State(int i) {
051      this.value = i;
052    }
053  }
054
055  /**
056   * Constructs a linear time-varying unicycle controller with default maximum desired error
057   * tolerances of (0.0625 m, 0.125 m, 2 rad) and default maximum desired control effort of (1 m/s,
058   * 2 rad/s).
059   *
060   * @param dt Discretization timestep in seconds.
061   */
062  public LTVUnicycleController(double dt) {
063    this(VecBuilder.fill(0.0625, 0.125, 2.0), VecBuilder.fill(1.0, 2.0), dt, 9.0);
064  }
065
066  /**
067   * Constructs a linear time-varying unicycle controller with default maximum desired error
068   * tolerances of (0.0625 m, 0.125 m, 2 rad) and default maximum desired control effort of (1 m/s,
069   * 2 rad/s).
070   *
071   * @param dt Discretization timestep in seconds.
072   * @param maxVelocity The maximum velocity in meters per second for the controller gain lookup
073   *     table. The default is 9 m/s.
074   * @throws IllegalArgumentException if maxVelocity &lt;= 0.
075   */
076  public LTVUnicycleController(double dt, double maxVelocity) {
077    this(VecBuilder.fill(0.0625, 0.125, 2.0), VecBuilder.fill(1.0, 2.0), dt, maxVelocity);
078  }
079
080  /**
081   * Constructs a linear time-varying unicycle controller.
082   *
083   * <p>See <a
084   * href="https://docs.wpilib.org/en/stable/docs/software/advanced-controls/state-space/state-space-intro.html#lqr-tuning">https://docs.wpilib.org/en/stable/docs/software/advanced-controls/state-space/state-space-intro.html#lqr-tuning</a>
085   * for how to select the tolerances.
086   *
087   * @param qelems The maximum desired error tolerance for each state.
088   * @param relems The maximum desired control effort for each input.
089   * @param dt Discretization timestep in seconds.
090   */
091  public LTVUnicycleController(Vector<N3> qelems, Vector<N2> relems, double dt) {
092    this(qelems, relems, dt, 9.0);
093  }
094
095  /**
096   * Constructs a linear time-varying unicycle controller.
097   *
098   * <p>See <a
099   * href="https://docs.wpilib.org/en/stable/docs/software/advanced-controls/state-space/state-space-intro.html#lqr-tuning">https://docs.wpilib.org/en/stable/docs/software/advanced-controls/state-space/state-space-intro.html#lqr-tuning</a>
100   * for how to select the tolerances.
101   *
102   * @param qelems The maximum desired error tolerance for each state.
103   * @param relems The maximum desired control effort for each input.
104   * @param dt Discretization timestep in seconds.
105   * @param maxVelocity The maximum velocity in meters per second for the controller gain lookup
106   *     table. The default is 9 m/s.
107   * @throws IllegalArgumentException if maxVelocity &lt;= 0 m/s or &gt;= 15 m/s.
108   */
109  public LTVUnicycleController(
110      Vector<N3> qelems, Vector<N2> relems, double dt, double maxVelocity) {
111    if (maxVelocity <= 0.0) {
112      throw new IllegalArgumentException("Max velocity must be greater than 0 m/s.");
113    }
114    if (maxVelocity >= 15.0) {
115      throw new IllegalArgumentException("Max velocity must be less than 15 m/s.");
116    }
117
118    // The change in global pose for a unicycle is defined by the following
119    // three equations.
120    //
121    // ẋ = v cosθ
122    // ẏ = v sinθ
123    // θ̇ = ω
124    //
125    // Here's the model as a vector function where x = [x  y  θ]ᵀ and
126    // u = [v  ω]ᵀ.
127    //
128    //           [v cosθ]
129    // f(x, u) = [v sinθ]
130    //           [  ω   ]
131    //
132    // To create an LQR, we need to linearize this.
133    //
134    //               [0  0  −v sinθ]                  [cosθ  0]
135    // ∂f(x, u)/∂x = [0  0   v cosθ]    ∂f(x, u)/∂u = [sinθ  0]
136    //               [0  0     0   ]                  [ 0    1]
137    //
138    // We're going to make a cross-track error controller, so we'll apply a
139    // clockwise rotation matrix to the global tracking error to transform it
140    // into the robot's coordinate frame. Since the cross-track error is always
141    // measured from the robot's coordinate frame, the model used to compute the
142    // LQR should be linearized around θ = 0 at all times.
143    //
144    //     [0  0  −v sin0]        [cos0  0]
145    // A = [0  0   v cos0]    B = [sin0  0]
146    //     [0  0     0   ]        [ 0    1]
147    //
148    //     [0  0  0]              [1  0]
149    // A = [0  0  v]          B = [0  0]
150    //     [0  0  0]              [0  1]
151    var A = new Matrix<>(Nat.N3(), Nat.N3());
152    var B = MatBuilder.fill(Nat.N3(), Nat.N2(), 1.0, 0.0, 0.0, 0.0, 0.0, 1.0);
153    var Q = StateSpaceUtil.makeCostMatrix(qelems);
154    var R = StateSpaceUtil.makeCostMatrix(relems);
155
156    for (double velocity = -maxVelocity; velocity < maxVelocity; velocity += 0.01) {
157      // The DARE is ill-conditioned if the velocity is close to zero, so don't
158      // let the system stop.
159      if (Math.abs(velocity) < 1e-4) {
160        A.set(State.kY.value, State.kHeading.value, 1e-4);
161      } else {
162        A.set(State.kY.value, State.kHeading.value, velocity);
163      }
164
165      var discABPair = Discretization.discretizeAB(A, B, dt);
166      var discA = discABPair.getFirst();
167      var discB = discABPair.getSecond();
168
169      var S = DARE.dareDetail(discA, discB, Q, R);
170
171      // K = (BᵀSB + R)⁻¹BᵀSA
172      m_table.put(
173          velocity,
174          discB
175              .transpose()
176              .times(S)
177              .times(discB)
178              .plus(R)
179              .solve(discB.transpose().times(S).times(discA)));
180    }
181  }
182
183  /**
184   * Returns true if the pose error is within tolerance of the reference.
185   *
186   * @return True if the pose error is within tolerance of the reference.
187   */
188  public boolean atReference() {
189    final var eTranslate = m_poseError.getTranslation();
190    final var eRotate = m_poseError.getRotation();
191    final var tolTranslate = m_poseTolerance.getTranslation();
192    final var tolRotate = m_poseTolerance.getRotation();
193    return Math.abs(eTranslate.getX()) < tolTranslate.getX()
194        && Math.abs(eTranslate.getY()) < tolTranslate.getY()
195        && Math.abs(eRotate.getRadians()) < tolRotate.getRadians();
196  }
197
198  /**
199   * Sets the pose error which is considered tolerable for use with atReference().
200   *
201   * @param poseTolerance Pose error which is tolerable.
202   */
203  public void setTolerance(Pose2d poseTolerance) {
204    m_poseTolerance = poseTolerance;
205  }
206
207  /**
208   * Returns the linear and angular velocity outputs of the LTV controller.
209   *
210   * <p>The reference pose, linear velocity, and angular velocity should come from a drivetrain
211   * trajectory.
212   *
213   * @param currentPose The current pose.
214   * @param poseRef The desired pose.
215   * @param linearVelocityRef The desired linear velocity in meters per second.
216   * @param angularVelocityRef The desired angular velocity in radians per second.
217   * @return The linear and angular velocity outputs of the LTV controller.
218   */
219  public ChassisSpeeds calculate(
220      Pose2d currentPose, Pose2d poseRef, double linearVelocityRef, double angularVelocityRef) {
221    if (!m_enabled) {
222      return new ChassisSpeeds(linearVelocityRef, 0.0, angularVelocityRef);
223    }
224
225    m_poseError = poseRef.relativeTo(currentPose);
226
227    var K = m_table.get(linearVelocityRef);
228    var e =
229        MatBuilder.fill(
230            Nat.N3(),
231            Nat.N1(),
232            m_poseError.getX(),
233            m_poseError.getY(),
234            m_poseError.getRotation().getRadians());
235    var u = K.times(e);
236
237    return new ChassisSpeeds(
238        linearVelocityRef + u.get(0, 0), 0.0, angularVelocityRef + u.get(1, 0));
239  }
240
241  /**
242   * Returns the next output of the LTV controller.
243   *
244   * <p>The reference pose, linear velocity, and angular velocity should come from a drivetrain
245   * trajectory.
246   *
247   * @param currentPose The current pose.
248   * @param desiredState The desired pose, linear velocity, and angular velocity from a trajectory.
249   * @return The linear and angular velocity outputs of the LTV controller.
250   */
251  public ChassisSpeeds calculate(Pose2d currentPose, Trajectory.State desiredState) {
252    return calculate(
253        currentPose,
254        desiredState.poseMeters,
255        desiredState.velocityMetersPerSecond,
256        desiredState.velocityMetersPerSecond * desiredState.curvatureRadPerMeter);
257  }
258
259  /**
260   * Enables and disables the controller for troubleshooting purposes.
261   *
262   * @param enabled If the controller is enabled or not.
263   */
264  public void setEnabled(boolean enabled) {
265    m_enabled = enabled;
266  }
267}