Commit graph

49 commits

Author SHA1 Message Date
dschlueter
99cb85907c Stichwortregister: automatische Markierung aller Glossarbegriffe (988 statt 456 Marker)
Neues Skript erzeuge_stichwortregister_04.py:
- Sucht alle Glossarbegriffe (Anzeigename) in den Kapiteldateien
  und markiert die erste Fundstelle je Datei mit {idx:Indexmarke}
- Termliste aus glossar_eintraege_04.py + vorhandenen {idx:...}-Markern
- Idempotent: erkennt vorhandene Marker und überspringt sie
- Überspringt Codeblöcke, Inline-Code und Math
- --check und --bericht Modi
- 605 Marker eingefügt (2 Durchläufe: 597 + 8)

Zusätzlich:
- titeltexte_04.py _saeubern(): entfernt {idx:...}-Marker vor
  Titeltext-Vergleich (pruefe_titeltexte schlug sonst fehl)
2026-09-10 00:07:01 +02:00
dschlueter
488c3d4368 Vorwort: 'auf Linux' → 'unter Linux' 2026-09-09 23:37:59 +02:00
dschlueter
374aaaee71 Vorwort: Backslash-Zeilenumbrüche entfernt, Tippfehler korrigiert (Windows, getestet) 2026-09-09 23:34:55 +02:00
dschlueter
7c8c4951af Vorwort: Überschrift 'Erforderliche Software-Umgebung' entfernt 2026-09-09 23:28:53 +02:00
dschlueter
a2d9dbfb30 Vorwort: Hinweise zur Entstehung und Software-Umgebung überarbeitet 2026-09-09 23:25:50 +02:00
dschlueter
f6a0d0deff Vorwort: Hinweis zur Entstehung und verwendeten Werkzeugen ergänzt 2026-09-09 23:09:40 +02:00
dschlueter
141178f4a1 PDF-TOC-Links: \phantomsection vor \addcontentsline für unnummerierte Überschriften
Pandoc generiert für {-}-Überschriften \section* + \addcontentsline,
aber ohne \phantomsection davor. Die TOC-Links zeigen dann auf die
falsche Position — erst ab der ersten nummerierten \section (Notation
und Abkürzungen) stimmen sie wieder, weil diese ihren hyperref-Anker
automatisch bekommt.

Fix in baue_pdf(): nach dem Pandoc-Lauf jeden \addcontentsline{toc}
ein \phantomsection voranstellen; eine zweite Sub verhindert Dopplung
beim Stichwortregister, das bereits eines hat.
2026-09-09 22:31:56 +02:00
dschlueter
1070baad7a TOC: Titel/Untertitel unlisted, 'Über den Kurs' als Teil 0 vor Vorwort
- Titel und Untertitel im Vorwort von {-} auf {.unlisted .unnumbered}
  umgestellt: verschwinden aus dem Inhaltsverzeichnis, bleiben sichtbar
- '# Über den Kurs {-}' als neue Oberüberschrift vor dem Vorwort
  eingefügt — gruppiert im TOC als 'Teil 0' den gesamten Front Matter
- TOC_MARKE auf '# Über den Kurs {-}' gesetzt, damit \tableofcontents
  davor eingefügt wird
- verschiebe_toc_vor_vorwort(): Anker auf <h1 id="über-den-kurs">
  umgestellt (Pandoc behält das ü in der ID)
- pruefe_titeltexte(): ohne_marke() generalisiert von {-} auf beliebige
  Attributblöcke {…}, sonst schlägt --check bei .unlisted .unnumbered fehl
2026-09-09 22:25:12 +02:00
dschlueter
0d76c1a89d Vorwort: Navigationszeile einzeilig, Linktexte statt Dateinamen; --check bewacht einzeilige Weiter-mit-Zeilen 2026-09-09 20:42:39 +02:00
dschlueter
abc7a5693f PDF: Font-Fallback für Emoji und Sonderzeichen - 2070 Missing-character-Faelle behoben 2026-09-09 19:10:50 +02:00
dschlueter
9b461426fc neue Titelseite 2026-09-09 08:33:01 +02:00
dschlueter
dc088fa214 kleine Änderungen 2026-09-09 07:37:07 +02:00
dschlueter
0f9c815b86 Grafik-Upgrade Stufe 1: elf neue Diagramme
Grafik_Upgrade.md verlangt "37 didaktisch wertvolle Visualisierungen". Es sind
19: Die Abschnitte "Modul 1-4" wiederholen die Grafiken 1-18 unter den Nummern
19-36, nur Grafik 37 ist zusaetzlich. Belegt an den Skriptnamen - 38
Nennungen, 19 verschiedene Dateien, erzeuge_kombinatorik_wand.py steht dreimal
darin. Von den 19 betreffen neun bereits vorhandene Diagramme.

Umgesetzt sind die zehn wirklich neuen plus eine elfte, die aus einem
Sachfehler des Auftrags entstand: Grafik 14 sollte in Abschnitt 12.3 stehen,
nannte aber Zahlen aus Kapitel 17. Beide Stellen haben jetzt ihre eigene
Grafik mit ihren eigenen Zahlen.

Vier Vorgaben des Auftrags wurden bewusst nicht befolgt: PNG bei 300 dpi (das
Buch kennt nur SVG - die 33 verwaisten PNGs waren gerade erst geloescht
worden), Matplotlib-Standardfarben (die Buchpalette steht in 33 Diagrammen),
harte Kapitelnummern im Fliesstext ({ref:sec:...} stattdessen) und die
fehlende Byte-Reproduzierbarkeit.

Der eigentliche Ertrag waren die Selbsttests. Jeder Generator prueft seine
Zahlen gegen das, was im Kapitel steht, und bricht bei Abweichung ab. Vier
haben angeschlagen:

* Kombinatorik-Wand: 25! ergibt 491,5 Mio. Jahre, die Tabelle nennt 490 Mio. -
  Rundung auf zwei signifikante Stellen, nicht Fehler des Buchs.
* Laufzeit-Diagramm: Die abgedruckte Prozentspalte laesst sich aus den
  abgedruckten Zeiten nicht exakt nachrechnen (0,001/0,008 ergibt 12 %,
  gedruckt sind 14 %) - die Zeiten sind gerundet, die Prozente nicht.
* Predict-then-Optimize: geratene Datenaufteilung ergab 197,35 statt 200,95
  MSE. Mit TRAINING aus dem Buchprogramm stimmen alle vier Kostenwerte.
* Almgren-Chriss: ein echter Fehler im Buchprogramm.

DER FUND: analytische_loesung() minimiert die eigene Kostenfunktion nicht. Ihr
Pfad kostet 10.860,40 EUR, der DP-Pfad 10.266,24 EUR - ein Gitterverfahren
kann das kontinuierliche Optimum aber nicht unterbieten. periodenkosten()
rechnet das Risiko mit lambda/2, die geschlossene Formel setzt
kappa~^2 = lambda*sigma^2*P0^2/eta ohne dieses Halbe. Mit lambda/2 liefert die
Formel 10.264,85 EUR und denselben Pfad wie die DP, knapp UNTER der
Gitterloesung. Die Grafik zeigt bis zur Entscheidung darueber den DP-Pfad; er
ist der, den das Kapitel abdruckt.

Nachweise: Alle elf Diagramme sind ueber zwei Laeufe byte-identisch. Kein
vorhandenes Diagramm wurde veraendert. Die Lastabwurf-Grafik reproduziert die
Befundtabelle exakt (874.870/2.504.154/28 von 40 gegen 351.356/415.924/0).

Zwei Abbildungen landeten beim automatischen Einfuegen im Codeblock (ihre
Anker standen in abgedruckten Ausgaben) und wurden dahinter verschoben; eine
Pruefung ueber alle elf zeigt null Fehlplatzierungen.

Neu: bilder_04/stil_04.py buendelt Palette, rcParams und speichere().
networkx wurde entgegen der Planung nicht gebraucht - pyproject.toml bleibt
unveraendert. PDF von 781 auf 788 Seiten, 44 Diagramme, 27 Generatoren.
Veroeffentlicht: 25 Dateien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 01:11:33 +02:00
dschlueter
509ce021da NEUER_TITEL.md zu den Quellen, Kritik-Verzeichnis ganz heraus
Der erste Anlauf schloss Kritik_und_Verbesserungsvorschlaege/ mit einer
Gegenausnahme aus, weil NEUER_TITEL.md darin lag und der Bau ohne diese Datei
abbricht:

    Kritik_und_Verbesserungsvorschlaege/*
    !Kritik_und_Verbesserungsvorschlaege/NEUER_TITEL.md

Das funktionierte, war aber die schlechtere Loesung: Eine Bauquelle blieb in
einem Verzeichnis liegen, das aus dem Repository verschwinden soll. Jetzt
steht sie als Operations_Research_mit_Python_Version_04/NEUER_TITEL.md bei den
uebrigen Quellen, und .gitignore braucht nur noch eine Zeile.

Eine Stelle im Build musste mit: pruefe_dateien() meldet jede .md im
Quellverzeichnis, die weder in DATEIEN steht noch mit README beginnt, als
"nicht eingebundene Datei". Die bis dahin leere Menge KEINE_KAPITELDATEIEN
fuehrt jetzt NEUER_TITEL.md - eine Quelle, aber keine Kapiteldatei.

Der Nachweis, dass der Umzug nichts veraendert hat, brauchte einen Umweg: Die
Pruefsumme von titelseite.svg aenderte sich - aber nur, weil das Fussband das
Tagesdatum traegt und inzwischen der 9. September war. Mit dem Datum des
Vortags erzeugt, ist die Datei byte-identisch zum committeten Stand
(1b076697...). Dass der Text unveraendert ist, zeigt schon der
Strukturvergleich: 721 Glyphenverweise und 81 Pfaddefinitionen in beiden
Fassungen.

Gegengeprueft mit einem simulierten Klon (git checkout-index in ein leeres
Verzeichnis): Das Kritik-Verzeichnis fehlt dort vollstaendig, NEUER_TITEL.md
liegt im Quellverzeichnis, und --check liefert dieselben Zahlen wie lokal.

Was das NICHT leistet: Die fuenf Dateien verschwinden aus kuenftigen Klons,
nicht aus der Historie. Wer einen alten Commit auscheckt, bekommt sie
weiterhin.

Veroeffentlicht: 5 Dateien uebertragen, PDF unveraendert 781 Seiten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 00:31:16 +02:00
dschlueter
7f32ef3b8d Kritik_und_Verbesserungsvorschlaege in .gitignore - mit einer Ausnahme
Rezension und Verbesserungsvorschlaege haben ihren Zweck erfuellt und gehoeren
nicht in die Veroeffentlichung. Fuenf der sechs Dateien sind ausgetragen; sie
bleiben lokal liegen, nur der Index kennt sie nicht mehr.

Die Ausnahme ist notwendig, nicht kosmetisch: NEUER_TITEL.md ist seit dem
Zusammenfuehren der Titeltexte die EINE Quelle, aus der titeltexte_04.py die
PDF-Titelseite und den Hero-Block der Startseite speist. Ohne die Datei bricht
lies_titeltexte() mit "Die Quelle des Titeltexts fehlt" ab - ein frischer Klon
koennte das Buch nicht mehr bauen.

Deshalb 'Kritik_und_Verbesserungsvorschlaege/*' plus Gegenausnahme statt eines
Verzeichnismusters: Git kann eine Datei nicht zurueckholen, deren Verzeichnis
als Ganzes ausgeschlossen ist.

Gegengeprueft mit einem simulierten Klon (git checkout-index in ein leeres
Verzeichnis): Dort liegt aus dem Verzeichnis nur NEUER_TITEL.md, und
build_version_04.py --check liefert dieselben Zahlen wie lokal - 5 Teile,
23 Kapitel, 6 Anhaenge, 998 Querverweise, 456 Indexmarken.

CLAUDE.md und README.md beschrieben das Verzeichnis noch als vollstaendige
Quelle; beide sind nachgezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 22:22:35 +02:00
dschlueter
803d8aa91e Literaturverzeichnis: alle Eintraege gegen Crossref geprueft
Die erste Fassung stuetzte sich bei rund 30 Eintraegen auf Kenntnis der
Standardliteratur statt auf eine Abfrage. Ein Skript hat jeden Eintrag einzeln
gegen die Crossref-API gestellt: DOIs ueber /works/{doi}, Eintraege ohne DOI
ueber die bibliografische Suche mit Aehnlichkeitsbewertung.

Ergebnis: 54 der 72 Eintraege tragen eine DOI, alle 54 loesen auf und zeigen
auf das richtige Werk. Die uebrigen 18 sind Webdokumentationen,
Institutsberichte, Konferenzbeitraege ohne DOI-Vergabe oder Buecher, die
Crossref nicht auf Buchebene fuehrt.

Zwei echte Fehler:

* Almgren & Chriss stand mit (2000) und der DOI 10.21314/JOR.2000.041 - die
  loest nicht auf. Richtig ist 2001 mit 10.21314/jor.2001.041; Band, Heft und
  Seitenzahlen stimmten.
* Rockafellar & Uryasev stand mit S. 21-42, und zwar schon in der
  Vorgaengerfassung. Der Verlagsdatensatz nennt S. 21-41.

Dazu sieben ergaenzte Buch-DOIs (Dantzig, Wolsey, Boyd/Vandenberghe, Ben-Tal
et al., Biere et al., Brandimarte/Zotteri, Powell), jede einzeln auf Verlag,
Jahr und Typ geprueft. Beim Handbook of Constraint Programming fuehrt Crossref
nur Kapitel-DOIs - statt eines geratenen Buch-DOI ist jetzt die Reihe genannt
(Foundations of Artificial Intelligence, Bd. 2).

Der lehrreichste Teil war das Verwerfen: Die Crossref-Suche liefert zu
"Network Flows" (Ahuja/Magnanti/Orlin) den Treffer 10.2307/2583863 mit 100 %
Titeluebereinstimmung - er gehoert zu einer Buchbesprechung von 1994, nicht
zum Buch. Ebenso falsch waren die Treffer zu Sutton/Barto (Rezension der
ersten Auflage, 1998), Bertsekas (1996), Kleppmann (Elsevier 2003) und dem
SCIP-Bericht (ein R-Paket). Eine automatische Uebernahme des besten Treffers
haette fuenf falsche DOIs ins Buch geschrieben; sichtbar wurde es erst beim
Abgleich von Typ, Verlag und Jahr.

Veroeffentlicht: 4 Dateien, PDF unveraendert 781 Seiten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 21:58:16 +02:00
dschlueter
b83bd392cd Literaturverzeichnis von 27 auf 87 Eintraege
Auftrag: bestehende Liste bewerten, Streichkandidaten benennen, moderne
Standardwerke recherchieren, neue Themenbloecke anlegen - mit Relevanzsternen
(3 Stufen) und Themen-Tags.

Welche Bloecke fehlen, entschied nicht die Vorschlagsliste, sondern das Buch.
Vorgeschlagen waren u. a. "Diskrete Ereignissimulation (SimPy)" und
"Cloud-basierte Solver-Nutzung". Beide wurden NICHT angelegt: "SimPy" kommt im
Buch null Mal vor, "Cloud" ebenfalls null. Eine Literaturliste soll das Buch
erschliessen, nicht darueber hinausweisen. Der API-gestuetzte Betrieb ist
stattdessen im neuen Praxisblock erfasst - dort gibt es mit dem
FastAPI-Optimierungsdienst tatsaechlich einen Anker im Text.

Zwoelf neue Bloecke, in der Reihenfolge der Kapitel: Modellierung in Python;
Lineare und ganzzahlige Optimierung; Moderne Solver und Benchmarks; Graphen,
Fluesse und Tourenplanung; Metaheuristiken; Spaltengenerierung und
Dekomposition; Optimierung unter Unsicherheit; Dynamische Programmierung;
Mehrkriterielle Optimierung; Lernbasierte Optimierung; Supply Chain, Energie
und Produktionsplanung; Praxis: Betrieb, Test und Auslieferung.

Streichkandidaten: keine. Alle 27 Alteintraege sind korrekt zitiert, in ihrem
Feld massgeblich und nicht redundant. Zwei wurden auf einen Stern herabgestuft
(Dantzig 1963, Bellman 1957) - historisch bedeutend, im selben Block aber
durch modernere Darstellungen ersetzt. Eine Streichliste zu erfinden, nur weil
der Auftrag eine vorsah, waere die schlechtere Antwort gewesen.

Zur Vorgabe "bevorzugt ab 2018": nur teilweise einloesbar, und zwar aus der
Sache heraus. 22 von 63 datierten Eintraegen sind von 2018 oder juenger. In
weiten Teilen des OR sind die massgeblichen Werke aelter - Ahuja/Magnanti/
Orlin (1993), Boyd/Vandenberghe (2004), Ehrgott (2005), Birge/Louveaux (2011).
Neu ist es dort, wo sich das Feld bewegt hat: Benchmarks, Python-Bibliotheken,
lernbasierte Optimierung.

Recherchiert, nicht erinnert: Bei einem Literaturverzeichnis ist eine
erfundene Quelle der schlimmste moegliche Fehler. Die unsicheren Angaben sind
per Websuche gegen die Primaerquelle geprueft - linopy (JOSS 8(84), 4823),
SCIP Optimization Suite 9.0, Elmachtoub & Grigas (Management Science 68(1),
9-26), MIPLIB 2017 (MPC 13(3), 443-490), HiGHS und Vidal/Laporte/Matl
(EJOR 286(2), 401-416).

Ein Fund dabei: Die Mittelmann-Benchmarks enthalten seit 2024 keine
kommerziellen Solver mehr - Gurobi, IBM und FICO haben ihre Ergebnisse
entfernen lassen. Ein direkter Vergleich Gurobi gegen HiGHS ist dort nicht
mehr moeglich. Das steht jetzt als Einschraenkung beim Eintrag.

Jeder Block verweist ueber {ref:kap:...} auf sein Kapitel - 37 Querverweise,
alle aufgeloest. PDF von 772 auf 781 Seiten. Veroeffentlicht: 4 Dateien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 21:44:01 +02:00
dschlueter
beff14094c Build-Kommando fuer das Glossar: erzeuge_glossar_04.py
Der Erzeuger lag als Wegwerfskript ausserhalb des Repositorys - genau die
Falle, die in diesem Projekt schon zweimal zugeschnappt ist (web_04/,
titeltexte_04.py): eine Quelle, die nicht dort liegt, wo gearbeitet wird.

    python3 Operations_Research_mit_Python_Version_04/erzeuge_glossar_04.py
    python3 .../erzeuge_glossar_04.py --check     # nur pruefen

Die 234 Eintraege stehen in glossar_eintraege_04.py als Viertupel
(Anzeigename, Indexmarke, Definition, Verweisziel). Das Verweisziel ist
eingefroren, nicht berechnet: Die erste Fundstelle eines Begriffs ist oft
nicht die erklaerende - "Schattenpreis" faellt erstmals im LP-Schnellstart,
erklaert wird er im Abschnitt ueber Dualitaet. Ein Automatismus haette diese
Entscheidung jedes Mal neu und jedes Mal gleich falsch getroffen.

Drei Pruefungen, alle gegen das Buch statt gegen eine Annahme:

* Jede Indexmarke muss im Buchtext buchstabengetreu vorkommen - sonst
  entstuende ein zweiter Registereintrag fuer dasselbe Konzept.
* Jedes {ref:}-Ziel muss existieren.
* Kein Verweis darf auf das Glossar selbst zeigen.

Dazu die Handaenderungs-Erkennung nach dem Muster von
spiegle_statische_assets(): Ist 94_Anhang_Glossar.md neuer als ihre Quelle und
weicht ab, bricht der Lauf ab und nennt die Quelle, statt die Arbeit still zu
ueberschreiben. build_version_04.py --check ruft den Erzeuger mit auf, damit
eine veraltete Fassung nicht erst auffaellt, wenn ihn jemand zufaellig startet.

Ein Zwischenfall beim Einbau hat die Pruefung verbessert: Die erste Fassung
meldete 24 angebliche Tippfehler. Es waren keine - es sind die Begriffe, die
im ganzen Buch NUR im Glossar indexiert sind (Rebalancing, Volatilitaet,
Solver-Status ...). Automatisch ist das von einem Tippfehler nicht zu
unterscheiden, also stehen sie jetzt ausdruecklich in NUR_IM_GLOSSAR.

Der Beleg, dass der Erzeuger die Handarbeit exakt trifft: Er schreibt die
vorhandene Datei byte-identisch neu. Beide Waechter gegengeprueft - mit einer
Handaenderung an der .md und mit einem Tippfehler in einer Indexmarke.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 21:28:53 +02:00
dschlueter
7835cf322d Glossar von 92 auf 234 Eintraege
Auftrag: alle Markdown-Dateien lesen, jeden Fachbegriff, jede Abkuerzung, jeden
Algorithmus- und Bibliotheksnamen extrahieren, das Glossar vollstaendig neu
aufsetzen.

Die Begriffsliste wurde geerntet, nicht erfunden. Grundlage sind die 219
{idx:}-Marken des Buchs - die bereits kuratierte Begriffsliste des Autors. 92
davon hatten einen Glossareintrag, 143 nicht. Dazu Abkuerzungen und
Bibliotheken aus dem Fliesstext ohne Indexmarke: GIL, GLOP, MIQP, MSE, MAPE,
DTO, Solver-Status, NumPy, SciPy, pandas, scikit-learn, yfinance, pydantic,
pymoo, Matplotlib, Plotly.

Auch die Querverweise sind gemessen: Fuer jeden Indexbegriff wurde die erste
Fundstelle bestimmt und daraus der umgebende {#sec:}-Abschnitt abgeleitet.
Damit steht am Ende jedes Eintrags ein {ref:}, das der Build zur richtigen
Nummer aufloest - eine harte "siehe Kapitel 5" haette gegen die Grundregel des
Projekts verstossen.

Zwei Funde beim Gegenpruefen:

* 23 Eintraege haetten auf sich selbst verwiesen. Ihr Begriff ist im ganzen
  Buch nur im Glossar mit {idx:} markiert - die "erste Fundstelle" war also
  diese Datei. Im PDF stand dann "-> Anhang E" mitten in Anhang E.
* Die erste Fundstelle ist nicht immer die erklaerende. "Schattenpreis"
  landete beim LP-Schnellstart statt beim Abschnitt ueber Dualitaet. Fuer alle
  92 Altbestandseintraege gelten jetzt die von Hand gesetzten Ziele der
  Vorgaengerfassung; der Erzeuger bricht ab, wenn ein Verweis ins Glossar
  selbst zeigt.

Zwei Formatfragen entschied die Probe am echten Build:

* "## A" als Buchstabengruppe funktioniert und wird NICHT nummeriert -
  ABSCHNITT_RE verlangt ein {#sec:}-Label, das hier fehlt.
* Pandoc-Definitionslisten ueberleben den Bau nicht: reflow_markdown() zieht
  die ":"-Fortsetzungszeile in den Begriffsabsatz, der Doppelpunkt bleibt
  sichtbar stehen. Die Absatzform rendert sauber in HTML und PDF.

Der Beleg, dass die Schreibweisen stimmen: Das Stichwortregister waechst von
219 auf 220 Hauptbegriffe - genau ein neuer Begriff (Solver-Status). Alle
uebrigen 218 Indexmarken trafen buchstabengetreu auf bestehende Eintraege.

Nebenbei: Die 25 Buchstabenueberschriften waeren als 25 Treffer in den
Suchindex geraten und bei jedem eingetippten Buchstaben oben gestanden.
_erfasse_suchindex() ueberspringt jetzt einbuchstabige Ueberschriften; der
Index bleibt bei 631 Eintraegen.

PDF von 759 auf 772 Seiten. Veroeffentlicht: 4 Dateien uebertragen, alle
dreizehn HTTP-Proben richtig, 234 Eintraege auf dem Server gegengeprueft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 21:17:20 +02:00
dschlueter
5d5bdb6508 Folium-Karten: die Blockade war eine falsche Alternative
Der einzige Punkt, der in diesem Projekt je als "blockiert" gefuehrt wurde,
ist erledigt. Die Begruendung lautete: Die VRP-Instanz sei ein synthetisches
100 x 100-km-Raster ohne Geokoordinaten, eine echte Karte verlange also
ENTWEDER eine neue Instanz (und damit zwei Dutzend neue abgedruckte Zahlen)
ODER eine willkuerliche Verankerung.

Es gibt einen dritten Weg, und er ist exakt. Eine azimutal-aequidistante
Projektion um den Schwerpunkt des Rasters bildet die 17 Punkte so auf Laenge
und Breite ab, dass die Grosskreisdistanzen mit den Rasterdistanzen
uebereinstimmen:

  festes cos(Breite)        Median 0,360 %   Maximum 2,003 %
  cos der eigenen Breite    Median 0,351 %   Maximum 0,908 %
  azimutal-aequidistant     Median 0,000 %   Maximum 0,001 %

Ein Meter auf hundert Kilometer. Kein abgedruckter Wert aendert sich, die
Gesamtstrecke bleibt bei 619 km, das SVG ist byte-identisch geblieben.

Zwei Befunde stuetzten die Entscheidung: "100 x 100 km Raster" steht nur in
zwei Codekommentaren, nirgends im Fliesstext; und der didaktische Punkt der
Bildunterschrift - die sich kreuzenden Touren, die KEIN Fehler sind - wird auf
einer Karte deutlicher.

erzeuge_vrp_touren.py schreibt jetzt SVG UND Karte aus einem Lauf. Ein zweites
Skript haette erneut geloest, und die Routing-Bibliothek arbeitet heuristisch.
pruefe_projektion() bricht ab, wenn die Verzerrung 0,01 % ueberschreitet - die
Zahl im Buch wird bei jedem Lauf bewiesen.

Neu im Build: die Marke {karte:name}, aufgeloest zu einem <iframe> (Leaflets
Stylesheet soll nicht in die Buchseite greifen), im PDF zum Verweissatz - wie
bei {plotly:}. --check bewacht jetzt beide Markenarten auf Existenz der Datei;
gegengeprueft mit einer absichtlich falschen Marke.

Zwei Vorkehrungen, die ohne Messung nicht aufgefallen waeren:

* Folium vergibt jedem Element eine uuid4 - jeder Lauf erzeugte eine andere
  Datei. normalisiere() ersetzt sie durch fortlaufende Nummern; zwei Laeufe
  liefern jetzt dieselbe Pruefsumme.
* Folium bindet ZEHN CDN-Ressourcen ein (jQuery, Bootstrap, Glyphicons,
  FontAwesome, awesome-markers ...). Gebraucht werden ZWEI. leaflet.js und
  leaflet.css liegen jetzt in web_04/assets/, die uebrigen acht entfallen.
  Nachgeprueft: Die Karte benutzt ausschliesslich L.*-Aufrufe. Ohne Netz
  fehlen nur die Kacheln, Marker und Touren bleiben sichtbar.

Gegenrechnung an vier Strecken: Depot -> K1 misst auf der Karte 27,20 km, die
Distanzmatrix sagt 27 - die Differenz stammt aus deren int(), nicht aus der
Geographie.

Veroeffentlicht: 8 Dateien uebertragen, alle dreizehn HTTP-Proben richtig, die
Karte auf dem Server unter 200 erreichbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 20:38:16 +02:00
dschlueter
d37834ccb5 PROGRESS.md: veralteten Upload-Warnhinweis ersetzen
Der Hinweis, die Website haenge einen Bau zurueck (toter Colab-Link, PDF-
Groesse), stammte aus der Zeit vor veroeffentliche_04.py und ist seit dem
ersten Skriptlauf falsch. An seiner Stelle steht jetzt, wer das Hochladen
uebernimmt und was dabei geprueft wird.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 20:06:28 +02:00
dschlueter
7757e33a97 veroeffentliche_04.py: leserliche Ausgabe, 404-Probe entfernt
Der erste Alltagseinsatz zeigte drei Schoenheitsfehler, alle in der Ausgabe.

Der Bericht stand verkehrt herum: Die rsync-Zeilen erschienen VOR "Ziel:" und
"Vorpruefung bestanden". Python puffert seine Ausgabe blockweise, sobald sie
nicht an ein Terminal geht, waehrend der Unterprozess unmittelbar schreibt.
sys.stdout.reconfigure(line_buffering=True) stellt die Reihenfolge her.

150 Zeilen fuer eine uebertragene Datei: --itemize-changes lief auch im echten
Durchgang mit, obwohl der Probelauf schon berichtet hatte, was passieren wird.
149 der 150 Zeilen besagten nur ".f..t......" - Inhalt gleich, nur Zeitstempel
nachgezogen, die erwartbare Folge von --checksum. Die Auflistung bleibt jetzt
dem Probelauf vorbehalten.

Der Ersatz war zuerst schlimmer als das Problem: --info=progress2 zeigt am
Terminal eine sich selbst ueberschreibende Zeile, in einer Pipe dagegen jeden
Zwischenstand einzeln - gemessen 213 Zeilen fuer 213 Dateien. Die Anzeige
haengt jetzt an sys.stdout.isatty().

Ausserdem: Die 404-Probe auf anhang-glossar-literatur.html ist entfernt. Die
Datei ist geloescht - im Arbeitsbaum, in Git und auf dem Server -, und die
Probe koennte einen Rueckschritt gar nicht mehr entdecken: Sie bliebe auch
dann gruen, wenn jemand --delete aus dem rsync-Aufruf naehme. Was sie belegt
hat, steht in PROGRESS.md. Von vierzehn Adressen bleiben dreizehn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 20:04:53 +02:00
dschlueter
d5d729650b Titelblatt: Anwendungsliste beginnt ohne "Von"
"Von Personaleinsatz-, Schicht- und Vertretungsplaenen, Fahrtrouten" wird zu
"Personaleinsatz-, Schicht- und Vertretungsplaene, Fahrtrouten" - Dativ zu
Nominativ, passend zum weggefallenen "Von".

Geaendert an EINER Stelle in NEUER_TITEL.md; PDF-Titelseite und Startseite
folgen daraus. Das Vorwort traegt denselben Text als Buchprosa und wurde
mitgezogen - pruefe_titeltexte() haette es sonst gemeldet.

Veroeffentlicht: 5 Dateien uebertragen, alle vierzehn HTTP-Proben richtig.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 19:49:03 +02:00
dschlueter
022bd4eaca Titeltext: eine Quelle fuer PDF-Titelseite und Startseite
Die Startseite zeigte einen anderen Titeltext als das PDF. Der Text stand
VIERMAL im Repository: NEUER_TITEL.md, die Konstanten in
bilder_04/erzeuge_titelseite.py, der Titelblock des Vorworts - und noch einmal
fest einprogrammiert im Hero-Block von baue_landingpage().

Die Ursache ist messbar: Die drei Commits, die den Titel ueberarbeitet haben
(ca5621f, 199dd3b, 1659fb9 im Ursprungs-Repository), fassten jedes Mal
dieselben drei Dateien an. build_version_04.py war in keinem davon, und
OR_HTML_04/index.html taucht in ihren Dateilisten gar nicht auf. Online stand
deshalb:

  Unterzeile  "Von den mathematischen Grundlagen zur quantitativen
              Portfolio-Optimierung an den Finanzmaerkten."
  Kompendium  "Ein vollstaendiges Kompendium mit Algorithmen ..."
  Anwendungen fehlte ganz

Behandlung wie bei den neun Nummern-Funden zuvor: nicht die Kopie korrigieren,
sondern die Kopie abschaffen. Das neue titeltexte_04.py zerlegt NEUER_TITEL.md
in Titel, Untertitel, Kompendium- und Anwendungen-Absatz; PDF-Titelseite und
Startseite lesen dieselbe Funktion. Der Autor bleibt bewusst Konstante im
Generator - die Vorlage beschreibt das Buch, nicht sein Impressum.

Der entscheidende Beleg: Die neu erzeugte titelseite.svg ist BYTE-IDENTISCH
(97fb40d7...) zur vorherigen - der Umbau hat am PDF nachweislich nichts
veraendert. Auch nicht am Umbruch: Die frueher handgesetzten drei Zeilen
entstehen jetzt automatisch aus _umbrechen(), und zwar exakt so. Ein
Textvergleich ueber das SVG war uebrigens nicht moeglich - es enthaelt kein
einziges <text>-Element, matplotlib zeichnet alle 726 Buchstaben als Kurven.

Das Vorwort bleibt von Hand gepflegter Buchtext, wird aber von
pruefe_titeltexte() bei jedem --check gegen die Quelle gestellt. Gleich beim
ersten Lauf fand es einen echten Unterschied: Das Vorwort setzte einen
Geviertstrich, die Vorlage einen Halbgeviertstrich. Im Deutschen ist der
Halbgeviertstrich der richtige - das Vorwort folgt jetzt der Quelle.

Veroeffentlicht: 5 Dateien uebertragen, alle vierzehn HTTP-Proben richtig.
Gegenprobe vom Server: alle drei Absaetze der Startseite sind zeichengenau
identisch mit NEUER_TITEL.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 19:39:49 +02:00
dschlueter
47d63dfe9c Menue: ZIP-Punkt nicht mehr eingerueckt, "(eine HTML-Seite)"
Zwei Beschriftungsaenderungen in baue_sidebar_html():

* "Download Notebooks als ZIP" steht jetzt auf gleicher Hoehe wie die
  uebrigen Punkte statt eingerueckt unter "Notebooks".
* "Gesamtdokument (eine Seite)" -> "(eine HTML-Seite)". Direkt darunter steht
  "Download Buch als PDF"; ohne das Wort HTML liess sich nicht erkennen, dass
  es zwei verschiedene Formate sind.

Mit der Einrueckung faellt ihre Ursache weg: Die CSS-Klasse
.sidebar-unterpunkt in web_04/assets/site.css war eigens dafuer angelegt
worden und wird von nichts mehr benutzt - entfernt, statt als tote Regel
stehen zu bleiben.

Veroeffentlicht: 42 Dateien uebertragen (die Seiten, nichts sonst - dank
--checksum), 0 geloescht, alle vierzehn HTTP-Proben richtig. Gegenprobe vom
Server: kein sidebar-unterpunkt mehr im ausgelieferten HTML.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 19:24:54 +02:00
dschlueter
3d85488d7c 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
dschlueter
e3ff5846f6 Menuepunkt praeziser: "Download Buch als PDF"
Der Eintrag stand direkt unter "Download Notebooks als ZIP"; ohne das Wort
"Buch" liess sich nicht auf einen Blick sehen, was da heruntergeladen wird.

Geaendert an der einen Stelle in baue_sidebar_html(), von dort auf alle 41
Seiten. Veroeffentlicht: 151 Dateien uebertragen, 0 geloescht, alle vierzehn
HTTP-Proben richtig.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 16:54:24 +02:00
dschlueter
ef79e6fdc1 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
dschlueter
d566cf8dac 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
dschlueter
567bfa799a 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
dschlueter
b6de2802fc 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
dschlueter
5c49725d5c 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
dschlueter
a1562d99e5 Referenzwerte in PROGRESS.md an den heutigen Bau angleichen
Vier Werte waren nach der Colab-Umstellung ueberholt: Zeilen (28.570 ->
28.547), Groesse (1.622 -> 1.620 KB) und die PDF-Seitenzahl an zwei Stellen
(760 -> 759). Die 760 im Abschnitt zu Phase 8.2 bleibt stehen - dort
beschreibt sie den damaligen Stand und waere nachtraeglich geaendert falsch.

Gegengeprueft: alle sechs aus --check ablesbaren Kennzahlen stimmen jetzt
mit der Tabelle ueberein.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:01:13 +02:00
dschlueter
6178afc263 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
dschlueter
4fa4e20b04 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
dschlueter
360912dbfa 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
dschlueter
3e5c6fc627 PROGRESS.md Abschnitt 7 auf den heutigen Stand bringen
Der Wiederaufnahme-Anker nannte noch 74 Programme, 32 Diagramme und "Phase 6
hat begonnen" - alles ueberholt. Jetzt steht dort: der erreichte Stand, die
drei Schritte, die von aussen kommen muessen (Remote-Repository,
COLAB_BASIS_URL, Veroeffentlichen), die fuenf bewusst zurueckgestellten
Pakete mit Begruendung, der eine rein redaktionelle Punkt (53 gekuerzte
Loesungstitel) und vier Dinge, die einer spaeteren Sitzung auffallen und
keine Fehler sind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 13:26:40 +02:00
dschlueter
35cc0da166 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
dschlueter
862e92bc7b 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
dschlueter
9c83716095 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
dschlueter
8ded88ac38 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:46:17 +02:00
dschlueter
a0691e1919 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
dschlueter
5a645a3944 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
dschlueter
293370b7a1 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
dschlueter
dd4acda129 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
dschlueter
cc990c2998 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
dschlueter
6c67e126dd 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
dschlueter
62952ba066 Fuenfte harte Nummer gefunden: eine Tabellenzelle - und die Pruefung dagegen
31_Unsicherheit.md trug in der Uebersicht "Die drei Ansaetze im Ueberblick"
noch die Zeile

    | Kapitel | 9.2 | 9.3 | 9.4 |

Die Abschnitte heissen in Version 04 aber 12.4 bis 12.6 - stehengebliebene
Version-03-Nummern, im PDF abgedruckt. Damit ist die Regel "keine
abgeleiteten Zahlen im Quelltext" zum fuenften Mal verletzt gefunden worden.

--check lief daran vorbei, weil der bestehende Ausdruck das Wort unmittelbar
vor der Zahl verlangt ((Kapitel|Abschnitt|...)\s+\d+). Hier steht "Kapitel"
als Zeilenkopf, durch " | " von der Zahl getrennt - \s+ matcht keinen
senkrechten Strich.

pruefe_dateien() bekommt daher eine zweite Pruefung: eine Tabellenzelle, die
nur aus einer Nummer der Form N.N besteht. Sie ueberspringt Codezaeune, weil
abgedruckte Programmausgaben ihre Spalten ebenfalls mit "|" setzen
(40_Finanzdaten.md Zeilen 381-385 waeren sonst fuenf Fehlalarme).

Gegengetestet: die wieder eingebaute Zeile wird mit allen drei Zellen
einzeln gemeldet, nach dem Rueckbau ist der Lauf sauber.

Die Zeile heisst jetzt "Nachzulesen in" und traegt drei {ref:sec:...};
Querverweise 703 -> 706. Referenzwerte in PROGRESS.md auf die gemessenen
Werte nachgezogen (27.015 Zeilen, 1512 KB) - sie lagen zwei Zeilen zurueck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:23:33 +02:00
dschlueter
b7af2f1d9a 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