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);
+}