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

@ -120,12 +120,12 @@
</thead>
<tbody>
<tr class="odd">
<td>Solver meldet <code>INFEASIBLE</code></td>
<td><a href="anhang-glossar.md#gloss:solver" class="glossar-link">Solver</a> meldet <code>INFEASIBLE</code></td>
<td><a href="#c1-infeasible">C1</a></td>
</tr>
<tr class="even">
<td><code>INFEASIBLE</code>, aber keine einzelne Bedingung ist schuld</td>
<td><a href="#c1-infeasible">C1</a>, Deletion Filter</td>
<td><a href="#c1-infeasible">C1</a>, <a href="anhang-glossar.md#gloss:deletion-filter" class="glossar-link">Deletion Filter</a></td>
</tr>
<tr class="odd">
<td>Solver meldet <code>UNBOUNDED</code></td>
@ -152,7 +152,7 @@
<td><a href="#c7-widersprüchliche-solver">C7</a></td>
</tr>
<tr class="odd">
<td>CVXPY wirft <code>DCPError</code></td>
<td><a href="anhang-glossar.md#gloss:cvxpy" class="glossar-link">CVXPY</a> wirft <code>DCPError</code></td>
<td><a href="#c8-dcperror">C8</a></td>
</tr>
<tr class="even">
@ -160,7 +160,7 @@
<td><a href="#c9-importfehler">C9</a></td>
</tr>
<tr class="odd">
<td>Backtest sieht zu gut aus</td>
<td><a href="anhang-glossar.md#gloss:backtest" class="glossar-link">Backtest</a> sieht zu gut aus</td>
<td><a href="#c10-verdächtig-guter-backtest">C10</a></td>
</tr>
<tr class="even">
@ -187,17 +187,17 @@
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>modell.Add(linke_seite <span class="op">&lt;=</span> rechte_seite <span class="op">+</span> schlupf)</span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>strafen.append(<span class="dv">10_000</span> <span class="op">*</span> schlupf)</span></code></pre></div>
<p>Nach dem Lösen zeigen die Schlupfvariablen mit Wert &gt; 0 <strong>präzise, welche Bedingung</strong> wo und um wie viel verletzt werden musste. Das ist zugleich die produktionstaugliche Lösung (Muster B18).</p>
<p><strong>Schritt 4 — Zeitfenster und Erreichbarkeit prüfen.</strong> Bei Scheduling und Routing: Ist jeder Termin überhaupt physisch erreichbar?</p>
<p><strong>Schritt 4 — Zeitfenster und Erreichbarkeit prüfen.</strong> Bei <a href="anhang-glossar.md#gloss:scheduling" class="glossar-link">Scheduling</a> und Routing: Ist jeder Termin überhaupt physisch erreichbar?</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> kunde, (frueh, spaet) <span class="kw">in</span> <span class="bu">enumerate</span>(zeitfenster):</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> distanz[depot][kunde] <span class="op">&lt;=</span> spaet, <span class="ss">f&quot;Kunde </span><span class="sc">{</span>kunde<span class="sc">}</span><span class="ss"> nicht rechtzeitig erreichbar&quot;</span></span></code></pre></div>
<p><strong>Schritt 5 — Rundungs- und Einheitenfehler.</strong> Sind Kapazitäten in Stunden, Verbräuche aber in Minuten? Rechnen CP-SAT-Modelle mit ganzen Zahlen, wo Sie Nachkommastellen brauchen? Ein klassischer Fall: <code>sum(w) == 1</code> mit Gleitkommazahlen — nutzen Sie eine Toleranz oder rechnen Sie in Ganzzahlen (Promille).</p>
<p><strong>Schritt 5 — Rundungs- und Einheitenfehler.</strong> Sind Kapazitäten in Stunden, Verbräuche aber in Minuten? Rechnen <a href="anhang-glossar.md#gloss:cp-sat" class="glossar-link">CP-SAT</a>-Modelle mit ganzen Zahlen, wo Sie Nachkommastellen brauchen? Ein klassischer Fall: <code>sum(w) == 1</code> mit Gleitkommazahlen — nutzen Sie eine Toleranz oder rechnen Sie in Ganzzahlen (Promille).</p>
<h3 id="wenn-die-fünf-schritte-nicht-reichen-den-konflikt-einkreisen">Wenn die fünf Schritte nicht reichen: den Konflikt einkreisen</h3>
<p><strong>Warum Schritt 2 so oft ins Leere läuft.</strong> Bedingungen einzeln abzuschalten funktioniert nur, solange es <em>einen</em> Schuldigen gibt. Überlagern sich zwei unabhängige Widersprüche, bleibt das Modell nach jeder einzelnen Abschaltung unlösbar — die Suche endet mit null Treffern, obwohl beide Widersprüche unverändert im Modell stehen. Genau dann steht man vor einem Modell mit tausenden Restriktionen und einem Statuscode.</p>
<p><strong>Der Deletion Filter</strong> dreht die Frage um. Er fragt nicht „ist <em>diese</em> Bedingung schuld?“, sondern „wird <em>diese</em> Bedingung für den Widerspruch überhaupt gebraucht?“ — und das ist eine Frage, die sich beantworten lässt:</p>
<blockquote>
<p>Nimm eine Bedingung versuchsweise heraus. Bleibt der Rest unlösbar, wurde sie nicht gebraucht: weg damit, endgültig. Wird der Rest lösbar, war sie beteiligt: sie bleibt.</p>
</blockquote>
<p>Nach genau einem Durchlauf über alle <span class="math inline">n</span> Bedingungen ist die übrig gebliebene Menge <strong>unreduzierbar</strong> — entfernt man aus ihr irgendeine Bedingung, ist der Rest lösbar. Das ist ein <em>Irreducible Infeasible Subset</em>, kurz IIS. Kommerzielle Solver bieten das als fertige Funktion an (<code>computeIIS</code> bei Gurobi, der Conflict Refiner bei CPLEX); im Open-Source-Werkzeugkasten dieses Buchs gibt es sie nicht — sie ist aber in zwanzig Zeilen selbst geschrieben, und sie kostet <span class="math inline">n</span> Aufrufe des Solvers statt <span class="math inline">2^n</span> durchprobierter Teilmengen.</p>
<p>Nach genau einem Durchlauf über alle <span class="math inline">n</span> Bedingungen ist die übrig gebliebene Menge <strong>unreduzierbar</strong> — entfernt man aus ihr irgendeine Bedingung, ist der Rest lösbar. Das ist ein <em>Irreducible Infeasible Subset</em>, kurz <a href="anhang-glossar.md#gloss:iis-irreducible-infeasible-subset" class="glossar-link">IIS</a>. Kommerzielle Solver bieten das als fertige Funktion an (<code>computeIIS</code> bei <a href="anhang-glossar.md#gloss:gurobi" class="glossar-link">Gurobi</a>, der Conflict Refiner bei CPLEX); im Open-Source-Werkzeugkasten dieses Buchs gibt es sie nicht — sie ist aber in zwanzig Zeilen selbst geschrieben, und sie kostet <span class="math inline">n</span> Aufrufe des Solvers statt <span class="math inline">2^n</span> durchprobierter Teilmengen.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Konfliktsuche.py</span></span>
@ -530,7 +530,7 @@ Insgesamt 2874 Solveraufrufe fuer die gesamte Diagnose.
====================================================================</code></pre>
<p><strong>Vier Dinge sind daran wichtig.</strong></p>
<ol type="1">
<li><strong>Die Zulässigkeitsprüfung braucht keine Zielfunktion.</strong> <code>linprog</code> bekommt einen Nullvektor als Ziel. Unlösbarkeit hängt nie an der Zielfunktion — wer beim Diagnostizieren die echte Zielfunktion mitschleppt, bezahlt Rechenzeit für nichts.</li>
<li><strong>Die Zulässigkeitsprüfung braucht keine <a href="anhang-glossar.md#gloss:zielfunktion" class="glossar-link">Zielfunktion</a>.</strong> <code>linprog</code> bekommt einen Nullvektor als Ziel. Unlösbarkeit hängt nie an der Zielfunktion — wer beim Diagnostizieren die echte Zielfunktion mitschleppt, bezahlt Rechenzeit für nichts.</li>
<li><strong>Gefiltert werden nur die aufgeführten Bedingungen, nicht die Variablenschranken.</strong> Die Nichtnegativität steht in <code>bounds</code> und bleibt in jedem Teilmodell stehen. Sitzt Ihr Widerspruch in den Schranken (<code>x &gt;= 5</code> als Bound statt als Zeile), findet ihn der Filter nicht — schreiben Sie solche Grenzen dann als benannte Bedingung.</li>
<li><strong>Sprechende Namen sind kein Luxus.</strong> Der Befund ist genau so brauchbar wie die Namen, die darin vorkommen. <code>constraint_47, constraint_112, constraint_9</code> ist kein Befund.</li>
<li><strong>Der Filter liefert <em>einen</em> kleinsten Konflikt, nicht <em>den</em> kleinsten.</strong> Dieses Modell mit vierzehn Bedingungen enthält neun verschiedene minimale Konflikte; welchen man zu sehen bekommt, entscheidet allein die Prüfreihenfolge. Das ist kein Mangel des Verfahrens, sondern eine Eigenschaft des Problems.</li>
@ -539,7 +539,7 @@ Insgesamt 2874 Solveraufrufe fuer die gesamte Diagnose.
<blockquote>
<p><strong>⚠️ Was der Filter kostet</strong> Ein Konflikt kostet <span class="math inline">n</span> Solverläufe. Bei 14 Bedingungen sind das 14 — bei 50 000 Restriktionen mit je zehn Sekunden Lösungszeit wären es knapp sechs Tage. Für große Modelle filtert man deshalb nicht einzeln, sondern <strong>gruppenweise</strong>: erst über Bedingungs<em>blöcke</em> (alle Kapazitäten, alle Verträge, alle Zeitfenster) laufen, dann den Filter nur noch innerhalb des einen Blocks anwenden, der übrig bleibt. Das ist derselbe Algorithmus auf einer gröberen Ebene und senkt die Zahl der Läufe um Größenordnungen.</p>
</blockquote>
<p><strong>Das Verhältnis zur Relaxation.</strong> Der Deletion Filter beantwortet die Frage <em>„was widerspricht sich?“</em>; die Schlupfvariablen aus Schritt 3 beantworten die Frage <em>„was tun wir jetzt?“</em>. Beide gehören in ein Produktionssystem, aber an verschiedene Stellen: der Filter in die Entwicklung und in die Fehlersuche, die Relaxation in den Betrieb (<code>Infeasibility_Diagnose.py</code>, <a href="praxisfallen.html#sec:praxisfallen-die-fuenf-typischen-praxisfallen">Abschnitt 22.3</a>). Ein System, das im Ernstfall den Konflikt <em>benennt</em> und trotzdem einen Notfallplan <em>liefert</em>, hat beides.</p>
<p><strong>Das Verhältnis zur <a href="anhang-glossar.md#gloss:relaxation" class="glossar-link">Relaxation</a>.</strong> Der Deletion Filter beantwortet die Frage <em>„was widerspricht sich?“</em>; die Schlupfvariablen aus Schritt 3 beantworten die Frage <em>„was tun wir jetzt?“</em>. Beide gehören in ein Produktionssystem, aber an verschiedene Stellen: der Filter in die Entwicklung und in die Fehlersuche, die Relaxation in den Betrieb (<code>Infeasibility_Diagnose.py</code>, <a href="praxisfallen.html#sec:praxisfallen-die-fuenf-typischen-praxisfallen">Abschnitt 22.3</a>). Ein System, das im Ernstfall den Konflikt <em>benennt</em> und trotzdem einen Notfallplan <em>liefert</em>, hat beides.</p>
<blockquote>
<p><strong>🎯 Die eigentliche Lehre</strong> <code>INFEASIBLE</code> in Produktion ist ein <strong>Entwurfsfehler</strong>, kein Betriebsfehler. Ein System, das im Ernstfall nur „geht nicht“ sagt, ist wertlos. Bauen Sie Schlupfvariablen von vornherein ein (<a href="praxisfallen.html#sec:praxisfallen-die-fuenf-typischen-praxisfallen">Abschnitt 22.3</a>).</p>
</blockquote>
@ -567,9 +567,9 @@ Insgesamt 2874 Solveraufrufe fuer die gesamte Diagnose.
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>loesen()<span class="op">;</span> t2 <span class="op">=</span> time.perf_counter()</span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Aufbau </span><span class="sc">{</span>t1<span class="op">-</span>t0<span class="sc">:.2f}</span><span class="ss">s | Loesen </span><span class="sc">{</span>t2<span class="op">-</span>t1<span class="sc">:.2f}</span><span class="ss">s&quot;</span>)</span></code></pre></div>
<p>Bei CVXPY und bei Schleifen über Szenarien ist oft der <strong>Aufbau</strong> der Engpass, nicht der Solver (siehe <a href="cvar.html#sec:cvar-implementierung-cvar-portfolio-mit-reibung">Abschnitt 20.6</a>).</p>
<p><strong>2. Big-M zu groß?</strong> Der häufigste Grund für explodierende MILP-Laufzeiten. Setzen Sie <span class="math inline">M</span> auf die kleinste gültige Schranke (<a href="milp.html#sec:milp-modellierungstricks-big-m-und-logische-bedingungen">Abschnitt 6.5</a>).</p>
<p><strong>2. Big-M zu groß?</strong> Der häufigste Grund für explodierende <a href="anhang-glossar.md#gloss:gemischt-ganzzahlige-optimierung-milp" class="glossar-link">MILP</a>-Laufzeiten. Setzen Sie <span class="math inline">M</span> auf die kleinste gültige Schranke (<a href="milp.html#sec:milp-modellierungstricks-big-m-und-logische-bedingungen">Abschnitt 6.5</a>).</p>
<p><strong>3. Symmetrie im Modell?</strong> Identische Maschinen, austauschbare Mitarbeitende, gleichwertige Fahrzeuge — der Solver durchsucht alle Vertauschungen. Abhilfe: Ordnungsbedingungen (Muster B20).</p>
<p><strong>4. Falsche Solverfamilie?</strong> Zuordnungsprobleme mit MILP, Routing mit CP-SAT von Hand nachgebaut, konvexe Probleme mit <code>scipy.optimize.minimize</code> — jeweils Größenordnungen langsamer als das passende Werkzeug (<a href="oekosystem.html#sec:oekosystem-wann-lohnt-sich-welche-ebene">Abschnitt 3.6</a>).</p>
<p><strong>4. Falsche Solverfamilie?</strong> Zuordnungsprobleme mit MILP, Routing mit <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> von Hand nachgebaut, konvexe Probleme mit <code>scipy.optimize.minimize</code> — jeweils Größenordnungen langsamer als das passende Werkzeug (<a href="oekosystem.html#sec:oekosystem-wann-lohnt-sich-welche-ebene">Abschnitt 3.6</a>).</p>
<p><strong>5. Wird vektorisiert?</strong></p>
<div class="sourceCode" id="cb8"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="co"># langsam: # schnell:</span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> s <span class="kw">in</span> <span class="bu">range</span>(S): constraints.append(u <span class="op">&gt;=</span> <span class="op">-</span>(R <span class="op">@</span> w) <span class="op">-</span> gamma)</span>
@ -616,9 +616,9 @@ Insgesamt 2874 Solveraufrufe fuer die gesamte Diagnose.
</thead>
<tbody>
<tr class="odd">
<td>Schlecht konditionierte Kovarianzmatrix</td>
<td>Schlecht konditionierte <a href="anhang-glossar.md#gloss:kovarianzmatrix" class="glossar-link">Kovarianzmatrix</a></td>
<td><code>np.linalg.cond(Sigma)</code></td>
<td>Ledoit-Wolf-Shrinkage (<a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a>)</td>
<td><a href="anhang-glossar.md#gloss:ledoit-wolf-shrinkage" class="glossar-link">Ledoit-Wolf-Shrinkage</a> (<a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a>)</td>
</tr>
<tr class="even">
<td>Zu wenige Beobachtungen</td>
@ -626,7 +626,7 @@ Insgesamt 2874 Solveraufrufe fuer die gesamte Diagnose.
<td>Historie verlängern oder Universum verkleinern</td>
</tr>
<tr class="odd">
<td>Alternativoptima</td>
<td><a href="anhang-glossar.md#gloss:alternativoptima" class="glossar-link">Alternativoptima</a></td>
<td>Zielfunktion bei mehreren Lösungen gleich?</td>
<td>Regularisierung: kleinen <span class="math inline">L_2</span>-Term addieren</td>
</tr>
@ -641,12 +641,12 @@ Insgesamt 2874 Solveraufrufe fuer die gesamte Diagnose.
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Kondition </span><span class="sc">{</span>eig<span class="sc">.</span><span class="bu">max</span>()<span class="op">/</span>eig<span class="sc">.</span><span class="bu">min</span>()<span class="sc">:.0f}</span><span class="ss"> | kleinster EW </span><span class="sc">{</span>eig<span class="sc">.</span><span class="bu">min</span>()<span class="sc">:.2e}</span><span class="ss">&quot;</span>)</span>
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Faustregel: Kondition &gt; 1000 ist bedenklich, &gt; 10000 kritisch</span></span></code></pre></div>
<h3 id="derselbe-fehler-außerhalb-der-finanzwelt">Derselbe Fehler außerhalb der Finanzwelt</h3>
<p>Instabilität ist kein Portfolioproblem, sondern ein Skalierungsproblem — und es trifft jedes Modell, in dem Größen sehr verschiedener Ordnung nebeneinanderstehen. Euro-Beträge (<span class="math inline">10^7</span>) und Tonnen-Angaben (<span class="math inline">10^{-3}</span>) in derselben Matrix erzeugen eine Konditionszahl, die aus einer Datenunsicherheit von 0,1 % eine Lösungsänderung von 100 % machen kann. Die Konditionszahl ist genau die Obergrenze dieses Verstärkungsfaktors.</p>
<p>Instabilität ist kein Portfolioproblem, sondern ein Skalierungsproblem — und es trifft jedes Modell, in dem Größen sehr verschiedener Ordnung nebeneinanderstehen. Euro-Beträge (<span class="math inline">10^7</span>) und Tonnen-Angaben (<span class="math inline">10^{-3}</span>) in derselben Matrix erzeugen eine <a href="anhang-glossar.md#gloss:konditionszahl" class="glossar-link">Konditionszahl</a>, die aus einer Datenunsicherheit von 0,1 % eine Lösungsänderung von 100 % machen kann. Die Konditionszahl ist genau die Obergrenze dieses Verstärkungsfaktors.</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Kondition der Restriktionsmatrix: </span><span class="sc">{</span>np<span class="sc">.</span>linalg<span class="sc">.</span>cond(A)<span class="sc">:.1e}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a><span class="co"># Grobe Peilung: Wertebereiche der Koeffizienten anschauen</span></span>
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;kleinster / groesster Betrag: </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">abs</span>(A[A <span class="op">!=</span> <span class="dv">0</span>])<span class="sc">.</span><span class="bu">min</span>()<span class="sc">:.1e}</span><span class="ss"> &quot;</span></span>
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;/ </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">abs</span>(A)<span class="sc">.</span><span class="bu">max</span>()<span class="sc">:.1e}</span><span class="ss">&quot;</span>)</span></code></pre></div>
<p>Klaffen die Beträge um mehr als drei, vier Größenordnungen auseinander, rechnen Sie in anderen Einheiten (Tausend Euro statt Euro, Kilogramm statt Tonnen) oder skalieren Sie Zeilen und Spalten automatisch. <code>Skalierung_Kondition.py</code> zeigt beides samt Ruiz-Equilibrierung (<a href="fundament.html#sec:fundament-kondition">Abschnitt 2.7</a>).</p>
<p>Klaffen die Beträge um mehr als drei, vier Größenordnungen auseinander, rechnen Sie in anderen Einheiten (Tausend Euro statt Euro, Kilogramm statt Tonnen) oder skalieren Sie Zeilen und Spalten automatisch. <code>Skalierung_Kondition.py</code> zeigt beides samt <a href="anhang-glossar.md#gloss:ruiz-equilibrierung" class="glossar-link">Ruiz-Equilibrierung</a> (<a href="fundament.html#sec:fundament-kondition">Abschnitt 2.7</a>).</p>
<hr />
<h2 id="c6-falsche-dualwerte">C6 — Falsche Dualwerte</h2>
<p><strong>Symptom.</strong> Schattenpreise sind negativ, wo sie positiv sein sollten — oder null bei einer offensichtlich knappen Ressource.</p>
@ -654,16 +654,16 @@ Insgesamt 2874 Solveraufrufe fuer die gesamte Diagnose.
<p><strong>1. Vorzeichenkonvention.</strong> Haben Sie zur Maximierung negiert?</p>
<div class="sourceCode" id="cb13"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a>schattenpreise <span class="op">=</span> <span class="op">-</span>res.ineqlin.marginals <span class="co"># bei linprog nach Negation</span></span></code></pre></div>
<p>Genau das ist die Vorzeichenfalle aus <a href="lp.html#sec:lp-die-vorzeichenfalle-bei-schattenpreisen">Abschnitt 5.7</a>.</p>
<p><strong>2. Komplementärer Schlupf.</strong></p>
<p><strong>2. <a href="anhang-glossar.md#gloss:komplementaerer-schlupf" class="glossar-link">Komplementärer Schlupf</a>.</strong></p>
<div class="sourceCode" id="cb14"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> s, y <span class="kw">in</span> <span class="bu">zip</span>(res.slack, schattenpreise):</span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> <span class="bu">abs</span>(s <span class="op">*</span> y) <span class="op">&lt;</span> <span class="fl">1e-6</span>, <span class="st">&quot;Komplementaerer Schlupf verletzt!&quot;</span></span></code></pre></div>
<p>Eine Ressource mit Reserve <strong>muss</strong> Schattenpreis 0 haben.</p>
<p>Eine Ressource mit Reserve <strong>muss</strong> <a href="anhang-glossar.md#gloss:schattenpreis" class="glossar-link">Schattenpreis</a> 0 haben.</p>
<p><strong>3. Numerische Gegenprobe — der zuverlässigste Test.</strong></p>
<div class="sourceCode" id="cb15"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a>b_plus <span class="op">=</span> b.copy()<span class="op">;</span> b_plus[i] <span class="op">+=</span> <span class="fl">1.0</span></span>
<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a>zuwachs <span class="op">=</span> loese(b_plus) <span class="op">-</span> loese(b)</span>
<span id="cb15-3"><a href="#cb15-3" aria-hidden="true" tabindex="-1"></a><span class="cf">assert</span> <span class="bu">abs</span>(zuwachs <span class="op">-</span> schattenpreise[i]) <span class="op">&lt;</span> <span class="fl">1e-6</span></span></code></pre></div>
<p>Dieser Test ist <strong>unabhängig von jeder Vorzeichenkonvention</strong>. Nutzen Sie ihn.</p>
<p><strong>4. Wenn alle drei Prüfungen bestehen und der Wert trotzdem seltsam ist: Entartung.</strong> Die drei Prüfungen oben setzen voraus, dass es <em>einen</em> richtigen Schattenpreis gibt. Bei einem entarteten Optimum — mehr aktive Nebenbedingungen als Variablen — gibt es den nicht. Der Wert ist dann nicht falsch, sondern <strong>mehrdeutig</strong>: zwei korrekte Solver liefern für dasselbe Optimum verschiedene Dualwerte, und die numerische Gegenprobe aus Schritt 3 schlägt fehl, weil der Zuwachs nach links und nach rechts unterschiedlich ausfällt.</p>
<p><strong>4. Wenn alle drei Prüfungen bestehen und der Wert trotzdem seltsam ist: <a href="anhang-glossar.md#gloss:entartung" class="glossar-link">Entartung</a>.</strong> Die drei Prüfungen oben setzen voraus, dass es <em>einen</em> richtigen Schattenpreis gibt. Bei einem entarteten Optimum — mehr aktive Nebenbedingungen als Variablen — gibt es den nicht. Der Wert ist dann nicht falsch, sondern <strong>mehrdeutig</strong>: zwei korrekte Solver liefern für dasselbe Optimum verschiedene Dualwerte, und die numerische Gegenprobe aus Schritt 3 schlägt fehl, weil der Zuwachs nach links und nach rechts unterschiedlich ausfällt.</p>
<div class="sourceCode" id="cb16"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a>aktiv <span class="op">=</span> <span class="bu">int</span>(np.<span class="bu">sum</span>(res.slack <span class="op">&lt;</span> <span class="fl">1e-9</span>)) <span class="co"># wie viele Bedingungen sind straff?</span></span>
<span id="cb16-2"><a href="#cb16-2" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> aktiv <span class="op">&gt;</span> <span class="bu">len</span>(res.x):</span>
<span id="cb16-3"><a href="#cb16-3" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Entartet: </span><span class="sc">{</span>aktiv<span class="sc">}</span><span class="ss"> aktive Bedingungen bei </span><span class="sc">{</span><span class="bu">len</span>(res.x)<span class="sc">}</span><span class="ss"> Variablen.&quot;</span>)</span>
@ -676,7 +676,7 @@ Insgesamt 2874 Solveraufrufe fuer die gesamte Diagnose.
<hr />
<h2 id="c8-dcperror">C8 — DCPError</h2>
<pre><code>cvxpy.error.DCPError: Problem does not follow DCP rules.</code></pre>
<p><strong>Bedeutung.</strong> CVXPY kann die Konvexität Ihres Ausdrucks nicht beweisen — es lehnt ab, statt ein Ergebnis ohne Garantie zu liefern. <strong>Das ist ein Feature.</strong></p>
<p><strong>Bedeutung.</strong> CVXPY kann die <a href="anhang-glossar.md#gloss:konvexitaet" class="glossar-link">Konvexität</a> Ihres Ausdrucks nicht beweisen — es lehnt ab, statt ein Ergebnis ohne Garantie zu liefern. <strong>Das ist ein Feature.</strong></p>
<table>
<colgroup>
<col style="width: 44%" />
@ -694,7 +694,7 @@ Insgesamt 2874 Solveraufrufe fuer die gesamte Diagnose.
<tr class="odd">
<td><code>x * y</code> (beide Variablen)</td>
<td>bilinear</td>
<td>umformulieren oder MILP/NLP</td>
<td>umformulieren oder MILP/<a href="anhang-glossar.md#gloss:nichtlineare-programmierung-nlp" class="glossar-link">NLP</a></td>
</tr>
<tr class="even">
<td><code>a / x</code></td>
@ -703,7 +703,7 @@ Insgesamt 2874 Solveraufrufe fuer die gesamte Diagnose.
</tr>
<tr class="odd">
<td><code>cp.sqrt(cp.quad_form(w, S))</code></td>
<td><code>cp.sqrt</code> ist <strong>konkav</strong> und verlangt ein konkaves Argument — <code>quad_form</code> ist konvex. Der Ausdruck ist nie DCP, egal wie sauber <span class="math inline">S</span> ist</td>
<td><code>cp.sqrt</code> ist <strong>konkav</strong> und verlangt ein konkaves Argument — <code>quad_form</code> ist konvex. Der Ausdruck ist nie <a href="anhang-glossar.md#gloss:dcp-disciplined-convex-programming" class="glossar-link">DCP</a>, egal wie sauber <span class="math inline">S</span> ist</td>
<td>Cholesky <span class="math inline">S = LL^\top</span>, dann <code>cp.norm2(L.T @ w)</code>: die 2-Norm eines <strong>affinen</strong> Ausdrucks (siehe <code>CVaR_Portfolio.py</code>)</td>
</tr>
<tr class="even">
@ -712,9 +712,9 @@ Insgesamt 2874 Solveraufrufe fuer die gesamte Diagnose.
<td><code>cp.psd_wrap(S)</code>, <strong>wenn</strong> <span class="math inline">S</span> nachweislich PSD ist (z. B. eine Kovarianzmatrix). Behebt nur diese numerische Beanstandung, keine Regelverletzung im Aufbau</td>
</tr>
<tr class="odd">
<td>Quotient (z. B. Sharpe Ratio)</td>
<td>Quotient (z. B. <a href="anhang-glossar.md#gloss:sharpe-ratio" class="glossar-link">Sharpe Ratio</a>)</td>
<td>nicht konvex</td>
<td>Korn-Transformation (<a href="markowitz.html#sec:markowitz-das-mean-variance-modell">Abschnitt 19.4</a>)</td>
<td><a href="anhang-glossar.md#gloss:korn-transformation" class="glossar-link">Korn-Transformation</a> (<a href="markowitz.html#sec:markowitz-das-mean-variance-modell">Abschnitt 19.4</a>)</td>
</tr>
<tr class="even">
<td><code>cp.log(x)</code> maximieren</td>
@ -730,7 +730,7 @@ Insgesamt 2874 Solveraufrufe fuer die gesamte Diagnose.
<hr />
<h2 id="c9-importfehler">C9 — Importfehler</h2>
<pre><code>ImportError: .../highspy/_core...so: undefined symbol: _ZN5Highs13releaseMemoryEv</code></pre>
<p><strong>Ursache.</strong> <code>ortools</code> und <code>highspy</code> bringen beide eine eigene HiGHS-Kopie mit; sie lassen sich auf vielen Systemen <strong>nicht im selben Prozess</strong> importieren (siehe <a href="oekosystem.html#sec:oekosystem-ein-system-vier-programmieransaetze">Abschnitt 3.5</a>). Der Konflikt entsteht auch <strong>indirekt</strong>: <code>cvxpy</code> importiert ein installiertes <code>highspy</code> bei der Solver-Erkennung selbst mit — ein Skript, das erst <code>cvxpy</code> und dann <code>ortools</code> importiert, crasht daher mit derselben Meldung.</p>
<p><strong>Ursache.</strong> <code>ortools</code> und <code>highspy</code> bringen beide eine eigene <a href="anhang-glossar.md#gloss:highs" class="glossar-link">HiGHS</a>-Kopie mit; sie lassen sich auf vielen Systemen <strong>nicht im selben Prozess</strong> importieren (siehe <a href="oekosystem.html#sec:oekosystem-ein-system-vier-programmieransaetze">Abschnitt 3.5</a>). Der Konflikt entsteht auch <strong>indirekt</strong>: <code>cvxpy</code> importiert ein installiertes <code>highspy</code> bei der Solver-Erkennung selbst mit — ein Skript, das erst <code>cvxpy</code> und dann <code>ortools</code> importiert, crasht daher mit derselben Meldung.</p>
<p><strong>Abhilfen (in dieser Reihenfolge):</strong> 1. Nur eines von beiden im selben Skript verwenden. 2. Getrennte Prozesse — ein <code>ProcessPoolExecutor</code> mit <code>mp_context="spawn"</code> und <code>max_tasks_per_child=1</code>, siehe <code>Ein_System_Vier_Ansaetze.py</code>. 3. Auf <code>highspy</code> verzichten: HiGHS ist ohnehin Backend von <code>scipy.optimize.linprog</code> und CVXPY. 4. Getrennte virtuelle Umgebungen.</p>
<hr />
<h2 id="c10-verdächtig-guter-backtest">C10 — Verdächtig guter Backtest</h2>