创建频率向量
fft() 输出的排列取决于您的 fft 是使用奇数点还是偶数点。我认为这篇文章很好地总结了频率的排列方式。看看它。
- 由于您使用的是偶数个点,因此Nyquist频率$F_N = F_s/2$出现在 fft 的输出中,并且是频率向量中的最大值。您的采样频率为$N/T=100/10=10\;\textrm{Hz}$,因此向量中的最大可检测频率将为$F_N=5\;\textrm{Hz}$。
- 其次,无论您使用偶数点还是奇数点,DC 值始终是 fft 输出中的第一个条目。
- 频率分辨率$df = F_s/N$是频率空间中 fft 输出箱之间的间隔。对你来说,这将是$0.1\;\textrm{Hz}$。
- 最后,以对称、居中的方式显示频谱也是最常见的,DC 频率位于中间 - 函数 fftshift 会为您执行此操作。
考虑到所有这些,与您的 fft 输出相对应的频率向量(在您应用 fftshift() 使其居中之后)将是
$$ f = [-F_N : df : (F_N-df)] $$
有一些讨论关于如果您有兴趣,请在此线程中使用fftshift 。请注意,在 fftshift() 之后,对应于 Nyquist 的分量$F_N$被放置在向量的开头,因此从 fftshift() 的角度来看,它被认为是负频率 - 末尾没有 Nyquist正频率中的矢量(然后相反,为了对称,直流分量被视为正频率)。
用正确的比例绘制频谱
就个人而言,我更喜欢根据波的功率(能量变化率)而不是 fft bin 中组件的幅度来处理这些事情。这只是帮助我跟踪单位,并保持物理状态。以下代码使用简单的 周期图估计为您的信号计算功率谱密度$S_{xx}$(我假设您的信号以伏特为单位 - 可能是也可能不是真的 - 但可以是任何东西:温度,股票价格,等等)。抱歉,我的代码是在 Matlab 中,而不是 Python,但希望您可以毫无问题地按照这些步骤操作。
N = 100; % Number of samples
T = 10; % Record window duration
dt = T/N; % Sampling period
Fs = 1/dt; % Sampling frequency
t = 0:dt:(T-dt); % Time vector for sampling
% generate samples at the specified times
x = 5*sin(2*pi*3*t - 2); % units: [V]
df = Fs/N; % frequency resolution (bin width in frequency space)
% generate frequency vector for 2-sided spectrum (NOTE, this arrangement
% only works for even number of points - otherwise, use f = -(Fs/2-df/2):df:(Fs/2-df/2))
f = -(Fs/2):df:(Fs/2-df);
% Calculate Fourier transform (approximating CFT), and shift DC term to centre
X = fftshift(fft(x))*dt; % units: [V sec]
X((abs(X)<1e-10)) = 0; % kill values below threshold, so phase is well-behaved
% Calculate power spectral density using periodogram estimation
Sxx = (X.*conj(X))/(N*dt); % units: [V^2 / Hz]
figure; stem(f,Sxx) % Plot power spectral density
figure; stem(f,angle(X)) % Plot phase
代码中的变量$X$是连续傅里叶变换积分的离散近似值,这就是为什么它乘以采样周期$dt$(近似值是黎曼和)。然后可以由此计算周期图 - 再次,在其连续对应物的离散版本中,我认为在这个答案中很好地解释了这一点。注意,Matlab 也有一个内置函数 periodogram()
,它也可以进行计算。
您可以看到在$\pm 3\;\textrm{Hz}$处有正确的峰值,与您的输入信号完全匹配(请记住,正弦波实际上是复指数的总和)。
-- 请注意,在信号处理中,“能量”定义为 [信号平方乘以时间],因此功率为 [能量/时间],然后是 [信号平方]。这就是为什么您在以$[V^2]$ 为单位给出的答案中看到“力量”的原因。要找到以 [Watts] 为单位的物理功率,您只需按您正在驱动的任何负载的电阻进行缩放。这通常在信号分析中设置为 1——
验证 Parseval 定理
您还可以通过检查时域和频域中的功率和能量是否相等来验证 fft 输出的缩放比例,因为它们应该符合Parseval 定理。请记住,如果您对所有频率的功率谱密度进行积分,您应该获得输入信号中存在的总功率。例如,使用
Energy_timedomain = sum(x.*conj(x))*dt = 125 [V^2 sec]
Power_timedomain = Energy_timedomain/T = 12.5 [V^2]
Energy_freqdomain = sum(X.*conj(X))*df = 125 [V^2 sec]
Power_freqdomain = sum(Sxx)*df = 12.5 [V^2]
请注意,如果您为输入信号采用更长的时间窗口持续时间$T$ ,则“能量”将增加(因为它更多),但平均功率将保持不变(因为这是活力)。
恢复幅度
功率谱密度图中每个峰值的值为$62.5\;[\textrm{V}^2 / \textrm{Hz}]$。要将其转换为功率,我们需要乘以 bin 宽度(本质上是对该 bin 上的功率谱密度进行积分)。在您的情况下,箱宽度为$df=F_s/N = 10\;\textrm{Hz}/100 = 0.1\;\textrm{Hz}$。因此,该频率下输入信号的总功率为
$$ P = 2 \times 62.5\;[V^2/\textrm{Hz}] \times 0.1\;[\textrm{Hz}] = 12.5\; [V^2] $$
其中$2$的因数是因为有 2 个峰值 - 频谱的负部分功率的一半和正部分的功率的一半。请注意,如果您的输入信号是真实的,那么输出频谱将始终是对称的,并且您经常会看到被丢弃的负频率(不包含任何额外信息)。如果您选择丢弃负频率,那么您应该将剩余的唯一正频率中的功率乘以 2 以进行补偿(但不要将 DC 或 Nyquist 乘以 2,因为它们中的每一个都只有一个在 fft 输出中-它们没有负面的对应物)。
我们知道,对于正弦曲线,功率和幅度$A$与$P = A^2/2$相关(同样,只需将任何负载电阻设置为$1$)。因此,重新排列这个,你的 3 Hz 波的振幅是
$$A = \sqrt{2\times 12.5} = 5\;[V]$$
恢复阶段
正如 Hilmar 所说,$-2$ 的相位可以通过获取$3\;\textrm{Hz}$处的相位值(即 2.712)并加上$\pi/2$然后减去$2\pi$来恢复。$\pi/2$是因为您的输入信号是正弦波,它本质上已经包含相移,因为
$$ \sin(2\pi f_0 t + \phi) = \cos(2\pi f_0 t + ( \phi - \pi/2)) = \cos(2\pi f_0 t + \phi_{\textrm{fft}}) $$
所以你需要将$\pi/2$添加到 fft( ) 得到你的输入阶段
$$\phi = \phi_{\textrm{fft}} + \pi/2 = 2.712 + \pi/2 = 4.283.$$
最后的$2\pi$减法是因为在执行此操作后,您会得到大于$\pi$的值,因此您只需解开相位以将其恢复到$[-\pi ... +\pi]$范围内。
最后的注意事项
在这种情况下一切都很好,因为由于采样率、点数、时间窗口和输入频率波的组合,所有信号能量都恰好落在单个 fft bin 中(参见此处)。如果在将输入信号传递给 fft() 之前对输入信号应用了任何窗口或零填充,事情就会变得更加困难。当你走到那一步时,这里和这里都有一些启发性的答案。