
今天来一个有趣的论文:

是一个 1954 年就有的刊物

86年的文章
就感觉知识没有改变,几十年前都一样。
它不是讲 FFT,而是在推导“如何用有限长、离散采样的数据更精确地计算傅里叶积分”,尤其解决“傅里叶变换本质是连续积分,但我们只有有限 N 点样本”这个根本矛盾。
这是国内早期经典研究,解决的问题是: 如何用 DFT(离散傅里叶变换)更精确地逼近连续傅里叶变换(Fourier Integral),尤其当数据不是周期信号而是有限长截断样本。
论文主要分五部分:
传统 DFT 做的是:
把有限采样信号强行看成“周期信号”,然后做 Fourier 展开。
但真实世界信号不是周期的,所以结果会有误差。
论文做的是:
不强行周期,而是将采样点还原成一个近似的连续函数,再对这个近似函数做真正的傅里叶积分。
自然就更准确。
论文不是像传统 FFT 那样假定:
信号是周期的
而是把信号视为在有限区间上给定的样本 mΔt,并用多项式插值模拟真实信号。
傅里叶积分本来应该是:
但数据只有有限个点,论文提出:
其中 是推导出来的“加权核(类似改进的 sinc kernel)”。
传统 DFT 存在误差:假定信号周期化 → 会产生“泄漏”;使用矩形窗 → 主瓣宽、副瓣大;离散求和逼近连续积分不够精确。作者通过多项式插值推导出更精确的公式,减少泄漏与边界误差。
论文反复强调:傅里叶积分 = 连续时间信号, DFT = 有限离散样本构造的周期信号。因此 DFT 是对真正信号的污染,因为周期延拓导致频谱泄漏,有限窗口导致卷积,采样带来混叠
作者认为:要减少误差应把采样点当成“测得信号在该点的真实值”,再用插值构造信号的近似表达,最后再做积分。
论文用的是类似:
把采样点转化为连续函数,然后再积分:
积分变成对多项式的积分 → 可以得到闭式表达式。(这非常像数字信号处理里的 spline interpolation或者信号重建理论中的 Whittaker–Shannon sinc 插值)
论文给出的新核函数形式(例如公式 (10)–(15))是这样:
是修正过的 sinc 核。
→ 这比传统 DFT:
要更接近连续傅里叶变换。
因为它揭示了 FFT 频谱为什么会泄漏、边界怎么影响频率分辨率
问题 | 论文的解释 |
|---|---|
矩形窗为什么泄漏严重? | sinc 核副瓣大,卷积导致频谱泄漏 |
为什么窗函数能改善频谱? | 是在修改核函数,使其更接近真实傅里叶积分 |
为什么有限观测长度导致偏差? | 截断使核函数变化,积分误差变大 |
为什么插值(zero padding)能改善显示? | 改变了核函数的采样密度 |
FFT != 真·傅里叶变换任何有限数据做出的 FFT 都是“污染过的频谱”。
必须用窗函数因为矩形窗会严重扭曲核函数。
采样点不是“孤立点”,而是连续函数的样本 → 插值很重要。
DFT 可以写成:采样点 × 核函数的加权求和这为设计“最佳核函数”提供了数学框架。
。
把论文里“用插值逼近连续傅里叶积分”的思路,写成一个现代的数值算法。
思路: 把离散点 当成连续信号在 的采样 在每个采样区间用线性插值重建 对这个分段线性函数做解析积分,得到比普通 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 轴)
同一个信号,频率范围放宽到 0–300 Hz,用对数纵轴:
两条曲线在远离主峰处几乎重合,说明: 线性插值不会神奇地“消灭泄漏”,泄漏模式仍主要由“矩形窗”决定。但在主峰附近,线性插值的曲线稍微更平滑,旁瓣高度有微小差异(更接近理论连续谱)。
泄漏本质上是“有限观察时间 + 窗形”的问题,而不是积分精度本身;新算法主要是在同样窗形前提下,给你更准确的核积分,所以“主瓣 + 邻近旁瓣”区域改善更明显,远处副瓣形状基本一样。

边界误差 + 插值误差
信号换成:
多加了一个线性趋势,边界处不再平滑。
步骤:
误差误差
结果(log 纵轴):
在全频段内,橙色(Linear-interp)误差普遍比蓝色(FFT)低约 1–2 个数量级。
在主峰附近(约 50–70 Hz),差别特别明显:FFT 的误差可到 ,线性插值在 ~ 级别。
线性趋势使得时间窗两端的值不相等 → 周期延拓后,边界出现“大跳变”;普通 DFT 把这当成真实信号的一部分,频域里会多出一堆低频能量和畸变 → 误差显著。
线性插值 FT 相当于对每一段都按“真实斜率”去积分,在有限观察区间内更精确地拟合连续曲线 → 边界对频谱的污染更小。
普通 FFT 相当于对“阶梯函数(零阶保持)”积分;Linear-interp 对“线性段”积分,是高一阶的数值积分精度;所以即便没有边界问题,线性插值也会系统性降低数值积分误差,图里整个频带误差都更低就是这个原因。

Window 影响分析
这里我们看 4 条曲线(log 轴):
可以看出:
矩形窗 vs Hann 窗:
矩形窗:旁瓣在约 附近慢慢衰减;
Hann 窗:主瓣变宽,但旁瓣瞬间掉到 以下,然后非常快地衰减→ 典型的“主瓣变宽、副瓣下降”的窗函数 trade-off,在两种算法中都一致。
同一窗形里,新算法 vs FFT:
对于矩形窗:两条矩形窗曲线整体很接近,但线性插值的细节更平滑;对于 Hann 窗:两条 Hann 曲线几乎“扣在一起”,说明当窗函数已经把时间域信号变得相对光滑时,线性插值带来的改进主要是数值精度,不再根本改变泄漏形态。
窗的作用是“改变核函数本身”(决定泄漏结构) 线性插值 FT 是在既定核函数下做“更精确积分”两者是正交的;可以 “先用窗 + 再用改进积分算法”,效果叠加。
我先放使用论文核心实现的函数(其实数值算法才是我的真爱):
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
假设采样间隔是 ,采样点是 :时间轴:
在区间 上,用线性插值逼近真实信号:
其中
这一段对连续傅里叶变换的贡献是:
把变量换成 ,区间变成 :
把它拆成两部分:
所以只要把两个“核函数积分” 推出来,就搞定整个算法。
这正是代码里的:
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)
可以自己用 sympy 验一下,这个式子是对的。
整个时间区间 被分成 段,每一段的贡献是 ,所以总的傅里叶变换近似是:
对每个频点 ,这是一个对所有段的加和。翻译成 numpy 的广播就是:
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” 型,直接代公式会数值不稳定,所以要用极限:
时,
代进去可以算出:
而此时 ,所以:
整段积分就是所有 的和:
seg_int = a * dt + b * (dt**2 / 2.0)
X[mask_zero] = np.sum(seg_int)
这个正好等于对分段线性函数做 ,所以频率 0 点的值就是信号的时间积分,完全符合连续傅里叶变换定义 。
再对比一下我们熟悉的 DFT/FFT。
如果我们用最简单的“矩形法”近似傅里叶积分:
这恰好就是 DFT(除了一个 的比例因子):
从数值分析角度看:普通 FFT = 零阶保持(piecewise constant)+ 矩形积分,把每个采样点当“方块高度”,中间完全不管。
我们先做“分段线性插值”(一阶多项式),再对这个连续函数做解析积分;所以从数值积分阶数上来说,它是比矩形法高一阶的近似。
可以这么理解:
方法 | 插值模型 | 数值积分方法 |
|---|---|---|
FFT / DFT | 每段是常数(阶梯) | 矩形规则 |
线性插值 FT | 每段是一次函数 | 解析积分(相当于梯形+修正项) |
更高阶多项式 | 每段是二次/三次多项式 | 再复杂一点的解析积分 |
这刚好对应了论文里说的“用多项式逼近 + 推导新的 DFT 核”的思想,只不过我们选的是最简单的一阶情况。
论文大致是:把原始信号在有限区间上写成多项式展开(或者分段多项式插值);对这个多项式形式的 做傅里叶积分 ⇒ 得到新的“核函数”(类似我们的 );用这些核函数去加权采样值,就得到比传统 DFT 更精确的频谱估计。
我们现在具体做的是:
只取到了“一次多项式”(线性插值),所以核只有两个:;把它写成“对每段线性插值积分”的形式,便于用 numpy 向量化计算;
实际上你可以用同样套路继续往上推:
分段二次:要算 三种核
分段三次:要再多一个
这就是论文里“多项式阶数越高,理论上越接近连续傅里叶积分”的原因。
里面的数学知识就是高等数学和泛函分析+信号与系统和数值分析这些。我为什么研究这些东西,因为在硬件同质化的今天,软件才是定义一切的杀手锏,这才是真真正正的竞争力;这些算法之后会加入到我的信号与系统库中,大家敬请期待。
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
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()
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