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