Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
# 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
Glossar und Literatur trennen, Menue um Glossar und ZIP-Download ergaenzen
94_Anhang_Glossar_und_Literatur.md enthielt zwei verschiedene Nachschlagewerke
in einer Datei. Jetzt sind es zwei Anhaenge: E = Glossar (92 Eintraege),
F = Literaturverzeichnis (6 Kategorien). Aus 5 Anhaengen werden 6, aus 36
Kapiteldateien 37.
Die Teilung war billig: Die Datei trug bereits zwei eigenstaendige
Ueberschriften mit nichts als einem --- dazwischen, und im ganzen Buch gab es
genau einen {ref:anhang:glossar-literatur} - die Weiter-mit-Zeile im
Spickzettel. Geprueft: keine Inhaltszeile verloren, alle 92 Glossareintraege
auf der neuen Seite, Indexmarken unveraendert bei 328.
Das Seitenleisten-Menue bekommt zwei Eintraege:
Beispielprogramme
Notebooks
Download Notebooks als ZIP <- neu, eingerueckt
Glossar <- neu
Stichwortverzeichnis
Gesamtdokument (eine Seite)
Download als PDF
Der ZIP-Link benutzt dieselbe Konstante wie baue_notebooks_seite() beim
Schreiben des Archivs, damit Link und Datei nicht auseinanderlaufen. Das
Glossar erscheint absichtlich doppelt: hier als Abkuerzung neben dem
Stichwortverzeichnis, und unten in der Gruppe "Anhaenge", weil es ein Anhang
ist.
Die verwaiste anhang-glossar-literatur.html von Hand entfernt - der Build
raeumt alte Seiten nicht ab.
Nebenbefund, in PROGRESS.md festgehalten: OR_HTML_04/assets/site.css ist eine
von Hand gepflegte Quelle, die im als "generiert" beschriebenen Verzeichnis
liegt. Der Build schreibt sie nie. Dasselbe gilt fuer site.js, icons.svg,
highlight.css und plotly.min.js.
--check: 5 Teile, 23 Kapitel, 6 Anhaenge, 296 Abschnitte, 825 Querverweise,
328 Indexmarken, 37 Dateien, 28.544 Zeilen, 305 Hauptueberschriften, keine
Warnung. PDF unveraendert 759 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:48:57 +02:00
# Soll: 5 Teile, 23 Kapitel, 6 Anhänge, 296 Abschnitte, 825 Querverweise,
# 328 Indexmarken, 37 Dateien, 28.544 Zeilen, 305 Hauptüberschriften
Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
# 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) |
| --- | --- | --- |
Glossar und Literatur trennen, Menue um Glossar und ZIP-Download ergaenzen
94_Anhang_Glossar_und_Literatur.md enthielt zwei verschiedene Nachschlagewerke
in einer Datei. Jetzt sind es zwei Anhaenge: E = Glossar (92 Eintraege),
F = Literaturverzeichnis (6 Kategorien). Aus 5 Anhaengen werden 6, aus 36
Kapiteldateien 37.
Die Teilung war billig: Die Datei trug bereits zwei eigenstaendige
Ueberschriften mit nichts als einem --- dazwischen, und im ganzen Buch gab es
genau einen {ref:anhang:glossar-literatur} - die Weiter-mit-Zeile im
Spickzettel. Geprueft: keine Inhaltszeile verloren, alle 92 Glossareintraege
auf der neuen Seite, Indexmarken unveraendert bei 328.
Das Seitenleisten-Menue bekommt zwei Eintraege:
Beispielprogramme
Notebooks
Download Notebooks als ZIP <- neu, eingerueckt
Glossar <- neu
Stichwortverzeichnis
Gesamtdokument (eine Seite)
Download als PDF
Der ZIP-Link benutzt dieselbe Konstante wie baue_notebooks_seite() beim
Schreiben des Archivs, damit Link und Datei nicht auseinanderlaufen. Das
Glossar erscheint absichtlich doppelt: hier als Abkuerzung neben dem
Stichwortverzeichnis, und unten in der Gruppe "Anhaenge", weil es ein Anhang
ist.
Die verwaiste anhang-glossar-literatur.html von Hand entfernt - der Build
raeumt alte Seiten nicht ab.
Nebenbefund, in PROGRESS.md festgehalten: OR_HTML_04/assets/site.css ist eine
von Hand gepflegte Quelle, die im als "generiert" beschriebenen Verzeichnis
liegt. Der Build schreibt sie nie. Dasselbe gilt fuer site.js, icons.svg,
highlight.css und plotly.min.js.
--check: 5 Teile, 23 Kapitel, 6 Anhaenge, 296 Abschnitte, 825 Querverweise,
328 Indexmarken, 37 Dateien, 28.544 Zeilen, 305 Hauptueberschriften, keine
Warnung. PDF unveraendert 759 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:48:57 +02:00
| Kapiteldateien | 22 | **37 ** (31 + 5 Teil-Synthesen + geteilter Anhang E/F) |
Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
| Kapitel | 15 | **23 ** |
Glossar und Literatur trennen, Menue um Glossar und ZIP-Download ergaenzen
94_Anhang_Glossar_und_Literatur.md enthielt zwei verschiedene Nachschlagewerke
in einer Datei. Jetzt sind es zwei Anhaenge: E = Glossar (92 Eintraege),
F = Literaturverzeichnis (6 Kategorien). Aus 5 Anhaengen werden 6, aus 36
Kapiteldateien 37.
Die Teilung war billig: Die Datei trug bereits zwei eigenstaendige
Ueberschriften mit nichts als einem --- dazwischen, und im ganzen Buch gab es
genau einen {ref:anhang:glossar-literatur} - die Weiter-mit-Zeile im
Spickzettel. Geprueft: keine Inhaltszeile verloren, alle 92 Glossareintraege
auf der neuen Seite, Indexmarken unveraendert bei 328.
Das Seitenleisten-Menue bekommt zwei Eintraege:
Beispielprogramme
Notebooks
Download Notebooks als ZIP <- neu, eingerueckt
Glossar <- neu
Stichwortverzeichnis
Gesamtdokument (eine Seite)
Download als PDF
Der ZIP-Link benutzt dieselbe Konstante wie baue_notebooks_seite() beim
Schreiben des Archivs, damit Link und Datei nicht auseinanderlaufen. Das
Glossar erscheint absichtlich doppelt: hier als Abkuerzung neben dem
Stichwortverzeichnis, und unten in der Gruppe "Anhaenge", weil es ein Anhang
ist.
Die verwaiste anhang-glossar-literatur.html von Hand entfernt - der Build
raeumt alte Seiten nicht ab.
Nebenbefund, in PROGRESS.md festgehalten: OR_HTML_04/assets/site.css ist eine
von Hand gepflegte Quelle, die im als "generiert" beschriebenen Verzeichnis
liegt. Der Build schreibt sie nie. Dasselbe gilt fuer site.js, icons.svg,
highlight.css und plotly.min.js.
--check: 5 Teile, 23 Kapitel, 6 Anhaenge, 296 Abschnitte, 825 Querverweise,
328 Indexmarken, 37 Dateien, 28.544 Zeilen, 305 Hauptueberschriften, keine
Warnung. PDF unveraendert 759 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:48:57 +02:00
| Anhänge | 4 | **6 ** |
| Zeilen im Gesamtdokument | 11 082 | **28 544 ** |
2026-09-08 15:01:13 +02:00
| Größe des Gesamtdokuments | 606 KB | **1 620 KB ** |
Glossar und Literatur trennen, Menue um Glossar und ZIP-Download ergaenzen
94_Anhang_Glossar_und_Literatur.md enthielt zwei verschiedene Nachschlagewerke
in einer Datei. Jetzt sind es zwei Anhaenge: E = Glossar (92 Eintraege),
F = Literaturverzeichnis (6 Kategorien). Aus 5 Anhaengen werden 6, aus 36
Kapiteldateien 37.
Die Teilung war billig: Die Datei trug bereits zwei eigenstaendige
Ueberschriften mit nichts als einem --- dazwischen, und im ganzen Buch gab es
genau einen {ref:anhang:glossar-literatur} - die Weiter-mit-Zeile im
Spickzettel. Geprueft: keine Inhaltszeile verloren, alle 92 Glossareintraege
auf der neuen Seite, Indexmarken unveraendert bei 328.
Das Seitenleisten-Menue bekommt zwei Eintraege:
Beispielprogramme
Notebooks
Download Notebooks als ZIP <- neu, eingerueckt
Glossar <- neu
Stichwortverzeichnis
Gesamtdokument (eine Seite)
Download als PDF
Der ZIP-Link benutzt dieselbe Konstante wie baue_notebooks_seite() beim
Schreiben des Archivs, damit Link und Datei nicht auseinanderlaufen. Das
Glossar erscheint absichtlich doppelt: hier als Abkuerzung neben dem
Stichwortverzeichnis, und unten in der Gruppe "Anhaenge", weil es ein Anhang
ist.
Die verwaiste anhang-glossar-literatur.html von Hand entfernt - der Build
raeumt alte Seiten nicht ab.
Nebenbefund, in PROGRESS.md festgehalten: OR_HTML_04/assets/site.css ist eine
von Hand gepflegte Quelle, die im als "generiert" beschriebenen Verzeichnis
liegt. Der Build schreibt sie nie. Dasselbe gilt fuer site.js, icons.svg,
highlight.css und plotly.min.js.
--check: 5 Teile, 23 Kapitel, 6 Anhaenge, 296 Abschnitte, 825 Querverweise,
328 Indexmarken, 37 Dateien, 28.544 Zeilen, 305 Hauptueberschriften, keine
Warnung. PDF unveraendert 759 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:48:57 +02:00
| Hauptüberschriften | 131 | **305 ** |
Phase 7.3: Glossar "Deutsch - OR" - und die Gegenrichtung
Setzt Paket 7 aus Verbesserungen_03.md um. Neuer Abschnitt im Kapitel "Vom
Management-Wunsch zum Modell", unmittelbar hinter dem Gespraechsleitfaden:
Die zwoelf Fragen liefern Saetze - hier werden sie uebersetzt.
Die Abgrenzung zu Anhang B war die eigentliche Entwurfsfrage. Der
Musterkatalog hat laengst eine Spalte "Umgangssprachlich", laeuft aber 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 Eintraege, jeweils mit B-Nummer), und Anhang B bekam einen
Rueckverweis. Kein Duplikat, sondern die fehlende Gegenrichtung.
Der Teil, der den Abschnitt traegt, ist aber nicht die Tabelle, sondern was
danach kommt: fuenf Wendungen, bei denen die Uebersetzung eine Entscheidung
erzwingt - "moeglichst", "nicht mehr als drei pro Woche", "im Durchschnitt",
"so schnell wie moeglich", "fair". Bei jeder gibt es zwei bis drei
vertretbare Modelle, und die Wahl gehoert dem Betrieb, nicht dem
Modellierer. Das ist die These des Kapitels, auf die Alltagssprache
angewandt.
Dazu die Gegenrichtung OR -> Deutsch: acht Saetze fuer die Erklaerung eines
Ergebnisses, einschliesslich der Formulierung fuer INFEASIBLE ("Ihre Regeln
widersprechen sich", nicht "der Computer schafft es nicht").
Die neue Uebungsaufgabe laesst drei Zielfunktionen fuer "gleichmaessig
ausgelastet" aufstellen; die Loesung nennt je einen Fall, in dem die Wahl
den Plan sichtbar aendert - und weist darauf hin, dass zwei der drei einen
Halbtagsmonteur bestrafen.
Mitgezogen: Lernziel, Zusammenfassung, Rueckverweis aus Anhang B, Pfad E im
Vorwort (er fuehrte bisher direkt zum Musterkatalog, jetzt ueber das
Lexikon).
Damit ist Phase 7 abgeschlossen. 296 Abschnitte, 766 Querverweise, 141
Aufgaben mit 141 Loesungen, PDF 749 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:45:42 +02:00
| registrierte Abschnitte | 122 | **296 ** |
Achter Fund (Uebung-Verweise) und Phase 8.3: Dockerfile
Zwei Dinge in einem Commit, weil beide den Plan abschliessen.
ACHTER FUND: sieben Saetze der Bauart "Uebung 8.5 laesst Sie diese Balance
untersuchen" - und sechs davon standen in alter Zaehlung. "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
Pruefungen kannte.
Aufgaben haben kein eigenes Label, ein {ref:} auf eine einzelne Aufgabe ist
also nicht moeglich. Verwiesen wird stattdessen auf Abschnitt plus
Aufgabentitel - und der ist stabil. Jedes Ziel wurde einzeln ueber die alte
Zaehlung bestimmt und am Zusammenhang geprueft.
Ein Fall war knifflig: "Uebung 6.7 (Wochendienstplan)" meinte die siebte
CP-SAT-Aufgabe der alten Zaehlung, also "Eigener Dienstplan" - die heute an
achter Stelle steht, weil in Phase 6.3 eine Aufgabe davor eingefuegt wurde.
Wer nur die Kapitelnummer angepasst haette, waere bei der falschen Aufgabe
gelandet.
--check kennt jetzt auch "Uebung"/"Übung". Gegengetestet.
PHASE 8.3: Dockerfile, zweistufig. Die erste Stufe uebersetzt die
Abhaengigkeiten in eine virtuelle Umgebung und braucht dafuer einen
Compiler, die zweite kopiert nur /opt/venv. Installiert werden die Gruppen
finance, large-scale, api und dev aus pyproject.toml; figures fehlt bewusst,
weil es zusaetzlich Graphviz verlangt.
Es wurde nicht behauptet, sondern gebaut. Ergebnis: 1,31 GB, und darin der
Installationstest mit allen drei Solver-Funktionstests bestanden, die 33
pytest-Tests bestanden und alle 69 netzfreien Programme fehlerfrei -
einschliesslich der drei aus 8.2, deren spawn-Isolation im Container ebenso
traegt wie ausserhalb.
Zwei Dinge, die der Bau gelehrt hat: libgomp1 fehlt im python:3.12-slim-Image
und wird von OR-Tools und HiGHS zur Laufzeit gebraucht (sonst
"libgomp.so.1: cannot open shared object file"). Und ein eigener Fehler:
USER kurs stand vor dem mkdir /buch/output, /buch gehoert root, der Bau
brach in der letzten Zeile ab. Beides steht jetzt als Kommentar im
Dockerfile.
Das Image fuehrt die Programme aus und baut das Buch nicht. Ein
.dockerignore haelt Website, PDF und Notebooks aus dem Build-Kontext. Und es
enthaelt ortools UND highspy, obwohl sie sich nicht gemeinsam importieren
lassen - der Konflikt wird zur Laufzeit durch getrennte Prozesse geloest,
nicht durch Weglassen.
Damit ist Phase 8 abgeschlossen und der Plan abgearbeitet.
Stand: 825 Querverweise, 76 Programme, 33 pytest-Tests, PDF 760 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 13:18:05 +02:00
| aufgelöste Querverweise | 314 | **825 ** (0 unaufgelöst) |
Phase 6.2: Constraint Attribution und Managementbericht
Neuer Abschnitt im Kapitel Praxisfallen plus Constraint_Attribution.py (75.
Programm). Setzt den fehlenden Teil von Paket 3 aus Verbesserungen_02.md um.
Die Luecke, die es schliesst: Erklaerbarkeit.py beantwortet "warum DIESE
Zuweisung?", Konfliktsuche.py "warum geht es GAR NICHT?". Der haeufigste Fall
der Praxis lag dazwischen und hatte kein Werkzeug - das Modell rechnet, der
Plan ist zulaessig, und trotzdem enttaeuschend, nur weiss niemand woran es
liegt.
Gerechnet wird auf derselben Fabrik wie die Konfliktsuche, nach der
Reparatur: Dort war die Lackierkapazitaet von 150 Stunden der Kern des
Widerspruchs (2*40 + 3*30 = 170 > 150), eine zweite Schicht bringt sie auf
210. Damit laesst sich dieselbe Instanz ueber zwei Kapitel verfolgen - erst
"warum nicht?", dann "was kostet was?".
Drei gemessene Befunde:
* Bindend heisst nicht teuer: Fuenf Bedingungen binden, "Lackierbudget
Schicht 2" mit einem Schattenpreis von 0,00 EUR.
* Der Schattenpreis ist eine Momentaufnahme: Fuer die Lackiererei verspricht
die Hochrechnung 4,00 * 30 = 120 EUR, gemessen kommen 60 EUR; der Preis
gilt exakt bis +15 Stunden. Die Ranglisten drehen sich dabei um - nach
Schattenpreis fuehrt der Traegervertrag (18 EUR/Stueck), nach
tatsaechlicher Wirkung die Montage-Sonderschicht (450 EUR).
* Ein Wunsch kann unmoeglich statt teuer sein: "mindestens 55 Rahmen" ist
INFEASIBLE, blockiert von drei Bedingungen gemeinsam. Mit +15 Stunden
Lackierkapazitaet wird er erfuellbar und kostet dann nichts - der Plan
waehlt diese Menge von sich aus. Der Konflikt bestand nie zwischen
Vertrieb und Produktion.
Der Managementbericht entsteht aus genau diesen Zahlen; kein Satz enthaelt
eine Zahl, die nicht vorher gerechnet wurde. Voraussetzung ist der
sprechende Name jeder Bedingung - aus A_ub[7] wird kein Satz.
Dazu die Grenze, als Warnkasten und als Uebungsaufgabe: Der Bericht erklaert
das Modell, nicht die Wirklichkeit. Steigt der Deckungsbeitrag von Deckel
von 9 auf 20 EUR, kippt der Plan (Rahmen auf das Vertragsminimum 40, Deckel
auf die Marktgrenze 120), "Kapazitaet Lackieren" hoert auf zu binden,
"Liefervertrag Rahmen" faengt damit an - und der Satz "Teuerste Bindung ist
Liefervertrag Traeger" steht woertlich unveraendert im Bericht, waehrend die
Zahl dahinter von 18 auf 58 EUR springt.
Die Zaehlprobe aus 6.1a hat sich sofort bewaehrt: Nach dem Einbau der
Uebungsaufgabe meldete --check "6 Loesungen, das Kapitel aber 7 Aufgaben",
bevor die Loesung geschrieben war.
Mitgezogen: Kapitelkopf, Lernziele, Selbsttest, Zusammenfassung,
Vorwort-Programmverzeichnis, Vorwaertsverweis aus Falle 2, Uebungsaufgabe
und Loesung in Anhang A.
Stand: 294 Abschnitte, 725 Querverweise, 328 Indexmarken, 75 Programme,
139 Aufgaben mit 139 Loesungen, 33 pytest-Tests, PDF 737 Seiten, 68
netzfreie Programme fehlerfrei.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 10:30:33 +02:00
| Indexmarken | 295 | **328 ** |
Phase 6.3: Parallelitaet und Determinismus - gemessen statt behauptet
Neuer Abschnitt im CP-SAT-Kapitel 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 "fuer eine reproduzierbare
Ausgabe", nachgeprueft hatte es nie jemand. Gerechnet wird auf demselben
Job-Shop wie das Intervallvariablen-Kapitel, nur gross genug, dass die Suche
arbeitet: 12 Auftraege, 10 Maschinen, 120 Arbeitsgaenge.
Zwei Befunde, beide staerker als die Behauptung:
* Die Beschleunigung ist ueberlinear. Acht Arbeiter waren im abgedruckten
Lauf nicht achtmal, sondern 12,3-mal schneller als einer. Kein Messfehler:
CP-SAT vervielfacht nicht dieselbe Suche, sondern laesst verschiedene
Strategien nebeneinander laufen, die einander ihre Schranken mitteilen.
* Der Seed genuegt nicht - und zwar schon ab ZWEI Arbeitern. Ein Arbeiter:
1 Plan aus 4 Laeufen. Zwei Arbeiter: 3 verschiedene Plaene aus 4 Laeufen,
bei identischem random_seed und identischem Zielwert 183.
Die vollstaendige Antwort kam erst ueber die Uebungsaufgabe: Mit
num_workers = 1, aber fuenf verschiedenen Seeds ergeben sich ebenfalls fuenf
verschiedene Plaene. Keiner der beiden Parameter sichert die
Reproduzierbarkeit allein - erst die Kombination traegt.
Weiter fuer die Aufgabe gemessen: Der Gewinn kehrt sich um (auf 24 Kernen
Faktor 10,2 bei 8 Arbeitern, 12,3 bei 16, 9,0 bei 24) - "so viele Arbeiter
wie Kerne" ist damit widerlegt. Und bei 15 Auftraegen laeuft ein Arbeiter
ins 60-s-Limit (FEASIBLE, Makespan 200), waehrend acht OPTIMAL mit
demselben Makespan 200 nach 26,4 s melden: Der Unterschied liegt nicht in
der Loesung, sondern im Beweis, dass es keine bessere gibt.
Die abgedruckte Ausgabe traegt die Kennzeichnung "Laufzeiten und die Zahl
der verschiedenen Plaene sind hardwareabhaengig" - nach dem Muster, das das
Testing-Kapitel fuer 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 Plaene bei 4 Arbeitern); Zielwert und
Struktur sind identisch. Hier ist die Nichtreproduzierbarkeit der
abgedruckten Zahl die Aussage selbst.
Mitgezogen: Kapitelkopf, Lernziele, Selbsttest, Zusammenfassung,
Vorwort-Programmverzeichnis, Uebungsaufgabe und Loesung in Anhang A, ein
Verweis aus dem bestehenden Callout zu mehrdeutigen Optima und einer aus
Warmstart_Effekt.py im MILP-Kapitel (dort nach Regel 12 der Kapitelname).
Stand: 295 Abschnitte, 730 Querverweise, 328 Indexmarken, 76 Programme,
140 Aufgaben mit 140 Loesungen, 33 pytest-Tests, PDF 744 Seiten, 69
netzfreie Programme fehlerfrei.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:19:53 +02:00
| Beispielprogramme | 41 | **76 ** (alle lauffähig) |
2026-09-08 15:01:13 +02:00
| PDF-Seiten | — | **759 ** |
Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
| Notebooks | — | **25 ** |
| Plotly-Figuren | — | **4 ** |
Aufgeraeumt: 33 PNG-Zweitfassungen, Bau-Ueberbleibsel, Ausgabepfade
Drei Aufraeumarbeiten - und zwei Funde, die dabei auffielen.
1. DIE 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 Veroeffentlichung auf den Webserver gingen. Die 15 Generatoren
schreiben jetzt nur noch SVG, die Docstrings sind mitgezogen. Vor dem
Loeschen geprueft: Jedes PNG hatte sein gleichnamiges SVG, alle 33 waren
versioniert.
FUND 1: erzeuge_kap06_gantt.py folgte als einziger Generator nicht der
Konvention - weder svg.hashsalt noch metadata={"Date": None}. Sein SVG trug
einen echten Zeitstempel und bei jedem Lauf andere clip-path-IDs, war also
nie byteidentisch reproduzierbar, obwohl CLAUDE.md genau das fuer alle
Generatoren festhaelt. Aufgefallen nur, weil nach der PNG-Umstellung 32 von
33 SVGs bitgleich blieben und eines nicht. Jetzt byteidentisch ueber zwei
Laeufe.
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. Die Verzeichnisliste wird jetzt vorher
gefiltert. Gegengetestet.
2. BAU-UEBERBLEIBSEL entfernt (alle ignoriert und neu erzeugbar):
svg-inkscape/, build_v04.log, .pytest_cache/, Programme/output/, vier
__pycache__/ und die Excel-Mappen. Arbeitsbaum 36 -> 33 MB, danach null
ignorierte Ueberbleibsel.
3. 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 ueber os.path.dirname(os.path.abspath(__file__)). Nachgemessen:
Lauf aus der Wurzel legt dort null Dateien ab.
Ausserdem git gc: 653 lose Objekte gepackt, .git von 71 MB auf 28 MB -
reines Repacken, kein Inhalt beruehrt.
Geprueft: alle 33 im Buch referenzierten SVGs vorhanden, in Quelle und
Website; 16 Generatoren fehlerfrei; keine fehlenden Bilder im LaTeX-Lauf;
33 pytest-Tests; PDF unveraendert 760 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:22:26 +02:00
| Diagramme (SVG) | 26 | **33 ** , davon **19 ** mit Generatorskript (16 Skripte); keine PNG-Zweitfassung mehr |
Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
**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.
2026-09-08 01:23:33 +02:00
### 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.
Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
### 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
2026-09-08 13:26:40 +02:00
### Der Stand
Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
2026-09-08 13:26:40 +02:00
**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
Glossar und Literatur trennen, Menue um Glossar und ZIP-Download ergaenzen
94_Anhang_Glossar_und_Literatur.md enthielt zwei verschiedene Nachschlagewerke
in einer Datei. Jetzt sind es zwei Anhaenge: E = Glossar (92 Eintraege),
F = Literaturverzeichnis (6 Kategorien). Aus 5 Anhaengen werden 6, aus 36
Kapiteldateien 37.
Die Teilung war billig: Die Datei trug bereits zwei eigenstaendige
Ueberschriften mit nichts als einem --- dazwischen, und im ganzen Buch gab es
genau einen {ref:anhang:glossar-literatur} - die Weiter-mit-Zeile im
Spickzettel. Geprueft: keine Inhaltszeile verloren, alle 92 Glossareintraege
auf der neuen Seite, Indexmarken unveraendert bei 328.
Das Seitenleisten-Menue bekommt zwei Eintraege:
Beispielprogramme
Notebooks
Download Notebooks als ZIP <- neu, eingerueckt
Glossar <- neu
Stichwortverzeichnis
Gesamtdokument (eine Seite)
Download als PDF
Der ZIP-Link benutzt dieselbe Konstante wie baue_notebooks_seite() beim
Schreiben des Archivs, damit Link und Datei nicht auseinanderlaufen. Das
Glossar erscheint absichtlich doppelt: hier als Abkuerzung neben dem
Stichwortverzeichnis, und unten in der Gruppe "Anhaenge", weil es ein Anhang
ist.
Die verwaiste anhang-glossar-literatur.html von Hand entfernt - der Build
raeumt alte Seiten nicht ab.
Nebenbefund, in PROGRESS.md festgehalten: OR_HTML_04/assets/site.css ist eine
von Hand gepflegte Quelle, die im als "generiert" beschriebenen Verzeichnis
liegt. Der Build schreibt sie nie. Dasselbe gilt fuer site.js, icons.svg,
highlight.css und plotly.min.js.
--check: 5 Teile, 23 Kapitel, 6 Anhaenge, 296 Abschnitte, 825 Querverweise,
328 Indexmarken, 37 Dateien, 28.544 Zeilen, 305 Hauptueberschriften, keine
Warnung. PDF unveraendert 759 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:48:57 +02:00
Git-Repository. 23 Kapitel, 6 Anhänge, 5 Teil-Synthesen, **76 ** lauffähige Programme,
2026-09-08 15:01:13 +02:00
141 Aufgaben mit 141 Lösungen, 33 Diagramme, PDF mit 759 Seiten, Website mit vier
2026-09-08 13:26:40 +02:00
interaktiven Figuren.
Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
Phase 6.1: Chance Constraints - die Zusage "mit 95 % Sicherheit"
Neuer Abschnitt im Kapitel Unsicherheit 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 tatsaechlich fragt. Setzt Paket 4
aus Verbesserungen_02.md um.
Beide Wege an DERSELBEN Instanz (Kraftwerkspark, 500 MW gesicherte Zusage):
analytisch als Second-Order-Cone-Bedingung (CLARABEL) und szenariobasiert als
Big-M-MILP (SciPy/HiGHS) - beides in einem Prozess, ohne ortools- oder
highspy-Import.
Drei gemessene Befunde:
* Der Mittelwertplan haelt 50,08 %. Kein Fehler, sondern die Definition des
Erwartungswerts.
* Sicherheit ist konvex bepreist: 56.289 EUR je Prozentpunkt auf dem Weg zu
80 %, 253.848 EUR zwischen 95 und 99 % - das 4,5-fache. Das Programm
rechnet die Tabelle selbst aus, statt sie zu behaupten.
* Die Zusage gilt nur fuer die unterstellte Verteilung: Der 95-%-Plan haelt
gemessen 87,44 %, sobald die Testverteilung eine Kaeltewelle mit
Dunkelflaute enthaelt, in der alles zugleich einbricht - auch das
Gaskraftwerk. In einer reinen Normalwelt liefert derselbe Plan 94,97 %.
Zwei eigene Fehlgriffe, beide durch Messen aufgefallen und korrigiert: Die
erste Kostentarierung ergab eine entartete Loesung (alles ins Gaskraftwerk),
womit die Kovarianzmatrix wirkungslos war - und gerade sie begruendet die
Kegelform. Und der erste Kaelteeinbruch traf nur Wind und Sonne; der SOC-Plan
hatte die ohnehin herausgehalten und war zufaellig robust, das Argument trug
nicht.
Ehrlich berichtet statt geglaettet: Die Szenariomethode ueberanpasst. Ueber
zwoelf Laeufe (S = 200, 400, 800) lag die tatsaechliche Quote zwischen
93,14 % und 96,71 %, und die Spanne wurde von S = 400 auf 800 wieder
breiter. Beide Solver bestaetigen optimal bei identischen Kosten - echte
Ueberanpassung, kein Solverartefakt. Steht als Warnkasten im Abschnitt.
Die Laufzeitmessung ist aus der Ausgabe entfernt: Eine Wanduhrzeit ist nie
byteidentisch reproduzierbar (2,2 s / 2,3 s zwischen zwei Laeufen) und haette
Regel 4 dauerhaft gebrochen. Danach drei Laeufe zeichengleich, und die
abgedruckte Ausgabe stimmt mit dem Lauf des extrahierten Programms ueberein.
Mitgezogen: Kapitelkopf, Lernziele, Uebersichtstabelle (drei -> vier
Ansaetze), Selbsttest, Zusammenfassung, Vorwort-Programmverzeichnis, eine
neue Uebungsaufgabe und ihre Loesung in Anhang A.
pyproject.toml korrigiert: cvxpy lag in [finance], wird aber von 13
Programmen gebraucht, darunter dreien in diesem Kapitel - jetzt in der
Grundausstattung. Es zieht kein highspy nach, der Solverkonflikt bleibt auf
[large-scale] beschraenkt.
Stand: 293 Abschnitte, 712 Querverweise, 327 Indexmarken, 74 Programme (0
Fehler), 33 pytest-Tests, PDF 725 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:57:19 +02:00
**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` .
2026-09-08 13:26:40 +02:00
### Was von außen kommen muss
Veroeffentlichung: Lizenzen, README, Colab-Entscheidung, JupyterLab im Image
Sechs Dinge, alle fuer das jetzt oeffentliche Repository.
LIZENZEN. LICENSE traegt die MIT-Lizenz fuer den Programmcode (alle .py,
Notebooks, Dockerfile, pyproject.toml), LICENSE-TEXT.md die CC BY-SA 4.0 fuer
Buchtext, PDF, Website und Diagramme. Beide Dateien nennen ausdruecklich, was
sie abdecken und was nicht - ein Codeblock im Buchtext bleibt Programmcode und
steht unter MIT. Vorher hatte das Repository gar keine Lizenz, womit
standardmaessig "alle Rechte vorbehalten" galt und niemand die 76 Programme
haette weiterverwenden duerfen.
COLAB. COLAB_BASIS_URL steht jetzt auf "". Der Platzhalter zeigte auf GitHub,
das Repository liegt auf einer eigenen Forgejo-Instanz - und Colab oeffnet
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 ergaebe keinen Link zum eigenen
Server, sondern einen toten GitHub-Link. Der Kommentar im Quelltext ging von
GitHub aus und war damit selbst irrefuehrend; er ist ersetzt.
Verloren geht nichts: Die 25 Notebooks liegen neben der Website und bekommen
einen echten Download-Link, jetzt mit dem Hinweis, was man damit tut -
"herunterladen und in Jupyter oeffnen, in Colab hochladen oder mit dem
Kurs-Image starten".
JUPYTERLAB IM KURS-IMAGE. Neue pyproject-Gruppe [notebook] mit jupyterlab,
die Notebooks werden ins Image kopiert, und ein kleiner Startbefehl macht
beide Betriebsarten ohne --entrypoint moeglich: ohne Argument JupyterLab, mit
Argument ein einzelnes Programm. Gebaut und geprueft - Rucksack.py laeuft,
JupyterLab antwortet mit HTTP 200 und zeigt alle 25 Notebooks. Image 1,31 ->
1,46 GB. Es laeuft ohne Token, deshalb im README die Portfreigabe an
127.0.0.1 gebunden.
README KOMPLETT NEU. Es war die Bau-Anleitung eines privaten Verzeichnisses
und ist jetzt die Visitenkarte eines oeffentlichen Repositorys: was das Buch
ist, wo man es liest, drei Wege die Beispiele auszufuehren (Container,
schlanke Installation, alles auf einmal), was hier liegt, wie man baut, die
Colab-Frage, die Lizenzen und wie man mitwirkt. Alle relativen Links
geprueft: 0 tot.
.gitattributes. Ohne die Datei entschied core.autocrlf des jeweiligen
Rechners, was beim Klonen passiert - ein Windows-Leser bekam CRLF-Rauschen in
jedem Diff. Jetzt: im Repository immer LF, im Arbeitsverzeichnis passend zum
System, Binaerdateien ausdruecklich ausgenommen. Der Bestand war bereits
sauber (git add --renormalize aendert null Dateien). Zusaetzlich sind die
erzeugten Verzeichnisse als linguist-generated markiert, sonst zaehlt die
Sprachstatistik das Repository als HTML-Projekt.
PROGRESS.md: Remote-Repository als erledigt markiert, der Colab-Befund
festgehalten. CLAUDE.md um Veroeffentlichung, Lizenzen und die neuen Dateien
ergaenzt.
Geprueft: --check ohne Fehler, 0 tote README-Links, 76 Programme unveraendert,
33 pytest-Tests, pyproject baut mit acht Gruppen, PDF 759 Seiten (eine
weniger - die Colab-Zeile entfaellt in 25 Kapiteln).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:54:04 +02:00
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.
Website-Adresse eintragen - und ein Fund beim Nachsehen
https://jamulix.de/OR/ steht jetzt im README (Kopfzeile und Lesen-Tabelle,
mit Direktlinks auf PDF, Gesamtdokument und Notebooks), in der
Namensnennungs-Vorlage von LICENSE-TEXT.md, in CLAUDE.md und in PROGRESS.md.
Beim Eintragen habe ich die Adresse geprueft: Landing-Page, Kapitelseiten,
Diagramme, Notebooks, Suchindex, KaTeX und PDF antworten alle mit HTTP 200,
und die fuenf neuen Synthese-Seiten sind da.
DABEI DER FUND: Die veroeffentlichte Fassung ist einen Bau alt. lp.html
enthaelt noch einen toten Colab-Link, die neue Notebook-Formulierung
("herunterladen und in Jupyter oeffnen...") fehlt, und das PDF dort ist
3.820.135 statt 3.816.207 Bytes gross. Der Stand liegt zwischen Phase 8.1
(die Synthese-Seiten sind schon oben) und der Colab-Umstellung.
OR_HTML_04/ muss also neu kopiert werden.
Damit das nicht wieder unbemerkt auseinanderlaeuft, steht in CLAUDE.md
jetzt, wie man es prueft: ein curl auf lp.html darf 0 Colab-Treffer ergeben,
und die Groesse des dort liegenden PDF muss zur lokalen Datei passen.
Nebenbei die Seitenzahl auf 759 nachgezogen - sie war seit dem Wegfall der
Colab-Zeile in 25 Kapiteln um eine Seite kleiner, im README stand noch 760.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:00:28 +02:00
3. ✅ **Veröffentlicht ** unter <https://jamulix.de/OR/> — Landing-Page, Kapitelseiten, Diagramme,
Notebooks, Suchindex, KaTeX und PDF sind erreichbar (alles HTTP 200 geprüft).
⚠️ **Aber: Die dort liegende Fassung ist einen Bau alt. ** Sie stammt aus der Zeit nach
Phase 8.1 (die fünf Synthese-Seiten sind da), aber vor der Colab-Umstellung: `lp.html`
enthält noch einen toten Colab-Link, die neue Notebook-Formulierung fehlt, und das PDF
ist 3 820 135 statt 3 816 207 Bytes groß. * * `OR_HTML_04/` muss neu kopiert werden** — und
künftig nach jedem `--pdf --html` .
2026-09-08 13:26:40 +02:00
### 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,
Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
Pfeile, beschriftete Formeln, generische Funktionsverläufe. Kriterium in Abschnitt 6c.
2026-09-08 13:26:40 +02:00
* **`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 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
* **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.
---
Phase 6.1: Chance Constraints - die Zusage "mit 95 % Sicherheit"
Neuer Abschnitt im Kapitel Unsicherheit 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 tatsaechlich fragt. Setzt Paket 4
aus Verbesserungen_02.md um.
Beide Wege an DERSELBEN Instanz (Kraftwerkspark, 500 MW gesicherte Zusage):
analytisch als Second-Order-Cone-Bedingung (CLARABEL) und szenariobasiert als
Big-M-MILP (SciPy/HiGHS) - beides in einem Prozess, ohne ortools- oder
highspy-Import.
Drei gemessene Befunde:
* Der Mittelwertplan haelt 50,08 %. Kein Fehler, sondern die Definition des
Erwartungswerts.
* Sicherheit ist konvex bepreist: 56.289 EUR je Prozentpunkt auf dem Weg zu
80 %, 253.848 EUR zwischen 95 und 99 % - das 4,5-fache. Das Programm
rechnet die Tabelle selbst aus, statt sie zu behaupten.
* Die Zusage gilt nur fuer die unterstellte Verteilung: Der 95-%-Plan haelt
gemessen 87,44 %, sobald die Testverteilung eine Kaeltewelle mit
Dunkelflaute enthaelt, in der alles zugleich einbricht - auch das
Gaskraftwerk. In einer reinen Normalwelt liefert derselbe Plan 94,97 %.
Zwei eigene Fehlgriffe, beide durch Messen aufgefallen und korrigiert: Die
erste Kostentarierung ergab eine entartete Loesung (alles ins Gaskraftwerk),
womit die Kovarianzmatrix wirkungslos war - und gerade sie begruendet die
Kegelform. Und der erste Kaelteeinbruch traf nur Wind und Sonne; der SOC-Plan
hatte die ohnehin herausgehalten und war zufaellig robust, das Argument trug
nicht.
Ehrlich berichtet statt geglaettet: Die Szenariomethode ueberanpasst. Ueber
zwoelf Laeufe (S = 200, 400, 800) lag die tatsaechliche Quote zwischen
93,14 % und 96,71 %, und die Spanne wurde von S = 400 auf 800 wieder
breiter. Beide Solver bestaetigen optimal bei identischen Kosten - echte
Ueberanpassung, kein Solverartefakt. Steht als Warnkasten im Abschnitt.
Die Laufzeitmessung ist aus der Ausgabe entfernt: Eine Wanduhrzeit ist nie
byteidentisch reproduzierbar (2,2 s / 2,3 s zwischen zwei Laeufen) und haette
Regel 4 dauerhaft gebrochen. Danach drei Laeufe zeichengleich, und die
abgedruckte Ausgabe stimmt mit dem Lauf des extrahierten Programms ueberein.
Mitgezogen: Kapitelkopf, Lernziele, Uebersichtstabelle (drei -> vier
Ansaetze), Selbsttest, Zusammenfassung, Vorwort-Programmverzeichnis, eine
neue Uebungsaufgabe und ihre Loesung in Anhang A.
pyproject.toml korrigiert: cvxpy lag in [finance], wird aber von 13
Programmen gebraucht, darunter dreien in diesem Kapitel - jetzt in der
Grundausstattung. Es zieht kein highspy nach, der Solverkonflikt bleibt auf
[large-scale] beschraenkt.
Stand: 293 Abschnitte, 712 Querverweise, 327 Indexmarken, 74 Programme (0
Fehler), 33 pytest-Tests, PDF 725 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:57:19 +02:00
## 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.
Sechster und siebter Fund: die letzten handgeschriebenen Nummern
Der Loesungsanhang trug 98 Marken der Bauart "**9.2 — Ansatz waehlen.**",
saemtlich 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 Schoenheitsfrage: "Aufgabe 9.2" existiert wirklich - sie
gehoert zum Kapitel Metaheuristiken ("Zuggroesse und Temperatur"). Wer die
Loesung zu 9.2 nachschlug, landete beim falschen Thema. Beide bisherigen
Pruefungen liefen vorbei: Vor der Zahl steht kein Schluesselwort, und es ist
keine Tabellenzelle.
Zwei Messungen machten die Reparatur billig. In allen 15 nummerierten
Abschnitten gab es exakt so viele Loesungen wie Aufgaben, lueckenlos 1..n -
nur der Kapitelteil war falsch. Und die acht in Phase 3 ergaenzten Kapitel
machten es laengst richtig (**Titel.** ohne Nummer); der Anhang wurde also
nicht auf etwas Neues umgestellt, sondern auf das, was seine neueren Teile
schon taten.
nummeriere_marken() vergibt die Marken jetzt selbst. Der Praefix kommt NICHT
aus "# Anhang A:" - der Anhang ist eine Ueberschrift, seine Abschnitte
gehoeren aber zu 23 verschiedenen Kapiteln. Er kommt aus
{#sec:loesungen-X} -> {#kap:X}; diese Zuordnung gilt geprueft fuer alle 23.
Im Quelltext steht "**{loesung} — Titel.**".
Siebter Fund unterwegs: neun Verweise auf Denkfehler-Nummern. Saetze wie
"siehe Denkfehler 8.1" standen in alter Zaehlung - "8" war in Version 03 das
QP/NLP-Kapitel, heute ist es Graphen. Eine blosse Umnummerierung haette sie
stillschweigend woanders hin zeigen lassen; das Ziel wurde deshalb fuer jeden
einzelnen aus dem Zusammenhang bestimmt und auf {ref:sec:<kapitel>-denkfehler}
umgestellt. Einer steckte in einem Programm-Docstring und bekam nach Regel 12
den Kapitelnamen statt eines Verweises.
Eine bewusste Abweichung vom Plan: Der Denkfehler bekommt GAR KEINE Nummer.
Es gibt je Kapitel genau einen, die zweite Stelle waere immer .1, und er
steht ohnehin unter einer nummerierten Ueberschrift. Eine Nummer, die nie
variiert, holte nur die Fragilitaet zurueck, die hier beseitigt wird. Auch
das folgt den acht neuen Kapiteln, die ihren Denkfehler schon vorher nur mit
dem Titel des Raetsels ankuendigen.
--check bewacht jetzt alle Familien (Denkfehler N.M, Micro-Quiz N, **N.M —
im Anhang) und zaehlt zusaetzlich ab, dass jeder Loesungsabschnitt so viele
{loesung}-Marken hat wie sein Kapitel Aufgaben - das faengt eine vergessene
oder doppelte Loesung, was keine Textsuche leisten kann. Gegengetestet mit
vier kuenstlichen Fehlern auf einmal: alle vier gemeldet, nach dem Rueckbau
null.
Gegenprobe der Umstellung: Gesamtdokument vorher gesichert, nachher
verglichen. 168 geaenderte Zeilen, restlos einer Familie zuzuordnen - 118
Loesungsmarken (138 gesamt minus 20, die in den Kapiteln 1 bis 3 zufaellig
schon stimmten), 15 Anhang-Ueberschriften, 15 Kapitel-Callouts, 12
Micro-Quiz, 7 Zeilen mit Verweisen, 1 Docstring. Keine verschobene oder
inhaltlich veraenderte Zeile.
Ergebnis: 138 Aufgaben, 138 Loesungen, keine ohne Gegenstueck. Micro-Quiz
tragen die echten Kapitelnummern. 293 Abschnitte, 720 Querverweise, 327
Indexmarken, PDF unveraendert 725 Seiten, 33 pytest-Tests, 67 netzfreie
Programme fehlerfrei (die vier yfinance-Programme scheiterten am Rate-Limit
des Anbieters, nicht an dieser Aenderung).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 09:52:41 +02:00
### ✅ 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.
Phase 6.1: Chance Constraints - die Zusage "mit 95 % Sicherheit"
Neuer Abschnitt im Kapitel Unsicherheit 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 tatsaechlich fragt. Setzt Paket 4
aus Verbesserungen_02.md um.
Beide Wege an DERSELBEN Instanz (Kraftwerkspark, 500 MW gesicherte Zusage):
analytisch als Second-Order-Cone-Bedingung (CLARABEL) und szenariobasiert als
Big-M-MILP (SciPy/HiGHS) - beides in einem Prozess, ohne ortools- oder
highspy-Import.
Drei gemessene Befunde:
* Der Mittelwertplan haelt 50,08 %. Kein Fehler, sondern die Definition des
Erwartungswerts.
* Sicherheit ist konvex bepreist: 56.289 EUR je Prozentpunkt auf dem Weg zu
80 %, 253.848 EUR zwischen 95 und 99 % - das 4,5-fache. Das Programm
rechnet die Tabelle selbst aus, statt sie zu behaupten.
* Die Zusage gilt nur fuer die unterstellte Verteilung: Der 95-%-Plan haelt
gemessen 87,44 %, sobald die Testverteilung eine Kaeltewelle mit
Dunkelflaute enthaelt, in der alles zugleich einbricht - auch das
Gaskraftwerk. In einer reinen Normalwelt liefert derselbe Plan 94,97 %.
Zwei eigene Fehlgriffe, beide durch Messen aufgefallen und korrigiert: Die
erste Kostentarierung ergab eine entartete Loesung (alles ins Gaskraftwerk),
womit die Kovarianzmatrix wirkungslos war - und gerade sie begruendet die
Kegelform. Und der erste Kaelteeinbruch traf nur Wind und Sonne; der SOC-Plan
hatte die ohnehin herausgehalten und war zufaellig robust, das Argument trug
nicht.
Ehrlich berichtet statt geglaettet: Die Szenariomethode ueberanpasst. Ueber
zwoelf Laeufe (S = 200, 400, 800) lag die tatsaechliche Quote zwischen
93,14 % und 96,71 %, und die Spanne wurde von S = 400 auf 800 wieder
breiter. Beide Solver bestaetigen optimal bei identischen Kosten - echte
Ueberanpassung, kein Solverartefakt. Steht als Warnkasten im Abschnitt.
Die Laufzeitmessung ist aus der Ausgabe entfernt: Eine Wanduhrzeit ist nie
byteidentisch reproduzierbar (2,2 s / 2,3 s zwischen zwei Laeufen) und haette
Regel 4 dauerhaft gebrochen. Danach drei Laeufe zeichengleich, und die
abgedruckte Ausgabe stimmt mit dem Lauf des extrahierten Programms ueberein.
Mitgezogen: Kapitelkopf, Lernziele, Uebersichtstabelle (drei -> vier
Ansaetze), Selbsttest, Zusammenfassung, Vorwort-Programmverzeichnis, eine
neue Uebungsaufgabe und ihre Loesung in Anhang A.
pyproject.toml korrigiert: cvxpy lag in [finance], wird aber von 13
Programmen gebraucht, darunter dreien in diesem Kapitel - jetzt in der
Grundausstattung. Es zieht kein highspy nach, der Solverkonflikt bleibt auf
[large-scale] beschraenkt.
Stand: 293 Abschnitte, 712 Querverweise, 327 Indexmarken, 74 Programme (0
Fehler), 33 pytest-Tests, PDF 725 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:57:19 +02:00
Phase 6.2: Constraint Attribution und Managementbericht
Neuer Abschnitt im Kapitel Praxisfallen plus Constraint_Attribution.py (75.
Programm). Setzt den fehlenden Teil von Paket 3 aus Verbesserungen_02.md um.
Die Luecke, die es schliesst: Erklaerbarkeit.py beantwortet "warum DIESE
Zuweisung?", Konfliktsuche.py "warum geht es GAR NICHT?". Der haeufigste Fall
der Praxis lag dazwischen und hatte kein Werkzeug - das Modell rechnet, der
Plan ist zulaessig, und trotzdem enttaeuschend, nur weiss niemand woran es
liegt.
Gerechnet wird auf derselben Fabrik wie die Konfliktsuche, nach der
Reparatur: Dort war die Lackierkapazitaet von 150 Stunden der Kern des
Widerspruchs (2*40 + 3*30 = 170 > 150), eine zweite Schicht bringt sie auf
210. Damit laesst sich dieselbe Instanz ueber zwei Kapitel verfolgen - erst
"warum nicht?", dann "was kostet was?".
Drei gemessene Befunde:
* Bindend heisst nicht teuer: Fuenf Bedingungen binden, "Lackierbudget
Schicht 2" mit einem Schattenpreis von 0,00 EUR.
* Der Schattenpreis ist eine Momentaufnahme: Fuer die Lackiererei verspricht
die Hochrechnung 4,00 * 30 = 120 EUR, gemessen kommen 60 EUR; der Preis
gilt exakt bis +15 Stunden. Die Ranglisten drehen sich dabei um - nach
Schattenpreis fuehrt der Traegervertrag (18 EUR/Stueck), nach
tatsaechlicher Wirkung die Montage-Sonderschicht (450 EUR).
* Ein Wunsch kann unmoeglich statt teuer sein: "mindestens 55 Rahmen" ist
INFEASIBLE, blockiert von drei Bedingungen gemeinsam. Mit +15 Stunden
Lackierkapazitaet wird er erfuellbar und kostet dann nichts - der Plan
waehlt diese Menge von sich aus. Der Konflikt bestand nie zwischen
Vertrieb und Produktion.
Der Managementbericht entsteht aus genau diesen Zahlen; kein Satz enthaelt
eine Zahl, die nicht vorher gerechnet wurde. Voraussetzung ist der
sprechende Name jeder Bedingung - aus A_ub[7] wird kein Satz.
Dazu die Grenze, als Warnkasten und als Uebungsaufgabe: Der Bericht erklaert
das Modell, nicht die Wirklichkeit. Steigt der Deckungsbeitrag von Deckel
von 9 auf 20 EUR, kippt der Plan (Rahmen auf das Vertragsminimum 40, Deckel
auf die Marktgrenze 120), "Kapazitaet Lackieren" hoert auf zu binden,
"Liefervertrag Rahmen" faengt damit an - und der Satz "Teuerste Bindung ist
Liefervertrag Traeger" steht woertlich unveraendert im Bericht, waehrend die
Zahl dahinter von 18 auf 58 EUR springt.
Die Zaehlprobe aus 6.1a hat sich sofort bewaehrt: Nach dem Einbau der
Uebungsaufgabe meldete --check "6 Loesungen, das Kapitel aber 7 Aufgaben",
bevor die Loesung geschrieben war.
Mitgezogen: Kapitelkopf, Lernziele, Selbsttest, Zusammenfassung,
Vorwort-Programmverzeichnis, Vorwaertsverweis aus Falle 2, Uebungsaufgabe
und Loesung in Anhang A.
Stand: 294 Abschnitte, 725 Querverweise, 328 Indexmarken, 75 Programme,
139 Aufgaben mit 139 Loesungen, 33 pytest-Tests, PDF 737 Seiten, 68
netzfreie Programme fehlerfrei.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 10:30:33 +02:00
### ✅ 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.
Phase 6.3: Parallelitaet und Determinismus - gemessen statt behauptet
Neuer Abschnitt im CP-SAT-Kapitel 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 "fuer eine reproduzierbare
Ausgabe", nachgeprueft hatte es nie jemand. Gerechnet wird auf demselben
Job-Shop wie das Intervallvariablen-Kapitel, nur gross genug, dass die Suche
arbeitet: 12 Auftraege, 10 Maschinen, 120 Arbeitsgaenge.
Zwei Befunde, beide staerker als die Behauptung:
* Die Beschleunigung ist ueberlinear. Acht Arbeiter waren im abgedruckten
Lauf nicht achtmal, sondern 12,3-mal schneller als einer. Kein Messfehler:
CP-SAT vervielfacht nicht dieselbe Suche, sondern laesst verschiedene
Strategien nebeneinander laufen, die einander ihre Schranken mitteilen.
* Der Seed genuegt nicht - und zwar schon ab ZWEI Arbeitern. Ein Arbeiter:
1 Plan aus 4 Laeufen. Zwei Arbeiter: 3 verschiedene Plaene aus 4 Laeufen,
bei identischem random_seed und identischem Zielwert 183.
Die vollstaendige Antwort kam erst ueber die Uebungsaufgabe: Mit
num_workers = 1, aber fuenf verschiedenen Seeds ergeben sich ebenfalls fuenf
verschiedene Plaene. Keiner der beiden Parameter sichert die
Reproduzierbarkeit allein - erst die Kombination traegt.
Weiter fuer die Aufgabe gemessen: Der Gewinn kehrt sich um (auf 24 Kernen
Faktor 10,2 bei 8 Arbeitern, 12,3 bei 16, 9,0 bei 24) - "so viele Arbeiter
wie Kerne" ist damit widerlegt. Und bei 15 Auftraegen laeuft ein Arbeiter
ins 60-s-Limit (FEASIBLE, Makespan 200), waehrend acht OPTIMAL mit
demselben Makespan 200 nach 26,4 s melden: Der Unterschied liegt nicht in
der Loesung, sondern im Beweis, dass es keine bessere gibt.
Die abgedruckte Ausgabe traegt die Kennzeichnung "Laufzeiten und die Zahl
der verschiedenen Plaene sind hardwareabhaengig" - nach dem Muster, das das
Testing-Kapitel fuer 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 Plaene bei 4 Arbeitern); Zielwert und
Struktur sind identisch. Hier ist die Nichtreproduzierbarkeit der
abgedruckten Zahl die Aussage selbst.
Mitgezogen: Kapitelkopf, Lernziele, Selbsttest, Zusammenfassung,
Vorwort-Programmverzeichnis, Uebungsaufgabe und Loesung in Anhang A, ein
Verweis aus dem bestehenden Callout zu mehrdeutigen Optima und einer aus
Warmstart_Effekt.py im MILP-Kapitel (dort nach Regel 12 der Kapitelname).
Stand: 295 Abschnitte, 730 Querverweise, 328 Indexmarken, 76 Programme,
140 Aufgaben mit 140 Loesungen, 33 pytest-Tests, PDF 744 Seiten, 69
netzfreie Programme fehlerfrei.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:19:53 +02:00
### ✅ 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.
Phase 7.1: Vier Zielgruppen-One-Pager im Vorwort
Setzt Paket 4 aus Verbesserungen_03.md um. Das Titelblatt wirbt seit dem
Umbau mit Entscheidern, Ingenieuren und Investoren - die Lernpfade waren
aber nach Themen geschnitten, nicht nach Rollen. Der neue Abschnitt "Fuer
wen dieses Buch geschrieben ist" steht zwischen "Wie dieses Buch aufgebaut
ist" und den Lernpfaden: 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 traegt: nur gemessene Zahlen. Jeder
Beleg ist ein Ergebnis, das im Buch gerechnet wird - der 149-%-Aufschlag des
Erwartungswert-Plans, der Schattenpreis, der nur bis +15 Stunden gilt, VaR
1,86 % gegen CVaR 2,99 % bei einem schlechtesten Tag von -23,0 %, das Modell
mit dem schlechteren MSE und der besseren Entscheidung.
Und eine Erwartung wird ausdruecklich enttaeuscht: Verbesserungen_03
verlangt fuer die Managementseite "typischer ROI und Amortisationszeit". Das
Buch hat diese Zahlen nicht, und jede hier genannte waere erfunden. Der
One-Pager sagt das offen und nennt stattdessen die beiden Werkzeuge, mit
denen der Leser seine eigene Zahl ausrechnet: den EVPI als Obergrenze fuer
jedes Prognoseprojekt und die Kostenzurechnung aus Constraint Attribution.
Kein neues Programm, keine neue Aufgabe - der Abschnitt buendelt
Vorhandenes. Querverweise 730 -> 755, PDF 744 -> 746 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:29:00 +02:00
### ✅ 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.
Phase 7.2: Wasserfalldiagramm - wofuer der Erwartungswert-Plan bezahlt
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 ueberhaupt.
Der Anlass: Die Tabelle nannte nur die Summen - 874.870 EUR gegen
351.356 EUR, 149 % mehr. Wofuer genau, stand nirgends.
Der Generator rechnet die Instanz erneut, wie es die Konvention verlangt.
Die Probe fiel gut aus: Der unabhaengige 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.
Was die Zerlegung zeigt und die Tabelle nicht: Die Anfahrkosten sind in
beiden Plaenen gleich (58.000 EUR). Der Erwartungswert-Plan spart also nicht
dort, wo man es vermuten wuerde. Er spart 12.725 EUR Brennstoff und bezahlt
dafuer 536.239 EUR Lastabwurf - das 42-fache. "149 % mehr" ist eine Zahl,
ueber die man streiten kann; "wir sparen 12.725 EUR und riskieren
536.239 EUR" ist eine, ueber die man entscheidet.
Die Trennung von Brennstoff- und Abwurfkosten ist der einzige Zusatz
gegenueber bewerte() im Buchprogramm; dort wird sie nicht gebraucht.
Ein eigener Fehler unterwegs: Ein .replace(",", ".") fuer 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.
SVG byteidentisch ueber zwei Laeufe. Diagramme jetzt 33, davon 19 mit
Generator (16 Skripte). PDF unveraendert 746 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:41:07 +02:00
### ✅ 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).
Phase 7.3: Glossar "Deutsch - OR" - und die Gegenrichtung
Setzt Paket 7 aus Verbesserungen_03.md um. Neuer Abschnitt im Kapitel "Vom
Management-Wunsch zum Modell", unmittelbar hinter dem Gespraechsleitfaden:
Die zwoelf Fragen liefern Saetze - hier werden sie uebersetzt.
Die Abgrenzung zu Anhang B war die eigentliche Entwurfsfrage. Der
Musterkatalog hat laengst eine Spalte "Umgangssprachlich", laeuft aber 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 Eintraege, jeweils mit B-Nummer), und Anhang B bekam einen
Rueckverweis. Kein Duplikat, sondern die fehlende Gegenrichtung.
Der Teil, der den Abschnitt traegt, ist aber nicht die Tabelle, sondern was
danach kommt: fuenf Wendungen, bei denen die Uebersetzung eine Entscheidung
erzwingt - "moeglichst", "nicht mehr als drei pro Woche", "im Durchschnitt",
"so schnell wie moeglich", "fair". Bei jeder gibt es zwei bis drei
vertretbare Modelle, und die Wahl gehoert dem Betrieb, nicht dem
Modellierer. Das ist die These des Kapitels, auf die Alltagssprache
angewandt.
Dazu die Gegenrichtung OR -> Deutsch: acht Saetze fuer die Erklaerung eines
Ergebnisses, einschliesslich der Formulierung fuer INFEASIBLE ("Ihre Regeln
widersprechen sich", nicht "der Computer schafft es nicht").
Die neue Uebungsaufgabe laesst drei Zielfunktionen fuer "gleichmaessig
ausgelastet" aufstellen; die Loesung nennt je einen Fall, in dem die Wahl
den Plan sichtbar aendert - und weist darauf hin, dass zwei der drei einen
Halbtagsmonteur bestrafen.
Mitgezogen: Lernziel, Zusammenfassung, Rueckverweis aus Anhang B, Pfad E im
Vorwort (er fuehrte bisher direkt zum Musterkatalog, jetzt ueber das
Lexikon).
Damit ist Phase 7 abgeschlossen. 296 Abschnitte, 766 Querverweise, 141
Aufgaben mit 141 Loesungen, PDF 749 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:45:42 +02:00
### ✅ 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.
Phase 8.1: Synthese-Seiten je Teil - und die gebrochene Lesekette
Setzt Paket 5 aus Verbesserungen_02.md um (den Teil, der nicht
zurueckgestellt wurde). Fuenf neue Dateien 19_/29_/39_/49_/52_Synthese_*.md,
je eine am Ende eines Teils, mit eigener Website-Seite ueber SONDERSEITEN -
sie tragen bewusst keine "# Kapitel:"-Ueberschrift, weil sie keine Kapitel
sind, sondern der Rueckblick auf einen Teil.
Der Entwurf musste sich abgrenzen: Die Teil-Einleitungen haben bereits
Entscheidungsdiagramme. Eine zweite Matrix am Teil-Ende waere eine Dopplung
gewesen. Die Synthesen leisten deshalb, was eine Einleitung nicht kann - den
Vergleich ueber die Kapitel hinweg (Verfahren nebeneinander, mit der Spalte
"wo es aufhoert"), eine Tabelle "was dieser Teil gemessen hat" (Behauptung
gegen Messung gegen Fundstelle) und drei Fehler, die der Teil verhindert.
Zitiert wird ausschliesslich, was im Buch tatsaechlich gerechnet wird.
Drei Funde beim Einbau:
* Teil III sagte "die drei Kapitel dieses Teils", hat aber fuenf. Phase 3
hatte Mehrziel und Predict-then-Optimize hinzugefuegt, die Einleitung
blieb stehen.
* 50_Praxis.md verwies auf die Projektwerkstatt mit "acht eigene
Anwendungen" - sie hat elf.
* Und der eigentliche Fund: Die Lesekette der Quelldateien fuehrte an ACHT
Kapiteln vorbei. 12_Python_Oekosystem zeigte direkt auf
20_Lineare_Programmierung, 23_Graphen direkt auf 30_QP, 32_Dynamische
direkt auf 40_Finanzdaten, 50_Praxis direkt auf die Projektwerkstatt. Wer
der Kette folgte, uebersprang acht von 23 Kapiteln - darunter
Metaheuristiken, Spaltengenerierung, Strukturbruecke, Supply-Chain und das
ganze Testing-Kapitel. Zehn weitere Dateien hatten gar keine
Navigationszeile.
Zur Reichweite, damit sie nicht ueberschaetzt wird: Diese Zeilen stehen nur
in den Quelldateien. entferne_navigation() streicht sie aus dem
Gesamtdokument, und die Website baut ihre Vor/Zurueck-Knoepfe selbst aus
DATEIEN. PDF und Website waren nie betroffen - wohl aber jeder, der die
Markdown-Dateien im Repository liest, und das wird nach der
Veroeffentlichung der Normalfall sein.
Die Kette ist jetzt ueber alle 35 Uebergaenge 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: 36 Dateien, 296 Abschnitte, 815 Querverweise, 328 Indexmarken, 76
Programme (unveraendert), 33 pytest-Tests, PDF 758 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 12:10:02 +02:00
### ✅ 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.
Phase 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.
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 fuer die Instanzgroesse. Aus jeder Variante ist jetzt
eine gewoehnliche Funktion mit lokalem Import geworden.
Solverwechsel_CPSAT_HiGHS.py rief sich selbst ueber sys.argv erneut auf;
auch das entfaellt.
Ausgefuehrt wird ueber einen ProcessPoolExecutor mit zwei Einstellungen, die
beide noetig sind: mp_context "spawn" (frischer Interpreter statt geerbtem
Speicher - unter Linux ist fork der Standard) und 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 zurueck). Nachgemessen:
vier Aufgaben, vier verschiedene PIDs.
Der zweite Punkt hat einen eigenen Warnkasten bekommen, weil der Fehler
leicht zu machen und schwer zu finden ist: Der Absturz kaeme nicht beim
ersten Solver, sondern beim zweiten - und saehe aus wie ein Problem des
zweiten.
Regel 4, dreifach geprueft. Ein_System_Vier_Ansaetze.py: identisch bis auf
die Zeitspalte, einschliesslich der Spannweite 2,41e-08, auf die sich der
Merksatz des Kapitels beruft. Benchmark_Skalierung.py: alle zwoelf
Zielwerte und alle drei Spannweiten bitgleich; Zeiten und Speicher haben
sich verschoben, beide sind im Abdruck seit jeher als hardwareabhaengig
gekennzeichnet. Solverwechsel_CPSAT_HiGHS.py: Ausgabe ohne Zeiten
unveraendert.
Bewusst subprocess bleibt Mutationstest.py: Dort wird pytest auf einer
mutierten Kopie in einem temporaeren Verzeichnis gestartet - ein externes
Werkzeug auf veraenderten Dateien, nicht die Isolation eines Imports.
Neu im Kapitel Oekosystem: ein Abschnitt "Wie die Isolation aussieht, wenn
sie tragen soll" - warum ein Codestring die schlechteste Umsetzung von
"eigener Prozess" ist. Anhang C nennt jetzt ebenfalls ProcessPoolExecutor.
Ein eigener Fehler, gefunden und abgesichert: Ich hatte dem neuen ### ein
{#sec:...}-Label gegeben. ABSCHNITT_RE erkennt nur "## " - das Label waere
nie registriert worden und jeder Verweis darauf ins Leere gelaufen, ohne
Warnung. Label entfernt, --check meldet den Fall jetzt. Gegengetestet.
Stand: 818 Querverweise, 76 Programme, 33 pytest-Tests, PDF 760 Seiten, 69
netzfreie Programme fehlerfrei.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 12:39:34 +02:00
### ✅ 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 (Uebung-Verweise) und Phase 8.3: Dockerfile
Zwei Dinge in einem Commit, weil beide den Plan abschliessen.
ACHTER FUND: sieben Saetze der Bauart "Uebung 8.5 laesst Sie diese Balance
untersuchen" - und sechs davon standen in alter Zaehlung. "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
Pruefungen kannte.
Aufgaben haben kein eigenes Label, ein {ref:} auf eine einzelne Aufgabe ist
also nicht moeglich. Verwiesen wird stattdessen auf Abschnitt plus
Aufgabentitel - und der ist stabil. Jedes Ziel wurde einzeln ueber die alte
Zaehlung bestimmt und am Zusammenhang geprueft.
Ein Fall war knifflig: "Uebung 6.7 (Wochendienstplan)" meinte die siebte
CP-SAT-Aufgabe der alten Zaehlung, also "Eigener Dienstplan" - die heute an
achter Stelle steht, weil in Phase 6.3 eine Aufgabe davor eingefuegt wurde.
Wer nur die Kapitelnummer angepasst haette, waere bei der falschen Aufgabe
gelandet.
--check kennt jetzt auch "Uebung"/"Übung". Gegengetestet.
PHASE 8.3: Dockerfile, zweistufig. Die erste Stufe uebersetzt die
Abhaengigkeiten in eine virtuelle Umgebung und braucht dafuer einen
Compiler, die zweite kopiert nur /opt/venv. Installiert werden die Gruppen
finance, large-scale, api und dev aus pyproject.toml; figures fehlt bewusst,
weil es zusaetzlich Graphviz verlangt.
Es wurde nicht behauptet, sondern gebaut. Ergebnis: 1,31 GB, und darin der
Installationstest mit allen drei Solver-Funktionstests bestanden, die 33
pytest-Tests bestanden und alle 69 netzfreien Programme fehlerfrei -
einschliesslich der drei aus 8.2, deren spawn-Isolation im Container ebenso
traegt wie ausserhalb.
Zwei Dinge, die der Bau gelehrt hat: libgomp1 fehlt im python:3.12-slim-Image
und wird von OR-Tools und HiGHS zur Laufzeit gebraucht (sonst
"libgomp.so.1: cannot open shared object file"). Und ein eigener Fehler:
USER kurs stand vor dem mkdir /buch/output, /buch gehoert root, der Bau
brach in der letzten Zeile ab. Beides steht jetzt als Kommentar im
Dockerfile.
Das Image fuehrt die Programme aus und baut das Buch nicht. Ein
.dockerignore haelt Website, PDF und Notebooks aus dem Build-Kontext. Und es
enthaelt ortools UND highspy, obwohl sie sich nicht gemeinsam importieren
lassen - der Konflikt wird zur Laufzeit durch getrennte Prozesse geloest,
nicht durch Weglassen.
Damit ist Phase 8 abgeschlossen und der Plan abgearbeitet.
Stand: 825 Querverweise, 76 Programme, 33 pytest-Tests, PDF 760 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 13:18:05 +02:00
### ✅ 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.**
Aufgeraeumt: 33 PNG-Zweitfassungen, Bau-Ueberbleibsel, Ausgabepfade
Drei Aufraeumarbeiten - und zwei Funde, die dabei auffielen.
1. DIE 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 Veroeffentlichung auf den Webserver gingen. Die 15 Generatoren
schreiben jetzt nur noch SVG, die Docstrings sind mitgezogen. Vor dem
Loeschen geprueft: Jedes PNG hatte sein gleichnamiges SVG, alle 33 waren
versioniert.
FUND 1: erzeuge_kap06_gantt.py folgte als einziger Generator nicht der
Konvention - weder svg.hashsalt noch metadata={"Date": None}. Sein SVG trug
einen echten Zeitstempel und bei jedem Lauf andere clip-path-IDs, war also
nie byteidentisch reproduzierbar, obwohl CLAUDE.md genau das fuer alle
Generatoren festhaelt. Aufgefallen nur, weil nach der PNG-Umstellung 32 von
33 SVGs bitgleich blieben und eines nicht. Jetzt byteidentisch ueber zwei
Laeufe.
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. Die Verzeichnisliste wird jetzt vorher
gefiltert. Gegengetestet.
2. BAU-UEBERBLEIBSEL entfernt (alle ignoriert und neu erzeugbar):
svg-inkscape/, build_v04.log, .pytest_cache/, Programme/output/, vier
__pycache__/ und die Excel-Mappen. Arbeitsbaum 36 -> 33 MB, danach null
ignorierte Ueberbleibsel.
3. 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 ueber os.path.dirname(os.path.abspath(__file__)). Nachgemessen:
Lauf aus der Wurzel legt dort null Dateien ab.
Ausserdem git gc: 653 lose Objekte gepackt, .git von 71 MB auf 28 MB -
reines Repacken, kein Inhalt beruehrt.
Geprueft: alle 33 im Buch referenzierten SVGs vorhanden, in Quelle und
Website; 16 Generatoren fehlerfrei; keine fehlenden Bilder im LaTeX-Lauf;
33 pytest-Tests; PDF unveraendert 760 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:22:26 +02:00
### ✅ 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-Uebersichtsseite - das fehlende Gegenstueck zu programme.html
Der 403 auf /OR/Notebooks_04/ hatte eine andere Ursache als vermutet: 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 Luecke sichtbar gemacht: baue_programme_seite() erzeugt seit
jeher programme.html, eine gestaltete Uebersicht der 76 Programme. Fuer die
25 Notebooks gab es kein Gegenstueck - sie wurden ausgeliefert, aber keine
Seite listete sie.
Neu ist baue_notebooks_seite(), 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 Anhaengen; der
einleitende Absatz nennt die drei Wege, ein Notebook auszufuehren.
Dazu ein ZIP mit allen 25 Notebooks (208 KB), byteidentisch ueber zwei
Laeufe. Ein ZIP speichert je Eintrag die Aenderungszeit; ohne festen Wert
entstuende bei jedem Bau eine andere Datei, und da OR_HTML_04/ versioniert
ist, wuechse das Repository bei jedem Lauf. Die Eintraege werden deshalb
sortiert und mit ZipInfo(date_time=(1980,1,1,0,0,0)) geschrieben - dieselbe
Sorgfalt wie svg.hashsalt bei den Diagrammen.
Gegen kuenftige 403 schreiben spiegle_notebooks() und baue_programme_seite()
je eine dreizeilige index.html mit meta refresh in ihr Downloadverzeichnis.
Das behebt zugleich denselben latenten Fall bei programme/.
Ein eigener Fehler, zum zweiten Mal derselbe: Das .replace(",", ".") fuer
deutsche Tausenderpunkte hing am Ende eines mehrzeiligen f-Strings - und in
Python bindet die Methode an die GESAMTE zusammengesetzte Zeichenkette. Aus
"Drei Wege, sie auszufuehren" wurde "Drei Wege. sie auszufuehren". Exakt der
Fehler, vor dem ich in erzeuge_wirkung.py selbst einen Kommentar hinterlassen
hatte. Die Zahl wird jetzt getrennt formatiert.
Geprueft: 25 gelistete Notebooks, 0 tote Links auf der Seite, ZIP mit 25
Eintraegen und identischen Zeitstempeln, Seitenleisteneintrag auf allen 40
Seiten mit Seitenleiste, --check fehlerfrei, PDF unveraendert 759 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:20:06 +02:00
### ✅ 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 trennen, Menue um Glossar und ZIP-Download ergaenzen
94_Anhang_Glossar_und_Literatur.md enthielt zwei verschiedene Nachschlagewerke
in einer Datei. Jetzt sind es zwei Anhaenge: E = Glossar (92 Eintraege),
F = Literaturverzeichnis (6 Kategorien). Aus 5 Anhaengen werden 6, aus 36
Kapiteldateien 37.
Die Teilung war billig: Die Datei trug bereits zwei eigenstaendige
Ueberschriften mit nichts als einem --- dazwischen, und im ganzen Buch gab es
genau einen {ref:anhang:glossar-literatur} - die Weiter-mit-Zeile im
Spickzettel. Geprueft: keine Inhaltszeile verloren, alle 92 Glossareintraege
auf der neuen Seite, Indexmarken unveraendert bei 328.
Das Seitenleisten-Menue bekommt zwei Eintraege:
Beispielprogramme
Notebooks
Download Notebooks als ZIP <- neu, eingerueckt
Glossar <- neu
Stichwortverzeichnis
Gesamtdokument (eine Seite)
Download als PDF
Der ZIP-Link benutzt dieselbe Konstante wie baue_notebooks_seite() beim
Schreiben des Archivs, damit Link und Datei nicht auseinanderlaufen. Das
Glossar erscheint absichtlich doppelt: hier als Abkuerzung neben dem
Stichwortverzeichnis, und unten in der Gruppe "Anhaenge", weil es ein Anhang
ist.
Die verwaiste anhang-glossar-literatur.html von Hand entfernt - der Build
raeumt alte Seiten nicht ab.
Nebenbefund, in PROGRESS.md festgehalten: OR_HTML_04/assets/site.css ist eine
von Hand gepflegte Quelle, die im als "generiert" beschriebenen Verzeichnis
liegt. Der Build schreibt sie nie. Dasselbe gilt fuer site.js, icons.svg,
highlight.css und plotly.min.js.
--check: 5 Teile, 23 Kapitel, 6 Anhaenge, 296 Abschnitte, 825 Querverweise,
328 Indexmarken, 37 Dateien, 28.544 Zeilen, 305 Hauptueberschriften, keine
Warnung. PDF unveraendert 759 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:48:57 +02:00
### ✅ 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 Seite)
2026-09-08 16:54:24 +02:00
Download Buch als PDF
Glossar und Literatur trennen, Menue um Glossar und ZIP-Download ergaenzen
94_Anhang_Glossar_und_Literatur.md enthielt zwei verschiedene Nachschlagewerke
in einer Datei. Jetzt sind es zwei Anhaenge: E = Glossar (92 Eintraege),
F = Literaturverzeichnis (6 Kategorien). Aus 5 Anhaengen werden 6, aus 36
Kapiteldateien 37.
Die Teilung war billig: Die Datei trug bereits zwei eigenstaendige
Ueberschriften mit nichts als einem --- dazwischen, und im ganzen Buch gab es
genau einen {ref:anhang:glossar-literatur} - die Weiter-mit-Zeile im
Spickzettel. Geprueft: keine Inhaltszeile verloren, alle 92 Glossareintraege
auf der neuen Seite, Indexmarken unveraendert bei 328.
Das Seitenleisten-Menue bekommt zwei Eintraege:
Beispielprogramme
Notebooks
Download Notebooks als ZIP <- neu, eingerueckt
Glossar <- neu
Stichwortverzeichnis
Gesamtdokument (eine Seite)
Download als PDF
Der ZIP-Link benutzt dieselbe Konstante wie baue_notebooks_seite() beim
Schreiben des Archivs, damit Link und Datei nicht auseinanderlaufen. Das
Glossar erscheint absichtlich doppelt: hier als Abkuerzung neben dem
Stichwortverzeichnis, und unten in der Gruppe "Anhaenge", weil es ein Anhang
ist.
Die verwaiste anhang-glossar-literatur.html von Hand entfernt - der Build
raeumt alte Seiten nicht ab.
Nebenbefund, in PROGRESS.md festgehalten: OR_HTML_04/assets/site.css ist eine
von Hand gepflegte Quelle, die im als "generiert" beschriebenen Verzeichnis
liegt. Der Build schreibt sie nie. Dasselbe gilt fuer site.js, icons.svg,
highlight.css und plotly.min.js.
--check: 5 Teile, 23 Kapitel, 6 Anhaenge, 296 Abschnitte, 825 Querverweise,
328 Indexmarken, 37 Dateien, 28.544 Zeilen, 305 Hauptueberschriften, keine
Warnung. PDF unveraendert 759 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:48:57 +02:00
```
Der ZIP-Link benutzt dieselbe Konstante wie `baue_notebooks_seite()` beim Schreiben des
Archivs — so können Link und Datei nicht auseinanderlaufen. 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
Statische Website-Assets in die Quelle web_04/ holen
site.css, site.js, icons.svg, plotly.min.js und die 22 KaTeX-Dateien lagen in
OR_HTML_04/, 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 Einruecken des ZIP-Menuepunkts passiert. Und wer OR_HTML_04/ geloescht
und neu gebaut haette, haette 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() fuellt die Quelle statt des Ziels.
_lade_icon_sprite_inline() und die beiden KaTeX-Pruefungen lesen die Quelle,
haengen also nicht mehr vom eigenen Ergebnis ab.
Zwei Waechter, weil genau diese Verwechslung schon vorgekommen ist:
* Wurde die Kopie in OR_HTML_04/ von Hand geaendert (Inhalt weicht ab UND
Zeitstempel ist neuer), bricht der Bau ab und nennt den mv-Befehl, der es
richtigstellt - kein stilles Ueberschreiben.
* pruefe_assets() liest die href=/src=-Literale aus dem Quelltext des
Bauskripts und verlangt fuer jedes einen Erzeuger: entweder web_04/ oder die
Liste ERZEUGTE_ASSETS (highlight.css, search-index.js, programme.js).
Probe: rm -rf OR_HTML_04 && --html baut alle 213 Dateien wieder auf,
Dateiliste identisch zur Sicherung.
Fund dabei: Das Stichwortverzeichnis war nicht byte-reproduzierbar
ziel_links() sortierte nach (seite, kontext) - und kontext ist der
Kapiteltitel, fuer alle Marken einer Datei also derselbe. Bei zwei Fundstellen
im selben Kapitel war der Schluessel gleich, und die Reihenfolge fiel auf die
eines set() zurueck, also auf den je Prozess zufaelligen PYTHONHASHSEED. Zwei
Laeufe erzeugten unterschiedliche Bytes ohne Quellaenderung.
Derselbe Fehler war auch sichtbar: vier {idx:Branch-and-Bound} in Kapitel 6
ergaben vier optisch identische Links nebeneinander; zehn Registereintraege
waren betroffen. Behoben durch einen Link je Kapitel (erste Fundstelle in
Dokumentreihenfolge, dict statt set) - das macht die Sortierung zugleich
eindeutig.
Gegenprobe: drei Laeufe mit PYTHONHASHSEED=random liefern dieselbe Pruefsumme.
219 Fachbegriffe und 328 Indexmarken unveraendert.
Nachtrag zum vorigen Commit: highlight.css gehoert NICHT zu den Handdateien -
erzeuge_highlight_css() erzeugt sie aus pandoc --print-highlight-style. Die
Notiz in PROGRESS.md ist korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 16:24:56 +02:00
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
Glossar und Literatur trennen, Menue um Glossar und ZIP-Download ergaenzen
94_Anhang_Glossar_und_Literatur.md enthielt zwei verschiedene Nachschlagewerke
in einer Datei. Jetzt sind es zwei Anhaenge: E = Glossar (92 Eintraege),
F = Literaturverzeichnis (6 Kategorien). Aus 5 Anhaengen werden 6, aus 36
Kapiteldateien 37.
Die Teilung war billig: Die Datei trug bereits zwei eigenstaendige
Ueberschriften mit nichts als einem --- dazwischen, und im ganzen Buch gab es
genau einen {ref:anhang:glossar-literatur} - die Weiter-mit-Zeile im
Spickzettel. Geprueft: keine Inhaltszeile verloren, alle 92 Glossareintraege
auf der neuen Seite, Indexmarken unveraendert bei 328.
Das Seitenleisten-Menue bekommt zwei Eintraege:
Beispielprogramme
Notebooks
Download Notebooks als ZIP <- neu, eingerueckt
Glossar <- neu
Stichwortverzeichnis
Gesamtdokument (eine Seite)
Download als PDF
Der ZIP-Link benutzt dieselbe Konstante wie baue_notebooks_seite() beim
Schreiben des Archivs, damit Link und Datei nicht auseinanderlaufen. Das
Glossar erscheint absichtlich doppelt: hier als Abkuerzung neben dem
Stichwortverzeichnis, und unten in der Gruppe "Anhaenge", weil es ein Anhang
ist.
Die verwaiste anhang-glossar-literatur.html von Hand entfernt - der Build
raeumt alte Seiten nicht ab.
Nebenbefund, in PROGRESS.md festgehalten: OR_HTML_04/assets/site.css ist eine
von Hand gepflegte Quelle, die im als "generiert" beschriebenen Verzeichnis
liegt. Der Build schreibt sie nie. Dasselbe gilt fuer site.js, icons.svg,
highlight.css und plotly.min.js.
--check: 5 Teile, 23 Kapitel, 6 Anhaenge, 296 Abschnitte, 825 Querverweise,
328 Indexmarken, 37 Dateien, 28.544 Zeilen, 305 Hauptueberschriften, keine
Warnung. PDF unveraendert 759 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:48:57 +02:00
gepflegt bzw. mitgeliefert — wer `OR_HTML_04/` löscht und neu baut, verliert sie.
Statische Website-Assets in die Quelle web_04/ holen
site.css, site.js, icons.svg, plotly.min.js und die 22 KaTeX-Dateien lagen in
OR_HTML_04/, 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 Einruecken des ZIP-Menuepunkts passiert. Und wer OR_HTML_04/ geloescht
und neu gebaut haette, haette 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() fuellt die Quelle statt des Ziels.
_lade_icon_sprite_inline() und die beiden KaTeX-Pruefungen lesen die Quelle,
haengen also nicht mehr vom eigenen Ergebnis ab.
Zwei Waechter, weil genau diese Verwechslung schon vorgekommen ist:
* Wurde die Kopie in OR_HTML_04/ von Hand geaendert (Inhalt weicht ab UND
Zeitstempel ist neuer), bricht der Bau ab und nennt den mv-Befehl, der es
richtigstellt - kein stilles Ueberschreiben.
* pruefe_assets() liest die href=/src=-Literale aus dem Quelltext des
Bauskripts und verlangt fuer jedes einen Erzeuger: entweder web_04/ oder die
Liste ERZEUGTE_ASSETS (highlight.css, search-index.js, programme.js).
Probe: rm -rf OR_HTML_04 && --html baut alle 213 Dateien wieder auf,
Dateiliste identisch zur Sicherung.
Fund dabei: Das Stichwortverzeichnis war nicht byte-reproduzierbar
ziel_links() sortierte nach (seite, kontext) - und kontext ist der
Kapiteltitel, fuer alle Marken einer Datei also derselbe. Bei zwei Fundstellen
im selben Kapitel war der Schluessel gleich, und die Reihenfolge fiel auf die
eines set() zurueck, also auf den je Prozess zufaelligen PYTHONHASHSEED. Zwei
Laeufe erzeugten unterschiedliche Bytes ohne Quellaenderung.
Derselbe Fehler war auch sichtbar: vier {idx:Branch-and-Bound} in Kapitel 6
ergaben vier optisch identische Links nebeneinander; zehn Registereintraege
waren betroffen. Behoben durch einen Link je Kapitel (erste Fundstelle in
Dokumentreihenfolge, dict statt set) - das macht die Sortierung zugleich
eindeutig.
Gegenprobe: drei Laeufe mit PYTHONHASHSEED=random liefern dieselbe Pruefsumme.
219 Fachbegriffe und 328 Indexmarken unveraendert.
Nachtrag zum vorigen Commit: highlight.css gehoert NICHT zu den Handdateien -
erzeuge_highlight_css() erzeugt sie aus pandoc --print-highlight-style. Die
Notiz in PROGRESS.md ist korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 16:24:56 +02:00
(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.)
Glossar und Literatur trennen, Menue um Glossar und ZIP-Download ergaenzen
94_Anhang_Glossar_und_Literatur.md enthielt zwei verschiedene Nachschlagewerke
in einer Datei. Jetzt sind es zwei Anhaenge: E = Glossar (92 Eintraege),
F = Literaturverzeichnis (6 Kategorien). Aus 5 Anhaengen werden 6, aus 36
Kapiteldateien 37.
Die Teilung war billig: Die Datei trug bereits zwei eigenstaendige
Ueberschriften mit nichts als einem --- dazwischen, und im ganzen Buch gab es
genau einen {ref:anhang:glossar-literatur} - die Weiter-mit-Zeile im
Spickzettel. Geprueft: keine Inhaltszeile verloren, alle 92 Glossareintraege
auf der neuen Seite, Indexmarken unveraendert bei 328.
Das Seitenleisten-Menue bekommt zwei Eintraege:
Beispielprogramme
Notebooks
Download Notebooks als ZIP <- neu, eingerueckt
Glossar <- neu
Stichwortverzeichnis
Gesamtdokument (eine Seite)
Download als PDF
Der ZIP-Link benutzt dieselbe Konstante wie baue_notebooks_seite() beim
Schreiben des Archivs, damit Link und Datei nicht auseinanderlaufen. Das
Glossar erscheint absichtlich doppelt: hier als Abkuerzung neben dem
Stichwortverzeichnis, und unten in der Gruppe "Anhaenge", weil es ein Anhang
ist.
Die verwaiste anhang-glossar-literatur.html von Hand entfernt - der Build
raeumt alte Seiten nicht ab.
Nebenbefund, in PROGRESS.md festgehalten: OR_HTML_04/assets/site.css ist eine
von Hand gepflegte Quelle, die im als "generiert" beschriebenen Verzeichnis
liegt. Der Build schreibt sie nie. Dasselbe gilt fuer site.js, icons.svg,
highlight.css und plotly.min.js.
--check: 5 Teile, 23 Kapitel, 6 Anhaenge, 296 Abschnitte, 825 Querverweise,
328 Indexmarken, 37 Dateien, 28.544 Zeilen, 305 Hauptueberschriften, keine
Warnung. PDF unveraendert 759 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:48:57 +02:00
Die alte `anhang-glossar-literatur.html` musste von Hand entfernt werden; der Build räumt
verwaiste Seiten nicht ab.
Statische Website-Assets in die Quelle web_04/ holen
site.css, site.js, icons.svg, plotly.min.js und die 22 KaTeX-Dateien lagen in
OR_HTML_04/, 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 Einruecken des ZIP-Menuepunkts passiert. Und wer OR_HTML_04/ geloescht
und neu gebaut haette, haette 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() fuellt die Quelle statt des Ziels.
_lade_icon_sprite_inline() und die beiden KaTeX-Pruefungen lesen die Quelle,
haengen also nicht mehr vom eigenen Ergebnis ab.
Zwei Waechter, weil genau diese Verwechslung schon vorgekommen ist:
* Wurde die Kopie in OR_HTML_04/ von Hand geaendert (Inhalt weicht ab UND
Zeitstempel ist neuer), bricht der Bau ab und nennt den mv-Befehl, der es
richtigstellt - kein stilles Ueberschreiben.
* pruefe_assets() liest die href=/src=-Literale aus dem Quelltext des
Bauskripts und verlangt fuer jedes einen Erzeuger: entweder web_04/ oder die
Liste ERZEUGTE_ASSETS (highlight.css, search-index.js, programme.js).
Probe: rm -rf OR_HTML_04 && --html baut alle 213 Dateien wieder auf,
Dateiliste identisch zur Sicherung.
Fund dabei: Das Stichwortverzeichnis war nicht byte-reproduzierbar
ziel_links() sortierte nach (seite, kontext) - und kontext ist der
Kapiteltitel, fuer alle Marken einer Datei also derselbe. Bei zwei Fundstellen
im selben Kapitel war der Schluessel gleich, und die Reihenfolge fiel auf die
eines set() zurueck, also auf den je Prozess zufaelligen PYTHONHASHSEED. Zwei
Laeufe erzeugten unterschiedliche Bytes ohne Quellaenderung.
Derselbe Fehler war auch sichtbar: vier {idx:Branch-and-Bound} in Kapitel 6
ergaben vier optisch identische Links nebeneinander; zehn Registereintraege
waren betroffen. Behoben durch einen Link je Kapitel (erste Fundstelle in
Dokumentreihenfolge, dict statt set) - das macht die Sortierung zugleich
eindeutig.
Gegenprobe: drei Laeufe mit PYTHONHASHSEED=random liefern dieselbe Pruefsumme.
219 Fachbegriffe und 328 Indexmarken unveraendert.
Nachtrag zum vorigen Commit: highlight.css gehoert NICHT zu den Handdateien -
erzeuge_highlight_css() erzeugt sie aus pandoc --print-highlight-style. Die
Notiz in PROGRESS.md ist korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 16:24:56 +02:00
### ✅ `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: Upload mit rsync --delete und Nachpruefung
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 ueberschreibt nur, es loescht nie - die alte Glossar-Seite bliebe
dauerhaft erreichbar. Das Zielverzeichnis vorher zu leeren waere die grobe
Antwort: Bricht die Uebertragung von 23 MB in 213 Dateien ab, ist die Website
tot. rsync --delete uebertraegt nur Geaendertes und entfernt genau das, was
verschwunden ist.
Der zweite Grund wiegt schwerer: Die Website hing wochenlang einen Bau
zurueck, ohne dass es auffiel. Vier Schritte:
1. Vorpruefung - ist eine Quelldatei neuer als die gebaute Website, Abbruch.
Ebenso: PDF der Website byte-genau wie das gebaute, Verzeichnis-
Weiterleitungen vorhanden (der Grund fuer den 403).
2. Probelauf - zaehlt Uebertragungen und Loeschungen. Mehr als 30 Loeschungen
brechen ab; das ist der Schutz gegen einen falschen Zielpfad, mit dem
--delete sonst ein fremdes Verzeichnis ausraeumt. --loeschgrenze hebt an.
3. Uebertragung (rsync -rltz --chmod=D755,F644 ueber ssh).
4. Nachpruefung - 14 Adressen per HTTP, Statuscode gegen Erwartung. Darunter
die Probe, dass die entfernte Seite wirklich 404 liefert.
Anmeldung per SSH-Schluessel, nicht per Passwort in der .env: rsync und scp
haben keine Passwort-Option, ein Passwort braeuchte sshpass und staende
waehrend der Uebertragung in der Prozessliste. Die .env (in .gitignore,
Vorlage .env.beispiel) enthaelt nur Host, Benutzer, Zielpfad und URL.
Beim ersten Test lief die Pruefung in eine irrefuehrende Meldung ("kein rsync
installiert"), waehrend in Wahrheit schon die Anmeldung fehlschlug. Die
Pruefung unterscheidet die Faelle jetzt am ssh-Rueckgabewert 255 und nennt
ssh-copy-id.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 16:28:22 +02:00
### ✅ `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` .
Website veroeffentlicht und die Diagnose des Skripts geschaerft
https://jamulix.de/OR/ steht auf dem Stand des heutigen Baus: 213 Dateien
uebertragen, 35 geloescht, alle vierzehn HTTP-Proben richtig - darunter die
entscheidende: /OR/anhang-glossar-literatur.html liefert jetzt 404 statt 200.
Der zweite Lauf unmittelbar danach uebertrug 0 Dateien und loeschte nichts.
Die Abbruchgrenze hat gleich beim ersten Einsatz gegriffen: 35 Loeschungen
ueber der Vorgabe von 30. Die Liste war vollstaendig erklaerbar - 33
verwaiste PNGs aus dem Aufraeumschritt, das leere bilder_04/__pycache__/ aus
dem spiegle_bilder()-Fehler und die geteilte Anhangseite. Dass ein
scp-Upload alle 35 stehen gelassen haette, ist damit belegt statt behauptet.
Zwei Huerden auf dem Weg, beide ins Skript uebersetzt:
ssh-copy-id braucht ein echtes Terminal. In der Claude-Code-Sitzung gibt es
keins, aber DISPLAY ist gesetzt - ssh sucht daraufhin ssh-askpass, das nicht
installiert ist. Der Schritt gehoert in ein normales Terminalfenster.
Danach sperrte fail2ban den Port: erste Verbindung erfolgreich, die folgenden
"Connection refused", waehrend die Website weiter mit 200 antwortete. Nach
sieben Minuten war die Sperre weg; weitere Versuche haetten sie verlaengert.
pruefe_ziel_erreichbar() unterscheidet jetzt die abgewiesene Verbindung von
der abgelehnten Anmeldung. Vorher meldete es in beiden Faellen "Der Schluessel
liegt noch nicht auf dem Server" - beim zweiten Mal glatt falsch, denn er lag
laengst dort.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 16:52:00 +02:00
### ✅ 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.
Dokumentation nachgezogen - und dabei den neunten Fund gemacht
Der Leser-Wegweiser Operations_Research_mit_Python_Version_04/README.md, die
Datei, die Leser als erstes oeffnen, beschrieb noch die Struktur der
Version 03:
* 12 der 15 Kapitelnummern falsch. "20_Lineare_Programmierung.md | 4" - es ist
Kapitel 5. Bei den Finanzkapiteln lag die Abweichung bei sieben: README 11,
tatsaechlich 18.
* 8 der 23 Kapitel fehlten ganz (Metaheuristiken, Spaltengenerierung,
Mehrziel, Predict-then-Optimize, Testing und drei weitere).
* 93_Anhang_Spickzettel.md fehlte in der Anhangtabelle; der Bauabschnitt nannte
dreimal build_version_03.py statt _04 und beschrieb --html als
"Single-Page-HTML", obwohl es laengst eine mehrseitige Website erzeugt.
Dieselbe Regel wie acht Mal zuvor, dieselbe Behandlung: Die Tabelle wird
erzeugt, nicht korrigiert. schreibe_kapiteltabelle() ersetzt den Block
zwischen den KAPITELTABELLE-Marken aus der Struktur, die
baue_seiten_registry() ohnehin ermittelt - derselben Quelle, aus der die
Nummern im Buch stammen. --check meldet einen veralteten Block und bricht ab.
Gegenprobe: alle 37 Dateien aus DATEIEN stehen in der Uebersicht.
Weiter nachgezogen:
* CLAUDE.md kannte veroeffentliche_04.py nicht und nannte .env.beispiel nicht.
Der Abschnitt "Veroeffentlichung" beschrieb noch das Kopieren von Hand samt
curl-Kontrolle - das erledigt jetzt das Skript.
* Fundzahl der Nummern-Regel: CLAUDE.md sagte sieben, README acht, PROGRESS
dokumentiert einen "Achter Fund". Jetzt ueberall neun.
* Diagrammzahl: CLAUDE.md zaehlt 32 Kapiteldiagramme, README 33 - der
Unterschied ist titelseite.svg. Das steht jetzt dabei.
Nebenbei am Upload-Skript: rsync bekommt --checksum. Der Bau schreibt alle 42
Seiten bei jedem Lauf neu, auch ohne inhaltliche Aenderung; nach Zeitstempel
und Groesse waeren das jedes Mal rund 19 MB sinnlose Uebertragung. Gemessen
nach einem Neubau ohne Aenderung: 0 statt 151 Dateien.
Website unveraendert - kein Upload noetig, der Probelauf meldet 0 zu
uebertragen und 0 zu loeschen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 19:13:34 +02:00
### 🐛 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 ** .
Phase 6.1: Chance Constraints - die Zusage "mit 95 % Sicherheit"
Neuer Abschnitt im Kapitel Unsicherheit 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 tatsaechlich fragt. Setzt Paket 4
aus Verbesserungen_02.md um.
Beide Wege an DERSELBEN Instanz (Kraftwerkspark, 500 MW gesicherte Zusage):
analytisch als Second-Order-Cone-Bedingung (CLARABEL) und szenariobasiert als
Big-M-MILP (SciPy/HiGHS) - beides in einem Prozess, ohne ortools- oder
highspy-Import.
Drei gemessene Befunde:
* Der Mittelwertplan haelt 50,08 %. Kein Fehler, sondern die Definition des
Erwartungswerts.
* Sicherheit ist konvex bepreist: 56.289 EUR je Prozentpunkt auf dem Weg zu
80 %, 253.848 EUR zwischen 95 und 99 % - das 4,5-fache. Das Programm
rechnet die Tabelle selbst aus, statt sie zu behaupten.
* Die Zusage gilt nur fuer die unterstellte Verteilung: Der 95-%-Plan haelt
gemessen 87,44 %, sobald die Testverteilung eine Kaeltewelle mit
Dunkelflaute enthaelt, in der alles zugleich einbricht - auch das
Gaskraftwerk. In einer reinen Normalwelt liefert derselbe Plan 94,97 %.
Zwei eigene Fehlgriffe, beide durch Messen aufgefallen und korrigiert: Die
erste Kostentarierung ergab eine entartete Loesung (alles ins Gaskraftwerk),
womit die Kovarianzmatrix wirkungslos war - und gerade sie begruendet die
Kegelform. Und der erste Kaelteeinbruch traf nur Wind und Sonne; der SOC-Plan
hatte die ohnehin herausgehalten und war zufaellig robust, das Argument trug
nicht.
Ehrlich berichtet statt geglaettet: Die Szenariomethode ueberanpasst. Ueber
zwoelf Laeufe (S = 200, 400, 800) lag die tatsaechliche Quote zwischen
93,14 % und 96,71 %, und die Spanne wurde von S = 400 auf 800 wieder
breiter. Beide Solver bestaetigen optimal bei identischen Kosten - echte
Ueberanpassung, kein Solverartefakt. Steht als Warnkasten im Abschnitt.
Die Laufzeitmessung ist aus der Ausgabe entfernt: Eine Wanduhrzeit ist nie
byteidentisch reproduzierbar (2,2 s / 2,3 s zwischen zwei Laeufen) und haette
Regel 4 dauerhaft gebrochen. Danach drei Laeufe zeichengleich, und die
abgedruckte Ausgabe stimmt mit dem Lauf des extrahierten Programms ueberein.
Mitgezogen: Kapitelkopf, Lernziele, Uebersichtstabelle (drei -> vier
Ansaetze), Selbsttest, Zusammenfassung, Vorwort-Programmverzeichnis, eine
neue Uebungsaufgabe und ihre Loesung in Anhang A.
pyproject.toml korrigiert: cvxpy lag in [finance], wird aber von 13
Programmen gebraucht, darunter dreien in diesem Kapitel - jetzt in der
Grundausstattung. Es zieht kein highspy nach, der Solverkonflikt bleibt auf
[large-scale] beschraenkt.
Stand: 293 Abschnitte, 712 Querverweise, 327 Indexmarken, 74 Programme (0
Fehler), 33 pytest-Tests, PDF 725 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:57:19 +02:00
---
Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
## 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 |