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