基于数据集大小的 FFT 偏移频率?
Frequencies from a FFT shift based on size of data set?
我正在努力从给定的数据集中查找频率,并且我正在努力了解 np.fft.fft()
的工作原理。我以为我有一个工作脚本,但 运行 遇到了一个我无法理解的奇怪问题。
我有一个大致为正弦曲线的数据集,我想了解信号由哪些频率组成。一旦我进行了 FFT,我得到了这个图:
然而,当我采用相同的数据集,将其切成两半并绘制相同的东西时,我得到了:
我不明白为什么频率从 144kHz 下降到 128kHz,这在技术上应该是相同的数据集,但长度更小。
我可以确认几件事:
- 数据点之间的步长 0.001
- 我尝试过插值,但运气不佳。
- 如果我对数据集的后半部分进行切片,我也会得到不同的频率。
- 如果我的数据集确实由 128 和 144kHz 组成,那么为什么第一个图中没有出现 128 峰值?
更令人困惑的是,我运行一个没有问题的纯正弦波脚本:
T = 0.001
fs = 1 / T
def find_nearest_ind(data, value):
return (np.abs(data - value)).argmin()
x = np.arange(0, 30, T)
ff = 0.2
y = np.sin(2 * ff * np.pi * x)
x = x[:len(x) // 2]
y = y[:len(y) // 2]
n = len(y) # length of the signal
k = np.arange(n)
T = n / fs
frq = k / T * 1e6 / 1000 # two sides frequency range
frq = frq[:len(frq) // 2] # one side frequency range
Y = np.fft.fft(y) / n # dft and normalization
Y = Y[:n // 2]
frq = frq[:50]
Y = Y[:50]
fig, (ax1, ax2) = plt.subplots(2)
ax1.plot(x, y)
ax1.set_xlabel("Time (us)")
ax1.set_ylabel("Electric Field (V / mm)")
peak_ind = find_nearest_ind(abs(Y), np.max(abs(Y)))
ax2.plot(frq, abs(Y))
ax2.axvline(frq[peak_ind], color = 'black', linestyle = '--', label = F"Frequency = {round(frq[peak_ind], 3)}kHz")
plt.legend()
plt.xlabel('Freq(kHz)')
ax1.title.set_text('dV/dX vs. Time')
ax2.title.set_text('Frequencies')
fig.tight_layout()
plt.show()
这是您的代码的细分,以及一些改进建议和额外的解释。仔细研究它会告诉你发生了什么。您得到的结果是完全可以预料的。我会在最后提出一个通用的解决方案。
首先正确设置你的单位。我假设您正在处理秒,而不是微秒。只要保持一致,您可以稍后进行调整。
确定采样的周期和频率。这意味着 FFT 的奈奎斯特频率将为 500Hz:
T = 0.001 # 1ms sampling period
fs = 1 / T # 1kHz sampling frequency
做一个30e3点的时域。半域将包含 15000 点。这意味着频率分辨率为 500Hz / 15k = 0.03333Hz。
x = np.arange(0, 30, T) # time domain
n = x.size # number of points: 30000
在做任何事情之前,我们可以在这里定义我们的时域。我更喜欢比您使用的方法更直观的方法。这样您就不必重新定义 T
或引入辅助变量 k
。不过只要结果一样就无所谓了:
F = np.linspace(0, 1 - 1/n, n) / T # Notice F[1] = 0.03333, as predicted
现在定义信号。您选择了 ff = 0.2
。注意 0.2Hz。 0.2 / 0.03333 = 6,因此您会期望在 bin 索引 6 (F[6] == 0.2
) 中看到峰值。为了更好地说明发生了什么,让我们以 ff = 0.22
为例。这会将频谱渗入相邻的频段。
ff = 0.22
y = np.sin(2 * np.pi * ff * x)
现在进行 FFT:
Y = np.fft.fft(y) / n
maxbin = np.abs(Y).argmax() # 7
maxF = F[maxbin] # 0.23333333: This is the nearest bin
由于您的频率区间为 0.03Hz 宽,因此您可以期望的最佳分辨率为 0.015Hz。对于分辨率低得多的真实数据,误差要大得多。
现在让我们看看将数据大小减半时会发生什么。其中,频率分辨率变得更小。现在你有 500Hz 的最大频率分布在 7.5k 个样本上,而不是 15k:分辨率下降到每个 bin 0.066666Hz:
n2 = n // 2 # 15000
F2 = np.linspace(0, 1 - 1 / n2, n2) / T # F[1] = 0.06666
Y2 = np.fft.fft(y[:n2]) / n2
看看频率估计发生了什么:
maxbin2 = np.abs(Y2).argmax() # 3
maxF2 = F2[maxbin2] # 0.2: This is the nearest bin
希望您能看到这如何适用于您的原始数据。在完整的 FFT 中,完整数据的分辨率为每箱 ~16.1,半数据的分辨率为 ~32.2kHz。因此,您的原始结果在右峰值的~±8kHz 范围内,而第二个结果在~±16kHz 范围内。因此,真实频率介于 136kHz 和 144kHz 之间。另一种看待它的方法是比较你给我看的箱子:
满:128.7 144.8 160.9
一半:96.6 128.7 160.9
当你取出恰好一半的数据时,你每隔一个频率仓就会丢弃一次。如果您的峰值最初最接近 144.8kHz,并且您放弃了那个 bin,它最终将在 128.7 或 160.9。
注意: 根据您显示的 bin 编号,我怀疑您对 frq
的计算有一点偏差。请注意我的 linspace
表达式中的 1 - 1/n
。您需要它来获得正确的频率轴:最后一个 bin 是 (1 - 1/n) / T
,而不是 1 / T
,无论您如何计算它。
那么如何解决这个问题呢?最简单的解决方案是对峰值周围的三个点进行抛物线拟合。当您寻找基本完美的正弦曲线时,这通常是对数据中真实频率的足够好的估计。
def peakF(F, Y):
index = np.abs(Y).argmax()
# Compute offset on normalized domain [-1, 0, 1], not F[index-1:index+2]
y = np.abs(Y[index - 1:index + 2])
# This is the offset from zero, which is the scaled offset from F[index]
vertex = (y[0] - y[2]) / (0.5 * (y[0] + y[2]) - y[1])
# F[1] is the bin resolution
return F[index] + vertex * F[1]
如果您想知道我是如何得到抛物线公式的:我用 x = [-1, 0, 1]
和 y = Y[index - 1:index + 2]
求解了系统。矩阵方程为
[(-1)^2 -1 1] [a] Y[index - 1]
[ 0^2 0 1] @ [b] = Y[index]
[ 1^2 1 1] [c] Y[index + 1]
使用归一化域计算偏移量并在之后进行缩放几乎总是比使用 F[index - 1:index + 2]
中的任何大数字在数值上更稳定。
您可以将示例中的结果插入此函数中,看看它是否有效:
>>> peakF(F, Y)
0.2261613409657391
>>> peakF(F2, Y2)
0.20401580936430794
如您所见,抛物线拟合带来了改善,但幅度很小。但是,通过更多样本来增加频率分辨率是无可替代的!
我正在努力从给定的数据集中查找频率,并且我正在努力了解 np.fft.fft()
的工作原理。我以为我有一个工作脚本,但 运行 遇到了一个我无法理解的奇怪问题。
我有一个大致为正弦曲线的数据集,我想了解信号由哪些频率组成。一旦我进行了 FFT,我得到了这个图:
然而,当我采用相同的数据集,将其切成两半并绘制相同的东西时,我得到了:
我不明白为什么频率从 144kHz 下降到 128kHz,这在技术上应该是相同的数据集,但长度更小。
我可以确认几件事:
- 数据点之间的步长 0.001
- 我尝试过插值,但运气不佳。
- 如果我对数据集的后半部分进行切片,我也会得到不同的频率。
- 如果我的数据集确实由 128 和 144kHz 组成,那么为什么第一个图中没有出现 128 峰值?
更令人困惑的是,我运行一个没有问题的纯正弦波脚本:
T = 0.001
fs = 1 / T
def find_nearest_ind(data, value):
return (np.abs(data - value)).argmin()
x = np.arange(0, 30, T)
ff = 0.2
y = np.sin(2 * ff * np.pi * x)
x = x[:len(x) // 2]
y = y[:len(y) // 2]
n = len(y) # length of the signal
k = np.arange(n)
T = n / fs
frq = k / T * 1e6 / 1000 # two sides frequency range
frq = frq[:len(frq) // 2] # one side frequency range
Y = np.fft.fft(y) / n # dft and normalization
Y = Y[:n // 2]
frq = frq[:50]
Y = Y[:50]
fig, (ax1, ax2) = plt.subplots(2)
ax1.plot(x, y)
ax1.set_xlabel("Time (us)")
ax1.set_ylabel("Electric Field (V / mm)")
peak_ind = find_nearest_ind(abs(Y), np.max(abs(Y)))
ax2.plot(frq, abs(Y))
ax2.axvline(frq[peak_ind], color = 'black', linestyle = '--', label = F"Frequency = {round(frq[peak_ind], 3)}kHz")
plt.legend()
plt.xlabel('Freq(kHz)')
ax1.title.set_text('dV/dX vs. Time')
ax2.title.set_text('Frequencies')
fig.tight_layout()
plt.show()
这是您的代码的细分,以及一些改进建议和额外的解释。仔细研究它会告诉你发生了什么。您得到的结果是完全可以预料的。我会在最后提出一个通用的解决方案。
首先正确设置你的单位。我假设您正在处理秒,而不是微秒。只要保持一致,您可以稍后进行调整。
确定采样的周期和频率。这意味着 FFT 的奈奎斯特频率将为 500Hz:
T = 0.001 # 1ms sampling period
fs = 1 / T # 1kHz sampling frequency
做一个30e3点的时域。半域将包含 15000 点。这意味着频率分辨率为 500Hz / 15k = 0.03333Hz。
x = np.arange(0, 30, T) # time domain
n = x.size # number of points: 30000
在做任何事情之前,我们可以在这里定义我们的时域。我更喜欢比您使用的方法更直观的方法。这样您就不必重新定义 T
或引入辅助变量 k
。不过只要结果一样就无所谓了:
F = np.linspace(0, 1 - 1/n, n) / T # Notice F[1] = 0.03333, as predicted
现在定义信号。您选择了 ff = 0.2
。注意 0.2Hz。 0.2 / 0.03333 = 6,因此您会期望在 bin 索引 6 (F[6] == 0.2
) 中看到峰值。为了更好地说明发生了什么,让我们以 ff = 0.22
为例。这会将频谱渗入相邻的频段。
ff = 0.22
y = np.sin(2 * np.pi * ff * x)
现在进行 FFT:
Y = np.fft.fft(y) / n
maxbin = np.abs(Y).argmax() # 7
maxF = F[maxbin] # 0.23333333: This is the nearest bin
由于您的频率区间为 0.03Hz 宽,因此您可以期望的最佳分辨率为 0.015Hz。对于分辨率低得多的真实数据,误差要大得多。
现在让我们看看将数据大小减半时会发生什么。其中,频率分辨率变得更小。现在你有 500Hz 的最大频率分布在 7.5k 个样本上,而不是 15k:分辨率下降到每个 bin 0.066666Hz:
n2 = n // 2 # 15000
F2 = np.linspace(0, 1 - 1 / n2, n2) / T # F[1] = 0.06666
Y2 = np.fft.fft(y[:n2]) / n2
看看频率估计发生了什么:
maxbin2 = np.abs(Y2).argmax() # 3
maxF2 = F2[maxbin2] # 0.2: This is the nearest bin
希望您能看到这如何适用于您的原始数据。在完整的 FFT 中,完整数据的分辨率为每箱 ~16.1,半数据的分辨率为 ~32.2kHz。因此,您的原始结果在右峰值的~±8kHz 范围内,而第二个结果在~±16kHz 范围内。因此,真实频率介于 136kHz 和 144kHz 之间。另一种看待它的方法是比较你给我看的箱子:
满:128.7 144.8 160.9 一半:96.6 128.7 160.9
当你取出恰好一半的数据时,你每隔一个频率仓就会丢弃一次。如果您的峰值最初最接近 144.8kHz,并且您放弃了那个 bin,它最终将在 128.7 或 160.9。
注意: 根据您显示的 bin 编号,我怀疑您对 frq
的计算有一点偏差。请注意我的 linspace
表达式中的 1 - 1/n
。您需要它来获得正确的频率轴:最后一个 bin 是 (1 - 1/n) / T
,而不是 1 / T
,无论您如何计算它。
那么如何解决这个问题呢?最简单的解决方案是对峰值周围的三个点进行抛物线拟合。当您寻找基本完美的正弦曲线时,这通常是对数据中真实频率的足够好的估计。
def peakF(F, Y):
index = np.abs(Y).argmax()
# Compute offset on normalized domain [-1, 0, 1], not F[index-1:index+2]
y = np.abs(Y[index - 1:index + 2])
# This is the offset from zero, which is the scaled offset from F[index]
vertex = (y[0] - y[2]) / (0.5 * (y[0] + y[2]) - y[1])
# F[1] is the bin resolution
return F[index] + vertex * F[1]
如果您想知道我是如何得到抛物线公式的:我用 x = [-1, 0, 1]
和 y = Y[index - 1:index + 2]
求解了系统。矩阵方程为
[(-1)^2 -1 1] [a] Y[index - 1]
[ 0^2 0 1] @ [b] = Y[index]
[ 1^2 1 1] [c] Y[index + 1]
使用归一化域计算偏移量并在之后进行缩放几乎总是比使用 F[index - 1:index + 2]
中的任何大数字在数值上更稳定。
您可以将示例中的结果插入此函数中,看看它是否有效:
>>> peakF(F, Y)
0.2261613409657391
>>> peakF(F2, Y2)
0.20401580936430794
如您所见,抛物线拟合带来了改善,但幅度很小。但是,通过更多样本来增加频率分辨率是无可替代的!