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}