001// Copyright (c) Choreo contributors
002
003package choreo.trajectory;
004
005import choreo.util.ChoreoAllianceFlipUtil;
006import edu.wpi.first.math.MathUtil;
007import edu.wpi.first.math.Matrix;
008import edu.wpi.first.math.VecBuilder;
009import edu.wpi.first.math.geometry.Pose2d;
010import edu.wpi.first.math.geometry.Rotation2d;
011import edu.wpi.first.math.kinematics.ChassisSpeeds;
012import edu.wpi.first.math.numbers.N1;
013import edu.wpi.first.math.numbers.N2;
014import edu.wpi.first.math.numbers.N6;
015import edu.wpi.first.math.system.NumericalIntegration;
016import edu.wpi.first.util.struct.Struct;
017import java.nio.ByteBuffer;
018import java.util.function.BiFunction;
019
020/** A single differential drive robot sample in a Trajectory. */
021public class DifferentialSample implements TrajectorySample<DifferentialSample> {
022  /** The timestamp of this sample relative to the beginning of the trajectory. */
023  public final double t;
024
025  /** The X position of the sample relative to the blue alliance wall origin in meters. */
026  public final double x;
027
028  /** The Y position of the sample relative to the blue alliance wall origin in meters. */
029  public final double y;
030
031  /** The heading of the sample in radians, with 0 being in the +X direction. */
032  public final double heading;
033
034  /** The velocity of the left side in m/s. */
035  public final double vl;
036
037  /** The velocity of the right side in m/s. */
038  public final double vr;
039
040  /** The chassis angular velocity in rad/s. */
041  public final double omega;
042
043  /** The acceleration of the left side in m/s². */
044  public final double al;
045
046  /** The acceleration of the right side in m/s². */
047  public final double ar;
048
049  /** The force of the left side in Newtons. */
050  public final double fl;
051
052  /** The force of the right side in Newtons. */
053  public final double fr;
054
055  /**
056   * Constructs a DifferentialSample with the specified parameters.
057   *
058   * @param timestamp The timestamp of this sample.
059   * @param x The X position of the sample in meters.
060   * @param y The Y position of the sample in meters.
061   * @param heading The heading of the sample in radians, with 0 being in the +X direction.
062   * @param vl The velocity of the left side in m/s.
063   * @param vr The velocity of the right side in m/s.
064   * @param omega The chassis angular velocity in rad/s.
065   * @param al The acceleration of the left side in m/s².
066   * @param ar The acceleration of the right side in m/s².
067   * @param fl The force of the left side in Newtons.
068   * @param fr The force of the right side in Newtons.
069   */
070  public DifferentialSample(
071      double timestamp,
072      double x,
073      double y,
074      double heading,
075      double vl,
076      double vr,
077      double omega,
078      double al,
079      double ar,
080      double fl,
081      double fr) {
082    this.t = timestamp;
083    this.x = x;
084    this.y = y;
085    this.heading = heading;
086    this.vl = vl;
087    this.vr = vr;
088    this.omega = omega;
089    this.al = al;
090    this.ar = ar;
091    this.fl = fl;
092    this.fr = fr;
093  }
094
095  @Override
096  public double getTimestamp() {
097    return t;
098  }
099
100  @Override
101  public Pose2d getPose() {
102    return new Pose2d(x, y, Rotation2d.fromRadians(heading));
103  }
104
105  /**
106   * Returns the field-relative chassis speeds of this sample.
107   *
108   * @return the field-relative chassis speeds of this sample.
109   * @see edu.wpi.first.math.kinematics.DifferentialDriveKinematics#toChassisSpeeds
110   */
111  @Override
112  public ChassisSpeeds getChassisSpeeds() {
113    return new ChassisSpeeds((vl + vr) / 2, 0, omega);
114  }
115
116  @Override
117  public DifferentialSample interpolate(DifferentialSample endValue, double timestamp) {
118    double scale = (timestamp - this.t) / (endValue.t - this.t);
119
120    // Integrate the acceleration to get the rest of the state, since linearly
121    // interpolating the state gives an inaccurate result if the accelerations are changing between
122    // states
123    Matrix<N6, N1> initialState = VecBuilder.fill(x, y, heading, vl, vr, omega);
124
125    double width = (vr - vl) / omega;
126    BiFunction<Matrix<N6, N1>, Matrix<N2, N1>, Matrix<N6, N1>> f =
127        (state, input) -> {
128          //  state =  [x, y, θ, vₗ, vᵣ, ω]
129          //  input =  [aₗ, aᵣ]
130          //
131          //  v = (vₗ + vᵣ)/2
132          //  ω = (vᵣ − vₗ)/width
133          //  α = (aᵣ − aₗ)/width
134          //
135          //  ẋ = v cosθ
136          //  ẏ = v sinθ
137          //  θ̇ = ω
138          //  v̇ₗ = aₗ
139          //  v̇ᵣ = aᵣ
140          //  ω̇ = α
141          var θ = state.get(2, 0);
142          var vl = state.get(3, 0);
143          var vr = state.get(4, 0);
144          var ω = state.get(5, 0);
145          var al = input.get(0, 0);
146          var ar = input.get(1, 0);
147          var v = (vl + vr) / 2;
148          var α = (ar - al) / width;
149          return VecBuilder.fill(v * Math.cos(θ), v * Math.sin(θ), ω, al, ar, α);
150        };
151
152    double τ = timestamp - this.t;
153    var sample = NumericalIntegration.rkdp(f, initialState, VecBuilder.fill(al, ar), τ);
154
155    double dt = endValue.t - this.t;
156    double jl = (endValue.al - this.al) / dt;
157    double jr = (endValue.ar - this.ar) / dt;
158
159    return new DifferentialSample(
160        MathUtil.interpolate(this.t, endValue.t, scale),
161        sample.get(0, 0),
162        sample.get(1, 0),
163        sample.get(2, 0),
164        sample.get(3, 0),
165        sample.get(4, 0),
166        sample.get(5, 0),
167        this.al + jl * τ,
168        this.ar + jr * τ,
169        MathUtil.interpolate(this.fl, endValue.fl, scale),
170        MathUtil.interpolate(this.fr, endValue.fr, scale));
171  }
172
173  public DifferentialSample flipped() {
174    return switch (ChoreoAllianceFlipUtil.getFlipper()) {
175      case MIRRORED ->
176          new DifferentialSample(
177              t,
178              ChoreoAllianceFlipUtil.flipX(x),
179              ChoreoAllianceFlipUtil.flipY(y), // No-op for mirroring
180              ChoreoAllianceFlipUtil.flipHeading(heading),
181              vr,
182              vl,
183              -omega,
184              ar,
185              al,
186              fr,
187              fl);
188      case ROTATE_AROUND ->
189          new DifferentialSample(
190              t,
191              ChoreoAllianceFlipUtil.flipX(x),
192              ChoreoAllianceFlipUtil.flipY(y),
193              ChoreoAllianceFlipUtil.flipHeading(heading),
194              vl,
195              vr,
196              omega,
197              al,
198              ar,
199              fl,
200              fr);
201    };
202  }
203
204  public DifferentialSample offsetBy(double timestampOffset) {
205    return new DifferentialSample(
206        t + timestampOffset, x, y, heading, vl, vr, omega, al, ar, fl, fr);
207  }
208
209  /** The struct for the DifferentialSample class. */
210  public static final Struct<DifferentialSample> struct = new DifferentialSampleStruct();
211
212  private static final class DifferentialSampleStruct implements Struct<DifferentialSample> {
213    @Override
214    public Class<DifferentialSample> getTypeClass() {
215      return DifferentialSample.class;
216    }
217
218    @Override
219    public String getTypeName() {
220      return "DifferentialSample";
221    }
222
223    @Override
224    public int getSize() {
225      return Struct.kSizeDouble * 10;
226    }
227
228    @Override
229    public String getSchema() {
230      return "double timestamp;"
231          + "Pose2d pose;"
232          + "double vl;"
233          + "double vr;"
234          + "double omega;"
235          + "double al;"
236          + "double ar;"
237          + "double fl;"
238          + "double fr;";
239    }
240
241    @Override
242    public Struct<?>[] getNested() {
243      return new Struct<?>[] {Pose2d.struct};
244    }
245
246    @Override
247    public DifferentialSample unpack(ByteBuffer bb) {
248      return new DifferentialSample(
249          bb.getDouble(),
250          bb.getDouble(),
251          bb.getDouble(),
252          bb.getDouble(),
253          bb.getDouble(),
254          bb.getDouble(),
255          bb.getDouble(),
256          bb.getDouble(),
257          bb.getDouble(),
258          bb.getDouble(),
259          bb.getDouble());
260    }
261
262    @Override
263    public void pack(ByteBuffer bb, DifferentialSample value) {
264      bb.putDouble(value.t);
265      bb.putDouble(value.x);
266      bb.putDouble(value.y);
267      bb.putDouble(value.heading);
268      bb.putDouble(value.vl);
269      bb.putDouble(value.vr);
270      bb.putDouble(value.omega);
271      bb.putDouble(value.al);
272      bb.putDouble(value.ar);
273      bb.putDouble(value.fl);
274      bb.putDouble(value.fr);
275    }
276  }
277
278  @Override
279  public boolean equals(Object obj) {
280    if (!(obj instanceof DifferentialSample)) {
281      return false;
282    }
283
284    var other = (DifferentialSample) obj;
285    return this.t == other.t
286        && this.x == other.x
287        && this.y == other.y
288        && this.heading == other.heading
289        && this.vl == other.vl
290        && this.vr == other.vr
291        && this.omega == other.omega
292        && this.al == other.al
293        && this.ar == other.ar
294        && this.fl == other.fl
295        && this.fr == other.fr;
296  }
297}