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}