001// Copyright (c) FIRST and other WPILib contributors. 002// Open Source Software; you can modify and/or share it under the terms of 003// the WPILib BSD license file in the root directory of this project. 004 005package edu.wpi.first.math.filter; 006 007import edu.wpi.first.math.MathSharedStore; 008import edu.wpi.first.math.MathUsageId; 009import edu.wpi.first.util.CircularBuffer; 010import java.util.Arrays; 011import org.ejml.simple.SimpleMatrix; 012 013/** 014 * This class implements a linear, digital filter. All types of FIR and IIR filters are supported. 015 * Static factory methods are provided to create commonly used types of filters. 016 * 017 * <p>Filters are of the form: y[n] = (b0 x[n] + b1 x[n-1] + ... + bP x[n-P]) - (a0 y[n-1] + a2 018 * y[n-2] + ... + aQ y[n-Q]) 019 * 020 * <p>Where: y[n] is the output at time "n" x[n] is the input at time "n" y[n-1] is the output from 021 * the LAST time step ("n-1") x[n-1] is the input from the LAST time step ("n-1") b0...bP are the 022 * "feedforward" (FIR) gains a0...aQ are the "feedback" (IIR) gains IMPORTANT! Note the "-" sign in 023 * front of the feedback term! This is a common convention in signal processing. 024 * 025 * <p>What can linear filters do? Basically, they can filter, or diminish, the effects of 026 * undesirable input frequencies. High frequencies, or rapid changes, can be indicative of sensor 027 * noise or be otherwise undesirable. A "low pass" filter smooths out the signal, reducing the 028 * impact of these high frequency components. Likewise, a "high pass" filter gets rid of slow-moving 029 * signal components, letting you detect large changes more easily. 030 * 031 * <p>Example FRC applications of filters: - Getting rid of noise from an analog sensor input (note: 032 * the roboRIO's FPGA can do this faster in hardware) - Smoothing out joystick input to prevent the 033 * wheels from slipping or the robot from tipping - Smoothing motor commands so that unnecessary 034 * strain isn't put on electrical or mechanical components - If you use clever gains, you can make a 035 * PID controller out of this class! 036 * 037 * <p>For more on filters, we highly recommend the following articles:<br> 038 * https://en.wikipedia.org/wiki/Linear_filter<br> 039 * https://en.wikipedia.org/wiki/Iir_filter<br> 040 * https://en.wikipedia.org/wiki/Fir_filter<br> 041 * 042 * <p>Note 1: calculate() should be called by the user on a known, regular period. You can use a 043 * Notifier for this or do it "inline" with code in a periodic function. 044 * 045 * <p>Note 2: For ALL filters, gains are necessarily a function of frequency. If you make a filter 046 * that works well for you at, say, 100Hz, you will most definitely need to adjust the gains if you 047 * then want to run it at 200Hz! Combining this with Note 1 - the impetus is on YOU as a developer 048 * to make sure calculate() gets called at the desired, constant frequency! 049 */ 050public class LinearFilter { 051 private final CircularBuffer m_inputs; 052 private final CircularBuffer m_outputs; 053 private final double[] m_inputGains; 054 private final double[] m_outputGains; 055 056 private static int instances; 057 058 /** 059 * Create a linear FIR or IIR filter. 060 * 061 * @param ffGains The "feedforward" or FIR gains. 062 * @param fbGains The "feedback" or IIR gains. 063 */ 064 public LinearFilter(double[] ffGains, double[] fbGains) { 065 m_inputs = new CircularBuffer(ffGains.length); 066 m_outputs = new CircularBuffer(fbGains.length); 067 m_inputGains = Arrays.copyOf(ffGains, ffGains.length); 068 m_outputGains = Arrays.copyOf(fbGains, fbGains.length); 069 070 instances++; 071 MathSharedStore.reportUsage(MathUsageId.kFilter_Linear, instances); 072 } 073 074 /** 075 * Creates a one-pole IIR low-pass filter of the form: y[n] = (1-gain) x[n] + gain y[n-1] where 076 * gain = e<sup>-dt / T</sup>, T is the time constant in seconds. 077 * 078 * <p>Note: T = 1 / (2 pi f) where f is the cutoff frequency in Hz, the frequency above which the 079 * input starts to attenuate. 080 * 081 * <p>This filter is stable for time constants greater than zero. 082 * 083 * @param timeConstant The discrete-time time constant in seconds. 084 * @param period The period in seconds between samples taken by the user. 085 * @return Linear filter. 086 */ 087 public static LinearFilter singlePoleIIR(double timeConstant, double period) { 088 double gain = Math.exp(-period / timeConstant); 089 double[] ffGains = {1.0 - gain}; 090 double[] fbGains = {-gain}; 091 092 return new LinearFilter(ffGains, fbGains); 093 } 094 095 /** 096 * Creates a first-order high-pass filter of the form: y[n] = gain x[n] + (-gain) x[n-1] + gain 097 * y[n-1] where gain = e<sup>-dt / T</sup>, T is the time constant in seconds. 098 * 099 * <p>Note: T = 1 / (2 pi f) where f is the cutoff frequency in Hz, the frequency below which the 100 * input starts to attenuate. 101 * 102 * <p>This filter is stable for time constants greater than zero. 103 * 104 * @param timeConstant The discrete-time time constant in seconds. 105 * @param period The period in seconds between samples taken by the user. 106 * @return Linear filter. 107 */ 108 public static LinearFilter highPass(double timeConstant, double period) { 109 double gain = Math.exp(-period / timeConstant); 110 double[] ffGains = {gain, -gain}; 111 double[] fbGains = {-gain}; 112 113 return new LinearFilter(ffGains, fbGains); 114 } 115 116 /** 117 * Creates a K-tap FIR moving average filter of the form: y[n] = 1/k (x[k] + x[k-1] + ... + x[0]). 118 * 119 * <p>This filter is always stable. 120 * 121 * @param taps The number of samples to average over. Higher = smoother but slower. 122 * @return Linear filter. 123 * @throws IllegalArgumentException if number of taps is less than 1. 124 */ 125 public static LinearFilter movingAverage(int taps) { 126 if (taps <= 0) { 127 throw new IllegalArgumentException("Number of taps was not at least 1"); 128 } 129 130 double[] ffGains = new double[taps]; 131 for (int i = 0; i < ffGains.length; i++) { 132 ffGains[i] = 1.0 / taps; 133 } 134 135 double[] fbGains = new double[0]; 136 137 return new LinearFilter(ffGains, fbGains); 138 } 139 140 /** 141 * Creates a backward finite difference filter that computes the nth derivative of the input given 142 * the specified number of samples. 143 * 144 * <p>For example, a first derivative filter that uses two samples and a sample period of 20 ms 145 * would be 146 * 147 * <pre><code> 148 * LinearFilter.backwardFiniteDifference(1, 2, 0.02); 149 * </code></pre> 150 * 151 * @param derivative The order of the derivative to compute. 152 * @param samples The number of samples to use to compute the given derivative. This must be one 153 * more than the order of derivative or higher. 154 * @param period The period in seconds between samples taken by the user. 155 * @return Linear filter. 156 */ 157 @SuppressWarnings("LocalVariableName") 158 public static LinearFilter backwardFiniteDifference(int derivative, int samples, double period) { 159 // See 160 // https://en.wikipedia.org/wiki/Finite_difference_coefficient#Arbitrary_stencil_points 161 // 162 // <p>For a given list of stencil points s of length n and the order of 163 // derivative d < n, the finite difference coefficients can be obtained by 164 // solving the following linear system for the vector a. 165 // 166 // <pre> 167 // [s₁⁰ ⋯ sₙ⁰ ][a₁] [ δ₀,d ] 168 // [ ⋮ ⋱ ⋮ ][⋮ ] = d! [ ⋮ ] 169 // [s₁ⁿ⁻¹ ⋯ sₙⁿ⁻¹][aₙ] [δₙ₋₁,d] 170 // </pre> 171 // 172 // <p>where δᵢ,ⱼ are the Kronecker delta. For backward finite difference, 173 // the stencil points are the range [-n + 1, 0]. The FIR gains are the 174 // elements of the vector a in reverse order divided by hᵈ. 175 // 176 // <p>The order of accuracy of the approximation is of the form O(hⁿ⁻ᵈ). 177 178 if (derivative < 1) { 179 throw new IllegalArgumentException( 180 "Order of derivative must be greater than or equal to one."); 181 } 182 183 if (samples <= 0) { 184 throw new IllegalArgumentException("Number of samples must be greater than zero."); 185 } 186 187 if (derivative >= samples) { 188 throw new IllegalArgumentException( 189 "Order of derivative must be less than number of samples."); 190 } 191 192 var S = new SimpleMatrix(samples, samples); 193 for (int row = 0; row < samples; ++row) { 194 for (int col = 0; col < samples; ++col) { 195 double s = 1 - samples + col; 196 S.set(row, col, Math.pow(s, row)); 197 } 198 } 199 200 // Fill in Kronecker deltas: https://en.wikipedia.org/wiki/Kronecker_delta 201 var d = new SimpleMatrix(samples, 1); 202 for (int i = 0; i < samples; ++i) { 203 d.set(i, 0, (i == derivative) ? factorial(derivative) : 0.0); 204 } 205 206 var a = S.solve(d).divide(Math.pow(period, derivative)); 207 208 // Reverse gains list 209 double[] ffGains = new double[samples]; 210 for (int i = 0; i < samples; ++i) { 211 ffGains[i] = a.get(samples - i - 1, 0); 212 } 213 214 double[] fbGains = new double[0]; 215 216 return new LinearFilter(ffGains, fbGains); 217 } 218 219 /** Reset the filter state. */ 220 public void reset() { 221 m_inputs.clear(); 222 m_outputs.clear(); 223 } 224 225 /** 226 * Calculates the next value of the filter. 227 * 228 * @param input Current input value. 229 * @return The filtered value at this step 230 */ 231 public double calculate(double input) { 232 double retVal = 0.0; 233 234 // Rotate the inputs 235 if (m_inputGains.length > 0) { 236 m_inputs.addFirst(input); 237 } 238 239 // Calculate the new value 240 for (int i = 0; i < m_inputGains.length; i++) { 241 retVal += m_inputs.get(i) * m_inputGains[i]; 242 } 243 for (int i = 0; i < m_outputGains.length; i++) { 244 retVal -= m_outputs.get(i) * m_outputGains[i]; 245 } 246 247 // Rotate the outputs 248 if (m_outputGains.length > 0) { 249 m_outputs.addFirst(retVal); 250 } 251 252 return retVal; 253 } 254 255 /** 256 * Factorial of n. 257 * 258 * @param n Argument of which to take factorial. 259 */ 260 private static int factorial(int n) { 261 if (n < 2) { 262 return 1; 263 } else { 264 return n * factorial(n - 1); 265 } 266 } 267}