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