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