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.controller;
006
007import edu.wpi.first.math.controller.proto.ArmFeedforwardProto;
008import edu.wpi.first.math.controller.struct.ArmFeedforwardStruct;
009import edu.wpi.first.math.jni.ArmFeedforwardJNI;
010import edu.wpi.first.util.protobuf.ProtobufSerializable;
011import edu.wpi.first.util.struct.StructSerializable;
012
013/**
014 * A helper class that computes feedforward outputs for a simple arm (modeled as a motor acting
015 * against the force of gravity on a beam suspended at an angle).
016 */
017public class ArmFeedforward implements ProtobufSerializable, StructSerializable {
018  /** The static gain, in volts. */
019  private double ks;
020
021  /** The gravity gain, in volts. */
022  private double kg;
023
024  /** The velocity gain, in V/(rad/s). */
025  private double kv;
026
027  /** The acceleration gain, in V/(rad/s²). */
028  private double ka;
029
030  /** The period, in seconds. */
031  private final double m_dt;
032
033  /**
034   * Creates a new ArmFeedforward with the specified gains and period.
035   *
036   * @param ks The static gain in volts.
037   * @param kg The gravity gain in volts.
038   * @param kv The velocity gain in V/(rad/s).
039   * @param ka The acceleration gain in V/(rad/s²).
040   * @param dtSeconds The period in seconds.
041   * @throws IllegalArgumentException for kv < zero.
042   * @throws IllegalArgumentException for ka < zero.
043   * @throws IllegalArgumentException for period ≤ zero.
044   */
045  public ArmFeedforward(double ks, double kg, double kv, double ka, double dtSeconds) {
046    this.ks = ks;
047    this.kg = kg;
048    this.kv = kv;
049    this.ka = ka;
050    if (kv < 0.0) {
051      throw new IllegalArgumentException("kv must be a non-negative number, got " + kv + "!");
052    }
053    if (ka < 0.0) {
054      throw new IllegalArgumentException("ka must be a non-negative number, got " + ka + "!");
055    }
056    if (dtSeconds <= 0.0) {
057      throw new IllegalArgumentException(
058          "period must be a positive number, got " + dtSeconds + "!");
059    }
060    m_dt = dtSeconds;
061  }
062
063  /**
064   * Creates a new ArmFeedforward with the specified gains. The period is defaulted to 20 ms.
065   *
066   * @param ks The static gain in volts.
067   * @param kg The gravity gain in volts.
068   * @param kv The velocity gain in V/(rad/s).
069   * @param ka The acceleration gain in V/(rad/s²).
070   * @throws IllegalArgumentException for kv &lt; zero.
071   * @throws IllegalArgumentException for ka &lt; zero.
072   */
073  public ArmFeedforward(double ks, double kg, double kv, double ka) {
074    this(ks, kg, kv, ka, 0.020);
075  }
076
077  /**
078   * Creates a new ArmFeedforward with the specified gains. The period is defaulted to 20 ms.
079   *
080   * @param ks The static gain in volts.
081   * @param kg The gravity gain in volts.
082   * @param kv The velocity gain in V/(rad/s).
083   * @throws IllegalArgumentException for kv &lt; zero.
084   */
085  public ArmFeedforward(double ks, double kg, double kv) {
086    this(ks, kg, kv, 0);
087  }
088
089  /**
090   * Sets the static gain.
091   *
092   * @param ks The static gain in volts.
093   */
094  public void setKs(double ks) {
095    this.ks = ks;
096  }
097
098  /**
099   * Sets the gravity gain.
100   *
101   * @param kg The gravity gain in volts.
102   */
103  public void setKg(double kg) {
104    this.kg = kg;
105  }
106
107  /**
108   * Sets the velocity gain.
109   *
110   * @param kv The velocity gain in V/(rad/s).
111   */
112  public void setKv(double kv) {
113    this.kv = kv;
114  }
115
116  /**
117   * Sets the acceleration gain.
118   *
119   * @param ka The acceleration gain in V/(rad/s²).
120   */
121  public void setKa(double ka) {
122    this.ka = ka;
123  }
124
125  /**
126   * Returns the static gain in volts.
127   *
128   * @return The static gain in volts.
129   */
130  public double getKs() {
131    return ks;
132  }
133
134  /**
135   * Returns the gravity gain in volts.
136   *
137   * @return The gravity gain in volts.
138   */
139  public double getKg() {
140    return kg;
141  }
142
143  /**
144   * Returns the velocity gain in V/(rad/s).
145   *
146   * @return The velocity gain.
147   */
148  public double getKv() {
149    return kv;
150  }
151
152  /**
153   * Returns the acceleration gain in V/(rad/s²).
154   *
155   * @return The acceleration gain.
156   */
157  public double getKa() {
158    return ka;
159  }
160
161  /**
162   * Returns the period in seconds.
163   *
164   * @return The period in seconds.
165   */
166  public double getDt() {
167    return m_dt;
168  }
169
170  /**
171   * Calculates the feedforward from the gains and setpoints.
172   *
173   * @param positionRadians The position (angle) setpoint. This angle should be measured from the
174   *     horizontal (i.e. if the provided angle is 0, the arm should be parallel with the floor). If
175   *     your encoder does not follow this convention, an offset should be added.
176   * @param velocityRadPerSec The velocity setpoint.
177   * @param accelRadPerSecSquared The acceleration setpoint.
178   * @return The computed feedforward.
179   */
180  @Deprecated(forRemoval = true, since = "2025")
181  public double calculate(
182      double positionRadians, double velocityRadPerSec, double accelRadPerSecSquared) {
183    return ks * Math.signum(velocityRadPerSec)
184        + kg * Math.cos(positionRadians)
185        + kv * velocityRadPerSec
186        + ka * accelRadPerSecSquared;
187  }
188
189  /**
190   * Calculates the feedforward from the gains and velocity setpoint assuming continuous control
191   * (acceleration is assumed to be zero).
192   *
193   * @param positionRadians The position (angle) setpoint. This angle should be measured from the
194   *     horizontal (i.e. if the provided angle is 0, the arm should be parallel with the floor). If
195   *     your encoder does not follow this convention, an offset should be added.
196   * @param velocity The velocity setpoint.
197   * @return The computed feedforward.
198   */
199  public double calculate(double positionRadians, double velocity) {
200    return calculate(positionRadians, velocity, 0);
201  }
202
203  /**
204   * Calculates the feedforward from the gains and setpoints assuming continuous control.
205   *
206   * @param currentAngle The current angle in radians. This angle should be measured from the
207   *     horizontal (i.e. if the provided angle is 0, the arm should be parallel to the floor). If
208   *     your encoder does not follow this convention, an offset should be added.
209   * @param currentVelocity The current velocity setpoint in radians per second.
210   * @param nextVelocity The next velocity setpoint in radians per second.
211   * @param dt Time between velocity setpoints in seconds.
212   * @return The computed feedforward in volts.
213   */
214  @SuppressWarnings("removal")
215  @Deprecated(forRemoval = true, since = "2025")
216  public double calculate(
217      double currentAngle, double currentVelocity, double nextVelocity, double dt) {
218    return ArmFeedforwardJNI.calculate(
219        ks, kv, ka, kg, currentAngle, currentVelocity, nextVelocity, dt);
220  }
221
222  /**
223   * Calculates the feedforward from the gains and setpoints assuming discrete control.
224   *
225   * @param currentAngle The current angle in radians. This angle should be measured from the
226   *     horizontal (i.e. if the provided angle is 0, the arm should be parallel to the floor). If
227   *     your encoder does not follow this convention, an offset should be added.
228   * @param currentVelocity The current velocity setpoint in radians per second.
229   * @param nextVelocity The next velocity setpoint in radians per second.
230   * @return The computed feedforward in volts.
231   */
232  public double calculateWithVelocities(
233      double currentAngle, double currentVelocity, double nextVelocity) {
234    return ArmFeedforwardJNI.calculate(
235        ks, kv, ka, kg, currentAngle, currentVelocity, nextVelocity, m_dt);
236  }
237
238  // Rearranging the main equation from the calculate() method yields the
239  // formulas for the methods below:
240
241  /**
242   * Calculates the maximum achievable velocity given a maximum voltage supply, a position, and an
243   * acceleration. Useful for ensuring that velocity and acceleration constraints for a trapezoidal
244   * profile are simultaneously achievable - enter the acceleration constraint, and this will give
245   * you a simultaneously-achievable velocity constraint.
246   *
247   * @param maxVoltage The maximum voltage that can be supplied to the arm.
248   * @param angle The angle of the arm, in radians. This angle should be measured from the
249   *     horizontal (i.e. if the provided angle is 0, the arm should be parallel with the floor). If
250   *     your encoder does not follow this convention, an offset should be added.
251   * @param acceleration The acceleration of the arm, in (rad/s²).
252   * @return The maximum possible velocity in (rad/s) at the given acceleration and angle.
253   */
254  public double maxAchievableVelocity(double maxVoltage, double angle, double acceleration) {
255    // Assume max velocity is positive
256    return (maxVoltage - ks - Math.cos(angle) * kg - acceleration * ka) / kv;
257  }
258
259  /**
260   * Calculates the minimum achievable velocity given a maximum voltage supply, a position, and an
261   * acceleration. Useful for ensuring that velocity and acceleration constraints for a trapezoidal
262   * profile are simultaneously achievable - enter the acceleration constraint, and this will give
263   * you a simultaneously-achievable velocity constraint.
264   *
265   * @param maxVoltage The maximum voltage that can be supplied to the arm, in volts.
266   * @param angle The angle of the arm, in radians. This angle should be measured from the
267   *     horizontal (i.e. if the provided angle is 0, the arm should be parallel with the floor). If
268   *     your encoder does not follow this convention, an offset should be added.
269   * @param acceleration The acceleration of the arm, in (rad/s²).
270   * @return The minimum possible velocity in (rad/s) at the given acceleration and angle.
271   */
272  public double minAchievableVelocity(double maxVoltage, double angle, double acceleration) {
273    // Assume min velocity is negative, ks flips sign
274    return (-maxVoltage + ks - Math.cos(angle) * kg - acceleration * ka) / kv;
275  }
276
277  /**
278   * Calculates the maximum achievable acceleration given a maximum voltage supply, a position, and
279   * a velocity. Useful for ensuring that velocity and acceleration constraints for a trapezoidal
280   * profile are simultaneously achievable - enter the velocity constraint, and this will give you a
281   * simultaneously-achievable acceleration constraint.
282   *
283   * @param maxVoltage The maximum voltage that can be supplied to the arm, in volts.
284   * @param angle The angle of the arm, in radians. This angle should be measured from the
285   *     horizontal (i.e. if the provided angle is 0, the arm should be parallel with the floor). If
286   *     your encoder does not follow this convention, an offset should be added.
287   * @param velocity The velocity of the elevator, in (rad/s)
288   * @return The maximum possible acceleration in (rad/s²) at the given velocity.
289   */
290  public double maxAchievableAcceleration(double maxVoltage, double angle, double velocity) {
291    return (maxVoltage - ks * Math.signum(velocity) - Math.cos(angle) * kg - velocity * kv) / ka;
292  }
293
294  /**
295   * Calculates the minimum achievable acceleration given a maximum voltage supply, a position, and
296   * a velocity. Useful for ensuring that velocity and acceleration constraints for a trapezoidal
297   * profile are simultaneously achievable - enter the velocity constraint, and this will give you a
298   * simultaneously-achievable acceleration constraint.
299   *
300   * @param maxVoltage The maximum voltage that can be supplied to the arm, in volts.
301   * @param angle The angle of the arm, in radians. This angle should be measured from the
302   *     horizontal (i.e. if the provided angle is 0, the arm should be parallel with the floor). If
303   *     your encoder does not follow this convention, an offset should be added.
304   * @param velocity The velocity of the elevator, in (rad/s)
305   * @return The maximum possible acceleration in (rad/s²) at the given velocity.
306   */
307  public double minAchievableAcceleration(double maxVoltage, double angle, double velocity) {
308    return maxAchievableAcceleration(-maxVoltage, angle, velocity);
309  }
310
311  /** Arm feedforward struct for serialization. */
312  public static final ArmFeedforwardStruct struct = new ArmFeedforwardStruct();
313
314  /** Arm feedforward protobuf for serialization. */
315  public static final ArmFeedforwardProto proto = new ArmFeedforwardProto();
316}