Source code for pyFDN.dsp.dfilt_matrix

"""Matrix of FIR filters with persistent state (dfiltMatrix translation).

The MATLAB toolbox wraps every matrix entry in a ``dfilt`` object; here only
the FIR-matrix case is needed (used for paraunitary / velvet feedback
matrices). Scalar matrices are handled directly by ``process_fdn`` and IIR
per-delay-line absorption is handled by
:class:`pyFDN.dsp.sos_filter_bank.SOSFilterBank`, so neither needs this class.
"""

from __future__ import annotations

import numpy as np
from numpy.typing import ArrayLike
from scipy.signal import lfilter


[docs] class FIRMatrixFilter: """Apply a matrix of FIR filters to a multichannel signal, block by block. Parameters ---------- coefficients : (n_out, n_in, order) array FIR coefficients per matrix entry in z^{-1} convention (``coefficients[i, j, k]`` is the tap of ``z^{-k}`` from input ``j`` to output ``i``). Filter state persists across calls to :meth:`filter`, so a long signal can be processed in consecutive blocks. """
[docs] def __init__(self, coefficients: ArrayLike): coeffs = np.asarray(coefficients, dtype=float) if coeffs.ndim != 3: raise ValueError("coefficients must have shape (n_out, n_in, order)") self.coefficients = coeffs self.n_out, self.n_in, self.order = coeffs.shape self._state = np.zeros((self.n_out, self.n_in, max(self.order - 1, 1)))
[docs] def filter(self, block: ArrayLike) -> np.ndarray: """Filter a block of shape (block_size, n_in) to (block_size, n_out).""" x = np.asarray(block, dtype=float) if x.ndim != 2 or x.shape[1] != self.n_in: raise ValueError(f"block must have shape (block_size, {self.n_in})") out = np.zeros((x.shape[0], self.n_out)) if self.order == 1: return x @ self.coefficients[:, :, 0].T for i in range(self.n_out): for j in range(self.n_in): y, self._state[i, j] = lfilter( self.coefficients[i, j], [1.0], x[:, j], zi=self._state[i, j] ) out[:, i] += y return out