Der Hinweis, die Website haenge einen Bau zurueck (toter Colab-Link, PDF- Groesse), stammte aus der Zeit vor veroeffentliche_04.py und ist seit dem ersten Skriptlauf falsch. An seiner Stelle steht jetzt, wer das Hochladen uebernimmt und was dabei geprueft wird. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2292 lines
138 KiB
Markdown
2292 lines
138 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, 6 Anhänge, 296 Abschnitte, 825 Querverweise,
|
||
# 328 Indexmarken, 37 Dateien, 28.544 Zeilen, 305 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 | **37** (31 + 5 Teil-Synthesen + geteilter Anhang E/F) |
|
||
| Kapitel | 15 | **23** |
|
||
| Anhänge | 4 | **6** |
|
||
| Zeilen im Gesamtdokument | 11 082 | **28 544** |
|
||
| Größe des Gesamtdokuments | 606 KB | **1 620 KB** |
|
||
| Hauptüberschriften | 131 | **305** |
|
||
| registrierte Abschnitte | 122 | **296** |
|
||
| aufgelöste Querverweise | 314 | **825** (0 unaufgelöst) |
|
||
| Indexmarken | 295 | **328** |
|
||
| Beispielprogramme | 41 | **76** (alle lauffähig) |
|
||
| PDF-Seiten | — | **759** |
|
||
| Notebooks | — | **25** |
|
||
| Plotly-Figuren | — | **4** |
|
||
| Diagramme (SVG) | 26 | **33**, davon **19** mit Generatorskript (16 Skripte); keine PNG-Zweitfassung mehr |
|
||
|
||
**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 Stand
|
||
|
||
**Alle Phasen sind abgeschlossen** — die sechs des Umbaus, dazu Phase 6 bis 8 aus
|
||
`Verbesserungen_02.md`/`_03.md`. `Version_04/` ist seit dem 08.09.2026 ein eigenes
|
||
Git-Repository. 23 Kapitel, 6 Anhänge, 5 Teil-Synthesen, **76** lauffähige Programme,
|
||
141 Aufgaben mit 141 Lösungen, 33 Diagramme, PDF mit 759 Seiten, 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`.
|
||
|
||
### Was von außen kommen muss
|
||
|
||
1. ✅ **Remote-Repository angelegt und gepusht** — <https://kitux.de/forgejo/dschlueter/operations_research>,
|
||
öffentlich, Branch `main`.
|
||
2. ✅ **`COLAB_BASIS_URL` geklärt — und zwar anders als geplant.** Der Platzhalter zeigte auf
|
||
GitHub, das Repository liegt auf einer eigenen Forgejo-Instanz. Colab öffnet Notebooks
|
||
**nur** aus GitHub, Google Drive oder einem Upload; die URL-Form
|
||
`colab.research.google.com/github/…` ist fest auf GitHub verdrahtet. Eine
|
||
selbstgehostete Adresse dort einzusetzen ergäbe keinen Link zum eigenen Server, sondern
|
||
einen toten GitHub-Link — der Kommentar im Quelltext („zu ändern sind nur Kontoname und
|
||
Repository-Name") ging von GitHub aus und war damit selbst irreführend.
|
||
Die Konstante steht jetzt auf `""`: Die Colab-Zeile entfällt, der **Download-Link auf das
|
||
Notebook bleibt**, ergänzt um den Hinweis, was man damit tut. Ein späterer GitHub-Spiegel
|
||
braucht weiterhin nur diesen einen Wert.
|
||
3. ✅ **Veröffentlicht** unter <https://jamulix.de/OR/> — Landing-Page, Kapitelseiten, Diagramme,
|
||
Notebooks, Suchindex, KaTeX und PDF sind erreichbar (alles HTTP 200 geprüft).
|
||
|
||
Das Hochladen ist seit dem 08.09.2026 Aufgabe von `veroeffentliche_04.py`
|
||
(`rsync --delete`, Vor- und Nachprüfung). Der frühere Warnhinweis an dieser Stelle — die
|
||
Website hänge einen Bau zurück — hat sich damit erledigt: Das Skript **weigert sich**,
|
||
einen veralteten Stand hochzuladen, und prüft hinterher dreizehn Adressen per HTTP.
|
||
Nach jedem `--pdf --html` folgt ein Lauf des Skripts.
|
||
|
||
### Bewusst zurückgestellt (aus `Verbesserungen_02/_03`)
|
||
|
||
* **Streamlit-Apps** (V03/1, 2, 5) — Entscheidung des Autors. Sie wären eine neue
|
||
Artefaktklasse: nicht über die Programmsuite prüfbar, ohne abgedruckte Ausgabe (Regel 4).
|
||
Der „Was-wäre-wenn"-Gedanke daraus ist in {ref:sec:praxisfallen-attribution} ohne UI
|
||
eingelöst.
|
||
* **Zielgruppen-Callouts in allen Kapiteln** (V02/5) — die Lernpfade und die One-Pager aus
|
||
7.1 leisten dasselbe gezielter, ohne Eingriff in jeden Abschnitt.
|
||
* **`joblib`/`ray`** (V02/2) — `num_workers` und `ProcessPoolExecutor` decken den Bedarf;
|
||
ein Cluster-Framework wäre eine Abhängigkeit ohne Beispiel, das sie braucht.
|
||
* **Folium-Karten** (V03/3) — **blockiert durch eine offene Entscheidung**: Die VRP-Instanz
|
||
ist ein synthetisches 100 × 100-km-Raster ohne Geokoordinaten. Eine echte Karte verlangt
|
||
entweder eine neue Instanz (und damit neue abgedruckte Zahlen) oder eine willkürliche
|
||
Verankerung an einem realen Ort.
|
||
* **Executive-PDF-Report** (V03/6) — ohne Streamlit-Apps fehlt der Ort, an dem er entstünde.
|
||
|
||
### Offen, rein redaktionell
|
||
|
||
**53 der 141 Lösungstitel weichen vom Aufgabentitel ab** — durchweg Kürzungen
|
||
(„Zweistufiges Modell rechnen" → „Zweistufig rechnen"), alle in den 15 Abschnitten aus
|
||
Version 03; die acht in Phase 3 ergänzten stimmen vollständig. Seit 6.1a trägt jede Lösung
|
||
wieder die richtige Nummer, die Navigation ist also intakt. Ein Angleichen wäre Kosmetik,
|
||
kein Fehler — aber es würde den Anhang lesbarer machen.
|
||
|
||
### Was einer späteren Sitzung auffällt und **kein** Fehler ist
|
||
|
||
* **14 der 33 Diagramme haben keinen Generator.** Dokumentierte Entscheidung — Kästen,
|
||
Pfeile, beschriftete Formeln, generische Funktionsverläufe. Kriterium in Abschnitt 6c.
|
||
* **`Mutationstest.py` benutzt weiterhin `subprocess`.** Richtig so: Dort läuft pytest auf
|
||
einer mutierten Kopie, das ist ein externes Werkzeug auf veränderten Dateien und keine
|
||
Import-Isolation (Abschnitt 8.2).
|
||
* **Die abgedruckte Ausgabe von `Parallele_Suche.py` ist absichtlich nicht byteidentisch
|
||
reproduzierbar.** Die Zahl der verschiedenen Pläne schwankt — und genau das ist die
|
||
Aussage des Abschnitts.
|
||
* **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.
|
||
|
||
### ✅ 6.3 Parallelität und Determinismus ({ref:kap:cpsat})
|
||
|
||
Neuer Abschnitt {ref:sec:cpsat-parallele-suche} plus `Parallele_Suche.py` (76. Programm).
|
||
Setzt Paket 2 aus `Verbesserungen_02.md` um.
|
||
|
||
**Was fehlte, war nicht die Regel, sondern die Messung.** Das Buch setzt an acht Stellen
|
||
`num_workers = 1` mit dem Kommentar „für eine reproduzierbare Ausgabe", und der Spickzettel
|
||
nennt den Grund. Nachgeprüft hatte es nie jemand. Gerechnet wird auf demselben Job-Shop wie
|
||
{ref:sec:cpsat-intervallvariablen-job-shop-scheduling}, nur groß genug, dass die Suche
|
||
arbeitet: 12 Aufträge, 10 Maschinen, 120 Arbeitsgänge.
|
||
|
||
**Zwei Befunde, beide stärker als die Behauptung:**
|
||
|
||
1. **Die Beschleunigung ist überlinear.** Acht Arbeiter waren im abgedruckten Lauf nicht
|
||
achtmal, sondern **12,3-mal** schneller als einer. Kein Messfehler: CP-SAT vervielfacht
|
||
nicht dieselbe Suche, sondern lässt **verschiedene Strategien** nebeneinander laufen, die
|
||
einander ihre Schranken mitteilen. Der zweite Arbeiter ist nicht „der zweite Kern",
|
||
sondern ein anderes Verfahren.
|
||
2. **Der Seed genügt nicht — und zwar schon ab zwei Arbeitern.** Ein Arbeiter: 1 Plan aus
|
||
4 Läufen. **Zwei** Arbeiter: 3 verschiedene Pläne aus 4 Läufen, bei identischem
|
||
`random_seed` und identischem Zielwert 183. Es braucht also keine acht Arbeiter, um die
|
||
Reproduzierbarkeit zu verlieren.
|
||
|
||
**Die vollständige Antwort kam erst über die Übungsaufgabe**, und sie ist besser als der
|
||
Abschnitt allein: Mit `num_workers = 1`, aber **fünf verschiedenen Seeds**, ergeben sich
|
||
ebenfalls **fünf verschiedene Pläne**. Keiner der beiden Parameter sichert die
|
||
Reproduzierbarkeit allein:
|
||
|
||
| | Seed fest | Seed variabel |
|
||
| --- | --- | --- |
|
||
| **1 Arbeiter** | reproduzierbar | 5 Pläne aus 5 Läufen |
|
||
| **mehrere Arbeiter** | 3 Pläne aus 4 Läufen | erst recht nicht |
|
||
|
||
Weiter gemessen für die Aufgabe: Der Gewinn kehrt sich um — auf 24 Kernen Faktor **10,2**
|
||
(8 Arbeiter), **12,3** (16), **9,0** (24). „So viele Arbeiter wie Kerne" ist damit
|
||
widerlegt. Und bei 15 Aufträgen läuft ein Arbeiter ins 60-s-Limit (`FEASIBLE`, Makespan
|
||
200), während acht `OPTIMAL` mit **demselben** Makespan 200 nach 26,4 s melden — der
|
||
Unterschied liegt nicht in der Lösung, sondern im **Beweis**, dass es keine bessere gibt.
|
||
|
||
**Zur abgedruckten Ausgabe:** Sie trägt die Kennzeichnung „Laufzeiten *und* die Zahl der
|
||
verschiedenen Pläne sind hardwareabhängig" — nach dem Muster, das
|
||
{ref:kap:testing} für `Benchmark_Skalierung.py` schon verwendet. Der Vergleich des
|
||
extrahierten Programms mit dem Abdruck weicht denn auch in genau einer Zelle ab (4 statt 3
|
||
verschiedene Pläne bei 4 Arbeitern); Zielwert und Struktur sind identisch. Hier ist die
|
||
Nichtreproduzierbarkeit der abgedruckten Zahl **die Aussage selbst** — der einzige Fall im
|
||
Buch, in dem das so ist.
|
||
|
||
Im MILP-Kapitel bekam die entsprechende Stelle in `Warmstart_Effekt.py` einen Verweis;
|
||
dort steht nach Regel 12 der Kapitel**name**, keine Nummer.
|
||
|
||
Stand danach: 295 Abschnitte, 730 Querverweise, **76** Programme, 140 Aufgaben mit
|
||
140 Lösungen, PDF **744** Seiten.
|
||
|
||
### ✅ 7.1 Vier Zielgruppen-One-Pager (Vorwort)
|
||
|
||
Setzt Paket 4 aus `Verbesserungen_03.md` um. Das Titelblatt wirbt seit dem Umbau mit
|
||
Entscheidern, Ingenieuren und Investoren — die Lernpfade im Vorwort waren aber nach
|
||
**Themen** geschnitten, nicht nach **Rollen**. Der neue Abschnitt „Für wen dieses Buch
|
||
geschrieben ist" steht zwischen „Wie dieses Buch aufgebaut ist" und den Lernpfaden und
|
||
schließt genau diese Lücke: vier Einstiege, jeder mit Schmerzpunkt, drei Belegen, dem
|
||
passenden Pfad und der Frage, an der es in dieser Rolle scheitert.
|
||
|
||
**Die Entscheidung, die den Abschnitt trägt: nur gemessene Zahlen.** Jeder Beleg ist ein
|
||
Ergebnis, das im Buch gerechnet wird — kein Werbeversprechen:
|
||
|
||
* Entscheider: der Erwartungswert-Plan aus {ref:kap:supplychain} kostet **149 % mehr**
|
||
(874 870 € statt 351 356 €) und führt in 28 von 40 Szenarien zum Lastabwurf; der Preis
|
||
eines Prozentpunkts Versorgungssicherheit steigt um das 4,5-fache
|
||
({ref:sec:unsicherheit-chance-constraints}).
|
||
* Ingenieure: der Schattenpreis der Lackiererei gilt bis +15 Stunden, eine 30-Stunden-Schicht
|
||
bringt deshalb 60 € statt 120 € ({ref:sec:praxisfallen-attribution}); das Gesamtbild der
|
||
Metaheuristiken 2 497 → 2 289 Minuten bei einer Schranke von 1 768.
|
||
* Investoren: VaR 1,86 % gegen CVaR 2,99 %, schlechtester Tag −23,0 % — der am VaR nichts
|
||
ändert ({ref:kap:cvar}).
|
||
* Studierende: das Modell mit dem **schlechteren** MSE trifft die günstigere Entscheidung
|
||
({ref:kap:prognose}).
|
||
|
||
**Und eine Erwartung wird ausdrücklich enttäuscht.** `Verbesserungen_03` verlangt für die
|
||
Managementseite „typischer ROI und Amortisationszeit". Das Buch hat diese Zahlen nicht, und
|
||
jede hier genannte wäre erfunden. Der One-Pager sagt das offen und nennt stattdessen die
|
||
beiden Werkzeuge, mit denen der Leser **seine eigene** Zahl ausrechnet: den EVPI aus
|
||
{ref:kap:unsicherheit} als Obergrenze für jedes Prognoseprojekt und die Kostenzurechnung
|
||
aus {ref:sec:praxisfallen-attribution}.
|
||
|
||
Kein neues Programm, keine neue Aufgabe — der Abschnitt bündelt Vorhandenes. Querverweise
|
||
730 → 755, PDF 744 → **746** Seiten.
|
||
|
||
### ✅ 7.2 Wasserfalldiagramm „was es bringt" ({ref:kap:supplychain})
|
||
|
||
Setzt den umsetzbaren Teil von Paket 3 aus `Verbesserungen_03.md` um (Vorher-Nachher-
|
||
Wasserfall, Business Impact). Neuer Generator `bilder_04/erzeuge_wirkung.py`; das
|
||
Supply-Chain-Kapitel bekommt damit **sein erstes Diagramm** überhaupt.
|
||
|
||
**Der Anlass:** Die Tabelle im Kapitel nannte nur die Summen — 874 870 € gegen 351 356 €,
|
||
149 % mehr. Wofür genau, stand nirgends.
|
||
|
||
**Der Generator rechnet die Instanz erneut**, wie es die Konvention verlangt („so können
|
||
Diagramm und Buchtext nicht auseinanderlaufen"). Die Probe fiel gut aus: Der unabhängige
|
||
Nachbau reproduziert beide abgedruckten Summen **exakt**, dazu 178,7 MWh Fehlmenge und
|
||
28/40 Szenarien mit Abwurf. Kein Auseinanderlaufen — der erste Generator dieser Reihe, der
|
||
nichts findet, und das ist ein Ergebnis für sich.
|
||
|
||
**Was die Zerlegung zeigt und die Tabelle nicht:**
|
||
|
||
| | Anfahrt | Brennstoff | Lastabwurf | Gesamt |
|
||
| --- | ---: | ---: | ---: | ---: |
|
||
| zweistufig | 58 000 € | 293 356 € | 0 € | **351 356 €** |
|
||
| Erwartungswert | 58 000 € | 280 631 € | 536 239 € | **874 870 €** |
|
||
|
||
Die **Anfahrkosten sind identisch** — der Erwartungswert-Plan spart also nicht dort, wo man
|
||
es vermuten würde. Er spart 12 725 € Brennstoff und bezahlt dafür 536 239 € Lastabwurf: das
|
||
**42-fache**. „149 % mehr" ist eine Zahl, über die man streiten kann; „wir sparen 12 725 €
|
||
und riskieren 536 239 €" ist eine, über die man entscheidet.
|
||
|
||
Die Trennung von Brennstoff- und Abwurfkosten ist der einzige Zusatz gegenüber `bewerte()`
|
||
im Buchprogramm; dort wird sie nicht gebraucht.
|
||
|
||
**Ein eigener Fehler unterwegs:** Ein `.replace(",", ".")` für deutsche Tausenderpunkte lag
|
||
auf dem **ganzen Titelsatz** und machte aus „Brennstoff, Preis" ein „Brennstoff. Preis".
|
||
Jetzt gibt es `euro()`, das nur auf Zahlen angewandt wird — mit einem Kommentar, der genau
|
||
davor warnt.
|
||
|
||
SVG byteidentisch über zwei Läufe. Diagramme jetzt **33**, davon **19** mit Generator
|
||
(16 Skripte).
|
||
|
||
### ✅ 7.3 Glossar „Deutsch — OR" ({ref:kap:modellierung})
|
||
|
||
Setzt Paket 7 aus `Verbesserungen_03.md` um. Neuer Abschnitt
|
||
{ref:sec:modellierung-glossar}, unmittelbar hinter dem Gesprächsleitfaden: Die zwölf Fragen
|
||
liefern Sätze — hier werden sie übersetzt.
|
||
|
||
**Die Abgrenzung zu Anhang B war die eigentliche Entwurfsfrage.** Der Musterkatalog hat
|
||
längst eine Spalte „Umgangssprachlich", aber er läuft in die andere Richtung: **Muster →
|
||
Formulierung**. Wer aus einer Besprechung kommt, hat den Satz und sucht den Baustein. Das
|
||
Lexikon ist genau so herum gebaut (21 Einträge, jeweils mit B-Nummer), und Anhang B bekam
|
||
einen Rückverweis. Kein Duplikat, sondern die fehlende Gegenrichtung.
|
||
|
||
**Der Teil, der den Abschnitt trägt, ist aber nicht die Tabelle**, sondern was danach kommt:
|
||
**fünf Wendungen, bei denen die Übersetzung eine Entscheidung erzwingt** — „möglichst",
|
||
„nicht mehr als drei pro Woche", „im Durchschnitt", „so schnell wie möglich", „fair". Bei
|
||
jeder gibt es zwei bis drei vertretbare Modelle, und die Wahl gehört dem Betrieb, nicht dem
|
||
Modellierer. Das ist die These des Kapitels („Ein Satz ist noch kein Modell"), hier auf die
|
||
Alltagssprache angewandt.
|
||
|
||
Dazu die **Gegenrichtung OR → Deutsch**: acht Sätze, die sich bewährt haben, wenn ein
|
||
Ergebnis jemandem erklärt werden muss, der das Modell nicht kennt — einschließlich der
|
||
Formulierung für `INFEASIBLE` („Ihre Regeln widersprechen sich", **nicht** „der Computer
|
||
schafft es nicht").
|
||
|
||
Die neue Übungsaufgabe lässt drei Zielfunktionen für „gleichmäßig ausgelastet" aufstellen;
|
||
die Lösung nennt je einen Fall, in dem die Wahl den Plan sichtbar ändert — und weist darauf
|
||
hin, dass zwei der drei einen Halbtagsmonteur bestrafen.
|
||
|
||
Mitgezogen: Lernziel, Zusammenfassung, Rückverweis aus Anhang B, Pfad E im Vorwort
|
||
(er führte bisher direkt zum Musterkatalog, jetzt über das Lexikon).
|
||
|
||
**Damit ist Phase 7 abgeschlossen.** 296 Abschnitte, 766 Querverweise, 141 Aufgaben mit
|
||
141 Lösungen, PDF **749** Seiten.
|
||
|
||
### ✅ 8.1 Synthese-Seiten je Teil — und ein Fund in der Lesekette
|
||
|
||
Setzt Paket 5 aus `Verbesserungen_02.md` um (den Teil, der nicht zurückgestellt wurde). Fünf
|
||
neue Dateien `19_/29_/39_/49_/52_Synthese_*.md`, je eine am Ende eines Teils, mit eigener
|
||
Website-Seite (`SONDERSEITEN`, weil sie bewusst **keine** `# Kapitel:`-Überschrift tragen —
|
||
sie sind keine Kapitel, sondern der Rückblick auf einen Teil).
|
||
|
||
**Der Entwurf musste sich abgrenzen:** Die Teil-**Einleitungen** haben bereits
|
||
Entscheidungsdiagramme („welches Werkzeug passt zu meinem Problem"). Eine zweite
|
||
Entscheidungsmatrix am Teil-Ende wäre eine Dopplung gewesen. Die Synthesen leisten deshalb
|
||
das, was eine Einleitung nicht kann: den **Vergleich über die Kapitel hinweg** (Verfahren
|
||
nebeneinander, mit der Spalte „wo es aufhört"), eine Tabelle **„was dieser Teil gemessen
|
||
hat"** — Behauptung gegen Messung gegen Fundstelle — und drei Fehler, die der Teil
|
||
verhindert. Zitiert wird ausschließlich, was im Buch tatsächlich gerechnet wird.
|
||
|
||
### Drei Funde beim Einbau
|
||
|
||
**Erstens: Teil III sagte „die drei Kapitel dieses Teils", hat aber fünf.** Phase 3 hatte
|
||
{ref:kap:mehrziel} und {ref:kap:prognose} hinzugefügt, die Einleitung blieb stehen. Sie
|
||
nennt die drei Abweichungen jetzt weiterhin, schließt die beiden späteren Kapitel aber
|
||
ausdrücklich an.
|
||
|
||
**Zweitens: `50_Praxis.md` verwies auf die Projektwerkstatt mit „acht eigene Anwendungen"** —
|
||
sie hat elf.
|
||
|
||
**Drittens, und das ist der eigentliche Fund: Die Lesekette der Quelldateien führte an acht
|
||
Kapiteln vorbei.** Nach Phase 3 zeigte `12_Python_Oekosystem.md` direkt auf
|
||
`20_Lineare_Programmierung.md`, `23_Graphen…` direkt auf `30_QP…`, `32_Dynamische…` direkt
|
||
auf `40_Finanzdaten.md`, `50_Praxis.md` direkt auf die Projektwerkstatt. Wer der Kette
|
||
folgte, übersprang **acht von 23 Kapiteln** — darunter Metaheuristiken, Spaltengenerierung,
|
||
Mehrziel, Predict-then-Optimize, Strukturbrücke, Supply-Chain und das ganze
|
||
Testing-Kapitel. Zehn weitere Dateien hatten gar keine Navigationszeile.
|
||
|
||
**Zur Reichweite, damit sie nicht überschätzt wird:** Diese Zeilen stehen **nur in den
|
||
Quelldateien**. `entferne_navigation()` streicht sie aus dem Gesamtdokument, und die Website
|
||
baut ihre Vor/Zurück-Knöpfe selbst aus `DATEIEN`. PDF und Website waren also nie betroffen —
|
||
wohl aber jeder, der die Markdown-Dateien im Repository liest, und das wird nach der
|
||
Veröffentlichung der Normalfall sein.
|
||
|
||
Die Kette ist jetzt über **alle 35 Übergänge** geschlossen, und `--check` bewacht sie: Fehlt
|
||
eine Zeile oder zeigt sie an der in `DATEIEN` folgenden Datei vorbei, ist der Lauf rot.
|
||
Gegengetestet mit beiden Bruchformen.
|
||
|
||
Stand danach: **36 Dateien** (31 + 5 Synthesen), 296 Abschnitte, **815** Querverweise,
|
||
303 Hauptüberschriften, PDF **758** Seiten.
|
||
|
||
### ✅ 8.2 Solver-Isolation ohne `subprocess`-Codestrings
|
||
|
||
Setzt den Isolationsteil von Paket 1 aus `Verbesserungen_02.md` um. Der Plan nannte zwei
|
||
Programme; beim Suchen kam ein **drittes** dazu, das dasselbe Muster verwendete.
|
||
|
||
**Was ersetzt wurde.** `Ein_System_Vier_Ansaetze.py` und `Benchmark_Skalierung.py` hielten
|
||
ihre vier Solvervarianten als **Zeichenketten** in einem Dictionary und gaben sie an
|
||
`python -c` weiter — bei `Benchmark_Skalierung.py` sogar mit `.format()`-Platzhaltern für
|
||
die Instanzgröße. Aus jeder Variante ist jetzt eine gewöhnliche Funktion mit **lokalem
|
||
Import** geworden. `Solverwechsel_CPSAT_HiGHS.py` rief sich selbst über `sys.argv` erneut
|
||
auf; auch das entfällt.
|
||
|
||
Ausgeführt wird über einen `ProcessPoolExecutor` mit zwei Einstellungen, die zusammen die
|
||
Garantie ergeben — und beide sind nötig:
|
||
|
||
* `mp_context="spawn"` — frischer Interpreter statt geerbtem Speicher. Unter Linux ist
|
||
`fork` der Standard, und damit wäre alles bereits Importierte auch im Kind importiert.
|
||
* `max_tasks_per_child=1` — ein **neuer** Prozess je Aufgabe. Ohne das verwendet der Pool
|
||
seinen Arbeiter wieder, und beim zweiten Solver ist der Konflikt zurück. Nachgemessen:
|
||
vier Aufgaben, vier verschiedene PIDs.
|
||
|
||
Der zweite Punkt hat einen eigenen ⚠️-Kasten bekommen, weil der Fehler leicht zu machen und
|
||
schwer zu finden ist: Der Absturz käme nicht beim ersten Solver, sondern beim zweiten — und
|
||
sähe aus wie ein Problem des zweiten.
|
||
|
||
**Regel 4, dreifach geprüft.** Alle drei Programme drucken Ausgaben, die im Buch stehen:
|
||
|
||
* `Ein_System_Vier_Ansaetze.py`: identisch bis auf die Zeitspalte, **einschließlich der
|
||
Spannweite 2,41 · 10⁻⁸**, auf die sich der Merksatz des Kapitels beruft.
|
||
* `Benchmark_Skalierung.py`: **alle zwölf Zielwerte und alle drei Spannweiten
|
||
bitgleich**; Zeiten und Speicher haben sich verschoben, beide sind im Abdruck seit jeher
|
||
als hardwareabhängig gekennzeichnet.
|
||
* `Solverwechsel_CPSAT_HiGHS.py`: Ausgabe ohne Zeiten unverändert.
|
||
|
||
**Was bewusst `subprocess` bleibt:** `Mutationstest.py`. Dort wird pytest auf einer
|
||
**mutierten Kopie** in einem temporären Verzeichnis gestartet — ein externes Werkzeug auf
|
||
veränderten Dateien, nicht die Isolation eines Imports. Für diesen Fall ist `subprocess`
|
||
richtig.
|
||
|
||
**Neu im Kapitel Ökosystem:** ein Abschnitt „Wie die Isolation aussieht, wenn sie tragen
|
||
soll" — warum ein Codestring die schlechteste Umsetzung von „eigener Prozess" ist (unsichtbar
|
||
für Editor, Linter und Testwerkzeug; Tippfehler fallen erst zur Laufzeit auf; übergeben
|
||
lassen sich nur Zeichenketten). Anhang C nennt jetzt ebenfalls `ProcessPoolExecutor` statt
|
||
`subprocess`.
|
||
|
||
**Ein eigener Fehler, gefunden und abgesichert:** Ich hatte dem neuen `###` ein
|
||
`{#sec:...}`-Label gegeben. `ABSCHNITT_RE` erkennt nur `## ` — das Label wäre nie
|
||
registriert worden, und jeder Verweis darauf ins Leere gelaufen, ohne Warnung. Label
|
||
entfernt, und `--check` meldet diesen Fall jetzt. Gegengetestet.
|
||
|
||
Stand danach: 818 Querverweise, PDF **760** Seiten, 69 netzfreie Programme fehlerfrei.
|
||
|
||
### ✅ Achter Fund: die „Übung N.N"-Verweise
|
||
|
||
Sieben Sätze der Bauart „Übung 8.5 lässt Sie diese Balance untersuchen" — und **sechs davon
|
||
standen in alter Zählung**. „8" war in Version 03 das QP/NLP-Kapitel, heute ist es
|
||
Kapitel 11. Dieselbe Familie wie die Denkfehler-Verweise aus 6.1a, nur mit einem Wort, das
|
||
keine der bestehenden Prüfungen kannte.
|
||
|
||
Aufgaben haben kein eigenes Label, ein `{ref:}` auf eine einzelne Aufgabe ist also nicht
|
||
möglich. Verwiesen wird stattdessen auf **Abschnitt plus Aufgabentitel** — und der ist
|
||
stabil: „die Aufgabe *Gültigkeitsbereich des Schattenpreises*
|
||
({ref:sec:lp-uebungsaufgaben})". Jedes Ziel wurde einzeln über die alte Zählung bestimmt
|
||
und am Zusammenhang geprüft.
|
||
|
||
**Ein Fall war knifflig:** „Übung 6.7 (Wochendienstplan)" meinte die siebte CP-SAT-Aufgabe
|
||
der alten Zählung, also *Eigener Dienstplan* — die heute an achter Stelle steht, weil ich in
|
||
Phase 6.3 selbst eine Aufgabe davor eingefügt habe. Wer nur die Kapitelnummer angepasst
|
||
hätte, wäre bei der falschen Aufgabe gelandet.
|
||
|
||
`--check` kennt jetzt auch „Übung"/„Uebung". Gegengetestet. Querverweise 818 → 825.
|
||
|
||
### ✅ 8.3 Dockerfile — gebaut und durchgemessen
|
||
|
||
Setzt den letzten Teil von Paket 1 aus `Verbesserungen_02.md` um. Zweistufig: Die erste
|
||
Stufe übersetzt die Abhängigkeiten in eine virtuelle Umgebung und braucht dafür einen
|
||
Compiler, die zweite kopiert nur `/opt/venv` — die Bauumgebung landet nicht im Image.
|
||
Installiert werden die Gruppen `finance`, `large-scale`, `api` und `dev` aus
|
||
`pyproject.toml`; `figures` fehlt bewusst, weil es zusätzlich Graphviz verlangt.
|
||
|
||
**Es wurde nicht behauptet, sondern gebaut.** Ergebnis: **1,31 GB**, und darin
|
||
|
||
* der `Installationstest.py` mit allen drei Solver-Funktionstests bestanden,
|
||
* die 33 pytest-Tests bestanden,
|
||
* **alle 69 netzfreien Programme fehlerfrei** — einschließlich der drei aus 8.2, deren
|
||
`spawn`-Isolation im Container ebenso trägt wie außerhalb.
|
||
|
||
**Zwei Dinge, die der Bau gelehrt hat.** Erstens: `libgomp1` fehlt im
|
||
`python:3.12-slim`-Image und wird von OR-Tools und HiGHS zur Laufzeit gebraucht — ohne es
|
||
scheitert der erste Solveraufruf mit `libgomp.so.1: cannot open shared object file`.
|
||
Zweitens, ein eigener Fehler: Ich hatte `USER kurs` **vor** das `mkdir /buch/output` gesetzt;
|
||
`/buch` gehört root, und der Bau brach in der letzten Zeile ab. Beides steht jetzt als
|
||
Kommentar im Dockerfile.
|
||
|
||
Das Image führt die Programme aus und **baut das Buch nicht** — pandoc, xelatex und inkscape
|
||
wären zusammen über ein Gigabyte ohne Nutzen für jemanden, der die Beispiele durchrechnen
|
||
will. Ein `.dockerignore` hält Website, PDF und Notebooks aus dem Build-Kontext.
|
||
|
||
Und der Punkt, der erklärungsbedürftig ist: Das Image enthält `ortools` **und** `highspy`,
|
||
obwohl sie sich nicht gemeinsam importieren lassen. Der Konflikt wird nicht durch Weglassen
|
||
gelöst, sondern zur Laufzeit durch getrennte Prozesse — ein Image mit nur einem von beiden
|
||
könnte die Hälfte der Beispiele nicht ausführen.
|
||
|
||
**Damit ist Phase 8 abgeschlossen und der Plan abgearbeitet.**
|
||
|
||
### ✅ Aufgeräumt — und dabei zwei Funde
|
||
|
||
**Die 33 PNG-Zweitfassungen sind weg.** Jedes Diagramm lag doppelt vor, als SVG und als PNG,
|
||
und **kein einziges `src=`/`href=` in der Website zeigte je auf ein PNG**. Der Build kopierte
|
||
sie trotzdem mit: 3,3 MB im Repository plus 3,3 MB, die bei jeder Veröffentlichung auf den
|
||
Webserver gingen, ohne dass sie jemand abrief. Die 15 Generatoren schreiben jetzt nur noch
|
||
SVG; die Docstrings sind mitgezogen. Vor dem Löschen geprüft: Jedes PNG hatte sein
|
||
gleichnamiges SVG, und alle 33 waren versioniert — rückholbar.
|
||
|
||
**Fund 1 dabei: `erzeuge_kap06_gantt.py` folgte als einziger Generator nicht der Konvention.**
|
||
Weder `svg.hashsalt` noch `metadata={"Date": None}` waren gesetzt. Sein SVG trug deshalb
|
||
einen echten Zeitstempel und bei jedem Lauf andere `clip-path`-IDs — es war **nie**
|
||
byteidentisch reproduzierbar, obwohl `CLAUDE.md` genau das für alle Generatoren festhält.
|
||
Aufgefallen ist es nur, weil nach der PNG-Umstellung 32 von 33 SVGs bitgleich blieben und
|
||
eines nicht. Jetzt byteidentisch über zwei Läufe.
|
||
|
||
**Fund 2: `spiegle_bilder()` legte leere Verzeichnisse auf dem Webserver an.** Der
|
||
Dateifilter arbeitete korrekt, aber `os.walk` durchlief auch `__pycache__/`, und
|
||
`os.makedirs()` erzeugte es am Ziel — ein leerer Ordner in `OR_HTML_04/bilder_04/`. Die
|
||
Verzeichnisliste wird jetzt vorher gefiltert. Gegengetestet.
|
||
|
||
**`Excel_Bruecke.py` schreibt neben das Skript statt ins Arbeitsverzeichnis.** Es benutzte
|
||
blanke relative Namen; wer es aus der Repository-Wurzel startete, verstreute dort
|
||
`produktionsmix.xlsx` und `produktionsmix_ergebnis.xlsx`. Jetzt wie die vier anderen
|
||
schreibenden Programme über `os.path.dirname(os.path.abspath(__file__))`. Nachgemessen: Lauf
|
||
aus der Wurzel legt dort **null** Dateien ab.
|
||
|
||
**Und `git gc`:** 653 lose Objekte gepackt, `.git` von 71 MB auf **28 MB** — reines
|
||
Repacken, kein Inhalt berührt.
|
||
|
||
### ✅ Notebook-Übersichtsseite — und ein 403, dessen Ursache woanders lag
|
||
|
||
`https://jamulix.de/OR/Notebooks_04/` lieferte 403. **Die Ursache war ein Link, den ich
|
||
selbst gesetzt hatte:** Beim Eintragen der Website-Adresse ins README hatte ich in der
|
||
Lesen-Tabelle das *Verzeichnis* verlinkt. Keine Seite der Website tut das — die
|
||
Kapitelseiten zeigen direkt auf `Notebooks_04/<name>.ipynb`, und das funktioniert. Der
|
||
Webserver verweigert Directory-Listing, zu Recht.
|
||
|
||
**Die Frage „gibt es so etwas nicht schon?" war aber berechtigt** — und hat die eigentliche
|
||
Lücke sichtbar gemacht: `baue_programme_seite()` erzeugt seit jeher `programme.html`, eine
|
||
gestaltete Übersicht der 76 Programme. Für die 25 Notebooks gab es kein Gegenstück; sie
|
||
wurden ausgeliefert, aber keine Seite listete sie.
|
||
|
||
Neu ist deshalb `baue_notebooks_seite()`, gebaut nach demselben Muster und mit denselben
|
||
Bausteinen: die Zuordnung Seite → Notebook kommt fertig aus `baue_notebooks()`, Rahmen und
|
||
Navigation aus `baue_seiten_schablone()` und `baue_sidebar_html()`. Gruppiert nach Vorspann,
|
||
Kapiteln und Anhängen; der einleitende Absatz nennt die drei Wege, ein Notebook auszuführen
|
||
(Jupyter, Colab-Upload, Kurs-Image).
|
||
|
||
**Dazu ein ZIP mit allen 25 Notebooks** — 208 KB, und **byteidentisch über zwei Läufe**. Ein
|
||
ZIP speichert je Eintrag die Änderungszeit; ohne festen Wert entstünde bei jedem Bau eine
|
||
andere Datei, und da `OR_HTML_04/` versioniert ist, wüchse das Repository bei jedem Lauf.
|
||
Die Einträge werden deshalb sortiert und mit `ZipInfo(date_time=(1980,1,1,0,0,0))`
|
||
geschrieben. Dieselbe Sorgfalt wie `svg.hashsalt` bei den Diagrammen.
|
||
|
||
**Gegen künftige 403** schreiben `spiegle_notebooks()` und `baue_programme_seite()` je eine
|
||
dreizeilige `index.html` mit `meta refresh` in ihr Downloadverzeichnis. Wer die URL rät oder
|
||
einen alten Link hat, landet auf der Übersicht statt auf einem Fehler — das behebt zugleich
|
||
denselben latenten Fall bei `programme/`.
|
||
|
||
**Ein eigener Fehler, zum zweiten Mal derselbe:** Das `.replace(",", ".")` für deutsche
|
||
Tausenderpunkte hing am Ende eines mehrzeiligen f-Strings — und in Python bindet die
|
||
Methode an die **gesamte** zusammengesetzte Zeichenkette. Aus „Drei Wege, sie auszuführen"
|
||
wurde „Drei Wege. sie auszuführen". Exakt der Fehler, vor dem ich in `erzeuge_wirkung.py`
|
||
selbst einen Kommentar hinterlassen hatte. Die Zahl wird jetzt getrennt formatiert, mit
|
||
demselben Warnhinweis daneben.
|
||
|
||
Geprüft: 25 gelistete Notebooks, 0 tote Links auf der Seite, ZIP mit 25 Einträgen und
|
||
identischen Zeitstempeln, Seitenleisteneintrag auf allen 40 Seiten mit Seitenleiste
|
||
(`gesamtdokument.html` hat konstruktionsbedingt keine).
|
||
|
||
### ✅ Glossar und Literatur getrennt, Menü ergänzt
|
||
|
||
`94_Anhang_Glossar_und_Literatur.md` enthielt zwei verschiedene Nachschlagewerke in einer
|
||
Datei. Jetzt sind es zwei Anhänge: **E = Glossar** (92 Einträge), **F =
|
||
Literaturverzeichnis** (6 Kategorien) — aus 5 Anhängen wurden 6.
|
||
|
||
**Die Teilung war billiger als erwartet.** Die Datei trug bereits zwei eigenständige
|
||
Überschriften mit nichts als einem `---` dazwischen, und im ganzen Buch gab es **genau
|
||
einen** `{ref:anhang:glossar-literatur}` — die „Weiter mit"-Zeile im Spickzettel. Die 47
|
||
Treffer in `OR_HTML_04/` waren erzeugt und lösten sich beim Bau von selbst auf. Geprüft:
|
||
keine einzige Inhaltszeile ging verloren, alle 92 Glossareinträge stehen auf der neuen
|
||
Seite, die Zahl der Indexmarken blieb bei 328.
|
||
|
||
**Das Menü der Website** hat zwei Einträge mehr:
|
||
|
||
```
|
||
Beispielprogramme
|
||
Notebooks
|
||
Download Notebooks als ZIP
|
||
Glossar
|
||
Stichwortverzeichnis
|
||
Gesamtdokument (eine HTML-Seite)
|
||
Download Buch als PDF
|
||
```
|
||
|
||
Der ZIP-Link benutzt dieselbe Konstante wie `baue_notebooks_seite()` beim Schreiben des
|
||
Archivs — so können Link und Datei nicht auseinanderlaufen. (Die Einrückung des ZIP-Punkts
|
||
wurde später wieder entfernt; die eigens dafür angelegte CSS-Klasse `.sidebar-unterpunkt`
|
||
ist mit ihr verschwunden, statt als tote Regel stehen zu bleiben.) Das Glossar erscheint
|
||
**absichtlich doppelt**: hier oben als Abkürzung neben dem Stichwortverzeichnis, weil beide
|
||
dasselbe leisten, und unten in der Gruppe „Anhänge", weil es ein Anhang ist.
|
||
|
||
**Nebenbefund: `OR_HTML_04/assets/site.css` ist eine Quelle, keine erzeugte Datei.** Der
|
||
Build schreibt sie nicht. Dasselbe gilt für `site.js`, `icons.svg`, `plotly.min.js` und
|
||
`katex/`. Sie liegen im als „generiert" beschriebenen Verzeichnis, sind aber von Hand
|
||
gepflegt bzw. mitgeliefert — wer `OR_HTML_04/` löscht und neu baut, verliert sie.
|
||
(Nachtrag: Der nächste Abschnitt räumt das auf. **Und `highlight.css` gehört nicht in diese
|
||
Liste** — die erzeugt `erzeuge_highlight_css()` aus `pandoc --print-highlight-style`; die
|
||
erste Fassung dieses Absatzes zählte sie fälschlich mit.)
|
||
|
||
Die alte `anhang-glossar-literatur.html` musste von Hand entfernt werden; der Build räumt
|
||
verwaiste Seiten nicht ab.
|
||
|
||
### ✅ `web_04/` — die statischen Assets bekommen eine Quelle
|
||
|
||
Vier Dateien und die 22 KaTeX-Dateien lagen im erzeugten Verzeichnis, obwohl kein Skript sie
|
||
je geschrieben hat. Die Falle daran: Wer eine CSS-Regel suchte, suchte sie in den Quellen und
|
||
fand nichts — genau das ist beim Einrücken des ZIP-Menüpunkts passiert. Und wer `OR_HTML_04/`
|
||
gelöscht und neu gebaut hätte, hätte eine Website ohne Stil, ohne Symbole und ohne
|
||
Formelsatz bekommen.
|
||
|
||
Jetzt liegen sie in `web_04/`, dessen Aufbau (`assets/`, `katex/`) das Ziel spiegelt.
|
||
`spiegle_statische_assets()` kopiert sie bei **jedem** Lauf; `kopiere_plotly_bibliothek()`
|
||
füllt die Quelle statt des Ziels; `_lade_icon_sprite_inline()` liest die Quelle, hängt also
|
||
nicht mehr vom eigenen Ergebnis ab.
|
||
|
||
**Zwei Wächter**, weil genau diese Verwechslung schon vorgekommen ist:
|
||
|
||
* Wurde die Kopie in `OR_HTML_04/` von Hand geändert (Inhalt weicht ab *und* Zeitstempel ist
|
||
neuer), **bricht der Bau ab** und nennt den `mv`-Befehl, der es richtigstellt. Kein
|
||
stilles Überschreiben.
|
||
* `pruefe_assets()` liest die `href=`/`src=`-Literale aus dem Quelltext des Bauskripts und
|
||
verlangt für jedes einen Erzeuger — entweder `web_04/` oder die Liste `ERZEUGTE_ASSETS`
|
||
(`highlight.css`, `search-index.js`, `programme.js`). Ein künftiger Eintrag im Seitenkopf
|
||
ohne Datei dahinter fällt sofort auf.
|
||
|
||
Die Probe aufs Exempel: `rm -rf OR_HTML_04 && --html` baut alle 213 Dateien wieder auf,
|
||
Dateiliste identisch zur Sicherung.
|
||
|
||
### 🐛 Fund: Das Stichwortverzeichnis war nicht byte-reproduzierbar
|
||
|
||
Beim Gegenprüfen der Neubau-Probe unterschied sich `stichwortverzeichnis.html` zwischen zwei
|
||
Läufen, ohne dass sich eine Quelle geändert hatte. Ursache in `ziel_links()`: sortiert wurde
|
||
nach `(seite, kontext)` — und `kontext` ist der **Kapiteltitel**, für alle Marken einer Datei
|
||
also derselbe. Bei zwei Fundstellen im selben Kapitel war der Schlüssel gleich, und die
|
||
Reihenfolge fiel auf die eines `set()` zurück, also auf den je Prozess zufälligen
|
||
`PYTHONHASHSEED`.
|
||
|
||
Derselbe Konstruktionsfehler war auch sichtbar: Vier `{idx:Branch-and-Bound}` in Kapitel 6
|
||
ergaben **vier optisch identische Links** nebeneinander — zehn Einträge im Register waren
|
||
davon betroffen. Behoben durch einen Link je Kapitel (die erste Fundstelle in
|
||
Dokumentreihenfolge, `dict` statt `set`). Damit ist die Sortierung zugleich eindeutig.
|
||
|
||
Gegenprobe: drei Läufe mit `PYTHONHASHSEED=random` liefern dieselbe Prüfsumme
|
||
`3da5b9b2…`. Keine Hauptzeile trägt mehr einen doppelten Link; die 219 Fachbegriffe und 328
|
||
Indexmarken bleiben unverändert.
|
||
|
||
### ✅ `veroeffentliche_04.py` — der Upload wird nachprüfbar
|
||
|
||
Der Anlass war messbar. Der Live-Stand vor dem Skript:
|
||
|
||
```
|
||
/OR/ 200
|
||
/OR/anhang-glossar-literatur.html 200 <- Leiche des vorigen Stands
|
||
/OR/anhang-glossar.html 404
|
||
/OR/notebooks.html 404
|
||
/OR/Notebooks_04.zip 404
|
||
/OR/Notebooks_04/ 403
|
||
```
|
||
|
||
**`scp -r` überschreibt nur, es löscht nie.** Die alte Glossar-Seite bliebe nach einem
|
||
`scp`-Upload dauerhaft erreichbar. Das Zielverzeichnis vorher zu leeren wäre die grobe
|
||
Antwort: Bricht die Übertragung von 23 MB in 213 Dateien ab, ist die Website tot.
|
||
`rsync --delete` überträgt nur Geändertes und entfernt genau das, was verschwunden ist.
|
||
|
||
Der zweite Grund wiegt schwerer: **Die Website hing wochenlang einen Bau zurück**, ohne dass
|
||
es auffiel. Deshalb prüft das Skript vorher, ob eine Quelldatei neuer ist als die gebaute
|
||
Website, ob das PDF der Website byte-genau dem gebauten entspricht und ob die
|
||
Verzeichnis-Weiterleitungen vorhanden sind (der Grund für den 403). Und es prüft
|
||
**hinterher per HTTP** vierzehn Adressen nach — einschließlich der Probe, dass die entfernte
|
||
Seite wirklich 404 liefert.
|
||
|
||
Dazwischen ein Probelauf, der zählt: Mehr als 30 Löschungen brechen ab. Das ist der Schutz
|
||
gegen einen falschen Zielpfad — mit `--delete` räumt ein Tippfehler sonst ein fremdes
|
||
Verzeichnis aus.
|
||
|
||
**Anmeldung per SSH-Schlüssel, nicht per Passwort in der `.env`.** `rsync` und `scp` haben
|
||
keine Passwort-Option; ein Passwort bräuchte `sshpass` und stünde während der Übertragung in
|
||
der Prozessliste. Die `.env` (in `.gitignore`, Vorlage `.env.beispiel`) enthält deshalb nur
|
||
Host, Benutzer, Zielpfad und URL.
|
||
|
||
**Beim ersten Test lief die Prüfung in eine irreführende Meldung:** „Auf dem Server ist kein
|
||
rsync installiert" — tatsächlich schlug schon die Anmeldung fehl, weil der öffentliche
|
||
Schlüssel noch nicht auf `jamulix.de` liegt. Die Prüfung unterscheidet die Fälle jetzt am
|
||
ssh-Rückgabewert 255 und nennt `ssh-copy-id`.
|
||
|
||
### ✅ Erstmals per Skript veröffentlicht — 08.09.2026
|
||
|
||
`https://jamulix.de/OR/` steht auf dem Stand des heutigen Baus. **213 Dateien übertragen, 35
|
||
gelöscht**, alle vierzehn HTTP-Proben richtig — darunter die entscheidende:
|
||
`/OR/anhang-glossar-literatur.html` liefert jetzt **404** statt 200. Der zweite Lauf
|
||
unmittelbar danach übertrug **0 Dateien** und löschte nichts.
|
||
|
||
**Die Abbruchgrenze hat gleich beim ersten Einsatz gegriffen** und sich damit bewährt: 35
|
||
Löschungen über der Vorgabe von 30. Die Liste war anzusehen, bevor sie angehoben wurde — und
|
||
sie war vollständig erklärbar:
|
||
|
||
| Anzahl | Was | Warum |
|
||
| --- | --- | --- |
|
||
| 33 | `bilder_04/*.png` | die verwaisten PNGs, die im Aufräumschritt lokal gelöscht wurden |
|
||
| 1 | `bilder_04/__pycache__/` | das leere Verzeichnis aus dem `spiegle_bilder()`-Fehler |
|
||
| 1 | `anhang-glossar-literatur.html` | die geteilte Anhangseite |
|
||
|
||
Dass ein `scp`-Upload alle 35 stehen gelassen hätte, ist damit belegt statt behauptet.
|
||
|
||
**Zwei Hürden auf dem Weg, beide in Konsequenzen übersetzt:**
|
||
|
||
*`ssh-copy-id` braucht ein echtes Terminal.* In der Claude-Code-Sitzung gibt es keins
|
||
(`tty` → „kein Terminal"), aber `DISPLAY=:1` ist gesetzt. `ssh` schließt daraus auf einen
|
||
grafischen Passwortdialog und sucht `/usr/bin/ssh-askpass`, das nicht installiert ist. Ein
|
||
Passwort über die normale Eingabe nimmt `ssh` grundsätzlich nicht entgegen. Der Schritt
|
||
gehört in ein normales Terminalfenster.
|
||
|
||
*Danach sperrte der Server den Port.* Die fehlgeschlagenen Passwortversuche plus die
|
||
Verbindungen des Skripts lösten eine Sperre aus (fail2ban): erste Verbindung erfolgreich,
|
||
die folgenden `Connection refused` — während die Website selbst weiter mit 200 antwortete.
|
||
Nach sieben Minuten Warten war sie weg. **Weitere Versuche hätten sie verlängert.**
|
||
|
||
Beides steckt jetzt im Skript: `pruefe_ziel_erreichbar()` unterscheidet die abgewiesene
|
||
Verbindung von der abgelehnten Anmeldung — vorher meldete es in beiden Fällen „Der
|
||
Schlüssel liegt noch nicht auf dem Server", was beim zweiten Mal glatt falsch war, denn er
|
||
lag längst dort.
|
||
|
||
Damit ist der seit dem Colab-Umbau offene Punkt erledigt: Online steht kein toter
|
||
Colab-Link mehr, `Notebooks_04/` liefert statt 403 eine Übersichtsseite, und das PDF
|
||
entspricht byte-genau dem gebauten.
|
||
|
||
### 🐛 Neunter Fund: die Dateiübersicht des Leser-Wegweisers
|
||
|
||
Beim Durchgehen der Dokumentation zeigte sich, dass
|
||
`Operations_Research_mit_Python_Version_04/README.md` — die Datei, die **Leser** als erstes
|
||
öffnen — noch die Struktur der Version 03 beschrieb:
|
||
|
||
* **12 der 15 Kapitelnummern waren falsch.** „20_Lineare_Programmierung.md | 4" — es ist
|
||
Kapitel 5. Bei den Finanzkapiteln lag die Abweichung bei sieben: README 11, tatsächlich 18.
|
||
* **8 der 23 Kapitel fehlten ganz** (unter anderem Metaheuristiken, Spaltengenerierung,
|
||
Mehrziel, Predict-then-Optimize, Testing).
|
||
* Auch `93_Anhang_Spickzettel.md` fehlte in der Anhangtabelle, und der Bauabschnitt nannte
|
||
dreimal `build_version_03.py` statt `_04`, dazu „Single-Page-HTML" für einen Schalter, der
|
||
längst eine mehrseitige Website erzeugt.
|
||
|
||
Dieselbe Regel, dieselbe Ursache: eine abgeleitete Zahl von Hand geschrieben. Deshalb dieselbe
|
||
Behandlung — **die Tabelle wird jetzt erzeugt**, nicht korrigiert. `schreibe_kapiteltabelle()`
|
||
ersetzt den Block zwischen `<!-- KAPITELTABELLE:ANFANG -->` und `<!-- KAPITELTABELLE:ENDE -->`
|
||
aus der Struktur, die `baue_seiten_registry()` ohnehin ermittelt — derselben Quelle, aus der
|
||
die Nummern im Buch stammen. Auseinanderlaufen können die beiden damit nicht mehr.
|
||
|
||
`--check` meldet einen veralteten Block und bricht ab; ein Lauf ohne `--check` zieht ihn nach.
|
||
Gegenprobe: Alle **37** Dateien aus `DATEIEN` stehen in der erzeugten Übersicht, mit 23
|
||
Kapiteln, 5 Teil-Synthesen und 6 Anhängen.
|
||
|
||
**Das Muster ist inzwischen belastbar:** Neun Funde, neun Prüfungen. Keiner davon wurde durch
|
||
Nachdenken gefunden — jeder durch einen Vergleich der Behauptung mit der Messung.
|
||
|
||
**Nebenbei am Upload-Skript:** `rsync` bekommt jetzt `--checksum`. Der Bau schreibt alle 42
|
||
Seiten bei jedem Lauf neu, auch wenn sich nichts geändert hat; nach Zeitstempel und Größe
|
||
hätte das jedes Mal rund 19 MB sinnlose Übertragung bedeutet. Gemessen nach einem Neubau
|
||
ohne inhaltliche Änderung: **0 statt 151 Dateien**.
|
||
|
||
### 🐛 Zehnter Fund: Startseite und PDF-Titelseite zeigten verschiedene Texte
|
||
|
||
Die Frage lautete, warum die Titelseite der Online-Fassung einen anderen Text trägt als die
|
||
des PDFs. Die Antwort war messbar. Der Titeltext stand **viermal** im Repository:
|
||
`NEUER_TITEL.md`, die Konstanten in `bilder_04/erzeuge_titelseite.py`, der Titelblock des
|
||
Vorworts — und noch einmal fest einprogrammiert im Hero-Block von `baue_landingpage()`.
|
||
|
||
Die drei Commits, die den Titel überarbeitet haben (`ca5621f`, `199dd3b`, `1659fb9` im
|
||
Ursprungs-Repository), fassten jedes Mal dieselben drei Dateien an. **`build_version_04.py`
|
||
war in keinem davon**, und `OR_HTML_04/index.html` taucht in ihren Dateilisten überhaupt
|
||
nicht auf. Die Startseite zeigte deshalb:
|
||
|
||
| | PDF | Online (vorher) |
|
||
| --- | --- | --- |
|
||
| Unterzeile | „Von der mathematischen Modellierung zu praktischen intelligenten Lösungen für Betrieb, Technik und Finanzmärkte" | „Von den mathematischen Grundlagen zur quantitativen Portfolio-Optimierung an den Finanzmärkten." |
|
||
| Kompendium | „Ein **praxisorientiertes** Kompendium **für Entscheider** …" | „Ein **vollständiges** Kompendium mit **Algorithmen** …" |
|
||
| Anwendungen | vorhanden | **fehlte ganz** |
|
||
|
||
**Die Behandlung ist die inzwischen eingeübte:** Nicht die Kopie korrigieren, sondern die
|
||
Kopie abschaffen. `titeltexte_04.py` zerlegt `NEUER_TITEL.md` in Titel, Untertitel,
|
||
Kompendium- und Anwendungen-Absatz; PDF-Titelseite und Startseite lesen dieselbe Funktion.
|
||
Der Autor steht bewusst weiter als Konstante im Generator — die Vorlage beschreibt das Buch,
|
||
nicht sein Impressum.
|
||
|
||
**Der entscheidende Beleg:** Die neu erzeugte `titelseite.svg` ist **byte-identisch**
|
||
(`97fb40d7…`) zur vorherigen. Der Umbau hat am PDF nachweislich nichts verändert — auch
|
||
nicht am Titelumbruch: Die früher handgesetzten drei Zeilen („Optimierte" /
|
||
„Entscheidungsfindung" / „mit Python") entstehen jetzt automatisch aus `_umbrechen()`, und
|
||
zwar exakt so. Der Textvergleich über das SVG scheiterte übrigens: Es enthält **kein einziges
|
||
`<text>`-Element**, matplotlib zeichnet alle 726 Buchstaben als Kurven. Die Prüfsumme ist
|
||
hier der bessere Zeuge.
|
||
|
||
**Das Vorwort** bleibt von Hand gepflegter Buchtext, wird aber von `pruefe_titeltexte()` bei
|
||
jedem `--check` gegen die Quelle gestellt. Gleich beim ersten Lauf fand es einen echten
|
||
Unterschied: Das Vorwort setzte einen Geviertstrich („—"), die Vorlage einen
|
||
Halbgeviertstrich („–"). Im Deutschen ist der Halbgeviertstrich der richtige — das Vorwort
|
||
folgt jetzt der Quelle.
|
||
|
||
Damit ist jede der vier Stellen abgesichert: eine ist die Quelle, zwei werden daraus erzeugt,
|
||
eine wird geprüft.
|
||
|
||
### ✅ Der Upload berichtet jetzt leserlich
|
||
|
||
Der erste Alltagseinsatz des Skripts zeigte drei Schönheitsfehler, alle in der Ausgabe.
|
||
|
||
**Der Bericht stand verkehrt herum.** Die `rsync`-Zeilen erschienen *vor* „Ziel:" und
|
||
„Vorprüfung bestanden". Ursache: Python puffert seine Ausgabe blockweise, sobald sie nicht
|
||
an ein Terminal geht, während der Unterprozess unmittelbar schreibt. Ein
|
||
`sys.stdout.reconfigure(line_buffering=True)` in `main()` stellt die Reihenfolge her.
|
||
|
||
**150 Zeilen für eine übertragene Datei.** `--itemize-changes` lief auch im echten Durchgang
|
||
mit, obwohl der Probelauf längst berichtet hatte, was passieren wird. Von den 150 Zeilen
|
||
besagten 149 nur `.f..t......` — „Inhalt gleich, nur Zeitstempel nachgezogen", die
|
||
erwartbare Folge von `--checksum`. Die Auflistung bleibt jetzt dem Probelauf vorbehalten.
|
||
|
||
**Und der Ersatz war zuerst schlimmer als das Problem.** Statt der Liste sollte
|
||
`--info=progress2` eine einzige, sich selbst überschreibende Fortschrittszeile zeigen. Am
|
||
Terminal tut es das; in einer Pipe bleibt jeder Zwischenstand als eigene Zeile stehen —
|
||
gemessen **213 Zeilen für 213 Dateien**. Die Anzeige hängt deshalb an
|
||
`sys.stdout.isatty()`.
|
||
|
||
**Nebenbei aufgeräumt:** Die 404-Probe auf `anhang-glossar-literatur.html` ist entfernt. Sie
|
||
hat ihren Zweck erfüllt — aber einen Rückschritt könnte sie gar nicht mehr entdecken: Die
|
||
Datei ist gelöscht, die Probe bliebe auch dann grün, wenn jemand `--delete` aus dem
|
||
`rsync`-Aufruf nähme. Von den ursprünglich vierzehn Adressen bleiben dreizehn.
|
||
|
||
---
|
||
|
||
## 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 |
|