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}