diff --git a/src/Numerics.Tests/IntegralTransformsTests/FourierTest.cs b/src/Numerics.Tests/IntegralTransformsTests/FourierTest.cs
index 480113980..d3db11e6a 100644
--- a/src/Numerics.Tests/IntegralTransformsTests/FourierTest.cs
+++ b/src/Numerics.Tests/IntegralTransformsTests/FourierTest.cs
@@ -167,5 +167,118 @@ public void FourierDefaultTransformsRealSineCorrectly64()
}
}
}
+ [Test]
+ public void Forward2DAndInverse2DRoundtrip64()
+ {
+ const int rows = 4;
+ const int cols = 8;
+ var original = new Complex[rows * cols];
+ var rng = new System.Random(42);
+ for (int i = 0; i < original.Length; i++)
+ {
+ original[i] = new Complex(rng.NextDouble(), rng.NextDouble());
+ }
+
+ var samples = (Complex[])original.Clone();
+
+ Fourier.Forward2D(samples, rows, cols, FourierOptions.Default);
+
+ // Verify the transform actually changed the data
+ bool anyDifferent = false;
+ for (int i = 0; i < samples.Length; i++)
+ {
+ if ((samples[i] - original[i]).Magnitude > 1e-10)
+ {
+ anyDifferent = true;
+ break;
+ }
+ }
+ Assert.IsTrue(anyDifferent, "Forward2D should modify the data");
+
+ Fourier.Inverse2D(samples, rows, cols, FourierOptions.Default);
+
+ // Verify roundtrip recovers original data
+ for (int i = 0; i < samples.Length; i++)
+ {
+ Assert.AreEqual(original[i].Real, samples[i].Real, 1e-10, $"Real part mismatch at index {i}");
+ Assert.AreEqual(original[i].Imaginary, samples[i].Imaginary, 1e-10, $"Imaginary part mismatch at index {i}");
+ }
+ }
+
+ [Test]
+ public void Forward2DAndInverse2DRoundtrip32()
+ {
+ const int rows = 4;
+ const int cols = 8;
+ var original = new Complex32[rows * cols];
+ var rng = new System.Random(42);
+ for (int i = 0; i < original.Length; i++)
+ {
+ original[i] = new Complex32((float)rng.NextDouble(), (float)rng.NextDouble());
+ }
+
+ var samples = (Complex32[])original.Clone();
+
+ Fourier.Forward2D(samples, rows, cols, FourierOptions.Default);
+ Fourier.Inverse2D(samples, rows, cols, FourierOptions.Default);
+
+ // Verify roundtrip recovers original data
+ for (int i = 0; i < samples.Length; i++)
+ {
+ Assert.AreEqual(original[i].Real, samples[i].Real, 1e-4, $"Real part mismatch at index {i}");
+ Assert.AreEqual(original[i].Imaginary, samples[i].Imaginary, 1e-4, $"Imaginary part mismatch at index {i}");
+ }
+ }
+
+ [Test]
+ public void Forward2DKnownDCValue64()
+ {
+ // A constant 4x4 array should produce all energy in the DC bin
+ const int rows = 4;
+ const int cols = 4;
+ var samples = new Complex[rows * cols];
+ for (int i = 0; i < samples.Length; i++)
+ {
+ samples[i] = new Complex(1.0, 0.0);
+ }
+
+ Fourier.Forward2D(samples, rows, cols, FourierOptions.NoScaling);
+
+ // DC component should equal rows*cols = 16
+ Assert.AreEqual(16.0, samples[0].Real, 1e-10, "DC real");
+ Assert.AreEqual(0.0, samples[0].Imaginary, 1e-10, "DC imag");
+
+ // All other components should be zero
+ for (int i = 1; i < samples.Length; i++)
+ {
+ Assert.AreEqual(0.0, samples[i].Real, 1e-10, $"Non-DC real at {i}");
+ Assert.AreEqual(0.0, samples[i].Imaginary, 1e-10, $"Non-DC imag at {i}");
+ }
+ }
+
+ [Test]
+ public void Forward2DNonPowerOfTwoDimensions64()
+ {
+ // Test with non-power-of-two dimensions to exercise Bluestein path
+ const int rows = 3;
+ const int cols = 5;
+ var original = new Complex[rows * cols];
+ var rng = new System.Random(123);
+ for (int i = 0; i < original.Length; i++)
+ {
+ original[i] = new Complex(rng.NextDouble(), rng.NextDouble());
+ }
+
+ var samples = (Complex[])original.Clone();
+
+ Fourier.Forward2D(samples, rows, cols, FourierOptions.Default);
+ Fourier.Inverse2D(samples, rows, cols, FourierOptions.Default);
+
+ for (int i = 0; i < samples.Length; i++)
+ {
+ Assert.AreEqual(original[i].Real, samples[i].Real, 1e-10, $"Real part mismatch at index {i}");
+ Assert.AreEqual(original[i].Imaginary, samples[i].Imaginary, 1e-10, $"Imaginary part mismatch at index {i}");
+ }
+ }
}
}
diff --git a/src/Numerics/Providers/FourierTransform/ManagedFourierTransformProvider.cs b/src/Numerics/Providers/FourierTransform/ManagedFourierTransformProvider.cs
index 36c6a4ba2..41eaf629a 100644
--- a/src/Numerics/Providers/FourierTransform/ManagedFourierTransformProvider.cs
+++ b/src/Numerics/Providers/FourierTransform/ManagedFourierTransformProvider.cs
@@ -320,22 +320,198 @@ public void BackwardReal(double[] spectrum, int n, FourierTransformScaling scali
public void ForwardMultidim(Complex32[] samples, int[] dimensions, FourierTransformScaling scaling)
{
- throw new NotSupportedException();
+ TransformMultidim(samples, dimensions, true);
+ ApplyMultidimScaling(samples, scaling, true);
}
public void ForwardMultidim(Complex[] samples, int[] dimensions, FourierTransformScaling scaling)
{
- throw new NotSupportedException();
+ TransformMultidim(samples, dimensions, true);
+ ApplyMultidimScaling(samples, scaling, true);
}
public void BackwardMultidim(Complex32[] spectrum, int[] dimensions, FourierTransformScaling scaling)
{
- throw new NotSupportedException();
+ TransformMultidim(spectrum, dimensions, false);
+ ApplyMultidimScaling(spectrum, scaling, false);
}
public void BackwardMultidim(Complex[] spectrum, int[] dimensions, FourierTransformScaling scaling)
{
- throw new NotSupportedException();
+ TransformMultidim(spectrum, dimensions, false);
+ ApplyMultidimScaling(spectrum, scaling, false);
+ }
+
+ ///
+ /// Apply separable 1D FFT along each dimension of a row-major N-D array.
+ ///
+ static void TransformMultidim(Complex32[] samples, int[] dimensions, bool forward)
+ {
+ int totalLength = samples.Length;
+
+ for (int d = 0; d < dimensions.Length; d++)
+ {
+ int dimLength = dimensions[d];
+ int innerLength = 1;
+ for (int k = d + 1; k < dimensions.Length; k++)
+ {
+ innerLength *= dimensions[k];
+ }
+
+ int outerLength = totalLength / (dimLength * innerLength);
+
+ var slice = new Complex32[dimLength];
+
+ for (int outer = 0; outer < outerLength; outer++)
+ {
+ for (int inner = 0; inner < innerLength; inner++)
+ {
+ int baseIndex = outer * dimLength * innerLength + inner;
+
+ // Extract slice along dimension d
+ for (int i = 0; i < dimLength; i++)
+ {
+ slice[i] = samples[baseIndex + i * innerLength];
+ }
+
+ // Apply 1D FFT (no scaling - we scale once at the end)
+ if (forward)
+ {
+ if (dimLength.IsPowerOfTwo())
+ {
+ if (dimLength >= 1024)
+ Radix2ForwardParallel(slice);
+ else
+ Radix2Forward(slice);
+ }
+ else
+ {
+ BluesteinForward(slice);
+ }
+ }
+ else
+ {
+ if (dimLength.IsPowerOfTwo())
+ {
+ if (dimLength >= 1024)
+ Radix2InverseParallel(slice);
+ else
+ Radix2Inverse(slice);
+ }
+ else
+ {
+ BluesteinInverse(slice);
+ }
+ }
+
+ // Write back
+ for (int i = 0; i < dimLength; i++)
+ {
+ samples[baseIndex + i * innerLength] = slice[i];
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Apply separable 1D FFT along each dimension of a row-major N-D array.
+ ///
+ static void TransformMultidim(Complex[] samples, int[] dimensions, bool forward)
+ {
+ int totalLength = samples.Length;
+
+ for (int d = 0; d < dimensions.Length; d++)
+ {
+ int dimLength = dimensions[d];
+ int innerLength = 1;
+ for (int k = d + 1; k < dimensions.Length; k++)
+ {
+ innerLength *= dimensions[k];
+ }
+
+ int outerLength = totalLength / (dimLength * innerLength);
+
+ var slice = new Complex[dimLength];
+
+ for (int outer = 0; outer < outerLength; outer++)
+ {
+ for (int inner = 0; inner < innerLength; inner++)
+ {
+ int baseIndex = outer * dimLength * innerLength + inner;
+
+ // Extract slice along dimension d
+ for (int i = 0; i < dimLength; i++)
+ {
+ slice[i] = samples[baseIndex + i * innerLength];
+ }
+
+ // Apply 1D FFT (no scaling - we scale once at the end)
+ if (forward)
+ {
+ if (dimLength.IsPowerOfTwo())
+ {
+ if (dimLength >= 1024)
+ Radix2ForwardParallel(slice);
+ else
+ Radix2Forward(slice);
+ }
+ else
+ {
+ BluesteinForward(slice);
+ }
+ }
+ else
+ {
+ if (dimLength.IsPowerOfTwo())
+ {
+ if (dimLength >= 1024)
+ Radix2InverseParallel(slice);
+ else
+ Radix2Inverse(slice);
+ }
+ else
+ {
+ BluesteinInverse(slice);
+ }
+ }
+
+ // Write back
+ for (int i = 0; i < dimLength; i++)
+ {
+ samples[baseIndex + i * innerLength] = slice[i];
+ }
+ }
+ }
+ }
+ }
+
+ static void ApplyMultidimScaling(Complex32[] samples, FourierTransformScaling scaling, bool forward)
+ {
+ switch (scaling)
+ {
+ case FourierTransformScaling.SymmetricScaling:
+ HalfRescale(samples);
+ break;
+ case FourierTransformScaling.ForwardScaling when forward:
+ case FourierTransformScaling.BackwardScaling when !forward:
+ FullRescale(samples);
+ break;
+ }
+ }
+
+ static void ApplyMultidimScaling(Complex[] samples, FourierTransformScaling scaling, bool forward)
+ {
+ switch (scaling)
+ {
+ case FourierTransformScaling.SymmetricScaling:
+ HalfRescale(samples);
+ break;
+ case FourierTransformScaling.ForwardScaling when forward:
+ case FourierTransformScaling.BackwardScaling when !forward:
+ FullRescale(samples);
+ break;
+ }
}
}
}