Glossar-Links: Fachbegriffe im Fließtext automatisch verlinken

Auto-Linking im Build: Glossar-Begriffe werden im Fließtext automatisch
erkannt und als Link auf den Glossar-Eintrag gesetzt — erste Nennung je
Begriff je Datei, dezent gestrichelte Unterstreichung im HTML, hyperref
im PDF. Kein manuelles Markieren bei Textänderungen nötig.

Neue Infrastruktur:
- glossar_eintraege_04.py: glossar_slug() + LINK_TEXTE-Map (automatisch
  generiert aus EINTRAEGEN + Abkürzungs-Extraktion)
- erzeuge_glossar_04.py: jeder Eintrag bekommt {#gloss:<slug>}-Anchor
- build_version_04.py: resolve_glossar() — Auto-Linking + {gl:}-Marke
- web_04/site.css: .glossar-link (dezent, nicht wie Navigationslink)

5 neue Glossar-Einträge (waren als Begriff im Text, aber nicht im Glossar):
Solver, Scheduling, Graphen, Backtest, DCP (Disciplined Convex Programming)

Schutzmechanismen:
- Code-Blöcke, Inline-Code, {idx:}/{ref:}-Marken, Markdown-Links und
  Überschriften werden nicht verlinkt
- Verschachtelung verhindert: 'CP-SAT-Solver' als Ganzes, nicht 'CP'+[
  -'SAT'+[-'Solver'
- 'OR' als 2-Zeichen-Abkürzung nicht auto-verlinkt (False Positive in
  'OR-Tools'); {gl:OR} als manuelle Marke
- Glossar selbst nicht auto-verlinkt (sonst Self-Links)
- reflow_markdown(): ::: als Block-Grenze (sonst kollabiert fenced div)

619 Glossar-Links im Gesamtdokument, 239 Anchor im Glossar, 34 im Vorwort.
This commit is contained in:
dschlueter 2026-09-10 17:40:29 +02:00
commit e8abb66d75
52 changed files with 4316 additions and 1921 deletions

View file

@ -103,7 +103,7 @@
<div class="card card-blick">
<blockquote>
<p><strong>📌 Kapitel auf einen Blick</strong></p>
<p><strong>Worum geht es?</strong> Um die Sprache, in der Optimierungsmodelle aufgeschrieben werden, und um die eine Eigenschaft, die darüber entscheidet, ob ein Problem verlässlich lösbar ist: Konvexität.</p>
<p><strong>Worum geht es?</strong> Um die Sprache, in der Optimierungsmodelle aufgeschrieben werden, und um die eine Eigenschaft, die darüber entscheidet, ob ein Problem verlässlich lösbar ist: <a href="anhang-glossar.md#gloss:konvexitaet" class="glossar-link">Konvexität</a>.</p>
<p><strong>Voraussetzungen:</strong> <a href="einfuehrung.html#kap-einfuehrung">Kapitel 1</a>. Lineare Algebra wird hier von Grund auf wiederholt.</p>
<p><strong>Danach können Sie:</strong> Ein Modell in Matrixform aufschreiben, den zulässigen Bereich geometrisch deuten, beurteilen ob ein Problem konvex ist — und einschätzen, wie viele Stellen Ihres Ergebnisses überhaupt belastbar sind.</p>
<p><strong>Zeitbedarf:</strong> ca. 5 Stunden.</p>
@ -117,7 +117,7 @@
</div>
<hr />
<h2 id="sec:fundament-schnellstart">2.1 In 5 Minuten gelöst</h2>
<p>Dieses Kapitel handelt von Geometrie. Bevor wir Vektoren und Matrizen einführen, sehen Sie die zentrale Einsicht in Aktion — ganz ohne Solver, mit acht Zeilen NumPy.</p>
<p>Dieses Kapitel handelt von Geometrie. Bevor wir Vektoren und Matrizen einführen, sehen Sie die zentrale Einsicht in Aktion — ganz ohne <a href="anhang-glossar.md#gloss:solver" class="glossar-link">Solver</a>, mit acht Zeilen <a href="anhang-glossar.md#gloss:numpy" class="glossar-link">NumPy</a>.</p>
<div class="card card-schnellstart">
<blockquote>
<p><strong>🚀 In 5 Minuten gelöst: Das Optimum sitzt immer in einer Ecke</strong></p>
@ -146,8 +146,8 @@ Ecke (6.0, 0.0) -&gt; Z = 18.00
Ecke (0.0, 0.0) -&gt; Z = 0.00</code></pre>
</blockquote>
</div>
<p><strong>Das Optimum lautet <span class="math inline">(0, 8)</span> mit <span class="math inline">Z = 40</span></strong> — und Sie haben es gefunden, ohne einen einzigen Solver zu starten. Vier Kandidaten statt unendlich vieler Punkte: Das ist der <strong>Fundamentalsatz der linearen Optimierung</strong>, und er ist der Grund, warum sich lineare Programme überhaupt zuverlässig lösen lassen.</p>
<p><strong>Warum funktioniert das?</strong> Weil der zulässige Bereich ein <strong>konvexes Polyeder</strong> ist — ein Vielflächner ohne Diagonalen nach innen — und weil eine lineare Zielfunktion darauf ihren größten Wert immer am Rand annimmt, genauer: in einer Ecke. Beides begründet dieses Kapitel. Der Simplex-Algorithmus in <a href="lp.html#kap-lp">Kapitel 5</a> tut im Kern nichts anderes als das Programm oben, nur klüger: Er probiert nicht alle Ecken durch, sondern läuft gezielt von Ecke zu Ecke bergauf.</p>
<p><strong>Das Optimum lautet <span class="math inline">(0, 8)</span> mit <span class="math inline">Z = 40</span></strong> — und Sie haben es gefunden, ohne einen einzigen Solver zu starten. Vier Kandidaten statt unendlich vieler Punkte: Das ist der <strong><a href="anhang-glossar.md#gloss:fundamentalsatz-der-linearen-optimierung" class="glossar-link">Fundamentalsatz der linearen Optimierung</a></strong>, und er ist der Grund, warum sich lineare Programme überhaupt zuverlässig lösen lassen.</p>
<p><strong>Warum funktioniert das?</strong> Weil der zulässige Bereich ein <strong>konvexes <a href="anhang-glossar.md#gloss:polyeder" class="glossar-link">Polyeder</a></strong> ist — ein Vielflächner ohne Diagonalen nach innen — und weil eine lineare <a href="anhang-glossar.md#gloss:zielfunktion" class="glossar-link">Zielfunktion</a> darauf ihren größten Wert immer am Rand annimmt, genauer: in einer Ecke. Beides begründet dieses Kapitel. Der <a href="anhang-glossar.md#gloss:simplex-algorithmus" class="glossar-link">Simplex-Algorithmus</a> in <a href="lp.html#kap-lp">Kapitel 5</a> tut im Kern nichts anderes als das Programm oben, nur klüger: Er probiert nicht alle Ecken durch, sondern läuft gezielt von Ecke zu Ecke bergauf.</p>
<blockquote>
<p><strong>⚠️ Und wo ist der Haken?</strong> Bei zwei Variablen gibt es 6 Eckenkandidaten, bei 50 Variablen und 50 Bedingungen sind es <span class="math inline">\binom{100}{50} \approx 10^{29}</span>. Der Fundamentalsatz sagt uns <em>wo</em> wir suchen müssen — nicht, dass die Suche leicht wird.</p>
</blockquote>
@ -160,7 +160,7 @@ Ecke (0.0, 0.0) -&gt; Z = 0.00</code></pre>
<li>… den Fundamentalsatz der linearen Optimierung anwenden, um Kandidaten für das Optimum zu finden, ohne alles durchzuprobieren.</li>
<li>… prüfen, ob eine Menge bzw. eine Funktion konvex ist, und begründen, warum das für die Lösbarkeit entscheidend ist.</li>
<li>… einen zweidimensionalen Lösungsraum mit <code>matplotlib</code> zeichnen und daraus die optimale Ecke ablesen.</li>
<li>… die <strong>Konditionszahl</strong> <span class="math inline">\kappa(\mathbf{A})</span> berechnen, ihren Wert deuten und ein schlecht skaliertes Modell mit Ruiz-Equilibrierung wieder rechenbar machen.</li>
<li>… die <strong><a href="anhang-glossar.md#gloss:konditionszahl" class="glossar-link">Konditionszahl</a></strong> <span class="math inline">\kappa(\mathbf{A})</span> berechnen, ihren Wert deuten und ein schlecht skaliertes Modell mit <a href="anhang-glossar.md#gloss:ruiz-equilibrierung" class="glossar-link">Ruiz-Equilibrierung</a> wieder rechenbar machen.</li>
<li>… erklären, warum ein Solver-Ergebnis von <code>0.99999998</code> niemals mit <code>int()</code> in eine ganze Zahl verwandelt werden darf.</li>
</ol>
<hr />
@ -169,7 +169,7 @@ Ecke (0.0, 0.0) -&gt; Z = 0.00</code></pre>
<p>Die lineare Algebra löst dieses Problem, indem sie <strong>viele gleichartige Zahlen zu einem Objekt bündelt</strong>. Das ist derselbe Gedanke wie eine Liste in Python: Statt <code>preis_1</code>, <code>preis_2</code>, …, <code>preis_1000</code> schreibt man <code>preise</code> und arbeitet mit dem Ganzen.</p>
<h3 id="die-bausteine">Die Bausteine</h3>
<p>Sei <span class="math inline">n</span> die Anzahl der Entscheidungsvariablen und <span class="math inline">m</span> die Anzahl der Nebenbedingungen.</p>
<p><strong>Entscheidungsvektor</strong> — was wir festlegen:</p>
<p><strong><a href="anhang-glossar.md#gloss:entscheidungsvektor" class="glossar-link">Entscheidungsvektor</a></strong> — was wir festlegen:</p>
<p><span class="math display">
\mathbf{x} = \begin{pmatrix} x_1 \\ x_2 \\ \vdots \\ x_n \end{pmatrix} \in \mathbb{R}^n
</span></p>
@ -177,7 +177,7 @@ Ecke (0.0, 0.0) -&gt; Z = 0.00</code></pre>
<p><span class="math display">
\mathbf{c} = \begin{pmatrix} c_1 \\ c_2 \\ \vdots \\ c_n \end{pmatrix} \in \mathbb{R}^n
</span></p>
<p><strong>Zielfunktion als Skalarprodukt:</strong></p>
<p><strong>Zielfunktion als <a href="anhang-glossar.md#gloss:skalarprodukt" class="glossar-link">Skalarprodukt</a>:</strong></p>
<p><span class="math display">
f(\mathbf{x}) = \mathbf{c}^\top \mathbf{x} = \sum_{j=1}^n c_j x_j
</span></p>
@ -185,7 +185,7 @@ f(\mathbf{x}) = \mathbf{c}^\top \mathbf{x} = \sum_{j=1}^n c_j x_j
<p><strong>📐 Formel-Lesehilfe</strong> * <span class="math inline">\mathbf{c}^\top</span> — der Ertragsvektor, „umgekippt“ zu einer Zeile. * <span class="math inline">\mathbf{c}^\top \mathbf{x}</span><strong>Skalarprodukt</strong>: Multipliziere jedes <span class="math inline">c_j</span> mit dem zugehörigen <span class="math inline">x_j</span> und addiere alles. Das Ergebnis ist <strong>eine einzige Zahl</strong>. * <span class="math inline">\sum_{j=1}^n</span> — „addiere für <span class="math inline">j = 1</span> bis <span class="math inline">j = n</span>“.</p>
<p><strong>Ohne Formel gesagt:</strong> „Nimm von jedem Produkt die hergestellte Menge mal den Gewinn pro Stück und zähle alles zusammen.“ Im Bot-Beispiel: <span class="math inline">\mathbf{c} = (150, 250)^\top</span>, <span class="math inline">\mathbf{x} = (7, 5)^\top</span>, also <span class="math inline">\mathbf{c}^\top\mathbf{x} = 150 \cdot 7 + 250 \cdot 5 = 2300</span>.</p>
</blockquote>
<p><strong>Technologiematrix</strong> — wer verbraucht wie viel wovon:</p>
<p><strong><a href="anhang-glossar.md#gloss:technologiematrix" class="glossar-link">Technologiematrix</a></strong> — wer verbraucht wie viel wovon:</p>
<p><span class="math display">
\mathbf{A} = \begin{pmatrix}
a_{11} &amp; a_{12} &amp; \cdots &amp; a_{1n} \\
@ -198,7 +198,7 @@ a_{m1} &amp; a_{m2} &amp; \cdots &amp; a_{mn}
<p><strong>📐 Formel-Lesehilfe</strong> <strong>Jede Zeile ist eine Ressource, jede Spalte eine Variable.</strong> Der Eintrag <span class="math inline">a_{ij}</span> beantwortet: „Wie viel von Ressource <span class="math inline">i</span> verbraucht eine Einheit von Variable <span class="math inline">j</span>?“</p>
<p>Im Bot-Beispiel: <span class="math display">\mathbf{A} = \begin{pmatrix} 2 &amp; 5 \\ 4 &amp; 6 \\ 1 &amp; 0 \end{pmatrix} \begin{matrix} \leftarrow \text{vCPU} \\ \leftarrow \text{RAM} \\ \leftarrow \text{Liquidität} \end{matrix}</span> Die erste Zeile <span class="math inline">(2, 5)</span> heißt: Ein A-Bot braucht 2 vCPUs, ein B-Bot braucht 5. Die dritte Zeile <span class="math inline">(1, 0)</span> heißt: Die Liquiditätsgrenze zählt nur A-Bots, B-Bots gar nicht.</p>
</blockquote>
<p><strong>Kapazitätsvektor (rechte Seite)</strong>:</p>
<p><strong><a href="anhang-glossar.md#gloss:kapazitaetsvektor" class="glossar-link">Kapazitätsvektor</a> (rechte Seite)</strong>:</p>
<p><span class="math display">
\mathbf{b} = \begin{pmatrix} b_1 \\ b_2 \\ \vdots \\ b_m \end{pmatrix} \in \mathbb{R}^m
\qquad \text{im Beispiel } \begin{pmatrix} 40 \\ 60 \\ 8 \end{pmatrix}
@ -436,7 +436,7 @@ f(\theta\mathbf{x} + (1-\theta)\mathbf{y}) \le \theta f(\mathbf{x}) + (1-\theta)
</thead>
<tbody>
<tr class="odd">
<td>Lineares Programm (LP)</td>
<td>Lineares Programm (<a href="anhang-glossar.md#gloss:lineare-programmierung-lp" class="glossar-link">LP</a>)</td>
<td><strong>ja</strong></td>
<td>Globales Optimum, Sekunden bis Minuten selbst bei <span class="math inline">10^6</span> Variablen</td>
</tr>
@ -446,17 +446,17 @@ f(\theta\mathbf{x} + (1-\theta)\mathbf{y}) \le \theta f(\mathbf{x}) + (1-\theta)
<td>Globales Optimum, effizient (<a href="qp-nlp.html#kap-qp-nlp">Kapitel 11</a>, <a href="markowitz.html#kap-markowitz">Kapitel 19</a>)</td>
</tr>
<tr class="odd">
<td>Ganzzahlige Variablen (MILP)</td>
<td>Ganzzahlige Variablen (<a href="anhang-glossar.md#gloss:gemischt-ganzzahlige-optimierung-milp" class="glossar-link">MILP</a>)</td>
<td><strong>nein</strong></td>
<td>Der zulässige Bereich ist ein Punktgitter, keine zusammenhängende Menge — NP-schwer (<a href="milp.html#kap-milp">Kapitel 6</a>)</td>
<td>Der zulässige Bereich ist ein Punktgitter, keine zusammenhängende Menge — <a href="anhang-glossar.md#gloss:np-schwer" class="glossar-link">NP-schwer</a> (<a href="milp.html#kap-milp">Kapitel 6</a>)</td>
</tr>
<tr class="even">
<td>Quadratisches Programm mit indefinitem <span class="math inline">\mathbf{P}</span></td>
<td><strong>nein</strong></td>
<td>Nur lokale Optima; eine ungültige Kovarianzmatrix ist ein typischer Auslöser (siehe <a href="qp-nlp.html#sec:qp-nlp-nichtlineare-optimierung-mit-scipy-optimize-minimize">Abschnitt 11.5</a>)</td>
<td>Nur lokale Optima; eine ungültige <a href="anhang-glossar.md#gloss:kovarianzmatrix" class="glossar-link">Kovarianzmatrix</a> ist ein typischer Auslöser (siehe <a href="qp-nlp.html#sec:qp-nlp-nichtlineare-optimierung-mit-scipy-optimize-minimize">Abschnitt 11.5</a>)</td>
</tr>
<tr class="odd">
<td>Allgemeines NLP mit beliebigen Funktionen</td>
<td>Allgemeines <a href="anhang-glossar.md#gloss:nichtlineare-programmierung-nlp" class="glossar-link">NLP</a> mit beliebigen Funktionen</td>
<td>meist <strong>nein</strong></td>
<td><code>scipy.optimize</code> liefert nur ein lokales Optimum, abhängig vom Startpunkt</td>
</tr>
@ -890,7 +890,7 @@ Preis der Ganzzahligkeit: 75.00 EUR (3.16 %)
<p>mit Diagonalmatrizen <span class="math inline">\mathbf{D}_r</span> (Zeilen) und <span class="math inline">\mathbf{D}_c</span> (Spalten). Das <strong>Ruiz-Verfahren</strong> bestimmt diese Faktoren iterativ: In jedem Durchlauf wird jede Zeile durch die Wurzel ihres größten Betrags geteilt, danach jede Spalte. Nach wenigen Durchläufen liegen alle Zeilen- und Spaltenmaxima bei 1.</p>
<p>Wirtschaftlich passiert dabei nichts: Eine Zeilenskalierung heißt „diese Nebenbedingung in einer anderen Einheit messen“, eine Spaltenskalierung „diese Variable in einer anderen Einheit messen“. Die Lösung rechnet man mit <span class="math inline">\mathbf{x} = \mathbf{D}_c\,\tilde{\mathbf{x}}</span> zurück.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Jeder ernsthafte Solver skaliert intern selbst — HiGHS, Gurobi, CP-SAT alle. Verlassen Sie sich trotzdem nicht darauf: Die interne Skalierung repariert die Rechnung, nicht die Modellierung. Wenn Ihre Koeffizienten zwölf Größenordnungen überspannen, sagt Ihnen das etwas über Ihr Modell, nicht über den Solver.</p>
<p><strong>🎯 Merksatz</strong> Jeder ernsthafte Solver skaliert intern selbst — <a href="anhang-glossar.md#gloss:highs" class="glossar-link">HiGHS</a>, <a href="anhang-glossar.md#gloss:gurobi" class="glossar-link">Gurobi</a>, <a href="anhang-glossar.md#gloss:cp-sat" class="glossar-link">CP-SAT</a> alle. Verlassen Sie sich trotzdem nicht darauf: Die interne Skalierung repariert die Rechnung, nicht die Modellierung. Wenn Ihre Koeffizienten zwölf Größenordnungen überspannen, sagt Ihnen das etwas über Ihr Modell, nicht über den Solver.</p>
</blockquote>
<h3 id="das-experiment">Das Experiment</h3>
<p>Das folgende Programm führt beides vor — die Fehlerverstärkung und ihre Behebung — und schließt mit der praktischen Konsequenz, die Sie in <a href="milp.html#kap-milp">Kapitel 6</a> wieder brauchen werden: warum eine Binärvariable mit dem Wert <code>0.99999998</code> niemals mit <code>int()</code> gerundet werden darf.</p>
@ -1233,7 +1233,7 @@ und runden Sie Solver-Ergebnisse NIE ohne Toleranzpruefung.
<p><span class="math display">\max\ 3x_1 + 5x_2 \quad \text{u. d. N.}\quad \underbrace{\begin{pmatrix} 1 &amp; 2 \\ 3 &amp; 1 \end{pmatrix}}_{\mathbf{A}} \begin{pmatrix} x_1 \\ x_2\end{pmatrix} \le \begin{pmatrix} 8 \\ 12 \end{pmatrix}</span></p>
<p>Beim Abtippen aus der Tabelle liest er sie spaltenweise statt zeilenweise ein:</p>
<div class="sourceCode" id="cb11"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linprog</span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> [linprog](anhang<span class="op">-</span>glossar.md<span class="co">#gloss:linprog){.glossar-link}</span></span>
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a>c <span class="op">=</span> np.array([<span class="fl">3.0</span>, <span class="fl">5.0</span>])</span>
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a>A <span class="op">=</span> np.array([[<span class="fl">1.0</span>, <span class="fl">3.0</span>], <span class="co"># spaltenweise abgetippt statt zeilenweise</span></span>