Pythonで離散時間フーリエ変換を実装

Pythonで離散時間フーリエ変換(DTFT, Discrete-Time Fourier Transform)と離散時間フーリエ逆変換を実装してみます。

アルゴリズム

離散時間フーリエ変換 (DTFT)

離散時間データを周波数データへ変換するDTFTの式は(1)で計算できます。

$$X[\omega] = \sum_{n=-\infty}^\infty x[n] \exp(-j \omega n) \tag{1}$$

(1)式の左辺の\(\omega\)を指定することで、その\(\omega\)におけるDTFTの値を計算することが出来ます。DTFTの結果は連続時間になるので、\(\omega\)は実数で指定できます。

離散時間データは次のようなものです。(1)式は\(-\infty\)から\(\infty\)まで総和を計算していますが、離散データは有限なので、例えば以下の図のようなデータであれば、総和は”0″から”4″までを利用します。

離散時間データの例

横軸はデータをサンプリング時間\(T_0\)で取得した値です。つまり \(n=0,1,2,3,4\)は時間ではなくデータのカウント数です。これを正規化時間と呼びます。実際のデータの時間は\(nT_0[sec]\)です。

それから、変換を行った\(X[\omega]\)の横軸も正規化時間をもとに計算しているので、正規化角周波数\(\omega\)[rad/sample]になります。正規化角周波数\(\omega\)は1サンプリング時間\(T_0\)毎に何\(rad\)進むかを表す値です。正規化されていない角周波数\(\Omega[rad]\)への変換は

$$
\Omega = \omega / T_0
$$

で計算します。

そして、DTFTは\([-\pi, \pi]\)で周期的な結果となります。

離散時間フーリエ逆変換 (IDTFT)

周波数データを離散時間データへ変換するIDTFTは(2)式で計算できます。

$$
x[n] = \frac{1}{2\pi} \int_{-\pi}^{\pi} X[\omega] \exp(j \omega n) d\omega \tag{2}
$$

(2)式の左辺の\(n\)を指定して、右辺の積分計算を行います。

積分計算はコンピュータで計算するには厄介ですが、数値積分のシンプソンの公式を使って行います。

アルゴリズム実装

使用するモジュール

複素数の計算が含まれているため、”cmath”モジュールを使用します。

import numpy as np
import cmath
import matplotlib.pyplot as plt # 結果表示用

離散時間フーリエ変換 (DTFT)

$$X[\omega] = \sum_{n=-\infty}^\infty x[n] \exp(-j \omega n)$$

# 離散時間フーリエ変換関数
def DTFT(omega, xn):
    _Xw = np.zeros(len(omega), dtype = np.complex)
    
    for i, w in enumerate(omega):
        for n, xn_i in enumerate(xn):
            _Xw[i] += xn_i * cmath.exp(-1j * w * n)
    
    return _Xw

この関数の戻り値は複素数型です。

離散時間フーリエ逆変換(IDTFT)

$$
x[n] = \frac{1}{2\pi} \int_{-\pi}^{\pi} X[\omega] \exp(j \omega n) d\omega
$$

# nを指定して、シンプソンの公式で逆離散時間フーリエ変換を計算
def InvertWithSimpsonRule(Xomega,omega_array,n):
    lim_n = int(len(Xomega)/2)
    h = abs(omega_array[1] - omega_array[0])
    
    sum = np.zeros(1, dtype=complex)
    for i in range(1, lim_n):
        f1 = (Xomega[2*i - 2] * (cmath.exp((1j) * omega_array[2*i - 2] * n )) )
        f2 = (Xomega[2*i - 1] * (cmath.exp((1j) * omega_array[2*i - 1] * n )) )
        f3 = (Xomega[2*i - 0] * (cmath.exp((1j) * omega_array[2*i - 0] * n )) )
        sum[0] += h*(f1 + 4*f2 + f3)/3

    
    return sum[0] / (2 * cmath.pi)

# nを変化させながら離散時間フーリエ逆変換計算
def InverseDTFT(omega_array, Xw_array, n_array):
    xn = np.zeros(len(n_array), dtype=np.complex)
    for n, n_atom in enumerate(n_array):
        xn[n] = InvertWithSimpsonRule(Xw_array, omega_array, n)
        
    return xn

この関数の戻り値は複素数型です。

周波数データ表示関数

離散時間フーリエ変換のデータを表示する関数も作成しておきます。この関数の引数’normalize’と’samplingTime’で、表示する離散時間フーリエ変換の横軸データを正規化角周波数[rad]から周波数[Hz]へ変換します。

import math
from matplotlib.ticker import (MultipleLocator, AutoLocator)

# 離散時間フーリエ変換データ表示関数
def showDTFTData(omega, Xw, normalize=True, SamplingTime = 1):
    fig = plt.figure(figsize=(12,5))
    ax = fig.add_subplot(111)
    if normalize:
        plt.plot(omega,Xw)
        plt.xlabel('Normalized angular frequency[rad]')
        plt.ylabel('X($\omega$)')
        ax.xaxis.set_major_locator(MultipleLocator(np.pi))
    else:
        x_data_hz = omega/SamplingTime/(2*np.pi) 
        plt.plot(x_data_hz ,Xw)
        plt.xlabel('Frequency[Hz]')
        plt.ylabel('abs(X($\omega$))')
        
        xtick = x_data_hz[-1]/10
        
        if xtick >= 1:
            ax.xaxis.set_major_locator(MultipleLocator(xtick))
        else:
            ax.xaxis.set_major_locator(AutoLocator())
        plt.minorticks_on()
        plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2)
        
    plt.grid(which='major', color='#666666', linestyle='-')
    plt.show()

離散時間データ表示関数

合わせて離散時間データ表示関数も定義します。

# 離散データ表示用関数
def showDiscreteData(x,y):
    markerline, stemline, baseline = plt.stem(x, y)
    plt.setp(markerline, marker='o', markeredgewidth=2, markersize=8, color='black', markerfacecolor='white', zorder=3)
    plt.setp(stemline, color='black', zorder=2, linewidth=1.0)
    plt.setp(baseline, color='black', zorder=1)
    plt.grid(True)
    plt.xlabel('n')
    plt.ylabel('y [n]')
    plt.show()

実装を行った関数で計算

用いる離散時間データ

# 離散時間データの定義
xd = np.array([0,0,1,1,1,0,0,0])
n = np.arrange(start = 0, stop = len(xd))

# 離散時間データの表示
showDiscreteData(n, xd)

データを直接’np.array’で作成しているので、正規化時間データになっているように考えます。

離散時間フーリエ変換計算

# DEFTの計算に用いる周波数 omega の範囲を指定
omega = np.linspace(-3*3.14, 3*3.14, 2020)

# 離散時間フーリエ変換の計算
Xw = DTFT(omega, xd)

# フーリエ変換結果の表示
showDTFTData(omega, Xw.real)

\(\pi\)毎にグリッドを表示しています。これで、\([-\pi, \pi]\)毎に周期的な結果となっていることが分かります。横軸は正規化角周波数となっていることに注意してください。

そして、DTFTの結果は複素数なので、showDTFTData関数の引数へは、’.real’で実部のみを抽出したデータを渡しています。今回は実部を表示していますが、この表示方法は複素数なので、実部、虚部、絶対値の3種類が考えられます。

離散時間フーリエ逆変換の計算

離散時間フーリエ逆変換は区間\([-\pi, \pi]\)の積分計算なので、もう一度範囲を\([-\pi, \pi]\)で離散時間フーリエ変換を行ったデータを用います。

# DEFTの計算に用いる周波数 omega の範囲を指定
omega = np.linspace(-1*3.14, 1*3.14, 2020)

# 離散時間フーリエ変換の計算
Xw = DTFT(omega, xd)

# 離散時間フーリエ逆変換の計算
i_xd = InverseDTFT(omega, Xw, n)

# 逆フーリエ変換結果の表示
showDiscreteData(n, i_xd.real)

InverseDTFT関数の戻り値は複素数型なので、showDiscreteData関数の引数には’.real’で実数部を与えています。これは、もともとのデータに虚数が含まれていないからですね。

試しにshowDiscreteData()の第二引数に”i_xd.imag”で虚数部を与えてグラフを表示すると、以下の図のようにほぼゼロです。

離散時間逆フーリエ変換の結果は、元のデータと比べて若干ずれは生じていますが、信号を再生できていると考えられる結果だと思います。

離散時間フーリエ逆変換の実装でシンプソンの公式を用いたので、数値計算誤差が生じた結果です。

でもしかし、離散時間フーリエ変換と離散時間フーリエ逆変換のアルゴリズムの確認をこれで出来ました。今回でアルゴリズムの確認が出来たので、次回はいろいろな信号に離散時間フーリエ変換を試してみます。

コメント

タイトルとURLをコピーしました