From 00b0aa0c5b59c1eaaf725fb639f58ead605b8923 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Mon, 21 Sep 2026 21:48:15 +0200 Subject: [PATCH] Parameters: expand logarithmic scale patch coverage The control logic for the various combinations of linear and logarithmic scales is nasty, and the added path coverage indeed unveiled several bugs. One of these issues, an unjustified limitation on the input value type of `asLogarithm`, convinced me to refactor the conversion functions into free functions, since they effectively ''always span the relation of two scales'' -- writing them as member function introduces an unnecessary asymetry. --- src/lib/par/scale.hpp | 166 ++++++++++++------------- src/lib/par/type-handler.hpp | 4 + tests/lib/par/parameter-scale-test.cpp | 35 +++++- wiki/thinkPad.ichthyo.mm | 77 +++++++++++- 4 files changed, 181 insertions(+), 101 deletions(-) diff --git a/src/lib/par/scale.hpp b/src/lib/par/scale.hpp index 4d6303107..67d05a6bd 100644 --- a/src/lib/par/scale.hpp +++ b/src/lib/par/scale.hpp @@ -90,16 +90,6 @@ namespace par { VAL join (VAL const&, SRC const&, Scale const&) const; VAL join (VAL const&, VAL const&) const; - /* ===== conversion helpers ===== */ - - template - auto asFactor (VAL const&) const; - - template - auto baseScale (Metric targetMetric) const; - - template - auto asLogarithm (Metric targetMetric, VAL const&) const; }; @@ -208,6 +198,80 @@ namespace par { } + namespace { // logarithmic scale conversions... + + /** transform a logarithmic value into an exponential factor */ + template + inline auto + expFactor (Metric metric, VAL const& value) + { + using WorkType = CommonComputeType; + switch (metric) + { + case NAT: return std::exp (WorkType(value)); + case BIN: return std::exp2 (WorkType(value)); + case DEC: return std::pow (WorkType(10), WorkType(value/10.0)); + default: + throw err::Logic{_Fmt{"Attempt to compute exponential " + "from a non-logarithmic value with metric %d"} + % metric}; + break; + } + } + + /** transform this value into a suitable logarithm for the target scale */ + template + inline auto + asLogarithm (Metric targetMetric, VAL const& value) + { + if (isNeg (value)) + throw err::Invalid{_Fmt{"Attempt to take logarithm from %f < 0"} + % value}; + using WorkType = CommonComputeType; + switch (targetMetric) + { + case NAT: return std::log (WorkType(value)); + case BIN: return std::log2 (WorkType(value)); + case DEC: return std::log10 (WorkType(value)) * 10; + default: + throw err::Logic{_Fmt{"Attempt to compute logarithm " + "for a non-logarithmic metric %d"} + % targetMetric}; + break; + } + } + + /** provide the adaptation factor from this logarithmic scale + * to the given other logarithmic scale and target type \a VAL + * @remarks + * - use the logarithm conversion formula logb(x) ≡ logb(k)·logk(x) + * - if target is in decibel, have to apply factor 10; asLogarithm() does that already + * - if we are in decibel, log10 = dB /10; fold that factor in the logarithm of the base, + * by using log(b)·x ≡ log(b^x) and rewrite that as b^x = exp(ln(b)·x) + */ + + template + inline auto + baseScale (Metric targetMetric, Metric srcMetric) + { + using WorkType = CommonComputeType; + if (srcMetric == targetMetric) + return WorkType(1); + + auto srcBase = [&]{switch (srcMetric) + { + case BIN: return WorkType(2); + case NAT: return std::exp (WorkType(1)); + case DEC: return std::exp (std::log (WorkType(10)) / 10); + default: + throw err::Logic{"Attempt to treat non-logarithmic scales as logarithms"}; + break; + }}; + return asLogarithm (targetMetric, srcBase()); + } + }//(End) log helpers + + /** * Join and combine an additional feed value with an anchor value. * @note actual joining operation is picked based on both scales and types involved. @@ -226,7 +290,7 @@ namespace par { if (LIN == metric) { if (feedScale.isLogarithmic()) - assignConverted (res, value * feedScale.template asFactor (srcFeed)); + assignConverted (res, value * expFactor (feedScale.metric, srcFeed)); else if (feedScale.isFactor()) assignConverted (res, JoinT(value) * JoinT(srcFeed)); @@ -237,10 +301,10 @@ namespace par { if (this->isLogarithmic()) { if (feedScale.isLogarithmic()) - assignConverted (res, value + feedScale.template baseScale(metric) * srcFeed); + assignConverted (res, value + baseScale (metric,feedScale.metric) * srcFeed); else if (feedScale.isFactor()) - assignConverted (res, value + feedScale.template asLogarithm (metric, srcFeed)); + assignConverted (res, value + asLogarithm (metric, srcFeed)); else throw err::Invalid {"adding a non-logarithmic data feed on top of a logarithmic base value is pointless."}; } @@ -264,81 +328,5 @@ namespace par { } - - - /** transform a logarithmic value into an exponential factor */ - template - template - inline auto - Scale::asFactor (VAL const& value) const - { - using WorkType = CommonComputeType; - switch (metric) - { - case NAT: return std::exp (WorkType(value)); - case BIN: return std::exp2 (WorkType(value)); - case DEC: return std::pow (WorkType(10), WorkType(value/10.0)); - default: - throw err::Logic{_Fmt{"Attempt to compute exponential " - "from a non-logarithmic value with metric %d"} - % metric}; - break; - } - } - - /** transform this value into a suitable logarithm for the target scale */ - template - template - inline auto - Scale::asLogarithm (Metric targetMetric, VAL const& value) const - { - if (isNeg (value)) - throw err::Invalid{_Fmt{"Attempt to take logarithm from %f < 0"} - % value}; - using WorkType = CommonComputeType; - switch (targetMetric) - { - case NAT: return std::log (WorkType(value)); - case BIN: return std::logb (WorkType(value)); - case DEC: return std::log10 (WorkType(value)) * 10; - default: - throw err::Logic{_Fmt{"Attempt to compute logarithm " - "for a non-logarithmic metric %d"} - % targetMetric}; - break; - } - } - - /** provide the adaptation factor from this logarithmic scale - * to the given other logarithmic scale and target type \a VAL - * @remarks - * - use the logarithm conversion formula logb(x) ≡ logb(k)·logk(x) - * - if target is in decibel, have to apply factor 10; asLogarithm() does that already - * - if we are in decibel, log10 = dB /10; fold that factor in the logarithm of the base, - * by using log(b)·x ≡ log(b^x) and rewrite that as b^x = exp(ln(b)·x) - */ - - template - template - inline auto - Scale::baseScale (Metric targetMetric) const - { - using WorkType = CommonComputeType; - if (metric == targetMetric) - return WorkType(1); - - auto myBase = [&]{switch (metric) - { - case NAT: return std::exp(WorkType(1)); - case BIN: return WorkType(2); - case DEC: return std::exp(std::log(WorkType(10)) / 10); - default: - throw err::Logic{"Attempt to treat non-logarithmic scales as logarithms"}; - break; - }}; - return asLogarithm (targetMetric, myBase()); - } - - }} // namespace lib::par #endif /*LIB_PAR_SCALE_H*/ diff --git a/src/lib/par/type-handler.hpp b/src/lib/par/type-handler.hpp index 1e79acc00..1f51fe721 100644 --- a/src/lib/par/type-handler.hpp +++ b/src/lib/par/type-handler.hpp @@ -158,6 +158,10 @@ _Pragma("GCC diagnostic pop") /** * 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 diff --git a/tests/lib/par/parameter-scale-test.cpp b/tests/lib/par/parameter-scale-test.cpp index bafc620f1..08058a848 100644 --- a/tests/lib/par/parameter-scale-test.cpp +++ b/tests/lib/par/parameter-scale-test.cpp @@ -202,23 +202,46 @@ 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 10dB ≙ factor 10 * lg(10) value divided / 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) + 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 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 + 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 + + 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 fscale.metric = LIN; - CHECK (roughEQ (fscale.join (1, int64_t(-3), feedScale), 1.0f/8 )); // binary log applied as factor to a linear scale + 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 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 + CHECK ( fscale.join (1, int64_t(8), feedScale) == 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) + fscale.metric = DEC; + CHECK (roughEQ (fscale.join (1, int64_t(100),feedScale), 1 + 20)); // feed factor 8 ⟼ now applied as offset in decibels feedScale.neutral = 0; CHECK (not feedScale.isFactor()); diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index c0d22dd37..32f4feb78 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -85973,7 +85973,7 @@ - + @@ -85985,7 +85985,7 @@ The persisted form. The moment a session file records a parameter with a scale, the meaning of its qualifiers is frozen or needs migration. It's worth spending more thought on what a Scale looks like when written out than on what it looks like in memory.
  • - Keeping Scale reducible to data. You mention a possible connection to rules later. A rules system can only reason about a Scale if the Scale is  a small tuple of facts (metric, bounds, cyclic flag, domain-set id, default) with a fixed interpreter, rather than a polymorphic object whose behaviour exists only as C++. That constraint costs you nothing today, since your qualifier-based design already satisfies it. It's just worth adopting as a deliberate rule so that a future conform()  override doing something clever doesn't quietly break it. + Keeping Scale reducible to data. You mention a possible connection to rules later. A rules system can only reason about a Scale if the Scale is a small tuple of facts (metric, bounds, cyclic flag, domain-set id, default) with a fixed interpreter, rather than a polymorphic object whose behaviour exists only as C++. That constraint costs you nothing today, since your qualifier-based design already satisfies it. It's just worth adopting as a deliberate rule so that a future conform() override doing something clever doesn't quietly break it.
  • @@ -87265,14 +87265,48 @@ Prop.1: dist%3 1 2 0 1 2 0
    - + + + + + + + + +

    + ...ich muß die Formeln jedes mal wieder nachschlagen, bzw mir herleiten, weil diese Formeln so eigenartig über Kreuz aufgebaut sind: +

    +
      +
    • + logb(x) ≡ logk(x)/logk(b) +
    • +
    • + logb(x) ≡ logb(k)·logk(x) +
    • +
    + + +
    +
    + + + + + + + + + + + + + +
    - - - +

    ...und dieses Herausdividieren falte ich trickreich in den log-Basis-Konvertierungsfaktor. @@ -87288,6 +87322,27 @@ Prop.1: dist%3 1 2 0 1 2 0 + + + + +

      +
    • + Type-promotion-Bug in der Anwendung der pow()-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 +
    • +
    + + +
    + + +
    @@ -87560,6 +87615,16 @@ Prop.1: dist%3 1 2 0 1 2 0
    + + + + + + + + + +