From fd5548b61b92ab00f6cda1661efd6af901d238e0 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Mon, 21 Sep 2026 23:12:12 +0200 Subject: [PATCH] Parameters: verify use of a long-double target scale Scrutinising the bounds of numeric precision. As it turns out, most cases work as expected, including the massive errors incurred by input-values given as float. However: a logarithmic combination integral -> integral runs in loss of precision So the selection logic for the intermediary working type is still overly simplistic! --- src/lib/par/scale.hpp | 2 +- tests/lib/par/parameter-domain-test.cpp | 4 +- tests/lib/par/parameter-scale-test.cpp | 134 ++++++++++++++------- wiki/thinkPad.ichthyo.mm | 147 +++++++++++++++++++++++- 4 files changed, 239 insertions(+), 48 deletions(-) diff --git a/src/lib/par/scale.hpp b/src/lib/par/scale.hpp index 67d05a6bd..7879202eb 100644 --- a/src/lib/par/scale.hpp +++ b/src/lib/par/scale.hpp @@ -181,7 +181,7 @@ namespace par { { REQUIRE (minVal and maxVal); REQUIRE (*minVal < *maxVal); - if (std::is_floating_point_v) + if constexpr (std::is_floating_point_v) if (not (std::abs (rawVal) < cyclicLim)) throw err::Invalid {_Fmt{"Parameter value %4.2g beyond supported numeric precision " "for cyclic wrapping into [%1.2g...%1.2g[ "} diff --git a/tests/lib/par/parameter-domain-test.cpp b/tests/lib/par/parameter-domain-test.cpp index 5d01a8588..8f8c9da24 100644 --- a/tests/lib/par/parameter-domain-test.cpp +++ b/tests/lib/par/parameter-domain-test.cpp @@ -39,7 +39,7 @@ namespace test{ // using lumiera::error::LUMIERA_ERROR_LOGIC; using std::floating_point; - namespace {//Test fixture.... + namespace {//Test helpers.... template constexpr NUM _MAX = std::numeric_limits::max(); @@ -50,7 +50,7 @@ namespace test{ template constexpr FLO _EPSILON = std::numeric_limits::epsilon(); - }//(End)Test fixture + }//(End)Test helpers diff --git a/tests/lib/par/parameter-scale-test.cpp b/tests/lib/par/parameter-scale-test.cpp index 08058a848..68d127641 100644 --- a/tests/lib/par/parameter-scale-test.cpp +++ b/tests/lib/par/parameter-scale-test.cpp @@ -37,9 +37,18 @@ namespace test{ // using lumiera::error::LUMIERA_ERROR_LOGIC; - namespace {//Test fixture.... + namespace {//Test helpers.... - }//(End)Test fixture + template + constexpr NUM _MAX = std::numeric_limits::max(); + + template + constexpr NUM _MIN = std::numeric_limits::lowest(); + + template + constexpr FLO _EPSILON = std::numeric_limits::epsilon(); + + }//(End)Test helpers @@ -178,18 +187,18 @@ namespace test{ verify_valueJoining() { Scale scale = {.minVal = 2, .maxVal = 10}; - Scale feedScale; + Scale lscale; - CHECK ( 5 == scale.join (2, int64_t(3), feedScale)); // 2+3 ≡ 5 (all computations done as double) - CHECK (10 == scale.join (5, int64_t(8), feedScale)); // 5+8 ≡ 13 ⟼ capped to 10 + CHECK ( 5 == scale.join (2, int64_t(3), lscale)); // 2+3 ≡ 5 (all computations done as double) + CHECK (10 == scale.join (5, int64_t(8), lscale)); // 5+8 ≡ 13 ⟼ capped to 10 - feedScale.neutral = 1; - CHECK (feedScale.isFactor()); - CHECK ( 6 == scale.join (2, int64_t(3), feedScale)); // 2*3 ≡ 6 - CHECK ( 2 == scale.join (2, int64_t(-3),feedScale)); // 2*-3 ≡ -6 ⟼ first conditioned to ushort domain, then capped to 2 + lscale.neutral = 1; + CHECK (lscale.isFactor()); + CHECK ( 6 == scale.join (2, int64_t(3), lscale)); // 2*3 ≡ 6 + CHECK ( 2 == scale.join (2, int64_t(-3),lscale)); // 2*-3 ≡ -6 ⟼ first conditioned to ushort domain, then capped to 2 - feedScale.metric = DEC; // in dB - CHECK ( 7 == scale.join (4, int64_t(3), feedScale)); // 4 + 3dB ⟼ 4 * 1.995 ≡ 7.98 ⟼ truncated to 7 + lscale.metric = DEC; // in dB + CHECK ( 7 == scale.join (4, int64_t(3), lscale)); // 4 + 3dB ⟼ 4 * 1.995 ≡ 7.98 ⟼ truncated to 7 Scale sscale; Scale uscale = {.neutral = 1}; @@ -202,51 +211,94 @@ namespace test{ // Scale fscale = {.metric = NAT}; Scale feed_dB = {.metric = DEC}; - CHECK (roughEQ (fscale.join (0, float(20), feed_dB) , log(100.0f) )); // dB ⟼ natural logarithm; note 20dB ≙ factor 10 * lg(100); input 20 divided by 10 + CHECK (roughEQ (fscale.join (0, float(20), feed_dB) , log(100.0f) )); // dB ⟼ natural logarithm; note 20dB ≙ factor 10 * lg(100); input 20 divided by 10 fscale.metric = BIN; - CHECK (roughEQ (fscale.join (0, float(20), feed_dB) , log2(100.0f))); // dB ⟼ binary logarithm; (factor 1/10 on input is folded into the log factor) + CHECK (roughEQ (fscale.join (0, float(20), feed_dB) , log2(100.0f))); // dB ⟼ binary logarithm; (factor 1/10 on input is folded into the log factor) fscale.metric = DEC; - CHECK (roughEQ (fscale.join (0, float(20), feed_dB) , 20.0f )); // dB ⟼ dB just passed-through and then added to value ≔ 0.0 + CHECK (roughEQ (fscale.join (0, float(20), feed_dB) , 20.0f )); // dB ⟼ dB just passed-through and then added to value ≔ 0.0 fscale.metric = DEC; - feedScale.metric = BIN; - CHECK (roughEQ (fscale.join (0, int64_t(-3), feedScale), log10(1.0f/8) *10)); // binary log (given as int value) ⟼ dB - feedScale.metric = NAT; - CHECK (roughEQ (fscale.join (0, int64_t(-3), feedScale), log10(exp(-3))*10)); // natural log (input as int) ⟼ dB - feedScale.metric = DEC; - CHECK (roughEQ (fscale.join (0, int64_t(-3), feedScale), -3 )); // dB ⟼ dB just passed-through + lscale.metric = BIN; + CHECK (roughEQ (fscale.join (0, int64_t(-3), lscale), log10(1.0f/8) *10)); // binary log (given as int value) ⟼ dB + lscale.metric = NAT; + CHECK (roughEQ (fscale.join (0, int64_t(-3), lscale), log10(exp(-3))*10)); // natural log (input as int) ⟼ dB + lscale.metric = DEC; + CHECK (roughEQ (fscale.join (0, int64_t(-3), lscale), -3 )); // dB ⟼ dB just passed-through fscale.metric = BIN; - feedScale.metric = BIN; - CHECK (roughEQ (fscale.join (0, int64_t(-3), feedScale), -3 )); // lb ⟼ lb just passed-through - feedScale.metric = NAT; - CHECK (roughEQ (fscale.join (0, int64_t(-3), feedScale), log2 (exp(-3)) )); // natural log ⟼ binary log - feedScale.metric = DEC; - CHECK (roughEQ (fscale.join (0, int64_t(-30),feedScale), log2 (1.0/1000) )); // conversion factor: dB/10 ⟼ binary log + lscale.metric = BIN; + CHECK (roughEQ (fscale.join (0, int64_t(-3), lscale), -3 )); // lb ⟼ lb just passed-through + lscale.metric = NAT; + CHECK (roughEQ (fscale.join (0, int64_t(-3), lscale), log2 (exp(-3)) )); // natural log ⟼ binary log + lscale.metric = DEC; + CHECK (roughEQ (fscale.join (0, int64_t(-30),lscale), log2 (1.0/1000) )); // conversion factor: dB/10 ⟼ binary log fscale.metric = LIN; - feedScale.metric = BIN; - CHECK (roughEQ (fscale.join (1, int64_t(-3), feedScale), 1.0f/8 )); // binary log applied as factor to a linear scale - feedScale.metric = NAT; - CHECK (roughEQ (fscale.join (1, int64_t(-1), feedScale), 1.0f/exp(1))); // natural log applied as factor 1/e - feedScale.metric = DEC; - CHECK (roughEQ (fscale.join (1, int64_t(-30),feedScale), 1.0f/1000 )); // decibels applied as factor : -30 dB ≙ 10^-3 + lscale.metric = BIN; + CHECK (roughEQ (fscale.join (1, int64_t(-3), lscale), 1.0f/8 )); // binary log applied as factor to a linear scale + lscale.metric = NAT; + CHECK (roughEQ (fscale.join (1, int64_t(-1), lscale), 1.0f/exp(1))); // natural log applied as factor 1/e + lscale.metric = DEC; + CHECK (roughEQ (fscale.join (1, int64_t(-30),lscale), 1.0f/1000 )); // decibels applied as factor : -30 dB ≙ 10^-3 fscale.metric = BIN; - feedScale.metric = LIN; - feedScale.neutral = 1; - CHECK (feedScale.isFactor()); + lscale.metric = LIN; + lscale.neutral = 1; + CHECK (lscale.isFactor()); CHECK (fscale.isLogarithmic()); - CHECK ( fscale.join (1, int64_t(8), feedScale) == 1 + 3); // feed factor 8 ⟼ to binary logarithmic fscale (8 ≡ 2^3) and added there (exact) + CHECK ( fscale.join (1, int64_t(8), lscale) == 1 + 3); // feed factor 8 ⟼ to binary logarithmic fscale (8 ≡ 2^3) and added there (exact) fscale.metric = NAT; - CHECK (roughEQ (fscale.join (1, int64_t(8), feedScale), 1 + logf(8))); // feed factor 8 ⟼ now applied to a natural log scale (and added to one, in float) + CHECK (roughEQ (fscale.join (1, int64_t(8), lscale), 1 + logf(8))); // feed factor 8 ⟼ now applied to a natural log scale (and added to one, in float) fscale.metric = DEC; - CHECK (roughEQ (fscale.join (1, int64_t(100),feedScale), 1 + 20)); // feed factor 8 ⟼ now applied as offset in decibels + CHECK (roughEQ (fscale.join (1, int64_t(100),lscale), 1 + 20)); // feed factor 8 ⟼ now applied as offset in decibels - feedScale.neutral = 0; - CHECK (not feedScale.isFactor()); + lscale.neutral = 0; + CHECK (not lscale.isFactor()); VERIFY_FAIL ("adding a non-logarithmic data feed on top of a logarithmic base value" - , fscale.join (1, int64_t(8), feedScale)); + , fscale.join (1, int64_t(8), lscale)); + + + //---verify a long-double target scale + // + Scale megaScale = {.minVal = -10, .maxVal = +10}; // setup a cyclic scale using the long double domain + megaScale.cyclicLim = util::limit_cyclicWrap(20); // (cyclic wrap is handled by conform(); just another little twist) + CHECK (megaScale.isCyclic()); + megaScale.metric = DEC; + fscale.metric = LIN; + fscale.neutral = 1; + CHECK (fscale.isFactor()); + CHECK ( 5 == megaScale.join ( 5, 100.0f, fscale)); // input ⟼ decibels, then added to the base value, and finally wrapped ∈ [-10 ... 10[ + CHECK (-5 == megaScale.join (-5, 100.0f, fscale)); + CHECK ( 5 == megaScale.join (-5, 1000.0f, fscale)); // ...so here we get -5dB + 30dB + + // now push that to the limits.... + CHECK (logb (1e18) < logb (_MAX)); // use the largest power of 10 representable in int64_t + 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) + + // but the computation is not exact... + CHECK (0 != std::fmod (res1, 1)); + // yet the cause lies in the input already: + lflp srcErr = lflp(1e18f) - pow (lflp(10), 18); + CHECK (1e10 < fabs (srcErr)); // ... so the float input incurs a gigantic error margin + + // however: use an exact input, and the result will be exact + lscale.neutral = 1; + CHECK (lscale.isFactor()); + CHECK (-5 == megaScale.join (-5, int64_t(1e18), lscale)); + lflp res2 = megaScale.join (-5, int64_t(1e18)+1, lscale); + CHECK (-5 != res2); // even a tiny offset makes it through the computation + CHECK (-16 > log10 (res2 -(-5))); // error is below the 16. decimal place (plausible, since log10 is computed) + + // 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 } diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 32f4feb78..0a072f602 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -87321,8 +87321,30 @@ Prop.1: dist%3 1 2 0 1 2 0 + + + + + +

+ die Berechnungen sind ungenau, mit floating-point und transzendentalen Funktionen. Aber die vorhandene Daten-Genauigkeit sollte ausgeschöpft werden, ohne jetzt gleich alles mit long double rechnen zu müssen +

+ +
- + + + + + + + + + + + +
+ @@ -87330,16 +87352,21 @@ Prop.1: dist%3 1 2 0 1 2 0
  • Type-promotion-Bug in der Anwendung der pow()-Funktion
  • +
  • + weiterer Type-promotion-Error in der baseScale-Funktion +
  • logb() liefert nur den exponenten, log2() den 2-er-Logarithmus
  • - das Wiederverwenden der asLogarithm()-Funktion macht deutlich, daß diese einen unnötigen Typ-Constraint auf den VAL-Parameter der Ausgans-Scale erzwingt + in conform steckt auch noch ein statischer Fehler: std::abs funktioniert nicht auf unsigned +
  • +
  • + das Wiederverwenden der asLogarithm()-Funktion macht deutlich, daß diese einen unnötigen Typ-Constraint auf den VAL-Parameter der Ausgangs-Scale erzwingt
  • - -
    +
    @@ -87624,6 +87651,118 @@ 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 +

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

    + woot?  kann keinen std::abs berechnen von .... uint64_t  ??! +

    +

    + "der mutt dat doch checken!" +

    + +
    + +
    + + + + +

    + 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! +

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

    + also nehme ich einen linearen Input +

    + +
    +
    + + + + +

    + damit's raucht! +

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

    + das ist die größtmögliche 10-er-Potenz, die noch in 63bit reinpaßt +

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