Skip to content

BUG: wrong frequency wrapping in fft.fftfreq for even n #29162

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
georgeef opened this issue Jun 10, 2025 · 5 comments
Closed

BUG: wrong frequency wrapping in fft.fftfreq for even n #29162

georgeef opened this issue Jun 10, 2025 · 5 comments
Labels
00 - Bug 57 - Close? Issues which may be closable unless discussion continued

Comments

@georgeef
Copy link

georgeef commented Jun 10, 2025

Describe the issue:

Problem

fft.fftfreq(n, ts) where

  • n is the number of samples
  • ts is the sampling time (step)

generates a vector of n frequencies in the range [-fs/2, fs/2), where fs=1/ts is the sampling frequency. The generated frequencies should be in the range (-fs/2, fs/2], i.e., if one of the frequencies is fs/2 (which happens when n is even), it should have a positive sign.

Fix

The following code gives the original source code (with cosmetic changes), the fixed code, and an example where the two functions give different result. The fix is in the calculation of m (first line in each function), which differs when n is even.

def fftfreq(n, ts):
    m = (n - 1) // 2 + 1
    f = np.empty(n, dtype=int)
    # note: m - n == -(n//2)
    f[:m] = np.arange(0,     m, dtype=int)
    f[m:] = np.arange(m - n, 0, dtype=int)
    return f / (n * ts)

def fftfreq_fix(n, ts):
    m = n // 2 + 1
    f = np.empty(n, dtype=int)
    f[:m] = np.arange(0,     m, dtype=int)
    f[m:] = np.arange(m - n, 0, dtype=int)
    return f / (n * ts)

def test(n):
    # normalize such that frequencies are integer numbers
    ts = 1/n  # sampling time (step)
    fs = 1/ts # sampling frequency
    print('frequencies in [-fs/2, fs/2) :', fftfreq(n, ts))
    print('frequencies in (-fs/2, fs/2] :', fftfreq_fix(n, ts))

test(6)
  frequencies in [-fs/2, fs/2) : [ 0.  1.  2. -3. -2. -1.]
  frequencies in (-fs/2, fs/2] : [ 0.  1.  2.  3. -2. -1.]

Notice that the frequency fs/2 = 3 is wrapped to -3, but it shouldn't.

Theory

For an even n, a sequence of n real numbers is transformed by FFT to a sequence of n complex numbers, of which the last n/2 - 1 numbers are redundant because they are complex conjugates of corresponding number in the first part. The remaining n/2 + 1 numbers are all necessary to reconstruct the original real signal. They represent n + 2 real numbers, but the imaginary part of the first (index 0) and the last (index n/2) are always 0, so they actually represent n independent real numbers.

For an odd n, the last (n-1)/2 transformed numbers are redundant, and of the remaining (n+1)/2 only the first always has zero imaginary part, so in total they represent n independent real numbers.

Impact

For an even n (which is the most common use case), when the FFT of a signal is plotted using fft.fftfreq(), the maximum frequency fs/2 is hidden. For a realistic sampling process, the sampled signal (e.g., audio) doesn't have frequency content at fs/2, since it has been filtered before sampling. However fft.fftfreq() should not make any assumption about the conditioning of the original signal.

Example: plot the FFT as in the documentation examples, with an even number of samples, and a signal which contains the following tone

  • cos(2*np.pi*(fs/2)*t) = cos(np.pi*t/ts) = [+1, -1, +1, ..., -1]

which is an alternating signal (after sampling). The plot should show a tone at fs/2, but this tone is removed by fft.fftfreq(). Notice that replacing cos by sin in the above example does not show the problem, because the signal is 0.

Reproduce the code example:

See description.

Error message:

Python and NumPy Versions:

numpy 2.3.0
python 3.13.3

Runtime Environment:

No response

Context for the issue:

No response

@DietBru
Copy link

DietBru commented Jun 10, 2025

It's a matter of (documented) convention (consult also this comment): Due to the periodicity of sampled spectra, the FFT values at ±n/2 are always equal, if n is even. This can easily be shown by looking at the corresponding complex exponential:

In [1]: import sympy as sy

In [2]: n = sy.symbols('n', integer=True)

In [3]: [sy.simplify(sy.exp(sy.I*2*sy.pi*k/n)) for k in (n/2, -n/2)]
Out[3]: [-1, -1]

This convention probably goes back to Matlab: Adapted from an example from Matlab's fft page (using Octave Online):

> L = 6; Fs = 6
Fs = 6
> Fs/L*(-L/2:L/2-1)
ans =

  -3  -2  -1   0   1   2

Note that this ambiguity can bite you, when utilizing the FFT for resampling signals. Consult the example section of SciPy's resample for a short discussion.

@seberg seberg added the 57 - Close? Issues which may be closable unless discussion continued label Jun 10, 2025
@georgeef
Copy link
Author

georgeef commented Jun 10, 2025

Thank you for the link, seems that this issue was spotted long time ago but it was not fixed.

Yes, you can think of the frequency at fs/2 either as +fs/2 or -fs/2, it doesn't matter because the transformed signal is periodic with period fs. The problem is that fftfreq (alone or together with fftshift, which has the same issue) are used to plot one side of the spectrum, since the other side is symmetric for real signals. See the example in https://docs.scipy.org/doc/scipy/tutorial/fft.html, which is a typical use of fftfreq.

In this common use, the point fs/2 is lost because fftfreq makes it negative, and of course the easiest next step is to plot only the segment with non-negative frequencies.

In this respect, it is not a matter of convention. Of course one can easily plot the spectrum including the missing point, either with further frequency wrapping or re-calculating the frequencies points of interest (which is pretty easy), but then what is the whole purpose of fftfreq (and fftshift)? It is usable to plot the whole spectrum, but not usable for one-sided spectrum.

[Out of topic: in most cases the signal is real, but when it is complex, I personally prefer to plot the spectrum from 0 to fs and see the (non-)symmetry around fs/2. So fftfreq is useless for me also in this case.]

@georgeef
Copy link
Author

@seberg
No, please don't close. It is not a matter of convention, it is about the usability of this function. I added more explanations.

@DietBru
Copy link

DietBru commented Jun 10, 2025

If you are interested only in the non-negative frequencies, just use rfftreq (and rfft)—it returns +n/2 instead of -n/2 for even n:

In [1]: import numpy as np

In [2]: n = 6
   ...: x = np.ones(n)  # n-sample input signal

In [3]: X = np.fft.fft(x)  # two-sided FFT
   ...: f_onesided = np.fft.rfftfreq(n, 1/n)
   ...: X_onesided = X[:len(f_onesided)]  # only non-negative frequency bins

In [4]: f_onesided
Out[4]: array([0., 1., 2., 3.])

The problem is that if you want to change the behavior of fftfreq, you'd potentially break the source code of tens of thousands of programs (as this Github search shows). If you have the need for special requirements, you can easily reimplement your own function, since it is only ten lines long.

@georgeef
Copy link
Author

Thanks, rfftfreq is good for the purpose. fftfreq is fine as it is for two-sided spectrum.

I create the frequencies myself anyway (it is 1-2 lines of code), but I was surprised by the example in https://docs.scipy.org/doc/scipy/tutorial/fft.html. It should use rfftfreq and the correct limits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
00 - Bug 57 - Close? Issues which may be closable unless discussion continued
Projects
None yet
Development

No branches or pull requests

3 participants