From 1ec089c7f9bc8848020c01986ab045e6e6734bb7 Mon Sep 17 00:00:00 2001 From: Nicholas Armstrong Date: Sun, 28 Apr 2024 15:01:08 -0400 Subject: [PATCH] [wpimath] Add ArmFeedforward calculate() overload that takes current and next velocity instead of acceleration (#6540) Co-authored-by: Tyler Veness --- wpimath/CMakeLists.txt | 1 + .../java/edu/wpi/first/math/WPIMathJNI.java | 27 ++++ .../first/math/controller/ArmFeedforward.java | 17 +++ .../native/cpp/controller/ArmFeedforward.cpp | 100 +++++++++++++++ .../cpp/jni/WPIMathJNI_ArmFeedforward.cpp | 36 ++++++ .../include/frc/controller/ArmFeedforward.h | 17 +++ .../math/controller/ArmFeedforwardTest.java | 64 +++++++++ .../cpp/controller/ArmFeedforwardTest.cpp | 121 ++++++++++++++---- 8 files changed, 355 insertions(+), 28 deletions(-) create mode 100644 wpimath/src/main/native/cpp/controller/ArmFeedforward.cpp create mode 100644 wpimath/src/main/native/cpp/jni/WPIMathJNI_ArmFeedforward.cpp diff --git a/wpimath/CMakeLists.txt b/wpimath/CMakeLists.txt index 0d11252619..ca49610175 100644 --- a/wpimath/CMakeLists.txt +++ b/wpimath/CMakeLists.txt @@ -19,6 +19,7 @@ protobuf_generate_cpp( file( GLOB wpimath_jni_src + src/main/native/cpp/jni/WPIMathJNI_ArmFeedforward.cpp src/main/native/cpp/jni/WPIMathJNI_DARE.cpp src/main/native/cpp/jni/WPIMathJNI_Eigen.cpp src/main/native/cpp/jni/WPIMathJNI_Exceptions.cpp diff --git a/wpimath/src/main/java/edu/wpi/first/math/WPIMathJNI.java b/wpimath/src/main/java/edu/wpi/first/math/WPIMathJNI.java index a60bcbae5f..412c1b1f74 100644 --- a/wpimath/src/main/java/edu/wpi/first/math/WPIMathJNI.java +++ b/wpimath/src/main/java/edu/wpi/first/math/WPIMathJNI.java @@ -44,6 +44,33 @@ public final class WPIMathJNI { libraryLoaded = true; } + // ArmFeedforward wrappers + + /** + * Obtain a feedforward voltage from a single jointed arm feedforward object. + * + *

Constructs an ArmFeedforward object and runs its currentVelocity and nextVelocity overload + * + * @param ks The ArmFeedforward's static gain in volts. + * @param kv The ArmFeedforward's velocity gain in volt seconds per radian. + * @param ka The ArmFeedforward's acceleration gain in volt seconds² per radian. + * @param kg The ArmFeedforward's gravity gain in volts. + * @param currentAngle The current angle in the calculation in radians. + * @param currentVelocity The current velocity in the calculation in radians per second. + * @param nextVelocity The next velocity in the calculation in radians per second. + * @param dt The time between velocity setpoints in seconds. + * @return The calculated feedforward in volts. + */ + public static native double calculate( + double ks, + double kv, + double ka, + double kg, + double currentAngle, + double currentVelocity, + double nextVelocity, + double dt); + // DARE wrappers /** diff --git a/wpimath/src/main/java/edu/wpi/first/math/controller/ArmFeedforward.java b/wpimath/src/main/java/edu/wpi/first/math/controller/ArmFeedforward.java index 0d8b11b465..0d47941806 100644 --- a/wpimath/src/main/java/edu/wpi/first/math/controller/ArmFeedforward.java +++ b/wpimath/src/main/java/edu/wpi/first/math/controller/ArmFeedforward.java @@ -4,6 +4,7 @@ package edu.wpi.first.math.controller; +import edu.wpi.first.math.WPIMathJNI; import edu.wpi.first.math.controller.proto.ArmFeedforwardProto; import edu.wpi.first.math.controller.struct.ArmFeedforwardStruct; import edu.wpi.first.util.protobuf.ProtobufSerializable; @@ -100,6 +101,22 @@ public class ArmFeedforward implements ProtobufSerializable, StructSerializable return calculate(positionRadians, velocity, 0); } + /** + * Calculates the feedforward from the gains and setpoints. + * + * @param currentAngle The current angle in radians. This angle should be measured from the + * horizontal (i.e. if the provided angle is 0, the arm should be parallel to the floor). If + * your encoder does not follow this convention, an offset should be added. + * @param currentVelocity The current velocity setpoint in radians per second. + * @param nextVelocity The next velocity setpoint in radians per second. + * @param dt Time between velocity setpoints in seconds. + * @return The computed feedforward in volts. + */ + public double calculate( + double currentAngle, double currentVelocity, double nextVelocity, double dt) { + return WPIMathJNI.calculate(ks, kv, ka, kg, currentAngle, currentVelocity, nextVelocity, dt); + } + // Rearranging the main equation from the calculate() method yields the // formulas for the methods below: diff --git a/wpimath/src/main/native/cpp/controller/ArmFeedforward.cpp b/wpimath/src/main/native/cpp/controller/ArmFeedforward.cpp new file mode 100644 index 0000000000..3c677c0267 --- /dev/null +++ b/wpimath/src/main/native/cpp/controller/ArmFeedforward.cpp @@ -0,0 +1,100 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "frc/controller/ArmFeedforward.h" + +#include + +#include +#include + +#include "frc/EigenCore.h" +#include "frc/system/NumericalIntegration.h" + +using namespace frc; + +units::volt_t ArmFeedforward::Calculate(units::unit_t currentAngle, + units::unit_t currentVelocity, + units::unit_t nextVelocity, + units::second_t dt) const { + using VarMat = sleipnir::VariableMatrix; + + // Arm dynamics + Matrixd<2, 2> A{{0.0, 1.0}, {0.0, -kV.value() / kA.value()}}; + Matrixd<2, 1> B{{0.0}, {1.0 / kA.value()}}; + const auto& f = [&](const VarMat& x, const VarMat& u) -> VarMat { + VarMat c{{0.0}, + {-(kS / kA).value() * sleipnir::sign(x(1)) - + (kG / kA).value() * sleipnir::cos(x(0))}}; + return A * x + B * u + c; + }; + + Vectord<2> r_k{currentAngle.value(), currentVelocity.value()}; + + sleipnir::Variable u_k; + + // Initial guess + auto acceleration = (nextVelocity - currentVelocity) / dt; + u_k.SetValue((kS * wpi::sgn(currentVelocity.value()) + kV * currentVelocity + + kA * acceleration + kG * units::math::cos(currentAngle)) + .value()); + + auto r_k1 = RK4(f, r_k, u_k, dt); + + // Minimize difference between desired and actual next velocity + auto cost = + (nextVelocity.value() - r_k1(1)) * (nextVelocity.value() - r_k1(1)); + + // Refine solution via Newton's method + { + auto xAD = u_k; + double x = xAD.Value(); + + sleipnir::Gradient gradientF{cost, xAD}; + Eigen::SparseVector g = gradientF.Value(); + + sleipnir::Hessian hessianF{cost, xAD}; + Eigen::SparseMatrix H = hessianF.Value(); + + double error = std::numeric_limits::infinity(); + while (error > 1e-8) { + // Iterate via Newton's method. + // + // xₖ₊₁ = xₖ − H⁻¹g + // + // The Hessian is regularized to at least 1e-4. + double p_x = -g.coeff(0) / std::max(H.coeff(0, 0), 1e-4); + + // Shrink step until cost goes down + { + double oldCost = cost.Value(); + + double α = 1.0; + double trial_x = x + α * p_x; + + xAD.SetValue(trial_x); + cost.Update(); + + while (cost.Value() > oldCost) { + α *= 0.5; + trial_x = x + α * p_x; + + xAD.SetValue(trial_x); + cost.Update(); + } + + x = trial_x; + } + + xAD.SetValue(x); + + g = gradientF.Value(); + H = hessianF.Value(); + + error = std::abs(g.coeff(0)); + } + } + + return units::volt_t{u_k.Value()}; +} diff --git a/wpimath/src/main/native/cpp/jni/WPIMathJNI_ArmFeedforward.cpp b/wpimath/src/main/native/cpp/jni/WPIMathJNI_ArmFeedforward.cpp new file mode 100644 index 0000000000..3428c52688 --- /dev/null +++ b/wpimath/src/main/native/cpp/jni/WPIMathJNI_ArmFeedforward.cpp @@ -0,0 +1,36 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include + +#include + +#include "edu_wpi_first_math_WPIMathJNI.h" +#include "frc/controller/ArmFeedforward.h" + +using namespace wpi::java; + +extern "C" { + +/* + * Class: edu_wpi_first_math_WPIMathJNI + * Method: calculate + * Signature: (DDDDDDDD)D + */ +JNIEXPORT jdouble JNICALL +Java_edu_wpi_first_math_WPIMathJNI_calculate + (JNIEnv* env, jclass, jdouble ks, jdouble kv, jdouble ka, jdouble kg, + jdouble currentAngle, jdouble currentVelocity, jdouble nextVelocity, + jdouble dt) +{ + return frc::ArmFeedforward{units::volt_t{ks}, units::volt_t{kg}, + units::unit_t{kv}, + units::unit_t{ka}} + .Calculate(units::radian_t{currentAngle}, + units::radians_per_second_t{currentVelocity}, + units::radians_per_second_t{nextVelocity}, units::second_t{dt}) + .value(); +} + +} // extern "C" diff --git a/wpimath/src/main/native/include/frc/controller/ArmFeedforward.h b/wpimath/src/main/native/include/frc/controller/ArmFeedforward.h index 69091ccd9a..28713003c9 100644 --- a/wpimath/src/main/native/include/frc/controller/ArmFeedforward.h +++ b/wpimath/src/main/native/include/frc/controller/ArmFeedforward.h @@ -76,6 +76,23 @@ class WPILIB_DLLEXPORT ArmFeedforward { kV * velocity + kA * acceleration; } + /** + * Calculates the feedforward from the gains and setpoints. + * + * @param currentAngle The current angle in radians. This angle should be + * measured from the horizontal (i.e. if the provided angle is 0, the arm + * should be parallel to the floor). If your encoder does not follow this + * convention, an offset should be added. + * @param currentVelocity The current velocity setpoint in radians per second. + * @param nextVelocity The next velocity setpoint in radians per second. + * @param dt Time between velocity setpoints in seconds. + * @return The computed feedforward in volts. + */ + units::volt_t Calculate(units::unit_t currentAngle, + units::unit_t currentVelocity, + units::unit_t nextVelocity, + units::second_t dt) const; + // Rearranging the main equation from the calculate() method yields the // formulas for the methods below: diff --git a/wpimath/src/test/java/edu/wpi/first/math/controller/ArmFeedforwardTest.java b/wpimath/src/test/java/edu/wpi/first/math/controller/ArmFeedforwardTest.java index 7b5cf77380..16d0269d0e 100644 --- a/wpimath/src/test/java/edu/wpi/first/math/controller/ArmFeedforwardTest.java +++ b/wpimath/src/test/java/edu/wpi/first/math/controller/ArmFeedforwardTest.java @@ -6,6 +6,13 @@ package edu.wpi.first.math.controller; import static org.junit.jupiter.api.Assertions.assertEquals; +import edu.wpi.first.math.MatBuilder; +import edu.wpi.first.math.Matrix; +import edu.wpi.first.math.Nat; +import edu.wpi.first.math.numbers.N1; +import edu.wpi.first.math.numbers.N2; +import edu.wpi.first.math.system.NumericalIntegration; +import java.util.function.BiFunction; import org.junit.jupiter.api.Test; class ArmFeedforwardTest { @@ -15,12 +22,69 @@ class ArmFeedforwardTest { private static final double ka = 2; private final ArmFeedforward m_armFF = new ArmFeedforward(ks, kg, kv, ka); + /** + * Simulates a single-jointed arm and returns the final state. + * + * @param currentAngle The starting angle in radians. + * @param currentVelocity The starting angular velocity in radians per second. + * @param input The input voltage. + * @param dt The simulation time in seconds. + * @return The final state as a 2-vector of angle and angular velocity. + */ + private Matrix simulate( + double currentAngle, double currentVelocity, double input, double dt) { + final Matrix A = + new Matrix<>(Nat.N2(), Nat.N2(), new double[] {0.0, 1.0, 0.0, -kv / ka}); + final Matrix B = new Matrix<>(Nat.N2(), Nat.N1(), new double[] {0.0, 1.0 / ka}); + + final BiFunction, Matrix, Matrix> f = + (x, u) -> { + Matrix c = + MatBuilder.fill( + Nat.N2(), + Nat.N1(), + 0.0, + Math.signum(x.get(1, 0)) * (-ks / ka) - (kg / ka) * Math.cos(x.get(0, 0))); + return A.times(x).plus(B.times(u)).plus(c); + }; + + return NumericalIntegration.rk4( + f, + MatBuilder.fill(Nat.N2(), Nat.N1(), currentAngle, currentVelocity), + MatBuilder.fill(Nat.N1(), Nat.N1(), input), + dt); + } + + /** + * Calculates a feedforward voltage using overload taking angle, two angular velocities, and dt; + * then simulates an arm using that voltage to verify correctness. + * + * @param currentAngle The starting angle in radians. + * @param currentVelocity The starting angular velocity in radians per second. + * @param input The input voltage. + * @param dt The simulation time in seconds. + */ + private void calculateAndSimulate( + double currentAngle, double currentVelocity, double nextVelocity, double dt) { + final double input = m_armFF.calculate(currentAngle, currentVelocity, nextVelocity, dt); + assertEquals(nextVelocity, simulate(currentAngle, currentVelocity, input, dt).get(1, 0), 1e-12); + } + @Test void testCalculate() { + // calculate(angle, angular velocity) assertEquals(0.5, m_armFF.calculate(Math.PI / 3, 0), 0.002); assertEquals(2.5, m_armFF.calculate(Math.PI / 3, 1), 0.002); + + // calculate(angle, angular velocity, angular acceleration) assertEquals(6.5, m_armFF.calculate(Math.PI / 3, 1, 2), 0.002); assertEquals(2.5, m_armFF.calculate(Math.PI / 3, -1, 2), 0.002); + + // calculate(currentAngle, currentVelocity, nextAngle, dt) + calculateAndSimulate(Math.PI / 3, 1.0, 1.05, 0.020); + calculateAndSimulate(Math.PI / 3, 1.0, 0.95, 0.020); + calculateAndSimulate(-Math.PI / 3, 1.0, 1.05, 0.020); + calculateAndSimulate(-Math.PI / 3, 1.0, 0.95, 0.020); } @Test diff --git a/wpimath/src/test/native/cpp/controller/ArmFeedforwardTest.cpp b/wpimath/src/test/native/cpp/controller/ArmFeedforwardTest.cpp index b7853b19ba..615f10ae89 100644 --- a/wpimath/src/test/native/cpp/controller/ArmFeedforwardTest.cpp +++ b/wpimath/src/test/native/cpp/controller/ArmFeedforwardTest.cpp @@ -7,46 +7,111 @@ #include +#include "frc/EigenCore.h" #include "frc/controller/ArmFeedforward.h" -#include "units/acceleration.h" -#include "units/length.h" +#include "frc/system/NumericalIntegration.h" +#include "units/angular_acceleration.h" +#include "units/angular_velocity.h" #include "units/time.h" +#include "units/voltage.h" static constexpr auto Ks = 0.5_V; -static constexpr auto Kv = 1.5_V * 1_s / 1_rad; -static constexpr auto Ka = 2_V * 1_s * 1_s / 1_rad; +static constexpr auto Kv = 1.5_V / 1_rad_per_s; +static constexpr auto Ka = 2_V / 1_rad_per_s_sq; static constexpr auto Kg = 1_V; +namespace { + +/** + * Simulates a single-jointed arm and returns the final state. + * + * @param currentAngle The starting angle. + * @param currentVelocity The starting angular velocity. + * @param input The input voltage. + * @param dt The simulation time. + * @return The final state as a 2-vector of angle and angular velocity. + */ +frc::Matrixd<2, 1> Simulate(units::radian_t currentAngle, + units::radians_per_second_t currentVelocity, + units::volt_t input, units::second_t dt) { + constexpr frc::Matrixd<2, 2> A{{0.0, 1.0}, {0.0, -Kv.value() / Ka.value()}}; + constexpr frc::Matrixd<2, 1> B{{0.0}, {1.0 / Ka.value()}}; + + return frc::RK4( + [&](const frc::Matrixd<2, 1>& x, + const frc::Matrixd<1, 1>& u) -> frc::Matrixd<2, 1> { + frc::Matrixd<2, 1> c{0.0, -Ks.value() / Ka.value() * wpi::sgn(x(1)) - + Kg.value() / Ka.value() * std::cos(x(0))}; + return A * x + B * u + c; + }, + frc::Matrixd<2, 1>{currentAngle.value(), currentVelocity.value()}, + frc::Matrixd<1, 1>{input.value()}, dt); +} + +/** + * Simulates a single-jointed arm and returns the final state. + * + * @param armFF The feedforward object. + * @param currentAngle The starting angle. + * @param currentVelocity The starting angular velocity. + * @param input The input voltage. + * @param dt The simulation time. + */ +void CalculateAndSimulate(const frc::ArmFeedforward& armFF, + units::radian_t currentAngle, + units::radians_per_second_t currentVelocity, + units::radians_per_second_t nextVelocity, + units::second_t dt) { + auto input = armFF.Calculate(currentAngle, currentVelocity, nextVelocity, dt); + EXPECT_NEAR(nextVelocity.value(), + Simulate(currentAngle, currentVelocity, input, dt)(1), 1e-12); +} + +} // namespace + TEST(ArmFeedforwardTest, Calculate) { frc::ArmFeedforward armFF{Ks, Kg, Kv, Ka}; + + // Calculate(angle, angular velocity) EXPECT_NEAR( - armFF.Calculate(std::numbers::pi * 1_rad / 3, 0_rad / 1_s).value(), 0.5, + armFF.Calculate(std::numbers::pi / 3 * 1_rad, 0_rad_per_s).value(), 0.5, 0.002); EXPECT_NEAR( - armFF.Calculate(std::numbers::pi * 1_rad / 3, 1_rad / 1_s).value(), 2.5, + armFF.Calculate(std::numbers::pi / 3 * 1_rad, 1_rad_per_s).value(), 2.5, 0.002); - EXPECT_NEAR(armFF - .Calculate(std::numbers::pi * 1_rad / 3, 1_rad / 1_s, - 2_rad / 1_s / 1_s) - .value(), - 6.5, 0.002); - EXPECT_NEAR(armFF - .Calculate(std::numbers::pi * 1_rad / 3, -1_rad / 1_s, - 2_rad / 1_s / 1_s) - .value(), - 2.5, 0.002); + + // Calculate(angle, angular velocity, angular acceleration) + EXPECT_NEAR( + armFF.Calculate(std::numbers::pi / 3 * 1_rad, 1_rad_per_s, 2_rad_per_s_sq) + .value(), + 6.5, 0.002); + EXPECT_NEAR( + armFF + .Calculate(std::numbers::pi / 3 * 1_rad, -1_rad_per_s, 2_rad_per_s_sq) + .value(), + 2.5, 0.002); + + // Calculate(currentAngle, currentVelocity, nextAngle, dt) + CalculateAndSimulate(armFF, std::numbers::pi / 3 * 1_rad, 1_rad_per_s, + 1.05_rad_per_s, 20_ms); + CalculateAndSimulate(armFF, std::numbers::pi / 3 * 1_rad, 1_rad_per_s, + 0.95_rad_per_s, 20_ms); + CalculateAndSimulate(armFF, -std::numbers::pi / 3 * 1_rad, 1_rad_per_s, + 1.05_rad_per_s, 20_ms); + CalculateAndSimulate(armFF, -std::numbers::pi / 3 * 1_rad, 1_rad_per_s, + 0.95_rad_per_s, 20_ms); } TEST(ArmFeedforwardTest, AchievableVelocity) { frc::ArmFeedforward armFF{Ks, Kg, Kv, Ka}; EXPECT_NEAR(armFF - .MaxAchievableVelocity(12_V, std::numbers::pi * 1_rad / 3, - 1_rad / 1_s / 1_s) + .MaxAchievableVelocity(12_V, std::numbers::pi / 3 * 1_rad, + 1_rad_per_s_sq) .value(), 6, 0.002); EXPECT_NEAR(armFF - .MinAchievableVelocity(11.5_V, std::numbers::pi * 1_rad / 3, - 1_rad / 1_s / 1_s) + .MinAchievableVelocity(11.5_V, std::numbers::pi / 3 * 1_rad, + 1_rad_per_s_sq) .value(), -9, 0.002); } @@ -54,23 +119,23 @@ TEST(ArmFeedforwardTest, AchievableVelocity) { TEST(ArmFeedforwardTest, AchievableAcceleration) { frc::ArmFeedforward armFF{Ks, Kg, Kv, Ka}; EXPECT_NEAR(armFF - .MaxAchievableAcceleration(12_V, std::numbers::pi * 1_rad / 3, - 1_rad / 1_s) + .MaxAchievableAcceleration(12_V, std::numbers::pi / 3 * 1_rad, + 1_rad_per_s) .value(), 4.75, 0.002); EXPECT_NEAR(armFF - .MaxAchievableAcceleration(12_V, std::numbers::pi * 1_rad / 3, - -1_rad / 1_s) + .MaxAchievableAcceleration(12_V, std::numbers::pi / 3 * 1_rad, + -1_rad_per_s) .value(), 6.75, 0.002); EXPECT_NEAR(armFF - .MinAchievableAcceleration(12_V, std::numbers::pi * 1_rad / 3, - 1_rad / 1_s) + .MinAchievableAcceleration(12_V, std::numbers::pi / 3 * 1_rad, + 1_rad_per_s) .value(), -7.25, 0.002); EXPECT_NEAR(armFF - .MinAchievableAcceleration(12_V, std::numbers::pi * 1_rad / 3, - -1_rad / 1_s) + .MinAchievableAcceleration(12_V, std::numbers::pi / 3 * 1_rad, + -1_rad_per_s) .value(), -5.25, 0.002); }