Source code for pyfda.libs.pyfda_sig_lib

# -*- coding: utf-8 -*-
#
# This file is part of the pyFDA project hosted at https://github.com/chipmuenk/pyfda
#
# Copyright © pyFDA Project Contributors
# Licensed under the terms of the MIT License
# (see file LICENSE in root directory for details)

"""
Library with various signal processing related functions
"""
import logging
logger = logging.getLogger(__name__)

import time
import numpy as np
from numpy import pi
import scipy.signal as sig

import pyfda.libs.pyfda_lib as pyfda_lib
import pyfda.filterbroker as fb


# ------------------------------------------------------------------------------

[docs] def impz(b, a=1, FS=1, N=0, step=False): """ Calculate impulse response of a discrete time filter, specified by numerator coefficients b and denominator coefficients a of the system function H(z). When only b is given, the impulse response of the transversal (FIR) filter specified by b is calculated. Parameters ---------- b : array_like Numerator coefficients (transversal part of filter) a : array_like (optional, default = 1 for FIR-filter) Denominator coefficients (recursive part of filter) FS : float (optional, default: FS = 1) Sampling frequency. N : float (optional) Number of calculated points. Default: N = len(b) for FIR filters, N = 100 for IIR filters step : bool (optional, default False) return the step response instead of the impulse response Returns ------- hn : ndarray impulse or step response with length N (see above) td : ndarray contains the time steps with same length as hn Examples -------- >>> b = [1,2,3] # Coefficients of H(z) = 1 + 2 z^2 + 3 z^3 >>> h, n = dsp_lib.impz(b) """ a = np.asarray(a) b = np.asarray(b) if len(a) == 1: if len(b) == 1: raise TypeError( 'No proper filter coefficients: len(a) = len(b) = 1 !') else: IIR = False else: if len(b) == 1: IIR = True # Test whether all elements except first are zero elif not np.any(a[1:]) and a[0] != 0: # same as: elif np.all(a[1:] == 0) and a[0] <> 0: IIR = False else: IIR = True if N == 0: # set number of data points automatically if IIR: N = impz_len([b, a]) else: N = len(b) # FIR: N = number of coefficients impulse = np.zeros(N) impulse[0] = 1.0 # create dirac impulse as input signal hn = np.array(sig.lfilter(b, a, impulse)) # calculate impulse response td = np.arange(len(hn)) / FS if step: # calculate step response hn = np.cumsum(hn) return hn, td
# ------------------------------------------------------------------------------
[docs] def impz_len(system, zpk: bool = False, level: float = -40) -> int: """ Calculate length of impulse response for FIR and IIR filters. Parameters ---------- system: array-like The discrete-time system, either specified by its coefficients or its poles and zeros. zpk: bool When False (default), the system is specified by its coefficients. level: float The relative level in dB to which the impulse response has decayed (relevant IIR filters only). Returns ------- N: int The length of the impulse response. Notes ------ For FIR filters, the lenngth of the impulse response is equal to the number of filter taps (= filter order + 1). For IIR filters, it is only possible to approximate the number of steps until the relative level of the impulse response is below the specified `level`. The most simple approximation for IIR filters is to only regard the pole :math:`p_{max}` nearest to the unit circle. For many real-world filters and systems, this pole dominates the transient response ("dominant pole approximation") [JOS_time_constant]_ with a time constant .. math:: \\tau\\approx \\frac{T_S}{1-\\abs{p}_{max}} When calculating the number of samples instead of an absolute time, `T_S = 1`. The number of samples required to reach `level` in dBs is calculated with .. math:: N \\approx -level/20 * \\ln(10) \\tau When poles are specified (`zpk == True`), it is of course easy to find the dominant pole. When coefficients of the system are specified, poles can be calculated as the roots of the system Alternatively, partial fraction expansion (PFE) yields poles and residues. These can be used for a more general solution, see [dsp_stackexchange_2021]_, [dsp_stackexchange_2022]_. """ if fb.fil[0]['ft'] == 'IIR': if zpk: p = system[1] else: r, p, k = sig.residuez(system[0], system[1], tol=0.001, rtype='avg') if max(abs(p)) >= 1: logger.warning( "Unstable system with one or more poles outside the unit circle!") N = 100 else: # only use dominant pole with for calculation and # estimate limit when impulse response has dropped to `level` N = int(round(abs(level)/20 * np.log(10) / (1 - max(np.abs(p))))) else: # FIR: length of impulse response = number of coefficients or order + 1 if zpk: N = len(system[0]) + 1 else: N = len(system[0]) return N
# --------------------------------------------------------------------------
[docs] def zeros_with_val(N: int, val: float = 1., pos: int = 0): """ Create a 1D array of `N` zeros where the element at position `pos` has the value `val`. Parameters ---------- N: int number of elements val: scalar value to be inserted at position `pos` (default: 1) pos: int Position of `val` to be inserted (default: 0) Returns ------- arr: np.ndarray Array with zeros except for element at position `pos` """ if pos >= N or -pos > N: raise(IndexError) a = np.zeros(N, dtype=type(val)) a[pos] = val return a
# ------------------------------------------------------------------------------
[docs] def zpk2array(zpk): """ Test whether Z = zpk[0] and P = zpk[1] have the same length, if not, equalize the lengths by adding zeros. Test whether gain = zpk[2] is a scalar or a vector and whether it (or the first element of the vector) is != 0. If the gain is 0, set gain = 1. Finally, convert the gain into an vector with the same length as P and Z and return the the three vectors as one array. Parameters ---------- zpk : list, tuple or ndarray Zeros, poles and gain of the system Returns ----- zpk as an array or an error string """ try: _ = len(zpk) except TypeError: return f"zpk is a scalar or 'None'!" if type(zpk) in {np.ndarray, list, tuple}: if len(zpk) == 3: # dimensions are ok, but poles / gain could be empty if np.isscalar(zpk[2]) or zpk[2] == []: if zpk[2] == 0: zpk[2] = 1 else: # logger.error(zpk[2]) if zpk[2][0] in {0, None}: zpk[2][0] = 1 elif len(zpk) == 2: # only poles and zeros given: zpk = list(zpk) zpk.append([1]) # set gain = 1 elif len(zpk) == 1: # only zeros given: zpk = list(zpk) zpk.append([0], [1]) # set pole = 0, gain = 1 else: logger.error(f"'zpk' has unsuitable shape '{np.shape(zpk)}'") return f"'zpk' has unsuitable shape '{np.shape(zpk)}'" else: return f"'zpk' has an unsuitable type '{type(zpk)}'" return pyfda_lib.iter2ndarray(zpk)
# ------------------- -----------------------------------------------------------
[docs] def angle_zero(X, n_eps=1e3, mode='auto', wrapped='auto'): """ Calculate angle of argument `X` when abs(X) > `n_eps` * machine resolution. Otherwise, zero is returned. """ phi = np.angle(np.where((np.abs(X) > n_eps * np.spacing(1)), X, 0)) return phi
# ------------------------------------------------------------------------------
[docs] def div_safe(num, den, n_eps: float = 1., i_scale: float = 1., verbose: bool = False): """ Perform elementwise array division after treating singularities, meaning: - check whether denominator (`den`) coefficients approach zero - check whether numerator (`num`) or denominator coefficients are non-finite, i.e. one of `nan`, `ìnf` or `ninf`. At each singularity, replace denominator coefficient by `1` and numerator coefficient by `0` Parameters ---------- num : array_like numerator coefficients den : array_like denominator coefficients n_eps : float n_eps * machine resolution is the limit for the denominator below which the ratio is set to zero. The machine resolution in numpy is given by `np.spacing(1)`, the distance to the nearest number which is equivalent to matlab's "eps". i_scale : float The scale for the index `i` for `num, den` for printing the index of the singularities. verbose : bool, optional whether to print The default is False. Returns ------- ratio : array_like The ratio of num and den (zero at singularities) """ singular = np.where(~np.isfinite(den) | ~np.isfinite(num) | (abs(den) < n_eps * np.spacing(1)))[0] if verbose and np.any(singular): logger.warning('div_safe singularity -> setting to 0 at:') for i in singular: logger.warning('i = {0} '.format(i * i_scale)) num[singular] = 0 den[singular] = 1 return num / den
# ------------------------------------------------------------------------------
[docs] def validate_sos(sos): """ Helper to validate a SOS input Copied from `scipy.signal._filter_design._validate_sos()` """ sos = np.atleast_2d(sos) if sos.ndim != 2: raise ValueError('sos array must be 2D') n_sections, m = sos.shape if m != 6: raise ValueError('sos array must be shape (n_sections, 6)') if not (sos[:, 3] == 1).all(): raise ValueError('sos[:, 3] should be all ones') return sos, n_sections
# ------------------------------------------------------------------------------
[docs] def group_delay(b, a=1, nfft=512, whole=False, analog=False, verbose=True, fs=2.*pi, sos=False, alg="scipy", n_eps=100): """ Calculate group delay of a discrete time filter, specified by numerator coefficients `b` and denominator coefficients `a` of the system function `H` ( `z`). When only `b` is given, the group delay of the transversal (FIR) filter specified by `b` is calculated. Parameters ---------- b : array_like Numerator coefficients (transversal part of filter) a : array_like (optional, default = 1 for FIR-filter) Denominator coefficients (recursive part of filter) whole : boolean (optional, default : False) Only when True calculate group delay around the complete unit circle (0 ... 2 pi) verbose : boolean (optional, default : True) Print warnings about frequency points with undefined group delay (amplitude = 0) and the time used for calculating the group delay nfft : integer (optional, default: 512) Number of FFT-points fs : float (optional, default: fs = 2*pi) Sampling frequency. alg : str (default: "scipy") The algorithm for calculating the group delay: - "scipy" The algorithm used by scipy's grpdelay, - "jos": The original J.O.Smith algorithm; same as in "scipy" except that the frequency response is calculated with the FFT instead of polyval - "diff": Group delay is calculated by differentiating the phase - "Shpakh": Group delay is calculated from second-order sections n_eps : integer (optional, default : 100) Minimum value in the calculation of intermediate values before tau_g is set to zero. Returns ------- tau_g : ndarray group delay w : ndarray angular frequency points where group delay was computed Notes ======= The following explanations follow [JOS]_. **Definition and direct calculation ('diff')** The group delay :math:`\\tau_g(\\omega)` of discrete time (DT) and continuous time (CT) systems is the rate of change of phase with respect to angular frequency. In the following, derivative is always meant w.r.t. :math:`\\omega`: .. math:: \\tau_g(\\omega) = -\\frac{\\partial }{\\partial \\omega}\\angle H( \\omega) = -\\frac{\\partial \\phi(\\omega)}{\\partial \\omega} = - \\phi'(\\omega) With numpy / scipy, the group delay can be calculated directly with .. code-block:: python w, H = sig.freqz(b, a, worN=nfft, whole=whole) tau_g = -np.diff(np.unwrap(np.angle(H)))/np.diff(w) The derivative can create numerical problems for e.g. phase jumps at zeros of frequency response or when the complex frequency response becomes very small e.g. in the stop band. This can be avoided by calculating the group delay from the derivative of the *logarithmic* frequency response in polar form (amplitude response and phase): .. math:: \\ln ( H( \\omega)) = \\ln \\left({H_A( \\omega)} e^{j \\phi(\\omega)} \\right) = \\ln \\left({H_A( \\omega)} \\right) + j \\phi(\\omega) \\Rightarrow \\; \\frac{\\partial }{\\partial \\omega} \\ln ( H( \\omega)) = \\frac{H_A'( \\omega)}{H_A( \\omega)} + j \\phi'(\\omega) where :math:`H_A(\\omega)` is the amplitude response. :math:`H_A(\\omega)` and its derivative :math:`H_A'(\\omega)` are real-valued, therefore, the group delay can be calculated by separating real and imginary components (and discarding the real part): .. math:: \\begin{align} \\Re \\left\\{\\frac{\\partial }{\\partial \\omega} \\ln ( H( \\omega))\\right\\} &= \\frac{H_A'( \\omega)}{H_A( \\omega)} \\\ \\Im \\left\\{\\frac{\\partial }{\\partial \\omega} \\ln ( H( \\omega))\\right\\} &= \\phi'(\\omega) \\end{align} and hence .. math:: \\tau_g(\\omega) = -\\phi'(\\omega) = -\\Im \\left\\{ \\frac{\\partial }{\\partial \\omega} \\ln ( H( \\omega)) \\right\\} =-\\Im \\left\\{ \\frac{H'(\\omega)}{H(\\omega)} \\right\\} Note: The last term contains the complex response :math:`H(\omega)`, not the amplitude response :math:`H_A(\omega)`! In the following, it will be shown that the derivative of birational functions (like DT and CT filters) can be calculated very efficiently and from this the group delay. **J.O. Smith's basic algorithm for FIR filters ('scipy')** An efficient form of calculating the group delay of FIR filters based on the derivative of the logarithmic frequency response has been described in [JOS]_ and [Lyons08]_ for discrete time systems. A FIR filter is defined via its polyome :math:`H(z) = \\sum_k b_k z^{-k}` and has the following derivative: .. math:: \\frac{\\partial }{\\partial \\omega} H(z = e^{j \\omega T}) = \\frac{\\partial }{\\partial \\omega} \\sum_{k = 0}^N b_k e^{-j k \\omega T} = -jT \\sum_{k = 0}^{N} k b_{k} e^{-j k \\omega T} = -jT H_R(e^{j \\omega T}) where :math:`H_R` is the "ramped" polynome, i.e. polynome :math:`H` multiplied with a ramp :math:`k`, yielding .. math:: \\tau_g(e^{j \\omega T}) = -\\Im \\left\\{ \\frac{H'(e^{j \\omega T})} {H(e^{j \\omega T})} \\right\\} = -\\Im \\left\\{ -j T \\frac{H_R(e^{j \\omega T})} {H(e^{j \\omega T})} \\right\\} = T \\, \\Re \\left\\{\\frac{H_R(e^{j \\omega T})} {H(e^{j \\omega T})} \\right\\} scipy's grpdelay directly calculates the complex frequency response :math:`H(e^{j\\omega T})` and its ramped function at the frequency points using the polyval function. When zeros of the frequency response are on or near the data points of the DFT, this algorithm runs into numerical problems. Hence, it is neccessary to check whether the magnitude of the denominator is less than e.g. 100 times the machine eps. In this case, :math:`\\tau_g` is set to zero. **J.O. Smith's basic algorithm for IIR filters ('scipy')** IIR filters are defined by .. math:: H(z) = \\frac {B(z)}{A(z)} = \\frac {\\sum b_k z^k}{\\sum a_k z^k}, their group delay can be calculated numerically via the logarithmic frequency response as well. The derivative of :math:`H(z)` w.r.t. :math:`\\omega` is calculated using the quotient rule and by replacing the derivatives of numerator and denominator polynomes with their ramp functions: .. math:: \\begin{align} \\frac{H'(e^{j \\omega T})}{H(e^{j \\omega T})} &= \\frac{\\left(B(e^{j \\omega T})/A(e^{j \\omega T})\\right)'}{B(e^{j \\omega T})/A(e^{j \\omega T})} = \\frac{B'(e^{j \\omega T}) A(e^{j \\omega T}) - A'(e^{j \\omega T})B(e^{j \\omega T})} { A(e^{j \\omega T}) B(e^{j \\omega T})} \\\\ &= \\frac {B'(e^{j \\omega T})} { B(e^{j \\omega T})} - \\frac { A'(e^{j \\omega T})} { A(e^{j \\omega T})} = -j T \\left(\\frac { B_R(e^{j \\omega T})} {B(e^{j \\omega T})} - \\frac { A_R(e^{j \\omega T})} {A(e^{j \\omega T})}\\right) \\end{align} This result is substituted once more into the log. derivative from above: .. math:: \\begin{align} \\tau_g(e^{j \\omega T}) =-\\Im \\left\\{ \\frac{H'(e^{j \\omega T})}{H(e^{j \\omega T})} \\right\\} &=-\\Im \\left\\{ -j T \\left(\\frac { B_R(e^{j \\omega T})} {B(e^{j \\omega T})} - \\frac { A_R(e^{j \\omega T})} {A(e^{j \\omega T})}\\right) \\right\\} \\\\ &= T \\Re \\left\\{\\frac { B_R(e^{j \\omega T})} {B(e^{j \\omega T})} - \\frac { A_R(e^{j \\omega T})} {A(e^{j \\omega T})} \\right\\} \\end{align} If the denominator of the computation becomes too small, the group delay is set to zero. (The group delay approaches infinity when there are poles or zeros very close to the unit circle in the z plane.) **J.O. Smith's algorithm for CT filters** The same process can be applied for CT systems as well: The derivative of a CT polynome :math:`P(s)` w.r.t. :math:`\\omega` is calculated by: .. math:: \\frac{\\partial }{\\partial \\omega} P(s = j \\omega) = \\frac{\\partial }{\\partial \\omega} \\sum_{k = 0}^N c_k (j \\omega)^k = j \\sum_{k = 0}^{N-1} (k+1) c_{k+1} (j \\omega)^{k} = j P_R(s = j \\omega) where :math:`P_R` is the "ramped" polynome, i.e. its `k` th coefficient is multiplied by the ramp `k` + 1, yielding the same form as for DT systems (but the ramped polynome has to be calculated differently). .. math:: \\tau_g(\\omega) = -\\Im \\left\\{ \\frac{H'(\\omega)}{H(\\omega)} \\right\\} = -\\Im \\left\\{j \\frac{H_R(\\omega)}{H(\\omega)} \\right\\} = -\\Re \\left\\{\\frac{H_R(\\omega)}{H(\\omega)} \\right\\} **J.O. Smith's improved algorithm for IIR filters ('jos')** J.O. Smith gives the following speed and accuracy optimizations for the basic algorithm: - convert the filter to a FIR filter with identical phase and group delay (but with different magnitude response) - use FFT instead of polyval to calculate the frequency response The group delay of an IIR filter :math:`H(z) = B(z)/A(z)` can also be calculated from an equivalent FIR filter :math:`C(z)` with the same phase response (and hence group delay) as the original filter. This filter is obtained by the following steps: - The zeros of :math:`A(z)` are the poles of :math:`1/A(z)`, its phase response is :math:`\\angle A(z) = - \\angle 1/A(z)`. - Transforming :math:`z \\rightarrow 1/z` mirrors the zeros at the unit circle, correcting the negative phase response. This can be performed numerically by "flipping" the order of the coefficients and multiplying by :math:`z^{-N}` where :math:`N` is the order of :math:`A(z)`. This operation also conjugates the coefficients (?) which mirrors the zeros at the real axis. This effect has to be compensated, yielding the polynome :math:`\\tilde{A}(z)`. It is the "flip-conjugate" or "Hermitian conjugate" of :math:`A(z)`. Frequently (e.g. in the scipy and until recently in the Matlab implementation) the conjugate operation is omitted which gives wrong results for complex coefficients. - Finally, :math:`C(z) = B(z) \\tilde{A}(z)`: .. math:: C(z) = B(z)\\left[ z^{-N}{A}^{*}(1/z)\\right] = B(z)\\tilde{A}(z) where .. math:: \\begin{align} \\tilde{A}(z) &= z^{-N}{A}^{*}(1/z) = {a}^{*}_N + {a}^{*}_{N-1}z^{-1} + \ldots + {a}^{*}_1 z^{-(N-1)}+z^{-N}\\\\ \Rightarrow \\tilde{A}(e^{j\omega T}) &= e^{-jN \omega T}{A}^{*}(e^{-j\omega T}) \\\\ \\Rightarrow \\angle\\tilde{A}(e^{j\omega T}) &= -\\angle A(e^{j\omega T}) - N\omega T \\end{align} In Python, the coefficients of :math:`C(z)` are calculated efficiently by convolving the coefficients of :math:`B(z)` and :math:`\\tilde{A}(z)`: .. code-block:: python c = np.convolve(b, np.conj(a[::-1])) where :math:`b` and :math:`a` are the coefficient vectors of the original numerator and denominator polynomes. The actual group delay is then calculated from the equivalent FIR filter as described above. Calculating the frequency response with the `np.polyval(p,z)` function at the `NFFT` frequency points along the unit circle, :math:`z = \\exp(-j \\omega)`, seems to be numerically less robust than using the FFT for the same task, it is also much slower. This measure fixes already most of the problems described for narrowband IIR filters in scipy issues [Scipy_9310]_ and [Scipy_1175]_. In my experience, these problems occur for all narrowband IIR response types. **Shpak algorithm for IIR filters** The algorithm described above is numerically efficient but not robust for narrowband IIR filters. Especially for filters defined by second-order sections, it is recommended to calculate the group delay using the D. J. Shpak's algorithm. Code is available at [Endolith_5828333]_ (GPL licensed) or at [SPA]_ (MIT licensed). This algorithm sums the group delays of the individual sections which is much more robust as only second-order functions are involved. However, converting `(b,a)` coefficients to SOS coefficients introduces inaccuracies. Examples -------- >>> b = [1,2,3] # Coefficients of H(z) = 1 + 2 z^2 + 3 z^3 >>> tau_g, td = pyfda_lib.grpdelay(b) """ if not whole: nfft = 2*nfft w = fs * np.arange(0, nfft)/nfft # create frequency vector tau_g = np.zeros_like(w) # initialize tau_g # ---------------- if alg == 'auto': if sos: alg = "shpak" if verbose: logger.info("Filter in SOS format, using Shpak algorithm for group delay.") elif fb.fil[0]['ft'] == 'IIR': alg = 'jos' # TODO: use 'shpak' here as well? if verbose: logger.info("IIR filter, using J.O. Smith's algorithm for group delay.") else: alg = 'jos' if verbose: logger.info("FIR filter, using J.O. Smith's algorithm for group delay.") if sos and alg != "shpak": b, a = sig.sos2tf(b) time_0 = int(time.perf_counter() * 1e9) # --------------------- if alg == 'diff': w, H = sig.freqz(b, a, worN=nfft, whole=whole) # np.spacing(1) is equivalent to matlab "eps" singular = np.absolute(H) < n_eps * 10 * np.spacing(1) H[singular] = 0 tau_g = -np.diff(np.unwrap(np.angle(H)))/np.diff(w) # differentiation returns one element less than its input w = w[:-1] # --------------------- elif alg == 'jos': b, a = map(np.atleast_1d, (b, a)) # when scalars, convert to 1-dim. arrays c = np.convolve(b, a[::-1]) # equivalent FIR polynome # c(z) = b(z) * a(1/z)*z^(-oa) cr = c * np.arange(c.size) # multiply with ramp -> derivative of c wrt 1/z den = np.fft.fft(c, nfft) # FFT = evaluate polynome around unit circle num = np.fft.fft(cr, nfft) # and ramped polynome at NFFT equidistant points # Check for singularities i.e. where denominator (`den`) coefficients # approach zero or numerator (`num`) or denominator coefficients are # non-finite, i.e. `nan`, `ìnf` or `ninf`. singular = np.where(~np.isfinite(den) | ~np.isfinite(num) | (abs(den) < n_eps * np.spacing(1)))[0] with np.errstate(invalid='ignore', divide='ignore'): # element-wise division of numerator and denominator FFTs tau_g = np.real(num / den) - a.size + 1 # set group delay = 0 at each singularity tau_g[singular] = 0 if verbose and np.any(singular): logger.warning('singularity -> setting to 0 at:') for i in singular: logger.warning('i = {0} '.format(i * fs/nfft)) if not whole: nfft = nfft/2 tau_g = tau_g[0:nfft] w = w[0:nfft] # if analog: # doesnt work yet # a_b = np.convolve(a,b) # if ob > 1: # br_a = np.convolve(b[1:] * np.arange(1,ob), a) # else: # br_a = 0 # ar_b = np.convolve(a[1:] * np.arange(1,oa), b) # num = np.fft.fft(ar_b - br_a, nfft) # den = np.fft.fft(a_b,nfft) # --------------------- elif alg == "scipy": # implementation as in scipy.signal w = np.atleast_1d(w) b, a = map(np.atleast_1d, (b, a)) c = np.convolve(b, a[::-1]) # coefficients of equivalent FIR polynome cr = c * np.arange(c.size) # and of the ramped polynome z = np.exp(-1j * w) # complex frequency points around the unit circle den = np.polyval(c[::-1], z) # evaluate polynome num = np.polyval(cr[::-1], z) # and ramped polynome # Check for singularities i.e. where denominator (`den`) coefficients # approach zero or numerator (`num`) or denominator coefficients are # non-finite, i.e. `nan`, `ìnf` or `ninf`. singular = np.where(~np.isfinite(den) | ~np.isfinite(num) | (abs(den) < n_eps * np.spacing(1)))[0] with np.errstate(invalid='ignore', divide='ignore'): # element-wise division of numerator and denominator FFTs tau_g = np.real(num / den) - a.size + 1 # set group delay = 0 at each singularity tau_g[singular] = 0 if verbose and np.any(singular): logger.warning('singularity -> setting to 0 at:') for i in singular: logger.warning('i = {0} '.format(i * fs/nfft)) if not whole: nfft = nfft/2 tau_g = tau_g[0:nfft] w = w[0:nfft] # --------------------- elif alg.lower() == "shpak": # Use Shpak's algorith if sos: w, tau_g = sos_group_delayz(b, w, fs=fs) else: w, tau_g = group_delayz(b, a, w, fs=fs) else: logger.error('Unknown algorithm "{0}"!'.format(alg)) tau_g = np.zeros_like(w) time_1 = int(time.perf_counter() * 1e9) delta_t = time_1 - time_0 if verbose: logger.info( "grpdelay calculation ({0}): {1:.3g} ms".format(alg, delta_t/1.e6)) return w, tau_g
# ------------------------------------------------------------------------------
[docs] def group_delayz(b, a, w, plot=None, fs=2*np.pi): """ Compute the group delay of digital filter. Parameters ---------- b : array_like Numerator of a linear filter. a : array_like Denominator of a linear filter. w : array_like Frequencies in the same units as `fs`. plot : callable A callable that takes two arguments. If given, the return parameters `w` and `gd` are passed to plot. fs : float, optional The angular sampling frequency of the digital system. Returns ------- w : ndarray The frequencies at which `gd` was computed, in the same units as `fs`. gd : ndarray The group delay in seconds. """ b, a = map(np.atleast_1d, (b, a)) if len(a) == 1: # scipy.signal.group_delay returns gd in samples thus scaled by 1/fs gd = sig.group_delay((b, a), w=w, fs=fs)[1] else: sos = sig.tf2sos(b, a) gd = sos_group_delayz(sos, w, plot, fs)[1] if plot is not None: plot(w, gd) return w, gd
# ------------------------------------------------------------------------------ # # The *_group_delayz routines and subroutines have been copied from # # https://github.com/spatialaudio/group-delay-of-filters # # committed by Nara Hahn under MIT license # # ------------------------------------------------------------------------------
[docs] def sos_group_delayz(sos, w, plot=None, fs=2*np.pi): """ Compute group delay of digital filter in SOS format. Parameters ---------- sos : array_like Array of second-order filter coefficients, must have shape ``(n_sections, 6)``. Each row corresponds to a second-order section, with the first three columns providing the numerator coefficients and the last three providing the denominator coefficients. w : array_like Frequencies in the same units as `fs`. plot : callable, optional A callable that takes two arguments. If given, the return parameters `w` and `gd` are passed to plot. fs : float, optional The sampling frequency of the digital system. Returns ------- w : ndarray The frequencies at which `gd` was computed. gd : ndarray The group delay in seconds. """ sos, n_sections = validate_sos(sos) if n_sections == 0: raise ValueError('Cannot compute group delay with no sections') gd = 0 for biquad in sos: gd += quadfilt_group_delayz(biquad[:3], w, fs)[1] gd -= quadfilt_group_delayz(biquad[3:], w, fs)[1] if plot is not None: plot(w, gd) return w, gd
[docs] def quadfilt_group_delayz(b, w, fs=2*np.pi): """ Compute group delay of 2nd-order digital filter. Parameters ---------- b : array_like Coefficients of a 2nd-order digital filter. w : array_like Frequencies in the same units as `fs`. fs : float, optional The sampling frequency of the digital system. Returns ------- w : ndarray The frequencies at which `gd` was computed. gd : ndarray The group delay in seconds. """ W = 2 * pi * w / fs c1 = np.cos(W) c2 = np.cos(2*W) u0, u1, u2 = b**2 # b[0]**2, b[1]**2, b[2]**2 v0, v1, v2 = b * np.roll(b, -1) # b[0]*b[1], b[1]*b[2], b[2]*b[0] num = (u1+2*u2) + (v0+3*v1)*c1 + 2*v2*c2 den = (u0+u1+u2) + 2*(v0+v1)*c1 + 2*v2*c2 ratio = div_safe(num, den, n_eps=100, verbose=False) return w, 2 * pi / fs * ratio
[docs] def zpk_group_delay(z, p, k, w, plot=None, fs=2*np.pi): """ Compute group delay of digital filter in zpk format. Parameters ---------- z : array_like Zeroes of a linear filter p : array_like Poles of a linear filter k : scalar Gain of a linear filter w : array_like Frequencies in the same units as `fs`. plot : callable, optional A callable that takes two arguments. If given, the return parameters `w` and `gd` are passed to plot. fs : float, optional The sampling frequency of the digital system. Returns ------- w : ndarray The frequencies at which `gd` was computed. gd : ndarray The group delay in seconds. """ gd = 0 for z_i in z: gd += zorp_group_delayz(z_i, w)[1] for p_i in p: gd -= zorp_group_delayz(p_i, w)[1] if plot is not None: plot(w, gd) return w, gd
[docs] def zorp_group_delayz(zorp, w, fs=1): """ Compute group delay of digital filter with a single zero/pole. Parameters ---------- zorp : complex Zero or pole of a 1st-order linear filter w : array_like Frequencies in the same units as `fs`. fs : float, optional The sampling frequency of the digital system. Returns ------- w : ndarray The frequencies at which `gd` was computed. gd : ndarray The group delay in seconds. """ W = 2 * pi * w / fs r, phi = np.abs(zorp), np.angle(zorp) r2 = r**2 cos = np.cos(W - phi) return w, 2 * pi * (r2 - r*cos) / (r2 + 1 - 2*r*cos)