kleine Änderungen

This commit is contained in:
dschlueter 2026-09-09 07:37:07 +02:00
commit dc088fa214
47 changed files with 183868 additions and 573 deletions

View file

@ -4767,6 +4767,11 @@ f(\theta\mathbf{x} + (1-\theta)\mathbf{y}) \le \theta f(\mathbf{x}) + (1-\theta)
<blockquote>
<p><strong>🎯 Merksatz</strong> Konvex heißt: Wer bergab geht, kommt am tiefsten Punkt an. Nicht-konvex heißt: Wer bergab geht, kommt <em>irgendwo</em> an — und weiß nicht, ob es der tiefste Punkt war.</p>
</blockquote>
<figure>
<img src="bilder_04/kap02_konvexitaet_landschaft.svg" alt="Abb. 2.3: Die Funktion f(x) = x^2 + 3\sin(3x), links eindimensional mit den fünf Startpunkten aus Konvexitaet_Demo.py, rechts als separable Fortsetzung f(x_1,x_2) = f(x_1) + f(x_2) — auf jeder Achse liegt genau die Kurve von links. Erzeugt von bilder_04/erzeuge_3d_konvexitaet.py." />
<figcaption aria-hidden="true">Abb. 2.3: Die Funktion <span class="math inline">f(x) = x^2 + 3\sin(3x)</span>, links eindimensional mit den fünf Startpunkten aus <code>Konvexitaet_Demo.py</code>, rechts als separable Fortsetzung <span class="math inline">f(x_1,x_2) = f(x_1) + f(x_2)</span> — auf jeder Achse liegt genau die Kurve von links. Erzeugt von <code>bilder_04/erzeuge_3d_konvexitaet.py</code>.</figcaption>
</figure>
<p><strong>Was Sie in der Abbildung sehen.</strong> Links sind es fünf Läufe desselben Verfahrens, die in vier verschiedenen Tälern enden — dieselben vier, die Teil 2 von <code>Konvexitaet_Demo.py</code> weiter unten auflistet. In zwei Dimensionen werden aus den Tälern Mulden in einem Gitter, und der Unterschied zwischen Start A und Start B beträgt hier bereits mehr als 24 Einheiten. Keiner der beiden Läufe hat etwas falsch gemacht: Beide enden dort, wo es nicht mehr bergab geht.</p>
<h3 id="was-ist-konvex-was-nicht">Was ist konvex, was nicht?</h3>
<table>
<colgroup>
@ -5129,8 +5134,8 @@ Preis der Ganzzahligkeit: 75.00 EUR (3.16 %)
<p><strong>Wenn meine Eingabedaten leicht ungenau sind — wie ungenau ist dann meine Lösung?</strong></p>
</blockquote>
<figure>
<img src="bilder_04/kap_fundament_kondition.svg" alt="Abb. 2.3: Was \kappa(\mathbf{A}) praktisch bedeutet. Links: Über zwölf Größenordnungen hinweg liegt die gemessene größte Fehlerverstärkung genau auf der theoretischen Schranke — \kappa ist keine grobe Faustregel. Rechts: Was die Ruiz-Equilibrierung an einem Modell mit unverträglichen Einheiten ausrichtet. Erzeugt von bilder_04/erzeuge_kondition.py, gerechnet mit derselben Instanz wie Skalierung_Kondition.py." />
<figcaption aria-hidden="true">Abb. 2.3: Was <span class="math inline">\kappa(\mathbf{A})</span> praktisch bedeutet. Links: Über zwölf Größenordnungen hinweg liegt die gemessene größte Fehlerverstärkung genau auf der theoretischen Schranke — <span class="math inline">\kappa</span> ist keine grobe Faustregel. Rechts: Was die Ruiz-Equilibrierung an einem Modell mit unverträglichen Einheiten ausrichtet. Erzeugt von <code>bilder_04/erzeuge_kondition.py</code>, gerechnet mit derselben Instanz wie <code>Skalierung_Kondition.py</code>.</figcaption>
<img src="bilder_04/kap_fundament_kondition.svg" alt="Abb. 2.4: Was \kappa(\mathbf{A}) praktisch bedeutet. Links: Über zwölf Größenordnungen hinweg liegt die gemessene größte Fehlerverstärkung genau auf der theoretischen Schranke — \kappa ist keine grobe Faustregel. Rechts: Was die Ruiz-Equilibrierung an einem Modell mit unverträglichen Einheiten ausrichtet. Erzeugt von bilder_04/erzeuge_kondition.py, gerechnet mit derselben Instanz wie Skalierung_Kondition.py." />
<figcaption aria-hidden="true">Abb. 2.4: Was <span class="math inline">\kappa(\mathbf{A})</span> praktisch bedeutet. Links: Über zwölf Größenordnungen hinweg liegt die gemessene größte Fehlerverstärkung genau auf der theoretischen Schranke — <span class="math inline">\kappa</span> ist keine grobe Faustregel. Rechts: Was die Ruiz-Equilibrierung an einem Modell mit unverträglichen Einheiten ausrichtet. Erzeugt von <code>bilder_04/erzeuge_kondition.py</code>, gerechnet mit derselben Instanz wie <code>Skalierung_Kondition.py</code>.</figcaption>
</figure>
<p>Die Antwort trägt einen Namen: die <strong>Konditionszahl</strong> <span class="math inline">\kappa(\mathbf{A})</span>. Sie ist der wichtigste Begriff dieses Kapitels für den Produktivbetrieb, weil er erklärt, warum ein mathematisch korrektes Modell trotzdem unbrauchbare Ergebnisse liefern kann — oder sich mit einem sachlich falschen <code>INFEASIBLE</code> verabschiedet.</p>
<h3 id="die-geometrische-vorstellung-zwei-fast-parallele-linien">Die geometrische Vorstellung: zwei fast parallele Linien</h3>
@ -8065,9 +8070,10 @@ Lesart C (weich): loesbar - 66,000 EUR Deckungsbeitrag, 1 Stammauftrag abgelehnt
<h2 id="sec:lp-der-simplex-algorithmus-schritt-fuer-schritt">5.4 Der Simplex-Algorithmus Schritt für Schritt</h2>
<p>Der von George Dantzig 1947 formulierte Simplex-Algorithmus nutzt den Fundamentalsatz aus <a href="#kap-fundament">Kapitel 2</a>: <strong>Das Optimum liegt in einer Ecke.</strong> Statt alle Ecken aufzuzählen, wandert er von Ecke zu Ecke — und zwar immer in Richtung Verbesserung.</p>
<figure>
<img src="bilder_04/kap_lp_simplex_ecken.svg" alt="Abb. 5.1: Derselbe zulässige Bereich, aber der Weg der Handrechnung weiter unten: vom Ursprung über (0; 8) zum Optimum (7,5; 5). Drei der fünf Ecken werden besucht, keine zweimal — das ist der ganze Gewinn gegenüber dem Aufzählen aller Ecken. Erzeugt von bilder_04/erzeuge_polyeder.py." />
<figcaption aria-hidden="true">Abb. 5.1: Derselbe zulässige Bereich, aber der Weg der Handrechnung weiter unten: vom Ursprung über (0; 8) zum Optimum (7,5; 5). Drei der fünf Ecken werden besucht, keine zweimal — das ist der ganze Gewinn gegenüber dem Aufzählen aller Ecken. Erzeugt von <code>bilder_04/erzeuge_polyeder.py</code>.</figcaption>
<img src="bilder_04/kap_lp_simplex_ecken.svg" alt="Abb. 5.1: Derselbe zulässige Bereich, aber der Weg der Handrechnung weiter unten: vom Ursprung über (0; 8) zum Optimum (7,5; 5). Drei der fünf Ecken werden besucht, keine zweimal — das ist der ganze Gewinn gegenüber dem Aufzählen aller Ecken. Punktiert die Niveaulinien der Zielfunktion, der Pfeil ihr Gradient \mathbf{c}. Erzeugt von bilder_04/erzeuge_polyeder.py." />
<figcaption aria-hidden="true">Abb. 5.1: Derselbe zulässige Bereich, aber der Weg der Handrechnung weiter unten: vom Ursprung über (0; 8) zum Optimum (7,5; 5). Drei der fünf Ecken werden besucht, keine zweimal — das ist der ganze Gewinn gegenüber dem Aufzählen aller Ecken. Punktiert die Niveaulinien der Zielfunktion, der Pfeil ihr Gradient <span class="math inline">\mathbf{c}</span>. Erzeugt von <code>bilder_04/erzeuge_polyeder.py</code>.</figcaption>
</figure>
<p><strong>Was Sie in der Abbildung sehen.</strong> Durch jede besuchte Ecke läuft genau eine Niveaulinie — die Linien sind nicht gewählt, sondern die Zielwerte 0 €, 2.000 € und 2.375 € der drei Ecken. Der Simplex schiebt diese Linie in Richtung des Pfeils so weit hinaus, wie das Polyeder es zulässt. Dass er dabei an einer Ecke anhält und nicht mittendrin, ist keine Eigenheit des Verfahrens, sondern der Fundamentalsatz: Weiter geht es nur noch hinaus.</p>
<h3 id="der-ablauf">Der Ablauf</h3>
<ol type="1">
<li><strong>Initialisierung.</strong> Starte an einer bekannten Ecke — meist dem Ursprung <span class="math inline">\mathbf{x} = \mathbf{0}</span>, bei dem alle Schlupfvariablen ihre Kapazität aufnehmen: <span class="math inline">\mathbf{s} = \mathbf{b}</span>. Das ist zulässig, solange <span class="math inline">\mathbf{b} \ge \mathbf{0}</span>.</li>
@ -8551,6 +8557,11 @@ Optimum nach 2 Pivotschritten erreicht.
<p><strong>Satz.</strong> Besitzt das primale Problem eine endliche Optimallösung <span class="math inline">\mathbf{x}^*</span>, so besitzt auch das duale eine Optimallösung <span class="math inline">\mathbf{y}^*</span>, und die Zielwerte sind <strong>exakt gleich</strong>: <span class="math display">\mathbf{c}^\top\mathbf{x}^* = \mathbf{b}^\top\mathbf{y}^*</span></p>
</blockquote>
<p>Prüfen wir das an unserem Beispiel: <span class="math inline">\mathbf{b}^\top\mathbf{y}^* = 40 \cdot 12{,}5 + 60 \cdot 31{,}25 + 8 \cdot 0 = 500 + 1875 + 0 = 2375</span> ✓ — identisch mit <span class="math inline">Z^* = 2375</span>.</p>
<figure>
<img src="bilder_04/kap_lp_dualitaets_bruecke.svg" alt="Abb. 5.3: Beide Probleme geometrisch, gerechnet mit Dualitaet_Nachweis.py. Rechts ist die Ebene y_3 = 0 gezeichnet — zulässig, weil das Marktlimit x_1 \le 8 bei x_1^* = 7{,}5 nicht bindet und deshalb y_3^* = 0 gilt. Erzeugt von bilder_04/erzeuge_dualitaets_bruecke.py." />
<figcaption aria-hidden="true">Abb. 5.3: Beide Probleme geometrisch, gerechnet mit <code>Dualitaet_Nachweis.py</code>. Rechts ist die Ebene <span class="math inline">y_3 = 0</span> gezeichnet — zulässig, weil das Marktlimit <span class="math inline">x_1 \le 8</span> bei <span class="math inline">x_1^* = 7{,}5</span> nicht bindet und deshalb <span class="math inline">y_3^* = 0</span> gilt. Erzeugt von <code>bilder_04/erzeuge_dualitaets_bruecke.py</code>.</figcaption>
</figure>
<p><strong>Was Sie in der Abbildung sehen.</strong> Die beiden Tafeln haben nichts gemeinsam: verschiedene Achsen, verschiedene Einheiten, links ein geschlossenes Polyeder, rechts ein nach oben offener Bereich. Links wird maximiert, rechts minimiert. Und doch steht an beiden Optima dieselbe Zahl — 2.375 €. Genau das behauptet der Satz, und mehr ist an ihm nicht zu sehen.</p>
<p>Das lässt sich auch unabhängig von der Handrechnung nachweisen: Primal- und Dualproblem werden als <strong>zwei getrennte, voneinander unabhängige LPs</strong> aufgestellt und gelöst — wenn der starke Dualitätssatz stimmt, müssen beide denselben Zielwert liefern, ohne dass eines vom anderen „weiß“.</p>
<div class="sourceCode" id="cb52"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb52-1"><a href="#cb52-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb52-2"><a href="#cb52-2" aria-hidden="true" tabindex="-1"></a></span>
@ -8939,8 +8950,8 @@ Pruefung: komplementaerer Schlupf fuer alle Ressourcen erfuellt.
<h2 id="sec:lp-entartung">5.9 Wann Schattenpreise lügen: Entartung und Toleranzen</h2>
<p>Der vorige Abschnitt hat gezeigt, wie wertvoll Schattenpreise sind. Dieser zeigt die zwei Fälle, in denen sie eine Entscheidung <strong>nicht</strong> tragen — und wie man beide erkennt, bevor jemand auf ihrer Grundlage eine Maschine kauft.</p>
<figure>
<img src="bilder_04/kap_lp_entartung.svg" alt="Abb. 5.3: Links laufen drei Geraden durch dieselbe Ecke — eine mehr, als zwei Dimensionen brauchen. Rechts die Folge: Der Schattenpreis ist keine Zahl mehr, sondern eine Spanne. Erzeugt von bilder_04/erzeuge_entartung_polyeder.py." />
<figcaption aria-hidden="true">Abb. 5.3: Links laufen drei Geraden durch dieselbe Ecke — eine mehr, als zwei Dimensionen brauchen. Rechts die Folge: Der Schattenpreis ist keine Zahl mehr, sondern eine Spanne. Erzeugt von <code>bilder_04/erzeuge_entartung_polyeder.py</code>.</figcaption>
<img src="bilder_04/kap_lp_entartung.svg" alt="Abb. 5.4: Links laufen drei Geraden durch dieselbe Ecke — eine mehr, als zwei Dimensionen brauchen. Rechts die Folge: Der Schattenpreis ist keine Zahl mehr, sondern eine Spanne. Erzeugt von bilder_04/erzeuge_entartung_polyeder.py." />
<figcaption aria-hidden="true">Abb. 5.4: Links laufen drei Geraden durch dieselbe Ecke — eine mehr, als zwei Dimensionen brauchen. Rechts die Folge: Der Schattenpreis ist keine Zahl mehr, sondern eine Spanne. Erzeugt von <code>bilder_04/erzeuge_entartung_polyeder.py</code>.</figcaption>
</figure>
<p><strong>Was Sie in der Abbildung sehen.</strong> Jeder Wert innerhalb der farbigen Balken erfüllt die Optimalitätsbedingungen. Zwei Solver dürfen also verschiedene Schattenpreise melden und haben beide recht — die Frage „was ist eine Stunde wert?” hat hier schlicht keine eindeutige Antwort.</p>
<h3 id="fall-1-entartung">Fall 1: Entartung</h3>
@ -9676,6 +9687,11 @@ Loesung eine Nebenbedingung verletzt. &#39;Verlust&#39; = Abstand zum exakten Op
<li>oder die LP-Lösung bereits ganzzahlig ist (dann ist dieser Ast fertig).</li>
</ul></li>
</ol>
<figure>
<img src="bilder_04/kap_milp_bnb_geometrie.svg" alt="Abb. 6.3: Was der erste Verzweigungsschritt des Suchbaums geometrisch bedeutet. Graue Punkte sind die 13 ganzzahligen zulässigen Punkte. Erzeugt von bilder_04/erzeuge_bnb_geometrie.py." />
<figcaption aria-hidden="true">Abb. 6.3: Was der erste Verzweigungsschritt des Suchbaums geometrisch bedeutet. Graue Punkte sind die 13 ganzzahligen zulässigen Punkte. Erzeugt von <code>bilder_04/erzeuge_bnb_geometrie.py</code>.</figcaption>
</figure>
<p><strong>Was Sie in der Abbildung sehen.</strong> Der rote Streifen ist genau das, was Phase 2 wegnimmt — und in ihm liegt kein einziger grauer Punkt. Deshalb ist Verzweigen verlustfrei. Weggenommen wird trotzdem etwas Wertvolles: das LP-Optimum (3; 1,5) selbst. Genau darum sinkt die Schranke von 21 auf 20,67, und genau dieses Sinken ist der Fortschritt, den der Suchbaum Knoten für Knoten protokolliert.</p>
<p>Zusätzlich fügen Solver <strong>Schnittebenen</strong> (<em>cutting planes</em>, z. B. Gomory-Cuts) hinzu: gültige Ungleichungen, die gebrochene Bereiche wegschneiden, ohne einen einzigen zulässigen ganzzahligen Punkt zu entfernen.</p>
<blockquote>
<p><strong>✏️ Handrechnung 6.2: Ein Suchbaum von Hand</strong></p>
@ -9690,8 +9706,8 @@ Loesung eine Nebenbedingung verletzt. &#39;Verlust&#39; = Abstand zum exakten Op
<h2 id="sec:milp-modellierungstricks-big-m-und-logische-bedingungen">6.5 Modellierungstricks: Big-M und logische Bedingungen</h2>
<p>Hier kommt das Handwerkszeug, das MILP so mächtig macht: Man übersetzt Geschäftsregeln in lineare Ungleichungen — mithilfe binärer Hilfsvariablen <span class="math inline">y \in \{0,1\}</span> und einer hinreichend großen Konstante <span class="math inline">M</span>.</p>
<figure>
<img src="bilder_04/kap_milp_big_m.svg" alt="Abb. 6.3: Dieselbe Bedingung x \le M\,y, zwei Zahlen für M. Gezeigt ist die LP-Relaxation — also das, was der Solver sieht, bevor y ganzzahlig wird. Erzeugt von bilder_04/erzeuge_big_m_vergleich.py." />
<figcaption aria-hidden="true">Abb. 6.3: Dieselbe Bedingung <span class="math inline">x \le M\,y</span>, zwei Zahlen für <span class="math inline">M</span>. Gezeigt ist die LP-Relaxation — also das, was der Solver sieht, bevor <span class="math inline">y</span> ganzzahlig wird. Erzeugt von <code>bilder_04/erzeuge_big_m_vergleich.py</code>.</figcaption>
<img src="bilder_04/kap_milp_big_m.svg" alt="Abb. 6.4: Dieselbe Bedingung x \le M\,y, zwei Zahlen für M. Gezeigt ist die LP-Relaxation — also das, was der Solver sieht, bevor y ganzzahlig wird. Erzeugt von bilder_04/erzeuge_big_m_vergleich.py." />
<figcaption aria-hidden="true">Abb. 6.4: Dieselbe Bedingung <span class="math inline">x \le M\,y</span>, zwei Zahlen für <span class="math inline">M</span>. Gezeigt ist die LP-Relaxation — also das, was der Solver sieht, bevor <span class="math inline">y</span> ganzzahlig wird. Erzeugt von <code>bilder_04/erzeuge_big_m_vergleich.py</code>.</figcaption>
</figure>
<p><strong>Was Sie in der Abbildung sehen.</strong> Für <span class="math inline">y \in \{0,1\}</span> sind beide Modelle gleichwertig; die beiden blauen Punkte stimmen überein. Verschieden ist nur, was dazwischen erlaubt ist — und rechts genügt bereits <span class="math inline">y = 0{,}08</span>, um die volle Kapazität freizugeben.</p>
<h3 id="muster-1-fixkosten-aktivierungsschalter">Muster 1 — Fixkosten / Aktivierungsschalter</h3>
@ -11199,6 +11215,11 @@ Fr -&gt; Abel</code></pre>
<p>Aus <span class="math inline">4 \times 4 = 16</span> Kombinationen sind ohne jedes Ausprobieren <strong>9</strong> geworden. Bei tausenden verketteten Bedingungen wirkt dieser Effekt lawinenartig — das ist der Kern der Leistungsfähigkeit von CP.</p>
<p><strong>Und wenn ein Widerspruch auftritt?</strong> Dann lernt der SAT-Kern eine <strong>Sperrklausel</strong> (<em>nogood</em>): „Diese Kombination nie wieder probieren.“ Der Solver springt dann nicht nur einen Schritt zurück, sondern direkt zur Ursache des Konflikts (<em>non-chronological backtracking</em>).</p>
</blockquote>
<figure>
<img src="bilder_04/kap_cpsat_propagation_ablauf.svg" alt="Abb. 7.2: Dieselbe Handrechnung als Bild: grün die Werte, die in der Domain bleiben, rot der Wert, den die jeweilige Runde streicht, grau die schon vorher gestrichenen. Erzeugt von bilder_04/erzeuge_propagation_ablauf.py." />
<figcaption aria-hidden="true">Abb. 7.2: Dieselbe Handrechnung als Bild: grün die Werte, die in der Domain bleiben, rot der Wert, den die jeweilige Runde streicht, grau die schon vorher gestrichenen. Erzeugt von <code>bilder_04/erzeuge_propagation_ablauf.py</code>.</figcaption>
</figure>
<p><strong>Was Sie in der Abbildung sehen.</strong> Jede Runde streicht genau <strong>einen</strong> Wert — und trotzdem sinkt die Zahl der Kombinationen von 16 auf 9, also um gut vierzig Prozent. Das ist der Hebel: Gestrichen wird nicht eine Kombination, sondern ein Wert, und mit ihm alle Kombinationen, in denen er vorkam.</p>
<div class="sourceCode" id="cb77"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb77-1"><a href="#cb77-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb77-2"><a href="#cb77-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb77-3"><a href="#cb77-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Propagation_Demo.py</span></span>
@ -11839,11 +11860,12 @@ Untere Schranke (laengster Auftrag / hoechste Maschinenlast): 10
Erreichter Makespan: 11
==========================================================================</code></pre>
<figure>
<img src="bilder_04/kap06_jobshop_gantt.svg" alt="Abb. 7.2: Job-Shop-Plan als Gantt-Diagramm" />
<figcaption aria-hidden="true">Abb. 7.2: Job-Shop-Plan als Gantt-Diagramm</figcaption>
<img src="bilder_04/kap06_jobshop_gantt.svg" alt="Abb. 7.3: Derselbe Plan als Gantt-Diagramm. Schraffiert die beiden Stillstände zwischen zwei Arbeitsgängen, gestrichelt die untere Schranke. Erzeugt von bilder_04/erzeuge_kap06_gantt.py." />
<figcaption aria-hidden="true">Abb. 7.3: Derselbe Plan als Gantt-Diagramm. Schraffiert die beiden Stillstände zwischen zwei Arbeitsgängen, gestrichelt die untere Schranke. Erzeugt von <code>bilder_04/erzeuge_kap06_gantt.py</code>.</figcaption>
</figure>
<p><em>Eine interaktive Fassung dieser Grafik steht auf den Kapitelseiten der Website bereit.</em></p>
<p><strong>Lesen Sie das Gantt-Diagramm.</strong> Auftrag 1 kommt zuerst an die Fräse (02), Auftrag 0 muss warten und ist dort erst von 2 bis 5 an der Reihe. Auffällig ist die Lücke in der Dreherei bei Zeitpunkt 4: Die Maschine steht still, obwohl Auftrag 0 schon Arbeit hätte — er ist aber bis 5 noch an der Fräse. Dieses <strong>Warten ist unvermeidlich</strong>; es entsteht aus dem Zusammenspiel von Reihenfolge- und Maschinenbedingungen, nicht aus einer schlechten Planung.</p>
<p><strong>Lesen Sie das Gantt-Diagramm.</strong> Auftrag 1 kommt zuerst an die Fräse (02), Auftrag 0 muss warten und ist dort erst von 2 bis 5 an der Reihe. Auffällig sind die beiden schraffierten Lücken. Die in der Dreherei bei Zeitpunkt 4 ist die sprechendere: Die Maschine steht still, obwohl Auftrag 0 schon Arbeit hätte — er ist aber bis 5 noch an der Fräse. Dieses <strong>Warten ist unvermeidlich</strong>; es entsteht aus dem Zusammenspiel von Reihenfolge- und Maschinenbedingungen, nicht aus einer schlechten Planung.</p>
<p>Die gestrichelte Linie bei 10 sagt dasselbe von der anderen Seite. Sie ist die untere Schranke aus dem Programm, und sie stammt von der Dreherei: Deren drei Arbeitsgänge dauern zusammen 2 + 4 + 4 = 10 Zeiteinheiten, die sich nicht überlappen dürfen. Die Dreherei ist im Plan von 0 bis 11 belegt — also genau ihre 10 Einheiten Arbeit plus die eine schraffierte Einheit Stillstand. Der Plan ist somit nicht deshalb elf lang, weil schlecht geplant wurde, sondern weil die Engpassmaschine einmal warten muss. (Die zweite Lücke, in der Lackiererei, kostet nichts: Diese Maschine ist ohnehin nicht der Engpass.)</p>
<blockquote>
<p><strong>⚠️ Mehrere gleich gute Pläne — und was das für Ihre Tests bedeutet</strong></p>
<p>Der kürzeste Makespan beträgt 11, aber es gibt <strong>mehrere verschiedene Pläne</strong>, die ihn erreichen. Welchen CP-SAT findet, hängt normalerweise davon ab, welcher seiner parallelen Suchstränge zuerst fertig wird — dasselbe Programm liefert dann von Lauf zu Lauf einen anderen (gleich guten) Plan.</p>
@ -19878,12 +19900,13 @@ dem CO2-Preis, den das Unternehmen ohnehin ansetzt.
<h2 id="sec:mehrziel-luecke">14.6 Was die gewichtete Summe nicht sieht</h2>
<p>Die Front hat <strong>zehn</strong> Punkte. Ein Raster aus 1 401 Gewichten findet <strong>sechs</strong> davon. Vier Kompromisse sind für kein Gewicht erreichbar — und das ist keine Frage der Rasterweite.</p>
<figure>
<img src="bilder_04/kap_mehrziel_pareto.svg" alt="Abb. 14.1: Pareto-Front: Kosten gegen CO₂. Die gefüllten Punkte liegen auf der unteren konvexen Hülle und sind über Gewichte erreichbar; die vier offenen Kreise sind pareto-optimal, werden aber von keiner Geraden gestützt." />
<figcaption aria-hidden="true">Abb. 14.1: Pareto-Front: Kosten gegen CO₂. Die gefüllten Punkte liegen auf der unteren konvexen Hülle und sind über Gewichte erreichbar; die vier offenen Kreise sind pareto-optimal, werden aber von keiner Geraden gestützt.</figcaption>
<img src="bilder_04/kap_mehrziel_pareto.svg" alt="Abb. 14.1: Pareto-Front: Kosten gegen CO₂. Die gefüllten Punkte liegen auf der unteren konvexen Hülle und sind über Gewichte erreichbar; die vier offenen Kreise sind pareto-optimal, werden aber von keiner Geraden gestützt. Die dünne Gerade ist eine solche Stützgerade, die punktierte die parallele Gerade durch den am weitesten abliegenden der vier Punkte. Erzeugt von bilder_04/erzeuge_pareto_front.py." />
<figcaption aria-hidden="true">Abb. 14.1: Pareto-Front: Kosten gegen CO₂. Die gefüllten Punkte liegen auf der unteren konvexen Hülle und sind über Gewichte erreichbar; die vier offenen Kreise sind pareto-optimal, werden aber von keiner Geraden gestützt. Die dünne Gerade ist eine solche Stützgerade, die punktierte die parallele Gerade durch den am weitesten abliegenden der vier Punkte. Erzeugt von <code>bilder_04/erzeuge_pareto_front.py</code>.</figcaption>
</figure>
<p><em>Eine interaktive Fassung dieser Grafik steht auf den Kapitelseiten der Website bereit.</em></p>
<p><strong>Warum es keine Frage des Rasters ist.</strong><span class="math inline">c(x) + w\,e(x)</span> minimieren” heißt geometrisch: eine Gerade der Steigung <span class="math inline">-1/w</span> von links unten an die Punktwolke schieben und schauen, welchen Punkt sie zuerst berührt. Eine Gerade berührt immer einen <strong>Eckpunkt der unteren konvexen Hülle</strong>. Punkte, die oberhalb der Hülle liegen, werden von keiner Geraden zuerst getroffen — für kein <span class="math inline">w</span>, bei beliebig feiner Abstufung.</p>
<p>Das Programm rechnet diese Hülle in <code>untere_huelle()</code> unabhängig aus und vergleicht: Genau die vier Punkte, die das Gewichtsraster verfehlt, liegen strikt oberhalb. Die Gegenprobe steht in der Ausgabe.</p>
<p><strong>Was Sie in der Abbildung sehen.</strong> Die beiden Geraden haben dieselbe Steigung, also dasselbe Gewicht <span class="math inline">w</span>. Die untere berührt die Hülle in zwei Ecken — eine von beiden meldet der Solver als Optimum. Die obere geht durch einen pareto-optimalen Punkt, der damit für dieses Gewicht chancenlos ist, und zwar nicht knapp: Er liegt eine ganze Stufe darüber. Verschieben Sie die Steigung, verschiebt sich die Berührung von einer Ecke zur nächsten — über den Punkt dazwischen springt sie hinweg. Die waagerechte Linie zeigt den Ausweg, um den es in <a href="#sec:mehrziel-epsilon">Abschnitt 14.7</a> geht: Eine Schranke für den CO₂-Ausstoß statt eines Gewichts findet genau diesen Punkt.</p>
<blockquote>
<p><strong>⚠️ Der Grund liegt in der Ganzzahligkeit</strong></p>
<p>Bei einem reinen LP ist der zulässige Bereich konvex, und die Pareto-Front liegt vollständig auf ihrer eigenen konvexen Hülle — dort findet die gewichtete Summe alles. Sobald Entscheidungen <strong>ganzzahlig</strong> werden (welcher Träger, welches Lager, welche Schicht), zerfällt der Bereich in einzelne Punkte, und zwischen ihnen entstehen Einbuchtungen.</p>
@ -24298,6 +24321,11 @@ CVaR: Portfolio 101.33 vs. Summe 159.24 -&gt; subadditiv (kohaerent)
========================================================================================</code></pre>
<p><strong>Die 6-Sigma-Zeile setzt die Sache ins Verhältnis:</strong> Unter Normalverteilung wäre ein solcher Tag ein Ereignis von einmal in <strong>zwei Millionen Jahren</strong>. Unter der t-Verteilung mit drei Freiheitsgraden — die realen Aktienrenditen deutlich näher kommt — passiert er etwa alle <strong>zwei Jahre</strong>. Der Faktor beträgt fast <strong>eine Million</strong>.</p>
<p>Und Teil 2 zeigt das Grundproblem des VaR an einem Beispiel, das Sie von Hand nachrechnen können: Einzeln meldet er für jede Anleihe einen <em>Gewinn</em> von 2 €, im Portfolio einen <em>Verlust</em> von 98 €. Diversifikation hätte demnach das Risiko um 102 € erhöht. Der CVaR dagegen verhält sich korrekt: 101,33 € im Portfolio gegenüber 159,24 € bei getrennter Betrachtung — die Streuung <strong>senkt</strong> das Risiko, wie es sein muss.</p>
<figure>
<img src="bilder_04/kap_cvar_subadditivitaet.svg" alt="Abb. 20.1: Dieselbe Tabelle als Bild, gerechnet mit denselben Ziehungen wie VaR_CVaR_Demo.py. Erzeugt von bilder_04/erzeuge_var_vs_cvar.py." />
<figcaption aria-hidden="true">Abb. 20.1: Dieselbe Tabelle als Bild, gerechnet mit denselben Ziehungen wie <code>VaR_CVaR_Demo.py</code>. Erzeugt von <code>bilder_04/erzeuge_var_vs_cvar.py</code>.</figcaption>
</figure>
<p><strong>Was Sie in der Abbildung sehen.</strong> Links wechselt der Balken über null das Vorzeichen: Aus zwei gemeldeten Gewinnen wird im Portfolio ein Verlust. Rechts passiert nichts dergleichen — der Portfoliobalken ist kürzer als der Summenbalken, und genau das heißt Subadditivität. Der Grund für den Sprung links ist keine Rechenungenauigkeit, sondern die Bauart des Maßes: Eine Schwelle bei 95 % sieht ein Ereignis mit 4 % Wahrscheinlichkeit nicht — bis zwei davon zusammenkommen.</p>
<blockquote>
<p><strong>⚠️ Eine Falle bei der CVaR-Berechnung</strong> Der naheliegende Weg — „Mittelwert aller Verluste <span class="math inline">\ge</span> VaR“ — ist <strong>falsch</strong>, sobald die Verteilung <strong>Atome</strong> hat (also einzelne Werte mit positiver Wahrscheinlichkeit, wie hier die zwei möglichen Ausgänge). Der VaR liegt dann selbst auf einem Atom, und der Vergleich <code>&gt;=</code> erfasst zu viel Wahrscheinlichkeitsmasse. Im Beispiel oben liefert dieser naive Schätzer für Anleihe A den Wert 2,08 statt der korrekten 79,59 — ein Fehler um Faktor 38.</p>
<p>Die Rockafellar-Uryasev-Formel behandelt Atome von sich aus korrekt. Verwenden Sie sie auch dann, wenn Sie „nur schnell“ einen CVaR ausrechnen wollen.</p>
@ -24305,8 +24333,8 @@ CVaR: Portfolio 101.33 vs. Summe 159.24 -&gt; subadditiv (kohaerent)
<hr />
<h2 id="sec:cvar-value-at-risk-und-conditional-value">20.4 Value at Risk und Conditional Value at Risk</h2>
<figure>
<img src="bilder_04/kap_cvar_var_vergleich.svg" alt="Abb. 20.1: Der VaR ist eine Schwelle, der CVaR ein Mittelwert. 20 000 simulierte Tagesrenditen aus dem Fat-Tail-Modell dieses Kapitels (t-Verteilung mit drei Freiheitsgraden). Der schraffierte Bereich sind die schlechtesten 5 % der Tage; der VaR markiert nur ihren Rand, der CVaR ihren Mittelwert. Erzeugt von bilder_04/erzeuge_var_cvar.py." />
<figcaption aria-hidden="true">Abb. 20.1: Der VaR ist eine Schwelle, der CVaR ein Mittelwert. 20 000 simulierte Tagesrenditen aus dem Fat-Tail-Modell dieses Kapitels (<span class="math inline">t</span>-Verteilung mit drei Freiheitsgraden). Der schraffierte Bereich sind die schlechtesten 5 % der Tage; der VaR markiert nur ihren Rand, der CVaR ihren Mittelwert. Erzeugt von <code>bilder_04/erzeuge_var_cvar.py</code>.</figcaption>
<img src="bilder_04/kap_cvar_var_vergleich.svg" alt="Abb. 20.2: Der VaR ist eine Schwelle, der CVaR ein Mittelwert. 20 000 simulierte Tagesrenditen aus dem Fat-Tail-Modell dieses Kapitels (t-Verteilung mit drei Freiheitsgraden). Der schraffierte Bereich sind die schlechtesten 5 % der Tage; der VaR markiert nur ihren Rand, der CVaR ihren Mittelwert. Erzeugt von bilder_04/erzeuge_var_cvar.py." />
<figcaption aria-hidden="true">Abb. 20.2: Der VaR ist eine Schwelle, der CVaR ein Mittelwert. 20 000 simulierte Tagesrenditen aus dem Fat-Tail-Modell dieses Kapitels (<span class="math inline">t</span>-Verteilung mit drei Freiheitsgraden). Der schraffierte Bereich sind die schlechtesten 5 % der Tage; der VaR markiert nur ihren Rand, der CVaR ihren Mittelwert. Erzeugt von <code>bilder_04/erzeuge_var_cvar.py</code>.</figcaption>
</figure>
<p><strong>Value at Risk (<span class="math inline">\text{VaR}_\alpha</span>):</strong> Der Verlust, der mit Wahrscheinlichkeit <span class="math inline">\alpha</span> nicht überschritten wird.</p>
<p><strong>Conditional Value at Risk (<span class="math inline">\text{CVaR}_\alpha</span>, auch <em>Expected Shortfall</em>):</strong> Der <strong>durchschnittliche Verlust in den schlimmsten <span class="math inline">(1-\alpha)</span> Prozent</strong> der Fälle.</p>
@ -24432,8 +24460,8 @@ CVaR: Portfolio 101.33 vs. Summe 159.24 -&gt; subadditiv (kohaerent)
= \lVert \mathbf{w}-\mathbf{w}_{\text{alt}}\rVert_1
</span></p>
<figure>
<img src="bilder_04/kap13_cvar_zielfunktion.svg" alt="Abb. 20.2: CVaR-Zielfunktion mit Reibungs-Penalty" />
<figcaption aria-hidden="true">Abb. 20.2: CVaR-Zielfunktion mit Reibungs-Penalty</figcaption>
<img src="bilder_04/kap13_cvar_zielfunktion.svg" alt="Abb. 20.3: CVaR-Zielfunktion mit Reibungs-Penalty" />
<figcaption aria-hidden="true">Abb. 20.3: CVaR-Zielfunktion mit Reibungs-Penalty</figcaption>
</figure>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong> Die <span class="math inline">L_1</span>-Norm ist die Summe der <strong>Beträge</strong>. Sie misst, wie viel Prozent des Portfolios insgesamt bewegt werden — Käufe und Verkäufe zusammen.</p>
@ -26611,8 +26639,8 @@ aus.
<hr />
<h2 id="sec:praxisfallen-architektur-einer-produktionsreifen-or-plattform">22.5 Architektur einer produktionsreifen OR-Plattform</h2>
<figure>
<img src="bilder_04/kap15_or_plattform.svg" alt="Abb. 22.2: Architektur einer produktionsreifen OR-Plattform" />
<figcaption aria-hidden="true">Abb. 22.2: Architektur einer produktionsreifen OR-Plattform</figcaption>
<img src="bilder_04/kap15_or_plattform.svg" alt="Abb. 22.2: Die Schichten dieses Abschnitts als Bild — erzeugt aus der Tabelle daneben, damit beide nicht auseinanderlaufen können. Erzeugt von bilder_04/erzeuge_or_architektur.py." />
<figcaption aria-hidden="true">Abb. 22.2: Die Schichten dieses Abschnitts als Bild — erzeugt <strong>aus</strong> der Tabelle daneben, damit beide nicht auseinanderlaufen können. Erzeugt von <code>bilder_04/erzeuge_or_architektur.py</code>.</figcaption>
</figure>
<p>In einer professionellen Umgebung ist der Optimierer kein Skript, sondern ein <strong>versionierter, zustandsloser Dienst</strong>:</p>
<table>
@ -26661,6 +26689,7 @@ aus.
</tr>
</tbody>
</table>
<p><strong>Was Sie in der Abbildung sehen.</strong> Fünf der sechs Schichten bilden einen Weg: Daten gehen oben hinein und unten als Handlung wieder hinaus. Die sechste steht daneben, und das ist keine zeichnerische Verlegenheit — Monitoring und Audit-Trail sind keine Station, die man durchläuft, sondern eine Anforderung an jede. Wer sie als sechsten Kasten unten anhängt, baut sie am Ende auch so: als etwas, das man hinterher noch macht.</p>
<p><strong>Fünf Prinzipien, die sich in der Praxis bewährt haben:</strong></p>
<ol type="1">
<li><strong>Snapshot-Prinzip.</strong> Jeder Optimierungslauf arbeitet auf einem unveränderlichen Datenschnappschuss mit eigener ID. Nur so ist ein Ergebnis später reproduzierbar — und im Streitfall belegbar.</li>