You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This project solves the IIR Filter Tolerance Design Problem (PPFTI) by implementing the bilinear transform method from scratch in Python. Four classical IIR filter families — Butterworth, Chebyshev I, Chebyshev II, and Cauer (Elliptic) — are designed, analyzed, and ranked against strict frequency-domain specifications across three progressive phases.
Quickstart (copy–paste)
# 1) Clone the repository
git clone https://github.com/cristi1710/Filter-Project.git
cd Filter-Project
# 2) Create and activate a virtual environment
python -m venv venv
# Windows:
venv\Scripts\activate
# macOS / Linux:source venv/bin/activate
# 3) Install dependencies
pip install numpy scipy matplotlib pytest
# 4) Create output directory for plots
mkdir plots
# 5) Run any phase
python phase1_butterworth.py
python phase2_comparison.py
python phase3_design_contest.py
# 6) Run unit tests
pytest test_filters.py -v
Description
The pipeline designs low-pass IIR filters from analog prototypes and maps them to the digital domain via the bilinear transform. All four classical approximation families are compared against the same frequency-domain specifications using a composite 5-dimensional performance criterion.
Main stages:
Prewarping — map digital band edges to analog frequencies, compensating for the nonlinear frequency compression of the bilinear transform
Analog prototype design — compute minimum order, cutoff frequency, and stable left-half-plane poles (Butterworth from scratch; Chebyshev I/II and Cauer via scipy)
Bilinear mapping — convert analog poles to digital (B, A) coefficients; zeros placed at z = −1 (Nyquist)
Specification verification — check passband and stopband constraints numerically on 5000-point frequency grid
Performance scoring — Phase 2 uses a compromise score (order + ripple penalty); Phase 3 uses a composite criterion J covering order, magnitude error, group delay variation, pole stability, and step overshoot
Ranking and plots — filters sorted by score; frequency response and phase plots saved to plots/
Key invariance property: the ratio Ωs/Ωp is independent of Ts (and of the bilinear scaling constant C = 2/Ts), so the filter order M and the digital transfer function H(z) are invariant to changes in sampling period. Verified analytically and numerically (errors at machine precision ~10⁻¹³) in Phase 1b and 1c.
Passband ripple is penalized more heavily (1.2 vs 0.1) because it directly distorts the useful signal. Ripple flags are assigned theoretically based on filter family properties, not by numerical detection.
Results
Phase 1 — Butterworth
Metric
Value
Filter order M
12
Min passband magnitude
0.9301 ≥ 0.9292 ✓
Max stopband magnitude
0.0543 ≤ 0.0708 ✓
Specs satisfied
Yes
Equivalent FIR order (Window / Hamming)
89 — 7.4× higher than IIR
Equivalent FIR order (Least Squares)
30 — 2.5× higher than IIR
Phase 1 sub-investigations:
1b — Bilinear constant invariance: Tustin (2/Ts) and Pseudo-Tustin (1/Ts) produce identical H(z); spectral error norm = 0
1c — Ts independence: H(z) invariant for Ts ∈ [0.1·Tref, 3.0·Tref]; errors at machine precision (~10⁻¹³)
1d — Order sensitivity: 16 combinations of (Δp, Δs) studied; M varies from 8 (tolerances doubled) to 15 (tolerances halved)
Phase 2 — Filter Comparison (Δs = 2Δp)
Rank
Filter
M
rip_pass
rip_stop
Score
1
Cauer (Elliptic)
3
Yes
Yes
4.3
2
Chebyshev II
5
No
Yes
5.1 — Best Buy
3
Chebyshev I
5
Yes
No
6.2
4
Butterworth
9
No
No
9.0
Chebyshev II is the best practical choice: it keeps the passband clean (no ripple affecting the useful signal) while reducing order from 9 to 5.
Key insight: Butterworth requires M = 54 for the narrow band (Δω = π/33), pushing poles very close to the unit circle (ρ ≈ 0.971) and causing a large J_stab penalty. Chebyshev II wins because it combines moderate order with excellent magnitude accuracy and favorable pole placement.
API Reference
filter_design.utils
Function
Signature
Description
prewarping
(w_norm, Ts) → float
Analog prewarped frequency: (2/Ts)·tan(w·π/2)
bilinear_transform
(poles_analog, Ts) → B, A
Maps analog poles to digital B, A coefficients; zeros at z = −1
compute_frequency_response
(B, A, n=5000) → H, w
Complex H(e^jω) on [0, π]
compute_group_delay
(B, A, n=5000) → gd, w
Group delay on [0, 0.99π] (avoids Nyquist singularity)
check_specs
(H, w, w_p, w_s, Δp, Δs) → ok, val_pass, val_stop
Verify passband/stopband constraints
filter_design.butterworth
Function
Signature
Description
design_butterworth
(w_p, w_s, Δp, Δs, Ts) → B, A, M
Full from-scratch Butterworth design
filter_design.chebyshev
Function
Signature
Description
design_chebyshev1
(w_p, w_s, Δp, Δs, Ts) → B, A, M
Chebyshev Type I via scipy
design_chebyshev2
(w_p, w_s, Δp, Δs, Ts) → B, A, M
Chebyshev Type II via scipy
filter_design.cauer
Function
Signature
Description
design_cauer
(w_p, w_s, Δp, Δs, Ts) → B, A, M
Cauer (elliptic) filter via scipy
filter_design.cost_function
Function
Signature
Description
compute_J
(B, A, w_p, w_s, weights=None) → dict
Compute all J components + total
rank_filters
(filters_dict, w_p, w_s) → list[dict]
Rank filters by J ascending
Frequency convention: design functions expect normalized [0, 1] frequencies (scipy convention). check_specs(), rank_filters(), and compute_J() expect rad/sample [0, π]. Convert with w_rad = w_norm × π.
Unit Tests
test_butterworth_order_matches_scipy PASSED — M ≥ 1, len(B) = len(A) = M+1
test_butterworth_specs_satisfied PASSED — passband min ≥ 1−Δp, stopband max ≤ Δs
test_butterworth_dc_gain PASSED — H(0) within 5% of 1.0
test_filter_stability_all PASSED — all poles inside unit circle (all 4 types)
test_prewarping_correctness PASSED — matches formula to 10⁻¹⁰ tolerance
test_bilinear_transform_produces_valid_coeff PASSED — B, A finite, correct length
test_chebyshev2_specs_satisfied PASSED — passband and stopband constraints met
test_cauer_lower_order_than_butterworth PASSED — M_cauer ≤ M_butterworth for same specs
Known Issues & Fixes
Issue
Root Cause
Fix Applied
UserWarning: denominator extremely small at Nyquist
scipy.group_delay() numerical singularity at ω = π for high-order filters
compute_group_delay() evaluates on [0, 0.99π] instead of [0, π]
Phase 2 ranking used Phase 3 criterion
rank_filters() (composite J) was called in phase2 script
Phase 2 now uses Score = M + 1.2·rip_pass + 0.1·rip_stop with theoretical ripple flags
scipy reference visually diverges from manual filter
Different DC gain normalization: scipy sets H(ωp) = 1−Δp; manual design sets H(0) = 1.0
Both are mathematically correct; passband zoom subplot added to Phase 1 to show tolerance bounds clearly
Dependencies
Package
Purpose
numpy
Array operations, polynomial arithmetic
scipy
Filter design (cheby1/2, ellip), frequency response, group delay
matplotlib
Frequency response and phase plots
pytest
Unit testing
Notes
Butterworth is implemented from scratch — order formula, cutoff computation, stable pole placement, and bilinear mapping are all done manually without calling scipy.butter
Chebyshev and Cauer use scipy — cheb1ord / cheby1, cheb2ord / cheby2, ellipord / ellip with tolerances converted to dB
Group delay clamped to 0.99π — avoids the numerical singularity that scipy's group_delay() produces near Nyquist for high-order filters
Phase 3 cost criterion — implements the exact formula from the project report (Faza 3, section 4.9.2); weights are fixed at W_M=2, W_freq=50, W_phase=20, W_stab=10, W_trans=20
About
IIR filter design via bilinear transform — Butterworth, Chebyshev I/II, Cauer. From-scratch Python implementation with composite performance criterion J and 8 pytest unit tests.