/* ParameterScale(Test) - verify parameter value scales Copyright (C) 2026, Hermann Vosseler   **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 parameter-test.cpp ** unit test \ref ParameterScale_test */ #include "test/run.hpp" #include "test/test-helper.hpp" #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; namespace lib { namespace par { namespace test{ // using lumiera::error::LUMIERA_ERROR_LOGIC; namespace {//Test fixture.... }//(End)Test fixture /**************************************************************************//** * @test cover properties of generic parameter containers. */ class ParameterScale_test : public Test { virtual void run (Arg) { simpleUsage(); verify_Conforming(); verify_cyclicScale(); verify_valueJoining(); } void simpleUsage() { Scale scale = {.minVal = -5, .maxVal = 23}; CHECK (scale.isValid()); CHECK (-5 == scale.conform (-55)); CHECK (23 == scale.conform (+55)); CHECK ( 5 == scale.join (2,3) ); CHECK (-5 == scale.join (23,-55)); } /** @test accommodate values to comply with the scale * - optionally a maximum and minimum can be enforced * - optionally the value can be cyclically wrapped * @todo WIP 9/26 ✔ define ⟶ ✔ implement */ void verify_Conforming() { Scale scale; CHECK (1e5f == scale.conform (1e5f)); scale.maxVal = -5.0f; CHECK (-5.0f == scale.conform (1e5f)); CHECK (scale.isValid()); scale.minVal = 5.0f; CHECK (not scale.isValid()); scale.maxVal = 8.0f; CHECK (scale.isValid()); CHECK (5.0f == scale.conform (-5.0f)); CHECK (not scale.isCyclic()); scale.cyclicLim = 100; CHECK (scale.isCyclic()); CHECK (7.0f == scale.conform (-5.0f)); CHECK (5.0f == scale.conform (-4.0f)); CHECK (6.0f == scale.conform (-3.0f)); CHECK (7.0f == scale.conform (-2.0f)); VERIFY_FAIL ("beyond supported numeric precision" , scale.conform (-1e5f)); } /** @test Scales can be configured to wrap cyclically, e.g. for angles. * - demonstrate an unsigned angle scale with short precision * - demonstrate a high-resolution scale symmetric to zero * @todo WIP 9/26 ✔define ⟶ ✔ implement */ void verify_cyclicScale() { Scale angleScale = {.minVal=0, .maxVal=360, .cyclicLim=0xFFFF}; CHECK (angleScale.isValid()); CHECK (angleScale.isCyclic()); CHECK ( 0 == angleScale.conform(0)); CHECK (180 == angleScale.conform(180)); CHECK (240 == angleScale.conform(240)); CHECK ( 0 == angleScale.conform(360)); CHECK (359 == angleScale.conform(719)); CHECK ( 0 == angleScale.conform(65520)); CHECK ( 15 == angleScale.conform(65535)); Scale tinyScale = {.minVal=-lflp(1)/100, .maxVal=lflp(1)/100}; tinyScale.cyclicLim = util::limit_cyclicWrap(*tinyScale.maxVal - *tinyScale.minVal); CHECK (1e16 < tinyScale.cyclicLim); CHECK (tinyScale.isValid()); CHECK (tinyScale.isCyclic()); CHECK ( tinyScale.conform(0 ) == 0 ); CHECK (roughEQ (tinyScale.conform(0.001) , 0.001)); CHECK (roughEQ (tinyScale.conform(0.005) , 0.005)); CHECK (roughEQ (tinyScale.conform(0.00999), 0.00999)); CHECK (roughEQ (tinyScale.conform(0.01 ),-0.0099999)); // surprise: 1/100 not exactly representable in floating-point // since the domain is symmetric and 2/100 long, the ten-folds will fall into the middle, // but they typically do not land precisely on zero due to inherent "number dust" CHECK (1e15 > fabs(tinyScale.conform(1))); CHECK (0 != tinyScale.conform(1) ); auto beyond_precision = -10 * tinyScale.cyclicLim; VERIFY_FAIL ("beyond supported numeric precision" , tinyScale.conform (beyond_precision)); } /** @test Base value and data feeds from differing Scales can be joined together * - when the target scale defines limits (or is cyclic), * this conforming is applied after all conversions * - a feed can be marked as "factor", in which case * the feed is multiplied to the target rather than added * - even scales with integral type can carry log meaning * - suitable intermediary types used for good precision, * and especially to avoid wrapping signed into unsigned * - the supported logarithmic flavours are accommodated, * possibly converting the base of the logarithm accordingly * - the factor 10 inherent to decibels is applied / removed * before combining decibel values with other logarithms * - 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 */ void verify_valueJoining() { Scale scale = {.minVal = 2, .maxVal = 10}; Scale feedScale; 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 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 feedScale.metric = DEC; // in dB CHECK ( 7 == scale.join (4, int64_t(3), feedScale)); // 4 + 3dB ⟼ 4 * 1.995 ≡ 7.98 ⟼ truncated to 7 Scale sscale; Scale uscale = {.neutral = 1}; // Note: number promotion rules of C++ would backfire nastily here... CHECK (short(-3) * uint64_t(2) > numeric_limits::max()); CHECK (short(-6) == sscale.join (-3, uint64_t(2), uscale)); //---floating-point logarithmic scale conversions--- // 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 10dB ≙ factor 10 * lg(10) value divided / 10 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 feedScale.metric = BIN; CHECK (roughEQ (fscale.join (0, int64_t(-3), feedScale), log10(1.0f/8)*10)); // binary log (given as int value) ⟼ dB fscale.metric = LIN; CHECK (roughEQ (fscale.join (1, int64_t(-3), feedScale), 1.0f/8 )); // binary log applied as factor to a linear scale fscale.metric = BIN; feedScale.metric = LIN; feedScale.neutral = 1; CHECK (feedScale.isFactor()); CHECK (fscale.isLogarithmic()); CHECK (1 + 3 == fscale.join (1, int64_t(8), feedScale)); // feed factor 8 ⟼ to binary logarithmic fscale (8 ≡ 2^3) and added there feedScale.neutral = 0; CHECK (not feedScale.isFactor()); VERIFY_FAIL ("adding a non-logarithmic data feed on top of a logarithmic base value" , fscale.join (1, int64_t(8), feedScale)); } /** @test * @todo WIP 9/26 🔁 define ⟶ implement */ void verify_NominalScale() { } }; /** Register this test class... */ LAUNCHER (ParameterScale_test, "unit lib"); }}} // namespace lib::par::test