├── .classpath ├── .project ├── .settings └── org.eclipse.jdt.core.prefs ├── Images ├── Image1.png ├── Image10.png ├── Image2.png ├── Image3.png ├── Image4.png ├── Image5.png ├── Image6.png ├── Image7.png ├── Image8.png └── Image9.png ├── README.md └── src └── usfirst └── frc └── team2168 └── robot ├── FalconLinePlot.java └── FalconPathPlanner.java /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | PathPlanner 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.jdt.core.javanature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 3 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 4 | org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve 5 | org.eclipse.jdt.core.compiler.compliance=1.7 6 | org.eclipse.jdt.core.compiler.debug.lineNumber=generate 7 | org.eclipse.jdt.core.compiler.debug.localVariable=generate 8 | org.eclipse.jdt.core.compiler.debug.sourceFile=generate 9 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 10 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 11 | org.eclipse.jdt.core.compiler.source=1.7 12 | -------------------------------------------------------------------------------- /Images/Image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KHEngineering/SmoothPathPlanner/11059aa2ec314ba20b364aeea3c968aca2672b49/Images/Image1.png -------------------------------------------------------------------------------- /Images/Image10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KHEngineering/SmoothPathPlanner/11059aa2ec314ba20b364aeea3c968aca2672b49/Images/Image10.png -------------------------------------------------------------------------------- /Images/Image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KHEngineering/SmoothPathPlanner/11059aa2ec314ba20b364aeea3c968aca2672b49/Images/Image2.png -------------------------------------------------------------------------------- /Images/Image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KHEngineering/SmoothPathPlanner/11059aa2ec314ba20b364aeea3c968aca2672b49/Images/Image3.png -------------------------------------------------------------------------------- /Images/Image4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KHEngineering/SmoothPathPlanner/11059aa2ec314ba20b364aeea3c968aca2672b49/Images/Image4.png -------------------------------------------------------------------------------- /Images/Image5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KHEngineering/SmoothPathPlanner/11059aa2ec314ba20b364aeea3c968aca2672b49/Images/Image5.png -------------------------------------------------------------------------------- /Images/Image6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KHEngineering/SmoothPathPlanner/11059aa2ec314ba20b364aeea3c968aca2672b49/Images/Image6.png -------------------------------------------------------------------------------- /Images/Image7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KHEngineering/SmoothPathPlanner/11059aa2ec314ba20b364aeea3c968aca2672b49/Images/Image7.png -------------------------------------------------------------------------------- /Images/Image8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KHEngineering/SmoothPathPlanner/11059aa2ec314ba20b364aeea3c968aca2672b49/Images/Image8.png -------------------------------------------------------------------------------- /Images/Image9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KHEngineering/SmoothPathPlanner/11059aa2ec314ba20b364aeea3c968aca2672b49/Images/Image9.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SmoothPathPlanner 2 | ================= 3 | 4 | 5 | After the Cheesy Poofs (FRC 254) put on an amazing display of robot path following during the 2014 FRC championship finals, a lot of buzz has been generated by other teams wishing to implement a similar feature on their robot. The poofs released their code and the community learned they used 5th order polynomial splines in order to find the best fit smooth curve from POint A to Point B. 6 | 7 | Path planning has been the primary focus in many other fields outside of FRC. For example in Game AI, generating smooths paths for the main character to follow when going around obstacles needs to be calculated in real-time. In other robotics fields such as autonomous car navigation, many smooth paths need to be generated simultaneously, and the "best" path choosen. 8 | 9 | These fields have spent many years adopting and optimizing the field of motion planning for their specific application. However, what they have in common is that the calculations for generating the path needs to be performed in real-time, and typically are performed on inexpensive embedded devices, such as gaming consoles or car computers. As part of my PhD thesis (developing a controller for an Autonomous Car) I had the opportunity to create a simplified path planner for real-time mobile robot applications. 10 | 11 | This class is a port of the code I developed for my PhD thesis, to perform the function of path calculation on a mobile, skid-steer robot very quickly. The algorithms provided here rely on algorithms used in optimal control theory to converge to a solution very quickly. The code here uses the "gradient descent" optimization technique to coerce a simple waypoint path, into a smooth trajectory. 12 | 13 | Once the smooth trajectory is determined, parallel paths for the left and right wheels are calculated based on the robots trackwidth distance, and corresponding velocities for each of the wheels are determined. As long as the robot has independant speed controllers for the left and right side of the drivetrain, the user can "play" through the velocity profile as if they are setpoints and the robot should drive the corresponding path. 14 | 15 | The function takes in just 3 paramters to get started and boost a very simple user interface. 16 | 17 | 1. `TotalTime` - is the time you wish the robot to complete the path, this is used to calculate the velocity 18 | 2. `Time Step` - this is the rate at which the controller on your robot runs. Typically for FRC we run our controllers at a 100ms rate in a separate thread from the main Robot. 19 | 3. `TrackWidth` - the distance between the left and right wheels of the robot. This is used to calculate the left and right wheel paths. 20 | 21 | The waypoints are simply global positions you would like your robot to drive, specified as a 2D array. The order of the points specify the path which the user wishes to drive, and the smooth path is generated accordingly. The smooth path generated also uses the Max Time, and controller periodic rate to automatically create the proper length velocity array so the user simply steps through each element once every time step. The end result is that the robot drives along the path, depending how well your speed controllers are tuned. So for example, if you have a controller running at 100ms, and you want a path that will take no more than 5 seconds to travsers, the algorithm will provide the left and right velocities as arrays with no more than 5/0.1 = 50 points. Each point will contain the velocity the robot should be at the time to make the end position by the end of travel. 22 | 23 | Here is an an example to generate a path using the default paramters. 24 | 25 | ```java 26 | //create waypoint path 27 | double[][] waypoints = new double[][]{ 28 | {1, 1}, 29 | {5, 1}, 30 | {9, 12}, 31 | {12, 9}, 32 | {15, 6}, 33 | {19, 12} 34 | }; 35 | 36 | double totalTime = 8; //max seconds we want to drive the path 37 | double timeStep = 0.1; //period of control loop on Rio, seconds 38 | double robotTrackWidth = 2; //distance between left and right wheels, feet 39 | 40 | FalconPathPlanner path = new FalconPathPlanner(waypoints); 41 | path.calculate(totalTime, timeStep, robotTrackWidth); 42 | ``` 43 | 44 | After the `calculate` method is called, the smooth path and left and right velocities are all calculated and stored into the class data members. Below is the list of each class path variable. Note, all of the data members of this class are public. While this is usually not a good idea, I think for this case, and how we use these classes in FRC, it makes sense to leave these as public so the user can access them and modify them as needed. Note `path` is the class instantiated from the example above. 45 | 46 | ```Java 47 | path.origPath; // 2D array of doubles which contains the original waypoint path provided 48 | path.nodeOnlyPath; // 2D array of doubles which filters the original path and only keeps points which 49 | //cause the path to change direction (i.e. all intermediate points on a straight line 50 | //are removed) 51 | path.smoothPath; //2D array of doubles represents the smooth path for the center of the robot. 52 | path.leftPath; //2D array of doubles represents the left wheel path (based on track width of robot) 53 | path.rightPath; //2D array of doubles represents the right wheel path (based on track width of robot) 54 | 55 | path.smoothCenterVelocity; //2D array of doubles which contains a smooth velocity profile of the smoothPath above 56 | path.smoothRightVelocity; //2D array of doubles which contains a smooth velocity profile of the rightPath above 57 | path.smoothLeftVelocity; //2D array of doubles which contains a smooth velocity profile of the leftPath above 58 | ``` 59 | 60 | At this point all you need to do is pass `path.smoothRightVelocity` and `path.smoothLeftVelocity` to the Robots left and right wheel speed controllers. "Play" through the array updating the setpoint of the controller with the next element of the array each time step. 61 | 62 | >Note: ALL PATH 2 DIMENSIONAL ARRAYS CONTAIN THE X-COORDINATE IN THE FIRST DIMENSION, AND THE Y COODINATE IN THE SECOND DIMENSION. SO `path.rightPath[i][0]` would be the ith X- coodinate, and `path.rightPath[i][1]` would be the ith Y- coordinate of the right wheel path for example. THE UNITS OF THESE ARE THE GLOBAL UNITS ASSUMED BY THE USER. 63 | 64 | >Note: ALL VELOCITY 2 DIMENSIONAL ARRAYS CONTAIN THE TIME VECOTOR IN THE FIRST DIMENSION, AND THE VELOCITY MAGNITUDE IN THE SECOND DIMENSION. So `path.smoothLeftVelocity[i][0]` would be the ith time step value in seconds, and `path.smoothLeftVelocity[i][1]` would be the ith velocity magnitude in units per second. **When attemping to command the speed controller, the user only needs to pass the `path.smoothLeftVelocity[i][1] array, which is a single dimension array of doubles to the speed controller.** 65 | 66 | Furthermore, the algorithm also implements a smooth start/stop routine and automatically ramps up and down to a zero velocity so at the end of travel the robot slows down to zero. The coerceion to zero velocity takes in to account the total distance needed to travel and modifies the velocity accordingly. 67 | 68 | Typically when I write complex algorithms, I tend to put the Big O notation in the JavaDoc comments of each method and I also write down a pseudocode to help guide me as I program the algorithm. Typically I remove these comments in the final product, but I decided to leave them in to help people follow my logic, and see which Algorithms I have optimized, and which algorithms could use more optimization. Most algorithms have been optimized to linear Big O notation. 69 | 70 | I have tested this algorithm on a 2015 RoboRio and for a path containing 71 | 72 | 73 | 74 | ## Using the Included Grapher 75 | 76 | I have also included a very crude graphing class to help visualize the output. The graphing class is also written using native Java SE classes and will run on any desktop. 77 | 78 | >Note: Because the RoboRio is headless (meaning no display), the Graph functions can not run on the RoboRio. Check `if(!GraphicsEnvironment.isHeadless())` before graphing this way the code is completely portable and will run on both desktop and RoboRio. 79 | 80 | So let’s work through an example of how to use this class: 81 | 82 | If we want to graph the output of the path above we can do the following and produce the following plot: 83 | ```Java 84 | FalconLinePlot fig1 = new FalconLinePlot(path.nodeOnlyPath,Color.blue,Color.green); 85 | ``` 86 | 87 | ![](Images/Image1.png) 88 | 89 | The plot above isn’t that helpful. However we can configure it to be more useful to us. Since this is the plot of the global path of the robot. Let’s change the X- and Y- axis to reflect the dimensions of half of the FRC field (24 x 27ft) and lets add some labels and grid lines. 90 | 91 | ```Java 92 | fig1.yGridOn(); 93 | fig1.xGridOn(); 94 | fig1.setYLabel("Y (feet)"); 95 | fig1.setXLabel("X (feet)"); 96 | fig1.setTitle("Top Down View of FRC Field (24ft x 27ft) \n shows global position of robot path, along with left and right wheel trajectories"); 97 | 98 | //force graph to show 1/2 field dimensions of 24ft x 27 feet 99 | fig1.setXTic(0, 27, 1); 100 | fig1.setYTic(0, 24, 1); 101 | ``` 102 | ![](Images/Image2.png) 103 | 104 | The above figure is more useful. It shows the general path we wish the robot to take. Starting at our waypoint {1,1} and ending at {19,12}. This plot is in feet. The real usefulness of the plotter function is the ability to add additional plots to one figure. You can add an unlimited amount of charts using the `addData` method. There are also options to specify line color, and marker color. If you do not wish to have lines or markers, set them to `null`. 105 | 106 | Lets add the smooth center path the algorithm calculated to this plot. Which gives the following: 107 | ```Java 108 | fig1.addData(path.smoothPath, Color.red, Color.blue); 109 | ``` 110 | ![Alt desc](Images/Image3.png) 111 | We can see the smooth path algorithm does two things. It creates many more data points and provides smooth transitions around the corners. The number of data points created is based on the max Time you wish the robot to complete the path, and the time step of the controller. 112 | 113 | We can also add the left and right path trajectories to the plot to provide all the useful information we need in one chart. 114 | 115 | ```Java 116 | fig1.addData(path.leftPath, Color.magenta); 117 | fig1.addData(path.rightPath, Color.magenta); 118 | ``` 119 | 120 | ![](Images/Image4.png) 121 | 122 | I choose this example for a reason, and we can see some tight turns on the robot in the example. (Maybe too tight) We will fix those later (todo: see section here), but first let’s see what the velocity looks like for this path. 123 | 124 | ```Java 125 | FalconLinePlot fig2 = new FalconLinePlot(path.smoothCenterVelocity,null,Color.blue); 126 | fig2.yGridOn(); 127 | fig2.xGridOn(); 128 | fig2.setYLabel("Velocity (ft/sec)"); 129 | fig2.setXLabel("time (seconds)"); 130 | fig2.setTitle("Velocity Profile for Left and Right Wheels \n Left = Cyan, Right = Magenta"); 131 | fig2.addData(path.smoothRightVelocity, Color.magenta); 132 | fig2.addData(path.smoothLeftVelocity, Color.cyan); 133 | ``` 134 | ![](Images/Image5.png) 135 | 136 | 137 | This is where you need to determine, if the velocities shown can be accomplished by your robot. Looks like the max and min velocity is achievable for most drivetrains so let’s continue. We also note there is a relatively smooth transition between data points. If your velocity isn’t smooth, modify the way points so that it is smooth. The robot will not react well to instant changes in velocity. Note the total time is 7.2 seconds which is less than the `8` second requirement we provided. If we increase or decrease the time requirement, the magnitude of the velocity will decrease or increase, respectively. 138 | 139 | ## Let’s recreate the Cheezy Poof Path 140 | As another example we are going to try and replicate the smooth path used by FRC Team 254 during the 2014 FRC Einstein Finals, Match 3. 141 | 142 | ```Java 143 | //Lets create a bank image 144 | FalconLinePlot fig3 = new FalconLinePlot(new double[][]{{0.0,0.0}}); 145 | fig3.yGridOn(); 146 | fig3.xGridOn(); 147 | fig3.setYLabel("Y (feet)"); 148 | fig3.setXLabel("X (feet)"); 149 | fig3.setTitle("Top Down View of FRC Field (28ft x 27ft) \n shows global position of robot path, along with left and right wheel trajectories"); 150 | 151 | //force graph to show 1/2 field dimensions of 24.8ft x 27 feet 152 | double fieldWidth = 32.0; 153 | fig3.setXTic(0, 27, 1); 154 | fig3.setYTic(0, fieldWidth, 1); 155 | ``` 156 | 157 | ![](Images/Image6.png) 158 | 159 | 160 | We can add the Goal wall, and low goals to the view. 161 | 162 | ```Java 163 | //lets add field markers to help visual 164 | //http://www.usfirst.org/sites/default/files/uploadedFiles/Robotics_Programs/FRC/Game_and_Season__Info/2014/fe-00037_RevB.pdf 165 | //Goal line 166 | double[][] goalLine = new double[][] {{26.5,0}, {26.5, fieldWidth}}; 167 | fig3.addData(goalLine, Color.black); 168 | 169 | //Low Goals roughly 33 inch x 33 inch and 24.6 ft apart (inside to inside) 170 | double[][] leftLowGoal = new double[][]{ 171 | {26.5, fieldWidth/2 + 24.6/2}, 172 | {26.5, (fieldWidth)/2 + 24.6/2 + 2.75}, 173 | {26.5 - 2.75, fieldWidth/2 + 24.6/2 + 2.75}, 174 | {26.5 - 2.75, fieldWidth/2 + 24.6/2}, 175 | {26.5, fieldWidth/2 + 24.6/2}, 176 | }; 177 | 178 | double[][] rightLowGoal = new double[][]{ 179 | {26.5, fieldWidth/2 - 24.6/2}, 180 | {26.5, fieldWidth/2 - 24.6/2 - 2.75}, 181 | {26.5 - 2.75, fieldWidth/2 - 24.6/2 - 2.75}, 182 | {26.5 - 2.75, fieldWidth/2 - 24.6/2}, 183 | {26.5, fieldWidth/2 - 24.6/2}, 184 | }; 185 | 186 | fig3.addData(leftLowGoal, Color.black); 187 | fig3.addData(rightLowGoal, Color.black); 188 | 189 | //Auto Line 190 | double[][] autoLine = new double[][] {{26.5-18,0}, {26.5-18, fieldWidth}}; 191 | fig3.addData(autoLine, Color.black); 192 | ``` 193 | ![](Images/Image7.png) 194 | 195 | Using the above plot, we can create the following waypoint trajectory to replicate the path taken by FRC 254 on Einstein Final Match 3; 196 | 197 | ```Java 198 | double[][] CheesyPath = new double[][]{ 199 | {7,16}, 200 | {11,16}, 201 | {17,28}, 202 | {22,28}, 203 | }; 204 | 205 | double totalTime = 4.5; //seconds 206 | double timeStep = 0.1; //period of control loop on Rio, seconds 207 | double robotTrackWidth = 2; //distance between left and right wheels, feet 208 | 209 | final FalconPathPlanner path = new FalconPathPlanner(CheesyPath); 210 | path.calculate(totalTime, timeStep, robotTrackWidth); 211 | 212 | fig3.addData(path.nodeOnlyPath,Color.blue,Color.green); 213 | ``` 214 | ![](Images/Image8.png) 215 | 216 | 217 | Adding all of the other path parameters as well: 218 | 219 | ```Java 220 | //add all other paths 221 | fig3.addData(path.smoothPath, Color.red, Color.blue); 222 | fig3.addData(path.leftPath, Color.magenta); 223 | fig3.addData(path.rightPath, Color.magenta); 224 | ``` 225 | ![](Images/Image9.png) 226 | 227 | 228 | We also plot the Velocity of this chart: 229 | ```Java 230 | //Velocity 231 | FalconLinePlot fig4 = new FalconLinePlot(path.smoothCenterVelocity,null,Color.blue); 232 | fig4.yGridOn(); 233 | fig4.xGridOn(); 234 | fig4.setYLabel("Velocity (ft/sec)"); 235 | fig4.setXLabel("time (seconds)"); 236 | fig4.setTitle("Velocity Profile for Left and Right Wheels \n Left = Cyan, Right = Magenta"); 237 | fig4.addData(path.smoothRightVelocity, Color.magenta); 238 | fig4.addData(path.smoothLeftVelocity, Color.cyan); 239 | ``` 240 | 241 | ![](Images/Image10.png) 242 | 243 | 244 | This is actually a really nice velocity curve. The transactions are very smooth for each wheel. Assuming our robot can meet these velocities we will traverse the smooth path in 4.2 seconds and stop! You will notice the left wheel slows down first to allow for the slight left turn in the beginning, and then the right wheel slows down to allow for the slight right turn. 245 | 246 | At this point, all we need to do is pass `path.smoothLeftVelocity[i][1]` and ``path.smoothRightVelocity[i][1]` to our robot Left and right wheel speed controllers, and update the setpoint to the next value in the array each robot loop iteration. The robot will “play” through the array and drive the smooth path. 247 | 248 | The complete code for this example is below: 249 | 250 | ```Java 251 | /***Poof Example***/ 252 | 253 | //Lets create a bank image 254 | FalconLinePlot fig3 = new FalconLinePlot(new double[][]{{0.0,0.0}}); 255 | fig3.yGridOn(); 256 | fig3.xGridOn(); 257 | fig3.setYLabel("Y (feet)"); 258 | fig3.setXLabel("X (feet)"); 259 | fig3.setTitle("Top Down View of FRC Field (30ft x 27ft) \n shows global position of robot path, along with left and right wheel trajectories"); 260 | 261 | 262 | //force graph to show 1/2 field dimensions of 24.8ft x 27 feet 263 | double fieldWidth = 32.0; 264 | fig3.setXTic(0, 27, 1); 265 | fig3.setYTic(0, fieldWidth, 1); 266 | 267 | 268 | //lets add field markers to help visual 269 | //http://www.usfirst.org/sites/default/files/uploadedFiles/Robotics_Programs/FRC/Game_and_Season__Info/2014/fe-00037_RevB.pdf 270 | //Goal line 271 | double[][] goalLine = new double[][] {{26.5,0}, {26.5, fieldWidth}}; 272 | fig3.addData(goalLine, Color.black); 273 | 274 | //Low Goals roughly 33 inch x 33 inch and 24.6 ft apart (inside to inside) 275 | double[][] leftLowGoal = new double[][]{ 276 | {26.5, fieldWidth/2 + 24.6/2}, 277 | {26.5, (fieldWidth)/2 + 24.6/2 + 2.75}, 278 | {26.5 - 2.75, fieldWidth/2 + 24.6/2 + 2.75}, 279 | {26.5 - 2.75, fieldWidth/2 + 24.6/2}, 280 | {26.5, fieldWidth/2 + 24.6/2}, 281 | }; 282 | 283 | double[][] rightLowGoal = new double[][]{ 284 | {26.5, fieldWidth/2 - 24.6/2}, 285 | {26.5, fieldWidth/2 - 24.6/2 - 2.75}, 286 | {26.5 - 2.75, fieldWidth/2 - 24.6/2 - 2.75}, 287 | {26.5 - 2.75, fieldWidth/2 - 24.6/2}, 288 | {26.5, fieldWidth/2 - 24.6/2}, 289 | }; 290 | 291 | fig3.addData(leftLowGoal, Color.black); 292 | fig3.addData(rightLowGoal, Color.black); 293 | 294 | //Auto Line 295 | double[][] autoLine = new double[][] {{26.5-18,0}, {26.5-18, fieldWidth}}; 296 | fig3.addData(autoLine, Color.black); 297 | 298 | 299 | double[][] CheesyPath = new double[][]{ 300 | {7,16}, 301 | {11,16}, 302 | {17,28}, 303 | {22,28}, 304 | }; 305 | 306 | double totalTime = 4.5; //seconds 307 | double timeStep = 0.1; //period of control loop on Rio, seconds 308 | double robotTrackWidth = 2; //distance between left and right wheels, feet 309 | 310 | final FalconPathPlanner path = new FalconPathPlanner(CheesyPath); 311 | path.calculate(totalTime, timeStep, robotTrackWidth); 312 | 313 | //waypoint path 314 | fig3.addData(path.nodeOnlyPath,Color.blue,Color.green); 315 | 316 | //add all other paths 317 | fig3.addData(path.smoothPath, Color.red, Color.blue); 318 | fig3.addData(path.leftPath, Color.magenta); 319 | fig3.addData(path.rightPath, Color.magenta); 320 | 321 | 322 | //Velocity 323 | FalconLinePlot fig4 = new FalconLinePlot(path.smoothCenterVelocity,null,Color.blue); 324 | fig4.yGridOn(); 325 | fig4.xGridOn(); 326 | fig4.setYLabel("Velocity (ft/sec)"); 327 | fig4.setXLabel("time (seconds)"); 328 | fig4.setTitle("Velocity Profile for Left and Right Wheels \n Left = Cyan, Right = Magenta"); 329 | fig4.addData(path.smoothRightVelocity, Color.magenta); 330 | fig4.addData(path.smoothLeftVelocity, Color.cyan); 331 | 332 | } 333 | ``` 334 | 335 | 336 | 337 | Modifying the calculation paramters 338 | ================= 339 | The "smoothness" of the path is controlled by two paramters: Alpha and Beta. These variables take on default values which I believe will work in most cases and do not need to be modified. However, this section covers how to modify these paramters so that you can generate the exact path you wish to traverse. 340 | 341 | You can change these paramters individually by calling the setter methods. 342 | 343 | ```Java 344 | path.setPathAlpha(0.5); 345 | path.setPathBeta(0.9); 346 | ``` 347 | 348 | After doing-so you will need to call the recalculate menthod to re-calculate the paths using the new paramters 349 | ```Java 350 | path.calculate(totalTime, timeStep, robotTrackWidth); 351 | ``` 352 | 353 | Alpha controls how close to the original line we wish the smooth points to be. Beta controls how curvy we wish our path be. The closer Alpha is to 1, the closer the smooth path will be to the original line, the closer Beta is to 1, the larger the path radii should be. Note, these are conflicting requirements, and thus why it is an optimization algorithm. You can think of it this way, as Alpha increases it wants to push the points toward the original path, as beta increses it want to push the points away from the original line and there is a constant tug of war between them. The algorithm converges when both are happy with where the points get pushed. 354 | 355 | Alpha and Beta should be a decimal between 0 and 1. If Alpha is zero regarless of Beta, the output will be a straight line from the starting waypoint to the ending waypoint. 356 | 357 | TODO: add image of different params 358 | 359 | Caution: Depending on the combination of the variables, it is very possible to set the parameters such that the algorithm never convergers! So be careful. In general I have found the following settings to be acceptable for converging. However, they are not guranteed and you should test many datasets to be sure, the algorithm will convege to a solution in all your scenarios. If you decide to change the paramters from their defaults. 360 | 361 | If the algorithm does not converge, the program will simply never finish, so it is pretty easy to identify. 362 | 363 | In genral if you must change the parameters Set Beta between 0.1 and 0.4 (Do not set to zero), and set Alpha between 0 and 1 to get desired path output, but here is a complete list of recommended paramters. 364 | 365 | | Alpha | Beta | 366 | | ------------- |-------------: | 367 | | Any val from 0 to 1 | 0.0 | 368 | | Any val from 0 to 1 | 0.1 | 369 | | Any val from 0 to 1 | 0.2 | 370 | | Any val from 0 to 1 | 0.3 | 371 | | Any val from 0 to 1 | 0.4 | 372 | | Any val from 0 to 0.9 | 0.5 | 373 | | Any val from 0 to 0.7 | 0.6 | 374 | | Any val from 0 to 0.5 | 0.7 | 375 | | Any val from 0 to 0.3 | 0.8 | 376 | | Any val from 0 to 0.1 | 0.9 | 377 | | None | 1.0 | 378 | 379 | 380 | 381 | 382 | Real-Time Course Correction 383 | ============================ 384 | The algorithm also provides what the robots heading should be at each time stamp, based on the global coordinate system. The user can use these headings to correct the robots course. As long as the robot maintains the appropriate headings and velocities of each wheels, the robot will drive the course outlined. 385 | 386 | The current implementation of the heading is accumulated, not absolute. This is so that users can use the Gyro class already included with WPI lib (which is also accumulated) as a sensor to detect the robots actual heading and course correct. 387 | 388 | the heading information is stored in member variable `heading`, so to access it one could call 389 | 390 | ```java 391 | path.heading[i][0]; //ith time vector for path heading 392 | path.heading[i][1]; //ith point heading in degrees 393 | ``` 394 | 395 | > Note: This algorithm doesn't consider pose of the robot. What that means is it doesn't require an initial or final orientation of the robot, and does not gurantee the final orientation. Once you reach the desintation, you may need to then command your chassis to rotate to the desired final heading. 396 | 397 | 398 | Assumptions 399 | ========================= 400 | 1. Robot Chassis is a skid-steer platform 401 | 2. Ground robot is to travel on is considered to be an even plane, with no hills, ramps, bumbs, or other obstacles 402 | 3. Robot has independant speed controllers for both sides of the Robot Chassis, tuned to meet the velocity profile changes 403 | 4. Sufficient time is given for the robot to complete the path. 404 | -------------------------------------------------------------------------------- /src/usfirst/frc/team2168/robot/FalconLinePlot.java: -------------------------------------------------------------------------------- 1 | package usfirst.frc.team2168.robot; 2 | 3 | import java.awt.*; 4 | import java.awt.datatransfer.Clipboard; 5 | import java.awt.datatransfer.ClipboardOwner; 6 | import java.awt.datatransfer.DataFlavor; 7 | import java.awt.datatransfer.Transferable; 8 | import java.awt.datatransfer.UnsupportedFlavorException; 9 | import java.awt.event.ActionEvent; 10 | import java.awt.event.ActionListener; 11 | import java.awt.event.MouseAdapter; 12 | import java.awt.event.MouseEvent; 13 | import java.awt.geom.*; 14 | import java.awt.image.BufferedImage; 15 | import java.io.IOException; 16 | import java.text.DecimalFormat; 17 | import java.util.LinkedList; 18 | 19 | import javax.swing.*; 20 | 21 | /** 22 | * This class is a basic plotting class using the Java AWT interface. It has basic features which allow the user 23 | * to plot multiple graphs on one figure, control axis dimensions, and specify colors. 24 | * 25 | * This is by all means not an extensive plotter, but it will help visualize data very quickly and accurately. If 26 | * a more robust plotting function is required, the user is encouraged to use Excel or Matlab. The purpose of this 27 | * class is to be easy to use with enought automation to have nice graphs with minimal effort, but give the user 28 | * control over as much as possible, so they can generate the perfect chart. 29 | * 30 | * The plotter also features the ability to capture screen shots directly from the right-click menu, this allows 31 | * the user to copy and paste plots into reports or other documents rather quickly. 32 | * 33 | * This class holds an interface similar to that of Matlab. 34 | * 35 | * This class currently only supports scatterd line charts. 36 | * 37 | * @author Kevin Harrilal 38 | * @email kevin@team2168.org 39 | * @version 1 40 | * @date 9 Sept 2014 41 | * 42 | */ 43 | 44 | class FalconLinePlot extends JPanel implements ClipboardOwner{ 45 | 46 | 47 | private static final long serialVersionUID = 3205256608145459434L; 48 | private final int yPAD = 60; //controls how far the X- and Y- axis lines are away from the window edge 49 | private final int xPAD = 70; //controls how far the X- and Y- axis lines are away from the window edge 50 | 51 | private double upperXtic; 52 | private double lowerXtic; 53 | private double upperYtic; 54 | private double lowerYtic; 55 | private boolean yGrid; 56 | private boolean xGrid; 57 | 58 | private double yMax; 59 | private double yMin; 60 | private double xMax; 61 | private double xMin; 62 | 63 | private int yticCount; 64 | private int xticCount; 65 | private double xTicStepSize; 66 | private double yTicStepSize; 67 | 68 | boolean userSetYTic; 69 | boolean userSetXTic; 70 | 71 | private String xLabel; 72 | private String yLabel; 73 | private String titleLabel; 74 | protected static int count = 0; 75 | 76 | JPopupMenu menu = new JPopupMenu("Popup"); 77 | 78 | //Link List to hold all different plots on one graph. 79 | private LinkedList link; 80 | 81 | 82 | /** 83 | * Constructor which Plots only Y-axis data. 84 | * @param yData is a array of doubles representing the Y-axis values of the data to be plotted. 85 | */ 86 | public FalconLinePlot(double[] yData) 87 | { 88 | this(null,yData,Color.red); 89 | } 90 | 91 | public FalconLinePlot(double[] yData,Color lineColor, Color marker) 92 | { 93 | this(null,yData,lineColor,marker); 94 | } 95 | 96 | /** 97 | * Constructor which Plots chart based on provided x and y data. X and Y arrays must be of the same length. 98 | * @param xData is an array of doubles representing the X-axis values of the data to be plotted. 99 | * @param yData is an array of double representing the Y-axis values of the data to be plotted. 100 | */ 101 | public FalconLinePlot(double[] xData, double[] yData) 102 | { 103 | this(xData,yData,Color.red,null); 104 | } 105 | 106 | /** 107 | * Constructor which Plots chart based on provided x and y axis data. 108 | * @param data is a 2D array of doubles of size Nx2 or 2xN. The plot assumes X is the first dimension, and y data 109 | * is the second dimension. 110 | */ 111 | public FalconLinePlot(double[][] data) 112 | { 113 | this(getXVector(data),getYVector(data),Color.red,null); 114 | } 115 | 116 | /** 117 | * Constructor which plots charts based on provided x and y axis data in a single two dimensional array. 118 | * @param data is a 2D array of doubles of size Nx2 or 2xN. The plot assumes X is the first dimension, and y data 119 | * is the second dimension. 120 | * @param lineColor is the color the user wishes to be displayed for the line connecting each datapoint 121 | * @param markerColor is the color the user which to be used for the data point. Make this null if the user wishes to 122 | * not have datapoint markers. 123 | */ 124 | public FalconLinePlot(double[][] data, Color lineColor, Color markerColor) 125 | { 126 | this(getXVector(data),getYVector(data),lineColor,markerColor); 127 | } 128 | 129 | /** 130 | * Constructor which plots charts based on provided x and y axis data provided as separate arrays. The user can also specify the color of the adjoining line. 131 | * Data markers are not displayed. 132 | * @param xData is an array of doubles representing the X-axis values of the data to be plotted. 133 | * @param yData is an array of double representing the Y-axis values of the data to be plotted. 134 | * @param lineColor is the color the user wishes to be displayed for the line connecting each datapoint 135 | */ 136 | public FalconLinePlot(double[] xData, double[] yData,Color lineColor) 137 | { 138 | this(xData,yData,lineColor,null); 139 | } 140 | 141 | 142 | 143 | /** 144 | * Constructor which plots charts based on provided x and y axis data, provided as separate arrays. The user 145 | * can also specify the color of the adjoining line and the color of the datapoint maker. 146 | * @param xData is an array of doubles representing the X-axis values of the data to be plotted. 147 | * @param yData is an array of double representing the Y-axis values of the data to be plotted. 148 | * @param lineColor is the color the user wishes to be displayed for the line connecting each datapoint 149 | * @param markerColor is the color the user which to be used for the data point. Make this null if the user wishes to 150 | * not have datapoint markers. 151 | */ 152 | public FalconLinePlot(double[] xData, double[] yData,Color lineColor, Color markerColor) 153 | { 154 | xLabel = "X axis"; 155 | yLabel = "Y axis"; 156 | titleLabel = "Title"; 157 | 158 | upperXtic = -Double.MAX_VALUE; 159 | lowerXtic = Double.MAX_VALUE; 160 | upperYtic = -Double.MAX_VALUE; 161 | lowerYtic = Double.MAX_VALUE; 162 | xticCount = -Integer.MAX_VALUE; 163 | 164 | this.userSetXTic = false; 165 | this.userSetYTic = false; 166 | 167 | link = new LinkedList(); 168 | 169 | addData(xData, yData, lineColor,markerColor); 170 | 171 | count ++; 172 | JFrame g = new JFrame("Figure " + count); 173 | g.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 174 | g.add(this); 175 | g.setSize(600,400); 176 | g.setLocationByPlatform(true); 177 | g.setVisible(true); 178 | 179 | menu(g,this); 180 | } 181 | 182 | /** 183 | * Adds a plot to an existing figure. 184 | * @param yData is a array of doubles representing the Y-axis values of the data to be plotted. 185 | * @param color is the color the user wishes to be displayed for the line connecting each datapoint 186 | */ 187 | 188 | public void addData(double[] y, Color lineColor) 189 | { 190 | addData(y, lineColor, null); 191 | } 192 | 193 | public void addData(double[] y, Color lineColor, Color marker) 194 | { 195 | //cant add y only data unless all other data is y only data 196 | for(xyNode data: link) 197 | if(data.x != null) 198 | throw new Error ("All previous chart series need to have only Y data arrays"); 199 | 200 | addData(null,y,lineColor, marker); 201 | } 202 | 203 | public void addData(double[] x, double[] y, Color lineColor) 204 | { 205 | addData(x,y,lineColor,null); 206 | } 207 | 208 | 209 | public void addData(double[][] data, Color lineColor) 210 | { 211 | addData(getXVector(data),getYVector(data),lineColor,null); 212 | } 213 | 214 | public void addData(double[][] data, Color lineColor, Color marker) 215 | { 216 | addData(getXVector(data),getYVector(data),lineColor,marker); 217 | } 218 | 219 | public void addData(double[] x, double[] y, Color lineColor, Color marker) 220 | { 221 | xyNode Data = new xyNode(); 222 | 223 | //copy y array into node 224 | Data.y = new double[y.length]; 225 | Data.lineColor = lineColor; 226 | 227 | if(marker == null) 228 | Data.lineMarker = false; 229 | else 230 | { 231 | Data.lineMarker = true; 232 | Data.markerColor = marker; 233 | } 234 | for(int i=0; i list) 368 | { 369 | for(xyNode node: list) 370 | { 371 | double tempYMax = getMax(node.y); 372 | double tempYMin = getMin(node.y); 373 | 374 | if(tempYMinyMax) 378 | yMax=tempYMax; 379 | 380 | if(xticCount < node.y.length) 381 | xticCount = node.y.length; 382 | 383 | 384 | if(node.x != null) 385 | { 386 | double tempXMax = getMax(node.x); 387 | double tempXMin = getMin(node.x); 388 | 389 | if(tempXMinxMax) 393 | xMax=tempXMax; 394 | 395 | } 396 | else 397 | { 398 | xMax=node.y.length-1; 399 | xMin=0; 400 | 401 | } 402 | 403 | } 404 | 405 | } 406 | 407 | private double getMax(double[] data) { 408 | double max = -Double.MAX_VALUE; 409 | for(int i = 0; i < data.length; i++) { 410 | if(data[i] > max) 411 | max = data[i]; 412 | } 413 | return max; 414 | } 415 | 416 | private double getMin(double[] data) { 417 | double min = Double.MAX_VALUE; 418 | for(int i = 0; i < data.length; i++) { 419 | if(data[i] < min) 420 | min = data[i]; 421 | } 422 | return min; 423 | } 424 | 425 | public void setYLabel(String s) 426 | { 427 | yLabel = s; 428 | } 429 | 430 | public void setXLabel(String s) 431 | { 432 | xLabel = s; 433 | } 434 | 435 | public void setTitle(String s) 436 | { 437 | titleLabel = s; 438 | } 439 | 440 | private void setYLabel(Graphics2D g2, String s) 441 | { 442 | FontMetrics fm = getFontMetrics(getFont()); 443 | int width = fm.stringWidth(s); 444 | 445 | AffineTransform temp = g2.getTransform(); 446 | 447 | AffineTransform at = new AffineTransform(); 448 | at.setToRotation(-Math.PI /2, 10, getHeight()/2+width/2); 449 | g2.setTransform(at); 450 | 451 | //draw string in center of y axis 452 | g2.drawString(s, 10, 7+getHeight()/2+width/2); 453 | 454 | g2.setTransform(temp); 455 | 456 | } 457 | 458 | private void setXLabel(Graphics2D g2, String s) 459 | { 460 | FontMetrics fm = getFontMetrics(getFont()); 461 | int width = fm.stringWidth(s); 462 | 463 | g2.drawString(s, getWidth()/2-(width/2), getHeight()-10); 464 | } 465 | 466 | private void setTitle(Graphics2D g2, String s) 467 | { 468 | FontMetrics fm = getFontMetrics(getFont()); 469 | 470 | String[] line = s.split("\n"); 471 | 472 | int height = xPAD/2 - ((line.length-1) * fm.getHeight()/2); 473 | 474 | for (int i=0; i {5,1} -> {9,12} -> {12, 9} -> {15,6} -> {15,4} 87 | 88 | The units of these coordinates are position units assumed by the user (i.e inch, foot, meters) 89 | * @param path 90 | */ 91 | public FalconPathPlanner(double[][] path) 92 | { 93 | this.origPath = doubleArrayCopy(path); 94 | 95 | //default values DO NOT MODIFY; 96 | pathAlpha = 0.7; 97 | pathBeta = 0.3; 98 | pathTolerance = 0.0000001; 99 | 100 | velocityAlpha = 0.1; 101 | velocityBeta = 0.3; 102 | velocityTolerance = 0.0000001; 103 | } 104 | 105 | public static void print(double[] path) 106 | { 107 | System.out.println("X: \t Y:"); 108 | 109 | for(double u: path) 110 | System.out.println(u); 111 | } 112 | 113 | 114 | 115 | /** 116 | * Prints Cartesian Coordinates to the System Output as Column Vectors in the Form X Y 117 | * @param path 118 | */ 119 | public static void print(double[][] path) 120 | { 121 | System.out.println("X: \t Y:"); 122 | 123 | for(double[] u: path) 124 | System.out.println(u[0]+ "\t" +u[1]); 125 | } 126 | 127 | /** 128 | * Performs a deep copy of a 2 Dimensional Array looping thorough each element in the 2D array 129 | * 130 | * BigO: Order N x M 131 | * @param arr 132 | * @return 133 | */ 134 | public static double[][] doubleArrayCopy(double[][] arr) 135 | { 136 | 137 | //size first dimension of array 138 | double[][] temp = new double[arr.length][arr[0].length]; 139 | 140 | for(int i=0; i= tolerance) 222 | { 223 | change = 0.0; 224 | for(int i=1; i li = new LinkedList(); 249 | 250 | //save first value 251 | li.add(path[0]); 252 | 253 | //find intermediate nodes 254 | for(int i=1; i=0.01) 262 | li.add(path[i]); 263 | } 264 | 265 | //save last 266 | li.add(path[path.length-1]); 267 | 268 | //re-write nodes into new 2D Array 269 | double[][] temp = new double[li.size()][2]; 270 | 271 | for (int i = 0; i < li.size(); i++) 272 | { 273 | temp[i][0] = li.get(i)[0]; 274 | temp[i][1] = li.get(i)[1]; 275 | } 276 | 277 | return temp; 278 | } 279 | 280 | 281 | /** 282 | * Returns Velocity as a double array. The First Column vector is time, based on the time step, the second vector 283 | * is the velocity magnitude. 284 | * 285 | * BigO: order N 286 | * @param smoothPath 287 | * @param timeStep 288 | * @return 289 | */ 290 | double[][] velocity(double[][] smoothPath, double timeStep) 291 | { 292 | double[] dxdt = new double[smoothPath.length]; 293 | double[] dydt = new double[smoothPath.length]; 294 | double[][] velocity = new double[smoothPath.length][2]; 295 | 296 | //set first instance to zero 297 | dxdt[0]=0; 298 | dydt[0]=0; 299 | velocity[0][0]=0; 300 | velocity[0][1]=0; 301 | heading[0][1]=0; 302 | 303 | for(int i=1; i tolerance) 362 | { 363 | increase = difference[difference.length-1]/1/50; 364 | 365 | for(int i=1;ioldPointsTotal) 453 | { 454 | first=i; 455 | second=j; 456 | numFinalPoints=pointsTotal; 457 | oldPointsTotal=pointsTotal; 458 | } 459 | } 460 | 461 | ret = new int[] {first, second, third}; 462 | } 463 | else 464 | { 465 | 466 | double pointsFirst = 0; 467 | double pointsSecond = 0; 468 | double pointsTotal = 0; 469 | 470 | for (int i=1; i<=5; i++) 471 | for (int j=1; j<=8; j++) 472 | for (int k=1; k<8; k++) 473 | { 474 | pointsFirst = i *(numNodeOnlyPoints-1) + numNodeOnlyPoints; 475 | pointsSecond = (j*(pointsFirst-1)+pointsFirst); 476 | pointsTotal = (k*(pointsSecond-1)+pointsSecond); 477 | 478 | if(pointsTotal<=totalPoints) 479 | { 480 | first=i; 481 | second=j; 482 | third=k; 483 | numFinalPoints=pointsTotal; 484 | } 485 | } 486 | 487 | ret = new int[] {first, second, third}; 488 | } 489 | 490 | 491 | return ret; 492 | } 493 | /** 494 | * Calculates the left and right wheel paths based on robot track width 495 | * 496 | * Big O: 2N 497 | * 498 | * @param smoothPath - center smooth path of robot 499 | * @param robotTrackWidth - width between left and right wheels of robot of skid steer chassis. 500 | */ 501 | public void leftRight(double[][] smoothPath, double robotTrackWidth) 502 | { 503 | 504 | double[][] leftPath = new double[smoothPath.length][2]; 505 | double[][] rightPath = new double[smoothPath.length][2]; 506 | 507 | double[][] gradient = new double[smoothPath.length][2]; 508 | 509 | for(int i = 0; i0) 529 | { 530 | if((deg-gradient[i-1][1])>180) 531 | gradient[i][1] = -360+deg; 532 | 533 | if((deg-gradient[i-1][1])<-180) 534 | gradient[i][1] = 360+deg; 535 | } 536 | 537 | 538 | 539 | } 540 | 541 | this.heading = gradient; 542 | this.leftPath = leftPath; 543 | this.rightPath = rightPath; 544 | } 545 | 546 | /** 547 | * Returns the first column of a 2D array of doubles 548 | * @param arr 2D array of doubles 549 | * @return array of doubles representing the 1st column of the initial parameter 550 | */ 551 | 552 | public static double[] getXVector(double[][] arr) 553 | { 554 | double[] temp = new double[arr.length]; 555 | 556 | for(int i=0; i