WPILibC++ 2024.3.2
LinearFilter.h
Go to the documentation of this file.
1// Copyright (c) FIRST and other WPILib contributors.
2// Open Source Software; you can modify and/or share it under the terms of
3// the WPILib BSD license file in the root directory of this project.
4
5#pragma once
6
7#include <algorithm>
8#include <cmath>
9#include <initializer_list>
10#include <span>
11#include <stdexcept>
12#include <vector>
13
14#include <Eigen/QR>
15#include <wpi/array.h>
16#include <wpi/circular_buffer.h>
17
18#include "frc/EigenCore.h"
19#include "units/time.h"
20#include "wpimath/MathShared.h"
21
22namespace frc {
23
24/**
25 * This class implements a linear, digital filter. All types of FIR and IIR
26 * filters are supported. Static factory methods are provided to create commonly
27 * used types of filters.
28 *
29 * Filters are of the form:<br>
30 * y[n] = (b0 x[n] + b1 x[n-1] + … + bP x[n-P]) -
31 * (a0 y[n-1] + a2 y[n-2] + … + aQ y[n-Q])
32 *
33 * Where:<br>
34 * y[n] is the output at time "n"<br>
35 * x[n] is the input at time "n"<br>
36 * y[n-1] is the output from the LAST time step ("n-1")<br>
37 * x[n-1] is the input from the LAST time step ("n-1")<br>
38 * b0 … bP are the "feedforward" (FIR) gains<br>
39 * a0 … aQ are the "feedback" (IIR) gains<br>
40 * IMPORTANT! Note the "-" sign in front of the feedback term! This is a common
41 * convention in signal processing.
42 *
43 * What can linear filters do? Basically, they can filter, or diminish, the
44 * effects of undesirable input frequencies. High frequencies, or rapid changes,
45 * can be indicative of sensor noise or be otherwise undesirable. A "low pass"
46 * filter smooths out the signal, reducing the impact of these high frequency
47 * components. Likewise, a "high pass" filter gets rid of slow-moving signal
48 * components, letting you detect large changes more easily.
49 *
50 * Example FRC applications of filters:
51 * - Getting rid of noise from an analog sensor input (note: the roboRIO's FPGA
52 * can do this faster in hardware)
53 * - Smoothing out joystick input to prevent the wheels from slipping or the
54 * robot from tipping
55 * - Smoothing motor commands so that unnecessary strain isn't put on
56 * electrical or mechanical components
57 * - If you use clever gains, you can make a PID controller out of this class!
58 *
59 * For more on filters, we highly recommend the following articles:<br>
60 * https://en.wikipedia.org/wiki/Linear_filter<br>
61 * https://en.wikipedia.org/wiki/Iir_filter<br>
62 * https://en.wikipedia.org/wiki/Fir_filter<br>
63 *
64 * Note 1: Calculate() should be called by the user on a known, regular period.
65 * You can use a Notifier for this or do it "inline" with code in a
66 * periodic function.
67 *
68 * Note 2: For ALL filters, gains are necessarily a function of frequency. If
69 * you make a filter that works well for you at, say, 100Hz, you will most
70 * definitely need to adjust the gains if you then want to run it at 200Hz!
71 * Combining this with Note 1 - the impetus is on YOU as a developer to make
72 * sure Calculate() gets called at the desired, constant frequency!
73 */
74template <class T>
76 public:
77 /**
78 * Create a linear FIR or IIR filter.
79 *
80 * @param ffGains The "feedforward" or FIR gains.
81 * @param fbGains The "feedback" or IIR gains.
82 */
83 LinearFilter(std::span<const double> ffGains, std::span<const double> fbGains)
84 : m_inputs(ffGains.size()),
85 m_outputs(fbGains.size()),
86 m_inputGains(ffGains.begin(), ffGains.end()),
87 m_outputGains(fbGains.begin(), fbGains.end()) {
88 for (size_t i = 0; i < ffGains.size(); ++i) {
89 m_inputs.emplace_front(0.0);
90 }
91 for (size_t i = 0; i < fbGains.size(); ++i) {
92 m_outputs.emplace_front(0.0);
93 }
94
95 static int instances = 0;
96 instances++;
99 }
100
101 /**
102 * Create a linear FIR or IIR filter.
103 *
104 * @param ffGains The "feedforward" or FIR gains.
105 * @param fbGains The "feedback" or IIR gains.
106 */
107 LinearFilter(std::initializer_list<double> ffGains,
108 std::initializer_list<double> fbGains)
109 : LinearFilter({ffGains.begin(), ffGains.end()},
110 {fbGains.begin(), fbGains.end()}) {}
111
112 // Static methods to create commonly used filters
113 /**
114 * Creates a one-pole IIR low-pass filter of the form:<br>
115 * y[n] = (1 - gain) x[n] + gain y[n-1]<br>
116 * where gain = e<sup>-dt / T</sup>, T is the time constant in seconds
117 *
118 * Note: T = 1 / (2 pi f) where f is the cutoff frequency in Hz, the frequency
119 * above which the input starts to attenuate.
120 *
121 * This filter is stable for time constants greater than zero.
122 *
123 * @param timeConstant The discrete-time time constant in seconds.
124 * @param period The period in seconds between samples taken by the
125 * user.
126 */
127 static LinearFilter<T> SinglePoleIIR(double timeConstant,
128 units::second_t period) {
129 double gain = std::exp(-period.value() / timeConstant);
130 return LinearFilter({1.0 - gain}, {-gain});
131 }
132
133 /**
134 * Creates a first-order high-pass filter of the form:<br>
135 * y[n] = gain x[n] + (-gain) x[n-1] + gain y[n-1]<br>
136 * where gain = e<sup>-dt / T</sup>, T is the time constant in seconds
137 *
138 * Note: T = 1 / (2 pi f) where f is the cutoff frequency in Hz, the frequency
139 * below which the input starts to attenuate.
140 *
141 * This filter is stable for time constants greater than zero.
142 *
143 * @param timeConstant The discrete-time time constant in seconds.
144 * @param period The period in seconds between samples taken by the
145 * user.
146 */
147 static LinearFilter<T> HighPass(double timeConstant, units::second_t period) {
148 double gain = std::exp(-period.value() / timeConstant);
149 return LinearFilter({gain, -gain}, {-gain});
150 }
151
152 /**
153 * Creates a K-tap FIR moving average filter of the form:<br>
154 * y[n] = 1/k (x[k] + x[k-1] + … + x[0])
155 *
156 * This filter is always stable.
157 *
158 * @param taps The number of samples to average over. Higher = smoother but
159 * slower
160 * @throws std::runtime_error if number of taps is less than 1.
161 */
163 if (taps <= 0) {
164 throw std::runtime_error("Number of taps must be greater than zero.");
165 }
166
167 std::vector<double> gains(taps, 1.0 / taps);
168 return LinearFilter(gains, {});
169 }
170
171 /**
172 * Creates a finite difference filter that computes the nth derivative of the
173 * input given the specified stencil points.
174 *
175 * Stencil points are the indices of the samples to use in the finite
176 * difference. 0 is the current sample, -1 is the previous sample, -2 is the
177 * sample before that, etc. Don't use positive stencil points (samples from
178 * the future) if the LinearFilter will be used for stream-based online
179 * filtering (e.g., taking derivative of encoder samples in real-time).
180 *
181 * @tparam Derivative The order of the derivative to compute.
182 * @tparam Samples The number of samples to use to compute the given
183 * derivative. This must be one more than the order of
184 * the derivative or higher.
185 * @param stencil List of stencil points.
186 * @param period The period in seconds between samples taken by the user.
187 */
188 template <int Derivative, int Samples>
190 const wpi::array<int, Samples> stencil, units::second_t period) {
191 // See
192 // https://en.wikipedia.org/wiki/Finite_difference_coefficient#Arbitrary_stencil_points
193 //
194 // For a given list of stencil points s of length n and the order of
195 // derivative d < n, the finite difference coefficients can be obtained by
196 // solving the following linear system for the vector a.
197 //
198 // [s₁⁰ ⋯ sₙ⁰ ][a₁] [ δ₀,d ]
199 // [ ⋮ ⋱ ⋮ ][⋮ ] = d! [ ⋮ ]
200 // [s₁ⁿ⁻¹ ⋯ sₙⁿ⁻¹][aₙ] [δₙ₋₁,d]
201 //
202 // where δᵢ,ⱼ are the Kronecker delta. The FIR gains are the elements of the
203 // vector a in reverse order divided by hᵈ.
204 //
205 // The order of accuracy of the approximation is of the form O(hⁿ⁻ᵈ).
206
207 static_assert(Derivative >= 1,
208 "Order of derivative must be greater than or equal to one.");
209 static_assert(Samples > 0, "Number of samples must be greater than zero.");
210 static_assert(Derivative < Samples,
211 "Order of derivative must be less than number of samples.");
212
214 for (int row = 0; row < Samples; ++row) {
215 for (int col = 0; col < Samples; ++col) {
216 S(row, col) = std::pow(stencil[col], row);
217 }
218 }
219
220 // Fill in Kronecker deltas: https://en.wikipedia.org/wiki/Kronecker_delta
222 for (int i = 0; i < Samples; ++i) {
223 d(i) = (i == Derivative) ? Factorial(Derivative) : 0.0;
224 }
225
227 S.householderQr().solve(d) / std::pow(period.value(), Derivative);
228
229 // Reverse gains list
230 std::vector<double> ffGains;
231 for (int i = Samples - 1; i >= 0; --i) {
232 ffGains.push_back(a(i));
233 }
234
235 return LinearFilter(ffGains, {});
236 }
237
238 /**
239 * Creates a backward finite difference filter that computes the nth
240 * derivative of the input given the specified number of samples.
241 *
242 * For example, a first derivative filter that uses two samples and a sample
243 * period of 20 ms would be
244 *
245 * <pre><code>
246 * LinearFilter<double>::BackwardFiniteDifference<1, 2>(20_ms);
247 * </code></pre>
248 *
249 * @tparam Derivative The order of the derivative to compute.
250 * @tparam Samples The number of samples to use to compute the given
251 * derivative. This must be one more than the order of
252 * derivative or higher.
253 * @param period The period in seconds between samples taken by the user.
254 */
255 template <int Derivative, int Samples>
256 static LinearFilter<T> BackwardFiniteDifference(units::second_t period) {
257 // Generate stencil points from -(samples - 1) to 0
259 for (int i = 0; i < Samples; ++i) {
260 stencil[i] = -(Samples - 1) + i;
261 }
262
263 return FiniteDifference<Derivative, Samples>(stencil, period);
264 }
265
266 /**
267 * Reset the filter state.
268 */
269 void Reset() {
270 std::fill(m_inputs.begin(), m_inputs.end(), T{0.0});
271 std::fill(m_outputs.begin(), m_outputs.end(), T{0.0});
272 }
273
274 /**
275 * Resets the filter state, initializing internal buffers to the provided
276 * values.
277 *
278 * These are the expected lengths of the buffers, depending on what type of
279 * linear filter used:
280 *
281 * <table>
282 * <tr>
283 * <th>Type</th>
284 * <th>Input Buffer Size</th>
285 * <th>Output Buffer Size</th>
286 * </tr>
287 * <tr>
288 * <td>Unspecified</td>
289 * <td>size of {@code ffGains}</td>
290 * <td>size of {@code fbGains}</td>
291 * </tr>
292 * <tr>
293 * <td>Single Pole IIR</td>
294 * <td>1</td>
295 * <td>1</td>
296 * </tr>
297 * <tr>
298 * <td>High-Pass</td>
299 * <td>2</td>
300 * <td>1</td>
301 * </tr>
302 * <tr>
303 * <td>Moving Average</td>
304 * <td>{@code taps}</td>
305 * <td>0</td>
306 * </tr>
307 * <tr>
308 * <td>Finite Difference</td>
309 * <td>size of {@code stencil}</td>
310 * <td>0</td>
311 * </tr>
312 * <tr>
313 * <td>Backward Finite Difference</td>
314 * <td>{@code Samples}</td>
315 * <td>0</td>
316 * </tr>
317 * </table>
318 *
319 * @param inputBuffer Values to initialize input buffer.
320 * @param outputBuffer Values to initialize output buffer.
321 * @throws std::runtime_error if size of inputBuffer or outputBuffer does not
322 * match the size of ffGains and fbGains provided in the constructor.
323 */
324 void Reset(std::span<const double> inputBuffer,
325 std::span<const double> outputBuffer) {
326 // Clear buffers
327 Reset();
328
329 if (inputBuffer.size() != m_inputGains.size() ||
330 outputBuffer.size() != m_outputGains.size()) {
331 throw std::runtime_error(
332 "Incorrect length of inputBuffer or outputBuffer");
333 }
334
335 for (size_t i = 0; i < inputBuffer.size(); ++i) {
336 m_inputs.push_front(inputBuffer[i]);
337 }
338 for (size_t i = 0; i < outputBuffer.size(); ++i) {
339 m_outputs.push_front(outputBuffer[i]);
340 }
341 }
342
343 /**
344 * Calculates the next value of the filter.
345 *
346 * @param input Current input value.
347 *
348 * @return The filtered value at this step
349 */
350 T Calculate(T input) {
351 T retVal{0.0};
352
353 // Rotate the inputs
354 if (m_inputGains.size() > 0) {
355 m_inputs.push_front(input);
356 }
357
358 // Calculate the new value
359 for (size_t i = 0; i < m_inputGains.size(); ++i) {
360 retVal += m_inputs[i] * m_inputGains[i];
361 }
362 for (size_t i = 0; i < m_outputGains.size(); ++i) {
363 retVal -= m_outputs[i] * m_outputGains[i];
364 }
365
366 // Rotate the outputs
367 if (m_outputGains.size() > 0) {
368 m_outputs.push_front(retVal);
369 }
370
371 return retVal;
372 }
373
374 /**
375 * Returns the last value calculated by the LinearFilter.
376 *
377 * @return The last value.
378 */
379 T LastValue() const { return m_outputs.front(); }
380
381 private:
383 wpi::circular_buffer<T> m_outputs;
384 std::vector<double> m_inputGains;
385 std::vector<double> m_outputGains;
386
387 /**
388 * Factorial of n.
389 *
390 * @param n Argument of which to take factorial.
391 */
392 static constexpr int Factorial(int n) {
393 if (n < 2) {
394 return 1;
395 } else {
396 return n * Factorial(n - 1);
397 }
398 }
399};
400
401} // namespace frc
This class implements a linear, digital filter.
Definition: LinearFilter.h:75
LinearFilter(std::span< const double > ffGains, std::span< const double > fbGains)
Create a linear FIR or IIR filter.
Definition: LinearFilter.h:83
void Reset(std::span< const double > inputBuffer, std::span< const double > outputBuffer)
Resets the filter state, initializing internal buffers to the provided values.
Definition: LinearFilter.h:324
T Calculate(T input)
Calculates the next value of the filter.
Definition: LinearFilter.h:350
static LinearFilter< T > HighPass(double timeConstant, units::second_t period)
Creates a first-order high-pass filter of the form: y[n] = gain x[n] + (-gain) x[n-1] + gain y[n-1] ...
Definition: LinearFilter.h:147
static LinearFilter< T > FiniteDifference(const wpi::array< int, Samples > stencil, units::second_t period)
Creates a finite difference filter that computes the nth derivative of the input given the specified ...
Definition: LinearFilter.h:189
static LinearFilter< T > BackwardFiniteDifference(units::second_t period)
Creates a backward finite difference filter that computes the nth derivative of the input given the s...
Definition: LinearFilter.h:256
static LinearFilter< T > MovingAverage(int taps)
Creates a K-tap FIR moving average filter of the form: y[n] = 1/k (x[k] + x[k-1] + … + x[0])
Definition: LinearFilter.h:162
void Reset()
Reset the filter state.
Definition: LinearFilter.h:269
LinearFilter(std::initializer_list< double > ffGains, std::initializer_list< double > fbGains)
Create a linear FIR or IIR filter.
Definition: LinearFilter.h:107
T LastValue() const
Returns the last value calculated by the LinearFilter.
Definition: LinearFilter.h:379
static LinearFilter< T > SinglePoleIIR(double timeConstant, units::second_t period)
Creates a one-pole IIR low-pass filter of the form: y[n] = (1 - gain) x[n] + gain y[n-1] where gain...
Definition: LinearFilter.h:127
This class is a wrapper around std::array that does compile time size checking.
Definition: array.h:26
This is a simple circular buffer so we don't need to "bucket brigade" copy old values.
Definition: circular_buffer.h:20
static void ReportUsage(MathUsageId id, int count)
Definition: MathShared.h:73
FMT_NOINLINE FMT_CONSTEXPR auto fill(OutputIt it, size_t n, const fill_t< Char > &fill) -> OutputIt
Definition: format.h:1779
Definition: AprilTagPoseEstimator.h:15
Eigen::Matrix< double, Rows, Cols, Options, MaxRows, MaxCols > Matrixd
Definition: EigenCore.h:21
Eigen::Vector< double, Size > Vectord
Definition: EigenCore.h:12
auto pow(const UnitType &value) noexcept -> unit_t< typename units::detail::power_of_unit< power, typename units::traits::unit_t_traits< UnitType >::unit_type >::type, typename units::traits::unit_t_traits< UnitType >::underlying_type, linear_scale >
computes the value of value raised to the power
Definition: base.h:2806
constexpr empty_array_t empty_array
Definition: array.h:16
#define S(label, offset, message)
Definition: Errors.h:119