Python计算傅里叶变换
技术背景
傅里叶变换在几乎所有计算相关领域都有可能被使用到,例如通信领域的滤波、材料领域的晶格倒易空间计算还有分子动力学中的倒易力场能量项等等。最简单的例子来说,计算周期性盒子的电势能\(k\sum_i\frac{q_i}{r_i}\)本身就是一个类似于调和级数的形式,很难求得精确解。但是在Edward求和方法中使用傅里叶变换,可以做到在倒易空间进行能量计算,可以逼近精确解。本文主要介绍傅里叶变换的原理及相应的Python代码实现。
DFT原理
DFT计算的本质是一个矩阵运算:
\]
\]
如果写成一个矩阵的形式,那就是:
y_1\\
y_2\\
y_3\\
...\\
y_{N-1}
\end{matrix}\right]=\left[\begin{matrix}
1&&1&&1&&...&&1\\
1&&e^{-j\frac{2\pi}{N}\cdot1}&&e^{-j\frac{2\pi}{N}\cdot2}&&...&&e^{-j\frac{2\pi}{N}\cdot(N-1)}\\
1&&e^{-j\frac{2\pi}{N}\cdot2}&&e^{-j\frac{2\pi}{N}\cdot4}&&...&&e^{-j\frac{2\pi}{N}\cdot2(N-1)}\\
...&&...&&...&&...&&...\\
1&&e^{-j\frac{2\pi}{N}\cdot(N-1)}&&e^{-j\frac{2\pi}{N}\cdot2(N-1)}&&...&&e^{-j\frac{2\pi}{N}\cdot(N-1)(N-1)}
\end{matrix}\right]\left[
\begin{matrix}
x_1\\
x_2\\
x_3\\
...\\
x_{N-1}
\end{matrix}\right]
\]
类似的,逆傅里叶变换的矩阵形式为:
\begin{matrix}
x_1\\
x_2\\
x_3\\
...\\
x_{N-1}
\end{matrix}\right]
=\left[\begin{matrix}
1&&1&&1&&...&&1\\
1&&e^{j\frac{2\pi}{N}\cdot1}&&e^{j\frac{2\pi}{N}\cdot2}&&...&&e^{j\frac{2\pi}{N}\cdot(N-1)}\\
1&&e^{j\frac{2\pi}{N}\cdot2}&&e^{j\frac{2\pi}{N}\cdot4}&&...&&e^{j\frac{2\pi}{N}\cdot2(N-1)}\\
...&&...&&...&&...&&...\\
1&&e^{j\frac{2\pi}{N}\cdot(N-1)}&&e^{j\frac{2\pi}{N}\cdot2(N-1)}&&...&&e^{j\frac{2\pi}{N}\cdot(N-1)(N-1)}
\end{matrix}\right]
\left[\begin{matrix}
y_1\\
y_2\\
y_3\\
...\\
y_{N-1}
\end{matrix}\right]
\]
如果记参数\(W_{N,n,k}=e^{-j\frac{2\pi}{N}nk}\),则其共轭\(W_{N,n,k}^*=e^{j\frac{2\pi}{N}nk}\)是逆傅里叶变换的参数。而且根据复变函数的性质,该参数具有周期性:\(W_{N,n+N,k}=W_{N,n,k+N}=W_{N,n,k}\),共轭参数同理。最后还有一个非常重要的性质:\(W_{N/m,n/m,k}=W_{N/m,n,k/m}=W_{N,n,k}\),根据这个特性,可以将大规模的运算变成小范围的计算。在不考虑这些参数特性的情况下,我们可以使用Python做一个初步的DFT简单实现。
初步Python实现
这里没有做任何的优化,仅仅是一个示例:
import numpy as np
def dft(x):
y = np.zeros_like(x, dtype=np.complex64)
N = x.shape[0]
for k in range(N):
y[k] = np.sum(x * np.exp(-1j*2*np.pi*k*np.arange(N)/N))
return y
def idft(y):
x = np.zeros_like(y, dtype=np.float32)
N = y.shape[0]
for n in range(N):
x[n] = np.real(np.sum(y * np.exp(1j*2*np.pi*n*np.arange(N)/N)) / N)
return x
N = 128
x = np.random.random(N).astype(np.float32)
y0 = dft(x)
y1 = np.fft.fft(x)
print (np.allclose(y0, y1))
yr = np.random.random(N).astype(np.float32)
yi = np.random.random(N).astype(np.float32)
y = yr + 1j*yi
x0 = idft(y)
x1 = np.fft.ifft(y).real
print (np.allclose(x0, x1))
# True
# True
输出的两个结果都是True
,也就说明这个计算结果是没问题的。
FFT快速傅里叶变换
首先我们整理一下所有参数相关的优化点:
\begin{matrix}
W_{N,n+N,k}=W_{N,n,k+N}=W_{N,n,k}\\
W_{N/m,n/m,k}=W_{N/m,n,k/m}=W_{N,n,k}\\
W_{N,\beta,\frac{N}{2\beta}}=W^*_{N,\beta,\frac{N}{2\beta}}=-1\Rightarrow W_{N,n,k}\cdot W_{N,\beta,\frac{N}{2\beta}}=-W_{N,n,k}\\
W_{N,N-n,k}=W_{N,N,k}*W_{N,-n,k}=W_{N,-n,k}=W_{N,n,N-k}
\end{matrix}
\right.
\]
此时如果我们把原始的输入\(x_n\)拆分为奇偶两组(如果总数N不是偶数,一般可以对输入数组做padding):
\begin{matrix}
x_{2r}\\
x_{2r+1}
\end{matrix},0\leq r\leq \frac{N}{2}-1
\right.
\]
则有:
\]
如果我们把\(x_{2r}\)和\(x_{2r+1}\)看作是两个独立的输入数据,那么上述分解可以进一步优化:
y_k&=\sum_{r=0}^{\frac{N}{2}-1}x_{2r}W_{N,2r,k}+\sum_{r=0}^{\frac{N}{2}-1}x_{2r+1}W_{N,2r+1,k}\\
&=\sum_{r=0}^{\frac{N}{2}-1}x^{(odd)}_{r'}W_{\frac{N}{2},r',k}+W_{N,1,k}\sum_{r=0}^{\frac{N}{2}-1}x^{(even)}_{r'}W_{\frac{N}{2},r',k}\\
&=y^{(odd)}_k+W_{N,1,k}y^{(even)}_k
\end{align*}
\]
同理可以得到:
y_{k+\frac{N}{2}}&=y^{(odd)}_{k+\frac{N}{2}}+W_{N,1,k+\frac{N}{2}}y^{(even)}_{k+\frac{N}{2}}\\
&=y^{(odd)}_{k+\frac{N}{2}}-W_{N,1,k}y^{(even)}_{k+\frac{N}{2}}
\end{align*}
\]
这就是所谓的蝶形运算(图像来自于参考链接):

这个运算式的意义在于,假如我们原本做一个$2^N$点数据的傅里叶变换,使用原始的DFT运算我们需要做$2^{2N}$次乘法和$2^N(2^N-1)$次加法,但是这种方法可以把计算量缩减到$2\cdot2^N+2^{\frac{N}{2}}$次乘法和$2^{\frac{N}{2}}(2^\frac{N}{2}-1)$次加法。做一次分解,就把复杂度从$O(2^{2N})$降到了$O(2^N)$(注意:这里的$N$跟前面用到的数据点总数不是一个含义,这里的$N$指代数据点总数是2的整数次方,只是两者的表述习惯都常用$N$)。相关代码实现如下:
import numpy as np
def dft(x):
y = np.zeros_like(x, dtype=np.complex64)
N = x.shape[0]
for k in range(N):
y[k] = np.sum(x * np.exp(-1j*2*np.pi*k*np.arange(N)/N))
return y
def dft2(x):
y = np.zeros_like(x, dtype=np.complex64)
N = x.shape[0]
for k in range(N//2):
c1 = np.exp(-1j*2*np.pi*k*np.arange(N//2)/(N//2))
c2 = np.exp(-1j*2*np.pi*k/N)
y1 = np.sum(x[::2] * c1)
y2 = np.sum(x[1::2] * c1)
y[k] = y1 + c2 * y2
y[k+N//2] = y1 - c2 * y2
return y
N = 128
x = np.random.random(N).astype(np.float32)
y0 = dft2(x)
y1 = np.fft.fft(x)
print (np.allclose(y0, y1))
# True
运行输出为True
,表示计算结果一致。需要注意的是,这里的代码未考虑padding问题,不能作为正式的代码实现,仅仅是一个算法演示。既然能够分割一次,那么就可以分割多次,直到无法分割为止,或者分割到一个指定的参数为止。这也就是多重蝶形运算的原理:

简单一点可以使用递归的方式进行计算:
import numpy as np
def dft(x):
y = np.zeros_like(x, dtype=np.complex64)
N = x.shape[0]
for k in range(N):
y[k] = np.sum(x * np.exp(-1j*2*np.pi*k*np.arange(N)/N))
return y
def dftn(x, N_cut=2):
y = np.zeros_like(x, dtype=np.complex64)
N = x.shape[0]
if N > N_cut:
y1 = dftn(x[::2])
y2 = dftn(x[1::2])
else:
return dft(x)
for k in range(N//2):
c2 = np.exp(-1j*2*np.pi*k/N)
y[k] = y1[k] + c2 * y2[k]
y[k+N//2] = y1[k] - c2 * y2[k]
return y
N = 1024
x = np.random.random(N).astype(np.float32)
y0 = dftn(x)
y1 = np.fft.fft(x)
print (np.allclose(y0, y1))
# True
这里的实现使用递归的方法,结合了前面实现的DFT算法和蝶形运算方法,得到的结果也是正确的。这里使用的蝶形运算优化方法,就是FFT快速傅里叶变换的基本思路。
N点快速傅里叶变换
所谓的N点FFT,其实就是每次只取N个数据点执行傅里叶变换。那么取数据点的方式就有很多种了,例如只取前N个数据点,或者降采样之后再取前N个数据点,再就是加窗,在经过窗函数的运算后,对每个窗体内的数据点做傅里叶变换。最简单的方式就是矩形窗,常见的还有汉宁窗和汉明窗,这里不做详细分析。值得注意的是,如果使用降采样的方法,采样率需要遵循奈奎斯特采样定理,要大于两倍的target frequency。尤其对于周期性边界条件和远程相互作用的场景,高频区域的贡献是不可忽视的。
至于为什么不使用全域数据点的傅里叶变换,即使我们可以用快速傅里叶变换把计算复杂度缩减到\(O(N\log N)\)(这里的\(N\)是数据点数)的级别,对于那些大规模数据传输和计算的场景,也是不适用的,因此使用降低傅里叶变换点数的思路对于大多数的场景来说可以兼顾到性能与精确度。而窗体函数的出现,进一步优化了截断处数据泄露的问题。
总结概要
本文介绍了离散傅里叶变换和快速傅里叶变换的基本原理及其对应的Python代码实现,并将计算结果与numpy所集成的fft函数进行对比。其实现在FFT计算的成熟工具已经有很多了,不论是CPU上scipy的fft模块还是GPU上的cufft动态链接库,都有非常好的性能。但还是得真正去了解计算背后的原理,和相关的物理图像,才能更恰当的使用这个强大的工具。
版权声明
本文首发链接为:https://www.cnblogs.com/dechinphy/p/fft.html
作者ID:DechinPhy
更多原著文章:https://www.cnblogs.com/dechinphy/
请博主喝咖啡:https://www.cnblogs.com/dechinphy/gallery/image/379634.html
参考链接
Python计算傅里叶变换的更多相关文章
- [转载] python 计算字符串长度
本文转载自: http://www.sharejs.com/codes/python/4843 python 计算字符串长度,一个中文算两个字符,先转换成utf8,然后通过计算utf8的长度和len函 ...
- Python计算斗牛游戏的概率
Python计算斗牛游戏的概率 过年回家,都会约上亲朋好友聚聚会,会上经常会打麻将,斗地主,斗牛.在这些游戏中,斗牛是最受欢迎的,因为可以很多人一起玩,而且没有技术含量,都是看运气(专业术语是概率). ...
- 利用Python计算π的值,并显示进度条
利用Python计算π的值,并显示进度条 第一步:下载tqdm 第二步;编写代码 from math import * from tqdm import tqdm from time import ...
- 用Python计算幂的两种方法,非递归和递归法
用Python计算幂的两种方法: #coding:utf-8 #计算幂的两种方法.py #1.常规方法利用函数 #不使用递归计算幂的方法 """ def power(x, ...
- Python计算分位数
Python计算分位数 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/gdkyxy2013/article/details/80911514 ...
- python 计算校验和
校验和是经常使用的,这里简单的列了一个针对按字节计算累加和的代码片段.其实,这种累加和的计算,将字节翻译为无符号整数和带符号整数,结果是一样的. 使用python计算校验和时记住做截断就可以了. 这里 ...
- 为了用python计算一个汉字的中心点,差点没绞尽脑汁活活累死
为了用python计算一个汉字的中心点,差点没绞尽脑汁活活累死
- python计算时间差的方法
本文实例讲述了python计算时间差的方法.分享给大家供大家参考.具体分析如下: 1.问题: 给定你两个日期,如何计算这两个日期之间间隔几天,几个星期,几个月,几年? 2.解决方法: 标准模块date ...
- 用Python计算最长公共子序列和最长公共子串
如何用Python计算最长公共子序列和最长公共子串 1. 什么是最长公共子序列?什么是最长公共子串? 1.1. 最长公共子序列(Longest-Common-Subsequences,LCS) 最长公 ...
- 用python计算100以内的素数
用python计算100以内的素数 : break else: list.append(i)print(list)
随机推荐
- TIER 1: Responder
TIER 1: Responder Active Directory Active Directory(AD)是由微软开发的目录服务,用于在网络环境中管理和组织用户.计算机.应用程序和其他资源.它是 ...
- Prometheus 使用Python推送指标数据到Pushgateway
使用Python推送指标数据到Pushgateway 需求描述 实践环境 Python 3.6.5 Django 3.0.6 prometheus-client 0.11.0 代码实现 !/usr/b ...
- 1、SpringMVC简介
1.1.MVC 概述 MVC:是一种软件架构的思想,将软件按照模型.视图.控制器来划分: M( Model ):模型层,指工程中的 JavaBean ,作用是处理数据: V( View ):视图层,指 ...
- 【VMware】将NAT虚拟机开放访问
NAT模式下面需要将主机内的虚拟机提供给外部访问 这个设置可以通过开启端口来实现外部访问NAT虚拟机 主机端口 - 映射 虚拟机 IP 的端口,问题是有多少个虚拟机应用就需要开多少个端口...
- TensorBoard标量图中的平滑曲线是如何做的平滑?—— tensorflow TensorBoard标量图中“平滑”参数背后的数学原理是什么?—— 指数移动平均(EMA)
TensorFlow的tensorboard的平滑曲线的实现代码: 使用"指数移动平均"技术实现. 地址: https://github.com/tensorflow/tensor ...
- python进程绑定CPU的意义
1. 绑定CPU后对计算密集型的任务可能会一定程度上提升运算性能:(小幅度的性能提升,甚至小幅度落后,总之就是差别不大) 对比1代码A: import os from multiprocessing ...
- 全网最适合入门的面向对象编程教程:35 Python的内置数据类型-文档字符串和__doc__属性
全网最适合入门的面向对象编程教程:35 Python 的内置数据类型-文档字符串和__doc__属性 摘要: 在 Python 中,文档字符串(Docstring)是一种用于为模块.类.方法或函数编写 ...
- br4gOnB4ll靶机笔记
br4gOnB4ll靶机笔记 这是一台vulnhub上的免费靶机,比较简单. 1.主机发现 主机发现 -sn 只做ping扫描,不做端口扫描 nmap -sn 192.168.84.1/24 Star ...
- DRM:清华提出无偏差的新类发现与定位新方法 | CVPR 2024
论文分析了现有的新类别发现和定位(NCDL)方法并确定了核心问题:目标检测器往往偏向已知的目标,忽略未知的目标.为了解决这个问题,论文提出了去偏差区域挖掘(DRM)方法,以互补的方式结合类无关RPN和 ...
- 简单理解.net 依赖注入的三种方式
前言 :.NET5.0 于2020年11月10日正式发布,它是3.1之后的 .NET Core 的下一个主要版本.微软将这个新版本命名为 .NET 5.0 而不是 .NET Core 4.0,其原因有 ...