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.MatBuilder;
009import edu.wpi.first.math.Matrix;
010import edu.wpi.first.math.Nat;
011import edu.wpi.first.math.StateSpaceUtil;
012import edu.wpi.first.math.VecBuilder;
013import edu.wpi.first.math.Vector;
014import edu.wpi.first.math.geometry.Pose2d;
015import edu.wpi.first.math.kinematics.ChassisSpeeds;
016import edu.wpi.first.math.numbers.N2;
017import edu.wpi.first.math.numbers.N3;
018import edu.wpi.first.math.system.Discretization;
019import edu.wpi.first.math.trajectory.Trajectory;
020
021/**
022 * The linear time-varying unicycle controller has a similar form to the LQR, but the model used to
023 * compute the controller gain is the nonlinear unicycle model linearized around the drivetrain's
024 * current state.
025 *
026 * <p>See section 8.9 in Controls Engineering in FRC for a derivation of the control law we used
027 * shown in theorem 8.9.1.
028 */
029public class LTVUnicycleController {
030  // LQR cost matrices
031  private Matrix<N3, N3> m_Q;
032  private Matrix<N2, N2> m_R;
033
034  private final double m_dt;
035
036  private Pose2d m_poseError;
037  private Pose2d m_poseTolerance;
038  private boolean m_enabled = true;
039
040  /**
041   * Constructs a linear time-varying unicycle controller with default maximum desired error
042   * tolerances of (x = 0.0625 m, y = 0.125 m, heading = 2 rad), default maximum desired control
043   * effort of (linear velocity = 1 m/s, angular velocity = 2 rad/s), and default maximum Velocity
044   * of 9 m/s.
045   *
046   * @param dt Discretization timestep in seconds.
047   */
048  public LTVUnicycleController(double dt) {
049    this(VecBuilder.fill(0.0625, 0.125, 2.0), VecBuilder.fill(1.0, 2.0), dt);
050  }
051
052  /**
053   * Constructs a linear time-varying unicycle controller.
054   *
055   * <p>See <a
056   * 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>
057   * for how to select the tolerances.
058   *
059   * <p>The default maximum Velocity is 9 m/s.
060   *
061   * @param qelems The maximum desired error tolerance for each state (x, y, heading).
062   * @param relems The maximum desired control effort for each input (linear velocity, angular
063   *     velocity).
064   * @param dt Discretization timestep in seconds.
065   */
066  public LTVUnicycleController(Vector<N3> qelems, Vector<N2> relems, double dt) {
067    m_Q = StateSpaceUtil.makeCostMatrix(qelems);
068    m_R = StateSpaceUtil.makeCostMatrix(relems);
069    m_dt = dt;
070  }
071
072  /**
073   * Returns true if the pose error is within tolerance of the reference.
074   *
075   * @return True if the pose error is within tolerance of the reference.
076   */
077  public boolean atReference() {
078    final var eTranslate = m_poseError.getTranslation();
079    final var eRotate = m_poseError.getRotation();
080    final var tolTranslate = m_poseTolerance.getTranslation();
081    final var tolRotate = m_poseTolerance.getRotation();
082    return Math.abs(eTranslate.getX()) < tolTranslate.getX()
083        && Math.abs(eTranslate.getY()) < tolTranslate.getY()
084        && Math.abs(eRotate.getRadians()) < tolRotate.getRadians();
085  }
086
087  /**
088   * Sets the pose error which is considered tolerable for use with atReference().
089   *
090   * @param poseTolerance Pose error which is tolerable.
091   */
092  public void setTolerance(Pose2d poseTolerance) {
093    m_poseTolerance = poseTolerance;
094  }
095
096  /**
097   * Returns the linear and angular velocity outputs of the LTV controller.
098   *
099   * <p>The reference pose, linear velocity, and angular velocity should come from a drivetrain
100   * trajectory.
101   *
102   * @param currentPose The current pose.
103   * @param poseRef The desired pose.
104   * @param linearVelocityRef The desired linear velocity in meters per second.
105   * @param angularVelocityRef The desired angular velocity in radians per second.
106   * @return The linear and angular velocity outputs of the LTV controller.
107   */
108  public ChassisSpeeds calculate(
109      Pose2d currentPose, Pose2d poseRef, double linearVelocityRef, double angularVelocityRef) {
110    // The change in global pose for a unicycle is defined by the following
111    // three equations.
112    //
113    // ẋ = v cosθ
114    // ẏ = v sinθ
115    // θ̇ = ω
116    //
117    // Here's the model as a vector function where x = [x  y  θ]ᵀ and
118    // u = [v  ω]ᵀ.
119    //
120    //           [v cosθ]
121    // f(x, u) = [v sinθ]
122    //           [  ω   ]
123    //
124    // To create an LQR, we need to linearize this.
125    //
126    //               [0  0  −v sinθ]                  [cosθ  0]
127    // ∂f(x, u)/∂x = [0  0   v cosθ]    ∂f(x, u)/∂u = [sinθ  0]
128    //               [0  0     0   ]                  [ 0    1]
129    //
130    // We're going to make a cross-track error controller, so we'll apply a
131    // clockwise rotation matrix to the global tracking error to transform it
132    // into the robot's coordinate frame. Since the cross-track error is always
133    // measured from the robot's coordinate frame, the model used to compute the
134    // LQR should be linearized around θ = 0 at all times.
135    //
136    //     [0  0  −v sin0]        [cos0  0]
137    // A = [0  0   v cos0]    B = [sin0  0]
138    //     [0  0     0   ]        [ 0    1]
139    //
140    //     [0  0  0]              [1  0]
141    // A = [0  0  v]          B = [0  0]
142    //     [0  0  0]              [0  1]
143
144    if (!m_enabled) {
145      return new ChassisSpeeds(linearVelocityRef, 0.0, angularVelocityRef);
146    }
147
148    // The DARE is ill-conditioned if the velocity is close to zero, so don't
149    // let the system stop.
150    if (Math.abs(linearVelocityRef) < 1e-4) {
151      linearVelocityRef = 1e-4;
152    }
153
154    m_poseError = poseRef.relativeTo(currentPose);
155
156    // spotless:off
157    var A = MatBuilder.fill(Nat.N3(), Nat.N3(),
158        0.0, 0.0, 0.0,
159        0.0, 0.0, linearVelocityRef,
160        0.0, 0.0, 0.0);
161    var B = MatBuilder.fill(Nat.N3(), Nat.N2(),
162        1.0, 0.0,
163        0.0, 0.0,
164        0.0, 1.0);
165    // spotless:on
166
167    var discABPair = Discretization.discretizeAB(A, B, m_dt);
168    var discA = discABPair.getFirst();
169    var discB = discABPair.getSecond();
170
171    var S = DARE.dareNoPrecond(discA, discB, m_Q, m_R);
172
173    // K = (BᵀSB + R)⁻¹BᵀSA
174    var K =
175        discB
176            .transpose()
177            .times(S)
178            .times(discB)
179            .plus(m_R)
180            .solve(discB.transpose().times(S).times(discA));
181
182    var e =
183        MatBuilder.fill(
184            Nat.N3(),
185            Nat.N1(),
186            m_poseError.getX(),
187            m_poseError.getY(),
188            m_poseError.getRotation().getRadians());
189    var u = K.times(e);
190
191    return new ChassisSpeeds(
192        linearVelocityRef + u.get(0, 0), 0.0, angularVelocityRef + u.get(1, 0));
193  }
194
195  /**
196   * Returns the next output of the LTV controller.
197   *
198   * <p>The reference pose, linear velocity, and angular velocity should come from a drivetrain
199   * trajectory.
200   *
201   * @param currentPose The current pose.
202   * @param desiredState The desired pose, linear velocity, and angular velocity from a trajectory.
203   * @return The linear and angular velocity outputs of the LTV controller.
204   */
205  public ChassisSpeeds calculate(Pose2d currentPose, Trajectory.State desiredState) {
206    return calculate(
207        currentPose,
208        desiredState.pose,
209        desiredState.velocity,
210        desiredState.velocity * desiredState.curvature);
211  }
212
213  /**
214   * Enables and disables the controller for troubleshooting purposes.
215   *
216   * @param enabled If the controller is enabled or not.
217   */
218  public void setEnabled(boolean enabled) {
219    m_enabled = enabled;
220  }
221}