diff --git a/include/flucoma/algorithms/util/EigenRandom.hpp b/include/flucoma/algorithms/util/EigenRandom.hpp new file mode 100644 index 00000000..15b81d3b --- /dev/null +++ b/include/flucoma/algorithms/util/EigenRandom.hpp @@ -0,0 +1,162 @@ +#include "../../data/FluidIndex.hpp" +#include +#include +#include +#include + +namespace fluid::algorithm { + +struct RandomSeed +{ + RandomSeed(index seed) : mSeed{seed} {} + RandomSeed() : RandomSeed(-1) {} + operator std::optional() + { + return mSeed >= 0 ? mSeed : std::optional{}; + } + + index mSeed; +}; + + +template +struct is_complex : std::false_type +{}; +template +struct is_complex> : std::true_type +{}; +template +inline constexpr bool is_complex_v = is_complex::value; + +template +struct value_type_of +{ + using type = T; +}; + +template +struct value_type_of> +{ + using type = T; +}; + +template +using value_type_of_t = typename value_type_of::type; + +template +struct Range +{ + T min; + T max; +}; + +// deduction guide +template +Range(T, T) -> Range; + +template +Range DefaultRange = + Range{std::numeric_limits::min(), std::numeric_limits::max()}; + +template <> +auto DefaultRange = Range{-1.0, 1.0}; + +template <> +auto DefaultRange = Range{-1.0f, 1.0f}; + + +// Wraps up C++ random stuff to suit our needs +// can cope with uniform distributions of integral or real or complex real types +// replciates Eigen::Random default ranges (-1:1 for reals, -max:max for +// integral types) but also allows settable ranges (unlike Eigen::Random) +template , std::uniform_int_distribution, + std::uniform_real_distribution>>> +struct RandomGenerator +{ + RandomGenerator(std::optional seed, Range> range) + : g{seed ? *seed : rd()}, d{range.min, range.max} + {} + RandomGenerator(std::optional seed) + : RandomGenerator(seed, DefaultRange>) + {} + RandomGenerator(const RandomGenerator& x) : g{x.g}, d{x.d} {} + + T operator()() const + { + // if complex, we need to generate two random values + if constexpr (!is_complex_v) + return d(g); + else + return T{d(g), d(g)}; + } + +private: + std::random_device rd; + mutable std::mt19937_64 g; + + mutable D d; +}; + + +template +auto EigenRandom(index rows, index cols, RandomSeed seed, + Range> range) +{ + return EigenType::NullaryExpr( + rows, cols, RandomGenerator{seed, range}); +} + +template +auto EigenRandom(index rows, index cols, RandomSeed seed) +{ + using T = value_type_of_t; + return EigenRandom(rows, cols, seed, DefaultRange); +} + +template +auto EigenRandom(index size, RandomSeed seed, + Range> range) +{ + static_assert(EigenType::RowsAtCompileTime == 1 || + EigenType::ColsAtCompileTime == 1, + "Eigen Type is not a vector, did you mean to call (rows,cols) " + "version?"); + if constexpr (EigenType::ColsAtCompileTime == 1) + { + return EigenType::NullaryExpr( + size, 1, RandomGenerator{seed, range}); + } + if constexpr (EigenType::RowsAtCompileTime == 1) + { + return EigenType::NullaryExpr( + 1, size, RandomGenerator{seed, range}); + } +} + +template +auto EigenRandom(index size, RandomSeed seed) +{ + using T = value_type_of_t; + return EigenRandom(size, seed, DefaultRange); +} + +template +auto EigenRandomPhase(index rows, index cols, RandomSeed seed) +{ + + static_assert(is_complex_v, + "This only works for complex valued Eigen types"); + + constexpr double twopi = 2 * M_PI; + using T = value_type_of_t; + auto f = [rg = RandomGenerator{seed, Range{0.0, twopi}}]() { + return std::polar(1.0, rg()); + }; + + return EigenType::NullaryExpr(rows, cols, f); +} + + +} // namespace fluid::algorithm \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8b47c820..74ec9efa 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -123,6 +123,9 @@ target_link_libraries(TestEnvelopeSeg PRIVATE TestSignals) target_link_libraries(TestEnvelopeGate PRIVATE TestSignals) target_link_libraries(TestTransientSlice PRIVATE TestSignals) +add_test_executable(TestEigenRandom algorithms/util/TestEigenRandom.cpp) + + include(CTest) include(Catch) @@ -149,5 +152,6 @@ catch_discover_tests(TestBufferedProcess WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" catch_discover_tests(TestMLP WORKING_DIRECTORY "${CMAKE_BINARY_DIR}") catch_discover_tests(TestKMeans WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) +catch_discover_tests(TestEigenRandom WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) add_compile_tests("FluidTensor Compilation Tests" data/compile_tests/TestFluidTensor_Compile.cpp) diff --git a/tests/algorithms/util/TestEigenRandom.cpp b/tests/algorithms/util/TestEigenRandom.cpp new file mode 100644 index 00000000..da3f006a --- /dev/null +++ b/tests/algorithms/util/TestEigenRandom.cpp @@ -0,0 +1,127 @@ +#define CATCH_CONFIG_MAIN +#include +#include +#include +#include +#include +#include +#include + +namespace fluid::algorithm { + +TEST_CASE("EigenRandom Can Make Basic Random Containers") +{ + + SECTION("Matrix of Double") + { + Eigen::MatrixXd mrnd = EigenRandom(3, 4, RandomSeed{}); + + // Right size + REQUIRE((mrnd.rows() == 3 && mrnd.cols() == 4)); + // No NaNs or infs + REQUIRE((mrnd.array().isFinite()).all()); + // Not constant value + auto first = mrnd(0, 0); + REQUIRE_FALSE((mrnd.array() == first).all()); + } + + SECTION("2D Array of Double") + { + Eigen::ArrayXXd mrnd = EigenRandom(3, 4, RandomSeed{}); + + // Right size + REQUIRE((mrnd.rows() == 3 && mrnd.cols() == 4)); + // No NaNs or infs + REQUIRE((mrnd.array().isFinite()).all()); + // Not constant value + auto first = mrnd(0, 0); + REQUIRE_FALSE((mrnd.array() == first).all()); + } + + SECTION("Vector of Double") + { + Eigen::VectorXd vrnd = EigenRandom(4, RandomSeed{}); + + // Right size + REQUIRE((vrnd.size() == 4)); + // No NaNs or infs + REQUIRE((vrnd.array().isFinite()).all()); + // Not constant value + auto first = vrnd(0); + REQUIRE_FALSE((vrnd.array() == first).all()); + } + + SECTION("Vector of int") + { + Eigen::VectorXi vrnd = EigenRandom(4, RandomSeed{}); + + // Right size + REQUIRE((vrnd.size() == 4)); + // Not constant value + auto first = vrnd(0); + REQUIRE_FALSE((vrnd.array() == first).all()); + } + + SECTION("1D Array of Double") + { + Eigen::ArrayXd mrnd = EigenRandom(4, RandomSeed{}); + + // Right size + REQUIRE(mrnd.size() == 4); + // No NaNs or infs + REQUIRE((mrnd.array().isFinite()).all()); + // Not constant value + auto first = mrnd(0); + REQUIRE_FALSE((mrnd.array() == first).all()); + } + + SECTION("2D Array of Complex Double") + { + Eigen::ArrayXXcd mrnd = EigenRandom(3, 4, RandomSeed{}); + + // Right size + REQUIRE((mrnd.rows() == 3 && mrnd.cols() == 4)); + // No NaNs or infs + REQUIRE((mrnd.array().isFinite()).all()); + // Not constant value + auto first = mrnd(0, 0); + REQUIRE_FALSE((mrnd.array() == first).all()); + } +} + +TEST_CASE("EigenRandom Can Manually Seet Random Seed For Repeatability") +{ + + Eigen::MatrixXd mrnd1 = EigenRandom(3, 4, RandomSeed{42}); + Eigen::MatrixXd mrnd2 = EigenRandom(3, 4, RandomSeed{42}); + + // Same seed -> same result + REQUIRE((mrnd1.array() == mrnd2.array()).all()); + + // Different seed -> different result + Eigen::MatrixXd mrnd3 = EigenRandom(3, 4, RandomSeed{4203}); + REQUIRE_FALSE((mrnd1.array() == mrnd3.array()).all()); +} + +TEST_CASE("Eigen Random Can Set Min and Max") +{ + double min = -0.4; + double max = 0.1; + Eigen::MatrixXd mrnd = + EigenRandom(3, 4, RandomSeed{42}, Range{-0.4, 0.1}); + + REQUIRE((mrnd.minCoeff() >= min && mrnd.maxCoeff() < max)); +} + +TEST_CASE("EigenRandom Can make complex array of random phase, unit magnitude") +{ + Eigen::ArrayXXcd a = EigenRandomPhase(4, 3, RandomSeed{}); + constexpr double twopi = 2 * M_PI; + + std::cout << a.arg(); + REQUIRE((a.arg().maxCoeff() - a.arg().minCoeff() <= twopi)); + REQUIRE_THAT(a.abs().reshaped(), + Catch::Matchers::AllMatch(Catch::Matchers::WithinRel(1.0))); +} + +} // namespace fluid::algorithm