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 com.fasterxml.jackson.annotation.JsonProperty;
008import edu.wpi.first.math.geometry.Pose2d;
009import edu.wpi.first.math.geometry.Transform2d;
010import edu.wpi.first.math.trajectory.proto.TrajectoryProto;
011import edu.wpi.first.math.trajectory.proto.TrajectoryStateProto;
012import edu.wpi.first.util.protobuf.ProtobufSerializable;
013import java.util.ArrayList;
014import java.util.List;
015import java.util.Objects;
016import java.util.stream.Collectors;
017
018/**
019 * Represents a time-parameterized trajectory. The trajectory contains of various States that
020 * represent the pose, curvature, time elapsed, velocity, and acceleration at that point.
021 */
022public class Trajectory implements ProtobufSerializable {
023  /** Trajectory protobuf for serialization. */
024  public static final TrajectoryProto proto = new TrajectoryProto();
025
026  private final double m_totalTimeSeconds;
027  private final List<State> m_states;
028
029  /** Constructs an empty trajectory. */
030  public Trajectory() {
031    m_states = new ArrayList<>();
032    m_totalTimeSeconds = 0.0;
033  }
034
035  /**
036   * Constructs a trajectory from a vector of states.
037   *
038   * @param states A vector of states.
039   * @throws IllegalArgumentException if the vector of states is empty.
040   */
041  public Trajectory(final List<State> states) {
042    m_states = states;
043
044    if (m_states.isEmpty()) {
045      throw new IllegalArgumentException("Trajectory manually created with no states.");
046    }
047
048    m_totalTimeSeconds = m_states.get(m_states.size() - 1).timeSeconds;
049  }
050
051  /**
052   * Linearly interpolates between two values.
053   *
054   * @param startValue The start value.
055   * @param endValue The end value.
056   * @param t The fraction for interpolation.
057   * @return The interpolated value.
058   */
059  private static double lerp(double startValue, double endValue, double t) {
060    return startValue + (endValue - startValue) * t;
061  }
062
063  /**
064   * Linearly interpolates between two poses.
065   *
066   * @param startValue The start pose.
067   * @param endValue The end pose.
068   * @param t The fraction for interpolation.
069   * @return The interpolated pose.
070   */
071  private static Pose2d lerp(Pose2d startValue, Pose2d endValue, double t) {
072    return startValue.plus((endValue.minus(startValue)).times(t));
073  }
074
075  /**
076   * Returns the initial pose of the trajectory.
077   *
078   * @return The initial pose of the trajectory.
079   */
080  public Pose2d getInitialPose() {
081    return sample(0).poseMeters;
082  }
083
084  /**
085   * Returns the overall duration of the trajectory.
086   *
087   * @return The duration of the trajectory.
088   */
089  public double getTotalTimeSeconds() {
090    return m_totalTimeSeconds;
091  }
092
093  /**
094   * Return the states of the trajectory.
095   *
096   * @return The states of the trajectory.
097   */
098  public List<State> getStates() {
099    return m_states;
100  }
101
102  /**
103   * Sample the trajectory at a point in time.
104   *
105   * @param timeSeconds The point in time since the beginning of the trajectory to sample.
106   * @return The state at that point in time.
107   * @throws IllegalStateException if the trajectory has no states.
108   */
109  public State sample(double timeSeconds) {
110    if (m_states.isEmpty()) {
111      throw new IllegalStateException("Trajectory cannot be sampled if it has no states.");
112    }
113
114    if (timeSeconds <= m_states.get(0).timeSeconds) {
115      return m_states.get(0);
116    }
117    if (timeSeconds >= m_totalTimeSeconds) {
118      return m_states.get(m_states.size() - 1);
119    }
120
121    // To get the element that we want, we will use a binary search algorithm
122    // instead of iterating over a for-loop. A binary search is O(std::log(n))
123    // whereas searching using a loop is O(n).
124
125    // This starts at 1 because we use the previous state later on for
126    // interpolation.
127    int low = 1;
128    int high = m_states.size() - 1;
129
130    while (low != high) {
131      int mid = (low + high) / 2;
132      if (m_states.get(mid).timeSeconds < timeSeconds) {
133        // This index and everything under it are less than the requested
134        // timestamp. Therefore, we can discard them.
135        low = mid + 1;
136      } else {
137        // t is at least as large as the element at this index. This means that
138        // anything after it cannot be what we are looking for.
139        high = mid;
140      }
141    }
142
143    // High and Low should be the same.
144
145    // The sample's timestamp is now greater than or equal to the requested
146    // timestamp. If it is greater, we need to interpolate between the
147    // previous state and the current state to get the exact state that we
148    // want.
149    final State sample = m_states.get(low);
150    final State prevSample = m_states.get(low - 1);
151
152    // If the difference in states is negligible, then we are spot on!
153    if (Math.abs(sample.timeSeconds - prevSample.timeSeconds) < 1E-9) {
154      return sample;
155    }
156    // Interpolate between the two states for the state that we want.
157    return prevSample.interpolate(
158        sample,
159        (timeSeconds - prevSample.timeSeconds) / (sample.timeSeconds - prevSample.timeSeconds));
160  }
161
162  /**
163   * Transforms all poses in the trajectory by the given transform. This is useful for converting a
164   * robot-relative trajectory into a field-relative trajectory. This works with respect to the
165   * first pose in the trajectory.
166   *
167   * @param transform The transform to transform the trajectory by.
168   * @return The transformed trajectory.
169   */
170  public Trajectory transformBy(Transform2d transform) {
171    var firstState = m_states.get(0);
172    var firstPose = firstState.poseMeters;
173
174    // Calculate the transformed first pose.
175    var newFirstPose = firstPose.plus(transform);
176    List<State> newStates = new ArrayList<>();
177
178    newStates.add(
179        new State(
180            firstState.timeSeconds,
181            firstState.velocityMetersPerSecond,
182            firstState.accelerationMetersPerSecondSq,
183            newFirstPose,
184            firstState.curvatureRadPerMeter));
185
186    for (int i = 1; i < m_states.size(); i++) {
187      var state = m_states.get(i);
188      // We are transforming relative to the coordinate frame of the new initial pose.
189      newStates.add(
190          new State(
191              state.timeSeconds,
192              state.velocityMetersPerSecond,
193              state.accelerationMetersPerSecondSq,
194              newFirstPose.plus(state.poseMeters.minus(firstPose)),
195              state.curvatureRadPerMeter));
196    }
197
198    return new Trajectory(newStates);
199  }
200
201  /**
202   * Transforms all poses in the trajectory so that they are relative to the given pose. This is
203   * useful for converting a field-relative trajectory into a robot-relative trajectory.
204   *
205   * @param pose The pose that is the origin of the coordinate frame that the current trajectory
206   *     will be transformed into.
207   * @return The transformed trajectory.
208   */
209  public Trajectory relativeTo(Pose2d pose) {
210    return new Trajectory(
211        m_states.stream()
212            .map(
213                state ->
214                    new State(
215                        state.timeSeconds,
216                        state.velocityMetersPerSecond,
217                        state.accelerationMetersPerSecondSq,
218                        state.poseMeters.relativeTo(pose),
219                        state.curvatureRadPerMeter))
220            .collect(Collectors.toList()));
221  }
222
223  /**
224   * Concatenates another trajectory to the current trajectory. The user is responsible for making
225   * sure that the end pose of this trajectory and the start pose of the other trajectory match (if
226   * that is the desired behavior).
227   *
228   * @param other The trajectory to concatenate.
229   * @return The concatenated trajectory.
230   */
231  public Trajectory concatenate(Trajectory other) {
232    // If this is a default constructed trajectory with no states, then we can
233    // simply return the rhs trajectory.
234    if (m_states.isEmpty()) {
235      return other;
236    }
237
238    // Deep copy the current states.
239    List<State> states =
240        m_states.stream()
241            .map(
242                state ->
243                    new State(
244                        state.timeSeconds,
245                        state.velocityMetersPerSecond,
246                        state.accelerationMetersPerSecondSq,
247                        state.poseMeters,
248                        state.curvatureRadPerMeter))
249            .collect(Collectors.toList());
250
251    // Here we omit the first state of the other trajectory because we don't want
252    // two time points with different states. Sample() will automatically
253    // interpolate between the end of this trajectory and the second state of the
254    // other trajectory.
255    for (int i = 1; i < other.getStates().size(); ++i) {
256      var s = other.getStates().get(i);
257      states.add(
258          new State(
259              s.timeSeconds + m_totalTimeSeconds,
260              s.velocityMetersPerSecond,
261              s.accelerationMetersPerSecondSq,
262              s.poseMeters,
263              s.curvatureRadPerMeter));
264    }
265    return new Trajectory(states);
266  }
267
268  /**
269   * Represents a time-parameterized trajectory. The trajectory contains of various States that
270   * represent the pose, curvature, time elapsed, velocity, and acceleration at that point.
271   */
272  public static class State implements ProtobufSerializable {
273    /** Trajectory.State protobuf for serialization. */
274    public static final TrajectoryStateProto proto = new TrajectoryStateProto();
275
276    /** The time elapsed since the beginning of the trajectory. */
277    @JsonProperty("time")
278    public double timeSeconds;
279
280    /** The speed at that point of the trajectory. */
281    @JsonProperty("velocity")
282    public double velocityMetersPerSecond;
283
284    /** The acceleration at that point of the trajectory. */
285    @JsonProperty("acceleration")
286    public double accelerationMetersPerSecondSq;
287
288    /** The pose at that point of the trajectory. */
289    @JsonProperty("pose")
290    public Pose2d poseMeters;
291
292    /** The curvature at that point of the trajectory. */
293    @JsonProperty("curvature")
294    public double curvatureRadPerMeter;
295
296    /** Default constructor. */
297    public State() {
298      poseMeters = new Pose2d();
299    }
300
301    /**
302     * Constructs a State with the specified parameters.
303     *
304     * @param timeSeconds The time elapsed since the beginning of the trajectory.
305     * @param velocityMetersPerSecond The speed at that point of the trajectory.
306     * @param accelerationMetersPerSecondSq The acceleration at that point of the trajectory.
307     * @param poseMeters The pose at that point of the trajectory.
308     * @param curvatureRadPerMeter The curvature at that point of the trajectory.
309     */
310    public State(
311        double timeSeconds,
312        double velocityMetersPerSecond,
313        double accelerationMetersPerSecondSq,
314        Pose2d poseMeters,
315        double curvatureRadPerMeter) {
316      this.timeSeconds = timeSeconds;
317      this.velocityMetersPerSecond = velocityMetersPerSecond;
318      this.accelerationMetersPerSecondSq = accelerationMetersPerSecondSq;
319      this.poseMeters = poseMeters;
320      this.curvatureRadPerMeter = curvatureRadPerMeter;
321    }
322
323    /**
324     * Interpolates between two States.
325     *
326     * @param endValue The end value for the interpolation.
327     * @param i The interpolant (fraction).
328     * @return The interpolated state.
329     */
330    State interpolate(State endValue, double i) {
331      // Find the new t value.
332      final double newT = lerp(timeSeconds, endValue.timeSeconds, i);
333
334      // Find the delta time between the current state and the interpolated state.
335      final double deltaT = newT - timeSeconds;
336
337      // If delta time is negative, flip the order of interpolation.
338      if (deltaT < 0) {
339        return endValue.interpolate(this, 1 - i);
340      }
341
342      // Check whether the robot is reversing at this stage.
343      final boolean reversing =
344          velocityMetersPerSecond < 0
345              || Math.abs(velocityMetersPerSecond) < 1E-9 && accelerationMetersPerSecondSq < 0;
346
347      // Calculate the new velocity
348      // v_f = v_0 + at
349      final double newV = velocityMetersPerSecond + (accelerationMetersPerSecondSq * deltaT);
350
351      // Calculate the change in position.
352      // delta_s = v_0 t + 0.5at²
353      final double newS =
354          (velocityMetersPerSecond * deltaT
355                  + 0.5 * accelerationMetersPerSecondSq * Math.pow(deltaT, 2))
356              * (reversing ? -1.0 : 1.0);
357
358      // Return the new state. To find the new position for the new state, we need
359      // to interpolate between the two endpoint poses. The fraction for
360      // interpolation is the change in position (delta s) divided by the total
361      // distance between the two endpoints.
362      final double interpolationFrac =
363          newS / endValue.poseMeters.getTranslation().getDistance(poseMeters.getTranslation());
364
365      return new State(
366          newT,
367          newV,
368          accelerationMetersPerSecondSq,
369          lerp(poseMeters, endValue.poseMeters, interpolationFrac),
370          lerp(curvatureRadPerMeter, endValue.curvatureRadPerMeter, interpolationFrac));
371    }
372
373    @Override
374    public String toString() {
375      return String.format(
376          "State(Sec: %.2f, Vel m/s: %.2f, Accel m/s/s: %.2f, Pose: %s, Curvature: %.2f)",
377          timeSeconds,
378          velocityMetersPerSecond,
379          accelerationMetersPerSecondSq,
380          poseMeters,
381          curvatureRadPerMeter);
382    }
383
384    @Override
385    public boolean equals(Object obj) {
386      if (this == obj) {
387        return true;
388      }
389      if (!(obj instanceof State)) {
390        return false;
391      }
392      State state = (State) obj;
393      return Double.compare(state.timeSeconds, timeSeconds) == 0
394          && Double.compare(state.velocityMetersPerSecond, velocityMetersPerSecond) == 0
395          && Double.compare(state.accelerationMetersPerSecondSq, accelerationMetersPerSecondSq) == 0
396          && Double.compare(state.curvatureRadPerMeter, curvatureRadPerMeter) == 0
397          && Objects.equals(poseMeters, state.poseMeters);
398    }
399
400    @Override
401    public int hashCode() {
402      return Objects.hash(
403          timeSeconds,
404          velocityMetersPerSecond,
405          accelerationMetersPerSecondSq,
406          poseMeters,
407          curvatureRadPerMeter);
408    }
409  }
410
411  @Override
412  public String toString() {
413    String stateList = m_states.stream().map(State::toString).collect(Collectors.joining(", \n"));
414    return String.format("Trajectory - Seconds: %.2f, States:\n%s", m_totalTimeSeconds, stateList);
415  }
416
417  @Override
418  public int hashCode() {
419    return m_states.hashCode();
420  }
421
422  @Override
423  public boolean equals(Object obj) {
424    return obj instanceof Trajectory && m_states.equals(((Trajectory) obj).getStates());
425  }
426}