Fourier Transforms

Highly useful in digital image processing and signal processing, Fourier Transforms are mathematical models that can transform functions of time (seconds) into functions of frequency (Hz). In computer science, there are 3 methods in which a Fourier transform can be computed. The first is the Discrete Fourier Transform (DFT), the second is a recursive Fast Fourier Transform (FFT1), and the third is an even faster, non-recursive Fast Fourier Transform that uses bit-reversal (FFT2).

Discrete Fourier Transform (DFT)

Discrete Fourier Transform (DFT) is the discrete version of the Fourier Transform (FT) that transforms a signal (or discrete sequence) from the time domain representation to its representation in the frequency domain. This version typically has a time complexity of O(n2).

#include <fstream>
#include <iostream>
#include <iomanip>
#include <string>
#include <vector>
#include <complex>

#define PI 3.14159265359

/**
 * @brief Computes the DFT
 * 
 * @param input 
 * @return std::vector<std::complex<double>> 
 */
std::vector<std::complex<double>> DFT(const std::vector<std::complex<double>> &input) 
{
	std::vector<std::complex<double>> output; // return vector
	size_t n = input.size();
	for (size_t k = 0; k < n; ++k)  // For each output element
        {
		std::complex<double> sum(0, 0); // exponential sum
		for (size_t t = 0; t < n; ++t)  // For each input element
                {  
			double angle = 2 * PI * t * k / n;
			sum += input[t] * std::exp(std::complex<double>(0, -angle));
		}
		output.push_back(sum);
	}
	return output;
}

int main()
{
  // Introduction
  std::cout << "This program calculates the Discrete Fourier Transform of complex numbers provided by a text file." << std::endl;
  std::cout << "======================================================" << std::endl;

  // Prompt user for file
  std::string filename;
  std::cout << "Please enter the name of the text file (with extension .txt) containing the input: " << std::flush;
  std::ifstream in;
  while (true)
  {
    in.close();
    in.clear();
    std::getline(std::cin, filename);
    in.open(filename.c_str());
    if (in)
    {
      break;
    }
    std::cout << "Invalid file. Please enter a valid input file name: " << std::flush; 
  }
  std::cout << std::endl;

  // Count the number of complex values (lines) in file
  // Assign to N
  int N = 0;
  std::string line;
  std::vector<std::complex<double>> vec;

  while (std::getline(in, line))
  {
    ++N;
    // Parse the real and imaginary parts of each complex value
    std::string re_s, im_s;
    double re, im;
    std::string::size_type pos = line.find('+');
    std::string::size_type neg = line.find('-');
    // EDGE CASE: Number begins with '-'
    if (neg == 0)
    {
      std::string newline = line.substr(1);
      neg = newline.find('-');
      if (neg < 8192)
      {
        ++neg;
      }
    }
    pos = std::min(pos, neg);

    char sign = line[pos];  // '+' or '-'
    re_s = line.substr(0, pos);
    re = stod(re_s);

    std::string::size_type i_pos = line.find('i');

    im_s = line.substr(pos + 1, i_pos - pos - 1);
    im = stod(im_s);
    if (sign == '-')
    {
      im *= -1;
    }
    std::complex<double> mycomplex(re, im);
    // Store them in a vector
    vec.push_back(mycomplex);
  }

  // Close file stream
  in.close();
  in.clear();

  // Do the math
  std::vector<std::complex<double>> res = DFT(vec);

  // File output
  std::string::size_type dot_pos = filename.find('.');
  std::string input = filename.substr(0, dot_pos);

  std::ofstream output_file("out_dft1_" + std::to_string(N) + "_" + input + ".txt");
  for (int i = 0; i < N; ++i)
  {
    // get sign
    std::string sign;
    if (res[i].imag() < 0)
    {
      sign = "";
    }
    else
    {
      sign = "+";
    }
    output_file << std::setprecision(4) << res[i].real() << sign << res[i].imag() << "i" << "\n";
  }
  std::cout << "==================================================" << std::endl;
  std::cout << "Calculation complete. Please check your directory for the output file." << std::endl;
  output_file.close();
  return 0;
}

The following input…

1+1i
-1+1i
1-1i
-1-1i
-3-1i
4-2i
0-3i
2-6i

…yields the following output:

3-12i
10.24+5.243i
3+2i
9.071+10.07i
-5+4i
1.757-3.243i
-9+6i
-5.071-4.071i

Recursive Fast Fourier Transform (FFT1)

Using recursion, the Discrete Fourier Transform algorithm can be optimized to O(nlog(n)) complexity:

void FFT(std::vector<std::complex<double>>& input) 
{
  int N = input.size();
  // Base case
  if (N == 1)
  {
    return;
  }
  // Recursive case
  std::vector<std::complex<double>> evens(N / 2);
  std::vector<std::complex<double>> odds(N / 2);
  for (int i = 0; 2 * i < N; i++) 
  {
      evens[i] = input[2 * i];
      odds[i] = input[2 * i + 1];
  }
  FFT(evens, tt);
  FFT(odds, tt);
  double angle = 2 * PI / N;
  std::complex<double> w(1);
  std::complex<double> wn(cos(angle), sin(angle));
  for (int i = 0; 2 * i < N; i++) 
  {
      input[i] = evens[i] + w * odds[i];
      input[i + N/2] = evens[i] - w * odds[i];
      w *= wn;
  }
}

Bit-Reversal Fast Fourier Transform (FFT2)

Finally, using bit-reversal, an even faster FFT algorithm can be written. My first step was to write a function for reversing all bit values from 0 to N:

/**
 * @brief Reverses the bits
 * 
 * @param N 
 * @return std::vector<std::vector<int>> 
 */
std::vector<std::vector<std::string>> bitrev(int N)
{
  std::vector<std::vector<std::string>> res;
  const int k = std::log2(N);
  for (int i = 0; i < N; ++i)
  {
    std::vector<std::string> line;

    // 1st: push back i
    line.push_back(std::to_string(i));

    // 2nd: push back b1 (binary form of i)
    std::bitset<16> b1bs(i);

    // convert bitset to string
    std::string b1String = b1bs.to_string();

    // excise the leading zeroes
    std::size_t pos = 16 - k;
    std::string b1SubStr = b1String.substr(pos);
    line.push_back(b1SubStr);

    // reverse bits of b1, store as b2
    std::bitset<16> b2TMP(b1bs);
    std::string b2String = b1SubStr;
    std::reverse(b2String.begin(), b2String.end());
    std::bitset<16> b2(b2String);

    // 3rd: convert b2 to integer ir, push back ir
    unsigned long ir = b2.to_ulong();
    line.push_back(std::to_string(ir));

    // 4th: push back b2
    line.push_back(b2String);

    res.push_back(line);
  }

  return res;
}

The following table was generated using this value with N = 16. The first column contains the original indices from 0 to N-1, the second column contains the binary representation of those indices, the third column contains the decimal representation of those reversed bits, and the fourth column contains the binary representation:

    0 0000    0 0000
    1 0001    8 1000
    2 0010    4 0100
    3 0011   12 1100
    4 0100    2 0010
    5 0101   10 1010
    6 0110    6 0110
    7 0111   14 1110
    8 1000    1 0001
    9 1001    9 1001
   10 1010    5 0101
   11 1011   13 1101
   12 1100    3 0011
   13 1101   11 1011
   14 1110    7 0111
   15 1111   15 1111

After the indices are reversed, the algorithm can be called:

void FFT2(std::complex<double>* input
          , std::complex<double>* output
          , int N)
{
  // Shuffle input with reversed bits
  std::vector<std::vector<std::string>> indices = bitrev(N);
  for (int i = 0; i < N; ++i)
  {
    // 3rd column contains reversed indices
    output[i] = input[std::stoi(indices[i][2])];
  }

  // Keep left shifting until the bitlength of N is reached
  for (int length = 2; length <= N; length <<= 1)
  {
    double angle = 2 * PI / length;
    std::complex<double> wlen(cos(angle), sin(angle));

    // Instead of recursion, sum up adjacent entries in output
    for (int i = 0; i < N; i += length)
    {
      std::complex<double> w(1);
      // Only need to go through half the length (powers of 2)
      for (int j = 0; j < length / 2; ++j)
      {
        std::complex<double> u = output[i + j];
        std::complex<double> v = output[i + j + length / 2] * w;
        output[i + j] = u + v;
        output[i + j + length / 2] = u - v;
        w *= wlen;
      }
    }
  }

  // Reverse bits
  std::complex<double>* temp = new std::complex<double>[N];
  for (int i = 0; i < N; ++i)
  {
    temp[i] = output[i];
  }
  for (int i = 1; i < N; ++i)
  {
    output[i] = temp[N - i];
  }
  delete [] temp;
}

Results

Using the std::chrono header, one can time each of the three implementations and clearly see how much more efficient FFT2 is then the rest:

FUNCTION            TIME
====================================
DFT                 42 microseconds
FFT                 10 microseconds
FFT2                3 microseconds