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 public final double maxVelocity; 055 056 public final double maxAcceleration; 057 058 /** 059 * Construct constraints for a TrapezoidProfile. 060 * 061 * @param maxVelocity maximum velocity 062 * @param maxAcceleration maximum acceleration 063 */ 064 public Constraints(double maxVelocity, double maxAcceleration) { 065 this.maxVelocity = maxVelocity; 066 this.maxAcceleration = maxAcceleration; 067 MathSharedStore.reportUsage(MathUsageId.kTrajectory_TrapezoidProfile, 1); 068 } 069 } 070 071 public static class State { 072 public double position; 073 074 public double velocity; 075 076 public State() {} 077 078 public State(double position, double velocity) { 079 this.position = position; 080 this.velocity = velocity; 081 } 082 083 @Override 084 public boolean equals(Object other) { 085 if (other instanceof State) { 086 State rhs = (State) other; 087 return this.position == rhs.position && this.velocity == rhs.velocity; 088 } else { 089 return false; 090 } 091 } 092 093 @Override 094 public int hashCode() { 095 return Objects.hash(position, velocity); 096 } 097 } 098 099 /** 100 * Construct a TrapezoidProfile. 101 * 102 * @param constraints The constraints on the profile, like maximum velocity. 103 * @param goal The desired state when the profile is complete. 104 * @param initial The initial state (usually the current state). 105 */ 106 public TrapezoidProfile(Constraints constraints, State goal, State initial) { 107 m_direction = shouldFlipAcceleration(initial, goal) ? -1 : 1; 108 m_constraints = constraints; 109 m_initial = direct(initial); 110 m_goal = direct(goal); 111 112 if (m_initial.velocity > m_constraints.maxVelocity) { 113 m_initial.velocity = m_constraints.maxVelocity; 114 } 115 116 // Deal with a possibly truncated motion profile (with nonzero initial or 117 // final velocity) by calculating the parameters as if the profile began and 118 // ended at zero velocity 119 double cutoffBegin = m_initial.velocity / m_constraints.maxAcceleration; 120 double cutoffDistBegin = cutoffBegin * cutoffBegin * m_constraints.maxAcceleration / 2.0; 121 122 double cutoffEnd = m_goal.velocity / m_constraints.maxAcceleration; 123 double cutoffDistEnd = cutoffEnd * cutoffEnd * m_constraints.maxAcceleration / 2.0; 124 125 // Now we can calculate the parameters as if it was a full trapezoid instead 126 // of a truncated one 127 128 double fullTrapezoidDist = 129 cutoffDistBegin + (m_goal.position - m_initial.position) + cutoffDistEnd; 130 double accelerationTime = m_constraints.maxVelocity / m_constraints.maxAcceleration; 131 132 double fullSpeedDist = 133 fullTrapezoidDist - accelerationTime * accelerationTime * m_constraints.maxAcceleration; 134 135 // Handle the case where the profile never reaches full speed 136 if (fullSpeedDist < 0) { 137 accelerationTime = Math.sqrt(fullTrapezoidDist / m_constraints.maxAcceleration); 138 fullSpeedDist = 0; 139 } 140 141 m_endAccel = accelerationTime - cutoffBegin; 142 m_endFullSpeed = m_endAccel + fullSpeedDist / m_constraints.maxVelocity; 143 m_endDeccel = m_endFullSpeed + accelerationTime - cutoffEnd; 144 } 145 146 /** 147 * Construct a TrapezoidProfile. 148 * 149 * @param constraints The constraints on the profile, like maximum velocity. 150 * @param goal The desired state when the profile is complete. 151 */ 152 public TrapezoidProfile(Constraints constraints, State goal) { 153 this(constraints, goal, new State(0, 0)); 154 } 155 156 /** 157 * Calculate the correct position and velocity for the profile at a time t where the beginning of 158 * the profile was at time t = 0. 159 * 160 * @param t The time since the beginning of the profile. 161 * @return The position and velocity of the profile at time t. 162 */ 163 public State calculate(double t) { 164 State result = new State(m_initial.position, m_initial.velocity); 165 166 if (t < m_endAccel) { 167 result.velocity += t * m_constraints.maxAcceleration; 168 result.position += (m_initial.velocity + t * m_constraints.maxAcceleration / 2.0) * t; 169 } else if (t < m_endFullSpeed) { 170 result.velocity = m_constraints.maxVelocity; 171 result.position += 172 (m_initial.velocity + m_endAccel * m_constraints.maxAcceleration / 2.0) * m_endAccel 173 + m_constraints.maxVelocity * (t - m_endAccel); 174 } else if (t <= m_endDeccel) { 175 result.velocity = m_goal.velocity + (m_endDeccel - t) * m_constraints.maxAcceleration; 176 double timeLeft = m_endDeccel - t; 177 result.position = 178 m_goal.position 179 - (m_goal.velocity + timeLeft * m_constraints.maxAcceleration / 2.0) * timeLeft; 180 } else { 181 result = m_goal; 182 } 183 184 return direct(result); 185 } 186 187 /** 188 * Returns the time left until a target distance in the profile is reached. 189 * 190 * @param target The target distance. 191 * @return The time left until a target distance in the profile is reached. 192 */ 193 public double timeLeftUntil(double target) { 194 double position = m_initial.position * m_direction; 195 double velocity = m_initial.velocity * m_direction; 196 197 double endAccel = m_endAccel * m_direction; 198 double endFullSpeed = m_endFullSpeed * m_direction - endAccel; 199 200 if (target < position) { 201 endAccel = -endAccel; 202 endFullSpeed = -endFullSpeed; 203 velocity = -velocity; 204 } 205 206 endAccel = Math.max(endAccel, 0); 207 endFullSpeed = Math.max(endFullSpeed, 0); 208 209 final double acceleration = m_constraints.maxAcceleration; 210 final double decceleration = -m_constraints.maxAcceleration; 211 212 double distToTarget = Math.abs(target - position); 213 if (distToTarget < 1e-6) { 214 return 0; 215 } 216 217 double accelDist = velocity * endAccel + 0.5 * acceleration * endAccel * endAccel; 218 219 double deccelVelocity; 220 if (endAccel > 0) { 221 deccelVelocity = Math.sqrt(Math.abs(velocity * velocity + 2 * acceleration * accelDist)); 222 } else { 223 deccelVelocity = velocity; 224 } 225 226 double fullSpeedDist = m_constraints.maxVelocity * endFullSpeed; 227 double deccelDist; 228 229 if (accelDist > distToTarget) { 230 accelDist = distToTarget; 231 fullSpeedDist = 0; 232 deccelDist = 0; 233 } else if (accelDist + fullSpeedDist > distToTarget) { 234 fullSpeedDist = distToTarget - accelDist; 235 deccelDist = 0; 236 } else { 237 deccelDist = distToTarget - fullSpeedDist - accelDist; 238 } 239 240 double accelTime = 241 (-velocity + Math.sqrt(Math.abs(velocity * velocity + 2 * acceleration * accelDist))) 242 / acceleration; 243 244 double deccelTime = 245 (-deccelVelocity 246 + Math.sqrt( 247 Math.abs(deccelVelocity * deccelVelocity + 2 * decceleration * deccelDist))) 248 / decceleration; 249 250 double fullSpeedTime = fullSpeedDist / m_constraints.maxVelocity; 251 252 return accelTime + fullSpeedTime + deccelTime; 253 } 254 255 /** 256 * Returns the total time the profile takes to reach the goal. 257 * 258 * @return The total time the profile takes to reach the goal. 259 */ 260 public double totalTime() { 261 return m_endDeccel; 262 } 263 264 /** 265 * Returns true if the profile has reached the goal. 266 * 267 * <p>The profile has reached the goal if the time since the profile started has exceeded the 268 * profile's total time. 269 * 270 * @param t The time since the beginning of the profile. 271 * @return True if the profile has reached the goal. 272 */ 273 public boolean isFinished(double t) { 274 return t >= totalTime(); 275 } 276 277 /** 278 * Returns true if the profile inverted. 279 * 280 * <p>The profile is inverted if goal position is less than the initial position. 281 * 282 * @param initial The initial state (usually the current state). 283 * @param goal The desired state when the profile is complete. 284 */ 285 private static boolean shouldFlipAcceleration(State initial, State goal) { 286 return initial.position > goal.position; 287 } 288 289 // Flip the sign of the velocity and position if the profile is inverted 290 private State direct(State in) { 291 State result = new State(in.position, in.velocity); 292 result.position = result.position * m_direction; 293 result.velocity = result.velocity * m_direction; 294 return result; 295 } 296}