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 <= 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 <= 0 m/s or >= 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}