首页 > 解决方案 > 如何找到信号周期(自相关与快速傅立叶变换与功率谱密度)?

问题描述

假设想找出给定正弦波信号的周期。从我在线阅读的内容来看,这两种主要方法似乎采用傅立叶分析或自相关。我正在尝试使用 python 自动化该过程,我的用例是将这个概念应用于来自模拟物体绕恒星运行的位置(或速度或加速度)的时间序列的类似信号。

对于简单示例,x = sin(t)请考虑0 ≤ t ≤ 10 pi.

import numpy as np
from scipy import signal
import matplotlib.pyplot as plt

## sample data
t = np.linspace(0, 10 * np.pi, 100)
x = np.sin(t)
fig, ax = plt.subplots()
ax.plot(t, x, color='b', marker='o')
ax.grid(color='k', alpha=0.3, linestyle=':')
plt.show()
plt.close(fig)

正弦波示例

给定一个形式x = a sin(b(t+c)) + d为 的正弦波,正弦波的周期为2 * pi / b。因为b=1(或通过目视检查),我们的正弦波的周期是2 * pi。我可以对照此基线检查从其他方法获得的结果。

尝试 1:自相关

据我了解(如果我错了,请纠正我),相关性可用于查看一个信号是否是另一个信号的时滞副本(类似于余弦和正弦如何因相位差而不同)。因此,自相关是针对自身测试信号,以测量时滞重复所述信号的时间。使用此处发布的示例

result = np.correlate(x, x, mode='full')

由于xand teach 由100元素组成,并且result199元素组成,我不确定为什么要随意选择最后一个100元素。

print("\n autocorrelation (shape={}):\n{}\n".format(result.shape, result))  

 autocorrelation (shape=(199,)):
[ 0.00000000e+00 -3.82130761e-16 -9.73648712e-02 -3.70014208e-01
 -8.59889695e-01 -1.56185995e+00 -2.41986054e+00 -3.33109112e+00
 -4.15799070e+00 -4.74662427e+00 -4.94918053e+00 -4.64762251e+00
 -3.77524157e+00 -2.33298717e+00 -3.97976240e-01  1.87752669e+00
  4.27722402e+00  6.54129270e+00  8.39434617e+00  9.57785701e+00
  9.88331103e+00  9.18204933e+00  7.44791758e+00  4.76948221e+00
  1.34963425e+00 -2.50822289e+00 -6.42666652e+00 -9.99116299e+00
 -1.27937834e+01 -1.44791297e+01 -1.47873668e+01 -1.35893098e+01
 -1.09091510e+01 -6.93157447e+00 -1.99159756e+00  3.45267493e+00
  8.86228186e+00  1.36707567e+01  1.73433176e+01  1.94357232e+01
  1.96463736e+01  1.78556800e+01  1.41478477e+01  8.81191526e+00
  2.32100171e+00 -4.70897483e+00 -1.15775811e+01 -1.75696560e+01
 -2.20296487e+01 -2.44327920e+01 -2.44454330e+01 -2.19677060e+01
 -1.71533510e+01 -1.04037163e+01 -2.33560966e+00  6.27458308e+00
  1.45655029e+01  2.16769872e+01  2.68391837e+01  2.94553896e+01
  2.91697473e+01  2.59122266e+01  1.99154591e+01  1.17007613e+01
  2.03381596e+00 -8.14633251e+00 -1.78184255e+01 -2.59814393e+01
 -3.17580589e+01 -3.44884934e+01 -3.38046447e+01 -2.96763956e+01
 -2.24244433e+01 -1.26974172e+01 -1.41464998e+00  1.03204331e+01
  2.13281784e+01  3.04712823e+01  3.67721634e+01  3.95170295e+01
  3.83356037e+01  3.32477037e+01  2.46710643e+01  1.33886439e+01
  4.77778141e-01 -1.27924775e+01 -2.50860560e+01 -3.51343866e+01
 -4.18671622e+01 -4.45258983e+01 -4.27482779e+01 -3.66140001e+01
 -2.66465884e+01 -1.37700036e+01  7.76494745e-01  1.55574483e+01
  2.90828312e+01  3.99582426e+01  4.70285203e+01  4.95000000e+01
  4.70285203e+01  3.99582426e+01  2.90828312e+01  1.55574483e+01
  7.76494745e-01 -1.37700036e+01 -2.66465884e+01 -3.66140001e+01
 -4.27482779e+01 -4.45258983e+01 -4.18671622e+01 -3.51343866e+01
 -2.50860560e+01 -1.27924775e+01  4.77778141e-01  1.33886439e+01
  2.46710643e+01  3.32477037e+01  3.83356037e+01  3.95170295e+01
  3.67721634e+01  3.04712823e+01  2.13281784e+01  1.03204331e+01
 -1.41464998e+00 -1.26974172e+01 -2.24244433e+01 -2.96763956e+01
 -3.38046447e+01 -3.44884934e+01 -3.17580589e+01 -2.59814393e+01
 -1.78184255e+01 -8.14633251e+00  2.03381596e+00  1.17007613e+01
  1.99154591e+01  2.59122266e+01  2.91697473e+01  2.94553896e+01
  2.68391837e+01  2.16769872e+01  1.45655029e+01  6.27458308e+00
 -2.33560966e+00 -1.04037163e+01 -1.71533510e+01 -2.19677060e+01
 -2.44454330e+01 -2.44327920e+01 -2.20296487e+01 -1.75696560e+01
 -1.15775811e+01 -4.70897483e+00  2.32100171e+00  8.81191526e+00
  1.41478477e+01  1.78556800e+01  1.96463736e+01  1.94357232e+01
  1.73433176e+01  1.36707567e+01  8.86228186e+00  3.45267493e+00
 -1.99159756e+00 -6.93157447e+00 -1.09091510e+01 -1.35893098e+01
 -1.47873668e+01 -1.44791297e+01 -1.27937834e+01 -9.99116299e+00
 -6.42666652e+00 -2.50822289e+00  1.34963425e+00  4.76948221e+00
  7.44791758e+00  9.18204933e+00  9.88331103e+00  9.57785701e+00
  8.39434617e+00  6.54129270e+00  4.27722402e+00  1.87752669e+00
 -3.97976240e-01 -2.33298717e+00 -3.77524157e+00 -4.64762251e+00
 -4.94918053e+00 -4.74662427e+00 -4.15799070e+00 -3.33109112e+00
 -2.41986054e+00 -1.56185995e+00 -8.59889695e-01 -3.70014208e-01
 -9.73648712e-02 -3.82130761e-16  0.00000000e+00]

尝试 2:傅立叶

由于我不确定从上次尝试到哪里去,我寻求新的尝试。据我了解,傅立叶分析基本上将信号从/到时域(x(t) vs t)转换到/从频域(x(t) vs f=1/t);频率空间中的信号应显示为随时间衰减的正弦波。该周期是从最观察到的频率中获得的,因为这是频率分布的峰值位置。

由于我的值都是实值,应用傅立叶变换应该意味着我的输出值都是复值。我不认为这是一个问题,除了scipy 有 real-values 的方法。我不完全理解所有不同 scipy 方法之间的区别。这使得我很难遵循这个发布的解决方案中提出的算法(即,如何/为什么选择阈值?)。

omega = np.fft.fft(x)
freq = np.fft.fftfreq(x.size, 1)
threshold = 0
idx = np.where(abs(omega)>threshold)[0][-1]
max_f = abs(freq[idx])
print(max_f)

这个输出0.01,意思是周期1/0.01 = 100。这也没有道理。

尝试 3:功率谱密度

根据scipy 文档,我应该能够使用周期图估计信号的功率谱密度(psd)(根据维基百科,它是自相关函数的傅立叶变换)。通过选择fmax信号达到峰值的主频率,可以得到信号的周期为1 / fmax

freq, pdensity = signal.periodogram(x)

fig, ax = plt.subplots()
ax.plot(freq, pdensity, color='r')
ax.grid(color='k', alpha=0.3, linestyle=':')
plt.show()
plt.close(fig)

49.076...下面显示的周期图在频率为时达到峰值fmax = 0.05。所以,period = 1/fmax = 20。这对我来说没有意义。我感觉它与采样率有关,但不知道足以确认或进一步进展。

我意识到我在理解这些事情是如何工作的方面缺少一些基本的空白。网上资源很多,但大海捞针难。有人可以帮助我了解更多信息吗?

周期图

标签: pythontime-seriessignalsfftwaveform

解决方案


让我们首先看一下您的信号(我添加endpoint=False了以使除法均匀):

t = np.linspace(0, 10*np.pi, 100, endpoint=False)
x = np.sin(t)

让我们划分弧度(主要是通过取t /= 2*np.pi)并通过与频率相关来创建相同的信号:

fs = 20 # Sampling rate of 100/5 = 20 (e.g. Hz)
f = 1 # Signal frequency of 1 (e.g. Hz)
t = np.linspace(0, 5, 5*fs, endpoint=False)
x = np.sin(2*np.pi*f*t)

这使得它更加突出f/fs == 1/20 == 0.05(即信号的周期性正好是 20 个样本)。正如您已经猜到的那样,数字信号的频率总是与其采样率有关。请注意,无论 和 的值是多少,实际信号都是完全相同的ffs只要它们的比率相同即可:

fs = 1 # Natural units
f = 0.05
t = np.linspace(0, 100, 100*fs, endpoint=False)
x = np.sin(2*np.pi*f*t)

下面我将使用这些自然单位 ( fs = 1)。唯一的区别在于t生成的频率轴。

自相关

您对自相关函数的作用的理解是正确的。它检测信号与其自身滞后版本的相关性。它通过将信号滑过自身来实现这一点,如右列所示(来自Wikipedia):

自相关

请注意,由于相关函数的两个输入相同,因此生成的信号必然是对称的。这就是为什么np.correlate通常从中间切出的输出:

acf = np.correlate(x, x, 'full')[-len(x):]

现在索引 0 对应于信号的两个副本之间的 0 延迟。

接下来,您将要找到呈现最大相关性的索引或延迟。由于重叠缩小,默认情况下这也是索引 0,因此以下内容将不起作用:

acf.argmax() # Always returns 0

相反,我建议找到最大的峰值,其中峰值被定义为任何值大于其直接邻居的索引:

inflection = np.diff(np.sign(np.diff(acf))) # Find the second-order differences
peaks = (inflection < 0).nonzero()[0] + 1 # Find where they are negative
delay = peaks[acf[peaks].argmax()] # Of those, find the index with the maximum value

现在delay == 20,它告诉你信号的频率是1/20它的采样率:

signal_freq = fs/delay # Gives 0.05

傅里叶变换

您使用以下方法计算 FFT:

omega = np.fft.fft(x)
freq = np.fft.fftfreq(x.size, 1)

这些函数重新设计用于复值信号。它们适用于实值信号,但您将获得对称输出,因为负频率分量与正频率分量相同。NumPy 为实值信号提供单独的函数:

ft = np.fft.rfft(x)
freqs = np.fft.rfftfreq(len(x), t[1]-t[0]) # Get frequency axis from the time axis
mags = abs(ft) # We don't care about the phase information here

我们来看一下:

plt.plot(freqs, mags)
plt.show()

快速傅里叶变换

注意两点:峰值在频率 0.05 处,轴上的最大频率为 0.5(奈奎斯特频率,恰好是采样率的一半)。如果我们选择fs = 20,这将是 10。

现在让我们找到最大值。您尝试过的阈值方法可以工作,但是目标频率仓是盲目选择的,因此这种方法会在存在其他信号的情况下受到影响。我们可以选择最大值:

signal_freq = freqs[mags.argmax()] # Gives 0.05

但是,如果,例如,我们有一个大的 DC 偏移量(因此索引 0 中的一个大分量),这将失败。在这种情况下,我们可以再次选择最高峰,使其更加稳健:

inflection = np.diff(np.sign(np.diff(mags)))
peaks = (inflection < 0).nonzero()[0] + 1
peak = peaks[mags[peaks].argmax()]
signal_freq = freqs[peak] # Gives 0.05

如果我们选择了fs = 20,这将是signal_freq == 1.0由于生成频率轴的时间轴不同。

周期图

这里的方法基本相同。的自相关函数与x具有相同的时间轴和周期x,因此我们可以使用上面的 FFT 来找到信号频率:

pdg = np.fft.rfft(acf)
freqs = np.fft.rfftfreq(len(x), t[1]-t[0])

plt.plot(freqs, abs(pdg))
plt.show()

周期图

这条曲线显然与直接 FFT 上的特性略有不同x,但主要内容是相同的:频率轴范围从00.5*fs,我们在与之前相同的信号频率处发现了一个峰值:freqs[abs(pdg).argmax()] == 0.05

编辑:

要测量 的实际周期性np.sin,我们可以np.sin在生成频率轴时使用我们传递给的“角度轴”而不是时间轴:

freqs = np.fft.rfftfreq(len(x), 2*np.pi*f*(t[1]-t[0]))
rad_period = 1/freqs[mags.argmax()] # 6.283185307179586

虽然这看起来毫无意义,对吧?我们通过2*np.pi,我们得到2*np.pi。但是,我们可以对任何常规时间轴做同样的事情,而无需pi在任何时候预先假设:

fs = 10
t = np.arange(1000)/fs
x = np.sin(t)
rad_period = 1/np.fft.rfftfreq(len(x), 1/fs)[abs(np.fft.rfft(x)).argmax()] # 6.25

自然,现在真正的价值在于两个箱子之间。这就是插值的用武之地,并且需要选择合适的窗口函数。


推荐阅读