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

26
OR_HTML_04/milp.html generated
View file

@ -104,8 +104,8 @@
<blockquote>
<p><strong>📌 Kapitel auf einen Blick</strong></p>
<p><strong>Worum geht es?</strong> Um Entscheidungen, die nicht teilbar sind: ganze Maschinen, Ja/Nein, „höchstens fünf davon“. Wir zeigen mit einem konkreten Gegenbeispiel, warum Runden <strong>falsch</strong> ist, und lernen die Modellierungstricks, mit denen sich logische Regeln in lineare Ungleichungen übersetzen lassen.</p>
<p><strong>Voraussetzungen:</strong> <a href="lp.html#kap-lp">Kapitel 5</a> (LP, Relaxation, Schranken).</p>
<p><strong>Danach können Sie:</strong> Fixkosten, Entweder-Oder, Wenn-Dann und Kardinalitätsgrenzen modellieren, ein MILP mit CP-SAT oder HiGHS lösen, den MIP-Gap als Zusage an den Auftraggeber lesen — und ein Ergebnis erkennen, das nur deshalb gut aussieht, weil das Big-M zu groß gewählt war.</p>
<p><strong>Voraussetzungen:</strong> <a href="lp.html#kap-lp">Kapitel 5</a> (<a href="anhang-glossar.md#gloss:lineare-programmierung-lp" class="glossar-link">LP</a>, <a href="anhang-glossar.md#gloss:relaxation" class="glossar-link">Relaxation</a>, Schranken).</p>
<p><strong>Danach können Sie:</strong> Fixkosten, Entweder-Oder, Wenn-Dann und Kardinalitätsgrenzen modellieren, ein <a href="anhang-glossar.md#gloss:gemischt-ganzzahlige-optimierung-milp" class="glossar-link">MILP</a> mit <a href="anhang-glossar.md#gloss:cp-sat" class="glossar-link">CP-SAT</a> oder <a href="anhang-glossar.md#gloss:highs" class="glossar-link">HiGHS</a> lösen, den <a href="anhang-glossar.md#gloss:mip-gap" class="glossar-link">MIP-Gap</a> als Zusage an den Auftraggeber lesen — und ein Ergebnis erkennen, das nur deshalb gut aussieht, weil das Big-M zu groß gewählt war.</p>
<p><strong>Zeitbedarf:</strong> ca. 6,5 Stunden.</p>
<p><strong>Programme:</strong><br />
<code>Runden_Gegenbeispiel.py</code><br />
@ -167,7 +167,7 @@
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a>nb <span class="op">=</span> LinearConstraint(A, [<span class="dv">100</span>, <span class="op">-</span>np.inf, <span class="op">-</span>np.inf, <span class="op">-</span>np.inf], [<span class="dv">100</span>, <span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">0</span>])</span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a>grenzen <span class="op">=</span> Bounds([<span class="dv">0</span>] <span class="op">*</span> <span class="dv">6</span>, <span class="bu">list</span>(kap) <span class="op">+</span> [<span class="dv">1</span>, <span class="dv">1</span>, <span class="dv">1</span>])</span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> name, ganzzahlig <span class="kw">in</span> [(<span class="st">&quot;LP-Relaxation&quot;</span>, [<span class="dv">0</span>] <span class="op">*</span> <span class="dv">6</span>), (<span class="st">&quot;MILP&quot;</span>, [<span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">1</span>, <span class="dv">1</span>, <span class="dv">1</span>])]:</span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> name, ganzzahlig <span class="kw">in</span> [(<span class="st">&quot;[LP-Relaxation](anhang-glossar.md#gloss:lp-relaxation){.glossar-link}&quot;</span>, [<span class="dv">0</span>] <span class="op">*</span> <span class="dv">6</span>), (<span class="st">&quot;MILP&quot;</span>, [<span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">1</span>, <span class="dv">1</span>, <span class="dv">1</span>])]:</span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a> r <span class="op">=</span> milp(c<span class="op">=</span>np.concatenate([var, fix]), constraints<span class="op">=</span>[nb],</span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a> integrality<span class="op">=</span>ganzzahlig, bounds<span class="op">=</span>grenzen)</span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">:14}</span><span class="ss"> </span><span class="sc">{</span>r<span class="sc">.</span>fun<span class="sc">:7.2f}</span><span class="ss"> EUR Mengen </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">round</span>(r.x[:<span class="dv">3</span>], <span class="dv">1</span>)<span class="sc">}</span><span class="ss"> &quot;</span></span>
@ -209,7 +209,7 @@ MILP 882.00 EUR Mengen [10. 0. 90.] Schalter [ 1. -0. 1. ]
<blockquote>
<p><strong>🎯 Merksatz</strong> Die LP-Relaxation ist keine ungefähre Antwort, die man nur noch glattziehen muss. Sie ist eine Antwort auf eine <strong>andere Frage</strong> — nämlich die, bei der man Lager auch zu 14,3 % eröffnen darf. Ihr Wert liegt nicht in ihrer Lösung, sondern in ihrer <strong>Schranke</strong>: Weniger als 724,14 € kann die richtige Antwort nicht kosten.</p>
</blockquote>
<p>Genau diese Schranke ist der Hebel, mit dem Branch-and-Bound arbeitet. Wie, zeigt der Abschnitt <a href="#sec:milp-branch-and-bound">Abschnitt 6.4</a>.</p>
<p>Genau diese Schranke ist der Hebel, mit dem <a href="anhang-glossar.md#gloss:branch-and-bound" class="glossar-link">Branch-and-Bound</a> arbeitet. Wie, zeigt der Abschnitt <a href="#sec:milp-branch-and-bound">Abschnitt 6.4</a>.</p>
<hr />
<h2 id="sec:milp-lernziele">6.2 Lernziele</h2>
<p>Nach diesem Kapitel können Sie …</p>
@ -220,8 +220,8 @@ MILP 882.00 EUR Mengen [10. 0. 90.] Schalter [ 1. -0. 1. ]
<li>… begründen, warum <span class="math inline">M</span> so klein wie möglich gewählt werden muss.</li>
<li>… ein Portfolioproblem mit Ordergebühren und Höchstzahl an Positionen lösen.</li>
<li><strong>Incumbent, Schranke und MIP-Gap</strong> unterscheiden und aus einem abgebrochenen Solverlauf eine belastbare Aussage ableiten.</li>
<li>… alle Solver-Statusfälle explizit behandeln, statt <code>OPTIMAL</code> vorauszusetzen.</li>
<li>… einen <strong>Warm-Start</strong> setzen — und messen, ob er auf Ihrer Problemklasse überhaupt etwas bringt.</li>
<li>… alle <a href="anhang-glossar.md#gloss:solver" class="glossar-link">Solver</a>-Statusfälle explizit behandeln, statt <code>OPTIMAL</code> vorauszusetzen.</li>
<li>… einen <strong><a href="anhang-glossar.md#gloss:warm-start" class="glossar-link">Warm-Start</a></strong> setzen — und messen, ob er auf Ihrer Problemklasse überhaupt etwas bringt.</li>
</ol>
<hr />
<h2 id="sec:milp-warum-runden-fundamental-scheitert">6.3 Warum Runden fundamental scheitert</h2>
@ -230,7 +230,7 @@ MILP 882.00 EUR Mengen [10. 0. 90.] Schalter [ 1. -0. 1. ]
<p>Der verlockende Gedanke lautet: <em>„Wir lösen das Problem kontinuierlich und runden.“</em> Eine verbreitete, aber unbelegte Behauptung dazu lautet, man verliere dadurch „oft 2050 % des Gewinns“. Rechnen wir es nach, statt es zu behaupten.</p>
<figure>
<img src="bilder_04/kap_milp_runden.svg" alt="Abb. 6.1: Links die Handrechnung von unten als Bild: Die Zielfunktion läuft parallel zur Restriktion, also ist die ganze Kante optimal. Aufrunden auf (1;1) führt aus dem zulässigen Bereich heraus, Abrunden auf (0;0) auf den Wert null. Rechts die Messung aus Runden_Gegenbeispiel.py über je 200 Zufallsinstanzen. Erzeugt von bilder_04/erzeuge_runden.py." />
<figcaption aria-hidden="true">Abb. 6.1: Links die Handrechnung von unten als Bild: Die Zielfunktion läuft parallel zur Restriktion, also ist die ganze Kante optimal. Aufrunden auf <span class="math inline">(1;1)</span> führt aus dem zulässigen Bereich heraus, Abrunden auf <span class="math inline">(0;0)</span> auf den Wert null. Rechts die Messung aus <code>Runden_Gegenbeispiel.py</code> über je 200 Zufallsinstanzen. Erzeugt von <code>bilder_04/erzeuge_runden.py</code>.</figcaption>
<figcaption aria-hidden="true">Abb. 6.1: Links die Handrechnung von unten als Bild: Die <a href="anhang-glossar.md#gloss:zielfunktion" class="glossar-link">Zielfunktion</a> läuft parallel zur Restriktion, also ist die ganze Kante optimal. Aufrunden auf <span class="math inline">(1;1)</span> führt aus dem zulässigen Bereich heraus, Abrunden auf <span class="math inline">(0;0)</span> auf den Wert null. Rechts die Messung aus <code>Runden_Gegenbeispiel.py</code> über je 200 Zufallsinstanzen. Erzeugt von <code>bilder_04/erzeuge_runden.py</code>.</figcaption>
</figure>
<blockquote>
<p><strong>✏️ Handrechnung 6.1: Ein Gegenbeispiel, das Sie im Kopf prüfen können</strong></p>
@ -366,7 +366,7 @@ Loesung eine Nebenbedingung verletzt. &#39;Verlust&#39; = Abstand zum exakten Op
</blockquote>
<hr />
<h2 id="sec:milp-branch-and-bound">6.4 Branch-and-Bound</h2>
<p>MILP-Probleme sind <strong>NP-schwer</strong>. Der Standardansatz moderner Solver (HiGHS, SCIP, Gurobi) heißt <strong>Branch-and-Cut</strong> — Branch-and-Bound plus Schnittebenen.</p>
<p>MILP-Probleme sind <strong><a href="anhang-glossar.md#gloss:np-schwer" class="glossar-link">NP-schwer</a></strong>. Der Standardansatz moderner Solver (HiGHS, <a href="anhang-glossar.md#gloss:scip" class="glossar-link">SCIP</a>, <a href="anhang-glossar.md#gloss:gurobi" class="glossar-link">Gurobi</a>) heißt <strong><a href="anhang-glossar.md#gloss:branch-and-cut" class="glossar-link">Branch-and-Cut</a></strong> — Branch-and-Bound plus Schnittebenen.</p>
<figure>
<img src="bilder_04/kap_milp_suchbaum.svg" alt="Abb. 6.2: Der Suchbaum der Handrechnung weiter unten — nicht gezeichnet, sondern gerechnet: Das Skript löst die LP-Relaxationen und kappt nach denselben drei Regeln, die oben stehen. Ast B wird verworfen, weil sein Wert 18 den Incumbent 20 aus Ast A nicht schlägt. Erzeugt von bilder_04/erzeuge_branch_and_bound.py." />
<figcaption aria-hidden="true">Abb. 6.2: Der Suchbaum der Handrechnung weiter unten — nicht gezeichnet, sondern gerechnet: Das Skript löst die LP-Relaxationen und kappt nach denselben drei Regeln, die oben stehen. Ast B wird verworfen, weil sein Wert 18 den Incumbent 20 aus Ast A nicht schlägt. Erzeugt von <code>bilder_04/erzeuge_branch_and_bound.py</code>.</figcaption>
@ -576,7 +576,7 @@ Genau hier wuerde Branch-and-Bound verzweigen:
<p><strong>Szenario.</strong> Ein Investor verteilt <span class="math inline">100\,000\,\text{€}</span> auf sechs Anlageklassen.</p>
<ul>
<li>Jede Transaktion kostet <strong>50 € Fixgebühr</strong>, unabhängig vom Betrag.</li>
<li>Höchstens <strong>3 Positionen</strong> gleichzeitig (<em>Kardinalitätsbeschränkung</em>).</li>
<li>Höchstens <strong>3 Positionen</strong> gleichzeitig (<em><a href="anhang-glossar.md#gloss:kardinalitaetsbeschraenkung" class="glossar-link">Kardinalitätsbeschränkung</a></em>).</li>
<li>Wird eine Position eröffnet, dann mit <strong>mindestens 10 000 €</strong> (Mindestordergröße).</li>
<li>Höchstens <strong>40 000 €</strong> pro Position.</li>
</ul>
@ -872,7 +872,7 @@ Abnahmepruefung: alle Nebenbedingungen geprueft und eingehalten.
</tr>
<tr class="even">
<td><code>status = status_von_scipy(ergebnis)</code></td>
<td>übersetzt den SciPy-Rückgabewert in die gemeinsame Sprache aus <a href="praxisfallen.html#sec:praxisfallen-or-kern">Abschnitt 22.6</a> — die drei <code>if</code>-Zweige darunter behandeln Modellfehler, Abbruch ohne Lösung und „zulässig, aber unbewiesen“ getrennt</td>
<td>übersetzt den <a href="anhang-glossar.md#gloss:scipy" class="glossar-link">SciPy</a>-Rückgabewert in die gemeinsame Sprache aus <a href="praxisfallen.html#sec:praxisfallen-or-kern">Abschnitt 22.6</a> — die drei <code>if</code>-Zweige darunter behandeln Modellfehler, Abbruch ohne Lösung und „zulässig, aber unbewiesen“ getrennt</td>
</tr>
<tr class="odd">
<td><code>schranke=-ergebnis.mip_dual_bound</code></td>
@ -884,7 +884,7 @@ Abnahmepruefung: alle Nebenbedingungen geprueft und eingehalten.
</tr>
</tbody>
</table>
<p><strong>Warum das Ergebnis wirtschaftlich Sinn ergibt:</strong> Der Solver wählt die drei renditestärksten Anlagen (13 %, 11 %, 8 %) und füllt sie in dieser Reihenfolge bis zur Obergrenze von 40 000 €. Die drittbeste Position (EU-Aktien) erhält nur den Rest von 20 000 € — sie liegt über der Mindestordergröße von 10 000 €, ist also zulässig. Ohne Kardinalitätsgrenze läge alles im Bestwert (13 %); die Restriktionen kosten 15 % des theoretischen Ertrags. Genau diese Zahl braucht man, wenn man mit dem Risikomanagement über die Sinnhaftigkeit einer Regel diskutiert: „Die Obergrenze von 40 % je Position kostet uns 1 950 € pro Jahr — ist uns die Diversifikation das wert?“ Das ist eine beantwortbare Frage; „wir sollten breiter streuen“ ist es nicht.</p>
<p><strong>Warum das Ergebnis wirtschaftlich Sinn ergibt:</strong> Der Solver wählt die drei renditestärksten Anlagen (13 %, 11 %, 8 %) und füllt sie in dieser Reihenfolge bis zur Obergrenze von 40 000 €. Die drittbeste Position (EU-Aktien) erhält nur den Rest von 20 000 € — sie liegt über der Mindestordergröße von 10 000 €, ist also zulässig. Ohne Kardinalitätsgrenze läge alles im Bestwert (13 %); die Restriktionen kosten 15 % des theoretischen Ertrags. Genau diese Zahl braucht man, wenn man mit dem Risikomanagement über die Sinnhaftigkeit einer Regel diskutiert: „Die Obergrenze von 40 % je Position kostet uns 1 950 € pro Jahr — ist uns die <a href="anhang-glossar.md#gloss:diversifikation" class="glossar-link">Diversifikation</a> das wert?“ Das ist eine beantwortbare Frage; „wir sollten breiter streuen“ ist es nicht.</p>
</blockquote>
<hr />
<h2 id="sec:milp-gap">6.8 Wenn der Solver nicht fertig wird: Gap, Zeitlimit und Warm-Start</h2>
@ -1440,7 +1440,7 @@ Instanz Variante Makespan Zeit Faktor
<li><strong><code>if status == OPTIMAL: ... else: return None</code>.</strong> Wirft eine Lösung weg, die 9 % vom Optimum entfernt und damit vollkommen brauchbar ist. Werten Sie den Gap aus, nicht nur den Status.</li>
<li><strong>Zeitlimit ohne Gap-Auswertung.</strong> Ein Limit erzwingt ein Ende, sagt aber nichts über die Qualität. Ohne den Gap wissen Sie nicht, ob Sie 0,1 % oder 60 % danebenliegen.</li>
<li><strong>Warm-Start als Selbstverständlichkeit.</strong> Messen Sie ihn — auf Ihrer Instanzklasse, gegen den kalten Lauf. In der Hälfte der Fälle bringt er nichts.</li>
<li><strong>Einen Hinweis für eine unzulässige Lösung geben.</strong> CP-SAT und HiGHS verwerfen ihn dann stillschweigend. Prüfen Sie Ihre Startlösung vorher auf Zulässigkeit — sonst messen Sie einen Effekt, den es gar nicht gibt.</li>
<li><strong>Einen Hinweis für eine unzulässige Lösung geben.</strong> <a href="anhang-glossar.md#gloss:constraint-programming-cp" class="glossar-link">CP</a>-<a href="anhang-glossar.md#gloss:sat-boolean-satisfiability" class="glossar-link">SAT</a> und HiGHS verwerfen ihn dann stillschweigend. Prüfen Sie Ihre Startlösung vorher auf Zulässigkeit — sonst messen Sie einen Effekt, den es gar nicht gibt.</li>
</ul>
</blockquote>
<blockquote>
@ -1784,7 +1784,7 @@ funktioniert immer.
<li><strong>Runden ist keine Lösung:</strong> Aufrunden ist fast immer unzulässig (98100 % der Fälle), Abrunden verliert im Mittel 1026 %, im Extremfall alles.</li>
<li><strong>Branch-and-Bound</strong> löst LP-Relaxationen, verzweigt an gebrochenen Variablen und kappt Äste anhand von Schranken. Der Preis: NP-Schwere.</li>
<li><strong>Fünf Modellierungsmuster</strong> decken die meisten Praxisfälle ab: Fixkosten, Entweder-Oder, Wenn-Dann, Kardinalität, semikontinuierlich.</li>
<li><strong><span class="math inline">M</span> so klein wie möglich</strong> — nicht nur wegen der Laufzeit. Ein übergroßes <span class="math inline">M</span> kann Binärvariablen bei <span class="math inline">10^{-8}</span> hängen lassen, wo sie dem Solver als 0 gelten: Anlagen produzieren, ohne dass ihre Fixkosten je verbucht werden (<em>Trickle Flow</em>).</li>
<li><strong><span class="math inline">M</span> so klein wie möglich</strong> — nicht nur wegen der Laufzeit. Ein übergroßes <span class="math inline">M</span> kann Binärvariablen bei <span class="math inline">10^{-8}</span> hängen lassen, wo sie dem Solver als 0 gelten: Anlagen produzieren, ohne dass ihre Fixkosten je verbucht werden (<em><a href="anhang-glossar.md#gloss:trickle-flow" class="glossar-link">Trickle Flow</a></em>).</li>
<li><strong><code>OPTIMAL</code> ist nicht der Normalfall.</strong> Werten Sie jeden Status aus und lesen Sie den <strong>MIP-Gap</strong> — er ist eine Garantie („höchstens 2,3 % vom Optimum“), keine Schätzung. Der Beweis der Optimalität kostet meist ein Vielfaches dessen, was das Finden der optimalen Lösung kostet.</li>
<li><strong>Warm-Starts können nichts verfälschen, aber auch nichts versprechen.</strong> Ob ein Startwert hilft, hängt davon ab, ob er besser ist als das, was der Solver in derselben Zeit von allein findet — im Buch gemessen: einmal Faktor 2,2, einmal gar nichts.</li>
<li><strong>Prüfen Sie jede modellierte Regel</strong> nach dem Lösen mit einer <code>assert</code>-Zeile — und rechnen Sie den Zielwert aus der Lösung nach, statt ihm zu glauben.</li>