高动态范围 FIR 滤波器

信息处理 过滤器 过滤器设计 有限脉冲响应 数字滤波器 最小二乘
2021-12-21 00:32:09

关于使用 Windowing 方法进行 FIR 滤波器设计与优化算法(如最小二乘法)(在 MATLAB、Octave 和 Python scipy.signal 中,我通常用于滤波器设计的“首选”)的这个问题firls相关,我遇到了另一个有趣的案例据我所知,无法用最小二乘法或 Parks-McClellan 算法解决。具体来说,我需要一组高动态范围滤波器,阻带抑制(和通带失真)小于 -212 dB(通常作为个人挑战,我想知道如何接近任何如果需要,可用的数值精度)。

请注意,因为有人问我:这不适用于处理使用 ADC 捕获的模拟信号或将使用 DAC 转换为模拟信号的应用。这适用于需要非常小的量化噪声或相当高的 SNR 的 dsp 算法。

我的问题如下:最小二乘算法“收敛”并提供了结果,但显然与我使用具有相同抽头数的 Kaiser 窗口实现的结果相比,最小二乘解决方案不是“最小二乘意义上的最优”如宣传的那样。这让我感到惊讶,因为没有任何迹象表明收敛问题(就像 Parks-McClellan 发生的那样,这里用最小二乘法我得到了没有抱怨的结果),所以即使在较低 SNR 的情况下,在这种情况下条件我们是否可以不再“相信”算法提供的解决方案实际上在最小二乘意义上是最优的?

作为最小二乘算法的一部分,我发现理论限制/约束是什么,如果是的话,那个约束是什么?还是我以某种方式错误地接近它?对于下面给出的具体示例,最小二乘算法能否实现低于 212 dB 的抑制?是否有其他最小二乘算法的实现可以做到这一点,而 Python 和 Octave 中的特定实现在某种程度上较差?

例子

在 Octave 或 Python 中使用以下内容:

coeff = firls(287, [0, .4, .6, 1], [1, 1, 0, 0])

对于下面显示的这个半带滤波器,我得到了以下结果,它直接与具有相同数量系数的 Kaiser 开窗 Sinc 脉冲响应(FIR 设计的窗口方法)进行比较。请注意,使用最小二乘算法进一步增加系数的数量并不能进一步减少阻带。我也没有成功使用加权函数,但在这种情况下,我确实希望通带和阻带的性能相同:这些图以橙色显示通带幅度误差以及20log10(1|H(ω)|)

firls 与最小二乘

我确实注意到在最小二乘情况下更宽的过渡带中浪费了“颤振”,我可以通过收紧过渡带(例如 [0, .49, .51, 1])来消除这种情况,但这样做也可以减少阻带拒绝(如预期)。从那里增加点击次数最终只会将“喋喋不休”添加回“无关区域”,而不是提供更多拒绝。

此处详细介绍了最小二乘算法,但我没有看到任何关于可以实现的最终动态范围的限制,也没有提及限制它的内容。确实让我担心的是,系数返回时没有任何问题,但它们实际上并不是最小二乘意义上的最优值,到目前为止,我认为算法保证了这一点,只要它成功收敛 - 所以在这一点上这种保证不再成立吗?

2个回答

问题在于所需响应的制定,尤其是在“无关”区域中,对于所选的滤波器长度而言,该区域非常宽。尽管我不能给出过渡带宽度和滤波器长度之间的任何确切关系,但我知道在最小二乘设计的情况下,如果“don对于给定的过滤器长度,“不在乎”区域太宽。

等波纹 (Parks-McClellan) FIR 滤波器的设计也有类似的效果,如果滤波器长度对于所选的过渡带宽度而言太大,则频率响应会在过渡带中表现出“凸起”。这种现象发生在带通和多波段滤波器上。

在您的具体示例中,使用 Octave 的firls函数,我收到一个警告,指出矩阵是病态的。我几乎可以肯定你应该得到同样的警告。当我尝试使用相同的规范来设计 Parks-McClellan 滤波器(使用remez.m)时,算法停止并显示错误消息“太多极值 - 无法继续”。

因此,确实这两种主流算法都无法满足该规范。然而,这两种算法可以用来实现极高的阻带衰减。但在这种情况下,需要更改规范。有两种方法: 1. 选择(通过反复试验)“匹配”给定滤波器长度的过渡带,这样过渡带中不再有频率响应的伪影。2. 在过渡带中选择所需的响应,而不是没有任何规范的“不关心”区域。

使用第一种方法,我们最终会得到一个非常高的滤波器阶数和一个非常窄的过渡带,这可能比需要的窄得多。第二种方法的困难在于在过渡带中选择所需的响应。如果我们为给定长度的 FIR 滤波器选择一些不“自然”的响应,我们最终会得到很大的近似误差。我记得有一篇论文讨论了这个问题:

Burrus、Soewito 和 Gopinath 的带过渡带的最小二乘误差 FIR 滤波器设计。

只是为了表明最小二乘法可以实现极小的误差,假设我们知道如何指定过渡带中的理想响应,我设计了一个线性相位最小二乘滤波器,其过渡带规范取自由 Kaiser 窗设计的滤波器。当然,在这种情况下我们还不如只使用 Kaiser 窗口滤波器,但实验只是为了证明,鉴于我们知道如何选择合适的过滤器规格。

该设计无法完成,firls因为该功能仅允许分段线性所需响应。我使用了函数lslevin,它允许在频率网格上指定任意复杂的期望响应。

下图显示了结果。两个滤波器(Kaiser 窗口和最小二乘)的响应几乎相同;它们的大小之间的最大差异在的数量级。1012

在此处输入图像描述

总之,最小二乘设计可以实现极小的误差。然而,对于频率选择滤波器,过渡带需要相对较宽才能实现如此小的误差,这会导致相应的线性方程组出现数值病态。为了避免这种情况,需要在过渡带中指定所需的响应。剩下的问题是如何最优地选择这样的响应。上述论文可能是一个很好的起点。但是,我认为这个问题主要是学术兴趣,因为如果真的需要这样的过滤器,(Kaiser)窗口方法很可能是最好的选择。

就像@MattL。和@aconcernedcitizen 说,这个问题是数字的。

Python 在scipy.signal.firls内部使用求解器scipy.linalg.solve对于您的输入,求解器会引发“矩阵奇异”错误,但firls会抑制错误并回退到另一个scipy.linalg.lstsq不会引发错误但也无法正确解决问题的求解器。

增加数值分辨率会有所帮助。

任意精度firls实现

这是我修改后的版本scipy.signal.firlsmpmath用于任意精度浮点数据类型和任意精度求解器。为简洁起见,我删除了所有评论。该代码原始 Scipy 1.7 源的BSD 3-Clause 许可下获得许可,版权所有 2001-2002 Enthought, Inc. 和 2003-2019, SciPy Developers;版权所有。

import numpy as np
from scipy.linalg import (hankel, toeplitz)
import mpmath as mp

def _get_fs(fs, nyq):
    if nyq is None and fs is None:
        fs = 2
    elif nyq is not None:
        if fs is not None:
            raise ValueError("Values cannot be given for both 'nyq' and 'fs'.")
        fs = 2*nyq
    return fs

def firls(numtaps, bands, desired, weight=None, nyq=None, fs=None):
    nyq = 0.5 * _get_fs(fs, nyq)

    numtaps = int(numtaps)
    if numtaps % 2 == 0 or numtaps < 1:
        raise ValueError("numtaps must be odd and >= 1")
    M = (numtaps-1) // 2

    nyq = mp.mpf(nyq)
    if nyq <= 0:
        raise ValueError('nyq must be positive, got %s <= 0.' % nyq)
    bands = np.asarray(bands, dtype=mp.mpf).flatten() / nyq
    if len(bands) % 2 != 0:
        raise ValueError("bands must contain frequency pairs.")
    if (bands < 0).any() or (bands > 1).any():
        raise ValueError("bands must be between 0 and 1 relative to Nyquist")
    bands.shape = (-1, 2)

    desired = np.asarray(desired, dtype=mp.mpf).flatten()
    if bands.size != desired.size:
        raise ValueError("desired must have one entry per frequency, got %s "
                         "gains for %s frequencies."
                         % (desired.size, bands.size))
    desired.shape = (-1, 2)
    if (np.diff(bands) <= 0).any() or (np.diff(bands[:, 0]) < 0).any():
        raise ValueError("bands must be monotonically nondecreasing and have "
                         "width > 0.")
    if (bands[:-1, 1] > bands[1:, 0]).any():
        raise ValueError("bands must not overlap.")
    if (desired < 0).any():
        raise ValueError("desired must be non-negative.")
    if weight is None:
        weight = np.ones(len(desired), dtype=mp.mpf)
    weight = np.asarray(weight, dtype=mp.mpf).flatten()
    if len(weight) != len(desired):
        raise ValueError("weight must be the same size as the number of "
                         "band pairs (%s)." % (len(bands),))
    if (weight < 0).any():
        raise ValueError("weight must be non-negative.")

    n = np.arange(numtaps)[:, np.newaxis, np.newaxis]
    q = np.dot(np.diff(np.vectorize(mp.sincpi)(bands * n) * bands, axis=2)[:, :, 0], weight)

    Q1 = toeplitz(q[:M+1])
    Q2 = hankel(q[:M+1], q[M:])
    Q = Q1 + Q2

    n = n[:M + 1]
    m = (np.diff(desired, axis=1) / np.diff(bands, axis=1))
    c = desired[:, [0]] - bands[:, [0]] * m
    b = bands * (m*bands + c) * np.vectorize(mp.sincpi)(bands * n)

    b[0] -= m * bands * bands / 2.
    b[1:] += m * np.vectorize(mp.cospi)(n[1:] * bands) / (mp.pi * n[1:]) ** 2
    b = np.dot(np.diff(b, axis=2)[:, :, 0], weight)

    a = mp.lu_solve(Q, b)

    coeffs = np.hstack((a[:0:-1], 2 * a[0], a[1:]))
    return coeffs

具有足够的精度(通过变量设置mp.mp.prec)这个版本firls在你的输入上工作得很好。在这种情况下,为了正确显示低于 -400 dB 的幅度值,幅度频率响应计算必须以任意精度进行,如果没有任意精度的快速傅里叶变换 (FFT) 实现,这将非常缓慢。

import mpmath as mp
import matplotlib.pyplot as plt

mp.mp.prec = 150

N = 287
coeff = firls(N, [0, .4, .6, 1], [1, 1, 0, 0])

num_freqs = 256 + 1
f = np.arange(num_freqs, dtype=mp.mpf)/(num_freqs - 1)
phases = np.arange(N)*f.reshape((num_freqs, 1))
phasors = np.vectorize(mp.cospi)(phases) - 1j*np.vectorize(mp.sinpi)(phases)
response = np.dot(phasors, coeff)
plt.plot(0.5*f, 20*np.vectorize(mp.log10)(abs(response)))
plt.grid(True)
plt.ylabel("dB")
plt.xlabel("Frequency (cycles/sample)")
plt.show()

输出:

在此处输入图像描述

所以,是的,只要有足够的精度,最小二乘滤波器设计就可以正常工作!

通过以下方式将任意精度系数四舍五入为双精度浮点:

coeff = coeff.astype(np.float64)

没有给出很好的结果:

在此处输入图像描述

任意精度 Kaiser 加窗 FIR 滤波器设计

这也是 Kaiser 加窗低通有限脉冲响应 (FIR) 滤波器设计的任意精度实现(未经过广泛测试)。在测试中,我调整了 Kaiser 参数 beta,使第一个频率响应零与最小二乘滤波器的频率响应与您的输入参数相匹配:

def mpf_kaiser_lowpass_fir(N, cutoff, beta):
  n = np.arange(N, dtype=mp.mpf)
  alpha = mp.mpf((N-1)/2)
  win = np.vectorize(lambda x: mp.besseli(0, x))(beta*np.vectorize(mp.sqrt)(1 - ((n-alpha)/alpha)**2))/mp.besseli(0, beta)
  coeffs = cutoff*win*np.vectorize(mp.sincpi)((n-(N-1)/2)*cutoff)
  return coeffs

coeff2 = mpf_kaiser_lowpass_fir(N, 0.5, 44.9535)

看起来最小二乘设计(蓝色)正在战胜 Kaiser 窗口设计(橙色):

在此处输入图像描述

放大:

在此处输入图像描述

调整 beta 以使 0.3 个周期/样本的两个频率响应相等,给出了质量相似的比较结果(未显示)。