From 17c95a9b18e93a7930e66aa9d95b7cf0fb410aa7 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Thu, 10 Sep 2026 23:51:45 +0200 Subject: [PATCH] Parameters: in-depth analysis regarding Scale parameters Notably I considered how and to what extent we will need to represent ''nominal'' or ''ordinal scales''... In fact we should represent them, yet as cascaded delegate, since that is a relevant topic, yet not the primary use case -- and a topic with very deep complexity in itself. --- src/lib/par/scale.hpp | 22 +- wiki/thinkPad.ichthyo.mm | 636 +++++++++++++++++++++++++++++++-------- 2 files changed, 529 insertions(+), 129 deletions(-) diff --git a/src/lib/par/scale.hpp b/src/lib/par/scale.hpp index bad098f2f..bb219c76e 100644 --- a/src/lib/par/scale.hpp +++ b/src/lib/par/scale.hpp @@ -30,22 +30,42 @@ #include "lib/par/domain.hpp" +#include + namespace lib { namespace par { + using std::optional; + enum Metric {LIN ///< linear ordinary numeric scale + ,BIN ///< binary logarithm + ,NAT ///< natural logarithm + ,DEC ///< decibels (1/10 of 10-based log) + ,NOM ///< value encodes a nominal or ordinal scale + }; + /** * Interface: describe the properties of a value scale to use for parameters, * including range limits and metric (linear, logarithmic). The core operation * is to conform a value to this specific scale. * @todo not clear if we want a virtual interface here....? */ + template class Scale { public: - virtual ~Scale(); ///< this is an interface + Metric metric{LIN}; + /////////////////////////////OOO Quantiser? + optional minVal{}; + optional maxVal{}; + optional minUse{}; + optional maxUse{}; + optional neutral{}; + optional defaultVal{}; + /////////////////////////////OOO »Sentinels« and nominal scales? + bool zyclic{false}; }; diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 9af6f9350..5e3529027 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -82325,9 +82325,7 @@ - - - +

Expanded list of levels of measurement @@ -82436,17 +82434,14 @@ - - + - - - +

alle diese Skalen-Arten sind auf die Perspektive einer Messung / Beobachtung und wissenschaftlichen Deskription festgelegt; diesen Ansatz halte ich für zu eng — einen entscheidenden Hinweis gibt mir hier die Existenz Verktor-wertiger Funktionen ⟹ generell können wir mehr als nur ein »zahlenwert-artiges« Objekt haben ⟹ ich weite sofort auf die dahinter liegende Domain-Ontology aus. Jeder Ansatz einer universellen Klassifikation aller Parameter-Typen ist von Vornherein zum Scheitern verurteilt, denn man muß immer etwas über die dahinterliegende Bedeutung wissen... @@ -82455,8 +82450,7 @@ Was wir hier höchstens erzielen können ist eine Standard-Codierung strukturierter Daten, deren Interpretation dann Hilfestellung benötigt durch Domain-spezifische Erweiterungen.

- -
+
@@ -84317,7 +84311,7 @@ - + @@ -85333,41 +85327,32 @@ - - - +

Und zwar weil ich im Sinne flexibler Domain-Ontologien denke ⟶ wir brauchen »strukturierte Typen« (algebraische Datentypen) sowie diskrete symbolische Skalen, Summen- und Produkttypen. Die erheblichen Komplexitäten entstehen erwartungsgemäß dadurch, daß man das System dieser Skalen nicht a priori festlegen kann (denn dann hätte man sich implizit auf eine Domain-Ontologie festgelegt). Die Konsequenz ist, daß wir Skalen- und Strukturdefinitionen dynamisch, zur Laufzeit hinzufügen bzw. auf Projekt-Ebene festlegen müssen....

- -
+ - - - +

...das heißt, eine ebenfalls durch strukturierte Daten vorgegebene Konfiguration möglicher Strukturen, einschließlich der Typ-IDs.

- -
+
- - - +

...letztlich werden diese strukturierten Parameter und Daten immer in Form eines algebraischen Datentyps darstellbar sein, wobei allerdings die »Bedeutung« aus dem Schema mit herangezogen werden muß; genauer gesagt, die Semantik kann nur innerhalb einer im Hintergrund vorausgesetzten Domain-Ontologie formal definiert werden und dann in eine Codierung übersetzt — insofern die Domain-Ontologien offen bleiben müssen, sehe ich nur den Weg, bestimmte Arten von Anfragen jeweils in die Implementierung auf Basis einer Domain-Ontologie zu delegieren (konkret: in ein Plug-in, welches auf der Domain-Ontology aufbaut). Die insgesamt möglichen Arten von Anfragen, zusammen mit der jeweiligen Codierungs-Regel, ergibt zusammen das effektive Schema, welches formale Transformationen und manipulationen der Daten ermöglicht

- -
+
@@ -85378,29 +85363,23 @@ - - - +

Die Domain selber gibt den Basis-Datentyp vor; innerhalb dessen gibt es einen minimalen und maximalen Wert

- -
+
- - - +

Im Grunde geht es hier um die Unterscheidung ob »linear«, »logarithmisch« oder »diskret«

- -
+
@@ -85416,40 +85395,31 @@ - - - +

wenn man dieses Muster streng nach den Regeln modelliert, dann wäre das ein algebraischer Datentyp mit einem Spezialfall-ENUM (Nominalskala), plus dem eigentlichen Zahlenwert, der aber nur bei bestimmten Nominal-Markern überhaupt betrachtet werden darf — also auch noch das Verhalten eines Summentyps

- -
+
- - - +

die prinzipiell-korrekte Modellierung wäre aber eine Verfehlung der Aufgabe

- -
+ - - - +

Denn letztlich geht es darum, ein Zahlenwert-artiges, einfaches, unstrukturietes Objekt weiterzugeben und zu manipulieren — und zwar hinreichend effizient für den Rahmen von High-Performance-Code (heißt: nicht in der innersten Schleife, aber direkt daneben). Weder ist die Programmiersprache dafür gerüstet, algebraische Datentypen in diesem Kontext zu verarbeiten, noch sind die entsprechenden Mechanismen performant genug, um mit dem einfachen Normalfall einer »Zahl« mithalten zu können. Eine Modellierung dieses Musters als algebraischer Datentyp würde als unangemessene Prinzipienreiterei gewertet; das heißt, man muß schon verstehen, worauf es ankommt

- -
+
@@ -85460,26 +85430,36 @@ - + - - - +

Das hier ist wirklich eine partielle thematische Überschneidung; denn Quantisierung ist auch unabhängig von Zeitangaben sehr relevant für Parameter, denn es kann alle möglichen Arten von Grids geben, und auch der Rundungs- bzw. Quantisierungsmechanismus könnte variabel sein. Die eigentliche Design-Frage jedoch ist, ob das auf dieser Ebene dargestellt werden muß, oder ob man das Thema komplett in die Automations-Provision hineinnimmt. Dafür, das Thema hier mit einer generischen Schnittstelle zu bedenken, würde eine Betrachtung sprechen, welche die »Parameter« als eine Codierungs-Basis versteht, deren Bedeutung durch ein Schema und eine dahinter liegende Ontologie festgelegt wird

- -
+
+ + + + + +

+ Beispiel Sound-Fader-Gain: man arbeitet zwischen -80dB und 0dB ... aber alle positivien Werte und negative Werte bis -132dB sind valide und repräsentierbar — somit ist diese Angabe ein Richtwert für das UI, darf aber nicht gelampt werden +

+ +
+ +
+
@@ -85489,69 +85469,54 @@ - + - - - +

zudem: muß sicherstellen daß jede zuweisende Operation  diese Methode anwendet

- -
+ - - - +

Der Knackpunkt dabei sind die TypeHandler-Implementierungen, denn diese verwenden zunächst (nur) das lokal implementierte generische preClamp (was die Grenzen der BaseDomain durchsetzt und Bool-werte per Schwelle bildet)

- -
+ - - - +

TypeHandler bezieht sich aber auf einen explizit und separat gegebenen Buffer, und konditioniert diesen nur im Hinblick auf den direkt spezifizierten Basis-Typ — wiewohl hier durchaus eine Unschärfe im Aufbau der Interfaces vorliegt, da ja in der conform() - Operation schon ein impliziter »Typ« angewendet wird, dessen Bedeutung nicht zweifelsfrei klar ist (man könnte geneigt sein, hier schon den vollständigen Parameter-Typ zu vermuten)

- -
+
- - - +

...denn es geht um die Durchsetzung einer Invariante für alle Werte »im« Parameter: sie müssen stets zum Parameter-Typ konform sein

- -
+
- - - +

...also jeweils wenn man sich einen Wert holt, was auch den »pull« der Automationsfunktion einschließt

- -
+
@@ -85561,9 +85526,7 @@ - - - +

...was aber als ein seltener Grenzfall zu betrachten ist, und normalerweise durch die übergeordnete Konfiguration ausgeschlossen werden soll. Für die geläufigen Szenarien gibt es zudem stets eine Regel, nach der konditioniert werden kann... @@ -85583,8 +85546,7 @@ Somit bleiben eigentlich nur die Fälle einer speziellen Domain ohne Defaultwert — denn in diesem Fall gibt es keine übergeordnete Metrik, nach der interpoliert werden könnte, sondern ein Code, der nicht zur Skala gehört, ist ungültig und muß zurückgewiesen werden

- -
+
@@ -85593,24 +85555,19 @@ - - - +

hier muß ggfs. eine Logarithmus / Exponential-Konvertierung stattfinden, möglicherweise auch eine Quantisierung

- -
+ - - - +

Grundsätzlich soll hier im Bereich der Scale nur eine Codierung  gehandhabt werden — es besteht die Gefahr, doch noch „durch die Hintertür“ eine universelle Wert-Ontologie einzuführen. @@ -85624,11 +85581,452 @@ - - + + + + + + + + + + + + + + + +

+ ...das, was er über Lumiera inferrieren kann, sowie das neue »Routing«-Dokument (design/model/Routing). Frage dann nach Möglichkeiten zur Repräsentation einer Nominal / Odinal-Skala... +

+

+ +

+

+ Hinweis: hierzu ist bedeutsam, daß »Claude« und »Claude-Code« zwei getrennte Produkte sind, welche sich offensichtlich keinen gemeinsamen »Memory« teilen (ausnahme könnte sein, was ich über mich selber im Account hinterlegt habe). Das gilt schon allein deshalb, weil man den Memory von Claude-Code eigens als Dateien verwalten kann (und ich habe x-mal die Erfahrung gemacht, daß beide Tools nicht direkt miteinander kommunizieren). Das bedeutet, »Claude« (der Chatbot) hat nicht diese Mindmap mit den Entwürfen gelesen, aber ich gebe ihm das aus der damals gemachten Auswertung resultierende Dokument zum »Routing« als Website; insofern bekomme ich durch diese Konversation nochmal einen neuen Blickwinkel, bzw. Projektion der üblichen Betrachtungsweisen zum Thema +

+ +
+ + + + + +

+ ...eine Mapping-Tabelle, eine Registry um die Scale per ID zu greifen, ein leichtgewichtiges front-End +

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

+ Die Engine bekommt stets nur die Codierung einer Scala zu sehen, und für Transformationen, Manipulationen und Automation bekommt sie einen Funktor, in denen die Scalen-Logik bereits eingelassen wurde. +

+ +
+
+
+ + + + +

+ Gebe nun Claude eine ganz kompakte Skizze meines Prototypen. Biete schon die Tendenz an, sich auf die numerischen Skalen zu fokussieren +

+ +
+ + + + + +

+ ...was ich aber gut finde, weil es zumindest die Konsistenz des Argumentations-Raumes aufzeigt (grade im Lichte des weiteren Kontexts, den ich im ersten Schritt ausgeleuchtet habe) +

+ +
+
+ + + + + + +

+ Separate hard bounds from the control range. These get conflated constantly and they are different things. A hard bound says values outside are illegal, and conform either clamps or errors. A control range says "this is the travel of the fader", and values outside are perfectly legal but unusual. Gain is the obvious case: the useful range is maybe −60 to +12 dB, but +30 dB is not an error. If you have one min/max pair it will silently become whichever of the two you happened to need first. Two pairs, or one pair plus a flag, costs almost nothing today. +

+ +
+
+ + + + +

+ Decide whether the stored value is in the metric's own units or in a canonical unit. This is the one place where your design page makes a claim that the implementation can quietly break. The page says combining is addition in a logarithmic scale and multiplication in a linear one, and that this is why scale belongs to the type. That's only true if the stored representation actually is in dB. If you store canonical linear and treat the metric as a display attribute, the joining operator becomes ambiguous again, and you're back to the thing the page argues against. My suggestion: store in the metric's units, so "the metric determines the join" is literally true rather than an interpretive claim. +

+ +
+
+ + + + + Cyclic interacts with min/max in a way you'll want to be explicit about: for a cyclic scale, min/max define the period rather than a bound, and conform wraps instead of clamping. That's cheap. What isn't cheap, and what you should defer, is cyclic + +

+ interpolation: shortest arc versus explicit winding for an automation curve crossing the seam. That belongs to the automation function, not to Scale. +

+ +
+
+ + + + +

+ Sentinels +

+

+ Worth separating two things that both look like sentinels. +

+

+ Some are genuine domain members. Minus-infinity gain, "off", the extremal end of a fader. These have a definite position in the ordering and definite algebraic behaviour: −∞ dB is the absorbing element for gain combination, 0 dB is the identity. They belong in the domain, and a log-metric numeric scale that can represent its own absorbing element is more useful than one that can't. IEEE gets this part right, and borrowing -inf for exactly this is reasonable. +

+

+ Others are not values at all. "Unset", "indeterminate", "multiple values selected in the GUI", "bypassed". These have no position in the ordering, no join behaviour, and every arithmetic operation on them is a bug. Putting them in-band is how you get NaN's contagion problem, plus a per-type encoding story (NaN payloads for float, INT_MIN for int, nothing sensible for bool). You have four 64-bit slots and only need one for the value. A small out-of-band state word in the container is uniform across every implementation type, costs you nothing, and keeps comparison honest. +

+

+ Then make conform the single choke point that recognises and resolves both kinds. Everything downstream gets to assume it's looking at a real value. +

+ +
+ + +
+ + + + +

+ If you want a rule for where to stop, it's this: build now only what you couldn't add later without changing code you can't reach. Behind your vtable, everything is reachable. Two things are not. +

+
    +
  • + 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. +
  • +
+ +
+ + + + + +

+ Diese zwei Askpekte waren es wert, diese ganze Konversation zu führen. Dadurch, daß ich den Fokus, rückwärts gehend auf "Overengineering" gelegt habe, und Claude da nach einer Grenze gefragt habe, sind diese beiden Aspekte hervorgetreten (sie überraschen mich nicht, aber in der Weise, wie ich in das ganze Design-Feld reingegangen bin, habe ich diese zwei Punkte nicht klar gesehen) +

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

+ Er argumentiert genauso, wie ich auch gedacht hatte (und Claude kann hier definitiv nicht meine Überlegungen kennen, denn die Mindmap habe ich nur in Claude-Code auslesen lassen): es sind höchstens ein paar hundert bis tausend. +

+ +
+ + + + +

+ es zeigt mir, daß meine Überlegungen sich hier im Rahmen einer allgemeinen Plausibilität des Faches bewegen, und nicht aus meinem eigenen Wunschdenken stammen +

+ +
+ + +
+
+ + + + +

+ Damit meint Claude, entweder eine gezielte Clean-up-Operation, oder die aktuelle Session schließen und aus dem Log neu öffnen. Damit kann man sich das ganze Thema "GC" sparen. +

+ +
+ +
+ + + + +

+ Claude meint, wenn Lumiera bereits ein System für interned Strings hätte (er hat auch irgendwie lib::Symbol entdeckt), dann bestünde die Gefahr, diese Funktionalität zu duplizieren... +

+

+ Ich habe da Zweifel bezüglich dieser Einschätzung: inwiefern überlappt sich das überhaupt? Wenn ich hier etwas »interne«, dann eine ganze Mapping-Tabelle; nach meinem Verständniss kommt einzelnen Enumeratoren eben grade keine eigenständige symbolische Bedeutung zu (das literal gleiche Symbol kann in verschiedenen Skalen auftreten, und dort unterschiedliche Bedeutung haben). Der reine Aspekt der Deduplikation erscheint mir demgegenüber sekundär — und wenn man dann später eben doch bestimmte Skalen aufräumt und verwirft,  dann stünde dem eine Registrierung als Symbol-String sogar im Wege +

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

+ ⟹ Parameter integrieren lediglich die Behandlung von Nominal/Ordinalskalen +

+ +
+
+
+ + + + + + +

+ ...also die gemeinhin problematische Struktur, daß eine Reihe von Kategorien an unabhängigen Stellen im System bekannt ist und jeweils nach Erkennen eines Markers verzweigt wird — wodurch die encapsulation gebrochen wird, und eine verteilte Logik entsteht +

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + +
    +
  • + sitzt im Implementierungs-Objekt innnerhalb der Parameters +
  • +
  • + die Scale greift in die Implementierung gewisser API-Methoden ein — join() und conform() +
  • +
  • + dort gibt es einen IDRecord, der eine Scale-ID enthält +
  • +
  • + per Scale-ID wird aus einer Registry der Scale-Descriptor geholt +
  • +
+ +
+
+ + + + +

+ Render-Worker verwenden bereits bekannte IDs; Builder-Lauf entdeckt neue Scales und belegt damit neue IDs, nach dem Builder-Lauf erfolgt ein commit + swap des Models ⟹ hier kann man release / acquire - barrier unterstellen +

+ +
+
+ + + + +

+ Diese Frage nutze ich als Challenge, um das Argument in Gang zu setzen; Claude greift das wie erwartet auf +

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

+ The structure that removes the problem +

+

+ Fixed top-level index, chunked storage, chunk pointers published + atomically. The top-level array never reallocates, so the reader's + address computation touches nothing a writer mutates. +

+
+
+
+
+
+class ScaleRegistry
+  {
+    static constexpr size_t CHUNK = 256;
+    static constexpr size_t MAX   = 256;          // ≙ 65536 scales
+
+    struct Chunk { ScaleDescriptor slot[CHUNK]; };
+
+    std::array, MAX> chunk_{};
+    std::mutex                           writeGuard_;   // writers only
+    size_t                               next_ {0};     // guarded
+
+  public:
+    ScaleDescriptor const&
+    operator[] (ScaleID id)  const                      // lock-free, engine path
+      {
+        return chunk_[id / CHUNK].load (std::memory_order_acquire)
+                    ->slot[id % CHUNK];
+      }
+
+    ScaleID
+    define (ScaleDescriptor def)                        // builder path
+      {
+        std::lock_guard lock{writeGuard_};
+        size_t id = next_++;
+        auto& head = chunk_[id / CHUNK];
+        if (not head.load (std::memory_order_relaxed))
+          head.store (new Chunk{}, std::memory_order_release);
+        head.load (std::memory_order_relaxed)->slot[id % CHUNK] = std::move(def);
+        return ScaleID{id};
+      }
+  };
+
+
+
+
+

+ +

+

+ The reader does one acquire load and two integer operations. On x86 the + acquire is free; on ARM it is a ldar. Nothing the writer does can + move a chunk or rewrite the index. +

+

+ Note what this buys beyond correctness: the guarantee becomes local. + You no longer have to reason about the whole build-publish pipeline to + convince yourself the registry is sound. Slot contents still rely on + your model-swap release, which is fine and correct, but the container no + longer participates in that argument at all. Global correctness + arguments that span subsystems are the ones that break silently when + someone later adds a shortcut. +

+

+ If you would rather not have chunks at all, the degenerate version is a + single reserved array with a capacity you decide is enough. Scale + definitions arrive at human speed, so a few thousand is a lot. The + chunked form costs about fifteen extra lines and removes the need to + guess. +

+ +
+ +
+ + + + + + +

+ Das wäre nur dann möglich, wenn das gesamte mögliche Verhalten bereits zur Compiletime feststeht, und sich rein aus den Definitions-Daten im Scale-Descriptor ergibt (ist plausibel, und ist nach meinem derzeitigen Verständnis tatsächlich der Fall — könnte aber in Zukunft doch noch anders sein). Dann soll der Builder bzw. die Konstruktion eines Paramers alles so einrichten, daß die Interpretation der Scale-Spec gar nicht mehr notwendig ist, sondern bereits in die relevanten (virtuellen) Zugriffs-Operationen konsolidiert wurde. Konsequenterweise mußte dann der Lese-Pfad (aus der Engine) überhaupt keinen ID-Index-Zugriff mehr machen, und das ganze Problem würde sich auflösen; man würde dann eine einfache Registry nehmen (meinetwegen eine Deque) und hinter ein Mutex stellen +

+ +
+
+ + + + +

+ ...wobei hier Claude mit 32-Bit argumentiert (ich weiß aber, daß ich die Zahl der Scales stärker limitiere, weil ich dort auch noch die Base-Domain und eine Parameter-ID unterbringen möchte). Das Argument als Solches aber bleibt valide +

+ +
+
+
+
+
+
+
@@ -85917,9 +86315,7 @@ - - - +

Da ist erst einmal das Problem daß hier eine separate Algebra definiert wurde; außerdem wird bei den Zeit-Entitäten auch eine Intention ausgedrückt (Offset, Duration, TimeSpan); und schließlich gilt es die Quantisierung und die Time-Codes zu beachten. Dieser Fall tritt definitiv auf und ist sehr relevant: es könnte einen animierbaren Delay geben, oder andere temporale Qualifikationen (Time-Trail, zeitliche Integration). @@ -85939,8 +86335,7 @@ - - + @@ -85985,42 +86380,33 @@ - - - +

Ganz ausdrücklich sollen hier auch komplex struktuierte Gebilde mit eingeschlossen sein, zu deren Interpretation eine Domain-Ontology mitwirken muß.

- -
+
- - - +

Ich versuche dem Drang zu widerstehen, hier doch noch ein allgemeines Schema anzulegen; vielmehr wird konsequent die Handhabung der Daten per Callback-Funktor in den Nutz-Kontext zurück deflektiert. Der einzige zulässige Mechanismus ist die Bildung von »Prototypen« — wobei dann an die Stelle der Scale (des einfachen Parameters) hier ein Schema  tritt

- -
+
- - - +

das ParamSet-Objekt ist grundsätzlich nur ein front-End-Handle, das auf anderweitig managed data verweist.

- -
+
@@ -86028,9 +86414,7 @@ - - - +

Es ist sinnlos, beide unter einen gemeinsamen Typ zu subsumieren, aber der Schematismus für beide Fälle soll jeweils analog sein... @@ -86047,8 +86431,7 @@ - - + @@ -86063,16 +86446,13 @@ - - - +

Grundsätzlich gibt die jeweilige Verwendung einen Typ vor, und in diesen Typ wird transparent konvertiert; wenn aber »rein zufällig« der geforderte Typ dem nativen Typ entspricht, dann wird der optimierte Code-Pfad getriggert und der Zugriff geht an jedweder Indirektion vorbei

- -
+