diff --git a/src/sdr/_filter/_fir.py b/src/sdr/_filter/_fir.py index d3460ad04..f0bce8c56 100644 --- a/src/sdr/_filter/_fir.py +++ b/src/sdr/_filter/_fir.py @@ -8,6 +8,7 @@ import numpy as np import numpy.typing as npt +import scipy.integrate import scipy.signal from typing_extensions import Literal @@ -418,6 +419,30 @@ def phase_delay(self, sample_rate: float = 1.0, N: int = 1024) -> tuple[npt.NDAr return f, tau_phi + def noise_bandwidth(self, sample_rate: float = 1.0) -> float: + r""" + Returns the noise bandwidth $B_n$ of the FIR filter. + + Arguments: + sample_rate: The sample rate $f_s$ of the filter in samples/s. + + Returns: + The noise bandwidth of the FIR filter $B_n$ in Hz. + + Examples: + See the :ref:`fir-filters` example. + """ + verify_scalar(sample_rate, float=True, positive=True) + + # Compute the frequency response + f, H = scipy.signal.freqz(self.taps, 1, worN=2**20, whole=True, fs=sample_rate) + H = np.abs(H) ** 2 + + # Compute the noise bandwidth + B_n = scipy.integrate.simpson(y=H, x=f) / np.max(H) + + return B_n + ############################################################################## # Properties ############################################################################## diff --git a/src/sdr/_filter/_iir.py b/src/sdr/_filter/_iir.py index c9a8214be..61d1912b5 100644 --- a/src/sdr/_filter/_iir.py +++ b/src/sdr/_filter/_iir.py @@ -8,6 +8,7 @@ import numpy as np import numpy.typing as npt +import scipy.integrate import scipy.signal from typing_extensions import Self @@ -347,6 +348,30 @@ def frequency_response( else: return H + def noise_bandwidth(self, sample_rate: float = 1.0) -> float: + r""" + Returns the noise bandwidth $B_n$ of the IIR filter. + + Arguments: + sample_rate: The sample rate $f_s$ of the filter in samples/s. + + Returns: + The noise bandwidth of the IIR filter $B_n$ in Hz. + + Examples: + See the :ref:`iir-filters` example. + """ + verify_scalar(sample_rate, float=True, positive=True) + + # Compute the frequency response + f, H = scipy.signal.freqz(self.b_taps, self.a_taps, worN=2**20, whole=True, fs=sample_rate) + H = np.abs(H) ** 2 + + # Compute the noise bandwidth + B_n = scipy.integrate.simpson(y=H, x=f) / np.max(H) + + return B_n + ############################################################################## # Properties ############################################################################## diff --git a/tests/dsp/filter/__init__.py b/tests/dsp/filter/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/dsp/filter/test_noise_bandwidth.py b/tests/dsp/filter/test_noise_bandwidth.py new file mode 100644 index 000000000..7d0c65e95 --- /dev/null +++ b/tests/dsp/filter/test_noise_bandwidth.py @@ -0,0 +1,89 @@ +""" +References: + - https://www.gaussianwaves.com/2020/09/equivalent-noise-bandwidth-enbw-of-window-functions/ +""" + +import pytest +import scipy.signal + +import sdr + + +def test_boxcar(): + n = 128 + h = scipy.signal.windows.get_window("boxcar", n) + fir = sdr.FIR(h) + B_n = fir.noise_bandwidth(n) + assert B_n == pytest.approx(1.000, rel=1e-3) + + +def test_barthann(): + n = 128 + h = scipy.signal.windows.get_window("barthann", n) + fir = sdr.FIR(h) + B_n = fir.noise_bandwidth(n) + assert B_n == pytest.approx(1.456, rel=1e-3) + + +def test_bartlett(): + n = 128 + h = scipy.signal.windows.get_window("bartlett", n) + fir = sdr.FIR(h) + B_n = fir.noise_bandwidth(n) + assert B_n == pytest.approx(1.333, rel=1e-3) + + +def test_blackman(): + n = 128 + h = scipy.signal.windows.get_window("blackman", n) + fir = sdr.FIR(h) + B_n = fir.noise_bandwidth(n) + assert B_n == pytest.approx(1.727, rel=1e-3) + + +def test_blackmanharris(): + n = 128 + h = scipy.signal.windows.get_window("blackmanharris", n) + fir = sdr.FIR(h) + B_n = fir.noise_bandwidth(n) + assert B_n == pytest.approx(2.004, rel=1e-3) + + +def test_bohman(): + n = 128 + h = scipy.signal.windows.get_window("bohman", n) + fir = sdr.FIR(h) + B_n = fir.noise_bandwidth(n) + assert B_n == pytest.approx(1.786, rel=1e-3) + + +def test_flattop(): + n = 128 + h = scipy.signal.windows.get_window("flattop", n) + fir = sdr.FIR(h) + B_n = fir.noise_bandwidth(n) + assert B_n == pytest.approx(3.770, rel=1e-3) + + +def test_hamming(): + n = 128 + h = scipy.signal.windows.get_window("hamming", n) + fir = sdr.FIR(h) + B_n = fir.noise_bandwidth(n) + assert B_n == pytest.approx(1.363, rel=1e-3) + + +def test_hann(): + n = 128 + h = scipy.signal.windows.get_window("hann", n) + fir = sdr.FIR(h) + B_n = fir.noise_bandwidth(n) + assert B_n == pytest.approx(1.500, rel=1e-3) + + +def test_nuttall(): + n = 128 + h = scipy.signal.windows.get_window("nuttall", n) + fir = sdr.FIR(h) + B_n = fir.noise_bandwidth(n) + assert B_n == pytest.approx(1.976, rel=1e-3)