Yet another chainsaw massacre. One of the most obnoxious annoyances with C++ metaprogramming is the need to insert `typename` and `template` qualifiers into most definitions, to help the compiler to cope with the syntax, which is not context-free. The recent standards adds several clarifications, so that most of these qualifiers are redundant now, at least at places where it is unambiguously clear that only a type can be given. GCC already supports most of these relaxing rules (Clang unfortunately lags way behind with support of newer language features...)
427 lines
17 KiB
C++
427 lines
17 KiB
C++
/*
|
||
RANDOM-DRAW.hpp - randomly pick limited values
|
||
|
||
Copyright (C)
|
||
2023, Hermann Vosseler <Ichthyostega@web.de>
|
||
|
||
**Lumiera** is free software; you can redistribute it and/or modify it
|
||
under the terms of the GNU General Public License as published by the
|
||
Free Software Foundation; either version 2 of the License, or (at your
|
||
option) any later version. See the file COPYING for further details.
|
||
|
||
*/
|
||
|
||
/** @file random-draw.hpp
|
||
** Build a component to select limited values randomly.
|
||
** 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 provided 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 wrap it into a library component.
|
||
**
|
||
** # 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, followed by mapping this _drawn value_ into the target value range. This mapping allows
|
||
** to discard some of the _possible drawn values_ however — 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 within the confines of the target type.
|
||
**
|
||
** 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 change parameters dynamically,
|
||
** based on the current input value. This requires the mapping function to construct a pristine
|
||
** instance of RandomDraw, apply configuration based on the input and then return this instance
|
||
** by value — without ever »engaging« and invoking; this dynamically configured instance will
|
||
** then be invoked once, passing the current input values to yield the result value.
|
||
**
|
||
** ## Policy template
|
||
** For practical use, the RandomDraw template must be instantiated 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<Sig>`, possibly with
|
||
** specialisations for various function signatures. These adaptors are used to
|
||
** conform any mapping function and thus allow to simplify or widen the
|
||
** possible configurations at usage site.
|
||
**
|
||
** ## Copy inhibition
|
||
** The configuration of the RandomDraw processing pipeline makes heavy use of function composition
|
||
** and adaptation to handle a wide selection of input types and usage patterns. Unfortunately this
|
||
** requires to link the generated configuration-λ to the object instance (capturing by reference);
|
||
** not allowing this would severely limit the possible configurations. This implies that an object
|
||
** instance must not be moved anymore, once the processing pipeline has been configured. And this
|
||
** in turn would severely limit it's usage in a DSL. As a compromise, RandomDraw relies on
|
||
** [lazy on-demand initialisation](\ref lazy-init.hpp): as long as the processing function has
|
||
** not been invoked, the internal pipeline is unconfigured, and the object can be moved and copied.
|
||
** Once invoked, the prepared configuration is assembled and the function »engaged«; from this point
|
||
** on, any attempt to move or copy the object will throw an exception, while it is still possible
|
||
** to assign other RandomDraw instances to this object.
|
||
** @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 lazy-init.hpp
|
||
** @see TestChainLoad_test
|
||
** @see SchedulerStress_test
|
||
*/
|
||
|
||
|
||
#ifndef LIB_RANDOM_DRAW_H
|
||
#define LIB_RANDOM_DRAW_H
|
||
|
||
|
||
#include "lib/error.h"
|
||
#include "lib/random.hpp"
|
||
#include "lib/lazy-init.hpp"
|
||
#include "lib/meta/function.hpp"
|
||
#include "lib/meta/function-closure.hpp"
|
||
#include "lib/util-quant.hpp"
|
||
#include "lib/util.hpp"
|
||
|
||
#include <functional>
|
||
#include <utility>
|
||
|
||
|
||
namespace lib {
|
||
namespace err = lumiera::error;
|
||
|
||
using lib::meta::disable_if_self;
|
||
using lib::meta::_Fun;
|
||
using std::function;
|
||
using std::forward;
|
||
using std::move;
|
||
|
||
|
||
|
||
/**
|
||
* A Result Value confined into fixed bounds.
|
||
* @tparam T underlying base type (number like)
|
||
* @tparam max maximum allowed param value (inclusive)
|
||
* @tparam max minimum allowed param value (inclusive) - defaults to "zero".
|
||
* @tparam zero the _neutral value_ in the value range
|
||
*/
|
||
template<typename T, T max, T min =T(0), T zero =min>
|
||
struct Limited
|
||
{
|
||
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;
|
||
|
||
template<typename X>
|
||
Limited (X raw)
|
||
: val(util::limited (X(minVal()), raw, X(maxVal())))
|
||
{ }
|
||
|
||
operator T&()
|
||
{
|
||
return val;
|
||
}
|
||
operator T const&() const
|
||
{
|
||
return val;
|
||
}
|
||
};
|
||
|
||
|
||
|
||
namespace random_draw { // Policy definitions
|
||
|
||
/**
|
||
* Default policy for RandomDraw: generate limted-range random numbers.
|
||
* @tparam max result values will be `uint` in the range `[0 ... max]`
|
||
*/
|
||
template<uint max>
|
||
struct LimitedRandomGenerate
|
||
: function<Limited<uint, max>(void)>
|
||
{
|
||
static double defaultSrc() { return lib::defaultGen.uni(); }
|
||
};
|
||
|
||
}//(End)Policy definitions
|
||
|
||
|
||
|
||
|
||
/**********************************************************//**
|
||
* 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.
|
||
* Probabilities and ranges can be configured by builder API.
|
||
* @tparam POL configuration policy baseclass
|
||
*/
|
||
template<class POL>
|
||
class RandomDraw
|
||
: public LazyInit<POL>
|
||
{
|
||
using Lazy = LazyInit<POL>;
|
||
using Disabled = Lazy::MarkDisabled;
|
||
|
||
using Sig = _Fun<POL>::Sig;
|
||
using Fun = function<Sig>;
|
||
using Tar = _Fun<POL>::Ret;
|
||
|
||
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 [min .. max] \ neutral
|
||
size_t shuffle_{0}; ///< statefull additional randomisation to inject into hash
|
||
|
||
|
||
/** @internal quantise into limited result value */
|
||
Tar
|
||
limited (double val)
|
||
{
|
||
if (probability_ == 0.0)
|
||
return Tar::zeroVal();
|
||
//
|
||
REQUIRE (Tar::minVal() <= minResult_);
|
||
REQUIRE (Tar::maxVal() >= maxResult_);
|
||
REQUIRE (minResult_ < maxResult_);
|
||
REQUIRE (0.0 <= probability_);
|
||
REQUIRE (probability_ <= 1.0);
|
||
double q = (1.0 - probability_);
|
||
if (val < q) // control probability of values ≠ neutral
|
||
return Tar::zeroVal();
|
||
if (val > 1.0)
|
||
val = 1.0;
|
||
val -= q; // [0 .. [q .. 1[
|
||
val /= probability_; // [0 .. 1[
|
||
auto org = Tar::zeroVal();
|
||
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{floor (val)};
|
||
}
|
||
else
|
||
if (org < minResult_ or org > maxResult_)
|
||
{ // disjoint form origin, but compact
|
||
org = minResult_; // ensure all values covered
|
||
val *= maxResult_ - org + 1; // [o .. m]
|
||
val += org;
|
||
val += CAP_EPSILON;
|
||
return Tar{floor (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_+1) // wrap the "negatives"
|
||
val -= maxResult_+1 - minResult_;
|
||
val += CAP_EPSILON;
|
||
return Tar{floor (val)};
|
||
}
|
||
} //----headroom to accommodate low probabilities
|
||
static constexpr size_t QUANTISER = 1 << 4 + util::ilog2 (Tar::maxVal()-Tar::minVal());
|
||
static constexpr double CAP_EPSILON = 1/(2.0 * QUANTISER);
|
||
|
||
|
||
/** @internal draw from source of randomness */
|
||
double
|
||
asRand (size_t hash)
|
||
{
|
||
if (shuffle_)
|
||
hash *= shuffle_++;
|
||
return double(hash % QUANTISER) / QUANTISER;
|
||
}
|
||
|
||
/** @internal core operation: draw and quantise into limited value */
|
||
Tar
|
||
drawLimited (size_t hash)
|
||
{
|
||
return limited (asRand (hash));
|
||
}
|
||
|
||
|
||
public:
|
||
/**
|
||
* Drawing is _disabled_ by default, always yielding "zero"
|
||
*/
|
||
RandomDraw()
|
||
: Lazy{Disabled()}
|
||
{
|
||
mapping (POL::defaultSrc);
|
||
}
|
||
|
||
/**
|
||
* Build a RandomDraw by attaching a value-processing function,
|
||
* which is adapted to accept the nominal input type. The effect
|
||
* of the given function is determined by its output value...
|
||
* - `size_t`: the function output is used as source of randomness
|
||
* - `double`: output is directly used as draw value `[0.0..1.0[`
|
||
* - `void(RandomDraw&, ...)` : the function manipulates the current
|
||
* instance, to control parameters dynamically, based on input.
|
||
*/
|
||
template<class FUN, typename =disable_if_self<RandomDraw, FUN>>
|
||
RandomDraw(FUN&& fun)
|
||
: Lazy{Disabled()}
|
||
, probability_{1.0}
|
||
{
|
||
mapping (forward<FUN> (fun));
|
||
}
|
||
|
||
|
||
|
||
/* ===== Builder API ===== */
|
||
|
||
RandomDraw&&
|
||
probability (double p)
|
||
{
|
||
probability_ = util::limited (0.0, p ,1.0);
|
||
return move (*this);
|
||
}
|
||
|
||
RandomDraw&&
|
||
maxVal (Tar m)
|
||
{
|
||
maxResult_ = util::min (m, Tar::maxVal());
|
||
if (minResult_>=maxResult_)
|
||
minVal (--m);
|
||
return move (*this);
|
||
}
|
||
|
||
RandomDraw&&
|
||
minVal (Tar m)
|
||
{
|
||
minResult_ = util::max (m, Tar::minVal());
|
||
if (maxResult_<=minResult_)
|
||
maxVal (++m);
|
||
return move (*this);
|
||
}
|
||
|
||
RandomDraw&&
|
||
shuffle (size_t seed =55)
|
||
{
|
||
shuffle_ = seed;
|
||
return move (*this);
|
||
}
|
||
|
||
RandomDraw&&
|
||
fixedVal (Tar v)
|
||
{
|
||
mapping ([v](size_t){ return v; });
|
||
return move (*this);
|
||
}
|
||
|
||
template<class FUN>
|
||
RandomDraw&&
|
||
mapping (FUN&& fun)
|
||
{
|
||
Fun& thisMapping = static_cast<Fun&> (*this);
|
||
Lazy::installInitialiser (thisMapping
|
||
,[theFun = forward<FUN> (fun)]
|
||
(RandomDraw* self)
|
||
{ // when lazy init is performed....
|
||
self->installAdapted (theFun);
|
||
});
|
||
return move (*this);
|
||
}
|
||
|
||
|
||
|
||
|
||
private:
|
||
/** @internal adapt a function and install it to control drawing and mapping */
|
||
template<class FUN>
|
||
void
|
||
installAdapted (FUN&& fun)
|
||
{
|
||
Fun& thisMapping = static_cast<Fun&> (*this);
|
||
thisMapping = adaptOut(adaptIn(std::forward<FUN> (fun)));
|
||
}
|
||
|
||
|
||
/** @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<class FUN>
|
||
auto
|
||
adaptIn (FUN&& fun)
|
||
{
|
||
using _Fun = lib::meta::_Fun<FUN>;
|
||
static_assert (_Fun(), "Need something function-like.");
|
||
|
||
using Sig = _Fun::Sig;
|
||
using Args = _Fun::Args;
|
||
using BaseIn = lib::meta::_Fun<POL>::Args;
|
||
|
||
if constexpr (std::is_same_v<Args, BaseIn>)
|
||
// function accepts same arguments as this RandomDraw
|
||
return forward<FUN> (fun); // pass-through directly
|
||
else
|
||
{// attempt to find a custom adaptor via Policy template
|
||
using Adaptor = POL::template Adaptor<Sig>;
|
||
return Adaptor::build (forward<FUN> (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 to 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 returning a `RandomDraw` instance
|
||
* by value; such a function is assumed to set some parametrisation based
|
||
* on the input data, allowing to change parameters dynamically.
|
||
* @return adapted function which produces a result value of type #Tar
|
||
*/
|
||
template<class FUN>
|
||
auto
|
||
adaptOut (FUN&& fun)
|
||
{
|
||
static_assert (lib::meta::_Fun<FUN>(), "Need something function-like.");
|
||
|
||
using Res = lib::meta::_Fun<FUN>::Ret;
|
||
using lib::meta::func::chained;
|
||
using lib::meta::_FunRet;
|
||
|
||
if constexpr (std::is_same_v<Res, Tar>)// ◁──────────────────────────┨ function produces result directly
|
||
return std::forward<FUN>(fun);
|
||
else
|
||
if constexpr (std::is_same_v<Res, size_t>)// ◁───────────────────────┨ function yields random source to draw value
|
||
return chained (std::forward<FUN>(fun)
|
||
,[this](size_t hash){ return drawLimited(hash); }
|
||
);
|
||
else
|
||
if constexpr (std::is_same_v<Res, double>)// ◁───────────────────────┨ function yields mapping value to be quantised
|
||
return chained (std::forward<FUN>(fun)
|
||
,[this](double rand){ return limited(rand); }
|
||
);
|
||
else
|
||
if constexpr (std::is_same_v<Res, RandomDraw>) // ◁───────────────────┨ RandomDraw with dynamically adjusted parameters
|
||
return [functor=std::forward<FUN>(fun)]
|
||
(auto&& ...inArgs) -> _FunRet<RandomDraw>
|
||
{ // invoke manipulator with copy
|
||
RandomDraw adaptedDraw = functor(inArgs...);
|
||
return adaptedDraw (forward<decltype(inArgs)> (inArgs)...);
|
||
}; // forward arguments to mapping-fun
|
||
else
|
||
static_assert (not sizeof(Res), "unable to adapt / handle result type");
|
||
NOTREACHED("Handle based on return type");
|
||
}
|
||
};
|
||
|
||
|
||
} // namespace lib
|
||
#endif /*LIB_RANDOM_DRAW_H*/
|