Skip to content

Commit 01c3655

Browse files
committed
Use new stateful filter API for filtfilt
This provides reduces the memory allocation and provides a small speed boost (on 0.4; on 0.3, where SubArrays are slow, it's probably substantially slower). We could conceivably use this to provide in-place filtfilt, although that is not yet implemented. I removed support for non-1 initial values of a in filt_stepstate, since we don't need it if the filter is normalized before it is used.
1 parent bcca68b commit 01c3655

File tree

2 files changed

+114
-98
lines changed

2 files changed

+114
-98
lines changed

src/Filters/filt.jl

Lines changed: 111 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -183,52 +183,113 @@ DF2TFilter(coef::FilterCoefficients) = DF2TFilter(convert(SecondOrderSections, c
183183
# filtfilt
184184
#
185185

186-
# Extrapolate the beginning of a signal for use by filtfilt. This
187-
# computes:
188-
#
189-
# [(2 * x[1]) .- x[pad_length+1:-1:2],
190-
# x,
191-
# (2 * x[end]) .- x[end-1:-1:end-pad_length]]
192-
#
193-
# in place in output. The istart and n parameters determine the portion
194-
# of the input signal x to extrapolate.
195-
function extrapolate_signal!(out, ostart, sig, istart, n, pad_length)
196-
length(out) >= n+2*pad_length || error("output is incorrectly sized")
197-
x = 2*sig[istart]
186+
# Extrapolate the beginning of a signal for use by filtfilt. Computes:
187+
# [(2 * x[1]) .- x[pad_length+1:-1:2]]
188+
# in place in output.
189+
function extrapolate_start!(out, sig)
190+
x = 2*sig[1]
191+
pad_length = length(out)
198192
for i = 1:pad_length
199-
out[ostart+i-1] = x - sig[istart+pad_length+1-i]
193+
out[i] = x - sig[pad_length+2-i]
200194
end
201-
copy!(out, ostart+pad_length, sig, istart, n)
202-
x = 2*sig[istart+n-1]
203-
for i = 1:pad_length
204-
out[ostart+n+pad_length+i-1] = x - sig[istart+n-1-i]
195+
out
196+
end
197+
198+
# Extrapolate the end of a signal for use by filtfilt. Computes:
199+
# (2 * x[end]) .- x[end-1:-1:end-pad_length]]
200+
# in place in output.
201+
function extrapolate_end!(out, sig)
202+
x = 2*sig[end]
203+
for i = 1:length(out)
204+
out[i] = x - sig[end-i]
205205
end
206206
out
207207
end
208208

209-
# Zero phase digital filtering by processing data in forward and reverse direction
210-
function iir_filtfilt(b::AbstractVector, a::AbstractVector, x::AbstractArray)
211-
zi = filt_stepstate(b, a)
209+
function filtfilt{T}(coefs::PolynomialRatio{T}, x::AbstractArray)
210+
zi = filt_stepstate(coefb(coefs), coefa(coefs))
212211
zitmp = copy(zi)
213-
pad_length = 3 * (max(length(a), length(b)) - 1)
214-
t = Base.promote_eltype(b, a, x)
215-
extrapolated = Array(t, size(x, 1)+pad_length*2)
212+
pad_length = 3 * (max(length(coefs.a), length(coefs.b)) - 1)
213+
t = Base.promote_eltype(T, x)
214+
extrapolated = Array(t, pad_length)
216215
out = similar(x, t)
216+
f = DF2TFilter(coefs, zitmp)
217217

218-
istart = 1
219218
for i = 1:Base.trailingsize(x, 2)
220-
extrapolate_signal!(extrapolated, 1, x, istart, size(x, 1), pad_length)
221-
reverse!(filt!(extrapolated, b, a, extrapolated, scale!(zitmp, zi, extrapolated[1])))
222-
filt!(extrapolated, b, a, extrapolated, scale!(zitmp, zi, extrapolated[1]))
223-
for j = 1:size(x, 1)
224-
@inbounds out[j, i] = extrapolated[end-pad_length+1-j]
219+
# Forward pass
220+
sigout = sub(out, :, i)
221+
sig = sub(x, :, i)
222+
extrapolate_start!(extrapolated, sig)
223+
scale!(zitmp, zi, extrapolated[1])
224+
filt!(extrapolated, f, extrapolated)
225+
filt!(sigout, f, sig)
226+
extrapolate_end!(extrapolated, sig)
227+
filt!(extrapolated, f, extrapolated)
228+
229+
# Reverse pass
230+
sigoutrev = sub(out, size(x, 1):-1:1, i)
231+
reverse!(extrapolated)
232+
scale!(zitmp, zi, extrapolated[1])
233+
filt!(extrapolated, f, extrapolated)
234+
filt!(sigoutrev, f, sigoutrev)
235+
end
236+
237+
out
238+
end
239+
240+
# Extract si for a biquad, multiplied by a scaling factor
241+
function biquad_si!(zitmp, zi, i, scal)
242+
zitmp[1] = zi[1, i]*scal
243+
zitmp[2] = zi[2, i]*scal
244+
zitmp
245+
end
246+
247+
# Zero phase digital filtering for second order sections
248+
function filtfilt{T,G,S}(coefs::SecondOrderSections{T,G}, x::AbstractArray{S})
249+
zi = filt_stepstate(coefs)
250+
zitmp = zeros(2)
251+
pad_length = 3*size(zi, 1)
252+
t = Base.promote_type(T, G, S)
253+
extrapolated_start = Array(t, pad_length)
254+
extrapolated_end = Array(t, pad_length)
255+
out = similar(x, t)
256+
257+
f1 = DF2TFilter(coefs.biquads[1]*coefs.g, zitmp)
258+
f = Array(typeof(f1), length(coefs.biquads))
259+
f[1] = f1
260+
for i = 2:length(coefs.biquads)
261+
f[i] = DF2TFilter(coefs.biquads[i], zitmp)
262+
end
263+
264+
istart = 1
265+
for i = 1:size(x, 2)
266+
sigout = sub(out, :, i)
267+
sig = sub(x, :, i)
268+
sigoutrev = sub(out, size(x, 1):-1:1, i)
269+
270+
for j = 1:length(f)
271+
# Forward pass
272+
extrapolate_start!(extrapolated_start, j == 1 ? sig : sigout)
273+
extrapolate_end!(extrapolated_end, j == 1 ? sig : sigout)
274+
biquad_si!(zitmp, zi, j, extrapolated_start[1])
275+
filt!(extrapolated_start, f[j], extrapolated_start)
276+
filt!(sigout, f[j], j == 1 ? sig : sigout)
277+
filt!(extrapolated_end, f[j], extrapolated_end)
278+
279+
# Reverse pass
280+
reverse!(extrapolated_end)
281+
biquad_si!(zitmp, zi, j, extrapolated_end[1])
282+
filt!(extrapolated_end, f[j], extrapolated_end)
283+
filt!(sigoutrev, f[j], sigoutrev)
225284
end
226-
istart += size(x, 1)
227285
end
228286

229287
out
230288
end
231289

290+
# Support for other filter types
291+
filtfilt(f::FilterCoefficients, x) = filtfilt(convert(SecondOrderSections, f), x)
292+
232293
# Zero phase digital filtering with an FIR filter in a single pass
233294
function fir_filtfilt(b::AbstractVector, x::AbstractArray)
234295
nb = length(b)
@@ -245,7 +306,10 @@ function fir_filtfilt(b::AbstractVector, x::AbstractArray)
245306

246307
# Extrapolate each column
247308
for i = 1:size(extrapolated, 2)
248-
extrapolate_signal!(extrapolated, (i-1)*size(extrapolated, 1)+1, x, (i-1)*size(x, 1)+1, size(x, 1), nb-1)
309+
sig = sub(x, :, i)
310+
extrapolate_start!(sub(extrapolated, 1:nb-1, i), sig)
311+
copy!(extrapolated, (i-1)*size(extrapolated, 1)+nb, x, (i-1)*size(x, 1)+1, size(x, 1))
312+
extrapolate_end!(sub(extrapolated, size(extrapolated, 1)-nb+2:size(extrapolated, 1), i), sig)
249313
end
250314

251315
# Filter
@@ -255,91 +319,43 @@ function fir_filtfilt(b::AbstractVector, x::AbstractArray)
255319
reshape(out[2nb-1:end, :], size(x))
256320
end
257321

258-
# Choose whether to use fir_filtfilt or iir_filtfilt depending on
259-
# length of a
260-
function filtfilt(b::AbstractVector, a::AbstractVector, x::AbstractArray)
322+
# Support for passing b and a
323+
function filtfilt(b::AbstractVector, a::AbstractVector, x)
261324
if length(a) == 1
262325
if a[1] != 1
263326
b /= a[1]
264327
end
265328
fir_filtfilt(b, x)
266329
else
267-
iir_filtfilt(b, a, x)
268-
end
269-
end
330+
bs = length(b)
331+
as = length(a)
332+
sz = max(bs, as)
270333

271-
# Extract si for a biquad, multiplied by a scaling factor
272-
function biquad_si!(zitmp, zi, i, scal)
273-
zitmp[1] = zi[1, i]*scal
274-
zitmp[2] = zi[2, i]*scal
275-
zitmp
276-
end
334+
# Pad the coefficients with zeros if needed
335+
bs<sz && (b = copy!(zeros(eltype(b), sz), b))
336+
as<sz && (a = copy!(zeros(eltype(a), sz), a))
337+
sz > 0 || error("a and b must have at least one element each")
277338

278-
# Zero phase digital filtering for second order sections
279-
function filtfilt{T,G,S}(f::SecondOrderSections{T,G}, x::AbstractArray{S})
280-
zi = filt_stepstate(f)
281-
zi2 = zeros(2)
282-
zitmp = zeros(2)
283-
pad_length = 3*size(zi, 1)
284-
t = Base.promote_type(T, G, S)
285-
extrapolated = Array(t, size(x, 1)+pad_length*2)
286-
out = similar(x, t)
287-
288-
istart = 1
289-
for i = 1:size(x, 2)
290-
# First biquad
291-
extrapolate_signal!(extrapolated, 1, x, istart, size(x, 1), pad_length)
292-
f2 = f.biquads[1]*f.g
293-
reverse!(filt!(extrapolated, f2, extrapolated, biquad_si!(zitmp, zi, 1, extrapolated[1])))
294-
reverse!(filt!(extrapolated, f2, extrapolated, biquad_si!(zitmp, zi, 1, extrapolated[1])))
295-
296-
# Subsequent biquads
297-
for j = 2:length(f.biquads)
298-
f2 = f.biquads[j]
299-
extrapolate_signal!(extrapolated, 1, extrapolated, pad_length+1, size(x, 1), pad_length)
300-
reverse!(filt!(extrapolated, f2, extrapolated, biquad_si!(zitmp, zi, j, extrapolated[1])))
301-
reverse!(filt!(extrapolated, f2, extrapolated, biquad_si!(zitmp, zi, j, extrapolated[1])))
302-
end
303-
304-
# Copy to output
305-
copy!(out, istart, extrapolated, pad_length+1, size(x, 1))
306-
istart += size(x, 1)
339+
filtfilt(PolynomialRatio(b, a), x)
307340
end
308-
309-
out
310341
end
311342

312-
# Support for other filter types
313-
filtfilt(f::FilterCoefficients, x) = filtfilt(convert(SecondOrderSections, f), x)
314-
filtfilt(f::PolynomialRatio, x) = filtfilt(coefb(f), coefa(f), x)
315-
316343
## Initial filter state
317344

318345
# Compute an initial state for filt with coefficients (b,a) such that its
319346
# response to a step function is steady state.
320347
function filt_stepstate{T<:Number}(b::Union(AbstractVector{T}, T), a::Union(AbstractVector{T}, T))
321-
scale_factor = a[1]
322-
if scale_factor != 1.0
323-
a = a ./ scale_factor
324-
b = b ./ scale_factor
325-
end
326-
327-
bs = length(b)
328-
as = length(a)
329-
sz = max(bs, as)
330-
sz > 0 || error("a and b must have at least one element each")
331-
sz == 1 && return T[]
332-
333-
# Pad the coefficients with zeros if needed
334-
bs<sz && (b = copy!(zeros(eltype(b), sz), b))
335-
as<sz && (a = copy!(zeros(eltype(a), sz), a))
348+
sz = length(b)
349+
sz == length(a) || throw(ArgumentError("a and b must have same length"))
350+
a[1] == 1 || throw(ArgumentError("first element of a must be 1"))
351+
max(length(b), length(a)) == 1 && return T[]
336352

337-
# construct the companion matrix A and vector B:
353+
# Construct the companion matrix A and vector B:
338354
A = [-a[2:end] [eye(T, sz-2); zeros(T, 1, sz-2)]]
339355
B = b[2:end] - a[2:end] * b[1]
340356
# Solve si = A*si + B
341357
# (I - A)*si = B
342-
scale_factor \ (I - A) \ B
358+
(I - A) \ B
343359
end
344360

345361
function filt_stepstate{T}(f::SecondOrderSections{T})

test/filt.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ zi_python = [0.55996501, -0.72343165, 0.68312446, -0.2220676 , 0.04030775]
107107
b = [ 0.00327922, 0.01639608, 0.03279216, 0.03279216, 0.01639608, 0.00327922]
108108
a = [ 1.1 , -2.47441617, 2.81100631, -1.70377224, 0.54443269, -0.07231567]
109109

110-
@test_approx_eq_eps zi_python DSP.Filters.filt_stepstate(b, a) 1e-7
110+
@test_throws ArgumentError DSP.Filters.filt_stepstate(b, a)
111111

112112

113113
##############
@@ -266,6 +266,6 @@ end
266266

267267
b = randn(10)
268268
for x in (randn(100), randn(100, 2))
269-
@test_approx_eq DSP.Filters.fir_filtfilt(b, x) DSP.Filters.iir_filtfilt(b, [1.0], x)
270-
@test_approx_eq DSP.Filters.filtfilt(b, [2.0], x) DSP.Filters.iir_filtfilt(b, [2.0], x)
269+
@test_approx_eq DSP.Filters.fir_filtfilt(b, x) DSP.Filters.filtfilt(PolynomialRatio(b, [1.0; zeros(9)]), x)
270+
@test_approx_eq DSP.Filters.filtfilt(b, [2.0], x) DSP.Filters.filtfilt(PolynomialRatio(b, [2.0; zeros(9)]), x)
271271
end

0 commit comments

Comments
 (0)