Neuer Abschnitt im Kapitel Praxisfallen plus Constraint_Attribution.py (75. Programm). Setzt den fehlenden Teil von Paket 3 aus Verbesserungen_02.md um. Die Luecke, die es schliesst: Erklaerbarkeit.py beantwortet "warum DIESE Zuweisung?", Konfliktsuche.py "warum geht es GAR NICHT?". Der haeufigste Fall der Praxis lag dazwischen und hatte kein Werkzeug - das Modell rechnet, der Plan ist zulaessig, und trotzdem enttaeuschend, nur weiss niemand woran es liegt. Gerechnet wird auf derselben Fabrik wie die Konfliktsuche, nach der Reparatur: Dort war die Lackierkapazitaet von 150 Stunden der Kern des Widerspruchs (2*40 + 3*30 = 170 > 150), eine zweite Schicht bringt sie auf 210. Damit laesst sich dieselbe Instanz ueber zwei Kapitel verfolgen - erst "warum nicht?", dann "was kostet was?". Drei gemessene Befunde: * Bindend heisst nicht teuer: Fuenf Bedingungen binden, "Lackierbudget Schicht 2" mit einem Schattenpreis von 0,00 EUR. * Der Schattenpreis ist eine Momentaufnahme: Fuer die Lackiererei verspricht die Hochrechnung 4,00 * 30 = 120 EUR, gemessen kommen 60 EUR; der Preis gilt exakt bis +15 Stunden. Die Ranglisten drehen sich dabei um - nach Schattenpreis fuehrt der Traegervertrag (18 EUR/Stueck), nach tatsaechlicher Wirkung die Montage-Sonderschicht (450 EUR). * Ein Wunsch kann unmoeglich statt teuer sein: "mindestens 55 Rahmen" ist INFEASIBLE, blockiert von drei Bedingungen gemeinsam. Mit +15 Stunden Lackierkapazitaet wird er erfuellbar und kostet dann nichts - der Plan waehlt diese Menge von sich aus. Der Konflikt bestand nie zwischen Vertrieb und Produktion. Der Managementbericht entsteht aus genau diesen Zahlen; kein Satz enthaelt eine Zahl, die nicht vorher gerechnet wurde. Voraussetzung ist der sprechende Name jeder Bedingung - aus A_ub[7] wird kein Satz. Dazu die Grenze, als Warnkasten und als Uebungsaufgabe: Der Bericht erklaert das Modell, nicht die Wirklichkeit. Steigt der Deckungsbeitrag von Deckel von 9 auf 20 EUR, kippt der Plan (Rahmen auf das Vertragsminimum 40, Deckel auf die Marktgrenze 120), "Kapazitaet Lackieren" hoert auf zu binden, "Liefervertrag Rahmen" faengt damit an - und der Satz "Teuerste Bindung ist Liefervertrag Traeger" steht woertlich unveraendert im Bericht, waehrend die Zahl dahinter von 18 auf 58 EUR springt. Die Zaehlprobe aus 6.1a hat sich sofort bewaehrt: Nach dem Einbau der Uebungsaufgabe meldete --check "6 Loesungen, das Kapitel aber 7 Aufgaben", bevor die Loesung geschrieben war. Mitgezogen: Kapitelkopf, Lernziele, Selbsttest, Zusammenfassung, Vorwort-Programmverzeichnis, Vorwaertsverweis aus Falle 2, Uebungsaufgabe und Loesung in Anhang A. Stand: 294 Abschnitte, 725 Querverweise, 328 Indexmarken, 75 Programme, 139 Aufgaben mit 139 Loesungen, 33 pytest-Tests, PDF 737 Seiten, 68 netzfreie Programme fehlerfrei. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1599 lines
97 KiB
Markdown
1599 lines
97 KiB
Markdown
# PROGRESS — Optimierte Entscheidungsfindung mit Python, Version 04
|
||
|
||
**Zweck dieser Datei:** Sie hält fest, was von **[PLAN.md](PLAN.md)** bereits erledigt ist —
|
||
mit den Zahlen, an denen sich das nachprüfen lässt, und mit den Funden, die unterwegs
|
||
angefallen sind. Wer nach einer Unterbrechung weitermacht, liest diese Datei zuerst;
|
||
`CLAUDE.md` verweist dafür auf sie.
|
||
|
||
**Stand:** 7. September 2026 · Branch `main` · **Phasen 0–5 abgeschlossen**
|
||
|
||
*(Die genaue Position in der Historie liefert `git log --oneline`; ein Hash steht hier
|
||
bewusst nicht, weil er sich mit genau dem Commit ändert, der diese Zeile enthält.)*
|
||
|
||
Legende: ✅ fertig und verifiziert · 🔄 in Arbeit · ⬜ offen
|
||
|
||
---
|
||
|
||
## 0. Wiederaufnahme in einer neuen Sitzung
|
||
|
||
Diese vier Schritte zuerst — sie dauern zusammen wenige Minuten und sagen, ob der Stand
|
||
unversehrt ist, **bevor** irgendetwas angefasst wird. Weicht ein Wert ab, ist die Ursache zu
|
||
klären und nicht zu überschreiben.
|
||
|
||
```bash
|
||
cd ~/Python_Programs/OR_mit_Python/Version_04
|
||
|
||
# 1. Arbeitsbaum sauber?
|
||
git status --short
|
||
# Soll: leer. Ausnahme siehe unten (bilder_04/titelseite.svg nach einem Build).
|
||
|
||
# 2. Struktur, Querverweise, Codezäune
|
||
python3 Operations_Research_mit_Python_Version_04/build_version_04.py --check
|
||
# Soll: 5 Teile, 23 Kapitel, 5 Anhänge, 292 Abschnitte, 703 Querverweise,
|
||
# 325 Indexmarken, 31 Dateien, 27.013 Zeilen, 276 Hauptüberschriften
|
||
# KEINE Zeile mit "WARNUNG", "FEHLENDE DATEIEN" oder "Nicht eingebundene".
|
||
|
||
# 3. Laufen alle Programme? (einige Minuten; die Finanzkapitel brauchen Internet)
|
||
python3 Operations_Research_mit_Python_Version_04/extract_programme_04.py
|
||
MPLBACKEND=Agg bash -c 'for p in Operations_Research_mit_Python_Version_04_Programme/*.py; do
|
||
timeout 600 python3 "$p" >/dev/null 2>&1 || echo "FEHLER: $p"; done'
|
||
# Soll: 73 Programme, keine FEHLER-Zeile.
|
||
|
||
# 4. Gegenprobe: Version 03 ist unberührt (liegt seit der Trennung daneben)
|
||
python3 ../Version_03/Operations_Research_mit_Python_Version_03/build_version_03.py --check
|
||
# Soll: 22 Dateien, 11.082 Zeilen, 131 Hauptüberschriften
|
||
```
|
||
|
||
Danach den Abschnitt **[7. Was als Nächstes ansteht](#7-was-als-nächstes-ansteht)** lesen —
|
||
dort steht der konkrete nächste Handgriff, nicht nur das nächste Thema.
|
||
|
||
> **Eine erwartete Abweichung:** `bilder_04/titelseite.svg` und die Kopie
|
||
> `OR_HTML_04/bilder_04/titelseite.svg` erscheinen als geändert, wenn seit dem letzten
|
||
> Build **ein Tag vergangen** ist. Das ist kein Schaden: Die Titelseite trägt im Fußband
|
||
> „Stand: <Datum>“. Einfach beide mitcommitten. Innerhalb desselben Tages ist die Ausgabe
|
||
> byteidentisch — dafür sorgen `svg.hashsalt` und `metadata={"Date": None}` in
|
||
> `bilder_04/erzeuge_titelseite.py`.
|
||
|
||
---
|
||
|
||
## 1. Der Stand in einem Blick
|
||
|
||
| Phase | Inhalt | Stand |
|
||
| --- | --- | --- |
|
||
| 0 | Infrastruktur (Build-Fork, Abschnittsnummerierung, Notebooks, Plotly) | ✅ |
|
||
| 1 | Didaktik-Layer über die 15 Bestandskapitel | ✅ |
|
||
| 2 | Code-Architektur (`or_kern.py`, Statusauswertung, Solverwechsel) | ✅ |
|
||
| 3 | Neue Kapitel | ✅ **abgeschlossen** — 7 geplante + Spaltengenerierung |
|
||
| 4 | Anhänge (C ausbauen, D neu, B Rezeptkarten) | ✅ **abgeschlossen** |
|
||
| 5 | Medien, Rahmen, Gesamtbau | ✅ **abgeschlossen** |
|
||
|
||
### Referenzwerte
|
||
|
||
Diese Zahlen sind mit `--check`, `--pdf` und dem Programmdurchlauf reproduzierbar. Weichen
|
||
sie unerwartet ab, ist etwas kaputtgegangen.
|
||
|
||
| Kennzahl | Version 03 | Version 04 (heute) |
|
||
| --- | --- | --- |
|
||
| Kapiteldateien | 22 | **31** |
|
||
| Kapitel | 15 | **23** |
|
||
| Anhänge | 4 | **5** |
|
||
| Zeilen im Gesamtdokument | 11 082 | **27 868** |
|
||
| Größe des Gesamtdokuments | 606 KB | **1 565 KB** |
|
||
| Hauptüberschriften | 131 | **297** |
|
||
| registrierte Abschnitte | 122 | **294** |
|
||
| aufgelöste Querverweise | 314 | **725** (0 unaufgelöst) |
|
||
| Indexmarken | 295 | **328** |
|
||
| Beispielprogramme | 41 | **75** (alle lauffähig) |
|
||
| PDF-Seiten | — | **737** |
|
||
| Notebooks | — | **25** |
|
||
| Plotly-Figuren | — | **4** |
|
||
| Diagramme (SVG) | 26 | **32**, davon **18** mit Generatorskript (15 Skripte) |
|
||
|
||
**Gegenprobe:** `build_version_03.py --check` läuft unverändert durch — Version 03 ist von
|
||
allen Änderungen unberührt.
|
||
|
||
---
|
||
|
||
## 2. Phase 0 — Infrastruktur ✅
|
||
|
||
*Commit `604c532`*
|
||
|
||
- ✅ Verzeichnisse angelegt: `Operations_Research_mit_Python_Version_04{,_Programme}`,
|
||
`bilder_04/`, `OR_HTML_04/`, `Notebooks_04/`
|
||
- ✅ **`build_version_04.py`** als Fork mit allen vier geplanten Eingriffen:
|
||
Versionsblock mit sämtlichen Pfaden; `nummeriere_abschnitte()` für die automatische
|
||
Abschnittsnummerierung; `spiegle_bilder()`; `resolve_plotly()` +
|
||
`kopiere_plotly_bibliothek()`; `baue_notebooks()` + `ergaenze_notebook_hinweis()` +
|
||
`spiegle_notebooks()`; erweitertes `markiere_karten()`; `KEINE_KAPITELDATEIEN`
|
||
- ✅ **`extract_programme_04.py`**
|
||
- ✅ `OR_HTML_04/assets/site.css` um die fünf neuen Kartenklassen erweitert
|
||
- ✅ Baseline-Build grün für `--check`, `--pdf`, `--html`
|
||
|
||
**Später ergänzt (in Phase 2):** `pruefe_dateien()` zählt jetzt die Codezäune jeder
|
||
Kapiteldatei und lässt `--check` fehlschlagen, wenn einer fehlt — siehe die Funde in
|
||
Abschnitt 6.
|
||
|
||
---
|
||
|
||
## 3. Phase 1 — Didaktik-Layer ✅
|
||
|
||
*Commits `a661a99` bis `b154fe8`, ein Commit je Kapitel*
|
||
|
||
Alle 15 Bestandskapitel haben den vollständigen Baukasten aus PLAN.md Abschnitt 8:
|
||
|
||
| Kapitel | Datei | Stand |
|
||
| --- | --- | --- |
|
||
| Einführung | `10_Einfuehrung_OR.md` | ✅ |
|
||
| Fundament | `11_Mathematisches_Fundament.md` | ✅ |
|
||
| Ökosystem | `12_Python_Oekosystem.md` | ✅ |
|
||
| LP | `20_Lineare_Programmierung.md` | ✅ |
|
||
| MILP | `21_MILP.md` | ✅ |
|
||
| CP-SAT | `22_CP_SAT.md` | ✅ |
|
||
| Graphen/Touren | `23_Graphen_Fluesse_Touren.md` | ✅ |
|
||
| QP/NLP | `30_QP_und_NLP.md` | ✅ |
|
||
| Unsicherheit | `31_Unsicherheit.md` | ✅ |
|
||
| Dynamische Prog. | `32_Dynamische_Programmierung.md` | ✅ |
|
||
| Finanzdaten | `40_Finanzdaten.md` | ✅ |
|
||
| Markowitz | `41_Markowitz.md` | ✅ |
|
||
| CVaR | `42_CVaR.md` | ✅ |
|
||
| Handelsmaschine | `43_Handelsmaschine.md` | ✅ |
|
||
| Praxisfallen | `50_Praxis.md` | ✅ |
|
||
|
||
*(Ohne Kapitelnummern — die verschieben sich mit jedem eingefügten Kapitel. Im Buch
|
||
geschieht das automatisch; in dieser Tabelle stünden sonst dauernd falsche Zahlen.)*
|
||
|
||
Jedes Kapitel hat: **„In 5 Minuten gelöst“** (15/15), **„Finde den Denkfehler“** (15/15),
|
||
**Micro-Quiz mit genau 3 Fragen** (15/15), Notebook-Badge. Anhang A enthält die Lösungen zu
|
||
allen neuen Aufgaben, Quizzes und Denkfehlern.
|
||
|
||
### Kapitelspezifische Ergänzungen über den Baukasten hinaus
|
||
|
||
- ✅ **Kap. 1 Einführung:** Excel-zu-Python-Brücke mit `Excel_Bruecke.py` — vollständiger
|
||
Weg Tabellenblatt → Modell → Lösung → Rückschreiben, plus Übersetzungstabelle
|
||
Excel-Solver ↔ Python
|
||
- ✅ **Kap. 2 Fundament:** `Skalierung_Kondition.py` — Konditionszahl als Experiment
|
||
- ✅ **Kap. 3 Ökosystem:** `Modellierungsschichten.py` und
|
||
`Vektorisierte_Modellgenerierung.py`
|
||
- ✅ **Kap. 4 LP:** Abschnitt „Wann Schattenpreise lügen“ mit `Toleranzen_und_Entartung.py`
|
||
— Simplex und Innere-Punkte-Verfahren liefern **gegensätzliche** Dualwerte;
|
||
Entartungstest; Schattenpreis-Spannen über alle optimalen Duallösungen
|
||
- ✅ **Kap. 5 MILP:** Abschnitt „Gap, Zeitlimit und Warm-Start“ mit
|
||
`Solverstatus_und_Gap.py` (2 s → 9,06 % Gap, 9 s → optimal; die zusätzlichen 7 Sekunden
|
||
bringen 0,17 %) und `Warmstart_Effekt.py`. **Ehrlich berichtet:** Der LPT-Hinweis bringt
|
||
bei HiGHS *nichts*, bei CP-SAT Faktor 2,2 bzw. 1,4 — statt der im Auftrag erhofften
|
||
„massiven Beschleunigung“. Denkfehler mit `Big_M_Falle.py`: Trickle Flow lässt 11 Lager
|
||
liefern und verbucht 0 € statt 35 848 € Fixkosten. Die Big-M-Tabelle trägt jetzt
|
||
**gemessene** Knotenzahlen (13 gegen 51) — die V03-Behauptung „Big-M ≥ 10⁹ führt oft zum
|
||
Timeout“ war so nicht haltbar und ist samt Presolve-Vorbehalt korrigiert
|
||
- ✅ **Kap. 6 CP-SAT:** Abschnitt „Die fünf Antworten von CP-SAT“ mit
|
||
`CP_SAT_Statusfaelle.py` (alle fünf Status absichtlich erzeugt); Denkfehler mit
|
||
`Strafgewichte.py` (Gewicht 1 statt 30 erzeugt 12 Verstöße gegen die
|
||
Betriebsvereinbarung); interaktives Gantt über `{plotly:kap06_jobshop_gantt}`
|
||
- ✅ **Kap. 7 Graphen:** Schnellstart mit Ungarischer Methode (gierig 128 min gegen optimal
|
||
118 min); Formel-Übersetzer zur totalen Unimodularität samt Warnung vor ihrer
|
||
Zerbrechlichkeit; Denkfehler mit `VRP_Kapazitaetsfalle.py` — ohne Ladungsdimension fährt
|
||
ein Fahrzeug alle 16 Kunden mit 37 Paletten bei Kapazität 10 und sieht dabei 43 % besser aus
|
||
- ✅ **Kap. 8 QP/NLP:** Schnellstart (die riskantere Anlage senkt das Risiko von 12 % auf
|
||
8,9 %, naives 50/50 verschlechtert es auf 12,84 %); Formel-Übersetzer zu den vier
|
||
KKT-Bedingungen; Abschnitt „Jenseits der Konvexität“ mit `Lokale_Optima_Multistart.py`
|
||
(5 lokale Optima, 8 % Spanne, der kaufmännisch naheliegende Startpunkt ist der
|
||
schlechteste); MINLP/Ipopt als **ausdrücklich nicht ausgeführter** Ausblick mit
|
||
Installationshinweis; Denkfehler zur nicht positiv semidefiniten Kovarianzmatrix
|
||
(Eigenwert −0,8, Varianz −0,254 bei erlaubten Leerverkäufen)
|
||
- ✅ **Kap. 9–14:** Newsvendor-Schnellstart, Rückwärtsinduktion, `Kovarianz_Falle.py`,
|
||
`Renditeschaetzung_Falle.py`, `Data_Snooping.py` (Bonferroni am Rauschen)
|
||
- ✅ **Kap. 15 Praxisfallen:** `Betriebsueberwachung.py` — das Modell, das still aufgehört
|
||
hat zu optimieren
|
||
- ✅ **Teil-Auftakte II und III:** beide Entscheidungs-Flussdiagramme erzeugt
|
||
(`bilder_04/erzeuge_solverwahl_diagramme.py`) und eingebunden
|
||
|
||
### Aus Phase 5 vorgezogen
|
||
|
||
- ✅ Generatorskripte in `bilder_04/`: `erzeuge_solverwahl_diagramme.py`,
|
||
`erzeuge_kap06_gantt.py`, `erzeuge_titelseite.py` — in V03 fehlten die Diagrammquellen
|
||
- ✅ erste Plotly-Figur: `bilder_04/plotly/kap06_jobshop_gantt.html`
|
||
- ✅ 16 Notebooks in `Notebooks_04/`
|
||
|
||
---
|
||
|
||
## 4. Phase 2 — Code-Architektur ✅
|
||
|
||
*Commits `33e9a36`, `eeea9e8`, `ed3640d`*
|
||
|
||
- ✅ **`or_kern.py`** angelegt (Kapitel *Praxisfallen*, Abschnitt „Der gemeinsame Unterbau“):
|
||
`SolverStatus`-Enum mit Übersetzern für alle fünf Bibliotheken (**lazy imports**, damit
|
||
`ortools` und `highspy` nicht kollidieren), `Loesung`-DTO mit `gap`, `schattenpreise` und
|
||
`als_bericht()`, Pydantic-Domänenmodell `Produktionsproblem` mit Validierung beim
|
||
Einlesen, `pruefe_loesung()` und zwei austauschbare Modellbauer (GLOP, SciPy)
|
||
- ✅ **Excel-Schicht** (`lade_produktionsproblem()` / `schreibe_ergebnis()`) nach `or_kern.py`
|
||
gezogen, pandas als lazy import. `Excel_Bruecke.py` schrumpfte dadurch von **156 auf 96
|
||
Zeilen** bei unveränderter Ausgabe
|
||
- ✅ **`schattenpreise` im DTO** — beide Modellbauer liefern identische Werte (40 / 20 €);
|
||
der Begriff ist solverunabhängig, die Abfrage nicht
|
||
- ✅ **`MILP_Portfolio_Fixgebuehren.py`** (Kap. 5) auf `status_von_scipy` + `Loesung`
|
||
umgestellt. `ergebnis.success` ist *ein Bit* und unterscheidet nicht zwischen Modell- und
|
||
Zeitproblem; mit `schranke=-mip_dual_bound` steht jetzt `Gap: 0.00%` im Bericht — die
|
||
Zusage, dass das Optimum *bewiesen* ist. Der `assert`-Block wurde zu `pruefe_portfolio()`
|
||
mit Beanstandungsliste (asserts verschwinden unter `python -O`)
|
||
- ✅ **`CP_SAT_Vertretungssystem.py`** (Kap. 6) auf `status_von_cpsat` umgestellt: die
|
||
Auswertung heißt jetzt `status.modellfehler` / `status.brauchbar` statt
|
||
`cp_model.INFEASIBLE`. Zusätzlich `num_workers=1`/`random_seed=1` und `pruefe_plan()` als
|
||
Abnahmeprüfung. Das Programm hatte in V03 **überhaupt keine abgedruckte Ausgabe** — sie
|
||
steht jetzt im Buch
|
||
- ✅ **`Bot_Allokation.py`** (Kap. 1) bewusst **nicht** umgestellt. Das erste Programm des
|
||
Buchs soll zeigen, wie ein Modell entsteht; eine Abstraktion würde an dieser Stelle genau
|
||
das verstecken, was man sehen will. Stattdessen ein 📎-Kasten mit der Regel: *Erst die
|
||
Wiederholung rechtfertigt die Abstraktion*
|
||
- ✅ **Solverwechsel CP-SAT ↔ HiGHS real vorgeführt:** neuer Abschnitt
|
||
„Der Solverwechsel in der Praxis“ im Kapitel *Praxisfallen* mit `Solverwechsel_CPSAT_HiGHS.py`
|
||
(59. Programm). Kapazitierte Standortplanung, 6 Lager / 12 Kunden; beide Solver beweisen
|
||
22 010 €. Solverabhängig sind genau die zwei Modellbauer (39 und 52 Zeilen);
|
||
Domänenmodell, Abnahmeprüfung und Bericht sind wörtlich dieselben. Die zwei Prozesse
|
||
(`ortools`/`highspy` kollidieren beim Import) zeigen nebenbei den zweiten Nutzen des DTOs:
|
||
`model_dump_json()` / `model_validate_json()` überleben eine Prozessgrenze — ein
|
||
`pywraplp.Solver` nicht. Die Kosten des Wechsels sind ehrlich benannt: CP-SAT rechnet nur
|
||
ganzzahlig, HiGHS will Matrixzeilen
|
||
|
||
---
|
||
|
||
## 5. Die 18 neuen Beispielprogramme
|
||
|
||
41 (V03) → 59 (V04). Neu hinzugekommen:
|
||
|
||
| Programm | Kapitel | Was es zeigt |
|
||
| --- | --- | --- |
|
||
| `Excel_Bruecke.py` | 1 | Tabellenblatt → Modell → Lösung → Rückschreiben |
|
||
| `Skalierung_Kondition.py` | 2 | Konditionszahl, Ruiz-Equilibrierung |
|
||
| `Modellierungsschichten.py` | 3 | Rohdaten / Domäne / Modell / Lösung |
|
||
| `Vektorisierte_Modellgenerierung.py` | 3 | NumPy/Polars gegen `for`-Schleifen |
|
||
| `Toleranzen_und_Entartung.py` | 4 | gegensätzliche Dualwerte, Entartungstest |
|
||
| `Solverstatus_und_Gap.py` | 5 | alle Statusfälle, MIP-Gap, Zeitbudget |
|
||
| `Warmstart_Effekt.py` | 5 | Warm-Start ehrlich gemessen |
|
||
| `Big_M_Falle.py` | 5 | Trickle Flow: 0 € statt 35 848 € Fixkosten |
|
||
| `CP_SAT_Statusfaelle.py` | 6 | alle fünf CP-SAT-Antworten erzeugt |
|
||
| `Strafgewichte.py` | 6 | Gewicht 1 statt 30 → 12 Regelverstöße |
|
||
| `VRP_Kapazitaetsfalle.py` | 7 | fehlende Ladungsdimension sieht 43 % besser aus |
|
||
| `Lokale_Optima_Multistart.py` | 8 | 5 lokale Optima, 8 % Spanne |
|
||
| `Kovarianz_Falle.py` | 11 | singuläre Kovarianz, Error-Maximizer |
|
||
| `Renditeschaetzung_Falle.py` | 12 | was Schätzfehler in $\mu$ kosten |
|
||
| `Data_Snooping.py` | 14 | die beste von N Strategien auf reinem Rauschen |
|
||
| `Betriebsueberwachung.py` | 15 | das Modell, das still aufgehört hat zu optimieren |
|
||
| `or_kern.py` | 15 | der gemeinsame Unterbau |
|
||
| `Solverwechsel_CPSAT_HiGHS.py` | 15 | derselbe Fall in zwei Solvern |
|
||
|
||
---
|
||
|
||
## 6. Funde und Korrekturen unterwegs
|
||
|
||
Diese Liste ist bewusst vollständig — sie ist der eigentliche Ertrag der Prüfschritte und
|
||
zeigt zugleich, welche Fehlerarten in diesem Projekt auftreten.
|
||
|
||
### Altlasten aus Version 03, die erst jetzt aufgefallen sind
|
||
|
||
1. **`CVaR_Portfolio.py` war überhaupt nicht lauffähig.** `cp.sqrt(cp.quad_form(w, sigma))`
|
||
ist nicht DCP — `cp.sqrt` ist konkav und verlangt ein konkaves Argument, `quad_form` ist
|
||
konvex. CVXPY lehnt das mit `DCPError` ab. Jetzt über die Cholesky-Zerlegung als
|
||
`cp.norm2(L.T @ w)`. **Verschärfend:** Die Fehlertabelle in Anhang C empfahl an genau
|
||
dieser Stelle `cp.psd_wrap(Sigma)` — das behebt eine *numerische* Beanstandung an der
|
||
Matrix, nicht die *strukturelle* Regelverletzung im Aufbau. Ein Leser wäre dieser
|
||
Empfehlung gefolgt und hätte denselben Fehler wieder bekommen. Zeile korrigiert,
|
||
⚠️-Kasten im Kapitel ergänzt.
|
||
2. **`JobShop_Intervalle.py` war nicht deterministisch.** Drei Läufe ergaben zwei
|
||
verschiedene, gleich optimale Pläne — die in V03 abgedruckte Ausgabe ließ sich nicht
|
||
reproduzieren. Mit `num_workers=1`/`random_seed=1` fixiert, Buchtext angeglichen und zum
|
||
Lehrpunkt gemacht: *nicht auf einen bestimmten Plan testen*.
|
||
3. **`CP_SAT_Vertretungssystem.py` hatte gar keine abgedruckte Ausgabe** — jetzt vorhanden,
|
||
nachdem die Reproduzierbarkeit hergestellt war.
|
||
4. **Vier veraltete Literalverweise auf Abschnittsnummern** („aus 3.2“, „in 4.4“, „aus 7.1“,
|
||
„in 8.0“) wurden durch die automatische Nummerierung falsch — durch `{ref:sec:…}` ersetzt.
|
||
5. **Ein falsch geschriebenes `{ref:sec:cvar-…}`-Label**, von `--check` als unbekannt
|
||
gemeldet.
|
||
6. **Die V03-Behauptung „Big-M ≥ 10⁹ führt oft zum Timeout“** war so nicht haltbar —
|
||
ersetzt durch gemessene Knotenzahlen (13 gegen 51) samt Presolve-Vorbehalt.
|
||
|
||
### Eigene Fehler, die die Prüfschritte abgefangen haben
|
||
|
||
7. **Zwei fehlende schließende Codezäune** (Kap. 1 und 15). Pandoc meldet so etwas nicht;
|
||
`reflow_markdown()` faltet stattdessen den gesamten Dateirest zu Fließtext, und im PDF
|
||
stünde Programmcode als Absatz. Aufgefallen nur an einer Unstimmigkeit der Kennzahlen:
|
||
Die Zeilenzahl brach um 2 700 ein, während die Dateigröße nur um 21 KB sank — das passt
|
||
nicht zusammen. ⇒ **`--check` zählt die Codezäune jetzt automatisch** (Regel 10).
|
||
8. **Eine falsche Behauptung „Risiko unter null“** (Kap. 8): Mit `nonneg=True` ist das
|
||
Minimum +0,002, nicht negativ. Numerisch geprüft, Aufgabe so umgeschrieben, dass
|
||
Leerverkäufe erlaubt sind — dann stimmt die verifizierte −0,254.
|
||
9. **Eine falsche Wahrscheinlichkeit** (Kap. 9): 97,7 % geschrieben, Simulation sagte
|
||
95,5 %. Korrekt ist $F(11{,}33) = 0{,}537$ und $0{,}537^5 = 0{,}045$ — die geschlossene
|
||
Form stimmt jetzt exakt mit der Simulation überein.
|
||
10. **Ein doppeltes Programm** (Kap. 13): `VaR_Nicht_Addierbar.py` fertig geschrieben und
|
||
getestet, bevor auffiel, dass `VaR_CVaR_Demo.py` Teil 2 dasselbe schon enthält.
|
||
Verworfen. ⇒ **Regel 9.**
|
||
11. **Eine CVaR-Falle im eigenen Prüfcode** (Kap. 13): `r[r <= quantile].mean()` wählte
|
||
100 % statt 5 % der Szenarien (0,08 % statt 39,33 %). Nur aufgefallen, weil das Ergebnis
|
||
der Subadditivitätsprüfung widersprach — jetzt als Warnkasten im Kapitel.
|
||
12. **Erfundene Schnellstart-Zahlen** (Kap. 14): Die erste Fassung enthielt Ausgabewerte,
|
||
die der Programmlauf widerlegte. Das Einfügen scheiterte zufällig an einem
|
||
Textanker — die falschen Zahlen erreichten die Datei nie. Neu aufgebaut mit denselben
|
||
Tagesrenditen in anderer Reihenfolge, damit der Vergleich nachweislich fair ist.
|
||
13. **`CVaR_Portfolio.py` blieb die ganze Phase 1 hindurch unentdeckt**, weil immer nur die
|
||
gerade bearbeiteten Kapitelprogramme geprüft wurden. ⇒ **Regel 11: Am Ende jeder Phase
|
||
die gesamte Programmsuite laufen lassen.**
|
||
|
||
### Funde in Phase 3
|
||
|
||
14. **Anhang A trug die Kapitelnummern hart im Überschriftentext** (`## Kapitel 13`), und die
|
||
60 Verweise aus den Kapiteln zeigten auf daraus abgeleitete Anker (`#kapitel-13`). Das
|
||
erste eingefügte Kapitel hätte sie alle um eins verschoben — die Links hätten weiterhin
|
||
funktioniert, aber auf die **falschen Lösungen** gezeigt. Das ist die tückischere Sorte
|
||
Fehler: kein toter Link, sondern ein stiller Inhaltsfehler. Umgestellt auf stabile Label
|
||
(`{#sec:loesungen-cvar}`) mit `{ref:...}`-Verweisen.
|
||
15. **Verweise zwischen Kapiteldateien meldet niemand.** `wandle_querverweise()` macht im
|
||
Gesamtdokument fetten Text aus einem unbekannten Ziel, die Website lässt ihn stehen.
|
||
`pruefe_dateien()` prüft solche Links jetzt gegen das Dateisystem.
|
||
16. **Zwei Instanzentwürfe für das Metaheuristik-Kapitel mussten verworfen werden**, beide aus demselben
|
||
Grund: Sie wurden mit wachsender Größe *leichter*. Beim Cutting-Stock war die
|
||
First-Fit-Heuristik schon optimal; bei den Rüstzeiten mit fester Familienzahl bestand
|
||
der Plan ab 200 Aufträgen nur noch aus wenigen großen Blöcken, die jede Faustregel
|
||
findet. Erst eine mit $n$ wachsende Familienzahl macht die Aufgabe durchgehend schwer.
|
||
**Lehre:** Bei einer Skalierungsaussage gehört geprüft, dass die Instanzfamilie in der
|
||
Größe nicht ihren Charakter ändert.
|
||
**Nachtrag (Kapitel Spaltengenerierung):** Beim Cutting-Stock-Fall ist die Ursache
|
||
inzwischen benannt und gemessen — sie liegt an der **Stückzahl je Behälter**. Bei drei
|
||
Stücken je Rolle bringt exakte Optimierung 11 %, bei sechs nichts. Aus dem verworfenen
|
||
Vorversuch ist damit ein eigener Kapitelabschnitt geworden.
|
||
17. **Eine falsche Erklärung im eigenen Text**, aufgefallen durch Nachrechnen: Zur zu heißen
|
||
Starttemperatur hatte ich geschrieben, es werde „jede Verschlechterung angenommen“.
|
||
Gemessen sind es 0,29 %. Der Effekt ist echt, die Erklärung war es nicht — jetzt steht
|
||
die Annahmequote als Spalte in der Tabelle, statt behauptet zu werden.
|
||
18. **Eine Behauptung zur LNS-Fenstergröße widerlegt.** Geschrieben hatte ich, bei gleichem
|
||
Zeitbudget gewinne das kleine Fenster. Die Messung sagt das Gegenteil: Fenster 10
|
||
sättigt (6 Verbesserungen in 4 000 Runden) und verliert gegen Fenster 20. Der Abschnitt
|
||
zeigt jetzt das Experiment statt der Regel.
|
||
|
||
---
|
||
|
||
## 6a. Phase 3 — neue Kapitel 🔄
|
||
|
||
### ✅ Kapitel „Metaheuristiken — wenn der exakte Solver aussteigt“
|
||
|
||
Datei `24_Metaheuristiken.md`, eingefügt zwischen Graphen/Touren und QP/NLP. Damit sind
|
||
alle folgenden Kapitel automatisch um eins weitergerückt — ohne eine einzige Handänderung
|
||
im Text. Genau dafür war Phase 0 da.
|
||
|
||
**Laufende Aufgabe:** Reihenfolge von Lackieraufträgen mit reihenfolgeabhängigen Rüstzeiten.
|
||
Drei Programme, alle mit festem **Zugbudget** statt Zeitbudget, damit die abgedruckten Werte
|
||
auf jeder Maschine reproduzierbar sind:
|
||
|
||
| Programm | Was es zeigt |
|
||
| --- | --- |
|
||
| `Simulated_Annealing.py` | Kostenänderung in O(1); Bergsteigen gegen Annealing; Temperaturkalibrierung mit Annahmequote und schlechtestem Zwischenwert |
|
||
| `Metaheuristik_vs_Exakt.py` | Der Umschlagpunkt bei 60 / 200 / 500 Aufträgen — und die Schranke als eigentlicher Ertrag des exakten Laufs |
|
||
| `Large_Neighborhood_Search.py` | Zerstören und exakt reparieren; Fenstergröße mit Optimum in der Mitte |
|
||
|
||
**Die gemessenen Kernbefunde** (alle in mehreren Läufen bestätigt):
|
||
|
||
* **Der Umschlagpunkt liegt viel höher, als die Intuition sagt.** Bei 60 Aufträgen beweist
|
||
CP-SAT in 2 Sekunden das Optimum, die Metaheuristik liegt 3,8 % daneben. Erst bei 500
|
||
Aufträgen kippt es — dort liefert CP-SAT nach 30 Sekunden eine Lösung, die **5,2 %
|
||
schlechter ist als die Faustregel eines Meisters**.
|
||
* **Der Beitrag des Annealings ist bescheiden.** Reines Bergsteigen holt 93 % dessen, was
|
||
Annealing schafft (917 gegen 907 Minuten). Deshalb steht im Kapitel die Anweisung, die
|
||
einfache Variante immer mitzurechnen.
|
||
* **Die Lehrbuchregel zur Starttemperatur ist auf dieser Aufgabe falsch.** „Anfangs sollen
|
||
20–50 % der Verschlechterungen durchgehen“ würde $T_0 \approx 50$ verlangen; die besten
|
||
Werte entstehen bei Annahmequoten **nahe null**. Als brauchbarer Ersatz ist der
|
||
*schlechteste Zwischenwert* im Kapitel eingeführt — er zeigt eine zu heiße Temperatur
|
||
sofort.
|
||
* **Das beste Ergebnis kommt aus der Kombination:** Annealing, dann LNS (2 289 Minuten)
|
||
schlägt beide Einzelverfahren. Die untere Schranke liegt bei 1 768 — auch das beste
|
||
Ergebnis ist noch 29,5 % davon entfernt, und das steht so im Buch.
|
||
|
||
**Bewusste Abweichung vom Plan:** Der Plan sah das Kapitel als „Metaheuristiken **&
|
||
Dekomposition**“ vor, mit Spaltengenerierung (Dantzig-Wolfe) am Cutting-Stock-Problem.
|
||
Das Kapitel behandelt jetzt nur die Metaheuristiken; Spaltengenerierung, genetische
|
||
Algorithmen und Tabu-Suche stehen in einem Abschnitt *Was dieses Kapitel nicht behandelt*
|
||
mit je einem Satz zur Einordnung. Gründe:
|
||
|
||
1. Das Kapitel ist mit drei Programmen und dem vollen Baukasten bereits umfangreich.
|
||
2. Spaltengenerierung gibt eine **andere Antwort auf dieselbe Frage** — sie approximiert
|
||
nicht die Lösung, sondern formuliert das Modell um. Sie neben LNS zu stellen, verwischt
|
||
genau diesen Unterschied.
|
||
3. Ein Vorversuch mit dem geplanten Cutting-Stock-Beispiel hat gezeigt, dass die
|
||
First-Fit-Heuristik dort **fast immer schon optimal** ist (bei 18/30/45 Stücken exakt
|
||
optimal). Als Aufhänger für Metaheuristiken taugt es nicht; für Spaltengenerierung
|
||
dagegen sehr wohl — also gehört es in ein eigenes Kapitel oder einen eigenen Abschnitt.
|
||
|
||
### ✅ Kapitel „Testen, Messen, Ausliefern“
|
||
|
||
Datei `51_Testing_und_Deployment.md`, zwischen Praxisfallen und Projektwerkstatt.
|
||
`51_Projektwerkstatt.md` wurde dabei zu `59_Projektwerkstatt.md` — die Präfixe sollen die
|
||
Lesereihenfolge zeigen, und zwischen 50 und 59 ist jetzt Platz für weitere Praxiskapitel.
|
||
|
||
Das Kapitel testet `or_kern.py`, also den Unterbau aus Phase 2. Vier Programme:
|
||
|
||
| Programm | Was es zeigt |
|
||
| --- | --- |
|
||
| `test_or_kern.py` | 33 Tests in vier Arten: Eigenschaften, Invarianten, Regression, Fehlerfälle. Jeder Test läuft über **beide** Modellbauer |
|
||
| `Mutationstest.py` | Baut zehn realistische Fehler in `or_kern.py` ein und misst, welche die Suite findet |
|
||
| `Benchmark_Skalierung.py` | Vier Bibliotheken, drei Größen, getrennte Prozesse; Aufbau und Lösen getrennt gemessen |
|
||
| `Optimierungsdienst.py` | FastAPI-Dienst mit Auftragswarteschlange, Selbsttest ohne Server über `TestClient` |
|
||
|
||
**Die tragende Idee:** Ein Optimierungsmodell lässt sich fast nie gegen den *richtigen Wert*
|
||
testen — niemand kann ihn unabhängig ausrechnen, sonst bräuchte man den Solver nicht. Also
|
||
testet man Eigenschaften, Invarianten, eine belegte Kleininstanz und die Fehlerfälle. Die
|
||
vierte Art fehlt fast überall: Die Abnahmeprüfung selbst muss mit kaputten Lösungen
|
||
gefüttert werden, sonst weiß niemand, ob sie überhaupt anschlägt.
|
||
|
||
**Der Mutationstest fand zwei echte Lücken** in der ersten Fassung der Suite (31 Tests):
|
||
Ein aufgeweichtes `SolverStatus.brauchbar` (ZEITLIMIT gilt fälschlich als brauchbar) und
|
||
eine nicht mehr stabile Ressourcenreihenfolge blieben unentdeckt. Beide Tests sind ergänzt,
|
||
tragen einen Kommentar zu ihrer Herkunft, und der Lauf steht jetzt bei 10 von 10.
|
||
|
||
**Zwei gemessene Nebenbefunde, die im Kapitel tragen:**
|
||
|
||
* Bei 10 000 Variablen gehen bei OR-Tools **rund drei Viertel der Zeit in den Modellaufbau**
|
||
in Python, nicht ins Lösen. Wer dort einen schnelleren Solver sucht, sucht in der falschen
|
||
Hälfte. CVXPY belegt konstant rund 240 MB — bei knappem Speicher entscheidet diese Spalte
|
||
und nicht die Laufzeit.
|
||
* OR-Tools **gibt den GIL während `Solve()` frei** (Faktor 3,5 bei vier Threads). Für einen
|
||
Dienst mit C++-Solvern genügt daher ein Threadpool; für eine Python-Heuristik aus
|
||
{ref:kap:metaheuristiken} nicht.
|
||
|
||
### ✅ Kapitel „Mehrere Ziele — Pareto-Fronten statt Gewichte“
|
||
|
||
Datei `33_Mehrziel_und_Pareto.md`, in Teil III hinter der Dynamischen Programmierung. Ein
|
||
Programm (`Mehrziel_Pareto.py`) und die erste neue Plotly-Figur seit Phase 1
|
||
(`bilder_04/erzeuge_pareto_front.py` → SVG + PNG + interaktive Fassung).
|
||
|
||
**Fall:** Eine Spedition vergibt zwölf Sendungen an drei Verkehrsträger, Kosten gegen CO₂.
|
||
Der Zielkonflikt entsteht nicht zwischen den Trägern — die Bahn ist immer billiger *und*
|
||
sauberer —, sondern durch die **Knappheit der Trassen**.
|
||
|
||
**Der Kernbefund, geometrisch abgesichert:** Die Pareto-Front hat 10 Punkte. Ein Raster aus
|
||
1 401 Gewichten findet davon nur **6**. Die anderen 4 sind für *kein* Gewicht erreichbar,
|
||
und das ist keine Frage der Rasterweite: Sie liegen strikt oberhalb der unteren konvexen
|
||
Hülle, und eine gewichtete Summe berührt geometrisch immer einen Hüllen-Eckpunkt. Das
|
||
Programm rechnet die Hülle unabhängig aus und vergleicht — die Gegenprobe steht in der
|
||
Ausgabe, damit die Aussage nicht auf dem Raster beruht.
|
||
|
||
Zweiter Befund aus derselben Messung: **Das Gewicht ist keine Feineinstellung.** 1 401
|
||
Gewichte erzeugen 6 verschiedene Pläne; weite Bereiche liefern dasselbe, dazwischen springt
|
||
es. Das macht „wir probieren ein paar Gewichte“ zum Glücksspiel.
|
||
|
||
Als Ersatz führt das Kapitel das **ε-Constraint-Verfahren** in der Form ein, die genau so
|
||
viele Solverläufe braucht, wie die Front Punkte hat (hier 10 statt 1 401) — plus
|
||
lexikografische Optimierung für den Fall einer echten Rangfolge. Entscheidbar wird die Front
|
||
durch die Spalte **€ je kg**, die mit dem internen CO₂-Preis vergleichbar ist.
|
||
|
||
Der Denkfehler ist der stärkste des Kapitels: Ein Team tastet 500 Gewichte ab, legt alle
|
||
gefundenen Pläne dem Fachbereich vor und hält den Lösungsraum damit für vollständig
|
||
abgetastet. Er ist es prinzipiell nicht — und der zweite Teil der Frage ist der
|
||
beunruhigende: Niemand hätte es bemerkt. Es fehlte kein *schlechter* Plan, sondern es
|
||
fehlten *gute*, und ein fehlender Kompromiss hinterlässt keine Spur.
|
||
|
||
### ✅ Kapitel „Die Strukturbrücke — dieselbe Mathematik, zwei Welten“
|
||
|
||
Datei `35_Strukturbruecke.md`. **Sie eröffnet jetzt Teil IV** — die Teil-Überschrift samt
|
||
Vorspann ist aus `40_Finanzdaten.md` hierher gewandert (36 Zeilen). Das war der erste
|
||
Einfügepunkt mit einer Teil-Grenze; es hat problemlos funktioniert, weil Teil-Überschriften
|
||
schlicht am Anfang der ersten Kapiteldatei stehen.
|
||
|
||
**Der Anlass:** Der Teil-Vorspann *behauptete* die Brücke bereits als Tabelle
|
||
(„Ressourcen auf Produkte verteilen" ↔ „Kapital auf Anlagen verteilen"). Solche Tabellen
|
||
sind billig. Das Kapitel macht daraus einen Nachweis.
|
||
|
||
`Strukturbruecke.py` führt zweimal denselben Code mit Daten aus zwei Welten aus:
|
||
|
||
* **Allokation:** Ein Depot passt unverändert in die Klasse `Produktionsproblem` aus
|
||
`or_kern.py` — kein Adapter, keine Unterklasse. Der Schattenpreis des Risikobudgets
|
||
(60,26 €) ist derselbe Dualwert derselben Nebenbedingung, der in der Werkstatt den Wert
|
||
einer Montagestunde angibt. In der Finanzwelt heißt er *Preis des Risikos*.
|
||
* **Absicherung:** **Eine** CVaR-Funktion, einmal mit Kursverlusten in Prozentpunkten,
|
||
einmal mit Lieferverzügen in Tagen. Die Rockafellar-Uryasev-Formel enthält kein Wort, das
|
||
nur an der Börse Sinn ergibt.
|
||
|
||
**Ein Nebenbefund, den das Programm selbst feststellt:** Die Streuungsgrenze von 40 % bindet
|
||
*nur* im Einkauf. Im Depot verhindert stattdessen die Mindestrendite die Konzentration.
|
||
Zwei verschiedene Bremsen an verschiedenen Stellen des Modells — wer eine Struktur
|
||
überträgt, überträgt also nicht mit, *welche* Nebenbedingung am Ende bindet.
|
||
|
||
**Der ehrliche Teil** ist der dritte Abschnitt: drei Dinge wandern nicht mit — gemessene
|
||
gegen geschätzte Zahlen, stabile gegen reagierende Prozesse, teilbare gegen ganzzahlige
|
||
Entscheidungen. Die letzte erklärt nebenbei eine Beobachtung, die aufmerksamen Lesern
|
||
auffällt: Teil II ist voller MILP, Teil IV fast frei davon. Das liegt an der Welt, nicht an
|
||
der Methode.
|
||
|
||
**Beim Schreiben aufgefallen:** Das Programm löste zunächst den in {ref:kap:oekosystem}
|
||
dokumentierten Importkonflikt aus — `loese_mit_glop` zieht ortools, danach scheitert CVXPY
|
||
beim Laden von highspy mit einem `undefined symbol`. Behoben durch `loese_mit_scipy`; der
|
||
Grund steht jetzt als Warnung im Programmkopf. Ein Beleg dafür, dass die lazy imports in
|
||
`or_kern.py` kein theoretisches Detail sind.
|
||
|
||
### ✅ Kapitel „Predict-then-Optimize“
|
||
|
||
Datei `34_Predict_then_Optimize.md`. **Abweichung vom Plan bei der Einordnung:** Der Plan
|
||
sah das Kapitel in Teil IV neben der Strukturbrücke vor. Es steht jetzt in **Teil III**,
|
||
direkt hinter Mehrziel — thematisch gehört es zu „was optimiert man, wenn man die Zahlen
|
||
nicht kennt", und der Beispielfall ist eine Bäckerei, keine Finanzanwendung. In Teil IV
|
||
(Finanzmärkte) wäre es ein Fremdkörper gewesen.
|
||
|
||
**Der Kernbefund:** Vier Verfahren auf denselben Daten. Das Verfahren mit dem **besten MSE
|
||
hat die höchsten Kosten** (42,63 €/Tag); das kostenbeste hat einen um 13 % **schlechteren**
|
||
MSE und spart 10,7 %. Wer Prognosemodelle nach MSE auswählt — die Voreinstellung jeder
|
||
Modellauswahl-Bibliothek —, wählt hier das falsche.
|
||
|
||
Der Grund ist die Asymmetrie der Newsvendor-Kosten: Fehlmenge 6 €, Überhang 3 €, also liegt
|
||
das Optimum beim 66,7-%-Quantil und nicht beim Erwartungswert. Die Quantilregression schätzt
|
||
dieses Quantil direkt aus den Merkmalen.
|
||
|
||
**Warum ein pauschaler Sicherheitszuschlag zu kurz greift** — nachgerechnet statt behauptet:
|
||
Der verwendete Zuschlag beträgt 5,9 Stück, richtig wären 3,5 (normale Tage) und 12,3
|
||
(Aktionstage). Ein einziger Wert ist an ruhigen Tagen 1,7-mal zu groß und an unruhigen halb
|
||
so groß wie nötig.
|
||
|
||
**Ein eigener Fehler, der zum Kapitelabschnitt wurde:** Der erste Entwurf bewertete auf 230
|
||
Testtagen — dort hatte die Quantilregression den *besseren* MSE, und die Kernaussage stand
|
||
auf dem Kopf. Ausgemessen: Bei 180-Tage-Fenstern sieht das schlechtere Modell in 11,8 % der
|
||
Fälle besser aus, bei 730 Tagen nie. Daraus ist ein eigener Abschnitt geworden: Ein
|
||
Kennzahlenvergleich ohne Streuungsangabe ist keine Aussage.
|
||
|
||
### ✅ Kapitel „Supply-Chain und Energieeinsatz unter Unsicherheit“
|
||
|
||
Datei `36_Supply_Chain_Energie.md`, in Teil IV zwischen Strukturbrücke und Finanzdaten.
|
||
**Damit ist Phase 3 abgeschlossen.**
|
||
|
||
**Regel 9 war hier ausschlaggebend:** Der Plan sah „Netzwerkdesign mit
|
||
Standortentscheidungen" vor — aber Standortplanung kommt bereits in *fünf* Programmen vor
|
||
(`Solverstatus_und_Gap`, `Betriebsueberwachung`, `Big_M_Falle`, `Solverwechsel_CPSAT_HiGHS`,
|
||
`Benchmark_Skalierung`). Eine sechste Fassung wäre Wiederholung gewesen. Stattdessen:
|
||
**Kraftwerkseinsatzplanung** — im Buch bisher nur als Domäne erwähnt, nie modelliert.
|
||
|
||
**Der methodische Beitrag über {ref:kap:unsicherheit} hinaus:** Dort ist die erste Stufe des
|
||
zweistufigen Modells kontinuierlich. Hier ist sie **binär** — das An/Aus je Block und Stunde
|
||
steht am Vorabend fest, für alle Szenarien gleich, und lässt sich nicht anteilig anpassen.
|
||
|
||
**Die gemessenen Befunde:**
|
||
|
||
* Planung auf den Wind-Erwartungswert kostet **149 % mehr** und führt in **28 von 40**
|
||
Szenarien zum Lastabwurf; der schlimmste Tag ist sechsmal so teuer.
|
||
* Der zweistufige Plan braucht dafür nur **sechs zusätzliche Blockstunden**. Der Unterschied
|
||
ist im Commitment-Muster mit bloßem Auge sichtbar.
|
||
* Die CVaR-Schranke auf die nicht gedeckte Energie kostet bei realistischen 3 000 €/MWh
|
||
**nichts** — der risikoneutrale Plan hält sie bereits ein. Erst bei einem Marktpreisdeckel
|
||
von 300 €/MWh greift sie und kostet 844 €/Tag für 18,4 vermiedene MWh, also **46 € je
|
||
vermiedener MWh**.
|
||
|
||
Der dritte Punkt war zunächst ein enttäuschendes Ergebnis (die Schranke tat nichts) und ist
|
||
zum stärksten Befund des Kapitels geworden: **Eine Risikoschranke ändert genau dann etwas,
|
||
wenn der Schaden zu niedrig bepreist ist.** Bei realistischer Bewertung erledigt die
|
||
Minimierung der erwarteten Kosten die Absicherung von selbst — was die reale Lage der
|
||
Energiewirtschaft beschreibt, in der ein Marktpreisdeckel den volkswirtschaftlichen Schaden
|
||
nicht abbildet.
|
||
|
||
**Strukturelle Anpassung:** Teil IV hieß „Operations Research an den Finanzmärkten" und
|
||
enthält jetzt ein Energiekapitel. Er heißt daher **„Anwendungen — Energiewirtschaft und
|
||
Finanzmärkte"** (Label `{#teil:anwendungen}`, 6 Verweise umgestellt), und sein Vorspann
|
||
gliedert ihn in drei Abschnitte: Brücke, Industrieanwendung, Finanzanwendung. Wer mit
|
||
Produktion oder Energie zu tun hat, liest die ersten beiden und kann den Rest überspringen —
|
||
genau die Wahlmöglichkeit, die die Rezension eingefordert hat.
|
||
|
||
### ✅ Kapitel „Spaltengenerierung“ — der nachgeholte Punkt
|
||
|
||
Datei `25_Spaltengenerierung.md`, direkt hinter den Metaheuristiken. Damit ist der Plan-Punkt
|
||
„Metaheuristiken **& Dekomposition**" vollständig eingelöst, und **Phase 3 hat keine offenen
|
||
Punkte mehr**.
|
||
|
||
Als eigenes Kapitel und nicht als Abschnitt: Das Metaheuristik-Kapitel ist mit 1 516 Zeilen
|
||
bereits das zweitlängste, und Spaltengenerierung ist ein *exaktes* Verfahren — unter einer
|
||
Metaheuristik-Überschrift wäre es falsch einsortiert.
|
||
|
||
**Der Aufbau steht auf Vorhandenem.** Das Pricing-Teilproblem ist ein **Rucksack**
|
||
(`Rucksack.py`, {ref:kap:milp}), die Preise dafür sind die **Dualwerte** aus
|
||
{ref:sec:lp-dualitaet-und-schattenpreise}. Neu ist allein die Schleife — das Kapitel führt
|
||
nichts ein, was die Leser nicht schon hätten.
|
||
|
||
**Die gemessenen Befunde:**
|
||
|
||
* Die Musterzahl springt beim Halbieren der Breiten von 309 auf 46 408 (Faktor 150), die
|
||
Zahl der *gebrauchten* Muster bleibt bei knapp 30. Bei der schmalen Instanz werden
|
||
**0,06 %** aller Muster erzeugt.
|
||
* Bei breiten Zuschnitten spart das Verfahren **9 von 82 Rollen (11 %)**. Bei schmalen spart
|
||
es **nichts** — dort ist First-Fit bereits optimal.
|
||
* Der Unterschied hängt an **einer** Kennzahl: Stücke je Rolle (3,0 gegen 6,4).
|
||
|
||
**Der wertvollste Teil war ein Nebenprodukt.** Der letzte Punkt erklärt nachträglich Fund 16
|
||
(siehe unten): Der beim Metaheuristik-Kapitel verworfene Cutting-Stock-Vorversuch scheiterte
|
||
nicht am Zufall, sondern an genau dieser Kennzahl. Das Buch beantwortet damit eine Frage, die
|
||
es vorher offengelassen hatte — und die Antwort ist eine Zahl, die man vor einem Projekt
|
||
ausrechnen kann.
|
||
|
||
**Denkfehler:** die LP-Lösung naiv aufrunden. Gemessen 79 statt 73 Rollen (+8 %), weil nicht
|
||
*eine* Zahl aufgerundet wird, sondern dreizehn — je Muster eine. Der Kollege verwechselt
|
||
untere Schranke mit Gütegarantie; das ganzzahlige Master über dieselben Spalten kostet eine
|
||
Zeile und liefert 73.
|
||
|
||
**Beim Schreiben verhindert:** Zwei Sackgassen sind durch Vorabprüfung ausgeschlossen worden.
|
||
Erstens findet First-Fit bei Instanzen in Handrechnungsgröße *immer* das Optimum (vier
|
||
Varianten geprüft) — der Schnellstart durfte also nicht auf einem Vergleich beruhen und zeigt
|
||
stattdessen den Perspektivwechsel. Zweitens hätte ein Verweis auf einen „Abschnitt zur
|
||
Symmetrie" in {ref:kap:milp} ins Leere gezeigt; den gibt es nicht, die Erklärung steht jetzt
|
||
im Kapitel selbst.
|
||
|
||
---
|
||
|
||
## 6b. Phase 4 — Anhänge 🔄
|
||
|
||
### Anhang C: vom Fehlerzettel zum Diagnosehandbuch ✅
|
||
|
||
Anhang C listete elf Symptome (C1–C11) mit je einer Handvoll Prüfungen. Was fehlte, war der
|
||
Fall, in dem diese Prüfungen **alle durchlaufen und trotzdem nichts finden**. Genau dort
|
||
setzt der Ausbau an.
|
||
|
||
**Neues Programm `Konfliktsuche.py` — Deletion Filter (IIS).** Der Kern ist eine einzige
|
||
Regel: Nimm eine Bedingung heraus; bleibt der Rest unlösbar, wurde sie nicht gebraucht und
|
||
darf endgültig weg. Nach einem Durchlauf über alle $n$ Bedingungen ist die Restmenge
|
||
unreduzierbar — $n$ Solveraufrufe statt $2^n$ Teilmengen.
|
||
|
||
Die Instanz ist ein Produktionsplan mit 5 Variablen und 14 benannten Bedingungen. Gemessen:
|
||
|
||
| Befund | Zahl |
|
||
| --- | --- |
|
||
| naive Suche („jede Bedingung einmal weglassen") | **0 Treffer** |
|
||
| Deletion Filter | 14 Aufrufe → Konflikt aus **3** Bedingungen, nachweislich minimal |
|
||
| Konflikte insgesamt im Modell | **9** verschiedene, Größe 3 bis 5 |
|
||
| Steuerung über die Prüfreihenfolge | 2 von 4 gegen 1 von 3 veränderlichen Bedingungen im Befund |
|
||
|
||
**Der eigentliche Fund war Teil 5.** Die erste Fassung behauptete, die Prüfreihenfolge
|
||
beeinflusse das Ergebnis, und belegte das mit einem einzigen umgekehrten Durchlauf — der
|
||
zufällig **denselben** Konflikt lieferte. Erst 200 gewürfelte Reihenfolgen zeigten, dass das
|
||
Modell neun verschiedene minimale Konflikte enthält. Die Behauptung stimmte, der Beleg nicht;
|
||
aus der Messung wurde ein eigener Programmteil und die praktisch wichtigste Aussage des
|
||
Abschnitts: **„den" kleinsten Konflikt gibt es nicht.**
|
||
|
||
Daraus folgt ein Praxisgriff, der ebenfalls gemessen ist: Wer die *unveränderlichen*
|
||
Bedingungen zuerst prüfen lässt, bekommt sie eher aus dem Befund heraus und sieht dafür mehr
|
||
Stellschrauben. Beide Befunde sind wahr — nur einer nennt dem Planer etwas, das er tun kann.
|
||
|
||
**Abgrenzung, die Regel 9 erzwungen hat.** `Infeasibility_Diagnose.py` (Kap. Praxisfallen)
|
||
war zuerst zu lesen. Es macht hierarchische Relaxation mit CP-SAT: *„was tun wir jetzt?"*.
|
||
Der Deletion Filter beantwortet *„was widerspricht sich?"* — andere Frage, andere
|
||
Solverfamilie, andere Instanz. Beide verweisen jetzt aufeinander, statt sich zu wiederholen.
|
||
|
||
**Zwei Anschlüsse an vorhandene Programme ergänzt:**
|
||
|
||
- **C6 (falsche Dualwerte)** setzte stillschweigend voraus, dass es *einen* richtigen
|
||
Schattenpreis gibt. Neue vierte Prüfung: Bei Entartung gibt es den nicht, der Wert ist
|
||
nicht falsch, sondern mehrdeutig — Verweis auf die Dualspannen aus
|
||
`Toleranzen_und_Entartung.py`.
|
||
- **C5 (instabile Lösung)** war ein reiner Finanzabschnitt (Kovarianzmatrix, Shrinkage).
|
||
Neuer Teil „Derselbe Fehler außerhalb der Finanzwelt": Kondition der Restriktionsmatrix,
|
||
Größenordnungen der Koeffizienten, Ruiz-Equilibrierung aus `Skalierung_Kondition.py`.
|
||
|
||
### Anhang-Dateinamen: den Buchstaben herausgenommen ✅
|
||
|
||
Die Dateien hießen `90_Anhang_A_Loesungen.md` … `93_Anhang_D_Glossar_und_Literatur.md`. Der
|
||
Buchstabe ist abgeleitete Information — dieselbe Sorte, die in diesem Projekt schon zweimal
|
||
gerottet ist (die Überschriften in Anhang A, die Kapitelnummern in 66 Docstrings). Ein neuer
|
||
Anhang vor dem Glossar hätte ihn in jedem Dateinamen falsch gemacht.
|
||
|
||
Jetzt heißen sie `90_Anhang_Loesungen.md`, `91_Anhang_Modellierungsmuster.md`,
|
||
`92_Anhang_Fehlerdiagnose.md`, `93_Anhang_Spickzettel.md`, `94_Anhang_Glossar_und_Literatur.md`
|
||
— Ordnungspräfix und Thema, kein Buchstabe. Denselben Grundsatz halten die Programmdateien
|
||
schon („`Sensitivitaetsanalyse.py`, nicht `Kap_04_…`"). 22 Dateien mit Verweisen mitgezogen;
|
||
die HTML-Seitennamen sind unberührt, weil `seiten_dateiname()` sie aus dem
|
||
`{#anhang:label}`-Slug bildet.
|
||
|
||
### Anhang D (neu): Spickzettel der Solver ✅
|
||
|
||
Sechs Seiten statt der geplanten fünf — `pywraplp` fehlte im Plan, steht aber in sechs
|
||
Programmen des Buchs. Reihenfolge nach tatsächlicher Verwendung: SciPy (21 Programme),
|
||
HiGHS (14), OR-Tools/`pywraplp` (6), CP-SAT (16), CVXPY (16), Pyomo/Linopy (je 1).
|
||
|
||
Jede Seite hat denselben Aufbau, damit sie sich nebeneinanderlegen lassen: wofür (und wofür
|
||
nicht), Modell, Variablen, Nebenbedingungen, Lösen, **alle** Statusfälle (Regel 5), Lösung
|
||
auslesen, drei Stolpersteine.
|
||
|
||
**Alle Schnipsel lösen dasselbe Problem** — das Produktionsprogramm aus dem
|
||
Ökosystem-Schnellstart mit dem bekannten Optimum 530 und den Schattenpreisen 12 und 1. Damit
|
||
ist jeder Schnipsel selbstprüfend. **Jeder wurde einzeln in einem eigenen Prozess
|
||
ausgeführt** (ortools und highspy vertragen sich nicht, siehe C9); alle sieben liefern 530
|
||
bzw. die CP-SAT-eigene Instanz. Ausnahme mit Ansage: CP-SAT bekommt ein ganzzahliges
|
||
Beispiel, weil ein stetiges LP dort das falsche Werkzeug wäre.
|
||
|
||
Abgegrenzt (Regel 9): `Ein_System_Vier_Ansaetze.py` zeigt dieselben Bibliotheken im
|
||
*Vergleich*, `Modellierungsschichten.py` die beiden Modellierungssprachen. Der Anhang ist
|
||
die *Nachschlagefassung* und verweist auf beide, statt sie abzuschreiben.
|
||
|
||
### Anhang B: drei Rezeptkarten — und eine Tabelle voller falscher Zahlen ✅
|
||
|
||
**Der eigentliche Fund war die Übersichtstabelle.** Sie nannte zu jedem der 24 Muster ein
|
||
Kapitel als Literalzahl: B1 „5.3", B18 „15.1", B21 „9.4". Nach den acht in Phase 3
|
||
eingefügten Kapiteln war **jede einzelne dieser Zahlen falsch** — MILP steht heute bei 6,
|
||
Praxisfallen bei 22, Unsicherheit bei 12. Ein Leser, der B18 nachschlägt, landete in
|
||
{ref:kap:prognose}. Die Spalte ist jetzt auf `{ref:kap:…}` umgestellt und rechnet sich beim
|
||
Bauen selbst aus.
|
||
|
||
**Drei neue Karten**, jeweils erst gegen den Bestand geprüft (Regel 9):
|
||
|
||
- **B25 Mindestabnahmemenge über einen Zeitraum.** B2 hatte die Struktur („entweder 0 oder
|
||
mindestens L"), aber nicht den Jahresvertrag: **ein** Schalter für den ganzen Zeitraum,
|
||
nicht einer je Periode. Die Verwechslung erzeugt ein unlösbares Modell, und man sucht den
|
||
Fehler dann in den Daten. Dazu die vergessene obere Kopplung — Trickle Flow andersherum.
|
||
- **B26 Rüstzeit als Kapazitätsverbrauch.** B17 verbucht den Wechsel in der *Zielfunktion*.
|
||
Sobald die Maschine ausgelastet ist, gehört er zusätzlich in die *Kapazitätszeile* — sonst
|
||
verteilt das Modell die Produktion auf viele kleine Lose, weil ein Wechsel zwar Geld, aber
|
||
keine Zeit kostet. Der Plan geht auf dem Papier auf und in der Schicht nicht.
|
||
- **B27 Budgetlimit.** War überhaupt nicht abgedeckt. Kern der Karte ist die Frage, die die
|
||
Geschäftsführung stellt („was bringt der nächste Euro?"): Beim LP ist das der
|
||
Schattenpreis, bei Ganzzahligkeit gibt es ihn **nicht** — dort mit erhöhtem Budget neu
|
||
rechnen. Und: Die LP-Relaxation verspricht zu viel, weil sie die letzte Maßnahme anteilig
|
||
kaufen darf.
|
||
|
||
---
|
||
|
||
## 6c. Phase 5 — Medien, Rahmen, Gesamtbau 🔄
|
||
|
||
### Formel-Übersetzer in Markowitz und CVaR ✅
|
||
|
||
Der letzte offene Punkt aus Phase 1. Beide Kapitel hatten je drei gute 📐-Lesehilfen — die
|
||
erklären die *Idee*. Was fehlte, war an beiden Stellen dasselbe: die **Notation** selbst.
|
||
|
||
- **{ref:kap:markowitz}:** Das Kapitel beginnt mit der ausgeschriebenen Zweititelformel und
|
||
springt dann zu $\mathbf{w}^\top\boldsymbol{\Sigma}\mathbf{w}$, ohne die beiden zu
|
||
verbinden. Die neue Karte löst das Matrixprodukt in die Doppelsumme über alle Paare auf und
|
||
zeigt, dass es für $n=2$ Buchstabe für Buchstabe die Eingangsformel ist. Nachgerechnet:
|
||
Eigenrisiken $0{,}0288$, Zusammenspiel $-0{,}0115$, Summe $0{,}0173$ → $\sigma_p = 13{,}15\,\%$,
|
||
also unter **beiden** Einzelrisiken (20 % und 30 %).
|
||
- **{ref:kap:cvar}:** Der Bruch $\frac{1}{S(1-\alpha)}$ war nirgends erklärt. Die Karte
|
||
übersetzt jedes Zeichen und benennt die teuerste Verwechslung: $\alpha$ und $1-\alpha$ zu
|
||
vertauschen wirft **keine Fehlermeldung**. An 2 000 simulierten Renditen mit Fat Tails
|
||
gemessen: richtig 3,43 % (Mittel über 100 Szenarien), vertauscht 0,18 % (über 1 900) —
|
||
Faktor **19**, und die zweite Zahl ist überhaupt kein Randmaß mehr.
|
||
|
||
### Lernpfade an den Stand nach Phase 3/4 angepasst ✅
|
||
|
||
Die Wegweiser stammten aus der Zeit der 15 Kapitel und waren an drei Stellen falsch:
|
||
|
||
- **Pfad A** endete bei {ref:kap:praxisfallen} — seit Phase 3 gibt es danach noch
|
||
{ref:kap:testing}. Auch die Zeitangabe (60–80 h) passte nicht mehr zu 23 Kapiteln.
|
||
- **Pfad B/C** verwiesen auf „{ref:kap:fundament} (nur 2.1–2.3)" — harte Abschnittsnummern
|
||
(Regel 1), die inzwischen auf Schnellstart und Lernziele zeigten statt auf den Stoff.
|
||
Jetzt `{ref:sec:…}`-Verweise.
|
||
- **Keiner** der Pfade kannte die acht neuen Kapitel. B und C bekommen jetzt den Hinweis auf
|
||
{ref:kap:metaheuristiken} bzw. {ref:kap:dekomposition} für den Fall, dass der Solver an der
|
||
echten Instanz stehen bleibt; **Pfad F (neu)** führt vom Prototyp in den Betrieb
|
||
({ref:kap:praxisfallen} → {ref:kap:testing} → Fehlerdiagnose). Pfad E nennt zusätzlich den
|
||
neuen Spickzettel.
|
||
|
||
### Aufgaben, Handrechnungen und Abbildungen: die letzten 153 harten Nummern ✅
|
||
|
||
Der im vorigen Schritt gefundene Defekt ist behoben — und zwar nach derselben Regel wie
|
||
Kapitel und Abschnitte: **die Zahl steht nicht mehr im Quelltext.**
|
||
|
||
**Im Quelltext** steht jetzt `**Aufgabe ⭐ — Titel.**`, `> **✏️ Handrechnung: Titel**` und
|
||
`` — genau so, wie es die acht in Phase 3 geschriebenen Kapitel schon
|
||
hielten. **Beim Bauen** vergibt die neue Funktion `nummeriere_marken()` daraus
|
||
`Aufgabe 5.3`, `Handrechnung 5.1`, `Abb. 5.2`.
|
||
|
||
Sie liest den Kapitelpräfix aus den bereits gesetzten `# Kapitel N:`-Überschriften, genau wie
|
||
`nummeriere_abschnitte()`. Deshalb genügt **ein** Aufruf für beide Baupfade: im
|
||
Gesamtdokument aus `resolve_numbering()`, im Mehrseiten-Bau aus `resolve_numbering_seite()`,
|
||
wo die Kapitelüberschrift zu diesem Zeitpunkt schon nummeriert auf der Seite steht. Beide
|
||
Pfade liefern nachweislich dieselben Nummern.
|
||
|
||
Zwei Entscheidungen, die den Rest der Arbeit bestimmt haben:
|
||
|
||
- **Codeblöcke werden übersprungen.** Was in einem Programm steht, löst
|
||
`extract_programme_04.py` als Datei heraus; eine Nummer, die nur der Buchsatz kennt, würde
|
||
Buch und Programm auseinanderlaufen lassen (Regel 6). Also dürfen Programme solche Nummern
|
||
gar nicht erst enthalten.
|
||
- **Bilder vor dem ersten Kapitel bleiben unnummeriert** — die beiden
|
||
Entscheidungsdiagramme unter den Teil-Überschriften. Dieselbe Regel wie bei Abschnitten.
|
||
|
||
**Was dafür umgestellt werden musste:**
|
||
|
||
| Was | Anzahl |
|
||
| --- | ---: |
|
||
| Marken entnummeriert (97 Aufgaben, 15 Handrechnungen, 23 Abbildungen) | 135 |
|
||
| Querverweise von Nummern auf **Namen** umgestellt | 21 |
|
||
| Kapitelnummern in Code-Kommentaren durch Kapitel**namen** ersetzt (Regel 2) | 12 |
|
||
| Stellen in `README.md` / `README_Programme.md` | 3 |
|
||
|
||
**Der zweite Fund dabei:** Die Programme trugen nicht nur Handrechnungsnummern, sondern auch
|
||
`Kapitel N` in Kommentaren. Zwei davon waren bereits falsch — `Installationstest.py` nannte
|
||
„nur Kapitel 11-14" für die Livedaten (heute 18–21), `Renditeschaetzung_Falle.py` verwies auf
|
||
„Kapitel 11" statt auf das Kapitel Finanzdaten (18). Auch die beiden READMEs führten noch
|
||
Lernpfade über „Kapitel 1 bis 15".
|
||
|
||
**Drei Programme drucken den geänderten Text**, ihre abgedruckte Ausgabe wurde gegen einen
|
||
echten Lauf gediffed (Regel 4): `Installationstest.py`, `Dualitaet_Nachweis.py`,
|
||
`Diversifikation_Demo.py`.
|
||
|
||
**Der Quelltext ist jetzt vollständig frei von abgeleiteten Nummern.** Die Gegenprobe
|
||
|
||
```bash
|
||
grep -nE '\b(Aufgabe|Handrechnung|Abschnitt|Kapitel) [0-9]+(\.[0-9]+)?\b' \
|
||
Operations_Research_mit_Python_Version_04/*.md
|
||
```
|
||
|
||
findet nichts mehr. Sie gehört in jede spätere Wiederaufnahme.
|
||
|
||
### Rahmen: Notation und Projektwerkstatt an die neuen Kapitel angeschlossen ✅
|
||
|
||
Zwei Wegweiser, die von Phase 3 überholt worden waren.
|
||
|
||
**Notation und Abkürzungen.** Das Verzeichnis kannte **kein einziges** Symbol der acht neuen
|
||
Kapitel. Ergänzt als neuer Abschnitt 1.5 (Verfahrenskapitel): Reihenfolge $\pi$,
|
||
Zugbewertung $\Delta$, Temperatur $T$ und das Metropolis-Kriterium
|
||
({ref:kap:metaheuristiken}); Dualpreis $\pi_i$, Musterspalte $a_i$ und reduzierte Kosten
|
||
({ref:kap:dekomposition}); Pareto-Dominanz und die $\varepsilon$-Schranke
|
||
({ref:kap:mehrziel}); das Dach $\hat{d}_t$ für Prognosewerte ({ref:kap:prognose});
|
||
Anfahr-/Einsatzvariable, Mindestlaufzeit und der Fensterindex $\tau$
|
||
({ref:kap:supplychain}). Sieben Abkürzungen kamen dazu: SA, LNS, CG, IIS, MSE, GIL, JSON.
|
||
|
||
Dabei ist $\pi$ als **neue Doppelbelegung** aufgefallen — Reihenfolge in
|
||
{ref:kap:metaheuristiken}, Dualpreis in {ref:kap:dekomposition}. Beide Bedeutungen sind in
|
||
der Literatur so verbreitet, dass ein Ausweichbuchstabe mehr verwirren würde; der Fall steht
|
||
jetzt in der Warnbox neben $\lambda$, $\gamma$ und $S$.
|
||
|
||
**Dritter Fundort derselben Rostspur.** Die Abkürzungstabellen führten eine Spalte
|
||
„Kapitel" mit nackten Zahlen: LP → 4, VaR → 13, TSP → 7. Alles V03-Nummern, alle falsch.
|
||
Die vorherige `grep`-Gegenprobe hatte sie nicht gefunden, weil dort die Zahl **allein in
|
||
einer Tabellenzelle** steht, nicht als „Kapitel N" im Fließtext. **38 Zellen** auf
|
||
`{ref:kap:…}` umgestellt. Dieselbe Bauform fand sich noch einmal in der Projektwerkstatt
|
||
(„**Kapitel:** 1, 5, 6, 15" in den Projektköpfen, plus eine Tabellenspalte in P8) — weitere
|
||
**9 Stellen**.
|
||
|
||
**Projektwerkstatt: drei neue Projekte.** Für Metaheuristik, Mehrziel und Deployment gab es
|
||
keinen Projektausgang. Neu sind:
|
||
|
||
- **P9 — Wenn der Solver aussteigt.** P3 mit 400 statt 30 Kunden: exakt gegen heuristisch
|
||
unter gleichem Zeitbudget, gesucht ist der **Umschlagpunkt**, nicht der Sieger. Mit der
|
||
Falle, in die hier fast jeder tappt — die Heuristik nur dort zu testen, wo man das Optimum
|
||
nicht kennt.
|
||
- **P10 — Zwei Ziele, eine Entscheidung.** Kosten gegen CO₂ über das
|
||
$\varepsilon$-Constraint-Verfahren. Ergebnis ist eine Kurve, keine Lösung; abzunehmen ist
|
||
unter anderem der Nachweis, welche Punkte eine gewichtete Summe **niemals** findet.
|
||
- **P11 — Vom Skript zum Dienst.** Kein neues Modell, sondern der Schritt danach: eines der
|
||
eigenen Projekte P1–P10 in vier Schichten übergeben (Domäne, Kern, Tests, HTTP). Prüfstein
|
||
ist, ob eine unbeteiligte Person es allein zum Laufen bringt.
|
||
|
||
Vorwort (Projektzahl, Pfade B/C/F mit P9 und P11 verknüpft) mitgezogen.
|
||
|
||
**Nebenbei in `PLAN.md` korrigiert:** Die Abnahmeliste in Abschnitt 10 rief
|
||
`pytest …/Programme/tests -q` auf — dieses Verzeichnis gibt es nicht. Die Suite heißt
|
||
`test_or_kern.py` und liegt flach im Programme-Verzeichnis; sie läuft grün (33 Tests).
|
||
|
||
### Die vier fehlenden Diagramme ✅
|
||
|
||
`PLAN.md` Abschnitt 7 nennt sieben neue Diagramme für Phase 5. Drei standen bereits (zwei
|
||
Entscheidungs-Flussdiagramme, Pareto-Front), vier fehlten. Sie sind jetzt da — jedes mit
|
||
Generatorskript, und die beiden gemessenen rechnen **dieselbe Instanz** wie das zugehörige
|
||
Buchprogramm:
|
||
|
||
- **Konditionszahl-Experiment** ({ref:kap:fundament}), `erzeuge_kondition.py`. Links die
|
||
gemessene Fehlerverstärkung gegen $\kappa(A)$ über zwölf Größenordnungen: Sie liegt
|
||
**auf** der theoretischen Schranke — $\kappa$ ist keine Faustregel, sondern wird im
|
||
schlimmsten Fall vollständig ausgeschöpft. Rechts die Ruiz-Equilibrierung,
|
||
$8{,}67\cdot10^{10} \to 4{,}69\cdot10^{1}$.
|
||
**Die knifflige Stelle:** `Skalierung_Kondition.py` zieht die Mischmodell-Matrix aus
|
||
*demselben* Generator, aus dem vorher schon die Störungen von Experiment 1 kamen. Der
|
||
Generator spielt die Ziehungsreihenfolge deshalb exakt nach — sonst zeigte das Diagramm
|
||
eine andere Konditionszahl als der abgedruckte Programmlauf.
|
||
- **Metaheuristik-Landschaft** ({ref:kap:metaheuristiken}),
|
||
`erzeuge_metaheuristik_landschaft.py`. Eine Landschaft über $200!$ Punkten lässt sich nicht
|
||
zeichnen — wohl aber, wie die Suche sie durchläuft. Aufgezeichnet wird der **aktuelle**
|
||
Zustand, nicht der beste: Der beste fällt bei jedem Verfahren monoton, und was die
|
||
Verfahren unterscheidet, ist gerade, wie weit sie sich unterwegs entfernen dürfen. Alle
|
||
Werte reproduzieren die Kapiteltabelle (1 049 / 917 / 907 und die Temperaturreihe
|
||
919 / 907 / 914 / 939 / 1 041).
|
||
- **Clean-Architecture-Schichten** ({ref:kap:praxisfallen}) und
|
||
**Dienst-Ablauf** ({ref:kap:testing}), beide in `erzeuge_architektur_diagramme.py`
|
||
(Graphviz, Palette der Entscheidungsdiagramme). Sie bilden ab, was in `or_kern.py` und
|
||
`Optimierungsdienst.py` **tatsächlich steht**, kein allgemeines Schema: beim ersten steckt
|
||
die Aussage in der Pfeilrichtung (alles zeigt nach innen, die Domäne auf nichts), beim
|
||
zweiten in der 202-Antwort.
|
||
|
||
Alle vier sind in die Kapitel eingebaut und werden von `nummeriere_marken()` automatisch
|
||
nummeriert (Abb. 2.3, 9.1, 22.3, 23.1).
|
||
|
||
### Plotly-Figuren: von zwei auf vier ✅
|
||
|
||
Maßstab war, ob der Hover **echte Zusatzinformation** trägt statt Kosmetik. Zwei Figuren
|
||
haben ihn erfüllt:
|
||
|
||
- **Suchverlauf** ({ref:kap:metaheuristiken}). Die Rechnung lag durch das
|
||
Landschafts-Diagramm schon vor; ergänzt wurde nur die Temperaturaufzeichnung. Im Hover
|
||
steht nicht bloß der Zielwert, sondern die **Temperatur an dieser Stelle** und — daraus
|
||
gerechnet — mit welcher Wahrscheinlichkeit dort ein Zug typischer Größe (Median 70 Minuten,
|
||
im Kapitel gemessen) noch angenommen würde. Damit wird an der Kurve ablesbar, *warum*
|
||
$T_0 = 8$ so lange braucht, um zurückzufinden.
|
||
- **Effizienzlinie mit und ohne Hausregeln** ({ref:kap:markowitz}), neues Skript
|
||
`erzeuge_effizienzlinie.py` (statisch **und** interaktiv). Im Hover stehen die **Gewichte**
|
||
des jeweiligen Portfolios plus die Sektorsummen — man sieht die Zusammensetzung entlang der
|
||
Linie wandern und erkennt, wo ein Titel an seine 20-%-Grenze stößt.
|
||
|
||
**Der Befund der neuen Figur.** Der senkrechte Abstand zwischen freier und beschränkter Linie
|
||
beträgt höchstens **0,58 Prozentpunkte** — das ist, was die meisten erwarten. Der eigentliche
|
||
Einschnitt steht am oberen Ende: Ohne Grenzen erreicht das Portfolio **10,55 %**, mit
|
||
20-%-Grenze und Sektorlimits ist bei **9,06 %** Schluss. Diese **1,50 Punkte** sind nicht
|
||
teuer erkauft, sondern überhaupt nicht mehr erreichbar — auch nicht gegen mehr Risiko. Das
|
||
Kapitel hatte diese Frage bisher nur als Übungsaufgabe gestellt.
|
||
|
||
**Warum hier eine feste Instanz.** `Markowitz_CVXPY.py` lädt Livedaten; ein Diagramm daraus
|
||
sähe an jedem Tag anders aus. Die Figur rechnet deshalb ein festes Universum aus einem
|
||
Faktormodell — dieselbe Struktur und dieselben Regeln wie im Kapitel, aber reproduzierbar.
|
||
Das steht so auch im Buch, damit niemand die Zahlen für einen Livebefund hält. Das vorhandene
|
||
`kap12_efficient_frontier.svg` bleibt unangetastet: Es ist eine schematische Lehrbuchskizze,
|
||
keine Rechnung, und kann deshalb nicht driften.
|
||
|
||
### Eine harte Nummer über zwei Zeilen — und die Prüfung dagegen ✅
|
||
|
||
Beim Einbau fiel auf, dass der Docstring von `Diversifikation_Demo.py` noch
|
||
`(Handrechnung\n12.1)` trug — die Zahl stand hinter einem Zeilenumbruch, und die
|
||
zeilenweise `grep`-Gegenprobe der vorigen Runde lief daran vorbei.
|
||
|
||
`pruefe_dateien()` prüft das jetzt selbst, mit `\s+` statt eines Leerzeichens, und meldet
|
||
Datei, Zeile und Fund. Damit ist die Gegenprobe nicht mehr etwas, woran man denken muss,
|
||
sondern Teil von `--check`. Gegengetestet: Eine künstlich eingefügte zweizeilige
|
||
`Aufgabe\n4.3` wird gefunden.
|
||
|
||
### Die fünfte harte Nummer: eine Tabellenzelle — gefunden in Phase 6 ✅
|
||
|
||
Beim Lesen von `31_Unsicherheit.md` für den Chance-Constraints-Abschnitt stand in der
|
||
Übersichtstabelle „Die drei Ansätze im Überblick“ die Zeile
|
||
|
||
```
|
||
| Kapitel | 9.2 | 9.3 | 9.4 |
|
||
```
|
||
|
||
Die Abschnitte heißen in Version 04 aber **12.4 bis 12.6** — es waren stehengebliebene
|
||
Version-03-Nummern, im PDF abgedruckt. Damit ist die Regel „keine abgeleiteten Zahlen im
|
||
Quelltext“ zum **fünften** Mal verletzt gefunden worden.
|
||
|
||
**Warum `--check` daran vorbeilief:** Der Ausdruck von damals verlangt das Wort unmittelbar
|
||
vor der Zahl (`(Kapitel|Abschnitt|…)\s+\d+`). Hier steht „Kapitel“ als *Zeilenkopf* und ist
|
||
durch ` | ` von der Zahl getrennt — `\s+` matcht keinen senkrechten Strich. Die Verschärfung
|
||
der vorigen Runde (Zwischenraum statt Leerzeichen) war also richtig und trotzdem nicht genug.
|
||
|
||
`pruefe_dateien()` hat jetzt eine **zweite** Prüfung: eine Tabellenzelle, die nur aus einer
|
||
Nummer der Form `N.N` besteht. Sie überspringt Codezäune — abgedruckte Programmausgaben
|
||
setzen ihre Spalten ebenfalls mit `|` und enthalten legitime Zahlen (`40_Finanzdaten.md`
|
||
Zeilen 381–385 wären sonst fünf Fehlalarme). Gegengetestet: Die wieder eingebaute Zeile wird
|
||
mit allen drei Zellen einzeln gemeldet, nach dem Rückbau ist der Lauf sauber.
|
||
|
||
Die Zeile heißt jetzt „Nachzulesen in“ und trägt drei `{ref:sec:…}` — Querverweise 703 → 706.
|
||
|
||
**Die Lehre:** Die vier früheren Fälle steckten alle im Fließtext oder in Docstrings. Dieser
|
||
saß in einer Tabelle, also in genau der Struktur, die man beim Korrekturlesen überfliegt.
|
||
Beide Prüfungen zusammen decken Fließtext, Docstrings und Tabellen ab.
|
||
|
||
### VaR/CVaR-Diagramm — und ein Widerspruch, den es aufgedeckt hat ✅
|
||
|
||
Beim Nachbau des ersten Finanzdiagramms stellte sich heraus, dass **Bild und Text
|
||
verschiedene Zahlen nannten**: Der Kapiteltext sagte „höchstens 2,3 %" (VaR) und „im Mittel
|
||
3,8 %" (CVaR), die Abbildung zeigte rund 3,1 % und 4,2 %. Beide stammten aus unbekannten,
|
||
nicht reproduzierbaren Läufen — genau der Schaden, gegen den die Generatorskripte
|
||
geschrieben werden.
|
||
|
||
`erzeuge_var_cvar.py` rechnet jetzt beides aus **einer** Instanz: 20 000 Tage aus dem
|
||
Fat-Tail-Modell des Kapitels ($t$-Verteilung mit drei Freiheitsgraden, wie in
|
||
`VaR_CVaR_Demo.py`), und die Kennzahlen über dieselben Funktionen wie dort — den CVaR also
|
||
über Rockafellar-Uryasev, nicht über den naheliegenden, aber falschen Mittelwert aller Werte
|
||
jenseits des VaR. Ergebnis: **VaR 1,86 %, CVaR 2,99 %.** Der Kapiteltext nennt diese Zahlen
|
||
jetzt und verweist auf das Skript.
|
||
|
||
**Eine Darstellungsentscheidung, die dokumentiert gehört:** $t(3)$ hat keine endliche
|
||
Wölbung. Ungeschnitten quetscht ein einzelner Tag bei $+70\,\%$ die ganze Verteilung zu
|
||
einem Strich. Die x-Achse ist deshalb auf $\pm 8\,\%$ beschnitten — und im Bild steht, wie
|
||
viele Tage draußen liegen (32) und wie schlecht der schlechteste war ($-23{,}0\,\%$). Das
|
||
Beschneiden verschweigt die Ausreißer nicht, sondern macht sie zum Argument: Genau diese
|
||
Tage zählt der VaR nicht mit.
|
||
|
||
Das alte `kap13_var_cvar.svg` ist damit unreferenziert. Es wurde **nicht gelöscht** — das
|
||
wäre eine Entscheidung des Autors, nicht meine.
|
||
|
||
### Die Priorisierung der Altbestände war falsch begründet — korrigiert
|
||
|
||
Die Reihenfolge im vorigen Eintrag ging davon aus, die vier „Finanzdiagramme" enthielten
|
||
Messwerte. Beim Ansehen stellte sich heraus: **Nur eines von ihnen rechnet.**
|
||
`kap11_ledoit_wolf_shrinkage` ist ein Kastendiagramm, `kap13_cvar_zielfunktion` eine
|
||
beschriftete Formel, `kap12_efficient_frontier` eine idealisierte Lehrbuchskizze mit runden
|
||
Zahlen. Alle drei können nicht driften.
|
||
|
||
Das brauchbare Kriterium ist nicht das Kapitel, sondern: **Zeigt das Bild Zahlen, die aus
|
||
einem Modell stammen?** Danach neu sortiert:
|
||
|
||
| Gruppe | Bilder | Warum |
|
||
| --- | --- | --- |
|
||
| **rechnet, Generator lohnt** | `kap02_feasible_region`, `kap02_konvexitaet`, `kap04_simplex_ecken`, `kap05_lp_vs_integer`, `kap05_branch_and_bound`, `kap07_min_cost_flow`, `kap07_vrp_touren`, `kap08_kkt_geometrie` | Ecken, Schranken, Flüsse und Touren stammen aus einem konkreten Modell. Weicht das Kapitel ab, lügt das Bild — `kap04_simplex_ecken` behauptet etwa C = (4,8; 4,8) als Optimum. |
|
||
| **halb** | `kap06_cp_sat_propagation`, `kap10_zustandskette` | Ablaufbilder mit einzelnen Zahlen; Nutzen mittel. |
|
||
| **schematisch, kein Generator nötig** | `kap01_analytik_reifegrad`, `kap01_vier_bausteine`, `kap03_solver_architektur`, `kap09_unsicherheit_ansaetze`, `kap11_ledoit_wolf_shrinkage`, `kap12_efficient_frontier`, `kap13_cvar_zielfunktion`, `kap14_engine_overview`, `kap15_or_plattform`, `kap15_praxisfallen` | Kästen, Pfeile, beschriftete Formeln. Es gibt nichts, was mit einem Programm auseinanderlaufen könnte. |
|
||
|
||
### Die beiden Polyeder-Bilder — und wofür die Generatoren eigentlich da sind ✅
|
||
|
||
Der bisher deutlichste Fall. `kap02_feasible_region` (Fundament) und `kap04_simplex_ecken`
|
||
(LP) zeigten beide **dasselbe fremde LP**:
|
||
|
||
$$2x_1+3x_2 \le 24,\qquad 3x_1+2x_2 \le 24,\qquad \text{Optimum } (4{,}8;\ 4{,}8)$$
|
||
|
||
Dieses Modell kommt im ganzen Buch nicht vor. Beide Kapitel rechnen an dieser Stelle das
|
||
**Bot-Allokationsproblem** aus {ref:kap:einfuehrung}:
|
||
|
||
$$\max\ 150x_1+250x_2 \quad\text{u.d.N.}\quad 2x_1+5x_2 \le 40,\ 4x_1+6x_2 \le 60,\ x_1 \le 8$$
|
||
|
||
mit fünf Ecken und dem Optimum $(7{,}5;\ 5)$, $Z = 2\,375$ €. Das Simplex-Bild stand
|
||
**unmittelbar über** der Handrechnung, die genau diese Zahlen herleitet — und behauptete
|
||
daneben ein anderes Optimum. Dass beide Modelle zufällig eine Ecke bei $x_1 = 8$ und eine bei
|
||
$x_2 = 8$ haben, machte den Widerspruch unauffällig.
|
||
|
||
`erzeuge_polyeder.py` erzeugt jetzt beide Bilder aus dem richtigen Modell. Die Ecken sind
|
||
**nicht eingetragen**, sondern nach demselben Verfahren berechnet wie in
|
||
`Visualisierung_Loesungsraum.py` (zulässige Schnittpunkte je zweier Begrenzungsgeraden) — die
|
||
Koordinaten im Bild stammen damit aus derselben Rechnung wie die abgedruckte Tabelle. Der
|
||
Simplex-Weg ist der Weg der Handrechnung, $(0;0) \to (0;8) \to (7{,}5;5)$, und das Skript
|
||
**prüft vor dem Zeichnen**, dass jeder Wegpunkt tatsächlich eine Ecke des Polyeders ist.
|
||
|
||
**Das ist der Beleg dafür, dass die Generatoren nicht Kosmetik sind.** Drei der fünf bisher
|
||
nachgebauten Bilder haben beim Nachrechnen einen Fehler oder Widerspruch zutage gefördert:
|
||
die VaR/CVaR-Zahlen, und jetzt zweimal ein komplett fremdes LP. Keiner dieser Fälle wäre
|
||
durch Hinsehen aufgefallen — alle drei sahen plausibel aus.
|
||
|
||
Das alte `kap02_feasible_region.svg` und `kap04_simplex_ecken.svg` sind damit unreferenziert;
|
||
gelöscht wurde wie zuvor nichts.
|
||
|
||
### Die beiden MILP-Bilder — dritter und vierter Fall, plus ein Zählfehler ✅
|
||
|
||
Wie erwartet zeigten auch `kap05_lp_vs_integer` und `kap05_branch_and_bound` fremde Modelle.
|
||
|
||
**`kap05_lp_vs_integer`** behauptete ein LP-Optimum $(1{,}8;\ 3{,}4)$ mit $Z = 42{,}6$ und ein
|
||
ganzzahliges Optimum $(3;1)$ mit $Z = 39$ — **ohne die Zielfunktion zu nennen**, sodass sich
|
||
nichts davon nachprüfen ließ. Rechnet man die Koeffizienten aus beiden Punkten zurück
|
||
($\approx 10{,}7$ und $\approx 6{,}9$), wäre die eingezeichnete Ecke $(4;0)$ mit
|
||
$Z \approx 42{,}9$ besser gewesen als das angebliche ganzzahlige Optimum. Das Bild war also
|
||
vermutlich auch in sich unstimmig.
|
||
|
||
`erzeuge_runden.py` zeichnet stattdessen zwei Dinge aus dem Kapitel selbst: links die
|
||
Handrechnung ($\max x_1+x_2$ u.d.N. $2x_1+2x_2 \le 3$), bei der die Zielfunktion **parallel**
|
||
zur Restriktion läuft — deshalb ist die ganze Kante optimal, Aufrunden auf $(1;1)$ liegt
|
||
außerhalb, Abrunden auf $(0;0)$ liefert null. Rechts die Messung aus
|
||
`Runden_Gegenbeispiel.py`, mit derselben Saat nachgerechnet; alle zwölf Werte reproduzieren
|
||
die abgedruckte Tabelle (82,0 / 17,9 / 3,1 % usw.).
|
||
|
||
**`kap05_branch_and_bound`** zeigte einen Baum mit Wurzel $x_1 = 2{,}7$, Schranke $150{,}4$
|
||
und Incumbent $138$ — zu einem Modell, das nirgends im Buch steht.
|
||
|
||
`erzeuge_branch_and_bound.py` **rechnet** den Baum der Handrechnung, statt ihn abzuschreiben:
|
||
eine kleine Branch-and-Bound-Schleife mit `scipy.linprog`, die nach denselben drei Regeln
|
||
kappt, die der Kapiteltext nennt. Ergebnis: $P_0\,(3;1{,}5)$, A $(3{,}33;1)$, A1 $(3;1)$,
|
||
A2 $(4;0)$, B gekappt — Optimum $(4;0)$, $Z^* = 20$. Das Skript **bricht ab**, wenn das
|
||
Ergebnis nicht mit der Handrechnung übereinstimmt.
|
||
|
||
**Eine Feinheit, die zur Aussage gehört:** Die Suche musste auf **Tiefensuche** umgestellt
|
||
werden. Mit Breitensuche wird Ast B bearbeitet, bevor die Blätter von Ast A fertig sind — B
|
||
wird dann zum Incumbent, und das Bild zeigte das Gegenteil dessen, was der Text beschreibt.
|
||
Der Kommentar im Skript hält das fest.
|
||
|
||
**Und ein Zählfehler im Buchtext:** Die Handrechnung schloss mit *„Wir haben vier LPs
|
||
gelöst"*. Es sind **fünf** — $P_0$, A, A1, A2 und B. Aufgefallen ist das nur, weil das Skript
|
||
mitzählt. Korrigiert, mit Verweis auf das Skript.
|
||
|
||
Damit haben **fünf von sieben** nachgebauten Bildern einen Fehler zutage gefördert.
|
||
|
||
### Die Gruppe „rechnet" ist abgeschlossen ✅
|
||
|
||
Drei weitere Generatoren — und diesmal war nicht jedes Bild falsch.
|
||
|
||
**`kap07_min_cost_flow` war korrekt.** Die Instanz stimmte mit `Min_Cost_Flow.py` überein.
|
||
Sie zeigte aber **nur die Instanz**: Kosten und Kapazitäten an den Kanten, den Fluss nicht.
|
||
`erzeuge_min_cost_flow.py` löst das Netz und zeichnet die Lösung ein — Strichstärke nach
|
||
Fluss, ausgelastete Kanten hervorgehoben, ungenutzte gestrichelt, Knotenpreise an den Knoten.
|
||
Sichtbar wird dadurch die eigentliche Aussage: Die billige Route über den Umschlag läuft
|
||
voll, und **trotzdem** muss die teure Direktkante Werk B → Kunde 2 bedient werden.
|
||
|
||
**`kap07_vrp_touren` war ein Schema** — zwei Touren, vier Kunden, Zeitfenster in Uhrzeiten.
|
||
Die Instanz des Kapitels hat 16 Kunden und vier Fahrzeuge. `erzeuge_vrp_touren.py` zeichnet
|
||
die tatsächliche Lösung als Karte (619 km, identisch zum abgedruckten Programmlauf).
|
||
**Reproduzierbarkeit:** Das Buchprogramm gibt der Suche ein *Zeitbudget* von fünf Sekunden —
|
||
auf einer anderen Maschine ein anderes Rechenbudget. Der Generator setzt zusätzlich eine
|
||
feste Lösungszahl als Abbruchkriterium; zwei Läufe liefern dieselben vier Touren.
|
||
|
||
Der Fund dabei ist didaktisch: **Die Touren kreuzen sich.** Bei einem reinen Tourenproblem
|
||
wäre das ein sicheres Zeichen für eine verbesserbare Lösung — hier erzwingen die Zeitfenster
|
||
die Reihenfolge. Das steht jetzt in der Bildunterschrift.
|
||
|
||
**`kap08_kkt_geometrie` hatte ein Vorzeichenproblem.** Der Zielpfeil war mit $-\nabla f$
|
||
beschriftet und zeigte entgegen $\lambda^*\nabla g$ — das ist die Konstellation einer
|
||
**Maximierung**. Das Kapitel formuliert KKT für die Minimierung, dort gilt
|
||
$\nabla f + \lambda^*\nabla g = 0$, also zeigen $\nabla f$ (**ohne** Minus) und
|
||
$\lambda^*\nabla g$ gegeneinander. Eine Beschriftung, ein Vorzeichen — und die Aussage
|
||
kippt. `erzeuge_kkt.py` zeichnet beides neu und **prüft vorher alle vier KKT-Bedingungen
|
||
numerisch**; das linke Panel zeigt zusätzlich die 1-D-Handrechnung des Kapitels
|
||
($x^*=3$, $\lambda^*=4$).
|
||
|
||
**Eine Korrektur an der eigenen Einteilung:** `kap02_konvexitaet` stand in der Gruppe
|
||
„rechnet". Beim Ansehen zeigt es eine generische Parabel und eine generische Welle — keine
|
||
Zahlen aus einem Modell. Nach dem selbst aufgestellten Kriterium gehört es zu den
|
||
schematischen Bildern; ein Generator brächte Reproduzierbarkeit, aber keinen
|
||
Korrektheitsgewinn. **Kein Generator geplant.**
|
||
|
||
**Bilanz der Aktion:** zehn nachgebaute Bilder, **sieben** davon mit einem Fehler oder einer
|
||
fehlenden Aussage — dreimal ein völlig fremdes Modell, einmal widersprüchliche Zahlen,
|
||
einmal ein gekipptes Vorzeichen, einmal eine verschwiegene Lösung, dazu ein Zählfehler im
|
||
Fließtext. Kein einziger dieser Fälle wäre durch Hinsehen aufgefallen.
|
||
|
||
### Aufgeräumt: die verwaisten Bilder ✅
|
||
|
||
Acht Bilder wurden durch Generatorfassungen ersetzt und danach von nichts mehr referenziert.
|
||
Gelöscht sind **nur die V04-Kopien** — je SVG und PNG in `bilder_04/` und in der Spiegelung
|
||
`OR_HTML_04/bilder_04/`, zusammen 32 Dateien.
|
||
|
||
**Die Vorsichtsmaßnahme, auf die es ankam:** Alle acht werden von **Version 03 weiterhin
|
||
gebraucht**. Deren Dateien liegen in `bilder/` und `OR_HTML/bilder/` — eigenen Verzeichnissen,
|
||
die unangetastet bleiben. Vor dem Löschen wurde geprüft, dass V03 für jedes der acht Bilder
|
||
seine eigene Kopie an beiden Orten hat; hätte man stattdessen „die Datei" gelöscht, wäre der
|
||
eingefrorene V03-Strang beschädigt worden. Nach dem Löschen bauen **beide** Stränge
|
||
unverändert durch.
|
||
|
||
`spiegle_bilder()` kopiert übrigens nur und räumt nicht auf — die Spiegelung unter
|
||
`OR_HTML_04/` verschwindet nicht von selbst und musste mitgelöscht werden. Das ist beim
|
||
nächsten Mal wieder so.
|
||
|
||
**Stand der Diagrammquellen:** 32 SVGs, davon 18 aus 15 Generatorskripten. Die restlichen 14
|
||
sind schematisch — Kästen, Pfeile, beschriftete Formeln, generische Funktionsverläufe. Für
|
||
sie ist **kein Generator geplant**; das ist eine Entscheidung, keine Lücke.
|
||
|
||
Zwei Bilder aus der früheren Gruppe „halb" sind dabei geprüft und abgehakt worden:
|
||
`kap06_cp_sat_propagation` rechnet zwar ein konkretes Beispiel vor, ist aber **korrekt** und
|
||
deckungsgleich mit der Handrechnung des Kapitels (nachgerechnet: $x_1=1 \Rightarrow x_2=6$
|
||
fällt heraus, $x_2=2 \Rightarrow x_1=5$ ebenso). `kap10_zustandskette` ist rein symbolisch
|
||
($S_0$, $S_1$, $S_T$) und kann nicht driften.
|
||
|
||
### Schlussabnahme ✅ — und ein Fund, den nur sie aufdecken konnte
|
||
|
||
Die Liste aus PLAN.md Abschnitt 10, vollständig abgearbeitet:
|
||
|
||
| Prüfung | Ergebnis |
|
||
| --- | --- |
|
||
| `build_version_04.py --check` | 5 Teile, 23 Kapitel, 5 Anhänge, 292 Abschnitte, **703 Querverweise**, 325 Indexmarken, 31 Dateien — keine Warnung |
|
||
| **gesamte Programmsuite** | **73 Programme, 0 Fehler** |
|
||
| `pytest test_or_kern.py` | 33 Tests grün |
|
||
| `--pdf --html` | PDF **715 Seiten**, Website vollständig |
|
||
| V03-Gegenprobe | 22 Dateien, 11 082 Zeilen — unverändert |
|
||
|
||
**Der Fund: alle vier Plotly-Figuren waren kaputt.** Sie erschienen auf den Kapitelseiten
|
||
nicht als Diagramm, sondern als **Wand aus escaptem Markup in einem Codeblock** — auch die
|
||
beiden, die seit Phase 0 bzw. Phase 3 im Buch standen.
|
||
|
||
Die Ursache steckt in einem Detail von Pandoc: Plotlys Fragment ist **eine einzige lange
|
||
Zeile**, die mit `<div>` beginnt und danach ein paar Dutzend Leerzeichen enthält. Pandoc
|
||
schließt den Roh-HTML-Block am ersten Blocktag und liest den eingerückten Rest als
|
||
**indented code block**. `resolve_plotly()` gibt das Fragment jetzt als Rohblock
|
||
(` ```{=html} `) aus, den Pandoc unverändert durchreicht — unabhängig von Einrückung und
|
||
Zeilenlänge. Nachgeprüft: vier eingebettete Figuren, kein `<pre>` mehr.
|
||
|
||
Das ist genau der Fall, für den die Schlussabnahme da ist: Der Build meldete nie einen
|
||
Fehler, `--check` war grün, und im PDF war alles in Ordnung, weil dort ohnehin die statische
|
||
Fassung steht.
|
||
|
||
**Zwei kleinere Korrekturen nebenbei.** Der Platzhaltertext im PDF lautete *„steht auf der
|
||
Website des Buches bereit"* — er erscheint aber auch in `gesamtdokument.html`, und wer das
|
||
liest, ist bereits auf der Website. Jetzt: *„auf den Kapitelseiten der Website"*.
|
||
|
||
Und ein eigener Fehlgriff: Für die V03-Gegenprobe hatte ich `--html` statt `--check` laufen
|
||
lassen. Das regenerierte V03-Artefakte (Datumsstempel, Tabellenausrichtung) — an einem
|
||
Strang, der eingefroren ist. Zurückgenommen; der Arbeitsbaum enthält keine V03-Änderung.
|
||
**Dabei ist allerdings aufgefallen:** V03s generiertes Gesamtdokument ist gegenüber seinen
|
||
Quelldateien leicht veraltet (Tabellenausrichtung, Stand-Datum). Das stammt aus einer
|
||
Quelländerung vor dieser Sitzung, nicht aus dem Umbau — wer V03 einmal neu baut, zieht es
|
||
nach.
|
||
|
||
**Sichtprüfung der Website** (strukturell nachgezählt statt behauptet): Landing-Page mit 35
|
||
Seitenverweisen, Sidebar mit Navigation und Suchfeld auf den Kapitelseiten, Suchindex mit
|
||
592 Einträgen, Stichwortverzeichnis mit 24 Buchstabengruppen und 323 Verweisen, didaktische
|
||
Karten (`card-blick`, `card-schnellstart`, `card-denkfehler`, `card-quiz`) auf den
|
||
Kapitelseiten, 73 Programme und 25 Notebooks zum Herunterladen, KaTeX lokal eingebunden,
|
||
**kein einziger toter Bildverweis**, und seitenübergreifende Querverweise lösen als
|
||
`andere-seite.html#anker` auf.
|
||
|
||
### Trennung von Version 03 und Version 04 ✅
|
||
|
||
Beide Ausbaustufen lagen bis dahin nebeneinander im selben Wurzelverzeichnis. Jetzt liegt
|
||
jede in einem eigenen Baum: **`Version_04/` ist die Wurzel dieses Strangs** und taugt
|
||
unmittelbar als Wurzel eines eigenen Repositorys; `Version_03/` steht daneben, eingefroren.
|
||
|
||
**Der Umbau kostete keine einzige Pfadänderung an den Skripten.** Alle leiten ihre Pfade aus
|
||
der eigenen Position ab — `BASIS = dirname(HIER)` in den Build-Skripten,
|
||
`BASIS = dirname(dirname(__file__))` in den 15 Bildgeneratoren — und jeder `subprocess.run`
|
||
läuft mit `cwd=BASIS`. Mit dem Verschieben wurde `BASIS` automatisch zu `Version_04`. Der
|
||
`--check` lieferte am neuen Ort auf Anhieb dieselben Zahlen wie vorher.
|
||
|
||
Verschoben wurde mit `git mv`, die Historie bleibt darum vollständig. Die Trennung in zwei
|
||
Repositories ist **noch nicht vollzogen**; sie erfolgt später mit
|
||
`git subtree split -P Version_04 -b v04-only`, was die Historie mitnimmt.
|
||
|
||
**Was dabei zu tun war, ging fast alles über Pfadangaben in Prosa hinaus:**
|
||
|
||
1. **Der PDF-Bau hing an zwei Dateien außerhalb des Repositorys.**
|
||
`~/.config/pandoc/defaults.yaml` (Schriften, Geometrie) lädt darin per **absolutem** Pfad
|
||
`~/.pandoc/templates/header-includes.tex`. Ein frischer Klon hätte **kein PDF bauen
|
||
können** — die Meldung wäre nur „pandoc-Defaults nicht gefunden" gewesen.
|
||
Beide liegen jetzt als `pandoc-defaults-basis.yaml` und `pandoc/header-includes.tex` im
|
||
Repository, mit relativiertem Pfad; `build_version_04.py` bevorzugt die Kopie und fällt
|
||
auf die Benutzerdatei zurück.
|
||
**Nachgewiesen**, nicht behauptet: Mit temporär entfernten Benutzerdateien baut das PDF
|
||
unverändert 715 Seiten.
|
||
2. **`pandoc-defaults-buch.yaml` brauchen beide Versionen** unter ihrem jeweiligen `BASIS` —
|
||
sie wurde kopiert, nicht verschoben.
|
||
3. **`.gitignore` dreigeteilt.** Die versionsspezifischen Regeln stehen jetzt **in**
|
||
`Version_03/` und `Version_04/`, relativ zum jeweiligen Verzeichnis. Damit gilt
|
||
`Version_04/.gitignore` unverändert weiter, sobald der Baum herausgelöst ist. In der
|
||
Wurzel bleibt nur, was überall gilt.
|
||
4. **Ein Altfehler im Buchtext**, bei der Gelegenheit gefunden:
|
||
`00_Vorwort_und_Lesehilfe.md` schickte Leser zu
|
||
`Operations_Research_mit_Python_Programme/requirements.txt` — ein Verzeichnis, das es nie
|
||
gegeben hat. Korrigiert auf `…_Version_04_Programme/`.
|
||
5. **Die Wiederaufnahme-Checkliste oben** hätte sonst mit einem falschen Verzeichnis begonnen
|
||
und in Schritt 4 auf ein Version-03-Skript gezeigt, das von hier aus nicht mehr auflöst.
|
||
|
||
**Neu ausgestattet, damit `Version_04/` als Repo-Wurzel taugt:** `README.md` (Bau- und
|
||
Laufanleitung, Werkzeugliste, die beim Veröffentlichen zu setzenden Werte), `CLAUDE.md`
|
||
(V04-Konventionen ohne V03-Bezüge) und der Kommentar an `COLAB_BASIS_URL`, der ihn als den
|
||
einen beim Veröffentlichen zu ändernden Wert ausweist — der Pfadteil `Notebooks_04/…` stimmt
|
||
bereits, weil er relativ zur Repository-Wurzel aufgelöst wird.
|
||
|
||
**Verifiziert am neuen Ort:** `--check` ohne Warnung (703 Querverweise, 325 Indexmarken),
|
||
**73 Programme mit 0 Fehlern**, 33 pytest-Tests grün, PDF 715 Seiten, ein Bildgenerator,
|
||
`OR_HTML_04/` selbstgenügsam (kein Verweisziel außerhalb), Version 03 unverändert baubar.
|
||
|
||
### Titel, Untertitel und Titelblatt geändert ✅
|
||
|
||
Das Buch heißt jetzt **„Optimierte Entscheidungsfindung mit Python"**, Untertitel
|
||
**„Von der mathematischen Modellierung zu praktischen intelligenten Lösungen für Betrieb,
|
||
Technik und Finanzmärkte"**. Bis Version 03 lautete der Titel „Operations Research mit
|
||
Python".
|
||
|
||
Auf dem Titelblatt stehen **vier** Textblöcke: Titel, Untertitel und in der Solver-Karte die
|
||
beiden Absätze aus `Kritik_und_Verbesserungsvorschlaege/NEUER_TITEL.md` — die
|
||
Kompendium-Beschreibung und die Anwendungsfelder. **Maßgeblich sind die Zeichenketten in
|
||
`erzeuge_titelseite.py`**; `NEUER_TITEL.md` ist die Vorlage, aus der sie stammen.
|
||
|
||
**Das Titelblatt musste dafür neu gesetzt werden, nicht nur umbenannt.** Die Textzeilen
|
||
standen mit von Hand vermessenen Koordinaten in `erzeuge_titelseite.py`, übernommen aus der
|
||
ursprünglichen, gezeichneten SVG — das trägt genau so lange, wie sich der Text nicht ändert.
|
||
Jetzt **misst** das Skript: `_textbreite()` fragt den echten Renderer, `_umbrechen()` bricht
|
||
auf den Satzspiegel um, `_setze_block()` stapelt mit definiertem Durchschuss. Der Titel wurde
|
||
seither dreimal geändert; ab dem zweiten Mal kostete das nur noch das Austauschen von
|
||
Zeichenketten.
|
||
|
||
Gemessene Werte: Titel **34 pt** in drei Zeilen, Untertitel **14 pt** in zwei Zeilen,
|
||
Kartentext **11,5 pt** in 3 + 1 + 5 Zeilen (Spanne 117 pt bei 141 pt Kartenhöhe).
|
||
|
||
Zwei dieser Größen sind gegenüber der ersten Fassung gefallen, und zwar aus messbaren
|
||
Gründen: Der Untertitel bräuchte bei 15 pt **drei** Zeilen, deren letzte nur noch das Wort
|
||
„Finanzmärkte" trüge — ein Hurenkind mitten auf dem Titelblatt; bei 14 pt sind es zwei
|
||
ausgeglichene Zeilen. Und der Kartentext ist auf neun Zeilen gewachsen: Bei 12 pt blieben nur
|
||
9 pt Luft über und unter dem Block, und der Text geriete in die abgerundeten Kartenecken
|
||
(Radius 11,9 pt); 11,5 pt lassen 12 pt.
|
||
|
||
**Warum der Titel dreizeilig ist:** „Entscheidungsfindung" ist bei 34 pt bereits 429 pt breit
|
||
und damit fast so breit wie der ganze Satzspiegel (453 pt). Jede zweizeilige Aufteilung
|
||
bräuchte mindestens 649 pt — das passt bei keiner lesbaren Schriftgröße. Der Umbruch wird
|
||
über ein `\n` im `TITEL` erzwungen, damit „mit Python" zusammenbleibt.
|
||
|
||
**Der Platz für den zweiten Kartenabsatz kam aus dem kürzeren Untertitel.** Er braucht nur
|
||
noch zwei statt vier Zeilen; Trennlinie und Kartenoberkante sind deshalb von y = 336,7 / 368,7
|
||
auf 308 / 340 gerückt, und die Karte wuchs von 113 auf 141 pt. Beide Werte stehen als
|
||
gemeinsame Konstanten am Dateikopf, aus denen `_design()` und `_texte()` lesen — Kasten und
|
||
Text können nicht auseinanderlaufen.
|
||
|
||
**Zwei Sicherungen sind eingebaut** und gegengetestet: Passt eine Titelzeile nicht in den
|
||
Satzspiegel oder läuft der Kartentext über den Kartenrand, bricht das Skript mit einer
|
||
Meldung ab, statt ein Titelblatt zu erzeugen, auf dem Text übereinanderliegt.
|
||
|
||
**Der Titel steht an genau zwei Stellen** statt verstreut: `SITE_TITEL` im Versionsblock von
|
||
`build_version_04.py` — von dort lesen `<title>`, Kopfleiste, Landing-Page, Notebook-Kopf und
|
||
der `pagetitle` des Gesamtdokuments — und `TITEL` in `erzeuge_titelseite.py`, weil er dort
|
||
umbrochen und vermessen werden muss.
|
||
|
||
**Nicht** geändert: die Verzeichnis- und Dateinamen (`Operations_Research_mit_Python_…`) und
|
||
die historischen Dokumente unter `Kritik_und_Verbesserungsvorschlaege/`.
|
||
|
||
**Die Anwendungsfelder auf dem Titelblatt sind gegen den Inhalt geprüft.** Zu jedem Begriff
|
||
gibt es ein gerechnetes Beispiel:
|
||
|
||
| Anspruch | belegt durch |
|
||
| --- | --- |
|
||
| Personaleinsatz-, Schicht- und Vertretungspläne | `CP_SAT_Vertretungssystem.py`, Schichtplan-Beispiel in {ref:kap:cpsat} (6 Personen, 14 Tage), Projekte P1/P2 |
|
||
| Fahrtrouten | `VRP_Flotten_Routing.py` (CVRPTW, 16 Kunden, 619 km), `VRP_Kapazitaetsfalle.py` |
|
||
| Logistik | Min-Cost-Flow-Transportnetz, VRP, {ref:kap:supplychain} |
|
||
| Maschinenauslastung | `JobShop_Intervalle.py`, Rüstzeitmuster B26 |
|
||
| Netzwerkauslastung | `Min_Cost_Flow.py` mit Kantenkapazitäten |
|
||
| Energieverteilung | `Kraftwerkseinsatz.py` (Unit Commitment mit Windunsicherheit) |
|
||
| Ressourcenplanung | `Bot_Allokation.py`, `Rucksack.py` |
|
||
| Portfoliooptimierung, Finanzmärkte | {ref:kap:markowitz}, {ref:kap:cvar}, {ref:kap:handelsmaschine} |
|
||
|
||
**Ein Begriff wurde dabei ausgetauscht.** Die erste Fassung warb mit „Stundenplänen" — das
|
||
Wort kommt im ganzen Buch nur zweimal vor, als Stichwort in einer CP-SAT-Tabelle und als
|
||
*Datenquelle* im Projektauftrag P1. Ein Stundenplan im engeren Sinne (Klassen, Fächer, Räume,
|
||
Lehrerverfügbarkeit gegeneinander optimieren) wird **nicht** gerechnet; gerechnet werden
|
||
Vertretungsplan und Schichtplan. Die Formulierung lautet jetzt „Personaleinsatz-, Schicht-
|
||
und Vertretungsplänen" und deckt sich damit mit dem, was das Buch einlöst.
|
||
|
||
**Verifiziert:** `--check` ohne Warnung, PDF 715 Seiten, Titelblatt in PDF und Website
|
||
sichtgeprüft, kein Rest eines früheren Titels in Quellen oder generierten Seiten, 33
|
||
pytest-Tests grün, Version 03 unberührt.
|
||
|
||
---
|
||
|
||
## 7. Was als Nächstes ansteht
|
||
|
||
### Der konkret nächste Handgriff
|
||
|
||
**Alle sechs Phasen sind abgeschlossen. Seit dem 08.09.2026 ist `Version_04/` ein eigenes
|
||
Git-Repository**, und Phase 6 (Umsetzung von `Verbesserungen_02/_03`) hat begonnen.
|
||
23 Kapitel, 5 Anhänge, 74 lauffähige Programme, Website mit vier interaktiven Figuren.
|
||
|
||
**Zum eigenen Repository — eine Korrektur der früheren Angabe hier:** `git subtree split -P
|
||
Version_04` hätte die Historie **nicht** mitgenommen. Der Pfad `Version_04/` existiert erst
|
||
seit der Verzeichnistrennung; der Split hätte 7 statt 41 einschlägiger Commits gebracht — eine
|
||
Teilhistorie, die vollständig aussieht und es nicht ist. Volle Historie ginge nur über
|
||
`git-filter-repo` (nicht installiert). Deshalb: frischer `git init -b main`, das
|
||
übergeordnete Repository bleibt das Archiv der 71 Commits und trägt `Version_04/` in seiner
|
||
`.gitignore`.
|
||
|
||
Offen ist weiterhin, was von außen kommt:
|
||
|
||
* **Veröffentlichen.** `Version_04/OR_HTML_04/` ist selbstgenügsam und direkt auf einen
|
||
Webserver kopierbar.
|
||
* **Remote-Repository anlegen.** Danach **`COLAB_BASIS_URL`** in `build_version_04.py` auf
|
||
Kontoname und Repository-Name setzen — die 25 Colab-Badges hängen daran, und ein falscher
|
||
Wert fällt erst beim Klicken auf.
|
||
|
||
Was einer späteren Sitzung als Erstes auffallen wird und **kein Fehler** ist:
|
||
|
||
* **14 der 32 Diagramme haben keinen Generator.** Dokumentierte Entscheidung — Kästen,
|
||
Pfeile, beschriftete Formeln, generische Funktionsverläufe. Kriterium in Abschnitt 6c.
|
||
* **Version 03 ist eingefroren** und liegt in `../Version_03/`. Ihre READMEs nennen als
|
||
Arbeitsverzeichnis noch `OR_mit_Python`, und ihr generiertes Gesamtdokument liegt minimal
|
||
hinter den Quellen zurück. Beides bleibt bewusst stehen.
|
||
|
||
### Kleinere offene Punkte aus abgeschlossenen Phasen
|
||
|
||
- ✅ **Formel-Übersetzer in {ref:kap:markowitz} und {ref:kap:cvar}** — erledigt, siehe
|
||
Abschnitt 6c. Damit hat jedes formelreiche Kapitel seinen 🔤-Übersetzer; die Kapitel
|
||
Finanzdaten, Handelsmaschine und Praxisfallen sind formelarm, dort besteht kein Bedarf.
|
||
- ✅ **`CLAUDE.md` um den V04-Strang ergänzt** — aus Phase 5 vorgezogen, weil `CLAUDE.md`
|
||
die einzige Datei ist, die eine neue Sitzung automatisch liest. Ohne diesen Abschnitt
|
||
hätte eine frische Sitzung am eingefrorenen V03 weitergearbeitet und `PLAN.md`/`PROGRESS.md`
|
||
nie gesehen. Damit war die Wiederaufnahme der eigentliche Schwachpunkt — nicht die Frage,
|
||
in welchem Verzeichnis die Planungsdateien liegen.
|
||
|
||
---
|
||
|
||
## 6d. Phase 6 — Umsetzung von `Verbesserungen_02` / `_03` 🔄
|
||
|
||
Grundlage: `Kritik_und_Verbesserungsvorschlaege/Verbesserungen_02.md` und `_03.md`, zwölf
|
||
Arbeitspakete. Nach Ertrag geordnet umgesetzt; **Streamlit-Apps sind auf Wunsch des Autors
|
||
zurückgestellt**, ebenso Folium-Karten (die VRP-Instanz ist ein synthetisches Raster ohne
|
||
Geokoordinaten) und Zielgruppen-Callouts in allen Kapiteln.
|
||
|
||
### ✅ Repository und `pyproject.toml`
|
||
|
||
`git init -b main` in `Version_04`, `Version_04` im übergeordneten Repository ausgetragen.
|
||
Neu: `pyproject.toml` mit den Gruppen `finance`, `large-scale`, `api`, `figures`, `dev`,
|
||
`empfehlungen` — die abgedruckte `requirements.txt` bleibt unverändert daneben bestehen.
|
||
|
||
**`ortools` steht in der Grundausstattung, `highspy` erst in `[large-scale]`.** Damit kann
|
||
der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten; `cvxpy`
|
||
zieht `highspy` nicht nach.
|
||
|
||
Zwei Funde beim Abgleich der Importe gegen `requirements.txt`:
|
||
|
||
* **`graphviz` fehlte** — `bilder_04/erzeuge_architektur_diagramme.py` importiert es, die
|
||
Leserliste kennt es nicht. Es gehört nicht in die abgedruckte Datei (kein
|
||
Beispielprogramm braucht es), steht jetzt aber in `[figures]`.
|
||
* **`pymoo` steht in `requirements.txt`, wird aber von keinem der 74 Programme importiert** —
|
||
es ist nur eine Empfehlung im Kapitel Metaheuristiken. Jetzt in `[empfehlungen]`.
|
||
|
||
Später korrigiert: **`cvxpy` lag zuerst in `[finance]`** — falsch, es wird von 13 Programmen
|
||
gebraucht, darunter dreien im Kapitel Unsicherheit, lange vor dem Finanzteil. Jetzt in der
|
||
Grundausstattung.
|
||
|
||
### ✅ 6.1 Chance Constraints ({ref:kap:unsicherheit})
|
||
|
||
Neuer Abschnitt {ref:sec:unsicherheit-chance-constraints} plus `Chance_Constraints.py`
|
||
(74. Programm). Das Kapitel hatte Monte-Carlo, Zweistufigkeit und Worst-Case-Robustheit; die
|
||
Wahrscheinlichkeitszusage war die fehlende vierte Antwort — und die, nach der das Management
|
||
tatsächlich fragt. Beide Wege an **derselben** Instanz: analytisch als
|
||
Second-Order-Cone-Bedingung (CLARABEL) und szenariobasiert als Big-M-MILP (SciPy/HiGHS),
|
||
beide in einem Prozess ohne `ortools`- oder `highspy`-Import.
|
||
|
||
Der Fall ist ein Kraftwerkspark mit 500 MW Zusage. **Drei Befunde, alle gemessen:**
|
||
|
||
1. **Der Mittelwertplan hält 50,08 %.** Kein Fehler, sondern die Definition des
|
||
Erwartungswerts — dieselbe Falle wie im Kapitelanfang, nur teurer.
|
||
2. **Sicherheit ist konvex bepreist.** 56 289 € je Prozentpunkt auf dem Weg zu 80 %,
|
||
253 848 € zwischen 95 % und 99 % — das **4,5-fache**. Das Programm rechnet die Tabelle
|
||
selbst aus, statt sie zu behaupten.
|
||
3. **Die Zusage gilt nur für die unterstellte Verteilung.** Der 95-%-Plan hält gemessen
|
||
**87,44 %**, sobald die Testverteilung eine Kältewelle mit Dunkelflaute enthält, in der
|
||
*alles zugleich* einbricht — auch das Gaskraftwerk. In einer reinen Normalwelt liefert
|
||
derselbe Plan 94,97 %. Nicht das Verfahren ist falsch, sondern die Annahme.
|
||
|
||
**Zwei eigene Fehlgriffe unterwegs, beide durch Messen aufgefallen:**
|
||
|
||
* Die erste Kostentarierung ergab eine **entartete Lösung** (alles ins Gaskraftwerk), womit
|
||
die Kovarianzmatrix wirkungslos war — und gerade sie ist der Grund für die Kegelform. Erst
|
||
Kosten, die je *erwarteter* MW vergleichbar sind, machen den Konflikt echt.
|
||
* Der erste Kälteeinbruch traf nur Wind und Sonne. Der SOC-Plan hatte die aber ohnehin fast
|
||
herausgehalten und war zufällig robust (94,40 %) — das Argument trug nicht. Physikalisch
|
||
richtig ist, dass bei Frost auch der Gasdruck fällt; damit trifft der Einbruch alles
|
||
zugleich, und genau das kann eine Kovarianzmatrix mit Korrelationen um null nicht
|
||
ausdrücken.
|
||
|
||
**Ehrlich berichtet statt geglättet:** Die Szenariomethode **überanpasst**. Über zwölf Läufe
|
||
(je vier Ziehungen bei S = 200, 400, 800, geprüft an 200 000 unabhängigen Szenarien) lag die
|
||
tatsächliche Quote zwischen **93,14 % und 96,71 %**, und die Spanne wurde von S = 400 auf
|
||
S = 800 wieder **breiter**. Beide Solver (SciPy und HiGHS) bestätigten `optimal` bei
|
||
identischen Kosten — es ist echte Überanpassung, kein Solverartefakt. Steht als
|
||
⚠️-Kasten im Abschnitt.
|
||
|
||
**Aus der Ausgabe entfernt: die Laufzeitmessung.** Eine Wanduhrzeit ist nie byteidentisch
|
||
reproduzierbar (2,2 s / 2,3 s zwischen zwei Läufen) und hätte Regel 4 dauerhaft gebrochen.
|
||
Die Beobachtung steht jetzt im Fließtext, wo eine Spanne ehrlich ist. Danach: drei Läufe
|
||
zeichengleich, und die abgedruckte Ausgabe stimmt mit dem Lauf des **extrahierten**
|
||
Programms überein.
|
||
|
||
Eine Aufgabenprämisse musste ich verwerfen: „Ab welcher Zusage wird das Modell unlösbar?“ —
|
||
das analytische Modell bleibt bis praktisch 100 % lösbar. Die Aufgabe fragt jetzt nach dem
|
||
Fall mit gedrosselter Biomasse, in dem das **szenariobasierte** Modell `infeasible` meldet
|
||
(7,50 % der Szenarien deckt selbst der Vollausbau nicht), das analytische aber `optimal`
|
||
liefert — und dann 87,39 % hält.
|
||
|
||
### ✅ 6.1a Die letzten handgeschriebenen Nummern — **sechster und siebter** Fund
|
||
|
||
**Der sechste Fund: 98 Lösungsmarken in Anhang A.** Marken der Bauart
|
||
`**9.2 — Ansatz wählen.**`, sämtlich aus Version 03, wo Unsicherheit Kapitel 9 war. Heute ist
|
||
es Kapitel 12, und die Aufgabe wird korrekt als „Aufgabe 12.2" gesetzt.
|
||
|
||
Das war keine Schönheitsfrage: **„Aufgabe 9.2" existiert wirklich** — sie gehört zum Kapitel
|
||
Metaheuristiken („Zuggröße und Temperatur"). Wer die Lösung zu 9.2 nachschlug, landete beim
|
||
falschen Thema. Beide bisherigen Prüfungen liefen vorbei: Vor der Zahl steht kein
|
||
Schlüsselwort, und es ist keine Tabellenzelle.
|
||
|
||
**Zwei Messungen machten die Reparatur billig.** Erstens: In allen 15 nummerierten
|
||
Abschnitten gab es exakt so viele Lösungen wie Aufgaben, lückenlos 1…n — nur der Kapitelteil
|
||
war falsch, die Umstellung also reine Mechanik. Zweitens: Die **acht in Phase 3 ergänzten
|
||
Kapitel machten es längst richtig** (`**Titel.**` ohne Nummer). Der Anhang wurde damit nicht
|
||
auf etwas Neues umgestellt, sondern auf das, was seine neueren Teile schon taten.
|
||
|
||
Der Build vergibt die Marken jetzt selbst. Der Präfix kommt **nicht** aus `# Anhang A:` —
|
||
der Anhang ist *eine* Überschrift, seine Abschnitte gehören aber zu 23 verschiedenen
|
||
Kapiteln. Er kommt aus `{#sec:loesungen-<X>}` → `{#kap:<X>}`; diese Zuordnung gilt geprüft
|
||
für alle 23. Im Quelltext steht `**{loesung} — Titel.**`.
|
||
|
||
**Der siebte Fund, unterwegs: neun Verweise auf Denkfehler-Nummern.** Sätze wie „siehe
|
||
Denkfehler 8.1" standen in alter Zählung — „8" war in Version 03 das QP/NLP-Kapitel, heute
|
||
ist es Graphen. Eine bloße Umnummerierung hätte sie **stillschweigend woanders hin zeigen
|
||
lassen**; das Ziel wurde deshalb für jeden einzelnen aus dem Zusammenhang bestimmt (die
|
||
Kovarianzmatrix-Stelle in Finanzdaten meint QP/NLP, die „vergessene Dimension" meint Graphen)
|
||
und auf `{ref:sec:<kapitel>-denkfehler}` umgestellt — ein Label, das alle 23 Kapitel haben.
|
||
Einer davon steckte in einem **Programm-Docstring** und bekam nach Regel 12 den Kapitelnamen
|
||
statt eines Verweises.
|
||
|
||
**Eine Abweichung vom Plan, bewusst:** Der Denkfehler bekommt **gar keine** Nummer. Es gibt
|
||
je Kapitel genau einen, die zweite Stelle wäre immer `.1`, und er steht ohnehin unter einer
|
||
nummerierten Überschrift. Eine Nummer, die nie variiert, holte nur die Fragilität zurück, die
|
||
hier gerade beseitigt wurde. Auch das folgt den acht neuen Kapiteln, die den Denkfehler schon
|
||
vorher nur mit dem Titel des Rätsels ankündigen.
|
||
|
||
**`--check` bewacht jetzt alle Familien** — `Denkfehler N.M`, `Micro-Quiz N`,
|
||
`**N.M — ` im Anhang — **und zählt zusätzlich ab**, dass jeder Lösungsabschnitt so viele
|
||
`{loesung}`-Marken hat wie sein Kapitel Aufgaben. Das ist die eigentliche Absicherung: Sie
|
||
fängt eine vergessene oder doppelte Lösung, was keine Textsuche leisten kann. Gegengetestet
|
||
mit vier künstlichen Fehlern auf einmal — alle vier gemeldet, nach dem Rückbau null.
|
||
|
||
**Die Gegenprobe der Umstellung:** Das gebaute Gesamtdokument vorher gesichert, nachher
|
||
verglichen. 168 geänderte Zeilen, restlos einer Familie zuzuordnen — **118** Lösungsmarken,
|
||
15 Anhang-Überschriften, 15 Kapitel-Callouts, 12 Micro-Quiz, 7 Zeilen mit Denkfehler-Verweisen
|
||
(darauf 8 `{ref:}`), 1 Docstring-Zeile. Keine verschobene, verlorene oder inhaltlich
|
||
veränderte Zeile.
|
||
|
||
Die 118 sind die Probe aufs Exempel: 138 Marken gesamt, davon **20 unverändert** — genau die
|
||
Kapitel 1 bis 3, deren alte Nummern zufällig noch stimmten, weil vor ihnen nichts eingefügt
|
||
worden war.
|
||
|
||
Ergebnis: **138 Aufgaben, 138 Lösungen, keine ohne Gegenstück.** Micro-Quiz tragen jetzt die
|
||
echten Kapitelnummern (1, 2, 3, 5, 6, 7, 8, 11, 12, 13, 18–22 — die Lücken sind die Kapitel
|
||
ohne Quiz). PDF unverändert 725 Seiten.
|
||
|
||
**Nicht Teil dieser Runde:** In den 15 alten Abschnitten weichen 69 der 138 Lösungstitel von
|
||
den Aufgabentiteln ab (gekürzt: „Zweistufiges Modell rechnen" → „Zweistufig rechnen"). Da die
|
||
Nummer wieder stimmt, ist sie der Schlüssel; das Angleichen wäre eine rein redaktionelle
|
||
Runde.
|
||
|
||
### ✅ 6.2 Constraint Attribution und Managementbericht ({ref:kap:praxisfallen})
|
||
|
||
Neuer Abschnitt {ref:sec:praxisfallen-attribution} plus `Constraint_Attribution.py`
|
||
(75. Programm). Setzt Paket 3 aus `Verbesserungen_02.md` um — den Teil, der noch fehlte.
|
||
|
||
**Die Lücke, die es schließt.** Zwei Fälle waren abgedeckt: `Erklaerbarkeit.py` beantwortet
|
||
„warum **diese** Zuweisung?", `Konfliktsuche.py` „warum geht es **gar nicht**?". Der
|
||
häufigste Fall der Praxis lag dazwischen und hatte kein Werkzeug: *Das Modell rechnet, der
|
||
Plan ist zulässig — und trotzdem enttäuschend, nur weiß niemand woran es liegt.*
|
||
|
||
**Dieselbe Fabrik wie die Konfliktsuche, nach der Reparatur.** Dort war die Lackierkapazität
|
||
von 150 Stunden der Kern des Widerspruchs ($2 \cdot 40 + 3 \cdot 30 = 170 > 150$); eine
|
||
zweite Schicht bringt sie auf 210, der Mindestumsatz sinkt auf 10 000 €. Damit kann ein
|
||
Leser dieselben 14 benannten Bedingungen über zwei Kapitel hinweg verfolgen — erst
|
||
„warum nicht?", dann „was kostet was?".
|
||
|
||
**Drei gemessene Befunde:**
|
||
|
||
1. **Bindend heißt nicht teuer.** Fünf Bedingungen binden, „Lackierbudget Schicht 2" mit
|
||
einem Schattenpreis von **0,00 €**. Dieselbe Unterscheidung, die `Erklaerbarkeit.py` an
|
||
einer einzelnen Zuweisung zeigt — hier im Dualwert eines LP.
|
||
2. **Der Schattenpreis ist eine Momentaufnahme.** Für die Lackiererei verspricht die
|
||
Hochrechnung $4{,}00 \cdot 30 = 120$ €, gemessen kommen **60 €**; der Preis gilt exakt
|
||
bis **+15 Stunden**. Die zweite Hälfte der Sonderschicht wäre bezahlt und wirkungslos.
|
||
Und die Ranglisten drehen sich: nach Schattenpreis führt der Trägervertrag (18 €/Stück),
|
||
nach tatsächlicher Wirkung die Montage-Sonderschicht (450 €).
|
||
3. **Ein Wunsch kann unmöglich statt teuer sein.** „Mindestens 55 Rahmen" ist `INFEASIBLE`,
|
||
blockiert von **drei** Bedingungen gemeinsam (jede einzeln entfernt macht ihn möglich) —
|
||
die Deletion-Filter-Struktur, angewandt auf einen Wunsch statt auf einen Widerspruch. Mit
|
||
+15 Stunden Lackierkapazität wird er erfüllbar und kostet dann **nichts**: Der Plan wählt
|
||
diese Menge von sich aus. Der Konflikt bestand nie zwischen Vertrieb und Produktion.
|
||
|
||
**Der Managementbericht** entsteht in Teil (4) aus genau diesen Zahlen — kein Satz enthält
|
||
eine Zahl, die nicht vorher gerechnet wurde, auch nicht die Rangfolge und nicht die Namen
|
||
der blockierenden Bedingungen. Voraussetzung dafür ist der sprechende Name jeder Bedingung;
|
||
aus `A_ub[7]` wird kein Satz.
|
||
|
||
**Und die Grenze, die dazugehört** — als ⚠️-Kasten und als Übungsaufgabe: Ein solcher
|
||
Bericht erklärt das **Modell**, nicht die Wirklichkeit. Die Aufgabe lässt den
|
||
Deckungsbeitrag von *Deckel* von 9 € auf 20 € steigen. Gemessen: Der Plan kippt (Rahmen auf
|
||
das Vertragsminimum 40, Deckel auf die Marktgrenze 120), „Kapazität Lackieren" hört auf zu
|
||
binden, „Liefervertrag Rahmen" fängt damit an — **und der Satz „Teuerste Bindung ist
|
||
Liefervertrag Träger" steht wörtlich unverändert im Bericht**, während die Zahl dahinter von
|
||
18 € auf 58 € springt. Ein Bericht, der sich nicht ändert, ist kein Beweis dafür, dass sich
|
||
nichts geändert hat.
|
||
|
||
**Die neue Zählprobe aus 6.1a hat sich sofort bewährt:** Nach dem Einbau der Übungsaufgabe
|
||
meldete `--check` „6 Loesungen, das Kapitel aber 7 Aufgaben", bevor die Lösung geschrieben
|
||
war. Genau der Fall, für den sie gebaut wurde.
|
||
|
||
Stand danach: 294 Abschnitte, 725 Querverweise, 328 Indexmarken, **75** Programme,
|
||
139 Aufgaben mit 139 Lösungen, PDF **737** Seiten.
|
||
|
||
---
|
||
|
||
## 8. Commit-Historie des V04-Strangs
|
||
|
||
> **Zu den Kapitelnummern in dieser Tabelle:** Sie stammen aus den Commit-Betreffs und geben
|
||
> die Nummerierung **zum Zeitpunkt des Commits** wieder. Nach den acht in Phase 3
|
||
> eingefügten Kapiteln stimmen sie nicht mehr mit dem heutigen Buch überein — sie hier
|
||
> nachzuziehen würde die Tabelle von den tatsächlichen Commits entkoppeln. Überall sonst in
|
||
> dieser Datei stehen Kapitel**namen**.
|
||
|
||
| Commit | Datum | Inhalt |
|
||
| --- | --- | --- |
|
||
| `604c532` | 06.09. | Version 04 anlegen: Infrastruktur und Kapitel 1–3 |
|
||
| `72fa8a8` | 06.09. | Arbeitsplan als versionierte Datei |
|
||
| `a661a99` | 06.09. | Kapitel 4: Schattenpreise, und wann sie eine Entscheidung nicht tragen |
|
||
| `1d0170a` | 06.09. | Kapitel 5: MIP-Gap, Warm-Starts und die stille Big-M-Falle |
|
||
| `138be3f` | 07.09. | Kapitel 6: Die fünf Antworten von CP-SAT und die Strafgewichts-Falle |
|
||
| `360ffb2` | 07.09. | Kapitel 7 und die Entscheidungs-Flussdiagramme für Teil II und III |
|
||
| `b052653` | 07.09. | Kapitel 8: Jenseits der Konvexität — lokale Optima, Multistart, MINLP |
|
||
| `4ab2ad3` | 07.09. | Kapitel 9: Newsvendor als Einstieg, und der zweite Fluch des Durchschnitts |
|
||
| `fe7c0b9` | 07.09. | Kapitel 10: Rückwärtsinduktion und der Zustand, der zu wenig weiß |
|
||
| `f519e2d` | 07.09. | Kapitel 11 und der Auftakt zu Teil IV |
|
||
| `2583bbc` | 07.09. | Kapitel 12: Die Effizienzlinie als Speisekarte |
|
||
| `a7049e5` | 07.09. | Kapitel 13: Der VaR sagt wie oft, der CVaR wie schlimm |
|
||
| `5213258` | 07.09. | Kapitel 14: Die Reihenfolge zählt, und die Zahl der Versuche auch |
|
||
| `b154fe8` | 07.09. | Kapitel 15 — Phase 1 abgeschlossen |
|
||
| `33e9a36` | 07.09. | Phase 2 beginnt: `or_kern.py` als gemeinsamer Unterbau |
|
||
| `eeea9e8` | 07.09. | Excel-Schicht nach `or_kern.py`, `Excel_Bruecke.py` umgestellt |
|
||
| `ed3640d` | 07.09. | Phase 2 abgeschlossen: Statusauswertung, Abnahmeprüfung, Solverwechsel |
|
||
| `5c6cc2d` | 07.09. | Plan und Fortschritt als eigene Dateien im Repo-Wurzelverzeichnis |
|
||
| `1c856f1` | 07.09. | Kapitel 1 der Version 03: Umformatierung durch den Editor übernehmen |
|
||
| *(dieser)* | 07.09. | Planungsdateien konsolidiert, `CLAUDE.md` kennt jetzt Version 04 |
|