blob: d4d31bc114306e83da612ab607e5bfcee65676b5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.math4.transform;
import java.util.function.DoubleUnaryOperator;
import org.junit.Assert;
import org.junit.Test;
import org.apache.commons.rng.UniformRandomProvider;
import org.apache.commons.rng.simple.RandomSource;
import org.apache.commons.numbers.complex.Complex;
import org.apache.commons.math3.analysis.UnivariateFunction;
import org.apache.commons.math3.analysis.function.Sin;
import org.apache.commons.math3.analysis.function.Sinc;
/**
* Test case for {@link FastFourierTransform}.
* <p>
* FFT algorithm is exact, the small tolerance number is used only
* to account for round-off errors.
*/
public final class FastFourierTransformerTest {
private static final Sin SIN_FUNCTION = new Sin();
private static final DoubleUnaryOperator SIN = x -> SIN_FUNCTION.value(x);
/** RNG. */
private static final UniformRandomProvider RNG = RandomSource.MWC_256.create();
// Precondition checks.
@Test
public void testTransformComplexSizeNotAPowerOfTwo() {
final int n = 127;
final Complex[] x = createComplexData(n);
final FastFourierTransform.Norm[] norm = FastFourierTransform.Norm.values();
for (int i = 0; i < norm.length; i++) {
for (boolean type : new boolean[] {true, false}) {
final FastFourierTransform fft = new FastFourierTransform(norm[i], type);
try {
fft.apply(x);
Assert.fail(norm[i] + ", " + type +
": IllegalArgumentException was expected");
} catch (IllegalArgumentException e) {
// Expected behaviour
}
}
}
}
@Test
public void testTransformRealSizeNotAPowerOfTwo() {
final int n = 127;
final double[] x = createRealData(n);
final FastFourierTransform.Norm[] norm = FastFourierTransform.Norm.values();
for (int i = 0; i < norm.length; i++) {
for (boolean type : new boolean[] {true, false}) {
final FastFourierTransform fft = new FastFourierTransform(norm[i], type);
try {
fft.apply(x);
Assert.fail(norm[i] + ", " + type +
": IllegalArgumentException was expected");
} catch (IllegalArgumentException e) {
// Expected behaviour
}
}
}
}
@Test
public void testTransformFunctionSizeNotAPowerOfTwo() {
final int n = 127;
final FastFourierTransform.Norm[] norm = FastFourierTransform.Norm.values();
for (int i = 0; i < norm.length; i++) {
for (boolean type : new boolean[] {true, false}) {
final FastFourierTransform fft = new FastFourierTransform(norm[i], type);
try {
fft.apply(SIN, 0.0, Math.PI, n);
Assert.fail(norm[i] + ", " + type +
": IllegalArgumentException was expected");
} catch (IllegalArgumentException e) {
// Expected behaviour
}
}
}
}
@Test
public void testTransformFunctionNotStrictlyPositiveNumberOfSamples() {
final int n = -128;
final FastFourierTransform.Norm[] norm = FastFourierTransform.Norm.values();
for (int i = 0; i < norm.length; i++) {
for (boolean type : new boolean[] {true, false}) {
final FastFourierTransform fft = new FastFourierTransform(norm[i], type);
try {
fft.apply(SIN, 0.0, Math.PI, n);
fft.apply(SIN, 0.0, Math.PI, n);
Assert.fail(norm[i] + ", " + type +
": IllegalArgumentException was expected");
} catch (IllegalArgumentException e) {
// Expected behaviour
}
}
}
}
@Test
public void testTransformFunctionInvalidBounds() {
final int n = 128;
final FastFourierTransform.Norm[] norm = FastFourierTransform.Norm.values();
for (int i = 0; i < norm.length; i++) {
for (boolean type : new boolean[] {true, false}) {
final FastFourierTransform fft = new FastFourierTransform(norm[i], type);
try {
fft.apply(SIN, Math.PI, 0.0, n);
Assert.fail(norm[i] + ", " + type +
": IllegalArgumentException was expected");
} catch (IllegalArgumentException e) {
// Expected behaviour
}
}
}
}
// Utility methods for checking (successful) transforms.
private static Complex[] createComplexData(final int n) {
final Complex[] data = new Complex[n];
for (int i = 0; i < n; i++) {
final double re = 2 * RNG.nextDouble() - 1;
final double im = 2 * RNG.nextDouble() - 1;
data[i] = Complex.ofCartesian(re, im);
}
return data;
}
private static double[] createRealData(final int n) {
final double[] data = new double[n];
for (int i = 0; i < n; i++) {
data[i] = 2 * RNG.nextDouble() - 1;
}
return data;
}
/** Naive implementation of DFT, for reference. */
private static Complex[] dft(final Complex[] x, final int sgn) {
final int n = x.length;
final double[] cos = new double[n];
final double[] sin = new double[n];
final Complex[] y = new Complex[n];
for (int i = 0; i < n; i++) {
final double arg = 2.0 * Math.PI * i / n;
cos[i] = Math.cos(arg);
sin[i] = Math.sin(arg);
}
for (int i = 0; i < n; i++) {
double yr = 0.0;
double yi = 0.0;
for (int j = 0; j < n; j++) {
final int index = (i * j) % n;
final double c = cos[index];
final double s = sin[index];
final double xr = x[j].getReal();
final double xi = x[j].getImaginary();
yr += c * xr - sgn * s * xi;
yi += sgn * s * xr + c * xi;
}
y[i] = Complex.ofCartesian(yr, yi);
}
return y;
}
private static void doTestTransformComplex(final int n,
final double tol,
final FastFourierTransform.Norm normalization,
boolean inverse) {
final FastFourierTransform fft = new FastFourierTransform(normalization, inverse);
final Complex[] x = createComplexData(n);
final Complex[] expected;
final double s;
if (!inverse) {
expected = dft(x, -1);
if (normalization == FastFourierTransform.Norm.STD) {
s = 1.0;
} else {
s = 1.0 / Math.sqrt(n);
}
} else {
expected = dft(x, 1);
if (normalization == FastFourierTransform.Norm.STD) {
s = 1.0 / n;
} else {
s = 1.0 / Math.sqrt(n);
}
}
final Complex[] actual = fft.apply(x);
for (int i = 0; i < n; i++) {
final String msg;
msg = String.format("%s, %s, %d, %d", normalization, inverse, n, i);
final double re = s * expected[i].getReal();
Assert.assertEquals(msg, re, actual[i].getReal(),
tol * Math.abs(re));
final double im = s * expected[i].getImaginary();
Assert.assertEquals(msg, im, actual[i].getImaginary(),
tol * Math.abs(re));
}
}
private static void doTestTransformReal(final int n,
final double tol,
final FastFourierTransform.Norm normalization,
final boolean inverse) {
final FastFourierTransform fft = new FastFourierTransform(normalization, inverse);
final double[] x = createRealData(n);
final Complex[] xc = new Complex[n];
for (int i = 0; i < n; i++) {
xc[i] = Complex.ofCartesian(x[i], 0.0);
}
final Complex[] expected;
final double s;
if (!inverse) {
expected = dft(xc, -1);
if (normalization == FastFourierTransform.Norm.STD) {
s = 1.0;
} else {
s = 1.0 / Math.sqrt(n);
}
} else {
expected = dft(xc, 1);
if (normalization == FastFourierTransform.Norm.STD) {
s = 1.0 / n;
} else {
s = 1.0 / Math.sqrt(n);
}
}
final Complex[] actual = fft.apply(x);
for (int i = 0; i < n; i++) {
final String msg;
msg = String.format("%s, %s, %d, %d", normalization, inverse, n, i);
final double re = s * expected[i].getReal();
Assert.assertEquals(msg, re, actual[i].getReal(),
tol * Math.abs(re));
final double im = s * expected[i].getImaginary();
Assert.assertEquals(msg, im, actual[i].getImaginary(),
tol * Math.abs(re));
}
}
private static void doTestTransformFunction(final DoubleUnaryOperator f,
final double min,
final double max,
int n,
final double tol,
final FastFourierTransform.Norm normalization,
final boolean inverse) {
final FastFourierTransform fft = new FastFourierTransform(normalization, inverse);
final Complex[] x = new Complex[n];
for (int i = 0; i < n; i++) {
final double t = min + i * (max - min) / n;
x[i] = Complex.ofCartesian(f.applyAsDouble(t), 0);
}
final Complex[] expected;
final double s;
if (!inverse) {
expected = dft(x, -1);
if (normalization == FastFourierTransform.Norm.STD) {
s = 1.0;
} else {
s = 1.0 / Math.sqrt(n);
}
} else {
expected = dft(x, 1);
if (normalization == FastFourierTransform.Norm.STD) {
s = 1.0 / n;
} else {
s = 1.0 / Math.sqrt(n);
}
}
final Complex[] actual = fft.apply(f, min, max, n);
for (int i = 0; i < n; i++) {
final String msg = String.format("%d, %d", n, i);
final double re = s * expected[i].getReal();
Assert.assertEquals(msg, re, actual[i].getReal(),
tol * Math.abs(re));
final double im = s * expected[i].getImaginary();
Assert.assertEquals(msg, im, actual[i].getImaginary(),
tol * Math.abs(re));
}
}
// Tests of standard transform (when data is valid).
@Test
public void testTransformComplex() {
final FastFourierTransform.Norm[] norm = FastFourierTransform.Norm.values();
for (int i = 0; i < norm.length; i++) {
for (boolean type : new boolean[] {true, false}) {
doTestTransformComplex(2, 1e-15, norm[i], type);
doTestTransformComplex(4, 1e-14, norm[i], type);
doTestTransformComplex(8, 1e-13, norm[i], type);
doTestTransformComplex(16, 1e-13, norm[i], type);
doTestTransformComplex(32, 1e-12, norm[i], type);
doTestTransformComplex(64, 1e-11, norm[i], type);
doTestTransformComplex(128, 1e-11, norm[i], type);
}
}
}
@Test
public void testStandardTransformReal() {
final FastFourierTransform.Norm[] norm = FastFourierTransform.Norm.values();
for (int i = 0; i < norm.length; i++) {
for (boolean type : new boolean[] {true, false}) {
doTestTransformReal(2, 1e-15, norm[i], type);
doTestTransformReal(4, 1e-14, norm[i], type);
doTestTransformReal(8, 1e-13, norm[i], type);
doTestTransformReal(16, 1e-13, norm[i], type);
doTestTransformReal(32, 1e-12, norm[i], type);
doTestTransformReal(64, 1e-12, norm[i], type);
doTestTransformReal(128, 1e-11, norm[i], type);
}
}
}
@Test
public void testStandardTransformFunction() {
final UnivariateFunction sinc = new Sinc();
final DoubleUnaryOperator f = x -> sinc.value(x);
final double min = -Math.PI;
final double max = Math.PI;
final FastFourierTransform.Norm[] norm = FastFourierTransform.Norm.values();
for (int i = 0; i < norm.length; i++) {
for (boolean type : new boolean[] {true, false}) {
doTestTransformFunction(f, min, max, 2, 1e-15, norm[i], type);
doTestTransformFunction(f, min, max, 4, 1e-14, norm[i], type);
doTestTransformFunction(f, min, max, 8, 1e-14, norm[i], type);
doTestTransformFunction(f, min, max, 16, 1e-13, norm[i], type);
doTestTransformFunction(f, min, max, 32, 1e-13, norm[i], type);
doTestTransformFunction(f, min, max, 64, 1e-12, norm[i], type);
doTestTransformFunction(f, min, max, 128, 1e-11, norm[i], type);
}
}
}
// Additional tests for 1D data.
/**
* Test of transformer for the ad hoc data taken from Mathematica.
*/
@Test
public void testAdHocData() {
FastFourierTransform transformer;
Complex[] result;
double tolerance = 1e-12;
final double[] x = {1.3, 2.4, 1.7, 4.1, 2.9, 1.7, 5.1, 2.7};
final Complex[] y = {
Complex.ofCartesian(21.9, 0.0),
Complex.ofCartesian(-2.09497474683058, 1.91507575950825),
Complex.ofCartesian(-2.6, 2.7),
Complex.ofCartesian(-1.10502525316942, -4.88492424049175),
Complex.ofCartesian(0.1, 0.0),
Complex.ofCartesian(-1.10502525316942, 4.88492424049175),
Complex.ofCartesian(-2.6, -2.7),
Complex.ofCartesian(-2.09497474683058, -1.91507575950825)};
transformer = new FastFourierTransform(FastFourierTransform.Norm.STD);
result = transformer.apply(x);
for (int i = 0; i < result.length; i++) {
Assert.assertEquals(y[i].getReal(), result[i].getReal(), tolerance);
Assert.assertEquals(y[i].getImaginary(), result[i].getImaginary(), tolerance);
}
transformer = new FastFourierTransform(FastFourierTransform.Norm.STD, true);
result = transformer.apply(y);
for (int i = 0; i < result.length; i++) {
Assert.assertEquals(x[i], result[i].getReal(), tolerance);
Assert.assertEquals(0.0, result[i].getImaginary(), tolerance);
}
double[] x2 = {10.4, 21.6, 40.8, 13.6, 23.2, 32.8, 13.6, 19.2};
TransformUtils.scaleInPlace(x2, 1.0 / Math.sqrt(x2.length));
Complex[] y2 = y;
transformer = new FastFourierTransform(FastFourierTransform.Norm.UNIT);
result = transformer.apply(y2);
for (int i = 0; i < result.length; i++) {
Assert.assertEquals(x2[i], result[i].getReal(), tolerance);
Assert.assertEquals(0.0, result[i].getImaginary(), tolerance);
}
transformer = new FastFourierTransform(FastFourierTransform.Norm.UNIT, true);
result = transformer.apply(x2);
for (int i = 0; i < result.length; i++) {
Assert.assertEquals(y2[i].getReal(), result[i].getReal(), tolerance);
Assert.assertEquals(y2[i].getImaginary(), result[i].getImaginary(), tolerance);
}
}
/**
* Test of transformer for the sine function.
*/
@Test
public void testSinFunction() {
FastFourierTransform transformer;
Complex[] result;
int size = 1 << 8;
double tolerance = 1e-12;
double min = 0.0;
double max = 2 * Math.PI;
transformer = new FastFourierTransform(FastFourierTransform.Norm.STD);
result = transformer.apply(SIN, min, max, size);
Assert.assertEquals(0.0, result[1].getReal(), tolerance);
Assert.assertEquals(-(size >> 1), result[1].getImaginary(), tolerance);
Assert.assertEquals(0.0, result[size - 1].getReal(), tolerance);
Assert.assertEquals(size >> 1, result[size - 1].getImaginary(), tolerance);
for (int i = 0; i < size - 1; i += i == 0 ? 2 : 1) {
Assert.assertEquals(0.0, result[i].getReal(), tolerance);
Assert.assertEquals(0.0, result[i].getImaginary(), tolerance);
}
min = -Math.PI;
max = Math.PI;
transformer = new FastFourierTransform(FastFourierTransform.Norm.STD, true);
result = transformer.apply(SIN, min, max, size);
Assert.assertEquals(0.0, result[1].getReal(), tolerance);
Assert.assertEquals(-0.5, result[1].getImaginary(), tolerance);
Assert.assertEquals(0.0, result[size - 1].getReal(), tolerance);
Assert.assertEquals(0.5, result[size - 1].getImaginary(), tolerance);
for (int i = 0; i < size - 1; i += i == 0 ? 2 : 1) {
Assert.assertEquals(0.0, result[i].getReal(), tolerance);
Assert.assertEquals(0.0, result[i].getImaginary(), tolerance);
}
}
}