1

Note: This question is building on another question of mine: Two dimensional FFT using python results in slightly shifted frequency

I have some data, basically a function E(x,y) with (x,y) being a (discrete) subset of R^2, mapping to real numbers. For the (x,y) plane i have a fixed distance between data points in x- as well as in y direction (0,2). I want to analyze the frequency spectrum of my E(x,y) signal using a two dimensional fast fourier transform (FFT) using python.

As far as i know, no matter which frequencies are actually contained in my signal, using FFT, i will only be able to see signals below the Nyquisit limit Ny, which is Ny = sampling frequency / 2. In my case i have a real spacing of 0,2, leading to a sampling frequency of 1 / 0,2 = 5 and therefore my Nyquisit limit is Ny = 5 / 2 = 2,5.

If my signal does have frequencies above the Nyquisit limit, they will be "folded" back into the Nyquisit domain, leading to false results (aliasing). But even though i might sample with a too low frequency, it should in theory not be possible to see any frequencies above the Niquisit limit, correct?

So here is my issue: Analyzing my signal should only lead to frequencies of 2,5 max., but i cleary get frequencies higher than that. Given that i am pretty sure about the theory here, there has to be some mistake in my code. I will provide a shortened code version, only providing necessary information for this issue:

simulationArea =...  # length of simulation area in x and y direction
x = np.linspace(0, simulationArea, numberOfGridPointsInX, endpoint=False)
y = x
xx, yy = np.meshgrid(x, y)
Ex = np.genfromtxt('E_field_x100.txt')  # this is the actual signal to be analyzed, which may have arbitrary frequencies
FTEx = np.fft.fft2(Ex)  # calculating fft coefficients of signal
dx = x[1] - x[0]  # calculating spacing of signals in real space. 'print(dx)' results in '0.2'

sampleFrequency = 1.0 / dx
nyquisitFrequency = sampleFrequency / 2.0
half = len(FTEx) / 2

fig, axarr = plt.subplots(2, 1)

im1 = axarr[0, 0].imshow(Ex,
                         origin='lower',
                         cmap='jet',
                         extent=(0, simulationArea, 0, simulationArea))
axarr[0, 0].set_xlabel('X', fontsize=14)
axarr[0, 0].set_ylabel('Y', fontsize=14)
axarr[0, 0].set_title('$E_x$', fontsize=14)
fig.colorbar(im1, ax=axarr[0, 0])

im2 = axarr[1, 0].matshow(2 * abs(FTEx[:half, :half]) / half,
                          aspect='equal',
                          origin='lower',
                          interpolation='nearest')
axarr[1, 0].set_xlabel('Frequency wx')
axarr[1, 0].set_ylabel('Frequency wy')
axarr[1, 0].xaxis.set_ticks_position('bottom')
axarr[1, 0].set_title('$FFT(E_x)$', fontsize=14)
fig.colorbar(im2, ax=axarr[1, 0])

The result of this is:

enter image description here

How is that possible? When i am using the same code for very simple signals, it works just fine (e.g. a sine wave in x or y direction with a specific frequency).

Ahmed Fasih
  • 6,458
  • 7
  • 54
  • 95
Alienbash
  • 554
  • 7
  • 17
  • The axes of the bottom plot are just pixels, not frequencies!!! There are also several conventions you need to know about using 2D FFT, such as how to build the vector of X and Y frequencies, etc., but in this answer, I give a very simple example using a complex exponential and 2D FFT but in Matlab: https://stackoverflow.com/a/39774496/500207 see if you can adapt this to Python and if not, let me know and I'll port it. – Ahmed Fasih Aug 04 '17 at 03:17
  • In Python it’s a bit easier because Numpy provides the `fftfreq` function. If you can upload some (real or fake) data for `Ex` and a complete set of values for `simulationArea` etc., it’ll be easy and convincing to show you what this should look like. – Ahmed Fasih Aug 04 '17 at 03:28
  • Thank you for your answer! Referring to your answer in stackoverflow.com/a/39774496/500207, how would i use 'fftreq' in python correctly, in order to obtain the appropriate frequency spaces for x and y? I guess it can be used to convert 'Nfft = 4 * 2 .^ nextpow2(size(im)); imF = fftshift(fft2(im, Nfft(1), Nfft(2))) / numel(im);' into python code. – Alienbash Aug 04 '17 at 12:12

1 Answers1

3

Ok here we go! Here’s a couple of simple functions and a complete example that you can use: it’s got a little bit of extra cruft related to plotting and for data generation but the first function, makeSpectrum shows you how to use fftfreq and fftshift plus fft2 to achieve what you want. Let me know if you have questions.

import numpy as np
import numpy.fft as fft
import matplotlib.pylab as plt


def makeSpectrum(E, dx, dy, upsample=10):
    """
    Convert a time-domain array `E` to the frequency domain via 2D FFT. `dx` and
    `dy` are sample spacing in x (left-right, 1st axis) and y (up-down, 0th
    axis) directions. An optional `upsample > 1` will zero-pad `E` to obtain an
    upsampled spectrum.

    Returns `(spectrum, xf, yf)` where `spectrum` contains the 2D FFT of `E`. If
    `Ny, Nx = spectrum.shape`, `xf` and `yf` will be vectors of length `Nx` and
    `Ny` respectively, containing the frequencies corresponding to each pixel of
    `spectrum`.

    The returned spectrum is zero-centered (via `fftshift`). The 2D FFT, and
    this function, assume your input `E` has its origin at the top-left of the
    array. If this is not the case, i.e., your input `E`'s origin is translated
    away from the first pixel, the returned `spectrum`'s phase will *not* match
    what you expect, since a translation in the time domain is a modulation of
    the frequency domain. (If you don't care about the spectrum's phase, i.e.,
    only magnitude, then you can ignore all these origin issues.)
    """
    zeropadded = np.array(E.shape) * upsample
    F = fft.fftshift(fft.fft2(E, zeropadded)) / E.size
    xf = fft.fftshift(fft.fftfreq(zeropadded[1], d=dx))
    yf = fft.fftshift(fft.fftfreq(zeropadded[0], d=dy))
    return (F, xf, yf)


def extents(f):
    "Convert a vector into the 2-element extents vector imshow needs"
    delta = f[1] - f[0]
    return [f[0] - delta / 2, f[-1] + delta / 2]


def plotSpectrum(F, xf, yf):
    "Plot a spectrum array and vectors of x and y frequency spacings"
    plt.figure()
    plt.imshow(abs(F),
               aspect="equal",
               interpolation="none",
               origin="lower",
               extent=extents(xf) + extents(yf))
    plt.colorbar()
    plt.xlabel('f_x (Hz)')
    plt.ylabel('f_y (Hz)')
    plt.title('|Spectrum|')
    plt.show()


if __name__ == '__main__':
    # In seconds
    x = np.linspace(0, 4, 20)
    y = np.linspace(0, 4, 30)
    # Uncomment the next two lines and notice that the spectral peak is no
    # longer equal to 1.0! That's because `makeSpectrum` expects its input's
    # origin to be at the top-left pixel, which isn't the case for the following
    # two lines.
    # x = np.linspace(.123 + 0, .123 + 4, 20)
    # y = np.linspace(.123 + 0, .123 + 4, 30)

    # Sinusoid frequency, in Hz
    x0 = 1.9
    y0 = -2.9

    # Generate data
    im = np.exp(2j * np.pi * (y[:, np.newaxis] * y0 + x[np.newaxis, :] * x0))

    # Generate spectrum and plot
    spectrum, xf, yf = makeSpectrum(im, x[1] - x[0], y[1] - y[0])
    plotSpectrum(spectrum, xf, yf)

    # Report peak
    peak = spectrum[:, np.isclose(xf, x0)][np.isclose(yf, y0)]
    peak = peak[0, 0]
    print('spectral peak={}'.format(peak))

Results in the following image, and prints out, spectral peak=(1+7.660797103157986e-16j), which is exactly the correct value for the spectrum at the frequency of a pure complex exponential.

Result

Ahmed Fasih
  • 6,458
  • 7
  • 54
  • 95
  • You're great, thank you very much ;-) One last question: In a last step, i was using your code example from above and replaced "x = np.linspace(0, 4, 20); y = np.linspace(0, 4, 30)" with " x = np.linspace(0, simulationArea, numberOfGridPointsInX, endpoint = False); y = x" and of course the exp() with my real data E (real physical data). The result looks good, since it is not showing frequencies above the Nyquisit limit anymore. But it's also showing negative frequencies and i have (in contrast to your example) a purely real signal. How do i get rid of those redundant negative frequencies then? – Alienbash Aug 05 '17 at 12:11
  • So i was trying to solve the issue of negative frequencies by taking your example with a purely real signal: "np.sin(2 * np.pi * (y[:, np.newaxis] * y0)) * np.sin(2 * np.pi * x[:, np.newaxis] * x0)" as time domain function and then inside of "plotSpectrum()" using: "imshow( 2 * abs(F[:half, :half])..." and then "plt.xlim(0, max(extents(xf)))" and "plt.xlim(0, max(extents(xf)))", but unfortunately this results in basically showing me nothing (just very weak frequencies, which are showing up periodically, so definitely not what i want) – Alienbash Aug 05 '17 at 12:46
  • Also knowing that the FT of a gaussian results in another gaussian function, i used "im = np.exp(-(np.power(x[:, np.newaxis] - mu, 2.) + np.power(y[:, np.newaxis] - mu, 2.)) / (2 * np.power(sig, 2.)))", with "mu = 0.5" and "sig = 1", from which one can see, that it does not result in a gaussian. What adjustments have to be done here for real signals? – Alienbash Aug 05 '17 at 15:12
  • About real signals. If you plot a 2D `sin`, you’ll get two peaks right, one at frequency `(x0, y0)` with amplitude 1/2j and the other at frequency `(-x0, -y0)` with amplitude -1/2j, so to see both, you’ll want to retain the full frequency extent. Yes, it’s true that for a real signal, there is redundancy, like the docstring for `rfftn` (or `rfft2`) explains: you can hide half the frequency plane, assuming that the other half is the conjugate-transpose of the half you do show. – Ahmed Fasih Aug 05 '17 at 20:28
  • About 2D Gaussian: you want to make sure you capture enough of the Gaussian’s behavior in the time-domain. Using the following time-domain limits, I get a nice Gaussian spectrum: ` x = np.linspace(-6, 6, 500); y = np.linspace(-6, 6, 500); im = np.exp(-(x[np.newaxis, :]**2 + y[:, np.newaxis]**2) / 2)`. – Ahmed Fasih Aug 05 '17 at 22:12
  • To finish the discussion about real signals: If i am just using the code you posted in your answer, but instead inserting a real signal, i.e. 'iml = np.sin(2 * np.pi * yy * y0) * np.sin(2 * np.pi * xx * x0)', with frequencies x0 = 10.0, and y0 = 15, i do not get peaks at (x0, y0) and (-x0, y0), but instead i get horizontal peaks (parallel to f_x- axis) at f_y = 5 ( = y0 - x0) and f_y = 25 (= x0 + y0), which means something detail seems to go still wrong with real signals here. – Alienbash Aug 06 '17 at 10:11
  • Can you post your full code on gist.github.com or somewhere, so I can see what `xx` and it's limits are? – Ahmed Fasih Aug 06 '17 at 10:22
  • Of course: https://gist.github.com/Alienbash/55b882fa7bd49f3aa84212d35bbf6b7b That is using real signals, showing wrong frequency peaks. Really appreciate your help! – Alienbash Aug 06 '17 at 10:33
  • 1
    I think you have a bug in your code: `im` is a `(500,1)` vector instead of `(500, 500)` array, because you *need* `x[np.newaxis, :]`, but you have it the other way around. Note carefully how in my example I have `x[np.newaxis, :]` vs `y[:, np.newaxis]`, how the arguments inside the brackets are carefully selected to expand `x` into a 2D *row* vector and `y` into a 2D *column* vector, so that the two [broadcast](https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html) into a 2D 500 by 500 array. In summary—rerun with `x[np.newaxis, :]` . – Ahmed Fasih Aug 07 '17 at 00:02
  • You are absolutely correct, thanks for pointing that out! I guess the fact that the amplitude of this FFT signal is not equal to 1, but rather 0,225 is due to spectral leakage, which comes from cutting the (infinite) signal to a finite signal, is that correct? So for "real" data i just have to deal with the fact, that the FFT amplitudes do not match the "original" amplitudes from the input signal? – Alienbash Aug 08 '17 at 20:38
  • When you say the peak amplitude is 0.225, do you mean for your `sin(yy * ...) * sin(xx * ...)` example? The DFT of that signal is *four* Dirac deltas in the frequency domain, each with amplitude 0.25! Recall that `sin(x) * sin(y) = (exp(-i x) - exp(i x)) * (exp(-i y) - exp(i y)) / 4` via Euler identities. The max amplitude is 0.225 vs 0.25 probably because the frequency grid doesn’t exactly include the exact frequencies of the peaks. If you increase the `upsample` to 100, you’ll see the max-amplitude going closer to 0.25. Spectral leakage isn’t playing a role in the diminished amplitudes. – Ahmed Fasih Aug 08 '17 at 23:57
  • Another way to check the peak amplitude is to fit a quadratic polynomial around the peaks and find that polynomial’s maximum—should be 0.25, assuming we’re talking about `np.sin(2 * np.pi * yy * y0) * np.sin(2 * np.pi * xx * x0)`. That is a bit more complicated of a signal than I try to deal with in these sanity-check examples. The simplest real-only simplification of the original complex example from my answer would be: `np.sin(2 * np.pi * (y[:, np.newaxis] * y0 + x[np.newaxis, :] * x0))`, which has two peaks of amplitude 0.5, one at (+x0, +y0) and another at (-x0, -y0). – Ahmed Fasih Aug 09 '17 at 00:01
  • The reason spectral leakage isn’t playing a role here is because, yes, although your data is a finite sequence, the FFT projects it onto a set of sinusoids which are *also* finite sequences, so the finite-vs-infinite mismatch isn’t a problem (so long as your examples are linear combinations of pure sinusoids). Makes sense? – Ahmed Fasih Aug 09 '17 at 00:03