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