通过 FFT 计算数值导数 - SciPy

计算科学 Python scipy 傅里叶变换
2021-11-25 09:07:58

我编写了以下代码来使用 FFT 计算函数的近似导数:

from scipy.fftpack import fft, ifft, dct, idct, dst, idst, fftshift, fftfreq
from numpy import linspace, zeros, array, pi, sin, cos, exp
import matplotlib.pyplot as plt

N = 100
x = linspace(0,2*pi,N)

dx = x[1]-x[0]
y = sin(2*x)+cos(5*x)
dydx = 2*cos(2*x)-5*sin(5*x)

k = fftfreq(N,dx)
k = fftshift(k)

dydx1 = ifft(-k*1j*fft(y)).real

plt.plot(x,dydx,'b',label='Exact value')
plt.plot(x,dydx1,'r',label='Derivative by FFT')
plt.legend()
plt.show()

但是,它给出了意想不到的结果,我认为这与数组 k 给出的波数输入不正确有关:

比较精确和数值导数

我知道 FFT 的不同实现对波数顺序的处理方式不同,那么我在这里缺少什么?任何想法将不胜感激。

2个回答

FFT 返回一个与输入数组具有相同维度的复数数组。输出数组的顺序如下:

  1. 元素 0 包含零频率分量 F0。
  2. 数组元素 F1 包含最小的非零正频率,它等于 1/(Ni Ti),其中 Ni 是元素的数量,Ti 是采样间隔。
  3. F2对应于频率2/(Ni Ti)。
  4. 负频率以正频率的相反顺序存储,范围从最高负频率到最低负频率。
  5. 对于偶数个点,返回的复数值对应的频率为:0, 1/(NiTi), 2/(NiTi), ..., (Ni/2–1)/(NiTi), 1/( 2Ti), –(Ni/2–1)/(NiTi), ..., –1/(NiTi) 其中 1/(2Ti) 是奈奎斯特临界频率。

  6. 对于奇数点,返回的复数值对应的频率为:0, 1/(NiTi), 2/(NiTi), ..., (Ni–1)/2)/(NiTi), –( Ni–1)/2)/(NiTi), ..., –1/(NiTi)

使用这些信息,我们可以构建正确的频率向量,用于计算导数。下面是一段 Python 代码,它可以正确完成这一切。请注意,因子 2πN 由于 FFT 的归一化而被抵消。

from scipy.fftpack import fft, ifft, dct, idct, dst, idst, fftshift, fftfreq
from numpy import linspace, zeros, array, pi, sin, cos, exp, arange
import matplotlib.pyplot as plt


N = 100
x = 2*pi*arange(0,N,1)/N #-open-periodic domain                                                   

dx = x[1]-x[0]
y = sin(2*x)+cos(5*x)
dydx = 2*cos(2*x)-5*sin(5*x)


k2=zeros(N)

if ((N%2)==0):
    #-even number                                                                                   
    for i in range(1,N//2):
        k2[i]=i
        k2[N-i]=-i
else:
    #-odd number                                                                                    
    for i in range(1,(N-1)//2):
        k2[i]=i
        k2[N-i]=-i

dydx1 = ifft(1j*k2*fft(y))

plt.plot(x,dydx,'b',label='Exact value')
plt.plot(x,dydx1, color='r', linestyle='--', label='Derivative by FFT')
plt.legend()
plt.show()

在此处输入图像描述

Maxim Umansky 的回答详细描述了 FFT 频率分量的存储约定,但不一定解释为什么原始代码不起作用。代码中存在三个主要问题:

  1. x = linspace(0,2*pi,N):通过像这样构建您的空间域,您的x值将范围从02π包容这是一个问题,因为您的函数y = sin(2*x)+cos(5*x)在此域上并不是完全周期性的(02π对应于同一点,但它们被包含两次)。这会导致频谱泄漏,从而导致结果出现小偏差。您可以通过使用x = linspace(0,2*pi,N, endpoint=False)(或者x = 2*pi*arange(0,N,1)/N,正如 Maxim Umansky 所做的那样,这就是他所说的“开放周期域”)来避免这种情况。
  2. k = fftshift(k):正如 Maxim Umansky 解释的那样,您的k值需要按特定顺序排列以匹配 FFT 约定。fftshift对值进行排序(从小/负到大/正),这很有用 e. G。用于绘图,但对于计算不正确。
  3. dydx1 = ifft(-k*1j*fft(y)).real:scipy将 FFT 定义为y(j) = (x * exp(-2*pi*sqrt(-1)*j*np.arange(n)/n)).sum(), i。e. 有一个因素2π在指数中,因此在推导导数公式时需要包含此因子。此外,对于scipy的 FFT 约定,这些k值不应带有减号。

因此,通过这三个更改,可以将原始代码更正如下:

from scipy.fftpack import fft, ifft, dct, idct, dst, idst, fftshift, fftfreq
from numpy import linspace, zeros, array, pi, sin, cos, exp
import matplotlib.pyplot as plt

N = 100
x = linspace(0,2*pi,N, endpoint=False) # (1.)

dx = x[1]-x[0]
y = sin(2*x)+cos(5*x)
dydx = 2*cos(2*x)-5*sin(5*x)

k = fftfreq(N,dx)
# (2.)

dydx1 = ifft(2*pi*k*1j*fft(y)).real # (3.)

plt.plot(x,dydx,'b',label='Exact value')
plt.plot(x,dydx1,'r',label='Derivative by FFT')
plt.legend()
plt.show()