001/*----------------------------------------------------------------------------*/
002/* Copyright (c) FIRST 2015-2017. All Rights Reserved.                        */
003/* Open Source Software - may be modified and shared by FRC teams. The code   */
004/* must be accompanied by the FIRST BSD license file in the root directory of */
005/* the project.                                                               */
006/*----------------------------------------------------------------------------*/
007
008package edu.wpi.first.wpilibj.filters;
009
010import edu.wpi.first.wpilibj.CircularBuffer;
011import edu.wpi.first.wpilibj.PIDSource;
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] +
018 * a2*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
029 * slow-moving 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, I highly recommend the following articles: http://en.wikipedia
038 * .org/wiki/Linear_filter http://en.wikipedia.org/wiki/Iir_filter http://en.wikipedia
039 * .org/wiki/Fir_filter
040 *
041 * <p>Note 1: PIDGet() should be called by the user on a known, regular period. You can set up a
042 * Notifier to do this (look at the WPILib PIDController class), or do it "inline" with code in a
043 * 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 PIDGet() gets called at the desired, constant frequency!
049 */
050public class LinearDigitalFilter extends Filter {
051  private CircularBuffer m_inputs;
052  private CircularBuffer m_outputs;
053  private double[] m_inputGains;
054  private double[] m_outputGains;
055
056  /**
057   * Create a linear FIR or IIR filter.
058   *
059   * @param source  The PIDSource object that is used to get values
060   * @param ffGains The "feed forward" or FIR gains
061   * @param fbGains The "feed back" or IIR gains
062   */
063  public LinearDigitalFilter(PIDSource source, double[] ffGains,
064                             double[] fbGains) {
065    super(source);
066    m_inputs = new CircularBuffer(ffGains.length);
067    m_outputs = new CircularBuffer(fbGains.length);
068    m_inputGains = ffGains;
069    m_outputGains = fbGains;
070  }
071
072  /**
073   * Creates a one-pole IIR low-pass filter of the form: y[n] = (1-gain)*x[n] + gain*y[n-1] where
074   * gain = e^(-dt / T), T is the time constant in seconds.
075   *
076   * <p>This filter is stable for time constants greater than zero.
077   *
078   * @param source       The PIDSource object that is used to get values
079   * @param timeConstant The discrete-time time constant in seconds
080   * @param period       The period in seconds between samples taken by the user
081   */
082  public static LinearDigitalFilter singlePoleIIR(PIDSource source,
083                                                  double timeConstant,
084                                                  double period) {
085    double gain = Math.exp(-period / timeConstant);
086    double[] ffGains = {1.0 - gain};
087    double[] fbGains = {-gain};
088
089    return new LinearDigitalFilter(source, ffGains, fbGains);
090  }
091
092  /**
093   * Creates a first-order high-pass filter of the form: y[n] = gain*x[n] + (-gain)*x[n-1] +
094   * gain*y[n-1] where gain = e^(-dt / T), T is the time constant in seconds.
095   *
096   * <p>This filter is stable for time constants greater than zero.
097   *
098   * @param source       The PIDSource object that is used to get values
099   * @param timeConstant The discrete-time time constant in seconds
100   * @param period       The period in seconds between samples taken by the user
101   */
102  public static LinearDigitalFilter highPass(PIDSource source,
103                                             double timeConstant,
104                                             double period) {
105    double gain = Math.exp(-period / timeConstant);
106    double[] ffGains = {gain, -gain};
107    double[] fbGains = {-gain};
108
109    return new LinearDigitalFilter(source, ffGains, fbGains);
110  }
111
112  /**
113   * Creates a K-tap FIR moving average filter of the form: y[n] = 1/k * (x[k] + x[k-1] + ... +
114   * x[0]).
115   *
116   * <p>This filter is always stable.
117   *
118   * @param source The PIDSource object that is used to get values
119   * @param taps   The number of samples to average over. Higher = smoother but slower
120   * @throws IllegalArgumentException if number of taps is less than 1
121   */
122  public static LinearDigitalFilter movingAverage(PIDSource source, int taps) {
123    if (taps <= 0) {
124      throw new IllegalArgumentException("Number of taps was not at least 1");
125    }
126
127    double[] ffGains = new double[taps];
128    for (int i = 0; i < ffGains.length; i++) {
129      ffGains[i] = 1.0 / taps;
130    }
131
132    double[] fbGains = new double[0];
133
134    return new LinearDigitalFilter(source, ffGains, fbGains);
135  }
136
137  @Override
138  public double get() {
139    double retVal = 0.0;
140
141    // Calculate the new value
142    for (int i = 0; i < m_inputGains.length; i++) {
143      retVal += m_inputs.get(i) * m_inputGains[i];
144    }
145    for (int i = 0; i < m_outputGains.length; i++) {
146      retVal -= m_outputs.get(i) * m_outputGains[i];
147    }
148
149    return retVal;
150  }
151
152  @Override
153  public void reset() {
154    m_inputs.reset();
155    m_outputs.reset();
156  }
157
158  /**
159   * Calculates the next value of the filter.
160   *
161   * @return The filtered value at this step
162   */
163  @Override
164  public double pidGet() {
165    double retVal = 0.0;
166
167    // Rotate the inputs
168    m_inputs.pushFront(pidGetSource());
169
170    // Calculate the new value
171    for (int i = 0; i < m_inputGains.length; i++) {
172      retVal += m_inputs.get(i) * m_inputGains[i];
173    }
174    for (int i = 0; i < m_outputGains.length; i++) {
175      retVal -= m_outputs.get(i) * m_outputGains[i];
176    }
177
178    // Rotate the outputs
179    m_outputs.pushFront(retVal);
180
181    return retVal;
182  }
183}