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