Skip to content

Commit 3ad3c1c

Browse files
gruberchrChristian GruberViralBShah
authored
Enable Analog Filter Design (#458)
* Shift frequency normalization from construction of FilterType to filter design When designing a digital filter using function digitalfilter() the sampling frequency is not passed to the FilterType definition anymore, but to function digitalfilter(). That means frequency normalization is not done during construction of FilterType anymore, but during filter design inside function digitalfilter(). For this the sampling frequency has to be passed to the internal functions prewarp(), firprototype() and scalefactor() as well. The intended side effect is, that an analog filter can now be designed by specifying analog frequencies directly without any frequency normalization and without a sampling frequency. * Update documentation * Extend filter design tests Extend test case 'digital IIR' to test digital filter design with non-normalized frequency values in the filter response definition and specified sampling frequency. * Fix test set "freq. scaling" This is an amendment to commit 54bc9d5 where test sets have been introduced. The code following the current test set "freq. scaling" is actually a "lowpass" test. It seems that a corresponding comment on this low pass filter test was already missing before this commit. And the comment "Freqency scaling" seems to be meant for all four following test sets "lowpass", "highpass", "bandpass" and "bandstop". * Rename test set "freq. scaling" to "analogfilter" The new test set name relates to the name of the tested function `analogfilter()` and not to what it is doing. * Remove unnecessary conversion of filter representation In the current version the function `analogfilter()` already generates a `ZeroPoleGain` filter representation. It seems, that in earlier versions this wasn't the case (see e.g. commit 7fd2102) * Update analog filter design tests using non-normalized frequency values For providing new test data I had no MATLAB available and therefore switched to Octave. Unfortunatelly Octave's elliptic filter design function `ellip()` generates different filter coefficients than Julia when designing a 20th order analog filter. Maybe this is a bug in Octave's 'signal' package. I changed the test to design a Chebyshev type II filter instead. --------- Co-authored-by: Christian Gruber <Christian.Gruber-e6t@rub.de> Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com>
1 parent 2855b3e commit 3ad3c1c

File tree

5 files changed

+382
-361
lines changed

5 files changed

+382
-361
lines changed

docs/src/filters.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,18 @@ Bandpass
115115
Bandstop
116116
```
117117

118+
The interpretation of the frequencies `Wn`, `Wn1` and `Wn2` depends on wether an analog
119+
or a digital filter is designed.
120+
1. If an analog filter is designed using [`analogfilter`](@ref), the frequencies are
121+
interpreted as analog frequencies in radians/second.
122+
1. If a digital filter is designed using [`digitalfilter`](@ref) and the sampling
123+
frequency `fs` is specified, the frequencies of the filter response type are
124+
normalized to `fs`. This requires that the sampling frequency and the filter response
125+
type use the same frequency unit (Hz, radians/second, ...). If `fs` is not specified,
126+
the frequencies of the filter response type are interpreted as normalized frequencies
127+
in half-cycles/sample.
128+
129+
118130
### [Filter design methods](@id design-methods)
119131

120132
#### IIR filter design methods
@@ -192,18 +204,18 @@ Filter the data in `x`, sampled at 1000 Hz, with a 4th order
192204
Butterworth bandpass filter between 10 and 40 Hz:
193205

194206
```julia
195-
responsetype = Bandpass(10, 40; fs=1000)
207+
responsetype = Bandpass(10, 40)
196208
designmethod = Butterworth(4)
197-
filt(digitalfilter(responsetype, designmethod), x)
209+
filt(digitalfilter(responsetype, designmethod; fs=1000), x)
198210
```
199211

200212
Filter the data in `x`, sampled at 50 Hz, with a 64 tap Hanning
201213
window FIR lowpass filter at 5 Hz:
202214

203215
```julia
204-
responsetype = Lowpass(5; fs=50)
216+
responsetype = Lowpass(5)
205217
designmethod = FIRWindow(hanning(64))
206-
filt(digitalfilter(responsetype, designmethod), x)
218+
filt(digitalfilter(responsetype, designmethod; fs=50), x)
207219
```
208220

209221
Estimate a Lowpass Elliptic filter order with a normalized

src/Filters/design.jl

Lines changed: 36 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -245,42 +245,36 @@ struct Lowpass{T} <: FilterType
245245
end
246246

247247
"""
248-
Lowpass(Wn[; fs])
248+
Lowpass(Wn)
249249
250-
Low pass filter with cutoff frequency `Wn`. If `fs` is not
251-
specified, `Wn` is interpreted as a normalized frequency in
252-
half-cycles/sample.
250+
Low pass filter with cutoff frequency `Wn`.
253251
"""
254-
Lowpass(w::Real; fs::Real=2) = Lowpass{typeof(w/1)}(normalize_freq(w, fs))
252+
Lowpass(w::Real) = Lowpass{typeof(w/1)}(w)
255253

256254
struct Highpass{T} <: FilterType
257255
w::T
258256
end
259257

260258
"""
261-
Highpass(Wn[; fs])
259+
Highpass(Wn)
262260
263-
High pass filter with cutoff frequency `Wn`. If `fs` is not
264-
specified, `Wn` is interpreted as a normalized frequency in
265-
half-cycles/sample.
261+
High pass filter with cutoff frequency `Wn`.
266262
"""
267-
Highpass(w::Real; fs::Real=2) = Highpass{typeof(w/1)}(normalize_freq(w, fs))
263+
Highpass(w::Real) = Highpass{typeof(w/1)}(w)
268264

269265
struct Bandpass{T} <: FilterType
270266
w1::T
271267
w2::T
272268
end
273269

274270
"""
275-
Bandpass(Wn1, Wn2[; fs])
271+
Bandpass(Wn1, Wn2)
276272
277-
Band pass filter with normalized pass band (`Wn1`, `Wn2`). If
278-
`fs` is not specified, `Wn1` and `Wn2` are interpreted as
279-
normalized frequencies in half-cycles/sample.
273+
Band pass filter with pass band frequencies (`Wn1`, `Wn2`).
280274
"""
281-
function Bandpass(w1::Real, w2::Real; fs::Real=2)
275+
function Bandpass(w1::Real, w2::Real)
282276
w1 < w2 || error("w1 must be less than w2")
283-
Bandpass{Base.promote_typeof(w1/1, w2/1)}(normalize_freq(w1, fs), normalize_freq(w2, fs))
277+
Bandpass{Base.promote_typeof(w1/1, w2/1)}(w1, w2)
284278
end
285279

286280
struct Bandstop{T} <: FilterType
@@ -289,15 +283,13 @@ struct Bandstop{T} <: FilterType
289283
end
290284

291285
"""
292-
Bandstop(Wn1, Wn2[; fs])
286+
Bandstop(Wn1, Wn2)
293287
294-
Band stop filter with normalized stop band (`Wn1`, `Wn2`). If
295-
`fs` is not specified, `Wn1` and `Wn2` are interpreted as
296-
normalized frequencies in half-cycles/sample.
288+
Band stop filter with stop band frequencies (`Wn1`, `Wn2`).
297289
"""
298-
function Bandstop(w1::Real, w2::Real; fs::Real=2)
290+
function Bandstop(w1::Real, w2::Real)
299291
w1 < w2 || error("w1 must be less than w2")
300-
Bandstop{Base.promote_typeof(w1/1, w2/1)}(normalize_freq(w1, fs), normalize_freq(w2, fs))
292+
Bandstop{Base.promote_typeof(w1/1, w2/1)}(w1, w2)
301293
end
302294

303295
#
@@ -444,20 +436,20 @@ function bilinear(f::ZeroPoleGain{:s,Z,P,K}, fs::Real) where {Z,P,K}
444436
end
445437

446438
# Pre-warp filter frequencies for digital filtering
447-
prewarp(ftype::Union{Lowpass, Highpass}) = (typeof(ftype))(prewarp(ftype.w))
448-
prewarp(ftype::Union{Bandpass, Bandstop}) = (typeof(ftype))(prewarp(ftype.w1), prewarp(ftype.w2))
439+
prewarp(ftype::Union{Lowpass, Highpass}, fs::Real) = (typeof(ftype))(prewarp(normalize_freq(ftype.w, fs)))
440+
prewarp(ftype::Union{Bandpass, Bandstop}, fs::Real) = (typeof(ftype))(prewarp(normalize_freq(ftype.w1, fs)), prewarp(normalize_freq(ftype.w2, fs)))
449441
# freq in half-samples per cycle
450442
prewarp(f::Real) = 4*tan(pi*f/2)
451443

452444
# Digital filter design
453445
"""
454-
digitalfilter(responsetype, designmethod)
446+
digitalfilter(responsetype, designmethod[; fs])
455447
456448
Construct a digital filter. See below for possible response and
457449
filter types.
458450
"""
459-
digitalfilter(ftype::FilterType, proto::FilterCoefficients) =
460-
bilinear(transform_prototype(prewarp(ftype), proto), 2)
451+
digitalfilter(ftype::FilterType, proto::FilterCoefficients; fs::Real=2) =
452+
bilinear(transform_prototype(prewarp(ftype, fs), proto), 2)
461453

462454
#
463455
# Special filter types
@@ -541,61 +533,61 @@ FIRWindow(; transitionwidth::Real=throw(ArgumentError("must specify transitionwi
541533
FIRWindow(kaiser(kaiserord(transitionwidth, attenuation)...), scale)
542534

543535
# Compute coefficients for FIR prototype with specified order
544-
function firprototype(n::Integer, ftype::Lowpass)
545-
w = ftype.w
536+
function firprototype(n::Integer, ftype::Lowpass, fs::Real)
537+
w = normalize_freq(ftype.w, fs)
546538

547539
[w*sinc(w*(k-(n-1)/2)) for k = 0:(n-1)]
548540
end
549541

550-
function firprototype(n::Integer, ftype::Bandpass)
551-
w1 = ftype.w1
552-
w2 = ftype.w2
542+
function firprototype(n::Integer, ftype::Bandpass, fs::Real)
543+
w1 = normalize_freq(ftype.w1, fs)
544+
w2 = normalize_freq(ftype.w2, fs)
553545

554546
[w2*sinc(w2*(k-(n-1)/2)) - w1*sinc(w1*(k-(n-1)/2)) for k = 0:(n-1)]
555547
end
556548

557-
function firprototype(n::Integer, ftype::Highpass)
558-
w = ftype.w
549+
function firprototype(n::Integer, ftype::Highpass, fs::Real)
550+
w = normalize_freq(ftype.w, fs)
559551
isodd(n) || throw(ArgumentError("FIRWindow highpass filters must have an odd number of coefficients"))
560552

561553
out = [-w*sinc(w*(k-(n-1)/2)) for k = 0:(n-1)]
562554
out[div(n, 2)+1] += 1
563555
out
564556
end
565557

566-
function firprototype(n::Integer, ftype::Bandstop)
567-
w1 = ftype.w1
568-
w2 = ftype.w2
558+
function firprototype(n::Integer, ftype::Bandstop, fs::Real)
559+
w1 = normalize_freq(ftype.w1, fs)
560+
w2 = normalize_freq(ftype.w2, fs)
569561
isodd(n) || throw(ArgumentError("FIRWindow bandstop filters must have an odd number of coefficients"))
570562

571563
out = [w1*sinc(w1*(k-(n-1)/2)) - w2*sinc(w2*(k-(n-1)/2)) for k = 0:(n-1)]
572564
out[div(n, 2)+1] += 1
573565
out
574566
end
575567

576-
scalefactor(coefs::Vector, ::Union{Lowpass, Bandstop}) = sum(coefs)
577-
function scalefactor(coefs::Vector, ::Highpass)
568+
scalefactor(coefs::Vector, ::Union{Lowpass, Bandstop}, fs::Real) = sum(coefs)
569+
function scalefactor(coefs::Vector, ::Highpass, fs::Real)
578570
c = zero(coefs[1])
579571
for k = 1:length(coefs)
580572
c += ifelse(isodd(k), coefs[k], -coefs[k])
581573
end
582574
c
583575
end
584-
function scalefactor(coefs::Vector, ftype::Bandpass)
576+
function scalefactor(coefs::Vector, ftype::Bandpass, fs::Real)
585577
n = length(coefs)
586-
freq = middle(ftype.w1, ftype.w2)
578+
freq = normalize_freq(middle(ftype.w1, ftype.w2), fs)
587579
c = zero(coefs[1])
588580
for k = 0:n-1
589581
c += coefs[k+1]*cospi(freq*(k-(n-1)/2))
590582
end
591583
c
592584
end
593585

594-
function digitalfilter(ftype::FilterType, proto::FIRWindow)
595-
coefs = firprototype(length(proto.window), ftype)
586+
function digitalfilter(ftype::FilterType, proto::FIRWindow; fs::Real=2)
587+
coefs = firprototype(length(proto.window), ftype, fs)
596588
@assert length(proto.window) == length(coefs)
597589
out = coefs .* proto.window
598-
proto.scale ? rmul!(out, 1/scalefactor(out, ftype)) : out
590+
proto.scale ? rmul!(out, 1/scalefactor(out, ftype, fs)) : out
599591
end
600592

601593

test/filt_stream.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ end
284284
function test_arbitrary(Th, x, resampleRate, numFilters)
285285
cutoffFreq = 0.45
286286
transitionWidth = 0.05
287-
h = digitalfilter(Lowpass(cutoffFreq, fs=numFilters), FIRWindow(transitionwidth=transitionWidth/numFilters)) .* numFilters
287+
h = digitalfilter(Lowpass(cutoffFreq), FIRWindow(transitionwidth=transitionWidth/numFilters); fs=numFilters) .* numFilters
288288
h = convert(Vector{Th}, h)
289289
myfilt = FIRFilter(h, resampleRate, numFilters)
290290
xLen = length(x)

test/filter_conversion.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ using Polynomials.PolyCompat
133133
1.0000000000000000e+00 -1.9021224191804869e+00 1.0000000000000000e+00 1.0000000000000000e+00 -1.8964983429993663e+00 9.9553672990017417e-01
134134
1.0000000000000000e+00 -1.9021224191804869e+00 1.0000000000000000e+00 1.0000000000000000e+00 -1.8992956433548462e+00 9.9559721515078736e-01
135135
]
136-
f = convert(SecondOrderSections, digitalfilter(Bandstop(49.5, 50.5; fs=1000), DSP.Butterworth(2)))
136+
f = convert(SecondOrderSections, digitalfilter(Bandstop(49.5, 50.5), DSP.Butterworth(2); fs=1000))
137137
@test m_sos_butterworth_bs sosfilter_to_matrix(f)
138138
@test f.g 0.995566972017647
139139

@@ -349,9 +349,9 @@ end
349349
bp1 = 0.75
350350
bp2 = 10.0
351351
Fsamp = 180
352-
responsetype = Bandpass(bp1, bp2; fs = Fsamp)
352+
responsetype = Bandpass(bp1, bp2)
353353
designmethod = Elliptic(11, 0.25, 40)
354-
H = digitalfilter(responsetype, designmethod)
354+
H = digitalfilter(responsetype, designmethod; fs = Fsamp)
355355
H′ = ZeroPoleGain(SecondOrderSections(H))
356356
@test sort(H.p, by=z->(real(z), imag(z))) sort(H′.p, by=z->(real(z), imag(z)))
357357
@test sort(H.z, by=z->(real(z), imag(z))) sort(H′.z, by=z->(real(z), imag(z)))

0 commit comments

Comments
 (0)