FIR Filter Design (cross-cutting/numerics/fir-filter-design)
Use when the task is designing a linear-phase finite-impulse-response
(FIR) lowpass filter with the windowed-sinc method: truncating the
ideal lowpass impulse response to a finite symmetric tap set, shaping
the sidelobes with a selected window (rectangular, Hann, Hamming, or
Blackman), normalizing the coefficients to unity DC gain, probing the
magnitude response in dB at any frequency through the real cosine sum
of the symmetric taps, and filtering a sampled signal by direct
convolution. Every coefficient is computed, none are lookup values.
The taps are symmetric about an integer center, so the filter is
linear phase with a constant group delay of (N-1)/2 samples and needs
no recursion and no feedback path: the all-zero transfer function is
unconditionally bounded. Frequency-content analysis of the filtered
output belongs to cross-cutting/numerics/fast-fourier-transform; delay
estimation between two filtered traces belongs to
cross-cutting/numerics/cross-correlation-analysis. This leaf pairs
with digital-filter-design, the numerics partner that covers the
feedback-form frequency-selective family (a separate design method
space that this leaf does not implement).
Domain quick reference
- Ideal lowpass impulse response (windowed-sinc prototype), centered
at tap index (N-1)/2:
h[n] = sin(2pifc/fs * (n - (N-1)/2)) / (pi * (n - (N-1)/2)), with
the center tap set to the limit value 2*fc/fs.
- Window weights w[n] for n = 0..N-1 (denominator N-1): rectangular
all 1.0; hann 0.5 - 0.5cos(2pin/(N-1)); hamming
0.54 - 0.46cos(2pin/(N-1)); blackman
0.42 - 0.5cos(2pin/(N-1)) + 0.08cos(4pin/(N-1)). The windows
are symmetric, so the windowed taps stay symmetric (linear phase).
- Designed taps: b[n] = h[n] * w[n], then normalized by the sum of
the weighted taps so the DC gain sum(b) is exactly 1.0. An odd
number of taps keeps the center integer; even N is rejected.
- Magnitude response: H(f) = sum_n b[n] * cos(2pif/fs * (n -
(N-1)/2)) is the real response of the symmetric tap set with the
linear phase term dropped; |H(f)| is the linear gain, and
20*log10(|H(f)|) the magnitude in dB.
- The windowed-sinc design places the -6 dB point (gain 0.5, the
midpoint of the transition) at the requested cutoff fc; passband
ripple is small near DC and the stopband floor follows the window
sidelobe level (about -31 dB rectangular, -44 dB Hann, -53 dB
Hamming, -74 dB Blackman asymptotic sidelobe peaks).
- Group delay of the symmetric tap set: (N-1)/2 samples, constant for
every frequency because the phase is exactly linear.
- Direct-form convolution filter: y[n] = sum_k b[k] * x[n-k], input
treated as zero outside its range, output the same length as the
input.
- Units are SI throughout: Hz for frequencies, samples per second for
fs. NACA TR-824 anchors the numerics-pack public-domain reference
set; the relations above are classical digital filter methodology
(Hamming, Oppenheim and Schafer style), summary-only.
Workflow
- Fix the operating point: sample rate fs, cutoff frequency fc
(below fs/2), tap count N (odd), and the window.
- Get the window weights with window_coefficients(window, num_taps)
and confirm their symmetry about the center.
- Build the ideal prototype with ideal_lowpass_taps(cutoff_hz,
sample_rate_hz, num_taps); the center tap equals 2*fc/fs.
- Design the filter with design_lowpass(cutoff_hz, sample_rate_hz,
num_taps, window); the returned dict carries the DC-normalized
coefficient vector, the window name, the group delay in samples,
and the achieved DC gain (1.0 to float precision).
- Probe the design at any in-band or stop-band frequency with
gain_at (linear) or magnitude_response_db (dB); the cutoff gain
should sit near 0.5 (-6 dB).
- Run the self-check with design_check(coefficients, cutoff_hz,
sample_rate_hz) to confirm the DC gain, the cutoff gain in dB, the
stopband gain at 2*fc, and the stopband attenuation in dB.
- Apply the filter to the sampled signal with filter_signal
(direct convolution, zero-padded boundaries, same output length);
allow the first (N-1)/2 samples to settle before reading
steady-state amplitudes.
- Confirm the deterministic checks with the contract test
scripts/test_fir_filter_design.py.
Worked example
Sample rate fs = 1000 Hz, cutoff fc = 100 Hz, N = 101 taps, Hamming
window (design_lowpass(100, 1000, 101, "hamming")). Real module
outputs:
- Ideal center tap: 2*fc/fs = 0.2 (taps[50]).
- Coefficients symmetric: max |b[n] - b[100-n]| = 1.4e-17 (linear
phase to float precision).
- DC gain after normalization: 1.0 (sum(b) = 1.0).
- Group delay: (101 - 1)/2 = 50.0 samples.
- Gain at 50 Hz (passband): 1.00319, within 0.98-1.02.
- Gain at 100 Hz (cutoff): 0.50039, magnitude -6.0138 dB, within
0.45-0.55 (the -6 dB point sits at the cutoff).
- Gain at 150 Hz: 0.000975 (-60.2 dB), below 0.01.
- Gain at 300 Hz: 0.000263 (-71.6 dB), below 0.001.
- design_check: dc_gain 1.0, cutoff_gain_db -6.0138 dB,
stopband_gain_db (at 200 Hz) -65.144 dB, stopband_attenuation_db
65.144 dB.
- Filtering a 1.0-amplitude 50 Hz cosine at fs = 1000: the steady-
state output amplitude after the first 50 samples is 1.00319,
within 0.95-1.05.
- Filtering a 1.0-amplitude 10 Hz sine (fully in-band): the tail
amplitude is 1.00296, amplitude preserved within 5%.
- Window trade at N = 51, fc = 100 Hz: stopband gain at 200 Hz is
-36.9 dB (rectangular), -61.7 dB (Hamming), -68.2 dB (Hann), and
-79.5 dB (Blackman); wider main lobe for deeper stopband.
Recorded assumptions: the defining window formulas divide by N-1, so
a degenerate single tap (N = 1) takes the center limit value 1.0 for
every window; design_check probes the stopband at 2fc, which is only
physical while 2fc stays at or below the Nyquist frequency, so
design_check requires fc <= fs/4 and raises ValueError otherwise.
Pitfalls
- Asking for an even number of taps: even N is rejected because the
center tap index (N-1)/2 must stay integer for the windowed taps to
remain symmetric (linear phase).
- Running design_check with the cutoff above fs/4: the stopband probe
sits at 2fc and is only physical while 2fc stays at or below the
Nyquist frequency, so design_check requires fc <= fs/4 and raises
ValueError otherwise.
- Expecting a brick-wall response at the cutoff: the windowed-sinc
design places the -6 dB point at fc with a transition band whose
width follows the window, and the stopband floor follows the window
sidelobe level (about -53 dB for Hamming at N = 51).
- Forgetting the group delay when comparing input and output: the
symmetric tap set delays by (N-1)/2 = 50 samples at N = 101, so the
steady-state amplitude checks apply only after the delay.
- Passing non-physical design inputs: even num_taps, num_taps < 1,
fc <= 0, fs <= 0, fc >= fs/2, unknown window names, negative or
above-Nyquist probe frequencies, and empty coefficient or signal
lists all raise ValueError.
- Choosing a window without the sidelobe trade: at N = 51, fc = 100 Hz
the stopband gain at 200 Hz runs from -36.9 dB (rectangular) to
-79.5 dB (Blackman) at the price of a wider main lobe.
Verification
- Confirm design_lowpass(100, 1000, 101, "hamming") returns a dict
with exactly the keys coefficients, num_taps, cutoff_hz,
sample_rate_hz, window, group_delay_samples, dc_gain, and that
coefficients[n] == coefficients[100-n] within 1e-12.
- Confirm the DC gain is within 1e-9 of 1.0 and the magnitude
response at 0 Hz is 0 dB.
- Confirm gain_at(b, 100, 1000) returns 0.50039 (about -6 dB at the
cutoff), gain_at(b, 50, 1000) returns 1.00319, and gain_at(b, 150,
- returns 0.000975 with the spec bounds (0.45-0.55 at the
cutoff, below 0.01 at 150 Hz, below 0.001 at 300 Hz).
- Confirm the group delay identity group_delay_samples(101) = 50.0
and the round trips: an impulse input reproduces the coefficient
vector exactly, a constant 5.0 input settles to 5.0 within 0.001,
and a below-cutoff 10 Hz sine keeps its amplitude within 5% after
the group delay.
- Confirm every non-physical input raises ValueError: even num_taps,
num_taps < 1, fc <= 0, fs <= 0, fc >= fs/2, unknown window,
negative or above-Nyquist probe frequency, and empty coefficient or
signal lists.
- Run the contract test offline: python3
scripts/test_fir_filter_design.py (34 tests, deterministic).
Related leaves
- cross-cutting/numerics/digital-filter-design: the numerics partner
covering the feedback-form frequency-selective family (separate
design method space, analog-prototype mapped coefficients).
- cross-cutting/numerics/fast-fourier-transform: spectral analysis of
the filtered signal, the frequency-domain partner of this leaf.
- cross-cutting/numerics/finite-difference-derivatives: differentiating
the filtered (denoised) signal with controlled step-size error.
Behavior contract (gate 3)
Run the deterministic contract test (stdlib unittest, offline):
python3 scripts/test_fir_filter_design.py
The test covers the window formulas (rectangular, Hann, Hamming,
Blackman) and their symmetry, the ideal-prototype center tap limit
2fc/fs, the exact convenience-dict keys, the worked-example anchors
(DC gain 1.0, gain 1.00319 at 50 Hz, gain 0.50039 and -6.0138 dB at
the 100 Hz cutoff, gains below 0.01 at 150 Hz and below 0.001 at 300
Hz, group delay 50.0), coefficient symmetry within 1e-12, the
steady-state amplitude of a filtered 50 Hz cosine within 0.95-1.05,
the 10 Hz sine round trip within 5%, the magnitude response in dB
matching 20log10 of the linear gain, the design self-check anchors,
and ValueError rejection of every non-physical input in the spec
validation list. Runs in well under a second.
Compliance
- Standards referenced, not reproduced: NACA TR-824 anchors the
numerics-pack public-domain reference set; windowed-sinc FIR design
and its windows are classical digital filter methodology (Hamming,
Oppenheim and Schafer style summaries), paraphrase-only per
standards-map.yaml.
- compliance: STANDARDS-REF, gated: false.
1---2name: fir-filter-design3description: Use when you must design a linear-phase finite-impulse-response lowpass filter with the windowed-sinc method: build the ideal lowpass impulse response from the cutoff frequency and the sample rate, apply a selected window (rectangular, Hann, Hamming, or Blackman), normalize the coefficient vector to unity DC gain, evaluate the magnitude response in dB at any frequency as the real cosine sum of the symmetric tap set, return the group delay of the taps, and filter a sampled signal by direct convolution. Produces the coefficient vector, the magnitude response checks (passband gain, -6 dB point at the cutoff, stopband attenuation), the group delay, and the filtered signal that gate a digital filter design task. Trigger: fir-filter-design, windowed-sinc, fir lowpass, finite impulse response, hamming window, filter taps, cutoff frequency, linear phase, magnitude response dB, group delay.4license: Apache-2.05---67# FIR Filter Design (cross-cutting/numerics/fir-filter-design)89Use when the task is designing a linear-phase finite-impulse-response10(FIR) lowpass filter with the windowed-sinc method: truncating the11ideal lowpass impulse response to a finite symmetric tap set, shaping12the sidelobes with a selected window (rectangular, Hann, Hamming, or13Blackman), normalizing the coefficients to unity DC gain, probing the14magnitude response in dB at any frequency through the real cosine sum15of the symmetric taps, and filtering a sampled signal by direct16convolution. Every coefficient is computed, none are lookup values.17The taps are symmetric about an integer center, so the filter is18linear phase with a constant group delay of (N-1)/2 samples and needs19no recursion and no feedback path: the all-zero transfer function is20unconditionally bounded. Frequency-content analysis of the filtered21output belongs to cross-cutting/numerics/fast-fourier-transform; delay22estimation between two filtered traces belongs to23cross-cutting/numerics/cross-correlation-analysis. This leaf pairs24with digital-filter-design, the numerics partner that covers the25feedback-form frequency-selective family (a separate design method26space that this leaf does not implement).2728## Domain quick reference2930- Ideal lowpass impulse response (windowed-sinc prototype), centered31 at tap index (N-1)/2:32 h[n] = sin(2*pi*fc/fs * (n - (N-1)/2)) / (pi * (n - (N-1)/2)), with33 the center tap set to the limit value 2*fc/fs.34- Window weights w[n] for n = 0..N-1 (denominator N-1): rectangular35 all 1.0; hann 0.5 - 0.5*cos(2*pi*n/(N-1)); hamming36 0.54 - 0.46*cos(2*pi*n/(N-1)); blackman37 0.42 - 0.5*cos(2*pi*n/(N-1)) + 0.08*cos(4*pi*n/(N-1)). The windows38 are symmetric, so the windowed taps stay symmetric (linear phase).39- Designed taps: b[n] = h[n] * w[n], then normalized by the sum of40 the weighted taps so the DC gain sum(b) is exactly 1.0. An odd41 number of taps keeps the center integer; even N is rejected.42- Magnitude response: H(f) = sum_n b[n] * cos(2*pi*f/fs * (n -43 (N-1)/2)) is the real response of the symmetric tap set with the44 linear phase term dropped; |H(f)| is the linear gain, and45 20*log10(|H(f)|) the magnitude in dB.46- The windowed-sinc design places the -6 dB point (gain 0.5, the47 midpoint of the transition) at the requested cutoff fc; passband48 ripple is small near DC and the stopband floor follows the window49 sidelobe level (about -31 dB rectangular, -44 dB Hann, -53 dB50 Hamming, -74 dB Blackman asymptotic sidelobe peaks).51- Group delay of the symmetric tap set: (N-1)/2 samples, constant for52 every frequency because the phase is exactly linear.53- Direct-form convolution filter: y[n] = sum_k b[k] * x[n-k], input54 treated as zero outside its range, output the same length as the55 input.56- Units are SI throughout: Hz for frequencies, samples per second for57 fs. NACA TR-824 anchors the numerics-pack public-domain reference58 set; the relations above are classical digital filter methodology59 (Hamming, Oppenheim and Schafer style), summary-only.6061## Workflow62631. Fix the operating point: sample rate fs, cutoff frequency fc64 (below fs/2), tap count N (odd), and the window.652. Get the window weights with window_coefficients(window, num_taps)66 and confirm their symmetry about the center.673. Build the ideal prototype with ideal_lowpass_taps(cutoff_hz,68 sample_rate_hz, num_taps); the center tap equals 2*fc/fs.694. Design the filter with design_lowpass(cutoff_hz, sample_rate_hz,70 num_taps, window); the returned dict carries the DC-normalized71 coefficient vector, the window name, the group delay in samples,72 and the achieved DC gain (1.0 to float precision).735. Probe the design at any in-band or stop-band frequency with74 gain_at (linear) or magnitude_response_db (dB); the cutoff gain75 should sit near 0.5 (-6 dB).766. Run the self-check with design_check(coefficients, cutoff_hz,77 sample_rate_hz) to confirm the DC gain, the cutoff gain in dB, the78 stopband gain at 2*fc, and the stopband attenuation in dB.797. Apply the filter to the sampled signal with filter_signal80 (direct convolution, zero-padded boundaries, same output length);81 allow the first (N-1)/2 samples to settle before reading82 steady-state amplitudes.838. Confirm the deterministic checks with the contract test84 scripts/test_fir_filter_design.py.8586## Worked example8788Sample rate fs = 1000 Hz, cutoff fc = 100 Hz, N = 101 taps, Hamming89window (design_lowpass(100, 1000, 101, "hamming")). Real module90outputs:9192- Ideal center tap: 2*fc/fs = 0.2 (taps[50]).93- Coefficients symmetric: max |b[n] - b[100-n]| = 1.4e-17 (linear94 phase to float precision).95- DC gain after normalization: 1.0 (sum(b) = 1.0).96- Group delay: (101 - 1)/2 = 50.0 samples.97- Gain at 50 Hz (passband): 1.00319, within 0.98-1.02.98- Gain at 100 Hz (cutoff): 0.50039, magnitude -6.0138 dB, within99 0.45-0.55 (the -6 dB point sits at the cutoff).100- Gain at 150 Hz: 0.000975 (-60.2 dB), below 0.01.101- Gain at 300 Hz: 0.000263 (-71.6 dB), below 0.001.102- design_check: dc_gain 1.0, cutoff_gain_db -6.0138 dB,103 stopband_gain_db (at 200 Hz) -65.144 dB, stopband_attenuation_db104 65.144 dB.105- Filtering a 1.0-amplitude 50 Hz cosine at fs = 1000: the steady-106 state output amplitude after the first 50 samples is 1.00319,107 within 0.95-1.05.108- Filtering a 1.0-amplitude 10 Hz sine (fully in-band): the tail109 amplitude is 1.00296, amplitude preserved within 5%.110- Window trade at N = 51, fc = 100 Hz: stopband gain at 200 Hz is111 -36.9 dB (rectangular), -61.7 dB (Hamming), -68.2 dB (Hann), and112 -79.5 dB (Blackman); wider main lobe for deeper stopband.113114Recorded assumptions: the defining window formulas divide by N-1, so115a degenerate single tap (N = 1) takes the center limit value 1.0 for116every window; design_check probes the stopband at 2*fc, which is only117physical while 2*fc stays at or below the Nyquist frequency, so118design_check requires fc <= fs/4 and raises ValueError otherwise.119120## Pitfalls121122- Asking for an even number of taps: even N is rejected because the123 center tap index (N-1)/2 must stay integer for the windowed taps to124 remain symmetric (linear phase).125- Running design_check with the cutoff above fs/4: the stopband probe126 sits at 2*fc and is only physical while 2*fc stays at or below the127 Nyquist frequency, so design_check requires fc <= fs/4 and raises128 ValueError otherwise.129- Expecting a brick-wall response at the cutoff: the windowed-sinc130 design places the -6 dB point at fc with a transition band whose131 width follows the window, and the stopband floor follows the window132 sidelobe level (about -53 dB for Hamming at N = 51).133- Forgetting the group delay when comparing input and output: the134 symmetric tap set delays by (N-1)/2 = 50 samples at N = 101, so the135 steady-state amplitude checks apply only after the delay.136- Passing non-physical design inputs: even num_taps, num_taps < 1,137 fc <= 0, fs <= 0, fc >= fs/2, unknown window names, negative or138 above-Nyquist probe frequencies, and empty coefficient or signal139 lists all raise ValueError.140- Choosing a window without the sidelobe trade: at N = 51, fc = 100 Hz141 the stopband gain at 200 Hz runs from -36.9 dB (rectangular) to142 -79.5 dB (Blackman) at the price of a wider main lobe.143144## Verification145146- Confirm design_lowpass(100, 1000, 101, "hamming") returns a dict147 with exactly the keys coefficients, num_taps, cutoff_hz,148 sample_rate_hz, window, group_delay_samples, dc_gain, and that149 coefficients[n] == coefficients[100-n] within 1e-12.150- Confirm the DC gain is within 1e-9 of 1.0 and the magnitude151 response at 0 Hz is 0 dB.152- Confirm gain_at(b, 100, 1000) returns 0.50039 (about -6 dB at the153 cutoff), gain_at(b, 50, 1000) returns 1.00319, and gain_at(b, 150,154 1000) returns 0.000975 with the spec bounds (0.45-0.55 at the155 cutoff, below 0.01 at 150 Hz, below 0.001 at 300 Hz).156- Confirm the group delay identity group_delay_samples(101) = 50.0157 and the round trips: an impulse input reproduces the coefficient158 vector exactly, a constant 5.0 input settles to 5.0 within 0.001,159 and a below-cutoff 10 Hz sine keeps its amplitude within 5% after160 the group delay.161- Confirm every non-physical input raises ValueError: even num_taps,162 num_taps < 1, fc <= 0, fs <= 0, fc >= fs/2, unknown window,163 negative or above-Nyquist probe frequency, and empty coefficient or164 signal lists.165- Run the contract test offline: python3166 scripts/test_fir_filter_design.py (34 tests, deterministic).167168## Related leaves169170- cross-cutting/numerics/digital-filter-design: the numerics partner171 covering the feedback-form frequency-selective family (separate172 design method space, analog-prototype mapped coefficients).173- cross-cutting/numerics/fast-fourier-transform: spectral analysis of174 the filtered signal, the frequency-domain partner of this leaf.175- cross-cutting/numerics/finite-difference-derivatives: differentiating176 the filtered (denoised) signal with controlled step-size error.177178## Behavior contract (gate 3)179180Run the deterministic contract test (stdlib unittest, offline):181182 python3 scripts/test_fir_filter_design.py183184The test covers the window formulas (rectangular, Hann, Hamming,185Blackman) and their symmetry, the ideal-prototype center tap limit1862*fc/fs, the exact convenience-dict keys, the worked-example anchors187(DC gain 1.0, gain 1.00319 at 50 Hz, gain 0.50039 and -6.0138 dB at188the 100 Hz cutoff, gains below 0.01 at 150 Hz and below 0.001 at 300189Hz, group delay 50.0), coefficient symmetry within 1e-12, the190steady-state amplitude of a filtered 50 Hz cosine within 0.95-1.05,191the 10 Hz sine round trip within 5%, the magnitude response in dB192matching 20*log10 of the linear gain, the design self-check anchors,193and ValueError rejection of every non-physical input in the spec194validation list. Runs in well under a second.195196## Compliance197198- Standards referenced, not reproduced: NACA TR-824 anchors the199 numerics-pack public-domain reference set; windowed-sinc FIR design200 and its windows are classical digital filter methodology (Hamming,201 Oppenheim and Schafer style summaries), paraphrase-only per202 standards-map.yaml.203- compliance: STANDARDS-REF, gated: false.