How to apply a butterworth filter in a loop?

I constructed a Butterworth filter, which works fine:

using ControlPlots, DSP

# Define the cut-off frequency in Hz
cut_off_freq = 2.0
Wn = cut_off_freq

# Define the sampling time in seconds
dt = 0.05
fs = 1/dt
sim_time = 4.0
N = Int(sim_time / dt)

# Design the filter
butter = Filters.digitalfilter(Filters.Lowpass(Wn; fs), Filters.Butterworth(4))

# Create an array of measurements
measurements = zeros(N)
for i in Int(N/2):N
    measurements[i] = 1.0
end

results = filt(butter, measurements)

# Plot the results
plot((1:N)*dt, [measurements, results]; xlabel="Time (s)", ylabel="Amplitude", 
     fig="Forth order Butterworth Filter")

But now I want to apply it online in a loop, similar to the way I am applying a first order filter in the following example:

results = zeros(N)

# Apply the EMA filter
last_measurement::Float64 = 0.0
for i in 1:N
    global last_measurement
    results[i] = ema_filter(measurements[i], last_measurement, cut_off_freq, dt)
    last_measurement = results[i]
end

function ema_filter(measurement, last_measurement, cut_off_freq, dt)
    if cut_off_freq > 0.0
        alpha = dt / (dt + one(measurement) / (2π * cut_off_freq))
        filtered_value = alpha * measurement + (one(measurement) - alpha) * last_measurement
    else
        filtered_value = measurement
    end
    return filtered_value
end

The point is, the filter will become part of a larger control system.

How can I apply the Butterworth filter sample-by-sample?

Can’t you just call DSP.filt! on one sample (or, more efficiently, on a batch of a few samples) at a time, passing the si array to track the cumulative effect of the previous samples?

More simply, you can use a stateful filter wrapper object with something like:

butterF = DSP.Filters.DF2TFilter(butter)

and then call filt!(out, butterF, x) to perform the filtering on a chunk of inputs x to get a chunk of outputs out.

3 Likes

This works, but is very inefficient:

using ControlPlots, DSP

# Define the cut-off frequency in Hz
cut_off_freq = 2.0

# Define the sampling time in seconds
dt = 0.05
fs = 1/dt
sim_time = 4.0
N = Int(sim_time / dt)

# Design the filter
butter = Filters.digitalfilter(Filters.Lowpass(cut_off_freq; fs), Filters.Butterworth(2))

# Create an array of measurements
measurements = zeros(N)
for i in Int(N/2):N
    measurements[i] = 1.0
end

function butter_filter(butter, measurement, buffer, index)
    buffer[index] = measurement
    res = filt(butter, buffer[1:index])
    return res[index]
end

buffer = zeros(N)
results = zeros(N)
for i in 1:N
    results[i] = butter_filter(butter, measurements[i], buffer, i)
end

# Plot the results
plot((1:N)*dt, [measurements, results]; xlabel="Time (s)", ylabel="Amplitude", 
     fig="Forth order Butterworth Filter")

I was reading I should convert the ZPG notation to transfer function coefficients and use those to create a difference equation which would be short and efficient. No idea how to do that, though.

Of course it’s inefficient — you are re-doing the entire filter from the beginning for every new measurement.

Using the filter state (see my suggestion) allows you to avoid this — each batch of new measurements requires only the new filter terms to be applied.

NIce, but I do not know how to put your suggestion into practice. A big issue with the DSP package is that there are no examples provided.

What did you try? The documentation may lack examples written out for you, but it documents the relevant functions.

For example, this code applies your filter 1 element at a time:

butterF = DSP.Filters.DF2TFilter(butter)
results2 = zeros(N)
for i = 1:N
    @views filt!(results2[i:i], butterF, measurements[i:i])
end

(Bigger batches should be more efficient, but the complexity should still be linear in N.)

3 Likes