Skip to content

Commit

Permalink
user-defined settings parameter limits and validation
Browse files Browse the repository at this point in the history
Signed-off-by: rstein <[email protected]>
Signed-off-by: Ralph J. Steinhagen <[email protected]>
  • Loading branch information
RalphSteinhagen committed Sep 15, 2023
1 parent 6791fcd commit a949de8
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 31 deletions.
86 changes: 86 additions & 0 deletions include/annotated.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <string_view>
#include <type_traits>
#include <utility>
#include <utils.hpp>

namespace fair::graph {
Expand Down Expand Up @@ -92,6 +93,73 @@ using DefaultSupportedTypes = SupportedTypes<>;
static_assert(fair::meta::is_instantiation_of<DefaultSupportedTypes, SupportedTypes>);
static_assert(fair::meta::is_instantiation_of<SupportedTypes<float, double>, 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<float, "example float", Visible, Limits<0.f, 1024.f>> exampleVar1;
* // or:
* constexpr auto isPowerOfTwo = [](const int &val) { return val > 0 && (val & (val - 1)) == 0; };
* Annotated<float, "example float", Visible, Limits<0.f, 1024.f, isPowerOfTwo>> exampleVar2;
* // or:
* Annotated<float, "example float", Visible, Limits<0.f, 1024.f, [](const int &val) { return val > 0 && (val & (val - 1)) == 0; }>> exampleVar2;
* ```
*/
template<auto LowerLimit, decltype(LowerLimit) UpperLimit, auto Validator = nullptr>
requires(requires(decltype(Validator) f, decltype(LowerLimit) v) {
{ f(v) } -> std::same_as<bool>;
} || 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<typename T>
struct is_limits : std::false_type {};

template<auto LowerLimit, decltype(LowerLimit) UpperLimit, auto Validator>
struct is_limits<Limits<LowerLimit, UpperLimit, Validator>> : std::true_type {};

template<typename T>
concept Limit = is_limits<T>::value;

using EmptyLimit = Limits<0, 0>; // nomen-est-omen

static_assert(Limit<EmptyLimit>);

/**
* @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.
Expand All @@ -100,6 +168,7 @@ static_assert(fair::meta::is_instantiation_of<SupportedTypes<float, double>, Sup
template<typename T, fair::meta::fixed_string description_ = "", typename... Arguments>
struct Annotated {
using value_type = T;
using LimitType = typename fair::meta::typelist<Arguments...>::template find_or_default<is_limits, EmptyLimit>;
T value;

Annotated() = default;
Expand Down Expand Up @@ -153,6 +222,23 @@ struct Annotated {
return *this;
}

template<typename U>
requires std::is_same_v<std::remove_cvref_t<U>, T>
constexpr bool
validate_and_set(U &&value_) {
if constexpr (std::is_same_v<LimitType, EmptyLimit>) {
value = std::forward<U>(value_);
return true;
} else {
if (LimitType::validate(static_cast<typename LimitType::ValueType>(value_))) { // N.B. implicit casting needed until clang supports floats as NTTPs
value = std::forward<U>(value_);
return true;
} else {
return false;
}
}
}

operator std::string_view() const noexcept
requires std::is_same_v<T, std::string>
{
Expand Down
12 changes: 6 additions & 6 deletions include/node.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -316,11 +316,11 @@ struct node : protected std::tuple<Arguments...> {
"node_thread_pool", fair::thread_pool::TaskType::IO_BOUND, 2_UZ, std::numeric_limits<uint32_t>::max());

constexpr static tag_propagation_policy_t tag_policy = tag_propagation_policy_t::TPP_ALL_TO_ALL;
A<std::size_t, "numerator", Doc<"The top number of a fraction = numerator/denominator: decimation (fraction < 1), interpolation (fraction > 1), no effect (fraction = 1)">> numerator = 1_UZ;
A<std::size_t, "denominator", Doc<"The bottom number of a fraction = numerator/denominator: decimation (fraction < 1), interpolation (fraction > 1), no effect (fraction = 1)">> denominator = 1_UZ;
A<std::size_t, "stride", Doc<"Number of samples between two data processing: overlap (stride < N), skip (stride > N), undefined-default (stride = 0)">> stride = 0_UZ;
std::size_t stride_counter = 0_UZ;
const std::size_t unique_id = _unique_id_counter++;
A<std::size_t, "numerator", Doc<"top number of input-to-output sample ratio: < 1 decimation, >1 interpolation, 1_ no effect">, Limits<1_UZ, std::size_t(-1)>> numerator = 1_UZ;
A<std::size_t, "denominator", Doc<"bottom number of input-to-output sample ratio: < 1 decimation, >1 interpolation, 1_ no effect">, Limits<1_UZ, std::size_t(-1)>> denominator = 1_UZ;
A<std::size_t, "stride", Doc<"samples between data processing. <N for overlap, >N 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<Derived>(), unique_id);
A<std::string, "user-defined name", Doc<"N.B. may not be unique -> ::unique_name">> name{ std::string(fair::meta::type_name<Derived>()) };
A<property_map, "meta-information", Doc<"store non-graph-processing information like UI block position etc.">> meta_information;
Expand Down Expand Up @@ -605,7 +605,7 @@ struct node : protected std::tuple<Arguments...> {

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
Expand Down
36 changes: 31 additions & 5 deletions include/settings.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::remove_cvref_t<decltype(member(*_node))>>;
using RawType = std::remove_cvref_t<decltype(member(*_node))>;
using Type = unwrap_if_wrapped_t<RawType>;
if constexpr (is_writable(member) && is_supported_type<Type>()) {
if (std::string(get_display_name(member)) == key && std::holds_alternative<Type>(staged_value)) {
member(*_node) = std::get<Type>(staged_value);
if constexpr (HasSettingsChangedCallback<Node>) {
staged.insert_or_assign(key, staged_value);
if constexpr (is_annotated<RawType>()) {
if (member(*_node).validate_and_set(std::get<Type>(staged_value))) {
if constexpr (HasSettingsChangedCallback<Node>) {
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<Type>(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<Type>(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<Type>(staged_value);
if constexpr (HasSettingsChangedCallback<Node>) {
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)) {
Expand Down
24 changes: 12 additions & 12 deletions test/grc/test.grc.expected
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ blocks:
stride: 0
unique_name: good::fixed_source<double>#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: ""
Expand All @@ -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 overlap, >N for skip, =0 for back-to-back.
stride::unit: ""
stride::visible: 0
unknown_property: 42
Expand All @@ -39,7 +39,7 @@ blocks:
stride: 0
unique_name: good::multiply<double>#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: ""
Expand All @@ -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 overlap, >N for skip, =0 for back-to-back.
stride::unit: ""
stride::visible: 0
- name: counter
Expand All @@ -68,7 +68,7 @@ blocks:
stride: 0
unique_name: builtin_counter<double>#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: ""
Expand All @@ -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 overlap, >N for skip, =0 for back-to-back.
stride::unit: ""
stride::visible: 0
- name: sink
Expand All @@ -98,7 +98,7 @@ blocks:
total_count: 100
unique_name: good::cout_sink<double>#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: ""
Expand All @@ -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 overlap, >N for skip, =0 for back-to-back.
stride::unit: ""
stride::visible: 0
unknown_property: 42
Expand Down
35 changes: 27 additions & 8 deletions test/qa_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,14 @@ struct TestBlock : public node<TestBlock<T>, BlockingIO<true>, TestBlockDoc, Sup
IN<T> in{};
OUT<T> out{};
// parameters
A<T, "scaling factor", Visible, Doc<"y = a * x">, Unit<"As">> scaling_factor = static_cast<T>(1); // N.B. unit 'As' = 'Coulomb'
A<std::string, "context information", Visible> context{};
std::int32_t n_samples_max = -1;
float sample_rate = 1000.0f;
std::vector<T> vector_setting{ T(3), T(2), T(1) };
int update_count = 0;
bool debug = false;
bool resetCalled = false;
A<T, "scaling factor", Visible, Doc<"y = a * x">, Unit<"As">> scaling_factor = static_cast<T>(1); // N.B. unit 'As' = 'Coulomb'
A<std::string, "context information", Visible> context{};
std::int32_t n_samples_max = -1;
A<float, "sample rate", Limits<int64_t(0), std::numeric_limits<int64_t>::max()>> sample_rate = 1000.0f;
std::vector<T> 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 {
Expand Down Expand Up @@ -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<int, "power of two", Limits<0, 0, isPowerOfTwo>> 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<int, "power of two", Limits<0, 0, [](const int &val) { return (val > 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<TestBlock<float>>());
};
};
Expand Down

0 comments on commit a949de8

Please sign in to comment.