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}