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 org.wpilib.math.controller; 006 007import static org.wpilib.math.autodiff.Variable.cos; 008import static org.wpilib.math.autodiff.Variable.signum; 009 010import org.wpilib.math.autodiff.Gradient; 011import org.wpilib.math.autodiff.Hessian; 012import org.wpilib.math.autodiff.NumericalIntegration; 013import org.wpilib.math.autodiff.Variable; 014import org.wpilib.math.autodiff.VariableMatrix; 015import org.wpilib.math.autodiff.VariablePool; 016import org.wpilib.math.controller.proto.ArmFeedforwardProto; 017import org.wpilib.math.controller.struct.ArmFeedforwardStruct; 018import org.wpilib.util.protobuf.ProtobufSerializable; 019import org.wpilib.util.struct.StructSerializable; 020 021/** 022 * A helper class that computes feedforward outputs for a simple arm (modeled as a motor acting 023 * against the force of gravity on a beam suspended at an angle). 024 */ 025public class ArmFeedforward implements ProtobufSerializable, StructSerializable { 026 /** The static gain, in volts. */ 027 private double ks; 028 029 /** The gravity gain, in volts. */ 030 private double kg; 031 032 /** The velocity gain, in V/(rad/s). */ 033 private double kv; 034 035 /** The acceleration gain, in V/(rad/s²). */ 036 private double ka; 037 038 /** The period, in seconds. */ 039 private final double m_dt; 040 041 /** 042 * Creates a new ArmFeedforward with the specified gains and period. 043 * 044 * @param ks The static gain in volts. 045 * @param kg The gravity gain in volts. 046 * @param kv The velocity gain in V/(rad/s). 047 * @param ka The acceleration gain in V/(rad/s²). 048 * @param dt The period in seconds. 049 * @throws IllegalArgumentException for kv < zero. 050 * @throws IllegalArgumentException for ka < zero. 051 * @throws IllegalArgumentException for period ≤ zero. 052 */ 053 public ArmFeedforward(double ks, double kg, double kv, double ka, double dt) { 054 this.ks = ks; 055 this.kg = kg; 056 this.kv = kv; 057 this.ka = ka; 058 if (kv < 0.0) { 059 throw new IllegalArgumentException("kv must be a non-negative number, got " + kv + "!"); 060 } 061 if (ka < 0.0) { 062 throw new IllegalArgumentException("ka must be a non-negative number, got " + ka + "!"); 063 } 064 if (dt <= 0.0) { 065 throw new IllegalArgumentException("period must be a positive number, got " + dt + "!"); 066 } 067 m_dt = dt; 068 } 069 070 /** 071 * Creates a new ArmFeedforward with the specified gains. The period is defaulted to 20 ms. 072 * 073 * @param ks The static gain in volts. 074 * @param kg The gravity gain in volts. 075 * @param kv The velocity gain in V/(rad/s). 076 * @param ka The acceleration gain in V/(rad/s²). 077 * @throws IllegalArgumentException for kv < zero. 078 * @throws IllegalArgumentException for ka < zero. 079 */ 080 public ArmFeedforward(double ks, double kg, double kv, double ka) { 081 this(ks, kg, kv, ka, 0.020); 082 } 083 084 /** 085 * Creates a new ArmFeedforward with the specified gains. The period is defaulted to 20 ms. 086 * 087 * @param ks The static gain in volts. 088 * @param kg The gravity gain in volts. 089 * @param kv The velocity gain in V/(rad/s). 090 * @throws IllegalArgumentException for kv < zero. 091 */ 092 public ArmFeedforward(double ks, double kg, double kv) { 093 this(ks, kg, kv, 0); 094 } 095 096 /** 097 * Sets the static gain. 098 * 099 * @param ks The static gain in volts. 100 */ 101 public void setKs(double ks) { 102 this.ks = ks; 103 } 104 105 /** 106 * Sets the gravity gain. 107 * 108 * @param kg The gravity gain in volts. 109 */ 110 public void setKg(double kg) { 111 this.kg = kg; 112 } 113 114 /** 115 * Sets the velocity gain. 116 * 117 * @param kv The velocity gain in V/(rad/s). 118 */ 119 public void setKv(double kv) { 120 this.kv = kv; 121 } 122 123 /** 124 * Sets the acceleration gain. 125 * 126 * @param ka The acceleration gain in V/(rad/s²). 127 */ 128 public void setKa(double ka) { 129 this.ka = ka; 130 } 131 132 /** 133 * Returns the static gain in volts. 134 * 135 * @return The static gain in volts. 136 */ 137 public double getKs() { 138 return ks; 139 } 140 141 /** 142 * Returns the gravity gain in volts. 143 * 144 * @return The gravity gain in volts. 145 */ 146 public double getKg() { 147 return kg; 148 } 149 150 /** 151 * Returns the velocity gain in V/(rad/s). 152 * 153 * @return The velocity gain. 154 */ 155 public double getKv() { 156 return kv; 157 } 158 159 /** 160 * Returns the acceleration gain in V/(rad/s²). 161 * 162 * @return The acceleration gain. 163 */ 164 public double getKa() { 165 return ka; 166 } 167 168 /** 169 * Returns the period in seconds. 170 * 171 * @return The period in seconds. 172 */ 173 public double getDt() { 174 return m_dt; 175 } 176 177 /** 178 * Calculates the feedforward from the gains and velocity setpoint assuming continuous control 179 * (acceleration is assumed to be zero). 180 * 181 * @param position The position setpoint in radians. This angle should be measured from the 182 * horizontal (i.e. if the provided angle is 0, the arm should be parallel with the floor). If 183 * your encoder does not follow this convention, an offset should be added. 184 * @param velocity The velocity setpoint in radians per second. 185 * @return The computed feedforward. 186 */ 187 public double calculate(double position, double velocity) { 188 return ks * Math.signum(velocity) + kg * Math.cos(position) + kv * velocity; 189 } 190 191 /** 192 * Calculates the feedforward from the gains and setpoints assuming continuous control. 193 * 194 * @param currentAngle The current angle in radians. This angle should be measured from the 195 * horizontal (i.e. if the provided angle is 0, the arm should be parallel to the floor). If 196 * your encoder does not follow this convention, an offset should be added. 197 * @param currentVelocity The current velocity setpoint in radians per second. 198 * @param nextVelocity The next velocity setpoint in radians per second. 199 * @return The computed feedforward in volts. 200 */ 201 public double calculate(double currentAngle, double currentVelocity, double nextVelocity) { 202 // Small kₐ values make the solver ill-conditioned 203 if (ka < 1e-1) { 204 double acceleration = (nextVelocity - currentVelocity) / m_dt; 205 return ks * Math.signum(currentVelocity) 206 + kv * currentVelocity 207 + ka * acceleration 208 + kg * Math.cos(currentAngle); 209 } 210 211 try (var pool = new VariablePool()) { 212 // Arm dynamics 213 var A = new VariableMatrix(new double[][] {{0.0, 1.0}, {0.0, -kv / ka}}); 214 var B = new VariableMatrix(new double[][] {{0.0}, {1.0 / ka}}); 215 216 var r_k = new VariableMatrix(new double[][] {{currentAngle}, {currentVelocity}}); 217 218 var u_k = new Variable(); 219 220 // Initial guess 221 double acceleration = (nextVelocity - currentVelocity) / m_dt; 222 u_k.setValue( 223 ks * Math.signum(currentVelocity) 224 + kv * currentVelocity 225 + ka * acceleration 226 + kg * Math.cos(currentAngle)); 227 228 var r_k1 = 229 NumericalIntegration.rk4( 230 (VariableMatrix x, VariableMatrix u) -> { 231 var c = 232 new VariableMatrix( 233 new Variable[][] { 234 {new Variable(0.0)}, 235 {signum(x.get(1)).times(-ks / ka).plus(cos(x.get(0)).times(-kg / ka))} 236 }); 237 return A.times(x).plus(B.times(u)).plus(c); 238 }, 239 r_k, 240 new VariableMatrix(u_k), 241 m_dt); 242 243 // Minimize difference between desired and actual next velocity 244 var cost = 245 new Variable(nextVelocity) 246 .minus(r_k1.get(1)) 247 .times(new Variable(nextVelocity).minus(r_k1.get(1))); 248 249 // Refine solution via Newton's method 250 { 251 var xAD = u_k; 252 double x = xAD.value(); 253 254 var gradientF = new Gradient(cost, xAD); 255 var g = gradientF.value(); 256 257 var hessianF = new Hessian(cost, xAD); 258 var H = hessianF.value(); 259 260 double error_k = Double.POSITIVE_INFINITY; 261 double error_k1 = Math.abs(g.get(0, 0)); 262 263 // Loop until error stops decreasing or max iterations is reached 264 for (int iteration = 0; iteration < 50 && error_k1 < (1.0 - 1e-10) * error_k; ++iteration) { 265 error_k = error_k1; 266 267 // Iterate via Newton's method. 268 // 269 // xₖ₊₁ = xₖ − H⁻¹g 270 // 271 // The Hessian is regularized to at least 1e-4. 272 double p_x = -g.get(0, 0) / Math.max(H.get(0, 0), 1e-4); 273 274 // Shrink step until cost goes down 275 { 276 double oldCost = cost.value(); 277 278 double α = 1.0; 279 double trial_x = x + α * p_x; 280 281 xAD.setValue(trial_x); 282 283 while (cost.value() > oldCost) { 284 α *= 0.5; 285 trial_x = x + α * p_x; 286 287 xAD.setValue(trial_x); 288 } 289 290 x = trial_x; 291 } 292 293 xAD.setValue(x); 294 295 g = gradientF.value(); 296 H = hessianF.value(); 297 298 error_k1 = Math.abs(g.get(0, 0)); 299 } 300 301 hessianF.close(); 302 gradientF.close(); 303 } 304 305 return u_k.value(); 306 } 307 } 308 309 // Rearranging the main equation from the calculate() method yields the 310 // formulas for the methods below: 311 312 /** 313 * Calculates the maximum achievable velocity given a maximum voltage supply, a position, and an 314 * acceleration. Useful for ensuring that velocity and acceleration constraints for a trapezoidal 315 * profile are simultaneously achievable - enter the acceleration constraint, and this will give 316 * you a simultaneously-achievable velocity constraint. 317 * 318 * @param maxVoltage The maximum voltage that can be supplied to the arm. 319 * @param angle The angle of the arm, in radians. This angle should be measured from the 320 * horizontal (i.e. if the provided angle is 0, the arm should be parallel with the floor). If 321 * your encoder does not follow this convention, an offset should be added. 322 * @param acceleration The acceleration of the arm, in (rad/s²). 323 * @return The maximum possible velocity in (rad/s) at the given acceleration and angle. 324 */ 325 public double maxAchievableVelocity(double maxVoltage, double angle, double acceleration) { 326 // Assume max velocity is positive 327 return (maxVoltage - ks - Math.cos(angle) * kg - acceleration * ka) / kv; 328 } 329 330 /** 331 * Calculates the minimum achievable velocity given a maximum voltage supply, a position, and an 332 * acceleration. Useful for ensuring that velocity and acceleration constraints for a trapezoidal 333 * profile are simultaneously achievable - enter the acceleration constraint, and this will give 334 * you a simultaneously-achievable velocity constraint. 335 * 336 * @param maxVoltage The maximum voltage that can be supplied to the arm, in volts. 337 * @param angle The angle of the arm, in radians. This angle should be measured from the 338 * horizontal (i.e. if the provided angle is 0, the arm should be parallel with the floor). If 339 * your encoder does not follow this convention, an offset should be added. 340 * @param acceleration The acceleration of the arm, in (rad/s²). 341 * @return The minimum possible velocity in (rad/s) at the given acceleration and angle. 342 */ 343 public double minAchievableVelocity(double maxVoltage, double angle, double acceleration) { 344 // Assume min velocity is negative, ks flips sign 345 return (-maxVoltage + ks - Math.cos(angle) * kg - acceleration * ka) / kv; 346 } 347 348 /** 349 * Calculates the maximum achievable acceleration given a maximum voltage supply, a position, and 350 * a velocity. Useful for ensuring that velocity and acceleration constraints for a trapezoidal 351 * profile are simultaneously achievable - enter the velocity constraint, and this will give you a 352 * simultaneously-achievable acceleration constraint. 353 * 354 * @param maxVoltage The maximum voltage that can be supplied to the arm, in volts. 355 * @param angle The angle of the arm, in radians. This angle should be measured from the 356 * horizontal (i.e. if the provided angle is 0, the arm should be parallel with the floor). If 357 * your encoder does not follow this convention, an offset should be added. 358 * @param velocity The velocity of the elevator, in (rad/s) 359 * @return The maximum possible acceleration in (rad/s²) at the given velocity. 360 */ 361 public double maxAchievableAcceleration(double maxVoltage, double angle, double velocity) { 362 return (maxVoltage - ks * Math.signum(velocity) - Math.cos(angle) * kg - velocity * kv) / ka; 363 } 364 365 /** 366 * Calculates the minimum achievable acceleration given a maximum voltage supply, a position, and 367 * a velocity. Useful for ensuring that velocity and acceleration constraints for a trapezoidal 368 * profile are simultaneously achievable - enter the velocity constraint, and this will give you a 369 * simultaneously-achievable acceleration constraint. 370 * 371 * @param maxVoltage The maximum voltage that can be supplied to the arm, in volts. 372 * @param angle The angle of the arm, in radians. This angle should be measured from the 373 * horizontal (i.e. if the provided angle is 0, the arm should be parallel with the floor). If 374 * your encoder does not follow this convention, an offset should be added. 375 * @param velocity The velocity of the elevator, in (rad/s) 376 * @return The maximum possible acceleration in (rad/s²) at the given velocity. 377 */ 378 public double minAchievableAcceleration(double maxVoltage, double angle, double velocity) { 379 return maxAchievableAcceleration(-maxVoltage, angle, velocity); 380 } 381 382 /** Arm feedforward struct for serialization. */ 383 public static final ArmFeedforwardStruct struct = new ArmFeedforwardStruct(); 384 385 /** Arm feedforward protobuf for serialization. */ 386 public static final ArmFeedforwardProto proto = new ArmFeedforwardProto(); 387}