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}