CTRE Phoenix 6 C++ 25.0.0-beta-4
Loading...
Searching...
No Matches
LinearFilter.hpp
Go to the documentation of this file.
1/*
2 * Copyright (C) Cross The Road Electronics.  All rights reserved.
3 * License information can be found in CTRE_LICENSE.txt
4 * For support and suggestions contact support@ctr-electronics.com or file
5 * an issue tracker at https://github.com/CrossTheRoadElec/Phoenix-Releases
6 */
7#pragma once
8
9#include <algorithm>
10#include <array>
11#include <cmath>
12#include <initializer_list>
13#include <stdexcept>
14
17#include "Eigen/Core"
18#include "units/time.h"
19
20namespace ctre {
21namespace phoenix6 {
22namespace swerve {
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 *
74 * This is a copy of the WPILib implementation to fix
75 * https://github.com/wpilibsuite/allwpilib/pull/6628.
76 */
77template <class T>
79 public:
80 /**
81 * Create a linear FIR or IIR filter.
82 *
83 * @param ffGains The "feedforward" or FIR gains.
84 * @param fbGains The "feedback" or IIR gains.
85 */
87 : m_inputs(ffGains.size()),
88 m_outputs(fbGains.size()),
89 m_inputGains(ffGains.begin(), ffGains.end()),
90 m_outputGains(fbGains.begin(), fbGains.end()) {
91 for (size_t i = 0; i < ffGains.size(); ++i) {
92 m_inputs.emplace_front(0.0);
93 }
94 for (size_t i = 0; i < fbGains.size(); ++i) {
95 m_outputs.emplace_front(0.0);
96 }
97 }
98
99 /**
100 * Create a linear FIR or IIR filter.
101 *
102 * @param ffGains The "feedforward" or FIR gains.
103 * @param fbGains The "feedback" or IIR gains.
104 */
105 LinearFilter(std::initializer_list<double> ffGains,
106 std::initializer_list<double> fbGains)
107 : LinearFilter({ffGains.begin(), ffGains.end()},
108 {fbGains.begin(), fbGains.end()}) {}
109
110 // Static methods to create commonly used filters
111 /**
112 * Creates a one-pole IIR low-pass filter of the form:<br>
113 * y[n] = (1 - gain) x[n] + gain y[n-1]<br>
114 * where gain = e<sup>-dt / T</sup>, T is the time constant in seconds
115 *
116 * Note: T = 1 / (2 pi f) where f is the cutoff frequency in Hz, the frequency
117 * above which the input starts to attenuate.
118 *
119 * This filter is stable for time constants greater than zero.
120 *
121 * @param timeConstant The discrete-time time constant in seconds.
122 * @param period The period in seconds between samples taken by the
123 * user.
124 */
125 static LinearFilter<T> SinglePoleIIR(double timeConstant,
126 units::second_t period) {
127 double gain = std::exp(-period.value() / timeConstant);
128 return LinearFilter({1.0 - gain}, {-gain});
129 }
130
131 /**
132 * Creates a first-order high-pass filter of the form:<br>
133 * y[n] = gain x[n] + (-gain) x[n-1] + gain y[n-1]<br>
134 * where gain = e<sup>-dt / T</sup>, T is the time constant in seconds
135 *
136 * Note: T = 1 / (2 pi f) where f is the cutoff frequency in Hz, the frequency
137 * below which the input starts to attenuate.
138 *
139 * This filter is stable for time constants greater than zero.
140 *
141 * @param timeConstant The discrete-time time constant in seconds.
142 * @param period The period in seconds between samples taken by the
143 * user.
144 */
145 static LinearFilter<T> HighPass(double timeConstant, units::second_t period) {
146 double gain = std::exp(-period.value() / timeConstant);
147 return LinearFilter({gain, -gain}, {-gain});
148 }
149
150 /**
151 * Creates a K-tap FIR moving average filter of the form:<br>
152 * y[n] = 1/k (x[k] + x[k-1] + … + x[0])
153 *
154 * This filter is always stable.
155 *
156 * @param taps The number of samples to average over. Higher = smoother but
157 * slower
158 * @throws std::runtime_error if number of taps is less than 1.
159 */
161 if (taps <= 0) {
162 throw std::runtime_error("Number of taps must be greater than zero.");
163 }
164
165 std::vector<double> gains(taps, 1.0 / taps);
166 return LinearFilter(gains, {});
167 }
168
169 /**
170 * Creates a finite difference filter that computes the nth derivative of the
171 * input given the specified stencil points.
172 *
173 * Stencil points are the indices of the samples to use in the finite
174 * difference. 0 is the current sample, -1 is the previous sample, -2 is the
175 * sample before that, etc. Don't use positive stencil points (samples from
176 * the future) if the LinearFilter will be used for stream-based online
177 * filtering (e.g., taking derivative of encoder samples in real-time).
178 *
179 * @tparam Derivative The order of the derivative to compute.
180 * @tparam Samples The number of samples to use to compute the given
181 * derivative. This must be one more than the order of
182 * the derivative or higher.
183 * @param stencil List of stencil points.
184 * @param period The period in seconds between samples taken by the user.
185 */
186 template <int Derivative, int Samples>
188 const std::array<int, Samples> stencil, units::second_t period) {
189 // See
190 // https://en.wikipedia.org/wiki/Finite_difference_coefficient#Arbitrary_stencil_points
191 //
192 // For a given list of stencil points s of length n and the order of
193 // derivative d < n, the finite difference coefficients can be obtained by
194 // solving the following linear system for the vector a.
195 //
196 // [s₁⁰ ⋯ sₙ⁰ ][a₁] [ δ₀,d ]
197 // [ ⋮ ⋱ ⋮ ][⋮ ] = d! [ ⋮ ]
198 // [s₁ⁿ⁻¹ ⋯ sₙⁿ⁻¹][aₙ] [δₙ₋₁,d]
199 //
200 // where δᵢ,ⱼ are the Kronecker delta. The FIR gains are the elements of the
201 // vector a in reverse order divided by hᵈ.
202 //
203 // The order of accuracy of the approximation is of the form O(hⁿ⁻ᵈ).
204
205 static_assert(Derivative >= 1,
206 "Order of derivative must be greater than or equal to one.");
207 static_assert(Samples > 0, "Number of samples must be greater than zero.");
208 static_assert(Derivative < Samples,
209 "Order of derivative must be less than number of samples.");
210
211 Eigen::Matrix<double, Samples, Samples> S;
212 for (int row = 0; row < Samples; ++row) {
213 for (int col = 0; col < Samples; ++col) {
214 S(row, col) = std::pow(stencil[col], row);
215 }
216 }
217
218 // Fill in Kronecker deltas: https://en.wikipedia.org/wiki/Kronecker_delta
219 Eigen::Vector<double, Samples> d;
220 for (int i = 0; i < Samples; ++i) {
221 d(i) = (i == Derivative) ? Factorial(Derivative) : 0.0;
222 }
223
224 Eigen::Vector<double, Samples> a =
225 S.householderQr().solve(d) / std::pow(period.value(), Derivative);
226
227 // Reverse gains list
228 std::vector<double> ffGains;
229 for (int i = Samples - 1; i >= 0; --i) {
230 ffGains.push_back(a(i));
231 }
232
233 return LinearFilter(ffGains, {});
234 }
235
236 /**
237 * Creates a backward finite difference filter that computes the nth
238 * derivative of the input given the specified number of samples.
239 *
240 * For example, a first derivative filter that uses two samples and a sample
241 * period of 20 ms would be
242 *
243 * <pre><code>
244 * LinearFilter<double>::BackwardFiniteDifference<1, 2>(20_ms);
245 * </code></pre>
246 *
247 * @tparam Derivative The order of the derivative to compute.
248 * @tparam Samples The number of samples to use to compute the given
249 * derivative. This must be one more than the order of
250 * derivative or higher.
251 * @param period The period in seconds between samples taken by the user.
252 */
253 template <int Derivative, int Samples>
254 static LinearFilter<T> BackwardFiniteDifference(units::second_t period) {
255 // Generate stencil points from -(samples - 1) to 0
256 std::array<int, Samples> stencil{};
257 for (int i = 0; i < Samples; ++i) {
258 stencil[i] = -(Samples - 1) + i;
259 }
260
261 return FiniteDifference<Derivative, Samples>(stencil, period);
262 }
263
264 /**
265 * Reset the filter state.
266 */
267 void Reset() {
268 std::fill(m_inputs.begin(), m_inputs.end(), T{0.0});
269 std::fill(m_outputs.begin(), m_outputs.end(), T{0.0});
270 }
271
272 /**
273 * Resets the filter state, initializing internal buffers to the provided
274 * values.
275 *
276 * These are the expected lengths of the buffers, depending on what type of
277 * linear filter used:
278 *
279 * <table>
280 * <tr>
281 * <th>Type</th>
282 * <th>Input Buffer Size</th>
283 * <th>Output Buffer Size</th>
284 * </tr>
285 * <tr>
286 * <td>Unspecified</td>
287 * <td>size of {@code ffGains}</td>
288 * <td>size of {@code fbGains}</td>
289 * </tr>
290 * <tr>
291 * <td>Single Pole IIR</td>
292 * <td>1</td>
293 * <td>1</td>
294 * </tr>
295 * <tr>
296 * <td>High-Pass</td>
297 * <td>2</td>
298 * <td>1</td>
299 * </tr>
300 * <tr>
301 * <td>Moving Average</td>
302 * <td>{@code taps}</td>
303 * <td>0</td>
304 * </tr>
305 * <tr>
306 * <td>Finite Difference</td>
307 * <td>size of {@code stencil}</td>
308 * <td>0</td>
309 * </tr>
310 * <tr>
311 * <td>Backward Finite Difference</td>
312 * <td>{@code Samples}</td>
313 * <td>0</td>
314 * </tr>
315 * </table>
316 *
317 * @param inputBuffer Values to initialize input buffer.
318 * @param outputBuffer Values to initialize output buffer.
319 * @throws std::runtime_error if size of inputBuffer or outputBuffer does not
320 * match the size of ffGains and fbGains provided in the constructor.
321 */
322 void Reset(span<const T> inputBuffer, span<const T> outputBuffer) {
323 // Clear buffers
324 Reset();
325
326 if (inputBuffer.size() != m_inputGains.size() ||
327 outputBuffer.size() != m_outputGains.size()) {
328 throw std::runtime_error(
329 "Incorrect length of inputBuffer or outputBuffer");
330 }
331
332 for (size_t i = 0; i < inputBuffer.size(); ++i) {
333 m_inputs.push_front(inputBuffer[i]);
334 }
335 for (size_t i = 0; i < outputBuffer.size(); ++i) {
336 m_outputs.push_front(outputBuffer[i]);
337 }
338 }
339
340 /**
341 * Calculates the next value of the filter.
342 *
343 * @param input Current input value.
344 *
345 * @return The filtered value at this step
346 */
347 T Calculate(T input) {
348 T retVal{0.0};
349
350 // Rotate the inputs
351 if (m_inputGains.size() > 0) {
352 m_inputs.push_front(input);
353 }
354
355 // Calculate the new value
356 for (size_t i = 0; i < m_inputGains.size(); ++i) {
357 retVal += m_inputs[i] * m_inputGains[i];
358 }
359 for (size_t i = 0; i < m_outputGains.size(); ++i) {
360 retVal -= m_outputs[i] * m_outputGains[i];
361 }
362
363 // Rotate the outputs
364 if (m_outputGains.size() > 0) {
365 m_outputs.push_front(retVal);
366 }
367
368 return retVal;
369 }
370
371 /**
372 * Returns the last value calculated by the LinearFilter.
373 *
374 * @return The last value.
375 */
376 T LastValue() const { return m_outputs.front(); }
377
378 private:
379 circular_buffer<T> m_inputs;
380 circular_buffer<T> m_outputs;
381 std::vector<double> m_inputGains;
382 std::vector<double> m_outputGains;
383
384 /**
385 * Factorial of n.
386 *
387 * @param n Argument of which to take factorial.
388 */
389 static constexpr int Factorial(int n) {
390 if (n < 2) {
391 return 1;
392 } else {
393 return n * Factorial(n - 1);
394 }
395 }
396};
397
398}
399}
400}
This class implements a linear, digital filter.
Definition LinearFilter.hpp:78
LinearFilter(std::initializer_list< double > ffGains, std::initializer_list< double > fbGains)
Create a linear FIR or IIR filter.
Definition LinearFilter.hpp:105
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.hpp:254
static LinearFilter< T > FiniteDifference(const std::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.hpp:187
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.hpp:125
void Reset()
Reset the filter state.
Definition LinearFilter.hpp:267
T LastValue() const
Returns the last value calculated by the LinearFilter.
Definition LinearFilter.hpp:376
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.hpp:145
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.hpp:160
LinearFilter(span< const double > ffGains, span< const double > fbGains)
Create a linear FIR or IIR filter.
Definition LinearFilter.hpp:86
T Calculate(T input)
Calculates the next value of the filter.
Definition LinearFilter.hpp:347
void Reset(span< const T > inputBuffer, span< const T > outputBuffer)
Resets the filter state, initializing internal buffers to the provided values.
Definition LinearFilter.hpp:322
This is a simple circular buffer so we don't need to "bucket brigade" copy old values.
Definition circular_buffer.hpp:24
Definition span.hpp:134
constexpr size_type size() const noexcept
Definition span.hpp:305
Definition StatusCodes.h:18