From b11abb2d158f389a98755daed7763a3348be5f7e Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Tue, 22 Sep 2026 18:39:09 +0200 Subject: [PATCH] Parameters: resolve type precision problems with scale-joinig The selection of the intermediary working type, used for exponential and logarithmic functions when combining values from different scales, was implemented too simplistic. Following the example of the `NumberJoinType`, it seems preferrable to spell out the decision logic explicitly. This allows to pick `long doubl` as working type especially in cases where 64bit integral parameters are involved, either as source or destination. With these final tweaks, the `verify_valueJoining()` test is not complete, presumably with path coverage of the conversion functions. The Scale descriptor and operational logic is now defined to the point that it can be integrated into the general Parameter implementation machinery. --- src/lib/par/scale.hpp | 13 +++- src/lib/par/type-handler.hpp | 49 ++++++++----- tests/16parameter.tests | 2 +- tests/lib/par/parameter-scale-test.cpp | 77 +++++++++++++++----- wiki/thinkPad.ichthyo.mm | 99 ++++++++++++++++---------- 5 files changed, 168 insertions(+), 72 deletions(-) diff --git a/src/lib/par/scale.hpp b/src/lib/par/scale.hpp index 7879202eb..5e4123c84 100644 --- a/src/lib/par/scale.hpp +++ b/src/lib/par/scale.hpp @@ -42,6 +42,7 @@ namespace lib { namespace par { namespace err = lumiera::error; + using std::abs; using util::_Fmt; using std::optional; @@ -79,6 +80,7 @@ namespace par { bool isValid() const; bool isFactor() const; bool isCyclic() const; + bool isLimited() const; bool isLogarithmic() const; /* ===== conforming operations ===== */ @@ -125,7 +127,8 @@ namespace par { and (not isCyclic() or (minVal and maxVal and *minVal < *maxVal - and VAL(0) < cyclicLim) + and VAL(0) < cyclicLim + and abs (*maxVal) <= cyclicLim) ) ; } @@ -146,6 +149,14 @@ namespace par { return VAL(0) != cyclicLim; } + template + inline bool + Scale::isLimited() const + { + return (maxVal or minVal) + and not isCyclic(); + } + template inline bool Scale::isLogarithmic() const diff --git a/src/lib/par/type-handler.hpp b/src/lib/par/type-handler.hpp index 1094c0a2f..b5f67b32d 100644 --- a/src/lib/par/type-handler.hpp +++ b/src/lib/par/type-handler.hpp @@ -154,20 +154,13 @@ _Pragma("GCC diagnostic pop") template concept number = integral or floating_point; - - /** - * the best-precision floating point type to carry out - * a numeric operation to combine two values - * @note the result type is always some floating point, - * since the goal is to use the exponential / logarithm family. - * @todo 9/26 we _could_ go from integral to long double for 64bit, - * yet it is not clear if we ever need that tiny bit of extra precision... - */ - template - using CommonComputeType = std::conditional_t and integral, double - , common_type_t>; - + + namespace { + static_assert (64 <= std::numeric_limits::digits + ,"long double type can not represent uint64_t exactly"); + + /** @internal decision logic for combining numeric parameters */ template auto @@ -190,8 +183,21 @@ _Pragma("GCC diagnostic pop") } } - static_assert (64 <= std::numeric_limits::digits - ,"long double type can not represent uint64_t exactly"); + /** @internal logic to pick a suitable floating-point for intermediary computations */ + template + auto + selectCommonComputeType() + { + if constexpr (is_integral_v and is_integral_v) + { // ensure integral precision is retained to the degree possible... + if constexpr (8 > sizeof(V) and 8 > sizeof(W)) + return std::type_identity{}; + else + return std::type_identity{}; + } + else + return std::type_identity>{}; + } } /** @@ -202,8 +208,17 @@ _Pragma("GCC diagnostic pop") * - in the remaining problematic case(s), long double is used, as it can accommodate * the full 64bit value range without loss on platforms relevant for Lumiera. */ - template - using NumberJoinType = typename decltype(selectNumberJoinType())::type; + template + using NumberJoinType = typename decltype(selectNumberJoinType())::type; + + /** + * best-precision floating point type to carry out + * a numeric operation to transform and adapt value scales. + * @note the result type is always some floating point, + * since the goal is to use the exponential / logarithm family. + */ + template + using CommonComputeType = typename decltype(selectCommonComputeType())::type; diff --git a/tests/16parameter.tests b/tests/16parameter.tests index 9e1a93c40..dc8b34a41 100644 --- a/tests/16parameter.tests +++ b/tests/16parameter.tests @@ -17,7 +17,7 @@ return: 0 END -PLANNED "Parameter value scales" ParameterScale_test << END +TEST "Parameter value scales" ParameterScale_test << END return: 0 END diff --git a/tests/lib/par/parameter-scale-test.cpp b/tests/lib/par/parameter-scale-test.cpp index 68d127641..b4b3196c0 100644 --- a/tests/lib/par/parameter-scale-test.cpp +++ b/tests/lib/par/parameter-scale-test.cpp @@ -21,13 +21,6 @@ #include "lib/par/scale.hpp" #include "test/diagnostic-output.hpp"/////////////TODO -//#include -//#include -//#include - -//using std::string; -//using std::vector; -//using std::swap; using test::roughEQ; @@ -35,8 +28,6 @@ namespace lib { namespace par { namespace test{ -// using lumiera::error::LUMIERA_ERROR_LOGIC; - namespace {//Test helpers.... template @@ -59,7 +50,13 @@ namespace test{ /**************************************************************************//** - * @test cover properties of generic parameter containers. + * @test demonstrate features and verify behaviour of value Scale definitions. + * - a par::Scale is a descriptor record with some feature configuration settings + * - some combination of setting marks specific scale features to be present + * - values can be _conformed_ to comply with the scale constraints + * - a Scale can be configured to maintain cyclic wrapping values + * - values defined on different scales can be _joined_ (numerically combined) + * - scales can be defined to be logarithmic, and work together with linear scales. */ class ParameterScale_test : public Test { @@ -68,6 +65,7 @@ namespace test{ run (Arg) { simpleUsage(); + verify_Predicate(); verify_Conforming(); verify_cyclicScale(); verify_valueJoining(); @@ -88,6 +86,51 @@ namespace test{ } + /** @test verify the feature detection and descriptor predicates */ + void + verify_Predicate() + { + Scale scale; + + CHECK (scale.isValid()); + CHECK (not scale.isCyclic()); + CHECK (not scale.isLimited()); + CHECK (not scale.isLogarithmic()); + + scale.minVal = -5; + CHECK (scale.isValid()); + CHECK (not scale.isCyclic()); + CHECK ( scale.isLimited()); + + scale.cyclicLim = 10; + CHECK (not scale.isValid()); + CHECK ( scale.isCyclic()); + CHECK (not scale.isLimited()); + + scale.maxVal = +5; + CHECK ( scale.isValid()); + CHECK ( scale.isCyclic()); + CHECK (not scale.isLimited()); + + CHECK (not scale.isFactor()); + scale.neutral = 2; + CHECK (not scale.isFactor()); + scale.neutral = 1; + CHECK ( scale.isFactor()); + + CHECK (scale.metric == LIN); + CHECK (not scale.isLogarithmic()); + scale.metric = NOM; + CHECK (not scale.isLogarithmic()); + scale.metric = DEC; + CHECK ( scale.isLogarithmic()); + scale.metric = BIN; + CHECK ( scale.isLogarithmic()); + scale.metric = NAT; + CHECK ( scale.isLogarithmic()); + } + + /** @test accommodate values to comply with the scale * - optionally a maximum and minimum can be enforced * - optionally the value can be cyclically wrapped @@ -181,7 +224,9 @@ namespace test{ * - applying a linear factor to a logarithmic scale is * transformed into an addition in the logarithmic domain * - adding a linear offset to a logarithmic scale is rejected - * @todo WIP 9/26 🔁 define ⟶ ✔ implement + * - extended precision scales handled using `lflp` (≙`long double`) + * - this allows to retain full precision of 64bit integrals + * @todo WIP 9/26 ✔ define ⟶ ✔ implement */ void verify_valueJoining() @@ -276,7 +321,7 @@ namespace test{ CHECK (logb (_MAX) < logb (megaScale.cyclicLim)); // ...which the (cyclic) target scale can still handle precisely! lflp res1 = megaScale.join (-5, 1e18f, fscale); // log10 ⟼ 18, so we add -5dB + 180dB; - CHECK (roughEQ (res1, -5)); // the cyclicWrap then computes 175 - (-10) ≡ 185 % 20 (≙5) + (minVal ≙ -10) + CHECK (roughEQ (res1, -5)); // the cyclicWrap then computes 175 - (-10) ≡ 185 % 20 (≙5) + (minVal ≙ -10) // but the computation is not exact... CHECK (0 != std::fmod (res1, 1)); @@ -295,10 +340,10 @@ namespace test{ // compare to target scale with lesser precision... Scale doubleTargetScale = {.metric = DEC}; Scale longTargetScale = {.metric = DEC}; - CHECK (180 == doubleTargetScale.join (0, int64_t(1e18), lscale)); - CHECK (180 == doubleTargetScale.join (0, int64_t(1e18)+1, lscale)); - CHECK (180 == longTargetScale.join (0, int64_t(1e18), lscale)); - CHECK (180 != longTargetScale.join (0, int64_t(1e18)+1, lscale)); /////////////OOO this is a Bug! we should indeed use lflp for the interim computation + CHECK (180 == doubleTargetScale.join (0, int64_t(1e18), lscale)); // uses double for internal log10 computation + CHECK (180 == doubleTargetScale.join (0, int64_t(1e18)-10, lscale)); // offset chosen large enough to prevail in int64_t but not in double + CHECK (180 == longTargetScale.join (0, int64_t(1e18), lscale)); // 64bit integral scales use long-double for log10 computation internally + CHECK (179 == longTargetScale.join (0, int64_t(1e18)-10, lscale)); // ...and thus the offset survives the log10 and is truncated down on conversion } diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 0a072f602..d069002ce 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -86269,16 +86269,17 @@ class ScaleRegistry - + + - - + + @@ -87235,9 +87236,9 @@ Prop.1: dist%3 1 2 0 1 2 0 - - - + + + @@ -87251,7 +87252,7 @@ Prop.1: dist%3 1 2 0 1 2 0 - + @@ -87263,7 +87264,7 @@ Prop.1: dist%3 1 2 0 1 2 0 - + @@ -87285,8 +87286,7 @@ Prop.1: dist%3 1 2 0 1 2 0 - - + @@ -87315,8 +87315,7 @@ Prop.1: dist%3 1 2 0 1 2 0 Überraschung: die Logarithmen-Matematik funktioniert ... aber das dumme Mensch braucht endlos um zu kapieren daß die Beispielwerte stimmen

- - +
@@ -87332,15 +87331,26 @@ Prop.1: dist%3 1 2 0 1 2 0
- + - - - - + + + + + + + + +

+ nicht mehr versuchen, alles in einen Typ-Ausdruck zu packen; stattdessen hier auch die Technik mit einer Constexpr-Funktion, die einen type_identity-Record liefert +

+ +
+ +
@@ -87373,6 +87383,15 @@ Prop.1: dist%3 1 2 0 1 2 0
+ + + + + + + + +
@@ -87442,8 +87461,8 @@ Prop.1: dist%3 1 2 0 1 2 0 - - + + @@ -87520,7 +87539,7 @@ Prop.1: dist%3 1 2 0 1 2 0 - + @@ -87576,8 +87595,8 @@ Prop.1: dist%3 1 2 0 1 2 0 - - + + @@ -87605,6 +87624,11 @@ Prop.1: dist%3 1 2 0 1 2 0 + + + + + @@ -87640,17 +87664,17 @@ Prop.1: dist%3 1 2 0 1 2 0 - - + + - + - + @@ -87659,12 +87683,12 @@ Prop.1: dist%3 1 2 0 1 2 0 jaja ... oft weiß man nicht ob es um "overengieering" geht, oder man selber einfach faul ist

- -
+
- - + + + @@ -87693,13 +87717,12 @@ Prop.1: dist%3 1 2 0 1 2 0 Tatsächlich ist der ganze Zweig mit dem Genauigkeits-Limit nur für floating-point-Typen notwendig; und diesen Branch-Guard kann man tatsächlich schon zur compile-time machen!

- - +
- + @@ -87738,8 +87761,7 @@ Prop.1: dist%3 1 2 0 1 2 0 das ist die größtmögliche 10-er-Potenz, die noch in 63bit reinpaßt

- -
+
@@ -87756,8 +87778,8 @@ Prop.1: dist%3 1 2 0 1 2 0
- - + + @@ -87765,6 +87787,9 @@ Prop.1: dist%3 1 2 0 1 2 0
+ + +