LUMIERA.clone/src/lib/random-draw.hpp
Ichthyostega 20f3252892 Upgrade: down with typename!!
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...)
2025-07-06 01:19:08 +02:00

427 lines
17 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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*/