Теорема о свёртке утверждает, что преобразование Фурье от свёртки двух функций является произведением их Фурье образов:

\mathcal{F}\{f*g\} = \mathcal{F}\{f\}\cdot\mathcal{F}\{g\}.

Как следствие, свёртку двух функций можно вычислить путём обратного преобразования Фурье:

(f*g)(x)\equiv\int\limits_{-\infty}^{\infty}f(t)g(x-t)dt=\mathcal{F}^{-1}\bigl\{\mathcal{F}\{f\}\cdot\mathcal{F}\{g\}\bigr\}.

Когда свёртку не удаётся получить аналитически, использование теоремы позволяет вместо численного интегрирования воспользоваться алгоритмом быстрого преобразования Фурье и существенно ускорить процесс вычислений. В ходе решения возникшей у меня задачи мне не удалось найти в интернете достаточно подробного описания алгоритма действий, поэтому я решил изложить его здесь, во-первых, как памятку для себя и во-вторых, на случай если это окажется полезным кому-то ещё. Не претендуя на общность изложения, ниже я приведу примеры найденных решений для функций от одной и от двух переменных.

Функция от одной переменной

Допустим, нам нужно вычислить свёртку функций

f(x)=\frac{1}{\pi\sqrt{1-x^2}}\hspace{5mm}\text{и}\hspace{5mm}g(x)=\frac{1}{\sqrt{2\pi}\sigma}\exp\left(-\frac{x^2}{2\sigma^2}\right),

причём имеется в виду, что нас интересует только действительная часть области значений функцииf(x), то есть область её определения лежит в интервале x \in [-1,1]. Интеграл функции в этом интервале равен 1, а преобразование Фурье вычисляется аналитически:

\mathcal{F}(f(x))= \int\limits_{-1}^{+1}\frac{\exp(-2\pi i u x)}{\pi\sqrt{1-x^2}}dx = J_0 (2\pi u),

где J_0- функция Бесселя первого рода. Фурье образ функции Гаусса:

\mathcal{F}(g(x)) = \exp(-2\,\pi^2\sigma^2u^2).

Дискретным преобразованием Фурье (ДПФ) называется метод вычислений, в котором и функция и ее преобразование заменяются дискретными аналогами. Быстрое преобразование Фурье (БПФ) относится к способу эффективного вычисления ДПФ с использованием симметрии в вычисляемых условиях. Симметрия наивысшая, когда число узловN_x, в которых вычисляется значение функции, и число гармоникN_u,  являются степенью двойки. В библиотеке SciPy с открытым исходным кодом для работы с БПФ предназначен модуль scipy.fft. Для того, чтобы вычислить искомую свёртку методом БФП, нужно задать интересующую нас область определения результата и задать количество узлов разбиения, в которых мы хотим получить значение свёртки.

Выберем область определения x\in[-R+D_x/2,R-D_x/2], где D_xиR- положительные вещественные числа, которые мы будем считать размерными, например, пусть это будут сантиметры. Разобъём этот интервал на N_xодинаковых отрезков, длина каждого из которых равна D_x = 2R/N_x. Интервал D_x\ll Rназывается шагом дискретизации и определяет расстояние между соседними узлами разбиения шкалы x. Обратная величина называется частотой дискретизации и в нашем примере измеряется в обратных сантиметрах.

В шкале частот нашему диапазону шкалыxбудет соответствовать набор из N_x положительных и отрицательных гармоник разложения Фурье. Если N_x- чётное, последовательность частот, которая генерируется функцией scipy.fft.fftfreq, будет такая:

u=\Bigl[0,\;\frac{1}{2R},\;\dots,\;\frac{(N_x-1)/2}{2R},\; -\frac{N_x/2}{2R},\;\dots,\; -\frac{1}{2R} \Bigr].

Если функция, которую мы ищем, вещественна (не имеет мнимой части), положительные частоты в последовательности являются сопряженными величинами отрицательных частот, за счёт этого можно примерно в два раза сократить время вычислений. Для получения укороченной (без отрицательных частот) последовательности используется функция scipy.fft.rfftfreq. Для выполнения прямого и обратного преобразований Фурье для вещественного сигнала предназначены функции scipy.fft.rfft и scipy.fft.irfft соответственно. Функция scipy.fft.fftshift располагает компоненту с нулевой частотой в середине частотного спектра, а scipy.fft.ifftshift осуществляет обратное преобразование. Ниже приведён код для вычисления свёртки:

import numpy as np
from scipy.special import j0 as BesselJ0 
import scipy.fft as FFT
from matplotlib import pyplot as plt

π    = np.pi
σ    = 0.01
R    = 1.5    # интервал, в котором вычисляется свёртка
Nx   = 4096   # число разбиений по оси x
Dx   = 2*R/Nx # шаг дискретизации
x    = np.linspace(0.5*Dx-R, R-0.5*Dx, num = Nx)
u    = FFT.rfftfreq(Nx//16, d = 16*Dx) # цифровой ФНЧ
J0   = BesselJ0(2*π*u)*np.exp(-2*(π*σ*u)**2)
y    = FFT.ifftshift(FFT.irfft(J0, n=Nx, norm='forward'))

plt.subplot(211)
plt.plot(u, J0, 'r-', label='спектр частот')
plt.xlabel('u, [1/см]')
plt.grid(True); plt.legend()
plt.subplot(212)
plt.plot(x, y, 'r-', label='результат свёртки')
plt.xlabel('x, [см]')
plt.grid(True); plt.legend()
plt.show()

Результат представлен на картинке. Как видно из спектра частот, амплитуда гармоник экспоненциально падает с ростом частоты (из-за \mathcal{F}(g(x)), см. выше) , поэтому число вычисляемых гармоник уменьшено в 16 раз с помощью "цифрового ФНЧ".

Функция от двух переменных

Перейдем к двумерному случаю. Пусть

f(x,y)=\frac{1}{\pi\sqrt{1-x^2-y^2}}\hspace{5mm}\text{и}\hspace{5mm}g(x,y)=\frac{1}{2\pi\sigma_x\sigma_y}\exp\left(-\frac{x^2}{2\sigma_x^2} - \frac{y^2}{2\sigma_y^2}\right),

где \sigma_xи \sigma_yразличны. Пользуясь полученными выше формулами и рецептами, создадим код для вычисления свёртки этих функций:

import numpy as np
from scipy.special import j0 as BesselJ0 
import scipy.fft as FFT
import matplotlib.pyplot as plt

π    = np.pi
σx   = 0.1 
σy   = 0.05
R    = 1.5
N    = 2048
D    = 2*R/N
s    = np.linspace(0.5*D-R, R-0.5*D, num = N)
f    = FFT.fftshift(FFT.fftfreq(N//4, d = 4*D))
x,y  = np.meshgrid(s,s)
u,v  = np.meshgrid(f,f)
J    = BesselJ0(2*π*(u**2 + v**2)**0.5)*np.exp(-2*π*π*((σx*u)**2 + (σy*v)**2))
z    = np.abs(FFT.ifftshift(FFT.ifft2(J, s=[N,N], workers=-1, norm='forward')))

plt.figure(figsize=(16, 9))
levels = np.linspace(J.min(), J.max(), 16)
plt.subplot(121)
plt.contourf(u, v, J, levels)
plt.colorbar()
plt.xlim(-5,5); plt.xlabel('u, [1/см]')
plt.ylim(-5,5); plt.ylabel('v, [1/см]')
plt.subplot(122)
levels = np.linspace(z.min(), z.max(), 16)
plt.contourf(x, y, z, levels)
plt.colorbar()
plt.xlabel('x, [см]')
plt.ylabel('y, [см]')
plt.show()

Основное изменение в коде, по сравнению с одномерной задачей, состоит в использовании scipy.fft.fftfreq вместо scipy.fft.rfftfreq и scipy.fft.ifft2 вместо scipy.fft.irfft. Опция workers=-1 в функции ifft2 позволяет параллельно использовать все имеющиеся в системе процессоры. Результат вычислений представлен на картинке:

Заключение

Вот вроде бы и всё о чём я хотел написать. Буду рад критике, вопросам и предложениям. Всех с наступающим 2022!

Комментарии (4)


  1. qbertych
    28.12.2021 15:14

    А почему вы используете ifft2 вместо irfft2? В 2D выигрыш по времени будет заметным.


  1. daifu Автор
    28.12.2021 16:19

    Если использовать irfft2 то функция считается только в 1 и 3 квадрантах. Пока не разобрался как обойти эту проблему иначе. Есть идеи?


    1. qbertych
      28.12.2021 16:48

      Ну да, она так и должна считаться. Для вещественных входных данных значения в остальных квадрантах будут такими же, просто в обратном порядке. Поэтому их можно скопировать туда вручную, не тратя время на вычисления.


  1. daifu Автор
    28.12.2021 17:55

    Наверное можно. Может так и сделаю, хотя там есть нюансы - сразу как-то не получилось...