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