Skip to content
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

FFTfreq: possible off-by-one error for one-sided FFTs #49

Closed
arthurits opened this issue Jun 15, 2022 · 7 comments · Fixed by #50
Closed

FFTfreq: possible off-by-one error for one-sided FFTs #49

arthurits opened this issue Jun 15, 2022 · 7 comments · Fixed by #50

Comments

@arthurits
Copy link
Contributor

arthurits commented Jun 15, 2022

Could it be possible that there is some offset when computing the sample frequency by calling FftSharp.Transform.FFTfreq()?

These are the steps I've followed and the results obtained:

  • Create a 1-cycle 2-Hz-sinus wave at a sampling rate of 16 Hz (see 16 points 2 Hz sinus .txt) (The offset is less noticeable the higher the sampling frequency).
  • Compute the FFT using:
signalFFT = FftSharp.Transform.FFTmagnitude(signalWindowed);
freq = FftSharp.Transform.FFTfreq(sampleFreq, signalFFT.Length);
plotFFT.Plot.AddScatterLines(freq, signalFFT);
  • The plot shows the frequency of the signal at 1.78 Hz instead of 2 Hz.
    Frequency offset

Possible explanation:

  • The array returned by FftSharp.Transform.FFTmagnitude() has one more point than half its size: new double[input.Length / 2 + 1] (2n-1 +1).
  • Inside function FftSharp.Transform.FFTfreq(), the frequency is computed as:
if (OneSided)
    double fftPeriodHz = sampleRate / pointCount / 2;
else
    double fftPeriodHz = sampleRate / pointCount;
  • The problem is that pointCount is passed as the length of the array returned by FftSharp.Transform.FFTmagnitude() (2n-1 +1), so it has one more point than what is required to compute fftPeriodHz.

So, i can think of two options:

  1. Subtract 1 from the array's length when calling FftSharp.Transform.FFTfreq(). This might be counterintuitive and not obvious.
  2. Perform the subtraction inside FftSharp.Transform.FFTfreq():
if (OneSided)
    double fftPeriodHz = sampleRate / (pointCount-1) / 2;
else
    double fftPeriodHz = sampleRate / (pointCount-1);

This last option produces the correct result for the sinus wave mentioned at the beginning. I hesitated before doing any PR since I'm not sure which one would you consider more appropriate.
Correct frequency

@swharden
Copy link
Owner

Hi @arthurits!

Regarding the two possible options, here are my thoughts:

Option 1: User supplies Length - 1

Subtract 1 from the array's length when calling FftSharp.Transform.FFTfreq(). This might be counterintuitive and not obvious.

I agree with you this would be confusing.

However, an overload that accepts the array itself could achieve this without requiring the user to have special knowledge. Adding this overload may be useful either way.

Option 2:

Perform the subtraction inside FftSharp.Transform.FFTfreq(): This last option produces the correct result for the sinus wave mentioned at the beginning.

This sounds like the way to go.

What does Python do?

When in doubt I often try to mimic what commonly used Python packages do.

It would be ideal to analyze sample data with Python's function and write a test to confirm our values match theirs exactly. If you create a PR you don't have to go through the trouble of doing all that, but I'll probably add it in before merging.

Relevant files (note to self):

Pull Request

I hesitated before doing any PR since I'm not sure which one would you consider more appropriate.

If you want to create a PR, I'd be happy to work on it and merge it in shortly! If not, that's okay too, I'll fix this on my own within the next few days 👍

@swharden swharden changed the title Offset while computing sample frequency in FFTfreq FFTfreq off-by-one error Jun 15, 2022
@arthurits
Copy link
Contributor Author

Thanks for your feedback @swharden! 👍
PR has been created implementing your suggestions, except the Python tests.

@arthurits
Copy link
Contributor Author

I realized that options 1 and 2 can't both be implemented at the same time, so I modified the PR.

Option 1: User supplies Length - 1

However, an overload that accepts the array itself could achieve this without requiring the user to have special knowledge. Adding this overload may be useful either way.

I believe the overload option to be the most practical one. This overload should compute the correct pointCount and call the original FFTfreq.

Option 2:

This sounds like the way to go.

However, the subtraction can't be implemented here because the user could call the overload passing the array which computes the correct length and calls the orginal FFTfreq function where no subtraction should be done.
In this case, the XML function documentation should clearly state what value is expected for the pointCount parameter.

@swharden
Copy link
Owner

Good points! I'm glad you liked the overload idea, and PR #50 is looking great

BTW I found I already implemented "official" Python/numpy fftfreq and comparison, so I think the correct behavior (or at least the common behavior shared with a really big Python library) is already locked-in

[Test]
public void Test_VsNumpy_FftFreq()
{
double[] fftFreq = FftSharp.Transform.FFTfreq(sampleRate: 48_000, pointCount: values.Length, oneSided: false);
double[] numpyFftFreq = LoadData.Double("fftFreq.txt");
Assert.AreEqual(numpyFftFreq.Length, fftFreq.Length);
for (int i = 0; i < fftFreq.Length; i++)
Assert.AreEqual(numpyFftFreq[i], fftFreq[i], 1e-10);
}

0.0
93.75
187.5
281.25
375.0
468.75
562.5
656.25
750.0
843.75
937.5
1031.25
1125.0
1218.75
1312.5
1406.25
1500.0
1593.75
1687.5
1781.25
1875.0
1968.75
2062.5
2156.25
2250.0
2343.75
2437.5
2531.25
2625.0
2718.75
2812.5
2906.25
3000.0
3093.75
3187.5
3281.25
3375.0
3468.75
3562.5
3656.25
3750.0
3843.75
3937.5
4031.25
4125.0
4218.75
4312.5
4406.25
4500.0
4593.75
4687.5
4781.25
4875.0
4968.75
5062.5
5156.25
5250.0
5343.75
5437.5
5531.25
5625.0
5718.75
5812.5
5906.25
6000.0
6093.75
6187.5
6281.25
6375.0
6468.75
6562.5
6656.25
6750.0
6843.75
6937.5
7031.25
7125.0
7218.75
7312.5
7406.25
7500.0
7593.75
7687.5
7781.25
7875.0
7968.75
8062.5
8156.25
8250.0
8343.75
8437.5
8531.25
8625.0
8718.75
8812.5
8906.25
9000.0
9093.75
9187.5
9281.25
9375.0
9468.75
9562.5
9656.25
9750.0
9843.75
9937.5
10031.25
10125.0
10218.75
10312.5
10406.25
10500.0
10593.75
10687.5
10781.25
10875.0
10968.75
11062.5
11156.25
11250.0
11343.75
11437.5
11531.25
11625.0
11718.75
11812.5
11906.25
12000.0
12093.75
12187.5
12281.25
12375.0
12468.75
12562.5
12656.25
12750.0
12843.75
12937.5
13031.25
13125.0
13218.75
13312.5
13406.25
13500.0
13593.75
13687.5
13781.25
13875.0
13968.75
14062.5
14156.25
14250.0
14343.75
14437.5
14531.25
14625.0
14718.75
14812.5
14906.25
15000.0
15093.75
15187.5
15281.25
15375.0
15468.75
15562.5
15656.25
15750.0
15843.75
15937.5
16031.25
16125.0
16218.75
16312.5
16406.25
16500.0
16593.75
16687.5
16781.25
16875.0
16968.75
17062.5
17156.25
17250.0
17343.75
17437.5
17531.25
17625.0
17718.75
17812.5
17906.25
18000.0
18093.75
18187.5
18281.25
18375.0
18468.75
18562.5
18656.25
18750.0
18843.75
18937.5
19031.25
19125.0
19218.75
19312.5
19406.25
19500.0
19593.75
19687.5
19781.25
19875.0
19968.75
20062.5
20156.25
20250.0
20343.75
20437.5
20531.25
20625.0
20718.75
20812.5
20906.25
21000.0
21093.75
21187.5
21281.25
21375.0
21468.75
21562.5
21656.25
21750.0
21843.75
21937.5
22031.25
22125.0
22218.75
22312.5
22406.25
22500.0
22593.75
22687.5
22781.25
22875.0
22968.75
23062.5
23156.25
23250.0
23343.75
23437.5
23531.25
23625.0
23718.75
23812.5
23906.25
-24000.0
-23906.25
-23812.5
-23718.75
-23625.0
-23531.25
-23437.5
-23343.75
-23250.0
-23156.25
-23062.5
-22968.75
-22875.0
-22781.25
-22687.5
-22593.75
-22500.0
-22406.25
-22312.5
-22218.75
-22125.0
-22031.25
-21937.5
-21843.75
-21750.0
-21656.25
-21562.5
-21468.75
-21375.0
-21281.25
-21187.5
-21093.75
-21000.0
-20906.25
-20812.5
-20718.75
-20625.0
-20531.25
-20437.5
-20343.75
-20250.0
-20156.25
-20062.5
-19968.75
-19875.0
-19781.25
-19687.5
-19593.75
-19500.0
-19406.25
-19312.5
-19218.75
-19125.0
-19031.25
-18937.5
-18843.75
-18750.0
-18656.25
-18562.5
-18468.75
-18375.0
-18281.25
-18187.5
-18093.75
-18000.0
-17906.25
-17812.5
-17718.75
-17625.0
-17531.25
-17437.5
-17343.75
-17250.0
-17156.25
-17062.5
-16968.75
-16875.0
-16781.25
-16687.5
-16593.75
-16500.0
-16406.25
-16312.5
-16218.75
-16125.0
-16031.25
-15937.5
-15843.75
-15750.0
-15656.25
-15562.5
-15468.75
-15375.0
-15281.25
-15187.5
-15093.75
-15000.0
-14906.25
-14812.5
-14718.75
-14625.0
-14531.25
-14437.5
-14343.75
-14250.0
-14156.25
-14062.5
-13968.75
-13875.0
-13781.25
-13687.5
-13593.75
-13500.0
-13406.25
-13312.5
-13218.75
-13125.0
-13031.25
-12937.5
-12843.75
-12750.0
-12656.25
-12562.5
-12468.75
-12375.0
-12281.25
-12187.5
-12093.75
-12000.0
-11906.25
-11812.5
-11718.75
-11625.0
-11531.25
-11437.5
-11343.75
-11250.0
-11156.25
-11062.5
-10968.75
-10875.0
-10781.25
-10687.5
-10593.75
-10500.0
-10406.25
-10312.5
-10218.75
-10125.0
-10031.25
-9937.5
-9843.75
-9750.0
-9656.25
-9562.5
-9468.75
-9375.0
-9281.25
-9187.5
-9093.75
-9000.0
-8906.25
-8812.5
-8718.75
-8625.0
-8531.25
-8437.5
-8343.75
-8250.0
-8156.25
-8062.5
-7968.75
-7875.0
-7781.25
-7687.5
-7593.75
-7500.0
-7406.25
-7312.5
-7218.75
-7125.0
-7031.25
-6937.5
-6843.75
-6750.0
-6656.25
-6562.5
-6468.75
-6375.0
-6281.25
-6187.5
-6093.75
-6000.0
-5906.25
-5812.5
-5718.75
-5625.0
-5531.25
-5437.5
-5343.75
-5250.0
-5156.25
-5062.5
-4968.75
-4875.0
-4781.25
-4687.5
-4593.75
-4500.0
-4406.25
-4312.5
-4218.75
-4125.0
-4031.25
-3937.5
-3843.75
-3750.0
-3656.25
-3562.5
-3468.75
-3375.0
-3281.25
-3187.5
-3093.75
-3000.0
-2906.25
-2812.5
-2718.75
-2625.0
-2531.25
-2437.5
-2343.75
-2250.0
-2156.25
-2062.5
-1968.75
-1875.0
-1781.25
-1687.5
-1593.75
-1500.0
-1406.25
-1312.5
-1218.75
-1125.0
-1031.25
-937.5
-843.75
-750.0
-656.25
-562.5
-468.75
-375.0
-281.25
-187.5
-93.75

@swharden
Copy link
Owner

correct behavior (or at least the common behavior shared with a really big Python library) is already locked-in

Actually, this discussion has been about one-sided FFT frequency calculation. That's not the default behavior of Python/numpy, and the one-sided behavior hasn't been sufficiently tested, so it's still worth this closer inspection

@swharden swharden changed the title FFTfreq off-by-one error FFTfreq: possible off-by-one error for one-sided FFTs Jun 16, 2022
swharden added a commit to arthurits/FftSharp that referenced this issue Jun 16, 2022
swharden added a commit to arthurits/FftSharp that referenced this issue Jun 16, 2022
swharden added a commit to arthurits/FftSharp that referenced this issue Jun 16, 2022
@swharden swharden linked a pull request Jun 16, 2022 that will close this issue
@swharden
Copy link
Owner

Wow, that got way more complicated than I expected!

This fix modifies the math of FftFreq() only for half-size FFTs so it doesn't affect the full FFT math.

This is the new behavior. What do you think? If you like it I'll merge and publish a new package 🚀

// generate signal with 2 Hz sine wave
int sampleCount = 16;
double sampleRateHz = 16;
double sinFrequencyHz = 2;
double[] samples = Enumerable.Range(0, sampleCount)
    .Select(i => Math.Sin(2 * Math.PI * sinFrequencyHz * i / sampleRateHz))
    .ToArray();

// get FFT
double[] fft = FftSharp.Transform.FFTmagnitude(samples);
double[] fftKnown = { 0, 0, 1, 0, 0, 0, 0, 0, 0 };
Assert.That(fft, Is.EqualTo(fftKnown).Within(1e-10));

// calculate FFT frequencies both ways
double[] fftFreqKnown = { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
double[] fftFreq = FftSharp.Transform.FFTfreq(sampleRateHz, fft.Length); // <---- original overload
double[] fftFreq2 = FftSharp.Transform.FFTfreq(sampleRateHz, fft); // <---- new overload
Assert.That(fftFreq, Is.EqualTo(fftFreqKnown));
Assert.That(fftFreq2, Is.EqualTo(fftFreqKnown));

Test_FftFreq_KnownValues_signal
Test_FftFreq_KnownValues_fft

@arthurits
Copy link
Contributor Author

arthurits commented Jun 16, 2022

This looks really good! Documentation of the functions is clear enough and the point about the one-sided FFT is just on the spot.
I'd say it's ready to be merged.
Nice work! 🤩

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants