// Copyright ©2018 The Gonum Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package fourier import ( "fmt" "math/rand/v2" "reflect" "testing" "gonum.org/v1/gonum/floats" ) func TestFFT(t *testing.T) { t.Parallel() const tol = 1e-10 rnd := rand.New(rand.NewPCG(1, 1)) t.Run("NewFFT", func(t *testing.T) { for n := 1; n <= 200; n++ { fft := NewFFT(n) want := make([]float64, n) for i := range want { want[i] = rnd.Float64() } coeff := fft.Coefficients(nil, want) got := fft.Sequence(nil, coeff) floats.Scale(1/float64(n), got) if !floats.EqualApprox(got, want, tol) { t.Errorf("unexpected result for sequence(coefficients(x)) for length %d", n) } } }) t.Run("Reset FFT", func(t *testing.T) { fft := NewFFT(1000) for n := 1; n <= 2000; n++ { fft.Reset(n) want := make([]float64, n) for i := range want { want[i] = rnd.Float64() } coeff := fft.Coefficients(nil, want) got := fft.Sequence(nil, coeff) floats.Scale(1/float64(n), got) if !floats.EqualApprox(got, want, tol) { t.Errorf("unexpected result for sequence(coefficients(x)) for length %d", n) } } }) t.Run("known FFT", func(t *testing.T) { // Values confirmed with reference to numpy rfft. fft := NewFFT(1000) cases := []struct { in []float64 want []complex128 }{ { in: []float64{1, 0, 1, 0, 1, 0, 1, 0}, want: []complex128{4, 0, 0, 0, 4}, }, { in: []float64{1, 0, 1, 0, 1, 0, 1}, want: []complex128{ 4, 0.5 + 0.24078730940376442i, 0.5 + 0.6269801688313512i, 0.5 + 2.190643133767413i, }, }, { in: []float64{1, 0, 2, 0, 1, 0, 4, 0, 1, 0, 2, 0, 1, 0}, want: []complex128{ 12, -2.301937735804838 - 1.108554787638881i, 0.7469796037174659 + 0.9366827961047095i, -0.9450418679126271 - 4.140498958131061i, -0.9450418679126271 + 4.140498958131061i, 0.7469796037174659 - 0.9366827961047095i, -2.301937735804838 + 1.108554787638881i, 12, }, }, } for _, test := range cases { fft.Reset(len(test.in)) got := fft.Coefficients(nil, test.in) if !equalApprox(got, test.want, tol) { t.Errorf("unexpected result for coefficients(%g):\ngot: %g\nwant:%g", test.in, got, test.want) } } }) t.Run("Freq", func(t *testing.T) { var fft FFT cases := []struct { n int want []float64 }{ {n: 1, want: []float64{0}}, {n: 2, want: []float64{0, 0.5}}, {n: 3, want: []float64{0, 1.0 / 3.0}}, {n: 4, want: []float64{0, 0.25, 0.5}}, } for _, test := range cases { fft.Reset(test.n) for i, want := range test.want { if got := fft.Freq(i); got != want { t.Errorf("unexpected result for freq(%d) for length %d: got:%v want:%v", i, test.n, got, want) } } } }) } func TestCmplxFFT(t *testing.T) { const tol = 1e-12 rnd := rand.New(rand.NewPCG(1, 1)) t.Run("NewFFT", func(t *testing.T) { for n := 1; n <= 200; n++ { fft := NewCmplxFFT(n) want := make([]complex128, n) for i := range want { want[i] = complex(rnd.Float64(), rnd.Float64()) } coeff := fft.Coefficients(nil, want) got := fft.Sequence(nil, coeff) sf := complex(1/float64(n), 0) for i := range got { got[i] *= sf } if !equalApprox(got, want, tol) { t.Errorf("unexpected result for complex sequence(coefficients(x)) for length %d", n) } } }) t.Run("Reset FFT", func(t *testing.T) { fft := NewCmplxFFT(1000) for n := 1; n <= 2000; n++ { fft.Reset(n) want := make([]complex128, n) for i := range want { want[i] = complex(rnd.Float64(), rnd.Float64()) } coeff := fft.Coefficients(nil, want) got := fft.Sequence(nil, coeff) sf := complex(1/float64(n), 0) for i := range got { got[i] *= sf } if !equalApprox(got, want, tol) { t.Errorf("unexpected result for complex sequence(coefficients(x)) for length %d", n) } } }) t.Run("Freq", func(t *testing.T) { var fft CmplxFFT cases := []struct { want []float64 }{ {want: []float64{0}}, {want: []float64{0, -0.5}}, {want: []float64{0, 1.0 / 3.0, -1.0 / 3.0}}, {want: []float64{0, 0.25, -0.5, -0.25}}, } for _, test := range cases { fft.Reset(len(test.want)) for i, want := range test.want { if got := fft.Freq(i); got != want { t.Errorf("unexpected result for freq(%d) for length %d: got:%v want:%v", i, len(test.want), got, want) } } } }) t.Run("Shift", func(t *testing.T) { var fft CmplxFFT cases := []struct { index []int want []int }{ {index: []int{0}, want: []int{0}}, {index: []int{0, -1}, want: []int{-1, 0}}, {index: []int{0, 1, -1}, want: []int{-1, 0, 1}}, {index: []int{0, 1, -2, -1}, want: []int{-2, -1, 0, 1}}, {index: []int{0, 1, 2, -2, -1}, want: []int{-2, -1, 0, 1, 2}}, } for _, test := range cases { fft.Reset(len(test.index)) got := make([]int, len(test.index)) for i := range test.index { got[i] = test.index[fft.ShiftIdx(i)] su := fft.UnshiftIdx(fft.ShiftIdx(i)) if su != i { t.Errorf("unexpected result for unshift(shift(%d)) with length %d:\ngot: %d\nwant:%d", i, len(test.index), su, i) } } if !reflect.DeepEqual(got, test.want) { t.Errorf("unexpected result for shift(%d):\ngot: %d\nwant:%d", test.index, got, test.want) } } }) } func TestDCT(t *testing.T) { t.Parallel() const tol = 1e-10 rnd := rand.New(rand.NewPCG(1, 1)) t.Run("NewDCT", func(t *testing.T) { for n := 2; n <= 200; n++ { dct := NewDCT(n) want := make([]float64, n) for i := range want { want[i] = rnd.Float64() } coeff := dct.Transform(nil, want) got := dct.Transform(nil, coeff) floats.Scale(1/float64(2*(n-1)), got) if !floats.EqualApprox(got, want, tol) { t.Errorf("unexpected result for transform(transform(x)) for length %d", n) } } }) t.Run("Reset DCT", func(t *testing.T) { dct := NewDCT(1000) for n := 2; n <= 2000; n++ { dct.Reset(n) want := make([]float64, n) for i := range want { want[i] = rnd.Float64() } coeff := dct.Transform(nil, want) got := dct.Transform(nil, coeff) floats.Scale(1/float64(2*(n-1)), got) if !floats.EqualApprox(got, want, tol) { t.Errorf("unexpected result for transform(transform(x)) for length %d", n) } } }) } func TestDST(t *testing.T) { t.Parallel() const tol = 1e-10 rnd := rand.New(rand.NewPCG(1, 1)) t.Run("NewDST", func(t *testing.T) { for n := 1; n <= 200; n++ { dst := NewDST(n) want := make([]float64, n) for i := range want { want[i] = rnd.Float64() } coeff := dst.Transform(nil, want) got := dst.Transform(nil, coeff) floats.Scale(1/float64(2*(n+1)), got) if !floats.EqualApprox(got, want, tol) { t.Errorf("unexpected result for transform(transform(x)) for length %d", n) } } }) t.Run("Reset DST", func(t *testing.T) { dst := NewDST(1000) for n := 1; n <= 2000; n++ { dst.Reset(n) want := make([]float64, n) for i := range want { want[i] = rnd.Float64() } coeff := dst.Transform(nil, want) got := dst.Transform(nil, coeff) floats.Scale(1/float64(2*(n+1)), got) if !floats.EqualApprox(got, want, tol) { t.Errorf("unexpected result for transform(transform(x)) for length %d", n) } } }) } func TestQuarterWaveFFT(t *testing.T) { t.Parallel() const tol = 1e-10 rnd := rand.New(rand.NewPCG(1, 1)) t.Run("NewQuarterWaveFFT", func(t *testing.T) { for n := 1; n <= 200; n++ { qw := NewQuarterWaveFFT(n) want := make([]float64, n) for i := range want { want[i] = rnd.Float64() } { coeff := qw.CosCoefficients(nil, want) got := qw.CosSequence(nil, coeff) floats.Scale(1/float64(4*n), got) if !floats.EqualApprox(got, want, tol) { t.Errorf("unexpected result for cossequence(coscoefficient(x)) for length %d", n) } } { coeff := qw.SinCoefficients(nil, want) got := qw.SinSequence(nil, coeff) floats.Scale(1/float64(4*n), got) if !floats.EqualApprox(got, want, tol) { t.Errorf("unexpected result for sinsequence(sincoefficient(x)) for length %d", n) } } } }) t.Run("Reset QuarterWaveFFT", func(t *testing.T) { qw := NewQuarterWaveFFT(1000) for n := 1; n <= 2000; n++ { qw.Reset(n) want := make([]float64, n) for i := range want { want[i] = rnd.Float64() } { coeff := qw.CosCoefficients(nil, want) got := qw.CosSequence(nil, coeff) floats.Scale(1/float64(4*n), got) if !floats.EqualApprox(got, want, tol) { t.Errorf("unexpected result for cossequence(coscoefficient(x)) for length %d", n) } } { coeff := qw.SinCoefficients(nil, want) got := qw.SinSequence(nil, coeff) floats.Scale(1/float64(4*n), got) if !floats.EqualApprox(got, want, tol) { t.Errorf("unexpected result for sinsequence(sincoefficient(x)) for length %d", n) } } } }) } func equalApprox(a, b []complex128, tol float64) bool { if len(a) != len(b) { return false } ar := make([]float64, len(a)) br := make([]float64, len(a)) ai := make([]float64, len(a)) bi := make([]float64, len(a)) for i, cv := range a { ar[i] = real(cv) ai[i] = imag(cv) } for i, cv := range b { br[i] = real(cv) bi[i] = imag(cv) } return floats.EqualApprox(ar, br, tol) && floats.EqualApprox(ai, bi, tol) } func BenchmarkRealFFTCoefficients(b *testing.B) { var sizes []int for n := 16; n < 1<<24; n <<= 3 { sizes = append(sizes, n) } sizes = append(sizes, 100, 4000, 1e6) for _, n := range sizes { fft := NewFFT(n) seq := randFloats(n, rand.NewPCG(1, 1)) dst := make([]complex128, n/2+1) b.Run(fmt.Sprint(n), func(b *testing.B) { for i := 0; i < b.N; i++ { fft.Coefficients(dst, seq) } }) } } func BenchmarkRealFFTSequence(b *testing.B) { var sizes []int for n := 16; n < 1<<24; n <<= 3 { sizes = append(sizes, n) } sizes = append(sizes, 100, 4000, 1e6) for _, n := range sizes { fft := NewFFT(n) coeff := randComplexes(n/2+1, rand.NewPCG(1, 1)) dst := make([]float64, n) b.Run(fmt.Sprint(n), func(b *testing.B) { for i := 0; i < b.N; i++ { fft.Sequence(dst, coeff) } }) } } func BenchmarkCmplxFFTCoefficients(b *testing.B) { var sizes []int for n := 16; n < 1<<24; n <<= 3 { sizes = append(sizes, n) } sizes = append(sizes, 100, 4000, 1e6) for _, n := range sizes { fft := NewCmplxFFT(n) d := randComplexes(n, rand.NewPCG(1, 1)) b.Run(fmt.Sprint(n), func(b *testing.B) { for i := 0; i < b.N; i++ { fft.Coefficients(d, d) } }) } } func BenchmarkCmplxFFTSequence(b *testing.B) { var sizes []int for n := 16; n < 1<<24; n <<= 3 { sizes = append(sizes, n) } sizes = append(sizes, 100, 4000, 1e6) for _, n := range sizes { fft := NewCmplxFFT(n) d := randComplexes(n, rand.NewPCG(1, 1)) b.Run(fmt.Sprint(n), func(b *testing.B) { for i := 0; i < b.N; i++ { fft.Sequence(d, d) } }) } } func randFloats(n int, src rand.Source) []float64 { rnd := rand.New(src) f := make([]float64, n) for i := range f { f[i] = rnd.Float64() } return f } func randComplexes(n int, src rand.Source) []complex128 { rnd := rand.New(src) c := make([]complex128, n) for i := range c { c[i] = complex(rnd.Float64(), rnd.Float64()) } return c }