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

@ -2104,6 +2104,11 @@ $$
> **🎯 Merksatz** Konvex heißt: Wer bergab geht, kommt am tiefsten Punkt an. Nicht-konvex heißt: Wer bergab geht, kommt *irgendwo* an — und weiß nicht, ob es der tiefste Punkt war.
![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`.](bilder_04/kap02_konvexitaet_landschaft.svg)
**Was Sie in der Abbildung sehen.** Links sind es fünf Läufe desselben Verfahrens, die in vier verschiedenen Tälern enden — dieselben vier, die Teil 2 von `Konvexitaet_Demo.py` 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.
### Was ist konvex, was nicht?
| Problemtyp | Konvex? | Konsequenz |
@ -2423,7 +2428,7 @@ Bis hierher war die lineare Algebra exakt. Ein Rechner rechnet aber nicht exakt,
> **Wenn meine Eingabedaten leicht ungenau sind — wie ungenau ist dann meine Lösung?**
![Abb. 2.3: Was $\kappa(\mathbf{A})$ praktisch bedeutet. Links: Über zwölf Größenordnungen hinweg
![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`.](bilder_04/kap_fundament_kondition.svg)
Die Antwort trägt einen Namen: die **Konditionszahl**`\index{Konditionszahl}`{=latex} $\kappa(\mathbf{A})$. 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 `INFEASIBLE` verabschiedet.
@ -4873,7 +4878,9 @@ $$
Der von George `\index{Dantzig, George}`{=latex}Dantzig 1947 formulierte `\index{Simplex-Algorithmus}`{=latex}Simplex-Algorithmus nutzt den Fundamentalsatz aus [Kapitel 2](#kap-fundament): **Das Optimum liegt in einer Ecke.** Statt alle Ecken aufzuzählen, wandert er von Ecke zu Ecke — und zwar immer in Richtung Verbesserung.
![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`.](bilder_04/kap_lp_simplex_ecken.svg)
(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`.](bilder_04/kap_lp_simplex_ecken.svg)
**Was Sie in der Abbildung sehen.** 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.
### Der Ablauf
@ -5187,6 +5194,11 @@ Zu **jedem** linearen Optimierungsproblem (dem **primalen Problem**`\index{Prima
Prüfen wir das an unserem Beispiel: $\mathbf{b}^\top\mathbf{y}^* = 40 \cdot 12{,}5 + 60 \cdot 31{,}25 + 8 \cdot 0 = 500 + 1875 + 0 = 2375$ ✓ — identisch mit $Z^* = 2375$.
![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`.](bilder_04/kap_lp_dualitaets_bruecke.svg)
**Was Sie in der Abbildung sehen.** 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.
Das lässt sich auch unabhängig von der Handrechnung nachweisen: Primal- und Dualproblem werden als **zwei getrennte, voneinander unabhängige LPs** aufgestellt und gelöst — wenn der starke Dualitätssatz`\index{Starker Dualitätssatz}`{=latex} stimmt, müssen beide denselben Zielwert liefern, ohne dass eines vom anderen „weiß“.
```python
@ -5555,7 +5567,7 @@ Der Zukauf für 18 €/h ist hochprofitabel: Jede zusätzliche Prüfstunde bring
Der vorige Abschnitt hat gezeigt, wie wertvoll Schattenpreise sind. Dieser zeigt die zwei Fälle, in denen sie eine Entscheidung **nicht** tragen — und wie man beide erkennt, bevor jemand auf ihrer Grundlage eine Maschine kauft.
![Abb. 5.3: Links laufen drei Geraden durch dieselbe Ecke — eine mehr, als zwei Dimensionen
![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`.](bilder_04/kap_lp_entartung.svg)
**Was Sie in der Abbildung sehen.** 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.
@ -6222,6 +6234,11 @@ Skript löst die LP-Relaxationen und kappt nach denselben drei Regeln, die oben
* seine LP-Schranke schlechter ist als die beste bereits gefundene ganzzahlige Lösung (der **Incumbent**`\index{Branch-and-Bound!Incumbent}`{=latex}),
* oder die LP-Lösung bereits ganzzahlig ist (dann ist dieser Ast fertig).
![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`.](bilder_04/kap_milp_bnb_geometrie.svg)
**Was Sie in der Abbildung sehen.** 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.
Zusätzlich fügen Solver **Schnittebenen**`\index{Schnittebenen}`{=latex} (*cutting planes*, z. B. Gomory-Cuts`\index{Schnittebenen!Gomory-Cuts}`{=latex}) hinzu: gültige Ungleichungen, die gebrochene Bereiche wegschneiden, ohne einen einzigen zulässigen ganzzahligen Punkt zu entfernen.
> **✏️ Handrechnung 6.2: Ein Suchbaum von Hand**
@ -6246,7 +6263,7 @@ Zusätzlich fügen Solver **Schnittebenen**`\index{Schnittebenen}`{=latex} (*cut
Hier kommt das Handwerkszeug, das MILP so mächtig macht:`\index{Big-M-Methode}`{=latex} Man übersetzt Geschäftsregeln in lineare Ungleichungen — mithilfe binärer Hilfsvariablen $y \in \{0,1\}$ und einer hinreichend großen Konstante $M$.
![Abb. 6.3: Dieselbe Bedingung $x \le M\,y$, zwei Zahlen für $M$. Gezeigt ist die LP-Relaxation —
![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`.](bilder_04/kap_milp_big_m.svg)
**Was Sie in der Abbildung sehen.** Für $y \in \{0,1\}$ sind beide Modelle gleichwertig; die beiden blauen Punkte stimmen überein. Verschieden ist nur, was dazwischen erlaubt ist — und rechts genügt bereits $y = 0{,}08$, um die volle Kapazität freizugeben.
@ -7693,6 +7710,11 @@ Der **CP-SAT-Solver**`\index{CP-SAT-Solver}`{=latex} von Google OR-Tools verbind
>
> **Und wenn ein Widerspruch auftritt?** Dann lernt der SAT-Kern eine **Sperrklausel** (*nogood*): „Diese Kombination nie wieder probieren.“ Der Solver springt dann nicht nur einen Schritt zurück, sondern direkt zur Ursache des Konflikts (*non-chronological backtracking*).
![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`.](bilder_04/kap_cpsat_propagation_ablauf.svg)
**Was Sie in der Abbildung sehen.** Jede Runde streicht genau **einen** 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.
```python
#!/usr/bin/env python3
@ -8273,11 +8295,14 @@ Erreichter Makespan: 11
==========================================================================
```
![Abb. 7.2: Job-Shop-Plan als Gantt-Diagramm](bilder_04/kap06_jobshop_gantt.svg)
![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`.](bilder_04/kap06_jobshop_gantt.svg)
*Eine interaktive Fassung dieser Grafik steht auf den Kapitelseiten der Website bereit.*
**Lesen Sie das Gantt-Diagramm.** 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 **Warten ist unvermeidlich**; es entsteht aus dem Zusammenspiel von Reihenfolge- und Maschinenbedingungen, nicht aus einer schlechten Planung.
**Lesen Sie das Gantt-Diagramm.** 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 **Warten ist unvermeidlich**; es entsteht aus dem Zusammenspiel von Reihenfolge- und Maschinenbedingungen, nicht aus einer schlechten Planung.
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.)
> **⚠️ Mehrere gleich gute Pläne — und was das für Ihre Tests bedeutet**
>
@ -15778,7 +15803,7 @@ dem CO2-Preis, den das Unternehmen ohnehin ansetzt.
Die Front hat **zehn** Punkte. Ein Raster aus 1 401 Gewichten findet **sechs** davon. Vier Kompromisse sind für kein Gewicht erreichbar — und das ist keine Frage der Rasterweite.
![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.](bilder_04/kap_mehrziel_pareto.svg)
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`.](bilder_04/kap_mehrziel_pareto.svg)
*Eine interaktive Fassung dieser Grafik steht auf den Kapitelseiten der Website bereit.*
@ -15786,6 +15811,8 @@ Hülle und sind über Gewichte erreichbar; die vier offenen Kreise sind pareto-o
Das Programm rechnet diese Hülle in `untere_huelle()` unabhängig aus und vergleicht: Genau die vier Punkte, die das Gewichtsraster verfehlt, liegen strikt oberhalb. Die Gegenprobe steht in der Ausgabe.
**Was Sie in der Abbildung sehen.** Die beiden Geraden haben dieselbe Steigung, also dasselbe Gewicht $w$. 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 [Abschnitt 14.7](#sec:mehrziel-epsilon) geht: Eine Schranke für den CO₂-Ausstoß statt eines Gewichts findet genau diesen Punkt.
> **⚠️ Der Grund liegt in der Ganzzahligkeit**
>
> 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 **ganzzahlig** werden (welcher Träger, welches Lager, welche Schicht), zerfällt der Bereich in einzelne Punkte, und zwischen ihnen entstehen Einbuchtungen.
@ -19842,6 +19869,11 @@ CVaR: Portfolio 101.33 vs. Summe 159.24 -> subadditiv (kohaerent)
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 *Gewinn* von 2 €, im Portfolio einen *Verlust* 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 **senkt** das Risiko, wie es sein muss.
![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`.](bilder_04/kap_cvar_subadditivitaet.svg)
**Was Sie in der Abbildung sehen.** 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.
> **⚠️ Eine Falle bei der CVaR-Berechnung** Der naheliegende Weg — „Mittelwert aller Verluste $\ge$ VaR“ — ist **falsch**, sobald die Verteilung **Atome** 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 `>=` 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.
>
> Die Rockafellar-Uryasev-Formel behandelt Atome von sich aus korrekt. Verwenden Sie sie auch dann, wenn Sie „nur schnell“ einen CVaR ausrechnen wollen.
@ -19850,7 +19882,7 @@ Und Teil 2 zeigt das Grundproblem des VaR an einem Beispiel, das Sie von Hand na
## 20.4 Value at Risk und Conditional Value at Risk {#sec:cvar-value-at-risk-und-conditional-value}
![Abb. 20.1: Der VaR ist eine Schwelle, der CVaR ein Mittelwert. 20 000 simulierte Tagesrenditen aus
![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`.](bilder_04/kap_cvar_var_vergleich.svg)
**Value at Risk ($\text{VaR}_\alpha$):**`\index{Value at Risk (VaR)}`{=latex} Der Verlust, der mit Wahrscheinlichkeit $\alpha$ nicht überschritten wird.
@ -19925,7 +19957,7 @@ $$
= \lVert \mathbf{w}-\mathbf{w}_{\text{alt}}\rVert_1
$$
![Abb. 20.2: CVaR-Zielfunktion mit Reibungs-Penalty](bilder_04/kap13_cvar_zielfunktion.svg)
![Abb. 20.3: CVaR-Zielfunktion mit Reibungs-Penalty](bilder_04/kap13_cvar_zielfunktion.svg)
> **📐 Formel-Lesehilfe** Die $L_1$-Norm ist die Summe der **Beträge**. Sie misst, wie viel Prozent des Portfolios insgesamt bewegt werden — Käufe und Verkäufe zusammen.
>
@ -22050,7 +22082,8 @@ Alle drei Antworten stehen bis hierhin in Tabellen. Tabellen liest niemand in ei
## 22.5 Architektur einer produktionsreifen OR-Plattform {#sec:praxisfallen-architektur-einer-produktionsreifen-or-plattform}
![Abb. 22.2: Architektur einer produktionsreifen OR-Plattform](bilder_04/kap15_or_plattform.svg)
![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`.](bilder_04/kap15_or_plattform.svg)
In einer professionellen Umgebung ist der Optimierer kein Skript, sondern ein **versionierter, zustandsloser Dienst**:
@ -22063,6 +22096,8 @@ In einer professionellen Umgebung ist der Optimierer kein Skript, sondern ein **
| **Ausführung** | Kalender-Sync, Benachrichtigung, Order-Routing | Kalender-APIs, Messaging, Broker |
| **Betrieb** | Monitoring, Audit-Trail, Alarme | Logging, Metriken, Versionierung |
**Was Sie in der Abbildung sehen.** 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.
**Fünf Prinzipien, die sich in der Praxis bewährt haben:**
1. **Snapshot-Prinzip.**`\index{Snapshot-Prinzip}`{=latex} Jeder Optimierungslauf arbeitet auf einem unveränderlichen Datenschnappschuss mit eigener ID. Nur so ist ein Ergebnis später reproduzierbar — und im Streitfall belegbar.