/* * FFT and convolution test (C++) * * Copyright (c) 2026 Project Nayuki. (MIT License) * https://www.nayuki.io/page/free-small-fft-in-multiple-languages * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * - The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * - The Software is provided "as is", without warranty of any kind, express or * implied, including but not limited to the warranties of merchantability, * fitness for a particular purpose and noninfringement. In no event shall the * authors or copyright holders be liable for any claim, damages or other * liability, whether in an action of contract, tort or otherwise, arising from, * out of or in connection with the Software or the use or other dealings in the * Software. */ #include #include #include #include #include #include #include #include #include #include #include "FftComplex.hpp" using std::complex; using std::cout; using std::endl; using std::size_t; using std::vector; // Private function prototypes static void testFft(size_t n); static void testConvolution(size_t n); static vector > naiveDft(const vector > &input, bool inverse); static vector > naiveConvolve(const vector > &xvec, const vector > &yvec); static double log10RmsErr(const vector > &xvec, const vector > &yvec); static vector > randomComplexes(size_t n); // Mutable global variable static double maxLogError = -INFINITY; // Random number generation std::default_random_engine randGen((std::random_device())()); /*---- Main and test functions ----*/ int main() { // Test power-of-2 size FFTs for (int i = 0; i <= 12; i++) testFft(static_cast(1) << i); // Test small size FFTs for (size_t i = 0; i < 30; i++) testFft(i); // Test diverse size FFTs for (int i = 0, prev = 0; i <= 100; i++) { int n = static_cast(std::lround(std::pow(1500.0, i / 100.0))); if (n > prev) { testFft(static_cast(n)); prev = n; } } // Test power-of-2 size convolutions for (int i = 0; i <= 12; i++) testConvolution(static_cast(1) << i); // Test diverse size convolutions for (int i = 0, prev = 0; i <= 100; i++) { int n = static_cast(std::lround(std::pow(1500.0, i / 100.0))); if (n > prev) { testConvolution(static_cast(n)); prev = n; } } cout << endl; cout << "Max log err = " << std::setprecision(3) << maxLogError << endl; cout << "Test " << (maxLogError < -10 ? "passed" : "failed") << endl; return EXIT_SUCCESS; } static void testFft(size_t n) { const vector > input = randomComplexes(n); const vector > expect = naiveDft(input, false); vector > actual = input; Fft::transform(actual, false); double err = log10RmsErr(expect, actual); for (auto it = actual.begin(); it != actual.end(); ++it) *it /= static_cast(n); Fft::transform(actual, true); err = std::max(log10RmsErr(input, actual), err); cout << "fftsize=" << std::setw(4) << std::setfill(' ') << n << " " << "logerr=" << std::setw(5) << std::setprecision(3) << std::setiosflags(std::ios::showpoint) << err << endl; } static void testConvolution(size_t n) { const vector > input0 = randomComplexes(n); const vector > input1 = randomComplexes(n); const vector > expect = naiveConvolve(input0, input1); const vector > actual = Fft::convolve(std::move(input0), std::move(input1)); cout << "convsize=" << std::setw(4) << std::setfill(' ') << n << " " << "logerr=" << std::setw(5) << std::setprecision(3) << std::setiosflags(std::ios::showpoint) << log10RmsErr(expect, actual) << endl; } /*---- Naive reference computation functions ----*/ static vector > naiveDft(const vector > &input, bool inverse) { size_t n = input.size(); vector > output; double coef = (inverse ? 2 : -2) * std::numbers::pi / static_cast(n); for (size_t k = 0; k < n; k++) { // For each output element complex sum(0); for (size_t t = 0; t < n; t++) { // For each input element double angle = coef * static_cast(static_cast(t) * k % n); sum += input[t] * std::polar(1.0, angle); } output.push_back(sum); } return output; } static vector > naiveConvolve( const vector > &xvec, const vector > &yvec) { size_t n = xvec.size(); vector > result(n); // All zeros for (size_t i = 0; i < n; i++) { for (size_t j = 0; j < n; j++) { size_t k = (i + j) % n; result[k] += xvec[i] * yvec[j]; } } return result; } /*---- Utility functions ----*/ static double log10RmsErr(const vector > &xvec, const vector > &yvec) { size_t n = xvec.size(); double err = std::pow(10, -99 * 2); for (size_t i = 0; i < n; i++) err += std::norm(xvec.at(i) - yvec.at(i)); err /= n > 0 ? static_cast(n) : 1.0; err = std::sqrt(err); // Now this is a root mean square (RMS) error err = std::log10(err); maxLogError = std::max(err, maxLogError); return err; } static vector > randomComplexes(size_t n) { std::uniform_real_distribution valueDist(-1.0, 1.0); vector > result; for (size_t i = 0; i < n; i++) result.push_back(complex(valueDist(randGen), valueDist(randGen))); return result; }