Notebooks/FIR Filter Quantization: firpm vs firpmq

FIR Filter Quantization: firpm vs firpmq

This notebook designs a lowpass FIR filter with firpm, quantizes its coefficients to 10 bits by simple rounding, then compares against firpmq which optimizes directly in the integer domain.

Spec

ParameterValue
Sample rate48,000 Hz
Taps63
Passband0 – 0.20 Nyquist (4,800 Hz)
Stopband0.30 – 1.00 Nyquist (7,200 Hz)
Coefficient bits10

Step 1 — Floating-point design with firpm

rustlab
h_float = firpm(n_taps, [0.0, 0.20, 0.30, 1.0], [1.0, 1.0, 0.0, 0.0]);
Hz_float = freqz(h_float, n_fft, fs);
rustlab
plot(Hz_float(1,:), 20*log10(abs(Hz_float(2,:))))
title("firpm — Floating-Point (64-bit)")
xlabel("Frequency (Hz)")
ylabel("dB")
grid on
ylim([-100, 5])

Step 2 — Naïve quantization to 10 bits

Scale the floating-point taps by \(2^{b-1}\) and round to the nearest integer. This is the simplest approach — and the one most commonly used in practice — but it throws away the equiripple optimality.

rustlab
scale = 2^(bits - 1);
h_naive_int = round(h_float * scale);
h_naive = h_naive_int / sum(h_naive_int);
Hz_naive = freqz(h_naive, n_fft, fs);

Step 3 — Optimized quantization with firpmq

firpmq runs the Remez exchange in a loop, requantizing the coefficients after each iteration so the optimizer can compensate for rounding errors. The result is an integer-tap filter that is optimal at the target bitwidth.

rustlab
h_q_int = firpmq(n_taps, [0.0, 0.20, 0.30, 1.0], [1.0, 1.0, 0.0, 0.0], [1.0, 1.0], bits, 12);
h_q = h_q_int / sum(h_q_int);
Hz_q = freqz(h_q, n_fft, fs);

Comparison

rustlab
subplot(3, 1, 1)

% Full range
plot(Hz_float(1,:), 20*log10(abs(Hz_float(2,:))), 'b')
hold on
plot(Hz_naive(1,:), 20*log10(abs(Hz_naive(2,:))), 'r')
plot(Hz_q(1,:), 20*log10(abs(Hz_q(2,:))), 'g')
hold off
legend("firpm (float)", "Naive 10-bit", "firpmq 10-bit")
title("Full Response")
xlabel("Frequency (Hz)")
ylabel("dB")
grid on
ylim([-100, 5])

% Passband detail
subplot(3, 1, 2)
plot(Hz_float(1,:), 20*log10(abs(Hz_float(2,:))), 'b')
hold on
plot(Hz_naive(1,:), 20*log10(abs(Hz_naive(2,:))), 'r')
plot(Hz_q(1,:), 20*log10(abs(Hz_q(2,:))), 'g')
hold off
legend("firpm (float)", "Naive 10-bit", "firpmq 10-bit")
title("Passband Detail")
xlabel("Frequency (Hz)")
ylabel("dB")
grid on
xlim([0, 0.20 * fs / 2])
ylim([-0.5, 0.5])

% Stopband detail
subplot(3, 1, 3)
plot(Hz_float(1,:), 20*log10(abs(Hz_float(2,:))), 'b')
hold on
plot(Hz_naive(1,:), 20*log10(abs(Hz_naive(2,:))), 'r')
plot(Hz_q(1,:), 20*log10(abs(Hz_q(2,:))), 'g')
hold off
legend("firpm (float)", "Naive 10-bit", "firpmq 10-bit")
title("Stopband Detail")
xlabel("Frequency (Hz)")
ylabel("dB")
grid on
xlim([0.30 * fs / 2, fs / 2])
ylim([-80, -20])

Results

rustlab
% Passband: indices 1 .. round(0.20 * n_fft)
% Stopband: indices round(0.30 * n_fft)+1 .. n_fft
pass_end   = round(0.20 * n_fft);
stop_start = round(0.30 * n_fft) + 1;

% Peak-to-peak passband ripple (dB)
Pb_f = 20*log10(abs(Hz_float(2, 1:pass_end)));
Pb_n = 20*log10(abs(Hz_naive(2, 1:pass_end)));
Pb_q = 20*log10(abs(Hz_q(2,     1:pass_end)));
ripple_float = max(Pb_f) - min(Pb_f);
ripple_naive = max(Pb_n) - min(Pb_n);
ripple_q     = max(Pb_q) - min(Pb_q);

% Peak stopband level (dB). `end` is the last frequency bin.
stop_float = max(20*log10(abs(Hz_float(2, stop_start:end))));
stop_naive = max(20*log10(abs(Hz_naive(2, stop_start:end))));
stop_q     = max(20*log10(abs(Hz_q(2,     stop_start:end))));

% Improvements (positive = firpmq better than naive)
ripple_improvement = ripple_naive - ripple_q;
stop_improvement   = stop_naive   - stop_q;
FilterPassband ripple (dB)Stopband peak (dB)
firpm (float)0.024-56.18
Naïve 10-bit0.098-38.66
firpmq 10-bit0.067-45.99

firpmq reduces passband ripple by 0.031 dB and lowers the peak stopband level by 7.3 dB versus naïve quantization at the same bitwidth.

Legend (plot colors): blue = firpm (float), red = naïve 10-bit quantization, green = firpmq 10-bit optimized.

The naïve approach degrades both the passband ripple and the stopband floor because rounding destroys the equiripple property. firpmq recovers most of the loss by re-optimizing with quantization in the loop.