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 &lt; zero.
078   * @throws IllegalArgumentException for ka &lt; 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 &lt; 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}