diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4c8115..c6be587 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + submodules: true - name: Install dependencies run: ./helper.py -d - name: Build diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 217abd7..f2806a9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,6 +27,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + submodules: true - name: Initialize CodeQL uses: github/codeql-action/init@v3 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d495cec --- /dev/null +++ b/.gitmodules @@ -0,0 +1,8 @@ +[submodule "tests/deps/toml++"] + path = tests/deps/toml++ + url = git@github.com:marzer/tomlplusplus + shallow = true +[submodule "tests/test_data"] + path = tests/test_data + url = git@github.com:ALFI-library/test_data + shallow = true \ No newline at end of file diff --git a/ALFI/ALFI/dist.h b/ALFI/ALFI/dist.h index 43c9778..3139089 100644 --- a/ALFI/ALFI/dist.h +++ b/ALFI/ALFI/dist.h @@ -1,9 +1,10 @@ #pragma once -#include "config.h" - #include +#include "config.h" +#include "util/points.h" + namespace alfi::dist { enum class Type { GENERAL, @@ -20,30 +21,6 @@ namespace alfi::dist { ERF_STRETCHED, }; - template - void stretch(auto& points, Number a, Number b) { - if (points.empty()) { - return; - } - - if (points.size() == 1 || points.front() == points.back()) { - std::fill(points.begin(), points.end(), (a + b) / 2); - return; - } - - const Number& min = points.front(); - const Number& max = points.back(); - const Number mid = (a + b) / 2; - const Number scale = (b - a) / (max - min); - - for (SizeT i = 1; i < points.size() - 1; ++i) { - points[i] = mid + scale * (points[i] - mid); - } - - points.front() = a; - points.back() = b; - } - template class Container = DefaultContainer> Container uniform(SizeT n, Number a, Number b) { if (n == 1) @@ -69,9 +46,7 @@ namespace alfi::dist { template class Container = DefaultContainer> Container chebyshev_stretched(SizeT n, Number a, Number b) { - Container points = chebyshev(n, a, b); - stretch(points, a, b); - return points; + return points::stretched(chebyshev(n, a, b), a, b); } template class Container = DefaultContainer> @@ -90,9 +65,7 @@ namespace alfi::dist { template class Container = DefaultContainer> Container chebyshev_ellipse_stretched(SizeT n, Number a, Number b, Number ratio) { - Container points = chebyshev_ellipse(n, a, b, ratio); - stretch(points, a, b); - return points; + return points::stretched(chebyshev_ellipse(n, a, b, ratio), a, b); } template class Container = DefaultContainer> @@ -121,14 +94,37 @@ namespace alfi::dist { return points; } + /** + @brief Generates a distribution of \p n points on the interval `(a, b)` using the sigmoid function. + + The following transform function \f(f\f): + \f[ + f(x) = \frac{1}{1 + e^{-steepness \cdot x}} + \f] + is applied to `n` points uniformly distributed on the interval `(-1, 1)`, mapping them onto the `(0, 1)` interval.\n + The resulting points are then linearly mapped to the target interval `(a, b)`. + + The slope of the transform function \f(f\f) at `x = 0` is determined by \p steepness and is given by: + \f[ + \left. \dv{f}{x} \right\vert_{x=0} = \frac14 \cdot steepness + \f] + + @note Extreme points don't lie on the interval `(a, b)` boundaries. + + @param n number of points + @param a left boundary of the interval + @param b right boundary of the interval + @param steepness determines the slope of the transform function at `x = 0` + @return a container with \p n points distributed on the interval `(a, b)` according to the transform function + */ template class Container = DefaultContainer> Container sigmoid(SizeT n, Number a, Number b, Number steepness) { if (n == 1) return {(a+b)/2}; Container points(n); for (SizeT i = 0; i < n; ++i) { - const Number x = static_cast(i) / (static_cast(n) - 1); - const Number sigmoidValue = 1.0 / (1.0 + exp(-steepness * (x - 0.5))); + const Number x = 2 * static_cast(i) / (static_cast(n) - 1) - 1; + const Number sigmoidValue = 1 / (1 + exp(-steepness * x)); points[i] = a + (b - a) * sigmoidValue; } return points; @@ -141,11 +137,11 @@ namespace alfi::dist { if (n == 1) return {(a+b)/2}; Container points(n); - const Number stretch_factor = 1 - 2 / (1 + std::exp(0.5 * steepness)); + const Number stretch_factor = 1 - 2 / (1 + std::exp(steepness)); for (SizeT i = 1; i < n - 1; ++i) { - const Number x = static_cast(i) / (static_cast(n) - 1); - const Number sigmoid = 1.0 / (1.0 + std::exp(-steepness * (x - 0.5))); - const Number stretched = (sigmoid - 1.0 / (1.0 + std::exp(0.5 * steepness))) / stretch_factor; + const Number x = 2 * static_cast(i) / (static_cast(n) - 1) - 1; + const Number sigmoid = 1 / (1 + std::exp(-steepness * x)); + const Number stretched = (sigmoid - 1 / (1 + std::exp(steepness))) / stretch_factor; points[i] = a + (b - a) * stretched; } points[0] = a; @@ -153,6 +149,29 @@ namespace alfi::dist { return points; } + /** + @brief Generates a distribution of \p n points on the interval `(a, b)` using the error function. + + The following transform function \f(f\f): + \f[ + f(x) = \operatorname{erf}(steepness \cdot x) + \f] + is applied to `n` points uniformly distributed on the interval `(-1, 1)`, mapping them onto the same interval.\n + The resulting points are then linearly mapped to the target interval `(a, b)`. + + The slope of the transform function \f(f\f) at `x = 0` is determined by \p steepness and is given by: + \f[ + \left. \dv{f}{x} \right\vert_{x=0} = \frac2\pi \cdot steepness + \f] + + @note Extreme points don't lie on the interval `(a, b)` boundaries. + + @param n number of points + @param a left boundary of the interval + @param b right boundary of the interval + @param steepness determines the slope of the transform function at `x = 0` + @return a container with \p n points distributed on the interval `(a, b)` according to the transform function + */ template class Container = DefaultContainer> Container erf(SizeT n, Number a, Number b, Number steepness) { if (n == 0) @@ -161,8 +180,8 @@ namespace alfi::dist { return {(a+b)/2}; Container points(n); for (SizeT i = 0; i < n; ++i) { - const Number x = static_cast(i) / (static_cast(n) - 1); - const Number erf_value = std::erf(steepness * (x - 0.5)); + const Number x = 2 * static_cast(i) / (static_cast(n) - 1) - 1; + const Number erf_value = std::erf(steepness * x); points[i] = a + (b - a) * (1 + erf_value) / 2; } return points; @@ -170,13 +189,11 @@ namespace alfi::dist { template class Container = DefaultContainer> Container erf_stretched(SizeT n, Number a, Number b, Number steepness) { - Container points = erf(n, a, b, steepness); - stretch(points, a, b); - return points; + return points::stretched(erf(n, a, b, steepness), a, b); } template class Container = DefaultContainer> - Container of_type(Type type, SizeT n, Number a, Number b, Number parameter = 0) { + Container of_type(Type type, SizeT n, Number a, Number b, Number parameter = NAN) { switch (type) { case Type::CHEBYSHEV: return chebyshev(n, a, b); diff --git a/ALFI/ALFI/util/points.h b/ALFI/ALFI/util/points.h new file mode 100644 index 0000000..d00060c --- /dev/null +++ b/ALFI/ALFI/util/points.h @@ -0,0 +1,43 @@ +#pragma once + +#include "../config.h" + +namespace alfi::points { + template + void lin_map(auto& points, Number a, Number b, Number c, Number d) { + const auto mid1 = (a + b) / 2; + const auto mid2 = (c + d) / 2; + const auto scale = (d - c) / (b - a); + for (auto& point : points) { + point = mid2 + scale * (point - mid1); + } + } + + template + auto lin_mapped(const auto& points, Number a, Number b, Number c, Number d) { + auto mapped_points = points; + lin_map(mapped_points, a, b, c, d); + return mapped_points; + } + + template + void stretch(auto& points, Number a, Number b) { + if (points.empty()) { + return; + } + if (points.size() == 1 || points.front() == points.back()) { + std::fill(points.begin(), points.end(), (a + b) / 2); + return; + } + lin_map(points, points.front(), points.back(), a, b); + points.front() = a; + points.back() = b; + } + + template + auto stretched(const auto& points, Number a, Number b) { + auto stretched_points = points; + stretch(stretched_points, a, b); + return stretched_points; + } +} \ No newline at end of file diff --git a/helper.py b/helper.py index 5253614..34c06fe 100755 --- a/helper.py +++ b/helper.py @@ -15,6 +15,7 @@ dependencies = [ 'build-essential', 'cmake', + 'git', 'libgnuplot-iostream-dev', 'libgtest-dev', 'libqcustomplot-dev', @@ -48,6 +49,7 @@ def execute_command(command): if args.deps: execute_command(['sudo', 'apt', 'update']) execute_command(['sudo', 'apt', 'install', '-y'] + dependencies) + execute_command(['git', 'submodule', 'update', '--init']) if args.build: execute_command(['cmake', '-DCMAKE_BUILD_TYPE=' + args.profile, '-B', profile_dir]) execute_command(['cmake', '--build', profile_dir, '-j', str(os.cpu_count())]) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8af51dc..9a32b38 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,13 +1,17 @@ set(CMAKE_CXX_STANDARD 23) find_package(GTest REQUIRED) +add_subdirectory(deps/toml++) + +set(TEST_DATA_DIR "${CMAKE_CURRENT_LIST_DIR}/test_data") macro(add_test_executable test_name) add_executable(${test_name} ${ARGN} test_utils.h) target_include_directories(${test_name} PRIVATE ${GTEST_INCLUDE_DIRS}) - target_link_libraries(${test_name} PRIVATE ALFI GTest::GTest GTest::Main) + target_link_libraries(${test_name} PRIVATE ALFI GTest::GTest GTest::Main tomlplusplus::tomlplusplus) target_compile_options(${test_name} PRIVATE -fsanitize=address,leak,undefined) target_link_options(${test_name} PRIVATE -fsanitize=address,leak,undefined) + target_compile_definitions(${test_name} PRIVATE TEST_DATA_DIR="${TEST_DATA_DIR}") add_test(NAME ${test_name} COMMAND ${test_name} --gtest_color=yes) endmacro() diff --git a/tests/deps/toml++ b/tests/deps/toml++ new file mode 160000 index 0000000..c4369ae --- /dev/null +++ b/tests/deps/toml++ @@ -0,0 +1 @@ +Subproject commit c4369ae1d8955cae20c4ab40b9813ef4b60e48be diff --git a/tests/dist/test_dist.cpp b/tests/dist/test_dist.cpp index cfd67c3..f8da026 100644 --- a/tests/dist/test_dist.cpp +++ b/tests/dist/test_dist.cpp @@ -2,210 +2,78 @@ #include -TEST(DistributionsTest, Uniform) { +#include "../test_utils.h" + +const auto test_data_path = TEST_DATA_DIR "/dist/dist.toml"; + +const auto test_data = toml::parse_file(test_data_path); + +const auto& mapping_intervals = test_data["mapping_intervals"].ref(); + +void test_distribution(const char*const name, const alfi::dist::Type type, double epsilon) { + const auto& test_cases = test_data[name]["test_cases"].ref(); + + test_cases.for_each([&](const toml::table& test_case) { + const auto n = test_case["n"].ref(); + const auto a = test_case["a"].value().value(); + const auto b = test_case["b"].value().value(); - static const auto test_case_uniform = [](size_t n, double a, double b, const std::vector& expected) { - const auto dist = alfi::dist::uniform(n, a, b); - for (size_t index = 0; index < expected.size(); index++) { - EXPECT_DOUBLE_EQ(dist[index], expected[index]); - } - }; - - test_case_uniform(0, 0, 1, {}); - test_case_uniform(1, 0, 1, {0.5}); - test_case_uniform(2, 0, 1, {0, 1}); - test_case_uniform(3, 0, 1, {0, 0.5, 1}); - test_case_uniform(4, 0, 1, {0, 0.33333333333333333, 0.66666666666666667, 1}); - test_case_uniform(5, 0, 1, {0, 0.25, 0.5, 0.75, 1}); - test_case_uniform(11, 0, 1, {0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0}); - - test_case_uniform(0, -10, 10, {}); - test_case_uniform(1, -10, 10, {0}); - test_case_uniform(2, -10, 10, {-10, 10}); - test_case_uniform(3, -10, 10, {-10, 0, 10}); - test_case_uniform(4, -10, 10, {-10, -3.3333333333333333, 3.3333333333333333, 10}); - test_case_uniform(5, -10, 10, {-10, -5, 0, 5, 10}); - test_case_uniform(6, -10, 10, {-10, -6, -2, 2, 6, 10}); + const auto parameter = test_case["ratio"].value_or(test_case["steepness"].value_or(static_cast(NAN))); + + const auto expected = to_vector(test_case["expected"].ref()); + const auto generated = of_type(type, n, a, b, parameter); + expect_eq(generated, expected, epsilon); + + mapping_intervals.for_each([&](const toml::array& interval) { + const auto c = interval[0].value().value(); + const auto d = interval[1].value().value(); + const auto mapped_expected = alfi::points::lin_mapped(expected, a, b, c, d); + const auto mapped_generated = of_type(type, n, c, d, parameter); + expect_eq(mapped_generated, mapped_expected, epsilon); + }); + }); } -TEST(DistributionsTest, Chebyshev) { +TEST(DistributionsTest, Uniform) { + test_distribution("uniform", alfi::dist::Type::UNIFORM, 1e-15); +} - static const auto test_case_chebyshev = [](size_t n, double a, double b, const std::vector& expected) { - const auto dist = alfi::dist::chebyshev(n, a, b); - for (size_t index = 0; index < expected.size(); index++) { - EXPECT_DOUBLE_EQ(dist[index], expected[index]); - } - }; - - test_case_chebyshev(0, 0, 1, {}); - test_case_chebyshev(1, 0, 1, {0.5}); - test_case_chebyshev(2, 0, 1, {0.14644660940672627, 0.85355339059327373}); - - {{ - const auto chebyshev = alfi::dist::chebyshev(3, 0, 1); - const auto uniform = alfi::dist::uniform(3, 0, 1); - EXPECT_LT(uniform[0], chebyshev[0]); - EXPECT_EQ(chebyshev[1], 0.5); - EXPECT_GT(uniform[2], chebyshev[2]); - }} - {{ - const auto chebyshev = alfi::dist::chebyshev(4, 0, 1); - const auto uniform = alfi::dist::uniform(4, 0, 1); - EXPECT_LT(uniform[0], chebyshev[0]); - EXPECT_GT(uniform[1], chebyshev[1]); - EXPECT_LT(uniform[2], chebyshev[2]); - EXPECT_GT(uniform[3], chebyshev[3]); - }} +TEST(DistributionsTest, Chebyshev) { + test_distribution("chebyshev", alfi::dist::Type::CHEBYSHEV, 1e-15); } TEST(DistributionsTest, ChebyshevStretched) { - - static const auto test_case_chebyshev_stretched = [](size_t n, double a, double b, const std::vector& expected) { - const auto dist = alfi::dist::chebyshev_stretched(n, a, b); - for (size_t index = 0; index < expected.size(); index++) { - EXPECT_DOUBLE_EQ(dist[index], expected[index]); - } - }; - - test_case_chebyshev_stretched(0, 0, 1, {}); - test_case_chebyshev_stretched(1, 0, 1, {0.5}); - test_case_chebyshev_stretched(2, 0, 1, {0, 1}); - test_case_chebyshev_stretched(3, 0, 1, {0, 0.5, 1}); + test_distribution("chebyshev_stretched", alfi::dist::Type::CHEBYSHEV_STRETCHED, 1e-11); } TEST(DistributionsTest, ChebyshevEllipse) { - - static const auto test_case_chebyshev_ellipse = [](size_t n, double a, double b, double ratio, const std::vector& expected) { - const auto dist = alfi::dist::chebyshev_ellipse(n, a, b, ratio); - for (size_t index = 0; index < expected.size(); index++) { - EXPECT_DOUBLE_EQ(dist[index], expected[index]); - } - }; - - test_case_chebyshev_ellipse(0, 0, 1, 1, {}); - test_case_chebyshev_ellipse(1, 0, 1, 1, {0.5}); - - {{ - const size_t n = 5; - const double a = 0, b = 1, ratio = 0.5; - const auto chebyshev_ellipse = alfi::dist::chebyshev_ellipse(n, a, b, ratio); - const auto chebyshev = alfi::dist::chebyshev(n, a, b); - EXPECT_GT(chebyshev_ellipse[0], chebyshev[0]); - EXPECT_GT(chebyshev_ellipse[1], chebyshev[1]); - EXPECT_EQ(chebyshev_ellipse[2], chebyshev[2]); - EXPECT_LT(chebyshev_ellipse[3], chebyshev[3]); - EXPECT_LT(chebyshev_ellipse[4], chebyshev[4]); - }} - {{ - const size_t n = 5; - const double a = 0, b = 1, ratio = 2; - const auto chebyshev_ellipse = alfi::dist::chebyshev_ellipse(n, a, b, ratio); - const auto chebyshev = alfi::dist::chebyshev(n, a, b); - EXPECT_LT(chebyshev_ellipse[0], chebyshev[0]); - EXPECT_LT(chebyshev_ellipse[1], chebyshev[1]); - EXPECT_EQ(chebyshev_ellipse[2], chebyshev[2]); - EXPECT_GT(chebyshev_ellipse[3], chebyshev[3]); - EXPECT_GT(chebyshev_ellipse[4], chebyshev[4]); - }} + test_distribution("chebyshev_ellipse", alfi::dist::Type::CHEBYSHEV_ELLIPSE, 1e-14); } TEST(DistributionsTest, ChebyshevEllipseStretched) { - - static const auto test_case_chebyshev_ellipse_stretched = [](size_t n, double a, double b, double ratio, const std::vector& expected) { - const auto dist = alfi::dist::chebyshev_ellipse_stretched(n, a, b, ratio); - for (size_t index = 0; index < expected.size(); index++) { - EXPECT_DOUBLE_EQ(dist[index], expected[index]); - } - }; - - test_case_chebyshev_ellipse_stretched(0, 0, 1, 1, {}); - test_case_chebyshev_ellipse_stretched(1, 0, 1, 1, {0.5}); - test_case_chebyshev_ellipse_stretched(2, 0, 1, 1, {0, 1}); - test_case_chebyshev_ellipse_stretched(3, 0, 1, 1, {0, 0.5, 1}); + test_distribution("chebyshev_ellipse_stretched", alfi::dist::Type::CHEBYSHEV_ELLIPSE_STRETCHED, 1e-11); } TEST(DistributionsTest, CircleProjection) { - - static const auto test_case_circle_projection = [](size_t n, double a, double b, const std::vector& expected) { - const auto dist = alfi::dist::circle_proj(n, a, b); - for (size_t index = 0; index < expected.size(); index++) { - EXPECT_DOUBLE_EQ(dist[index], expected[index]); - } - }; - - test_case_circle_projection(0, 0, 1, {}); - test_case_circle_projection(1, 0, 1, {0.5}); - test_case_circle_projection(2, 0, 1, {0, 1}); - test_case_circle_projection(3, 0, 1, {0, 0.5, 1}); + test_distribution("circle_proj", alfi::dist::Type::CIRCLE_PROJECTION, 1e-11); } TEST(DistributionsTest, EllipseProjection) { - - static const auto test_case_ellipse_projection = [](size_t n, double a, double b, double B, const std::vector& expected) { - const auto dist = alfi::dist::ellipse_proj(n, a, b, B); - for (size_t index = 0; index < expected.size(); index++) { - EXPECT_DOUBLE_EQ(dist[index], expected[index]); - } - }; - - test_case_ellipse_projection(0, 0, 1, 1, {}); - test_case_ellipse_projection(1, 0, 1, 1, {0.5}); - test_case_ellipse_projection(2, 0, 1, 1, {0, 1}); - test_case_ellipse_projection(3, 0, 1, 1, {0, 0.5, 1}); + test_distribution("ellipse_proj", alfi::dist::Type::ELLIPSE_PROJECTION, 1e-14); } TEST(DistributionsTest, Sigmoid) { - - static const auto test_case_sigmoid = [](size_t n, double a, double b, double steepness, const std::vector& expected) { - const auto dist = alfi::dist::sigmoid(n, a, b, steepness); - for (size_t index = 0; index < expected.size(); index++) { - EXPECT_DOUBLE_EQ(dist[index], expected[index]); - } - }; - - test_case_sigmoid(0, 0, 1, 1, {}); - test_case_sigmoid(1, 0, 1, 1, {0.5}); + test_distribution("sigmoid", alfi::dist::Type::SIGMOID, 1e-13); } TEST(DistributionsTest, SigmoidStretched) { - - static const auto test_case_sigmoid_stretched = [](size_t n, double a, double b, double steepness, const std::vector& expected) { - const auto dist = alfi::dist::sigmoid_stretched(n, a, b, steepness); - for (size_t index = 0; index < expected.size(); index++) { - EXPECT_DOUBLE_EQ(dist[index], expected[index]); - } - }; - - test_case_sigmoid_stretched(0, 0, 1, 1, {}); - test_case_sigmoid_stretched(1, 0, 1, 1, {0.5}); - test_case_sigmoid_stretched(2, 0, 1, 1, {0, 1}); - test_case_sigmoid_stretched(3, 0, 1, 1, {0, 0.5, 1}); + test_distribution("sigmoid_stretched", alfi::dist::Type::SIGMOID_STRETCHED, 1e-11); } TEST(DistributionsTest, Erf) { - - static const auto test_case_erf = [](size_t n, double a, double b, double steepness, const std::vector& expected) { - const auto dist = alfi::dist::erf(n, a, b, steepness); - for (size_t index = 0; index < expected.size(); index++) { - EXPECT_DOUBLE_EQ(dist[index], expected[index]); - } - }; - - test_case_erf(0, 0, 1, 1, {}); - test_case_erf(1, 0, 1, 1, {0.5}); + test_distribution("erf", alfi::dist::Type::ERF, 1e-14); } TEST(DistributionsTest, ErfStretched) { - - static const auto test_case_erf_stretched = [](size_t n, double a, double b, double steepness, const std::vector& expected) { - const auto dist = alfi::dist::erf_stretched(n, a, b, steepness); - for (size_t index = 0; index < expected.size(); index++) { - EXPECT_DOUBLE_EQ(dist[index], expected[index]); - } - }; - - test_case_erf_stretched(0, 0, 1, 1, {}); - test_case_erf_stretched(1, 0, 1, 1, {0.5}); - test_case_erf_stretched(2, 0, 1, 1, {0, 1}); - test_case_erf_stretched(3, 0, 1, 1, {0, 0.5, 1}); + test_distribution("erf_stretched", alfi::dist::Type::ERF_STRETCHED, 1e-11); } \ No newline at end of file diff --git a/tests/test_data b/tests/test_data new file mode 160000 index 0000000..e9dc09d --- /dev/null +++ b/tests/test_data @@ -0,0 +1 @@ +Subproject commit e9dc09d927df8057c82bb72d3ca8dd50556cb3fc diff --git a/tests/test_utils.h b/tests/test_utils.h index 2776754..082a022 100644 --- a/tests/test_utils.h +++ b/tests/test_utils.h @@ -2,9 +2,24 @@ #include +#include + +#include + inline void expect_eq(const std::vector& arr1, const std::vector& arr2, double epsilon = 1e-12) { ASSERT_EQ(arr1.size(), arr2.size()); for (std::size_t i = 0; i < arr1.size(); ++i) { - EXPECT_NEAR(arr1[i], arr2[i], epsilon) << "i = " << i; + EXPECT_TRUE(alfi::util::numeric::are_equal(arr1[i], arr2[i], epsilon)) + << "i = " << i << "\narr1[i] = " << arr1[i] << "\narr2[i] = " << arr2[i] << "\nepsilon = " << epsilon; + } +} + +template +std::vector to_vector(const toml::array& array) { + std::vector result; + result.reserve(array.size()); + for (const auto& value : array) { + result.push_back(value.value().value()); } + return result; } \ No newline at end of file