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