首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >来自 1986 的 DFT 改进算法(YUNSWJ 数值分析实现版)

来自 1986 的 DFT 改进算法(YUNSWJ 数值分析实现版)

作者头像
云深无际
发布2026-01-07 14:17:40
发布2026-01-07 14:17:40
1360
举报
文章被收录于专栏:云深之无迹云深之无迹

今天来一个有趣的论文:

是一个 1954 年就有的刊物
是一个 1954 年就有的刊物

是一个 1954 年就有的刊物

86年的文章
86年的文章

86年的文章

就感觉知识没有改变,几十年前都一样。

它不是讲 FFT,而是在推导“如何用有限长、离散采样的数据更精确地计算傅里叶积分”,尤其解决“傅里叶变换本质是连续积分,但我们只有有限 N 点样本”这个根本矛盾。

这是国内早期经典研究,解决的问题是: 如何用 DFT(离散傅里叶变换)更精确地逼近连续傅里叶变换(Fourier Integral),尤其当数据不是周期信号而是有限长截断样本。

论文主要分五部分:

  1. 有限时间 / 多项式模拟信号的 DFT 表达式推导
  2. 推导傅里叶变换中核函数(sinc + Dirichlet kernel)的逼近误差
  3. 周期函数 DFT 的严格公式
  4. 截断函数(有限窗口)DFT 的改进公式(核心贡献)
  5. 给出新算法,并与传统 FFT 结果对比,证明误差更小

传统 DFT 做的是:

把有限采样信号强行看成“周期信号”,然后做 Fourier 展开。

但真实世界信号不是周期的,所以结果会有误差。

论文做的是:

不强行周期,而是将采样点还原成一个近似的连续函数,再对这个近似函数做真正的傅里叶积分。

自然就更准确。

论文的贡献

对“有限长采样”重新建模

论文不是像传统 FFT 那样假定:

信号是周期的

而是把信号视为在有限区间上给定的样本 mΔt,并用多项式插值模拟真实信号

推导精确的傅里叶积分公式

傅里叶积分本来应该是:

但数据只有有限个点,论文提出:

其中 是推导出来的“加权核(类似改进的 sinc kernel)”。

得到比传统 DFT 更接近真实傅里叶变换的结果

传统 DFT 存在误差:假定信号周期化 → 会产生“泄漏”;使用矩形窗 → 主瓣宽、副瓣大;离散求和逼近连续积分不够精确。作者通过多项式插值推导出更精确的公式,减少泄漏与边界误差。

连续傅里叶变换 vs DFT 的本质差异

论文反复强调:傅里叶积分 = 连续时间信号DFT = 有限离散样本构造的周期信号。因此 DFT 是对真正信号的污染,因为周期延拓导致频谱泄漏,有限窗口导致卷积,采样带来混叠

作者认为:要减少误差应把采样点当成“测得信号在该点的真实值”,再用插值构造信号的近似表达,最后再做积分。

用多项式插值重建信号

论文用的是类似:

把采样点转化为连续函数,然后再积分:

积分变成对多项式的积分 → 可以得到闭式表达式。(这非常像数字信号处理里的 spline interpolation或者信号重建理论中的 Whittaker–Shannon sinc 插值

得到改进的核函数(比 sinc 更复杂但更准确)

论文给出的新核函数形式(例如公式 (10)–(15))是这样:

是修正过的 sinc 核。

→ 这比传统 DFT:

要更接近连续傅里叶变换。

为什么这篇论文今天仍然有价值?

因为它揭示了 FFT 频谱为什么会泄漏、边界怎么影响频率分辨率

问题

论文的解释

矩形窗为什么泄漏严重?

sinc 核副瓣大,卷积导致频谱泄漏

为什么窗函数能改善频谱?

是在修改核函数,使其更接近真实傅里叶积分

为什么有限观测长度导致偏差?

截断使核函数变化,积分误差变大

为什么插值(zero padding)能改善显示?

改变了核函数的采样密度

FFT != 真·傅里叶变换任何有限数据做出的 FFT 都是“污染过的频谱”。

必须用窗函数因为矩形窗会严重扭曲核函数。

采样点不是“孤立点”,而是连续函数的样本 → 插值很重要

DFT 可以写成:采样点 × 核函数的加权求和这为设计“最佳核函数”提供了数学框架。

把这个新算法写成现代 Python 代码

把论文里“用插值逼近连续傅里叶积分”的思路,写成一个现代的数值算法。

思路: 把离散点 当成连续信号在 的采样 在每个采样区间用线性插值重建 对这个分段线性函数做解析积分,得到比普通 DFT 更接近“真·傅里叶积分”的结果

这和论文里“先用多项式拟合,再求傅里叶积分”的思想是一致的,只是选了工程上最好实现、数值最稳的一阶多项式(线性)版本

会看到:两条曲线主峰位置基本一致,线性插值版本在邻近频点的泄漏会略有不同(一般更接近真实连续谱),对包含斜坡、趋势的信号,传统 DFT 的边界误差更明显,而这版会好一些

和论文的关系怎么理解?

论文里做的是更一般的“多项式(甚至高阶)拟合 + 解析积分”,上面我这段 Python 用的是最简单、直接可用的线性多项式版本,但是本质一样:不把采样点当“方块”做矩形积分(普通 DFT),而是先恢复一个连续函数近似,再做积分 → 更接近真正的 Fourier integral。

继续深入,做更多的误差分析

与 FFT 对比,生成图像: 研究主瓣差异,泄漏差异,边界误差,插值误差以及Window 影响。

主瓣差异
主瓣差异

主瓣差异

设定

信号:, Hz(刻意选成非整数 bin)

N = 256,fs = 1 kHz

图里两条曲线:

Linear-interp FT:线性插值积分算法

Rectangular FFT:普通 DFT(矩形窗)

可以看到:主峰顶点附近,线性插值 FT 的曲线更“平滑”,峰值位置更靠近真实 57.3 Hz;普通 FFT 因为只能在离散 bin 上取值,相当于在“锯齿状的 Dirichlet 核”上采样,主瓣有明显的“折线感”。

非整数 bin 信号时,线性插值积分比单纯在 FFT bin 采样更接近真实谱峰。

泄漏差异 / 旁瓣(第二张图,log 轴)
泄漏差异 / 旁瓣(第二张图,log 轴)

泄漏差异 / 旁瓣(第二张图,log 轴)

同一个信号,频率范围放宽到 0–300 Hz,用对数纵轴:

两条曲线在远离主峰处几乎重合,说明: 线性插值不会神奇地“消灭泄漏”,泄漏模式仍主要由“矩形窗”决定。但在主峰附近,线性插值的曲线稍微更平滑,旁瓣高度有微小差异(更接近理论连续谱)。

泄漏本质上是“有限观察时间 + 窗形”的问题,而不是积分精度本身;新算法主要是在同样窗形前提下,给你更准确的核积分,所以“主瓣 + 邻近旁瓣”区域改善更明显,远处副瓣形状基本一样。

边界误差 + 插值误差
边界误差 + 插值误差

边界误差 + 插值误差

信号换成:

多加了一个线性趋势,边界处不再平滑。

步骤:

  1. 超稠密采样 + 数值积分 做一版 “高精度 Fourier 积分” 当作 真值 X_true
  2. 对同一组频率:用普通 FFT(矩形窗)得到 ,再插值到同一频率轴;用线性插值 FT 得到 。
  3. 画:

误差误差

结果(log 纵轴):

在全频段内,橙色(Linear-interp)误差普遍比蓝色(FFT)低约 1–2 个数量级

在主峰附近(约 50–70 Hz),差别特别明显:FFT 的误差可到 ,线性插值在 ~ 级别。

边界误差

线性趋势使得时间窗两端的值不相等 → 周期延拓后,边界出现“大跳变”;普通 DFT 把这当成真实信号的一部分,频域里会多出一堆低频能量和畸变 → 误差显著。

线性插值 FT 相当于对每一段都按“真实斜率”去积分,在有限观察区间内更精确地拟合连续曲线 → 边界对频谱的污染更小。

插值误差

普通 FFT 相当于对“阶梯函数(零阶保持)”积分;Linear-interp 对“线性段”积分,是高一阶的数值积分精度;所以即便没有边界问题,线性插值也会系统性降低数值积分误差,图里整个频带误差都更低就是这个原因。

Window 影响分析
Window 影响分析

Window 影响分析

这里我们看 4 条曲线(log 轴):

  1. Rectangular window - FFT
  2. Rectangular window - Linear-interp
  3. Hann window - FFT
  4. Hann window - Linear-interp

可以看出:

矩形窗 vs Hann 窗:

矩形窗:旁瓣在约 附近慢慢衰减;

Hann 窗:主瓣变宽,但旁瓣瞬间掉到 以下,然后非常快地衰减→ 典型的“主瓣变宽、副瓣下降”的窗函数 trade-off,在两种算法中都一致。

同一窗形里,新算法 vs FFT:

对于矩形窗:两条矩形窗曲线整体很接近,但线性插值的细节更平滑;对于 Hann 窗:两条 Hann 曲线几乎“扣在一起”,说明当窗函数已经把时间域信号变得相对光滑时,线性插值带来的改进主要是数值精度,不再根本改变泄漏形态。

窗的作用是“改变核函数本身”(决定泄漏结构) 线性插值 FT 是在既定核函数下做“更精确积分”两者是正交的;可以 “先用窗 + 再用改进积分算法”,效果叠加。

数值算法实现

我先放使用论文核心实现的函数(其实数值算法才是我的真爱):

代码语言:javascript
复制
import numpy as np

def fourier_linear_interp(x, dt, freqs, angular_freq=False):
    """
    Improved DFT-like Fourier transform based on piecewise-linear interpolation.

    Parameters
    ----------
    x : array_like, shape (N,)
        Time-domain samples x[n] of a real or complex signal.
        Assumed uniformly spaced with sampling interval dt.
        You should provide N >= 2 samples.
    dt : float
        Sampling interval in seconds.
    freqs : array_like
        Frequencies at which to evaluate the Fourier transform.
        If angular_freq=False (default), this is in Hz.
        If angular_freq=True, this is in rad/s.
    angular_freq : bool, optional
        If True, `freqs` is interpreted as angular frequency ω (rad/s).
        If False (default), `freqs` is in Hz and ω = 2π f.

    Returns
    -------
    X : ndarray, shape (len(freqs),)
        Approximation to the continuous-time Fourier transform
        X(ω) = ∫ x(t) e^{-j ω t} dt
        computed using piecewise-linear interpolation between samples.

    Notes
    -----
    - This is *not* the same as the standard DFT / FFT.
      Standard DFT corresponds roughly to a rectangular-rule
      numerical integral; this function corresponds to integrating
      a piecewise-linear interpolant exactly.
    - For ω = 0, the formula is handled using exact limits:
        A(0) = Δt
        B(0) = Δt^2 / 2
      which avoids division-by-zero and improves numerical stability.
    """
    x = np.asarray(x, dtype=np.complex128)
    freqs = np.asarray(freqs, dtype=np.float64)

    if x.ndim != 1:
        raise ValueError("x must be 1-D array of samples.")
    if x.size < 2:
        raise ValueError("Need at least 2 samples for linear interpolation.")

    # Angular frequency array
    if angular_freq:
        omega = freqs
    else:
        omega = 2.0 * np.pi * freqs  # rad/s

    N = x.size
    # Segment endpoints: x_n on [t_n, t_{n+1}]
    x0 = x[:-1]          # shape (N-1,)
    x1 = x[1:]           # shape (N-1,)
    a = x0               # intercept
    b = (x1 - x0) / dt   # slope
    t_n = dt * np.arange(N - 1, dtype=np.float64)  # left endpoints

    # Prepare output
    X = np.zeros_like(omega, dtype=np.complex128)

    # Handle ω = 0 separately (exact limits)
    eps = 1e-12  # small tolerance
    mask_zero = np.abs(omega) < eps
    mask_nonzero = ~mask_zero

    # ---- ω = 0 branch ----
    if np.any(mask_zero):
        # ∫ x(t) dt over each linear segment:
        # x(t) = a + b (t - t_n), t ∈ [t_n, t_n + dt]
        # Integral is: a*dt + b*(dt^2 / 2)  (shift does not matter for area)
        seg_int = a * dt + b * (dt**2 / 2.0)  # shape (N-1,)
        X[mask_zero] = np.sum(seg_int)

    # ---- ω ≠ 0 branch ----
    if np.any(mask_nonzero):
        w = omega[mask_nonzero]            # shape (M,)
        # Common factors in closed-form integrals over [0, dt]
        # A(w) = ∫_0^dt e^{-j w t} dt
        A = (1.0 - np.exp(-1j * w * dt)) / (1j * w)  # shape (M,)
        # B(w) = ∫_0^dt t e^{-j w t} dt
        # from sympy: ((j*dt*w + 1)*e^{-j*w*dt} - 1)/w^2
        B = (((1j * dt * w + 1.0) * np.exp(-1j * w * dt)) - 1.0) / (w**2)

        # Broadcast to (M, N-1)
        A = A[:, None]   # (M,1)
        B = B[:, None]   # (M,1)

        a_col = a[None, :]  # (1, N-1)
        b_col = b[None, :]  # (1, N-1)
        t_col = t_n[None, :]  # (1, N-1)

        # e^{-j w t_n} factor for each segment and frequency
        exp_term = np.exp(-1j * w[:, None] * t_col)  # (M, N-1)

        # Segment contributions:
        # I_n(w) = e^{-j w t_n} [ a_n A(w) + b_n B(w) ]
        seg_contrib = exp_term * (a_col * A + b_col * B)  # (M, N-1)

        X[mask_nonzero] = np.sum(seg_contrib, axis=1)

    return X

从一段线性插值开始

假设采样间隔是 ,采样点是 :时间轴:

在区间 上,用线性插值逼近真实信号:

其中

这一段对连续傅里叶变换的贡献是:

把变量换成 ,区间变成 :

把它拆成两部分:

所以只要把两个“核函数积分” 推出来,就搞定整个算法。

推出 的闭式表达

这正是代码里的:

代码语言:javascript
复制
A = (1.0 - np.exp(-1j * w * dt)) / (1j * w)

这个可以用“分部积分”或者把 看成核,对 求导得到。最后结果:

对应代码是:

代码语言:javascript
复制
B = (((1j * dt * w + 1.0) * np.exp(-1j * w * dt)) - 1.0) / (w**2)

可以自己用 sympy 验一下,这个式子是对的。

把所有区间叠加,得到总频谱

整个时间区间 被分成 段,每一段的贡献是 ,所以总的傅里叶变换近似是:

对每个频点 ,这是一个对所有段的加和。翻译成 numpy 的广播就是:

代码语言:javascript
复制
a_col = a[None, :]    # (1, N-1)
b_col = b[None, :]
t_col = t_n[None, :]
exp_term = np.exp(-1j * w[:, None] * t_col)  # (M, N-1), M 个频点
seg_contrib = exp_term * (a_col * A + b_col * B)  # (M, N-1)
X[mask_nonzero] = np.sum(seg_contrib, axis=1)

这就是改进算法的核心公式——对每一段的线性插值做解析积分。

为啥 ω=0 要单独处理?

当 时, 都是 “0/0” 型,直接代公式会数值不稳定,所以要用极限:

时,

代进去可以算出:

而此时 ,所以:

整段积分就是所有 的和:

代码语言:javascript
复制
seg_int = a * dt + b * (dt**2 / 2.0)
X[mask_zero] = np.sum(seg_int)

这个正好等于对分段线性函数做 ,所以频率 0 点的值就是信号的时间积分,完全符合连续傅里叶变换定义 。

和普通 FFT 的关系:零阶 vs 一阶

再对比一下我们熟悉的 DFT/FFT。

普通 DFT 是什么数值积分?

如果我们用最简单的“矩形法”近似傅里叶积分:

这恰好就是 DFT(除了一个 的比例因子):

从数值分析角度看:普通 FFT = 零阶保持(piecewise constant)+ 矩形积分,把每个采样点当“方块高度”,中间完全不管。

现在这个算法是什么?

我们先做“分段线性插值”(一阶多项式),再对这个连续函数做解析积分;所以从数值积分阶数上来说,它是比矩形法高一阶的近似。

可以这么理解:

方法

插值模型

数值积分方法

FFT / DFT

每段是常数(阶梯)

矩形规则

线性插值 FT

每段是一次函数

解析积分(相当于梯形+修正项)

更高阶多项式

每段是二次/三次多项式

再复杂一点的解析积分

这刚好对应了论文里说的“用多项式逼近 + 推导新的 DFT 核”的思想,只不过我们选的是最简单的一阶情况。

和论文思路的对应关系

论文大致是:把原始信号在有限区间上写成多项式展开(或者分段多项式插值);对这个多项式形式的 做傅里叶积分 ⇒ 得到新的“核函数”(类似我们的 );用这些核函数去加权采样值,就得到比传统 DFT 更精确的频谱估计。

我们现在具体做的是:

只取到了“一次多项式”(线性插值),所以核只有两个:;把它写成“对每段线性插值积分”的形式,便于用 numpy 向量化计算;

实际上你可以用同样套路继续往上推:

分段二次:要算 三种核

分段三次:要再多一个

这就是论文里“多项式阶数越高,理论上越接近连续傅里叶积分”的原因。

后记

里面的数学知识就是高等数学和泛函分析+信号与系统和数值分析这些。我为什么研究这些东西,因为在硬件同质化的今天,软件才是定义一切的杀手锏,这才是真真正正的竞争力;这些算法之后会加入到我的信号与系统库中,大家敬请期待。

代码

代码语言:javascript
复制
import numpy as np

def fourier_linear_interp(x, dt, freqs, angular_freq=False):
    """
    Improved DFT-like Fourier transform based on piecewise-linear interpolation.

    Parameters
    ----------
    x : array_like, shape (N,)
        Time-domain samples x[n] of a real or complex signal.
        Assumed uniformly spaced with sampling interval dt.
        You should provide N >= 2 samples.
    dt : float
        Sampling interval in seconds.
    freqs : array_like
        Frequencies at which to evaluate the Fourier transform.
        If angular_freq=False (default), this is in Hz.
        If angular_freq=True, this is in rad/s.
    angular_freq : bool, optional
        If True, `freqs` is interpreted as angular frequency ω (rad/s).
        If False (default), `freqs` is in Hz and ω = 2π f.

    Returns
    -------
    X : ndarray, shape (len(freqs),)
        Approximation to the continuous-time Fourier transform
        X(ω) = ∫ x(t) e^{-j ω t} dt
        computed using piecewise-linear interpolation between samples.

    Notes
    -----
    - This is *not* the same as the standard DFT / FFT.
      Standard DFT corresponds roughly to a rectangular-rule
      numerical integral; this function corresponds to integrating
      a piecewise-linear interpolant exactly.
    - For ω = 0, the formula is handled using exact limits:
        A(0) = Δt
        B(0) = Δt^2 / 2
      which avoids division-by-zero and improves numerical stability.
    """
    x = np.asarray(x, dtype=np.complex128)
    freqs = np.asarray(freqs, dtype=np.float64)

    if x.ndim != 1:
        raise ValueError("x must be 1-D array of samples.")
    if x.size < 2:
        raise ValueError("Need at least 2 samples for linear interpolation.")

    # Angular frequency array
    if angular_freq:
        omega = freqs
    else:
        omega = 2.0 * np.pi * freqs  # rad/s

    N = x.size
    # Segment endpoints: x_n on [t_n, t_{n+1}]
    x0 = x[:-1]          # shape (N-1,)
    x1 = x[1:]           # shape (N-1,)
    a = x0               # intercept
    b = (x1 - x0) / dt   # slope
    t_n = dt * np.arange(N - 1, dtype=np.float64)  # left endpoints

    # Prepare output
    X = np.zeros_like(omega, dtype=np.complex128)

    # Handle ω = 0 separately (exact limits)
    eps = 1e-12  # small tolerance
    mask_zero = np.abs(omega) < eps
    mask_nonzero = ~mask_zero

    # ---- ω = 0 branch ----
    if np.any(mask_zero):
        # ∫ x(t) dt over each linear segment:
        # x(t) = a + b (t - t_n), t ∈ [t_n, t_n + dt]
        # Integral is: a*dt + b*(dt^2 / 2)  (shift does not matter for area)
        seg_int = a * dt + b * (dt**2 / 2.0)  # shape (N-1,)
        X[mask_zero] = np.sum(seg_int)

    # ---- ω ≠ 0 branch ----
    if np.any(mask_nonzero):
        w = omega[mask_nonzero]            # shape (M,)
        # Common factors in closed-form integrals over [0, dt]
        # A(w) = ∫_0^dt e^{-j w t} dt
        A = (1.0 - np.exp(-1j * w * dt)) / (1j * w)  # shape (M,)
        # B(w) = ∫_0^dt t e^{-j w t} dt
        # from sympy: ((j*dt*w + 1)*e^{-j*w*dt} - 1)/w^2
        B = (((1j * dt * w + 1.0) * np.exp(-1j * w * dt)) - 1.0) / (w**2)

        # Broadcast to (M, N-1)
        A = A[:, None]   # (M,1)
        B = B[:, None]   # (M,1)

        a_col = a[None, :]  # (1, N-1)
        b_col = b[None, :]  # (1, N-1)
        t_col = t_n[None, :]  # (1, N-1)

        # e^{-j w t_n} factor for each segment and frequency
        exp_term = np.exp(-1j * w[:, None] * t_col)  # (M, N-1)

        # Segment contributions:
        # I_n(w) = e^{-j w t_n} [ a_n A(w) + b_n B(w) ]
        seg_contrib = exp_term * (a_col * A + b_col * B)  # (M, N-1)

        X[mask_nonzero] = np.sum(seg_contrib, axis=1)

    return X
代码语言:javascript
复制
if __name__ == "__main__":
    import matplotlib.pyplot as plt

    # 构造一个简单信号:非整数周期的正弦 + 线性趋势
    fs = 1000.0   # 采样率 Hz
    dt = 1.0 / fs
    T = 1.0       # 观测长度 1 s
    t = np.arange(0, T, dt)
    f0 = 57.3     # 非整数 bin 频率
    x = 0.8 * np.sin(2*np.pi*f0*t) + 0.1*t

    # 频率轴:和 FFT 一样取 0..fs/2
    N = len(t)
    freqs = np.linspace(0, fs/2, 2048, endpoint=False)

    # 新算法(线性插值积分近似傅里叶变换)
    X_lin = fourier_linear_interp(x, dt, freqs, angular_freq=False)

    # 标准 FFT(只看正频)
    X_fft = np.fft.rfft(x) * dt               # 简单把和式当积分
    f_fft = np.fft.rfftfreq(N, dt)

    # 画幅度谱比较
    plt.figure()
    plt.semilogy(freqs, np.abs(X_lin), label="Linear-interp FT")
    plt.semilogy(f_fft, np.abs(X_fft), "--", label="Rectangular DFT/FFT")
    plt.xlim(0, 200)
    plt.xlabel("Frequency (Hz)")
    plt.ylabel("|X(f)|")
    plt.legend()
    plt.grid(True)
    plt.show()
代码语言:javascript
复制
import numpy as np
import matplotlib.pyplot as plt

def fourier_linear_interp(x, dt, freqs, angular_freq=False):
    x = np.asarray(x, dtype=np.complex128)
    freqs = np.asarray(freqs, dtype=np.float64)

    if x.ndim != 1:
        raise ValueError("x must be 1-D array of samples.")
    if x.size < 2:
        raise ValueError("Need at least 2 samples for linear interpolation.")

    if angular_freq:
        omega = freqs
    else:
        omega = 2.0 * np.pi * freqs  # rad/s

    N = x.size
    x0 = x[:-1]
    x1 = x[1:]
    a = x0
    b = (x1 - x0) / dt
    t_n = dt * np.arange(N - 1, dtype=np.float64)

    X = np.zeros_like(omega, dtype=np.complex128)

    eps = 1e-12
    mask_zero = np.abs(omega) < eps
    mask_nonzero = ~mask_zero

    if np.any(mask_zero):
        seg_int = a * dt + b * (dt**2 / 2.0)
        X[mask_zero] = np.sum(seg_int)

    if np.any(mask_nonzero):
        w = omega[mask_nonzero]
        A = (1.0 - np.exp(-1j * w * dt)) / (1j * w)
        B = (((1j * dt * w + 1.0) * np.exp(-1j * w * dt)) - 1.0) / (w**2)

        A = A[:, None]
        B = B[:, None]

        a_col = a[None, :]
        b_col = b[None, :]
        t_col = t_n[None, :]

        exp_term = np.exp(-1j * w[:, None] * t_col)
        seg_contrib = exp_term * (a_col * A + b_col * B)

        X[mask_nonzero] = np.sum(seg_contrib, axis=1)

    return X

# Common signal parameters
fs = 1000.0
dt = 1.0 / fs
N = 256
t = np.arange(N) * dt
f0 = 57.3  # non-bin-centered frequency
x_base = np.sin(2 * np.pi * f0 * t)

# === Figure 1: Main lobe comparison ===
freqs_dense = np.linspace(f0 - 20, f0 + 20, 2000)
X_lin_dense = fourier_linear_interp(x_base, dt, freqs_dense, angular_freq=False)

X_fft = np.fft.rfft(x_base) * dt
f_fft = np.fft.rfftfreq(N, dt)

plt.figure()
plt.plot(freqs_dense, np.abs(X_lin_dense), label="Linear-interp FT")
plt.plot(f_fft, np.abs(X_fft), linestyle="--", marker="", label="Rectangular FFT")
plt.xlim(f0 - 10, f0 + 10)
plt.xlabel("Frequency (Hz)")
plt.ylabel("Amplitude")
plt.title("Main lobe comparison (non-bin-centered sine)")
plt.grid(True)
plt.legend()
plt.show()

# === Figure 2: Leakage / sidelobe comparison (wider view, log scale) ===
freqs_wide = np.linspace(0, fs/2, 4000)
X_lin_wide = fourier_linear_interp(x_base, dt, freqs_wide, angular_freq=False)

plt.figure()
plt.semilogy(freqs_wide, np.abs(X_lin_wide), label="Linear-interp FT")
plt.semilogy(f_fft, np.abs(X_fft), linestyle="--", marker="", label="Rectangular FFT")
plt.xlim(0, 300)
plt.xlabel("Frequency (Hz)")
plt.ylabel("Magnitude (log scale)")
plt.title("Leakage / sidelobes comparison")
plt.grid(True)
plt.legend()
plt.show()

# === Figure 3: Boundary + interpolation error vs high-res numeric FT ===
# Build a slightly more complicated signal with trend to emphasize boundary
x_trend = 0.8 * np.sin(2 * np.pi * f0 * t) + 0.2 * (t / t[-1])

# High-res numeric FT as "truth"
oversample = 50
t_fine = np.arange(0, t[-1] + dt/oversample, dt/oversample)
x_fine = 0.8 * np.sin(2 * np.pi * f0 * t_fine) + 0.2 * (t_fine / t[-1])

freqs_err = np.linspace(0, 200, 300)
omega_err = 2 * np.pi * freqs_err

X_true = np.zeros_like(freqs_err, dtype=np.complex128)
for i, w in enumerate(omega_err):
    X_true[i] = np.trapz(x_fine * np.exp(-1j * w * t_fine), t_fine)

X_fft_trend = np.fft.rfft(x_trend) * dt
f_fft_trend = np.fft.rfftfreq(N, dt)
X_lin_trend = fourier_linear_interp(x_trend, dt, freqs_err, angular_freq=False)

# Interpolate FFT result onto freqs_err grid for fair comparison
X_fft_interp = np.interp(freqs_err, f_fft_trend, np.abs(X_fft_trend))

err_fft = np.abs(X_fft_interp - np.abs(X_true))
err_lin = np.abs(np.abs(X_lin_trend) - np.abs(X_true))

plt.figure()
plt.semilogy(freqs_err, err_fft + 1e-18, label="Rectangular FFT error")
plt.semilogy(freqs_err, err_lin + 1e-18, label="Linear-interp FT error")
plt.xlabel("Frequency (Hz)")
plt.ylabel("Absolute error vs high-res numeric FT")
plt.title("Boundary + interpolation error comparison")
plt.grid(True)
plt.legend()
plt.show()

# === Figure 4: Window effect (Hann) on both methods ===
hann = 0.5 - 0.5 * np.cos(2 * np.pi * np.arange(N) / (N - 1))
x_win = x_base * hann

X_fft_win = np.fft.rfft(x_win) * dt
X_lin_win = fourier_linear_interp(x_win, dt, freqs_wide, angular_freq=False)

plt.figure()
plt.semilogy(freqs_wide, np.abs(X_lin_wide), label="Rectangular window - Linear-interp")
plt.semilogy(f_fft, np.abs(X_fft), linestyle="--", label="Rectangular window - FFT")
plt.semilogy(freqs_wide, np.abs(X_lin_win), label="Hann window - Linear-interp")
plt.semilogy(f_fft, np.abs(X_fft_win), linestyle="--", label="Hann window - FFT")
plt.xlim(0, 300)
plt.xlabel("Frequency (Hz)")
plt.ylabel("Magnitude (log scale)")
plt.title("Window effect on leakage (Rectangular vs Hann)")
plt.grid(True)
plt.legend()
plt.show()

http://www.geophy.cn/article/id/cjg_4985

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-12-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 云深之无迹 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 论文的贡献
    • 对“有限长采样”重新建模
    • 推导精确的傅里叶积分公式
    • 得到比传统 DFT 更接近真实傅里叶变换的结果
    • 连续傅里叶变换 vs DFT 的本质差异
    • 用多项式插值重建信号
    • 得到改进的核函数(比 sinc 更复杂但更准确)
    • 为什么这篇论文今天仍然有价值?
  • 把这个新算法写成现代 Python 代码
    • 和论文的关系怎么理解?
  • 继续深入,做更多的误差分析
    • 边界误差
    • 插值误差
    • 数值算法实现
    • 从一段线性插值开始
    • 推出 的闭式表达
    • 把所有区间叠加,得到总频谱
    • 为啥 ω=0 要单独处理?
    • 和普通 FFT 的关系:零阶 vs 一阶
      • 普通 DFT 是什么数值积分?
      • 现在这个算法是什么?
    • 和论文思路的对应关系
    • 后记
  • 代码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档