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   * @deprecated Use {@link #calculateWithVelocities(double, double, double)} instead
180   */
181  @Deprecated(forRemoval = true, since = "2025")
182  public double calculate(
183      double positionRadians, double velocityRadPerSec, double accelRadPerSecSquared) {
184    return ks * Math.signum(velocityRadPerSec)
185        + kg * Math.cos(positionRadians)
186        + kv * velocityRadPerSec
187        + ka * accelRadPerSecSquared;
188  }
189
190  /**
191   * Calculates the feedforward from the gains and velocity setpoint assuming continuous control
192   * (acceleration is assumed to be zero).
193   *
194   * @param positionRadians The position (angle) setpoint. This angle should be measured from the
195   *     horizontal (i.e. if the provided angle is 0, the arm should be parallel with the floor). If
196   *     your encoder does not follow this convention, an offset should be added.
197   * @param velocity The velocity setpoint.
198   * @return The computed feedforward.
199   */
200  public double calculate(double positionRadians, double velocity) {
201    return calculate(positionRadians, velocity, 0);
202  }
203
204  /**
205   * Calculates the feedforward from the gains and setpoints assuming continuous control.
206   *
207   * @param currentAngle The current angle in radians. This angle should be measured from the
208   *     horizontal (i.e. if the provided angle is 0, the arm should be parallel to the floor). If
209   *     your encoder does not follow this convention, an offset should be added.
210   * @param currentVelocity The current velocity setpoint in radians per second.
211   * @param nextVelocity The next velocity setpoint in radians per second.
212   * @param dt Time between velocity setpoints in seconds.
213   * @return The computed feedforward in volts.
214   * @deprecated Use {@link #calculateWithVelocities(double, double, double)} instead.
215   */
216  @SuppressWarnings("removal")
217  @Deprecated(forRemoval = true, since = "2025")
218  public double calculate(
219      double currentAngle, double currentVelocity, double nextVelocity, double dt) {
220    return ArmFeedforwardJNI.calculate(
221        ks, kv, ka, kg, currentAngle, currentVelocity, nextVelocity, dt);
222  }
223
224  /**
225   * Calculates the feedforward from the gains and setpoints assuming discrete control.
226   *
227   * @param currentAngle The current angle in radians. This angle should be measured from the
228   *     horizontal (i.e. if the provided angle is 0, the arm should be parallel to the floor). If
229   *     your encoder does not follow this convention, an offset should be added.
230   * @param currentVelocity The current velocity setpoint in radians per second.
231   * @param nextVelocity The next velocity setpoint in radians per second.
232   * @return The computed feedforward in volts.
233   */
234  public double calculateWithVelocities(
235      double currentAngle, double currentVelocity, double nextVelocity) {
236    return ArmFeedforwardJNI.calculate(
237        ks, kv, ka, kg, currentAngle, currentVelocity, nextVelocity, m_dt);
238  }
239
240  // Rearranging the main equation from the calculate() method yields the
241  // formulas for the methods below:
242
243  /**
244   * Calculates the maximum achievable velocity given a maximum voltage supply, a position, and an
245   * acceleration. Useful for ensuring that velocity and acceleration constraints for a trapezoidal
246   * profile are simultaneously achievable - enter the acceleration constraint, and this will give
247   * you a simultaneously-achievable velocity constraint.
248   *
249   * @param maxVoltage The maximum voltage that can be supplied to the arm.
250   * @param angle The angle of the arm, in radians. This angle should be measured from the
251   *     horizontal (i.e. if the provided angle is 0, the arm should be parallel with the floor). If
252   *     your encoder does not follow this convention, an offset should be added.
253   * @param acceleration The acceleration of the arm, in (rad/s²).
254   * @return The maximum possible velocity in (rad/s) at the given acceleration and angle.
255   */
256  public double maxAchievableVelocity(double maxVoltage, double angle, double acceleration) {
257    // Assume max velocity is positive
258    return (maxVoltage - ks - Math.cos(angle) * kg - acceleration * ka) / kv;
259  }
260
261  /**
262   * Calculates the minimum achievable velocity given a maximum voltage supply, a position, and an
263   * acceleration. Useful for ensuring that velocity and acceleration constraints for a trapezoidal
264   * profile are simultaneously achievable - enter the acceleration constraint, and this will give
265   * you a simultaneously-achievable velocity constraint.
266   *
267   * @param maxVoltage The maximum voltage that can be supplied to the arm, in volts.
268   * @param angle The angle of the arm, in radians. This angle should be measured from the
269   *     horizontal (i.e. if the provided angle is 0, the arm should be parallel with the floor). If
270   *     your encoder does not follow this convention, an offset should be added.
271   * @param acceleration The acceleration of the arm, in (rad/s²).
272   * @return The minimum possible velocity in (rad/s) at the given acceleration and angle.
273   */
274  public double minAchievableVelocity(double maxVoltage, double angle, double acceleration) {
275    // Assume min velocity is negative, ks flips sign
276    return (-maxVoltage + ks - Math.cos(angle) * kg - acceleration * ka) / kv;
277  }
278
279  /**
280   * Calculates the maximum achievable acceleration given a maximum voltage supply, a position, and
281   * a velocity. Useful for ensuring that velocity and acceleration constraints for a trapezoidal
282   * profile are simultaneously achievable - enter the velocity constraint, and this will give you a
283   * simultaneously-achievable acceleration constraint.
284   *
285   * @param maxVoltage The maximum voltage that can be supplied to the arm, in volts.
286   * @param angle The angle of the arm, in radians. This angle should be measured from the
287   *     horizontal (i.e. if the provided angle is 0, the arm should be parallel with the floor). If
288   *     your encoder does not follow this convention, an offset should be added.
289   * @param velocity The velocity of the elevator, in (rad/s)
290   * @return The maximum possible acceleration in (rad/s²) at the given velocity.
291   */
292  public double maxAchievableAcceleration(double maxVoltage, double angle, double velocity) {
293    return (maxVoltage - ks * Math.signum(velocity) - Math.cos(angle) * kg - velocity * kv) / ka;
294  }
295
296  /**
297   * Calculates the minimum achievable acceleration given a maximum voltage supply, a position, and
298   * a velocity. Useful for ensuring that velocity and acceleration constraints for a trapezoidal
299   * profile are simultaneously achievable - enter the velocity constraint, and this will give you a
300   * simultaneously-achievable acceleration constraint.
301   *
302   * @param maxVoltage The maximum voltage that can be supplied to the arm, in volts.
303   * @param angle The angle of the arm, in radians. This angle should be measured from the
304   *     horizontal (i.e. if the provided angle is 0, the arm should be parallel with the floor). If
305   *     your encoder does not follow this convention, an offset should be added.
306   * @param velocity The velocity of the elevator, in (rad/s)
307   * @return The maximum possible acceleration in (rad/s²) at the given velocity.
308   */
309  public double minAchievableAcceleration(double maxVoltage, double angle, double velocity) {
310    return maxAchievableAcceleration(-maxVoltage, angle, velocity);
311  }
312
313  /** Arm feedforward struct for serialization. */
314  public static final ArmFeedforwardStruct struct = new ArmFeedforwardStruct();
315
316  /** Arm feedforward protobuf for serialization. */
317  public static final ArmFeedforwardProto proto = new ArmFeedforwardProto();
318}