From 5b9a463b38d102dbbb9727c9a0d800135eb1819a Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Tue, 21 Nov 2023 17:49:50 +0100 Subject: [PATCH] Library: RandomDraw - rework mapping rule to support origin The first step was to allow setting a minimum value, which in theory could also be negative (at no point is the code actually limited to unsigned values; this is rather the default in practice). But reconsidering this extensions, then you'd also want the "neutral value" to be handled properly. Within context, this means that the *probability* controls when values other than the neutral value are produced; especially with p = 1.0 the neutral value shall not be produced at all --- src/lib/random-draw.hpp | 141 +++++++++++++++++++++-------- tests/library/random-draw-test.cpp | 4 +- wiki/thinkPad.ichthyo.mm | 113 ++++++++++++++++++++--- 3 files changed, 209 insertions(+), 49 deletions(-) diff --git a/src/lib/random-draw.hpp b/src/lib/random-draw.hpp index 79c480587..108ab4b6e 100644 --- a/src/lib/random-draw.hpp +++ b/src/lib/random-draw.hpp @@ -22,25 +22,54 @@ /** @file random-draw.hpp ** Build a component to select limited values randomly. - ** Prototyping to find a suitable DSL to configure drawing of random numbers and mapping results. - ** The underlying implementation shall be extracted from (and later used by) TestChainLoad; the - ** random numbers will be derived from node hash values and must be mapped to yield parameters - ** limited to a very small value range. While numerically simple, this turns out to be rather - ** error-prone, hence the desire to put a DSL in front. The challenge however arises from - ** the additional requirement to support various usage patters, all with minimal specs. + ** Generally speaking, RandomDraw uses some suitable source of randomness to "draw" a result + ** value with a limited target domain. The intended usage scenario is to parametrise some + ** configuration or computation »randomly«, with well defined probabilities and value ranges. + ** A DSL is provide to simplify the common configuration and value mapping scenarios. + ** @paragraph The underlying implementation was extracted 11/2023 from (and later used by) + ** TestChainLoad; there, random numbers are derived from node hash values and must be mapped + ** to yield control parameters governing the topology of a DAG datastructure. Notably, a + ** draw is performed on each step to decide if the graph should fork. While numerically + ** simple, this turned out to be rather error-prone, and resulting code is dense and + ** difficult to understand, hence the desire to put a library component in front. ** - ** The following code lays out the ground structure, while treating Spec as a distinct - ** type, which is then mixed into Draw. This logical separation basically was led me to the - ** final solution: Draw both _is_ a function and _embodies_ the implementation of this function. - ** This somewhat surprising layout is what enables use as a DSL builder, because it allows both - ** to have the _builder use_ and the _converter use_ in the same class, even allowing to _define_ - ** a Draw by giving a function which _produces_ a (dynamically parametrised) Draw. + ** # Implementation structure + ** RandomDraw inherits from a _policy template_, which in turn is-a std::function. The signature + ** of this function defines the input to work on; its output is assumed to be some variation of + ** a [»limited value«](\ref Limited). Notably, results are assumed to conform to an ordered + ** interval of integral values. The [core functionality](\ref drawLimited) is to use the value + ** from the random source (a `size_t` hash), break it down by some _modulus_ to create an arbitrary + ** selection and then map this _drawn value_ into the target value range. This mapping however allows + ** to discard some of the _possible drawn values_ — which equates to define a probability of producing + ** a result different than "zero" (the neutral value of the result range). Moreover, the actual value + ** mapping can be limited and configured even more within the confines of the target type. ** - ** In this prototype, all of the functor adaptation is also part of the Draw template; for the - ** real implementation this will have to be supplied at usage site through a traits template, - ** otherwise it would not be possible to integrate seamlessly with custom data sources (as - ** happens in the intended use case, where actually a Node is the data source) + ** Additional flexibility can be gained by _binding a functor_ thereby defining further mapping and + ** transformations. A wide array of function signatures can be accepted, as long as it is possible + ** somehow to _adapt_ those functions to conform to the overall scheme as defined by the Policy base. + ** Such a mapping function can be given directly at construction, or it can be set up later through + ** the configuration DSL. As a special twist, it is even possible to bind a function exposing a + ** reference to some RandomDraw instance (which can be the host object itself). Since such a + ** function can likewise accept the input randomness source, this setup opens the ability + ** for dynamic parametrisation of the result probabilities. ** + ** ## Policy template + ** For practical use, the RandomDraw template must be instantiate with a custom provided + ** policy template. This configuration allows to attach to locally defined types and facilities. + ** The policy template is assumed to conform to the following requirements + ** - its base type is std::function, with a result value similar to \ref Limited + ** - more specifically, the result type must be number-like and expose extension points + ** to determine the `minVal()`, `maxVal()` and `zeroVal()` + ** - moreover, the policy must define a function `defaultSrc(args...)`; this function must + ** accept input arguments in accordance to the function signature of the Policy (i.e. it + ** must read "the randomness source") and produce a result that can be adapted and fed + ** into the regular processing chain (the same as for any mapping function) + ** - optionally, this policy may also define a template `Adaptor`, possibly with + ** specialisations for various function signatures. These adaptors are used to + ** conform any mapping function and thus allow to simplify or widen the + ** possibly configurations at usage site. + ** @todo 11/2023 This is a first draft and was extracted from an actual usage scenario. + ** It remains to be seen if the scheme as defined is of any further use henceforth. ** @see RandomDraw_test ** @see TestChainLoad_test ** @see SchedulerStress_test @@ -53,6 +82,7 @@ #include "lib/meta/function.hpp" #include "lib/meta/function-closure.hpp" +#include "lib/util-quant.hpp" #include "lib/util.hpp" #include @@ -74,11 +104,15 @@ namespace lib { * @tparam max maximum allowed param value (inclusive) * @tparam max minimum allowed param value (inclusive) - defaults to "zero". */ - template + template struct Limited { - static constexpr T minVal() { return min; } + static_assert (min < max); + static_assert (min <= zero and zero < max); + static constexpr T maxVal() { return max; } + static constexpr T minVal() { return min; } + static constexpr T zeroVal(){ return zero;} T val; @@ -98,6 +132,7 @@ namespace lib { }; + namespace random_draw { // Policy definitions /** @@ -115,7 +150,7 @@ namespace lib { - /** + /**********************************************************//** * A component and builder to draw limited parameter values * based on some source of randomness (or hash input). * Effectively this is a function which "draws" on invocation. @@ -128,7 +163,8 @@ namespace lib { using Fun = typename _Fun::Functor; using Tar = typename _Fun::Ret; - Tar maxResult_{Tar::maxVal()}; ///< maximum parameter val actually to produce < max + Tar maxResult_{Tar::maxVal()}; ///< maximum result val actually to produce < max + Tar minResult_{Tar::minVal()}; ///< minimum result val actually to produce > min double probability_{0}; ///< probability that value is in [1 .. m] @@ -137,15 +173,34 @@ namespace lib { limited (double val) { if (probability_ == 0.0 or val == 0.0) - return Tar{0}; + return Tar::zeroVal(); + // + REQUIRE (Tar::minVal() <= minResult_); + REQUIRE (Tar::maxVal() >= maxResult_); + REQUIRE (minResult_ < maxResult_); + REQUIRE (0.0 <= probability_); + REQUIRE (probability_ <= 1.0); + auto org = util::max (Tar::zeroVal(), minResult_); double q = (1.0 - probability_); - auto org = Tar::minVal(); val -= q; // [0 .. [q .. 1[ val /= probability_; // [0 .. 1[ - val *= maxResult_ - org; // [0 .. m[ - val += org+1; // [1 .. m] - val += CAP_EPSILON; // round down yet absorb dust - return Tar{val}; + if (org == minResult_) + { // simple standard case + val *= maxResult_ - org; // [0 .. m[ + val += org+1; // [1 .. m] + val += CAP_EPSILON; // round down yet absorb dust + return Tar{val}; + } + else// Origin is somewhere within value range + {// ==> wrap "negative" part above max + // to map 0.0 ⟼ org (≙neutral) + val *= maxResult_ - minResult_; + val += org+1; // max inclusive but <0 ⟼ org + if (val > maxResult_) // wrap the "negatives" + val -= maxResult_+1 - minResult_; + val += CAP_EPSILON; + return Tar{val}; + } } static size_t constexpr QUANTISER = 1 << 8; @@ -167,7 +222,9 @@ namespace lib { public: - /** Drawing is _disabled_ by default, always yielding "zero" */ + /** + * Drawing is _disabled_ by default, always yielding "zero" + */ RandomDraw() : Fun{adaptOut(POL::defaultSrc)} { } @@ -204,7 +261,7 @@ namespace lib { maxResult_ = m; return move (*this); } - + template RandomDraw&& mapping (FUN&& fun) @@ -217,7 +274,9 @@ namespace lib { private: - + /** @internal adapt input side of a given function to conform to the + * global input arguments as defined in the Policy base function. + * @return a function pre-fitted with a suitable Adapter from the Policy */ template decltype(auto) adaptIn (FUN&& fun) @@ -230,6 +289,15 @@ namespace lib { return Adaptor::build (forward (fun)); } + /** @internal adapt output side of a given function, allowing to handle it's results + * - a function producing the overall result-type is installed as-is + * - a `size_t` result is assumed be a hash and passed into #drawLimited + * - likewise a `double` is assumed to be already a random val to be #limited + * - special treatment is given to a function which produces reference to some + * `RandomDraw`; this allows to produce a dynamic parametrisation, which is + * then invoked on the same input arguments to produce the result value. + * @return adapted function which produces a result value of type #Tar + */ template decltype(auto) adaptOut (FUN&& fun) @@ -244,24 +312,25 @@ namespace lib { else if constexpr (std::is_same_v)// ◁───────────────────────┨ function yields random source to draw value return chained (std::forward(fun) - ,[this](size_t hash){ return drawLimited(hash); }); + ,[this](size_t hash){ return drawLimited(hash); } + ); else if constexpr (std::is_same_v)// ◁───────────────────────┨ function yields random value to be quantised return chained (std::forward(fun) - ,[this](double rand){ return limited(rand); }); + ,[this](double rand){ return limited(rand); } + ); else - if constexpr (std::is_same_v)// ◁────────────────────┨ function yields parametrised RandomDraw to invoke + if constexpr (std::is_same_v)// ◁─────────────┨ function yields parametrised RandomDraw to invoke return [functor=std::forward(fun), this] (auto&& ...inArgs) - { // invoke with copy - RandomDraw parametricDraw = functor(inArgs...); + { // invoke with copy + RandomDraw const& parametricDraw = functor(inArgs...); return parametricDraw (forward (inArgs)...); - }; + }; // forward arguments else static_assert (not sizeof(Res), "unable to adapt / handle result type"); NOTREACHED("Handle based on return type"); } - }; diff --git a/tests/library/random-draw-test.cpp b/tests/library/random-draw-test.cpp index 6d1abde88..86886e5e2 100644 --- a/tests/library/random-draw-test.cpp +++ b/tests/library/random-draw-test.cpp @@ -129,7 +129,7 @@ namespace test{ /** @test TODO demonstrate a basic usage scenario - * @todo WIP 11/23 🔁 define ⟶ implement + * @todo WIP 11/23 ✔ define ⟶ 🔁 implement */ void simpleUse() @@ -152,7 +152,7 @@ SHOW_EXPR (int(draw(256))); /** @test TODO verify configuration through policy template - * @todo WIP 11/23 🔁 define ⟶ implement + * @todo WIP 11/23 🔁 define ⟶ 🔁 implement */ void verify_policy() diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 67c1ca642..49cd052e2 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -96631,17 +96631,19 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + - + + + - - - + + + @@ -96717,18 +96719,107 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + + + + + - + + + + + + + + + + + + + + +

+ Beispiel: -2 .. 0 ..+2  ⟹ Wahrscheinlichkeit definiert für Werte ≠ 0 +

+ + +
+ +
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

+ grade wenn die Maximalgrenze nahe an einer Zweierpotenz liegt (Beispiel 10 Werte) dann werden einige Werte deutlich wahrscheinlicher +

+ + +
+
+
+
+
+ + + +
+ + + + + +
- - - - + + + + + + + + + + + +