From a949de86a330980f443dea6b1e3cd8324dff2dd0 Mon Sep 17 00:00:00 2001 From: rstein Date: Thu, 14 Sep 2023 16:41:02 +0200 Subject: [PATCH] user-defined settings parameter limits and validation Signed-off-by: rstein Signed-off-by: Ralph J. Steinhagen --- include/annotated.hpp | 86 ++++++++++++++++++++++++++++++++++++++ include/node.hpp | 12 +++--- include/settings.hpp | 36 +++++++++++++--- test/grc/test.grc.expected | 24 +++++------ test/qa_settings.cpp | 35 ++++++++++++---- 5 files changed, 162 insertions(+), 31 deletions(-) diff --git a/include/annotated.hpp b/include/annotated.hpp index 98d5ae90..02c6ce55 100644 --- a/include/annotated.hpp +++ b/include/annotated.hpp @@ -3,6 +3,7 @@ #include #include +#include #include namespace fair::graph { @@ -92,6 +93,73 @@ using DefaultSupportedTypes = SupportedTypes<>; static_assert(fair::meta::is_instantiation_of); static_assert(fair::meta::is_instantiation_of, SupportedTypes>); +/** + * @brief Represents limits and optional validation for an Annotated<..> type. + * + * The `Limits` structure defines lower and upper bounds for a value of type `T`. + * Additionally, it allows for an optional custom validation function to be provided. + * This function should take a value of type `T` and return a `bool`, indicating + * whether the value passes the custom validation or not. + * + * Example: + * ``` + * Annotated> exampleVar1; + * // or: + * constexpr auto isPowerOfTwo = [](const int &val) { return val > 0 && (val & (val - 1)) == 0; }; + * Annotated> exampleVar2; + * // or: + * Annotated 0 && (val & (val - 1)) == 0; }>> exampleVar2; + * ``` + */ +template + requires(requires(decltype(Validator) f, decltype(LowerLimit) v) { + { f(v) } -> std::same_as; + } || Validator == nullptr) +struct Limits { + using ValueType = decltype(LowerLimit); + static constexpr ValueType MinRange = LowerLimit; + static constexpr ValueType MaxRange = UpperLimit; + static constexpr decltype(Validator) ValidatorFunc = Validator; + + static constexpr bool + validate(const ValueType &value) noexcept { + if constexpr (LowerLimit == UpperLimit) { // ignore range checks + if constexpr (Validator != nullptr) { + try { + return Validator(value); + } catch (...) { + return false; + } + } else { + return true; // if no validator and limits are same, return true by default + } + } + if constexpr (Validator != nullptr) { + try { + return value >= LowerLimit && value <= UpperLimit && Validator(value); + } catch (...) { + return false; + } + } else { + return value >= LowerLimit && value <= UpperLimit; + } + return true; + } +}; + +template +struct is_limits : std::false_type {}; + +template +struct is_limits> : std::true_type {}; + +template +concept Limit = is_limits::value; + +using EmptyLimit = Limits<0, 0>; // nomen-est-omen + +static_assert(Limit); + /** * @brief Annotated is a template class that acts as a transparent wrapper around another type. * It allows adding additional meta-information to a type, such as documentation, unit, and visibility. @@ -100,6 +168,7 @@ static_assert(fair::meta::is_instantiation_of, Sup template struct Annotated { using value_type = T; + using LimitType = typename fair::meta::typelist::template find_or_default; T value; Annotated() = default; @@ -153,6 +222,23 @@ struct Annotated { return *this; } + template + requires std::is_same_v, T> + constexpr bool + validate_and_set(U &&value_) { + if constexpr (std::is_same_v) { + value = std::forward(value_); + return true; + } else { + if (LimitType::validate(static_cast(value_))) { // N.B. implicit casting needed until clang supports floats as NTTPs + value = std::forward(value_); + return true; + } else { + return false; + } + } + } + operator std::string_view() const noexcept requires std::is_same_v { diff --git a/include/node.hpp b/include/node.hpp index 6467015b..1bbb1eca 100644 --- a/include/node.hpp +++ b/include/node.hpp @@ -316,11 +316,11 @@ struct node : protected std::tuple { "node_thread_pool", fair::thread_pool::TaskType::IO_BOUND, 2_UZ, std::numeric_limits::max()); constexpr static tag_propagation_policy_t tag_policy = tag_propagation_policy_t::TPP_ALL_TO_ALL; - A 1), no effect (fraction = 1)">> numerator = 1_UZ; - A 1), no effect (fraction = 1)">> denominator = 1_UZ; - A N), undefined-default (stride = 0)">> stride = 0_UZ; - std::size_t stride_counter = 0_UZ; - const std::size_t unique_id = _unique_id_counter++; + A1 interpolation, 1_ no effect">, Limits<1_UZ, std::size_t(-1)>> numerator = 1_UZ; + A1 interpolation, 1_ no effect">, Limits<1_UZ, std::size_t(-1)>> denominator = 1_UZ; + AN for skip, =0 for back-to-back.">> stride = 0_UZ; + std::size_t stride_counter = 0_UZ; + const std::size_t unique_id = _unique_id_counter++; const std::string unique_name = fmt::format("{}#{}", fair::meta::type_name(), unique_id); A ::unique_name">> name{ std::string(fair::meta::type_name()) }; A> meta_information; @@ -605,7 +605,7 @@ struct node : protected std::tuple { constexpr void forward_tags() noexcept { - if (!_output_tags_changed && !_input_tags_present) { + if (!(_output_tags_changed || _input_tags_present)) { return; } std::size_t port_id = 0; // TODO absorb this as optional tuple_for_each argument diff --git a/include/settings.hpp b/include/settings.hpp index 1d0a98b2..aac29d4d 100644 --- a/include/settings.hpp +++ b/include/settings.hpp @@ -468,14 +468,40 @@ class basic_settings : public settings_base { const auto &key = localKey; const auto &staged_value = localStaged_value; auto apply_member_changes = [&key, &staged, &forward_parameters, &staged_value, this](auto member) { - using Type = unwrap_if_wrapped_t>; + using RawType = std::remove_cvref_t; + using Type = unwrap_if_wrapped_t; if constexpr (is_writable(member) && is_supported_type()) { if (std::string(get_display_name(member)) == key && std::holds_alternative(staged_value)) { - member(*_node) = std::get(staged_value); - if constexpr (HasSettingsChangedCallback) { - staged.insert_or_assign(key, staged_value); + if constexpr (is_annotated()) { + if (member(*_node).validate_and_set(std::get(staged_value))) { + if constexpr (HasSettingsChangedCallback) { + staged.insert_or_assign(key, staged_value); + } else { + std::ignore = staged; // help clang to see why staged is not unused + } + } else { + // TODO: replace with pmt error message on msgOut port (to note: clang compiler bug/issue) +#if !defined(__EMSCRIPTEN__) && !defined(__clang__) + fmt::print(stderr, " cannot set field {}({})::{} = {} to {} due to limit constraints [{}, {}] validate func is {} defined\n", // + _node->unique_name, _node->name, member(*_node), std::get(staged_value), // + std::string(get_display_name(member)), RawType::LimitType::MinRange, + RawType::LimitType::MaxRange, // + RawType::LimitType::ValidatorFunc == nullptr ? "not" : ""); +#else + fmt::print(stderr, " cannot set field {}({})::{} = {} to {} due to limit constraints [{}, {}] validate func is {} defined\n", // + "_node->unique_name", "_node->name", member(*_node), std::get(staged_value), // + std::string(get_display_name(member)), RawType::LimitType::MinRange, + RawType::LimitType::MaxRange, // + RawType::LimitType::ValidatorFunc == nullptr ? "not" : ""); +#endif + } } else { - std::ignore = staged; // help clang to see why staged is not unused + member(*_node) = std::get(staged_value); + if constexpr (HasSettingsChangedCallback) { + staged.insert_or_assign(key, staged_value); + } else { + std::ignore = staged; // help clang to see why staged is not unused + } } } if (_auto_forward.contains(key)) { diff --git a/test/grc/test.grc.expected b/test/grc/test.grc.expected index 5b8703eb..daf61092 100644 --- a/test/grc/test.grc.expected +++ b/test/grc/test.grc.expected @@ -9,7 +9,7 @@ blocks: stride: 0 unique_name: good::fixed_source#0 denominator::description: denominator - denominator::documentation: "The bottom number of a fraction = numerator/denominator: decimation (fraction < 1), interpolation (fraction > 1), no effect (fraction = 1)" + denominator::documentation: "bottom number of input-to-output sample ratio: < 1 decimation, >1 interpolation, 1_ no effect" denominator::unit: "" denominator::visible: 0 description: "" @@ -22,11 +22,11 @@ blocks: name::unit: "" name::visible: 0 numerator::description: numerator - numerator::documentation: "The top number of a fraction = numerator/denominator: decimation (fraction < 1), interpolation (fraction > 1), no effect (fraction = 1)" + numerator::documentation: "top number of input-to-output sample ratio: < 1 decimation, >1 interpolation, 1_ no effect" numerator::unit: "" numerator::visible: 0 stride::description: stride - stride::documentation: "Number of samples between two data processing: overlap (stride < N), skip (stride > N), undefined-default (stride = 0)" + stride::documentation: samples between data processing. N for skip, =0 for back-to-back. stride::unit: "" stride::visible: 0 unknown_property: 42 @@ -39,7 +39,7 @@ blocks: stride: 0 unique_name: good::multiply#0 denominator::description: denominator - denominator::documentation: "The bottom number of a fraction = numerator/denominator: decimation (fraction < 1), interpolation (fraction > 1), no effect (fraction = 1)" + denominator::documentation: "bottom number of input-to-output sample ratio: < 1 decimation, >1 interpolation, 1_ no effect" denominator::unit: "" denominator::visible: 0 description: "" @@ -52,11 +52,11 @@ blocks: name::unit: "" name::visible: 0 numerator::description: numerator - numerator::documentation: "The top number of a fraction = numerator/denominator: decimation (fraction < 1), interpolation (fraction > 1), no effect (fraction = 1)" + numerator::documentation: "top number of input-to-output sample ratio: < 1 decimation, >1 interpolation, 1_ no effect" numerator::unit: "" numerator::visible: 0 stride::description: stride - stride::documentation: "Number of samples between two data processing: overlap (stride < N), skip (stride > N), undefined-default (stride = 0)" + stride::documentation: samples between data processing. N for skip, =0 for back-to-back. stride::unit: "" stride::visible: 0 - name: counter @@ -68,7 +68,7 @@ blocks: stride: 0 unique_name: builtin_counter#0 denominator::description: denominator - denominator::documentation: "The bottom number of a fraction = numerator/denominator: decimation (fraction < 1), interpolation (fraction > 1), no effect (fraction = 1)" + denominator::documentation: "bottom number of input-to-output sample ratio: < 1 decimation, >1 interpolation, 1_ no effect" denominator::unit: "" denominator::visible: 0 description: "" @@ -81,11 +81,11 @@ blocks: name::unit: "" name::visible: 0 numerator::description: numerator - numerator::documentation: "The top number of a fraction = numerator/denominator: decimation (fraction < 1), interpolation (fraction > 1), no effect (fraction = 1)" + numerator::documentation: "top number of input-to-output sample ratio: < 1 decimation, >1 interpolation, 1_ no effect" numerator::unit: "" numerator::visible: 0 stride::description: stride - stride::documentation: "Number of samples between two data processing: overlap (stride < N), skip (stride > N), undefined-default (stride = 0)" + stride::documentation: samples between data processing. N for skip, =0 for back-to-back. stride::unit: "" stride::visible: 0 - name: sink @@ -98,7 +98,7 @@ blocks: total_count: 100 unique_name: good::cout_sink#0 denominator::description: denominator - denominator::documentation: "The bottom number of a fraction = numerator/denominator: decimation (fraction < 1), interpolation (fraction > 1), no effect (fraction = 1)" + denominator::documentation: "bottom number of input-to-output sample ratio: < 1 decimation, >1 interpolation, 1_ no effect" denominator::unit: "" denominator::visible: 0 description: "" @@ -111,11 +111,11 @@ blocks: name::unit: "" name::visible: 0 numerator::description: numerator - numerator::documentation: "The top number of a fraction = numerator/denominator: decimation (fraction < 1), interpolation (fraction > 1), no effect (fraction = 1)" + numerator::documentation: "top number of input-to-output sample ratio: < 1 decimation, >1 interpolation, 1_ no effect" numerator::unit: "" numerator::visible: 0 stride::description: stride - stride::documentation: "Number of samples between two data processing: overlap (stride < N), skip (stride > N), undefined-default (stride = 0)" + stride::documentation: samples between data processing. N for skip, =0 for back-to-back. stride::unit: "" stride::visible: 0 unknown_property: 42 diff --git a/test/qa_settings.cpp b/test/qa_settings.cpp index 0699fdfa..314f9c59 100644 --- a/test/qa_settings.cpp +++ b/test/qa_settings.cpp @@ -124,14 +124,14 @@ struct TestBlock : public node, BlockingIO, TestBlockDoc, Sup IN in{}; OUT out{}; // parameters - A, Unit<"As">> scaling_factor = static_cast(1); // N.B. unit 'As' = 'Coulomb' - A context{}; - std::int32_t n_samples_max = -1; - float sample_rate = 1000.0f; - std::vector vector_setting{ T(3), T(2), T(1) }; - int update_count = 0; - bool debug = false; - bool resetCalled = false; + A, Unit<"As">> scaling_factor = static_cast(1); // N.B. unit 'As' = 'Coulomb' + A context{}; + std::int32_t n_samples_max = -1; + A::max()>> sample_rate = 1000.0f; + std::vector vector_setting{ T(3), T(2), T(1) }; + int update_count = 0; + bool debug = false; + bool resetCalled = false; void settings_changed(const property_map &old_settings, property_map &new_settings) noexcept { @@ -501,6 +501,25 @@ const boost::ut::suite AnnotationTests = [] { expect(eq(block.scaling_factor.value, 42.f)) << "the answer to everything failed -- by value"; expect(eq(block.scaling_factor, 42.f)) << "the answer to everything failed -- direct"; + // check validator + expect(block.sample_rate.validate_and_set(1.f)); + expect(not block.sample_rate.validate_and_set(-1.f)); + + constexpr auto isPowerOfTwo = [](const int &val) { return val > 0 && (val & (val - 1)) == 0; }; + Annotated> needPowerOfTwo = 2; + expect(isPowerOfTwo(4)); + expect(!isPowerOfTwo(5)); + expect(needPowerOfTwo.validate_and_set(4)); + expect(not needPowerOfTwo.validate_and_set(5)); + expect(eq(needPowerOfTwo.value, 4)); + + Annotated 0) && (val & (val - 1)) == 0; }>> needPowerOfTwoAlt = 2; + expect(needPowerOfTwoAlt.validate_and_set(4)); + expect(not needPowerOfTwoAlt.validate_and_set(5)); + + std::ignore = block.settings().set({ { "sample_rate", -1.0f } }); + std::ignore = block.settings().apply_staged_parameters(); // should print out a warning -> TODO: replace with pmt error message on msgOut port + // fmt::print("description:\n {}", fair::graph::node_description>()); }; };