operations_research/OR_HTML_04/Notebooks_04/oekosystem.ipynb

734 lines
32 KiB
Text
Raw Permalink Normal View History

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
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Kapitel 3: Das Python-Ökosystem für OR — Solver, Bindings und Modellierungsschichten\n",
"\n",
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
"# Lokal in einer virtuellen Umgebung genauso gueltig wie in Google Colab.\n",
"%pip install --quiet ortools highspy cvxpy scipy numpy pandas polars \\\n",
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Die Entscheidung in drei Fragen\n",
"\n",
"`Solver_Wahl.py`\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#!/usr/bin/env python3\n",
"\n",
"# Solver_Wahl.py\n",
"\"\"\"\n",
"Kapitel Oekosystem: Entscheidungshilfe zur Solverwahl.\n",
"Beantwortet drei Fragen und empfiehlt Bibliothek + Backend.\n",
"\"\"\"\n",
"\n",
"from dataclasses import dataclass\n",
"\n",
"\n",
"@dataclass\n",
"class Problem:\n",
" diskrete_entscheidungen: bool # Ja/Nein-Variablen, Zuordnungen, Reihenfolgen?\n",
" zielfunktion: str # \"linear\" | \"quadratisch\" | \"konvex\" | \"beliebig\"\n",
" routing: bool = False # Fahrzeuge, Depots, Zeitfenster?\n",
" groesse: str = \"klein\" # \"klein\" (<10^3 Var.) | \"gross\"\n",
" wiederholte_laeufe: bool = False\n",
"\n",
"\n",
"def empfehle(p: Problem) -> tuple[str, str, str]:\n",
" \"\"\"Gibt (Bibliothek, Backend, Begründung) zurück.\"\"\"\n",
" if p.routing:\n",
" return (\"ortools.constraint_solver (Routing)\", \"Routing Engine\",\n",
" \"Spezialisierte Heuristiken für VRP/TSP schlagen jedes selbstgebaute Modell.\")\n",
"\n",
" if p.diskrete_entscheidungen:\n",
" if p.zielfunktion in (\"linear\",) and not p.routing:\n",
" if p.groesse == \"gross\":\n",
" return (\"highspy\", \"HiGHS Branch-and-Cut\",\n",
" \"MILP mit ökonomischer Struktur (Fixkosten, Kardinalität).\")\n",
" return (\"ortools.sat (CP-SAT)\", \"CP-SAT\",\n",
" \"Logische Regeln und Zuweisungen: CP-SAT propagiert sehr effizient.\")\n",
" return (\"ortools.sat (CP-SAT)\", \"CP-SAT\",\n",
" \"Diskrete Struktur dominiert; CP-SAT verarbeitet auch nichtlineare Logik.\")\n",
"\n",
" if p.zielfunktion == \"linear\":\n",
" if p.wiederholte_laeufe or p.groesse == \"gross\":\n",
" return (\"highspy\", \"HiGHS Dual Simplex\",\n",
" \"Modell einmal aufbauen, Parameter ändern, wiederholt lösen.\")\n",
" return (\"scipy.optimize.linprog\", \"HiGHS\",\n",
" \"Kleinstes Setup, keine zusätzliche Abhängigkeit.\")\n",
"\n",
" if p.zielfunktion in (\"quadratisch\", \"konvex\"):\n",
" return (\"cvxpy\", \"Clarabel / OSQP\",\n",
" \"Konvexität wird automatisch geprüft; Portfolio-Standard.\")\n",
"\n",
" return (\"scipy.optimize.minimize\", \"SLSQP / trust-constr\",\n",
" \"Nicht konvex: nur lokales Optimum, Startpunkt variieren und vergleichen!\")\n",
"\n",
"\n",
"BEISPIELE = {\n",
" \"Vertretungsplan Schule\": Problem(True, \"linear\", groesse=\"klein\"),\n",
" \"Produktionsplanung (LP)\": Problem(False, \"linear\", groesse=\"klein\"),\n",
" \"Produktionsplanung, 50k Var.\": Problem(False, \"linear\", groesse=\"gross\"),\n",
" \"Portfolio Markowitz\": Problem(False, \"quadratisch\"),\n",
" \"Portfolio mit max. 5 Titeln\": Problem(True, \"quadratisch\"),\n",
" \"Liefertouren mit Zeitfenstern\": Problem(True, \"linear\", routing=True),\n",
" \"Entropie-Allokation (NLP)\": Problem(False, \"beliebig\"),\n",
" \"Backtest, 60x neu optimieren\": Problem(False, \"konvex\", wiederholte_laeufe=True),\n",
"}\n",
"\n",
"if __name__ == \"__main__\":\n",
" print(\"=\" * 92)\n",
" print(\" SOLVER-EMPFEHLUNG\")\n",
" print(\"=\" * 92)\n",
" for name, p in BEISPIELE.items():\n",
" lib, backend, grund = empfehle(p)\n",
" print(f\"\\n{name}\")\n",
" print(f\" -> Bibliothek: {lib}\")\n",
" print(f\" Backend: {backend}\")\n",
" print(f\" Grund: {grund}\")\n",
" print(\"\\n\" + \"=\" * 92)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
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
"## Wie die Isolation aussieht, wenn sie tragen soll\n",
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
"\n",
"`Ein_System_Vier_Ansaetze.py`\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#!/usr/bin/env python3\n",
"\n",
"# Ein_System_Vier_Ansaetze.py\n",
"\"\"\"\n",
"Kapitel Oekosystem: Dasselbe LP in vier Bibliotheken.\n",
" max 10*x1 + 15*x2 + 25*x3\n",
" u.d.N. x1 + x2 + 2*x3 <= 40\n",
" 2*x1 + 3*x2 + x3 <= 50\n",
" x >= 0\n",
"\n",
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
"Deckt scipy.optimize, highspy, CVXPY und OR-Tools/GLOP ab, mit Kreuzvergleich\n",
"am Ende.\n",
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
"\n",
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
"WICHTIG: Jeder Solver laeuft in einem EIGENEN Prozess, weil sich ortools und\n",
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
"highspy auf vielen Systemen nicht gemeinsam importieren lassen (beide bringen\n",
"eine eigene HiGHS-Kopie mit -> Symbolkonflikt).\n",
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
"\n",
"Die Isolation besorgt ein ProcessPoolExecutor. Drei Einstellungen ergeben\n",
"zusammen die Garantie:\n",
"\n",
" mp_context \"spawn\" Der Kindprozess startet mit einem FRISCHEN\n",
" Interpreter, statt den Speicher des Elternprozesses\n",
" zu erben. Was hier schon importiert ist, ist dort\n",
" nicht importiert. Mit dem Standard \"fork\" auf Linux\n",
" waere das nicht so.\n",
" max_tasks_per_child=1 Jede Aufgabe bekommt einen NEUEN Prozess. Ohne das\n",
" wuerde der Pool seinen Arbeiter wiederverwenden - und\n",
" beim zweiten Solver waere der Konflikt zurueck.\n",
" max_workers=1 Haelt die vier Laeufe nacheinander. Nicht aus\n",
" Vorsicht, sondern damit die gemessenen Zeiten\n",
" vergleichbar bleiben.\n",
"\n",
"Jeder Solver steht in einer eigenen Funktion mit LOKALEM Import. Das ist der\n",
"Unterschied zu einem Codestring, den man an 'python -c' uebergibt: Die\n",
"Funktion laesst sich einzeln aufrufen, testen und vom Editor pruefen - ein\n",
"String nicht.\n",
"\n",
"Benoetigt: scipy, highspy, cvxpy, ortools\n",
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
"\"\"\"\n",
"\n",
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
"import multiprocessing\n",
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
"import time\n",
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
"from concurrent.futures import ProcessPoolExecutor\n",
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
"\n",
"ERWARTET = 530.0 # Ergebnis der Handrechnung zum Produktionsprogramm\n",
"\n",
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
"# Die Instanz - einmal notiert, von allen vier Funktionen benutzt.\n",
"ZIEL = [10.0, 15.0, 25.0]\n",
"MATRIX = [[1, 1, 2], [2, 3, 1]]\n",
"KAPAZITAET = [40.0, 50.0]\n",
"\n",
"\n",
"def loese_mit_scipy() -> tuple[float, list[float]]:\n",
" from scipy.optimize import linprog\n",
" ergebnis = linprog(c=[-w for w in ZIEL], # linprog MINIMIERT -> negieren\n",
" A_ub=MATRIX, b_ub=KAPAZITAET,\n",
" bounds=[(0, None)] * 3, method=\"highs\")\n",
" return -ergebnis.fun, list(ergebnis.x)\n",
"\n",
"\n",
"def loese_mit_highspy() -> tuple[float, list[float]]:\n",
" import highspy\n",
" import numpy as np\n",
" h = highspy.Highs()\n",
" h.setOptionValue(\"output_flag\", False)\n",
" h.addVars(3, np.zeros(3), np.full(3, highspy.kHighsInf))\n",
" h.changeObjectiveSense(highspy.ObjSense.kMaximize)\n",
" for j, wert in enumerate(ZIEL):\n",
" h.changeColCost(j, wert)\n",
" # CSR-Format: starts[i] = Beginn von Zeile i in indices/values\n",
" h.addRows(2, np.full(2, -highspy.kHighsInf), np.array(KAPAZITAET), 6,\n",
" np.array([0, 3], dtype=np.int32),\n",
" np.array([0, 1, 2, 0, 1, 2], dtype=np.int32),\n",
" np.array([float(w) for zeile in MATRIX for w in zeile]))\n",
" h.run()\n",
" return (h.getInfo().objective_function_value,\n",
" list(h.getSolution().col_value[:3]))\n",
"\n",
"\n",
"def loese_mit_cvxpy() -> tuple[float, list[float]]:\n",
" import cvxpy as cp\n",
" import numpy as np\n",
" x = cp.Variable(3, nonneg=True)\n",
" problem = cp.Problem(cp.Maximize(np.array(ZIEL) @ x),\n",
" [np.array(MATRIX) @ x <= np.array(KAPAZITAET)])\n",
" problem.solve()\n",
" return float(problem.value), [float(v) for v in x.value]\n",
"\n",
"\n",
"def loese_mit_ortools() -> tuple[float, list[float]]:\n",
" from ortools.linear_solver import pywraplp\n",
" s = pywraplp.Solver.CreateSolver(\"GLOP\")\n",
" x = [s.NumVar(0, s.infinity(), f\"x{j+1}\") for j in range(3)]\n",
" for i, kapazitaet in enumerate(KAPAZITAET):\n",
" s.Add(sum(MATRIX[i][j] * x[j] for j in range(3)) <= kapazitaet)\n",
" s.Maximize(sum(ZIEL[j] * x[j] for j in range(3)))\n",
" s.Solve()\n",
" return s.Objective().Value(), [v.solution_value() for v in x]\n",
"\n",
"\n",
"ANSAETZE = {\n",
" \"scipy.optimize.linprog\": loese_mit_scipy,\n",
" \"highspy (natives HiGHS)\": loese_mit_highspy,\n",
" \"cvxpy\": loese_mit_cvxpy,\n",
" \"ortools / GLOP\": loese_mit_ortools,\n",
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
"}\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" print(\"=\" * 78)\n",
" print(\" EIN SYSTEM - VIER ANSAETZE (je eigener Prozess)\")\n",
" print(\"=\" * 78)\n",
" print(f\"{'Bibliothek':<26} {'Z*':>10} {'x1':>7} {'x2':>7} {'x3':>7} {'Zeit':>10}\")\n",
" print(\"-\" * 78)\n",
"\n",
" werte = []\n",
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
" # Ein Pool, vier Aufgaben, vier frische Prozesse. Der Kontext muss\n",
" # \"spawn\" sein - siehe Modulkommentar.\n",
" with ProcessPoolExecutor(\n",
" max_workers=1,\n",
" mp_context=multiprocessing.get_context(\"spawn\"),\n",
" max_tasks_per_child=1) as pool:\n",
" for name, funktion in ANSAETZE.items():\n",
" beginn = time.perf_counter()\n",
" try:\n",
" wert, x = pool.submit(funktion).result(timeout=120)\n",
" except Exception as fehler: # Bibliothek fehlt o. Ae.\n",
" print(f\"{name:<26} nicht verfuegbar: {str(fehler)[:40]}\")\n",
" continue\n",
" dauer = time.perf_counter() - beginn\n",
" werte.append(wert)\n",
" print(f\"{name:<26} {wert:>10.2f} {x[0]:>7.2f} {x[1]:>7.2f} {x[2]:>7.2f} \"\n",
" f\"{dauer:>8.2f} s\")\n",
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
"\n",
" print(\"-\" * 78)\n",
" spanne = max(werte) - min(werte)\n",
" print(f\"Spannweite zwischen den Bibliotheken: {spanne:.2e}\")\n",
" print(f\"Abweichung zur Handrechnung ({ERWARTET:.0f}): \"\n",
" f\"{abs(werte[0] - ERWARTET):.2e}\")\n",
" assert spanne < 1e-6, \"Die Bibliotheken widersprechen sich!\"\n",
" assert abs(werte[0] - ERWARTET) < 1e-6, \"Ergebnis weicht von der Handrechnung ab!\"\n",
" print(\"Alle Wege fuehren zum selben, von Hand bestaetigten Optimum.\")\n",
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
" print(\"(Die Zeiten enthalten Prozessstart und Import - sie messen NICHT die\")\n",
" print(\" reine Solverleistung. Die Uebungsaufgabe 'Laufzeitvergleich' trennt beides.)\")\n",
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
" print(\"=\" * 78)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Die Schichten im Überblick\n",
"\n",
"`Modellierungsschichten.py`\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#!/usr/bin/env python3\n",
"\n",
"# Modellierungsschichten.py\n",
"\"\"\"\n",
"Kapitel Oekosystem: Pyomo und Linopy - zwei Modellierungsschichten fuer grosse Modelle.\n",
"\n",
"Geloest wird dasselbe Produktionsproblem wie im Vierfach-Vergleich:\n",
"\n",
" max 10*x1 + 15*x2 + 25*x3\n",
" u.d.N. x1 + x2 + 2*x3 <= 40\n",
" 2*x1 + 3*x2 + x3 <= 50\n",
" x >= 0\n",
"\n",
"Handrechnung: Z* = 530 bei x = (0, 12, 14).\n",
"\n",
"Der Vergleich zeigt die beiden Denkweisen:\n",
" * Pyomo - algebraisch, indexbasiert, Industriestandard fuer Grossmodelle,\n",
" trennt Modellstruktur sauber von den Daten (AbstractModel).\n",
" * Linopy - beschriftete Arrays (xarray): eine Zeile Code erzeugt Tausende\n",
" Nebenbedingungen auf einmal, ohne Python-Schleife.\n",
"\n",
"Beide bringen KEINEN eigenen Solver mit; hier rechnet in beiden Faellen HiGHS.\n",
"\n",
"WICHTIG: ortools wird in diesem Programm bewusst NICHT importiert - es\n",
"vertraegt sich nicht mit der HiGHS-Kopie, die Pyomo und Linopy laden\n",
"(siehe die Stolperfalle im Abschnitt 'Ein System - vier Programmieransaetze').\n",
"\n",
"Benoetigt: pyomo, linopy, xarray, pandas, highspy, numpy\n",
"\"\"\"\n",
"\n",
"from __future__ import annotations\n",
"\n",
"import time\n",
"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import pyomo.environ as pyo\n",
"import xarray as xr\n",
"import linopy\n",
"\n",
"ERWARTET = 530.0 # Ergebnis der Handrechnung\n",
"\n",
"PRODUKTE = [\"Standard\", \"Komfort\", \"Premium\"]\n",
"RESSOURCEN = [\"Material\", \"Montage\"]\n",
"\n",
"DECKUNGSBEITRAG = np.array([10.0, 15.0, 25.0])\n",
"VERBRAUCH = np.array([[1.0, 1.0, 2.0], # Material je Produkt\n",
" [2.0, 3.0, 1.0]]) # Montage je Produkt\n",
"VORRAT = np.array([40.0, 50.0])\n",
"\n",
"\n",
"# --- Pyomo: algebraisch und indexbasiert ------------------------------------\n",
"\n",
"def loese_mit_pyomo() -> tuple[float, list[float], float]:\n",
" \"\"\"Pyomo denkt in Mengen und Indizes, wie ein Mathematiker es aufschreibt.\n",
"\n",
" `Constraint(RESSOURCEN, rule=...)` erzeugt fuer JEDES Element der Menge\n",
" eine Nebenbedingung - das ist das 'fuer alle i' der Formelsprache,\n",
" unmittelbar in Code uebersetzt.\n",
" \"\"\"\n",
" t0 = time.perf_counter()\n",
"\n",
" modell = pyo.ConcreteModel(name=\"Produktionsprogramm\")\n",
" modell.P = pyo.Set(initialize=PRODUKTE)\n",
" modell.R = pyo.Set(initialize=RESSOURCEN)\n",
"\n",
" modell.db = pyo.Param(modell.P, initialize=dict(zip(PRODUKTE, DECKUNGSBEITRAG)))\n",
" modell.a = pyo.Param(modell.R, modell.P, initialize={\n",
" (r, p): VERBRAUCH[i, j]\n",
" for i, r in enumerate(RESSOURCEN) for j, p in enumerate(PRODUKTE)})\n",
" modell.vorrat = pyo.Param(modell.R, initialize=dict(zip(RESSOURCEN, VORRAT)))\n",
"\n",
" modell.x = pyo.Var(modell.P, domain=pyo.NonNegativeReals)\n",
"\n",
" modell.ziel = pyo.Objective(\n",
" expr=sum(modell.db[p] * modell.x[p] for p in modell.P),\n",
" sense=pyo.maximize)\n",
"\n",
" def kapazitaet(m, r):\n",
" return sum(m.a[r, p] * m.x[p] for p in m.P) <= m.vorrat[r]\n",
"\n",
" modell.kapazitaet = pyo.Constraint(modell.R, rule=kapazitaet)\n",
"\n",
" ergebnis = pyo.SolverFactory(\"appsi_highs\").solve(modell)\n",
" dauer = time.perf_counter() - t0\n",
"\n",
" status = ergebnis.solver.termination_condition\n",
" if status != pyo.TerminationCondition.optimal:\n",
" raise RuntimeError(f\"Pyomo meldet Status: {status}\")\n",
"\n",
" return (float(pyo.value(modell.ziel)),\n",
" [float(pyo.value(modell.x[p])) for p in PRODUKTE],\n",
" dauer)\n",
"\n",
"\n",
"# --- Linopy: beschriftete Arrays --------------------------------------------\n",
"\n",
"def loese_mit_linopy() -> tuple[float, list[float], float]:\n",
" \"\"\"Linopy denkt in beschrifteten Arrays (xarray).\n",
"\n",
" Der entscheidende Unterschied: `(verbrauch * x).sum(\"produkt\") <= vorrat`\n",
" ist EINE Zeile und erzeugt so viele Nebenbedingungen, wie die Dimension\n",
" 'ressource' Eintraege hat. Bei 2 Ressourcen faellt das nicht auf, bei\n",
" 200 000 schon - dort entstehen sie als Matrixoperation statt in einer\n",
" Python-Schleife.\n",
" \"\"\"\n",
" t0 = time.perf_counter()\n",
"\n",
" # Benannte Indizes statt blosser Listen: Dadurch heissen die Achsen\n",
" # 'produkt' und 'ressource', und xarray fuehrt sie beim Rechnen von allein\n",
" # richtig zusammen. Ohne Namen vergibt linopy 'dim_0', und man muss\n",
" # spaeter umbenennen - eine haeufige Stolperstelle.\n",
" produkt = pd.Index(PRODUKTE, name=\"produkt\")\n",
" ressource = pd.Index(RESSOURCEN, name=\"ressource\")\n",
"\n",
" modell = linopy.Model()\n",
" modell.add_variables(lower=0, coords=[produkt], name=\"menge\")\n",
" x = modell.variables[\"menge\"]\n",
"\n",
" db = xr.DataArray(DECKUNGSBEITRAG, coords=[produkt])\n",
" verbrauch = xr.DataArray(VERBRAUCH, coords=[ressource, produkt])\n",
" vorrat = xr.DataArray(VORRAT, coords=[ressource])\n",
"\n",
" # EINE Zeile - sie erzeugt so viele Nebenbedingungen, wie die Achse\n",
" # 'ressource' Eintraege hat. Genau das ist der Punkt.\n",
" modell.add_constraints((verbrauch * x).sum(\"produkt\") <= vorrat,\n",
" name=\"kapazitaet\")\n",
" modell.add_objective((db * x).sum(), sense=\"max\")\n",
"\n",
" modell.solve(solver_name=\"highs\", output_flag=False)\n",
" dauer = time.perf_counter() - t0\n",
"\n",
" if modell.termination_condition != \"optimal\":\n",
" raise RuntimeError(f\"Linopy meldet Status: {modell.termination_condition}\")\n",
"\n",
" loesung = modell.variables[\"menge\"].solution.to_series()\n",
" return (float(modell.objective.value),\n",
" [float(loesung[p]) for p in PRODUKTE],\n",
" dauer)\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" print(\"=\" * 80)\n",
" print(\" MODELLIERUNGSSCHICHTEN FUER GROSSE MODELLE\")\n",
" print(\"=\" * 80)\n",
" print(f\"{'Schicht':<14} {'Z*':>10} {'Standard':>10} {'Komfort':>10} \"\n",
" f\"{'Premium':>10} {'Zeit':>10}\")\n",
" print(\"-\" * 80)\n",
"\n",
" ergebnisse = []\n",
" for name, loeser in [(\"Pyomo\", loese_mit_pyomo), (\"Linopy\", loese_mit_linopy)]:\n",
" ziel, mengen, dauer = loeser()\n",
" ergebnisse.append(ziel)\n",
" print(f\"{name:<14} {ziel:>10.2f} {mengen[0]:>10.2f} {mengen[1]:>10.2f} \"\n",
" f\"{mengen[2]:>10.2f} {dauer:>8.2f} s\")\n",
"\n",
" print(\"-\" * 80)\n",
" for ziel in ergebnisse:\n",
" assert abs(ziel - ERWARTET) < 1e-6, \\\n",
" f\"Abweichung von der Handrechnung: {ziel} statt {ERWARTET}\"\n",
" print(f\"Beide stimmen mit der Handrechnung ueberein (Z* = {ERWARTET:.0f}).\")\n",
" print()\n",
" print(\"Wann welche Schicht?\")\n",
" print(\" Pyomo -> wenn Modellstruktur und Daten getrennt bleiben sollen,\")\n",
" print(\" wenn nichtlineare Terme oder MINLP dazukommen koennen,\")\n",
" print(\" wenn spaeter ein kommerzieller Solver angebunden wird.\")\n",
" print(\" Linopy -> wenn die Daten ohnehin als beschriftete Arrays vorliegen\")\n",
" print(\" (Energiesystem-, Netz- und Zeitreihenmodelle) und das\")\n",
" print(\" Modell zehntausende gleichartige Nebenbedingungen hat.\")\n",
" print(\"=\" * 80)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Vier Stufen an einem Transportproblem\n",
"\n",
"`Vektorisierte_Modellgenerierung.py`\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#!/usr/bin/env python3\n",
"\n",
"# Vektorisierte_Modellgenerierung.py\n",
"\"\"\"\n",
"Kapitel Oekosystem: Warum der Solver oft gar nicht der Engpass ist.\n",
"\n",
"In realen Projekten geht ein grosser Teil der Rechenzeit nicht ins Loesen,\n",
"sondern ins AUFBAUEN des Modells. Dieses Programm misst das an einem\n",
"Transportproblem wachsender Groesse in vier Stufen:\n",
"\n",
" A Modellierungsschicht, Nebenbedingung fuer Nebenbedingung (OR-Tools)\n",
" B Matrix direkt, aber mit Python-Schleifen ueber die Eintraege (COO)\n",
" C Matrix vektorisiert ueber Kronecker-Produkte (NumPy/SciPy)\n",
" D Daten kommen als lange Tabelle, aufbereitet mit Polars\n",
"\n",
"Alle Varianten loesen dasselbe Problem und muessen denselben Zielwert\n",
"liefern - das wird am Ende geprueft.\n",
"\n",
"Benoetigt: numpy, scipy, ortools; Variante D zusaetzlich polars (optional).\n",
"\"\"\"\n",
"\n",
"from __future__ import annotations\n",
"\n",
"import importlib.util\n",
"import time\n",
"\n",
"import numpy as np\n",
"import scipy.sparse as sp\n",
"from ortools.linear_solver import pywraplp\n",
"from scipy.optimize import linprog\n",
"\n",
"HAT_POLARS = importlib.util.find_spec(\"polars\") is not None\n",
"\n",
"\n",
"def erzeuge_daten(m: int, n: int, saat: int = 3\n",
" ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:\n",
" \"\"\"Transportproblem: m Werke, n Kunden.\n",
"\n",
" Liefert (kosten[m, n], angebot[m], bedarf[n]). Das Gesamtangebot liegt\n",
" 20 % ueber dem Gesamtbedarf, damit das Modell sicher loesbar ist.\n",
" \"\"\"\n",
" rng = np.random.default_rng(saat)\n",
" kosten = rng.uniform(1.0, 20.0, size=(m, n))\n",
" bedarf = rng.uniform(10.0, 50.0, size=n)\n",
" angebot = np.full(m, 1.2 * bedarf.sum() / m)\n",
" return kosten, angebot, bedarf\n",
"\n",
"\n",
"# --- Variante A: Modellierungsschicht, Bedingung fuer Bedingung -------------\n",
"\n",
"def loese_mit_modellierungsschicht(kosten: np.ndarray, angebot: np.ndarray,\n",
" bedarf: np.ndarray) -> tuple[float, float, float]:\n",
" \"\"\"So schreibt man ein Transportproblem zuerst hin - gut lesbar, nah an\n",
" der mathematischen Formulierung, jede Nebenbedingung ein eigener Aufruf.\n",
"\n",
" Jedes `s.Add(sum(...))` baut in Python einen Ausdrucksbaum aus m bzw. n\n",
" Termen auf und uebergibt ihn einzeln an die C++-Schicht. Das ist der\n",
" Preis der Bequemlichkeit - und er waechst linear mit der Modellgroesse.\n",
" \"\"\"\n",
" m, n = kosten.shape\n",
"\n",
" t0 = time.perf_counter()\n",
" s = pywraplp.Solver.CreateSolver(\"GLOP\")\n",
" x = [[s.NumVar(0, s.infinity(), f\"x_{i}_{j}\") for j in range(n)]\n",
" for i in range(m)]\n",
" for i in range(m):\n",
" s.Add(sum(x[i][j] for j in range(n)) <= angebot[i])\n",
" for j in range(n):\n",
" s.Add(sum(x[i][j] for i in range(m)) >= bedarf[j])\n",
" s.Minimize(sum(kosten[i][j] * x[i][j] for i in range(m) for j in range(n)))\n",
" t_aufbau = time.perf_counter() - t0\n",
"\n",
" t0 = time.perf_counter()\n",
" status = s.Solve()\n",
" t_loesen = time.perf_counter() - t0\n",
" if status != pywraplp.Solver.OPTIMAL:\n",
" raise RuntimeError(f\"Solver-Status: {status}\")\n",
" return t_aufbau, t_loesen, s.Objective().Value()\n",
"\n",
"\n",
"# --- Varianten B bis D: Matrix selbst bauen, dann SciPy/HiGHS ---------------\n",
"\n",
"def baue_mit_schleifen(kosten: np.ndarray, angebot: np.ndarray,\n",
" bedarf: np.ndarray):\n",
" \"\"\"Die Nebenbedingungsmatrix als COO-Tripel (Zeile, Spalte, Wert), erzeugt\n",
" in verschachtelten Python-Schleifen.\n",
"\n",
" Schon deutlich naeher am Blech als Variante A - es entsteht kein\n",
" Ausdrucksbaum mehr. Die Schleife selbst bleibt aber Python.\n",
" \"\"\"\n",
" m, n = kosten.shape\n",
" zeilen: list[int] = []\n",
" spalten: list[int] = []\n",
" werte: list[float] = []\n",
" rechte_seite: list[float] = []\n",
"\n",
" for i in range(m): # Angebot je Werk\n",
" for j in range(n):\n",
" zeilen.append(i)\n",
" spalten.append(i * n + j)\n",
" werte.append(1.0)\n",
" rechte_seite.append(float(angebot[i]))\n",
"\n",
" for j in range(n): # Bedarf je Kunde, als -x <= -bedarf\n",
" for i in range(m):\n",
" zeilen.append(m + j)\n",
" spalten.append(i * n + j)\n",
" werte.append(-1.0)\n",
" rechte_seite.append(-float(bedarf[j]))\n",
"\n",
" A_ub = sp.csr_matrix((werte, (zeilen, spalten)), shape=(m + n, m * n))\n",
" return A_ub, np.array(rechte_seite), kosten.ravel()\n",
"\n",
"\n",
"def baue_vektorisiert(kosten: np.ndarray, angebot: np.ndarray,\n",
" bedarf: np.ndarray):\n",
" \"\"\"Dieselbe Matrix ohne eine einzige Schleife - ueber Kronecker-Produkte.\n",
"\n",
" Die Angebotsmatrix ist kron(I_m, 1_n^T): je Werk eine Zeile mit Einsen\n",
" an genau den n Spalten dieses Werks. Die Bedarfsmatrix ist\n",
" kron(1_m^T, I_n). Beide entstehen in je einem Aufruf und sind sofort\n",
" duennbesetzt.\n",
" \"\"\"\n",
" m, n = kosten.shape\n",
" angebots_matrix = sp.kron(sp.identity(m, format=\"csr\"), np.ones((1, n)))\n",
" bedarfs_matrix = sp.kron(np.ones((1, m)), sp.identity(n, format=\"csr\"))\n",
"\n",
" A_ub = sp.vstack([angebots_matrix, -bedarfs_matrix], format=\"csr\")\n",
" b_ub = np.concatenate([angebot, -bedarf])\n",
" return A_ub, b_ub, kosten.ravel()\n",
"\n",
"\n",
"def baue_mit_polars(kosten: np.ndarray, angebot: np.ndarray, bedarf: np.ndarray):\n",
" \"\"\"Der realistische Fall: Die Kosten kommen als LANGE Tabelle\n",
" (werk, kunde, kosten) aus Datenbank, Data Lake oder CSV-Datei.\n",
"\n",
" Polars berechnet den Spaltenindex jeder Variablen in einem einzigen\n",
" Spaltenausdruck - ohne Python-Schleife ueber die Zeilen. Genau so baut man\n",
" Modelle aus Millionen Tabellenzeilen.\n",
" \"\"\"\n",
" import polars as pl\n",
"\n",
" m, n = kosten.shape\n",
" tabelle = pl.DataFrame({\n",
" \"werk\": np.repeat(np.arange(m), n),\n",
" \"kunde\": np.tile(np.arange(n), m),\n",
" \"kosten\": kosten.ravel(),\n",
" }).with_columns(\n",
" (pl.col(\"werk\") * n + pl.col(\"kunde\")).alias(\"var_index\")\n",
" )\n",
"\n",
" var_index = tabelle[\"var_index\"].to_numpy()\n",
" werk = tabelle[\"werk\"].to_numpy()\n",
" kunde = tabelle[\"kunde\"].to_numpy()\n",
" eins = np.ones(var_index.size)\n",
"\n",
" angebots_matrix = sp.csr_matrix((eins, (werk, var_index)), shape=(m, m * n))\n",
" bedarfs_matrix = sp.csr_matrix((eins, (kunde, var_index)), shape=(n, m * n))\n",
"\n",
" A_ub = sp.vstack([angebots_matrix, -bedarfs_matrix], format=\"csr\")\n",
" b_ub = np.concatenate([angebot, -bedarf])\n",
" return A_ub, b_ub, tabelle[\"kosten\"].to_numpy()\n",
"\n",
"\n",
"def messe_matrixvariante(bauer, kosten, angebot, bedarf\n",
" ) -> tuple[float, float, float]:\n",
" \"\"\"Liefert (Aufbauzeit, Loesezeit, Zielwert) fuer die Varianten B bis D.\"\"\"\n",
" t0 = time.perf_counter()\n",
" A_ub, b_ub, c = bauer(kosten, angebot, bedarf)\n",
" t_aufbau = time.perf_counter() - t0\n",
"\n",
" t0 = time.perf_counter()\n",
" ergebnis = linprog(c, A_ub=A_ub, b_ub=b_ub, bounds=(0, None), method=\"highs\")\n",
" t_loesen = time.perf_counter() - t0\n",
"\n",
" if not ergebnis.success:\n",
" raise RuntimeError(f\"Solver-Status: {ergebnis.message}\")\n",
" return t_aufbau, t_loesen, float(ergebnis.fun)\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" print(\"=\" * 88)\n",
" print(\" MODELLAUFBAU: WO DIE ZEIT WIRKLICH HINGEHT\")\n",
" print(\"=\" * 88)\n",
" print(\"Transportproblem mit m Werken und n Kunden -> m*n Variablen.\\n\")\n",
"\n",
" varianten: list[tuple[str, object]] = [\n",
" (\"A: OR-Tools, Add() je NB\", None), # Sonderfall, eigener Messpfad\n",
" (\"B: COO in Schleifen\", baue_mit_schleifen),\n",
" (\"C: NumPy vektorisiert\", baue_vektorisiert),\n",
" ]\n",
" if HAT_POLARS:\n",
" varianten.append((\"D: Polars-Tabelle\", baue_mit_polars))\n",
" else:\n",
" print(\"Hinweis: polars nicht installiert - Variante D wird uebersprungen.\\n\")\n",
"\n",
" # Aufwaermlauf: Der erste Aufruf bezahlt Importe und einmalige\n",
" # Initialisierungen. Wer den mitmisst, vergleicht Startkosten statt\n",
" # Rechenarbeit - ein klassischer Benchmark-Fehler.\n",
" aufwaerm = erzeuge_daten(10, 10)\n",
" loese_mit_modellierungsschicht(*aufwaerm)\n",
" for _, bauer in varianten[1:]:\n",
" bauer(*aufwaerm)\n",
"\n",
" print(f\"{'Groesse':<26} {'Variante':<26} {'Aufbau':>9} {'Loesen':>9} \"\n",
" f\"{'Aufbauanteil':>13}\")\n",
" print(\"-\" * 88)\n",
"\n",
" for m, n in [(40, 40), (120, 120), (250, 250)]:\n",
" kosten, angebot, bedarf = erzeuge_daten(m, n)\n",
" zielwerte = []\n",
" for nummer, (name, bauer) in enumerate(varianten):\n",
" if bauer is None:\n",
" t_aufbau, t_loesen, ziel = loese_mit_modellierungsschicht(\n",
" kosten, angebot, bedarf)\n",
" else:\n",
" t_aufbau, t_loesen, ziel = messe_matrixvariante(\n",
" bauer, kosten, angebot, bedarf)\n",
" zielwerte.append(ziel)\n",
" anteil = 100.0 * t_aufbau / (t_aufbau + t_loesen)\n",
" groesse = f\"{m}x{n} = {m*n:,} Variablen\" if nummer == 0 else \"\"\n",
" print(f\"{groesse:<26} {name:<26} {t_aufbau:>8.3f}s {t_loesen:>8.3f}s \"\n",
" f\"{anteil:>12.0f} %\")\n",
"\n",
" # Alle Varianten muessen dasselbe Problem beschreiben.\n",
" spanne = max(zielwerte) - min(zielwerte)\n",
" assert spanne < 1e-6 * max(abs(z) for z in zielwerte), \\\n",
" f\"Varianten widersprechen sich: {zielwerte}\"\n",
" print(f\"{'':<26} {'-> Zielwert (alle gleich)':<26} {zielwerte[0]:>9.2f}\"\n",
" f\" Spanne {spanne:.1e}\")\n",
" print(\"-\" * 88)\n",
"\n",
" print(\"\\nZwei Lehren aus der Tabelle:\")\n",
" print(\"1. Bei Variante A geht mehr Zeit in den AUFBAU als ins Loesen. Wer hier\")\n",
" print(\" einen schnelleren Solver kauft, beschleunigt den kleineren Teil.\")\n",
" print(\"2. Zwischen B und C liegt keine andere Mathematik, nur eine andere\")\n",
" print(\" Schreibweise derselben Matrix - Schleife gegen Kronecker-Produkt.\")\n",
" print(\"\\nDie Lesbarkeit von Variante A ist trotzdem viel wert: Fangen Sie dort an,\")\n",
" print(\"und vektorisieren Sie erst, wenn die Messung es verlangt.\")\n",
" print(\"=\" * 88)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}