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>
261 lines
15 KiB
Markdown
261 lines
15 KiB
Markdown
# CLAUDE.md
|
||
|
||
Kontext zum Repository *Optimierte Entscheidungsfindung mit Python*, Version 04.
|
||
|
||
## Vor jeder Arbeit zuerst lesen
|
||
|
||
* **`PLAN.md`** — das Ziel und der Weg dorthin: alle sechs Phasen, die Zielstruktur, der
|
||
Kapitel-Baukasten, die dreizehn verbindlichen Regeln, die Verifikationsschritte. Ändert
|
||
sich nur bei einem Kurswechsel.
|
||
* **`PROGRESS.md`** — der erreichte Stand: eine Checkliste zur Wiederaufnahme, die
|
||
Referenzwerte zum Gegenprüfen, was erledigt ist, welche Funde es unterwegs gab und **was
|
||
der konkret nächste Schritt ist**. Wird nach jedem Arbeitsschritt fortgeschrieben.
|
||
|
||
Ohne diese beiden Dateien fehlt der Kontext, um sinnvoll weiterzuarbeiten — die Kapiteltexte
|
||
allein verraten weder den Stand noch die Konventionen. Alle sechs Phasen sind abgeschlossen;
|
||
`PROGRESS.md` Abschnitt 7 nennt, was bewusst offen geblieben ist und warum.
|
||
|
||
---
|
||
|
||
## Struktur
|
||
|
||
Dieses Verzeichnis ist die Wurzel; alle Skripte leiten ihre Pfade daraus ab
|
||
(`BASIS = dirname(dirname(__file__))` bzw. `dirname(HIER)`).
|
||
|
||
```
|
||
Operations_Research_mit_Python_Version_04/ Quelle: 37 Kapiteldateien + Build-Skripte
|
||
+ NEUER_TITEL.md (Titeltext)
|
||
bilder_04/ Quelle: Diagramme + erzeuge_*.py-Generatoren
|
||
web_04/ Quelle: site.css, site.js, icons.svg,
|
||
plotly.min.js, katex/ - die statischen
|
||
Bestandteile der Website
|
||
Operations_Research_mit_Python_Version_04.md generiert: Gesamtdokument
|
||
Operations_Research_mit_Python_Version_04.pdf generiert: PDF (xelatex)
|
||
OR_HTML_04/ generiert: Mehrseiten-Website
|
||
Operations_Research_mit_Python_Version_04_Programme/ generiert: Beispielprogramme
|
||
Notebooks_04/ generiert: ein .ipynb je Kapitel
|
||
pyproject.toml Quelle: Abhängigkeiten in Gruppen
|
||
Dockerfile, .dockerignore Quelle: Kurs-Image (Programme + JupyterLab)
|
||
LICENSE, LICENSE-TEXT.md MIT fuer Code, CC BY-SA 4.0 fuer den Text
|
||
.gitattributes LF im Repository, egal auf welchem System
|
||
.env.beispiel Vorlage; die echte .env (Ziel des Uploads)
|
||
ist bewusst NICHT im Repository
|
||
pandoc-defaults-basis.yaml, pandoc/, pandoc-defaults-buch.yaml PDF-Konfiguration
|
||
```
|
||
|
||
Nur die als **Quelle** markierten Verzeichnisse werden von Hand bearbeitet. Seit dem
|
||
Umzug nach `web_04/` stimmt dieser Satz auch: Vorher lagen `site.css`, `site.js`,
|
||
`icons.svg` und `katex/` mitten im erzeugten `OR_HTML_04/`. `--check` bewacht das jetzt
|
||
(`pruefe_assets()`), und der Bau bricht ab, wenn jemand die Kopie statt der Quelle
|
||
bearbeitet hat.
|
||
|
||
Dieses Verzeichnis ist seit dem 08.09.2026 ein **eigenes Git-Repository** (`main`). Das
|
||
übergeordnete `OR_mit_Python/` ist nur noch das Archiv der Historie bis zur Trennung und
|
||
verwaltet aktiv allein `Version_03`; es trägt `Version_04/` in seiner `.gitignore`. Commits
|
||
gehören ab jetzt hierher.
|
||
|
||
## Build
|
||
|
||
```bash
|
||
cd Version_04
|
||
python3 Operations_Research_mit_Python_Version_04/build_version_04.py --check # nur prüfen
|
||
python3 Operations_Research_mit_Python_Version_04/build_version_04.py --pdf --html
|
||
python3 Operations_Research_mit_Python_Version_04/extract_programme_04.py
|
||
python3 Operations_Research_mit_Python_Version_04/erzeuge_glossar_04.py # Anhang E
|
||
python3 Operations_Research_mit_Python_Version_04/veroeffentliche_04.py # hochladen
|
||
```
|
||
|
||
`veroeffentliche_04.py` lädt `OR_HTML_04/` per `rsync --delete` nach <https://jamulix.de/OR/>
|
||
— **nicht** per `scp`, denn das überschreibt nur und löscht nie; eine aus dem Buch entfernte
|
||
Seite bliebe sonst dauerhaft online. Vorher prüft es, ob eine Quelldatei neuer ist als die
|
||
gebaute Website (die Website hing einmal wochenlang einen Bau zurück, ohne dass es auffiel),
|
||
hinterher ruft es dreizehn Adressen per HTTP ab. Bei mehr als 30 Löschungen bricht es ab —
|
||
die Liste ist dann anzusehen, bevor `--loeschgrenze N` sie freigibt. Ziel in `.env`
|
||
(nicht im Repository, Vorlage `.env.beispiel`); Anmeldung per SSH-Schlüssel, kein Passwort.
|
||
|
||
`--check` prüft mehr als die Struktur: fehlende Codezäune, Links auf nicht existierende
|
||
Dateien und **harte Kapitel-/Abschnitts-/Aufgabennummern im Quelltext**. Es meldet Datei und
|
||
Zeile. Der Suchausdruck erlaubt beliebigen Zwischenraum — der letzte gefundene Fall war ein
|
||
`Handrechnung\n12.1` über zwei Zeilen, an dem jede zeilenweise Suche vorbeiläuft.
|
||
|
||
`build_version_04.py` fasst nicht notwendige harte Zeilenumbrüche innerhalb von Absätzen
|
||
zusammen (`reflow_markdown()`) — viele Markdown-Renderer stellen einen einzelnen Umbruch
|
||
sonst fälschlich als sichtbaren Umbruch dar.
|
||
|
||
---
|
||
|
||
## Die wichtigsten Konventionen
|
||
|
||
**Keine abgeleiteten Zahlen im Quelltext.** Das ist die Regel, an der dieses Projekt am
|
||
häufigsten gescheitert ist — **zehn** Mal an verschiedenen Stellen verletzt gefunden
|
||
(Anhang A, 66 Programm-Docstrings, die Übersichtstabellen der Anhänge, `CLAUDE.md` selbst,
|
||
eine Tabellenzelle, die 98 Lösungsmarken des Anhangs, neun Denkfehler-Verweise, sieben
|
||
„Übung N.N"-Verweise in alter Zählung, die Dateiübersicht des Leser-Wegweisers,
|
||
zuletzt der Titeltext der Startseite).
|
||
Seit dem letzten Fund trägt **keine** Marke mehr eine handgeschriebene Nummer, und `--check`
|
||
bewacht jede Familie. Konkret:
|
||
|
||
* Überschriften: `# Kapitel: <Titel> {#kap:<label>}`, `## Titel {#sec:<label>}` — die Nummer
|
||
vergibt der Build (`resolve_numbering()`, `nummeriere_abschnitte()`).
|
||
* Aufgaben, Handrechnungen, Abbildungen, Micro-Quiz: `**Aufgabe ⭐ — Titel.**`,
|
||
`> **✏️ Handrechnung: Titel**`, ``,
|
||
`> **❓ Micro-Quiz: Titel**` — Nummern von `nummeriere_marken()`.
|
||
* **Lösungen in Anhang A: `**{loesung} — Titel.**`** Der Präfix kommt nicht aus
|
||
`# Anhang A:`, sondern aus dem Kapitel des umgebenden `{#sec:loesungen-<X>}`-Abschnitts —
|
||
die Zuordnung `sec:loesungen-<X>` ↔ `kap:<X>` gilt für alle 23. `--check` zählt zusätzlich
|
||
ab, dass es je Kapitel so viele Lösungen wie Aufgaben gibt.
|
||
* **Der Denkfehler bekommt bewusst gar keine Nummer.** Es gibt je Kapitel genau einen, unter
|
||
einer bereits nummerierten Überschrift. Verwiesen wird auf
|
||
`{ref:sec:<kapitel>-denkfehler}`.
|
||
* Im Fließtext `{ref:<label>}`, nie eine Literalzahl.
|
||
* **Das Glossar (Anhang E) wird erzeugt.** Die 234 Einträge stehen in
|
||
`glossar_eintraege_04.py` als Viertupel *(Anzeigename, Indexmarke, Definition,
|
||
Verweisziel)*; `erzeuge_glossar_04.py` schreibt daraus `94_Anhang_Glossar.md`. Die `.md`
|
||
**nicht** von Hand bearbeiten — `--check` bemerkt es und nennt die Quelle. Der Erzeuger
|
||
prüft außerdem gegen das Buch: Jede Indexmarke muss dort buchstabengetreu vorkommen (sonst
|
||
entstünde ein zweiter Registereintrag für dasselbe Konzept), jedes `{ref:}`-Ziel muss
|
||
existieren, und kein Verweis darf auf das Glossar selbst zeigen. Begriffe, die es nur im
|
||
Glossar gibt, stehen ausdrücklich in `NUR_IM_GLOSSAR` — sonst ließe sich ein solcher Fall
|
||
nicht von einem Tippfehler unterscheiden.
|
||
* **Der Titeltext steht in `Operations_Research_mit_Python_Version_04/NEUER_TITEL.md`** und
|
||
nirgends sonst. `titeltexte_04.py` liest ihn; die PDF-Titelseite
|
||
(`bilder_04/erzeuge_titelseite.py`) und der Hero-Block der Startseite
|
||
(`baue_landingpage()`) speisen sich daraus. Eine Titeländerung wird **einmal** dort
|
||
gemacht. Das Vorwort trägt denselben Text als Buchprosa — `pruefe_titeltexte()`
|
||
vergleicht ihn bei jedem `--check` mit der Quelle.
|
||
* **Die Dateiübersicht in `Operations_Research_mit_Python_Version_04/README.md`** steht
|
||
zwischen `<!-- KAPITELTABELLE:ANFANG -->` und `<!-- KAPITELTABELLE:ENDE -->` und wird von
|
||
`schreibe_kapiteltabelle()` aus derselben Struktur erzeugt wie die Nummern im Buch. Nicht
|
||
von Hand bearbeiten — `--check` meldet einen veralteten Block, ein Lauf ohne `--check`
|
||
zieht ihn nach.
|
||
* In Code-Kommentaren und Docstrings der Kapitel**name** („Kapitel Metaheuristiken:"), nie
|
||
die Nummer. Auch nicht im Dateinamen.
|
||
|
||
**Querverweise für die Website** laufen über `baue_seiten_registry()` /
|
||
`resolve_numbering_seite()`, nicht über `resolve_numbering()` — nur dort entsteht aus einem
|
||
seitenübergreifenden Verweis `andere-seite.html#anker`.
|
||
|
||
**Stichwortregister:** `{idx:Begriff}` bzw. `{idx:Oberbegriff!Unterbegriff}`. Vor einem neuen
|
||
Begriff prüfen, ob er schon existiert:
|
||
`grep -ohE '\{idx:[^}]+\}' Operations_Research_mit_Python_Version_04/*.md | sort -u` — sonst
|
||
entstehen zwei Registereinträge für dasselbe Konzept. `texindy` sortiert mit dem deutschen
|
||
`din5007`-Modul (Umlaute wie im Telefonbuch); das generische `-L german` bricht ab.
|
||
|
||
**Programme sind ein Artefakt, keine zweite Quelle.** Änderungen gehören in den
|
||
Kapitel-Codeblock, danach `extract_programme_04.py`. Ein Codeblock gilt als vollständiges
|
||
Programm, wenn er mit `#!/usr/bin/env python3`, einer Leerzeile und `# Name.py` beginnt.
|
||
Auch das `README.md` im Programme-Verzeichnis wird erzeugt — aus der Vorlage
|
||
`README_Programme.md`; `requirements.txt` ist die einzige dort von Hand gepflegte Datei.
|
||
|
||
**`or_kern.py` ist der gemeinsame Unterbau** aller Programme (Domänenmodell, `SolverStatus`,
|
||
`Loesung`-DTO, Abnahmeprüfung). Abgedruckt im Kapitel Praxisfallen.
|
||
|
||
**`ortools` und `highspy` lassen sich nicht im selben Prozess importieren** (beide bringen
|
||
eine eigene HiGHS-Kopie mit). Deshalb lädt `or_kern.py` Solverbibliotheken erst in der
|
||
aufrufenden Funktion, und Programme, die beide brauchen, starten getrennte Prozesse (Muster:
|
||
`Ein_System_Vier_Ansaetze.py`, `Solverwechsel_CPSAT_HiGHS.py`). `cvxpy` zieht ein
|
||
installiertes `highspy` bei der Solver-Erkennung selbst mit hinein — der Konflikt entsteht
|
||
also auch indirekt.
|
||
|
||
**Neue Kapiteldatei** ⇒ in die `DATEIEN`-Liste in `build_version_04.py`.
|
||
**Neues Programm** ⇒ drei Stellen: Kapitelkopf („Programme:"), Vorwort
|
||
(„Verzeichnis der Beispielprogramme"), bei neuer Abhängigkeit `requirements.txt` **und**
|
||
`pyproject.toml` (dort in die passende Gruppe, nicht pauschal in die Grundausstattung).
|
||
|
||
Die vollständigen **dreizehn Regeln** stehen in `PLAN.md` Abschnitt 9 — darunter, dass jede
|
||
abgedruckte Ausgabe aus einem echten Lauf stammt und dass `PROGRESS.md` in denselben Commit
|
||
gehört wie die Arbeit, die sie beschreibt.
|
||
|
||
---
|
||
|
||
## Diagramme
|
||
|
||
`bilder_04/erzeuge_*.py` erzeugen 18 der 32 Kapiteldiagramme, jeweils **aus derselben
|
||
Instanz wie das zugehörige Buchprogramm**. (Das 33. SVG im Verzeichnis ist `titelseite.svg`,
|
||
die der Build bei jedem Lauf selbst schreibt — deshalb nennt `README.md` 33 und diese Datei
|
||
32.) Das ist kein Selbstzweck: Von zehn nachgebauten Bildern förderten
|
||
sieben einen Fehler zutage — dreimal ein Modell, das im Buch gar nicht vorkommt, einmal
|
||
widersprüchliche Zahlen zwischen Bild und Text, einmal ein gekipptes Vorzeichen. Wer ein
|
||
Diagramm anfasst, vergleicht es zuerst mit dem Modell des Kapitels.
|
||
|
||
Die übrigen 14 sind schematisch (Kästen, Pfeile, beschriftete Formeln) und bekommen bewusst
|
||
keinen Generator — dort kann nichts driften. Das Kriterium steht in `PROGRESS.md`
|
||
Abschnitt 6c.
|
||
|
||
Konventionen der Generatoren: `plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"` und
|
||
`metadata={"Date": None}` für byteidentische Läufe — **beides, sonst trägt das SVG einen
|
||
Zeitstempel und wechselnde `clip-path`-IDs**. Geschrieben wird **nur SVG**; PNG-Zweitfassungen
|
||
gab es einmal, sie wurden nirgends referenziert; stammt die Instanz aus einem
|
||
Zufallsstrom, wird die Ziehungsreihenfolge des Buchprogramms nachgespielt.
|
||
|
||
## Plotly-Figuren
|
||
|
||
`{plotly:name}` bindet `bilder_04/plotly/<name>.html` in die Kapitelseite ein. Das Fragment
|
||
wird als Rohblock ` ```{=html} ` ausgegeben — **nicht** als blankes HTML: Plotlys Fragment ist
|
||
eine einzige lange Zeile mit eingebetteten Leerzeichenketten, aus der Pandoc sonst einen
|
||
Codeblock macht. Genau daran waren alle vier Figuren kaputt, bis es die Schlussabnahme fand.
|
||
Im PDF steht stattdessen ein Hinweis auf die Website.
|
||
|
||
## Karten
|
||
|
||
`{karte:name}` bindet `bilder_04/karten/<name>.html` als `<iframe>` ein — anders als bei
|
||
Plotly **nicht** inline, weil Leaflets Stylesheet sonst in die Buchseite hineingriffe. Im PDF
|
||
steht wieder ein Hinweis auf die Website. `--check` prüft für beide Markenarten, dass die
|
||
Datei existiert.
|
||
|
||
Erzeugt werden die Karten von demselben `bilder_04/erzeuge_*.py`, das auch das zugehörige SVG
|
||
schreibt — **aus einem Lauf**, sonst zeigten die beiden Darstellungen bei einem heuristischen
|
||
Solver womöglich verschiedene Lösungen. Zwei Vorkehrungen in `erzeuge_vrp_touren.py` sind
|
||
übertragbar:
|
||
|
||
* **Byte-Reproduzierbarkeit:** Folium vergibt jedem Element eine `uuid4`. `normalisiere()`
|
||
ersetzt sie durch fortlaufende Nummern — dieselbe Sorgfalt wie `svg.hashsalt` bei den
|
||
matplotlib-Bildern.
|
||
* **Netzunabhängigkeit:** Von Foliums zehn CDN-Einbindungen bleiben die zwei gebrauchten,
|
||
umgeschrieben auf `../../assets/leaflet.*` (mitgeliefert in `web_04/assets/`); die übrigen
|
||
acht entfallen, weil die Karte nur `CircleMarker`, `PolyLine` und `Tooltip` benutzt. Ohne
|
||
Netz fehlen nur die Kartenkacheln.
|
||
|
||
**Die Instanz ist synthetisch und bleibt es.** `projiziere()` legt das 100 × 100-km-Raster
|
||
azimutal-äquidistant auf eine reale Region; `pruefe_projektion()` bricht ab, wenn die
|
||
Abstände dabei um mehr als 0,01 % verzerrt würden (gemessen: 0,001 %). Kein abgedruckter Wert
|
||
ändert sich dadurch — das war die Bedingung, unter der die Karte überhaupt möglich wurde.
|
||
|
||
---
|
||
|
||
## Veroeffentlichung
|
||
|
||
Das Buch steht online auf <https://jamulix.de/OR/>, das Repository oeffentlich auf
|
||
<https://kitux.de/forgejo/dschlueter/operations_research>.
|
||
|
||
Die Website hat vier Übersichtsseiten: `index.html` (Landing), `programme.html`,
|
||
`notebooks.html` und `stichwortverzeichnis.html`. Die beiden Downloadverzeichnisse
|
||
`programme/` und `Notebooks_04/` bekommen je eine `index.html`, die auf ihre Übersicht
|
||
weiterleitet — sonst antwortet der Webserver mit 403, weil Directory-Listing abgeschaltet
|
||
ist.
|
||
|
||
**Nach jedem `--pdf --html` gehoert `veroeffentliche_04.py` hinterher** — sonst laeuft die
|
||
veroeffentlichte Fassung dem Repository hinterher, und genau das ist einmal wochenlang
|
||
unbemerkt geblieben. Von Hand muss dabei nichts mehr geprueft werden: Das Skript weigert
|
||
sich, einen veralteten Bau hochzuladen, und ruft anschliessend vierzehn Adressen ab.
|
||
|
||
Der erste Lauf am 08.09.2026 uebertrug 213 Dateien und loeschte 35 — 33 verwaiste PNGs, ein
|
||
leeres `__pycache__/` und die geteilte Anhangseite. Alle 35 haette ein `scp`-Upload stehen
|
||
gelassen.
|
||
|
||
**`COLAB_BASIS_URL` ist bewusst leer.** Colab oeffnet Notebooks nur aus GitHub, Google Drive
|
||
oder einem Upload; die URL-Form `colab.research.google.com/github/...` ist fest auf GitHub
|
||
verdrahtet und kann eine selbstgehostete Git-Instanz nicht lesen. Statt eines toten Knopfes
|
||
bekommt jedes Kapitel einen Download-Link auf sein Notebook. Wer spaeter nach GitHub
|
||
spiegelt, traegt dort **einen** Wert ein.
|
||
|
||
Wer ohne eigene Installation rechnen will, nimmt das Kurs-Image:
|
||
`docker run --rm -p 127.0.0.1:8888:8888 or-mit-python` startet JupyterLab mit allen
|
||
25 Notebooks. Ohne Portfreigabe und mit einem Programmnamen als Argument fuehrt dasselbe
|
||
Image ein einzelnes Programm aus.
|
||
|
||
---
|
||
|
||
## Sprache
|
||
|
||
Kommentare, Ausgaben und Fließtext sind durchgängig **Deutsch** — diesen Stil beibehalten.
|