Filters#

class SRRCFilter(rho: float, oversampling: int, *, N_h: int = 10, norm: bool = True, scale: float = 1.0, method: Literal['lfilter', 'fft'] = 'lfilter', axis: int = -1, name: str = 'SRRCFilter')[source]#

Square-root raised cosine (SRRC) FIR pulse-shaping filter.

Signal Model#

The output is the discrete convolution of the input with the truncated SRRC impulse response \(h[m]\):

\[y[n] = \sum_{m=0}^{2 N_h L} h[m] \, x[n - m]\]

The taps are the continuous-time SRRC response sampled at \(t = m / L\) symbol periods, where \(L\) is the oversampling factor. With a symbol period normalized to \(T = 1\) and a roll-off factor \(\rho \in [0, 1]\):

\[h(t) = \frac{\sin\left(\pi t (1 - \rho)\right) + 4 \rho t \cos\left(\pi t (1 + \rho)\right)} {\pi t \left(1 - (4 \rho t)^2\right)}\]

completed at its two removable singularities by

\[h(0) = 1 + \rho \left(\frac{4}{\pi} - 1\right), \qquad h\left(\pm \frac{1}{4 \rho}\right) = \frac{\rho}{\sqrt{2}} \left[ \left(1 + \frac{2}{\pi}\right) \sin \frac{\pi}{4 \rho} + \left(1 - \frac{2}{\pi}\right) \cos \frac{\pi}{4 \rho} \right]\]

The response is truncated to \(t \in [-N_h, N_h]\), i.e. \(2 N_h L + 1\) taps, and scaled to unit energy \(\sum_m h^2[m] = 1\) when norm is True.

Why the square root, and why on both sides. Nyquist’s first criterion states that a pulse \(g(t)\) produces no intersymbol interference when it vanishes at every non-zero multiple of the symbol period, \(g(mT) = 0\) for \(m \neq 0\). The raised cosine pulse satisfies it. Here the criterion must hold on the cascade transmit filter / matched receive filter, not on either one alone: splitting the raised cosine into two identical square-root factors,

\[G_{\mathrm{RC}}(f) = H_{\mathrm{SRRC}}(f) \, H_{\mathrm{SRRC}}^*(f) = \left| H_{\mathrm{SRRC}}(f) \right|^2\]

gives a receive filter matched to the transmit pulse – which maximizes the sampled signal-to-noise ratio in additive white Gaussian noise – while their convolution is the raised cosine, hence ISI-free at the symbol instants. The same SRRCFilter instance is therefore placed at the transmitter and at the receiver.

Axes: axis -1 – both methods filter along the last axis and broadcast over the leading ones, so a polarization pair (..., 2, N) is shaped one polarization at a time.

param rho:

Roll-off factor \(\rho \in [0, 1]\). rho = 0 gives the sinc pulse (minimum bandwidth \(1/2T\)), rho = 1 doubles the excess bandwidth.

type rho:

float

param oversampling:

Oversampling factor \(L\) in samples per symbol; the taps are the response sampled at \(t = m / L\).

type oversampling:

int

param N_h:

Half-length \(N_h\) of the filter in symbol periods, so the FIR has \(2 N_h L + 1\) taps. Default is 10.

type N_h:

int, optional, keyword-only

param norm:

If True, normalize the taps to unit energy \(\sum_m h^2[m] = 1\). Default is True.

type norm:

bool, optional, keyword-only

param scale:

Amplitude scaling factor applied to the output. Default is 1.0, applied by both methods.

type scale:

float, optional, keyword-only

param method:

Filtering method: "lfilter" for the causal FIR convolution (scipy.signal.lfilter), "fft" for the circular frequency-domain product with the delay-compensated response returned by H(). Default is "lfilter".

type method:

{“lfilter”, “fft”}, optional, keyword-only

param axis:

Filtering axis. Default is -1. The "lfilter" method honours it; the "fft" method always filters along the last axis.

type axis:

int, optional, keyword-only

param name:

Name of the filter instance. Default is "SRRCFilter".

type name:

str, optional, keyword-only

References

J. G. Proakis, M. Salehi, Digital Communications, 5th ed., McGraw-Hill, 2008, Chapter 9 (signal design for band-limited channels: Nyquist criterion, raised cosine and its square root).

Examples

>>> srrc = SRRCFilter(0.25, 4)  # roll-off 0.25, 4 samples per symbol
>>> h = srrc.h()
>>> h.shape, round(float(np.sum(h ** 2)), 6)
((81,), 1.0)
>>> g = np.convolve(h, h)  # transmit pulse * matched filter = raised cosine
>>> print(np.round(np.abs(g[len(h) - 1 + 4::4][:5]), 3))  # t = T, 2T, ...
[0. 0. 0. 0. 0.]
H(NFFT: int) → ndarray[source]#

Frequency response for fft method

class BWFilter(wn: float)[source]#

Ideal (brick-wall) low-pass filter applied in the frequency domain.

Note

Despite the class name, this block does not implement a Butterworth response: the mask below is rectangular, i.e. an ideal low-pass with infinite roll-off, not the \(|H(f)|^2 = 1 / (1 + (f/f_c)^{2N})\) magnitude of a Butterworth filter of order \(N\). The docstring describes what the code does.

Signal Model#

The input block of \(N\) samples is transformed, masked and transformed back:

\[y[n] = \frac{1}{N} \sum_{k=0}^{N-1} H[k] \, X[k] \, e^{j 2 \pi k n / N}, \qquad X[k] = \sum_{n=0}^{N-1} x[n] \, e^{-j 2 \pi k n / N}\]

with the rectangular mask

\[\begin{split}H[k] = \begin{cases} 1 & \text{if } |f[k]| \leq w_n / 2 \\ 0 & \text{if } |f[k]| > w_n / 2 \end{cases}\end{split}\]

where \(f[k]\) is the frequency of bin \(k\) in cycles per sample (fftfreq(N, d=1), so \(|f[k]| \leq 1/2\)) and the cutoff \(w_n\) is normalized to the Nyquist frequency, the convention of scipy.signal: \(w_n = 1\) passes the whole band, \(w_n = 1/L\) keeps the band of a signal oversampled by \(L\). Because the mask is applied to a length-\(N\) DFT, the operation is a circular convolution with a periodic sinc of length \(N\), not a linear one.

Axes: axis -1 – the FFT mask acts on the last axis; leading axes are batch.

param wn:

Critical (cutoff) frequency \(w_n\), normalized so that \(w_n = 1\) is the Nyquist frequency (scipy.signal convention), hence \(0 < w_n \leq 1\). The retained band in cycles per sample is \(|f| \leq w_n / 2\).

type wn:

float

raises ValueError:

If wn is outside \((0, 1]\).

References

A. V. Oppenheim, R. W. Schafer, Discrete-Time Signal Processing, 3rd ed., Prentice Hall, 2010, Chapter 2 (ideal frequency-selective filters) and Chapter 8 (filtering by DFT multiplication and its circular nature).

Examples

>>> n = np.arange(64)
>>> x = np.cos(2*np.pi*4*n/64) + np.cos(2*np.pi*20*n/64)  # f = 1/16 and 5/16
>>> y = BWFilter(0.2)(x)  # cutoff 0.2 x Nyquist = 0.1 cycles/sample
>>> print(np.round(y[:4].real, 6))
[1.       0.92388  0.707107 0.382683]