4

I have a little script for calculating the Fourier Transform of a square wave which works well and returns the square wave correctly when I invert the fft using numpy.fft.ifft(). However, I am unable to invert the transform by manually adding up harmonics after multiplying them by their respective coefficients that I obtain from numpy.fft.fft() Below is my script and I believe you'll see my intention.

from numpy import zeros, concatenate, sin, pi, linspace
from numpy.fft import fft, fftfreq, ifft
import numpy as np
import matplotlib.pyplot as plt

N = 1024 # samples
T = 1 # period
dt = T/N # sampling period
fs = 1/dt # sampling frequency
t = linspace(0, T, N) # time points
functime = .... # square wave

funcfft = fft(functime) # fft
fftcoeffs = np.abs(funcfft)/N # coefficients, divide by N to get actual coeff.s(I believe?)
freqs = fftfreq(N, dt) # frequencies

plt.plot(freqs, fftcoeffs) # gives me reasonable output
plt.show()

FF = ifft(funcfft)
plt.plot(t, FF) # plots exactly the same function as functime defined above
plt.show()

All is well until this far. Now my question is, shouldn't I converge to the original function if I run the below script after the above script?:

FFF = zeros(N)
for i in range(300):
    FFF += fftcoeffs[i]*sin(2*pi*freqs[i]*t)
plt.plot(t, FFF)
plt.show()

Assume that range(300) is good enough for convergence. Now when I do this, FFF is different than my original function. I thought that if I multiplied harmonics of respective frequencies by their corresponding coefficients, which I think are stored in fftcoeffs, I would then converge to the original function. What am I doing wrong?

Update: According to DanielSank's suggestions, I have updated my for loop as below, which unfortunately did not give me the desired results:

freqs2 = np.abs(freqs)
freqs2 = np.sort(freqs2)
for k in range(300):
    FFF += fftcoeffs[k]*exp(2j*pi*freqs2[k]*t/N)

I am not sure if I am doing the "sort fftfreq by absolute value" part right here.

Deniz
  • 509
  • 7
  • 26
  • You're not sorting the frequencies correctly :D Also, in general, when you post code on any online forum, please include a self contained working example (SCWE). This is considered standard practice. Not doing this will very, very often make someone who would otherwise give you an answer just ignore the post instead. – DanielSank Nov 29 '15 at 07:16

2 Answers2

7

Terminology

Nothing in this question is specific to the fast Fourier transform (FFT). The FFT is a particular algorithm for computing the discrete Fourier transform (DFT), so I'm going to say "DFT" instead of "FFT".

In this answer, m indicates a discrete time index and k indicates a discrete frequency index.

What is the DFT?

There are several problems here, all of which are of mathematical nature coming from misunderstanding how the DFT works. Taken from the numpy.fft module docstring, numpy defines the discrete Fourier transform as

A_k = \sum_{m=0}^{n-1} a_m \exp[-2 \pi i (m k / n)]

That's LaTeX notation saying that the discrete Fourier transform is a linear combination of complex exponentials exp[2 pi i m k / n] where n is the total number of points and m is the discrete time index. In your notation this would be exp[2 pi i m k / N] because you're using N to mean the total number of points.

Use exp, not sine

Note that the DFT uses exponentials; these are not sine functions. If you want to build up the time domain signal from the discrete Fourier transform coefficients, you have to use the same functions that the DFT itself used! Therefore, you could try this:

FFF = zeros(N)
for i in range(300):
    FFF += fftcoeffs[i]*np.exp(2*pi*i*freqs[i]*t)
plt.plot(t, FFF)
plt.show()

However, that, too, will fail in a way which might puzzle you.

Aliasing

The final puzzle piece has to do with an effect called aliasing. Suppose you take the DFT of a signal exp[2 pi i (N + p) m / N]. If you compute it, you find that all of the A_k are zero except for A_p. In fact, you get the same thing that you would get if you took the DFT of exp[2 pi i p m / N]. You can see that any complex exponential with frequency larger than N shows up as if it were a lower frequency. In particular, any complex exponential with frequency q + b N where b is any integer looks like it has frequency q.

Now suppose we have a time domain signal cos(2 pi p m / N). That's equal to

(1/2)[ (exp(2 pi i p m / N) + exp(-2 pi i p m / N) ].

That negative frequency has interesting consequences for the DFT. The frequency -p can be written as (N-p) + N. This has the form q + b N with q = N - p and b=1. So, that negative frequency -p winds up looking like N - p!

The numpy function fftfreq knows this. Take at look at the output of fftfreq and you'll see that it starts at zero, runs up to half the sampling frequency (called the Nyquist frequency), and then goes negative! This is to help you deal with the aliasing effect we just described.

The upshot of all this, is that if you want to approximate a function by summing the lowest frequency Fourier components, you do not want to take the lowest few elements from fftfreq. Instead, you want to sort fftfreq by absolute value and then sum up complex exponentials with those frequencies.

Also take a look at np.fft.hfft. This function is designed to handle real valued functions and the aliasing issues associated with them.

Code

Since this is a fairly difficult issue to discuss verbally, here's a script which does exactly what you want. Note that I put comments after the block of code those comments describe. Make sure you have matplotlib installed (in your virtual environment... you use virtual environments, right?). If you have questions leave a comment.

from __future__ import division
import numpy as np
import matplotlib.pyplot as plt


pi = np.pi


def square_function(N, square_width):
    """Generate a square signal.

    Args:
        N (int): Total number of points in the signal.
        square_width (int): Number of "high" points.

    Returns (ndarray):
        A square signal which looks like this:

              |____________________
              |<-- square_width -->
              |                    ______________
              |
              |^                   ^            ^
        index |0             square_width      N-1

        In other words, the output has [0:N]=1 and [N:]=0.
    """
    signal = np.zeros(N)
    signal[0:square_width] = 1
    return signal


def check_num_coefficients_ok(N, num_coefficients):
    """Make sure we're not trying to add more coefficients than we have."""
    limit = None
    if N % 2 == 0 and num_coefficients > N // 2:
        limit = N/2
    elif N % 2 == 1 and num_coefficients > (N - 1)/2:
        limit = (N - 1)/2
    if limit is not None:
        raise ValueError(
            "num_coefficients is {} but should not be larger than {}".format(
                num_coefficients, limit))


def test(N, square_width, num_coefficients):
    """Test partial (i.e. filtered) Fourier reconstruction of a square signal.

    Args:
        N (int): Number of time (and frequency) points. We support both even
            and odd N.
        square_width (int): Number of "high" points in the time domain signal.
            This number must be less than or equal to N.
        num_coefficients (int): Number of frequencies, in addition to the dc
            term, to use in Fourier reconstruction. This is the number of
            positive frequencies _and_ the number of negative frequencies.
            Therefore, if N is odd, this number cannot be larger than
            (N - 1)/2, and if N is even this number cannot be larger than
            N/2.
    """
    if square_width > N:
        raise ValueError("square_width cannot be larger than N")
    check_num_coefficients_ok(N, num_coefficients)

    time_axis = np.linspace(0, N-1, N)

    signal = square_function(N, square_width)
    ft = np.fft.fft(signal)

    reconstructed_signal = np.zeros(N)
    reconstructed_signal += ft[0] * np.ones(N)
    # Adding the dc term explicitly makes the looping easier in the next step.

    for k in range(num_coefficients):
        k += 1  # Bump by one since we already took care of the dc term.
        if k == N-k:
            reconstructed_signal += ft[k] * np.exp(
                1.0j*2 * pi * (k) * time_axis / N)
        # This catches the case where N is even and ensures we don't double-
        # count the frequency k=N/2.

        else:
            reconstructed_signal += ft[k] * np.exp(
                1.0j*2 * pi * (k) * time_axis / N)
            reconstructed_signal += ft[N-k] * np.exp(
                1.0j*2 * pi * (N-k) * time_axis / N)
        # In this case we're just adding a frequency component and it's
        # "partner" at minus the frequency

    reconstructed_signal = reconstructed_signal / N
    # Normalize by the number of points in the signal. numpy's discete Fourier
    # transform convention puts the (1/N) normalization factor in the inverse
    # transform, so we have to do it here.

    plt.plot(time_axis, signal,
             'b.', markersize=20,
             label='original')
    plt.plot(time_axis, reconstructed_signal.real,
             'r-', linewidth=3,
             label='reconstructed')
    # The imaginary part is zero anyway. We take the real part to
    # avoid matplotlib warnings.

    plt.grid()
    plt.legend(loc='upper right')
DanielSank
  • 3,303
  • 3
  • 24
  • 42
  • Thank you for your response. Firstly, I really have no idea how I can define `i` a.k.a. `-1**0.5`. Apparently `-1**0.5` doesn't define it. But anyway, I did `freqs2 = np.abs(freqs)` and then `freqs2 = np.sort(freqs2)` then updated my for loop as you suggested, using `exp` instead of `sin`. I used `k` as the for loop variable instead of `i` so that it doesn't get mixed up with the imaginary `i` there. I guess you forgot to include `/N` in `exp()`, I tried with and without it but I got worse results in all of my tries. I doubt that I fully understood your suggestions. – Deniz Nov 28 '15 at 20:39
  • @Deniz It's really hard to understand a written description of code. Can you just edit the original post to show what you've tried? – DanielSank Nov 29 '15 at 06:19
  • I've just seen your update to this post and tried out your script. It works flawlessly. In my case, the square(actually rectangle) signal of one period isn't like a symmetric +a/-a kind of square signal so I'll have to adapt this to my case but still, your code helps me understand the procedure and theory of DFT greatly. Thank you very much for your effort in helping me Daniel. I am selecting this answer as the correct answer. – Deniz Nov 29 '15 at 10:58
  • @Deniz I'm glad I could help. I have found the literature on signal processing to be unsatisfactory, so I wrote my own set of notes on these topics. I am happy to give you a copy if you like. You can contact me if you look at my physics stack exchange user profile and follow the link to my professional website. – DanielSank Nov 29 '15 at 11:02
  • @Deniz please also note the existence of the [signal processing stack exchange](http://dsp.stackexchange.com/). – DanielSank Nov 29 '15 at 11:02
  • For a weird reason I seem to be not able to `@DanielSank` you. Anyway, and yes thank you for the offer, I am surely interested in seeing your notes and for which I will be contacting you later. Thanks for letting me know of that stack exchange too. – Deniz Nov 29 '15 at 11:06
  • 2
    @Deniz You can't @ me beacuse you're commenting on my post. A user automatically gets notifications on comments on their own post. Also, I wanted to draw your attention to the fact that I provided you a complete working code sample and it helped you. Please do this in your questions as well. Questions are much easier to understand with an example; describing code is almost useless. Also, writing a minimal example makes you solve your own problem 99% of the time. – DanielSank Nov 29 '15 at 11:09
0

The biggest problem is you used only the magnitude (np.abs) of your FFT results, thus throwing away all the phase information.

If you keep the complex phase results from the FFT, and use that phase information in your sinewave re-synthesis, your manual addition of the harmonics will be a lot closer to the original.

Possible hint: You may have to do an fftshift of the waveform before the FFT to get useful phase results, depending on the frequency of the square-wave versus the FFT aperture width.

hotpaw2
  • 70,107
  • 14
  • 90
  • 153
  • Thank you for your answer. Using the same for loop I have given above with the only exception of using `funcfft` instead of `fftcoeffs`, I have got worse results. With my current code, what it gives me is totally wrong but it's a square, when I do not use `fftcoeffs` it gives me exponentials rather than squares. – Deniz Nov 28 '15 at 20:46
  • To get a real result from complex exponentials, all the frequencies have to mirrored with complex conjugates. Note the the full complex FFT result is also mirrored with complex conjugates. You need all of those conjugates to get an inverse. – hotpaw2 Nov 28 '15 at 21:38