diff --git a/blocks/math/CMakeLists.txt b/blocks/math/CMakeLists.txt index 643e822d..d8a9d909 100644 --- a/blocks/math/CMakeLists.txt +++ b/blocks/math/CMakeLists.txt @@ -1,5 +1,5 @@ add_library(gr-math INTERFACE) -target_link_libraries(gr-math INTERFACE gnuradio-core gnuradio-algorithm) +target_link_libraries(gr-math INTERFACE gnuradio-core gnuradio-algorithm exprtk) target_include_directories(gr-math INTERFACE $ $) if (ENABLE_TESTING) diff --git a/blocks/math/include/gnuradio-4.0/math/ExpressionBlocks.hpp b/blocks/math/include/gnuradio-4.0/math/ExpressionBlocks.hpp new file mode 100644 index 00000000..955a9741 --- /dev/null +++ b/blocks/math/include/gnuradio-4.0/math/ExpressionBlocks.hpp @@ -0,0 +1,349 @@ +#ifndef EXPRESSIONBLOCKS_HPP +#define EXPRESSIONBLOCKS_HPP + +#include +#include +#include + +#include + +namespace gr::blocks::math { + +namespace detail { + +inline std::string formatParserError(const auto& parser, std::string_view expression) { + std::stringstream ss; + for (std::size_t i = 0; i < parser.error_count(); ++i) { + const auto error = parser.get_error(i); + + ss << fmt::format("ExprTk Parser Error({:2}): Position: {:2}\nType: [{:14}] Msg: {}; expression:\n{}\n", // + static_cast(i), // + static_cast(error.token.position), // + exprtk::parser_error::to_str(error.mode), error.diagnostic, expression); + } + return ss.str(); +} + +struct VectorInfo { + std::size_t size; + ssize_t index; +}; + +inline VectorInfo computeVectorInfo(void* base_ptr, void* end_ptr, std::size_t elementSize, void* access_ptr) { + if (!base_ptr || !end_ptr || !access_ptr) { + throw std::invalid_argument("null pointer(s) provided."); + } + + auto base = static_cast(base_ptr); + auto end = static_cast(end_ptr); + auto access = static_cast(access_ptr); + + if (end < base) { + throw std::out_of_range(fmt::format("invalid vector boundaries [{}, {}]", base_ptr, end_ptr)); + } + + return {static_cast(end - base) / elementSize, (access - base) / static_cast(elementSize)}; +} + +struct vector_access_rtc : public exprtk::vector_access_runtime_check { + std::unordered_map vector_map; + + bool handle_runtime_violation(violation_context& context) override { + auto itr = vector_map.find(static_cast(context.base_ptr)); + const std::string& vector_name = (itr != vector_map.end()) ? itr->second : "Unknown"; + + const auto typeSize = static_cast(context.type_size); + auto [vecSize, vecIndex] = computeVectorInfo(context.base_ptr, context.end_ptr, typeSize, context.access_ptr); + throw gr::exception(fmt::format("vector access '{name}[{index}]' outside of [0, {size}[ (typesize: {typesize})", // + fmt::arg("name", vector_name), fmt::arg("size", vecSize), fmt::arg("index", vecIndex), fmt::arg("typesize", typeSize))); + return false; // should never reach here + } +}; + +} // namespace detail + +template +requires std::floating_point +struct ExpressionSISO : Block> { + using Description = Doc, Visible> param_a = T(1.0); + A, Visible> param_b = T(0.0); + A, Visible> param_c = T(0.0); + + GR_MAKE_REFLECTABLE(ExpressionSISO, in, out, expr_string, param_a, param_b, param_c); + + exprtk::symbol_table _symbol_table; + exprtk::expression _expression; + T _in; + T _out; + + void initExpression(std::source_location location = std::source_location::current()) { + reset(); + _symbol_table.clear(); + _symbol_table.add_variable("x", _in); + _symbol_table.add_variable("y", _out); + + _symbol_table.add_variable(std::string(param_a.description()), param_a.value); + _symbol_table.add_variable(std::string(param_b.description()), param_b.value); + _symbol_table.add_variable(std::string(param_c.description()), param_c.value); + + _symbol_table.add_constants(); + _expression.register_symbol_table(_symbol_table); + + if (exprtk::parser parser; !parser.compile(expr_string, _expression)) { + throw gr::exception(detail::formatParserError(parser, expr_string), location); + } + } + + void settingsChanged(const property_map& /*oldSettings*/, const property_map& newSettings) { + if (newSettings.contains("expr_string")) { + initExpression(); + } + } + + void start() { initExpression(); } + + void reset() { + _in = T(0); + _out = T(0); + } + + [[nodiscard]] constexpr T processOne(T input) { + _in = input; + _out = _expression.value(); // evaluate expression, _out == 'y' defined to allow for recursion + return _out; + } +}; + +template +requires std::floating_point +struct ExpressionDISO : Block> { + using Description = Doc, Visible> param_a = T(1.0); + A, Visible> param_b = T(0.0); + A, Visible> param_c = T(0.0); + + GR_MAKE_REFLECTABLE(ExpressionDISO, in0, in1, out, expr_string, param_a, param_b, param_c); + + exprtk::symbol_table _symbol_table; + exprtk::expression _expression; + T _in0; + T _in1; + T _out; + + void initExpression(std::source_location location = std::source_location::current()) { + reset(); + _symbol_table.clear(); + _symbol_table.add_variable("x", _in0); + _symbol_table.add_variable("y", _in1); + _symbol_table.add_variable("z", _out); + + _symbol_table.add_variable(std::string(param_a.description()), param_a.value); + _symbol_table.add_variable(std::string(param_b.description()), param_b.value); + _symbol_table.add_variable(std::string(param_c.description()), param_c.value); + + _symbol_table.add_constants(); + _expression.register_symbol_table(_symbol_table); + + if (exprtk::parser parser; !parser.compile(expr_string, _expression)) { + throw gr::exception(detail::formatParserError(parser, expr_string), location); + } + } + + void settingsChanged(const property_map& /*oldSettings*/, const property_map& newSettings) { + if (newSettings.contains("expr_string")) { + initExpression(); + } + } + + void start() { initExpression(); }; + + void reset() { + _in0 = T(0); + _in1 = T(0); + _out = T(0); + } + + [[nodiscard]] constexpr T processOne(T input0, T input1) { + _in0 = input0; + _in1 = input1; + _out = _expression.value(); // evaluate expression, _out == 'y' defined to allow for recursion + return _out; + } +}; + +template +requires std::floating_point +struct ExpressionBulk : public gr::Block> { + using Description = Doc; + template + using A = Annotated; + + PortIn in; + PortOut out; + + A> expr_string = "vecOut := a * vecIn;"; + A, Visible> param_a = T(1.0); + A, Visible> param_b = T(0.0); + A, Visible> param_c = T(0.0); + A, Visible> runtime_checks = true; + + GR_MAKE_REFLECTABLE(ExpressionBulk, in, out, expr_string, param_a, param_b, param_c, runtime_checks); + + exprtk::symbol_table _symbol_table; + exprtk::expression _expression; + detail::vector_access_rtc _vec_rtc; + + std::vector _vecInData; + std::vector _vecOutData; + // vector_views that reference _vecInData and _vecOutData + // will be registered once and then just rebased as needed. + // N.B. _maxBaseSize limits the maximum chunk size and needs + // to be defined in-advance due to ExprTk constraints + std::array _arrOutDummy{T(0)}; + static constexpr std::size_t _maxBaseSize = 1UZ << 16; + exprtk::vector_view _vecIn = exprtk::make_vector_view(_arrOutDummy.data(), _maxBaseSize); + exprtk::vector_view _vecOut = exprtk::make_vector_view(_arrOutDummy.data(), _maxBaseSize); + + void initExpression(std::source_location location = std::source_location::current()) { + _expression = exprtk::expression(); + _symbol_table.clear(); + + if (_vecInData.empty()) { + _vecInData.resize(2UZ); + } + if (_vecOutData.empty()) { + _vecOutData.resize(2UZ); + } + + // rebase vector views to current data + _vecIn.rebase(_vecInData.data()); + _vecIn.set_size(_vecInData.size()); + _vecOut.rebase(_vecOutData.data()); + _vecOut.set_size(_vecOutData.size()); + + _symbol_table.add_vector("vecIn", _vecIn); + _symbol_table.add_vector("vecOut", _vecOut); + + _symbol_table.add_variable(std::string(param_a.description()), param_a.value); + _symbol_table.add_variable(std::string(param_b.description()), param_b.value); + _symbol_table.add_variable(std::string(param_c.description()), param_c.value); + + _symbol_table.add_constants(); + _expression.register_symbol_table(_symbol_table); + + exprtk::parser parser; + if (runtime_checks) { + _vec_rtc.vector_map[_vecIn.data()] = "vecIn"; + _vec_rtc.vector_map[_vecOut.data()] = "vecOut"; + parser.register_vector_access_runtime_check(_vec_rtc); + } + + if (!parser.compile(expr_string, _expression)) { + throw gr::exception(detail::formatParserError(parser, expr_string), location); + } + } + + void settingsChanged(const gr::property_map& /*oldSettings*/, const gr::property_map& newSettings) { + if (newSettings.contains("expr_string")) { + initExpression(); + } + } + + void start() { initExpression(); } + + work::Status processBulk(InputSpanLike auto& inputSpan, OutputSpanLike auto& outputSpan) { + if (inputSpan.size() != _vecInData.size() || outputSpan.size() != _vecOutData.size()) { + _vecInData.resize(std::min(inputSpan.size(), _maxBaseSize)); + _vecOutData.resize(std::min(outputSpan.size(), _maxBaseSize)); + + // rebase vector views to new internal memory storage of backing buffer vectors + _vecIn.rebase(_vecInData.data()); + _vecIn.set_size(_vecInData.size()); + _vecOut.rebase(_vecOutData.data()); + _vecOut.set_size(_vecOutData.size()); + + if (runtime_checks) { + _vec_rtc.vector_map.clear(); + _vec_rtc.vector_map[_vecIn.data()] = "vecIn"; + _vec_rtc.vector_map[_vecOut.data()] = "vecOut"; + } + } + + using PtrDiff_t = std::iter_difference_t; + std::ranges::copy_n(inputSpan.begin(), static_cast(_vecInData.size()), _vecInData.begin()); + _expression.value(); // evaluate expression, exception handled by caller + std::ranges::copy_n(_vecOutData.begin(), static_cast(_vecOutData.size()), outputSpan.begin()); + + std::ignore = inputSpan.consume(_vecInData.size()); + outputSpan.publish(_vecOutData.size()); + return work::Status::OK; + } +}; + +} // namespace gr::blocks::math + +// const inline static auto registerConstMath = gr::registerBlock(gr::globalBlockRegistry()) // +// + gr::registerBlock(gr::globalBlockRegistry()) // +// + gr::registerBlock(gr::globalBlockRegistry()); + +#endif // EXPRESSIONBLOCKS_HPP diff --git a/blocks/math/test/CMakeLists.txt b/blocks/math/test/CMakeLists.txt index 29187d87..fdb63354 100644 --- a/blocks/math/test/CMakeLists.txt +++ b/blocks/math/test/CMakeLists.txt @@ -1 +1,2 @@ add_ut_test(qa_Math) +add_ut_test(qa_ExpressionBlocks) diff --git a/blocks/math/test/qa_ExpressionBlocks.cpp b/blocks/math/test/qa_ExpressionBlocks.cpp new file mode 100644 index 00000000..4a6c6611 --- /dev/null +++ b/blocks/math/test/qa_ExpressionBlocks.cpp @@ -0,0 +1,105 @@ +#include + +#include + +#include +#include +#include +#include + +const boost::ut::suite<"basic expression block tests"> basicMath = [] { + using namespace boost::ut; + using namespace gr; + using namespace gr::blocks::math; + using testing::ProcessFunction::USE_PROCESS_ONE; + + "ExpressionSISO"_test = [](const T&) { + Graph graph; + + auto& source = graph.emplaceBlock>({{"n_samples_max", 10U}, {"default_value", T(21)}}); + auto& exprBlock = graph.emplaceBlock>({{"expr_string", "a*x"}, {"param_a", T(2)}}); + auto& tagSink = graph.emplaceBlock>({{"log_tags", true}, {"log_samples", true}}); + expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(source).template to<"in">(exprBlock))); + expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(exprBlock).template to<"in">(tagSink))); + + auto sched = gr::scheduler::Simple<>(std::move(graph)); + expect(sched.runAndWait().has_value()); + + expect(approx(source.default_value, T(21), T(1e-3f))); + expect(approx(exprBlock.param_a.value, T(2), T(1e-3f))); + expect(eq(tagSink._samples.size(), source.n_samples_max)); + expect(approx(tagSink._samples[0], T(42), T(1e-3f))); + } | std::tuple{}; + + "ExpressionDISO"_test = [](const T&) { + Graph graph; + + auto& source1 = graph.emplaceBlock>({{"n_samples_max", 10U}, {"default_value", T(7)}}); + auto& source2 = graph.emplaceBlock>({{"n_samples_max", 10U}, {"default_value", T(5)}}); + auto& exprBlock = graph.emplaceBlock>({{"expr_string", "z := a * (x + y + 2)"}, {"param_a", T(3)}}); + auto& tagSink = graph.emplaceBlock>({{"log_tags", true}, {"log_samples", true}}); + expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(source1).template to<"in0">(exprBlock))); + expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(source2).template to<"in1">(exprBlock))); + expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(exprBlock).template to<"in">(tagSink))); + + auto sched = gr::scheduler::Simple<>(std::move(graph)); + expect(sched.runAndWait().has_value()); + + expect(approx(source1.default_value, T(7), T(1e-3f))); + expect(approx(source2.default_value, T(5), T(1e-3f))); + expect(approx(exprBlock.param_a.value, T(3), T(1e-3f))); + expect(eq(tagSink._samples.size(), source1.n_samples_max)); + expect(eq(tagSink._samples.size(), source2.n_samples_max)); + expect(approx(tagSink._samples[0], T(42), T(1e-3f))); + } | std::tuple{}; + + "ExpressionBulk"_test = [](const T&) { + Graph graph; + + auto& source = graph.emplaceBlock>({{"n_samples_max", 10U}}); + auto& exprBlock = graph.emplaceBlock>({{"expr_string", "vecOut := a * vecIn"}, {"param_a", T(2)}}); + auto& tagSink = graph.emplaceBlock>({{"log_samples", true}}); + + expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(source).template to<"in">(exprBlock))); + expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(exprBlock).template to<"in">(tagSink))); + + auto sched = gr::scheduler::Simple<>(std::move(graph)); + expect(sched.runAndWait().has_value()); + + expect(eq(tagSink._samples.size(), source.n_samples_max)); + expect(approx(exprBlock.param_a.value, T(2), T(1e-3f))); + for (std::size_t i = 0; i < tagSink._samples.size(); ++i) { + T expected = T(1 + i) * T(2); + expect(approx(tagSink._samples[i], expected, T(1e-6))) << fmt::format("should be: output[{}] ({}) == {} * input[{}] ({}) ", i, tagSink._samples[i], exprBlock.param_a, i, expected); + } + } | std::tuple{}; + + "ExpressionBulk - exceptions"_test = [](const bool enableERuntimeChecks) { + Graph graph; + + auto& source = graph.emplaceBlock>({{"n_samples_max", 10U}}); + const std::string exprStr = R""(for (var i := 0; i < 100; i += 1) { + vecOut[i] := vecIn[i+1]; +} +)""; + auto& exprBlock = graph.emplaceBlock>({{"expr_string", exprStr}, {"runtime_checks", enableERuntimeChecks}}); + auto& tagSink = graph.emplaceBlock>({{"log_samples", true}}); + + expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(source).template to<"in">(exprBlock))); + expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(exprBlock).template to<"in">(tagSink))); + + auto sched = gr::scheduler::Simple<>(std::move(graph)); + + try { + expect(sched.runAndWait().has_value()); + expect(false) << fmt::format("should have failed"); + } catch (const gr::exception& ex) { + expect(true); + fmt::println("failed correctly with:\n{}\n", ex); + } catch (...) { + expect(false) << fmt::format("caught unknown/unexpected exception"); + } + } | std::vector{true/*, false -- disabled on purpose as this would trigger correctly trigger the ASAN checks*/}; +}; + +int main() { /* not needed for UT */ } diff --git a/blocks/math/test/qa_Math.cpp b/blocks/math/test/qa_Math.cpp index 68ac0fcd..f0158fa1 100644 --- a/blocks/math/test/qa_Math.cpp +++ b/blocks/math/test/qa_Math.cpp @@ -144,4 +144,4 @@ std::complex, std::complex*/>(); } | kArithmeticTypes; }; -int main() { /* not needed for UT */ } \ No newline at end of file +int main() { /* not needed for UT */ } diff --git a/blocks/testing/include/gnuradio-4.0/testing/NullSources.hpp b/blocks/testing/include/gnuradio-4.0/testing/NullSources.hpp index 1feef960..8d5cfb4f 100644 --- a/blocks/testing/include/gnuradio-4.0/testing/NullSources.hpp +++ b/blocks/testing/include/gnuradio-4.0/testing/NullSources.hpp @@ -35,7 +35,7 @@ Commonly used for testing and simulations where consistent output and finite exe Annotatedn_samples_max -> signal DONE (0: infinite)">> n_samples_max = 0U; Annotated> count = 0U; - GR_MAKE_REFLECTABLE(ConstantSource, out, n_samples_max, count); + GR_MAKE_REFLECTABLE(ConstantSource, out, default_value, n_samples_max, count); void reset() { count = 0U; } @@ -91,7 +91,7 @@ Commonly used for testing and simulations where consistent output and finite exe Annotatedn_samples_max -> signal DONE (0: infinite)">> n_samples_max = 0U; Annotated> count = 0U; - GR_MAKE_REFLECTABLE(CountingSource, out, n_samples_max, count); + GR_MAKE_REFLECTABLE(CountingSource, out, default_value, n_samples_max, count); void reset() { count = 0U; }