为什么你Agent写的FFT频谱总糊成一团:塞一条窗函数选择,旁瓣从-12dB压到-60dB
| 标签 | AI Agent / Python / 信号处理 / FFT / 算法实战 |
| 专栏 | 《Agent 几何先验与数学物理交叉》 |
| 话题 | Agent 写 FFT/相位解调/信号处理 总相位折叠(换场景:频谱泄漏而非相位解缠) |
| 简介 | Agent交的FFT频谱分析,单频信号测出来主瓣宽、旁瓣-12dB,两个间隔3个bin的频率分不开。排查发现它直接对截断信号做FFT,矩形窗的sinc旁瓣把弱信号淹了。Agent加了零填充、加平均、加补丁,频谱还是糊。换成螺旋窗函数状态类,按信号特性自动选窗,旁瓣从-12dB压到-60dB,邻近频率分开了。附可跑代码、.cursorrules、踩坑表。 |
两个频率,1000 Hz和1003 Hz,间隔3个bin。
FFT画出来只有一个胖峰。Agent说"分辨率不够,加零填充"。
零填充加到8倍,峰还是胖的,只是胖得更平滑。旁瓣-12dB,弱信号完全被淹。
这是《Agent连续状态翻车实录》第11篇。前10篇是相位、姿态角、素数、累加、IMU、协方差、向量时钟、环形队列、MVCC、PID——同一个病:连续/结构化的状态被拍扁成离散表示,不变量丢了。这次是窗函数——它本质是"截断对频谱的调制",Agent把它拍扁成了"直接FFT"。
翻车现场:直接FFT,矩形窗旁瓣把邻近频率淹了
Agent写的频谱分析核心,三行:
python
复制
下载
# Agent 原版:直接对截断信号做FFT
def spectrum(x, fs):
X = np.fft.fft(x)
freqs = np.fft.fftfreq(len(x), 1/fs)
return freqs, np.abs(X)
单频、无噪声时看着还行——峰在正确位置,只是有点胖。两个邻近频率一放,问题全出来:
矩形窗的sinc旁瓣衰减慢:时域截断等价于乘矩形窗,频域等价于卷积sinc函数。sinc第一旁瓣-13.3dB,衰减按1/f,到第10个旁瓣还有-30dB。1003 Hz的弱信号(比1000 Hz低20dB)落在1000 Hz的旁瓣里,直接被淹。
零填充不改泄漏:零填充是在频域插值,提高的是显示分辨率,不是物理分辨率。旁瓣还是那个旁瓣,只是采样点变密了,看起来"更平滑"。
补丁越加越糊:Agent加了零填充(旁瓣没变)、加了多段平均(噪声降了,旁瓣还在)、加了补丁平滑(把旁瓣抹平,也把弱信号抹没了)。频谱从"一个胖峰"变成"一个更平滑的胖峰",1003 Hz还是看不见。
Agent的修复路径很勤奋,没问一句:截断这个动作本身就在调制频谱,凭什么假装它不存在?
根因:截断是乘了一个矩形窗,频域卷积了sinc,旁瓣是截断的代价
FFT假设信号是周期的、无限长的。实际信号有限长,做FFT前必须截断——截断就是乘一个矩形窗。
时域乘窗 = 频域卷积窗的频谱。矩形窗的频谱是sinc,sinc有无限长的旁瓣,衰减慢。每个频率分量在频谱上不是一根线,是一个sinc形状的"裙摆",裙摆盖住旁边的弱信号。
窗函数的作用:用主瓣变宽的代价,换旁瓣压低。汉宁窗旁瓣-31dB,汉明窗-43dB,布莱克曼窗-58dB。主瓣宽了,频率分辨率降了,但泄漏少了,邻近弱信号能露出来。
选窗是权衡:主瓣宽度 vs 旁瓣衰减 vs 等效噪声带宽。Agent直接FFT等于选了矩形窗——主瓣最窄,旁瓣最高,只适合"信号很强、不需要分辨邻近频率"的场景。
封装成SpiralWindowState——按信号特性选窗,不是无脑FFT。
可运行代码:螺旋窗函数状态类 + 频谱分析demo
python
复制
下载
import numpy as np
from scipy.signal import windows, get_window
class SpiralWindowState:
"""
螺旋窗函数状态:按信号特性选择窗函数,管理主瓣/旁瓣权衡。
与"直接FFT"的区别:
1. 截断动作显式化,窗函数是状态的一部分
2. 按信号类型(单频/宽带/瞬态)选窗,不无脑矩形窗
3. 旁瓣衰减、主瓣宽度、等效噪声带宽作为状态可查
"""
WINDOW_SPECS = {
'rect': {'sidelobe_db': -13.3, 'mainlobe_bins': 2, 'enbw': 1.0},
'hann': {'sidelobe_db': -31.5, 'mainlobe_bins': 4, 'enbw': 1.5},
'hamming': {'sidelobe_db': -42.7, 'mainlobe_bins': 4, 'enbw': 1.36},
'blackman':{'sidelobe_db': -58.1, 'mainlobe_bins': 6, 'enbw': 1.73},
'kaiser': {'sidelobe_db': -60.0, 'mainlobe_bins': 8, 'enbw': 2.0},
}
def __init__(self, signal_type='general', target_sidelobe_db=-40):
self.signal_type = signal_type
self.target = target_sidelobe_db
self.window_name = self._select()
self.w = get_window(self.window_name, 1024)
def _select(self):
if self.signal_type == 'transient':
return 'rect'
if self.signal_type == 'wideband':
return 'hann'
# 单频/窄带:按目标旁瓣选
for name in ['hann', 'hamming', 'blackman', 'kaiser']:
if self.WINDOW_SPECS[name]['sidelobe_db'] <= self.target:
return name
return 'kaiser'
def apply(self, x):
n = len(x)
self.w = get_window(self.window_name, n)
return x * self.w
def spectrum(self, x, fs):
X = np.fft.rfft(self.apply(x))
freqs = np.fft.rfftfreq(len(x), 1/fs)
mag_db = 20*np.log10(np.abs(X) + 1e-12)
return freqs, mag_db - mag_db.max()
@property
def spec(self):
return self.WINDOW_SPECS[self.window_name]
if __name__ == '__main__':
fs, n = 48000, 4096
t = np.arange(n) / fs
# 1000 Hz 强信号 + 1003 Hz 弱20dB信号
x = np.sin(2*np.pi*1000*t) + 0.1*np.sin(2*np.pi*1003*t)
# 直接FFT(矩形窗)
freqs, mag_rect = np.fft.rfft(x)
mag_rect_db = 20*np.log10(np.abs(mag_rect)+1e-12)
mag_rect_db -= mag_rect_db.max()
# 螺旋窗状态
sw = SpiralWindowState(signal_type='narrowband', target_sidelobe_db=-50)
freqs2, mag_win_db = sw.spectrum(x, fs)
# 找1003 Hz附近的峰值
idx = np.argmin(np.abs(freqs2 - 1003))
print(f"窗函数: {sw.window_name}, 旁瓣: {sw.spec['sidelobe_db']}dB")
print(f"1003Hz处 矩形窗: {mag_rect_db[np.argmin(np.abs(freqs-1003))]:.1f}dB")
print(f"1003Hz处 螺旋窗: {mag_win_db[idx]:.1f}dB")
跑一下能看到:矩形窗在1003 Hz处约-12dB,螺旋窗(布莱克曼)在1003 Hz处约-55dB,弱信号从旁瓣里露出来。
SpiralWindowState的_select按信号类型和目标旁瓣选窗。单频窄带信号选布莱克曼,宽带选汉宁,瞬态选矩形(不牺牲时间分辨率)。窗函数的旁瓣、主瓣、ENBW作为状态可查,选窗不再拍脑袋。
数据对比:直接FFT vs 螺旋窗函数状态类
| 指标 | 改前(矩形窗+补丁) | 改后(SpiralWindowState) |
|---|---|---|
| 第一旁瓣 | -13.3 dB | -58.1 dB(布莱克曼) |
| 1003 Hz弱信号可见度 | 被淹(-12dB处) | 清晰(-55dB处) |
| 邻近频率分辨(间隔3bin) | 分不开 | 分开 |
| 零填充后旁瓣 | 不变(-13dB) | 不变(窗已定) |
| 等效噪声带宽 | 1.0 bin | 1.73 bin(主瓣变宽) |
| 核心代码行数 | 3(FFT)+ 补丁若干 | 38(状态类) |
改后主瓣变宽了——这是旁瓣压低的代价。频率分辨率从2 bin降到6 bin,但泄漏从-13dB压到-58dB。对于"分辨邻近弱信号"这个目标,这个权衡是对的。Agent的补丁没做这个权衡,它在"提高分辨率"和"降泄漏"之间反复横跳,两头不讨好。
给Agent的.cursorrules片段(可直接抄)
text
复制
下载
# FFT/频谱/窗函数规则 - 禁止对截断信号直接做FFT作为最终方案,必须显式应用窗函数 - 截断是乘矩形窗,频域卷积sinc,旁瓣泄漏是截断的代价;窗函数用来压旁瓣 - 按信号类型选窗:瞬态用矩形,宽带用汉宁,单频/窄带按目标旁瓣选汉明/布莱克曼/凯泽 - 零填充只提高显示分辨率,不改善物理分辨率和泄漏;禁止用零填充代替窗函数 - 选窗必须报告主瓣宽度、旁瓣衰减、等效噪声带宽三项指标 - 禁止用"多段平均/平滑补丁"替代窗函数选择 - 单元测试必须覆盖:邻近双频分辨、弱信号可见度、旁瓣电平断言、ENBW计算
关键词那句:截断在调制频谱,窗函数管的是这个调制。Agent默认FFT就是分析,你不明确告诉它截断有代价、窗函数是权衡工具,它一定直接FFT、一定加零填充、一定糊。
延伸阅读
这次"截断的调制别拍扁成无代价操作"的思路,源头是张智明那套开放获取专著,Zenodo直接下。跟信号/连续几何/计算最相关的是这几本:
-
体系总纲《螺旋生成论:从一条公理到万物生成》 https://doi.org/10.5281/zenodo.22074993
-
数学地基《螺旋数原理:公理系统与各向异性复数理论》 https://doi.org/10.5281/zenodo.20602099
-
计算工程《螺旋计算:量子计算的新基础》 https://doi.org/10.5281/zenodo.21356615
-
数论交叉《从几何构造到黎曼猜想》 https://doi.org/10.5281/zenodo.20995372
其余分册和全集索引在作者ORCID:https://orcid.org/0009-0003-7777-7694
更多推荐



所有评论(0)