From aa83f4862e1a5dbf7b24cc469c4e2840a8772dac Mon Sep 17 00:00:00 2001 From: AlexHild Date: Thu, 23 Mar 2017 07:21:58 +0100 Subject: [PATCH] Single precision Fourier Transform support (#481) --- src/Numerics/ComplexExtensions.cs | 10 + .../IntegralTransforms/Fourier.Bluestein.cs | 126 +++++ .../IntegralTransforms/Fourier.Naive.cs | 30 ++ .../IntegralTransforms/Fourier.RadixN.cs | 74 +++ src/Numerics/IntegralTransforms/Fourier.cs | 468 ++++++++++++++++++ .../IFourierTransformProvider.cs | 10 +- .../ManagedFourierTransformProvider.cs | 104 ++++ .../Mkl/MklFourierTransformProvider.cs | 119 ++++- .../IntegralTransformsTests/FourierTest.cs | 49 ++ .../InverseTransformTest.cs | 99 ++++ .../MatchingNaiveTransformTest.cs | 222 +++++++++ .../ParsevalTheoremTest.cs | 23 + 12 files changed, 1313 insertions(+), 21 deletions(-) diff --git a/src/Numerics/ComplexExtensions.cs b/src/Numerics/ComplexExtensions.cs index 78d46b92..532ea9dd 100644 --- a/src/Numerics/ComplexExtensions.cs +++ b/src/Numerics/ComplexExtensions.cs @@ -45,6 +45,16 @@ namespace MathNet.Numerics /// public static class ComplexExtensions { + /// + /// Gets the squared magnitude of the Complex number. + /// + /// The number to perfom this operation on. + /// The squared magnitude of the Complex number. + public static double MagnitudeSquared(this Complex32 complex) + { + return (complex.Real * complex.Real) + (complex.Imaginary * complex.Imaginary); + } + /// /// Gets the squared magnitude of the Complex number. /// diff --git a/src/Numerics/IntegralTransforms/Fourier.Bluestein.cs b/src/Numerics/IntegralTransforms/Fourier.Bluestein.cs index 820c88e3..46c38d30 100644 --- a/src/Numerics/IntegralTransforms/Fourier.Bluestein.cs +++ b/src/Numerics/IntegralTransforms/Fourier.Bluestein.cs @@ -47,6 +47,38 @@ namespace MathNet.Numerics.IntegralTransforms /// const int BluesteinSequenceLengthThreshold = 46341; + /// + /// Generate the bluestein sequence for the provided problem size. + /// + /// Number of samples. + /// Bluestein sequence exp(I*Pi*k^2/N) + static Complex32[] BluesteinSequence32(int n) + { + double s = Constants.Pi / n; + var sequence = new Complex32[n]; + + // TODO: benchmark whether the second variation is significantly + // faster than the former one. If not just use the former one always. + if (n > BluesteinSequenceLengthThreshold) + { + for (int k = 0; k < sequence.Length; k++) + { + double t = (s * k) * k; + sequence[k] = new Complex32((float)Math.Cos(t), (float)Math.Sin(t)); + } + } + else + { + for (int k = 0; k < sequence.Length; k++) + { + double t = s * (k * k); + sequence[k] = new Complex32((float)Math.Cos(t), (float)Math.Sin(t)); + } + } + + return sequence; + } + /// /// Generate the bluestein sequence for the provided problem size. /// @@ -79,6 +111,61 @@ namespace MathNet.Numerics.IntegralTransforms return sequence; } + /// + /// Convolution with the bluestein sequence (Parallel Version). + /// + /// Sample Vector. + static void BluesteinConvolutionParallel(Complex32[] samples) + { + int n = samples.Length; + Complex32[] sequence = BluesteinSequence32(n); + + // Padding to power of two >= 2N–1 so we can apply Radix-2 FFT. + int m = ((n << 1) - 1).CeilingToPowerOfTwo(); + var b = new Complex32[m]; + var a = new Complex32[m]; + + CommonParallel.Invoke( + () => + { + // Build and transform padded sequence b_k = exp(I*Pi*k^2/N) + for (int i = 0; i < n; i++) + { + b[i] = sequence[i]; + } + + for (int i = m - n + 1; i < b.Length; i++) + { + b[i] = sequence[m - i]; + } + + Radix2(b, -1); + }, + () => + { + // Build and transform padded sequence a_k = x_k * exp(-I*Pi*k^2/N) + for (int i = 0; i < samples.Length; i++) + { + a[i] = sequence[i].Conjugate() * samples[i]; + } + + Radix2(a, -1); + }); + + for (int i = 0; i < a.Length; i++) + { + a[i] *= b[i]; + } + + Radix2Parallel(a, 1); + + var nbinv = 1.0f / m; + for (int i = 0; i < samples.Length; i++) + { + samples[i] = nbinv * sequence[i].Conjugate() * a[i]; + } + } + /// /// Convolution with the bluestein sequence (Parallel Version). /// @@ -134,6 +221,18 @@ namespace MathNet.Numerics.IntegralTransforms } } + /// + /// Swap the real and imaginary parts of each sample. + /// + /// Sample Vector. + static void SwapRealImaginary(Complex32[] samples) + { + for (int i = 0; i < samples.Length; i++) + { + samples[i] = new Complex32(samples[i].Imaginary, samples[i].Real); + } + } + /// /// Swap the real and imaginary parts of each sample. /// @@ -146,6 +245,33 @@ namespace MathNet.Numerics.IntegralTransforms } } + /// + /// Bluestein generic FFT for arbitrary sized sample vectors. + /// + /// Time-space sample vector. + /// Fourier series exponent sign. + internal static void Bluestein(Complex32[] samples, int exponentSign) + { + int n = samples.Length; + if (n.IsPowerOfTwo()) + { + Radix2Parallel(samples, exponentSign); + return; + } + + if (exponentSign == 1) + { + SwapRealImaginary(samples); + } + + BluesteinConvolutionParallel(samples); + + if (exponentSign == 1) + { + SwapRealImaginary(samples); + } + } + /// /// Bluestein generic FFT for arbitrary sized sample vectors. /// diff --git a/src/Numerics/IntegralTransforms/Fourier.Naive.cs b/src/Numerics/IntegralTransforms/Fourier.Naive.cs index 609abbde..b03ebca7 100644 --- a/src/Numerics/IntegralTransforms/Fourier.Naive.cs +++ b/src/Numerics/IntegralTransforms/Fourier.Naive.cs @@ -41,6 +41,36 @@ namespace MathNet.Numerics.IntegralTransforms /// public static partial class Fourier { + /// + /// Naive generic DFT, useful e.g. to verify faster algorithms. + /// + /// Time-space sample vector. + /// Fourier series exponent sign. + /// Corresponding frequency-space vector. + internal static Complex32[] Naive(Complex32[] samples, int exponentSign) + { + var w0 = exponentSign * Constants.Pi2 / samples.Length; + var spectrum = new Complex32[samples.Length]; + + CommonParallel.For(0, samples.Length, (u, v) => + { + for (int i = u; i < v; i++) + { + var wk = w0 * i; + var sum = Complex32.Zero; + for (var n = 0; n < samples.Length; n++) + { + var w = n * wk; + sum += samples[n] * new Complex32((float)Math.Cos(w), (float)Math.Sin(w)); + } + + spectrum[i] = sum; + } + }); + + return spectrum; + } + /// /// Naive generic DFT, useful e.g. to verify faster algorithms. /// diff --git a/src/Numerics/IntegralTransforms/Fourier.RadixN.cs b/src/Numerics/IntegralTransforms/Fourier.RadixN.cs index 98564ee5..0d029bff 100644 --- a/src/Numerics/IntegralTransforms/Fourier.RadixN.cs +++ b/src/Numerics/IntegralTransforms/Fourier.RadixN.cs @@ -70,6 +70,29 @@ namespace MathNet.Numerics.IntegralTransforms } } + /// + /// Radix-2 Step Helper Method + /// + /// Sample vector. + /// Fourier series exponent sign. + /// Level Group Size. + /// Index inside of the level. + static void Radix2Step(Complex32[] samples, int exponentSign, int levelSize, int k) + { + // Twiddle Factor + var exponent = (exponentSign * k) * Constants.Pi / levelSize; + var w = new Complex32((float)Math.Cos(exponent), (float)Math.Sin(exponent)); + + var step = levelSize << 1; + for (var i = k; i < samples.Length; i += step) + { + var ai = samples[i]; + var t = w * samples[i + levelSize]; + samples[i] = ai + t; + samples[i + levelSize] = ai - t; + } + } + /// /// Radix-2 Step Helper Method /// @@ -93,6 +116,29 @@ namespace MathNet.Numerics.IntegralTransforms } } + /// + /// Radix-2 generic FFT for power-of-two sized sample vectors. + /// + /// Sample vector, where the FFT is evaluated in place. + /// Fourier series exponent sign. + /// + internal static void Radix2(Complex32[] samples, int exponentSign) + { + if (!samples.Length.IsPowerOfTwo()) + { + throw new ArgumentException(Resources.ArgumentPowerOfTwo); + } + + Radix2Reorder(samples); + for (var levelSize = 1; levelSize < samples.Length; levelSize *= 2) + { + for (var k = 0; k < levelSize; k++) + { + Radix2Step(samples, exponentSign, levelSize, k); + } + } + } + /// /// Radix-2 generic FFT for power-of-two sized sample vectors. /// @@ -116,6 +162,34 @@ namespace MathNet.Numerics.IntegralTransforms } } + /// + /// Radix-2 generic FFT for power-of-two sample vectors (Parallel Version). + /// + /// Sample vector, where the FFT is evaluated in place. + /// Fourier series exponent sign. + /// + internal static void Radix2Parallel(Complex32[] samples, int exponentSign) + { + if (!samples.Length.IsPowerOfTwo()) + { + throw new ArgumentException(Resources.ArgumentPowerOfTwo); + } + + Radix2Reorder(samples); + for (var levelSize = 1; levelSize < samples.Length; levelSize *= 2) + { + var size = levelSize; + + CommonParallel.For(0, size, 64, (u, v) => + { + for (int i = u; i < v; i++) + { + Radix2Step(samples, exponentSign, size, i); + } + }); + } + } + /// /// Radix-2 generic FFT for power-of-two sample vectors (Parallel Version). /// diff --git a/src/Numerics/IntegralTransforms/Fourier.cs b/src/Numerics/IntegralTransforms/Fourier.cs index dcf8710f..52943aa4 100644 --- a/src/Numerics/IntegralTransforms/Fourier.cs +++ b/src/Numerics/IntegralTransforms/Fourier.cs @@ -44,6 +44,15 @@ namespace MathNet.Numerics.IntegralTransforms /// public static partial class Fourier { + /// + /// Applies the forward Fast Fourier Transform (FFT) to arbitrary-length sample vectors. + /// + /// Sample vector, where the FFT is evaluated in place. + public static void Forward(Complex32[] samples) + { + Control.FourierTransformProvider.Forward(samples, FourierTransformScaling.SymmetricScaling); + } + /// /// Applies the forward Fast Fourier Transform (FFT) to arbitrary-length sample vectors. /// @@ -52,6 +61,32 @@ namespace MathNet.Numerics.IntegralTransforms { Control.FourierTransformProvider.Forward(samples, FourierTransformScaling.SymmetricScaling); } + + /// + /// Applies the forward Fast Fourier Transform (FFT) to arbitrary-length sample vectors. + /// + /// Sample vector, where the FFT is evaluated in place. + /// Fourier Transform Convention Options. + public static void Forward(Complex32[] samples, FourierOptions options) + { + switch (options) + { + case FourierOptions.NoScaling: + case FourierOptions.AsymmetricScaling: + Control.FourierTransformProvider.Forward(samples, FourierTransformScaling.NoScaling); + break; + case FourierOptions.InverseExponent: + Control.FourierTransformProvider.Backward(samples, FourierTransformScaling.SymmetricScaling); + break; + case FourierOptions.InverseExponent | FourierOptions.NoScaling: + case FourierOptions.InverseExponent | FourierOptions.AsymmetricScaling: + Control.FourierTransformProvider.Backward(samples, FourierTransformScaling.NoScaling); + break; + default: + Control.FourierTransformProvider.Forward(samples, FourierTransformScaling.SymmetricScaling); + break; + } + } /// /// Applies the forward Fast Fourier Transform (FFT) to arbitrary-length sample vectors. @@ -78,6 +113,37 @@ namespace MathNet.Numerics.IntegralTransforms break; } } + + /// + /// Applies the forward Fast Fourier Transform (FFT) to arbitrary-length sample vectors. + /// + /// Real part of the sample vector, where the FFT is evaluated in place. + /// Imaginary part of the sample vector, where the FFT is evaluated in place. + /// Fourier Transform Convention Options. + public static void Forward(float[] real, float[] imaginary, FourierOptions options = FourierOptions.Default) + { + if (real.Length != imaginary.Length) + { + throw new ArgumentException(Resources.ArgumentArraysSameLength); + } + + // TODO: consider to support this natively by the provider, without the need for copying + // TODO: otherwise, consider ArrayPool + + Complex32[] data = new Complex32[real.Length]; + for (int i = 0; i < data.Length; i++) + { + data[i] = new Complex32(real[i], imaginary[i]); + } + + Forward(data, options); + + for (int i = 0; i < data.Length; i++) + { + real[i] = data[i].Real; + imaginary[i] = data[i].Imaginary; + } + } /// /// Applies the forward Fast Fourier Transform (FFT) to arbitrary-length sample vectors. @@ -109,6 +175,40 @@ namespace MathNet.Numerics.IntegralTransforms imaginary[i] = data[i].Imaginary; } } + + /// + /// Packed Real-Complex forward Fast Fourier Transform (FFT) to arbitrary-length sample vectors. + /// Since for real-valued time samples the complex spectrum is conjugate-even (symmetry), + /// the spectrum can be fully reconstructed form the positive frequencies only (first half). + /// The data array needs to be N+2 (if N is even) or N+1 (if N is odd) long in order to support such a packed spectrum. + /// + /// Data array of length N+2 (if N is even) or N+1 (if N is odd). + /// The number of samples. + /// Fourier Transform Convention Options. + public static void ForwardReal(float[] data, int n, FourierOptions options = FourierOptions.Default) + { + int length = n.IsEven() ? n + 2 : n + 1; + if (data.Length < length) + { + throw new ArgumentException(string.Format(Resources.ArrayTooSmall, length)); + } + + if ((options & FourierOptions.InverseExponent) == FourierOptions.InverseExponent) + { + throw new NotSupportedException(); + } + + switch (options) + { + case FourierOptions.NoScaling: + case FourierOptions.AsymmetricScaling: + Control.FourierTransformProvider.ForwardReal(data, n, FourierTransformScaling.NoScaling); + break; + default: + Control.FourierTransformProvider.ForwardReal(data, n, FourierTransformScaling.SymmetricScaling); + break; + } + } /// /// Packed Real-Complex forward Fast Fourier Transform (FFT) to arbitrary-length sample vectors. @@ -144,6 +244,36 @@ namespace MathNet.Numerics.IntegralTransforms } } + /// + /// Applies the forward Fast Fourier Transform (FFT) to multiple dimensional sample data. + /// + /// Sample data, where the FFT is evaluated in place. + /// + /// The data size per dimension. The first dimension is the major one. + /// For example, with two dimensions "rows" and "columns" the samples are assumed to be organized row by row. + /// + /// Fourier Transform Convention Options. + public static void ForwardMultiDim(Complex32[] samples, int[] dimensions, FourierOptions options = FourierOptions.Default) + { + switch (options) + { + case FourierOptions.NoScaling: + case FourierOptions.AsymmetricScaling: + Control.FourierTransformProvider.ForwardMultidim(samples, dimensions, FourierTransformScaling.NoScaling); + break; + case FourierOptions.InverseExponent: + Control.FourierTransformProvider.BackwardMultidim(samples, dimensions, FourierTransformScaling.SymmetricScaling); + break; + case FourierOptions.InverseExponent | FourierOptions.NoScaling: + case FourierOptions.InverseExponent | FourierOptions.AsymmetricScaling: + Control.FourierTransformProvider.BackwardMultidim(samples, dimensions, FourierTransformScaling.NoScaling); + break; + default: + Control.FourierTransformProvider.ForwardMultidim(samples, dimensions, FourierTransformScaling.SymmetricScaling); + break; + } + } + /// /// Applies the forward Fast Fourier Transform (FFT) to multiple dimensional sample data. /// @@ -173,6 +303,19 @@ namespace MathNet.Numerics.IntegralTransforms break; } } + + /// + /// Applies the forward Fast Fourier Transform (FFT) to two dimensional sample data. + /// + /// Sample data, organized row by row, where the FFT is evaluated in place + /// The number of rows. + /// The number of columns. + /// Data available organized column by column instead of row by row can be processed directly by swapping the rows and columns arguments. + /// Fourier Transform Convention Options. + public static void Forward2D(Complex32[] samplesRowWise, int rows, int columns, FourierOptions options = FourierOptions.Default) + { + ForwardMultiDim(samplesRowWise, new[] { rows, columns }, options); + } /// /// Applies the forward Fast Fourier Transform (FFT) to two dimensional sample data. @@ -186,6 +329,34 @@ namespace MathNet.Numerics.IntegralTransforms { ForwardMultiDim(samplesRowWise, new[] { rows, columns }, options); } + + /// + /// Applies the forward Fast Fourier Transform (FFT) to a two dimensional data in form of a matrix. + /// + /// Sample matrix, where the FFT is evaluated in place + /// Fourier Transform Convention Options. + public static void Forward2D(Matrix samples, FourierOptions options = FourierOptions.Default) + { + var rowMajorArray = samples.AsRowMajorArray(); + if (rowMajorArray != null) + { + ForwardMultiDim(rowMajorArray, new[] { samples.RowCount, samples.ColumnCount }, options); + return; + } + + var columnMajorArray = samples.AsColumnMajorArray(); + if (columnMajorArray != null) + { + ForwardMultiDim(columnMajorArray, new[] { samples.ColumnCount, samples.RowCount }, options); + return; + } + + // Fall Back + columnMajorArray = samples.ToColumnMajorArray(); + ForwardMultiDim(columnMajorArray, new[] { samples.ColumnCount, samples.RowCount }, options); + var denseStorage = new DenseColumnMajorMatrixStorage(samples.RowCount, samples.ColumnCount, columnMajorArray); + denseStorage.CopyToUnchecked(samples.Storage, ExistingData.Clear); + } /// /// Applies the forward Fast Fourier Transform (FFT) to a two dimensional data in form of a matrix. @@ -215,6 +386,15 @@ namespace MathNet.Numerics.IntegralTransforms denseStorage.CopyToUnchecked(samples.Storage, ExistingData.Clear); } + /// + /// Applies the inverse Fast Fourier Transform (iFFT) to arbitrary-length sample vectors. + /// + /// Spectrum data, where the iFFT is evaluated in place. + public static void Inverse(Complex32[] spectrum) + { + Control.FourierTransformProvider.Backward(spectrum, FourierTransformScaling.SymmetricScaling); + } + /// /// Applies the inverse Fast Fourier Transform (iFFT) to arbitrary-length sample vectors. /// @@ -224,6 +404,36 @@ namespace MathNet.Numerics.IntegralTransforms Control.FourierTransformProvider.Backward(spectrum, FourierTransformScaling.SymmetricScaling); } + /// + /// Applies the inverse Fast Fourier Transform (iFFT) to arbitrary-length sample vectors. + /// + /// Spectrum data, where the iFFT is evaluated in place. + /// Fourier Transform Convention Options. + public static void Inverse(Complex32[] spectrum, FourierOptions options) + { + switch (options) + { + case FourierOptions.NoScaling: + Control.FourierTransformProvider.Backward(spectrum, FourierTransformScaling.NoScaling); + break; + case FourierOptions.AsymmetricScaling: + Control.FourierTransformProvider.Backward(spectrum, FourierTransformScaling.BackwardScaling); + break; + case FourierOptions.InverseExponent: + Control.FourierTransformProvider.Forward(spectrum, FourierTransformScaling.SymmetricScaling); + break; + case FourierOptions.InverseExponent | FourierOptions.NoScaling: + Control.FourierTransformProvider.Forward(spectrum, FourierTransformScaling.NoScaling); + break; + case FourierOptions.InverseExponent | FourierOptions.AsymmetricScaling: + Control.FourierTransformProvider.Forward(spectrum, FourierTransformScaling.ForwardScaling); + break; + default: + Control.FourierTransformProvider.Backward(spectrum, FourierTransformScaling.SymmetricScaling); + break; + } + } + /// /// Applies the inverse Fast Fourier Transform (iFFT) to arbitrary-length sample vectors. /// @@ -254,6 +464,37 @@ namespace MathNet.Numerics.IntegralTransforms } } + /// + /// Applies the inverse Fast Fourier Transform (iFFT) to arbitrary-length sample vectors. + /// + /// Real part of the sample vector, where the iFFT is evaluated in place. + /// Imaginary part of the sample vector, where the iFFT is evaluated in place. + /// Fourier Transform Convention Options. + public static void Inverse(float[] real, float[] imaginary, FourierOptions options = FourierOptions.Default) + { + if (real.Length != imaginary.Length) + { + throw new ArgumentException(Resources.ArgumentArraysSameLength); + } + + // TODO: consider to support this natively by the provider, without the need for copying + // TODO: otherwise, consider ArrayPool + + Complex32[] data = new Complex32[real.Length]; + for (int i = 0; i < data.Length; i++) + { + data[i] = new Complex32(real[i], imaginary[i]); + } + + Inverse(data, options); + + for (int i = 0; i < data.Length; i++) + { + real[i] = data[i].Real; + imaginary[i] = data[i].Imaginary; + } + } + /// /// Applies the inverse Fast Fourier Transform (iFFT) to arbitrary-length sample vectors. /// @@ -285,6 +526,42 @@ namespace MathNet.Numerics.IntegralTransforms } } + /// + /// Packed Real-Complex inverse Fast Fourier Transform (iFFT) to arbitrary-length sample vectors. + /// Since for real-valued time samples the complex spectrum is conjugate-even (symmetry), + /// the spectrum can be fully reconstructed form the positive frequencies only (first half). + /// The data array needs to be N+2 (if N is even) or N+1 (if N is odd) long in order to support such a packed spectrum. + /// + /// Data array of length N+2 (if N is even) or N+1 (if N is odd). + /// The number of samples. + /// Fourier Transform Convention Options. + public static void InverseReal(float[] data, int n, FourierOptions options = FourierOptions.Default) + { + int length = n.IsEven() ? n + 2 : n + 1; + if (data.Length < length) + { + throw new ArgumentException(string.Format(Resources.ArrayTooSmall, length)); + } + + if ((options & FourierOptions.InverseExponent) == FourierOptions.InverseExponent) + { + throw new NotSupportedException(); + } + + switch (options) + { + case FourierOptions.NoScaling: + Control.FourierTransformProvider.BackwardReal(data, n, FourierTransformScaling.NoScaling); + break; + case FourierOptions.AsymmetricScaling: + Control.FourierTransformProvider.BackwardReal(data, n, FourierTransformScaling.BackwardScaling); + break; + default: + Control.FourierTransformProvider.BackwardReal(data, n, FourierTransformScaling.SymmetricScaling); + break; + } + } + /// /// Packed Real-Complex inverse Fast Fourier Transform (iFFT) to arbitrary-length sample vectors. /// Since for real-valued time samples the complex spectrum is conjugate-even (symmetry), @@ -321,6 +598,40 @@ namespace MathNet.Numerics.IntegralTransforms } } + /// + /// Applies the inverse Fast Fourier Transform (iFFT) to multiple dimensional sample data. + /// + /// Spectrum data, where the iFFT is evaluated in place. + /// + /// The data size per dimension. The first dimension is the major one. + /// For example, with two dimensions "rows" and "columns" the samples are assumed to be organized row by row. + /// + /// Fourier Transform Convention Options. + public static void InverseMultiDim(Complex32[] spectrum, int[] dimensions, FourierOptions options = FourierOptions.Default) + { + switch (options) + { + case FourierOptions.NoScaling: + Control.FourierTransformProvider.BackwardMultidim(spectrum, dimensions, FourierTransformScaling.NoScaling); + break; + case FourierOptions.AsymmetricScaling: + Control.FourierTransformProvider.BackwardMultidim(spectrum, dimensions, FourierTransformScaling.BackwardScaling); + break; + case FourierOptions.InverseExponent: + Control.FourierTransformProvider.ForwardMultidim(spectrum, dimensions, FourierTransformScaling.SymmetricScaling); + break; + case FourierOptions.InverseExponent | FourierOptions.NoScaling: + Control.FourierTransformProvider.ForwardMultidim(spectrum, dimensions, FourierTransformScaling.NoScaling); + break; + case FourierOptions.InverseExponent | FourierOptions.AsymmetricScaling: + Control.FourierTransformProvider.ForwardMultidim(spectrum, dimensions, FourierTransformScaling.ForwardScaling); + break; + default: + Control.FourierTransformProvider.BackwardMultidim(spectrum, dimensions, FourierTransformScaling.SymmetricScaling); + break; + } + } + /// /// Applies the inverse Fast Fourier Transform (iFFT) to multiple dimensional sample data. /// @@ -355,6 +666,19 @@ namespace MathNet.Numerics.IntegralTransforms } } + /// + /// Applies the inverse Fast Fourier Transform (iFFT) to two dimensional sample data. + /// + /// Sample data, organized row by row, where the iFFT is evaluated in place + /// The number of rows. + /// The number of columns. + /// Data available organized column by column instead of row by row can be processed directly by swapping the rows and columns arguments. + /// Fourier Transform Convention Options. + public static void Inverse2D(Complex32[] spectrumRowWise, int rows, int columns, FourierOptions options = FourierOptions.Default) + { + InverseMultiDim(spectrumRowWise, new[] { rows, columns }, options); + } + /// /// Applies the inverse Fast Fourier Transform (iFFT) to two dimensional sample data. /// @@ -368,6 +692,34 @@ namespace MathNet.Numerics.IntegralTransforms InverseMultiDim(spectrumRowWise, new[] { rows, columns }, options); } + /// + /// Applies the inverse Fast Fourier Transform (iFFT) to a two dimensional data in form of a matrix. + /// + /// Sample matrix, where the iFFT is evaluated in place + /// Fourier Transform Convention Options. + public static void Inverse2D(Matrix spectrum, FourierOptions options = FourierOptions.Default) + { + var rowMajorArray = spectrum.AsRowMajorArray(); + if (rowMajorArray != null) + { + InverseMultiDim(rowMajorArray, new[] { spectrum.RowCount, spectrum.ColumnCount }, options); + return; + } + + var columnMajorArray = spectrum.AsColumnMajorArray(); + if (columnMajorArray != null) + { + InverseMultiDim(columnMajorArray, new[] { spectrum.ColumnCount, spectrum.RowCount }, options); + return; + } + + // Fall Back + columnMajorArray = spectrum.ToColumnMajorArray(); + InverseMultiDim(columnMajorArray, new[] { spectrum.ColumnCount, spectrum.RowCount }, options); + var denseStorage = new DenseColumnMajorMatrixStorage(spectrum.RowCount, spectrum.ColumnCount, columnMajorArray); + denseStorage.CopyToUnchecked(spectrum.Storage, ExistingData.Clear); + } + /// /// Applies the inverse Fast Fourier Transform (iFFT) to a two dimensional data in form of a matrix. /// @@ -396,6 +748,19 @@ namespace MathNet.Numerics.IntegralTransforms denseStorage.CopyToUnchecked(spectrum.Storage, ExistingData.Clear); } + /// + /// Naive forward DFT, useful e.g. to verify faster algorithms. + /// + /// Time-space sample vector. + /// Fourier Transform Convention Options. + /// Corresponding frequency-space vector. + public static Complex32[] NaiveForward(Complex32[] samples, FourierOptions options = FourierOptions.Default) + { + var frequencySpace = Naive(samples, SignByOptions(options)); + ForwardScaleByOptions(options, frequencySpace); + return frequencySpace; + } + /// /// Naive forward DFT, useful e.g. to verify faster algorithms. /// @@ -409,6 +774,19 @@ namespace MathNet.Numerics.IntegralTransforms return frequencySpace; } + /// + /// Naive inverse DFT, useful e.g. to verify faster algorithms. + /// + /// Frequency-space sample vector. + /// Fourier Transform Convention Options. + /// Corresponding time-space vector. + public static Complex32[] NaiveInverse(Complex32[] spectrum, FourierOptions options = FourierOptions.Default) + { + var timeSpace = Naive(spectrum, -SignByOptions(options)); + InverseScaleByOptions(options, timeSpace); + return timeSpace; + } + /// /// Naive inverse DFT, useful e.g. to verify faster algorithms. /// @@ -422,6 +800,18 @@ namespace MathNet.Numerics.IntegralTransforms return timeSpace; } + /// + /// Radix-2 forward FFT for power-of-two sized sample vectors. + /// + /// Sample vector, where the FFT is evaluated in place. + /// Fourier Transform Convention Options. + /// + public static void Radix2Forward(Complex32[] samples, FourierOptions options = FourierOptions.Default) + { + Radix2Parallel(samples, SignByOptions(options)); + ForwardScaleByOptions(options, samples); + } + /// /// Radix-2 forward FFT for power-of-two sized sample vectors. /// @@ -434,6 +824,18 @@ namespace MathNet.Numerics.IntegralTransforms ForwardScaleByOptions(options, samples); } + /// + /// Radix-2 inverse FFT for power-of-two sized sample vectors. + /// + /// Sample vector, where the FFT is evaluated in place. + /// Fourier Transform Convention Options. + /// + public static void Radix2Inverse(Complex32[] spectrum, FourierOptions options = FourierOptions.Default) + { + Radix2Parallel(spectrum, -SignByOptions(options)); + InverseScaleByOptions(options, spectrum); + } + /// /// Radix-2 inverse FFT for power-of-two sized sample vectors. /// @@ -446,6 +848,17 @@ namespace MathNet.Numerics.IntegralTransforms InverseScaleByOptions(options, spectrum); } + /// + /// Bluestein forward FFT for arbitrary sized sample vectors. + /// + /// Sample vector, where the FFT is evaluated in place. + /// Fourier Transform Convention Options. + public static void BluesteinForward(Complex32[] samples, FourierOptions options = FourierOptions.Default) + { + Bluestein(samples, SignByOptions(options)); + ForwardScaleByOptions(options, samples); + } + /// /// Bluestein forward FFT for arbitrary sized sample vectors. /// @@ -457,6 +870,17 @@ namespace MathNet.Numerics.IntegralTransforms ForwardScaleByOptions(options, samples); } + /// + /// Bluestein inverse FFT for arbitrary sized sample vectors. + /// + /// Sample vector, where the FFT is evaluated in place. + /// Fourier Transform Convention Options. + public static void BluesteinInverse(Complex32[] spectrum, FourierOptions options = FourierOptions.Default) + { + Bluestein(spectrum, -SignByOptions(options)); + InverseScaleByOptions(options, spectrum); + } + /// /// Bluestein inverse FFT for arbitrary sized sample vectors. /// @@ -479,6 +903,26 @@ namespace MathNet.Numerics.IntegralTransforms return (options & FourierOptions.InverseExponent) == FourierOptions.InverseExponent ? 1 : -1; } + /// + /// Rescale FFT-the resulting vector according to the provided convention options. + /// + /// Fourier Transform Convention Options. + /// Sample Vector. + static void ForwardScaleByOptions(FourierOptions options, Complex32[] samples) + { + if ((options & FourierOptions.NoScaling) == FourierOptions.NoScaling || + (options & FourierOptions.AsymmetricScaling) == FourierOptions.AsymmetricScaling) + { + return; + } + + var scalingFactor = (float)Math.Sqrt(1.0 / samples.Length); + for (int i = 0; i < samples.Length; i++) + { + samples[i] *= scalingFactor; + } + } + /// /// Rescale FFT-the resulting vector according to the provided convention options. /// @@ -499,6 +943,30 @@ namespace MathNet.Numerics.IntegralTransforms } } + /// + /// Rescale the iFFT-resulting vector according to the provided convention options. + /// + /// Fourier Transform Convention Options. + /// Sample Vector. + static void InverseScaleByOptions(FourierOptions options, Complex32[] samples) + { + if ((options & FourierOptions.NoScaling) == FourierOptions.NoScaling) + { + return; + } + + var scalingFactor = (float)1.0 / samples.Length; + if ((options & FourierOptions.AsymmetricScaling) != FourierOptions.AsymmetricScaling) + { + scalingFactor = (float)Math.Sqrt(scalingFactor); + } + + for (int i = 0; i < samples.Length; i++) + { + samples[i] *= scalingFactor; + } + } + /// /// Rescale the iFFT-resulting vector according to the provided convention options. /// diff --git a/src/Numerics/Providers/FourierTransform/IFourierTransformProvider.cs b/src/Numerics/Providers/FourierTransform/IFourierTransformProvider.cs index 4809a664..86cc9a79 100644 --- a/src/Numerics/Providers/FourierTransform/IFourierTransformProvider.cs +++ b/src/Numerics/Providers/FourierTransform/IFourierTransformProvider.cs @@ -55,13 +55,19 @@ namespace MathNet.Numerics.Providers.FourierTransform /// void InitializeVerify(); + void Forward(Complex32[] samples, FourierTransformScaling scaling); void Forward(Complex[] samples, FourierTransformScaling scaling); + void Backward(Complex32[] spectrum, FourierTransformScaling scaling); void Backward(Complex[] spectrum, FourierTransformScaling scaling); - + + void ForwardReal(float[] samples, int n, FourierTransformScaling scaling); void ForwardReal(double[] samples, int n, FourierTransformScaling scaling); + void BackwardReal(float[] spectrum, int n, FourierTransformScaling scaling); void BackwardReal(double[] spectrum, int n, FourierTransformScaling scaling); - + + void ForwardMultidim(Complex32[] samples, int[] dimensions, FourierTransformScaling scaling); void ForwardMultidim(Complex[] samples, int[] dimensions, FourierTransformScaling scaling); + void BackwardMultidim(Complex32[] spectrum, int[] dimensions, FourierTransformScaling scaling); void BackwardMultidim(Complex[] spectrum, int[] dimensions, FourierTransformScaling scaling); } } diff --git a/src/Numerics/Providers/FourierTransform/ManagedFourierTransformProvider.cs b/src/Numerics/Providers/FourierTransform/ManagedFourierTransformProvider.cs index f29e36a8..463e138c 100644 --- a/src/Numerics/Providers/FourierTransform/ManagedFourierTransformProvider.cs +++ b/src/Numerics/Providers/FourierTransform/ManagedFourierTransformProvider.cs @@ -58,6 +58,23 @@ namespace MathNet.Numerics.Providers.FourierTransform return "Managed"; } + public void Forward(Complex32[] samples, FourierTransformScaling scaling) + { + switch (scaling) + { + case FourierTransformScaling.SymmetricScaling: + Fourier.BluesteinForward(samples, FourierOptions.Default); + break; + case FourierTransformScaling.ForwardScaling: + // Only backward scaling can be expressed with options, hence the double-inverse + Fourier.BluesteinInverse(samples, FourierOptions.AsymmetricScaling | FourierOptions.InverseExponent); + break; + default: + Fourier.BluesteinForward(samples, FourierOptions.NoScaling); + break; + } + } + public void Forward(Complex[] samples, FourierTransformScaling scaling) { switch (scaling) @@ -75,6 +92,22 @@ namespace MathNet.Numerics.Providers.FourierTransform } } + public void Backward(Complex32[] spectrum, FourierTransformScaling scaling) + { + switch (scaling) + { + case FourierTransformScaling.SymmetricScaling: + Fourier.BluesteinInverse(spectrum, FourierOptions.Default); + break; + case FourierTransformScaling.BackwardScaling: + Fourier.BluesteinInverse(spectrum, FourierOptions.AsymmetricScaling); + break; + default: + Fourier.BluesteinInverse(spectrum, FourierOptions.NoScaling); + break; + } + } + public void Backward(Complex[] spectrum, FourierTransformScaling scaling) { switch (scaling) @@ -91,6 +124,37 @@ namespace MathNet.Numerics.Providers.FourierTransform } } + public void ForwardReal(float[] samples, int n, FourierTransformScaling scaling) + { + // TODO: backport proper, optimized implementation from Iridium + + Complex32[] data = new Complex32[n]; + for (int i = 0; i < data.Length; i++) + { + data[i] = new Complex32(samples[i], 0.0f); + } + + Forward(data, scaling); + + samples[0] = data[0].Real; + samples[1] = 0f; + for (int i = 1, j = 2; i < data.Length / 2; i++) + { + samples[j++] = data[i].Real; + samples[j++] = data[i].Imaginary; + } + if (n.IsEven()) + { + samples[n] = data[data.Length / 2].Real; + samples[n + 1] = 0f; + } + else + { + samples[n - 1] = data[data.Length / 2].Real; + samples[n] = data[data.Length / 2].Imaginary; + } + } + public void ForwardReal(double[] samples, int n, FourierTransformScaling scaling) { // TODO: backport proper, optimized implementation from Iridium @@ -122,6 +186,36 @@ namespace MathNet.Numerics.Providers.FourierTransform } } + public void BackwardReal(float[] spectrum, int n, FourierTransformScaling scaling) + { + // TODO: backport proper, optimized implementation from Iridium + + Complex32[] data = new Complex32[n]; + data[0] = new Complex32(spectrum[0], 0f); + for (int i = 1, j = 2; i < data.Length / 2; i++) + { + data[i] = new Complex32(spectrum[j++], spectrum[j++]); + data[data.Length - i] = data[i].Conjugate(); + } + if (n.IsEven()) + { + data[data.Length / 2] = new Complex32(spectrum[n], 0f); + } + else + { + data[data.Length / 2] = new Complex32(spectrum[n - 1], spectrum[n]); + data[data.Length / 2 + 1] = data[data.Length / 2].Conjugate(); + } + + Backward(data, scaling); + + for (int i = 0; i < data.Length; i++) + { + spectrum[i] = data[i].Real; + } + spectrum[n] = 0f; + } + public void BackwardReal(double[] spectrum, int n, FourierTransformScaling scaling) { // TODO: backport proper, optimized implementation from Iridium @@ -152,11 +246,21 @@ namespace MathNet.Numerics.Providers.FourierTransform spectrum[n] = 0d; } + public void ForwardMultidim(Complex32[] samples, int[] dimensions, FourierTransformScaling scaling) + { + throw new NotSupportedException(); + } + public void ForwardMultidim(Complex[] samples, int[] dimensions, FourierTransformScaling scaling) { throw new NotSupportedException(); } + public void BackwardMultidim(Complex32[] spectrum, int[] dimensions, FourierTransformScaling scaling) + { + throw new NotSupportedException(); + } + public void BackwardMultidim(Complex[] spectrum, int[] dimensions, FourierTransformScaling scaling) { throw new NotSupportedException(); diff --git a/src/Numerics/Providers/FourierTransform/Mkl/MklFourierTransformProvider.cs b/src/Numerics/Providers/FourierTransform/Mkl/MklFourierTransformProvider.cs index 4566d565..514d99f9 100644 --- a/src/Numerics/Providers/FourierTransform/Mkl/MklFourierTransformProvider.cs +++ b/src/Numerics/Providers/FourierTransform/Mkl/MklFourierTransformProvider.cs @@ -43,6 +43,7 @@ namespace MathNet.Numerics.Providers.FourierTransform.Mkl public int[] Dimensions; public FourierTransformScaling Scaling; public bool Real; + public bool Single; } Kernel _kernel; @@ -143,7 +144,7 @@ namespace MathNet.Numerics.Providers.FourierTransform.Mkl return MklProvider.Describe(); } - Kernel Configure(int length, FourierTransformScaling scaling, bool real) + Kernel Configure(int length, FourierTransformScaling scaling, bool real, bool single) { Kernel kernel = Interlocked.Exchange(ref _kernel, null); @@ -153,36 +154,54 @@ namespace MathNet.Numerics.Providers.FourierTransform.Mkl { Dimensions = new[] {length}, Scaling = scaling, - Real = real + Real = real, + Single = single }; - if (real) SafeNativeMethods.d_fft_create(out kernel.Handle, length, ForwardScaling(scaling, length), BackwardScaling(scaling, length)); - else SafeNativeMethods.z_fft_create(out kernel.Handle, length, ForwardScaling(scaling, length), BackwardScaling(scaling, length)); + if (single) + { + if (real) SafeNativeMethods.s_fft_create(out kernel.Handle, length, (float)ForwardScaling(scaling, length), (float)BackwardScaling(scaling, length)); + else SafeNativeMethods.c_fft_create(out kernel.Handle, length, (float)ForwardScaling(scaling, length), (float)BackwardScaling(scaling, length)); + } + else + { + if (real) SafeNativeMethods.d_fft_create(out kernel.Handle, length, ForwardScaling(scaling, length), BackwardScaling(scaling, length)); + else SafeNativeMethods.z_fft_create(out kernel.Handle, length, ForwardScaling(scaling, length), BackwardScaling(scaling, length)); + } return kernel; } - if (kernel.Dimensions.Length != 1 || kernel.Dimensions[0] != length || kernel.Scaling != scaling || kernel.Real != real) + if (kernel.Dimensions.Length != 1 || kernel.Dimensions[0] != length || kernel.Scaling != scaling || kernel.Real != real || kernel.Single != single) { SafeNativeMethods.x_fft_free(ref kernel.Handle); - if (real) SafeNativeMethods.d_fft_create(out kernel.Handle, length, ForwardScaling(scaling, length), BackwardScaling(scaling, length)); - else SafeNativeMethods.z_fft_create(out kernel.Handle, length, ForwardScaling(scaling, length), BackwardScaling(scaling, length)); + if (single) + { + if (real) SafeNativeMethods.s_fft_create(out kernel.Handle, length, (float)ForwardScaling(scaling, length), (float)BackwardScaling(scaling, length)); + else SafeNativeMethods.c_fft_create(out kernel.Handle, length, (float)ForwardScaling(scaling, length), (float)BackwardScaling(scaling, length)); + } + else + { + if (real) SafeNativeMethods.d_fft_create(out kernel.Handle, length, ForwardScaling(scaling, length), BackwardScaling(scaling, length)); + else SafeNativeMethods.z_fft_create(out kernel.Handle, length, ForwardScaling(scaling, length), BackwardScaling(scaling, length)); + } kernel.Dimensions = new[] {length}; kernel.Scaling = scaling; kernel.Real = real; + kernel.Single = single; return kernel; } return kernel; } - Kernel Configure(int[] dimensions, FourierTransformScaling scaling) + Kernel Configure(int[] dimensions, FourierTransformScaling scaling, bool single) { if (dimensions.Length == 1) { - return Configure(dimensions[0], scaling, false); + return Configure(dimensions[0], scaling, false, single); } Kernel kernel = Interlocked.Exchange(ref _kernel, null); @@ -193,7 +212,8 @@ namespace MathNet.Numerics.Providers.FourierTransform.Mkl { Dimensions = dimensions, Scaling = scaling, - Real = false + Real = false, + Single = single }; long length = 1; @@ -202,11 +222,19 @@ namespace MathNet.Numerics.Providers.FourierTransform.Mkl length *= dimensions[i]; } - SafeNativeMethods.z_fft_create_multidim(out kernel.Handle, dimensions.Length, dimensions, ForwardScaling(scaling, length), BackwardScaling(scaling, length)); + if (single) + { + SafeNativeMethods.c_fft_create_multidim(out kernel.Handle, dimensions.Length, dimensions, (float)ForwardScaling(scaling, length), (float)BackwardScaling(scaling, length)); + } + else + { + SafeNativeMethods.z_fft_create_multidim(out kernel.Handle, dimensions.Length, dimensions, ForwardScaling(scaling, length), BackwardScaling(scaling, length)); + } + return kernel; } - bool mismatch = kernel.Dimensions.Length != dimensions.Length || kernel.Scaling != scaling || kernel.Real != false; + bool mismatch = kernel.Dimensions.Length != dimensions.Length || kernel.Scaling != scaling || kernel.Real != false || kernel.Single != single; if (!mismatch) { for (int i = 0; i < dimensions.Length; i++) @@ -228,11 +256,20 @@ namespace MathNet.Numerics.Providers.FourierTransform.Mkl } SafeNativeMethods.x_fft_free(ref kernel.Handle); - SafeNativeMethods.z_fft_create_multidim(out kernel.Handle, dimensions.Length, dimensions, ForwardScaling(scaling, length), BackwardScaling(scaling, length)); + + if (single) + { + SafeNativeMethods.c_fft_create_multidim(out kernel.Handle, dimensions.Length, dimensions, (float)ForwardScaling(scaling, length), (float)BackwardScaling(scaling, length)); + } + else + { + SafeNativeMethods.z_fft_create_multidim(out kernel.Handle, dimensions.Length, dimensions, ForwardScaling(scaling, length), BackwardScaling(scaling, length)); + } kernel.Dimensions = dimensions; kernel.Scaling = scaling; kernel.Real = false; + kernel.Single = single; return kernel; } @@ -248,46 +285,90 @@ namespace MathNet.Numerics.Providers.FourierTransform.Mkl } } + public void Forward(Complex32[] samples, FourierTransformScaling scaling) + { + Kernel kernel = Configure(samples.Length, scaling, false, true); + SafeNativeMethods.c_fft_forward(kernel.Handle, samples); + Release(kernel); + } + public void Forward(Complex[] samples, FourierTransformScaling scaling) { - Kernel kernel = Configure(samples.Length, scaling, false); + Kernel kernel = Configure(samples.Length, scaling, false, false); SafeNativeMethods.z_fft_forward(kernel.Handle, samples); Release(kernel); } + public void Backward(Complex32[] spectrum, FourierTransformScaling scaling) + { + Kernel kernel = Configure(spectrum.Length, scaling, false, true); + SafeNativeMethods.c_fft_backward(kernel.Handle, spectrum); + Release(kernel); + } + public void Backward(Complex[] spectrum, FourierTransformScaling scaling) { - Kernel kernel = Configure(spectrum.Length, scaling, false); + Kernel kernel = Configure(spectrum.Length, scaling, false, false); SafeNativeMethods.z_fft_backward(kernel.Handle, spectrum); Release(kernel); } + public void ForwardReal(float[] samples, int n, FourierTransformScaling scaling) + { + Kernel kernel = Configure(n, scaling, true, true); + SafeNativeMethods.s_fft_forward(kernel.Handle, samples); + Release(kernel); + } + public void ForwardReal(double[] samples, int n, FourierTransformScaling scaling) { - Kernel kernel = Configure(n, scaling, true); + Kernel kernel = Configure(n, scaling, true, false); SafeNativeMethods.d_fft_forward(kernel.Handle, samples); Release(kernel); } + public void BackwardReal(float[] spectrum, int n, FourierTransformScaling scaling) + { + Kernel kernel = Configure(n, scaling, true, true); + SafeNativeMethods.s_fft_backward(kernel.Handle, spectrum); + Release(kernel); + + spectrum[n] = 0f; + } + public void BackwardReal(double[] spectrum, int n, FourierTransformScaling scaling) { - Kernel kernel = Configure(n, scaling, true); + Kernel kernel = Configure(n, scaling, true, false); SafeNativeMethods.d_fft_backward(kernel.Handle, spectrum); Release(kernel); spectrum[n] = 0d; } + public void ForwardMultidim(Complex32[] samples, int[] dimensions, FourierTransformScaling scaling) + { + Kernel kernel = Configure(dimensions, scaling, true); + SafeNativeMethods.c_fft_forward(kernel.Handle, samples); + Release(kernel); + } + public void ForwardMultidim(Complex[] samples, int[] dimensions, FourierTransformScaling scaling) { - Kernel kernel = Configure(dimensions, scaling); + Kernel kernel = Configure(dimensions, scaling, false); SafeNativeMethods.z_fft_forward(kernel.Handle, samples); Release(kernel); } + public void BackwardMultidim(Complex32[] spectrum, int[] dimensions, FourierTransformScaling scaling) + { + Kernel kernel = Configure(dimensions, scaling, true); + SafeNativeMethods.c_fft_backward(kernel.Handle, spectrum); + Release(kernel); + } + public void BackwardMultidim(Complex[] spectrum, int[] dimensions, FourierTransformScaling scaling) { - Kernel kernel = Configure(dimensions, scaling); + Kernel kernel = Configure(dimensions, scaling, false); SafeNativeMethods.z_fft_backward(kernel.Handle, spectrum); Release(kernel); } diff --git a/src/UnitTests/IntegralTransformsTests/FourierTest.cs b/src/UnitTests/IntegralTransformsTests/FourierTest.cs index 17443794..0c5a8a3d 100644 --- a/src/UnitTests/IntegralTransformsTests/FourierTest.cs +++ b/src/UnitTests/IntegralTransformsTests/FourierTest.cs @@ -53,6 +53,41 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests return new ContinuousUniform(-1, 1, new System.Random(seed)); } + /// + /// Naive transforms real sine correctly. + /// + [Test] + public void NaiveTransformsRealSineCorrectly32() + { + var samples = Generate.PeriodicMap(16, w => new Complex32((float)Math.Sin(w), 0), 16, 1.0, Constants.Pi2); + + // real-odd transforms to imaginary odd + var spectrum = Fourier.NaiveForward(samples, FourierOptions.Matlab); + + // all real components must be zero + foreach (var c in spectrum) + { + Assert.AreEqual(0, c.Real, 1e-6, "real"); + } + + // all imaginary components except second and last musth be zero + for (var i = 0; i < spectrum.Length; i++) + { + if (i == 1) + { + Assert.AreEqual(-8, spectrum[i].Imaginary, 1e-12, "imag second"); + } + else if (i == spectrum.Length - 1) + { + Assert.AreEqual(8, spectrum[i].Imaginary, 1e-12, "imag last"); + } + else + { + Assert.AreEqual(0, spectrum[i].Imaginary, 1e-6, "imag"); + } + } + } + /// /// Naive transforms real sine correctly. /// @@ -88,6 +123,20 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests } } + /// + /// Radix2XXX when not power of two throws ArgumentException. + /// + [Test] + public void Radix2ThrowsWhenNotPowerOfTwo32() + { + var samples = Generate.RandomComplex32(0x7F, GetUniform(1)); + + Assert.Throws(typeof(ArgumentException), () => Fourier.Radix2Forward(samples, FourierOptions.Default)); + Assert.Throws(typeof(ArgumentException), () => Fourier.Radix2Inverse(samples, FourierOptions.Default)); + Assert.Throws(typeof(ArgumentException), () => Fourier.Radix2(samples, -1)); + Assert.Throws(typeof(ArgumentException), () => Fourier.Radix2Parallel(samples, -1)); + } + /// /// Radix2XXX when not power of two throws ArgumentException. /// diff --git a/src/UnitTests/IntegralTransformsTests/InverseTransformTest.cs b/src/UnitTests/IntegralTransformsTests/InverseTransformTest.cs index 7db77ffc..f1ce9e62 100644 --- a/src/UnitTests/IntegralTransformsTests/InverseTransformTest.cs +++ b/src/UnitTests/IntegralTransformsTests/InverseTransformTest.cs @@ -52,6 +52,25 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests return new ContinuousUniform(-1, 1, new System.Random(seed)); } + /// + /// Fourier naive is reversible. + /// + /// Fourier options. + [TestCase(FourierOptions.Default)] + [TestCase(FourierOptions.Matlab)] + public void FourierNaiveIsReversible32(FourierOptions options) + { + var samples = Generate.RandomComplex32(0x80, GetUniform(1)); + var work = new Complex32[samples.Length]; + samples.CopyTo(work, 0); + + work = Fourier.NaiveForward(work, options); + Assert.IsFalse(work.ListAlmostEqual(samples, 6)); + + work = Fourier.NaiveInverse(work, options); + AssertHelpers.AlmostEqual(samples, work, 11); + } + /// /// Fourier naive is reversible. /// @@ -71,6 +90,25 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests AssertHelpers.AlmostEqual(samples, work, 12); } + /// + /// Fourier radix2xx is reversible. + /// + /// Fourier options. + [TestCase(FourierOptions.Default)] + [TestCase(FourierOptions.Matlab)] + public void FourierRadix2IsReversible32(FourierOptions options) + { + var samples = Generate.RandomComplex32(0x8000, GetUniform(1)); + var work = new Complex32[samples.Length]; + samples.CopyTo(work, 0); + + Fourier.Radix2Forward(work, options); + Assert.IsFalse(work.ListAlmostEqual(samples, 6)); + + Fourier.Radix2Inverse(work, options); + AssertHelpers.AlmostEqual(samples, work, 12); + } + /// /// Fourier radix2xx is reversible. /// @@ -90,6 +128,25 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests AssertHelpers.AlmostEqual(samples, work, 12); } + /// + /// Fourier bluestein is reversible. + /// + /// Fourier options. + [TestCase(FourierOptions.Default)] + [TestCase(FourierOptions.Matlab)] + public void FourierBluesteinIsReversible32(FourierOptions options) + { + var samples = Generate.RandomComplex32(0x7FFF, GetUniform(1)); + var work = new Complex32[samples.Length]; + samples.CopyTo(work, 0); + + Fourier.BluesteinForward(work, options); + Assert.IsFalse(work.ListAlmostEqual(samples, 6)); + + Fourier.BluesteinInverse(work, options); + AssertHelpers.AlmostEqual(samples, work, 10); + } + /// /// Fourier bluestein is reversible. /// @@ -109,6 +166,25 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests AssertHelpers.AlmostEqual(samples, work, 10); } + /// + /// Fourier bluestein is reversible. + /// + /// Fourier options. + [TestCase(FourierOptions.Default)] + [TestCase(FourierOptions.Matlab)] + public void FourierRealIsReversible32(FourierOptions options) + { + var samples = Generate.RandomSingle(0x7FFF, GetUniform(1)); + var work = new float[samples.Length + 2]; + samples.CopyTo(work, 0); + + Fourier.ForwardReal(work, samples.Length, options); + Assert.IsFalse(work.ListAlmostEqual(samples, 6)); + + Fourier.InverseReal(work, samples.Length, options); + AssertHelpers.AlmostEqual(samples, work, 5); + } + /// /// Fourier bluestein is reversible. /// @@ -147,6 +223,29 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests AssertHelpers.AlmostEqual(samples, work, 12); } + /// + /// Fourier default transform is reversible. + /// + [Test] + public void FourierDefaultTransformIsReversible32() + { + var samples = Generate.RandomComplex32(0x7FFF, GetUniform(1)); + var work = new Complex32[samples.Length]; + samples.CopyTo(work, 0); + + Fourier.Forward(work); + Assert.IsFalse(work.ListAlmostEqual(samples, 6)); + + Fourier.Inverse(work); + AssertHelpers.AlmostEqual(samples, work, 10); + + Fourier.Inverse(work, FourierOptions.Default); + Assert.IsFalse(work.ListAlmostEqual(samples, 6)); + + Fourier.Forward(work, FourierOptions.Default); + AssertHelpers.AlmostEqual(samples, work, 10); + } + /// /// Fourier default transform is reversible. /// diff --git a/src/UnitTests/IntegralTransformsTests/MatchingNaiveTransformTest.cs b/src/UnitTests/IntegralTransformsTests/MatchingNaiveTransformTest.cs index 2fc25462..43235569 100644 --- a/src/UnitTests/IntegralTransformsTests/MatchingNaiveTransformTest.cs +++ b/src/UnitTests/IntegralTransformsTests/MatchingNaiveTransformTest.cs @@ -54,6 +54,22 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests return new ContinuousUniform(-1, 1, new System.Random(seed)); } + static void Verify( + Complex32[] samples, + int maximumErrorDecimalPlaces, + FourierOptions options, + Func naive, + Action fast) + { + var spectrumNaive = naive(samples, options); + + var spectrumFast = new Complex32[samples.Length]; + samples.CopyTo(spectrumFast, 0); + fast(spectrumFast, options); + + AssertHelpers.AlmostEqual(spectrumNaive, spectrumFast, maximumErrorDecimalPlaces); + } + static void Verify( Complex[] samples, int maximumErrorDecimalPlaces, @@ -70,6 +86,24 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests AssertHelpers.AlmostEqual(spectrumNaive, spectrumFast, maximumErrorDecimalPlaces); } + static void VerifyInplace( + Complex32[] samples, + int maximumErrorDecimalPlaces, + FourierOptions options, + Action expected, + Action actual) + { + var spectrumExpected = new Complex32[samples.Length]; + samples.CopyTo(spectrumExpected, 0); + expected(spectrumExpected, options); + + var spectrumActual = new Complex32[samples.Length]; + samples.CopyTo(spectrumActual, 0); + actual(spectrumActual, options); + + AssertHelpers.AlmostEqual(spectrumExpected, spectrumActual, maximumErrorDecimalPlaces); + } + static void VerifyInplace( Complex[] samples, int maximumErrorDecimalPlaces, @@ -88,6 +122,21 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests AssertHelpers.AlmostEqual(spectrumExpected, spectrumActual, maximumErrorDecimalPlaces); } + /// + /// Fourier Radix2XX matches naive on real sine. + /// + /// Fourier options. + [TestCase(FourierOptions.Default)] + [TestCase(FourierOptions.Matlab)] + [TestCase(FourierOptions.NumericalRecipes)] + public void FourierRadix2MatchesNaive_RealSine_32(FourierOptions options) + { + var samples = Generate.PeriodicMap(16, w => new Complex32((float)Math.Sin(w), 0), 16, 1.0, Constants.Pi2); + + Verify(samples, 12, options, Fourier.NaiveForward, Fourier.Radix2Forward); + Verify(samples, 12, options, Fourier.NaiveInverse, Fourier.Radix2Inverse); + } + /// /// Fourier Radix2XX matches naive on real sine. /// @@ -103,6 +152,21 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests Verify(samples, 12, options, Fourier.NaiveInverse, Fourier.Radix2Inverse); } + /// + /// Fourier Radix2XX matches naive on random. + /// + /// Fourier options. + [TestCase(FourierOptions.Default)] + [TestCase(FourierOptions.Matlab)] + [TestCase(FourierOptions.NumericalRecipes)] + public void FourierRadix2MatchesNaive_Random_32(FourierOptions options) + { + var samples = Generate.RandomComplex32(0x80, GetUniform(1)); + + Verify(samples, 10, options, Fourier.NaiveForward, Fourier.Radix2Forward); + Verify(samples, 10, options, Fourier.NaiveInverse, Fourier.Radix2Inverse); + } + /// /// Fourier Radix2XX matches naive on random. /// @@ -118,6 +182,21 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests Verify(samples, 10, options, Fourier.NaiveInverse, Fourier.Radix2Inverse); } + /// + /// Fourier bluestein matches naive on real sine non-power of two. + /// + /// Fourier options. + [TestCase(FourierOptions.Default)] + [TestCase(FourierOptions.Matlab)] + [TestCase(FourierOptions.NumericalRecipes)] + public void FourierBluesteinMatchesNaive_RealSine_Arbitrary_32(FourierOptions options) + { + var samples = Generate.PeriodicMap(14, w => new Complex32((float)Math.Sin(w), 0), 14, 1.0, Constants.Pi2); + + Verify(samples, 11, options, Fourier.NaiveForward, Fourier.BluesteinForward); + Verify(samples, 11, options, Fourier.NaiveInverse, Fourier.BluesteinInverse); + } + /// /// Fourier bluestein matches naive on real sine non-power of two. /// @@ -133,6 +212,21 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests Verify(samples, 12, options, Fourier.NaiveInverse, Fourier.BluesteinInverse); } + /// + /// Fourier bluestein matches naive on random power of two. + /// + /// Fourier options. + [TestCase(FourierOptions.Default)] + [TestCase(FourierOptions.Matlab)] + [TestCase(FourierOptions.NumericalRecipes)] + public void FourierBluesteinMatchesNaive_Random_PowerOfTwo_32(FourierOptions options) + { + var samples = Generate.RandomComplex32(0x80, GetUniform(1)); + + Verify(samples, 10, options, Fourier.NaiveForward, Fourier.BluesteinForward); + Verify(samples, 10, options, Fourier.NaiveInverse, Fourier.BluesteinInverse); + } + /// /// Fourier bluestein matches naive on random power of two. /// @@ -148,6 +242,21 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests Verify(samples, 10, options, Fourier.NaiveInverse, Fourier.BluesteinInverse); } + /// + /// Fourier bluestein matches naive on random non-power of two. + /// + /// Fourier options. + [TestCase(FourierOptions.Default)] + [TestCase(FourierOptions.Matlab)] + [TestCase(FourierOptions.NumericalRecipes)] + public void FourierBluesteinMatchesNaive_Random_Arbitrary_32(FourierOptions options) + { + var samples = Generate.RandomComplex32(0x7F, GetUniform(1)); + + Verify(samples, 9, options, Fourier.NaiveForward, Fourier.BluesteinForward); + Verify(samples, 9, options, Fourier.NaiveInverse, Fourier.BluesteinInverse); + } + /// /// Fourier bluestein matches naive on random non-power of two. /// @@ -163,6 +272,24 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests Verify(samples, 10, options, Fourier.NaiveInverse, Fourier.BluesteinInverse); } + /// + /// Fourier bluestein matches providers on random power of two. + /// + /// Fourier options. + [TestCase(FourierOptions.Default)] + [TestCase(FourierOptions.NoScaling)] + [TestCase(FourierOptions.AsymmetricScaling)] + [TestCase(FourierOptions.InverseExponent)] + [TestCase(FourierOptions.InverseExponent | FourierOptions.NoScaling)] + [TestCase(FourierOptions.InverseExponent | FourierOptions.AsymmetricScaling)] + public void FourierBluesteinMatchesProvider_Random_Arbitrary_32(FourierOptions options) + { + var samples = Generate.RandomComplex32(0x7F, GetUniform(1)); + + VerifyInplace(samples, 10, options, Fourier.Forward, Fourier.BluesteinForward); + VerifyInplace(samples, 10, options, Fourier.Inverse, Fourier.BluesteinInverse); + } + /// /// Fourier bluestein matches providers on random power of two. /// @@ -181,6 +308,33 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests VerifyInplace(samples, 10, options, Fourier.Inverse, Fourier.BluesteinInverse); } + [TestCase(FourierOptions.Default, 128)] + [TestCase(FourierOptions.Default, 129)] + [TestCase(FourierOptions.NoScaling, 128)] + [TestCase(FourierOptions.NoScaling, 129)] + [TestCase(FourierOptions.AsymmetricScaling, 128)] + [TestCase(FourierOptions.AsymmetricScaling, 129)] + public void RealMatchesComplex32(FourierOptions options, int n) + { + var real = Generate.RandomSingle(n.IsEven() ? n + 2 : n + 1, GetUniform(1)); + real[n] = 0f; + if (n.IsEven()) real[n + 1] = 0f; + var complex = new Complex32[n]; + for (int i = 0; i < complex.Length; i++) + { + complex[i] = new Complex32(real[i], 0f); + } + + Fourier.Forward(complex, options); + Fourier.ForwardReal(real, n, options); + + int m = (n + 1) / 2; + for (int i = 0, j = 0; i < m; i++) + { + AssertHelpers.AlmostEqual(complex[i], new Complex32(real[j++], real[j++]), 6); + } + } + [TestCase(FourierOptions.Default, 128)] [TestCase(FourierOptions.Default, 129)] [TestCase(FourierOptions.NoScaling, 128)] @@ -208,6 +362,16 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests } } + [Test] + public void AlgorithmsMatchProvider_PowerOfTwo_Large_32() + { + // 65536 = 2^16 + var samples = Generate.RandomComplex32(65536, GetUniform(1)); + + VerifyInplace(samples, 10, FourierOptions.NoScaling, (s, o) => Control.FourierTransformProvider.Forward(s, FourierTransformScaling.NoScaling), Fourier.Radix2Forward); + VerifyInplace(samples, 10, FourierOptions.NoScaling, (s, o) => Control.FourierTransformProvider.Forward(s, FourierTransformScaling.NoScaling), Fourier.BluesteinForward); + } + [Test] public void AlgorithmsMatchProvider_PowerOfTwo_Large() { @@ -218,6 +382,20 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests VerifyInplace(samples, 10, FourierOptions.NoScaling, (s, o) => Control.FourierTransformProvider.Forward(s, FourierTransformScaling.NoScaling), Fourier.BluesteinForward); } + [Test] + public void AlgorithmsMatchProvider_Arbitrary_Large_32() + { + // 30870 = 2*3*3*5*7*7*7 + const FourierOptions options = FourierOptions.NoScaling; + var samples = Generate.RandomComplex32(30870, GetUniform(1)); + + var provider = new Complex32[samples.Length]; + samples.Copy(provider); + Control.FourierTransformProvider.Forward(provider, FourierTransformScaling.NoScaling); + + Verify(samples, 10, options, (a, b) => provider, Fourier.BluesteinForward); + } + [Test] public void AlgorithmsMatchProvider_Arbitrary_Large() { @@ -232,6 +410,19 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests Verify(samples, 10, options, (a, b) => provider, Fourier.BluesteinForward); } + [Test] + public void AlgorithmsMatchProvider_Arbitrary_Large_GH286_32() + { + const FourierOptions options = FourierOptions.NoScaling; + var samples = Generate.RandomComplex32(46500, GetUniform(1)); + + var provider = new Complex32[samples.Length]; + samples.Copy(provider); + Control.FourierTransformProvider.Forward(provider, FourierTransformScaling.NoScaling); + + Verify(samples, 10, options, (a, b) => provider, Fourier.BluesteinForward); + } + [Test] public void AlgorithmsMatchProvider_Arbitrary_Large_GH286() { @@ -245,6 +436,18 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests Verify(samples, 10, options, (a, b) => provider, Fourier.BluesteinForward); } + [Test, Explicit("Long-Running")] + public void AlgorithmsMatchNaive_PowerOfTwo_Large_32() + { + // 65536 = 2^16 + const FourierOptions options = FourierOptions.NoScaling; + var samples = Generate.RandomComplex32(65536, GetUniform(1)); + var naive = Fourier.NaiveForward(samples, options); + + Verify(samples, 3, options, (a, b) => naive, Fourier.Radix2Forward); + Verify(samples, 3, options, (a, b) => naive, Fourier.BluesteinForward); + } + [Test, Explicit("Long-Running")] public void AlgorithmsMatchNaive_PowerOfTwo_Large() { @@ -257,6 +460,16 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests Verify(samples, 10, options, (a, b) => naive, Fourier.BluesteinForward); } + [Test, Explicit("Long-Running")] + public void AlgorithmsMatchNaive_Arbitrary_Large_32() + { + // 30870 = 2*3*3*5*7*7*7 + const FourierOptions options = FourierOptions.NoScaling; + var samples = Generate.RandomComplex32(30870, GetUniform(1)); + + Verify(samples, 4, options, Fourier.NaiveForward, Fourier.BluesteinForward); + } + [Test, Explicit("Long-Running")] public void AlgorithmsMatchNaive_Arbitrary_Large() { @@ -268,6 +481,15 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests Verify(samples, 10, options, (a, b) => naive, Fourier.BluesteinForward); } + [Test, Explicit("Long-Running")] + public void AlgorithmsMatchNaive_Arbitrary_Large_GH286_32() + { + const FourierOptions options = FourierOptions.NoScaling; + var samples = Generate.RandomComplex32(46500, GetUniform(1)); + + Verify(samples, 4, options, Fourier.NaiveForward, Fourier.BluesteinForward); + } + [Test, Explicit("Long-Running")] public void AlgorithmsMatchNaive_Arbitrary_Large_GH286() { diff --git a/src/UnitTests/IntegralTransformsTests/ParsevalTheoremTest.cs b/src/UnitTests/IntegralTransformsTests/ParsevalTheoremTest.cs index de5aaadb..b0f8189e 100644 --- a/src/UnitTests/IntegralTransformsTests/ParsevalTheoremTest.cs +++ b/src/UnitTests/IntegralTransformsTests/ParsevalTheoremTest.cs @@ -54,6 +54,29 @@ namespace MathNet.Numerics.UnitTests.IntegralTransformsTests return new ContinuousUniform(-1, 1, new System.Random(seed)); } + /// + /// Fourier default transform satisfies Parseval's theorem. + /// + /// Samples count. + [TestCase(0x1000)] + [TestCase(0x7FF)] + public void FourierDefaultTransformSatisfiesParsevalsTheorem32(int count) + { + var samples = Generate.RandomComplex32(count, GetUniform(1)); + + var timeSpaceEnergy = (from s in samples select s.MagnitudeSquared()).Mean(); + + var work = new Complex32[samples.Length]; + samples.CopyTo(work, 0); + + // Default -> Symmetric Scaling + Fourier.Forward(work); + + var frequencySpaceEnergy = (from s in work select s.MagnitudeSquared()).Mean(); + + Assert.AreEqual(timeSpaceEnergy, frequencySpaceEnergy, 1e-7); + } + /// /// Fourier default transform satisfies Parseval's theorem. ///