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