diff --git a/wpimath/src/main/java/edu/wpi/first/math/filter/LinearFilter.java b/wpimath/src/main/java/edu/wpi/first/math/filter/LinearFilter.java index d85d663891..e91ab504a4 100644 --- a/wpimath/src/main/java/edu/wpi/first/math/filter/LinearFilter.java +++ b/wpimath/src/main/java/edu/wpi/first/math/filter/LinearFilter.java @@ -8,6 +8,7 @@ import edu.wpi.first.math.MathSharedStore; import edu.wpi.first.math.MathUsageId; import edu.wpi.first.util.CircularBuffer; import java.util.Arrays; +import org.ejml.simple.SimpleMatrix; /** * This class implements a linear, digital filter. All types of FIR and IIR filters are supported. @@ -57,8 +58,8 @@ public class LinearFilter { /** * Create a linear FIR or IIR filter. * - * @param ffGains The "feed forward" or FIR gains. - * @param fbGains The "feed back" or IIR gains. + * @param ffGains The "feedforward" or FIR gains. + * @param fbGains The "feedback" or IIR gains. */ public LinearFilter(double[] ffGains, double[] fbGains) { m_inputs = new CircularBuffer(ffGains.length); @@ -136,6 +137,84 @@ public class LinearFilter { return new LinearFilter(ffGains, fbGains); } + /** + * Creates a backward finite difference filter that computes the nth derivative of the input given + * the specified number of samples. + * + *

For example, a first derivative filter that uses two samples and a sample period of 20 ms + * would be + * + *


+   * LinearFilter.backwardFiniteDifference(1, 2, 0.02);
+   * 
+ * + * @param derivative The order of the derivative to compute. + * @param samples The number of samples to use to compute the given derivative. This must be one + * more than the order of derivative or higher. + * @param period The period in seconds between samples taken by the user. + */ + @SuppressWarnings("LocalVariableName") + public static LinearFilter backwardFiniteDifference(int derivative, int samples, double period) { + // See + // https://en.wikipedia.org/wiki/Finite_difference_coefficient#Arbitrary_stencil_points + // + //

For a given list of stencil points s of length n and the order of + // derivative d < n, the finite difference coefficients can be obtained by + // solving the following linear system for the vector a. + // + //

+    // [s₁⁰   ⋯  sₙ⁰ ][a₁]      [ δ₀,d ]
+    // [ ⋮    ⋱  ⋮   ][⋮ ] = d! [  ⋮   ]
+    // [s₁ⁿ⁻¹ ⋯ sₙⁿ⁻¹][aₙ]      [δₙ₋₁,d]
+    // 
+ // + //

where δᵢ,ⱼ are the Kronecker delta. For backward finite difference, + // the stencil points are the range [-n + 1, 0]. The FIR gains are the + // elements of the vector a in reverse order divided by hᵈ. + // + //

The order of accuracy of the approximation is of the form O(hⁿ⁻ᵈ). + + if (derivative < 1) { + throw new IllegalArgumentException( + "Order of derivative must be greater than or equal to one."); + } + + if (samples <= 0) { + throw new IllegalArgumentException("Number of samples must be greater than zero."); + } + + if (derivative >= samples) { + throw new IllegalArgumentException( + "Order of derivative must be less than number of samples."); + } + + var S = new SimpleMatrix(samples, samples); + for (int row = 0; row < samples; ++row) { + for (int col = 0; col < samples; ++col) { + double s = 1 - samples + col; + S.set(row, col, Math.pow(s, row)); + } + } + + // Fill in Kronecker deltas: https://en.wikipedia.org/wiki/Kronecker_delta + var d = new SimpleMatrix(samples, 1); + for (int i = 0; i < samples; ++i) { + d.set(i, 0, (i == derivative) ? factorial(derivative) : 0.0); + } + + var a = S.solve(d).divide(Math.pow(period, derivative)); + + // Reverse gains list + double[] ffGains = new double[samples]; + for (int i = 0; i < samples; ++i) { + ffGains[i] = a.get(samples - i - 1, 0); + } + + double[] fbGains = new double[0]; + + return new LinearFilter(ffGains, fbGains); + } + /** Reset the filter state. */ public void reset() { m_inputs.clear(); @@ -171,4 +250,17 @@ public class LinearFilter { return retVal; } + + /** + * Factorial of n. + * + * @param n Argument of which to take factorial. + */ + private static int factorial(int n) { + if (n < 2) { + return 1; + } else { + return n * factorial(n - 1); + } + } } diff --git a/wpimath/src/main/native/include/frc/filter/LinearFilter.h b/wpimath/src/main/native/include/frc/filter/LinearFilter.h index 925c67b8aa..266d149b9a 100644 --- a/wpimath/src/main/native/include/frc/filter/LinearFilter.h +++ b/wpimath/src/main/native/include/frc/filter/LinearFilter.h @@ -13,6 +13,8 @@ #include #include +#include "Eigen/Core" +#include "Eigen/QR" #include "units/time.h" #include "wpimath/MathShared.h" @@ -74,8 +76,8 @@ class LinearFilter { /** * Create a linear FIR or IIR filter. * - * @param ffGains The "feed forward" or FIR gains. - * @param fbGains The "feed back" or IIR gains. + * @param ffGains The "feedforward" or FIR gains. + * @param fbGains The "feedback" or IIR gains. */ LinearFilter(wpi::span ffGains, wpi::span fbGains) : m_inputs(ffGains.size()), @@ -98,8 +100,8 @@ class LinearFilter { /** * Create a linear FIR or IIR filter. * - * @param ffGains The "feed forward" or FIR gains. - * @param fbGains The "feed back" or IIR gains. + * @param ffGains The "feedforward" or FIR gains. + * @param fbGains The "feedback" or IIR gains. */ LinearFilter(std::initializer_list ffGains, std::initializer_list fbGains) @@ -164,6 +166,76 @@ class LinearFilter { return LinearFilter(gains, {}); } + /** + * Creates a backward finite difference filter that computes the nth + * derivative of the input given the specified number of samples. + * + * For example, a first derivative filter that uses two samples and a sample + * period of 20 ms would be + * + *


+   * LinearFilter::BackwardFiniteDifference<1, 2>(20_ms);
+   * 
+ * + * @tparam Derivative The order of the derivative to compute. + * @tparam Samples The number of samples to use to compute the given + * derivative. This must be one more than the order of + * derivative or higher. + * @param period The period in seconds between samples taken by the user. + */ + template + static auto BackwardFiniteDifference(units::second_t period) { + // See + // https://en.wikipedia.org/wiki/Finite_difference_coefficient#Arbitrary_stencil_points + // + // For a given list of stencil points s of length n and the order of + // derivative d < n, the finite difference coefficients can be obtained by + // solving the following linear system for the vector a. + // + // @verbatim + // [s₁⁰ ⋯ sₙ⁰ ][a₁] [ δ₀,d ] + // [ ⋮ ⋱ ⋮ ][⋮ ] = d! [ ⋮ ] + // [s₁ⁿ⁻¹ ⋯ sₙⁿ⁻¹][aₙ] [δₙ₋₁,d] + // @endverbatim + // + // where δᵢ,ⱼ are the Kronecker delta. For backward finite difference, the + // stencil points are the range [-n + 1, 0]. The FIR gains are the elements + // of the vector a in reverse order divided by hᵈ. + // + // The order of accuracy of the approximation is of the form O(hⁿ⁻ᵈ). + + static_assert(Derivative >= 1, + "Order of derivative must be greater than or equal to one."); + static_assert(Samples > 0, "Number of samples must be greater than zero."); + static_assert(Derivative < Samples, + "Order of derivative must be less than number of samples."); + + Eigen::Matrix S; + for (int row = 0; row < Samples; ++row) { + for (int col = 0; col < Samples; ++col) { + double s = 1 - Samples + col; + S(row, col) = std::pow(s, row); + } + } + + // Fill in Kronecker deltas: https://en.wikipedia.org/wiki/Kronecker_delta + Eigen::Matrix d; + for (int i = 0; i < Samples; ++i) { + d(i) = (i == Derivative) ? Factorial(Derivative) : 0.0; + } + + Eigen::Matrix a = + S.householderQr().solve(d) / std::pow(period.to(), Derivative); + + // Reverse gains list + std::vector gains; + for (int i = Samples - 1; i >= 0; --i) { + gains.push_back(a(i)); + } + + return LinearFilter(gains, {}); + } + /** * Reset the filter state. */ @@ -208,6 +280,19 @@ class LinearFilter { wpi::circular_buffer m_outputs; std::vector m_inputGains; std::vector m_outputGains; + + /** + * Factorial of n. + * + * @param n Argument of which to take factorial. + */ + static int Factorial(int n) { + if (n < 2) { + return 1; + } else { + return n * Factorial(n - 1); + } + } }; } // namespace frc diff --git a/wpimath/src/test/java/edu/wpi/first/math/filter/LinearFilterTest.java b/wpimath/src/test/java/edu/wpi/first/math/filter/LinearFilterTest.java index 41be6f62c4..805129f95b 100644 --- a/wpimath/src/test/java/edu/wpi/first/math/filter/LinearFilterTest.java +++ b/wpimath/src/test/java/edu/wpi/first/math/filter/LinearFilterTest.java @@ -108,4 +108,113 @@ class LinearFilterTest { (DoubleFunction) LinearFilterTest::getPulseData, 0.0)); } + + /** Test backward finite difference. */ + @Test + void backwardFiniteDifferenceTest() { + double h = 0.005; + + assertResults( + 1, + 2, + // f(x) = x² + (double x) -> x * x, + // df/dx = 2x + (double x) -> 2.0 * x, + h, + -20.0, + 20.0); + + assertResults( + 1, + 2, + // f(x) = sin(x) + (double x) -> Math.sin(x), + // df/dx = cos(x) + (double x) -> Math.cos(x), + h, + -20.0, + 20.0); + + assertResults( + 1, + 2, + // f(x) = ln(x) + (double x) -> Math.log(x), + // df/dx = 1 / x + (double x) -> 1.0 / x, + h, + 1.0, + 20.0); + + assertResults( + 2, + 4, + // f(x) = x² + (double x) -> x * x, + // d²f/dx² = 2 + (double x) -> 2.0, + h, + -20.0, + 20.0); + + assertResults( + 2, + 4, + // f(x) = sin(x) + (double x) -> Math.sin(x), + // d²f/dx² = -sin(x) + (double x) -> -Math.sin(x), + h, + -20.0, + 20.0); + + assertResults( + 2, + 4, + // f(x) = ln(x) + (double x) -> Math.log(x), + // d²f/dx² = -1 / x² + (double x) -> -1.0 / (x * x), + h, + 1.0, + 20.0); + } + + /** + * Helper for checking results of backward finite difference. + * + * @param derivative The order of the derivative. + * @param samples The number of sample points. + * @param f Function of which to take derivative. + * @param dfdx Derivative of f. + * @param h Sample period in seconds. + * @param min Minimum of f's domain to test. + * @param max Maximum of f's domain to test. + */ + void assertResults( + int derivative, + int samples, + DoubleFunction f, + DoubleFunction dfdx, + double h, + double min, + double max) { + var filter = LinearFilter.backwardFiniteDifference(derivative, samples, h); + + for (int i = (int) (min / h); i < (int) (max / h); ++i) { + // Let filter initialize + if (i < (int) (min / h) + samples) { + filter.calculate(f.apply(i * h)); + continue; + } + + // The order of accuracy is O(h^(N - d)) where N is number of stencil + // points and d is order of derivative + assertEquals( + dfdx.apply(i * h), + filter.calculate(f.apply(i * h)), + 10.0 * Math.pow(h, samples - derivative)); + } + } } diff --git a/wpimath/src/test/native/cpp/filter/LinearFilterOutputTest.cpp b/wpimath/src/test/native/cpp/filter/LinearFilterOutputTest.cpp index 6b944121c7..ae9d943d5d 100644 --- a/wpimath/src/test/native/cpp/filter/LinearFilterOutputTest.cpp +++ b/wpimath/src/test/native/cpp/filter/LinearFilterOutputTest.cpp @@ -118,3 +118,98 @@ TEST_P(LinearFilterOutputTest, Output) { INSTANTIATE_TEST_SUITE_P(Test, LinearFilterOutputTest, testing::Values(kTestSinglePoleIIR, kTestHighPass, kTestMovAvg, kTestPulse)); + +template +void AssertResults(F&& f, DfDx&& dfdx, units::second_t h, double min, + double max) { + auto filter = + frc::LinearFilter::BackwardFiniteDifference( + h); + + for (int i = min / h.to(); i < max / h.to(); ++i) { + // Let filter initialize + if (i < static_cast(min / h.to()) + Samples) { + filter.Calculate(f(i * h.to())); + continue; + } + + // The order of accuracy is O(h^(N - d)) where N is number of stencil + // points and d is order of derivative + EXPECT_NEAR(dfdx(i * h.to()), + filter.Calculate(f(i * h.to())), + 10.0 * std::pow(h.to(), Samples - Derivative)); + } +} + +/** + * Test backward finite difference. + */ +TEST(LinearFilterOutputTest, BackwardFiniteDifference) { + constexpr auto h = 5_ms; + + AssertResults<1, 2>( + [](double x) { + // f(x) = x² + return x * x; + }, + [](double x) { + // df/dx = 2x + return 2.0 * x; + }, + h, -20.0, 20.0); + + AssertResults<1, 2>( + [](double x) { + // f(x) = std::sin(x) + return std::sin(x); + }, + [](double x) { + // df/dx = std::cos(x) + return std::cos(x); + }, + h, -20.0, 20.0); + + AssertResults<1, 2>( + [](double x) { + // f(x) = ln(x) + return std::log(x); + }, + [](double x) { + // df/dx = 1 / x + return 1.0 / x; + }, + h, 1.0, 20.0); + + AssertResults<2, 4>( + [](double x) { + // f(x) = x^2 + return x * x; + }, + [](double x) { + // d²f/dx² = 2 + return 2.0; + }, + h, -20.0, 20.0); + + AssertResults<2, 4>( + [](double x) { + // f(x) = std::sin(x) + return std::sin(x); + }, + [](double x) { + // d²f/dx² = -std::sin(x) + return -std::sin(x); + }, + h, -20.0, 20.0); + + AssertResults<2, 4>( + [](double x) { + // f(x) = ln(x) + return std::log(x); + }, + [](double x) { + // d²f/dx² = -1 / x² + return -1.0 / (x * x); + }, + h, 1.0, 20.0); +}