diff --git a/src/lib/iter-explorer.hpp b/src/lib/iter-explorer.hpp index c5d251d6e..92dbd897b 100644 --- a/src/lib/iter-explorer.hpp +++ b/src/lib/iter-explorer.hpp @@ -117,6 +117,7 @@ #include "lib/util.hpp" #include +#include #include @@ -420,6 +421,7 @@ namespace lib { using Sig = typename FunDetector::Sig; using Arg = typename _Fun::Args::List::Head; // assuming function with a single argument using Res = typename _Fun::Ret; + static_assert (meta::is_UnaryFun()); @@ -971,6 +973,107 @@ namespace lib { + /** + * @internal Decorator for IterExplorer to group consecutive elements controlled by some + * grouping value \a GRP and compute an aggregate value \a AGG for each such group as + * iterator yield. The first group is consumed eagerly, each further group on iteration; + * thus when the aggregate for the last group appears as result, the source iterator has + * already been exhausted. The aggregate is default-initialised at start of each group + * and then the computation functor \a FAGG is invoked for each consecutive element marked + * with the same _grouping value_ — and this grouping value itself is obtained by invoking + * the functor \a FGRP on each source value. No capturing or even reordering of the source + * elements takes place, rather groups are formed based on the changes of the grouping value + * over the source iterator's result sequence. + * @tparam AGG data type to collect the aggregate; must be default constructible and assignable + * @tparam GRP value type to indicate a group + * @note while `groupFun` is adapted, the `aggFun` is _not adapted_ to the source iterator, + * but expected always to take the _value type_ of the preceding iterator, i.e. `*srcIter`. + * This limitation was deemed acceptable (adapting a function with several arguments would + * require quite some nasty technicalities). The first argument of this `aggFun` refers + * to the accumulator by value, and thereby also implititly defines the aggregate result type. + */ + template + class GroupAggregator + : public SRC + { + static_assert(can_IterForEach::value, "Lumiera Iterator required as source"); + + protected: + using SrcValue = typename meta::ValueTypeBinding::value_type; + using Grouping = function; + using Aggregator = function; + + std::optional agg_{}; + + Grouping grouping_; + Aggregator aggregate_; + + public: + using value_type = typename meta::RefTraits::Value; + using reference = typename meta::RefTraits::Reference; + using pointer = typename meta::RefTraits::Pointer; + + GroupAggregator() =default; + // inherited default copy operations + + template + GroupAggregator (SRC&& dataSrc, FGRP&& groupFun, FAGG&& aggFun) + : SRC{move (dataSrc)} + , grouping_{_FunTraits::adaptFunctor (forward (groupFun))} + , aggregate_{forward (aggFun)} + { + pullGroup(); // initially pull to establish the invariant + } + + + public: /* === Iteration control API for IterableDecorator === */ + bool + checkPoint() const + { + return bool(agg_); + } + + reference + yield() const + { + return *unConst(this)->agg_; + } + + void + iterNext() + { + if (srcIter()) + pullGroup(); + else + agg_ = std::nullopt; + } + + + protected: + SRC& + srcIter() const + { + return unConst(*this); + } + + /** @note establishes the invariant: + * source has been consumed up to the beginning of next group */ + void + pullGroup() + { + GRP group = grouping_(srcIter()); + agg_ = AGG{}; + do{ + aggregate_(*agg_, *srcIter()); + ++ srcIter(); + } + while (srcIter() and group == grouping_(srcIter())); + } + }; + + + + /** * @internal Decorator for IterExplorer to filter elements based on a predicate. * Similar to the Transformer, the given functor is adapted as appropriate. However, @@ -1581,7 +1684,7 @@ namespace lib { } - /** adapt this IterExplorer group result elements into fixed size chunks, packaged as std::array. + /** adapt this IterExplorer to group result elements into fixed size chunks, packaged as std::array. * The first group of elements is pulled eagerly at construction, while further groups are formed * on consecutive iteration. Iteration ends when no further full group can be formed; this may * leave out some leftover elements, which can then be retrieved by iteration through the @@ -1601,6 +1704,47 @@ namespace lib { } + /** adapt this IterExplorer to group elements by a custom criterium and aggregate the group members. + * The first group of elements is pulled eagerly at construction, further groups are formed on each + * iteration. Aggregation is done by a custom functor, which takes an _aggregator value_ as first + * argument and the current element (or iterator) as second argument. Downstream, the aggregator + * value computed for each group is yielded on iteration. + * @param groupFun a functor to derive a grouping criterium from the source sequence; consecutive elements + * yielding the same grouping value will be combined / aggregated + * @param aggFun a functor to compute contribution to the aggregate value. Signature `void(AGG&, Val&)`, + * where the type AGG implicitly also defines the _value_ to use for accumulation and the result value, + * while Val must be assignable from the _source value_ provided by the preceding iterator in the pipeline. + */ + template + auto + groupedBy (FGRP&& groupFun, FAGG&& aggFun) + { + using GroupVal = typename iter_explorer::_FunTraits::Res; + + static_assert (meta::is_BinaryFun()); + using ArgType1 = typename _Fun::Args::List::Head; + using Aggregate = typename meta::RefTraits::Value; + + using ResCore = iter_explorer::GroupAggregator; + using ResIter = typename _DecoratorTraits::SrcIter; + + return IterExplorer (ResCore {move(*this) + ,forward (groupFun) + ,forward (aggFun)}); + } + + /** simplified grouping to sum / combine all values in a group */ + template + auto + groupedBy (FGRP&& groupFun) + { + using Value = typename meta::ValueTypeBinding::value_type; + return groupedBy (forward (groupFun) + ,[](Value& agg, Value const& val){ agg += val; } + ); + } + + /** adapt this IterExplorer to iterate only as long as a condition holds true. * @return processing pipeline with attached [stop condition](\ref iter_explorer::StopTrigger) */ diff --git a/tests/library/iter-explorer-test.cpp b/tests/library/iter-explorer-test.cpp index 12b70a3d4..ff471b9f2 100644 --- a/tests/library/iter-explorer-test.cpp +++ b/tests/library/iter-explorer-test.cpp @@ -67,6 +67,7 @@ #include #include #include +#include namespace lib { @@ -279,6 +280,7 @@ namespace test{ verify_expand_rootCurrent(); verify_transformOperation(); verify_elementGroupingOperation(); + verify_aggregatingGroupItration(); verify_combinedExpandTransform(); verify_customProcessingLayer(); verify_scheduledExpansion(); @@ -668,6 +670,7 @@ namespace test{ } + /** @test package elements from the source pipeline into fixed-sized groups. * These groups are implemented as std::array and initialised with the values * yielded consecutively from the underlying source pipeline. The main iterator @@ -729,6 +732,47 @@ namespace test{ } + /** @test another form of grouping, where groups are formed by a derived property + * thereby passing each element in the group to an aggregator function, working on + * an accumulator per group. Downstream, the resulting, accumulated value is exposed + * for each group, while consuming all source values belonging to this group. + * - in the simple form, all members of a group are "added" together + * - the elaborate form allows to provide a custom aggregation function, which takes + * the »accumulator« as first argument by reference; the type of this argument + * implicitly defines what is instantiated for each group and yielded as result. + */ + void + verify_aggregatingGroupItration() + { + CHECK (materialise ( + explore(CountDown{10}) + .groupedBy(std::ilogbf) + ) + == "27-22-5-1"_expect); // 10+9+8|7+6+5+4|3+2|1 + + CHECK (materialise ( + explore(CountDown{10}) + .transform(util::toString) + .groupedBy([](auto& it) { return std::ilogbf (it.p); }) // note trickery: takes not the value, rather the iterator and + ) // accesses internals of CountDown, bypassing the transform layer above + == "1098-7654-32-1"_expect); // `+` does string concatenation + + + auto showGroup = [](auto it){ return "["+util::join(*it)+"]"; }; + // elaborate form with custom aggregation... + CHECK (materialise ( + explore(CountDown{10}) + .groupedBy(std::ilogbf + ,[](vector& accum, uint val) + { + accum.push_back (val); + }) + .transform(showGroup) + ) + == "[10, 9, 8]-[7, 6, 5, 4]-[3, 2]-[1]"_expect); + } + + /** @test combine the recursion into children with a tail mapping operation. * Wile basically this is just the layering structure of IterExplorer put into action, * you should note one specific twist: the iter_explorer::Expander::expandChildren() call diff --git a/tests/vault/gear/scheduler-stress-test.cpp b/tests/vault/gear/scheduler-stress-test.cpp index 00119394f..026ddc5e6 100644 --- a/tests/vault/gear/scheduler-stress-test.cpp +++ b/tests/vault/gear/scheduler-stress-test.cpp @@ -161,7 +161,7 @@ SHOW_EXPR(micros); CHECK (micros < 550); CHECK (micros > 450); - std::vector::Agg> levelWeights = testLoad.allNodes().processingLayer::WeightAggregator>().effuse(); + std::vector levelWeights = testLoad.allLevelWeights().effuse(); SHOW_EXPR(levelWeights.size()) for (auto& w : levelWeights) { @@ -175,7 +175,7 @@ SHOW_EXPR(levelWeights.size()) .effuse(); for (auto& n : nodeData) cout<<"level:"< - + + @@ -107489,7 +107490,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + + @@ -107518,16 +107520,64 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - - + + + + + + + - - + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +