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.trajectory; 006 007import edu.wpi.first.math.MathSharedStore; 008import edu.wpi.first.math.MathUsageId; 009import java.util.Objects; 010 011/** 012 * A trapezoid-shaped velocity profile. 013 * 014 * <p>While this class can be used for a profiled movement from start to finish, the intended usage 015 * is to filter a reference's dynamics based on trapezoidal velocity constraints. To compute the 016 * reference obeying this constraint, do the following. 017 * 018 * <p>Initialization: 019 * 020 * <pre><code> 021 * TrapezoidProfile.Constraints constraints = 022 * new TrapezoidProfile.Constraints(kMaxV, kMaxA); 023 * TrapezoidProfile.State previousProfiledReference = 024 * new TrapezoidProfile.State(initialReference, 0.0); 025 * </code></pre> 026 * 027 * <p>Run on update: 028 * 029 * <pre><code> 030 * TrapezoidProfile profile = 031 * new TrapezoidProfile(constraints, unprofiledReference, previousProfiledReference); 032 * previousProfiledReference = profile.calculate(timeSincePreviousUpdate); 033 * </code></pre> 034 * 035 * <p>where `unprofiledReference` is free to change between calls. Note that when the unprofiled 036 * reference is within the constraints, `calculate()` returns the unprofiled reference unchanged. 037 * 038 * <p>Otherwise, a timer can be started to provide monotonic values for `calculate()` and to 039 * determine when the profile has completed via `isFinished()`. 040 */ 041public class TrapezoidProfile { 042 // The direction of the profile, either 1 for forwards or -1 for inverted 043 private int m_direction; 044 045 private Constraints m_constraints; 046 private State m_initial; 047 private State m_goal; 048 049 private double m_endAccel; 050 private double m_endFullSpeed; 051 private double m_endDeccel; 052 053 public static class Constraints { 054 @SuppressWarnings("MemberName") 055 public final double maxVelocity; 056 057 @SuppressWarnings("MemberName") 058 public final double maxAcceleration; 059 060 /** 061 * Construct constraints for a TrapezoidProfile. 062 * 063 * @param maxVelocity maximum velocity 064 * @param maxAcceleration maximum acceleration 065 */ 066 public Constraints(double maxVelocity, double maxAcceleration) { 067 this.maxVelocity = maxVelocity; 068 this.maxAcceleration = maxAcceleration; 069 MathSharedStore.reportUsage(MathUsageId.kTrajectory_TrapezoidProfile, 1); 070 } 071 } 072 073 public static class State { 074 @SuppressWarnings("MemberName") 075 public double position; 076 077 @SuppressWarnings("MemberName") 078 public double velocity; 079 080 public State() {} 081 082 public State(double position, double velocity) { 083 this.position = position; 084 this.velocity = velocity; 085 } 086 087 @Override 088 public boolean equals(Object other) { 089 if (other instanceof State) { 090 State rhs = (State) other; 091 return this.position == rhs.position && this.velocity == rhs.velocity; 092 } else { 093 return false; 094 } 095 } 096 097 @Override 098 public int hashCode() { 099 return Objects.hash(position, velocity); 100 } 101 } 102 103 /** 104 * Construct a TrapezoidProfile. 105 * 106 * @param constraints The constraints on the profile, like maximum velocity. 107 * @param goal The desired state when the profile is complete. 108 * @param initial The initial state (usually the current state). 109 */ 110 public TrapezoidProfile(Constraints constraints, State goal, State initial) { 111 m_direction = shouldFlipAcceleration(initial, goal) ? -1 : 1; 112 m_constraints = constraints; 113 m_initial = direct(initial); 114 m_goal = direct(goal); 115 116 if (m_initial.velocity > m_constraints.maxVelocity) { 117 m_initial.velocity = m_constraints.maxVelocity; 118 } 119 120 // Deal with a possibly truncated motion profile (with nonzero initial or 121 // final velocity) by calculating the parameters as if the profile began and 122 // ended at zero velocity 123 double cutoffBegin = m_initial.velocity / m_constraints.maxAcceleration; 124 double cutoffDistBegin = cutoffBegin * cutoffBegin * m_constraints.maxAcceleration / 2.0; 125 126 double cutoffEnd = m_goal.velocity / m_constraints.maxAcceleration; 127 double cutoffDistEnd = cutoffEnd * cutoffEnd * m_constraints.maxAcceleration / 2.0; 128 129 // Now we can calculate the parameters as if it was a full trapezoid instead 130 // of a truncated one 131 132 double fullTrapezoidDist = 133 cutoffDistBegin + (m_goal.position - m_initial.position) + cutoffDistEnd; 134 double accelerationTime = m_constraints.maxVelocity / m_constraints.maxAcceleration; 135 136 double fullSpeedDist = 137 fullTrapezoidDist - accelerationTime * accelerationTime * m_constraints.maxAcceleration; 138 139 // Handle the case where the profile never reaches full speed 140 if (fullSpeedDist < 0) { 141 accelerationTime = Math.sqrt(fullTrapezoidDist / m_constraints.maxAcceleration); 142 fullSpeedDist = 0; 143 } 144 145 m_endAccel = accelerationTime - cutoffBegin; 146 m_endFullSpeed = m_endAccel + fullSpeedDist / m_constraints.maxVelocity; 147 m_endDeccel = m_endFullSpeed + accelerationTime - cutoffEnd; 148 } 149 150 /** 151 * Construct a TrapezoidProfile. 152 * 153 * @param constraints The constraints on the profile, like maximum velocity. 154 * @param goal The desired state when the profile is complete. 155 */ 156 public TrapezoidProfile(Constraints constraints, State goal) { 157 this(constraints, goal, new State(0, 0)); 158 } 159 160 /** 161 * Calculate the correct position and velocity for the profile at a time t where the beginning of 162 * the profile was at time t = 0. 163 * 164 * @param t The time since the beginning of the profile. 165 * @return The position and velocity of the profile at time t. 166 */ 167 public State calculate(double t) { 168 State result = new State(m_initial.position, m_initial.velocity); 169 170 if (t < m_endAccel) { 171 result.velocity += t * m_constraints.maxAcceleration; 172 result.position += (m_initial.velocity + t * m_constraints.maxAcceleration / 2.0) * t; 173 } else if (t < m_endFullSpeed) { 174 result.velocity = m_constraints.maxVelocity; 175 result.position += 176 (m_initial.velocity + m_endAccel * m_constraints.maxAcceleration / 2.0) * m_endAccel 177 + m_constraints.maxVelocity * (t - m_endAccel); 178 } else if (t <= m_endDeccel) { 179 result.velocity = m_goal.velocity + (m_endDeccel - t) * m_constraints.maxAcceleration; 180 double timeLeft = m_endDeccel - t; 181 result.position = 182 m_goal.position 183 - (m_goal.velocity + timeLeft * m_constraints.maxAcceleration / 2.0) * timeLeft; 184 } else { 185 result = m_goal; 186 } 187 188 return direct(result); 189 } 190 191 /** 192 * Returns the time left until a target distance in the profile is reached. 193 * 194 * @param target The target distance. 195 * @return The time left until a target distance in the profile is reached. 196 */ 197 public double timeLeftUntil(double target) { 198 double position = m_initial.position * m_direction; 199 double velocity = m_initial.velocity * m_direction; 200 201 double endAccel = m_endAccel * m_direction; 202 double endFullSpeed = m_endFullSpeed * m_direction - endAccel; 203 204 if (target < position) { 205 endAccel = -endAccel; 206 endFullSpeed = -endFullSpeed; 207 velocity = -velocity; 208 } 209 210 endAccel = Math.max(endAccel, 0); 211 endFullSpeed = Math.max(endFullSpeed, 0); 212 213 final double acceleration = m_constraints.maxAcceleration; 214 final double decceleration = -m_constraints.maxAcceleration; 215 216 double distToTarget = Math.abs(target - position); 217 if (distToTarget < 1e-6) { 218 return 0; 219 } 220 221 double accelDist = velocity * endAccel + 0.5 * acceleration * endAccel * endAccel; 222 223 double deccelVelocity; 224 if (endAccel > 0) { 225 deccelVelocity = Math.sqrt(Math.abs(velocity * velocity + 2 * acceleration * accelDist)); 226 } else { 227 deccelVelocity = velocity; 228 } 229 230 double fullSpeedDist = m_constraints.maxVelocity * endFullSpeed; 231 double deccelDist; 232 233 if (accelDist > distToTarget) { 234 accelDist = distToTarget; 235 fullSpeedDist = 0; 236 deccelDist = 0; 237 } else if (accelDist + fullSpeedDist > distToTarget) { 238 fullSpeedDist = distToTarget - accelDist; 239 deccelDist = 0; 240 } else { 241 deccelDist = distToTarget - fullSpeedDist - accelDist; 242 } 243 244 double accelTime = 245 (-velocity + Math.sqrt(Math.abs(velocity * velocity + 2 * acceleration * accelDist))) 246 / acceleration; 247 248 double deccelTime = 249 (-deccelVelocity 250 + Math.sqrt( 251 Math.abs(deccelVelocity * deccelVelocity + 2 * decceleration * deccelDist))) 252 / decceleration; 253 254 double fullSpeedTime = fullSpeedDist / m_constraints.maxVelocity; 255 256 return accelTime + fullSpeedTime + deccelTime; 257 } 258 259 /** 260 * Returns the total time the profile takes to reach the goal. 261 * 262 * @return The total time the profile takes to reach the goal. 263 */ 264 public double totalTime() { 265 return m_endDeccel; 266 } 267 268 /** 269 * Returns true if the profile has reached the goal. 270 * 271 * <p>The profile has reached the goal if the time since the profile started has exceeded the 272 * profile's total time. 273 * 274 * @param t The time since the beginning of the profile. 275 * @return True if the profile has reached the goal. 276 */ 277 public boolean isFinished(double t) { 278 return t >= totalTime(); 279 } 280 281 /** 282 * Returns true if the profile inverted. 283 * 284 * <p>The profile is inverted if goal position is less than the initial position. 285 * 286 * @param initial The initial state (usually the current state). 287 * @param goal The desired state when the profile is complete. 288 */ 289 private static boolean shouldFlipAcceleration(State initial, State goal) { 290 return initial.position > goal.position; 291 } 292 293 // Flip the sign of the velocity and position if the profile is inverted 294 private State direct(State in) { 295 State result = new State(in.position, in.velocity); 296 result.position = result.position * m_direction; 297 result.velocity = result.velocity * m_direction; 298 return result; 299 } 300}