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.spline;
006
007import edu.wpi.first.math.geometry.Pose2d;
008import edu.wpi.first.math.geometry.Translation2d;
009import java.util.Arrays;
010import java.util.List;
011
012public final class SplineHelper {
013  /** Private constructor because this is a utility class. */
014  private SplineHelper() {}
015
016  /**
017   * Returns 2 cubic control vectors from a set of exterior waypoints and interior translations.
018   *
019   * @param start The starting pose.
020   * @param interiorWaypoints The interior waypoints.
021   * @param end The ending pose.
022   * @return 2 cubic control vectors.
023   */
024  public static Spline.ControlVector[] getCubicControlVectorsFromWaypoints(
025      Pose2d start, Translation2d[] interiorWaypoints, Pose2d end) {
026    // Generate control vectors from poses.
027    Spline.ControlVector initialCV;
028    Spline.ControlVector endCV;
029
030    // Chooses a magnitude automatically that makes the splines look better.
031    if (interiorWaypoints.length < 1) {
032      double scalar = start.getTranslation().getDistance(end.getTranslation()) * 1.2;
033      initialCV = getCubicControlVector(scalar, start);
034      endCV = getCubicControlVector(scalar, end);
035    } else {
036      double scalar = start.getTranslation().getDistance(interiorWaypoints[0]) * 1.2;
037      initialCV = getCubicControlVector(scalar, start);
038      scalar =
039          end.getTranslation().getDistance(interiorWaypoints[interiorWaypoints.length - 1]) * 1.2;
040      endCV = getCubicControlVector(scalar, end);
041    }
042    return new Spline.ControlVector[] {initialCV, endCV};
043  }
044
045  /**
046   * Returns quintic splines from a set of waypoints.
047   *
048   * @param waypoints The waypoints
049   * @return array of splines.
050   */
051  public static QuinticHermiteSpline[] getQuinticSplinesFromWaypoints(List<Pose2d> waypoints) {
052    QuinticHermiteSpline[] splines = new QuinticHermiteSpline[waypoints.size() - 1];
053    for (int i = 0; i < waypoints.size() - 1; ++i) {
054      var p0 = waypoints.get(i);
055      var p1 = waypoints.get(i + 1);
056
057      // This just makes the splines look better.
058      final var scalar = 1.2 * p0.getTranslation().getDistance(p1.getTranslation());
059
060      var controlVecA = getQuinticControlVector(scalar, p0);
061      var controlVecB = getQuinticControlVector(scalar, p1);
062
063      splines[i] =
064          new QuinticHermiteSpline(controlVecA.x, controlVecB.x, controlVecA.y, controlVecB.y);
065    }
066    return splines;
067  }
068
069  /**
070   * Returns a set of cubic splines corresponding to the provided control vectors. The user is free
071   * to set the direction of the start and end point. The directions for the middle waypoints are
072   * determined automatically to ensure continuous curvature throughout the path.
073   *
074   * @param start The starting control vector.
075   * @param waypoints The middle waypoints. This can be left blank if you only wish to create a path
076   *     with two waypoints.
077   * @param end The ending control vector.
078   * @return A vector of cubic hermite splines that interpolate through the provided waypoints and
079   *     control vectors.
080   */
081  public static CubicHermiteSpline[] getCubicSplinesFromControlVectors(
082      Spline.ControlVector start, Translation2d[] waypoints, Spline.ControlVector end) {
083    CubicHermiteSpline[] splines = new CubicHermiteSpline[waypoints.length + 1];
084
085    double[] xInitial = start.x;
086    double[] yInitial = start.y;
087    double[] xFinal = end.x;
088    double[] yFinal = end.y;
089
090    if (waypoints.length > 1) {
091      Translation2d[] newWaypts = new Translation2d[waypoints.length + 2];
092
093      // Create an array of all waypoints, including the start and end.
094      newWaypts[0] = new Translation2d(xInitial[0], yInitial[0]);
095      System.arraycopy(waypoints, 0, newWaypts, 1, waypoints.length);
096      newWaypts[newWaypts.length - 1] = new Translation2d(xFinal[0], yFinal[0]);
097
098      // Populate tridiagonal system for clamped cubic
099      /* See:
100      https://www.uio.no/studier/emner/matnat/ifi/nedlagte-emner/INF-MAT4350/h08
101      /undervisningsmateriale/chap7alecture.pdf
102      */
103      // Above-diagonal of tridiagonal matrix, zero-padded
104      final double[] a = new double[newWaypts.length - 2];
105
106      // Diagonal of tridiagonal matrix
107      final double[] b = new double[newWaypts.length - 2];
108      Arrays.fill(b, 4.0);
109
110      // Below-diagonal of tridiagonal matrix, zero-padded
111      final double[] c = new double[newWaypts.length - 2];
112
113      // rhs vectors
114      final double[] dx = new double[newWaypts.length - 2];
115      final double[] dy = new double[newWaypts.length - 2];
116
117      // solution vectors
118      final double[] fx = new double[newWaypts.length - 2];
119      final double[] fy = new double[newWaypts.length - 2];
120
121      // populate above-diagonal and below-diagonal vectors
122      a[0] = 0.0;
123      for (int i = 0; i < newWaypts.length - 3; i++) {
124        a[i + 1] = 1;
125        c[i] = 1;
126      }
127      c[c.length - 1] = 0.0;
128
129      // populate rhs vectors
130      dx[0] = 3 * (newWaypts[2].getX() - newWaypts[0].getX()) - xInitial[1];
131      dy[0] = 3 * (newWaypts[2].getY() - newWaypts[0].getY()) - yInitial[1];
132
133      if (newWaypts.length > 4) {
134        for (int i = 1; i <= newWaypts.length - 4; i++) {
135          // dx and dy represent the derivatives of the internal waypoints. The derivative
136          // of the second internal waypoint should involve the third and first internal waypoint,
137          // which have indices of 1 and 3 in the newWaypts list (which contains ALL waypoints).
138          dx[i] = 3 * (newWaypts[i + 2].getX() - newWaypts[i].getX());
139          dy[i] = 3 * (newWaypts[i + 2].getY() - newWaypts[i].getY());
140        }
141      }
142
143      dx[dx.length - 1] =
144          3 * (newWaypts[newWaypts.length - 1].getX() - newWaypts[newWaypts.length - 3].getX())
145              - xFinal[1];
146      dy[dy.length - 1] =
147          3 * (newWaypts[newWaypts.length - 1].getY() - newWaypts[newWaypts.length - 3].getY())
148              - yFinal[1];
149
150      // Compute solution to tridiagonal system
151      thomasAlgorithm(a, b, c, dx, fx);
152      thomasAlgorithm(a, b, c, dy, fy);
153
154      double[] newFx = new double[fx.length + 2];
155      double[] newFy = new double[fy.length + 2];
156
157      newFx[0] = xInitial[1];
158      newFy[0] = yInitial[1];
159      System.arraycopy(fx, 0, newFx, 1, fx.length);
160      System.arraycopy(fy, 0, newFy, 1, fy.length);
161      newFx[newFx.length - 1] = xFinal[1];
162      newFy[newFy.length - 1] = yFinal[1];
163
164      for (int i = 0; i < newFx.length - 1; i++) {
165        splines[i] =
166            new CubicHermiteSpline(
167                new double[] {newWaypts[i].getX(), newFx[i]},
168                new double[] {newWaypts[i + 1].getX(), newFx[i + 1]},
169                new double[] {newWaypts[i].getY(), newFy[i]},
170                new double[] {newWaypts[i + 1].getY(), newFy[i + 1]});
171      }
172    } else if (waypoints.length == 1) {
173      final var xDeriv = (3 * (xFinal[0] - xInitial[0]) - xFinal[1] - xInitial[1]) / 4.0;
174      final var yDeriv = (3 * (yFinal[0] - yInitial[0]) - yFinal[1] - yInitial[1]) / 4.0;
175
176      double[] midXControlVector = {waypoints[0].getX(), xDeriv};
177      double[] midYControlVector = {waypoints[0].getY(), yDeriv};
178
179      splines[0] =
180          new CubicHermiteSpline(
181              xInitial, midXControlVector,
182              yInitial, midYControlVector);
183      splines[1] =
184          new CubicHermiteSpline(
185              midXControlVector, xFinal,
186              midYControlVector, yFinal);
187    } else {
188      splines[0] =
189          new CubicHermiteSpline(
190              xInitial, xFinal,
191              yInitial, yFinal);
192    }
193    return splines;
194  }
195
196  /**
197   * Returns a set of quintic splines corresponding to the provided control vectors. The user is
198   * free to set the direction of all control vectors. Continuous curvature is guaranteed throughout
199   * the path.
200   *
201   * @param controlVectors The control vectors.
202   * @return A vector of quintic hermite splines that interpolate through the provided waypoints.
203   */
204  public static QuinticHermiteSpline[] getQuinticSplinesFromControlVectors(
205      Spline.ControlVector[] controlVectors) {
206    QuinticHermiteSpline[] splines = new QuinticHermiteSpline[controlVectors.length - 1];
207    for (int i = 0; i < controlVectors.length - 1; i++) {
208      var xInitial = controlVectors[i].x;
209      var xFinal = controlVectors[i + 1].x;
210      var yInitial = controlVectors[i].y;
211      var yFinal = controlVectors[i + 1].y;
212      splines[i] =
213          new QuinticHermiteSpline(
214              xInitial, xFinal,
215              yInitial, yFinal);
216    }
217    return splines;
218  }
219
220  /**
221   * Thomas algorithm for solving tridiagonal systems Af = d.
222   *
223   * @param a the values of A above the diagonal
224   * @param b the values of A on the diagonal
225   * @param c the values of A below the diagonal
226   * @param d the vector on the rhs
227   * @param solutionVector the unknown (solution) vector, modified in-place
228   */
229  private static void thomasAlgorithm(
230      double[] a, double[] b, double[] c, double[] d, double[] solutionVector) {
231    int N = d.length;
232
233    double[] cStar = new double[N];
234    double[] dStar = new double[N];
235
236    // This updates the coefficients in the first row
237    // Note that we should be checking for division by zero here
238    cStar[0] = c[0] / b[0];
239    dStar[0] = d[0] / b[0];
240
241    // Create the c_star and d_star coefficients in the forward sweep
242    for (int i = 1; i < N; i++) {
243      double m = 1.0 / (b[i] - a[i] * cStar[i - 1]);
244      cStar[i] = c[i] * m;
245      dStar[i] = (d[i] - a[i] * dStar[i - 1]) * m;
246    }
247    solutionVector[N - 1] = dStar[N - 1];
248    // This is the reverse sweep, used to update the solution vector f
249    for (int i = N - 2; i >= 0; i--) {
250      solutionVector[i] = dStar[i] - cStar[i] * solutionVector[i + 1];
251    }
252  }
253
254  private static Spline.ControlVector getCubicControlVector(double scalar, Pose2d point) {
255    return new Spline.ControlVector(
256        new double[] {point.getX(), scalar * point.getRotation().getCos()},
257        new double[] {point.getY(), scalar * point.getRotation().getSin()});
258  }
259
260  private static Spline.ControlVector getQuinticControlVector(double scalar, Pose2d point) {
261    return new Spline.ControlVector(
262        new double[] {point.getX(), scalar * point.getRotation().getCos(), 0.0},
263        new double[] {point.getY(), scalar * point.getRotation().getSin(), 0.0});
264  }
265}