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.
This commit is contained in:
Fischlurch 2026-09-21 21:48:15 +02:00
parent ef60681b1d
commit 346d663d51
4 changed files with 181 additions and 101 deletions

View file

@ -90,16 +90,6 @@ namespace par {
VAL join (VAL const&, SRC const&, Scale<SRC> const&) const; VAL join (VAL const&, SRC const&, Scale<SRC> const&) const;
VAL join (VAL const&, VAL const&) const; VAL join (VAL const&, VAL const&) const;
/* ===== conversion helpers ===== */
template<typename TAR>
auto asFactor (VAL const&) const;
template<typename TAR>
auto baseScale (Metric targetMetric) const;
template<typename TAR>
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<typename TAR, typename VAL>
inline auto
expFactor (Metric metric, VAL const& value)
{
using WorkType = CommonComputeType<VAL,TAR>;
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<typename TAR, typename VAL>
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<VAL,TAR>;
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<typename TAR, typename VAL>
inline auto
baseScale (Metric targetMetric, Metric srcMetric)
{
using WorkType = CommonComputeType<VAL,TAR>;
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<WorkType> (targetMetric, srcBase());
}
}//(End) log helpers
/** /**
* Join and combine an additional feed value with an anchor value. * Join and combine an additional feed value with an anchor value.
* @note actual joining operation is picked based on both scales and types involved. * @note actual joining operation is picked based on both scales and types involved.
@ -226,7 +290,7 @@ namespace par {
if (LIN == metric) if (LIN == metric)
{ {
if (feedScale.isLogarithmic()) if (feedScale.isLogarithmic())
assignConverted (res, value * feedScale.template asFactor<VAL> (srcFeed)); assignConverted (res, value * expFactor<VAL> (feedScale.metric, srcFeed));
else else
if (feedScale.isFactor()) if (feedScale.isFactor())
assignConverted (res, JoinT(value) * JoinT(srcFeed)); assignConverted (res, JoinT(value) * JoinT(srcFeed));
@ -237,10 +301,10 @@ namespace par {
if (this->isLogarithmic()) if (this->isLogarithmic())
{ {
if (feedScale.isLogarithmic()) if (feedScale.isLogarithmic())
assignConverted (res, value + feedScale.template baseScale<VAL>(metric) * srcFeed); assignConverted (res, value + baseScale<VAL,SRC> (metric,feedScale.metric) * srcFeed);
else else
if (feedScale.isFactor()) if (feedScale.isFactor())
assignConverted (res, value + feedScale.template asLogarithm<VAL> (metric, srcFeed)); assignConverted (res, value + asLogarithm<VAL> (metric, srcFeed));
else else
throw err::Invalid {"adding a non-logarithmic data feed on top of a logarithmic base value is pointless."}; 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<typename VAL>
template<typename TAR>
inline auto
Scale<VAL>::asFactor (VAL const& value) const
{
using WorkType = CommonComputeType<VAL,TAR>;
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<typename VAL>
template<typename TAR>
inline auto
Scale<VAL>::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<VAL,TAR>;
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<typename VAL>
template<typename TAR>
inline auto
Scale<VAL>::baseScale (Metric targetMetric) const
{
using WorkType = CommonComputeType<VAL,TAR>;
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<WorkType> (targetMetric, myBase());
}
}} // namespace lib::par }} // namespace lib::par
#endif /*LIB_PAR_SCALE_H*/ #endif /*LIB_PAR_SCALE_H*/

View file

@ -158,6 +158,10 @@ _Pragma("GCC diagnostic pop")
/** /**
* the best-precision floating point type to carry out * the best-precision floating point type to carry out
* a numeric operation to combine two values * 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<number V, number W> template<number V, number W>
using CommonComputeType = std::conditional_t<integral<V> and integral<W>, double using CommonComputeType = std::conditional_t<integral<V> and integral<W>, double

View file

@ -202,23 +202,46 @@ namespace test{
// //
Scale<float> fscale = {.metric = NAT}; Scale<float> fscale = {.metric = NAT};
Scale<float> feed_dB = {.metric = DEC}; Scale<float> 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; 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; 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; 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; fscale.metric = BIN;
feedScale.metric = LIN; feedScale.metric = LIN;
feedScale.neutral = 1; feedScale.neutral = 1;
CHECK (feedScale.isFactor()); CHECK (feedScale.isFactor());
CHECK (fscale.isLogarithmic()); 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; feedScale.neutral = 0;
CHECK (not feedScale.isFactor()); CHECK (not feedScale.isFactor());

View file

@ -85973,7 +85973,7 @@
<linktarget COLOR="#c50711" DESTINATION="ID_621869035" ENDARROW="Default" ENDINCLINATION="108;0;" ID="Arrow_ID_191032855" SOURCE="ID_1491788669" STARTARROW="None" STARTINCLINATION="34;-37;"/> <linktarget COLOR="#c50711" DESTINATION="ID_621869035" ENDARROW="Default" ENDINCLINATION="108;0;" ID="Arrow_ID_191032855" SOURCE="ID_1491788669" STARTARROW="None" STARTINCLINATION="34;-37;"/>
<icon BUILTIN="messagebox_warning"/> <icon BUILTIN="messagebox_warning"/>
</node> </node>
<node CREATED="1789003479184" ID="ID_833220665" MODIFIED="1789003552060" TEXT="The two doors that actually close"> <node CREATED="1789003479184" ID="ID_833220665" MODIFIED="1789003479184" TEXT="The two doors that actually close">
<richcontent TYPE="NOTE"><html> <richcontent TYPE="NOTE"><html>
<head/> <head/>
<body> <body>
@ -85985,7 +85985,7 @@
<b>The persisted form</b>. 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. <b>The persisted form</b>. 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.
</li> </li>
<li> <li>
<b>Keeping Scale reducible to data</b>. You mention a possible connection to rules later. A rules system can only reason about a Scale if the Scale <em>is</em>&#160;&#160;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 <code>conform()</code>&#160;&#160;override doing something clever doesn't quietly break it. <b>Keeping Scale reducible to data</b>. You mention a possible connection to rules later. A rules system can only reason about a Scale if the Scale <em>is</em>&#160;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 <code><font color="#673a3a">conform()</font></code>&#160;override doing something clever doesn't quietly break it.
</li> </li>
</ul> </ul>
</body> </body>
@ -87265,14 +87265,48 @@ Prop.1: dist%3 1 2 0 1 2 0
</node> </node>
<node COLOR="#338800" CREATED="1789961742643" ID="ID_949990390" MODIFIED="1789961760832" TEXT="Verbindung verschiedener log-Scales"> <node COLOR="#338800" CREATED="1789961742643" ID="ID_949990390" MODIFIED="1789961760832" TEXT="Verbindung verschiedener log-Scales">
<icon BUILTIN="button_ok"/> <icon BUILTIN="button_ok"/>
<node COLOR="#435e98" CREATED="1789961765581" ID="ID_1536983378" MODIFIED="1789961783683" TEXT="Umwandlung linear &#x27f7; log"/> <node COLOR="#435e98" CREATED="1789961765581" ID="ID_1536983378" MODIFIED="1789961783683" TEXT="Umwandlung linear &#x27f7; log">
<node CREATED="1790021597423" ID="ID_5025438" MODIFIED="1790021610941" TEXT="die Konvertierungs-Funktionen sind strukturell eigenartig">
<icon BUILTIN="messagebox_warning"/>
<node CREATED="1790021612645" ID="ID_1614531537" MODIFIED="1790021624855" TEXT="sie h&#xe4;ngen eigentlich zwischen zwei Scales"/>
<node CREATED="1790021853785" ID="ID_1972131978" LINK="https://math.stackexchange.com/questions/14133/clarify-why-all-logarithms-differ-by-a-constant" MODIFIED="1790022525577" TEXT="sie reflektieren die (verwirrende) Struktur der Logarithmen-Konvertierung">
<richcontent TYPE="NOTE"><html>
<head/>
<body>
<p>
...ich mu&#223; die Formeln jedes mal wieder nachschlagen, bzw mir herleiten, weil diese Formeln so eigenartig <i>&#252;ber Kreuz</i>&#160;aufgebaut sind:
</p>
<ul>
<li>
<font color="#79320b" face="Monospaced">log</font><font color="#79320b" face="Monospaced" size="2"><i>b</i></font><font color="#79320b" face="Monospaced">(x) &#8801; log</font><font color="#79320b" face="Monospaced" size="2"><i>k</i></font><font color="#79320b" face="Monospaced">(x)/log</font><font color="#79320b" face="Monospaced" size="2"><i>k</i></font><font color="#79320b" face="Monospaced">(b)</font>
</li>
<li>
<font color="#79320b" face="Monospaced">log</font><font color="#79320b" face="Monospaced" size="2"><i>b</i></font><font color="#79320b" face="Monospaced">(x) &#8801; log</font><font color="#79320b" face="Monospaced" size="2"><i>b</i></font><font color="#79320b" face="Monospaced">(k)&#183;log</font><font color="#79320b" face="Monospaced" size="2"><i>k</i></font><font color="#79320b" face="Monospaced">(x)</font>
</li>
</ul>
</body>
</html>
</richcontent>
</node>
<node CREATED="1790021537463" ID="ID_1193038289" MODIFIED="1790022831555" TEXT="Funktion Scale&lt;VAL&gt;::asLogarithm ist zudem gar nicht von der Quell-Skala abh&#xe4;ngig"/>
</node>
<node BACKGROUND_COLOR="#c8c0b6" COLOR="#435e98" CREATED="1790022911017" ID="ID_761833274" MODIFIED="1790024210457" TEXT="Code-Struktur / Design-Frage : als freie Funktionen?">
<node CREATED="1790022936425" ID="ID_1681792313" MODIFIED="1790023005221" TEXT="als Member-Funktionen spart man sich 2 Parameter in zwei F&#xe4;llen">
<icon BUILTIN="smiley-oh"/>
</node>
<node CREATED="1790022966402" ID="ID_1905759427" MODIFIED="1790022996702" TEXT="allerdings m&#xfc;ssen sie dann auf das Scale-API, und man ruft sie mit &quot;template&quot;-Qualifier auf">
<icon BUILTIN="smiley-neutral"/>
</node>
<node COLOR="#135457" CREATED="1790023008316" ID="ID_1604048781" MODIFIED="1790024202124" TEXT="also: in den sauren Apfel bei&#xdf;en &#x2014; isbesserso">
<icon BUILTIN="yes"/>
</node>
</node>
</node>
<node COLOR="#435e98" CREATED="1789961784810" ID="ID_245445484" MODIFIED="1789961910375" TEXT="Basis-Konvertierung logarithmischer werte"/> <node COLOR="#435e98" CREATED="1789961784810" ID="ID_245445484" MODIFIED="1789961910375" TEXT="Basis-Konvertierung logarithmischer werte"/>
<node COLOR="#435e98" CREATED="1789961796062" ID="ID_1145409092" MODIFIED="1789961910354" TEXT="spezieller Twist: in Decibels steckt ein Faktor 10"> <node COLOR="#435e98" CREATED="1789961796062" ID="ID_1145409092" MODIFIED="1789961910354" TEXT="spezieller Twist: in Decibels steckt ein Faktor 10">
<node CREATED="1789961817086" ID="ID_909916918" MODIFIED="1789961906542" TEXT="wenn als Input verwendet, mu&#xdf; der rausdividiert werden"> <node CREATED="1789961817086" ID="ID_909916918" MODIFIED="1789961906542" TEXT="wenn als Input verwendet, mu&#xdf; der rausdividiert werden">
<richcontent TYPE="NOTE"><html> <richcontent TYPE="NOTE"><html>
<head> <head/>
</head>
<body> <body>
<p> <p>
...und dieses Herausdividieren falte ich trickreich in den log-Basis-Konvertierungsfaktor. ...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
</node> </node>
<node COLOR="#435e98" CREATED="1789961912393" ID="ID_1778190029" MODIFIED="1789961925432" TEXT="linearer Offset auf Logarithmus wird verweigert"/> <node COLOR="#435e98" CREATED="1789961912393" ID="ID_1778190029" MODIFIED="1789961925432" TEXT="linearer Offset auf Logarithmus wird verweigert"/>
</node> </node>
<node COLOR="#5928b5" CREATED="1789961961491" ID="ID_219252546" MODIFIED="1790021532727" TEXT="und ja: wer testet, der findet">
<richcontent TYPE="NOTE"><html>
<head/>
<body>
<ul>
<li>
Type-promotion-Bug in der Anwendung der pow()-Funktion
</li>
<li>
logb() liefert nur den exponenten, log2() den 2-er-Logarithmus
</li>
<li>
das <i>Wiederverwenden</i>&#160;der asLogarithm()-Funktion macht deutlich, da&#223;&#160;diese einen unn&#246;tigen Typ-Constraint auf den VAL-Parameter der Ausgans-Scale erzwingt
</li>
</ul>
</body>
</html>
</richcontent>
<arrowlink COLOR="#3f3ba8" DESTINATION="ID_1207373600" ENDARROW="Default" ENDINCLINATION="534;0;" ID="Arrow_ID_625720992" STARTARROW="None" STARTINCLINATION="16;533;"/>
<font ITALIC="true" NAME="SansSerif" SIZE="12"/>
</node>
</node> </node>
</node> </node>
</node> </node>
@ -87560,6 +87615,16 @@ Prop.1: dist%3 1 2 0 1 2 0
</node> </node>
<node BACKGROUND_COLOR="#eef0c5" COLOR="#990000" CREATED="1789675604211" ID="ID_201071963" MODIFIED="1789676070358" TEXT="verify_valueJoining"> <node BACKGROUND_COLOR="#eef0c5" COLOR="#990000" CREATED="1789675604211" ID="ID_201071963" MODIFIED="1789676070358" TEXT="verify_valueJoining">
<icon BUILTIN="pencil"/> <icon BUILTIN="pencil"/>
<node COLOR="#338800" CREATED="1790015115678" ID="ID_1510723485" MODIFIED="1790015142255" TEXT="Schritt-1 : teste Typ-Konvertierung und Zusammenspiel mit conform()">
<icon BUILTIN="button_ok"/>
</node>
<node COLOR="#338800" CREATED="1790015143816" ID="ID_1173177816" MODIFIED="1790015164942" TEXT="Schritt-2 : kl&#xe4;re und repariere das noch bestehende signed/unsigned-Problem">
<icon BUILTIN="button_ok"/>
</node>
<node BACKGROUND_COLOR="#fdfdcf" COLOR="#ff0000" CREATED="1790015165677" ID="ID_1207373600" MODIFIED="1790021319393" TEXT="Schritt-2 : Pfadabdeckung der Logarithmus-Skalen-Behandlung">
<linktarget COLOR="#3f3ba8" DESTINATION="ID_1207373600" ENDARROW="Default" ENDINCLINATION="534;0;" ID="Arrow_ID_625720992" SOURCE="ID_219252546" STARTARROW="None" STARTINCLINATION="16;533;"/>
<icon BUILTIN="flag-pink"/>
</node>
</node> </node>
</node> </node>
</node> </node>