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