diff --git a/engine/NBData/CMakeLists.txt b/engine/NBData/CMakeLists.txt new file mode 100644 index 0000000..8e5cb0c --- /dev/null +++ b/engine/NBData/CMakeLists.txt @@ -0,0 +1,70 @@ +cmake_minimum_required(VERSION 3.10) +project(NBData VERSION 0.1) + +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) + +toAbsolutePath(NB_DATA_SOURCE + ./src/FFT.cpp +) +toAbsolutePath(NB_DATA_INCLUDE + ./include/NBData/FFT.hpp +) + +set(NB_DATA_SOURCE ${NB_DATA_SOURCE} PARENT_SCOPE) +set(NB_DATA_INCLUDE ${NB_DATA_INCLUDE} PARENT_SCOPE) + +add_library(NBData ${NB_DATA_SOURCE}) +add_library(NBEngine::Data ALIAS NBData) +target_link_libraries(NBData + NBCore +) +target_include_directories(NBData + PUBLIC "$" + PUBLIC "$" +) +export( + TARGETS NBData + FILE "${CMAKE_BINARY_DIR}/cmake/NBDataTargets.cmake" + NAMESPACE NBEngine:: +) +configure_package_config_file( + "NBDataConfig.cmake.in" + "${CMAKE_BINARY_DIR}/cmake/NBDataConfig.cmake" + INSTALL_DESTINATION "${CMAKE_INSTALL_PREFIX}/cmake" + PATH_VARS CMAKE_INSTALL_LIBDIR +) +write_basic_package_version_file( + "${CMAKE_BINARY_DIR}/cmake/NBDataConfigVersion.cmake" + COMPATIBILITY AnyNewerVersion +) + +if (NBENGINE_INSTALL) + message("Installing NBData to ${CMAKE_INSTALL_PREFIX}") + install( + TARGETS NBData + EXPORT NBDataTargets + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib + INCLUDES DESTINATION include + ) + install( + DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include/NBData" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" + FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" + ) + install( + EXPORT NBDataTargets + DESTINATION "${CMAKE_INSTALL_PREFIX}/cmake" + NAMESPACE NBEngine:: + ) + install(FILES + "${CMAKE_BINARY_DIR}/cmake/NBDataConfig.cmake" + "${CMAKE_BINARY_DIR}/cmake/NBDataConfigVersion.cmake" + DESTINATION "${CMAKE_INSTALL_PREFIX}/CMake" + ) +endif() + +if (NB_BUILD_TESTS) + add_subdirectory(./tests ) +endif() \ No newline at end of file diff --git a/engine/NBData/NBDataConfig.cmake.in b/engine/NBData/NBDataConfig.cmake.in new file mode 100644 index 0000000..4316140 --- /dev/null +++ b/engine/NBData/NBDataConfig.cmake.in @@ -0,0 +1,2 @@ +@PACKAGE_INIT@ +include("${CMAKE_CURRENT_LIST_DIR}/NBDataTargets.cmake") \ No newline at end of file diff --git a/engine/NBData/include/NBData/FFT.hpp b/engine/NBData/include/NBData/FFT.hpp new file mode 100644 index 0000000..cb10383 --- /dev/null +++ b/engine/NBData/include/NBData/FFT.hpp @@ -0,0 +1,252 @@ +#pragma once +#ifndef _NB_DATA_FFT +#define _NB_DATA_FFT + +#include +#include +#include +#include + +#include + +#ifndef PI_VALUE + #define PI_VALUE 3.1415926535 +#endif + +/*! @file FFT.hpp */ + +namespace nb { + +using namespace std::complex_literals; + +constexpr double operator""_pi(long double val) { + return val*PI_VALUE; +} +constexpr double operator""_pi(unsigned long long val) { + return val*PI_VALUE; +} + +constexpr float operator""_pif(long double val) { + return val*PI_VALUE; +} +constexpr float operator""_pif(unsigned long long val) { + return val*PI_VALUE; +} + +template +bool isPowerOf2(const T& x) { + bool ret = false; + unsigned int mask=1; + for (int i=0; i<(8*sizeof(std::declval())); ++i) {ret ^= x&mask; mask<<=1;} + return ret; +} + +/*! + @brief Calculates the complex-valued twiddle factor given the `phase` + @param phase A double representing the phase, or `(k*n)/N`, of a DFT, + in units of 2*Pi. + @return A std::complex representing the complex twiddle factor of + the DFT at that `phase`, calculated via `exp(-2*Pi*i*phase)`. + + Calculates the complex-valued twiddle factor given the `phase` in units + of the ratio around the circle (i.e. in units of 2*Pi), calculated via + `exp(-2*Pi*i*phase)`. Note the minus sign, as this function is for the + *forward* DFT. +*/ +template +std::complex calculateTwiddleFactor(double phase) { + int sign = -1; + while(phase>0.5) { phase-=0.5; sign*=-1; } + return static_cast>(std::exp(2_pi*(sign)*(1i)*phase)); +} + +/*! + Stores the twiddle factors for Radix-2 DFT matrices for a given data type + (i.e. stores std::complex values), which can be retrieved using `TwiddleFactor::get`. +*/ +template +class TwiddleFactors2 { + protected: + static std::unordered_map> factors; + + public: + /*! + @brief Retrieves the twiddle factor for a Radix-2 DFT + @param numerator The numerator (i.e. `k*n` of a DFT matrix) part of the phase + when defined in units of 2*Pi. + @param log2N The `log2(denominator)` (i.e. `/N` of a DFT matrix) part of the phase + when defined in units of 2*Pi. + @return A std::complex for the value of the `numerator / (2^log2N)` twiddle factor + */ + static std::complex get(unsigned int numerator, unsigned int log2N); +}; + +template +std::complex* fft2_impl( + const std::complex* source, + std::complex* dest, + unsigned int log2N, + unsigned int stride=1 +) { + constexpr std::complex J(0, 1); + switch(log2N) { + case 0: + dest[0] = source[0]; + break; + + case 1: + dest[0] = source[0] + source[stride]; + dest[1] = source[0] - source[stride]; + break; + + case 2: + dest[0] = source[0] + source[stride] + source[2*stride] + source[3*stride]; + dest[1] = source[0] - J*source[stride] - source[2*stride] + J*source[3*stride]; + dest[2] = source[0] - source[stride] + source[2*stride] - source[3*stride]; + dest[3] = source[0] + J*source[stride] - source[2*stride] - J*source[3*stride]; + break; + + default: + const unsigned int halfway = 1<<(log2N-1); + fft2_impl(source, dest, log2N-1, 2*stride); + fft2_impl(&(source[stride]), &(dest[halfway]), log2N-1, 2*stride); + std::complex twiddle; + std::complex p, q; + for (unsigned int i=0; i < halfway; ++i) { + twiddle = TwiddleFactors2::get(i, log2N); + p = dest[i]; + q = dest[halfway+i]*twiddle; + dest[i] = p+q; + dest[halfway+i] = p-q; + } + break; + } + return dest; + +} + +template +std::complex* dft_impl( + const std::complex* source, + std::complex* dest, + unsigned int N +) { + for (int k=0; k < N; ++k) { + dest[k] = 0; + for (int n = 0; n < N; ++n) { + dest[k] += source[n] * calculateTwiddleFactor(double(n*k)/double(N)); + } + } + return dest; +} + +/*! + @brief Computes the minimal Radix-2 DFT on a std::vector> + @param values A std::vector> representing the time domain values + @return A std::vector>, representing the minimal Radix-2 DFT of `values` + + Computes the Radix-2 DFT on a std::vector>. If the input vector + `values` is not a power of 2, then the number of points in the DFT (and thus the + size of the returned frequency domain vector) is determined by + `2^ceil(log2(values.size()))` +*/ +template +std::vector> fft2(const std::vector>& values) { + const auto N = values.size(); + unsigned int log2N=0; + while(1<> temp(1<> ret = temp; + for (int i=0; i < N; ++i) { temp[i] = values[i]; } + std::complex* fft_results = fft2_impl(temp.data(), ret.data(), log2N); + return ret; +} + +/*! + @brief Computes the minimal Radix-2 DFT on a `std::vector` + @param values A std::vector representing the time domain values with packed real and + imaginary parts + @return A `std::vector`, representing the minimal Radix-2 DFT of `values` with packed + real and imaginary parts + @throw std::string Thrown when the size of the vector is not divisible by 2 (i.e. + there are unmatched real/imaginary pairs). + + Computes the Radix-2 DFT on a std::vector>. Equivalent to + `fft2(const std::vector>)` if `values[2*i]` and `values[2*i+1]` + were mapped to the real and imaginary parts of the complex vector. +*/ +template +std::vector fft2(const std::vector& values) { + if (values.size()&1) { + THROW(Error<>(Error<>::VALUE_ERROR, + "Vector must be of even length to represent complex values." + )); + } + std::complex complex_input(values.data(), values.size()/2); + std::complex complex_ret = fft2(complex_input); + return std::vector(complex_ret.data(), complex_ret.size()*2); +} + +/*! + @brief Computes the DFT on a std::vector> + @param values A std::vector> representing the time domain values + @return A std::vector> of the DFT of `values` in the frequency domain + + Computes the DFT of the std::vector> `values`. Does so using the + naive O(N^2) matrix-multiplication method. +*/ +template +std::vector> dft(const std::vector>& x) { + std::vector> ret = x; + std::complex* fft_results = dft_impl(x.data(), ret.data(), x.size()); + return ret; +} + +/*! + @brief Computes the DFT on a `std::vector` + @param values A `std::vector` representing the time domain values with packed real and + imaginary parts + @return A `std::vector` of the DFT of `values` in the frequency domain with + packed real and imaginary parts + @throw std::string Thrown when the size of the vector is not divisible by 2 (i.e. + there are unmatched real/imaginary pairs). + + Computes the DFT on a `std::vector`. Equivalent to + `dft(const std::vector>)` if `values[2*i]` and `values[2*i+1]` + were mapped to the real and imaginary parts of the complex vector. +*/ +template +std::vector dft(const std::vector& values) { + if (values.size()&1) { + THROW(Error<>(Error<>::VALUE_ERROR, + "Vector must be of even length to represent complex values." + )); + } + std::complex complex_input(values.data(), values.size()/2); + std::complex complex_ret = dft(complex_input); + return std::vector(complex_ret.data(), complex_ret.size()*2); +} + +template +std::unordered_map> TwiddleFactors2::factors = {}; + +template +std::complex TwiddleFactors2::get(unsigned int a, unsigned int log2N) { + while(a && !(a&1)) { a>>=1; log2N--; } + const unsigned int N = 1< N>>1 ) { + return (factors[key] = std::conj(TwiddleFactors2::get(N-a, log2N))); + } else { + return (factors[key] = calculateTwiddleFactor(phase)); + } + } +} + +} // namespace n + +#endif // _NB_DATA_FFT \ No newline at end of file diff --git a/engine/NBData/src/FFT.cpp b/engine/NBData/src/FFT.cpp new file mode 100644 index 0000000..7a5e829 --- /dev/null +++ b/engine/NBData/src/FFT.cpp @@ -0,0 +1,6 @@ +#include +#include + +namespace nb { + +} // namespace nb \ No newline at end of file diff --git a/engine/NBData/tests/CMakeLists.txt b/engine/NBData/tests/CMakeLists.txt new file mode 100644 index 0000000..f548e1d --- /dev/null +++ b/engine/NBData/tests/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.26.0) + +if (NB_BUILD_TESTS) + enable_testing() + include(GoogleTest) + + add_executable(TestData + ./testFFT.cpp + ) + target_link_libraries(TestData + NBData + GTest::gtest_main + ) + gtest_discover_tests(TestData) + + add_executable(FFTSpeedTest + ./fftspeedtest.cpp + ) + target_link_libraries(FFTSpeedTest + NBData + ) +endif() \ No newline at end of file diff --git a/engine/NBData/tests/fftspeedtest.cpp b/engine/NBData/tests/fftspeedtest.cpp new file mode 100644 index 0000000..970d396 --- /dev/null +++ b/engine/NBData/tests/fftspeedtest.cpp @@ -0,0 +1,5 @@ +#include + +int main() { + return 0; +} \ No newline at end of file diff --git a/engine/NBData/tests/testComplex.cpp b/engine/NBData/tests/testComplex.cpp new file mode 100644 index 0000000..60b37e8 --- /dev/null +++ b/engine/NBData/tests/testComplex.cpp @@ -0,0 +1,27 @@ +#include + +#include + +template +void COMPLEX_EQ(const nb::Complex& a, const nb::Complex& b) { + ASSERT_EQ(a.r, b.r); + ASSERT_EQ(a.i, b.i); +} + +template +void COMPLEX_ALMOST_EQ(const nb::Complex& a, const nb::Complex& b) { + ASSERT_FLOAT_EQ(a.r, b.r); + ASSERT_FLOAT_EQ(a.i, b.i); +} + +TEST(ComplexNumberTests, TestConstruction) { + COMPLEX_ALMOST_EQ(nb::Complex(), nb::Complex(0,0)); + COMPLEX_ALMOST_EQ(nb::Complex(5.0), nb::Complex(5, 0)); + COMPLEX_ALMOST_EQ(1.5_j, nb::Complex(0.0, 1.5)); +} + +TEST(ComplexNumberTests, TestAdd) { + auto x = nb::Complex(1.0) - nb::Complex(2.0, 1.0); + ASSERT_FLOAT_EQ(x.r, -1); + ASSERT_FLOAT_EQ(x.i, -1); +} \ No newline at end of file diff --git a/engine/NBData/tests/testFFT.cpp b/engine/NBData/tests/testFFT.cpp new file mode 100644 index 0000000..30429e5 --- /dev/null +++ b/engine/NBData/tests/testFFT.cpp @@ -0,0 +1,68 @@ +#include + +#include + +using namespace nb; + +template +void COMPLEX_EQ(const std::complex& a, const std::complex& b) { + ASSERT_EQ(a.real(), b.real()); + ASSERT_EQ(a.imag(), b.imag()); +} + +template +void COMPLEX_FLOAT_EQ( + const std::complex& a, + const std::complex& b, + const long double epsilon=0 +) { + if (epsilon) { + ASSERT_NEAR(a.real(), b.real(), epsilon); + ASSERT_NEAR(a.imag(), b.imag(), epsilon); + } else { + ASSERT_FLOAT_EQ(a.real(), b.real()); + ASSERT_FLOAT_EQ(a.imag(), b.imag()); + } +} + +template +void COMPLEX_EQ_VEC( + const std::vector>& a, + const std::vector>& b +) { + if (a.size() != b.size()) { FAIL(); } + for (int i = 0; i < a.size(); ++i) { + COMPLEX_EQ(a[i], b[i]); + } +} + +template +void COMPLEX_FLOAT_EQ_VEC( + const std::vector>& a, + const std::vector>& b, + const long double epsilon +) { + if (a.size() != b.size()) { FAIL(); } + for (int i = 0; i < a.size(); ++i) { + COMPLEX_FLOAT_EQ(a[i], b[i], epsilon); + } +} + +TEST(FFTTest, TestFFT) { + std::vector>> input = { + {1.0, 0.5, 0}, + {1,1,1,1,1,1,1,1}, + + }; + std::vector>> expected = { + {1.5,1.f-0.5if,0.5,1.f+0.5if}, + {8, 0, 0, 0, 0, 0, 0, 0} + }; + for (int i = 0; i < input.size(); ++i) { + COMPLEX_FLOAT_EQ_VEC( + nb::fft2(input[i]), + expected[i], + 1e-10 + ); + } +}