1.一次元高速フーリエ変換

 一次元関数 f(x) を離散化したうえで数値的にフーリエ変換する方法とそのプログラムを以下で 紹介します。
 なおここでの計算方法は「光とフーリエ変換」の「5.高速フーリエ変換」を参考にしています。
 (ただしこのページに載せたプログラムは独自に作成したものです)
 2007年1月24日 説明を一部追加するとともに「6.プログラム使用上の注意」を追加しました

目   次

1.離散化フーリエ変換の式

2.高速フーリエ変換とは

3.高速フーリエ変換のアルゴリズム

4.具体的な計算の手順

5.プログラムの例

6.プログラム使用上の注意

1.離散化フーリエ変換の式

 まず f(x) を間隔Tに離散化して x= n*T (nは整数)の値のみを考えるようにし、さらに n を0からN-1 までの N個の点に制限します。

 この離散化関数を、フーリエ変換したものをF(ν) とします。(引数 νは周波数になります)
 この段階で F(ν)は周波数に対して連続関数となっていますが、次にこれを離散化します。
元の関数 f(x) を間隔TのN個の点に離散化したことにより、フーリエ変換した式は間隔 1/(N*T) で離散化することに なります(詳しい事は上の参考文献や、高速フーリエ変換に関する本を参照してください。)。

 離散化した結果の式を書くと以下の様になります。なおこの式で k は0からN-1の値をとる整数で、引数の k/( N*T) は 離散化した周波数のk-1番目(k =0 が1番目だから)の値を表します。

離散化フーリエ変換式...(1)

 注意 :以下の式では(1)式のΣの前に掛けてある間隔T が省略されています。また例として 載せているプログラムでもこのTが掛けてないので、実際に使用するときは間隔Tを掛けるように修正してください。

2.高速フーリエ変換とは

 (1)式で F( k/NT) をF(k)に、f( nT )をf(n)に、exp(-i2π/N) を W と書き換えて下の(2)式のように 簡単に書くことにします。

離散フーリエ簡易表記...(2)
 式のコンピュータによる計算時間を考える場合、和より積の方がずっと時間がかかるとされているので計算に必要な積の 回数で計算時間を評価します。

 (2)式を計算する場合離散化した点がN個の場合、あるkについてN回の積が行なわれ、それがN回繰り返されるので (kは0からN-1のN個の値をとるから)N2回の積が 行なわれる事になります。

 Nが少ない数ならたいして計算時間はかからないでしょうが、Nが増えれば急激に計算時間が増える事が予想されます。
この計算時間を減らすための工夫が高速フーリエ変換です。

3.高速フーリエ変換のアルゴリズム

 まず離散点の数Nを、N = r1*r2 と二つの数の積に分解します。
 つぎに以下に示すようにk を二つの数k0、k1で、n も同じように二つの数n0、n1を 使って書き直します。

k =k1r1 + k0   
n =n1r2 + n0   

k1 =0,1,...r2-1  k0 =0,1,...r1-1
n1 =0,1,...r1-1  n0 =0,1,...r2-1
 k0、k1、n0、n1の変化範囲に注意してください

 次にF( k) をk1、k0の関数としてF(k1,k0)に書き直して、 f(n)を同じ様にn1、k0の関数としてf(n1,n0)に書き直すと(2)式は、
式の分解...(3)
 この式からn1に関する和の部分のみを取り出すと、
フーリエ変換の分解2...(4-2)
ここでr1r2= N とWN=exp(-i2πN/N )=exp(-i2π) = 1から を利用して
を(4-2)に使った。

 (4-2)を用いて(3)式を書くと、
フーリエ変換の分解1...(4-1)
となります。
(2)式を比べると(4-2)式もフーリエ変換の形をしている事がわかります。以上の操作でフーリエ変換式は 2段階のフーリエ変換に分解されました。

 (4-1)式の計算に必要な積の回数を考えると(Wの指数に表れる積を無視した場合)N(r1 + r2)回となって (2)式に必要なN2より計算回数がへります。

 (4-1)、(4-2)に対しても同じ様にr1、r2を更に分解する操作をおこなえば更なる計算時間の 短縮が可能になります。

4.具体的な計算の手順

 ここではN=2m の場合のより具体的な手順について記します。

@r1=2、r2= N /2 の場合

k =2k1 + k0   
n =(N/2)n1 + n0   

k1 =0,1,...(N/2)-1  k0 =0,1
n1 =0,1        n0 =0,1,...(N/2)-1
となり、以上を(3)式に代入すると、
分割数2のm乗の場合...(5)
となります。
 なお(5)式下の段のWk0(N/2)は、n1=1の時 Wkn1(N/2) =W(2k1+k0)(N/2)で k1= 0 の場合はWk0(N/2)、k1= 1 の場合も W(2+k0)(N/2) =WNWk0 (N/2) =Wk0(N/2) より得られます。

 この式をk0= 0 とk0= 1 に分けて書くと、
...(5-1)
...(5-2)
となりますがここで(5-1)で f0(n0) =f(0,n0) + f(1,n0)、 (5-2)で f1(n0) ={f(0,n0) - f(1,n0)}Wn0 とすると(5-1),(5-2)もフーリエ変換の式になっている事がわかります。


A(5-1)、(5-2)の各式をN1=N/2、r1=2、r2= N1 /2 として
k1 =2k'1 + k'0   
n0 =(N1/2)n'1 + n'0   

k'1 =0,1,...(N1/2)-1  k'0 =0,1
n'1 =0,1        n'0 =0,1,...(N1/2)-1
として今度はk'1、n'0の式に分解すると
...(6-1)
...(6-2)
となります。
 ただしこれらの式で関数 F 'は F(k1 ,0)または F(k1 ,1)、f ' はf0または f1を表すものとします。

Bこのような分解操作を((6-1)、(6-2)の各々の式に)次々おこないr2= N1 /2= 1になるまで繰り返す事で離散フーリエ変換を計算します。

 繰り返し 回目の式を求めておくと ( (5)式が j = 1 、(6)式が j = 2 の場合になります)以下のようになります。
...(7-1)
...(7-2)
ここで、Nj = N /2j-1 で、関数fj は繰り返し(j-1)回目の関数fj-1から fj(nj-10) =fj-1(0,nj-10) + fj-1(1,nj-10)または f(nj-10) ={fj-1(0,nj-10) - fj-1(1,nj-10)}Wnj-10で 作った関数です。なおこの式の引数nj0は、j=1のときn0 でjが2以上の場合は
nj-10 =(Nj/2)n1 + nj0 
で計算されます。
 kj1は、j = 1 でk1で j =2 以上は
kj-11 =2kj1 + k0
 これらの式でk0、n1はjに係わらず0または1の値をとり、k1、n0は 0から (Nj/2) - 1 の値をとります。

 このようにして計算すると、計算が終了したときフーリエ変換F( k ) ( (2) 式のF( k ) )は F( 0 ) ,F( 1 ).... という順番には並んでいません。
 例えばN = 23 = 8 の場合、(2)式から(5-1) 、(5-2)に分ける時(5-1)でF(0) , F(2) ,F(4) ,F(6)が 計算され、(5-2)ではF(1),F(3),F(5),F(7)が計算される事になり、次の段階では(5-1)のF(0) , F(2) ,F(4) ,F(6)が 更にひとつとびに並び替えられF(0),F(4),F(2),F(6)という順番になります。(5-2)の方で計算されるものは更に F(1),F(5),F(3),F(7)になるので、この場合計算が終了したときには、F(0),F(4),F(2),F(6),F(1),F(5),F(3),F(7)という 順番になります。

 計算繰り返しによって、どう順番が並び替えられるかを下に表にまとめました。

F(0)
F(1)
F(2)
F(3)
F(4)
F(5)
F(6)
F(7)
F(0)
F(2)
F(4)
F(6)
F(0) 0=(000)--->(000)
F(4) 1=(001)--->(100)
F(2) 2=(010)--->(010)
F(6) 3=(011)--->(110)
F(1)
F(3)
F(5)
F(7)
F(1) 4=(100)--->(001)
F(5) 5=(101)--->(101)
F(3) 6=(110)--->(011)
F(7) 7=(111)--->(111)

 これを本来の順番に並び替えるために次の方法を使います。
 上の例を見ると1番目(先頭を0番目と数える場合)に4番目に来るべきF(4)があり、逆に4番目にはF(1)が あります。1を二進数で表現すると001ですが(N =8 なので二進数の桁数を3桁で表現)、これを逆に並べると 100 = 4 になります。

 このように先頭を0とした時の順番を二進数で表現し、その表現を逆転した順番のところにあるフーリエ変換の値と 交換すると本来の順番に並び替える事ができます。
 以下の5でのプログラムで、関数 CalcRev_BinaryExp はこの順番の交換を行なっている部分です。

5.プログラムの例

 プログラムは C++ で書いてあります。ソースファイルの拡張子は cpp にしておいて下さい。
 以下に各部分について簡単な説明を入れておきます。

 関数 DecimFrq_FFTCall が計算の本体で第一引数に計算したい関数を離散化した値を収めた配列を与えます。
この配列に計算したフーリエ変換値をいれて送り返すので、元の関数の値を保持しておきたいときには注意してください。
なおこのフーリエ変換値には式(1)に掛けてある間隔 T が掛けてないので注意してください。
この引数は complex型になっています。
第二引数は第一引数の配列要素数を与えてください。

 DecimFrq_FFT は繰り返し計算をおこなう部分です。

 CalcRev_BinaryExp は計算結果の順番を入れ替えている部分です。

宣言ファイルfftdef.h
/*
    高速フーリエ変換 周波数間引き法プログラム
    関数宣言ファイル
 
    関数の分割数が2^N のもの
*/
#ifndef ___fastfrt
 #define ___fastfrt

#include <complex>

using namespace std;  //introduces namespace std

/*
     関数宣言
*/
//高速フーリエ変換
int    DecimFrq_FFTCall(complex<double>* ,unsigned long );

//高速フーリエ変換計算部分
int    DecimFrq_FFT( complex<double>* ,unsigned long ,unsigned long ,complex<double> ) ;

//二進表記で逆の順にした数値を作成
unsigned long  CalcRev_BinaryExp( unsigned long ,unsigned long );

//与えた数値が2^N の形をしているか調べる
int    Check_PowerOf2( unsigned long , unsigned long* ) ;
 
#endif


定義ファイルfftdef.cpp
#include "fftdef.h"
#include <cmath>

/*

     func_array ==>計算終了時はフーリエ変換したものが入っている
                   元の関数は、別に保管しておく事
*/
int    DecimFrq_FFTCall(complex<double> *func_array ,//対象離散化関数
                        unsigned long div_numb )//関数分割数 を与える
{
    //div_numb のチェック
    unsigned long pow_num ; //関数分割数2^N の Nを与える
 
       if( !Check_PowerOf2( div_numb ,&pow_num ) )
         return 0 ; //2^N の形になっていない場合
//    unsigned long div_numb = pow(2,pow_num ) ;
 
    //高速フーリエ変換の計算
    double pi = acos(-1.0) ;
    complex<double> exp_arg = complex<double>(0.0 ,-2.0*pi/div_numb ) ;
    complex<double> the_factor = exp(exp_arg) ;
 
       DecimFrq_FFT(func_array ,0 ,div_numb ,the_factor) ;
 
    //フーリエ変換 func_array は順番が変わっているので元の順に並び替える
    int the_roop = 0 ;
    unsigned long *flag_array = new unsigned long[div_numb] ;
       for(the_roop = 0 ;the_roop < div_numb ;the_roop++)
         flag_array[the_roop] = 0 ;
 
    unsigned long rebny_num = 0 ;
    complex<double> temp_value ;
 
       for(the_roop = 0 ;the_roop < div_numb ;the_roop++ ){
 
         if( flag_array[the_roop] == 0 ){//まだ交換されていない
           rebny_num = CalcRev_BinaryExp((unsigned long)the_roop ,pow_num ) ;
           //入れ替え
           temp_value = func_array[rebny_num] ;
           func_array[rebny_num] =func_array[the_roop] ;
           func_array[the_roop] = temp_value ;
 
           flag_array[the_roop] = flag_array[rebny_num] = 1 ;
         }
 
       }
 
       delete[] flag_array ;//動的配列解放
 
    return 1 ;
}

/*
    高速フーリエ変換 周波数間引き法 計算本体
 
    関数分割数 2^N
*/
int    DecimFrq_FFT( complex<double> *func_array ,//対象離散化関数
                     unsigned long step_num , //繰り返し回数 root で0
                     unsigned long div_numb , //関数分割数(rootで2^N )
                     complex<double> the_factor ) //exp(-i*2*PI/(2^N) )
{
    unsigned long nwdiv_numb = div_numb/2 ;
    complex<double> *fun_pre = &(func_array[0]) ;
    complex<double> *fun_aft = &(func_array[nwdiv_numb]) ;
    unsigned long stp_factor= pow(2,step_num) ;  //2^step_num
    complex<double>  tmp_sav1 ,tmp_sav2 ; //値の一時保管用
 
       for( int the_roop=0 ;the_roop < nwdiv_numb ;the_roop++ ){
         tmp_sav1 = func_array[the_roop] ;
         tmp_sav2 = func_array[ the_roop + nwdiv_numb] ;
 
         fun_pre[the_roop] = tmp_sav1 + tmp_sav2 ;
         fun_aft[the_roop] =(tmp_sav1 - tmp_sav2 )*pow(the_factor ,the_roop*stp_factor) ;
 
       }
 
       if( nwdiv_numb != 1 ){//再帰呼び出し
 
         step_num++ ; //繰り返し回数増やす
         DecimFrq_FFT(fun_pre ,step_num ,nwdiv_numb ,the_factor ) ;
         DecimFrq_FFT(fun_aft ,step_num ,nwdiv_numb ,the_factor ) ;
 
       }
 
    return 1 ;

}
 

/*
   二進表示で逆に読む数値を作成
   (unsigned long とする)
*/
unsigned long  CalcRev_BinaryExp( unsigned long the_source , //対象となる数字
                                  unsigned long the_num ) //pow(2,N ) の N
{
    unsigned long num_revby = 0UL ;
 
       for( int the_roop = 0 ;the_roop <= the_num-2 ;the_roop++ ){
 
         if( the_source & 1UL ){
           num_revby += 1UL ;
         }
 
         the_source >>= 1 ;
         num_revby <<= 1 ;
 
       }
 
       //the_roop = the_num -1
       if( the_source & 1UL ){
         num_revby += 1UL ;
       }
 

    return num_revby ;
}

/*
    与えた数値が2^N の形をしているかチェックするとともに、Nを獲得する
 
    返し値
        0 :2^N の形をしていない
        1 :2^N の形をしている
*/
int    Check_PowerOf2( unsigned long the_number , //対象となる数値
                       unsigned long *the_power )//2^N のN
{
    bool the_flag = false ;
 
       *the_power = 0 ;//初期化
 
       while( the_number && !the_flag ){//the_number 二進表示で0以外の数値が含まれる
         if( the_number & 1UL )//二進表示の末尾が1の場合
           the_flag = true ;
 
         the_number>>=1 ;//ビットシフトして二進表示を左へずらす
         (*the_power)++ ;
       }
 
       if( the_number==0 && the_flag ){//2^N の形をしている場合のみここに来る
         (*the_power)-- ;//最後にひとつ余計にカウントするのでひとつ戻す
 
         return 1 ;
       }
 
    return 0 ;

}


アルゴリズムへ戻る 使用上の注意