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