Compare commits
116 changed files with 13723 additions and 27739 deletions
|
|
@ -1,28 +0,0 @@
|
||||||
# Was NICHT in den Build-Kontext gehoert. Ohne diese Datei schickt Docker das
|
|
||||||
# ganze Verzeichnis an den Daemon - mitsamt Website (rund 50 MB), PDF und
|
|
||||||
# Notebooks, die im Image nichts zu suchen haben.
|
|
||||||
|
|
||||||
# Erzeugte Ausgaben
|
|
||||||
OR_HTML_04/
|
|
||||||
Notebooks_04/
|
|
||||||
*.pdf
|
|
||||||
Operations_Research_mit_Python_Version_04.md
|
|
||||||
|
|
||||||
# Quellen des Buchsatzes - das Image fuehrt die Programme aus, es baut das
|
|
||||||
# Buch nicht. Wer das PDF bauen will, braucht pandoc, xelatex und inkscape
|
|
||||||
# und ist mit einer lokalen Installation besser bedient.
|
|
||||||
bilder_04/
|
|
||||||
pandoc/
|
|
||||||
pandoc-defaults-*.yaml
|
|
||||||
Kritik_und_Verbesserungsvorschlaege/
|
|
||||||
PLAN.md
|
|
||||||
PROGRESS.md
|
|
||||||
CLAUDE.md
|
|
||||||
|
|
||||||
# Von Laeufen erzeugte Dateien
|
|
||||||
**/__pycache__/
|
|
||||||
**/output/
|
|
||||||
*.xlsx
|
|
||||||
.git/
|
|
||||||
.venv/
|
|
||||||
venv/
|
|
||||||
24
CLAUDE.md
24
CLAUDE.md
|
|
@ -23,7 +23,7 @@ Dieses Verzeichnis ist die Wurzel; alle Skripte leiten ihre Pfade daraus ab
|
||||||
(`BASIS = dirname(dirname(__file__))` bzw. `dirname(HIER)`).
|
(`BASIS = dirname(dirname(__file__))` bzw. `dirname(HIER)`).
|
||||||
|
|
||||||
```
|
```
|
||||||
Operations_Research_mit_Python_Version_04/ Quelle: 36 Kapiteldateien + Build-Skripte
|
Operations_Research_mit_Python_Version_04/ Quelle: 31 Kapiteldateien + Build-Skripte
|
||||||
bilder_04/ Quelle: Diagramme + erzeuge_*.py-Generatoren
|
bilder_04/ Quelle: Diagramme + erzeuge_*.py-Generatoren
|
||||||
Operations_Research_mit_Python_Version_04.md generiert: Gesamtdokument
|
Operations_Research_mit_Python_Version_04.md generiert: Gesamtdokument
|
||||||
Operations_Research_mit_Python_Version_04.pdf generiert: PDF (xelatex)
|
Operations_Research_mit_Python_Version_04.pdf generiert: PDF (xelatex)
|
||||||
|
|
@ -33,7 +33,6 @@ Notebooks_04/ generiert: ein .ipynb je Kapitel
|
||||||
Kritik_und_Verbesserungsvorschlaege/ Quelle: Rezension, Verbesserungsvorschläge,
|
Kritik_und_Verbesserungsvorschlaege/ Quelle: Rezension, Verbesserungsvorschläge,
|
||||||
NEUER_TITEL.md (Vorlage des Titelblatts)
|
NEUER_TITEL.md (Vorlage des Titelblatts)
|
||||||
pyproject.toml Quelle: Abhängigkeiten in Gruppen
|
pyproject.toml Quelle: Abhängigkeiten in Gruppen
|
||||||
Dockerfile, .dockerignore Quelle: Kurs-Image (nur Programme)
|
|
||||||
pandoc-defaults-basis.yaml, pandoc/, pandoc-defaults-buch.yaml PDF-Konfiguration
|
pandoc-defaults-basis.yaml, pandoc/, pandoc-defaults-buch.yaml PDF-Konfiguration
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -67,24 +66,15 @@ sonst fälschlich als sichtbaren Umbruch dar.
|
||||||
## Die wichtigsten Konventionen
|
## Die wichtigsten Konventionen
|
||||||
|
|
||||||
**Keine abgeleiteten Zahlen im Quelltext.** Das ist die Regel, an der dieses Projekt am
|
**Keine abgeleiteten Zahlen im Quelltext.** Das ist die Regel, an der dieses Projekt am
|
||||||
häufigsten gescheitert ist — **sieben** Mal an verschiedenen Stellen verletzt gefunden
|
häufigsten gescheitert ist — sie wurde vier Mal an verschiedenen Stellen verletzt gefunden
|
||||||
(Anhang A, 66 Programm-Docstrings, die Übersichtstabellen der Anhänge, `CLAUDE.md` selbst,
|
(Anhang A, 66 Programm-Docstrings, die Übersichtstabellen der Anhänge, `CLAUDE.md` selbst).
|
||||||
eine Tabellenzelle, die 98 Lösungsmarken des Anhangs, neun Denkfehler-Verweise).
|
Konkret:
|
||||||
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
|
* Überschriften: `# Kapitel: <Titel> {#kap:<label>}`, `## Titel {#sec:<label>}` — die Nummer
|
||||||
vergibt der Build (`resolve_numbering()`, `nummeriere_abschnitte()`).
|
vergibt der Build (`resolve_numbering()`, `nummeriere_abschnitte()`).
|
||||||
* Aufgaben, Handrechnungen, Abbildungen, Micro-Quiz: `**Aufgabe ⭐ — Titel.**`,
|
* Aufgaben, Handrechnungen, Abbildungen: `**Aufgabe ⭐ — Titel.**`,
|
||||||
`> **✏️ Handrechnung: Titel**`, ``,
|
`> **✏️ Handrechnung: Titel**`, `` — Nummern von
|
||||||
`> **❓ Micro-Quiz: Titel**` — Nummern von `nummeriere_marken()`.
|
`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.
|
* Im Fließtext `{ref:<label>}`, nie eine Literalzahl.
|
||||||
* In Code-Kommentaren und Docstrings der Kapitel**name** („Kapitel Metaheuristiken:"), nie
|
* In Code-Kommentaren und Docstrings der Kapitel**name** („Kapitel Metaheuristiken:"), nie
|
||||||
die Nummer. Auch nicht im Dateinamen.
|
die Nummer. Auch nicht im Dateinamen.
|
||||||
|
|
|
||||||
79
Dockerfile
79
Dockerfile
|
|
@ -1,79 +0,0 @@
|
||||||
# Kurs-Image zu "Optimierte Entscheidungsfindung mit Python".
|
|
||||||
#
|
|
||||||
# Was es ist: eine reproduzierbare Umgebung, in der alle Beispielprogramme
|
|
||||||
# laufen - ohne dass jemand Solver, BLAS-Bibliotheken und Python-Versionen von
|
|
||||||
# Hand zusammensuchen muss.
|
|
||||||
#
|
|
||||||
# Was es NICHT ist: eine Bauumgebung fuer das Buch. Das PDF braucht pandoc,
|
|
||||||
# xelatex und inkscape, zusammen ueber ein Gigabyte. Wer das Buch selbst baut,
|
|
||||||
# ist mit einer lokalen Installation besser bedient (siehe README.md).
|
|
||||||
#
|
|
||||||
# docker build -t or-mit-python .
|
|
||||||
# docker run --rm or-mit-python Installationstest.py
|
|
||||||
# docker run --rm -it or-mit-python # Python-Eingabe
|
|
||||||
# docker run --rm -v "$PWD/ausgabe:/buch/output" or-mit-python Excel_Bruecke.py
|
|
||||||
#
|
|
||||||
# Zwei Stufen: Die erste baut die Abhaengigkeiten in eine virtuelle Umgebung
|
|
||||||
# (und braucht dafuer Compiler), die zweite kopiert nur das Ergebnis. Das
|
|
||||||
# spart im fertigen Image die gesamte Bauumgebung.
|
|
||||||
|
|
||||||
# --- Stufe 1: Abhaengigkeiten uebersetzen und einsammeln ---------------------
|
|
||||||
FROM python:3.12-slim AS bau
|
|
||||||
|
|
||||||
# build-essential wird von Paketen gebraucht, die kein passendes Wheel
|
|
||||||
# mitbringen. Bleibt in dieser Stufe und landet nie im fertigen Image.
|
|
||||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
|
||||||
build-essential \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
ENV PIP_NO_CACHE_DIR=1 \
|
|
||||||
PIP_DISABLE_PIP_VERSION_CHECK=1
|
|
||||||
RUN python -m venv /opt/venv
|
|
||||||
ENV PATH="/opt/venv/bin:$PATH"
|
|
||||||
|
|
||||||
# Nur die Abhaengigkeitsbeschreibung kopieren, nicht den Quelltext: So bleibt
|
|
||||||
# diese Schicht im Cache, solange sich die Abhaengigkeiten nicht aendern.
|
|
||||||
WORKDIR /buch
|
|
||||||
COPY pyproject.toml README.md ./
|
|
||||||
|
|
||||||
# Die Gruppen aus pyproject.toml. 'figures' fehlt bewusst - es dient dem
|
|
||||||
# Neuerzeugen der Diagramme, wofuer zusaetzlich Graphviz noetig waere.
|
|
||||||
#
|
|
||||||
# ACHTUNG, gewollt: Das Image enthaelt ortools UND highspy, obwohl beide eine
|
|
||||||
# eigene HiGHS-Kopie mitbringen und sich nicht gemeinsam importieren lassen.
|
|
||||||
# Der Konflikt wird nicht durch Weglassen geloest, sondern zur Laufzeit durch
|
|
||||||
# getrennte Prozesse (Kapitel Oekosystem). Ein Image, das nur eines von beiden
|
|
||||||
# enthaelt, koennte die Haelfte der Beispiele nicht ausfuehren.
|
|
||||||
RUN pip install ".[finance,large-scale,api,dev]"
|
|
||||||
|
|
||||||
# --- Stufe 2: das eigentliche Image -----------------------------------------
|
|
||||||
FROM python:3.12-slim AS laufzeit
|
|
||||||
|
|
||||||
# libgomp1 wird von OR-Tools und HiGHS zur Laufzeit gebraucht (OpenMP) und ist
|
|
||||||
# im slim-Image nicht enthalten. Ohne es scheitert der erste Solveraufruf mit
|
|
||||||
# einem 'libgomp.so.1: cannot open shared object file'.
|
|
||||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
|
||||||
libgomp1 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY --from=bau /opt/venv /opt/venv
|
|
||||||
ENV PATH="/opt/venv/bin:$PATH" \
|
|
||||||
PYTHONUNBUFFERED=1 \
|
|
||||||
MPLBACKEND=Agg
|
|
||||||
|
|
||||||
# Kein root. Die Programme brauchen keine Rechte, und ein Kurs-Image, das als
|
|
||||||
# root laeuft, ist eine schlechte Angewohnheit, die man weitergibt.
|
|
||||||
RUN useradd --create-home --uid 1000 kurs
|
|
||||||
WORKDIR /buch
|
|
||||||
COPY --chown=kurs:kurs Operations_Research_mit_Python_Version_04_Programme/ ./
|
|
||||||
|
|
||||||
# Verzeichnis fuer erzeugte Dateien - als Volume einhaengbar, damit
|
|
||||||
# Excel-Ausgaben und Diagramme den Container ueberleben. Angelegt wird es
|
|
||||||
# VOR dem Benutzerwechsel, denn /buch gehoert root.
|
|
||||||
RUN mkdir -p /buch/output && chown kurs:kurs /buch/output
|
|
||||||
USER kurs
|
|
||||||
|
|
||||||
# Ohne Argument eine Python-Eingabeaufforderung, mit Argument das genannte
|
|
||||||
# Programm: 'docker run --rm or-mit-python Rucksack.py'
|
|
||||||
ENTRYPOINT ["python"]
|
|
||||||
CMD []
|
|
||||||
|
|
@ -496,218 +496,6 @@
|
||||||
" loese_jobshop()"
|
" loese_jobshop()"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"## Parallele Suche: was `num_workers` wirklich bewirkt\n",
|
|
||||||
"\n",
|
|
||||||
"`Parallele_Suche.py`\n"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": null,
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"#!/usr/bin/env python3\n",
|
|
||||||
"\n",
|
|
||||||
"# Parallele_Suche.py\n",
|
|
||||||
"\"\"\"\n",
|
|
||||||
"Kapitel CP-SAT: Was mehrere Arbeiter wirklich bringen - und was sie kosten.\n",
|
|
||||||
"\n",
|
|
||||||
"Das Buch setzt an mehreren Stellen 'num_workers = 1', damit die abgedruckte\n",
|
|
||||||
"Ausgabe reproduzierbar ist. Behauptet wird dabei zweierlei: dass parallele\n",
|
|
||||||
"Suche schneller ist, und dass sie von Lauf zu Lauf verschiedene, gleich gute\n",
|
|
||||||
"Loesungen findet. Beides wird hier gemessen statt geglaubt.\n",
|
|
||||||
"\n",
|
|
||||||
"Gerechnet wird auf einem Job-Shop wie in JobShop_Intervalle.py, nur gross\n",
|
|
||||||
"genug, dass die Suche wirklich arbeitet: 12 Auftraege auf 10 Maschinen,\n",
|
|
||||||
"jeder Auftrag besucht jede Maschine.\n",
|
|
||||||
"\n",
|
|
||||||
"Zwei Messungen:\n",
|
|
||||||
" 1. Dieselbe Aufgabe mit 1, 2, 4 und 8 Arbeitern - Laufzeit und Ergebnis.\n",
|
|
||||||
" 2. Dieselbe Konfiguration mehrfach, bei FESTEM random_seed - wie oft kommt\n",
|
|
||||||
" derselbe Plan heraus?\n",
|
|
||||||
"\n",
|
|
||||||
"Die zweite ist die wichtigere: Sie entscheidet, wie man Tests schreibt.\n",
|
|
||||||
"\n",
|
|
||||||
"Achtung, Laufzeiten sind hardwareabhaengig. Die ZAHL der verschiedenen\n",
|
|
||||||
"Plaene ist es auch - und genau das ist die Aussage.\n",
|
|
||||||
"\n",
|
|
||||||
"Benoetigt: numpy, ortools\n",
|
|
||||||
"\"\"\"\n",
|
|
||||||
"\n",
|
|
||||||
"from __future__ import annotations\n",
|
|
||||||
"\n",
|
|
||||||
"import collections\n",
|
|
||||||
"import time\n",
|
|
||||||
"\n",
|
|
||||||
"import numpy as np\n",
|
|
||||||
"from ortools.sat.python import cp_model\n",
|
|
||||||
"\n",
|
|
||||||
"AUFTRAEGE = 12\n",
|
|
||||||
"MASCHINEN = 10\n",
|
|
||||||
"INSTANZ_SEED = 20260908\n",
|
|
||||||
"SOLVER_SEED = 1 # bleibt ueber ALLE Laeufe gleich - das ist der Punkt\n",
|
|
||||||
"ARBEITERZAHLEN = (1, 2, 4, 8)\n",
|
|
||||||
"WIEDERHOLUNGEN = 4\n",
|
|
||||||
"ZEITLIMIT = 120.0\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"def baue_instanz() -> list[list[tuple[int, int]]]:\n",
|
|
||||||
" \"\"\"Klassischer Job-Shop: Jeder Auftrag besucht jede Maschine genau einmal,\n",
|
|
||||||
" in einer eigenen zufaelligen Reihenfolge.\"\"\"\n",
|
|
||||||
" rng = np.random.default_rng(INSTANZ_SEED)\n",
|
|
||||||
" return [[(int(m), int(rng.integers(2, 20))) for m in rng.permutation(MASCHINEN)]\n",
|
|
||||||
" for _ in range(AUFTRAEGE)]\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"def loese(auftraege, arbeiter: int):\n",
|
|
||||||
" \"\"\"Minimiert den Makespan. Gibt Status, Zielwert, Laufzeit und den Plan\n",
|
|
||||||
" zurueck - den Plan als Tupel aller Startzeiten, damit sich zwei Laeufe\n",
|
|
||||||
" vergleichen lassen.\"\"\"\n",
|
|
||||||
" horizont = sum(dauer for auftrag in auftraege for _, dauer in auftrag)\n",
|
|
||||||
" modell = cp_model.CpModel()\n",
|
|
||||||
" Gang = collections.namedtuple(\"Gang\", \"start ende intervall\")\n",
|
|
||||||
" plaene: dict[tuple[int, int], Gang] = {}\n",
|
|
||||||
" je_maschine = collections.defaultdict(list)\n",
|
|
||||||
"\n",
|
|
||||||
" for a, auftrag in enumerate(auftraege):\n",
|
|
||||||
" for g, (maschine, dauer) in enumerate(auftrag):\n",
|
|
||||||
" start = modell.NewIntVar(0, horizont, f\"start_{a}_{g}\")\n",
|
|
||||||
" ende = modell.NewIntVar(0, horizont, f\"ende_{a}_{g}\")\n",
|
|
||||||
" intervall = modell.NewIntervalVar(start, dauer, ende, f\"iv_{a}_{g}\")\n",
|
|
||||||
" plaene[a, g] = Gang(start, ende, intervall)\n",
|
|
||||||
" je_maschine[maschine].append(intervall)\n",
|
|
||||||
"\n",
|
|
||||||
" for maschine in range(MASCHINEN):\n",
|
|
||||||
" modell.AddNoOverlap(je_maschine[maschine])\n",
|
|
||||||
" for a, auftrag in enumerate(auftraege):\n",
|
|
||||||
" for g in range(len(auftrag) - 1):\n",
|
|
||||||
" modell.Add(plaene[a, g + 1].start >= plaene[a, g].ende)\n",
|
|
||||||
"\n",
|
|
||||||
" makespan = modell.NewIntVar(0, horizont, \"makespan\")\n",
|
|
||||||
" modell.AddMaxEquality(\n",
|
|
||||||
" makespan, [plaene[a, len(auftrag) - 1].ende\n",
|
|
||||||
" for a, auftrag in enumerate(auftraege)])\n",
|
|
||||||
" modell.Minimize(makespan)\n",
|
|
||||||
"\n",
|
|
||||||
" loeser = cp_model.CpSolver()\n",
|
|
||||||
" loeser.parameters.num_workers = arbeiter\n",
|
|
||||||
" loeser.parameters.random_seed = SOLVER_SEED\n",
|
|
||||||
" loeser.parameters.max_time_in_seconds = ZEITLIMIT\n",
|
|
||||||
"\n",
|
|
||||||
" beginn = time.perf_counter()\n",
|
|
||||||
" status = loeser.Solve(modell)\n",
|
|
||||||
" dauer = time.perf_counter() - beginn\n",
|
|
||||||
"\n",
|
|
||||||
" if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):\n",
|
|
||||||
" return loeser.StatusName(status), None, dauer, None\n",
|
|
||||||
" plan = tuple(loeser.Value(plaene[a, g].start)\n",
|
|
||||||
" for a, auftrag in enumerate(auftraege)\n",
|
|
||||||
" for g in range(len(auftrag)))\n",
|
|
||||||
" return loeser.StatusName(status), int(loeser.ObjectiveValue()), dauer, plan\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"if __name__ == \"__main__\":\n",
|
|
||||||
" auftraege = baue_instanz()\n",
|
|
||||||
" gaenge = sum(len(a) for a in auftraege)\n",
|
|
||||||
"\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(\" WAS MEHRERE ARBEITER WIRKLICH BRINGEN\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(f\"Job-Shop: {AUFTRAEGE} Auftraege, {MASCHINEN} Maschinen, \"\n",
|
|
||||||
" f\"{gaenge} Arbeitsgaenge.\")\n",
|
|
||||||
" print(f\"random_seed = {SOLVER_SEED} in JEDEM Lauf, \"\n",
|
|
||||||
" f\"{WIEDERHOLUNGEN} Wiederholungen je Arbeiterzahl.\\n\")\n",
|
|
||||||
"\n",
|
|
||||||
" # Jede Konfiguration mehrfach - ein einzelner Zeitwert sagt nichts.\n",
|
|
||||||
" ergebnisse: dict[int, list[tuple]] = {}\n",
|
|
||||||
" for arbeiter in ARBEITERZAHLEN:\n",
|
|
||||||
" ergebnisse[arbeiter] = [loese(auftraege, arbeiter)\n",
|
|
||||||
" for _ in range(WIEDERHOLUNGEN)]\n",
|
|
||||||
"\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(\" (1) Laufzeit: was die Arbeiter bringen\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(f\"{'Arbeiter':>9} {'Status':>9} {'Makespan':>9} \"\n",
|
|
||||||
" f\"{'schnellste':>11} {'langsamste':>11} {'Beschleunigung':>15}\")\n",
|
|
||||||
" print(\"-\" * 78)\n",
|
|
||||||
" basis_zeit = min(d for _, _, d, _ in ergebnisse[1])\n",
|
|
||||||
" for arbeiter in ARBEITERZAHLEN:\n",
|
|
||||||
" laeufe = ergebnisse[arbeiter]\n",
|
|
||||||
" zeiten = [d for _, _, d, _ in laeufe]\n",
|
|
||||||
" ziele = {o for _, o, _, _ in laeufe}\n",
|
|
||||||
" status = {s for s, _, _, _ in laeufe}\n",
|
|
||||||
" print(f\"{arbeiter:>9} {'/'.join(sorted(status)):>9} \"\n",
|
|
||||||
" f\"{'/'.join(str(z) for z in sorted(ziele)):>9} \"\n",
|
|
||||||
" f\"{min(zeiten):>10.2f}s {max(zeiten):>10.2f}s \"\n",
|
|
||||||
" f\"{basis_zeit / min(zeiten):>14.1f}x\")\n",
|
|
||||||
"\n",
|
|
||||||
" beste = min(ARBEITERZAHLEN, key=lambda w: min(d for _, _, d, _ in ergebnisse[w]))\n",
|
|
||||||
" faktor = basis_zeit / min(d for _, _, d, _ in ergebnisse[beste])\n",
|
|
||||||
" print(f\"\\n Am schnellsten sind {beste} Arbeiter: Faktor {faktor:.1f} gegenueber\")\n",
|
|
||||||
" print(f\" einem einzigen - also MEHR, als {beste} Kerne hergeben sollten.\")\n",
|
|
||||||
" print(f\" Das ist kein Messfehler. CP-SAT laesst nicht {beste}-mal dieselbe Suche\")\n",
|
|
||||||
" print(\" laufen, sondern verschiedene Strategien nebeneinander, die einander\")\n",
|
|
||||||
" print(\" ihre Schranken zurufen. Der zweite Arbeiter ist deshalb nicht der\")\n",
|
|
||||||
" print(\" 'zweite Kern', sondern ein anderes Verfahren.\")\n",
|
|
||||||
" letzte = ARBEITERZAHLEN[-1]\n",
|
|
||||||
" if beste != letzte:\n",
|
|
||||||
" zeit_letzte = min(d for _, _, d, _ in ergebnisse[letzte])\n",
|
|
||||||
" print(f\"\\n Und mehr ist nicht immer besser: {letzte} Arbeiter brauchen \"\n",
|
|
||||||
" f\"{zeit_letzte:.2f}s\")\n",
|
|
||||||
" print(f\" gegenueber {min(d for _, _, d, _ in ergebnisse[beste]):.2f}s bei \"\n",
|
|
||||||
" f\"{beste}. Ab einer gewissen Zahl kosten Abstimmung\")\n",
|
|
||||||
" print(\" und Speicherbandbreite mehr, als ein weiterer Suchstrang einbringt.\")\n",
|
|
||||||
"\n",
|
|
||||||
" print(\"\\n\" + \"=\" * 78)\n",
|
|
||||||
" print(\" (2) Reproduzierbarkeit: derselbe Seed, derselbe Plan?\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(f\"{'Arbeiter':>9} {'Laeufe':>7} {'verschiedene Plaene':>21} \"\n",
|
|
||||||
" f\"{'Makespan':>10}\")\n",
|
|
||||||
" print(\"-\" * 78)\n",
|
|
||||||
" for arbeiter in ARBEITERZAHLEN:\n",
|
|
||||||
" plaene = {p for _, _, _, p in ergebnisse[arbeiter] if p is not None}\n",
|
|
||||||
" ziele = {o for _, o, _, _ in ergebnisse[arbeiter]}\n",
|
|
||||||
" print(f\"{arbeiter:>9} {WIEDERHOLUNGEN:>7} {len(plaene):>21} \"\n",
|
|
||||||
" f\"{'/'.join(str(z) for z in sorted(ziele)):>10}\")\n",
|
|
||||||
"\n",
|
|
||||||
" einer = len({p for _, _, _, p in ergebnisse[1]})\n",
|
|
||||||
" # Ab welcher Arbeiterzahl bricht die Reproduzierbarkeit? Das ist die\n",
|
|
||||||
" # eigentliche Zahl - nicht, was acht Arbeiter anrichten.\n",
|
|
||||||
" ab = next((w for w in ARBEITERZAHLEN\n",
|
|
||||||
" if len({p for _, _, _, p in ergebnisse[w]}) > 1), None)\n",
|
|
||||||
" print(f\"\\n Mit einem Arbeiter: {einer} Plan aus {WIEDERHOLUNGEN} Laeufen.\")\n",
|
|
||||||
" if ab is not None:\n",
|
|
||||||
" viele = len({p for _, _, _, p in ergebnisse[ab]})\n",
|
|
||||||
" print(f\" Schon mit {ab} Arbeitern: {viele} verschiedene Plaene aus \"\n",
|
|
||||||
" f\"{WIEDERHOLUNGEN} Laeufen -\")\n",
|
|
||||||
" print(\" bei identischem random_seed und identischem Zielwert.\")\n",
|
|
||||||
" print(f\" Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit\")\n",
|
|
||||||
" print(f\" zu verlieren. {ab} genuegen.\")\n",
|
|
||||||
" print(\"\\n Der Seed allein macht einen Lauf also NICHT reproduzierbar. Er legt\")\n",
|
|
||||||
" print(\" fest, wie ein einzelner Suchstrang wuerfelt - nicht, welcher von\")\n",
|
|
||||||
" print(\" mehreren zuerst fertig wird. Das entscheidet die Uhr.\")\n",
|
|
||||||
"\n",
|
|
||||||
" print(\"\\n\" + \"=\" * 78)\n",
|
|
||||||
" print(\" (3) Was daraus fuer Tests folgt\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(\" Ein Test der Form assert plan == erwarteter_plan besteht mal und\")\n",
|
|
||||||
" print(\" scheitert mal, ohne dass sich am Code etwas geaendert haette.\")\n",
|
|
||||||
" print(\" Zu pruefen sind stattdessen:\")\n",
|
|
||||||
" print(\" * der ZIELWERT (hier in allen Laeufen gleich),\")\n",
|
|
||||||
" print(\" * die Einhaltung aller Regeln (Abnahmepruefung, Kapitel Praxisfallen),\")\n",
|
|
||||||
" print(\" * und der Status - nicht die Gestalt der Loesung.\")\n",
|
|
||||||
" print(\"\\n Wer doch einen bestimmten Plan braucht - fuer eine abgedruckte\")\n",
|
|
||||||
" print(\" Ausgabe, fuer einen Regressionstest -, setzt num_workers = 1.\")\n",
|
|
||||||
" print(f\" Das kostet hier den Faktor {faktor:.1f} an Laufzeit und ist genau\")\n",
|
|
||||||
" print(\" deshalb eine Entscheidung fuer den Test, nicht fuer den Betrieb.\")\n",
|
|
||||||
" print(\"=\" * 78)"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
|
|
|
||||||
|
|
@ -455,8 +455,7 @@
|
||||||
" exakt positiv semidefinit ist - rechne trotzdem.' Das ist hier bewusst so\n",
|
" exakt positiv semidefinit ist - rechne trotzdem.' Das ist hier bewusst so\n",
|
||||||
" gewaehlt, denn genau diese Eigenschaft wollen wir vorfuehren. Im\n",
|
" gewaehlt, denn genau diese Eigenschaft wollen wir vorfuehren. Im\n",
|
||||||
" Produktivbetrieb waere die DCP-Fehlermeldung das Warnsignal, dem man\n",
|
" Produktivbetrieb waere die DCP-Fehlermeldung das Warnsignal, dem man\n",
|
||||||
" nachgehen muss (siehe den Denkfehler im Kapitel Quadratische und\n",
|
" nachgehen muss (siehe Denkfehler 8.1).\n",
|
||||||
" nichtlineare Optimierung).\n",
|
|
||||||
" \"\"\"\n",
|
" \"\"\"\n",
|
||||||
" gewichte = cp.Variable(ANZAHL_ANLAGEN)\n",
|
" gewichte = cp.Variable(ANZAHL_ANLAGEN)\n",
|
||||||
" problem = cp.Problem(cp.Minimize(cp.quad_form(gewichte, cp.psd_wrap(kovarianz))),\n",
|
" problem = cp.Problem(cp.Minimize(cp.quad_form(gewichte, cp.psd_wrap(kovarianz))),\n",
|
||||||
|
|
|
||||||
|
|
@ -757,8 +757,7 @@
|
||||||
"\n",
|
"\n",
|
||||||
" loeser = cp_model.CpSolver()\n",
|
" loeser = cp_model.CpSolver()\n",
|
||||||
" loeser.parameters.max_time_in_seconds = zeitlimit\n",
|
" loeser.parameters.max_time_in_seconds = zeitlimit\n",
|
||||||
" # Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist -\n",
|
" # Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist.\n",
|
||||||
" # der Seed allein genuegt dafuer NICHT (Kapitel Constraint Programming).\n",
|
|
||||||
" # Im Produktivbetrieb laesst man beides auf den Standardwerten.\n",
|
" # Im Produktivbetrieb laesst man beides auf den Standardwerten.\n",
|
||||||
" loeser.parameters.num_workers = 1\n",
|
" loeser.parameters.num_workers = 1\n",
|
||||||
" loeser.parameters.random_seed = 1\n",
|
" loeser.parameters.random_seed = 1\n",
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"source": [
|
"source": [
|
||||||
"## Wie die Isolation aussieht, wenn sie tragen soll\n",
|
"## Ein System — vier Programmieransätze\n",
|
||||||
"\n",
|
"\n",
|
||||||
"`Ein_System_Vier_Ansaetze.py`\n"
|
"`Ein_System_Vier_Ansaetze.py`\n"
|
||||||
]
|
]
|
||||||
|
|
@ -136,104 +136,85 @@
|
||||||
" 2*x1 + 3*x2 + x3 <= 50\n",
|
" 2*x1 + 3*x2 + x3 <= 50\n",
|
||||||
" x >= 0\n",
|
" x >= 0\n",
|
||||||
"\n",
|
"\n",
|
||||||
"Deckt scipy.optimize, highspy, CVXPY und OR-Tools/GLOP ab, mit Kreuzvergleich\n",
|
"Deckt scipy.optimize, highspy, CVXPY und OR-Tools/GLOP ab, mit Kreuzvergleich am Ende.\n",
|
||||||
"am Ende.\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"WICHTIG: Jeder Solver laeuft in einem EIGENEN Prozess, weil sich ortools und\n",
|
"WICHTIG: Jeder Solver läuft in einem EIGENEN Prozess, weil sich ortools und\n",
|
||||||
"highspy auf vielen Systemen nicht gemeinsam importieren lassen (beide bringen\n",
|
"highspy auf vielen Systemen nicht gemeinsam importieren lassen (beide bringen\n",
|
||||||
"eine eigene HiGHS-Kopie mit -> Symbolkonflikt).\n",
|
"eine eigene HiGHS-Kopie mit -> Symbolkonflikt).\n",
|
||||||
"\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",
|
|
||||||
"\"\"\"\n",
|
"\"\"\"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"import multiprocessing\n",
|
"import json\n",
|
||||||
|
"import subprocess\n",
|
||||||
|
"import sys\n",
|
||||||
|
"import textwrap\n",
|
||||||
"import time\n",
|
"import time\n",
|
||||||
"from concurrent.futures import ProcessPoolExecutor\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"ERWARTET = 530.0 # Ergebnis der Handrechnung zum Produktionsprogramm\n",
|
"ERWARTET = 530.0 # Ergebnis der Handrechnung zum Produktionsprogramm\n",
|
||||||
"\n",
|
"\n",
|
||||||
"# Die Instanz - einmal notiert, von allen vier Funktionen benutzt.\n",
|
"# Jeder Eintrag ist ein eigenständiges Miniprogramm, das sein Ergebnis als\n",
|
||||||
"ZIEL = [10.0, 15.0, 25.0]\n",
|
"# JSON auf stdout ausgibt. So bleibt jeder Import in seinem eigenen Prozess.\n",
|
||||||
"MATRIX = [[1, 1, 2], [2, 3, 1]]\n",
|
"ANSAETZE: dict[str, str] = {\n",
|
||||||
"KAPAZITAET = [40.0, 50.0]\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
" \"scipy.optimize.linprog\": \"\"\"\n",
|
||||||
"def loese_mit_scipy() -> tuple[float, list[float]]:\n",
|
|
||||||
" from scipy.optimize import linprog\n",
|
" from scipy.optimize import linprog\n",
|
||||||
" ergebnis = linprog(c=[-w for w in ZIEL], # linprog MINIMIERT -> negieren\n",
|
" res = linprog(c=[-10.0, -15.0, -25.0], # linprog MINIMIERT -> negieren\n",
|
||||||
" A_ub=MATRIX, b_ub=KAPAZITAET,\n",
|
" A_ub=[[1, 1, 2], [2, 3, 1]], b_ub=[40, 50],\n",
|
||||||
" bounds=[(0, None)] * 3, method=\"highs\")\n",
|
" bounds=[(0, None)] * 3, method=\"highs\")\n",
|
||||||
" return -ergebnis.fun, list(ergebnis.x)\n",
|
" ausgabe = (-res.fun, list(res.x))\n",
|
||||||
|
" \"\"\",\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
" \"highspy (natives HiGHS)\": \"\"\"\n",
|
||||||
"def loese_mit_highspy() -> tuple[float, list[float]]:\n",
|
" import numpy as np, highspy\n",
|
||||||
" import highspy\n",
|
|
||||||
" import numpy as np\n",
|
|
||||||
" h = highspy.Highs()\n",
|
" h = highspy.Highs()\n",
|
||||||
" h.setOptionValue(\"output_flag\", False)\n",
|
" h.setOptionValue(\"output_flag\", False)\n",
|
||||||
" h.addVars(3, np.zeros(3), np.full(3, highspy.kHighsInf))\n",
|
" h.addVars(3, np.zeros(3), np.full(3, highspy.kHighsInf))\n",
|
||||||
" h.changeObjectiveSense(highspy.ObjSense.kMaximize)\n",
|
" h.changeObjectiveSense(highspy.ObjSense.kMaximize)\n",
|
||||||
" for j, wert in enumerate(ZIEL):\n",
|
" for j, wert in enumerate([10.0, 15.0, 25.0]):\n",
|
||||||
" h.changeColCost(j, wert)\n",
|
" h.changeColCost(j, wert)\n",
|
||||||
" # CSR-Format: starts[i] = Beginn von Zeile i in indices/values\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",
|
" h.addRows(2, np.full(2, -highspy.kHighsInf), np.array([40.0, 50.0]), 6,\n",
|
||||||
" np.array([0, 3], dtype=np.int32),\n",
|
" np.array([0, 3], dtype=np.int32),\n",
|
||||||
" np.array([0, 1, 2, 0, 1, 2], 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",
|
" np.array([1.0, 1.0, 2.0, 2.0, 3.0, 1.0]))\n",
|
||||||
" h.run()\n",
|
" h.run()\n",
|
||||||
" return (h.getInfo().objective_function_value,\n",
|
" ausgabe = (h.getInfo().objective_function_value,\n",
|
||||||
" list(h.getSolution().col_value[:3]))\n",
|
" list(h.getSolution().col_value[:3]))\n",
|
||||||
|
" \"\"\",\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
" \"cvxpy\": \"\"\"\n",
|
||||||
"def loese_mit_cvxpy() -> tuple[float, list[float]]:\n",
|
" import numpy as np, cvxpy as cp\n",
|
||||||
" import cvxpy as cp\n",
|
|
||||||
" import numpy as np\n",
|
|
||||||
" x = cp.Variable(3, nonneg=True)\n",
|
" x = cp.Variable(3, nonneg=True)\n",
|
||||||
" problem = cp.Problem(cp.Maximize(np.array(ZIEL) @ x),\n",
|
" problem = cp.Problem(cp.Maximize(np.array([10.0, 15.0, 25.0]) @ x),\n",
|
||||||
" [np.array(MATRIX) @ x <= np.array(KAPAZITAET)])\n",
|
" [np.array([[1, 1, 2], [2, 3, 1]]) @ x <= np.array([40, 50])])\n",
|
||||||
" problem.solve()\n",
|
" problem.solve()\n",
|
||||||
" return float(problem.value), [float(v) for v in x.value]\n",
|
" ausgabe = (float(problem.value), [float(v) for v in x.value])\n",
|
||||||
|
" \"\"\",\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
" \"ortools / GLOP\": \"\"\"\n",
|
||||||
"def loese_mit_ortools() -> tuple[float, list[float]]:\n",
|
|
||||||
" from ortools.linear_solver import pywraplp\n",
|
" from ortools.linear_solver import pywraplp\n",
|
||||||
" s = pywraplp.Solver.CreateSolver(\"GLOP\")\n",
|
" s = pywraplp.Solver.CreateSolver(\"GLOP\")\n",
|
||||||
" x = [s.NumVar(0, s.infinity(), f\"x{j+1}\") for j in range(3)]\n",
|
" x = [s.NumVar(0, s.infinity(), f\"x{j+1}\") for j in range(3)]\n",
|
||||||
" for i, kapazitaet in enumerate(KAPAZITAET):\n",
|
" A = [[1, 1, 2], [2, 3, 1]]\n",
|
||||||
" s.Add(sum(MATRIX[i][j] * x[j] for j in range(3)) <= kapazitaet)\n",
|
" for i, kap in enumerate([40, 50]):\n",
|
||||||
" s.Maximize(sum(ZIEL[j] * x[j] for j in range(3)))\n",
|
" s.Add(sum(A[i][j] * x[j] for j in range(3)) <= kap)\n",
|
||||||
|
" s.Maximize(10 * x[0] + 15 * x[1] + 25 * x[2])\n",
|
||||||
" s.Solve()\n",
|
" s.Solve()\n",
|
||||||
" return s.Objective().Value(), [v.solution_value() for v in x]\n",
|
" ausgabe = (s.Objective().Value(), [v.solution_value() for v in x])\n",
|
||||||
"\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",
|
|
||||||
"}\n",
|
"}\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
|
"def fuehre_in_eigenem_prozess_aus(quelltext: str) -> tuple[float, list[float]]:\n",
|
||||||
|
" \"\"\"Startet den Codeschnipsel als separaten Python-Prozess und liest das Ergebnis.\"\"\"\n",
|
||||||
|
" programm = textwrap.dedent(quelltext) + \"\\nimport json; print(json.dumps(ausgabe))\\n\"\n",
|
||||||
|
" ergebnis = subprocess.run([sys.executable, \"-c\", programm],\n",
|
||||||
|
" capture_output=True, text=True, timeout=120)\n",
|
||||||
|
" if ergebnis.returncode != 0:\n",
|
||||||
|
" raise RuntimeError(ergebnis.stderr.strip().splitlines()[-1])\n",
|
||||||
|
" wert, loesung = json.loads(ergebnis.stdout.strip().splitlines()[-1])\n",
|
||||||
|
" return wert, loesung\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
"if __name__ == \"__main__\":\n",
|
"if __name__ == \"__main__\":\n",
|
||||||
" print(\"=\" * 78)\n",
|
" print(\"=\" * 78)\n",
|
||||||
" print(\" EIN SYSTEM - VIER ANSAETZE (je eigener Prozess)\")\n",
|
" print(\" EIN SYSTEM - VIER ANSAETZE (je eigener Prozess)\")\n",
|
||||||
|
|
@ -242,20 +223,14 @@
|
||||||
" print(\"-\" * 78)\n",
|
" print(\"-\" * 78)\n",
|
||||||
"\n",
|
"\n",
|
||||||
" werte = []\n",
|
" werte = []\n",
|
||||||
" # Ein Pool, vier Aufgaben, vier frische Prozesse. Der Kontext muss\n",
|
" for name, quelltext in ANSAETZE.items():\n",
|
||||||
" # \"spawn\" sein - siehe Modulkommentar.\n",
|
" t0 = time.perf_counter()\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",
|
" try:\n",
|
||||||
" wert, x = pool.submit(funktion).result(timeout=120)\n",
|
" wert, x = fuehre_in_eigenem_prozess_aus(quelltext)\n",
|
||||||
" except Exception as fehler: # Bibliothek fehlt o. Ae.\n",
|
" except RuntimeError as fehler:\n",
|
||||||
" print(f\"{name:<26} nicht verfuegbar: {str(fehler)[:40]}\")\n",
|
" print(f\"{name:<26} nicht verfuegbar: {fehler[:40]}\")\n",
|
||||||
" continue\n",
|
" continue\n",
|
||||||
" dauer = time.perf_counter() - beginn\n",
|
" dauer = time.perf_counter() - t0\n",
|
||||||
" werte.append(wert)\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",
|
" 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",
|
" f\"{dauer:>8.2f} s\")\n",
|
||||||
|
|
@ -268,8 +243,8 @@
|
||||||
" assert spanne < 1e-6, \"Die Bibliotheken widersprechen sich!\"\n",
|
" assert spanne < 1e-6, \"Die Bibliotheken widersprechen sich!\"\n",
|
||||||
" assert abs(werte[0] - ERWARTET) < 1e-6, \"Ergebnis weicht von der Handrechnung ab!\"\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",
|
" print(\"Alle Wege fuehren zum selben, von Hand bestaetigten Optimum.\")\n",
|
||||||
" print(\"(Die Zeiten enthalten Prozessstart und Import - sie messen NICHT die\")\n",
|
" print(\"(Die Zeiten enthalten den Prozessstart und den Import - sie messen\")\n",
|
||||||
" print(\" reine Solverleistung. Die Uebungsaufgabe 'Laufzeitvergleich' trennt beides.)\")\n",
|
" print(\" NICHT die reine Solverleistung, siehe Uebung 3.5.)\")\n",
|
||||||
" print(\"=\" * 78)"
|
" print(\"=\" * 78)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -252,328 +252,6 @@
|
||||||
" print(\"=\" * 78)"
|
" print(\"=\" * 78)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"## Constraint Attribution: welche Bedingung kostet wie viel?\n",
|
|
||||||
"\n",
|
|
||||||
"`Constraint_Attribution.py`\n"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": null,
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"#!/usr/bin/env python3\n",
|
|
||||||
"\n",
|
|
||||||
"# Constraint_Attribution.py\n",
|
|
||||||
"\"\"\"\n",
|
|
||||||
"Kapitel Praxisfallen: Welche Bedingung kostet wie viel - und was sagt man\n",
|
|
||||||
"dem Management?\n",
|
|
||||||
"\n",
|
|
||||||
"Zwischen den beiden bekannten Faellen klafft eine Luecke:\n",
|
|
||||||
"\n",
|
|
||||||
" * Erklaerbarkeit.py beantwortet \"warum diese eine Zuweisung?\"\n",
|
|
||||||
" * Konfliktsuche.py (Anhang Fehlerdiagnose) beantwortet \"warum geht es gar\n",
|
|
||||||
" nicht?\"\n",
|
|
||||||
"\n",
|
|
||||||
"Der haeufigste Fall in der Praxis liegt dazwischen: Das Modell rechnet, der\n",
|
|
||||||
"Plan ist zulaessig - und trotzdem unbefriedigend. Niemand weiss, WORAN es\n",
|
|
||||||
"liegt. Genau das beantwortet Constraint Attribution.\n",
|
|
||||||
"\n",
|
|
||||||
"Gerechnet wird auf derselben Fabrik wie in Konfliktsuche.py, nur nach der\n",
|
|
||||||
"Reparatur: Dort war die Lackierkapazitaet von 150 Stunden der Kern des\n",
|
|
||||||
"Widerspruchs (2*40 + 3*30 = 170 > 150). Eine zweite Schicht hat sie auf 210\n",
|
|
||||||
"gebracht, und der Mindestumsatz wurde auf 10.000 EUR gesenkt. Jetzt ist das\n",
|
|
||||||
"Modell loesbar - und die Frage lautet nicht mehr \"warum nicht?\", sondern\n",
|
|
||||||
"\"was kostet uns was?\".\n",
|
|
||||||
"\n",
|
|
||||||
"Gezeigt werden vier Dinge:\n",
|
|
||||||
" 1. Welche Bedingungen binden - und dass \"bindend\" nicht \"teuer\" heisst.\n",
|
|
||||||
" 2. Dass der Schattenpreis eine MOMENTAUFNAHME ist: hochgerechnet auf eine\n",
|
|
||||||
" realistische Sonderschicht verspricht er hier das Doppelte des\n",
|
|
||||||
" tatsaechlichen Nutzens.\n",
|
|
||||||
" 3. Dass ein Wunsch aus dem Vertrieb nicht teuer, sondern unmoeglich sein\n",
|
|
||||||
" kann - und welche Bedingungen ihn gemeinsam blockieren.\n",
|
|
||||||
" 4. Wie aus alledem ein Bericht in Alltagssprache wird.\n",
|
|
||||||
"\n",
|
|
||||||
"Benoetigt: numpy, scipy\n",
|
|
||||||
"\"\"\"\n",
|
|
||||||
"\n",
|
|
||||||
"from __future__ import annotations\n",
|
|
||||||
"\n",
|
|
||||||
"import textwrap\n",
|
|
||||||
"\n",
|
|
||||||
"import numpy as np\n",
|
|
||||||
"from scipy.optimize import linprog\n",
|
|
||||||
"\n",
|
|
||||||
"PRODUKTE = [\"Rahmen\", \"Gehaeuse\", \"Deckel\", \"Traeger\", \"Halter\"]\n",
|
|
||||||
"DECKUNGSBEITRAG = np.array([35.0, 28.0, 9.0, 22.0, 6.0]) # EUR je Stueck\n",
|
|
||||||
"\n",
|
|
||||||
"# Dieselben Bedingungen wie in Konfliktsuche.py, mit den beiden Reparaturen.\n",
|
|
||||||
"# Der sprechende Name ist keine Kosmetik: Ohne ihn ist kein Bericht moeglich,\n",
|
|
||||||
"# der jemandem ausserhalb der Modellierung etwas sagt.\n",
|
|
||||||
"BEDINGUNGEN: list[tuple[str, list[float], str, float]] = [\n",
|
|
||||||
" (\"Kapazitaet Montage\", [3, 2, 1, 4, 2], \"<=\", 400),\n",
|
|
||||||
" (\"Kapazitaet Lackieren\", [2, 3, 0, 1, 0], \"<=\", 210),\n",
|
|
||||||
" (\"Kapazitaet Pruefung\", [1, 1, 1, 1, 1], \"<=\", 300),\n",
|
|
||||||
" (\"Liefervertrag Rahmen\", [1, 0, 0, 0, 0], \">=\", 40),\n",
|
|
||||||
" (\"Liefervertrag Gehaeuse\", [0, 1, 0, 0, 0], \">=\", 30),\n",
|
|
||||||
" (\"Liefervertrag Deckel\", [0, 0, 1, 0, 0], \">=\", 20),\n",
|
|
||||||
" (\"Liefervertrag Traeger\", [0, 0, 0, 1, 0], \">=\", 25),\n",
|
|
||||||
" (\"Marktgrenze Rahmen\", [1, 0, 0, 0, 0], \"<=\", 120),\n",
|
|
||||||
" (\"Marktgrenze Gehaeuse\", [0, 1, 0, 0, 0], \"<=\", 90),\n",
|
|
||||||
" (\"Marktgrenze Deckel\", [0, 0, 1, 0, 0], \"<=\", 150),\n",
|
|
||||||
" (\"Marktgrenze Halter\", [0, 0, 0, 0, 1], \"<=\", 200),\n",
|
|
||||||
" (\"Mindestumsatz\", [90, 70, 30, 60, 20], \">=\", 10000),\n",
|
|
||||||
" (\"Sortimentsbreite\", [0, 0, 1, 1, 1], \">=\", 100),\n",
|
|
||||||
" (\"Lackierbudget Schicht 2\", [0, 1, 0, 1, 0], \"<=\", 55),\n",
|
|
||||||
"]\n",
|
|
||||||
"\n",
|
|
||||||
"# Was liesse sich im Ernstfall wirklich bewegen - und um wie viel? Nicht \"eine\n",
|
|
||||||
"# Einheit\", sondern der Schritt, den ein Planer tatsaechlich gehen kann: eine\n",
|
|
||||||
"# Sonderschicht, ein nachverhandelter Vertrag. Genau an dieser Groesse\n",
|
|
||||||
"# scheitert das blosse Hochrechnen des Schattenpreises.\n",
|
|
||||||
"STELLSCHRAUBEN: dict[str, tuple[float, str]] = {\n",
|
|
||||||
" \"Kapazitaet Montage\": (+50, \"eine Sonderschicht Montage\"),\n",
|
|
||||||
" \"Kapazitaet Lackieren\": (+30, \"eine Sonderschicht Lackieren\"),\n",
|
|
||||||
" \"Kapazitaet Pruefung\": (+50, \"eine Sonderschicht Pruefung\"),\n",
|
|
||||||
" \"Lackierbudget Schicht 2\": (+10, \"10 Stunden mehr Budget in Schicht 2\"),\n",
|
|
||||||
" \"Liefervertrag Traeger\": (-10, \"10 Stueck weniger Abnahmepflicht Traeger\"),\n",
|
|
||||||
" \"Liefervertrag Gehaeuse\": (-10, \"10 Stueck weniger Abnahmepflicht Gehaeuse\"),\n",
|
|
||||||
"}\n",
|
|
||||||
"\n",
|
|
||||||
"WUNSCH_MENGE = 55.0 # der Vertrieb haette gern mehr Rahmen\n",
|
|
||||||
"WUNSCH_PRODUKT = \"Rahmen\"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"def loese(bedingungen: list[tuple[str, list[float], str, float]]):\n",
|
|
||||||
" \"\"\"Maximiert den Deckungsbeitrag. linprog minimiert, daher das Vorzeichen.\"\"\"\n",
|
|
||||||
" matrix, rechte_seite = [], []\n",
|
|
||||||
" for _, koeffizienten, richtung, grenze in bedingungen:\n",
|
|
||||||
" zeile = np.array(koeffizienten, dtype=float)\n",
|
|
||||||
" matrix.append(zeile if richtung == \"<=\" else -zeile)\n",
|
|
||||||
" rechte_seite.append(grenze if richtung == \"<=\" else -grenze)\n",
|
|
||||||
" return linprog(-DECKUNGSBEITRAG,\n",
|
|
||||||
" A_ub=np.array(matrix), b_ub=np.array(rechte_seite),\n",
|
|
||||||
" bounds=[(0, None)] * len(PRODUKTE), method=\"highs\")\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"def geaendert(name: str, delta: float,\n",
|
|
||||||
" basis=None) -> list[tuple[str, list[float], str, float]]:\n",
|
|
||||||
" \"\"\"Dieselben Bedingungen, eine davon um delta verschoben.\"\"\"\n",
|
|
||||||
" return [(n, k, r, g + delta if n == name else g)\n",
|
|
||||||
" for n, k, r, g in (basis or BEDINGUNGEN)]\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"def auswertung(ergebnis, bedingungen=None):\n",
|
|
||||||
" \"\"\"Schlupf und Schattenpreis je Bedingung.\"\"\"\n",
|
|
||||||
" x = ergebnis.x\n",
|
|
||||||
" dual = ergebnis.ineqlin.marginals\n",
|
|
||||||
" zeilen = []\n",
|
|
||||||
" for i, (name, koeff, richtung, grenze) in enumerate(bedingungen or BEDINGUNGEN):\n",
|
|
||||||
" links = float(np.array(koeff, dtype=float) @ x)\n",
|
|
||||||
" schlupf = (grenze - links) if richtung == \"<=\" else (links - grenze)\n",
|
|
||||||
" zeilen.append((name, links, richtung, grenze, schlupf, abs(dual[i])))\n",
|
|
||||||
" return zeilen\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"def blockierer(bedingungen, zusatz) -> list[str]:\n",
|
|
||||||
" \"\"\"Welche Bedingungen verhindern 'zusatz' - einzeln weggelassen.\n",
|
|
||||||
"\n",
|
|
||||||
" Dasselbe Vorgehen wie der Deletion Filter in Konfliktsuche.py, nur mit\n",
|
|
||||||
" umgekehrtem Vorzeichen: Dort wird gesucht, was den Widerspruch AUSMACHT,\n",
|
|
||||||
" hier, was einen Wunsch VERHINDERT. Wird das Modell ohne eine Bedingung\n",
|
|
||||||
" loesbar, gehoert sie zum Konflikt.\n",
|
|
||||||
" \"\"\"\n",
|
|
||||||
" gefunden = []\n",
|
|
||||||
" for i, (name, _, _, _) in enumerate(bedingungen):\n",
|
|
||||||
" rest = [c for j, c in enumerate(bedingungen) if j != i] + [zusatz]\n",
|
|
||||||
" if loese(rest).success:\n",
|
|
||||||
" gefunden.append(name)\n",
|
|
||||||
" return gefunden\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"def euro(betrag: float) -> str:\n",
|
|
||||||
" return f\"{betrag:,.2f} EUR\"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"if __name__ == \"__main__\":\n",
|
|
||||||
" basis = loese(BEDINGUNGEN)\n",
|
|
||||||
" db_basis = -basis.fun\n",
|
|
||||||
" plan = dict(zip(PRODUKTE, basis.x))\n",
|
|
||||||
"\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(\" CONSTRAINT ATTRIBUTION: Was kostet uns welche Bedingung?\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(f\"Status: {basis.message.split('(')[0].strip()}\")\n",
|
|
||||||
" print(f\"Deckungsbeitrag des Plans: {euro(db_basis)}\\n\")\n",
|
|
||||||
" print(\" \" + \" \".join(f\"{p}: {m:.1f}\" for p, m in plan.items()))\n",
|
|
||||||
"\n",
|
|
||||||
" # --- 1. Wer begrenzt den Plan? -----------------------------------------\n",
|
|
||||||
" print(\"\\n\" + \"=\" * 78)\n",
|
|
||||||
" print(\" (1) Welche Bedingungen binden - und was kostet die naechste Einheit?\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(f\"{'Bedingung':<24} {'genutzt':>9} {'Grenze':>7} {'Schlupf':>8} \"\n",
|
|
||||||
" f\"{'Schattenpreis':>11}\")\n",
|
|
||||||
" print(\"-\" * 78)\n",
|
|
||||||
" bindend = []\n",
|
|
||||||
" for name, links, richtung, grenze, schlupf, dual in auswertung(basis):\n",
|
|
||||||
" marke = \"\"\n",
|
|
||||||
" if abs(schlupf) < 1e-6:\n",
|
|
||||||
" bindend.append((name, dual))\n",
|
|
||||||
" marke = \" <-- bindend\"\n",
|
|
||||||
" print(f\"{name:<24} {links:>9.1f} {grenze:>7.0f} {schlupf:>8.1f} \"\n",
|
|
||||||
" f\"{dual:>11.2f}{marke}\")\n",
|
|
||||||
"\n",
|
|
||||||
" ohne_preis = [n for n, d in bindend if d < 1e-9]\n",
|
|
||||||
" print(f\"\\n {len(bindend)} Bedingungen binden. Aber: \"\n",
|
|
||||||
" f\"{', '.join(ohne_preis)} bindet\")\n",
|
|
||||||
" print(\" mit einem Schattenpreis von 0,00 EUR - sie beruehrt den Plan, ohne ihn\")\n",
|
|
||||||
" print(\" zu verteuern. Bindend und kostenrelevant sind zwei verschiedene Dinge.\")\n",
|
|
||||||
"\n",
|
|
||||||
" # --- 2. Der Schattenpreis als Momentaufnahme ---------------------------\n",
|
|
||||||
" print(\"\\n\" + \"=\" * 78)\n",
|
|
||||||
" print(\" (2) Was der Schattenpreis verschweigt\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(\"Der Schattenpreis gilt fuer die NAECHSTE Einheit. Ein Planer kauft aber\")\n",
|
|
||||||
" print(\"keine Einheit, sondern eine ganze Schicht. Beides gegenuebergestellt:\\n\")\n",
|
|
||||||
" preise = {name: dual for name, *_, dual in auswertung(basis)}\n",
|
|
||||||
" print(f\"{'Stellschraube':<24} {'Preis':>9} {'Schritt':>7} \"\n",
|
|
||||||
" f\"{'hochgerechnet':>13} {'gemessen':>9} {'Abw.':>9}\")\n",
|
|
||||||
" print(\"-\" * 78)\n",
|
|
||||||
" wirkung = {}\n",
|
|
||||||
" for name, (delta, _) in STELLSCHRAUBEN.items():\n",
|
|
||||||
" ergebnis = loese(geaendert(name, delta))\n",
|
|
||||||
" echt = (-ergebnis.fun) - db_basis if ergebnis.success else 0.0\n",
|
|
||||||
" linear = preise[name] * abs(delta)\n",
|
|
||||||
" wirkung[name] = echt\n",
|
|
||||||
" print(f\"{name:<24} {preise[name]:>9.2f} {delta:>+7.0f} \"\n",
|
|
||||||
" f\"{linear:>13.2f} {echt:>9.2f} {echt - linear:>+9.2f}\")\n",
|
|
||||||
"\n",
|
|
||||||
" # Wo endet der Gueltigkeitsbereich? Schrittweise nachfahren.\n",
|
|
||||||
" abweichler = max(STELLSCHRAUBEN,\n",
|
|
||||||
" key=lambda n: preise[n] * abs(STELLSCHRAUBEN[n][0]) - wirkung[n])\n",
|
|
||||||
" delta_max = STELLSCHRAUBEN[abweichler][0]\n",
|
|
||||||
" grenze_gueltig = 0.0\n",
|
|
||||||
" schritt = delta_max / 60.0\n",
|
|
||||||
" vorher = db_basis\n",
|
|
||||||
" for i in range(1, 61):\n",
|
|
||||||
" d = schritt * i\n",
|
|
||||||
" e = loese(geaendert(abweichler, d))\n",
|
|
||||||
" db = -e.fun if e.success else vorher\n",
|
|
||||||
" if db - vorher > 1e-9:\n",
|
|
||||||
" grenze_gueltig = d\n",
|
|
||||||
" vorher = db\n",
|
|
||||||
" print(f\"\\n '{abweichler}': hochgerechnet \"\n",
|
|
||||||
" f\"{preise[abweichler] * abs(delta_max):.2f} EUR, tatsaechlich \"\n",
|
|
||||||
" f\"{wirkung[abweichler]:.2f} EUR.\")\n",
|
|
||||||
" print(f\" Der Preis von {preise[abweichler]:.2f} EUR gilt nur bis \"\n",
|
|
||||||
" f\"+{grenze_gueltig:.0f} Stunden. Jede weitere Stunde\")\n",
|
|
||||||
" print(\" dieser Sonderschicht bringt exakt nichts - der Engpass ist dann ein anderer.\")\n",
|
|
||||||
"\n",
|
|
||||||
" nach_preis = sorted(STELLSCHRAUBEN, key=lambda n: -preise[n])[0]\n",
|
|
||||||
" nach_wirkung = max(wirkung, key=wirkung.get)\n",
|
|
||||||
" print(f\"\\n Und die Ranglisten drehen sich: Nach Schattenpreis fuehrt\")\n",
|
|
||||||
" print(f\" '{nach_preis}' ({preise[nach_preis]:.2f} EUR je Einheit),\")\n",
|
|
||||||
" print(f\" nach tatsaechlicher Wirkung '{nach_wirkung}' \"\n",
|
|
||||||
" f\"({wirkung[nach_wirkung]:.2f} EUR).\")\n",
|
|
||||||
" print(\" Der Schattenpreis sagt, was eine Einheit wert ist - nicht, wie viele\")\n",
|
|
||||||
" print(\" Einheiten zu haben sind.\")\n",
|
|
||||||
"\n",
|
|
||||||
" # --- 3. Der Wunsch aus dem Vertrieb ------------------------------------\n",
|
|
||||||
" print(\"\\n\" + \"=\" * 78)\n",
|
|
||||||
" print(f\" (3) Was-waere-wenn: mindestens {WUNSCH_MENGE:.0f} \"\n",
|
|
||||||
" f\"{WUNSCH_PRODUKT} statt {plan[WUNSCH_PRODUKT]:.1f}\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" wunsch = (f\"Wunsch Vertrieb {WUNSCH_PRODUKT}\",\n",
|
|
||||||
" [1 if p == WUNSCH_PRODUKT else 0 for p in PRODUKTE], \">=\", WUNSCH_MENGE)\n",
|
|
||||||
" mit_wunsch = loese(BEDINGUNGEN + [wunsch])\n",
|
|
||||||
" schuldige: list[str] = []\n",
|
|
||||||
" engpass = noetig = None\n",
|
|
||||||
" wunsch_gratis = False\n",
|
|
||||||
"\n",
|
|
||||||
" if mit_wunsch.success:\n",
|
|
||||||
" print(f\"Erfuellbar, kostet {euro(db_basis - (-mit_wunsch.fun))}.\")\n",
|
|
||||||
" else:\n",
|
|
||||||
" schuldige = blockierer(BEDINGUNGEN, wunsch)\n",
|
|
||||||
" print(\"Der Wunsch ist nicht teuer - er ist UNMOEGLICH.\\n\")\n",
|
|
||||||
" print(f\"Blockiert wird er von {len(schuldige)} Bedingungen gemeinsam. \"\n",
|
|
||||||
" f\"Faellt eine davon\")\n",
|
|
||||||
" print(\"weg, ist er erfuellbar; keine allein ist 'der' Grund:\")\n",
|
|
||||||
" for name in schuldige:\n",
|
|
||||||
" e = loese([c for c in BEDINGUNGEN if c[0] != name] + [wunsch])\n",
|
|
||||||
" print(f\" ohne '{name}': moeglich, Deckungsbeitrag {euro(-e.fun)}\")\n",
|
|
||||||
"\n",
|
|
||||||
" engpass = next((n for n in schuldige if n in STELLSCHRAUBEN), schuldige[0])\n",
|
|
||||||
" for schritt in range(1, 61):\n",
|
|
||||||
" if loese(geaendert(engpass, schritt) + [wunsch]).success:\n",
|
|
||||||
" noetig = schritt\n",
|
|
||||||
" break\n",
|
|
||||||
" if noetig is not None:\n",
|
|
||||||
" e = loese(geaendert(engpass, noetig) + [wunsch])\n",
|
|
||||||
" ohne = loese(geaendert(engpass, noetig))\n",
|
|
||||||
" wunsch_gratis = abs((-e.fun) - (-ohne.fun)) < 1e-6\n",
|
|
||||||
" print(f\"\\n Mit +{noetig} bei '{engpass}' wird der Wunsch erfuellbar:\")\n",
|
|
||||||
" print(f\" Deckungsbeitrag {euro(-e.fun)} \"\n",
|
|
||||||
" f\"({-e.fun - db_basis:+,.2f} gegenueber heute).\")\n",
|
|
||||||
" if wunsch_gratis:\n",
|
|
||||||
" print(f\" Und ohne den Wunsch ergaebe dieselbe Lockerung \"\n",
|
|
||||||
" f\"{euro(-ohne.fun)} bei\")\n",
|
|
||||||
" print(f\" {ohne.x[PRODUKTE.index(WUNSCH_PRODUKT)]:.0f} \"\n",
|
|
||||||
" f\"{WUNSCH_PRODUKT} - der Wunsch kostet also NICHTS.\")\n",
|
|
||||||
" print(\" Er war nie das Problem. Der Engpass war es.\")\n",
|
|
||||||
"\n",
|
|
||||||
" # --- 4. Der Bericht ----------------------------------------------------\n",
|
|
||||||
" # Aus denselben Zahlen, die oben in Tabellen stehen, wird hier Prosa. Kein\n",
|
|
||||||
" # Satz enthaelt eine Zahl, die nicht vorher gerechnet wurde - das ist der\n",
|
|
||||||
" # ganze Trick und zugleich die Bedingung dafuer, dass der Bericht nicht\n",
|
|
||||||
" # irgendwann leise falsch wird.\n",
|
|
||||||
" print(\"\\n\" + \"=\" * 78)\n",
|
|
||||||
" print(\" (4) Und so steht es in der Vorlage fuer die Abteilungsleiterrunde\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
"\n",
|
|
||||||
" teuerste = sorted(((n, d) for n, d in bindend if d > 0), key=lambda t: -t[1])\n",
|
|
||||||
" gross = STELLSCHRAUBEN[nach_wirkung][1]\n",
|
|
||||||
" klein = STELLSCHRAUBEN[abweichler][1]\n",
|
|
||||||
"\n",
|
|
||||||
" absaetze = [\n",
|
|
||||||
" f\"Der Plan bringt {euro(db_basis)} Deckungsbeitrag. Begrenzt wird er von \"\n",
|
|
||||||
" f\"{len(bindend)} Bedingungen, von denen {len(teuerste)} tatsaechlich Geld \"\n",
|
|
||||||
" f\"kosten; \\\"{ohne_preis[0]}\\\" beruehrt den Plan, ohne ihn zu verteuern.\",\n",
|
|
||||||
"\n",
|
|
||||||
" f\"Teuerste Bindung ist \\\"{teuerste[0][0]}\\\" mit \"\n",
|
|
||||||
" f\"{teuerste[0][1]:.2f} EUR je Einheit, gefolgt von \\\"{teuerste[1][0]}\\\" \"\n",
|
|
||||||
" f\"mit {teuerste[1][1]:.2f} EUR.\",\n",
|
|
||||||
"\n",
|
|
||||||
" f\"Die groesste einzelne Verbesserung bringt {gross}: \"\n",
|
|
||||||
" f\"{euro(wirkung[nach_wirkung])} mehr Deckungsbeitrag. {klein[0].upper()}{klein[1:]} \"\n",
|
|
||||||
" f\"dagegen bringt nur {euro(wirkung[abweichler])} statt der rechnerischen \"\n",
|
|
||||||
" f\"{euro(preise[abweichler] * abs(delta_max))} - ab +{grenze_gueltig:.0f} \"\n",
|
|
||||||
" f\"Stunden begrenzt uns etwas anderes. Wer die ganze Schicht bezahlt, \"\n",
|
|
||||||
" f\"bezahlt die zweite Haelfte umsonst.\",\n",
|
|
||||||
" ]\n",
|
|
||||||
"\n",
|
|
||||||
" if schuldige and noetig is not None:\n",
|
|
||||||
" namen = \", \".join(f'\"{s}\"' for s in schuldige)\n",
|
|
||||||
" satz = (f\"Zum Wunsch des Vertriebs ({WUNSCH_MENGE:.0f} {WUNSCH_PRODUKT} statt \"\n",
|
|
||||||
" f\"{plan[WUNSCH_PRODUKT]:.1f}): Er ist mit der heutigen Ausstattung \"\n",
|
|
||||||
" f\"nicht erfuellbar - nicht aus Kostengruenden, sondern weil {namen} \"\n",
|
|
||||||
" f\"ihn gemeinsam blockieren. Mit +{noetig} Stunden bei \"\n",
|
|
||||||
" f'\"{engpass}\" wird er moeglich')\n",
|
|
||||||
" satz += (\", und er kostet dann nichts: Der Plan waehlt diese Menge von \"\n",
|
|
||||||
" \"sich aus.\" if wunsch_gratis else \".\")\n",
|
|
||||||
" absaetze.append(satz)\n",
|
|
||||||
"\n",
|
|
||||||
" for absatz in absaetze:\n",
|
|
||||||
" print()\n",
|
|
||||||
" print(textwrap.fill(absatz, width=76))\n",
|
|
||||||
" print(\"\\n\" + \"=\" * 78)"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
|
|
@ -1148,9 +826,9 @@
|
||||||
"\n",
|
"\n",
|
||||||
"from __future__ import annotations\n",
|
"from __future__ import annotations\n",
|
||||||
"\n",
|
"\n",
|
||||||
"import multiprocessing\n",
|
"import subprocess\n",
|
||||||
|
"import sys\n",
|
||||||
"import time\n",
|
"import time\n",
|
||||||
"from concurrent.futures import ProcessPoolExecutor\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"import numpy as np\n",
|
"import numpy as np\n",
|
||||||
"from pydantic import BaseModel, Field, model_validator\n",
|
"from pydantic import BaseModel, Field, model_validator\n",
|
||||||
|
|
@ -1353,29 +1031,25 @@
|
||||||
" if any(loesung.werte[problem.schluessel(i, j)] > 0.5 for j in range(m))]\n",
|
" if any(loesung.werte[problem.schluessel(i, j)] > 0.5 for j in range(m))]\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"def loese_in_eigenem_prozess(name: str, problem: Standortproblem) -> Loesung:\n",
|
"def loese_in_eigenem_prozess(name: str) -> Loesung:\n",
|
||||||
" \"\"\"Laesst genau einen Modellbauer in einem frischen Prozess rechnen.\n",
|
" \"\"\"Startet dieses Programm noch einmal - mit genau einem Solverimport.\"\"\"\n",
|
||||||
"\n",
|
" ergebnis = subprocess.run([sys.executable, __file__, name],\n",
|
||||||
" 'spawn' statt des Linux-Standards 'fork': Der Kindprozess startet mit\n",
|
" capture_output=True, text=True, timeout=300)\n",
|
||||||
" einem leeren Interpreter und importiert nur den Solver, den SEIN\n",
|
" if ergebnis.returncode != 0:\n",
|
||||||
" Modellbauer braucht. max_tasks_per_child=1 sorgt dafuer, dass der Pool\n",
|
" raise RuntimeError(ergebnis.stderr.strip().splitlines()[-1])\n",
|
||||||
" seinen Arbeiter nicht wiederverwendet - sonst saessen beim zweiten Aufruf\n",
|
" # Das DTO als JSON - genau dafuer ist ein Datenobjekt ohne Solverbezug gut.\n",
|
||||||
" wieder beide Bibliotheken im selben Prozess.\n",
|
" return Loesung.model_validate_json(ergebnis.stdout.strip().splitlines()[-1])\n",
|
||||||
"\n",
|
|
||||||
" Hin und zurueck wandert das Domaenenmodell bzw. das Loesungs-DTO. Beide\n",
|
|
||||||
" kennen keinen Solver, sind also serialisierbar - genau dafuer sind sie da.\n",
|
|
||||||
" \"\"\"\n",
|
|
||||||
" with ProcessPoolExecutor(\n",
|
|
||||||
" max_workers=1,\n",
|
|
||||||
" mp_context=multiprocessing.get_context(\"spawn\"),\n",
|
|
||||||
" max_tasks_per_child=1) as pool:\n",
|
|
||||||
" return pool.submit(MODELLBAUER[name], problem).result(timeout=300)\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"if __name__ == \"__main__\":\n",
|
"if __name__ == \"__main__\":\n",
|
||||||
" problem = beispielproblem()\n",
|
" problem = beispielproblem()\n",
|
||||||
"\n",
|
"\n",
|
||||||
" # --- Beide Solver anstossen und vergleichen ---------------------------\n",
|
" # --- Kindprozess: rechnen und das DTO als JSON ausgeben ---------------\n",
|
||||||
|
" if len(sys.argv) > 1:\n",
|
||||||
|
" print(MODELLBAUER[sys.argv[1]](problem).model_dump_json())\n",
|
||||||
|
" sys.exit(0)\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Hauptprozess: beide Solver anstossen und vergleichen -------------\n",
|
||||||
" print(\"=\" * 82)\n",
|
" print(\"=\" * 82)\n",
|
||||||
" print(\" DERSELBE FALL, ZWEI SOLVER - UND EIN AUSWERTUNGSCODE\")\n",
|
" print(\" DERSELBE FALL, ZWEI SOLVER - UND EIN AUSWERTUNGSCODE\")\n",
|
||||||
" print(\"=\" * 82)\n",
|
" print(\"=\" * 82)\n",
|
||||||
|
|
@ -1387,7 +1061,7 @@
|
||||||
" loesungen: dict[str, Loesung] = {}\n",
|
" loesungen: dict[str, Loesung] = {}\n",
|
||||||
" for name, beschriftung in [(\"cpsat\", \"OR-Tools CP-SAT\"),\n",
|
" for name, beschriftung in [(\"cpsat\", \"OR-Tools CP-SAT\"),\n",
|
||||||
" (\"highs\", \"HiGHS (highspy)\")]:\n",
|
" (\"highs\", \"HiGHS (highspy)\")]:\n",
|
||||||
" loesung = loesungen[name] = loese_in_eigenem_prozess(name, problem)\n",
|
" loesung = loesungen[name] = loese_in_eigenem_prozess(name)\n",
|
||||||
" beanstandungen = pruefe_zuordnung(problem, loesung)\n",
|
" beanstandungen = pruefe_zuordnung(problem, loesung)\n",
|
||||||
"\n",
|
"\n",
|
||||||
" print(f\"{beschriftung}\")\n",
|
" print(f\"{beschriftung}\")\n",
|
||||||
|
|
|
||||||
|
|
@ -593,124 +593,113 @@
|
||||||
"\n",
|
"\n",
|
||||||
"from __future__ import annotations\n",
|
"from __future__ import annotations\n",
|
||||||
"\n",
|
"\n",
|
||||||
"import multiprocessing\n",
|
"import json\n",
|
||||||
"import resource\n",
|
"import subprocess\n",
|
||||||
"import time\n",
|
"import sys\n",
|
||||||
"from concurrent.futures import ProcessPoolExecutor\n",
|
"import textwrap\n",
|
||||||
"\n",
|
|
||||||
"import numpy as np\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"GROESSEN = [(10, 10), (32, 32), (100, 100)] # (Lager, Kunden) -> 100 / 1.024 / 10.000 Variablen\n",
|
"GROESSEN = [(10, 10), (32, 32), (100, 100)] # (Lager, Kunden) -> 100 / 1.024 / 10.000 Variablen\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"# Instanz und Speichermessung stehen als gewoehnliche Funktionen hier - nicht\n",
|
"# Jeder Eintrag ist ein eigenstaendiges Programm: Instanz aufbauen, loesen,\n",
|
||||||
"# in einem String, den ein Kindprozess ausfuehrt. Jede Messfunktion baut die\n",
|
"# Ergebnis als JSON ausgeben. Die Instanz wird in jedem Kindprozess aus\n",
|
||||||
"# Instanz aus derselben Saat neu auf, damit ueber die Prozessgrenze nichts\n",
|
"# derselben Saat neu erzeugt - so reist nichts ueber die Prozessgrenze,\n",
|
||||||
"# reist, was das Ergebnis verfaelschen koennte.\n",
|
"# was das Ergebnis verfaelschen koennte.\n",
|
||||||
|
"VORSPANN = \"\"\"\n",
|
||||||
|
"import json, time, resource\n",
|
||||||
|
"import numpy as np\n",
|
||||||
"\n",
|
"\n",
|
||||||
"def instanz(m: int, n: int):\n",
|
"def instanz(m, n):\n",
|
||||||
" rng = np.random.default_rng(20)\n",
|
" rng = np.random.default_rng(20)\n",
|
||||||
" kosten = rng.integers(5, 95, (m, n)).astype(float)\n",
|
" kosten = rng.integers(5, 95, (m, n)).astype(float)\n",
|
||||||
" angebot = rng.integers(50, 150, m).astype(float)\n",
|
" angebot = rng.integers(50, 150, m).astype(float)\n",
|
||||||
" bedarf = angebot.sum() * rng.dirichlet(np.ones(n))\n",
|
" bedarf = angebot.sum() * rng.dirichlet(np.ones(n))\n",
|
||||||
" return kosten, angebot, bedarf\n",
|
" return kosten, angebot, bedarf\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"def speicher_mb():\n",
|
||||||
"def speicher_mb() -> float:\n",
|
" # ru_maxrss ist unter Linux in Kilobyte\n",
|
||||||
" # ru_maxrss ist unter Linux in Kilobyte. Gemessen wird der Kindprozess -\n",
|
|
||||||
" # deshalb muss jede Messung einen eigenen bekommen.\n",
|
|
||||||
" return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024\n",
|
" return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024\n",
|
||||||
"\n",
|
"\n",
|
||||||
|
"M, N = {m}, {n}\n",
|
||||||
|
"kosten, angebot, bedarf = instanz(M, N)\n",
|
||||||
|
"\"\"\"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"def messe_scipy(m: int, n: int):\n",
|
"ANSAETZE = {\n",
|
||||||
|
" \"scipy.linprog\": \"\"\"\n",
|
||||||
" from scipy.optimize import linprog\n",
|
" from scipy.optimize import linprog\n",
|
||||||
" kosten, angebot, bedarf = instanz(m, n)\n",
|
|
||||||
" t0 = time.perf_counter()\n",
|
" t0 = time.perf_counter()\n",
|
||||||
" c = kosten.reshape(-1)\n",
|
" c = kosten.reshape(-1)\n",
|
||||||
" A_ub = np.zeros((m, m * n)); A_eq = np.zeros((n, m * n))\n",
|
" A_ub = np.zeros((M, M * N)); A_eq = np.zeros((N, M * N))\n",
|
||||||
" for i in range(m):\n",
|
" for i in range(M):\n",
|
||||||
" A_ub[i, i * n:(i + 1) * n] = 1.0\n",
|
" A_ub[i, i * N:(i + 1) * N] = 1.0\n",
|
||||||
" for j in range(n):\n",
|
" for j in range(N):\n",
|
||||||
" A_eq[j, j::n] = 1.0\n",
|
" A_eq[j, j::N] = 1.0\n",
|
||||||
" aufbau = time.perf_counter() - t0\n",
|
" aufbau = time.perf_counter() - t0\n",
|
||||||
" t0 = time.perf_counter()\n",
|
" t0 = time.perf_counter()\n",
|
||||||
" r = linprog(c=c, A_ub=A_ub, b_ub=angebot, A_eq=A_eq, b_eq=bedarf,\n",
|
" r = linprog(c=c, A_ub=A_ub, b_ub=angebot, A_eq=A_eq, b_eq=bedarf,\n",
|
||||||
" bounds=(0, None), method=\"highs\")\n",
|
" bounds=(0, None), method=\"highs\")\n",
|
||||||
" loesen = time.perf_counter() - t0\n",
|
" loesen = time.perf_counter() - t0\n",
|
||||||
" return float(r.fun), aufbau, loesen, speicher_mb()\n",
|
" ausgabe = (float(r.fun), aufbau, loesen, speicher_mb())\n",
|
||||||
|
" \"\"\",\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
" \"highspy\": \"\"\"\n",
|
||||||
"def messe_highspy(m: int, n: int):\n",
|
|
||||||
" import highspy\n",
|
" import highspy\n",
|
||||||
" kosten, angebot, bedarf = instanz(m, n)\n",
|
|
||||||
" t0 = time.perf_counter()\n",
|
" t0 = time.perf_counter()\n",
|
||||||
" h = highspy.Highs(); h.setOptionValue(\"output_flag\", False)\n",
|
" h = highspy.Highs(); h.setOptionValue(\"output_flag\", False)\n",
|
||||||
" h.addVars(m * n, np.zeros(m * n), np.full(m * n, highspy.kHighsInf))\n",
|
" h.addVars(M * N, np.zeros(M * N), np.full(M * N, highspy.kHighsInf))\n",
|
||||||
" for k in range(m * n):\n",
|
" for k in range(M * N):\n",
|
||||||
" h.changeColCost(k, float(kosten.reshape(-1)[k]))\n",
|
" h.changeColCost(k, float(kosten.reshape(-1)[k]))\n",
|
||||||
" for i in range(m):\n",
|
" for i in range(M):\n",
|
||||||
" idx = np.arange(i * n, (i + 1) * n, dtype=np.int32)\n",
|
" idx = np.arange(i * N, (i + 1) * N, dtype=np.int32)\n",
|
||||||
" h.addRow(-highspy.kHighsInf, float(angebot[i]), n, idx, np.ones(n))\n",
|
" h.addRow(-highspy.kHighsInf, float(angebot[i]), N, idx, np.ones(N))\n",
|
||||||
" for j in range(n):\n",
|
" for j in range(N):\n",
|
||||||
" idx = np.arange(j, m * n, n, dtype=np.int32)\n",
|
" idx = np.arange(j, M * N, N, dtype=np.int32)\n",
|
||||||
" h.addRow(float(bedarf[j]), float(bedarf[j]), m, idx, np.ones(m))\n",
|
" h.addRow(float(bedarf[j]), float(bedarf[j]), M, idx, np.ones(M))\n",
|
||||||
" aufbau = time.perf_counter() - t0\n",
|
" aufbau = time.perf_counter() - t0\n",
|
||||||
" t0 = time.perf_counter(); h.run(); loesen = time.perf_counter() - t0\n",
|
" t0 = time.perf_counter(); h.run(); loesen = time.perf_counter() - t0\n",
|
||||||
" return h.getInfo().objective_function_value, aufbau, loesen, speicher_mb()\n",
|
" ausgabe = (h.getInfo().objective_function_value, aufbau, loesen, speicher_mb())\n",
|
||||||
|
" \"\"\",\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
" \"ortools/GLOP\": \"\"\"\n",
|
||||||
"def messe_ortools(m: int, n: int):\n",
|
|
||||||
" from ortools.linear_solver import pywraplp\n",
|
" from ortools.linear_solver import pywraplp\n",
|
||||||
" kosten, angebot, bedarf = instanz(m, n)\n",
|
|
||||||
" t0 = time.perf_counter()\n",
|
" t0 = time.perf_counter()\n",
|
||||||
" s = pywraplp.Solver.CreateSolver(\"GLOP\")\n",
|
" s = pywraplp.Solver.CreateSolver(\"GLOP\")\n",
|
||||||
" x = [[s.NumVar(0, s.infinity(), f\"x{i}_{j}\") for j in range(n)]\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",
|
||||||
" for i in range(m):\n",
|
" for i in range(M):\n",
|
||||||
" s.Add(sum(x[i]) <= float(angebot[i]))\n",
|
" s.Add(sum(x[i]) <= float(angebot[i]))\n",
|
||||||
" for j in range(n):\n",
|
" for j in range(N):\n",
|
||||||
" s.Add(sum(x[i][j] for i in range(m)) == float(bedarf[j]))\n",
|
" s.Add(sum(x[i][j] for i in range(M)) == float(bedarf[j]))\n",
|
||||||
" s.Minimize(sum(float(kosten[i, j]) * x[i][j]\n",
|
" s.Minimize(sum(float(kosten[i, j]) * x[i][j]\n",
|
||||||
" for i in range(m) for j in range(n)))\n",
|
" for i in range(M) for j in range(N)))\n",
|
||||||
" aufbau = time.perf_counter() - t0\n",
|
" aufbau = time.perf_counter() - t0\n",
|
||||||
" t0 = time.perf_counter(); s.Solve(); loesen = time.perf_counter() - t0\n",
|
" t0 = time.perf_counter(); s.Solve(); loesen = time.perf_counter() - t0\n",
|
||||||
" return s.Objective().Value(), aufbau, loesen, speicher_mb()\n",
|
" ausgabe = (s.Objective().Value(), aufbau, loesen, speicher_mb())\n",
|
||||||
|
" \"\"\",\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
" \"cvxpy\": \"\"\"\n",
|
||||||
"def messe_cvxpy(m: int, n: int):\n",
|
|
||||||
" import cvxpy as cp\n",
|
" import cvxpy as cp\n",
|
||||||
" kosten, angebot, bedarf = instanz(m, n)\n",
|
|
||||||
" t0 = time.perf_counter()\n",
|
" t0 = time.perf_counter()\n",
|
||||||
" x = cp.Variable((m, n), nonneg=True)\n",
|
" x = cp.Variable((M, N), nonneg=True)\n",
|
||||||
" problem = cp.Problem(cp.Minimize(cp.sum(cp.multiply(kosten, x))),\n",
|
" problem = cp.Problem(cp.Minimize(cp.sum(cp.multiply(kosten, x))),\n",
|
||||||
" [cp.sum(x, axis=1) <= angebot,\n",
|
" [cp.sum(x, axis=1) <= angebot,\n",
|
||||||
" cp.sum(x, axis=0) == bedarf])\n",
|
" cp.sum(x, axis=0) == bedarf])\n",
|
||||||
" aufbau = time.perf_counter() - t0\n",
|
" aufbau = time.perf_counter() - t0\n",
|
||||||
" t0 = time.perf_counter(); problem.solve(); loesen = time.perf_counter() - t0\n",
|
" t0 = time.perf_counter(); problem.solve(); loesen = time.perf_counter() - t0\n",
|
||||||
" return float(problem.value), aufbau, loesen, speicher_mb()\n",
|
" ausgabe = (float(problem.value), aufbau, loesen, speicher_mb())\n",
|
||||||
|
" \"\"\",\n",
|
||||||
|
"}\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"ANSAETZE = {\"scipy.linprog\": messe_scipy, \"highspy\": messe_highspy,\n",
|
"def messe(name: str, quelltext: str, m: int, n: int):\n",
|
||||||
" \"ortools/GLOP\": messe_ortools, \"cvxpy\": messe_cvxpy}\n",
|
" \"\"\"Fuehrt einen Ansatz in einem eigenen Prozess aus.\"\"\"\n",
|
||||||
"\n",
|
" programm = (VORSPANN.format(m=m, n=n) + textwrap.dedent(quelltext)\n",
|
||||||
"\n",
|
" + \"\\nprint(json.dumps(ausgabe))\\n\")\n",
|
||||||
"def messe(funktion, m: int, n: int):\n",
|
" ergebnis = subprocess.run([sys.executable, \"-c\", programm],\n",
|
||||||
" \"\"\"Fuehrt eine Messfunktion in einem FRISCHEN Prozess aus.\n",
|
" capture_output=True, text=True, timeout=600)\n",
|
||||||
"\n",
|
" if ergebnis.returncode != 0:\n",
|
||||||
" 'spawn' und max_tasks_per_child=1 zusammen garantieren, was Regel 1\n",
|
" return None, ergebnis.stderr.strip().splitlines()[-1][:60]\n",
|
||||||
" verlangt: Jede Messung sieht einen leeren Interpreter. Ohne das\n",
|
" return json.loads(ergebnis.stdout.strip().splitlines()[-1]), None\n",
|
||||||
" zweite wuerde der Pool seinen Arbeiter wiederverwenden - dann waere\n",
|
|
||||||
" der Speicherwert der zweiten Bibliothek um die erste zu hoch, und\n",
|
|
||||||
" ortools und highspy saessen im selben Prozess.\n",
|
|
||||||
" \"\"\"\n",
|
|
||||||
" with ProcessPoolExecutor(\n",
|
|
||||||
" max_workers=1,\n",
|
|
||||||
" mp_context=multiprocessing.get_context(\"spawn\"),\n",
|
|
||||||
" max_tasks_per_child=1) as pool:\n",
|
|
||||||
" try:\n",
|
|
||||||
" return pool.submit(funktion, m, n).result(timeout=600), None\n",
|
|
||||||
" except Exception as fehler:\n",
|
|
||||||
" return None, str(fehler).strip().splitlines()[-1][:60]\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"if __name__ == \"__main__\":\n",
|
"if __name__ == \"__main__\":\n",
|
||||||
|
|
@ -728,8 +717,8 @@
|
||||||
" f\"{'Loesen':>9} {'Anteil':>8} {'Speicher':>10}\")\n",
|
" f\"{'Loesen':>9} {'Anteil':>8} {'Speicher':>10}\")\n",
|
||||||
" print(\" \" + \"-\" * 72)\n",
|
" print(\" \" + \"-\" * 72)\n",
|
||||||
" zielwerte = {}\n",
|
" zielwerte = {}\n",
|
||||||
" for name, funktion in ANSAETZE.items():\n",
|
" for name, quelltext in ANSAETZE.items():\n",
|
||||||
" werte, fehler = messe(funktion, m, n)\n",
|
" werte, fehler = messe(name, quelltext, m, n)\n",
|
||||||
" if werte is None:\n",
|
" if werte is None:\n",
|
||||||
" print(f\" {name:<16} nicht verfuegbar: {fehler}\")\n",
|
" print(f\" {name:<16} nicht verfuegbar: {fehler}\")\n",
|
||||||
" continue\n",
|
" continue\n",
|
||||||
|
|
|
||||||
|
|
@ -496,218 +496,6 @@
|
||||||
" loese_jobshop()"
|
" loese_jobshop()"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"## Parallele Suche: was `num_workers` wirklich bewirkt\n",
|
|
||||||
"\n",
|
|
||||||
"`Parallele_Suche.py`\n"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": null,
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"#!/usr/bin/env python3\n",
|
|
||||||
"\n",
|
|
||||||
"# Parallele_Suche.py\n",
|
|
||||||
"\"\"\"\n",
|
|
||||||
"Kapitel CP-SAT: Was mehrere Arbeiter wirklich bringen - und was sie kosten.\n",
|
|
||||||
"\n",
|
|
||||||
"Das Buch setzt an mehreren Stellen 'num_workers = 1', damit die abgedruckte\n",
|
|
||||||
"Ausgabe reproduzierbar ist. Behauptet wird dabei zweierlei: dass parallele\n",
|
|
||||||
"Suche schneller ist, und dass sie von Lauf zu Lauf verschiedene, gleich gute\n",
|
|
||||||
"Loesungen findet. Beides wird hier gemessen statt geglaubt.\n",
|
|
||||||
"\n",
|
|
||||||
"Gerechnet wird auf einem Job-Shop wie in JobShop_Intervalle.py, nur gross\n",
|
|
||||||
"genug, dass die Suche wirklich arbeitet: 12 Auftraege auf 10 Maschinen,\n",
|
|
||||||
"jeder Auftrag besucht jede Maschine.\n",
|
|
||||||
"\n",
|
|
||||||
"Zwei Messungen:\n",
|
|
||||||
" 1. Dieselbe Aufgabe mit 1, 2, 4 und 8 Arbeitern - Laufzeit und Ergebnis.\n",
|
|
||||||
" 2. Dieselbe Konfiguration mehrfach, bei FESTEM random_seed - wie oft kommt\n",
|
|
||||||
" derselbe Plan heraus?\n",
|
|
||||||
"\n",
|
|
||||||
"Die zweite ist die wichtigere: Sie entscheidet, wie man Tests schreibt.\n",
|
|
||||||
"\n",
|
|
||||||
"Achtung, Laufzeiten sind hardwareabhaengig. Die ZAHL der verschiedenen\n",
|
|
||||||
"Plaene ist es auch - und genau das ist die Aussage.\n",
|
|
||||||
"\n",
|
|
||||||
"Benoetigt: numpy, ortools\n",
|
|
||||||
"\"\"\"\n",
|
|
||||||
"\n",
|
|
||||||
"from __future__ import annotations\n",
|
|
||||||
"\n",
|
|
||||||
"import collections\n",
|
|
||||||
"import time\n",
|
|
||||||
"\n",
|
|
||||||
"import numpy as np\n",
|
|
||||||
"from ortools.sat.python import cp_model\n",
|
|
||||||
"\n",
|
|
||||||
"AUFTRAEGE = 12\n",
|
|
||||||
"MASCHINEN = 10\n",
|
|
||||||
"INSTANZ_SEED = 20260908\n",
|
|
||||||
"SOLVER_SEED = 1 # bleibt ueber ALLE Laeufe gleich - das ist der Punkt\n",
|
|
||||||
"ARBEITERZAHLEN = (1, 2, 4, 8)\n",
|
|
||||||
"WIEDERHOLUNGEN = 4\n",
|
|
||||||
"ZEITLIMIT = 120.0\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"def baue_instanz() -> list[list[tuple[int, int]]]:\n",
|
|
||||||
" \"\"\"Klassischer Job-Shop: Jeder Auftrag besucht jede Maschine genau einmal,\n",
|
|
||||||
" in einer eigenen zufaelligen Reihenfolge.\"\"\"\n",
|
|
||||||
" rng = np.random.default_rng(INSTANZ_SEED)\n",
|
|
||||||
" return [[(int(m), int(rng.integers(2, 20))) for m in rng.permutation(MASCHINEN)]\n",
|
|
||||||
" for _ in range(AUFTRAEGE)]\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"def loese(auftraege, arbeiter: int):\n",
|
|
||||||
" \"\"\"Minimiert den Makespan. Gibt Status, Zielwert, Laufzeit und den Plan\n",
|
|
||||||
" zurueck - den Plan als Tupel aller Startzeiten, damit sich zwei Laeufe\n",
|
|
||||||
" vergleichen lassen.\"\"\"\n",
|
|
||||||
" horizont = sum(dauer for auftrag in auftraege for _, dauer in auftrag)\n",
|
|
||||||
" modell = cp_model.CpModel()\n",
|
|
||||||
" Gang = collections.namedtuple(\"Gang\", \"start ende intervall\")\n",
|
|
||||||
" plaene: dict[tuple[int, int], Gang] = {}\n",
|
|
||||||
" je_maschine = collections.defaultdict(list)\n",
|
|
||||||
"\n",
|
|
||||||
" for a, auftrag in enumerate(auftraege):\n",
|
|
||||||
" for g, (maschine, dauer) in enumerate(auftrag):\n",
|
|
||||||
" start = modell.NewIntVar(0, horizont, f\"start_{a}_{g}\")\n",
|
|
||||||
" ende = modell.NewIntVar(0, horizont, f\"ende_{a}_{g}\")\n",
|
|
||||||
" intervall = modell.NewIntervalVar(start, dauer, ende, f\"iv_{a}_{g}\")\n",
|
|
||||||
" plaene[a, g] = Gang(start, ende, intervall)\n",
|
|
||||||
" je_maschine[maschine].append(intervall)\n",
|
|
||||||
"\n",
|
|
||||||
" for maschine in range(MASCHINEN):\n",
|
|
||||||
" modell.AddNoOverlap(je_maschine[maschine])\n",
|
|
||||||
" for a, auftrag in enumerate(auftraege):\n",
|
|
||||||
" for g in range(len(auftrag) - 1):\n",
|
|
||||||
" modell.Add(plaene[a, g + 1].start >= plaene[a, g].ende)\n",
|
|
||||||
"\n",
|
|
||||||
" makespan = modell.NewIntVar(0, horizont, \"makespan\")\n",
|
|
||||||
" modell.AddMaxEquality(\n",
|
|
||||||
" makespan, [plaene[a, len(auftrag) - 1].ende\n",
|
|
||||||
" for a, auftrag in enumerate(auftraege)])\n",
|
|
||||||
" modell.Minimize(makespan)\n",
|
|
||||||
"\n",
|
|
||||||
" loeser = cp_model.CpSolver()\n",
|
|
||||||
" loeser.parameters.num_workers = arbeiter\n",
|
|
||||||
" loeser.parameters.random_seed = SOLVER_SEED\n",
|
|
||||||
" loeser.parameters.max_time_in_seconds = ZEITLIMIT\n",
|
|
||||||
"\n",
|
|
||||||
" beginn = time.perf_counter()\n",
|
|
||||||
" status = loeser.Solve(modell)\n",
|
|
||||||
" dauer = time.perf_counter() - beginn\n",
|
|
||||||
"\n",
|
|
||||||
" if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):\n",
|
|
||||||
" return loeser.StatusName(status), None, dauer, None\n",
|
|
||||||
" plan = tuple(loeser.Value(plaene[a, g].start)\n",
|
|
||||||
" for a, auftrag in enumerate(auftraege)\n",
|
|
||||||
" for g in range(len(auftrag)))\n",
|
|
||||||
" return loeser.StatusName(status), int(loeser.ObjectiveValue()), dauer, plan\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"if __name__ == \"__main__\":\n",
|
|
||||||
" auftraege = baue_instanz()\n",
|
|
||||||
" gaenge = sum(len(a) for a in auftraege)\n",
|
|
||||||
"\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(\" WAS MEHRERE ARBEITER WIRKLICH BRINGEN\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(f\"Job-Shop: {AUFTRAEGE} Auftraege, {MASCHINEN} Maschinen, \"\n",
|
|
||||||
" f\"{gaenge} Arbeitsgaenge.\")\n",
|
|
||||||
" print(f\"random_seed = {SOLVER_SEED} in JEDEM Lauf, \"\n",
|
|
||||||
" f\"{WIEDERHOLUNGEN} Wiederholungen je Arbeiterzahl.\\n\")\n",
|
|
||||||
"\n",
|
|
||||||
" # Jede Konfiguration mehrfach - ein einzelner Zeitwert sagt nichts.\n",
|
|
||||||
" ergebnisse: dict[int, list[tuple]] = {}\n",
|
|
||||||
" for arbeiter in ARBEITERZAHLEN:\n",
|
|
||||||
" ergebnisse[arbeiter] = [loese(auftraege, arbeiter)\n",
|
|
||||||
" for _ in range(WIEDERHOLUNGEN)]\n",
|
|
||||||
"\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(\" (1) Laufzeit: was die Arbeiter bringen\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(f\"{'Arbeiter':>9} {'Status':>9} {'Makespan':>9} \"\n",
|
|
||||||
" f\"{'schnellste':>11} {'langsamste':>11} {'Beschleunigung':>15}\")\n",
|
|
||||||
" print(\"-\" * 78)\n",
|
|
||||||
" basis_zeit = min(d for _, _, d, _ in ergebnisse[1])\n",
|
|
||||||
" for arbeiter in ARBEITERZAHLEN:\n",
|
|
||||||
" laeufe = ergebnisse[arbeiter]\n",
|
|
||||||
" zeiten = [d for _, _, d, _ in laeufe]\n",
|
|
||||||
" ziele = {o for _, o, _, _ in laeufe}\n",
|
|
||||||
" status = {s for s, _, _, _ in laeufe}\n",
|
|
||||||
" print(f\"{arbeiter:>9} {'/'.join(sorted(status)):>9} \"\n",
|
|
||||||
" f\"{'/'.join(str(z) for z in sorted(ziele)):>9} \"\n",
|
|
||||||
" f\"{min(zeiten):>10.2f}s {max(zeiten):>10.2f}s \"\n",
|
|
||||||
" f\"{basis_zeit / min(zeiten):>14.1f}x\")\n",
|
|
||||||
"\n",
|
|
||||||
" beste = min(ARBEITERZAHLEN, key=lambda w: min(d for _, _, d, _ in ergebnisse[w]))\n",
|
|
||||||
" faktor = basis_zeit / min(d for _, _, d, _ in ergebnisse[beste])\n",
|
|
||||||
" print(f\"\\n Am schnellsten sind {beste} Arbeiter: Faktor {faktor:.1f} gegenueber\")\n",
|
|
||||||
" print(f\" einem einzigen - also MEHR, als {beste} Kerne hergeben sollten.\")\n",
|
|
||||||
" print(f\" Das ist kein Messfehler. CP-SAT laesst nicht {beste}-mal dieselbe Suche\")\n",
|
|
||||||
" print(\" laufen, sondern verschiedene Strategien nebeneinander, die einander\")\n",
|
|
||||||
" print(\" ihre Schranken zurufen. Der zweite Arbeiter ist deshalb nicht der\")\n",
|
|
||||||
" print(\" 'zweite Kern', sondern ein anderes Verfahren.\")\n",
|
|
||||||
" letzte = ARBEITERZAHLEN[-1]\n",
|
|
||||||
" if beste != letzte:\n",
|
|
||||||
" zeit_letzte = min(d for _, _, d, _ in ergebnisse[letzte])\n",
|
|
||||||
" print(f\"\\n Und mehr ist nicht immer besser: {letzte} Arbeiter brauchen \"\n",
|
|
||||||
" f\"{zeit_letzte:.2f}s\")\n",
|
|
||||||
" print(f\" gegenueber {min(d for _, _, d, _ in ergebnisse[beste]):.2f}s bei \"\n",
|
|
||||||
" f\"{beste}. Ab einer gewissen Zahl kosten Abstimmung\")\n",
|
|
||||||
" print(\" und Speicherbandbreite mehr, als ein weiterer Suchstrang einbringt.\")\n",
|
|
||||||
"\n",
|
|
||||||
" print(\"\\n\" + \"=\" * 78)\n",
|
|
||||||
" print(\" (2) Reproduzierbarkeit: derselbe Seed, derselbe Plan?\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(f\"{'Arbeiter':>9} {'Laeufe':>7} {'verschiedene Plaene':>21} \"\n",
|
|
||||||
" f\"{'Makespan':>10}\")\n",
|
|
||||||
" print(\"-\" * 78)\n",
|
|
||||||
" for arbeiter in ARBEITERZAHLEN:\n",
|
|
||||||
" plaene = {p for _, _, _, p in ergebnisse[arbeiter] if p is not None}\n",
|
|
||||||
" ziele = {o for _, o, _, _ in ergebnisse[arbeiter]}\n",
|
|
||||||
" print(f\"{arbeiter:>9} {WIEDERHOLUNGEN:>7} {len(plaene):>21} \"\n",
|
|
||||||
" f\"{'/'.join(str(z) for z in sorted(ziele)):>10}\")\n",
|
|
||||||
"\n",
|
|
||||||
" einer = len({p for _, _, _, p in ergebnisse[1]})\n",
|
|
||||||
" # Ab welcher Arbeiterzahl bricht die Reproduzierbarkeit? Das ist die\n",
|
|
||||||
" # eigentliche Zahl - nicht, was acht Arbeiter anrichten.\n",
|
|
||||||
" ab = next((w for w in ARBEITERZAHLEN\n",
|
|
||||||
" if len({p for _, _, _, p in ergebnisse[w]}) > 1), None)\n",
|
|
||||||
" print(f\"\\n Mit einem Arbeiter: {einer} Plan aus {WIEDERHOLUNGEN} Laeufen.\")\n",
|
|
||||||
" if ab is not None:\n",
|
|
||||||
" viele = len({p for _, _, _, p in ergebnisse[ab]})\n",
|
|
||||||
" print(f\" Schon mit {ab} Arbeitern: {viele} verschiedene Plaene aus \"\n",
|
|
||||||
" f\"{WIEDERHOLUNGEN} Laeufen -\")\n",
|
|
||||||
" print(\" bei identischem random_seed und identischem Zielwert.\")\n",
|
|
||||||
" print(f\" Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit\")\n",
|
|
||||||
" print(f\" zu verlieren. {ab} genuegen.\")\n",
|
|
||||||
" print(\"\\n Der Seed allein macht einen Lauf also NICHT reproduzierbar. Er legt\")\n",
|
|
||||||
" print(\" fest, wie ein einzelner Suchstrang wuerfelt - nicht, welcher von\")\n",
|
|
||||||
" print(\" mehreren zuerst fertig wird. Das entscheidet die Uhr.\")\n",
|
|
||||||
"\n",
|
|
||||||
" print(\"\\n\" + \"=\" * 78)\n",
|
|
||||||
" print(\" (3) Was daraus fuer Tests folgt\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(\" Ein Test der Form assert plan == erwarteter_plan besteht mal und\")\n",
|
|
||||||
" print(\" scheitert mal, ohne dass sich am Code etwas geaendert haette.\")\n",
|
|
||||||
" print(\" Zu pruefen sind stattdessen:\")\n",
|
|
||||||
" print(\" * der ZIELWERT (hier in allen Laeufen gleich),\")\n",
|
|
||||||
" print(\" * die Einhaltung aller Regeln (Abnahmepruefung, Kapitel Praxisfallen),\")\n",
|
|
||||||
" print(\" * und der Status - nicht die Gestalt der Loesung.\")\n",
|
|
||||||
" print(\"\\n Wer doch einen bestimmten Plan braucht - fuer eine abgedruckte\")\n",
|
|
||||||
" print(\" Ausgabe, fuer einen Regressionstest -, setzt num_workers = 1.\")\n",
|
|
||||||
" print(f\" Das kostet hier den Faktor {faktor:.1f} an Laufzeit und ist genau\")\n",
|
|
||||||
" print(\" deshalb eine Entscheidung fuer den Test, nicht fuer den Betrieb.\")\n",
|
|
||||||
" print(\"=\" * 78)"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
|
|
|
||||||
|
|
@ -455,8 +455,7 @@
|
||||||
" exakt positiv semidefinit ist - rechne trotzdem.' Das ist hier bewusst so\n",
|
" exakt positiv semidefinit ist - rechne trotzdem.' Das ist hier bewusst so\n",
|
||||||
" gewaehlt, denn genau diese Eigenschaft wollen wir vorfuehren. Im\n",
|
" gewaehlt, denn genau diese Eigenschaft wollen wir vorfuehren. Im\n",
|
||||||
" Produktivbetrieb waere die DCP-Fehlermeldung das Warnsignal, dem man\n",
|
" Produktivbetrieb waere die DCP-Fehlermeldung das Warnsignal, dem man\n",
|
||||||
" nachgehen muss (siehe den Denkfehler im Kapitel Quadratische und\n",
|
" nachgehen muss (siehe Denkfehler 8.1).\n",
|
||||||
" nichtlineare Optimierung).\n",
|
|
||||||
" \"\"\"\n",
|
" \"\"\"\n",
|
||||||
" gewichte = cp.Variable(ANZAHL_ANLAGEN)\n",
|
" gewichte = cp.Variable(ANZAHL_ANLAGEN)\n",
|
||||||
" problem = cp.Problem(cp.Minimize(cp.quad_form(gewichte, cp.psd_wrap(kovarianz))),\n",
|
" problem = cp.Problem(cp.Minimize(cp.quad_form(gewichte, cp.psd_wrap(kovarianz))),\n",
|
||||||
|
|
|
||||||
|
|
@ -757,8 +757,7 @@
|
||||||
"\n",
|
"\n",
|
||||||
" loeser = cp_model.CpSolver()\n",
|
" loeser = cp_model.CpSolver()\n",
|
||||||
" loeser.parameters.max_time_in_seconds = zeitlimit\n",
|
" loeser.parameters.max_time_in_seconds = zeitlimit\n",
|
||||||
" # Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist -\n",
|
" # Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist.\n",
|
||||||
" # der Seed allein genuegt dafuer NICHT (Kapitel Constraint Programming).\n",
|
|
||||||
" # Im Produktivbetrieb laesst man beides auf den Standardwerten.\n",
|
" # Im Produktivbetrieb laesst man beides auf den Standardwerten.\n",
|
||||||
" loeser.parameters.num_workers = 1\n",
|
" loeser.parameters.num_workers = 1\n",
|
||||||
" loeser.parameters.random_seed = 1\n",
|
" loeser.parameters.random_seed = 1\n",
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"source": [
|
"source": [
|
||||||
"## Wie die Isolation aussieht, wenn sie tragen soll\n",
|
"## Ein System — vier Programmieransätze\n",
|
||||||
"\n",
|
"\n",
|
||||||
"`Ein_System_Vier_Ansaetze.py`\n"
|
"`Ein_System_Vier_Ansaetze.py`\n"
|
||||||
]
|
]
|
||||||
|
|
@ -136,104 +136,85 @@
|
||||||
" 2*x1 + 3*x2 + x3 <= 50\n",
|
" 2*x1 + 3*x2 + x3 <= 50\n",
|
||||||
" x >= 0\n",
|
" x >= 0\n",
|
||||||
"\n",
|
"\n",
|
||||||
"Deckt scipy.optimize, highspy, CVXPY und OR-Tools/GLOP ab, mit Kreuzvergleich\n",
|
"Deckt scipy.optimize, highspy, CVXPY und OR-Tools/GLOP ab, mit Kreuzvergleich am Ende.\n",
|
||||||
"am Ende.\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"WICHTIG: Jeder Solver laeuft in einem EIGENEN Prozess, weil sich ortools und\n",
|
"WICHTIG: Jeder Solver läuft in einem EIGENEN Prozess, weil sich ortools und\n",
|
||||||
"highspy auf vielen Systemen nicht gemeinsam importieren lassen (beide bringen\n",
|
"highspy auf vielen Systemen nicht gemeinsam importieren lassen (beide bringen\n",
|
||||||
"eine eigene HiGHS-Kopie mit -> Symbolkonflikt).\n",
|
"eine eigene HiGHS-Kopie mit -> Symbolkonflikt).\n",
|
||||||
"\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",
|
|
||||||
"\"\"\"\n",
|
"\"\"\"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"import multiprocessing\n",
|
"import json\n",
|
||||||
|
"import subprocess\n",
|
||||||
|
"import sys\n",
|
||||||
|
"import textwrap\n",
|
||||||
"import time\n",
|
"import time\n",
|
||||||
"from concurrent.futures import ProcessPoolExecutor\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"ERWARTET = 530.0 # Ergebnis der Handrechnung zum Produktionsprogramm\n",
|
"ERWARTET = 530.0 # Ergebnis der Handrechnung zum Produktionsprogramm\n",
|
||||||
"\n",
|
"\n",
|
||||||
"# Die Instanz - einmal notiert, von allen vier Funktionen benutzt.\n",
|
"# Jeder Eintrag ist ein eigenständiges Miniprogramm, das sein Ergebnis als\n",
|
||||||
"ZIEL = [10.0, 15.0, 25.0]\n",
|
"# JSON auf stdout ausgibt. So bleibt jeder Import in seinem eigenen Prozess.\n",
|
||||||
"MATRIX = [[1, 1, 2], [2, 3, 1]]\n",
|
"ANSAETZE: dict[str, str] = {\n",
|
||||||
"KAPAZITAET = [40.0, 50.0]\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
" \"scipy.optimize.linprog\": \"\"\"\n",
|
||||||
"def loese_mit_scipy() -> tuple[float, list[float]]:\n",
|
|
||||||
" from scipy.optimize import linprog\n",
|
" from scipy.optimize import linprog\n",
|
||||||
" ergebnis = linprog(c=[-w for w in ZIEL], # linprog MINIMIERT -> negieren\n",
|
" res = linprog(c=[-10.0, -15.0, -25.0], # linprog MINIMIERT -> negieren\n",
|
||||||
" A_ub=MATRIX, b_ub=KAPAZITAET,\n",
|
" A_ub=[[1, 1, 2], [2, 3, 1]], b_ub=[40, 50],\n",
|
||||||
" bounds=[(0, None)] * 3, method=\"highs\")\n",
|
" bounds=[(0, None)] * 3, method=\"highs\")\n",
|
||||||
" return -ergebnis.fun, list(ergebnis.x)\n",
|
" ausgabe = (-res.fun, list(res.x))\n",
|
||||||
|
" \"\"\",\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
" \"highspy (natives HiGHS)\": \"\"\"\n",
|
||||||
"def loese_mit_highspy() -> tuple[float, list[float]]:\n",
|
" import numpy as np, highspy\n",
|
||||||
" import highspy\n",
|
|
||||||
" import numpy as np\n",
|
|
||||||
" h = highspy.Highs()\n",
|
" h = highspy.Highs()\n",
|
||||||
" h.setOptionValue(\"output_flag\", False)\n",
|
" h.setOptionValue(\"output_flag\", False)\n",
|
||||||
" h.addVars(3, np.zeros(3), np.full(3, highspy.kHighsInf))\n",
|
" h.addVars(3, np.zeros(3), np.full(3, highspy.kHighsInf))\n",
|
||||||
" h.changeObjectiveSense(highspy.ObjSense.kMaximize)\n",
|
" h.changeObjectiveSense(highspy.ObjSense.kMaximize)\n",
|
||||||
" for j, wert in enumerate(ZIEL):\n",
|
" for j, wert in enumerate([10.0, 15.0, 25.0]):\n",
|
||||||
" h.changeColCost(j, wert)\n",
|
" h.changeColCost(j, wert)\n",
|
||||||
" # CSR-Format: starts[i] = Beginn von Zeile i in indices/values\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",
|
" h.addRows(2, np.full(2, -highspy.kHighsInf), np.array([40.0, 50.0]), 6,\n",
|
||||||
" np.array([0, 3], dtype=np.int32),\n",
|
" np.array([0, 3], dtype=np.int32),\n",
|
||||||
" np.array([0, 1, 2, 0, 1, 2], 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",
|
" np.array([1.0, 1.0, 2.0, 2.0, 3.0, 1.0]))\n",
|
||||||
" h.run()\n",
|
" h.run()\n",
|
||||||
" return (h.getInfo().objective_function_value,\n",
|
" ausgabe = (h.getInfo().objective_function_value,\n",
|
||||||
" list(h.getSolution().col_value[:3]))\n",
|
" list(h.getSolution().col_value[:3]))\n",
|
||||||
|
" \"\"\",\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
" \"cvxpy\": \"\"\"\n",
|
||||||
"def loese_mit_cvxpy() -> tuple[float, list[float]]:\n",
|
" import numpy as np, cvxpy as cp\n",
|
||||||
" import cvxpy as cp\n",
|
|
||||||
" import numpy as np\n",
|
|
||||||
" x = cp.Variable(3, nonneg=True)\n",
|
" x = cp.Variable(3, nonneg=True)\n",
|
||||||
" problem = cp.Problem(cp.Maximize(np.array(ZIEL) @ x),\n",
|
" problem = cp.Problem(cp.Maximize(np.array([10.0, 15.0, 25.0]) @ x),\n",
|
||||||
" [np.array(MATRIX) @ x <= np.array(KAPAZITAET)])\n",
|
" [np.array([[1, 1, 2], [2, 3, 1]]) @ x <= np.array([40, 50])])\n",
|
||||||
" problem.solve()\n",
|
" problem.solve()\n",
|
||||||
" return float(problem.value), [float(v) for v in x.value]\n",
|
" ausgabe = (float(problem.value), [float(v) for v in x.value])\n",
|
||||||
|
" \"\"\",\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
" \"ortools / GLOP\": \"\"\"\n",
|
||||||
"def loese_mit_ortools() -> tuple[float, list[float]]:\n",
|
|
||||||
" from ortools.linear_solver import pywraplp\n",
|
" from ortools.linear_solver import pywraplp\n",
|
||||||
" s = pywraplp.Solver.CreateSolver(\"GLOP\")\n",
|
" s = pywraplp.Solver.CreateSolver(\"GLOP\")\n",
|
||||||
" x = [s.NumVar(0, s.infinity(), f\"x{j+1}\") for j in range(3)]\n",
|
" x = [s.NumVar(0, s.infinity(), f\"x{j+1}\") for j in range(3)]\n",
|
||||||
" for i, kapazitaet in enumerate(KAPAZITAET):\n",
|
" A = [[1, 1, 2], [2, 3, 1]]\n",
|
||||||
" s.Add(sum(MATRIX[i][j] * x[j] for j in range(3)) <= kapazitaet)\n",
|
" for i, kap in enumerate([40, 50]):\n",
|
||||||
" s.Maximize(sum(ZIEL[j] * x[j] for j in range(3)))\n",
|
" s.Add(sum(A[i][j] * x[j] for j in range(3)) <= kap)\n",
|
||||||
|
" s.Maximize(10 * x[0] + 15 * x[1] + 25 * x[2])\n",
|
||||||
" s.Solve()\n",
|
" s.Solve()\n",
|
||||||
" return s.Objective().Value(), [v.solution_value() for v in x]\n",
|
" ausgabe = (s.Objective().Value(), [v.solution_value() for v in x])\n",
|
||||||
"\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",
|
|
||||||
"}\n",
|
"}\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
|
"def fuehre_in_eigenem_prozess_aus(quelltext: str) -> tuple[float, list[float]]:\n",
|
||||||
|
" \"\"\"Startet den Codeschnipsel als separaten Python-Prozess und liest das Ergebnis.\"\"\"\n",
|
||||||
|
" programm = textwrap.dedent(quelltext) + \"\\nimport json; print(json.dumps(ausgabe))\\n\"\n",
|
||||||
|
" ergebnis = subprocess.run([sys.executable, \"-c\", programm],\n",
|
||||||
|
" capture_output=True, text=True, timeout=120)\n",
|
||||||
|
" if ergebnis.returncode != 0:\n",
|
||||||
|
" raise RuntimeError(ergebnis.stderr.strip().splitlines()[-1])\n",
|
||||||
|
" wert, loesung = json.loads(ergebnis.stdout.strip().splitlines()[-1])\n",
|
||||||
|
" return wert, loesung\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
"if __name__ == \"__main__\":\n",
|
"if __name__ == \"__main__\":\n",
|
||||||
" print(\"=\" * 78)\n",
|
" print(\"=\" * 78)\n",
|
||||||
" print(\" EIN SYSTEM - VIER ANSAETZE (je eigener Prozess)\")\n",
|
" print(\" EIN SYSTEM - VIER ANSAETZE (je eigener Prozess)\")\n",
|
||||||
|
|
@ -242,20 +223,14 @@
|
||||||
" print(\"-\" * 78)\n",
|
" print(\"-\" * 78)\n",
|
||||||
"\n",
|
"\n",
|
||||||
" werte = []\n",
|
" werte = []\n",
|
||||||
" # Ein Pool, vier Aufgaben, vier frische Prozesse. Der Kontext muss\n",
|
" for name, quelltext in ANSAETZE.items():\n",
|
||||||
" # \"spawn\" sein - siehe Modulkommentar.\n",
|
" t0 = time.perf_counter()\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",
|
" try:\n",
|
||||||
" wert, x = pool.submit(funktion).result(timeout=120)\n",
|
" wert, x = fuehre_in_eigenem_prozess_aus(quelltext)\n",
|
||||||
" except Exception as fehler: # Bibliothek fehlt o. Ae.\n",
|
" except RuntimeError as fehler:\n",
|
||||||
" print(f\"{name:<26} nicht verfuegbar: {str(fehler)[:40]}\")\n",
|
" print(f\"{name:<26} nicht verfuegbar: {fehler[:40]}\")\n",
|
||||||
" continue\n",
|
" continue\n",
|
||||||
" dauer = time.perf_counter() - beginn\n",
|
" dauer = time.perf_counter() - t0\n",
|
||||||
" werte.append(wert)\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",
|
" 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",
|
" f\"{dauer:>8.2f} s\")\n",
|
||||||
|
|
@ -268,8 +243,8 @@
|
||||||
" assert spanne < 1e-6, \"Die Bibliotheken widersprechen sich!\"\n",
|
" assert spanne < 1e-6, \"Die Bibliotheken widersprechen sich!\"\n",
|
||||||
" assert abs(werte[0] - ERWARTET) < 1e-6, \"Ergebnis weicht von der Handrechnung ab!\"\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",
|
" print(\"Alle Wege fuehren zum selben, von Hand bestaetigten Optimum.\")\n",
|
||||||
" print(\"(Die Zeiten enthalten Prozessstart und Import - sie messen NICHT die\")\n",
|
" print(\"(Die Zeiten enthalten den Prozessstart und den Import - sie messen\")\n",
|
||||||
" print(\" reine Solverleistung. Die Uebungsaufgabe 'Laufzeitvergleich' trennt beides.)\")\n",
|
" print(\" NICHT die reine Solverleistung, siehe Uebung 3.5.)\")\n",
|
||||||
" print(\"=\" * 78)"
|
" print(\"=\" * 78)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -252,328 +252,6 @@
|
||||||
" print(\"=\" * 78)"
|
" print(\"=\" * 78)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"## Constraint Attribution: welche Bedingung kostet wie viel?\n",
|
|
||||||
"\n",
|
|
||||||
"`Constraint_Attribution.py`\n"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": null,
|
|
||||||
"metadata": {},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"#!/usr/bin/env python3\n",
|
|
||||||
"\n",
|
|
||||||
"# Constraint_Attribution.py\n",
|
|
||||||
"\"\"\"\n",
|
|
||||||
"Kapitel Praxisfallen: Welche Bedingung kostet wie viel - und was sagt man\n",
|
|
||||||
"dem Management?\n",
|
|
||||||
"\n",
|
|
||||||
"Zwischen den beiden bekannten Faellen klafft eine Luecke:\n",
|
|
||||||
"\n",
|
|
||||||
" * Erklaerbarkeit.py beantwortet \"warum diese eine Zuweisung?\"\n",
|
|
||||||
" * Konfliktsuche.py (Anhang Fehlerdiagnose) beantwortet \"warum geht es gar\n",
|
|
||||||
" nicht?\"\n",
|
|
||||||
"\n",
|
|
||||||
"Der haeufigste Fall in der Praxis liegt dazwischen: Das Modell rechnet, der\n",
|
|
||||||
"Plan ist zulaessig - und trotzdem unbefriedigend. Niemand weiss, WORAN es\n",
|
|
||||||
"liegt. Genau das beantwortet Constraint Attribution.\n",
|
|
||||||
"\n",
|
|
||||||
"Gerechnet wird auf derselben Fabrik wie in Konfliktsuche.py, nur nach der\n",
|
|
||||||
"Reparatur: Dort war die Lackierkapazitaet von 150 Stunden der Kern des\n",
|
|
||||||
"Widerspruchs (2*40 + 3*30 = 170 > 150). Eine zweite Schicht hat sie auf 210\n",
|
|
||||||
"gebracht, und der Mindestumsatz wurde auf 10.000 EUR gesenkt. Jetzt ist das\n",
|
|
||||||
"Modell loesbar - und die Frage lautet nicht mehr \"warum nicht?\", sondern\n",
|
|
||||||
"\"was kostet uns was?\".\n",
|
|
||||||
"\n",
|
|
||||||
"Gezeigt werden vier Dinge:\n",
|
|
||||||
" 1. Welche Bedingungen binden - und dass \"bindend\" nicht \"teuer\" heisst.\n",
|
|
||||||
" 2. Dass der Schattenpreis eine MOMENTAUFNAHME ist: hochgerechnet auf eine\n",
|
|
||||||
" realistische Sonderschicht verspricht er hier das Doppelte des\n",
|
|
||||||
" tatsaechlichen Nutzens.\n",
|
|
||||||
" 3. Dass ein Wunsch aus dem Vertrieb nicht teuer, sondern unmoeglich sein\n",
|
|
||||||
" kann - und welche Bedingungen ihn gemeinsam blockieren.\n",
|
|
||||||
" 4. Wie aus alledem ein Bericht in Alltagssprache wird.\n",
|
|
||||||
"\n",
|
|
||||||
"Benoetigt: numpy, scipy\n",
|
|
||||||
"\"\"\"\n",
|
|
||||||
"\n",
|
|
||||||
"from __future__ import annotations\n",
|
|
||||||
"\n",
|
|
||||||
"import textwrap\n",
|
|
||||||
"\n",
|
|
||||||
"import numpy as np\n",
|
|
||||||
"from scipy.optimize import linprog\n",
|
|
||||||
"\n",
|
|
||||||
"PRODUKTE = [\"Rahmen\", \"Gehaeuse\", \"Deckel\", \"Traeger\", \"Halter\"]\n",
|
|
||||||
"DECKUNGSBEITRAG = np.array([35.0, 28.0, 9.0, 22.0, 6.0]) # EUR je Stueck\n",
|
|
||||||
"\n",
|
|
||||||
"# Dieselben Bedingungen wie in Konfliktsuche.py, mit den beiden Reparaturen.\n",
|
|
||||||
"# Der sprechende Name ist keine Kosmetik: Ohne ihn ist kein Bericht moeglich,\n",
|
|
||||||
"# der jemandem ausserhalb der Modellierung etwas sagt.\n",
|
|
||||||
"BEDINGUNGEN: list[tuple[str, list[float], str, float]] = [\n",
|
|
||||||
" (\"Kapazitaet Montage\", [3, 2, 1, 4, 2], \"<=\", 400),\n",
|
|
||||||
" (\"Kapazitaet Lackieren\", [2, 3, 0, 1, 0], \"<=\", 210),\n",
|
|
||||||
" (\"Kapazitaet Pruefung\", [1, 1, 1, 1, 1], \"<=\", 300),\n",
|
|
||||||
" (\"Liefervertrag Rahmen\", [1, 0, 0, 0, 0], \">=\", 40),\n",
|
|
||||||
" (\"Liefervertrag Gehaeuse\", [0, 1, 0, 0, 0], \">=\", 30),\n",
|
|
||||||
" (\"Liefervertrag Deckel\", [0, 0, 1, 0, 0], \">=\", 20),\n",
|
|
||||||
" (\"Liefervertrag Traeger\", [0, 0, 0, 1, 0], \">=\", 25),\n",
|
|
||||||
" (\"Marktgrenze Rahmen\", [1, 0, 0, 0, 0], \"<=\", 120),\n",
|
|
||||||
" (\"Marktgrenze Gehaeuse\", [0, 1, 0, 0, 0], \"<=\", 90),\n",
|
|
||||||
" (\"Marktgrenze Deckel\", [0, 0, 1, 0, 0], \"<=\", 150),\n",
|
|
||||||
" (\"Marktgrenze Halter\", [0, 0, 0, 0, 1], \"<=\", 200),\n",
|
|
||||||
" (\"Mindestumsatz\", [90, 70, 30, 60, 20], \">=\", 10000),\n",
|
|
||||||
" (\"Sortimentsbreite\", [0, 0, 1, 1, 1], \">=\", 100),\n",
|
|
||||||
" (\"Lackierbudget Schicht 2\", [0, 1, 0, 1, 0], \"<=\", 55),\n",
|
|
||||||
"]\n",
|
|
||||||
"\n",
|
|
||||||
"# Was liesse sich im Ernstfall wirklich bewegen - und um wie viel? Nicht \"eine\n",
|
|
||||||
"# Einheit\", sondern der Schritt, den ein Planer tatsaechlich gehen kann: eine\n",
|
|
||||||
"# Sonderschicht, ein nachverhandelter Vertrag. Genau an dieser Groesse\n",
|
|
||||||
"# scheitert das blosse Hochrechnen des Schattenpreises.\n",
|
|
||||||
"STELLSCHRAUBEN: dict[str, tuple[float, str]] = {\n",
|
|
||||||
" \"Kapazitaet Montage\": (+50, \"eine Sonderschicht Montage\"),\n",
|
|
||||||
" \"Kapazitaet Lackieren\": (+30, \"eine Sonderschicht Lackieren\"),\n",
|
|
||||||
" \"Kapazitaet Pruefung\": (+50, \"eine Sonderschicht Pruefung\"),\n",
|
|
||||||
" \"Lackierbudget Schicht 2\": (+10, \"10 Stunden mehr Budget in Schicht 2\"),\n",
|
|
||||||
" \"Liefervertrag Traeger\": (-10, \"10 Stueck weniger Abnahmepflicht Traeger\"),\n",
|
|
||||||
" \"Liefervertrag Gehaeuse\": (-10, \"10 Stueck weniger Abnahmepflicht Gehaeuse\"),\n",
|
|
||||||
"}\n",
|
|
||||||
"\n",
|
|
||||||
"WUNSCH_MENGE = 55.0 # der Vertrieb haette gern mehr Rahmen\n",
|
|
||||||
"WUNSCH_PRODUKT = \"Rahmen\"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"def loese(bedingungen: list[tuple[str, list[float], str, float]]):\n",
|
|
||||||
" \"\"\"Maximiert den Deckungsbeitrag. linprog minimiert, daher das Vorzeichen.\"\"\"\n",
|
|
||||||
" matrix, rechte_seite = [], []\n",
|
|
||||||
" for _, koeffizienten, richtung, grenze in bedingungen:\n",
|
|
||||||
" zeile = np.array(koeffizienten, dtype=float)\n",
|
|
||||||
" matrix.append(zeile if richtung == \"<=\" else -zeile)\n",
|
|
||||||
" rechte_seite.append(grenze if richtung == \"<=\" else -grenze)\n",
|
|
||||||
" return linprog(-DECKUNGSBEITRAG,\n",
|
|
||||||
" A_ub=np.array(matrix), b_ub=np.array(rechte_seite),\n",
|
|
||||||
" bounds=[(0, None)] * len(PRODUKTE), method=\"highs\")\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"def geaendert(name: str, delta: float,\n",
|
|
||||||
" basis=None) -> list[tuple[str, list[float], str, float]]:\n",
|
|
||||||
" \"\"\"Dieselben Bedingungen, eine davon um delta verschoben.\"\"\"\n",
|
|
||||||
" return [(n, k, r, g + delta if n == name else g)\n",
|
|
||||||
" for n, k, r, g in (basis or BEDINGUNGEN)]\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"def auswertung(ergebnis, bedingungen=None):\n",
|
|
||||||
" \"\"\"Schlupf und Schattenpreis je Bedingung.\"\"\"\n",
|
|
||||||
" x = ergebnis.x\n",
|
|
||||||
" dual = ergebnis.ineqlin.marginals\n",
|
|
||||||
" zeilen = []\n",
|
|
||||||
" for i, (name, koeff, richtung, grenze) in enumerate(bedingungen or BEDINGUNGEN):\n",
|
|
||||||
" links = float(np.array(koeff, dtype=float) @ x)\n",
|
|
||||||
" schlupf = (grenze - links) if richtung == \"<=\" else (links - grenze)\n",
|
|
||||||
" zeilen.append((name, links, richtung, grenze, schlupf, abs(dual[i])))\n",
|
|
||||||
" return zeilen\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"def blockierer(bedingungen, zusatz) -> list[str]:\n",
|
|
||||||
" \"\"\"Welche Bedingungen verhindern 'zusatz' - einzeln weggelassen.\n",
|
|
||||||
"\n",
|
|
||||||
" Dasselbe Vorgehen wie der Deletion Filter in Konfliktsuche.py, nur mit\n",
|
|
||||||
" umgekehrtem Vorzeichen: Dort wird gesucht, was den Widerspruch AUSMACHT,\n",
|
|
||||||
" hier, was einen Wunsch VERHINDERT. Wird das Modell ohne eine Bedingung\n",
|
|
||||||
" loesbar, gehoert sie zum Konflikt.\n",
|
|
||||||
" \"\"\"\n",
|
|
||||||
" gefunden = []\n",
|
|
||||||
" for i, (name, _, _, _) in enumerate(bedingungen):\n",
|
|
||||||
" rest = [c for j, c in enumerate(bedingungen) if j != i] + [zusatz]\n",
|
|
||||||
" if loese(rest).success:\n",
|
|
||||||
" gefunden.append(name)\n",
|
|
||||||
" return gefunden\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"def euro(betrag: float) -> str:\n",
|
|
||||||
" return f\"{betrag:,.2f} EUR\"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"if __name__ == \"__main__\":\n",
|
|
||||||
" basis = loese(BEDINGUNGEN)\n",
|
|
||||||
" db_basis = -basis.fun\n",
|
|
||||||
" plan = dict(zip(PRODUKTE, basis.x))\n",
|
|
||||||
"\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(\" CONSTRAINT ATTRIBUTION: Was kostet uns welche Bedingung?\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(f\"Status: {basis.message.split('(')[0].strip()}\")\n",
|
|
||||||
" print(f\"Deckungsbeitrag des Plans: {euro(db_basis)}\\n\")\n",
|
|
||||||
" print(\" \" + \" \".join(f\"{p}: {m:.1f}\" for p, m in plan.items()))\n",
|
|
||||||
"\n",
|
|
||||||
" # --- 1. Wer begrenzt den Plan? -----------------------------------------\n",
|
|
||||||
" print(\"\\n\" + \"=\" * 78)\n",
|
|
||||||
" print(\" (1) Welche Bedingungen binden - und was kostet die naechste Einheit?\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(f\"{'Bedingung':<24} {'genutzt':>9} {'Grenze':>7} {'Schlupf':>8} \"\n",
|
|
||||||
" f\"{'Schattenpreis':>11}\")\n",
|
|
||||||
" print(\"-\" * 78)\n",
|
|
||||||
" bindend = []\n",
|
|
||||||
" for name, links, richtung, grenze, schlupf, dual in auswertung(basis):\n",
|
|
||||||
" marke = \"\"\n",
|
|
||||||
" if abs(schlupf) < 1e-6:\n",
|
|
||||||
" bindend.append((name, dual))\n",
|
|
||||||
" marke = \" <-- bindend\"\n",
|
|
||||||
" print(f\"{name:<24} {links:>9.1f} {grenze:>7.0f} {schlupf:>8.1f} \"\n",
|
|
||||||
" f\"{dual:>11.2f}{marke}\")\n",
|
|
||||||
"\n",
|
|
||||||
" ohne_preis = [n for n, d in bindend if d < 1e-9]\n",
|
|
||||||
" print(f\"\\n {len(bindend)} Bedingungen binden. Aber: \"\n",
|
|
||||||
" f\"{', '.join(ohne_preis)} bindet\")\n",
|
|
||||||
" print(\" mit einem Schattenpreis von 0,00 EUR - sie beruehrt den Plan, ohne ihn\")\n",
|
|
||||||
" print(\" zu verteuern. Bindend und kostenrelevant sind zwei verschiedene Dinge.\")\n",
|
|
||||||
"\n",
|
|
||||||
" # --- 2. Der Schattenpreis als Momentaufnahme ---------------------------\n",
|
|
||||||
" print(\"\\n\" + \"=\" * 78)\n",
|
|
||||||
" print(\" (2) Was der Schattenpreis verschweigt\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" print(\"Der Schattenpreis gilt fuer die NAECHSTE Einheit. Ein Planer kauft aber\")\n",
|
|
||||||
" print(\"keine Einheit, sondern eine ganze Schicht. Beides gegenuebergestellt:\\n\")\n",
|
|
||||||
" preise = {name: dual for name, *_, dual in auswertung(basis)}\n",
|
|
||||||
" print(f\"{'Stellschraube':<24} {'Preis':>9} {'Schritt':>7} \"\n",
|
|
||||||
" f\"{'hochgerechnet':>13} {'gemessen':>9} {'Abw.':>9}\")\n",
|
|
||||||
" print(\"-\" * 78)\n",
|
|
||||||
" wirkung = {}\n",
|
|
||||||
" for name, (delta, _) in STELLSCHRAUBEN.items():\n",
|
|
||||||
" ergebnis = loese(geaendert(name, delta))\n",
|
|
||||||
" echt = (-ergebnis.fun) - db_basis if ergebnis.success else 0.0\n",
|
|
||||||
" linear = preise[name] * abs(delta)\n",
|
|
||||||
" wirkung[name] = echt\n",
|
|
||||||
" print(f\"{name:<24} {preise[name]:>9.2f} {delta:>+7.0f} \"\n",
|
|
||||||
" f\"{linear:>13.2f} {echt:>9.2f} {echt - linear:>+9.2f}\")\n",
|
|
||||||
"\n",
|
|
||||||
" # Wo endet der Gueltigkeitsbereich? Schrittweise nachfahren.\n",
|
|
||||||
" abweichler = max(STELLSCHRAUBEN,\n",
|
|
||||||
" key=lambda n: preise[n] * abs(STELLSCHRAUBEN[n][0]) - wirkung[n])\n",
|
|
||||||
" delta_max = STELLSCHRAUBEN[abweichler][0]\n",
|
|
||||||
" grenze_gueltig = 0.0\n",
|
|
||||||
" schritt = delta_max / 60.0\n",
|
|
||||||
" vorher = db_basis\n",
|
|
||||||
" for i in range(1, 61):\n",
|
|
||||||
" d = schritt * i\n",
|
|
||||||
" e = loese(geaendert(abweichler, d))\n",
|
|
||||||
" db = -e.fun if e.success else vorher\n",
|
|
||||||
" if db - vorher > 1e-9:\n",
|
|
||||||
" grenze_gueltig = d\n",
|
|
||||||
" vorher = db\n",
|
|
||||||
" print(f\"\\n '{abweichler}': hochgerechnet \"\n",
|
|
||||||
" f\"{preise[abweichler] * abs(delta_max):.2f} EUR, tatsaechlich \"\n",
|
|
||||||
" f\"{wirkung[abweichler]:.2f} EUR.\")\n",
|
|
||||||
" print(f\" Der Preis von {preise[abweichler]:.2f} EUR gilt nur bis \"\n",
|
|
||||||
" f\"+{grenze_gueltig:.0f} Stunden. Jede weitere Stunde\")\n",
|
|
||||||
" print(\" dieser Sonderschicht bringt exakt nichts - der Engpass ist dann ein anderer.\")\n",
|
|
||||||
"\n",
|
|
||||||
" nach_preis = sorted(STELLSCHRAUBEN, key=lambda n: -preise[n])[0]\n",
|
|
||||||
" nach_wirkung = max(wirkung, key=wirkung.get)\n",
|
|
||||||
" print(f\"\\n Und die Ranglisten drehen sich: Nach Schattenpreis fuehrt\")\n",
|
|
||||||
" print(f\" '{nach_preis}' ({preise[nach_preis]:.2f} EUR je Einheit),\")\n",
|
|
||||||
" print(f\" nach tatsaechlicher Wirkung '{nach_wirkung}' \"\n",
|
|
||||||
" f\"({wirkung[nach_wirkung]:.2f} EUR).\")\n",
|
|
||||||
" print(\" Der Schattenpreis sagt, was eine Einheit wert ist - nicht, wie viele\")\n",
|
|
||||||
" print(\" Einheiten zu haben sind.\")\n",
|
|
||||||
"\n",
|
|
||||||
" # --- 3. Der Wunsch aus dem Vertrieb ------------------------------------\n",
|
|
||||||
" print(\"\\n\" + \"=\" * 78)\n",
|
|
||||||
" print(f\" (3) Was-waere-wenn: mindestens {WUNSCH_MENGE:.0f} \"\n",
|
|
||||||
" f\"{WUNSCH_PRODUKT} statt {plan[WUNSCH_PRODUKT]:.1f}\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
" wunsch = (f\"Wunsch Vertrieb {WUNSCH_PRODUKT}\",\n",
|
|
||||||
" [1 if p == WUNSCH_PRODUKT else 0 for p in PRODUKTE], \">=\", WUNSCH_MENGE)\n",
|
|
||||||
" mit_wunsch = loese(BEDINGUNGEN + [wunsch])\n",
|
|
||||||
" schuldige: list[str] = []\n",
|
|
||||||
" engpass = noetig = None\n",
|
|
||||||
" wunsch_gratis = False\n",
|
|
||||||
"\n",
|
|
||||||
" if mit_wunsch.success:\n",
|
|
||||||
" print(f\"Erfuellbar, kostet {euro(db_basis - (-mit_wunsch.fun))}.\")\n",
|
|
||||||
" else:\n",
|
|
||||||
" schuldige = blockierer(BEDINGUNGEN, wunsch)\n",
|
|
||||||
" print(\"Der Wunsch ist nicht teuer - er ist UNMOEGLICH.\\n\")\n",
|
|
||||||
" print(f\"Blockiert wird er von {len(schuldige)} Bedingungen gemeinsam. \"\n",
|
|
||||||
" f\"Faellt eine davon\")\n",
|
|
||||||
" print(\"weg, ist er erfuellbar; keine allein ist 'der' Grund:\")\n",
|
|
||||||
" for name in schuldige:\n",
|
|
||||||
" e = loese([c for c in BEDINGUNGEN if c[0] != name] + [wunsch])\n",
|
|
||||||
" print(f\" ohne '{name}': moeglich, Deckungsbeitrag {euro(-e.fun)}\")\n",
|
|
||||||
"\n",
|
|
||||||
" engpass = next((n for n in schuldige if n in STELLSCHRAUBEN), schuldige[0])\n",
|
|
||||||
" for schritt in range(1, 61):\n",
|
|
||||||
" if loese(geaendert(engpass, schritt) + [wunsch]).success:\n",
|
|
||||||
" noetig = schritt\n",
|
|
||||||
" break\n",
|
|
||||||
" if noetig is not None:\n",
|
|
||||||
" e = loese(geaendert(engpass, noetig) + [wunsch])\n",
|
|
||||||
" ohne = loese(geaendert(engpass, noetig))\n",
|
|
||||||
" wunsch_gratis = abs((-e.fun) - (-ohne.fun)) < 1e-6\n",
|
|
||||||
" print(f\"\\n Mit +{noetig} bei '{engpass}' wird der Wunsch erfuellbar:\")\n",
|
|
||||||
" print(f\" Deckungsbeitrag {euro(-e.fun)} \"\n",
|
|
||||||
" f\"({-e.fun - db_basis:+,.2f} gegenueber heute).\")\n",
|
|
||||||
" if wunsch_gratis:\n",
|
|
||||||
" print(f\" Und ohne den Wunsch ergaebe dieselbe Lockerung \"\n",
|
|
||||||
" f\"{euro(-ohne.fun)} bei\")\n",
|
|
||||||
" print(f\" {ohne.x[PRODUKTE.index(WUNSCH_PRODUKT)]:.0f} \"\n",
|
|
||||||
" f\"{WUNSCH_PRODUKT} - der Wunsch kostet also NICHTS.\")\n",
|
|
||||||
" print(\" Er war nie das Problem. Der Engpass war es.\")\n",
|
|
||||||
"\n",
|
|
||||||
" # --- 4. Der Bericht ----------------------------------------------------\n",
|
|
||||||
" # Aus denselben Zahlen, die oben in Tabellen stehen, wird hier Prosa. Kein\n",
|
|
||||||
" # Satz enthaelt eine Zahl, die nicht vorher gerechnet wurde - das ist der\n",
|
|
||||||
" # ganze Trick und zugleich die Bedingung dafuer, dass der Bericht nicht\n",
|
|
||||||
" # irgendwann leise falsch wird.\n",
|
|
||||||
" print(\"\\n\" + \"=\" * 78)\n",
|
|
||||||
" print(\" (4) Und so steht es in der Vorlage fuer die Abteilungsleiterrunde\")\n",
|
|
||||||
" print(\"=\" * 78)\n",
|
|
||||||
"\n",
|
|
||||||
" teuerste = sorted(((n, d) for n, d in bindend if d > 0), key=lambda t: -t[1])\n",
|
|
||||||
" gross = STELLSCHRAUBEN[nach_wirkung][1]\n",
|
|
||||||
" klein = STELLSCHRAUBEN[abweichler][1]\n",
|
|
||||||
"\n",
|
|
||||||
" absaetze = [\n",
|
|
||||||
" f\"Der Plan bringt {euro(db_basis)} Deckungsbeitrag. Begrenzt wird er von \"\n",
|
|
||||||
" f\"{len(bindend)} Bedingungen, von denen {len(teuerste)} tatsaechlich Geld \"\n",
|
|
||||||
" f\"kosten; \\\"{ohne_preis[0]}\\\" beruehrt den Plan, ohne ihn zu verteuern.\",\n",
|
|
||||||
"\n",
|
|
||||||
" f\"Teuerste Bindung ist \\\"{teuerste[0][0]}\\\" mit \"\n",
|
|
||||||
" f\"{teuerste[0][1]:.2f} EUR je Einheit, gefolgt von \\\"{teuerste[1][0]}\\\" \"\n",
|
|
||||||
" f\"mit {teuerste[1][1]:.2f} EUR.\",\n",
|
|
||||||
"\n",
|
|
||||||
" f\"Die groesste einzelne Verbesserung bringt {gross}: \"\n",
|
|
||||||
" f\"{euro(wirkung[nach_wirkung])} mehr Deckungsbeitrag. {klein[0].upper()}{klein[1:]} \"\n",
|
|
||||||
" f\"dagegen bringt nur {euro(wirkung[abweichler])} statt der rechnerischen \"\n",
|
|
||||||
" f\"{euro(preise[abweichler] * abs(delta_max))} - ab +{grenze_gueltig:.0f} \"\n",
|
|
||||||
" f\"Stunden begrenzt uns etwas anderes. Wer die ganze Schicht bezahlt, \"\n",
|
|
||||||
" f\"bezahlt die zweite Haelfte umsonst.\",\n",
|
|
||||||
" ]\n",
|
|
||||||
"\n",
|
|
||||||
" if schuldige and noetig is not None:\n",
|
|
||||||
" namen = \", \".join(f'\"{s}\"' for s in schuldige)\n",
|
|
||||||
" satz = (f\"Zum Wunsch des Vertriebs ({WUNSCH_MENGE:.0f} {WUNSCH_PRODUKT} statt \"\n",
|
|
||||||
" f\"{plan[WUNSCH_PRODUKT]:.1f}): Er ist mit der heutigen Ausstattung \"\n",
|
|
||||||
" f\"nicht erfuellbar - nicht aus Kostengruenden, sondern weil {namen} \"\n",
|
|
||||||
" f\"ihn gemeinsam blockieren. Mit +{noetig} Stunden bei \"\n",
|
|
||||||
" f'\"{engpass}\" wird er moeglich')\n",
|
|
||||||
" satz += (\", und er kostet dann nichts: Der Plan waehlt diese Menge von \"\n",
|
|
||||||
" \"sich aus.\" if wunsch_gratis else \".\")\n",
|
|
||||||
" absaetze.append(satz)\n",
|
|
||||||
"\n",
|
|
||||||
" for absatz in absaetze:\n",
|
|
||||||
" print()\n",
|
|
||||||
" print(textwrap.fill(absatz, width=76))\n",
|
|
||||||
" print(\"\\n\" + \"=\" * 78)"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
|
|
@ -1148,9 +826,9 @@
|
||||||
"\n",
|
"\n",
|
||||||
"from __future__ import annotations\n",
|
"from __future__ import annotations\n",
|
||||||
"\n",
|
"\n",
|
||||||
"import multiprocessing\n",
|
"import subprocess\n",
|
||||||
|
"import sys\n",
|
||||||
"import time\n",
|
"import time\n",
|
||||||
"from concurrent.futures import ProcessPoolExecutor\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"import numpy as np\n",
|
"import numpy as np\n",
|
||||||
"from pydantic import BaseModel, Field, model_validator\n",
|
"from pydantic import BaseModel, Field, model_validator\n",
|
||||||
|
|
@ -1353,29 +1031,25 @@
|
||||||
" if any(loesung.werte[problem.schluessel(i, j)] > 0.5 for j in range(m))]\n",
|
" if any(loesung.werte[problem.schluessel(i, j)] > 0.5 for j in range(m))]\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"def loese_in_eigenem_prozess(name: str, problem: Standortproblem) -> Loesung:\n",
|
"def loese_in_eigenem_prozess(name: str) -> Loesung:\n",
|
||||||
" \"\"\"Laesst genau einen Modellbauer in einem frischen Prozess rechnen.\n",
|
" \"\"\"Startet dieses Programm noch einmal - mit genau einem Solverimport.\"\"\"\n",
|
||||||
"\n",
|
" ergebnis = subprocess.run([sys.executable, __file__, name],\n",
|
||||||
" 'spawn' statt des Linux-Standards 'fork': Der Kindprozess startet mit\n",
|
" capture_output=True, text=True, timeout=300)\n",
|
||||||
" einem leeren Interpreter und importiert nur den Solver, den SEIN\n",
|
" if ergebnis.returncode != 0:\n",
|
||||||
" Modellbauer braucht. max_tasks_per_child=1 sorgt dafuer, dass der Pool\n",
|
" raise RuntimeError(ergebnis.stderr.strip().splitlines()[-1])\n",
|
||||||
" seinen Arbeiter nicht wiederverwendet - sonst saessen beim zweiten Aufruf\n",
|
" # Das DTO als JSON - genau dafuer ist ein Datenobjekt ohne Solverbezug gut.\n",
|
||||||
" wieder beide Bibliotheken im selben Prozess.\n",
|
" return Loesung.model_validate_json(ergebnis.stdout.strip().splitlines()[-1])\n",
|
||||||
"\n",
|
|
||||||
" Hin und zurueck wandert das Domaenenmodell bzw. das Loesungs-DTO. Beide\n",
|
|
||||||
" kennen keinen Solver, sind also serialisierbar - genau dafuer sind sie da.\n",
|
|
||||||
" \"\"\"\n",
|
|
||||||
" with ProcessPoolExecutor(\n",
|
|
||||||
" max_workers=1,\n",
|
|
||||||
" mp_context=multiprocessing.get_context(\"spawn\"),\n",
|
|
||||||
" max_tasks_per_child=1) as pool:\n",
|
|
||||||
" return pool.submit(MODELLBAUER[name], problem).result(timeout=300)\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"if __name__ == \"__main__\":\n",
|
"if __name__ == \"__main__\":\n",
|
||||||
" problem = beispielproblem()\n",
|
" problem = beispielproblem()\n",
|
||||||
"\n",
|
"\n",
|
||||||
" # --- Beide Solver anstossen und vergleichen ---------------------------\n",
|
" # --- Kindprozess: rechnen und das DTO als JSON ausgeben ---------------\n",
|
||||||
|
" if len(sys.argv) > 1:\n",
|
||||||
|
" print(MODELLBAUER[sys.argv[1]](problem).model_dump_json())\n",
|
||||||
|
" sys.exit(0)\n",
|
||||||
|
"\n",
|
||||||
|
" # --- Hauptprozess: beide Solver anstossen und vergleichen -------------\n",
|
||||||
" print(\"=\" * 82)\n",
|
" print(\"=\" * 82)\n",
|
||||||
" print(\" DERSELBE FALL, ZWEI SOLVER - UND EIN AUSWERTUNGSCODE\")\n",
|
" print(\" DERSELBE FALL, ZWEI SOLVER - UND EIN AUSWERTUNGSCODE\")\n",
|
||||||
" print(\"=\" * 82)\n",
|
" print(\"=\" * 82)\n",
|
||||||
|
|
@ -1387,7 +1061,7 @@
|
||||||
" loesungen: dict[str, Loesung] = {}\n",
|
" loesungen: dict[str, Loesung] = {}\n",
|
||||||
" for name, beschriftung in [(\"cpsat\", \"OR-Tools CP-SAT\"),\n",
|
" for name, beschriftung in [(\"cpsat\", \"OR-Tools CP-SAT\"),\n",
|
||||||
" (\"highs\", \"HiGHS (highspy)\")]:\n",
|
" (\"highs\", \"HiGHS (highspy)\")]:\n",
|
||||||
" loesung = loesungen[name] = loese_in_eigenem_prozess(name, problem)\n",
|
" loesung = loesungen[name] = loese_in_eigenem_prozess(name)\n",
|
||||||
" beanstandungen = pruefe_zuordnung(problem, loesung)\n",
|
" beanstandungen = pruefe_zuordnung(problem, loesung)\n",
|
||||||
"\n",
|
"\n",
|
||||||
" print(f\"{beschriftung}\")\n",
|
" print(f\"{beschriftung}\")\n",
|
||||||
|
|
|
||||||
|
|
@ -593,124 +593,113 @@
|
||||||
"\n",
|
"\n",
|
||||||
"from __future__ import annotations\n",
|
"from __future__ import annotations\n",
|
||||||
"\n",
|
"\n",
|
||||||
"import multiprocessing\n",
|
"import json\n",
|
||||||
"import resource\n",
|
"import subprocess\n",
|
||||||
"import time\n",
|
"import sys\n",
|
||||||
"from concurrent.futures import ProcessPoolExecutor\n",
|
"import textwrap\n",
|
||||||
"\n",
|
|
||||||
"import numpy as np\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"GROESSEN = [(10, 10), (32, 32), (100, 100)] # (Lager, Kunden) -> 100 / 1.024 / 10.000 Variablen\n",
|
"GROESSEN = [(10, 10), (32, 32), (100, 100)] # (Lager, Kunden) -> 100 / 1.024 / 10.000 Variablen\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"# Instanz und Speichermessung stehen als gewoehnliche Funktionen hier - nicht\n",
|
"# Jeder Eintrag ist ein eigenstaendiges Programm: Instanz aufbauen, loesen,\n",
|
||||||
"# in einem String, den ein Kindprozess ausfuehrt. Jede Messfunktion baut die\n",
|
"# Ergebnis als JSON ausgeben. Die Instanz wird in jedem Kindprozess aus\n",
|
||||||
"# Instanz aus derselben Saat neu auf, damit ueber die Prozessgrenze nichts\n",
|
"# derselben Saat neu erzeugt - so reist nichts ueber die Prozessgrenze,\n",
|
||||||
"# reist, was das Ergebnis verfaelschen koennte.\n",
|
"# was das Ergebnis verfaelschen koennte.\n",
|
||||||
|
"VORSPANN = \"\"\"\n",
|
||||||
|
"import json, time, resource\n",
|
||||||
|
"import numpy as np\n",
|
||||||
"\n",
|
"\n",
|
||||||
"def instanz(m: int, n: int):\n",
|
"def instanz(m, n):\n",
|
||||||
" rng = np.random.default_rng(20)\n",
|
" rng = np.random.default_rng(20)\n",
|
||||||
" kosten = rng.integers(5, 95, (m, n)).astype(float)\n",
|
" kosten = rng.integers(5, 95, (m, n)).astype(float)\n",
|
||||||
" angebot = rng.integers(50, 150, m).astype(float)\n",
|
" angebot = rng.integers(50, 150, m).astype(float)\n",
|
||||||
" bedarf = angebot.sum() * rng.dirichlet(np.ones(n))\n",
|
" bedarf = angebot.sum() * rng.dirichlet(np.ones(n))\n",
|
||||||
" return kosten, angebot, bedarf\n",
|
" return kosten, angebot, bedarf\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"def speicher_mb():\n",
|
||||||
"def speicher_mb() -> float:\n",
|
" # ru_maxrss ist unter Linux in Kilobyte\n",
|
||||||
" # ru_maxrss ist unter Linux in Kilobyte. Gemessen wird der Kindprozess -\n",
|
|
||||||
" # deshalb muss jede Messung einen eigenen bekommen.\n",
|
|
||||||
" return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024\n",
|
" return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024\n",
|
||||||
"\n",
|
"\n",
|
||||||
|
"M, N = {m}, {n}\n",
|
||||||
|
"kosten, angebot, bedarf = instanz(M, N)\n",
|
||||||
|
"\"\"\"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"def messe_scipy(m: int, n: int):\n",
|
"ANSAETZE = {\n",
|
||||||
|
" \"scipy.linprog\": \"\"\"\n",
|
||||||
" from scipy.optimize import linprog\n",
|
" from scipy.optimize import linprog\n",
|
||||||
" kosten, angebot, bedarf = instanz(m, n)\n",
|
|
||||||
" t0 = time.perf_counter()\n",
|
" t0 = time.perf_counter()\n",
|
||||||
" c = kosten.reshape(-1)\n",
|
" c = kosten.reshape(-1)\n",
|
||||||
" A_ub = np.zeros((m, m * n)); A_eq = np.zeros((n, m * n))\n",
|
" A_ub = np.zeros((M, M * N)); A_eq = np.zeros((N, M * N))\n",
|
||||||
" for i in range(m):\n",
|
" for i in range(M):\n",
|
||||||
" A_ub[i, i * n:(i + 1) * n] = 1.0\n",
|
" A_ub[i, i * N:(i + 1) * N] = 1.0\n",
|
||||||
" for j in range(n):\n",
|
" for j in range(N):\n",
|
||||||
" A_eq[j, j::n] = 1.0\n",
|
" A_eq[j, j::N] = 1.0\n",
|
||||||
" aufbau = time.perf_counter() - t0\n",
|
" aufbau = time.perf_counter() - t0\n",
|
||||||
" t0 = time.perf_counter()\n",
|
" t0 = time.perf_counter()\n",
|
||||||
" r = linprog(c=c, A_ub=A_ub, b_ub=angebot, A_eq=A_eq, b_eq=bedarf,\n",
|
" r = linprog(c=c, A_ub=A_ub, b_ub=angebot, A_eq=A_eq, b_eq=bedarf,\n",
|
||||||
" bounds=(0, None), method=\"highs\")\n",
|
" bounds=(0, None), method=\"highs\")\n",
|
||||||
" loesen = time.perf_counter() - t0\n",
|
" loesen = time.perf_counter() - t0\n",
|
||||||
" return float(r.fun), aufbau, loesen, speicher_mb()\n",
|
" ausgabe = (float(r.fun), aufbau, loesen, speicher_mb())\n",
|
||||||
|
" \"\"\",\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
" \"highspy\": \"\"\"\n",
|
||||||
"def messe_highspy(m: int, n: int):\n",
|
|
||||||
" import highspy\n",
|
" import highspy\n",
|
||||||
" kosten, angebot, bedarf = instanz(m, n)\n",
|
|
||||||
" t0 = time.perf_counter()\n",
|
" t0 = time.perf_counter()\n",
|
||||||
" h = highspy.Highs(); h.setOptionValue(\"output_flag\", False)\n",
|
" h = highspy.Highs(); h.setOptionValue(\"output_flag\", False)\n",
|
||||||
" h.addVars(m * n, np.zeros(m * n), np.full(m * n, highspy.kHighsInf))\n",
|
" h.addVars(M * N, np.zeros(M * N), np.full(M * N, highspy.kHighsInf))\n",
|
||||||
" for k in range(m * n):\n",
|
" for k in range(M * N):\n",
|
||||||
" h.changeColCost(k, float(kosten.reshape(-1)[k]))\n",
|
" h.changeColCost(k, float(kosten.reshape(-1)[k]))\n",
|
||||||
" for i in range(m):\n",
|
" for i in range(M):\n",
|
||||||
" idx = np.arange(i * n, (i + 1) * n, dtype=np.int32)\n",
|
" idx = np.arange(i * N, (i + 1) * N, dtype=np.int32)\n",
|
||||||
" h.addRow(-highspy.kHighsInf, float(angebot[i]), n, idx, np.ones(n))\n",
|
" h.addRow(-highspy.kHighsInf, float(angebot[i]), N, idx, np.ones(N))\n",
|
||||||
" for j in range(n):\n",
|
" for j in range(N):\n",
|
||||||
" idx = np.arange(j, m * n, n, dtype=np.int32)\n",
|
" idx = np.arange(j, M * N, N, dtype=np.int32)\n",
|
||||||
" h.addRow(float(bedarf[j]), float(bedarf[j]), m, idx, np.ones(m))\n",
|
" h.addRow(float(bedarf[j]), float(bedarf[j]), M, idx, np.ones(M))\n",
|
||||||
" aufbau = time.perf_counter() - t0\n",
|
" aufbau = time.perf_counter() - t0\n",
|
||||||
" t0 = time.perf_counter(); h.run(); loesen = time.perf_counter() - t0\n",
|
" t0 = time.perf_counter(); h.run(); loesen = time.perf_counter() - t0\n",
|
||||||
" return h.getInfo().objective_function_value, aufbau, loesen, speicher_mb()\n",
|
" ausgabe = (h.getInfo().objective_function_value, aufbau, loesen, speicher_mb())\n",
|
||||||
|
" \"\"\",\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
" \"ortools/GLOP\": \"\"\"\n",
|
||||||
"def messe_ortools(m: int, n: int):\n",
|
|
||||||
" from ortools.linear_solver import pywraplp\n",
|
" from ortools.linear_solver import pywraplp\n",
|
||||||
" kosten, angebot, bedarf = instanz(m, n)\n",
|
|
||||||
" t0 = time.perf_counter()\n",
|
" t0 = time.perf_counter()\n",
|
||||||
" s = pywraplp.Solver.CreateSolver(\"GLOP\")\n",
|
" s = pywraplp.Solver.CreateSolver(\"GLOP\")\n",
|
||||||
" x = [[s.NumVar(0, s.infinity(), f\"x{i}_{j}\") for j in range(n)]\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",
|
||||||
" for i in range(m):\n",
|
" for i in range(M):\n",
|
||||||
" s.Add(sum(x[i]) <= float(angebot[i]))\n",
|
" s.Add(sum(x[i]) <= float(angebot[i]))\n",
|
||||||
" for j in range(n):\n",
|
" for j in range(N):\n",
|
||||||
" s.Add(sum(x[i][j] for i in range(m)) == float(bedarf[j]))\n",
|
" s.Add(sum(x[i][j] for i in range(M)) == float(bedarf[j]))\n",
|
||||||
" s.Minimize(sum(float(kosten[i, j]) * x[i][j]\n",
|
" s.Minimize(sum(float(kosten[i, j]) * x[i][j]\n",
|
||||||
" for i in range(m) for j in range(n)))\n",
|
" for i in range(M) for j in range(N)))\n",
|
||||||
" aufbau = time.perf_counter() - t0\n",
|
" aufbau = time.perf_counter() - t0\n",
|
||||||
" t0 = time.perf_counter(); s.Solve(); loesen = time.perf_counter() - t0\n",
|
" t0 = time.perf_counter(); s.Solve(); loesen = time.perf_counter() - t0\n",
|
||||||
" return s.Objective().Value(), aufbau, loesen, speicher_mb()\n",
|
" ausgabe = (s.Objective().Value(), aufbau, loesen, speicher_mb())\n",
|
||||||
|
" \"\"\",\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
" \"cvxpy\": \"\"\"\n",
|
||||||
"def messe_cvxpy(m: int, n: int):\n",
|
|
||||||
" import cvxpy as cp\n",
|
" import cvxpy as cp\n",
|
||||||
" kosten, angebot, bedarf = instanz(m, n)\n",
|
|
||||||
" t0 = time.perf_counter()\n",
|
" t0 = time.perf_counter()\n",
|
||||||
" x = cp.Variable((m, n), nonneg=True)\n",
|
" x = cp.Variable((M, N), nonneg=True)\n",
|
||||||
" problem = cp.Problem(cp.Minimize(cp.sum(cp.multiply(kosten, x))),\n",
|
" problem = cp.Problem(cp.Minimize(cp.sum(cp.multiply(kosten, x))),\n",
|
||||||
" [cp.sum(x, axis=1) <= angebot,\n",
|
" [cp.sum(x, axis=1) <= angebot,\n",
|
||||||
" cp.sum(x, axis=0) == bedarf])\n",
|
" cp.sum(x, axis=0) == bedarf])\n",
|
||||||
" aufbau = time.perf_counter() - t0\n",
|
" aufbau = time.perf_counter() - t0\n",
|
||||||
" t0 = time.perf_counter(); problem.solve(); loesen = time.perf_counter() - t0\n",
|
" t0 = time.perf_counter(); problem.solve(); loesen = time.perf_counter() - t0\n",
|
||||||
" return float(problem.value), aufbau, loesen, speicher_mb()\n",
|
" ausgabe = (float(problem.value), aufbau, loesen, speicher_mb())\n",
|
||||||
|
" \"\"\",\n",
|
||||||
|
"}\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"ANSAETZE = {\"scipy.linprog\": messe_scipy, \"highspy\": messe_highspy,\n",
|
"def messe(name: str, quelltext: str, m: int, n: int):\n",
|
||||||
" \"ortools/GLOP\": messe_ortools, \"cvxpy\": messe_cvxpy}\n",
|
" \"\"\"Fuehrt einen Ansatz in einem eigenen Prozess aus.\"\"\"\n",
|
||||||
"\n",
|
" programm = (VORSPANN.format(m=m, n=n) + textwrap.dedent(quelltext)\n",
|
||||||
"\n",
|
" + \"\\nprint(json.dumps(ausgabe))\\n\")\n",
|
||||||
"def messe(funktion, m: int, n: int):\n",
|
" ergebnis = subprocess.run([sys.executable, \"-c\", programm],\n",
|
||||||
" \"\"\"Fuehrt eine Messfunktion in einem FRISCHEN Prozess aus.\n",
|
" capture_output=True, text=True, timeout=600)\n",
|
||||||
"\n",
|
" if ergebnis.returncode != 0:\n",
|
||||||
" 'spawn' und max_tasks_per_child=1 zusammen garantieren, was Regel 1\n",
|
" return None, ergebnis.stderr.strip().splitlines()[-1][:60]\n",
|
||||||
" verlangt: Jede Messung sieht einen leeren Interpreter. Ohne das\n",
|
" return json.loads(ergebnis.stdout.strip().splitlines()[-1]), None\n",
|
||||||
" zweite wuerde der Pool seinen Arbeiter wiederverwenden - dann waere\n",
|
|
||||||
" der Speicherwert der zweiten Bibliothek um die erste zu hoch, und\n",
|
|
||||||
" ortools und highspy saessen im selben Prozess.\n",
|
|
||||||
" \"\"\"\n",
|
|
||||||
" with ProcessPoolExecutor(\n",
|
|
||||||
" max_workers=1,\n",
|
|
||||||
" mp_context=multiprocessing.get_context(\"spawn\"),\n",
|
|
||||||
" max_tasks_per_child=1) as pool:\n",
|
|
||||||
" try:\n",
|
|
||||||
" return pool.submit(funktion, m, n).result(timeout=600), None\n",
|
|
||||||
" except Exception as fehler:\n",
|
|
||||||
" return None, str(fehler).strip().splitlines()[-1][:60]\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"if __name__ == \"__main__\":\n",
|
"if __name__ == \"__main__\":\n",
|
||||||
|
|
@ -728,8 +717,8 @@
|
||||||
" f\"{'Loesen':>9} {'Anteil':>8} {'Speicher':>10}\")\n",
|
" f\"{'Loesen':>9} {'Anteil':>8} {'Speicher':>10}\")\n",
|
||||||
" print(\" \" + \"-\" * 72)\n",
|
" print(\" \" + \"-\" * 72)\n",
|
||||||
" zielwerte = {}\n",
|
" zielwerte = {}\n",
|
||||||
" for name, funktion in ANSAETZE.items():\n",
|
" for name, quelltext in ANSAETZE.items():\n",
|
||||||
" werte, fehler = messe(funktion, m, n)\n",
|
" werte, fehler = messe(name, quelltext, m, n)\n",
|
||||||
" if werte is None:\n",
|
" if werte is None:\n",
|
||||||
" print(f\" {name:<16} nicht verfuegbar: {fehler}\")\n",
|
" print(f\" {name:<16} nicht verfuegbar: {fehler}\")\n",
|
||||||
" continue\n",
|
" continue\n",
|
||||||
|
|
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
||||||
window.OR_PROGRAMME = ["Backtest_Fallen.py", "Bausteine_Vorlage.py", "Bellman_Minimalbeispiel.py", "Benchmark_Skalierung.py", "Betriebsueberwachung.py", "Big_M_Falle.py", "Bot_Allokation.py", "Brute_Force_Vergleich.py", "CP_SAT_Statusfaelle.py", "CP_SAT_Vertretungssystem.py", "CVaR_Portfolio.py", "Chance_Constraints.py", "Constraint_Attribution.py", "Data_Snooping.py", "Diversifikation_Demo.py", "Dualitaet_Nachweis.py", "Ein_System_Vier_Ansaetze.py", "Entropie_Maximierte_Allokation.py", "Erklaerbarkeit.py", "Excel_Bruecke.py", "Finanzdaten_Ledoit_Wolf.py", "Fluch_des_Durchschnitts.py", "Infeasibility_Diagnose.py", "Installationstest.py", "JobShop_Intervalle.py", "KKT_Nachweis.py", "Konfliktsuche.py", "Konvexitaet_Demo.py", "Kovarianz_Falle.py", "Kraftwerkseinsatz.py", "Large_Neighborhood_Search.py", "Lokale_Optima_Multistart.py", "MILP_Portfolio_Fixgebuehren.py", "Markowitz_CVXPY.py", "Matrixform.py", "Mehrperiodige_Order_Execution.py", "Mehrziel_Pareto.py", "Metaheuristik_vs_Exakt.py", "Min_Cost_Flow.py", "Modellierungsschichten.py", "Monte_Carlo.py", "Mutationstest.py", "Optimierungsdienst.py", "Parallele_Suche.py", "Predict_then_Optimize.py", "Propagation_Demo.py", "QP_Grundlagen.py", "QuantitativeTradingEngine.py", "Renditen_Vergleich.py", "Renditeschaetzung_Falle.py", "Robuste_Optimierung.py", "Rucksack.py", "Runden_Gegenbeispiel.py", "Schaetzrauschen_Demo.py", "Sensitivitaetsanalyse.py", "Simplex_Tableau_LP.py", "Simulated_Annealing.py", "Skalierung_Kondition.py", "Solver_Wahl.py", "Solverstatus_und_Gap.py", "Solverwechsel_CPSAT_HiGHS.py", "Spaltengenerierung.py", "Stochastische_Optimierung.py", "Strafgewichte.py", "Strukturbruecke.py", "Toleranzen_und_Entartung.py", "VRP_Flotten_Routing.py", "VRP_Kapazitaetsfalle.py", "VaR_CVaR_Demo.py", "Vektorisierte_Modellgenerierung.py", "Visualisierung_Loesungsraum.py", "Vom_Wunsch_zum_Modell.py", "Warmstart_Effekt.py", "Zuordnung_Ungarisch.py", "or_kern.py", "test_or_kern.py"];
|
window.OR_PROGRAMME = ["Backtest_Fallen.py", "Bausteine_Vorlage.py", "Bellman_Minimalbeispiel.py", "Benchmark_Skalierung.py", "Betriebsueberwachung.py", "Big_M_Falle.py", "Bot_Allokation.py", "Brute_Force_Vergleich.py", "CP_SAT_Statusfaelle.py", "CP_SAT_Vertretungssystem.py", "CVaR_Portfolio.py", "Chance_Constraints.py", "Data_Snooping.py", "Diversifikation_Demo.py", "Dualitaet_Nachweis.py", "Ein_System_Vier_Ansaetze.py", "Entropie_Maximierte_Allokation.py", "Erklaerbarkeit.py", "Excel_Bruecke.py", "Finanzdaten_Ledoit_Wolf.py", "Fluch_des_Durchschnitts.py", "Infeasibility_Diagnose.py", "Installationstest.py", "JobShop_Intervalle.py", "KKT_Nachweis.py", "Konfliktsuche.py", "Konvexitaet_Demo.py", "Kovarianz_Falle.py", "Kraftwerkseinsatz.py", "Large_Neighborhood_Search.py", "Lokale_Optima_Multistart.py", "MILP_Portfolio_Fixgebuehren.py", "Markowitz_CVXPY.py", "Matrixform.py", "Mehrperiodige_Order_Execution.py", "Mehrziel_Pareto.py", "Metaheuristik_vs_Exakt.py", "Min_Cost_Flow.py", "Modellierungsschichten.py", "Monte_Carlo.py", "Mutationstest.py", "Optimierungsdienst.py", "Predict_then_Optimize.py", "Propagation_Demo.py", "QP_Grundlagen.py", "QuantitativeTradingEngine.py", "Renditen_Vergleich.py", "Renditeschaetzung_Falle.py", "Robuste_Optimierung.py", "Rucksack.py", "Runden_Gegenbeispiel.py", "Schaetzrauschen_Demo.py", "Sensitivitaetsanalyse.py", "Simplex_Tableau_LP.py", "Simulated_Annealing.py", "Skalierung_Kondition.py", "Solver_Wahl.py", "Solverstatus_und_Gap.py", "Solverwechsel_CPSAT_HiGHS.py", "Spaltengenerierung.py", "Stochastische_Optimierung.py", "Strafgewichte.py", "Strukturbruecke.py", "Toleranzen_und_Entartung.py", "VRP_Flotten_Routing.py", "VRP_Kapazitaetsfalle.py", "VaR_CVaR_Demo.py", "Vektorisierte_Modellgenerierung.py", "Visualisierung_Loesungsraum.py", "Vom_Wunsch_zum_Modell.py", "Warmstart_Effekt.py", "Zuordnung_Ungarisch.py", "or_kern.py", "test_or_kern.py"];
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 101 KiB |
File diff suppressed because it is too large
Load diff
|
Before Width: | Height: | Size: 63 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -33,124 +33,113 @@ Benoetigt: numpy; in den Kindprozessen scipy, highspy, ortools, cvxpy
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import multiprocessing
|
import json
|
||||||
import resource
|
import subprocess
|
||||||
import time
|
import sys
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
import textwrap
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
GROESSEN = [(10, 10), (32, 32), (100, 100)] # (Lager, Kunden) -> 100 / 1.024 / 10.000 Variablen
|
GROESSEN = [(10, 10), (32, 32), (100, 100)] # (Lager, Kunden) -> 100 / 1.024 / 10.000 Variablen
|
||||||
|
|
||||||
|
|
||||||
# Instanz und Speichermessung stehen als gewoehnliche Funktionen hier - nicht
|
# Jeder Eintrag ist ein eigenstaendiges Programm: Instanz aufbauen, loesen,
|
||||||
# in einem String, den ein Kindprozess ausfuehrt. Jede Messfunktion baut die
|
# Ergebnis als JSON ausgeben. Die Instanz wird in jedem Kindprozess aus
|
||||||
# Instanz aus derselben Saat neu auf, damit ueber die Prozessgrenze nichts
|
# derselben Saat neu erzeugt - so reist nichts ueber die Prozessgrenze,
|
||||||
# reist, was das Ergebnis verfaelschen koennte.
|
# was das Ergebnis verfaelschen koennte.
|
||||||
|
VORSPANN = """
|
||||||
|
import json, time, resource
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
def instanz(m: int, n: int):
|
def instanz(m, n):
|
||||||
rng = np.random.default_rng(20)
|
rng = np.random.default_rng(20)
|
||||||
kosten = rng.integers(5, 95, (m, n)).astype(float)
|
kosten = rng.integers(5, 95, (m, n)).astype(float)
|
||||||
angebot = rng.integers(50, 150, m).astype(float)
|
angebot = rng.integers(50, 150, m).astype(float)
|
||||||
bedarf = angebot.sum() * rng.dirichlet(np.ones(n))
|
bedarf = angebot.sum() * rng.dirichlet(np.ones(n))
|
||||||
return kosten, angebot, bedarf
|
return kosten, angebot, bedarf
|
||||||
|
|
||||||
|
def speicher_mb():
|
||||||
def speicher_mb() -> float:
|
# ru_maxrss ist unter Linux in Kilobyte
|
||||||
# ru_maxrss ist unter Linux in Kilobyte. Gemessen wird der Kindprozess -
|
|
||||||
# deshalb muss jede Messung einen eigenen bekommen.
|
|
||||||
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
|
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
|
||||||
|
|
||||||
|
M, N = {m}, {n}
|
||||||
|
kosten, angebot, bedarf = instanz(M, N)
|
||||||
|
"""
|
||||||
|
|
||||||
def messe_scipy(m: int, n: int):
|
ANSAETZE = {
|
||||||
|
"scipy.linprog": """
|
||||||
from scipy.optimize import linprog
|
from scipy.optimize import linprog
|
||||||
kosten, angebot, bedarf = instanz(m, n)
|
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
c = kosten.reshape(-1)
|
c = kosten.reshape(-1)
|
||||||
A_ub = np.zeros((m, m * n)); A_eq = np.zeros((n, m * n))
|
A_ub = np.zeros((M, M * N)); A_eq = np.zeros((N, M * N))
|
||||||
for i in range(m):
|
for i in range(M):
|
||||||
A_ub[i, i * n:(i + 1) * n] = 1.0
|
A_ub[i, i * N:(i + 1) * N] = 1.0
|
||||||
for j in range(n):
|
for j in range(N):
|
||||||
A_eq[j, j::n] = 1.0
|
A_eq[j, j::N] = 1.0
|
||||||
aufbau = time.perf_counter() - t0
|
aufbau = time.perf_counter() - t0
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
r = linprog(c=c, A_ub=A_ub, b_ub=angebot, A_eq=A_eq, b_eq=bedarf,
|
r = linprog(c=c, A_ub=A_ub, b_ub=angebot, A_eq=A_eq, b_eq=bedarf,
|
||||||
bounds=(0, None), method="highs")
|
bounds=(0, None), method="highs")
|
||||||
loesen = time.perf_counter() - t0
|
loesen = time.perf_counter() - t0
|
||||||
return float(r.fun), aufbau, loesen, speicher_mb()
|
ausgabe = (float(r.fun), aufbau, loesen, speicher_mb())
|
||||||
|
""",
|
||||||
|
|
||||||
|
"highspy": """
|
||||||
def messe_highspy(m: int, n: int):
|
|
||||||
import highspy
|
import highspy
|
||||||
kosten, angebot, bedarf = instanz(m, n)
|
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
h = highspy.Highs(); h.setOptionValue("output_flag", False)
|
h = highspy.Highs(); h.setOptionValue("output_flag", False)
|
||||||
h.addVars(m * n, np.zeros(m * n), np.full(m * n, highspy.kHighsInf))
|
h.addVars(M * N, np.zeros(M * N), np.full(M * N, highspy.kHighsInf))
|
||||||
for k in range(m * n):
|
for k in range(M * N):
|
||||||
h.changeColCost(k, float(kosten.reshape(-1)[k]))
|
h.changeColCost(k, float(kosten.reshape(-1)[k]))
|
||||||
for i in range(m):
|
for i in range(M):
|
||||||
idx = np.arange(i * n, (i + 1) * n, dtype=np.int32)
|
idx = np.arange(i * N, (i + 1) * N, dtype=np.int32)
|
||||||
h.addRow(-highspy.kHighsInf, float(angebot[i]), n, idx, np.ones(n))
|
h.addRow(-highspy.kHighsInf, float(angebot[i]), N, idx, np.ones(N))
|
||||||
for j in range(n):
|
for j in range(N):
|
||||||
idx = np.arange(j, m * n, n, dtype=np.int32)
|
idx = np.arange(j, M * N, N, dtype=np.int32)
|
||||||
h.addRow(float(bedarf[j]), float(bedarf[j]), m, idx, np.ones(m))
|
h.addRow(float(bedarf[j]), float(bedarf[j]), M, idx, np.ones(M))
|
||||||
aufbau = time.perf_counter() - t0
|
aufbau = time.perf_counter() - t0
|
||||||
t0 = time.perf_counter(); h.run(); loesen = time.perf_counter() - t0
|
t0 = time.perf_counter(); h.run(); loesen = time.perf_counter() - t0
|
||||||
return h.getInfo().objective_function_value, aufbau, loesen, speicher_mb()
|
ausgabe = (h.getInfo().objective_function_value, aufbau, loesen, speicher_mb())
|
||||||
|
""",
|
||||||
|
|
||||||
|
"ortools/GLOP": """
|
||||||
def messe_ortools(m: int, n: int):
|
|
||||||
from ortools.linear_solver import pywraplp
|
from ortools.linear_solver import pywraplp
|
||||||
kosten, angebot, bedarf = instanz(m, n)
|
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
s = pywraplp.Solver.CreateSolver("GLOP")
|
s = pywraplp.Solver.CreateSolver("GLOP")
|
||||||
x = [[s.NumVar(0, s.infinity(), f"x{i}_{j}") for j in range(n)]
|
x = [[s.NumVar(0, s.infinity(), f"x{i}_{j}") for j in range(N)]
|
||||||
for i in range(m)]
|
for i in range(M)]
|
||||||
for i in range(m):
|
for i in range(M):
|
||||||
s.Add(sum(x[i]) <= float(angebot[i]))
|
s.Add(sum(x[i]) <= float(angebot[i]))
|
||||||
for j in range(n):
|
for j in range(N):
|
||||||
s.Add(sum(x[i][j] for i in range(m)) == float(bedarf[j]))
|
s.Add(sum(x[i][j] for i in range(M)) == float(bedarf[j]))
|
||||||
s.Minimize(sum(float(kosten[i, j]) * x[i][j]
|
s.Minimize(sum(float(kosten[i, j]) * x[i][j]
|
||||||
for i in range(m) for j in range(n)))
|
for i in range(M) for j in range(N)))
|
||||||
aufbau = time.perf_counter() - t0
|
aufbau = time.perf_counter() - t0
|
||||||
t0 = time.perf_counter(); s.Solve(); loesen = time.perf_counter() - t0
|
t0 = time.perf_counter(); s.Solve(); loesen = time.perf_counter() - t0
|
||||||
return s.Objective().Value(), aufbau, loesen, speicher_mb()
|
ausgabe = (s.Objective().Value(), aufbau, loesen, speicher_mb())
|
||||||
|
""",
|
||||||
|
|
||||||
|
"cvxpy": """
|
||||||
def messe_cvxpy(m: int, n: int):
|
|
||||||
import cvxpy as cp
|
import cvxpy as cp
|
||||||
kosten, angebot, bedarf = instanz(m, n)
|
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
x = cp.Variable((m, n), nonneg=True)
|
x = cp.Variable((M, N), nonneg=True)
|
||||||
problem = cp.Problem(cp.Minimize(cp.sum(cp.multiply(kosten, x))),
|
problem = cp.Problem(cp.Minimize(cp.sum(cp.multiply(kosten, x))),
|
||||||
[cp.sum(x, axis=1) <= angebot,
|
[cp.sum(x, axis=1) <= angebot,
|
||||||
cp.sum(x, axis=0) == bedarf])
|
cp.sum(x, axis=0) == bedarf])
|
||||||
aufbau = time.perf_counter() - t0
|
aufbau = time.perf_counter() - t0
|
||||||
t0 = time.perf_counter(); problem.solve(); loesen = time.perf_counter() - t0
|
t0 = time.perf_counter(); problem.solve(); loesen = time.perf_counter() - t0
|
||||||
return float(problem.value), aufbau, loesen, speicher_mb()
|
ausgabe = (float(problem.value), aufbau, loesen, speicher_mb())
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ANSAETZE = {"scipy.linprog": messe_scipy, "highspy": messe_highspy,
|
def messe(name: str, quelltext: str, m: int, n: int):
|
||||||
"ortools/GLOP": messe_ortools, "cvxpy": messe_cvxpy}
|
"""Fuehrt einen Ansatz in einem eigenen Prozess aus."""
|
||||||
|
programm = (VORSPANN.format(m=m, n=n) + textwrap.dedent(quelltext)
|
||||||
|
+ "\nprint(json.dumps(ausgabe))\n")
|
||||||
def messe(funktion, m: int, n: int):
|
ergebnis = subprocess.run([sys.executable, "-c", programm],
|
||||||
"""Fuehrt eine Messfunktion in einem FRISCHEN Prozess aus.
|
capture_output=True, text=True, timeout=600)
|
||||||
|
if ergebnis.returncode != 0:
|
||||||
'spawn' und max_tasks_per_child=1 zusammen garantieren, was Regel 1
|
return None, ergebnis.stderr.strip().splitlines()[-1][:60]
|
||||||
verlangt: Jede Messung sieht einen leeren Interpreter. Ohne das
|
return json.loads(ergebnis.stdout.strip().splitlines()[-1]), None
|
||||||
zweite wuerde der Pool seinen Arbeiter wiederverwenden - dann waere
|
|
||||||
der Speicherwert der zweiten Bibliothek um die erste zu hoch, und
|
|
||||||
ortools und highspy saessen im selben Prozess.
|
|
||||||
"""
|
|
||||||
with ProcessPoolExecutor(
|
|
||||||
max_workers=1,
|
|
||||||
mp_context=multiprocessing.get_context("spawn"),
|
|
||||||
max_tasks_per_child=1) as pool:
|
|
||||||
try:
|
|
||||||
return pool.submit(funktion, m, n).result(timeout=600), None
|
|
||||||
except Exception as fehler:
|
|
||||||
return None, str(fehler).strip().splitlines()[-1][:60]
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
@ -168,8 +157,8 @@ if __name__ == "__main__":
|
||||||
f"{'Loesen':>9} {'Anteil':>8} {'Speicher':>10}")
|
f"{'Loesen':>9} {'Anteil':>8} {'Speicher':>10}")
|
||||||
print(" " + "-" * 72)
|
print(" " + "-" * 72)
|
||||||
zielwerte = {}
|
zielwerte = {}
|
||||||
for name, funktion in ANSAETZE.items():
|
for name, quelltext in ANSAETZE.items():
|
||||||
werte, fehler = messe(funktion, m, n)
|
werte, fehler = messe(name, quelltext, m, n)
|
||||||
if werte is None:
|
if werte is None:
|
||||||
print(f" {name:<16} nicht verfuegbar: {fehler}")
|
print(f" {name:<16} nicht verfuegbar: {fehler}")
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -1,305 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# Constraint_Attribution.py
|
|
||||||
"""
|
|
||||||
Kapitel Praxisfallen: Welche Bedingung kostet wie viel - und was sagt man
|
|
||||||
dem Management?
|
|
||||||
|
|
||||||
Zwischen den beiden bekannten Faellen klafft eine Luecke:
|
|
||||||
|
|
||||||
* Erklaerbarkeit.py beantwortet "warum diese eine Zuweisung?"
|
|
||||||
* Konfliktsuche.py (Anhang Fehlerdiagnose) beantwortet "warum geht es gar
|
|
||||||
nicht?"
|
|
||||||
|
|
||||||
Der haeufigste Fall in der Praxis liegt dazwischen: Das Modell rechnet, der
|
|
||||||
Plan ist zulaessig - und trotzdem unbefriedigend. Niemand weiss, WORAN es
|
|
||||||
liegt. Genau das beantwortet Constraint Attribution.
|
|
||||||
|
|
||||||
Gerechnet wird auf derselben Fabrik wie in Konfliktsuche.py, nur nach der
|
|
||||||
Reparatur: Dort war die Lackierkapazitaet von 150 Stunden der Kern des
|
|
||||||
Widerspruchs (2*40 + 3*30 = 170 > 150). Eine zweite Schicht hat sie auf 210
|
|
||||||
gebracht, und der Mindestumsatz wurde auf 10.000 EUR gesenkt. Jetzt ist das
|
|
||||||
Modell loesbar - und die Frage lautet nicht mehr "warum nicht?", sondern
|
|
||||||
"was kostet uns was?".
|
|
||||||
|
|
||||||
Gezeigt werden vier Dinge:
|
|
||||||
1. Welche Bedingungen binden - und dass "bindend" nicht "teuer" heisst.
|
|
||||||
2. Dass der Schattenpreis eine MOMENTAUFNAHME ist: hochgerechnet auf eine
|
|
||||||
realistische Sonderschicht verspricht er hier das Doppelte des
|
|
||||||
tatsaechlichen Nutzens.
|
|
||||||
3. Dass ein Wunsch aus dem Vertrieb nicht teuer, sondern unmoeglich sein
|
|
||||||
kann - und welche Bedingungen ihn gemeinsam blockieren.
|
|
||||||
4. Wie aus alledem ein Bericht in Alltagssprache wird.
|
|
||||||
|
|
||||||
Benoetigt: numpy, scipy
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import textwrap
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
from scipy.optimize import linprog
|
|
||||||
|
|
||||||
PRODUKTE = ["Rahmen", "Gehaeuse", "Deckel", "Traeger", "Halter"]
|
|
||||||
DECKUNGSBEITRAG = np.array([35.0, 28.0, 9.0, 22.0, 6.0]) # EUR je Stueck
|
|
||||||
|
|
||||||
# Dieselben Bedingungen wie in Konfliktsuche.py, mit den beiden Reparaturen.
|
|
||||||
# Der sprechende Name ist keine Kosmetik: Ohne ihn ist kein Bericht moeglich,
|
|
||||||
# der jemandem ausserhalb der Modellierung etwas sagt.
|
|
||||||
BEDINGUNGEN: list[tuple[str, list[float], str, float]] = [
|
|
||||||
("Kapazitaet Montage", [3, 2, 1, 4, 2], "<=", 400),
|
|
||||||
("Kapazitaet Lackieren", [2, 3, 0, 1, 0], "<=", 210),
|
|
||||||
("Kapazitaet Pruefung", [1, 1, 1, 1, 1], "<=", 300),
|
|
||||||
("Liefervertrag Rahmen", [1, 0, 0, 0, 0], ">=", 40),
|
|
||||||
("Liefervertrag Gehaeuse", [0, 1, 0, 0, 0], ">=", 30),
|
|
||||||
("Liefervertrag Deckel", [0, 0, 1, 0, 0], ">=", 20),
|
|
||||||
("Liefervertrag Traeger", [0, 0, 0, 1, 0], ">=", 25),
|
|
||||||
("Marktgrenze Rahmen", [1, 0, 0, 0, 0], "<=", 120),
|
|
||||||
("Marktgrenze Gehaeuse", [0, 1, 0, 0, 0], "<=", 90),
|
|
||||||
("Marktgrenze Deckel", [0, 0, 1, 0, 0], "<=", 150),
|
|
||||||
("Marktgrenze Halter", [0, 0, 0, 0, 1], "<=", 200),
|
|
||||||
("Mindestumsatz", [90, 70, 30, 60, 20], ">=", 10000),
|
|
||||||
("Sortimentsbreite", [0, 0, 1, 1, 1], ">=", 100),
|
|
||||||
("Lackierbudget Schicht 2", [0, 1, 0, 1, 0], "<=", 55),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Was liesse sich im Ernstfall wirklich bewegen - und um wie viel? Nicht "eine
|
|
||||||
# Einheit", sondern der Schritt, den ein Planer tatsaechlich gehen kann: eine
|
|
||||||
# Sonderschicht, ein nachverhandelter Vertrag. Genau an dieser Groesse
|
|
||||||
# scheitert das blosse Hochrechnen des Schattenpreises.
|
|
||||||
STELLSCHRAUBEN: dict[str, tuple[float, str]] = {
|
|
||||||
"Kapazitaet Montage": (+50, "eine Sonderschicht Montage"),
|
|
||||||
"Kapazitaet Lackieren": (+30, "eine Sonderschicht Lackieren"),
|
|
||||||
"Kapazitaet Pruefung": (+50, "eine Sonderschicht Pruefung"),
|
|
||||||
"Lackierbudget Schicht 2": (+10, "10 Stunden mehr Budget in Schicht 2"),
|
|
||||||
"Liefervertrag Traeger": (-10, "10 Stueck weniger Abnahmepflicht Traeger"),
|
|
||||||
"Liefervertrag Gehaeuse": (-10, "10 Stueck weniger Abnahmepflicht Gehaeuse"),
|
|
||||||
}
|
|
||||||
|
|
||||||
WUNSCH_MENGE = 55.0 # der Vertrieb haette gern mehr Rahmen
|
|
||||||
WUNSCH_PRODUKT = "Rahmen"
|
|
||||||
|
|
||||||
|
|
||||||
def loese(bedingungen: list[tuple[str, list[float], str, float]]):
|
|
||||||
"""Maximiert den Deckungsbeitrag. linprog minimiert, daher das Vorzeichen."""
|
|
||||||
matrix, rechte_seite = [], []
|
|
||||||
for _, koeffizienten, richtung, grenze in bedingungen:
|
|
||||||
zeile = np.array(koeffizienten, dtype=float)
|
|
||||||
matrix.append(zeile if richtung == "<=" else -zeile)
|
|
||||||
rechte_seite.append(grenze if richtung == "<=" else -grenze)
|
|
||||||
return linprog(-DECKUNGSBEITRAG,
|
|
||||||
A_ub=np.array(matrix), b_ub=np.array(rechte_seite),
|
|
||||||
bounds=[(0, None)] * len(PRODUKTE), method="highs")
|
|
||||||
|
|
||||||
|
|
||||||
def geaendert(name: str, delta: float,
|
|
||||||
basis=None) -> list[tuple[str, list[float], str, float]]:
|
|
||||||
"""Dieselben Bedingungen, eine davon um delta verschoben."""
|
|
||||||
return [(n, k, r, g + delta if n == name else g)
|
|
||||||
for n, k, r, g in (basis or BEDINGUNGEN)]
|
|
||||||
|
|
||||||
|
|
||||||
def auswertung(ergebnis, bedingungen=None):
|
|
||||||
"""Schlupf und Schattenpreis je Bedingung."""
|
|
||||||
x = ergebnis.x
|
|
||||||
dual = ergebnis.ineqlin.marginals
|
|
||||||
zeilen = []
|
|
||||||
for i, (name, koeff, richtung, grenze) in enumerate(bedingungen or BEDINGUNGEN):
|
|
||||||
links = float(np.array(koeff, dtype=float) @ x)
|
|
||||||
schlupf = (grenze - links) if richtung == "<=" else (links - grenze)
|
|
||||||
zeilen.append((name, links, richtung, grenze, schlupf, abs(dual[i])))
|
|
||||||
return zeilen
|
|
||||||
|
|
||||||
|
|
||||||
def blockierer(bedingungen, zusatz) -> list[str]:
|
|
||||||
"""Welche Bedingungen verhindern 'zusatz' - einzeln weggelassen.
|
|
||||||
|
|
||||||
Dasselbe Vorgehen wie der Deletion Filter in Konfliktsuche.py, nur mit
|
|
||||||
umgekehrtem Vorzeichen: Dort wird gesucht, was den Widerspruch AUSMACHT,
|
|
||||||
hier, was einen Wunsch VERHINDERT. Wird das Modell ohne eine Bedingung
|
|
||||||
loesbar, gehoert sie zum Konflikt.
|
|
||||||
"""
|
|
||||||
gefunden = []
|
|
||||||
for i, (name, _, _, _) in enumerate(bedingungen):
|
|
||||||
rest = [c for j, c in enumerate(bedingungen) if j != i] + [zusatz]
|
|
||||||
if loese(rest).success:
|
|
||||||
gefunden.append(name)
|
|
||||||
return gefunden
|
|
||||||
|
|
||||||
|
|
||||||
def euro(betrag: float) -> str:
|
|
||||||
return f"{betrag:,.2f} EUR"
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
basis = loese(BEDINGUNGEN)
|
|
||||||
db_basis = -basis.fun
|
|
||||||
plan = dict(zip(PRODUKTE, basis.x))
|
|
||||||
|
|
||||||
print("=" * 78)
|
|
||||||
print(" CONSTRAINT ATTRIBUTION: Was kostet uns welche Bedingung?")
|
|
||||||
print("=" * 78)
|
|
||||||
print(f"Status: {basis.message.split('(')[0].strip()}")
|
|
||||||
print(f"Deckungsbeitrag des Plans: {euro(db_basis)}\n")
|
|
||||||
print(" " + " ".join(f"{p}: {m:.1f}" for p, m in plan.items()))
|
|
||||||
|
|
||||||
# --- 1. Wer begrenzt den Plan? -----------------------------------------
|
|
||||||
print("\n" + "=" * 78)
|
|
||||||
print(" (1) Welche Bedingungen binden - und was kostet die naechste Einheit?")
|
|
||||||
print("=" * 78)
|
|
||||||
print(f"{'Bedingung':<24} {'genutzt':>9} {'Grenze':>7} {'Schlupf':>8} "
|
|
||||||
f"{'Schattenpreis':>11}")
|
|
||||||
print("-" * 78)
|
|
||||||
bindend = []
|
|
||||||
for name, links, richtung, grenze, schlupf, dual in auswertung(basis):
|
|
||||||
marke = ""
|
|
||||||
if abs(schlupf) < 1e-6:
|
|
||||||
bindend.append((name, dual))
|
|
||||||
marke = " <-- bindend"
|
|
||||||
print(f"{name:<24} {links:>9.1f} {grenze:>7.0f} {schlupf:>8.1f} "
|
|
||||||
f"{dual:>11.2f}{marke}")
|
|
||||||
|
|
||||||
ohne_preis = [n for n, d in bindend if d < 1e-9]
|
|
||||||
print(f"\n {len(bindend)} Bedingungen binden. Aber: "
|
|
||||||
f"{', '.join(ohne_preis)} bindet")
|
|
||||||
print(" mit einem Schattenpreis von 0,00 EUR - sie beruehrt den Plan, ohne ihn")
|
|
||||||
print(" zu verteuern. Bindend und kostenrelevant sind zwei verschiedene Dinge.")
|
|
||||||
|
|
||||||
# --- 2. Der Schattenpreis als Momentaufnahme ---------------------------
|
|
||||||
print("\n" + "=" * 78)
|
|
||||||
print(" (2) Was der Schattenpreis verschweigt")
|
|
||||||
print("=" * 78)
|
|
||||||
print("Der Schattenpreis gilt fuer die NAECHSTE Einheit. Ein Planer kauft aber")
|
|
||||||
print("keine Einheit, sondern eine ganze Schicht. Beides gegenuebergestellt:\n")
|
|
||||||
preise = {name: dual for name, *_, dual in auswertung(basis)}
|
|
||||||
print(f"{'Stellschraube':<24} {'Preis':>9} {'Schritt':>7} "
|
|
||||||
f"{'hochgerechnet':>13} {'gemessen':>9} {'Abw.':>9}")
|
|
||||||
print("-" * 78)
|
|
||||||
wirkung = {}
|
|
||||||
for name, (delta, _) in STELLSCHRAUBEN.items():
|
|
||||||
ergebnis = loese(geaendert(name, delta))
|
|
||||||
echt = (-ergebnis.fun) - db_basis if ergebnis.success else 0.0
|
|
||||||
linear = preise[name] * abs(delta)
|
|
||||||
wirkung[name] = echt
|
|
||||||
print(f"{name:<24} {preise[name]:>9.2f} {delta:>+7.0f} "
|
|
||||||
f"{linear:>13.2f} {echt:>9.2f} {echt - linear:>+9.2f}")
|
|
||||||
|
|
||||||
# Wo endet der Gueltigkeitsbereich? Schrittweise nachfahren.
|
|
||||||
abweichler = max(STELLSCHRAUBEN,
|
|
||||||
key=lambda n: preise[n] * abs(STELLSCHRAUBEN[n][0]) - wirkung[n])
|
|
||||||
delta_max = STELLSCHRAUBEN[abweichler][0]
|
|
||||||
grenze_gueltig = 0.0
|
|
||||||
schritt = delta_max / 60.0
|
|
||||||
vorher = db_basis
|
|
||||||
for i in range(1, 61):
|
|
||||||
d = schritt * i
|
|
||||||
e = loese(geaendert(abweichler, d))
|
|
||||||
db = -e.fun if e.success else vorher
|
|
||||||
if db - vorher > 1e-9:
|
|
||||||
grenze_gueltig = d
|
|
||||||
vorher = db
|
|
||||||
print(f"\n '{abweichler}': hochgerechnet "
|
|
||||||
f"{preise[abweichler] * abs(delta_max):.2f} EUR, tatsaechlich "
|
|
||||||
f"{wirkung[abweichler]:.2f} EUR.")
|
|
||||||
print(f" Der Preis von {preise[abweichler]:.2f} EUR gilt nur bis "
|
|
||||||
f"+{grenze_gueltig:.0f} Stunden. Jede weitere Stunde")
|
|
||||||
print(" dieser Sonderschicht bringt exakt nichts - der Engpass ist dann ein anderer.")
|
|
||||||
|
|
||||||
nach_preis = sorted(STELLSCHRAUBEN, key=lambda n: -preise[n])[0]
|
|
||||||
nach_wirkung = max(wirkung, key=wirkung.get)
|
|
||||||
print(f"\n Und die Ranglisten drehen sich: Nach Schattenpreis fuehrt")
|
|
||||||
print(f" '{nach_preis}' ({preise[nach_preis]:.2f} EUR je Einheit),")
|
|
||||||
print(f" nach tatsaechlicher Wirkung '{nach_wirkung}' "
|
|
||||||
f"({wirkung[nach_wirkung]:.2f} EUR).")
|
|
||||||
print(" Der Schattenpreis sagt, was eine Einheit wert ist - nicht, wie viele")
|
|
||||||
print(" Einheiten zu haben sind.")
|
|
||||||
|
|
||||||
# --- 3. Der Wunsch aus dem Vertrieb ------------------------------------
|
|
||||||
print("\n" + "=" * 78)
|
|
||||||
print(f" (3) Was-waere-wenn: mindestens {WUNSCH_MENGE:.0f} "
|
|
||||||
f"{WUNSCH_PRODUKT} statt {plan[WUNSCH_PRODUKT]:.1f}")
|
|
||||||
print("=" * 78)
|
|
||||||
wunsch = (f"Wunsch Vertrieb {WUNSCH_PRODUKT}",
|
|
||||||
[1 if p == WUNSCH_PRODUKT else 0 for p in PRODUKTE], ">=", WUNSCH_MENGE)
|
|
||||||
mit_wunsch = loese(BEDINGUNGEN + [wunsch])
|
|
||||||
schuldige: list[str] = []
|
|
||||||
engpass = noetig = None
|
|
||||||
wunsch_gratis = False
|
|
||||||
|
|
||||||
if mit_wunsch.success:
|
|
||||||
print(f"Erfuellbar, kostet {euro(db_basis - (-mit_wunsch.fun))}.")
|
|
||||||
else:
|
|
||||||
schuldige = blockierer(BEDINGUNGEN, wunsch)
|
|
||||||
print("Der Wunsch ist nicht teuer - er ist UNMOEGLICH.\n")
|
|
||||||
print(f"Blockiert wird er von {len(schuldige)} Bedingungen gemeinsam. "
|
|
||||||
f"Faellt eine davon")
|
|
||||||
print("weg, ist er erfuellbar; keine allein ist 'der' Grund:")
|
|
||||||
for name in schuldige:
|
|
||||||
e = loese([c for c in BEDINGUNGEN if c[0] != name] + [wunsch])
|
|
||||||
print(f" ohne '{name}': moeglich, Deckungsbeitrag {euro(-e.fun)}")
|
|
||||||
|
|
||||||
engpass = next((n for n in schuldige if n in STELLSCHRAUBEN), schuldige[0])
|
|
||||||
for schritt in range(1, 61):
|
|
||||||
if loese(geaendert(engpass, schritt) + [wunsch]).success:
|
|
||||||
noetig = schritt
|
|
||||||
break
|
|
||||||
if noetig is not None:
|
|
||||||
e = loese(geaendert(engpass, noetig) + [wunsch])
|
|
||||||
ohne = loese(geaendert(engpass, noetig))
|
|
||||||
wunsch_gratis = abs((-e.fun) - (-ohne.fun)) < 1e-6
|
|
||||||
print(f"\n Mit +{noetig} bei '{engpass}' wird der Wunsch erfuellbar:")
|
|
||||||
print(f" Deckungsbeitrag {euro(-e.fun)} "
|
|
||||||
f"({-e.fun - db_basis:+,.2f} gegenueber heute).")
|
|
||||||
if wunsch_gratis:
|
|
||||||
print(f" Und ohne den Wunsch ergaebe dieselbe Lockerung "
|
|
||||||
f"{euro(-ohne.fun)} bei")
|
|
||||||
print(f" {ohne.x[PRODUKTE.index(WUNSCH_PRODUKT)]:.0f} "
|
|
||||||
f"{WUNSCH_PRODUKT} - der Wunsch kostet also NICHTS.")
|
|
||||||
print(" Er war nie das Problem. Der Engpass war es.")
|
|
||||||
|
|
||||||
# --- 4. Der Bericht ----------------------------------------------------
|
|
||||||
# Aus denselben Zahlen, die oben in Tabellen stehen, wird hier Prosa. Kein
|
|
||||||
# Satz enthaelt eine Zahl, die nicht vorher gerechnet wurde - das ist der
|
|
||||||
# ganze Trick und zugleich die Bedingung dafuer, dass der Bericht nicht
|
|
||||||
# irgendwann leise falsch wird.
|
|
||||||
print("\n" + "=" * 78)
|
|
||||||
print(" (4) Und so steht es in der Vorlage fuer die Abteilungsleiterrunde")
|
|
||||||
print("=" * 78)
|
|
||||||
|
|
||||||
teuerste = sorted(((n, d) for n, d in bindend if d > 0), key=lambda t: -t[1])
|
|
||||||
gross = STELLSCHRAUBEN[nach_wirkung][1]
|
|
||||||
klein = STELLSCHRAUBEN[abweichler][1]
|
|
||||||
|
|
||||||
absaetze = [
|
|
||||||
f"Der Plan bringt {euro(db_basis)} Deckungsbeitrag. Begrenzt wird er von "
|
|
||||||
f"{len(bindend)} Bedingungen, von denen {len(teuerste)} tatsaechlich Geld "
|
|
||||||
f"kosten; \"{ohne_preis[0]}\" beruehrt den Plan, ohne ihn zu verteuern.",
|
|
||||||
|
|
||||||
f"Teuerste Bindung ist \"{teuerste[0][0]}\" mit "
|
|
||||||
f"{teuerste[0][1]:.2f} EUR je Einheit, gefolgt von \"{teuerste[1][0]}\" "
|
|
||||||
f"mit {teuerste[1][1]:.2f} EUR.",
|
|
||||||
|
|
||||||
f"Die groesste einzelne Verbesserung bringt {gross}: "
|
|
||||||
f"{euro(wirkung[nach_wirkung])} mehr Deckungsbeitrag. {klein[0].upper()}{klein[1:]} "
|
|
||||||
f"dagegen bringt nur {euro(wirkung[abweichler])} statt der rechnerischen "
|
|
||||||
f"{euro(preise[abweichler] * abs(delta_max))} - ab +{grenze_gueltig:.0f} "
|
|
||||||
f"Stunden begrenzt uns etwas anderes. Wer die ganze Schicht bezahlt, "
|
|
||||||
f"bezahlt die zweite Haelfte umsonst.",
|
|
||||||
]
|
|
||||||
|
|
||||||
if schuldige and noetig is not None:
|
|
||||||
namen = ", ".join(f'"{s}"' for s in schuldige)
|
|
||||||
satz = (f"Zum Wunsch des Vertriebs ({WUNSCH_MENGE:.0f} {WUNSCH_PRODUKT} statt "
|
|
||||||
f"{plan[WUNSCH_PRODUKT]:.1f}): Er ist mit der heutigen Ausstattung "
|
|
||||||
f"nicht erfuellbar - nicht aus Kostengruenden, sondern weil {namen} "
|
|
||||||
f"ihn gemeinsam blockieren. Mit +{noetig} Stunden bei "
|
|
||||||
f'"{engpass}" wird er moeglich')
|
|
||||||
satz += (", und er kostet dann nichts: Der Plan waehlt diese Menge von "
|
|
||||||
"sich aus." if wunsch_gratis else ".")
|
|
||||||
absaetze.append(satz)
|
|
||||||
|
|
||||||
for absatz in absaetze:
|
|
||||||
print()
|
|
||||||
print(textwrap.fill(absatz, width=76))
|
|
||||||
print("\n" + "=" * 78)
|
|
||||||
|
|
@ -8,104 +8,85 @@ Kapitel Oekosystem: Dasselbe LP in vier Bibliotheken.
|
||||||
2*x1 + 3*x2 + x3 <= 50
|
2*x1 + 3*x2 + x3 <= 50
|
||||||
x >= 0
|
x >= 0
|
||||||
|
|
||||||
Deckt scipy.optimize, highspy, CVXPY und OR-Tools/GLOP ab, mit Kreuzvergleich
|
Deckt scipy.optimize, highspy, CVXPY und OR-Tools/GLOP ab, mit Kreuzvergleich am Ende.
|
||||||
am Ende.
|
|
||||||
|
|
||||||
WICHTIG: Jeder Solver laeuft in einem EIGENEN Prozess, weil sich ortools und
|
WICHTIG: Jeder Solver läuft in einem EIGENEN Prozess, weil sich ortools und
|
||||||
highspy auf vielen Systemen nicht gemeinsam importieren lassen (beide bringen
|
highspy auf vielen Systemen nicht gemeinsam importieren lassen (beide bringen
|
||||||
eine eigene HiGHS-Kopie mit -> Symbolkonflikt).
|
eine eigene HiGHS-Kopie mit -> Symbolkonflikt).
|
||||||
|
|
||||||
Die Isolation besorgt ein ProcessPoolExecutor. Drei Einstellungen ergeben
|
|
||||||
zusammen die Garantie:
|
|
||||||
|
|
||||||
mp_context "spawn" Der Kindprozess startet mit einem FRISCHEN
|
|
||||||
Interpreter, statt den Speicher des Elternprozesses
|
|
||||||
zu erben. Was hier schon importiert ist, ist dort
|
|
||||||
nicht importiert. Mit dem Standard "fork" auf Linux
|
|
||||||
waere das nicht so.
|
|
||||||
max_tasks_per_child=1 Jede Aufgabe bekommt einen NEUEN Prozess. Ohne das
|
|
||||||
wuerde der Pool seinen Arbeiter wiederverwenden - und
|
|
||||||
beim zweiten Solver waere der Konflikt zurueck.
|
|
||||||
max_workers=1 Haelt die vier Laeufe nacheinander. Nicht aus
|
|
||||||
Vorsicht, sondern damit die gemessenen Zeiten
|
|
||||||
vergleichbar bleiben.
|
|
||||||
|
|
||||||
Jeder Solver steht in einer eigenen Funktion mit LOKALEM Import. Das ist der
|
|
||||||
Unterschied zu einem Codestring, den man an 'python -c' uebergibt: Die
|
|
||||||
Funktion laesst sich einzeln aufrufen, testen und vom Editor pruefen - ein
|
|
||||||
String nicht.
|
|
||||||
|
|
||||||
Benoetigt: scipy, highspy, cvxpy, ortools
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import multiprocessing
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
|
||||||
|
|
||||||
ERWARTET = 530.0 # Ergebnis der Handrechnung zum Produktionsprogramm
|
ERWARTET = 530.0 # Ergebnis der Handrechnung zum Produktionsprogramm
|
||||||
|
|
||||||
# Die Instanz - einmal notiert, von allen vier Funktionen benutzt.
|
# Jeder Eintrag ist ein eigenständiges Miniprogramm, das sein Ergebnis als
|
||||||
ZIEL = [10.0, 15.0, 25.0]
|
# JSON auf stdout ausgibt. So bleibt jeder Import in seinem eigenen Prozess.
|
||||||
MATRIX = [[1, 1, 2], [2, 3, 1]]
|
ANSAETZE: dict[str, str] = {
|
||||||
KAPAZITAET = [40.0, 50.0]
|
|
||||||
|
|
||||||
|
"scipy.optimize.linprog": """
|
||||||
def loese_mit_scipy() -> tuple[float, list[float]]:
|
|
||||||
from scipy.optimize import linprog
|
from scipy.optimize import linprog
|
||||||
ergebnis = linprog(c=[-w for w in ZIEL], # linprog MINIMIERT -> negieren
|
res = linprog(c=[-10.0, -15.0, -25.0], # linprog MINIMIERT -> negieren
|
||||||
A_ub=MATRIX, b_ub=KAPAZITAET,
|
A_ub=[[1, 1, 2], [2, 3, 1]], b_ub=[40, 50],
|
||||||
bounds=[(0, None)] * 3, method="highs")
|
bounds=[(0, None)] * 3, method="highs")
|
||||||
return -ergebnis.fun, list(ergebnis.x)
|
ausgabe = (-res.fun, list(res.x))
|
||||||
|
""",
|
||||||
|
|
||||||
|
"highspy (natives HiGHS)": """
|
||||||
def loese_mit_highspy() -> tuple[float, list[float]]:
|
import numpy as np, highspy
|
||||||
import highspy
|
|
||||||
import numpy as np
|
|
||||||
h = highspy.Highs()
|
h = highspy.Highs()
|
||||||
h.setOptionValue("output_flag", False)
|
h.setOptionValue("output_flag", False)
|
||||||
h.addVars(3, np.zeros(3), np.full(3, highspy.kHighsInf))
|
h.addVars(3, np.zeros(3), np.full(3, highspy.kHighsInf))
|
||||||
h.changeObjectiveSense(highspy.ObjSense.kMaximize)
|
h.changeObjectiveSense(highspy.ObjSense.kMaximize)
|
||||||
for j, wert in enumerate(ZIEL):
|
for j, wert in enumerate([10.0, 15.0, 25.0]):
|
||||||
h.changeColCost(j, wert)
|
h.changeColCost(j, wert)
|
||||||
# CSR-Format: starts[i] = Beginn von Zeile i in indices/values
|
# CSR-Format: starts[i] = Beginn von Zeile i in indices/values
|
||||||
h.addRows(2, np.full(2, -highspy.kHighsInf), np.array(KAPAZITAET), 6,
|
h.addRows(2, np.full(2, -highspy.kHighsInf), np.array([40.0, 50.0]), 6,
|
||||||
np.array([0, 3], dtype=np.int32),
|
np.array([0, 3], dtype=np.int32),
|
||||||
np.array([0, 1, 2, 0, 1, 2], dtype=np.int32),
|
np.array([0, 1, 2, 0, 1, 2], dtype=np.int32),
|
||||||
np.array([float(w) for zeile in MATRIX for w in zeile]))
|
np.array([1.0, 1.0, 2.0, 2.0, 3.0, 1.0]))
|
||||||
h.run()
|
h.run()
|
||||||
return (h.getInfo().objective_function_value,
|
ausgabe = (h.getInfo().objective_function_value,
|
||||||
list(h.getSolution().col_value[:3]))
|
list(h.getSolution().col_value[:3]))
|
||||||
|
""",
|
||||||
|
|
||||||
|
"cvxpy": """
|
||||||
def loese_mit_cvxpy() -> tuple[float, list[float]]:
|
import numpy as np, cvxpy as cp
|
||||||
import cvxpy as cp
|
|
||||||
import numpy as np
|
|
||||||
x = cp.Variable(3, nonneg=True)
|
x = cp.Variable(3, nonneg=True)
|
||||||
problem = cp.Problem(cp.Maximize(np.array(ZIEL) @ x),
|
problem = cp.Problem(cp.Maximize(np.array([10.0, 15.0, 25.0]) @ x),
|
||||||
[np.array(MATRIX) @ x <= np.array(KAPAZITAET)])
|
[np.array([[1, 1, 2], [2, 3, 1]]) @ x <= np.array([40, 50])])
|
||||||
problem.solve()
|
problem.solve()
|
||||||
return float(problem.value), [float(v) for v in x.value]
|
ausgabe = (float(problem.value), [float(v) for v in x.value])
|
||||||
|
""",
|
||||||
|
|
||||||
|
"ortools / GLOP": """
|
||||||
def loese_mit_ortools() -> tuple[float, list[float]]:
|
|
||||||
from ortools.linear_solver import pywraplp
|
from ortools.linear_solver import pywraplp
|
||||||
s = pywraplp.Solver.CreateSolver("GLOP")
|
s = pywraplp.Solver.CreateSolver("GLOP")
|
||||||
x = [s.NumVar(0, s.infinity(), f"x{j+1}") for j in range(3)]
|
x = [s.NumVar(0, s.infinity(), f"x{j+1}") for j in range(3)]
|
||||||
for i, kapazitaet in enumerate(KAPAZITAET):
|
A = [[1, 1, 2], [2, 3, 1]]
|
||||||
s.Add(sum(MATRIX[i][j] * x[j] for j in range(3)) <= kapazitaet)
|
for i, kap in enumerate([40, 50]):
|
||||||
s.Maximize(sum(ZIEL[j] * x[j] for j in range(3)))
|
s.Add(sum(A[i][j] * x[j] for j in range(3)) <= kap)
|
||||||
|
s.Maximize(10 * x[0] + 15 * x[1] + 25 * x[2])
|
||||||
s.Solve()
|
s.Solve()
|
||||||
return s.Objective().Value(), [v.solution_value() for v in x]
|
ausgabe = (s.Objective().Value(), [v.solution_value() for v in x])
|
||||||
|
""",
|
||||||
|
|
||||||
ANSAETZE = {
|
|
||||||
"scipy.optimize.linprog": loese_mit_scipy,
|
|
||||||
"highspy (natives HiGHS)": loese_mit_highspy,
|
|
||||||
"cvxpy": loese_mit_cvxpy,
|
|
||||||
"ortools / GLOP": loese_mit_ortools,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fuehre_in_eigenem_prozess_aus(quelltext: str) -> tuple[float, list[float]]:
|
||||||
|
"""Startet den Codeschnipsel als separaten Python-Prozess und liest das Ergebnis."""
|
||||||
|
programm = textwrap.dedent(quelltext) + "\nimport json; print(json.dumps(ausgabe))\n"
|
||||||
|
ergebnis = subprocess.run([sys.executable, "-c", programm],
|
||||||
|
capture_output=True, text=True, timeout=120)
|
||||||
|
if ergebnis.returncode != 0:
|
||||||
|
raise RuntimeError(ergebnis.stderr.strip().splitlines()[-1])
|
||||||
|
wert, loesung = json.loads(ergebnis.stdout.strip().splitlines()[-1])
|
||||||
|
return wert, loesung
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("=" * 78)
|
print("=" * 78)
|
||||||
print(" EIN SYSTEM - VIER ANSAETZE (je eigener Prozess)")
|
print(" EIN SYSTEM - VIER ANSAETZE (je eigener Prozess)")
|
||||||
|
|
@ -114,20 +95,14 @@ if __name__ == "__main__":
|
||||||
print("-" * 78)
|
print("-" * 78)
|
||||||
|
|
||||||
werte = []
|
werte = []
|
||||||
# Ein Pool, vier Aufgaben, vier frische Prozesse. Der Kontext muss
|
for name, quelltext in ANSAETZE.items():
|
||||||
# "spawn" sein - siehe Modulkommentar.
|
t0 = time.perf_counter()
|
||||||
with ProcessPoolExecutor(
|
|
||||||
max_workers=1,
|
|
||||||
mp_context=multiprocessing.get_context("spawn"),
|
|
||||||
max_tasks_per_child=1) as pool:
|
|
||||||
for name, funktion in ANSAETZE.items():
|
|
||||||
beginn = time.perf_counter()
|
|
||||||
try:
|
try:
|
||||||
wert, x = pool.submit(funktion).result(timeout=120)
|
wert, x = fuehre_in_eigenem_prozess_aus(quelltext)
|
||||||
except Exception as fehler: # Bibliothek fehlt o. Ae.
|
except RuntimeError as fehler:
|
||||||
print(f"{name:<26} nicht verfuegbar: {str(fehler)[:40]}")
|
print(f"{name:<26} nicht verfuegbar: {fehler[:40]}")
|
||||||
continue
|
continue
|
||||||
dauer = time.perf_counter() - beginn
|
dauer = time.perf_counter() - t0
|
||||||
werte.append(wert)
|
werte.append(wert)
|
||||||
print(f"{name:<26} {wert:>10.2f} {x[0]:>7.2f} {x[1]:>7.2f} {x[2]:>7.2f} "
|
print(f"{name:<26} {wert:>10.2f} {x[0]:>7.2f} {x[1]:>7.2f} {x[2]:>7.2f} "
|
||||||
f"{dauer:>8.2f} s")
|
f"{dauer:>8.2f} s")
|
||||||
|
|
@ -140,6 +115,6 @@ if __name__ == "__main__":
|
||||||
assert spanne < 1e-6, "Die Bibliotheken widersprechen sich!"
|
assert spanne < 1e-6, "Die Bibliotheken widersprechen sich!"
|
||||||
assert abs(werte[0] - ERWARTET) < 1e-6, "Ergebnis weicht von der Handrechnung ab!"
|
assert abs(werte[0] - ERWARTET) < 1e-6, "Ergebnis weicht von der Handrechnung ab!"
|
||||||
print("Alle Wege fuehren zum selben, von Hand bestaetigten Optimum.")
|
print("Alle Wege fuehren zum selben, von Hand bestaetigten Optimum.")
|
||||||
print("(Die Zeiten enthalten Prozessstart und Import - sie messen NICHT die")
|
print("(Die Zeiten enthalten den Prozessstart und den Import - sie messen")
|
||||||
print(" reine Solverleistung. Die Uebungsaufgabe 'Laufzeitvergleich' trennt beides.)")
|
print(" NICHT die reine Solverleistung, siehe Uebung 3.5.)")
|
||||||
print("=" * 78)
|
print("=" * 78)
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,7 @@ def minimales_risiko(kovarianz: np.ndarray) -> tuple[np.ndarray, float]:
|
||||||
exakt positiv semidefinit ist - rechne trotzdem.' Das ist hier bewusst so
|
exakt positiv semidefinit ist - rechne trotzdem.' Das ist hier bewusst so
|
||||||
gewaehlt, denn genau diese Eigenschaft wollen wir vorfuehren. Im
|
gewaehlt, denn genau diese Eigenschaft wollen wir vorfuehren. Im
|
||||||
Produktivbetrieb waere die DCP-Fehlermeldung das Warnsignal, dem man
|
Produktivbetrieb waere die DCP-Fehlermeldung das Warnsignal, dem man
|
||||||
nachgehen muss (siehe den Denkfehler im Kapitel Quadratische und
|
nachgehen muss (siehe Denkfehler 8.1).
|
||||||
nichtlineare Optimierung).
|
|
||||||
"""
|
"""
|
||||||
gewichte = cp.Variable(ANZAHL_ANLAGEN)
|
gewichte = cp.Variable(ANZAHL_ANLAGEN)
|
||||||
problem = cp.Problem(cp.Minimize(cp.quad_form(gewichte, cp.psd_wrap(kovarianz))),
|
problem = cp.Problem(cp.Minimize(cp.quad_form(gewichte, cp.psd_wrap(kovarianz))),
|
||||||
|
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# Parallele_Suche.py
|
|
||||||
"""
|
|
||||||
Kapitel CP-SAT: Was mehrere Arbeiter wirklich bringen - und was sie kosten.
|
|
||||||
|
|
||||||
Das Buch setzt an mehreren Stellen 'num_workers = 1', damit die abgedruckte
|
|
||||||
Ausgabe reproduzierbar ist. Behauptet wird dabei zweierlei: dass parallele
|
|
||||||
Suche schneller ist, und dass sie von Lauf zu Lauf verschiedene, gleich gute
|
|
||||||
Loesungen findet. Beides wird hier gemessen statt geglaubt.
|
|
||||||
|
|
||||||
Gerechnet wird auf einem Job-Shop wie in JobShop_Intervalle.py, nur gross
|
|
||||||
genug, dass die Suche wirklich arbeitet: 12 Auftraege auf 10 Maschinen,
|
|
||||||
jeder Auftrag besucht jede Maschine.
|
|
||||||
|
|
||||||
Zwei Messungen:
|
|
||||||
1. Dieselbe Aufgabe mit 1, 2, 4 und 8 Arbeitern - Laufzeit und Ergebnis.
|
|
||||||
2. Dieselbe Konfiguration mehrfach, bei FESTEM random_seed - wie oft kommt
|
|
||||||
derselbe Plan heraus?
|
|
||||||
|
|
||||||
Die zweite ist die wichtigere: Sie entscheidet, wie man Tests schreibt.
|
|
||||||
|
|
||||||
Achtung, Laufzeiten sind hardwareabhaengig. Die ZAHL der verschiedenen
|
|
||||||
Plaene ist es auch - und genau das ist die Aussage.
|
|
||||||
|
|
||||||
Benoetigt: numpy, ortools
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import time
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
from ortools.sat.python import cp_model
|
|
||||||
|
|
||||||
AUFTRAEGE = 12
|
|
||||||
MASCHINEN = 10
|
|
||||||
INSTANZ_SEED = 20260908
|
|
||||||
SOLVER_SEED = 1 # bleibt ueber ALLE Laeufe gleich - das ist der Punkt
|
|
||||||
ARBEITERZAHLEN = (1, 2, 4, 8)
|
|
||||||
WIEDERHOLUNGEN = 4
|
|
||||||
ZEITLIMIT = 120.0
|
|
||||||
|
|
||||||
|
|
||||||
def baue_instanz() -> list[list[tuple[int, int]]]:
|
|
||||||
"""Klassischer Job-Shop: Jeder Auftrag besucht jede Maschine genau einmal,
|
|
||||||
in einer eigenen zufaelligen Reihenfolge."""
|
|
||||||
rng = np.random.default_rng(INSTANZ_SEED)
|
|
||||||
return [[(int(m), int(rng.integers(2, 20))) for m in rng.permutation(MASCHINEN)]
|
|
||||||
for _ in range(AUFTRAEGE)]
|
|
||||||
|
|
||||||
|
|
||||||
def loese(auftraege, arbeiter: int):
|
|
||||||
"""Minimiert den Makespan. Gibt Status, Zielwert, Laufzeit und den Plan
|
|
||||||
zurueck - den Plan als Tupel aller Startzeiten, damit sich zwei Laeufe
|
|
||||||
vergleichen lassen."""
|
|
||||||
horizont = sum(dauer for auftrag in auftraege for _, dauer in auftrag)
|
|
||||||
modell = cp_model.CpModel()
|
|
||||||
Gang = collections.namedtuple("Gang", "start ende intervall")
|
|
||||||
plaene: dict[tuple[int, int], Gang] = {}
|
|
||||||
je_maschine = collections.defaultdict(list)
|
|
||||||
|
|
||||||
for a, auftrag in enumerate(auftraege):
|
|
||||||
for g, (maschine, dauer) in enumerate(auftrag):
|
|
||||||
start = modell.NewIntVar(0, horizont, f"start_{a}_{g}")
|
|
||||||
ende = modell.NewIntVar(0, horizont, f"ende_{a}_{g}")
|
|
||||||
intervall = modell.NewIntervalVar(start, dauer, ende, f"iv_{a}_{g}")
|
|
||||||
plaene[a, g] = Gang(start, ende, intervall)
|
|
||||||
je_maschine[maschine].append(intervall)
|
|
||||||
|
|
||||||
for maschine in range(MASCHINEN):
|
|
||||||
modell.AddNoOverlap(je_maschine[maschine])
|
|
||||||
for a, auftrag in enumerate(auftraege):
|
|
||||||
for g in range(len(auftrag) - 1):
|
|
||||||
modell.Add(plaene[a, g + 1].start >= plaene[a, g].ende)
|
|
||||||
|
|
||||||
makespan = modell.NewIntVar(0, horizont, "makespan")
|
|
||||||
modell.AddMaxEquality(
|
|
||||||
makespan, [plaene[a, len(auftrag) - 1].ende
|
|
||||||
for a, auftrag in enumerate(auftraege)])
|
|
||||||
modell.Minimize(makespan)
|
|
||||||
|
|
||||||
loeser = cp_model.CpSolver()
|
|
||||||
loeser.parameters.num_workers = arbeiter
|
|
||||||
loeser.parameters.random_seed = SOLVER_SEED
|
|
||||||
loeser.parameters.max_time_in_seconds = ZEITLIMIT
|
|
||||||
|
|
||||||
beginn = time.perf_counter()
|
|
||||||
status = loeser.Solve(modell)
|
|
||||||
dauer = time.perf_counter() - beginn
|
|
||||||
|
|
||||||
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
|
|
||||||
return loeser.StatusName(status), None, dauer, None
|
|
||||||
plan = tuple(loeser.Value(plaene[a, g].start)
|
|
||||||
for a, auftrag in enumerate(auftraege)
|
|
||||||
for g in range(len(auftrag)))
|
|
||||||
return loeser.StatusName(status), int(loeser.ObjectiveValue()), dauer, plan
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
auftraege = baue_instanz()
|
|
||||||
gaenge = sum(len(a) for a in auftraege)
|
|
||||||
|
|
||||||
print("=" * 78)
|
|
||||||
print(" WAS MEHRERE ARBEITER WIRKLICH BRINGEN")
|
|
||||||
print("=" * 78)
|
|
||||||
print(f"Job-Shop: {AUFTRAEGE} Auftraege, {MASCHINEN} Maschinen, "
|
|
||||||
f"{gaenge} Arbeitsgaenge.")
|
|
||||||
print(f"random_seed = {SOLVER_SEED} in JEDEM Lauf, "
|
|
||||||
f"{WIEDERHOLUNGEN} Wiederholungen je Arbeiterzahl.\n")
|
|
||||||
|
|
||||||
# Jede Konfiguration mehrfach - ein einzelner Zeitwert sagt nichts.
|
|
||||||
ergebnisse: dict[int, list[tuple]] = {}
|
|
||||||
for arbeiter in ARBEITERZAHLEN:
|
|
||||||
ergebnisse[arbeiter] = [loese(auftraege, arbeiter)
|
|
||||||
for _ in range(WIEDERHOLUNGEN)]
|
|
||||||
|
|
||||||
print("=" * 78)
|
|
||||||
print(" (1) Laufzeit: was die Arbeiter bringen")
|
|
||||||
print("=" * 78)
|
|
||||||
print(f"{'Arbeiter':>9} {'Status':>9} {'Makespan':>9} "
|
|
||||||
f"{'schnellste':>11} {'langsamste':>11} {'Beschleunigung':>15}")
|
|
||||||
print("-" * 78)
|
|
||||||
basis_zeit = min(d for _, _, d, _ in ergebnisse[1])
|
|
||||||
for arbeiter in ARBEITERZAHLEN:
|
|
||||||
laeufe = ergebnisse[arbeiter]
|
|
||||||
zeiten = [d for _, _, d, _ in laeufe]
|
|
||||||
ziele = {o for _, o, _, _ in laeufe}
|
|
||||||
status = {s for s, _, _, _ in laeufe}
|
|
||||||
print(f"{arbeiter:>9} {'/'.join(sorted(status)):>9} "
|
|
||||||
f"{'/'.join(str(z) for z in sorted(ziele)):>9} "
|
|
||||||
f"{min(zeiten):>10.2f}s {max(zeiten):>10.2f}s "
|
|
||||||
f"{basis_zeit / min(zeiten):>14.1f}x")
|
|
||||||
|
|
||||||
beste = min(ARBEITERZAHLEN, key=lambda w: min(d for _, _, d, _ in ergebnisse[w]))
|
|
||||||
faktor = basis_zeit / min(d for _, _, d, _ in ergebnisse[beste])
|
|
||||||
print(f"\n Am schnellsten sind {beste} Arbeiter: Faktor {faktor:.1f} gegenueber")
|
|
||||||
print(f" einem einzigen - also MEHR, als {beste} Kerne hergeben sollten.")
|
|
||||||
print(f" Das ist kein Messfehler. CP-SAT laesst nicht {beste}-mal dieselbe Suche")
|
|
||||||
print(" laufen, sondern verschiedene Strategien nebeneinander, die einander")
|
|
||||||
print(" ihre Schranken zurufen. Der zweite Arbeiter ist deshalb nicht der")
|
|
||||||
print(" 'zweite Kern', sondern ein anderes Verfahren.")
|
|
||||||
letzte = ARBEITERZAHLEN[-1]
|
|
||||||
if beste != letzte:
|
|
||||||
zeit_letzte = min(d for _, _, d, _ in ergebnisse[letzte])
|
|
||||||
print(f"\n Und mehr ist nicht immer besser: {letzte} Arbeiter brauchen "
|
|
||||||
f"{zeit_letzte:.2f}s")
|
|
||||||
print(f" gegenueber {min(d for _, _, d, _ in ergebnisse[beste]):.2f}s bei "
|
|
||||||
f"{beste}. Ab einer gewissen Zahl kosten Abstimmung")
|
|
||||||
print(" und Speicherbandbreite mehr, als ein weiterer Suchstrang einbringt.")
|
|
||||||
|
|
||||||
print("\n" + "=" * 78)
|
|
||||||
print(" (2) Reproduzierbarkeit: derselbe Seed, derselbe Plan?")
|
|
||||||
print("=" * 78)
|
|
||||||
print(f"{'Arbeiter':>9} {'Laeufe':>7} {'verschiedene Plaene':>21} "
|
|
||||||
f"{'Makespan':>10}")
|
|
||||||
print("-" * 78)
|
|
||||||
for arbeiter in ARBEITERZAHLEN:
|
|
||||||
plaene = {p for _, _, _, p in ergebnisse[arbeiter] if p is not None}
|
|
||||||
ziele = {o for _, o, _, _ in ergebnisse[arbeiter]}
|
|
||||||
print(f"{arbeiter:>9} {WIEDERHOLUNGEN:>7} {len(plaene):>21} "
|
|
||||||
f"{'/'.join(str(z) for z in sorted(ziele)):>10}")
|
|
||||||
|
|
||||||
einer = len({p for _, _, _, p in ergebnisse[1]})
|
|
||||||
# Ab welcher Arbeiterzahl bricht die Reproduzierbarkeit? Das ist die
|
|
||||||
# eigentliche Zahl - nicht, was acht Arbeiter anrichten.
|
|
||||||
ab = next((w for w in ARBEITERZAHLEN
|
|
||||||
if len({p for _, _, _, p in ergebnisse[w]}) > 1), None)
|
|
||||||
print(f"\n Mit einem Arbeiter: {einer} Plan aus {WIEDERHOLUNGEN} Laeufen.")
|
|
||||||
if ab is not None:
|
|
||||||
viele = len({p for _, _, _, p in ergebnisse[ab]})
|
|
||||||
print(f" Schon mit {ab} Arbeitern: {viele} verschiedene Plaene aus "
|
|
||||||
f"{WIEDERHOLUNGEN} Laeufen -")
|
|
||||||
print(" bei identischem random_seed und identischem Zielwert.")
|
|
||||||
print(f" Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit")
|
|
||||||
print(f" zu verlieren. {ab} genuegen.")
|
|
||||||
print("\n Der Seed allein macht einen Lauf also NICHT reproduzierbar. Er legt")
|
|
||||||
print(" fest, wie ein einzelner Suchstrang wuerfelt - nicht, welcher von")
|
|
||||||
print(" mehreren zuerst fertig wird. Das entscheidet die Uhr.")
|
|
||||||
|
|
||||||
print("\n" + "=" * 78)
|
|
||||||
print(" (3) Was daraus fuer Tests folgt")
|
|
||||||
print("=" * 78)
|
|
||||||
print(" Ein Test der Form assert plan == erwarteter_plan besteht mal und")
|
|
||||||
print(" scheitert mal, ohne dass sich am Code etwas geaendert haette.")
|
|
||||||
print(" Zu pruefen sind stattdessen:")
|
|
||||||
print(" * der ZIELWERT (hier in allen Laeufen gleich),")
|
|
||||||
print(" * die Einhaltung aller Regeln (Abnahmepruefung, Kapitel Praxisfallen),")
|
|
||||||
print(" * und der Status - nicht die Gestalt der Loesung.")
|
|
||||||
print("\n Wer doch einen bestimmten Plan braucht - fuer eine abgedruckte")
|
|
||||||
print(" Ausgabe, fuer einen Regressionstest -, setzt num_workers = 1.")
|
|
||||||
print(f" Das kostet hier den Faktor {faktor:.1f} an Laufzeit und ist genau")
|
|
||||||
print(" deshalb eine Entscheidung fuer den Test, nicht fuer den Betrieb.")
|
|
||||||
print("=" * 78)
|
|
||||||
|
|
@ -36,9 +36,9 @@ Benoetigt: numpy, pydantic, ortools, highspy (jeweils im eigenen Prozess)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import multiprocessing
|
import subprocess
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import BaseModel, Field, model_validator
|
from pydantic import BaseModel, Field, model_validator
|
||||||
|
|
@ -241,29 +241,25 @@ def geoeffnete_lager(problem: Standortproblem, loesung: Loesung) -> list[str]:
|
||||||
if any(loesung.werte[problem.schluessel(i, j)] > 0.5 for j in range(m))]
|
if any(loesung.werte[problem.schluessel(i, j)] > 0.5 for j in range(m))]
|
||||||
|
|
||||||
|
|
||||||
def loese_in_eigenem_prozess(name: str, problem: Standortproblem) -> Loesung:
|
def loese_in_eigenem_prozess(name: str) -> Loesung:
|
||||||
"""Laesst genau einen Modellbauer in einem frischen Prozess rechnen.
|
"""Startet dieses Programm noch einmal - mit genau einem Solverimport."""
|
||||||
|
ergebnis = subprocess.run([sys.executable, __file__, name],
|
||||||
'spawn' statt des Linux-Standards 'fork': Der Kindprozess startet mit
|
capture_output=True, text=True, timeout=300)
|
||||||
einem leeren Interpreter und importiert nur den Solver, den SEIN
|
if ergebnis.returncode != 0:
|
||||||
Modellbauer braucht. max_tasks_per_child=1 sorgt dafuer, dass der Pool
|
raise RuntimeError(ergebnis.stderr.strip().splitlines()[-1])
|
||||||
seinen Arbeiter nicht wiederverwendet - sonst saessen beim zweiten Aufruf
|
# Das DTO als JSON - genau dafuer ist ein Datenobjekt ohne Solverbezug gut.
|
||||||
wieder beide Bibliotheken im selben Prozess.
|
return Loesung.model_validate_json(ergebnis.stdout.strip().splitlines()[-1])
|
||||||
|
|
||||||
Hin und zurueck wandert das Domaenenmodell bzw. das Loesungs-DTO. Beide
|
|
||||||
kennen keinen Solver, sind also serialisierbar - genau dafuer sind sie da.
|
|
||||||
"""
|
|
||||||
with ProcessPoolExecutor(
|
|
||||||
max_workers=1,
|
|
||||||
mp_context=multiprocessing.get_context("spawn"),
|
|
||||||
max_tasks_per_child=1) as pool:
|
|
||||||
return pool.submit(MODELLBAUER[name], problem).result(timeout=300)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
problem = beispielproblem()
|
problem = beispielproblem()
|
||||||
|
|
||||||
# --- Beide Solver anstossen und vergleichen ---------------------------
|
# --- Kindprozess: rechnen und das DTO als JSON ausgeben ---------------
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
print(MODELLBAUER[sys.argv[1]](problem).model_dump_json())
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# --- Hauptprozess: beide Solver anstossen und vergleichen -------------
|
||||||
print("=" * 82)
|
print("=" * 82)
|
||||||
print(" DERSELBE FALL, ZWEI SOLVER - UND EIN AUSWERTUNGSCODE")
|
print(" DERSELBE FALL, ZWEI SOLVER - UND EIN AUSWERTUNGSCODE")
|
||||||
print("=" * 82)
|
print("=" * 82)
|
||||||
|
|
@ -275,7 +271,7 @@ if __name__ == "__main__":
|
||||||
loesungen: dict[str, Loesung] = {}
|
loesungen: dict[str, Loesung] = {}
|
||||||
for name, beschriftung in [("cpsat", "OR-Tools CP-SAT"),
|
for name, beschriftung in [("cpsat", "OR-Tools CP-SAT"),
|
||||||
("highs", "HiGHS (highspy)")]:
|
("highs", "HiGHS (highspy)")]:
|
||||||
loesung = loesungen[name] = loese_in_eigenem_prozess(name, problem)
|
loesung = loesungen[name] = loese_in_eigenem_prozess(name)
|
||||||
beanstandungen = pruefe_zuordnung(problem, loesung)
|
beanstandungen = pruefe_zuordnung(problem, loesung)
|
||||||
|
|
||||||
print(f"{beschriftung}")
|
print(f"{beschriftung}")
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,7 @@ def loese(dauer: np.ndarray, n_maschinen: int, zeitlimit: float,
|
||||||
|
|
||||||
loeser = cp_model.CpSolver()
|
loeser = cp_model.CpSolver()
|
||||||
loeser.parameters.max_time_in_seconds = zeitlimit
|
loeser.parameters.max_time_in_seconds = zeitlimit
|
||||||
# Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist -
|
# Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist.
|
||||||
# der Seed allein genuegt dafuer NICHT (Kapitel Constraint Programming).
|
|
||||||
# Im Produktivbetrieb laesst man beides auf den Standardwerten.
|
# Im Produktivbetrieb laesst man beides auf den Standardwerten.
|
||||||
loeser.parameters.num_workers = 1
|
loeser.parameters.num_workers = 1
|
||||||
loeser.parameters.random_seed = 1
|
loeser.parameters.random_seed = 1
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
|
@ -334,134 +334,6 @@ können Sie gezielt springen.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Für wen dieses Buch geschrieben ist {-}
|
|
||||||
|
|
||||||
Die Lernpfade weiter unten sind nach **Themen** geschnitten. Dieser Abschnitt ist nach
|
|
||||||
**Rollen** geschnitten: vier Einstiege für vier Arten, mit Optimierung zu tun zu haben.
|
|
||||||
Jeder nennt den Schmerzpunkt, drei Dinge, die das Buch dafür wirklich liefert — mit den
|
|
||||||
Zahlen, die es dazu misst —, den passenden Lernpfad und die Frage, an der es in dieser
|
|
||||||
Rolle am häufigsten scheitert.
|
|
||||||
|
|
||||||
Suchen Sie sich einen aus. Sie können auch alle vier überspringen; nötig sind sie nicht.
|
|
||||||
|
|
||||||
### Für Entscheider und Manager — der Geschäftsfall {-}
|
|
||||||
|
|
||||||
> *„Wir planen seit Jahren mit Erfahrungswerten, und es funktioniert. Warum sollte ein
|
|
||||||
> Modell das besser können — und was kostet mich der Irrtum, wenn ich es nicht tue?“*
|
|
||||||
|
|
||||||
* **Was Planen mit Durchschnitten kostet, wird hier beziffert, nicht behauptet.** Im
|
|
||||||
Kraftwerkseinsatz aus {ref:kap:supplychain} kostet ein Plan, der mit dem
|
|
||||||
Wind-**Erwartungswert** rechnet, in der Wirklichkeit **149 % mehr** als einer, der
|
|
||||||
40 Szenarien berücksichtigt — 874 870 € statt 351 356 € — und führt in **28 von
|
|
||||||
40 Szenarien** zum Lastabwurf statt in keinem.
|
|
||||||
* **„Wie sicher soll es sein?“ wird eine Preisfrage.**
|
|
||||||
{ref:sec:unsicherheit-chance-constraints} rechnet aus, was ein Prozentpunkt
|
|
||||||
Versorgungssicherheit kostet: rund 56 000 € auf dem Weg zu 80 %, rund 254 000 € zwischen
|
|
||||||
95 und 99 %. Das **4,5-fache**. Wer 99,9 % fordert, ohne diese Kurve gesehen zu haben,
|
|
||||||
fordert ins Blaue.
|
|
||||||
* **Für die Sitzung gibt es einen Bericht, keine Tabelle.**
|
|
||||||
{ref:sec:praxisfallen-attribution} beziffert je Nebenbedingung, was sie den Plan kostet,
|
|
||||||
und formuliert daraus Sätze in Alltagssprache. Für die Frage des Betriebsrats — *„warum
|
|
||||||
ausgerechnet ich?“* — liefert {ref:kap:praxisfallen} den Constraint-Trace, der die
|
|
||||||
einzelne Zuweisung begründet.
|
|
||||||
|
|
||||||
**Ihr Weg.** Zuerst Pfad E (2–4 Stunden): {ref:sec:einfuehrung-historischer-kontext-und-evolution}
|
|
||||||
und {ref:anhang:modellierungsmuster}. Wenn Sie danach wissen wollen, was in Ihrem Betrieb
|
|
||||||
geht, Pfad B und Projekt **P1** oder **P2**.
|
|
||||||
|
|
||||||
**Woran es scheitert.** Nicht am Modell, sondern an der Einführung — das ist der
|
|
||||||
meistgenannte Befund in {ref:kap:praxisfallen}. Und an einer Verwechslung, die dieses Buch
|
|
||||||
Ihnen nicht abnehmen kann: Ein Bericht erklärt das **Modell**, nicht die **Wirklichkeit**.
|
|
||||||
Ist ein Deckungsbeitrag falsch geschätzt, ist er überzeugend **und** falsch. Fragen Sie
|
|
||||||
deshalb jedes Mal, worauf eine Zahl beruht.
|
|
||||||
|
|
||||||
> **Eine Erwartung, die dieses Buch enttäuscht:** Es nennt keine ROI-Kennzahl und keine
|
|
||||||
> Amortisationsdauer. Beide hängen an Ihrem Betrieb, und jede Zahl hier wäre erfunden.
|
|
||||||
> Was es stattdessen liefert, ist das Werkzeug, Ihre eigene auszurechnen: den **EVPI** in
|
|
||||||
> {ref:kap:unsicherheit} (was eine perfekte Prognose überhaupt wert wäre — eine
|
|
||||||
> Obergrenze für jedes Prognoseprojekt) und die Kostenzurechnung aus
|
|
||||||
> {ref:sec:praxisfallen-attribution}.
|
|
||||||
|
|
||||||
### Für Ingenieure und Produktionsplaner — die Physik des Systems {-}
|
|
||||||
|
|
||||||
> *„Der Engpass wandert. Kaum ist eine Maschine entlastet, steht die nächste — und niemand
|
|
||||||
> kann mir vorher sagen, welche.“*
|
|
||||||
|
|
||||||
* **Der Engpass lässt sich benennen und bepreisen.**
|
|
||||||
{ref:sec:praxisfallen-attribution} rechnet für jede Kapazität aus, was die nächste Stunde
|
|
||||||
wert ist — und wie weit dieser Wert trägt. Im Beispiel gilt der Schattenpreis der
|
|
||||||
Lackiererei exakt bis **+15 Stunden**; eine ganze Sonderschicht von 30 Stunden bringt
|
|
||||||
deshalb 60 € statt der rechnerischen 120 €. **Die zweite Hälfte wäre bezahlt und
|
|
||||||
wirkungslos.**
|
|
||||||
* **Rüstzeiten, Reihenfolgen, Taktzeiten sind der Kern, nicht die Ausnahme.**
|
|
||||||
{ref:kap:cpsat} modelliert Maschinenbelegung mit Intervallvariablen,
|
|
||||||
{ref:kap:metaheuristiken} zeigt am selben Fall das Gesamtbild: Faustregel 2 497 Minuten
|
|
||||||
Rüstzeit, CP-SAT allein 2 628, Simulated Annealing 2 343, Annealing plus LNS **2 289** —
|
|
||||||
und die untere Schranke bei 1 768, damit sichtbar bleibt, wie viel Luft noch da ist.
|
|
||||||
* **Wenn der Solver zu lange braucht, gibt es Zahlen statt Ratschläge.**
|
|
||||||
{ref:sec:cpsat-parallele-suche} misst, was mehrere Suchstränge bringen (hier Faktor 12,3)
|
|
||||||
— und ab wo mehr Arbeiter wieder schaden.
|
|
||||||
|
|
||||||
**Ihr Weg.** Pfad B bis {ref:kap:cpsat}, dann Projekt **P5** (Produktionsplanung mit
|
|
||||||
Rüstkosten). Wenn Ihre echten Instanzen den Solver stehen lassen: {ref:kap:metaheuristiken}
|
|
||||||
und Projekt **P9**.
|
|
||||||
|
|
||||||
**Woran es scheitert.** An der Verwechslung von *bindend* und *teuer*. Eine Bedingung kann
|
|
||||||
den Plan berühren und trotzdem **nichts** kosten — im Beispiel von
|
|
||||||
{ref:sec:praxisfallen-attribution} gilt das für eine von fünf bindenden. Wer sie
|
|
||||||
nachverhandelt, gewinnt nichts und hält das Verfahren für nutzlos.
|
|
||||||
|
|
||||||
### Für Investoren und Finanzanalysten — Risiko und Ertrag {-}
|
|
||||||
|
|
||||||
> *„Die Optimierung liefert mir Gewichte, die kein Mensch halten würde: 60 % in einem
|
|
||||||
> Titel, null in fünf anderen. Und im nächsten Quartal sieht sie völlig anders aus.“*
|
|
||||||
|
|
||||||
* **Der Grund dafür steht in {ref:kap:finanzdaten}, nicht in {ref:kap:markowitz}.** Eine
|
|
||||||
aus kurzen Zeitreihen geschätzte Kovarianzmatrix ist schlecht konditioniert; der
|
|
||||||
Optimierer setzt dann auf Schätzrauschen. Shrinkage nach Ledoit-Wolf ist das Gegenmittel;
|
|
||||||
ihre Wirkung wird an **Eigenwertspektrum und Konditionszahl** gemessen, nicht behauptet.
|
|
||||||
* **Der VaR verschweigt genau das, worauf es ankommt.** In der Stichprobe aus
|
|
||||||
{ref:kap:cvar} sagt der VaR: „an 95 % der Tage höchstens **1,86 %** Verlust“. Der CVaR
|
|
||||||
sagt: „und wenn doch, dann im Mittel **2,99 %**“. Der schlechteste Tag der Stichprobe
|
|
||||||
liegt bei **−23,0 %** — am VaR ändert dieser eine Tag **nichts**, am CVaR sehr wohl.
|
|
||||||
Deshalb ist der CVaR die Größe, die man optimiert.
|
|
||||||
* **Transaktionskosten und Rebalancing sind Teil des Modells, nicht ein Abzug danach.**
|
|
||||||
{ref:kap:cvar} nimmt sie in die Zielfunktion, {ref:kap:handelsmaschine} setzt den
|
|
||||||
Backtest daneben — samt der Fallen, die einen guten Backtest wertlos machen.
|
|
||||||
|
|
||||||
**Ihr Weg.** Pfad D, und zwar **mit** {ref:kap:finanzdaten}. Danach Projekt **P6**
|
|
||||||
(Portfolio-Rebalancer) oder **P7** (Risikoreport mit CVaR und Stresstests).
|
|
||||||
|
|
||||||
**Woran es scheitert.** An der Reihenfolge. Wer bei Markowitz einsteigt und die
|
|
||||||
Datenkapitel überspringt, bekommt ein Modell, das seine eigenen Schätzfehler maximiert —
|
|
||||||
und merkt es erst im Echtgeldbetrieb.
|
|
||||||
|
|
||||||
### Für Studierende und Data Scientists — die Methodik {-}
|
|
||||||
|
|
||||||
> *„Ich kann ein Modell trainieren und den Fehler senken. Aber wie wird aus einer Prognose
|
|
||||||
> eine Entscheidung — und woher weiß ich, dass sie gut ist?“*
|
|
||||||
|
|
||||||
* **Die unbequemste Erkenntnis zuerst.** {ref:kap:prognose} zeigt an einem gerechneten
|
|
||||||
Fall, dass das Modell mit dem **schlechteren** MSE die **günstigere** Entscheidung
|
|
||||||
trifft. Prognosegüte und Entscheidungsqualität sind zwei verschiedene Größen; wer die
|
|
||||||
eine optimiert, bekommt die andere nicht geschenkt.
|
|
||||||
* **Dieselbe Mathematik, zwei Welten.** {ref:kap:bruecke} lässt **denselben Code** einmal
|
|
||||||
über eine Werkstatt und einmal über ein Depot laufen — und benennt die drei Stellen, an
|
|
||||||
denen die Analogie endet. Struktur zu erkennen ist die eigentliche Fähigkeit, nicht
|
|
||||||
Bibliothekskenntnis; zu wissen, wo eine Analogie aufhört, gehört dazu.
|
|
||||||
* **Es wird gerechnet, nicht nacherzählt.** Zu jedem Kapitel gehören Aufgaben mit
|
|
||||||
vollständigen Lösungen, Handrechnungen und ein „Finde den Denkfehler“ — und jedes
|
|
||||||
abgedruckte Ergebnis stammt aus einem echten Lauf des danebenstehenden Programms.
|
|
||||||
|
|
||||||
**Ihr Weg.** Pfad A, wenn Sie Zeit haben. Sonst Pfad B oder D bis zum ersten Projekt und
|
|
||||||
von dort zurück in die Grundlagen. Für eigene Fragestellungen: Projekt **P8**.
|
|
||||||
|
|
||||||
**Woran es scheitert.** An der Annahme, ein Solver sei eine Bibliothek wie jede andere.
|
|
||||||
Die schwierige Arbeit steckt in der Formulierung — {ref:kap:modellierung} ist deshalb kein
|
|
||||||
Vorgeplänkel, sondern das Kapitel, auf das alle anderen aufbauen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Lernpfade {-}
|
## Lernpfade {-}
|
||||||
|
|
||||||
Sie müssen nicht alles lesen, um etwas Nützliches bauen zu können.
|
Sie müssen nicht alles lesen, um etwas Nützliches bauen zu können.
|
||||||
|
|
@ -504,8 +376,7 @@ bei Markowitz einsteigt, optimiert Schätzrauschen und wundert sich über absurd
|
||||||
|
|
||||||
### Pfad E — Ich habe morgen ein konkretes Problem (2–4 Stunden) {-}
|
### Pfad E — Ich habe morgen ein konkretes Problem (2–4 Stunden) {-}
|
||||||
|
|
||||||
Lesen Sie {ref:sec:einfuehrung-historischer-kontext-und-evolution} (die vier Bausteine) und
|
Lesen Sie {ref:sec:einfuehrung-historischer-kontext-und-evolution} (die vier Bausteine), dann {ref:anhang:modellierungsmuster}
|
||||||
{ref:sec:modellierung-glossar} (welcher Satz welcher Baustein ist), dann {ref:anhang:modellierungsmuster}
|
|
||||||
([Modellierungsmuster](91_Anhang_Modellierungsmuster.md)) und suchen Sie dort das Muster,
|
([Modellierungsmuster](91_Anhang_Modellierungsmuster.md)) und suchen Sie dort das Muster,
|
||||||
das zu Ihrem Problem passt. Von jedem Muster führt ein Verweis in das zuständige Kapitel.
|
das zu Ihrem Problem passt. Von jedem Muster führt ein Verweis in das zuständige Kapitel.
|
||||||
Wie die gewählte Bibliothek es schreibt, steht in {ref:anhang:spickzettel}
|
Wie die gewählte Bibliothek es schreibt, steht in {ref:anhang:spickzettel}
|
||||||
|
|
@ -560,7 +431,6 @@ Pfad E oder um gezielt nach einem Thema zu suchen:
|
||||||
| `Propagation_Demo.py` | Propagation messbar | {ref:kap:cpsat} |
|
| `Propagation_Demo.py` | Propagation messbar | {ref:kap:cpsat} |
|
||||||
| `CP_SAT_Vertretungssystem.py` | Vertretungsplan | {ref:kap:cpsat} |
|
| `CP_SAT_Vertretungssystem.py` | Vertretungsplan | {ref:kap:cpsat} |
|
||||||
| `JobShop_Intervalle.py` | Job-Shop-Scheduling | {ref:kap:cpsat} |
|
| `JobShop_Intervalle.py` | Job-Shop-Scheduling | {ref:kap:cpsat} |
|
||||||
| `Parallele_Suche.py` | num_workers: Tempo gegen Reproduzierbarkeit | {ref:kap:cpsat} |
|
|
||||||
| `CP_SAT_Statusfaelle.py` | Die fünf Solver-Antworten | {ref:kap:cpsat} |
|
| `CP_SAT_Statusfaelle.py` | Die fünf Solver-Antworten | {ref:kap:cpsat} |
|
||||||
| `Strafgewichte.py` | Gewichte als Wechselkurse | {ref:kap:cpsat} |
|
| `Strafgewichte.py` | Gewichte als Wechselkurse | {ref:kap:cpsat} |
|
||||||
| `Min_Cost_Flow.py` | Netzwerkfluss | {ref:kap:graphen} |
|
| `Min_Cost_Flow.py` | Netzwerkfluss | {ref:kap:graphen} |
|
||||||
|
|
@ -600,7 +470,6 @@ Pfad E oder um gezielt nach einem Thema zu suchen:
|
||||||
| `Data_Snooping.py` | Bestes aus N Versuchen auf Rauschen | {ref:kap:handelsmaschine} |
|
| `Data_Snooping.py` | Bestes aus N Versuchen auf Rauschen | {ref:kap:handelsmaschine} |
|
||||||
| `Infeasibility_Diagnose.py` | Notfallplan statt Fehler | {ref:kap:praxisfallen} |
|
| `Infeasibility_Diagnose.py` | Notfallplan statt Fehler | {ref:kap:praxisfallen} |
|
||||||
| `Erklaerbarkeit.py` | Constraint-Trace, Was-wäre-wenn | {ref:kap:praxisfallen} |
|
| `Erklaerbarkeit.py` | Constraint-Trace, Was-wäre-wenn | {ref:kap:praxisfallen} |
|
||||||
| `Constraint_Attribution.py` | Was kostet welche Bedingung? Managementbericht | {ref:kap:praxisfallen} |
|
|
||||||
| `Betriebsueberwachung.py` | Status, Gap und Zeitausschöpfung | {ref:kap:praxisfallen} |
|
| `Betriebsueberwachung.py` | Status, Gap und Zeitausschöpfung | {ref:kap:praxisfallen} |
|
||||||
| `or_kern.py` | Gemeinsamer Unterbau: Domäne, Status, Prüfung | {ref:kap:praxisfallen} |
|
| `or_kern.py` | Gemeinsamer Unterbau: Domäne, Status, Prüfung | {ref:kap:praxisfallen} |
|
||||||
| `Solverwechsel_CPSAT_HiGHS.py` | Derselbe Fall in zwei Solvern | {ref:kap:praxisfallen} |
|
| `Solverwechsel_CPSAT_HiGHS.py` | Derselbe Fall in zwei Solvern | {ref:kap:praxisfallen} |
|
||||||
|
|
|
||||||
|
|
@ -528,8 +528,7 @@ ausfüllen können — vor der ersten Codezeile.
|
||||||
> $4x_1 + 3x_2 \le 600$ (Ofen); $x_1 \ge 40$ (Vertrag).
|
> $4x_1 + 3x_2 \le 600$ (Ofen); $x_1 \ge 40$ (Vertrag).
|
||||||
> * **Weiche Bedingungen:** keine.
|
> * **Weiche Bedingungen:** keine.
|
||||||
>
|
>
|
||||||
> Dieses Problem lösen Sie in der Aufgabe *Die Bäckerei programmieren*
|
> Dieses Problem lösen Sie in Übung 1.5 selbst.
|
||||||
> ({ref:sec:einfuehrung-uebungsaufgaben}) selbst.
|
|
||||||
|
|
||||||
Die Vorlage lässt sich fast wörtlich in Code übersetzen — wer die fünf Zeilen ausgefüllt
|
Die Vorlage lässt sich fast wörtlich in Code übersetzen — wer die fünf Zeilen ausgefüllt
|
||||||
hat, hat den größten Teil der Modellierungsarbeit schon erledigt:
|
hat, hat den größten Teil der Modellierungsarbeit schon erledigt:
|
||||||
|
|
@ -1143,7 +1142,7 @@ Ein Programm, das abstürzt, ist harmlos — Sie sehen sofort, dass etwas nicht
|
||||||
Gefährlich sind Programme, die fehlerfrei durchlaufen, einen Status `OPTIMAL` melden und
|
Gefährlich sind Programme, die fehlerfrei durchlaufen, einen Status `OPTIMAL` melden und
|
||||||
eine Zahl liefern, die betriebswirtschaftlich Unsinn ist. Genau solche Fälle üben wir hier.
|
eine Zahl liefern, die betriebswirtschaftlich Unsinn ist. Genau solche Fälle üben wir hier.
|
||||||
|
|
||||||
> **🐛 Finde den Denkfehler: Die Schreinerei verdoppelt ihren Gewinn**
|
> **🐛 Finde den Denkfehler 1.1: Die Schreinerei verdoppelt ihren Gewinn**
|
||||||
>
|
>
|
||||||
> Eine Kollegin hat das Schreinerei-Modell aus dem Kapitelanfang leicht umgebaut — sie
|
> Eine Kollegin hat das Schreinerei-Modell aus dem Kapitelanfang leicht umgebaut — sie
|
||||||
> fand die Schleife übersichtlicher als zwei einzelne `Add`-Aufrufe. Das Programm läuft,
|
> fand die Schleife übersichtlicher als zwei einzelne `Add`-Aufrufe. Das Programm läuft,
|
||||||
|
|
@ -1192,7 +1191,7 @@ eine Zahl liefern, die betriebswirtschaftlich Unsinn ist. Genau solche Fälle ü
|
||||||
|
|
||||||
## Micro-Quiz {#sec:einfuehrung-quiz}
|
## Micro-Quiz {#sec:einfuehrung-quiz}
|
||||||
|
|
||||||
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
|
> **❓ Micro-Quiz 1: Drei Fragen zum Selbstcheck**
|
||||||
>
|
>
|
||||||
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
||||||
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-einfuehrung).
|
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-einfuehrung).
|
||||||
|
|
|
||||||
|
|
@ -1243,7 +1243,7 @@ Prüfen Sie das Ergebnis und vergleichen Sie es mit dem Original.
|
||||||
|
|
||||||
## Finde den Denkfehler {#sec:fundament-denkfehler}
|
## Finde den Denkfehler {#sec:fundament-denkfehler}
|
||||||
|
|
||||||
> **🐛 Finde den Denkfehler: Die unauffällige Transposition**
|
> **🐛 Finde den Denkfehler 2.1: Die unauffällige Transposition**
|
||||||
>
|
>
|
||||||
> Ein Kollege überträgt ein Produktionsmodell in Matrixform. Zwei Produkte, zwei
|
> Ein Kollege überträgt ein Produktionsmodell in Matrixform. Zwei Produkte, zwei
|
||||||
> Ressourcen — die Technologiematrix ist quadratisch:
|
> Ressourcen — die Technologiematrix ist quadratisch:
|
||||||
|
|
@ -1288,7 +1288,7 @@ Prüfen Sie das Ergebnis und vergleichen Sie es mit dem Original.
|
||||||
|
|
||||||
## Micro-Quiz {#sec:fundament-quiz}
|
## Micro-Quiz {#sec:fundament-quiz}
|
||||||
|
|
||||||
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
|
> **❓ Micro-Quiz 2: Drei Fragen zum Selbstcheck**
|
||||||
>
|
>
|
||||||
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
||||||
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-fundament).
|
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-fundament).
|
||||||
|
|
|
||||||
|
|
@ -308,34 +308,6 @@ $$
|
||||||
> prüft `highspy` und `cvxpy` in der Paketübersicht nur auf Anwesenheit
|
> prüft `highspy` und `cvxpy` in der Paketübersicht nur auf Anwesenheit
|
||||||
> (`importlib.util.find_spec`) und importiert CVXPY erst im Funktionstest.
|
> (`importlib.util.find_spec`) und importiert CVXPY erst im Funktionstest.
|
||||||
|
|
||||||
### Wie die Isolation aussieht, wenn sie tragen soll
|
|
||||||
|
|
||||||
„Eigener Prozess" ist schnell gesagt. Die naheliegende Umsetzung — ein Codeschnipsel als
|
|
||||||
Zeichenkette an `python -c` übergeben — funktioniert und ist trotzdem die schlechteste:
|
|
||||||
Der Schnipsel ist für Editor, Linter und Testwerkzeug unsichtbar, ein Tippfehler darin
|
|
||||||
fällt erst zur Laufzeit auf, und übergeben lassen sich nur Zeichenketten.
|
|
||||||
|
|
||||||
Tragfähig ist stattdessen: **jeder Solver eine gewöhnliche Funktion mit lokalem Import**,
|
|
||||||
ausgeführt von einem `ProcessPoolExecutor` mit zwei Einstellungen, die zusammen die
|
|
||||||
Garantie ergeben:
|
|
||||||
|
|
||||||
| Einstellung | Wozu |
|
|
||||||
| --- | --- |
|
|
||||||
| `mp_context=multiprocessing.get_context("spawn")` | Der Kindprozess startet mit einem **frischen** Interpreter, statt den Speicher des Elternprozesses zu erben. Unter Linux ist `fork` der Standard — und damit wäre alles, was hier schon importiert ist, auch dort importiert. |
|
|
||||||
| `max_tasks_per_child=1` | Jede Aufgabe bekommt einen **neuen** Prozess. Ohne das verwendet der Pool seinen Arbeiter wieder, und beim zweiten Solver ist der Konflikt zurück. Genau dieser Fehler ist leicht zu machen und schwer zu finden. |
|
|
||||||
|
|
||||||
> **⚠️ `max_tasks_per_child=1` ist nicht optional**
|
|
||||||
> Ein Pool ohne diese Angabe ist der **Normalfall** — er soll seine Arbeiter ja
|
|
||||||
> wiederverwenden. Wer die Isolation über einen Pool herstellt und das vergisst, hat einen
|
|
||||||
> Prozesswechsel programmiert, aber keine Isolation gewonnen: Die zweite Aufgabe landet im
|
|
||||||
> selben Interpreter wie die erste. Der Absturz kommt dann nicht beim ersten Solver,
|
|
||||||
> sondern beim zweiten — und sieht aus wie ein Problem des zweiten.
|
|
||||||
|
|
||||||
Denselben Aufbau verwenden `Solverwechsel_CPSAT_HiGHS.py` ({ref:kap:praxisfallen}) und
|
|
||||||
`Benchmark_Skalierung.py` ({ref:kap:testing}). Dort wandern zusätzlich **Datenobjekte** über
|
|
||||||
die Prozessgrenze statt Zeichenketten — möglich, weil Domänenmodell und Lösungs-DTO keinen
|
|
||||||
Solver kennen ({ref:sec:praxisfallen-or-kern}).
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
|
@ -347,104 +319,85 @@ Kapitel Oekosystem: Dasselbe LP in vier Bibliotheken.
|
||||||
2*x1 + 3*x2 + x3 <= 50
|
2*x1 + 3*x2 + x3 <= 50
|
||||||
x >= 0
|
x >= 0
|
||||||
|
|
||||||
Deckt scipy.optimize, highspy, CVXPY und OR-Tools/GLOP ab, mit Kreuzvergleich
|
Deckt scipy.optimize, highspy, CVXPY und OR-Tools/GLOP ab, mit Kreuzvergleich am Ende.
|
||||||
am Ende.
|
|
||||||
|
|
||||||
WICHTIG: Jeder Solver laeuft in einem EIGENEN Prozess, weil sich ortools und
|
WICHTIG: Jeder Solver läuft in einem EIGENEN Prozess, weil sich ortools und
|
||||||
highspy auf vielen Systemen nicht gemeinsam importieren lassen (beide bringen
|
highspy auf vielen Systemen nicht gemeinsam importieren lassen (beide bringen
|
||||||
eine eigene HiGHS-Kopie mit -> Symbolkonflikt).
|
eine eigene HiGHS-Kopie mit -> Symbolkonflikt).
|
||||||
|
|
||||||
Die Isolation besorgt ein ProcessPoolExecutor. Drei Einstellungen ergeben
|
|
||||||
zusammen die Garantie:
|
|
||||||
|
|
||||||
mp_context "spawn" Der Kindprozess startet mit einem FRISCHEN
|
|
||||||
Interpreter, statt den Speicher des Elternprozesses
|
|
||||||
zu erben. Was hier schon importiert ist, ist dort
|
|
||||||
nicht importiert. Mit dem Standard "fork" auf Linux
|
|
||||||
waere das nicht so.
|
|
||||||
max_tasks_per_child=1 Jede Aufgabe bekommt einen NEUEN Prozess. Ohne das
|
|
||||||
wuerde der Pool seinen Arbeiter wiederverwenden - und
|
|
||||||
beim zweiten Solver waere der Konflikt zurueck.
|
|
||||||
max_workers=1 Haelt die vier Laeufe nacheinander. Nicht aus
|
|
||||||
Vorsicht, sondern damit die gemessenen Zeiten
|
|
||||||
vergleichbar bleiben.
|
|
||||||
|
|
||||||
Jeder Solver steht in einer eigenen Funktion mit LOKALEM Import. Das ist der
|
|
||||||
Unterschied zu einem Codestring, den man an 'python -c' uebergibt: Die
|
|
||||||
Funktion laesst sich einzeln aufrufen, testen und vom Editor pruefen - ein
|
|
||||||
String nicht.
|
|
||||||
|
|
||||||
Benoetigt: scipy, highspy, cvxpy, ortools
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import multiprocessing
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
|
||||||
|
|
||||||
ERWARTET = 530.0 # Ergebnis der Handrechnung zum Produktionsprogramm
|
ERWARTET = 530.0 # Ergebnis der Handrechnung zum Produktionsprogramm
|
||||||
|
|
||||||
# Die Instanz - einmal notiert, von allen vier Funktionen benutzt.
|
# Jeder Eintrag ist ein eigenständiges Miniprogramm, das sein Ergebnis als
|
||||||
ZIEL = [10.0, 15.0, 25.0]
|
# JSON auf stdout ausgibt. So bleibt jeder Import in seinem eigenen Prozess.
|
||||||
MATRIX = [[1, 1, 2], [2, 3, 1]]
|
ANSAETZE: dict[str, str] = {
|
||||||
KAPAZITAET = [40.0, 50.0]
|
|
||||||
|
|
||||||
|
"scipy.optimize.linprog": """
|
||||||
def loese_mit_scipy() -> tuple[float, list[float]]:
|
|
||||||
from scipy.optimize import linprog
|
from scipy.optimize import linprog
|
||||||
ergebnis = linprog(c=[-w for w in ZIEL], # linprog MINIMIERT -> negieren
|
res = linprog(c=[-10.0, -15.0, -25.0], # linprog MINIMIERT -> negieren
|
||||||
A_ub=MATRIX, b_ub=KAPAZITAET,
|
A_ub=[[1, 1, 2], [2, 3, 1]], b_ub=[40, 50],
|
||||||
bounds=[(0, None)] * 3, method="highs")
|
bounds=[(0, None)] * 3, method="highs")
|
||||||
return -ergebnis.fun, list(ergebnis.x)
|
ausgabe = (-res.fun, list(res.x))
|
||||||
|
""",
|
||||||
|
|
||||||
|
"highspy (natives HiGHS)": """
|
||||||
def loese_mit_highspy() -> tuple[float, list[float]]:
|
import numpy as np, highspy
|
||||||
import highspy
|
|
||||||
import numpy as np
|
|
||||||
h = highspy.Highs()
|
h = highspy.Highs()
|
||||||
h.setOptionValue("output_flag", False)
|
h.setOptionValue("output_flag", False)
|
||||||
h.addVars(3, np.zeros(3), np.full(3, highspy.kHighsInf))
|
h.addVars(3, np.zeros(3), np.full(3, highspy.kHighsInf))
|
||||||
h.changeObjectiveSense(highspy.ObjSense.kMaximize)
|
h.changeObjectiveSense(highspy.ObjSense.kMaximize)
|
||||||
for j, wert in enumerate(ZIEL):
|
for j, wert in enumerate([10.0, 15.0, 25.0]):
|
||||||
h.changeColCost(j, wert)
|
h.changeColCost(j, wert)
|
||||||
# CSR-Format: starts[i] = Beginn von Zeile i in indices/values
|
# CSR-Format: starts[i] = Beginn von Zeile i in indices/values
|
||||||
h.addRows(2, np.full(2, -highspy.kHighsInf), np.array(KAPAZITAET), 6,
|
h.addRows(2, np.full(2, -highspy.kHighsInf), np.array([40.0, 50.0]), 6,
|
||||||
np.array([0, 3], dtype=np.int32),
|
np.array([0, 3], dtype=np.int32),
|
||||||
np.array([0, 1, 2, 0, 1, 2], dtype=np.int32),
|
np.array([0, 1, 2, 0, 1, 2], dtype=np.int32),
|
||||||
np.array([float(w) for zeile in MATRIX for w in zeile]))
|
np.array([1.0, 1.0, 2.0, 2.0, 3.0, 1.0]))
|
||||||
h.run()
|
h.run()
|
||||||
return (h.getInfo().objective_function_value,
|
ausgabe = (h.getInfo().objective_function_value,
|
||||||
list(h.getSolution().col_value[:3]))
|
list(h.getSolution().col_value[:3]))
|
||||||
|
""",
|
||||||
|
|
||||||
|
"cvxpy": """
|
||||||
def loese_mit_cvxpy() -> tuple[float, list[float]]:
|
import numpy as np, cvxpy as cp
|
||||||
import cvxpy as cp
|
|
||||||
import numpy as np
|
|
||||||
x = cp.Variable(3, nonneg=True)
|
x = cp.Variable(3, nonneg=True)
|
||||||
problem = cp.Problem(cp.Maximize(np.array(ZIEL) @ x),
|
problem = cp.Problem(cp.Maximize(np.array([10.0, 15.0, 25.0]) @ x),
|
||||||
[np.array(MATRIX) @ x <= np.array(KAPAZITAET)])
|
[np.array([[1, 1, 2], [2, 3, 1]]) @ x <= np.array([40, 50])])
|
||||||
problem.solve()
|
problem.solve()
|
||||||
return float(problem.value), [float(v) for v in x.value]
|
ausgabe = (float(problem.value), [float(v) for v in x.value])
|
||||||
|
""",
|
||||||
|
|
||||||
|
"ortools / GLOP": """
|
||||||
def loese_mit_ortools() -> tuple[float, list[float]]:
|
|
||||||
from ortools.linear_solver import pywraplp
|
from ortools.linear_solver import pywraplp
|
||||||
s = pywraplp.Solver.CreateSolver("GLOP")
|
s = pywraplp.Solver.CreateSolver("GLOP")
|
||||||
x = [s.NumVar(0, s.infinity(), f"x{j+1}") for j in range(3)]
|
x = [s.NumVar(0, s.infinity(), f"x{j+1}") for j in range(3)]
|
||||||
for i, kapazitaet in enumerate(KAPAZITAET):
|
A = [[1, 1, 2], [2, 3, 1]]
|
||||||
s.Add(sum(MATRIX[i][j] * x[j] for j in range(3)) <= kapazitaet)
|
for i, kap in enumerate([40, 50]):
|
||||||
s.Maximize(sum(ZIEL[j] * x[j] for j in range(3)))
|
s.Add(sum(A[i][j] * x[j] for j in range(3)) <= kap)
|
||||||
|
s.Maximize(10 * x[0] + 15 * x[1] + 25 * x[2])
|
||||||
s.Solve()
|
s.Solve()
|
||||||
return s.Objective().Value(), [v.solution_value() for v in x]
|
ausgabe = (s.Objective().Value(), [v.solution_value() for v in x])
|
||||||
|
""",
|
||||||
|
|
||||||
ANSAETZE = {
|
|
||||||
"scipy.optimize.linprog": loese_mit_scipy,
|
|
||||||
"highspy (natives HiGHS)": loese_mit_highspy,
|
|
||||||
"cvxpy": loese_mit_cvxpy,
|
|
||||||
"ortools / GLOP": loese_mit_ortools,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fuehre_in_eigenem_prozess_aus(quelltext: str) -> tuple[float, list[float]]:
|
||||||
|
"""Startet den Codeschnipsel als separaten Python-Prozess und liest das Ergebnis."""
|
||||||
|
programm = textwrap.dedent(quelltext) + "\nimport json; print(json.dumps(ausgabe))\n"
|
||||||
|
ergebnis = subprocess.run([sys.executable, "-c", programm],
|
||||||
|
capture_output=True, text=True, timeout=120)
|
||||||
|
if ergebnis.returncode != 0:
|
||||||
|
raise RuntimeError(ergebnis.stderr.strip().splitlines()[-1])
|
||||||
|
wert, loesung = json.loads(ergebnis.stdout.strip().splitlines()[-1])
|
||||||
|
return wert, loesung
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("=" * 78)
|
print("=" * 78)
|
||||||
print(" EIN SYSTEM - VIER ANSAETZE (je eigener Prozess)")
|
print(" EIN SYSTEM - VIER ANSAETZE (je eigener Prozess)")
|
||||||
|
|
@ -453,20 +406,14 @@ if __name__ == "__main__":
|
||||||
print("-" * 78)
|
print("-" * 78)
|
||||||
|
|
||||||
werte = []
|
werte = []
|
||||||
# Ein Pool, vier Aufgaben, vier frische Prozesse. Der Kontext muss
|
for name, quelltext in ANSAETZE.items():
|
||||||
# "spawn" sein - siehe Modulkommentar.
|
t0 = time.perf_counter()
|
||||||
with ProcessPoolExecutor(
|
|
||||||
max_workers=1,
|
|
||||||
mp_context=multiprocessing.get_context("spawn"),
|
|
||||||
max_tasks_per_child=1) as pool:
|
|
||||||
for name, funktion in ANSAETZE.items():
|
|
||||||
beginn = time.perf_counter()
|
|
||||||
try:
|
try:
|
||||||
wert, x = pool.submit(funktion).result(timeout=120)
|
wert, x = fuehre_in_eigenem_prozess_aus(quelltext)
|
||||||
except Exception as fehler: # Bibliothek fehlt o. Ae.
|
except RuntimeError as fehler:
|
||||||
print(f"{name:<26} nicht verfuegbar: {str(fehler)[:40]}")
|
print(f"{name:<26} nicht verfuegbar: {fehler[:40]}")
|
||||||
continue
|
continue
|
||||||
dauer = time.perf_counter() - beginn
|
dauer = time.perf_counter() - t0
|
||||||
werte.append(wert)
|
werte.append(wert)
|
||||||
print(f"{name:<26} {wert:>10.2f} {x[0]:>7.2f} {x[1]:>7.2f} {x[2]:>7.2f} "
|
print(f"{name:<26} {wert:>10.2f} {x[0]:>7.2f} {x[1]:>7.2f} {x[2]:>7.2f} "
|
||||||
f"{dauer:>8.2f} s")
|
f"{dauer:>8.2f} s")
|
||||||
|
|
@ -479,8 +426,8 @@ if __name__ == "__main__":
|
||||||
assert spanne < 1e-6, "Die Bibliotheken widersprechen sich!"
|
assert spanne < 1e-6, "Die Bibliotheken widersprechen sich!"
|
||||||
assert abs(werte[0] - ERWARTET) < 1e-6, "Ergebnis weicht von der Handrechnung ab!"
|
assert abs(werte[0] - ERWARTET) < 1e-6, "Ergebnis weicht von der Handrechnung ab!"
|
||||||
print("Alle Wege fuehren zum selben, von Hand bestaetigten Optimum.")
|
print("Alle Wege fuehren zum selben, von Hand bestaetigten Optimum.")
|
||||||
print("(Die Zeiten enthalten Prozessstart und Import - sie messen NICHT die")
|
print("(Die Zeiten enthalten den Prozessstart und den Import - sie messen")
|
||||||
print(" reine Solverleistung. Die Uebungsaufgabe 'Laufzeitvergleich' trennt beides.)")
|
print(" NICHT die reine Solverleistung, siehe Uebung 3.5.)")
|
||||||
print("=" * 78)
|
print("=" * 78)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -492,16 +439,16 @@ if __name__ == "__main__":
|
||||||
==============================================================================
|
==============================================================================
|
||||||
Bibliothek Z* x1 x2 x3 Zeit
|
Bibliothek Z* x1 x2 x3 Zeit
|
||||||
------------------------------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
scipy.optimize.linprog 530.00 0.00 12.00 14.00 0.59 s
|
scipy.optimize.linprog 530.00 0.00 12.00 14.00 0.55 s
|
||||||
highspy (natives HiGHS) 530.00 0.00 12.00 14.00 0.12 s
|
highspy (natives HiGHS) 530.00 0.00 12.00 14.00 0.17 s
|
||||||
cvxpy 530.00 0.00 12.00 14.00 1.24 s
|
cvxpy 530.00 0.00 12.00 14.00 1.52 s
|
||||||
ortools / GLOP 530.00 0.00 12.00 14.00 0.33 s
|
ortools / GLOP 530.00 0.00 12.00 14.00 0.09 s
|
||||||
------------------------------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
Spannweite zwischen den Bibliotheken: 2.41e-08
|
Spannweite zwischen den Bibliotheken: 2.41e-08
|
||||||
Abweichung zur Handrechnung (530): 0.00e+00
|
Abweichung zur Handrechnung (530): 0.00e+00
|
||||||
Alle Wege fuehren zum selben, von Hand bestaetigten Optimum.
|
Alle Wege fuehren zum selben, von Hand bestaetigten Optimum.
|
||||||
(Die Zeiten enthalten Prozessstart und Import - sie messen NICHT die
|
(Die Zeiten enthalten den Prozessstart und den Import - sie messen
|
||||||
reine Solverleistung. Die Uebungsaufgabe 'Laufzeitvergleich' trennt beides.)
|
NICHT die reine Solverleistung, siehe Uebung 3.5.)
|
||||||
==============================================================================
|
==============================================================================
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -1215,7 +1162,7 @@ passende Empfehlungen.
|
||||||
|
|
||||||
## Finde den Denkfehler {#sec:oekosystem-denkfehler}
|
## Finde den Denkfehler {#sec:oekosystem-denkfehler}
|
||||||
|
|
||||||
> **🐛 Finde den Denkfehler: Der Solver, der angeblich dreimal schneller ist**
|
> **🐛 Finde den Denkfehler 3.1: Der Solver, der angeblich dreimal schneller ist**
|
||||||
>
|
>
|
||||||
> Ein Team vergleicht zwei Bibliotheken für sein Zuordnungsproblem und schreibt ins
|
> Ein Team vergleicht zwei Bibliotheken für sein Zuordnungsproblem und schreibt ins
|
||||||
> Protokoll: *„CVXPY braucht 1,9 s, SciPy nur 0,6 s — wir setzen auf SciPy.“* Gemessen
|
> Protokoll: *„CVXPY braucht 1,9 s, SciPy nur 0,6 s — wir setzen auf SciPy.“* Gemessen
|
||||||
|
|
@ -1252,7 +1199,7 @@ passende Empfehlungen.
|
||||||
|
|
||||||
## Micro-Quiz {#sec:oekosystem-quiz}
|
## Micro-Quiz {#sec:oekosystem-quiz}
|
||||||
|
|
||||||
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
|
> **❓ Micro-Quiz 3: Drei Fragen zum Selbstcheck**
|
||||||
>
|
>
|
||||||
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
||||||
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-oekosystem).
|
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-oekosystem).
|
||||||
|
|
@ -1317,4 +1264,4 @@ Schattenpreise kommen — und was sie wirtschaftlich bedeuten.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Weiter mit:* [{ref:kap:modellierung} — Vom Management-Wunsch zum Modell](13_Vom_Wunsch_zum_Modell.md)
|
*Weiter mit:* [{ref:kap:lp} — Lineare Programmierung](20_Lineare_Programmierung.md)
|
||||||
|
|
|
||||||
|
|
@ -99,9 +99,6 @@ Nach diesem Kapitel können Sie …
|
||||||
4. … begründen, wann eine Regel hart und wann sie weich sein muss — und was eine harte Regel
|
4. … begründen, wann eine Regel hart und wann sie weich sein muss — und was eine harte Regel
|
||||||
im Betrieb anrichtet, wenn sie einmal nicht erfüllbar ist.
|
im Betrieb anrichtet, wenn sie einmal nicht erfüllbar ist.
|
||||||
5. … einen Kennzahlen-Fehlanreiz benennen, bevor er in die Zielfunktion gerät.
|
5. … einen Kennzahlen-Fehlanreiz benennen, bevor er in die Zielfunktion gerät.
|
||||||
6. … einen Satz aus einer Besprechung in den passenden Modellierungsbaustein
|
|
||||||
übersetzen — und die fünf Wendungen erkennen, bei denen genau das nicht geht,
|
|
||||||
ohne vorher nachzufragen.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -617,75 +614,6 @@ stellt, bevor eine Zeile Code entsteht:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deutsch — OR: ein Übersetzungslexikon {#sec:modellierung-glossar}
|
|
||||||
|
|
||||||
Die zwölf Fragen liefern Sätze. Sätze sind noch kein Modell — aber die meisten von ihnen
|
|
||||||
haben eine **Standardübersetzung**, und wer sie kennt, spart sich das Nachdenken für die
|
|
||||||
Fälle, in denen es keine gibt.
|
|
||||||
|
|
||||||
Die Tabelle liest sich von links nach rechts: Was in der Besprechung **gesagt** wird, was
|
|
||||||
es **bedeutet**, und welcher **Baustein** daraus wird. Die B-Nummern verweisen auf
|
|
||||||
{ref:anhang:modellierungsmuster}, wo jedes Muster mit Formulierung und Code steht.
|
|
||||||
|
|
||||||
| Was gesagt wird | Was gemeint ist | Baustein |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| „Wir haben nur 400 Stunden." | eine Obergrenze, die nicht überschritten werden darf | harte Kapazitätsgrenze $\le b$ ({ref:kap:lp}) |
|
|
||||||
| „Mehr als zwei Millionen gibt es nicht." | dasselbe, auf Geld | Budgetlimit (B27) |
|
|
||||||
| „Möglichst nicht über 40 Stunden." | eine Grenze, die im Notfall gebrochen werden darf | weiche Grenze mit Strafkosten (B8) |
|
|
||||||
| „Wir können nur ganze Kisten liefern." | die Menge ist nicht teilbar | Ganzzahligkeitsbedingung ({ref:kap:milp}) |
|
|
||||||
| „Wenn wir die Anlage nutzen, fallen Fixkosten an." | Kosten, die erst ab der ersten Einheit anfallen | Aktivierungsschalter (B1) |
|
|
||||||
| „Entweder gar nicht oder mindestens 50." | kein Betrieb im Kleinen | semikontinuierliche Variable (B2) |
|
|
||||||
| „Wenn wir A bauen, brauchen wir auch B." | eine Folgeverpflichtung | Implikation (B3) |
|
|
||||||
| „Höchstens drei Positionen im Depot." | eine Obergrenze auf die **Anzahl**, nicht die Menge | Kardinalität (B6) |
|
|
||||||
| „Der Anteil muss mindestens 30 % betragen." | eine Bedingung auf ein **Verhältnis** | Verhältnis-Bedingung (B12) |
|
|
||||||
| „Jeder Auftrag genau einem Mitarbeiter." | eine vollständige, eindeutige Zuordnung | Zuordnung 1:1 (B23) |
|
|
||||||
| „Eine Maschine kann nur eines gleichzeitig." | zeitliche Ausschließlichkeit | Nichtüberlappung (B14) |
|
|
||||||
| „B kann erst nach A." | eine Reihenfolge | Vorrangbeziehung (B13) |
|
|
||||||
| „Höchstens fünf Tage am Stück." | eine Bedingung über ein **gleitendes Fenster** | gleitendes Fenster (B16) |
|
|
||||||
| „Umstellen kostet uns eine halbe Stunde." | reihenfolgeabhängige Kosten | Umrüstkosten (B17), Rüstzeit als Kapazität (B26) |
|
|
||||||
| „Was reinkommt, muss auch wieder raus." | Erhaltung an jedem Knoten | Flusserhaltung (B22) |
|
|
||||||
| „Notfalls brechen wir die Regel." | die Bedingung ist weich, aber teuer | Schlupfvariablen (B18) |
|
|
||||||
| „Erst Termintreue, dann Kosten." | eine Rangfolge zwischen Zielen | hierarchische Ziele (B19) |
|
|
||||||
| „Es soll gerecht zugehen." | den Schlechtestgestellten verbessern | Min/Max in der Zielfunktion (B11) |
|
|
||||||
| „Was brächte uns eine zusätzliche Stunde?" | die Frage nach dem Grenzwert einer Ressource | Schattenpreis ({ref:kap:lp}, {ref:sec:praxisfallen-attribution}) |
|
|
||||||
| „Mit 95 % Sicherheit muss es halten." | eine Zusage auf eine **Quote**, nicht auf einen Mittelwert | Chance Constraint ({ref:sec:unsicherheit-chance-constraints}) |
|
|
||||||
| „Wir sichern uns gegen Schätzfehler ab." | gegen die ungünstigste Parameterlage planen | Worst-Case-Abzug (B21) |
|
|
||||||
|
|
||||||
### Fünf Wendungen, bei denen die Übersetzung eine Entscheidung erzwingt
|
|
||||||
|
|
||||||
Die Tabelle oben tut so, als sei die Zuordnung eindeutig. Bei den folgenden fünf ist sie es
|
|
||||||
nicht — und **das Nachfragen ist die eigentliche Modellierungsarbeit**:
|
|
||||||
|
|
||||||
| Wendung | Die Frage, die dahinter steckt |
|
|
||||||
| --- | --- |
|
|
||||||
| **„möglichst"** | Hart oder weich? Und wenn weich: Was darf ein Verstoß kosten? Ohne Zahl ist „möglichst" keine Bedingung, sondern eine Stimmung. |
|
|
||||||
| **„nicht mehr als drei pro Woche"** | Je Person oder über alle zusammen? Kalenderwoche oder gleitende sieben Tage? Beide Lesarten sind vertretbar und ergeben verschiedene Modelle. |
|
|
||||||
| **„im Durchschnitt"** | Fast immer eine Falle. Der Durchschnittskunde kauft nie — siehe {ref:sec:unsicherheit-der-fluch-des-durchschnitts}. Gemeint ist meist ein Quantil („in 9 von 10 Fällen"). |
|
|
||||||
| **„so schnell wie möglich"** | Welche Zahl? Ende des letzten Auftrags, Summe aller Fertigstellungszeiten oder größte Verspätung? Drei Zielfunktionen, drei verschiedene Pläne. |
|
|
||||||
| **„fair"** | Den Schlechtesten verbessern, die Spannweite verkleinern oder gleiche Anteile erzwingen? Erst die Wahl macht daraus ein Modell — und sie gehört dem Betrieb, nicht dem Modellierer. |
|
|
||||||
|
|
||||||
> **🎯 Der Nutzen dieser Liste**
|
|
||||||
> Sie ersetzt kein Nachdenken. Sie sorgt dafür, dass das Nachdenken an den **richtigen** vier
|
|
||||||
> oder fünf Stellen stattfindet, statt an allen zwanzig gleichzeitig.
|
|
||||||
|
|
||||||
### Und die Gegenrichtung: OR — Deutsch
|
|
||||||
|
|
||||||
Dieselbe Barriere gibt es rückwärts. Diese Sätze haben sich bewährt, wenn ein Ergebnis
|
|
||||||
jemandem erklärt werden muss, der das Modell nicht kennt:
|
|
||||||
|
|
||||||
| Fachbegriff | Was Sie in der Besprechung sagen |
|
|
||||||
| --- | --- |
|
|
||||||
| Schattenpreis / Dualwert | „Die nächste Stunde auf dieser Maschine wäre uns 9 € wert." |
|
|
||||||
| bindende Bedingung mit Dualwert 0 | „Die Regel berührt den Plan, kostet uns aber nichts — dort zu verhandeln bringt nichts." |
|
|
||||||
| `INFEASIBLE` | „Ihre Regeln widersprechen sich." **Nicht:** „Der Computer schafft es nicht." |
|
|
||||||
| Optimalitätslücke von 2 % | „Besser als das hier geht es höchstens um 2 % — und die liegen unter unserer Datenungenauigkeit." |
|
|
||||||
| CVaR | „Wenn es schiefgeht, verlieren wir im Mittel so viel." (Der VaR sagt nur, **ob** es schiefgeht.) |
|
|
||||||
| EVPI | „Selbst eine perfekte Prognose wäre uns höchstens so viel wert." |
|
|
||||||
| Relaxation | „Wir haben die Regel probeweise gelockert, um zu sehen, was sie kostet." |
|
|
||||||
| Chance Constraint | „Der Plan hält in 95 von 100 Fällen — und der nächste Prozentpunkt kostet …" |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Übungsaufgaben {#sec:modellierung-uebungsaufgaben}
|
## Übungsaufgaben {#sec:modellierung-uebungsaufgaben}
|
||||||
|
|
||||||
> Lösungen: {ref:sec:loesungen-modellierung}.
|
> Lösungen: {ref:sec:loesungen-modellierung}.
|
||||||
|
|
@ -709,16 +637,6 @@ Maximieren Sie $\text{DB} + \lambda \cdot \text{Umsatz}$ für $\lambda \in \{0;
|
||||||
und tragen Sie die Ergebnisse als Tabelle auf. Ab welchem $\lambda$ kippt der Plan von der
|
und tragen Sie die Ergebnisse als Tabelle auf. Ab welchem $\lambda$ kippt der Plan von der
|
||||||
DB- in die Umsatzlösung? Was sagt dieser Wert dem Betrieb?
|
DB- in die Umsatzlösung? Was sagt dieser Wert dem Betrieb?
|
||||||
|
|
||||||
**Aufgabe ⭐⭐ — Übersetzen, wo es nicht eindeutig ist.**
|
|
||||||
Übersetzen Sie mit {ref:sec:modellierung-glossar}:
|
|
||||||
(a) „Die Monteure sollen möglichst gleichmäßig ausgelastet sein." Nennen Sie **drei**
|
|
||||||
Zielfunktionen, die alle „gleichmäßig" bedeuten können, und je einen Fall, in dem die
|
|
||||||
Wahl den Plan sichtbar ändert.
|
|
||||||
(b) „Kein Kunde darf länger als drei Tage warten." Formulieren Sie die Bedingung einmal
|
|
||||||
hart und einmal weich. Welche Frage müssen Sie dem Betrieb stellen, um zu entscheiden?
|
|
||||||
(c) Suchen Sie in einer echten Anforderung aus Ihrem Umfeld einen Satz, der nach der
|
|
||||||
Tabelle eindeutig aussieht, es aber nicht ist.
|
|
||||||
|
|
||||||
**Aufgabe ⭐⭐⭐ — Die Anforderung schreiben.**
|
**Aufgabe ⭐⭐⭐ — Die Anforderung schreiben.**
|
||||||
Formulieren Sie für den Fall dieses Kapitels eine vollständige, prüfbare Anforderung: Ziel,
|
Formulieren Sie für den Fall dieses Kapitels eine vollständige, prüfbare Anforderung: Ziel,
|
||||||
harte Regeln, weiche Regeln mit Strafgewichten, Verhalten bei Unlösbarkeit, auszuweisende
|
harte Regeln, weiche Regeln mit Strafgewichten, Verhalten bei Unlösbarkeit, auszuweisende
|
||||||
|
|
@ -802,14 +720,6 @@ Kennzahlen. Zwei Seiten genügen — sie sind mehr wert als zwei Wochen Programm
|
||||||
* **Hart ist nur, was rechtlich oder physikalisch unmöglich ist.** Eine harte Regel ist eine
|
* **Hart ist nur, was rechtlich oder physikalisch unmöglich ist.** Eine harte Regel ist eine
|
||||||
weiche mit hinreichend großer Strafe; der Unterschied zeigt sich erst, wenn sie einmal
|
weiche mit hinreichend großer Strafe; der Unterschied zeigt sich erst, wenn sie einmal
|
||||||
nicht erfüllbar ist — dann liefert die harte Variante gar nichts mehr.
|
nicht erfüllbar ist — dann liefert die harte Variante gar nichts mehr.
|
||||||
* **Die meisten Sätze haben eine Standardübersetzung** — das Lexikon in
|
|
||||||
{ref:sec:modellierung-glossar} nennt sie. Sein Wert liegt aber in den fünf Wendungen, bei
|
|
||||||
denen es keine gibt: „möglichst", „nicht mehr als", „im Durchschnitt", „so schnell wie
|
|
||||||
möglich", „fair". Dort ist Nachfragen die Modellierungsarbeit.
|
|
||||||
* Die vier Fragen — worüber wird entschieden, woran gemessen, was ist unverhandelbar, was
|
* Die vier Fragen — worüber wird entschieden, woran gemessen, was ist unverhandelbar, was
|
||||||
bei Unlösbarkeit — gehören in die erste Besprechung. Der Leitfaden in
|
bei Unlösbarkeit — gehören in die erste Besprechung. Der Leitfaden in
|
||||||
{ref:sec:modellierung-leitfaden} macht zwölf daraus.
|
{ref:sec:modellierung-leitfaden} macht zwölf daraus.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Weiter mit:* [Synthese Teil I](19_Synthese_Grundlagen.md)
|
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
# Synthese Teil I — Grundlagen auf einen Blick
|
|
||||||
|
|
||||||
Vier Kapitel, eine Frage: **Was muss vorliegen, bevor ein Solver überhaupt sinnvoll
|
|
||||||
gestartet werden kann?** Die Antwort ist unbequem, weil das meiste davon nichts mit
|
|
||||||
Programmieren zu tun hat.
|
|
||||||
|
|
||||||
## Was welches Kapitel klärt
|
|
||||||
|
|
||||||
| Frage | Kapitel | Was Sie danach können |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Was ist überhaupt ein Optimierungsmodell? | {ref:kap:einfuehrung} | Die vier Bausteine benennen — Variablen, Zielfunktion, Nebenbedingungen, Daten — und sie in einem fremden Problem wiedererkennen |
|
|
||||||
| Warum rechnet der Solver falsch, obwohl das Modell stimmt? | {ref:kap:fundament} | Konvexität beurteilen, den zulässigen Bereich als Polyeder sehen, schlechte Konditionierung erkennen und beheben |
|
|
||||||
| Welche Bibliothek, und warum vertragen sie sich nicht? | {ref:kap:oekosystem} | Solver, Bindings und Modellierungsschichten unterscheiden — und den HiGHS-Symbolkonflikt zwischen `ortools` und `highspy` umgehen |
|
|
||||||
| Wie wird aus einem Satz ein Modell? | {ref:kap:modellierung} | Die vier Fragen stellen, harte von weichen Regeln trennen, einen Besprechungssatz übersetzen ({ref:sec:modellierung-glossar}) |
|
|
||||||
|
|
||||||
## Die Reihenfolge ist die Aussage
|
|
||||||
|
|
||||||
Dieser Teil steht bewusst **vor** den Verfahren. Die drei häufigsten Gründe, aus denen ein
|
|
||||||
OR-Projekt scheitert, liegen alle hier — und keiner davon ist ein Solverproblem:
|
|
||||||
|
|
||||||
1. **Die falsche Zielgröße.** Sie ist die folgenreichste Entscheidung des ganzen Projekts,
|
|
||||||
und sie wird meist in den ersten zehn Minuten nebenbei getroffen. Auslastung ist ein
|
|
||||||
Ergebnis, kein Ziel; „Umsatz" wird gesagt, wenn „Deckungsbeitrag" gemeint ist.
|
|
||||||
2. **Zu viele harte Regeln.** Jede unnötig harte Bedingung ist eine Zeitbombe: Irgendwann
|
|
||||||
ist sie nicht erfüllbar, und das System antwortet gar nicht mehr. Hart ist nur, was
|
|
||||||
rechtlich oder physikalisch unmöglich ist.
|
|
||||||
3. **Ein Satz, der mehrere Lesarten hat.** „Stammkunden dürfen wir nicht verlieren" lässt
|
|
||||||
sich auf mindestens drei Arten modellieren, und keine ist aus dem Satz ableitbar. Die
|
|
||||||
Wahl trifft man — im Zweifel unbewusst.
|
|
||||||
|
|
||||||
## Wenn Sie nur eines mitnehmen
|
|
||||||
|
|
||||||
> **🎯** Ein Modell rechnet aus, was **aufgeschrieben** wurde, nicht was gemeint war. Die
|
|
||||||
> Arbeit, die darüber entscheidet, ob am Ende etwas Brauchbares herauskommt, findet vor der
|
|
||||||
> ersten Zeile Code statt — und sie besteht aus Fragen an andere Menschen, nicht aus
|
|
||||||
> Mathematik.
|
|
||||||
|
|
||||||
**Vor {ref:teil:kernverfahren}** sollten Sie an einem eigenen Beispiel die vier Fragen aus
|
|
||||||
{ref:sec:modellierung-vier-fragen} durchgespielt haben. Ohne das bleiben die Verfahren
|
|
||||||
Technik ohne Anwendung.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Weiter mit:* [{ref:kap:lp} — Lineare Programmierung](20_Lineare_Programmierung.md)
|
|
||||||
|
|
@ -933,8 +933,7 @@ Deckungsbeitrag, also 15,33 € Reingewinn.
|
||||||
> Kapazität. Kauft man 200 Prüfstunden zu, wird irgendwann eine andere Ressource zum
|
> Kapazität. Kauft man 200 Prüfstunden zu, wird irgendwann eine andere Ressource zum
|
||||||
> Engpass, und der Schattenpreis springt auf einen neuen Wert (oder auf null). Wer große
|
> Engpass, und der Schattenpreis springt auf einen neuen Wert (oder auf null). Wer große
|
||||||
> Kapazitätsänderungen bewerten will, muss das Modell **neu rechnen** — nicht linear
|
> Kapazitätsänderungen bewerten will, muss das Modell **neu rechnen** — nicht linear
|
||||||
> hochrechnen. Die Aufgabe *Gültigkeitsbereich des Schattenpreises*
|
> hochrechnen. Übung 4.6 macht diesen Effekt sichtbar.
|
||||||
> ({ref:sec:lp-uebungsaufgaben}) macht diesen Effekt sichtbar.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -1318,7 +1317,7 @@ $$\max\ 3x_1+2x_2 \quad\text{u. d. N.}\quad x_1+x_2 \ge 4,\quad x_1+3x_2 \le 12,
|
||||||
|
|
||||||
## Finde den Denkfehler {#sec:lp-denkfehler}
|
## Finde den Denkfehler {#sec:lp-denkfehler}
|
||||||
|
|
||||||
> **🐛 Finde den Denkfehler: Die 380 000-Euro-Maschine**
|
> **🐛 Finde den Denkfehler 4.1: Die 380 000-Euro-Maschine**
|
||||||
>
|
>
|
||||||
> Ein Werksleiter lässt die Engpässe seiner Fertigung analysieren. Das Skript liefert
|
> Ein Werksleiter lässt die Engpässe seiner Fertigung analysieren. Das Skript liefert
|
||||||
> für den Lackierofen einen Schattenpreis von **50 €/Stunde**. Er rechnet:
|
> für den Lackierofen einen Schattenpreis von **50 €/Stunde**. Er rechnet:
|
||||||
|
|
@ -1357,7 +1356,7 @@ $$\max\ 3x_1+2x_2 \quad\text{u. d. N.}\quad x_1+x_2 \ge 4,\quad x_1+3x_2 \le 12,
|
||||||
|
|
||||||
## Micro-Quiz {#sec:lp-quiz}
|
## Micro-Quiz {#sec:lp-quiz}
|
||||||
|
|
||||||
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
|
> **❓ Micro-Quiz 4: Drei Fragen zum Selbstcheck**
|
||||||
>
|
>
|
||||||
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
||||||
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-lp).
|
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-lp).
|
||||||
|
|
|
||||||
|
|
@ -1308,8 +1308,7 @@ def loese(dauer: np.ndarray, n_maschinen: int, zeitlimit: float,
|
||||||
|
|
||||||
loeser = cp_model.CpSolver()
|
loeser = cp_model.CpSolver()
|
||||||
loeser.parameters.max_time_in_seconds = zeitlimit
|
loeser.parameters.max_time_in_seconds = zeitlimit
|
||||||
# Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist -
|
# Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist.
|
||||||
# der Seed allein genuegt dafuer NICHT (Kapitel Constraint Programming).
|
|
||||||
# Im Produktivbetrieb laesst man beides auf den Standardwerten.
|
# Im Produktivbetrieb laesst man beides auf den Standardwerten.
|
||||||
loeser.parameters.num_workers = 1
|
loeser.parameters.num_workers = 1
|
||||||
loeser.parameters.random_seed = 1
|
loeser.parameters.random_seed = 1
|
||||||
|
|
@ -1472,7 +1471,7 @@ eröffnete Lager hat Kapazität 80. Minimieren Sie Fix- plus Transportkosten.
|
||||||
Die Warnung „$M$ so klein wie möglich“ wird meist mit der Laufzeit begründet: schwache
|
Die Warnung „$M$ so klein wie möglich“ wird meist mit der Laufzeit begründet: schwache
|
||||||
Relaxation, mehr Knoten. Das stimmt — und ist die **harmlosere** Hälfte der Wahrheit.
|
Relaxation, mehr Knoten. Das stimmt — und ist die **harmlosere** Hälfte der Wahrheit.
|
||||||
|
|
||||||
> **🐛 Finde den Denkfehler: Elf Lager, die keine Fixkosten kosten**
|
> **🐛 Finde den Denkfehler 5.1: Elf Lager, die keine Fixkosten kosten**
|
||||||
>
|
>
|
||||||
> Ein Team plant Standorte: 12 mögliche Lager, 40 Kunden, Fixkosten je eröffnetem Lager.
|
> Ein Team plant Standorte: 12 mögliche Lager, 40 Kunden, Fixkosten je eröffnetem Lager.
|
||||||
> Für das Big-M in der Kopplung
|
> Für das Big-M in der Kopplung
|
||||||
|
|
@ -1759,7 +1758,7 @@ funktioniert immer.
|
||||||
|
|
||||||
## Micro-Quiz {#sec:milp-quiz}
|
## Micro-Quiz {#sec:milp-quiz}
|
||||||
|
|
||||||
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
|
> **❓ Micro-Quiz 5: Drei Fragen zum Selbstcheck**
|
||||||
>
|
>
|
||||||
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
||||||
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-milp).
|
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-milp).
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
> `Propagation_Demo.py`\
|
> `Propagation_Demo.py`\
|
||||||
> `CP_SAT_Vertretungssystem.py`\
|
> `CP_SAT_Vertretungssystem.py`\
|
||||||
> `JobShop_Intervalle.py`\
|
> `JobShop_Intervalle.py`\
|
||||||
> `Parallele_Suche.py`\
|
|
||||||
> `CP_SAT_Statusfaelle.py`\
|
> `CP_SAT_Statusfaelle.py`\
|
||||||
> `Strafgewichte.py`
|
> `Strafgewichte.py`
|
||||||
|
|
||||||
|
|
@ -102,9 +101,7 @@ Nach diesem Kapitel können Sie …
|
||||||
6. … die fünf CP-SAT-Statusfälle unterscheiden — insbesondere `INFEASIBLE` (es gibt keine
|
6. … die fünf CP-SAT-Statusfälle unterscheiden — insbesondere `INFEASIBLE` (es gibt keine
|
||||||
Lösung) von `UNKNOWN` (es wurde keine gefunden) und `MODEL_INVALID` (Ihr Code ist
|
Lösung) von `UNKNOWN` (es wurde keine gefunden) und `MODEL_INVALID` (Ihr Code ist
|
||||||
fehlerhaft).
|
fehlerhaft).
|
||||||
7. … begründen, warum `num_workers = 1` für einen reproduzierbaren Lauf nötig ist und ein
|
7. … begründen, warum ein Strafgewicht ein **Wechselkurs** ist, und welche Regeln deshalb
|
||||||
fester `random_seed` allein nicht genügt — und was ein Test deshalb prüfen darf.
|
|
||||||
8. … begründen, warum ein Strafgewicht ein **Wechselkurs** ist, und welche Regeln deshalb
|
|
||||||
niemals in die Zielfunktion gehören.
|
niemals in die Zielfunktion gehören.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -797,9 +794,6 @@ Zusammenspiel von Reihenfolge- und Maschinenbedingungen, nicht aus einer schlech
|
||||||
> abgedruckte Ausgabe reproduzierbar. **Im Produktivbetrieb lässt man beides weg** — mehrere
|
> abgedruckte Ausgabe reproduzierbar. **Im Produktivbetrieb lässt man beides weg** — mehrere
|
||||||
> Arbeiter sind deutlich schneller.
|
> Arbeiter sind deutlich schneller.
|
||||||
>
|
>
|
||||||
> Wie viel schneller, und warum der Seed allein **nicht** genügt, misst
|
|
||||||
> {ref:sec:cpsat-parallele-suche} an genau diesem Modell.
|
|
||||||
>
|
|
||||||
> Die Lehre für Ihre eigenen Tests: Prüfen Sie bei mehrdeutigen Optima nie auf einen
|
> Die Lehre für Ihre eigenen Tests: Prüfen Sie bei mehrdeutigen Optima nie auf einen
|
||||||
> **bestimmten Plan**, sondern immer auf den **Zielwert** und auf die Einhaltung aller
|
> **bestimmten Plan**, sondern immer auf den **Zielwert** und auf die Einhaltung aller
|
||||||
> Regeln. Ein Test wie `assert plan == erwarteter_plan` besteht sonst mal und scheitert mal,
|
> Regeln. Ein Test wie `assert plan == erwarteter_plan` besteht sonst mal und scheitert mal,
|
||||||
|
|
@ -823,336 +817,6 @@ theoretischen Ideal entfernt sein können, noch bevor der Solver fertig war.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Parallele Suche: was `num_workers` wirklich bewirkt {#sec:cpsat-parallele-suche}
|
|
||||||
|
|
||||||
An mehreren Stellen dieses Buchs steht `loeser.parameters.num_workers = 1`, mit dem
|
|
||||||
Kommentar „für eine reproduzierbare Ausgabe“. Darin stecken zwei Behauptungen: dass
|
|
||||||
parallele Suche **schneller** ist, und dass sie **von Lauf zu Lauf andere, gleich gute**
|
|
||||||
Lösungen liefert. Beides lässt sich messen — und beim Messen kommt mehr heraus als erwartet.
|
|
||||||
|
|
||||||
Gerechnet wird auf demselben Modell wie
|
|
||||||
{ref:sec:cpsat-intervallvariablen-job-shop-scheduling}, nur groß genug, dass die Suche
|
|
||||||
wirklich arbeitet: 12 Aufträge auf 10 Maschinen, jeder Auftrag besucht jede Maschine.
|
|
||||||
Entscheidend ist, dass `random_seed` in **jedem** Lauf denselben Wert hat.
|
|
||||||
|
|
||||||
```python
|
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# Parallele_Suche.py
|
|
||||||
"""
|
|
||||||
Kapitel CP-SAT: Was mehrere Arbeiter wirklich bringen - und was sie kosten.
|
|
||||||
|
|
||||||
Das Buch setzt an mehreren Stellen 'num_workers = 1', damit die abgedruckte
|
|
||||||
Ausgabe reproduzierbar ist. Behauptet wird dabei zweierlei: dass parallele
|
|
||||||
Suche schneller ist, und dass sie von Lauf zu Lauf verschiedene, gleich gute
|
|
||||||
Loesungen findet. Beides wird hier gemessen statt geglaubt.
|
|
||||||
|
|
||||||
Gerechnet wird auf einem Job-Shop wie in JobShop_Intervalle.py, nur gross
|
|
||||||
genug, dass die Suche wirklich arbeitet: 12 Auftraege auf 10 Maschinen,
|
|
||||||
jeder Auftrag besucht jede Maschine.
|
|
||||||
|
|
||||||
Zwei Messungen:
|
|
||||||
1. Dieselbe Aufgabe mit 1, 2, 4 und 8 Arbeitern - Laufzeit und Ergebnis.
|
|
||||||
2. Dieselbe Konfiguration mehrfach, bei FESTEM random_seed - wie oft kommt
|
|
||||||
derselbe Plan heraus?
|
|
||||||
|
|
||||||
Die zweite ist die wichtigere: Sie entscheidet, wie man Tests schreibt.
|
|
||||||
|
|
||||||
Achtung, Laufzeiten sind hardwareabhaengig. Die ZAHL der verschiedenen
|
|
||||||
Plaene ist es auch - und genau das ist die Aussage.
|
|
||||||
|
|
||||||
Benoetigt: numpy, ortools
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import time
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
from ortools.sat.python import cp_model
|
|
||||||
|
|
||||||
AUFTRAEGE = 12
|
|
||||||
MASCHINEN = 10
|
|
||||||
INSTANZ_SEED = 20260908
|
|
||||||
SOLVER_SEED = 1 # bleibt ueber ALLE Laeufe gleich - das ist der Punkt
|
|
||||||
ARBEITERZAHLEN = (1, 2, 4, 8)
|
|
||||||
WIEDERHOLUNGEN = 4
|
|
||||||
ZEITLIMIT = 120.0
|
|
||||||
|
|
||||||
|
|
||||||
def baue_instanz() -> list[list[tuple[int, int]]]:
|
|
||||||
"""Klassischer Job-Shop: Jeder Auftrag besucht jede Maschine genau einmal,
|
|
||||||
in einer eigenen zufaelligen Reihenfolge."""
|
|
||||||
rng = np.random.default_rng(INSTANZ_SEED)
|
|
||||||
return [[(int(m), int(rng.integers(2, 20))) for m in rng.permutation(MASCHINEN)]
|
|
||||||
for _ in range(AUFTRAEGE)]
|
|
||||||
|
|
||||||
|
|
||||||
def loese(auftraege, arbeiter: int):
|
|
||||||
"""Minimiert den Makespan. Gibt Status, Zielwert, Laufzeit und den Plan
|
|
||||||
zurueck - den Plan als Tupel aller Startzeiten, damit sich zwei Laeufe
|
|
||||||
vergleichen lassen."""
|
|
||||||
horizont = sum(dauer for auftrag in auftraege for _, dauer in auftrag)
|
|
||||||
modell = cp_model.CpModel()
|
|
||||||
Gang = collections.namedtuple("Gang", "start ende intervall")
|
|
||||||
plaene: dict[tuple[int, int], Gang] = {}
|
|
||||||
je_maschine = collections.defaultdict(list)
|
|
||||||
|
|
||||||
for a, auftrag in enumerate(auftraege):
|
|
||||||
for g, (maschine, dauer) in enumerate(auftrag):
|
|
||||||
start = modell.NewIntVar(0, horizont, f"start_{a}_{g}")
|
|
||||||
ende = modell.NewIntVar(0, horizont, f"ende_{a}_{g}")
|
|
||||||
intervall = modell.NewIntervalVar(start, dauer, ende, f"iv_{a}_{g}")
|
|
||||||
plaene[a, g] = Gang(start, ende, intervall)
|
|
||||||
je_maschine[maschine].append(intervall)
|
|
||||||
|
|
||||||
for maschine in range(MASCHINEN):
|
|
||||||
modell.AddNoOverlap(je_maschine[maschine])
|
|
||||||
for a, auftrag in enumerate(auftraege):
|
|
||||||
for g in range(len(auftrag) - 1):
|
|
||||||
modell.Add(plaene[a, g + 1].start >= plaene[a, g].ende)
|
|
||||||
|
|
||||||
makespan = modell.NewIntVar(0, horizont, "makespan")
|
|
||||||
modell.AddMaxEquality(
|
|
||||||
makespan, [plaene[a, len(auftrag) - 1].ende
|
|
||||||
for a, auftrag in enumerate(auftraege)])
|
|
||||||
modell.Minimize(makespan)
|
|
||||||
|
|
||||||
loeser = cp_model.CpSolver()
|
|
||||||
loeser.parameters.num_workers = arbeiter
|
|
||||||
loeser.parameters.random_seed = SOLVER_SEED
|
|
||||||
loeser.parameters.max_time_in_seconds = ZEITLIMIT
|
|
||||||
|
|
||||||
beginn = time.perf_counter()
|
|
||||||
status = loeser.Solve(modell)
|
|
||||||
dauer = time.perf_counter() - beginn
|
|
||||||
|
|
||||||
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
|
|
||||||
return loeser.StatusName(status), None, dauer, None
|
|
||||||
plan = tuple(loeser.Value(plaene[a, g].start)
|
|
||||||
for a, auftrag in enumerate(auftraege)
|
|
||||||
for g in range(len(auftrag)))
|
|
||||||
return loeser.StatusName(status), int(loeser.ObjectiveValue()), dauer, plan
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
auftraege = baue_instanz()
|
|
||||||
gaenge = sum(len(a) for a in auftraege)
|
|
||||||
|
|
||||||
print("=" * 78)
|
|
||||||
print(" WAS MEHRERE ARBEITER WIRKLICH BRINGEN")
|
|
||||||
print("=" * 78)
|
|
||||||
print(f"Job-Shop: {AUFTRAEGE} Auftraege, {MASCHINEN} Maschinen, "
|
|
||||||
f"{gaenge} Arbeitsgaenge.")
|
|
||||||
print(f"random_seed = {SOLVER_SEED} in JEDEM Lauf, "
|
|
||||||
f"{WIEDERHOLUNGEN} Wiederholungen je Arbeiterzahl.\n")
|
|
||||||
|
|
||||||
# Jede Konfiguration mehrfach - ein einzelner Zeitwert sagt nichts.
|
|
||||||
ergebnisse: dict[int, list[tuple]] = {}
|
|
||||||
for arbeiter in ARBEITERZAHLEN:
|
|
||||||
ergebnisse[arbeiter] = [loese(auftraege, arbeiter)
|
|
||||||
for _ in range(WIEDERHOLUNGEN)]
|
|
||||||
|
|
||||||
print("=" * 78)
|
|
||||||
print(" (1) Laufzeit: was die Arbeiter bringen")
|
|
||||||
print("=" * 78)
|
|
||||||
print(f"{'Arbeiter':>9} {'Status':>9} {'Makespan':>9} "
|
|
||||||
f"{'schnellste':>11} {'langsamste':>11} {'Beschleunigung':>15}")
|
|
||||||
print("-" * 78)
|
|
||||||
basis_zeit = min(d for _, _, d, _ in ergebnisse[1])
|
|
||||||
for arbeiter in ARBEITERZAHLEN:
|
|
||||||
laeufe = ergebnisse[arbeiter]
|
|
||||||
zeiten = [d for _, _, d, _ in laeufe]
|
|
||||||
ziele = {o for _, o, _, _ in laeufe}
|
|
||||||
status = {s for s, _, _, _ in laeufe}
|
|
||||||
print(f"{arbeiter:>9} {'/'.join(sorted(status)):>9} "
|
|
||||||
f"{'/'.join(str(z) for z in sorted(ziele)):>9} "
|
|
||||||
f"{min(zeiten):>10.2f}s {max(zeiten):>10.2f}s "
|
|
||||||
f"{basis_zeit / min(zeiten):>14.1f}x")
|
|
||||||
|
|
||||||
beste = min(ARBEITERZAHLEN, key=lambda w: min(d for _, _, d, _ in ergebnisse[w]))
|
|
||||||
faktor = basis_zeit / min(d for _, _, d, _ in ergebnisse[beste])
|
|
||||||
print(f"\n Am schnellsten sind {beste} Arbeiter: Faktor {faktor:.1f} gegenueber")
|
|
||||||
print(f" einem einzigen - also MEHR, als {beste} Kerne hergeben sollten.")
|
|
||||||
print(f" Das ist kein Messfehler. CP-SAT laesst nicht {beste}-mal dieselbe Suche")
|
|
||||||
print(" laufen, sondern verschiedene Strategien nebeneinander, die einander")
|
|
||||||
print(" ihre Schranken zurufen. Der zweite Arbeiter ist deshalb nicht der")
|
|
||||||
print(" 'zweite Kern', sondern ein anderes Verfahren.")
|
|
||||||
letzte = ARBEITERZAHLEN[-1]
|
|
||||||
if beste != letzte:
|
|
||||||
zeit_letzte = min(d for _, _, d, _ in ergebnisse[letzte])
|
|
||||||
print(f"\n Und mehr ist nicht immer besser: {letzte} Arbeiter brauchen "
|
|
||||||
f"{zeit_letzte:.2f}s")
|
|
||||||
print(f" gegenueber {min(d for _, _, d, _ in ergebnisse[beste]):.2f}s bei "
|
|
||||||
f"{beste}. Ab einer gewissen Zahl kosten Abstimmung")
|
|
||||||
print(" und Speicherbandbreite mehr, als ein weiterer Suchstrang einbringt.")
|
|
||||||
|
|
||||||
print("\n" + "=" * 78)
|
|
||||||
print(" (2) Reproduzierbarkeit: derselbe Seed, derselbe Plan?")
|
|
||||||
print("=" * 78)
|
|
||||||
print(f"{'Arbeiter':>9} {'Laeufe':>7} {'verschiedene Plaene':>21} "
|
|
||||||
f"{'Makespan':>10}")
|
|
||||||
print("-" * 78)
|
|
||||||
for arbeiter in ARBEITERZAHLEN:
|
|
||||||
plaene = {p for _, _, _, p in ergebnisse[arbeiter] if p is not None}
|
|
||||||
ziele = {o for _, o, _, _ in ergebnisse[arbeiter]}
|
|
||||||
print(f"{arbeiter:>9} {WIEDERHOLUNGEN:>7} {len(plaene):>21} "
|
|
||||||
f"{'/'.join(str(z) for z in sorted(ziele)):>10}")
|
|
||||||
|
|
||||||
einer = len({p for _, _, _, p in ergebnisse[1]})
|
|
||||||
# Ab welcher Arbeiterzahl bricht die Reproduzierbarkeit? Das ist die
|
|
||||||
# eigentliche Zahl - nicht, was acht Arbeiter anrichten.
|
|
||||||
ab = next((w for w in ARBEITERZAHLEN
|
|
||||||
if len({p for _, _, _, p in ergebnisse[w]}) > 1), None)
|
|
||||||
print(f"\n Mit einem Arbeiter: {einer} Plan aus {WIEDERHOLUNGEN} Laeufen.")
|
|
||||||
if ab is not None:
|
|
||||||
viele = len({p for _, _, _, p in ergebnisse[ab]})
|
|
||||||
print(f" Schon mit {ab} Arbeitern: {viele} verschiedene Plaene aus "
|
|
||||||
f"{WIEDERHOLUNGEN} Laeufen -")
|
|
||||||
print(" bei identischem random_seed und identischem Zielwert.")
|
|
||||||
print(f" Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit")
|
|
||||||
print(f" zu verlieren. {ab} genuegen.")
|
|
||||||
print("\n Der Seed allein macht einen Lauf also NICHT reproduzierbar. Er legt")
|
|
||||||
print(" fest, wie ein einzelner Suchstrang wuerfelt - nicht, welcher von")
|
|
||||||
print(" mehreren zuerst fertig wird. Das entscheidet die Uhr.")
|
|
||||||
|
|
||||||
print("\n" + "=" * 78)
|
|
||||||
print(" (3) Was daraus fuer Tests folgt")
|
|
||||||
print("=" * 78)
|
|
||||||
print(" Ein Test der Form assert plan == erwarteter_plan besteht mal und")
|
|
||||||
print(" scheitert mal, ohne dass sich am Code etwas geaendert haette.")
|
|
||||||
print(" Zu pruefen sind stattdessen:")
|
|
||||||
print(" * der ZIELWERT (hier in allen Laeufen gleich),")
|
|
||||||
print(" * die Einhaltung aller Regeln (Abnahmepruefung, Kapitel Praxisfallen),")
|
|
||||||
print(" * und der Status - nicht die Gestalt der Loesung.")
|
|
||||||
print("\n Wer doch einen bestimmten Plan braucht - fuer eine abgedruckte")
|
|
||||||
print(" Ausgabe, fuer einen Regressionstest -, setzt num_workers = 1.")
|
|
||||||
print(f" Das kostet hier den Faktor {faktor:.1f} an Laufzeit und ist genau")
|
|
||||||
print(" deshalb eine Entscheidung fuer den Test, nicht fuer den Betrieb.")
|
|
||||||
print("=" * 78)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Erwartete Ausgabe** (Laufzeiten *und* die Zahl der verschiedenen Pläne sind
|
|
||||||
hardwareabhängig — genau das ist die Aussage; der Zielwert ist es nicht):
|
|
||||||
|
|
||||||
```
|
|
||||||
==============================================================================
|
|
||||||
WAS MEHRERE ARBEITER WIRKLICH BRINGEN
|
|
||||||
==============================================================================
|
|
||||||
Job-Shop: 12 Auftraege, 10 Maschinen, 120 Arbeitsgaenge.
|
|
||||||
random_seed = 1 in JEDEM Lauf, 4 Wiederholungen je Arbeiterzahl.
|
|
||||||
|
|
||||||
==============================================================================
|
|
||||||
(1) Laufzeit: was die Arbeiter bringen
|
|
||||||
==============================================================================
|
|
||||||
Arbeiter Status Makespan schnellste langsamste Beschleunigung
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
1 OPTIMAL 183 5.62s 5.82s 1.0x
|
|
||||||
2 OPTIMAL 183 1.43s 2.04s 3.9x
|
|
||||||
4 OPTIMAL 183 0.60s 0.66s 9.4x
|
|
||||||
8 OPTIMAL 183 0.46s 0.65s 12.3x
|
|
||||||
|
|
||||||
Am schnellsten sind 8 Arbeiter: Faktor 12.3 gegenueber
|
|
||||||
einem einzigen - also MEHR, als 8 Kerne hergeben sollten.
|
|
||||||
Das ist kein Messfehler. CP-SAT laesst nicht 8-mal dieselbe Suche
|
|
||||||
laufen, sondern verschiedene Strategien nebeneinander, die einander
|
|
||||||
ihre Schranken zurufen. Der zweite Arbeiter ist deshalb nicht der
|
|
||||||
'zweite Kern', sondern ein anderes Verfahren.
|
|
||||||
|
|
||||||
==============================================================================
|
|
||||||
(2) Reproduzierbarkeit: derselbe Seed, derselbe Plan?
|
|
||||||
==============================================================================
|
|
||||||
Arbeiter Laeufe verschiedene Plaene Makespan
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
1 4 1 183
|
|
||||||
2 4 3 183
|
|
||||||
4 4 3 183
|
|
||||||
8 4 4 183
|
|
||||||
|
|
||||||
Mit einem Arbeiter: 1 Plan aus 4 Laeufen.
|
|
||||||
Schon mit 2 Arbeitern: 3 verschiedene Plaene aus 4 Laeufen -
|
|
||||||
bei identischem random_seed und identischem Zielwert.
|
|
||||||
Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit
|
|
||||||
zu verlieren. 2 genuegen.
|
|
||||||
|
|
||||||
Der Seed allein macht einen Lauf also NICHT reproduzierbar. Er legt
|
|
||||||
fest, wie ein einzelner Suchstrang wuerfelt - nicht, welcher von
|
|
||||||
mehreren zuerst fertig wird. Das entscheidet die Uhr.
|
|
||||||
|
|
||||||
==============================================================================
|
|
||||||
(3) Was daraus fuer Tests folgt
|
|
||||||
==============================================================================
|
|
||||||
Ein Test der Form assert plan == erwarteter_plan besteht mal und
|
|
||||||
scheitert mal, ohne dass sich am Code etwas geaendert haette.
|
|
||||||
Zu pruefen sind stattdessen:
|
|
||||||
* der ZIELWERT (hier in allen Laeufen gleich),
|
|
||||||
* die Einhaltung aller Regeln (Abnahmepruefung, Kapitel Praxisfallen),
|
|
||||||
* und der Status - nicht die Gestalt der Loesung.
|
|
||||||
|
|
||||||
Wer doch einen bestimmten Plan braucht - fuer eine abgedruckte
|
|
||||||
Ausgabe, fuer einen Regressionstest -, setzt num_workers = 1.
|
|
||||||
Das kostet hier den Faktor 12.3 an Laufzeit und ist genau
|
|
||||||
deshalb eine Entscheidung fuer den Test, nicht fuer den Betrieb.
|
|
||||||
==============================================================================
|
|
||||||
```
|
|
||||||
|
|
||||||
### Was die Messung zeigt
|
|
||||||
|
|
||||||
**Erstens: Die Beschleunigung ist überlinear.** Acht Arbeiter sind im abgedruckten Lauf
|
|
||||||
nicht achtmal, sondern **gut zwölfmal** schneller als einer. Das sieht nach Messfehler aus, ist aber die
|
|
||||||
korrekte Beschreibung dessen, was CP-SAT tut: Es lässt **nicht** achtmal dieselbe Suche auf
|
|
||||||
verschiedenen Kernen laufen, sondern **verschiedene Strategien nebeneinander** — Suche mit
|
|
||||||
LP-Relaxation, Suche mit anderen Verzweigungsregeln, lokale Suche —, und die teilen
|
|
||||||
einander ihre Schranken mit. Der zweite Arbeiter ist deshalb nicht „der zweite Kern“,
|
|
||||||
sondern ein anderes Verfahren, das zufällig auch rechnet.
|
|
||||||
|
|
||||||
Daraus folgt zweierlei. Ein Speedup über der Kernzahl ist bei CP-SAT normal und kein Grund
|
|
||||||
zum Misstrauen. Und der Zugewinn flacht ab: Zwischen vier und acht Arbeitern lag in unseren
|
|
||||||
Wiederholungen mal das eine, mal das andere vorn — ab einer gewissen Zahl kosten Abstimmung
|
|
||||||
und Speicherbandbreite mehr, als ein weiterer Suchstrang einbringt. Auch hier gilt: messen,
|
|
||||||
nicht schätzen.
|
|
||||||
|
|
||||||
**Zweitens — und das ist der Befund, der die Testpraxis bestimmt: Der Seed genügt nicht.**
|
|
||||||
Bei einem Arbeiter liefern vier Läufe **einen** Plan. Schon bei **zwei** Arbeitern liefern
|
|
||||||
vier Läufe **drei verschiedene** — bei identischem `random_seed` und identischem Zielwert
|
|
||||||
183.
|
|
||||||
|
|
||||||
Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit zu verlieren. Zwei genügen.
|
|
||||||
Der Grund ist einfach, sobald man ihn ausspricht: Der Seed legt fest, wie ein **einzelner**
|
|
||||||
Suchstrang würfelt. Er legt nicht fest, **welcher von mehreren zuerst fertig wird** — das
|
|
||||||
entscheidet die Uhr, und die ist bei jedem Lauf anders.
|
|
||||||
|
|
||||||
> **⚠️ `random_seed` allein macht nichts reproduzierbar**
|
|
||||||
> Ein häufiger Irrtum: „Ich habe doch einen Seed gesetzt.“ Für einen Zufallsgenerator
|
|
||||||
> stimmt das. Für einen **parallelen** Solver nicht — dort ist die zweite Zufallsquelle die
|
|
||||||
> Ausführungsreihenfolge, und die lässt sich nicht mit einem Parameter festnageln.
|
|
||||||
> Reproduzierbar wird der Lauf erst mit `num_workers = 1` **und** Seed. Wer nur eines von
|
|
||||||
> beidem setzt, hat einen Test, der gelegentlich grundlos rot wird.
|
|
||||||
|
|
||||||
### Was daraus für Tests folgt
|
|
||||||
|
|
||||||
Der Zielwert war in **allen** sechzehn Läufen derselbe: 183. Die Gestalt der Lösung war es
|
|
||||||
nicht. Genau daran entscheidet sich, was ein Test prüfen darf:
|
|
||||||
|
|
||||||
| Prüfen | Nicht prüfen |
|
|
||||||
| --- | --- |
|
|
||||||
| den **Zielwert** — er ist eindeutig, wenn das Modell optimal gelöst wurde | die **Gestalt** des Plans (`assert plan == erwarteter_plan`) |
|
|
||||||
| die **Einhaltung aller Regeln** über eine Abnahmeprüfung ({ref:sec:praxisfallen-or-kern}) | die Reihenfolge, in der gleichwertige Aufträge zugewiesen wurden |
|
|
||||||
| den **Status** (`OPTIMAL`, `FEASIBLE`, `INFEASIBLE`) | die Laufzeit als feste Zahl |
|
|
||||||
|
|
||||||
Wer doch einen bestimmten Plan braucht — für eine abgedruckte Ausgabe, für einen
|
|
||||||
Regressionstest —, setzt `num_workers = 1`. Das kostet hier den Faktor 12 an Laufzeit, und
|
|
||||||
genau deshalb ist es eine Entscheidung **für den Test**, nicht für den Betrieb. In
|
|
||||||
Produktion lässt man beide Parameter auf ihren Standardwerten.
|
|
||||||
|
|
||||||
> **🎯 Merksatz**
|
|
||||||
> Ein paralleler Solver hat zwei Zufallsquellen: den Seed und die Uhr. Nur die erste lässt
|
|
||||||
> sich setzen. Wer Reproduzierbarkeit braucht, muss die zweite abschalten — und das heißt
|
|
||||||
> `num_workers = 1`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Die fünf Antworten von CP-SAT {#sec:cpsat-statusfaelle}
|
## Die fünf Antworten von CP-SAT {#sec:cpsat-statusfaelle}
|
||||||
|
|
||||||
{ref:sec:milp-gap} hat gezeigt, warum `OPTIMAL` bei ganzzahligen Problemen nicht der
|
{ref:sec:milp-gap} hat gezeigt, warum `OPTIMAL` bei ganzzahligen Problemen nicht der
|
||||||
|
|
@ -1516,16 +1180,6 @@ Lösen Sie ein 9×9-Sudoku mit CP-SAT. Sie brauchen nur `NewIntVar(1, 9, ...)` u
|
||||||
`AddAllDifferent` für Zeilen, Spalten und die neun 3×3-Blöcke. Wie viele Zeilen Modellcode
|
`AddAllDifferent` für Zeilen, Spalten und die neun 3×3-Blöcke. Wie viele Zeilen Modellcode
|
||||||
benötigen Sie?
|
benötigen Sie?
|
||||||
|
|
||||||
**Aufgabe ⭐⭐ — Die Grenze der Parallelität finden.**
|
|
||||||
`Parallele_Suche.py` misst 1, 2, 4 und 8 Arbeiter.
|
|
||||||
(a) Ergänzen Sie 16 und 24 (bzw. die Kernzahl Ihres Rechners). Ab wo bringt ein weiterer
|
|
||||||
Arbeiter nichts mehr — und wird es irgendwann wieder schlechter?
|
|
||||||
(b) Setzen Sie `random_seed` bei jedem Lauf auf einen **anderen** Wert, aber
|
|
||||||
`num_workers = 1`. Wie viele verschiedene Pläne kommen jetzt heraus? Was folgt daraus
|
|
||||||
für die Frage, welcher der beiden Parameter die Reproduzierbarkeit sichert?
|
|
||||||
(c) Vergrößern Sie die Instanz auf 15 Aufträge. Ab welcher Größe erreicht der Lauf mit
|
|
||||||
einem Arbeiter das Zeitlimit, während acht noch `OPTIMAL` melden?
|
|
||||||
|
|
||||||
**Aufgabe ⭐⭐⭐ — Job-Shop erweitern.**
|
**Aufgabe ⭐⭐⭐ — Job-Shop erweitern.**
|
||||||
Ergänzen Sie `JobShop_Intervalle.py` um:
|
Ergänzen Sie `JobShop_Intervalle.py` um:
|
||||||
(a) Rüstzeiten von 1 Zeiteinheit zwischen zwei Aufträgen auf derselben Maschine.
|
(a) Rüstzeiten von 1 Zeiteinheit zwischen zwei Aufträgen auf derselben Maschine.
|
||||||
|
|
@ -1542,7 +1196,7 @@ Wünsche als weiche Ziele. Geben Sie den Plan als Wochentabelle aus.
|
||||||
|
|
||||||
## Finde den Denkfehler {#sec:cpsat-denkfehler}
|
## Finde den Denkfehler {#sec:cpsat-denkfehler}
|
||||||
|
|
||||||
> **🐛 Finde den Denkfehler: Die Betriebsvereinbarung, die niemanden interessiert**
|
> **🐛 Finde den Denkfehler 6.1: Die Betriebsvereinbarung, die niemanden interessiert**
|
||||||
>
|
>
|
||||||
> Ein Klinikteam lässt seinen Schichtplan optimieren. Zwei Wünsche kommen aus dem Betrieb:
|
> Ein Klinikteam lässt seinen Schichtplan optimieren. Zwei Wünsche kommen aus dem Betrieb:
|
||||||
>
|
>
|
||||||
|
|
@ -1774,7 +1428,7 @@ Nebenbedingungen. Alles, was einen Preis hat, wird irgendwann gekauft.
|
||||||
|
|
||||||
## Micro-Quiz {#sec:cpsat-quiz}
|
## Micro-Quiz {#sec:cpsat-quiz}
|
||||||
|
|
||||||
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
|
> **❓ Micro-Quiz 6: Drei Fragen zum Selbstcheck**
|
||||||
>
|
>
|
||||||
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
||||||
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-cpsat).
|
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-cpsat).
|
||||||
|
|
@ -1812,10 +1466,6 @@ Nebenbedingungen. Alles, was einen Preis hat, wird irgendwann gekauft.
|
||||||
3. Was ist eine Intervallvariable, und welche Bedingung garantiert sie automatisch?
|
3. Was ist eine Intervallvariable, und welche Bedingung garantiert sie automatisch?
|
||||||
4. Warum sollten möglichst wenige Bedingungen hart formuliert werden?
|
4. Warum sollten möglichst wenige Bedingungen hart formuliert werden?
|
||||||
5. Warum ist die Aufschlüsselung der Strafkosten in der Praxis wichtiger als der Zielwert
|
5. Warum ist die Aufschlüsselung der Strafkosten in der Praxis wichtiger als der Zielwert
|
||||||
6. Sie setzen `random_seed = 1`, bekommen aber trotzdem bei jedem Lauf einen anderen Plan.
|
|
||||||
Woran liegt das, und was hilft?
|
|
||||||
7. Warum kann ein CP-SAT-Lauf mit acht Arbeitern **mehr** als achtmal schneller sein als
|
|
||||||
mit einem?
|
|
||||||
selbst?
|
selbst?
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -1841,17 +1491,10 @@ Nebenbedingungen. Alles, was einen Preis hat, wird irgendwann gekauft.
|
||||||
* **Bei mehrdeutigen Optima nie auf einen bestimmten Plan testen** — nur auf den Zielwert
|
* **Bei mehrdeutigen Optima nie auf einen bestimmten Plan testen** — nur auf den Zielwert
|
||||||
und die Einhaltung aller Regeln.
|
und die Einhaltung aller Regeln.
|
||||||
|
|
||||||
* **Ein paralleler Solver hat zwei Zufallsquellen: den Seed und die Uhr.** Nur die erste
|
|
||||||
lässt sich setzen. Reproduzierbar wird ein Lauf erst mit `num_workers = 1` **und** Seed —
|
|
||||||
gemessen in {ref:sec:cpsat-parallele-suche}: schon zwei Arbeiter liefern bei identischem
|
|
||||||
Seed drei verschiedene Pläne zum selben Zielwert.
|
|
||||||
* **Parallele Suche kann überlinear beschleunigen**, weil CP-SAT nicht dieselbe Suche
|
|
||||||
vervielfacht, sondern verschiedene Strategien nebeneinander laufen lässt, die einander
|
|
||||||
ihre Schranken mitteilen.
|
|
||||||
**Ausblick.** {ref:kap:graphen} behandelt Probleme, die sich am besten als **Graph** begreifen
|
**Ausblick.** {ref:kap:graphen} behandelt Probleme, die sich am besten als **Graph** begreifen
|
||||||
lassen: Flüsse durch Netzwerke, Zuordnungen und die Tourenplanung mit Fahrzeugen und
|
lassen: Flüsse durch Netzwerke, Zuordnungen und die Tourenplanung mit Fahrzeugen und
|
||||||
Zeitfenstern.
|
Zeitfenstern.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Weiter mit:* [{ref:kap:graphen} — Graphen, Flüsse und Touren](23_Graphen_Fluesse_Touren.md)
|
*Weiter mit:* [{ref:kap:graphen} — Graphen, Flüsse, Touren](23_Graphen_Fluesse_Touren.md)
|
||||||
|
|
|
||||||
|
|
@ -751,7 +751,7 @@ OR-Tools. Was beobachten Sie ab 12 Städten?
|
||||||
|
|
||||||
## Finde den Denkfehler {#sec:graphen-denkfehler}
|
## Finde den Denkfehler {#sec:graphen-denkfehler}
|
||||||
|
|
||||||
> **🐛 Finde den Denkfehler: Die vergessene Dimension**
|
> **🐛 Finde den Denkfehler 7.1: Die vergessene Dimension**
|
||||||
>
|
>
|
||||||
> Eine Spedition lässt ihre Tagestouren optimieren: 16 Kunden, 4 Fahrzeuge zu je 10
|
> Eine Spedition lässt ihre Tagestouren optimieren: 16 Kunden, 4 Fahrzeuge zu je 10
|
||||||
> Paletten, Gesamtbedarf 37 Paletten. Das erste Ergebnis begeistert alle — **326 km**.
|
> Paletten, Gesamtbedarf 37 Paletten. Das erste Ergebnis begeistert alle — **326 km**.
|
||||||
|
|
@ -1018,7 +1018,7 @@ plausibel aussieht.
|
||||||
|
|
||||||
## Micro-Quiz {#sec:graphen-quiz}
|
## Micro-Quiz {#sec:graphen-quiz}
|
||||||
|
|
||||||
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
|
> **❓ Micro-Quiz 7: Drei Fragen zum Selbstcheck**
|
||||||
>
|
>
|
||||||
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
||||||
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-graphen).
|
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-graphen).
|
||||||
|
|
@ -1084,4 +1084,4 @@ und die KKT-Bedingungen ein — das mathematische Fundament der Portfoliooptimie
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Weiter mit:* [{ref:kap:metaheuristiken} — Metaheuristiken](24_Metaheuristiken.md)
|
*Weiter mit:* [{ref:kap:qp-nlp} — Quadratische und nichtlineare Optimierung](30_QP_und_NLP.md)
|
||||||
|
|
|
||||||
|
|
@ -1526,7 +1526,3 @@ liegt es an der Strategie oder an der Zahl der Runden, die sie schafft?
|
||||||
könnte.
|
könnte.
|
||||||
* **LNS** kombiniert beide: zerstören, exakt reparieren, übernehmen. Die Fenstergröße hat
|
* **LNS** kombiniert beide: zerstören, exakt reparieren, übernehmen. Die Fenstergröße hat
|
||||||
ein Optimum in der Mitte und gehört gemessen.
|
ein Optimum in der Mitte und gehört gemessen.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Weiter mit:* [{ref:kap:dekomposition} — Spaltengenerierung](25_Spaltengenerierung.md)
|
|
||||||
|
|
|
||||||
|
|
@ -750,7 +750,3 @@ Spalten verzweigen kann.
|
||||||
Verfahren 11 %, bei sechs nichts. Diese Kennzahl lässt sich vor dem Projekt ausrechnen.
|
Verfahren 11 %, bei sechs nichts. Diese Kennzahl lässt sich vor dem Projekt ausrechnen.
|
||||||
* Auch wo es nichts spart, liefert es die **Schranke** — und damit die Erlaubnis,
|
* Auch wo es nichts spart, liefert es die **Schranke** — und damit die Erlaubnis,
|
||||||
aufzuhören.
|
aufzuhören.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Weiter mit:* [Synthese Teil II](29_Synthese_Kernverfahren.md)
|
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
# Synthese Teil II — Die Kernverfahren nebeneinander
|
|
||||||
|
|
||||||
Sechs Kapitel, sechs Werkzeuge. Jedes einzelne wurde im Zusammenhang gezeigt; hier stehen
|
|
||||||
sie zum ersten Mal **nebeneinander**, mit der Frage, die vor jedem Projekt zu beantworten
|
|
||||||
ist: Welches nehme ich, und was bekomme ich dafür?
|
|
||||||
|
|
||||||
## Die Entscheidungsmatrix
|
|
||||||
|
|
||||||
| Verfahren | Wofür es gebaut ist | Was Sie bekommen | Wo es aufhört | Kapitel |
|
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| **LP** | teilbare Mengen, lineare Zusammenhänge | beweisbares Optimum **plus Schattenpreise** | sobald etwas ganzzahlig sein muss | {ref:kap:lp} |
|
|
||||||
| **MILP** | Ja/Nein-Entscheidungen, Fixkosten, Logik | beweisbares Optimum, Gap als Fortschrittsmaß | Laufzeit wächst mit der Zahl der Binärvariablen | {ref:kap:milp} |
|
|
||||||
| **CP-SAT** | Zuweisung, Reihenfolge, Kalender, harte Logik | beweisbares Optimum, sehr ausdrucksstarke Bedingungen | keine Schattenpreise, keine stetigen Größen | {ref:kap:cpsat} |
|
|
||||||
| **Graphenalgorithmen** | Flüsse, Zuordnung, Touren | oft **polynomiell** statt exponentiell | nur, wenn die Struktur wirklich passt | {ref:kap:graphen} |
|
|
||||||
| **Metaheuristiken** | wenn der exakte Solver aussteigt | eine gute Lösung in **fester Zeit** | keine Garantie, kein Beweis, keine Schranke | {ref:kap:metaheuristiken} |
|
|
||||||
| **Spaltengenerierung** | Modelle mit astronomisch vielen Variablen | beweisbares Optimum über nie erzeugte Spalten | lohnt nur bei bestimmter Instanzstruktur | {ref:kap:dekomposition} |
|
|
||||||
|
|
||||||
**Die Spalte, die am meisten wert ist, ist die vierte.** Ein Verfahren zu kennen heißt zu
|
|
||||||
wissen, wo es aufhört — nicht, wofür es gedacht ist.
|
|
||||||
|
|
||||||
## Was dieser Teil gemessen hat
|
|
||||||
|
|
||||||
| Behauptung | Gemessen | Wo |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| „Der exakte Solver ist immer besser." | Bei 500 Aufträgen liefert CP-SAT eine Lösung, die **5,2 % schlechter** ist als die Faustregel eines Meisters | {ref:kap:metaheuristiken} |
|
|
||||||
| „Metaheuristiken sind ungenau." | Faustregel 2 497 → Annealing 2 343 → Annealing plus LNS **2 289** Minuten Rüstzeit; untere Schranke 1 768 | {ref:kap:metaheuristiken} |
|
|
||||||
| „Man muss alle Variablen aufschreiben." | 28 erzeugte Muster von **46 408** möglichen — und die Lösung ist beweisbar optimal | {ref:kap:dekomposition} |
|
|
||||||
| „Dekomposition lohnt sich immer." | Bei drei Stücken je Rolle **11 %** Ersparnis, bei sechs **nichts** | {ref:kap:dekomposition} |
|
|
||||||
| „Mit Seed ist der Lauf reproduzierbar." | Schon **zwei** Arbeiter liefern bei identischem Seed drei verschiedene Pläne zum selben Zielwert | {ref:sec:cpsat-parallele-suche} |
|
|
||||||
|
|
||||||
## Drei Fehler, die dieser Teil verhindert
|
|
||||||
|
|
||||||
1. **Runden.** Die LP-Lösung ist keine Näherung der ganzzahligen Lösung — sie kann beliebig
|
|
||||||
weit danebenliegen, und gerundet sogar unzulässig werden.
|
|
||||||
2. **Ein zu großes Big-M.** Es macht das Modell nicht falsch, sondern die Suche langsam und
|
|
||||||
die Schranken wertlos. So klein wie zulässig, nie „sicherheitshalber groß".
|
|
||||||
3. **Zu früh heuristisch werden.** Der Umschlagpunkt, ab dem eine Metaheuristik den exakten
|
|
||||||
Solver schlägt, ist eine Eigenschaft **des Problems** und lässt sich messen. Wer ihn
|
|
||||||
nicht misst, verzichtet auf Optimalitätsgarantien, die er hätte haben können.
|
|
||||||
|
|
||||||
## Wenn Sie nur eines mitnehmen
|
|
||||||
|
|
||||||
> **🎯** Die Frage lautet nie „welcher Solver ist der beste", sondern „welche Garantie
|
|
||||||
> brauche ich, und was bin ich bereit, dafür an Laufzeit zu zahlen". Ein Verfahren ohne
|
|
||||||
> Schranke liefert eine Lösung; ein Verfahren mit Schranke liefert die Erlaubnis,
|
|
||||||
> aufzuhören.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Weiter mit:* [{ref:kap:qp-nlp} — Quadratische und nichtlineare Optimierung](30_QP_und_NLP.md)
|
|
||||||
|
|
@ -5,7 +5,7 @@ in der Praxis oft nicht: Risiko wächst **quadratisch** mit dem Einsatz, Nachfra
|
||||||
morgen bekannt, und eine Entscheidung heute verändert, welche Möglichkeiten übermorgen noch
|
morgen bekannt, und eine Entscheidung heute verändert, welche Möglichkeiten übermorgen noch
|
||||||
offenstehen.
|
offenstehen.
|
||||||
|
|
||||||
Drei Kapitel dieses Teils behandeln genau diese drei Abweichungen. Welches Werkzeug Sie
|
Die drei Kapitel dieses Teils behandeln genau diese drei Abweichungen. Welches Werkzeug Sie
|
||||||
brauchen, hängt davon ab, **welche** davon bei Ihnen vorliegt:
|
brauchen, hängt davon ab, **welche** davon bei Ihnen vorliegt:
|
||||||
|
|
||||||

|

|
||||||
|
|
@ -21,11 +21,6 @@ brauchen, hängt davon ab, **welche** davon bei Ihnen vorliegt:
|
||||||
> und dann deterministisch zu rechnen. Warum das systematisch danebengeht, zeigt
|
> und dann deterministisch zu rechnen. Warum das systematisch danebengeht, zeigt
|
||||||
> {ref:kap:unsicherheit} unter dem Namen *Fluch des Durchschnitts*.
|
> {ref:kap:unsicherheit} unter dem Namen *Fluch des Durchschnitts*.
|
||||||
|
|
||||||
Zwei weitere Kapitel schließen sich an, weil sie dieselbe Voraussetzung aufgeben, nur an
|
|
||||||
einer anderen Stelle: {ref:kap:mehrziel} gibt das **eine Ziel** auf, {ref:kap:prognose} die
|
|
||||||
Annahme, die **Eingabedaten** seien gegeben statt selbst geschätzt. Was dieser Teil
|
|
||||||
insgesamt leistet, fasst die Synthese an seinem Ende zusammen.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Kapitel: Quadratische und nichtlineare Optimierung — KKT, Lagrange, Konvexität {#kap:qp-nlp}
|
# Kapitel: Quadratische und nichtlineare Optimierung — KKT, Lagrange, Konvexität {#kap:qp-nlp}
|
||||||
|
|
@ -783,8 +778,7 @@ unverändert gelassen. **Der weiche Term verteilt, die harte Schranke kappt.**
|
||||||
>
|
>
|
||||||
> **Zur Skalierung:** Mit $\alpha = 1$ ist der Ertragsterm ($\approx 0{,}1$) zehnmal größer
|
> **Zur Skalierung:** Mit $\alpha = 1$ ist der Ertragsterm ($\approx 0{,}1$) zehnmal größer
|
||||||
> als der Varianzterm ($\approx 0{,}01$). Das Modell ist also stark renditegetrieben.
|
> als der Varianzterm ($\approx 0{,}01$). Das Modell ist also stark renditegetrieben.
|
||||||
> Die Aufgabe *Effekt der Gewichtung untersuchen* ({ref:sec:qp-nlp-uebungsaufgaben}) lässt
|
> Übung 8.5 lässt Sie diese Balance untersuchen — sie ist eine fachliche Entscheidung, keine
|
||||||
> Sie diese Balance untersuchen — sie ist eine fachliche Entscheidung, keine
|
|
||||||
> technische.
|
> technische.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -1217,7 +1211,7 @@ groß ist der Unterschied zwischen bestem und schlechtestem Ergebnis? Kann die
|
||||||
|
|
||||||
## Finde den Denkfehler {#sec:qp-nlp-denkfehler}
|
## Finde den Denkfehler {#sec:qp-nlp-denkfehler}
|
||||||
|
|
||||||
> **🐛 Finde den Denkfehler: Die Kovarianzmatrix aus dem Controlling**
|
> **🐛 Finde den Denkfehler 8.1: Die Kovarianzmatrix aus dem Controlling**
|
||||||
>
|
>
|
||||||
> Ein Analyst soll das Risiko eines Portfolios aus drei Anlagen minimieren. Die
|
> Ein Analyst soll das Risiko eines Portfolios aus drei Anlagen minimieren. Die
|
||||||
> Korrelationen hat er aus drei verschiedenen Quartalsberichten zusammengetragen — jede
|
> Korrelationen hat er aus drei verschiedenen Quartalsberichten zusammengetragen — jede
|
||||||
|
|
@ -1285,7 +1279,7 @@ groß ist der Unterschied zwischen bestem und schlechtestem Ergebnis? Kann die
|
||||||
|
|
||||||
## Micro-Quiz {#sec:qp-nlp-quiz}
|
## Micro-Quiz {#sec:qp-nlp-quiz}
|
||||||
|
|
||||||
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
|
> **❓ Micro-Quiz 8: Drei Fragen zum Selbstcheck**
|
||||||
>
|
>
|
||||||
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
||||||
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-qp-nlp).
|
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-qp-nlp).
|
||||||
|
|
|
||||||
|
|
@ -428,7 +428,7 @@ Feinsuche (Raster 100..500): Optimum bei Kapazitaet 180, Kosten 12,810 EUR
|
||||||
> allgemeine Antwort — **rechnen Sie es aus.** Genau dafür ist Monte-Carlo da. Und beachten
|
> allgemeine Antwort — **rechnen Sie es aus.** Genau dafür ist Monte-Carlo da. Und beachten
|
||||||
> Sie die Spalte „Unterdeckung“: Bei Kapazität 200 wird in **24 % der Fälle** nachgekauft.
|
> Sie die Spalte „Unterdeckung“: Bei Kapazität 200 wird in **24 % der Fälle** nachgekauft.
|
||||||
> Ob das akzeptabel ist, entscheidet kein Optimierer, sondern ein Servicelevel-Ziel
|
> Ob das akzeptabel ist, entscheidet kein Optimierer, sondern ein Servicelevel-Ziel
|
||||||
> (Aufgabe *Monte-Carlo erweitern*, {ref:sec:unsicherheit-uebungsaufgaben}).
|
> (Übung 9.5).
|
||||||
|
|
||||||
> **💡 Wann Monte-Carlo das richtige Werkzeug ist**
|
> **💡 Wann Monte-Carlo das richtige Werkzeug ist**
|
||||||
> Immer dann, wenn Sie **eine Kennzahl brauchen, die keine Formel hat**: die
|
> Immer dann, wenn Sie **eine Kennzahl brauchen, die keine Formel hat**: die
|
||||||
|
|
@ -642,8 +642,7 @@ auch nicht optimal.
|
||||||
|
|
||||||
Der **EVPI von 7 375 €** ist bemerkenswert hoch: 45 % der Gesamtkosten entstehen allein
|
Der **EVPI von 7 375 €** ist bemerkenswert hoch: 45 % der Gesamtkosten entstehen allein
|
||||||
daraus, dass man die Zukunft nicht kennt. In so einem Fall lohnt sich Investition in bessere
|
daraus, dass man die Zukunft nicht kennt. In so einem Fall lohnt sich Investition in bessere
|
||||||
Prognosen tatsächlich — anders als in dem Beispiel aus der Aufgabe *EVPI interpretieren*
|
Prognosen tatsächlich — anders als in dem Beispiel aus Übung 9.4.
|
||||||
({ref:sec:unsicherheit-uebungsaufgaben}).
|
|
||||||
|
|
||||||
> **💡 Was ist der EVPI?**
|
> **💡 Was ist der EVPI?**
|
||||||
> Der **Expected Value of Perfect Information**{idx:EVPI} beziffert, wie viel eine perfekte Prognose
|
> Der **Expected Value of Perfect Information**{idx:EVPI} beziffert, wie viel eine perfekte Prognose
|
||||||
|
|
@ -1279,7 +1278,7 @@ das lässt sich in CVXPY mit `cp.sum_largest` formulieren.)
|
||||||
|
|
||||||
## Finde den Denkfehler {#sec:unsicherheit-denkfehler}
|
## Finde den Denkfehler {#sec:unsicherheit-denkfehler}
|
||||||
|
|
||||||
> **🐛 Finde den Denkfehler: Warum jedes Projekt zu spät fertig wird**
|
> **🐛 Finde den Denkfehler 9.1: Warum jedes Projekt zu spät fertig wird**
|
||||||
>
|
>
|
||||||
> Ein Projektleiter plant eine Umbaumaßnahme. Fünf Gewerke arbeiten **parallel** und
|
> Ein Projektleiter plant eine Umbaumaßnahme. Fünf Gewerke arbeiten **parallel** und
|
||||||
> unabhängig voneinander; fertig ist der Umbau, wenn das letzte fertig ist. Für jedes
|
> unabhängig voneinander; fertig ist der Umbau, wenn das letzte fertig ist. Für jedes
|
||||||
|
|
@ -1335,7 +1334,7 @@ das lässt sich in CVXPY mit `cp.sum_largest` formulieren.)
|
||||||
|
|
||||||
## Micro-Quiz {#sec:unsicherheit-quiz}
|
## Micro-Quiz {#sec:unsicherheit-quiz}
|
||||||
|
|
||||||
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
|
> **❓ Micro-Quiz 9: Drei Fragen zum Selbstcheck**
|
||||||
>
|
>
|
||||||
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
||||||
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-unsicherheit).
|
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-unsicherheit).
|
||||||
|
|
|
||||||
|
|
@ -739,7 +739,7 @@ Bei dynamischer Programmierung entscheidet eine einzige Frage über Erfolg oder
|
||||||
**Was gehört in den Zustand?** Ist er zu klein, rechnet das Verfahren völlig korrekt — nur
|
**Was gehört in den Zustand?** Ist er zu klein, rechnet das Verfahren völlig korrekt — nur
|
||||||
eben an einem anderen Problem.
|
eben an einem anderen Problem.
|
||||||
|
|
||||||
> **🐛 Finde den Denkfehler: Der Zustand, der zu wenig weiß**
|
> **🐛 Finde den Denkfehler 10.1: Der Zustand, der zu wenig weiß**
|
||||||
>
|
>
|
||||||
> Eine Werkstatt plant vier Perioden. Der Bedarf beträgt 3, 1, 4, 2 Stück; produziert werden
|
> Eine Werkstatt plant vier Perioden. Der Bedarf beträgt 3, 1, 4, 2 Stück; produziert werden
|
||||||
> können höchstens 5 je Periode, gelagert höchstens 6 Stück.
|
> können höchstens 5 je Periode, gelagert höchstens 6 Stück.
|
||||||
|
|
@ -796,7 +796,7 @@ eben an einem anderen Problem.
|
||||||
|
|
||||||
## Micro-Quiz {#sec:dynamische-programmierung-quiz}
|
## Micro-Quiz {#sec:dynamische-programmierung-quiz}
|
||||||
|
|
||||||
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
|
> **❓ Micro-Quiz 10: Drei Fragen zum Selbstcheck**
|
||||||
>
|
>
|
||||||
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
||||||
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-dynamische-programmierung).
|
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-dynamische-programmierung).
|
||||||
|
|
@ -862,4 +862,4 @@ Marktdaten — und lernen zuerst, warum diese Daten trügerisch sind.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Weiter mit:* [{ref:kap:mehrziel} — Mehrere Ziele](33_Mehrziel_und_Pareto.md)
|
*Weiter mit:* [{ref:kap:finanzdaten} — Finanzdaten und Kovarianz-Shrinkage](40_Finanzdaten.md)
|
||||||
|
|
|
||||||
|
|
@ -757,7 +757,3 @@ einer Fläche. Wie ändert sich das ε-Verfahren, und warum wächst der Aufwand
|
||||||
dafür so sauber wie möglich" ist eine Frage, die ein Kaufmann beantworten kann.
|
dafür so sauber wie möglich" ist eine Frage, die ein Kaufmann beantworten kann.
|
||||||
* Die Front wird erst entscheidbar durch die Spalte **€ je kg**: Sie ist mit dem internen
|
* Die Front wird erst entscheidbar durch die Spalte **€ je kg**: Sie ist mit dem internen
|
||||||
CO₂-Preis vergleichbar und verwandelt eine Wertefrage in eine Rechnung.
|
CO₂-Preis vergleichbar und verwandelt eine Wertefrage in eine Rechnung.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Weiter mit:* [{ref:kap:prognose} — Predict-then-Optimize](34_Predict_then_Optimize.md)
|
|
||||||
|
|
|
||||||
|
|
@ -700,7 +700,3 @@ und zwar **in unterschiedlichem Ausmaß**? Was sagt das über den Wert eines Mer
|
||||||
* Ein Kennzahlenvergleich braucht eine **Streuungsangabe**. Bei 180 Testtagen sah das
|
* Ein Kennzahlenvergleich braucht eine **Streuungsangabe**. Bei 180 Testtagen sah das
|
||||||
schlechtere Modell in gut jedem zehnten Fall besser aus — und wer einmal misst, hat genau
|
schlechtere Modell in gut jedem zehnten Fall besser aus — und wer einmal misst, hat genau
|
||||||
eine Ziehung.
|
eine Ziehung.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Weiter mit:* [Synthese Teil III](39_Synthese_Nichtlinear.md)
|
|
||||||
|
|
|
||||||
|
|
@ -735,7 +735,3 @@ trägt — und schreiben Sie auf, welche der drei Zeilen im Weg steht.
|
||||||
gegen reagierende Marktteilnehmer) und ob Teilbarkeit erlaubt ist (MILP gegen LP).
|
gegen reagierende Marktteilnehmer) und ob Teilbarkeit erlaubt ist (MILP gegen LP).
|
||||||
* Die erste dieser Grenzen ist die gefährlichste: Dieselbe Optimierung ist in der Werkstatt
|
* Die erste dieser Grenzen ist die gefährlichste: Dieselbe Optimierung ist in der Werkstatt
|
||||||
Planung und im Depot Fehlerverstärkung.
|
Planung und im Depot Fehlerverstärkung.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Weiter mit:* [{ref:kap:supplychain} — Supply-Chain und Energieeinsatz unter Unsicherheit](36_Supply_Chain_Energie.md)
|
|
||||||
|
|
|
||||||
|
|
@ -595,26 +595,6 @@ Was Versorgungssicherheit kostet
|
||||||
Der Erwartungswert-Plan kostet in der Wirklichkeit **149 % mehr**, und sein schlimmster Tag
|
Der Erwartungswert-Plan kostet in der Wirklichkeit **149 % mehr**, und sein schlimmster Tag
|
||||||
ist sechsmal so teuer.
|
ist sechsmal so teuer.
|
||||||
|
|
||||||
**Wofür genau?** Die Summendifferenz von 523 514 € sagt noch nicht, wo sie entsteht. Zerlegt
|
|
||||||
man sie nach Kostenarten, wird aus dem Befund eine Aussage, die man vorlegen kann:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Drei Dinge stehen darin, die die Tabelle nicht zeigt. Die **Anfahrkosten sind in beiden
|
|
||||||
Plänen gleich** (58 000 €) — der Erwartungswert-Plan spart also nicht dort, wo man es
|
|
||||||
vermuten würde. Er spart **12 725 € Brennstoff**, weil er weniger Blockstunden vorhält. Und
|
|
||||||
er bezahlt dafür **536 239 € Lastabwurf**: das **42-fache** seiner Ersparnis.
|
|
||||||
|
|
||||||
Genau diese Gegenüberstellung ist es, die eine Investitionsentscheidung trägt. „149 % mehr"
|
|
||||||
ist eine Zahl, über die man streiten kann; „wir sparen 12 725 € und riskieren dafür 536 239 €"
|
|
||||||
ist eine, über die man entscheidet.
|
|
||||||
|
|
||||||
> **📌 Zum Diagramm**
|
|
||||||
> Es wird von `bilder_04/erzeuge_wirkung.py` erzeugt, das die Instanz aus
|
|
||||||
> `Kraftwerkseinsatz.py` **erneut rechnet** statt Zahlen zu übernehmen — und dabei die
|
|
||||||
> beiden abgedruckten Summen exakt reproduziert. Die Trennung von Brennstoff- und
|
|
||||||
> Abwurfkosten ist der einzige Zusatz; das Buchprogramm braucht sie nicht.
|
|
||||||
|
|
||||||
Der Unterschied zwischen den beiden Plänen ist dabei erstaunlich klein:
|
Der Unterschied zwischen den beiden Plänen ist dabei erstaunlich klein:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
@ -785,7 +765,3 @@ und welches Verfahren aus {ref:kap:metaheuristiken} würden Sie einsetzen?
|
||||||
der erwarteten Kosten die Absicherung von selbst.
|
der erwarteten Kosten die Absicherung von selbst.
|
||||||
* Wo sie greift, liefert sie den **Preis der Vorgabe**: 46 € je vermiedener MWh — eine Zahl,
|
* Wo sie greift, liefert sie den **Preis der Vorgabe**: 46 € je vermiedener MWh — eine Zahl,
|
||||||
über die man verhandeln kann.
|
über die man verhandeln kann.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Weiter mit:* [{ref:kap:finanzdaten} — Finanzdaten-Modellierung](40_Finanzdaten.md)
|
|
||||||
|
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
# Synthese Teil III — Wenn die Idealwelt nicht gilt
|
|
||||||
|
|
||||||
{ref:teil:kernverfahren} setzte voraus: feste Daten, lineare Zusammenhänge, ein Ziel, eine
|
|
||||||
Periode. Dieser Teil nimmt jede dieser vier Voraussetzungen einzeln weg. Die Tabelle ordnet
|
|
||||||
die Kapitel danach, **welche** Voraussetzung bei Ihnen verletzt ist.
|
|
||||||
|
|
||||||
## Welche Annahme fällt weg?
|
|
||||||
|
|
||||||
| Was nicht gilt | Woran Sie es merken | Werkzeug | Kapitel |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| **Linearität** | Risiko wächst quadratisch, Kosten degressiv, Sättigung | QP, konvexe Optimierung, KKT | {ref:kap:qp-nlp} |
|
|
||||||
| **Feste Daten** | Nachfrage, Wind, Rendite stehen erst morgen fest | Monte-Carlo, Zweistufigkeit, Robustheit, Chance Constraints | {ref:kap:unsicherheit} |
|
|
||||||
| **Eine Periode** | die Entscheidung heute verändert, was übermorgen möglich ist | Bellman-Gleichung, Rückwärtsinduktion | {ref:kap:dynamische-programmierung} |
|
|
||||||
| **Ein Ziel** | zwei Kennzahlen sollen gleichzeitig stimmen | Pareto-Front, ε-Constraint | {ref:kap:mehrziel} |
|
|
||||||
| **Daten sind gegeben** | die Eingabe ist selbst eine Prognose | Predict-then-Optimize, entscheidungsorientiertes Lernen | {ref:kap:prognose} |
|
|
||||||
|
|
||||||
Die letzte Zeile ist die unbequemste: Dort ist nicht das Modell unsicher, sondern seine
|
|
||||||
**Eingabe** — und die kommt aus einem zweiten Modell, das nach anderen Kriterien gebaut
|
|
||||||
wurde.
|
|
||||||
|
|
||||||
## Was dieser Teil gemessen hat
|
|
||||||
|
|
||||||
| Behauptung | Gemessen | Wo |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| „Mit dem Mittelwert zu rechnen ist eine brauchbare Näherung." | Der Mittelwertplan hält seine Zusage in **50,08 %** der Fälle — per Definition | {ref:sec:unsicherheit-chance-constraints} |
|
|
||||||
| „Versorgungssicherheit kostet linear." | 56 289 € je Prozentpunkt auf dem Weg zu 80 %, **253 848 €** zwischen 95 und 99 % — das 4,5-fache | {ref:sec:unsicherheit-chance-constraints} |
|
|
||||||
| „Eine Zusage aus dem Modell hält." | Der 95-%-Plan hält **87,44 %**, sobald die Testverteilung einen Fall enthält, in dem alles zugleich ausfällt | {ref:sec:unsicherheit-chance-constraints} |
|
|
||||||
| „Die bessere Prognose führt zur besseren Entscheidung." | Das Modell mit dem **schlechteren** MSE trifft die günstigere Entscheidung | {ref:kap:prognose} |
|
|
||||||
|
|
||||||
## Drei Fehler, die dieser Teil verhindert
|
|
||||||
|
|
||||||
1. **Unsicherheit durch Mittelwerte ersetzen.** Der Durchschnittskunde kauft nie. Bei
|
|
||||||
asymmetrischen Kosten liegt das Optimum systematisch neben dem Mittelwert, und eine
|
|
||||||
Terminzusage ist ein **Quantil**, kein Erwartungswert.
|
|
||||||
2. **Konvexität voraussetzen, ohne sie zu prüfen.** Ohne sie gibt es keine
|
|
||||||
Optimalitätsgarantie, sondern nur ein lokales Ergebnis — und der Solver sagt das nicht
|
|
||||||
von selbst.
|
|
||||||
3. **Zwei Ziele mit einem Gewicht verrechnen, bevor die Front bekannt ist.** Ein Gewicht
|
|
||||||
ist eine Antwort auf eine Frage, die noch niemand gestellt hat. Die Pareto-Front stellt
|
|
||||||
sie zuerst.
|
|
||||||
|
|
||||||
## Wenn Sie nur eines mitnehmen
|
|
||||||
|
|
||||||
> **🎯** Jede der vier Annahmen lässt sich einzeln aufgeben — aber jede kostet etwas
|
|
||||||
> Bestimmtes: Linearität kostet die Optimalitätsgarantie, feste Daten kosten Rechenzeit
|
|
||||||
> oder Vorsicht, eine Periode kostet Zustandsraum, ein Ziel kostet eine Entscheidung, die
|
|
||||||
> nicht der Modellierer treffen darf. Wer weiß, **welche** Annahme bei ihm fällt, weiß
|
|
||||||
> auch, welchen Preis er zahlt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Weiter mit:* [{ref:kap:bruecke} — Die Strukturbrücke](35_Strukturbruecke.md)
|
|
||||||
|
|
@ -448,8 +448,7 @@ beide und macht transparent, welches wo zum Einsatz kommt.
|
||||||
> unkorreliert sind — eine starke, aber sehr stabile Annahme. Das
|
> unkorreliert sind — eine starke, aber sehr stabile Annahme. Das
|
||||||
> **Konstant-Korrelations-Ziel** behält die geschätzten Einzelvarianzen und glättet nur die
|
> **Konstant-Korrelations-Ziel** behält die geschätzten Einzelvarianzen und glättet nur die
|
||||||
> Korrelationen; das ist bei Aktien meist realistischer, weil Titel tatsächlich sehr
|
> Korrelationen; das ist bei Aktien meist realistischer, weil Titel tatsächlich sehr
|
||||||
> unterschiedliche Volatilitäten haben. In der Aufgabe *Beide Shrinkage-Ziele vergleichen*
|
> unterschiedliche Volatilitäten haben. In Übung 11.5 implementieren Sie es selbst und
|
||||||
> ({ref:sec:finanzdaten-uebungsaufgaben}) implementieren Sie es selbst und
|
|
||||||
> vergleichen.
|
> vergleichen.
|
||||||
|
|
||||||
Das Ergebnis ist in jedem Fall garantiert **positiv definit**, wohlkonditioniert und stabil
|
Das Ergebnis ist in jedem Fall garantiert **positiv definit**, wohlkonditioniert und stabil
|
||||||
|
|
@ -681,7 +680,7 @@ Erweitern Sie `Schaetzrauschen_Demo.py` um eine dritte Variante: die
|
||||||
|
|
||||||
## Finde den Denkfehler {#sec:finanzdaten-denkfehler}
|
## Finde den Denkfehler {#sec:finanzdaten-denkfehler}
|
||||||
|
|
||||||
> **🐛 Finde den Denkfehler: Das risikofreie Portfolio**
|
> **🐛 Finde den Denkfehler 11.1: Das risikofreie Portfolio**
|
||||||
>
|
>
|
||||||
> Ein Analyst stellt ein Portfolio aus **30 Titeln** zusammen. Für die Kovarianzmatrix
|
> Ein Analyst stellt ein Portfolio aus **30 Titeln** zusammen. Für die Kovarianzmatrix
|
||||||
> nimmt er das letzte Quartal — bei Wochendaten sind das **20 Beobachtungen**. Dann
|
> nimmt er das letzte Quartal — bei Wochendaten sind das **20 Beobachtungen**. Dann
|
||||||
|
|
@ -761,8 +760,7 @@ def minimales_risiko(kovarianz: np.ndarray) -> tuple[np.ndarray, float]:
|
||||||
exakt positiv semidefinit ist - rechne trotzdem.' Das ist hier bewusst so
|
exakt positiv semidefinit ist - rechne trotzdem.' Das ist hier bewusst so
|
||||||
gewaehlt, denn genau diese Eigenschaft wollen wir vorfuehren. Im
|
gewaehlt, denn genau diese Eigenschaft wollen wir vorfuehren. Im
|
||||||
Produktivbetrieb waere die DCP-Fehlermeldung das Warnsignal, dem man
|
Produktivbetrieb waere die DCP-Fehlermeldung das Warnsignal, dem man
|
||||||
nachgehen muss (siehe den Denkfehler im Kapitel Quadratische und
|
nachgehen muss (siehe Denkfehler 8.1).
|
||||||
nichtlineare Optimierung).
|
|
||||||
"""
|
"""
|
||||||
gewichte = cp.Variable(ANZAHL_ANLAGEN)
|
gewichte = cp.Variable(ANZAHL_ANLAGEN)
|
||||||
problem = cp.Problem(cp.Minimize(cp.quad_form(gewichte, cp.psd_wrap(kovarianz))),
|
problem = cp.Problem(cp.Minimize(cp.quad_form(gewichte, cp.psd_wrap(kovarianz))),
|
||||||
|
|
@ -887,7 +885,7 @@ e 0: \mathbf{w}^ op\mathbf{S}\mathbf{w} = 0$ | „In genau diesen Richtungen mis
|
||||||
|
|
||||||
## Micro-Quiz {#sec:finanzdaten-quiz}
|
## Micro-Quiz {#sec:finanzdaten-quiz}
|
||||||
|
|
||||||
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
|
> **❓ Micro-Quiz 11: Drei Fragen zum Selbstcheck**
|
||||||
>
|
>
|
||||||
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
||||||
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-finanzdaten).
|
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-finanzdaten).
|
||||||
|
|
@ -952,4 +950,4 @@ Markowitz-Effizienzgrenze mit realistischen institutionellen Restriktionen.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Weiter mit:* [{ref:kap:markowitz} — Die moderne Portfoliotheorie nach Markowitz](41_Markowitz.md)
|
*Weiter mit:* [{ref:kap:markowitz} — Markowitz-Portfoliotheorie](41_Markowitz.md)
|
||||||
|
|
|
||||||
|
|
@ -763,7 +763,7 @@ zweiten. Vergleichen Sie GMV, Max Sharpe und 1/N.
|
||||||
anrichtet. Bei den erwarteten **Renditen** ist es schlimmer — und weil sie in der
|
anrichtet. Bei den erwarteten **Renditen** ist es schlimmer — und weil sie in der
|
||||||
Zielfunktion stehen statt in der Nebenbedingung, fällt es schwerer auf.
|
Zielfunktion stehen statt in der Nebenbedingung, fällt es schwerer auf.
|
||||||
|
|
||||||
> **🐛 Finde den Denkfehler: Zwölf gleiche Anlagen, ein sehr ungleiches Portfolio**
|
> **🐛 Finde den Denkfehler 12.1: Zwölf gleiche Anlagen, ein sehr ungleiches Portfolio**
|
||||||
>
|
>
|
||||||
> Ein Analyst optimiert ein Portfolio aus zwölf Anlagen. Erwartete Renditen und
|
> Ein Analyst optimiert ein Portfolio aus zwölf Anlagen. Erwartete Renditen und
|
||||||
> Kovarianzen schätzt er aus einem Jahr Tagesdaten — 250 Beobachtungen, in der Praxis
|
> Kovarianzen schätzt er aus einem Jahr Tagesdaten — 250 Beobachtungen, in der Praxis
|
||||||
|
|
@ -986,7 +986,7 @@ Drei praktische Konsequenzen:
|
||||||
|
|
||||||
## Micro-Quiz {#sec:markowitz-quiz}
|
## Micro-Quiz {#sec:markowitz-quiz}
|
||||||
|
|
||||||
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
|
> **❓ Micro-Quiz 12: Drei Fragen zum Selbstcheck**
|
||||||
>
|
>
|
||||||
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
||||||
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-markowitz).
|
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-markowitz).
|
||||||
|
|
|
||||||
|
|
@ -720,7 +720,7 @@ $\tau \in \{0{,}05;\ 0{,}1;\ 0{,}25;\ 0{,}5;\ 1{,}0\}$.
|
||||||
|
|
||||||
## Finde den Denkfehler {#sec:cvar-denkfehler}
|
## Finde den Denkfehler {#sec:cvar-denkfehler}
|
||||||
|
|
||||||
> **🐛 Finde den Denkfehler: Das Risikobudget, das durch Diversifikation stieg**
|
> **🐛 Finde den Denkfehler 13.1: Das Risikobudget, das durch Diversifikation stieg**
|
||||||
>
|
>
|
||||||
> Eine Bank vergibt Risikobudgets je Abteilung. Zwei Kreditabteilungen halten je eine
|
> Eine Bank vergibt Risikobudgets je Abteilung. Zwei Kreditabteilungen halten je eine
|
||||||
> Unternehmensanleihe über 100 € Nominal mit **4 % Ausfallwahrscheinlichkeit** (bei
|
> Unternehmensanleihe über 100 € Nominal mit **4 % Ausfallwahrscheinlichkeit** (bei
|
||||||
|
|
@ -779,7 +779,7 @@ $\tau \in \{0{,}05;\ 0{,}1;\ 0{,}25;\ 0{,}5;\ 1{,}0\}$.
|
||||||
|
|
||||||
## Micro-Quiz {#sec:cvar-quiz}
|
## Micro-Quiz {#sec:cvar-quiz}
|
||||||
|
|
||||||
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
|
> **❓ Micro-Quiz 13: Drei Fragen zum Selbstcheck**
|
||||||
>
|
>
|
||||||
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
||||||
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-cvar).
|
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-cvar).
|
||||||
|
|
@ -846,4 +846,4 @@ Rebalancing und ein Walk-Forward-Backtest ohne Lookahead-Bias.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Weiter mit:* [{ref:kap:handelsmaschine} — Die vollständige quantitative Handelsmaschine](43_Handelsmaschine.md)
|
*Weiter mit:* [{ref:kap:handelsmaschine} — Die vollständige Handelsmaschine](43_Handelsmaschine.md)
|
||||||
|
|
|
||||||
|
|
@ -754,7 +754,7 @@ sondern in der **Arbeitsweise davor** — und hinterlassen im Programm keine Spu
|
||||||
Backtest, der zwanzigmal nachjustiert wurde, sieht am Ende genauso sauber aus wie einer,
|
Backtest, der zwanzigmal nachjustiert wurde, sieht am Ende genauso sauber aus wie einer,
|
||||||
der beim ersten Versuch funktioniert hat.
|
der beim ersten Versuch funktioniert hat.
|
||||||
|
|
||||||
> **🐛 Finde den Denkfehler: Die Strategie mit dem hochsignifikanten Ergebnis**
|
> **🐛 Finde den Denkfehler 14.1: Die Strategie mit dem hochsignifikanten Ergebnis**
|
||||||
>
|
>
|
||||||
> Eine Analystin entwickelt über mehrere Wochen eine Handelsstrategie. Sie probiert
|
> Eine Analystin entwickelt über mehrere Wochen eine Handelsstrategie. Sie probiert
|
||||||
> verschiedene Signalfenster, Schwellenwerte, Filter und Haltedauern durch — insgesamt
|
> verschiedene Signalfenster, Schwellenwerte, Filter und Haltedauern durch — insgesamt
|
||||||
|
|
@ -980,7 +980,7 @@ von allen Fehlversuchen genau einen: den erfolgreichsten.
|
||||||
|
|
||||||
## Micro-Quiz {#sec:handelsmaschine-quiz}
|
## Micro-Quiz {#sec:handelsmaschine-quiz}
|
||||||
|
|
||||||
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
|
> **❓ Micro-Quiz 14: Drei Fragen zum Selbstcheck**
|
||||||
>
|
>
|
||||||
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
||||||
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-handelsmaschine).
|
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-handelsmaschine).
|
||||||
|
|
@ -1047,4 +1047,4 @@ der Weg vom Skript zum produktiven System.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Weiter mit:* [Synthese Teil IV](49_Synthese_Anwendungen.md)
|
*Weiter mit:* [{ref:kap:praxisfallen} — Praxisfallen und produktiver Einsatz](50_Praxis.md)
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
# Synthese Teil IV — Zwei Domänen, dieselbe Mathematik
|
|
||||||
|
|
||||||
Sechs Kapitel, zwei Branchen, die sich für unvergleichbar halten. Die Werkstatt plant
|
|
||||||
Maschinen, das Depot plant Titel — und beide lösen dasselbe Problem: **knappe Mittel auf
|
|
||||||
konkurrierende Verwendungen verteilen, unter Unsicherheit.**
|
|
||||||
|
|
||||||
## Dieselbe Struktur, andere Namen
|
|
||||||
|
|
||||||
| Werkstatt und Kraftwerk | Depot und Handel | Was mathematisch dahintersteht |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Produkt, Maschinenstunde | Titel, Kapital | Entscheidungsvariable |
|
|
||||||
| Deckungsbeitrag | erwartete Rendite | linearer Zielterm |
|
|
||||||
| Kapazität, Liefervertrag | Positionsgrenze, Sektorlimit | Nebenbedingung |
|
|
||||||
| Rüstkosten, Anfahrkosten | Transaktionskosten | Fixkosten mit Binärvariable (B1) |
|
|
||||||
| Lastabwurf, Konventionalstrafe | Tail-Verlust | Straf- bzw. Risikoterm |
|
|
||||||
| Wind- und Nachfrageszenarien | Renditeszenarien | Szenariomenge |
|
|
||||||
|
|
||||||
**Und wo die Analogie endet**, benennt {ref:kap:bruecke} an drei Stellen ausdrücklich — das
|
|
||||||
ist der wertvollere Teil. Eine Analogie, die man nicht begrenzen kann, ist keine Erkenntnis,
|
|
||||||
sondern eine Redewendung.
|
|
||||||
|
|
||||||
## Was dieser Teil gemessen hat
|
|
||||||
|
|
||||||
| Behauptung | Gemessen | Wo |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| „Planen mit der Prognose ist gut genug." | Der Erwartungswert-Plan kostet **149 % mehr** (874 870 € statt 351 356 €) und wirft in **28 von 40** Szenarien Last ab | {ref:kap:supplychain} |
|
|
||||||
| „Dann sparen wir eben beim Brennstoff." | Genau das tut er: 12 725 € gespart, **536 239 €** Lastabwurf bezahlt — das 42-fache. Die Anfahrkosten sind in beiden Plänen gleich | {ref:kap:supplychain} |
|
|
||||||
| „Der VaR sagt, wie riskant es ist." | VaR **1,86 %**, CVaR **2,99 %** — und der schlechteste Tag der Stichprobe (**−23,0 %**) ändert am VaR **nichts** | {ref:kap:cvar} |
|
|
||||||
| „Die Kovarianzmatrix ist eine Eingabe wie jede andere." | Aus kurzen Reihen geschätzt ist sie schlecht konditioniert; die Optimierung setzt dann auf Schätzrauschen | {ref:kap:finanzdaten} |
|
|
||||||
|
|
||||||
## Drei Fehler, die dieser Teil verhindert
|
|
||||||
|
|
||||||
1. **Bei Markowitz einsteigen.** Wer {ref:kap:finanzdaten} überspringt, optimiert seine
|
|
||||||
eigenen Schätzfehler und wundert sich über Gewichte, die niemand halten würde.
|
|
||||||
2. **Den VaR für ein Risikomaß halten.** Er sagt, **ob** die Schwelle gerissen wird, nicht
|
|
||||||
**wie schlimm** es dahinter aussieht — und er ist nicht kohärent. Optimiert wird der
|
|
||||||
CVaR.
|
|
||||||
3. **Trägheit unterschätzen.** Im Kraftwerkspark ist nicht die Prognosegüte das Problem,
|
|
||||||
sondern dass sich ein Block mit acht Stunden Mindestlaufzeit um 18 Uhr nicht mehr
|
|
||||||
herbeirufen lässt. Dieselbe Trägheit heißt im Depot Transaktionskosten.
|
|
||||||
|
|
||||||
## Wenn Sie nur eines mitnehmen
|
|
||||||
|
|
||||||
> **🎯** Wer die Struktur erkennt, kann sein Werkzeug mitnehmen, wenn er die Branche
|
|
||||||
> wechselt. Wer nur die Bibliothek kennt, fängt jedes Mal von vorn an — und wer die Grenzen
|
|
||||||
> der Analogie nicht kennt, überträgt irgendwann auch das, was nicht überträgt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Weiter mit:* [{ref:kap:praxisfallen} — Praxisfallen und der Weg zum produktiven Einsatz](50_Praxis.md)
|
|
||||||
|
|
@ -20,7 +20,6 @@
|
||||||
> **Programme:**\
|
> **Programme:**\
|
||||||
> `Infeasibility_Diagnose.py`\
|
> `Infeasibility_Diagnose.py`\
|
||||||
> `Erklaerbarkeit.py`\
|
> `Erklaerbarkeit.py`\
|
||||||
> `Constraint_Attribution.py`\
|
|
||||||
> `Betriebsueberwachung.py`\
|
> `Betriebsueberwachung.py`\
|
||||||
> `or_kern.py`\
|
> `or_kern.py`\
|
||||||
> `Solverwechsel_CPSAT_HiGHS.py`
|
> `Solverwechsel_CPSAT_HiGHS.py`
|
||||||
|
|
@ -90,17 +89,15 @@ Nach diesem Kapitel können Sie …
|
||||||
1. … die fünf typischen Praxisfallen benennen und je ein Gegenmittel nennen.
|
1. … die fünf typischen Praxisfallen benennen und je ein Gegenmittel nennen.
|
||||||
2. … ein unlösbares Modell in einen „am wenigsten schlechten“ Plan verwandeln.
|
2. … ein unlösbares Modell in einen „am wenigsten schlechten“ Plan verwandeln.
|
||||||
3. … Solver-Ergebnisse erklärbar machen (*Explainable OR*).
|
3. … Solver-Ergebnisse erklärbar machen (*Explainable OR*).
|
||||||
4. … für jede Nebenbedingung beziffern, was sie den Plan kostet — und begründen, warum
|
4. … Zeitlimits und Optimalitätslücken sinnvoll setzen.
|
||||||
der Schattenpreis diese Frage nur für die *nächste* Einheit beantwortet.
|
5. … die Architektur einer produktionsreifen OR-Plattform skizzieren.
|
||||||
5. … Zeitlimits und Optimalitätslücken sinnvoll setzen.
|
6. … eine **modellunabhängige Abnahmeprüfung** schreiben, die eine Lösung gegen die
|
||||||
6. … die Architektur einer produktionsreifen OR-Plattform skizzieren.
|
|
||||||
7. … eine **modellunabhängige Abnahmeprüfung** schreiben, die eine Lösung gegen die
|
|
||||||
Anforderungen prüft statt gegen das Modell.
|
Anforderungen prüft statt gegen das Modell.
|
||||||
8. … Status, Gap und Zeitausschöpfung protokollieren und begründen, warum eine konstante
|
7. … Status, Gap und Zeitausschöpfung protokollieren und begründen, warum eine konstante
|
||||||
Laufzeit bei Zeitlimit ein Warnsignal ist.
|
Laufzeit bei Zeitlimit ein Warnsignal ist.
|
||||||
9. … ein Modell in Domänenschicht, Modellbauer und Lösungs-DTO trennen — und begründen,
|
8. … ein Modell in Domänenschicht, Modellbauer und Lösungs-DTO trennen — und begründen,
|
||||||
warum sich das nicht wegen des Solverwechsels lohnt, sondern wegen der Prüfbarkeit.
|
warum sich das nicht wegen des Solverwechsels lohnt, sondern wegen der Prüfbarkeit.
|
||||||
10. … die Statuswerte verschiedener Solverbibliotheken auf eine gemeinsame Sprache
|
9. … die Statuswerte verschiedener Solverbibliotheken auf eine gemeinsame Sprache
|
||||||
abbilden.
|
abbilden.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -376,10 +373,6 @@ technisch aus, aber selbst wenn er dürfte, wäre er wegen seiner Vorbelastung i
|
||||||
teurer als Anna. **Eine bindende Bedingung zu lockern hilft nur, wenn sie tatsächlich die
|
teurer als Anna. **Eine bindende Bedingung zu lockern hilft nur, wenn sie tatsächlich die
|
||||||
kostenrelevante ist** — sonst ist die Mühe umsonst.
|
kostenrelevante ist** — sonst ist die Mühe umsonst.
|
||||||
|
|
||||||
Derselbe Unterschied kehrt auf Modellebene wieder, und dort lässt er sich in Euro
|
|
||||||
beziffern: {ref:sec:praxisfallen-attribution} misst für jede Bedingung, was sie den
|
|
||||||
ganzen Plan kostet — und baut daraus einen Bericht, den man vorlegen kann.
|
|
||||||
|
|
||||||
### Falle 3 — Regimewechsel und Schätzfehler
|
### Falle 3 — Regimewechsel und Schätzfehler
|
||||||
|
|
||||||
Ein Modell, das auf einer mehrjährigen Aufwärtsphase kalibriert wurde, kennt keine
|
Ein Modell, das auf einer mehrjährigen Aufwärtsphase kalibriert wurde, kennt keine
|
||||||
|
|
@ -422,500 +415,6 @@ wächst nicht linear mit der Problemgröße.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Constraint Attribution: welche Bedingung kostet wie viel? {#sec:praxisfallen-attribution}
|
|
||||||
|
|
||||||
Der Constraint-Trace beantwortet „warum **diese** Zuweisung?“. Der Deletion Filter in
|
|
||||||
{ref:anhang:fehlerdiagnose} beantwortet „warum geht es **gar nicht**?“. Der häufigste Fall
|
|
||||||
in der Praxis liegt dazwischen, und für ihn hat bisher niemand ein Werkzeug bekommen:
|
|
||||||
|
|
||||||
> *Das Modell rechnet. Der Plan ist zulässig. Und trotzdem ist er enttäuschend — nur weiß
|
|
||||||
> niemand, woran es liegt.*
|
|
||||||
|
|
||||||
Die Antwort heißt **Constraint Attribution**{idx:Constraint Attribution}: Jede
|
|
||||||
Bedingungsgruppe wird einzeln gelockert, und gemessen wird, was sich am Zielwert ändert. Das
|
|
||||||
ist dasselbe Verfahren wie der Deletion Filter — nur mit **Kosten** statt Zulässigkeit als
|
|
||||||
Kriterium.
|
|
||||||
|
|
||||||
Gerechnet wird auf derselben Fabrik wie die Konfliktsuche in {ref:anhang:fehlerdiagnose},
|
|
||||||
aber **nach der Reparatur**: Dort war die Lackierkapazität von 150 Stunden der Kern des
|
|
||||||
Widerspruchs ($2 \cdot 40 + 3 \cdot 30 = 170 > 150$). Eine zweite Schicht hat sie auf 210
|
|
||||||
gebracht, der Mindestumsatz wurde auf 10 000 € gesenkt. Jetzt ist das Modell lösbar — und
|
|
||||||
die Frage lautet nicht mehr „warum nicht?“, sondern „was kostet uns was?“.
|
|
||||||
|
|
||||||
```python
|
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# Constraint_Attribution.py
|
|
||||||
"""
|
|
||||||
Kapitel Praxisfallen: Welche Bedingung kostet wie viel - und was sagt man
|
|
||||||
dem Management?
|
|
||||||
|
|
||||||
Zwischen den beiden bekannten Faellen klafft eine Luecke:
|
|
||||||
|
|
||||||
* Erklaerbarkeit.py beantwortet "warum diese eine Zuweisung?"
|
|
||||||
* Konfliktsuche.py (Anhang Fehlerdiagnose) beantwortet "warum geht es gar
|
|
||||||
nicht?"
|
|
||||||
|
|
||||||
Der haeufigste Fall in der Praxis liegt dazwischen: Das Modell rechnet, der
|
|
||||||
Plan ist zulaessig - und trotzdem unbefriedigend. Niemand weiss, WORAN es
|
|
||||||
liegt. Genau das beantwortet Constraint Attribution.
|
|
||||||
|
|
||||||
Gerechnet wird auf derselben Fabrik wie in Konfliktsuche.py, nur nach der
|
|
||||||
Reparatur: Dort war die Lackierkapazitaet von 150 Stunden der Kern des
|
|
||||||
Widerspruchs (2*40 + 3*30 = 170 > 150). Eine zweite Schicht hat sie auf 210
|
|
||||||
gebracht, und der Mindestumsatz wurde auf 10.000 EUR gesenkt. Jetzt ist das
|
|
||||||
Modell loesbar - und die Frage lautet nicht mehr "warum nicht?", sondern
|
|
||||||
"was kostet uns was?".
|
|
||||||
|
|
||||||
Gezeigt werden vier Dinge:
|
|
||||||
1. Welche Bedingungen binden - und dass "bindend" nicht "teuer" heisst.
|
|
||||||
2. Dass der Schattenpreis eine MOMENTAUFNAHME ist: hochgerechnet auf eine
|
|
||||||
realistische Sonderschicht verspricht er hier das Doppelte des
|
|
||||||
tatsaechlichen Nutzens.
|
|
||||||
3. Dass ein Wunsch aus dem Vertrieb nicht teuer, sondern unmoeglich sein
|
|
||||||
kann - und welche Bedingungen ihn gemeinsam blockieren.
|
|
||||||
4. Wie aus alledem ein Bericht in Alltagssprache wird.
|
|
||||||
|
|
||||||
Benoetigt: numpy, scipy
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import textwrap
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
from scipy.optimize import linprog
|
|
||||||
|
|
||||||
PRODUKTE = ["Rahmen", "Gehaeuse", "Deckel", "Traeger", "Halter"]
|
|
||||||
DECKUNGSBEITRAG = np.array([35.0, 28.0, 9.0, 22.0, 6.0]) # EUR je Stueck
|
|
||||||
|
|
||||||
# Dieselben Bedingungen wie in Konfliktsuche.py, mit den beiden Reparaturen.
|
|
||||||
# Der sprechende Name ist keine Kosmetik: Ohne ihn ist kein Bericht moeglich,
|
|
||||||
# der jemandem ausserhalb der Modellierung etwas sagt.
|
|
||||||
BEDINGUNGEN: list[tuple[str, list[float], str, float]] = [
|
|
||||||
("Kapazitaet Montage", [3, 2, 1, 4, 2], "<=", 400),
|
|
||||||
("Kapazitaet Lackieren", [2, 3, 0, 1, 0], "<=", 210),
|
|
||||||
("Kapazitaet Pruefung", [1, 1, 1, 1, 1], "<=", 300),
|
|
||||||
("Liefervertrag Rahmen", [1, 0, 0, 0, 0], ">=", 40),
|
|
||||||
("Liefervertrag Gehaeuse", [0, 1, 0, 0, 0], ">=", 30),
|
|
||||||
("Liefervertrag Deckel", [0, 0, 1, 0, 0], ">=", 20),
|
|
||||||
("Liefervertrag Traeger", [0, 0, 0, 1, 0], ">=", 25),
|
|
||||||
("Marktgrenze Rahmen", [1, 0, 0, 0, 0], "<=", 120),
|
|
||||||
("Marktgrenze Gehaeuse", [0, 1, 0, 0, 0], "<=", 90),
|
|
||||||
("Marktgrenze Deckel", [0, 0, 1, 0, 0], "<=", 150),
|
|
||||||
("Marktgrenze Halter", [0, 0, 0, 0, 1], "<=", 200),
|
|
||||||
("Mindestumsatz", [90, 70, 30, 60, 20], ">=", 10000),
|
|
||||||
("Sortimentsbreite", [0, 0, 1, 1, 1], ">=", 100),
|
|
||||||
("Lackierbudget Schicht 2", [0, 1, 0, 1, 0], "<=", 55),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Was liesse sich im Ernstfall wirklich bewegen - und um wie viel? Nicht "eine
|
|
||||||
# Einheit", sondern der Schritt, den ein Planer tatsaechlich gehen kann: eine
|
|
||||||
# Sonderschicht, ein nachverhandelter Vertrag. Genau an dieser Groesse
|
|
||||||
# scheitert das blosse Hochrechnen des Schattenpreises.
|
|
||||||
STELLSCHRAUBEN: dict[str, tuple[float, str]] = {
|
|
||||||
"Kapazitaet Montage": (+50, "eine Sonderschicht Montage"),
|
|
||||||
"Kapazitaet Lackieren": (+30, "eine Sonderschicht Lackieren"),
|
|
||||||
"Kapazitaet Pruefung": (+50, "eine Sonderschicht Pruefung"),
|
|
||||||
"Lackierbudget Schicht 2": (+10, "10 Stunden mehr Budget in Schicht 2"),
|
|
||||||
"Liefervertrag Traeger": (-10, "10 Stueck weniger Abnahmepflicht Traeger"),
|
|
||||||
"Liefervertrag Gehaeuse": (-10, "10 Stueck weniger Abnahmepflicht Gehaeuse"),
|
|
||||||
}
|
|
||||||
|
|
||||||
WUNSCH_MENGE = 55.0 # der Vertrieb haette gern mehr Rahmen
|
|
||||||
WUNSCH_PRODUKT = "Rahmen"
|
|
||||||
|
|
||||||
|
|
||||||
def loese(bedingungen: list[tuple[str, list[float], str, float]]):
|
|
||||||
"""Maximiert den Deckungsbeitrag. linprog minimiert, daher das Vorzeichen."""
|
|
||||||
matrix, rechte_seite = [], []
|
|
||||||
for _, koeffizienten, richtung, grenze in bedingungen:
|
|
||||||
zeile = np.array(koeffizienten, dtype=float)
|
|
||||||
matrix.append(zeile if richtung == "<=" else -zeile)
|
|
||||||
rechte_seite.append(grenze if richtung == "<=" else -grenze)
|
|
||||||
return linprog(-DECKUNGSBEITRAG,
|
|
||||||
A_ub=np.array(matrix), b_ub=np.array(rechte_seite),
|
|
||||||
bounds=[(0, None)] * len(PRODUKTE), method="highs")
|
|
||||||
|
|
||||||
|
|
||||||
def geaendert(name: str, delta: float,
|
|
||||||
basis=None) -> list[tuple[str, list[float], str, float]]:
|
|
||||||
"""Dieselben Bedingungen, eine davon um delta verschoben."""
|
|
||||||
return [(n, k, r, g + delta if n == name else g)
|
|
||||||
for n, k, r, g in (basis or BEDINGUNGEN)]
|
|
||||||
|
|
||||||
|
|
||||||
def auswertung(ergebnis, bedingungen=None):
|
|
||||||
"""Schlupf und Schattenpreis je Bedingung."""
|
|
||||||
x = ergebnis.x
|
|
||||||
dual = ergebnis.ineqlin.marginals
|
|
||||||
zeilen = []
|
|
||||||
for i, (name, koeff, richtung, grenze) in enumerate(bedingungen or BEDINGUNGEN):
|
|
||||||
links = float(np.array(koeff, dtype=float) @ x)
|
|
||||||
schlupf = (grenze - links) if richtung == "<=" else (links - grenze)
|
|
||||||
zeilen.append((name, links, richtung, grenze, schlupf, abs(dual[i])))
|
|
||||||
return zeilen
|
|
||||||
|
|
||||||
|
|
||||||
def blockierer(bedingungen, zusatz) -> list[str]:
|
|
||||||
"""Welche Bedingungen verhindern 'zusatz' - einzeln weggelassen.
|
|
||||||
|
|
||||||
Dasselbe Vorgehen wie der Deletion Filter in Konfliktsuche.py, nur mit
|
|
||||||
umgekehrtem Vorzeichen: Dort wird gesucht, was den Widerspruch AUSMACHT,
|
|
||||||
hier, was einen Wunsch VERHINDERT. Wird das Modell ohne eine Bedingung
|
|
||||||
loesbar, gehoert sie zum Konflikt.
|
|
||||||
"""
|
|
||||||
gefunden = []
|
|
||||||
for i, (name, _, _, _) in enumerate(bedingungen):
|
|
||||||
rest = [c for j, c in enumerate(bedingungen) if j != i] + [zusatz]
|
|
||||||
if loese(rest).success:
|
|
||||||
gefunden.append(name)
|
|
||||||
return gefunden
|
|
||||||
|
|
||||||
|
|
||||||
def euro(betrag: float) -> str:
|
|
||||||
return f"{betrag:,.2f} EUR"
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
basis = loese(BEDINGUNGEN)
|
|
||||||
db_basis = -basis.fun
|
|
||||||
plan = dict(zip(PRODUKTE, basis.x))
|
|
||||||
|
|
||||||
print("=" * 78)
|
|
||||||
print(" CONSTRAINT ATTRIBUTION: Was kostet uns welche Bedingung?")
|
|
||||||
print("=" * 78)
|
|
||||||
print(f"Status: {basis.message.split('(')[0].strip()}")
|
|
||||||
print(f"Deckungsbeitrag des Plans: {euro(db_basis)}\n")
|
|
||||||
print(" " + " ".join(f"{p}: {m:.1f}" for p, m in plan.items()))
|
|
||||||
|
|
||||||
# --- 1. Wer begrenzt den Plan? -----------------------------------------
|
|
||||||
print("\n" + "=" * 78)
|
|
||||||
print(" (1) Welche Bedingungen binden - und was kostet die naechste Einheit?")
|
|
||||||
print("=" * 78)
|
|
||||||
print(f"{'Bedingung':<24} {'genutzt':>9} {'Grenze':>7} {'Schlupf':>8} "
|
|
||||||
f"{'Schattenpreis':>11}")
|
|
||||||
print("-" * 78)
|
|
||||||
bindend = []
|
|
||||||
for name, links, richtung, grenze, schlupf, dual in auswertung(basis):
|
|
||||||
marke = ""
|
|
||||||
if abs(schlupf) < 1e-6:
|
|
||||||
bindend.append((name, dual))
|
|
||||||
marke = " <-- bindend"
|
|
||||||
print(f"{name:<24} {links:>9.1f} {grenze:>7.0f} {schlupf:>8.1f} "
|
|
||||||
f"{dual:>11.2f}{marke}")
|
|
||||||
|
|
||||||
ohne_preis = [n for n, d in bindend if d < 1e-9]
|
|
||||||
print(f"\n {len(bindend)} Bedingungen binden. Aber: "
|
|
||||||
f"{', '.join(ohne_preis)} bindet")
|
|
||||||
print(" mit einem Schattenpreis von 0,00 EUR - sie beruehrt den Plan, ohne ihn")
|
|
||||||
print(" zu verteuern. Bindend und kostenrelevant sind zwei verschiedene Dinge.")
|
|
||||||
|
|
||||||
# --- 2. Der Schattenpreis als Momentaufnahme ---------------------------
|
|
||||||
print("\n" + "=" * 78)
|
|
||||||
print(" (2) Was der Schattenpreis verschweigt")
|
|
||||||
print("=" * 78)
|
|
||||||
print("Der Schattenpreis gilt fuer die NAECHSTE Einheit. Ein Planer kauft aber")
|
|
||||||
print("keine Einheit, sondern eine ganze Schicht. Beides gegenuebergestellt:\n")
|
|
||||||
preise = {name: dual for name, *_, dual in auswertung(basis)}
|
|
||||||
print(f"{'Stellschraube':<24} {'Preis':>9} {'Schritt':>7} "
|
|
||||||
f"{'hochgerechnet':>13} {'gemessen':>9} {'Abw.':>9}")
|
|
||||||
print("-" * 78)
|
|
||||||
wirkung = {}
|
|
||||||
for name, (delta, _) in STELLSCHRAUBEN.items():
|
|
||||||
ergebnis = loese(geaendert(name, delta))
|
|
||||||
echt = (-ergebnis.fun) - db_basis if ergebnis.success else 0.0
|
|
||||||
linear = preise[name] * abs(delta)
|
|
||||||
wirkung[name] = echt
|
|
||||||
print(f"{name:<24} {preise[name]:>9.2f} {delta:>+7.0f} "
|
|
||||||
f"{linear:>13.2f} {echt:>9.2f} {echt - linear:>+9.2f}")
|
|
||||||
|
|
||||||
# Wo endet der Gueltigkeitsbereich? Schrittweise nachfahren.
|
|
||||||
abweichler = max(STELLSCHRAUBEN,
|
|
||||||
key=lambda n: preise[n] * abs(STELLSCHRAUBEN[n][0]) - wirkung[n])
|
|
||||||
delta_max = STELLSCHRAUBEN[abweichler][0]
|
|
||||||
grenze_gueltig = 0.0
|
|
||||||
schritt = delta_max / 60.0
|
|
||||||
vorher = db_basis
|
|
||||||
for i in range(1, 61):
|
|
||||||
d = schritt * i
|
|
||||||
e = loese(geaendert(abweichler, d))
|
|
||||||
db = -e.fun if e.success else vorher
|
|
||||||
if db - vorher > 1e-9:
|
|
||||||
grenze_gueltig = d
|
|
||||||
vorher = db
|
|
||||||
print(f"\n '{abweichler}': hochgerechnet "
|
|
||||||
f"{preise[abweichler] * abs(delta_max):.2f} EUR, tatsaechlich "
|
|
||||||
f"{wirkung[abweichler]:.2f} EUR.")
|
|
||||||
print(f" Der Preis von {preise[abweichler]:.2f} EUR gilt nur bis "
|
|
||||||
f"+{grenze_gueltig:.0f} Stunden. Jede weitere Stunde")
|
|
||||||
print(" dieser Sonderschicht bringt exakt nichts - der Engpass ist dann ein anderer.")
|
|
||||||
|
|
||||||
nach_preis = sorted(STELLSCHRAUBEN, key=lambda n: -preise[n])[0]
|
|
||||||
nach_wirkung = max(wirkung, key=wirkung.get)
|
|
||||||
print(f"\n Und die Ranglisten drehen sich: Nach Schattenpreis fuehrt")
|
|
||||||
print(f" '{nach_preis}' ({preise[nach_preis]:.2f} EUR je Einheit),")
|
|
||||||
print(f" nach tatsaechlicher Wirkung '{nach_wirkung}' "
|
|
||||||
f"({wirkung[nach_wirkung]:.2f} EUR).")
|
|
||||||
print(" Der Schattenpreis sagt, was eine Einheit wert ist - nicht, wie viele")
|
|
||||||
print(" Einheiten zu haben sind.")
|
|
||||||
|
|
||||||
# --- 3. Der Wunsch aus dem Vertrieb ------------------------------------
|
|
||||||
print("\n" + "=" * 78)
|
|
||||||
print(f" (3) Was-waere-wenn: mindestens {WUNSCH_MENGE:.0f} "
|
|
||||||
f"{WUNSCH_PRODUKT} statt {plan[WUNSCH_PRODUKT]:.1f}")
|
|
||||||
print("=" * 78)
|
|
||||||
wunsch = (f"Wunsch Vertrieb {WUNSCH_PRODUKT}",
|
|
||||||
[1 if p == WUNSCH_PRODUKT else 0 for p in PRODUKTE], ">=", WUNSCH_MENGE)
|
|
||||||
mit_wunsch = loese(BEDINGUNGEN + [wunsch])
|
|
||||||
schuldige: list[str] = []
|
|
||||||
engpass = noetig = None
|
|
||||||
wunsch_gratis = False
|
|
||||||
|
|
||||||
if mit_wunsch.success:
|
|
||||||
print(f"Erfuellbar, kostet {euro(db_basis - (-mit_wunsch.fun))}.")
|
|
||||||
else:
|
|
||||||
schuldige = blockierer(BEDINGUNGEN, wunsch)
|
|
||||||
print("Der Wunsch ist nicht teuer - er ist UNMOEGLICH.\n")
|
|
||||||
print(f"Blockiert wird er von {len(schuldige)} Bedingungen gemeinsam. "
|
|
||||||
f"Faellt eine davon")
|
|
||||||
print("weg, ist er erfuellbar; keine allein ist 'der' Grund:")
|
|
||||||
for name in schuldige:
|
|
||||||
e = loese([c for c in BEDINGUNGEN if c[0] != name] + [wunsch])
|
|
||||||
print(f" ohne '{name}': moeglich, Deckungsbeitrag {euro(-e.fun)}")
|
|
||||||
|
|
||||||
engpass = next((n for n in schuldige if n in STELLSCHRAUBEN), schuldige[0])
|
|
||||||
for schritt in range(1, 61):
|
|
||||||
if loese(geaendert(engpass, schritt) + [wunsch]).success:
|
|
||||||
noetig = schritt
|
|
||||||
break
|
|
||||||
if noetig is not None:
|
|
||||||
e = loese(geaendert(engpass, noetig) + [wunsch])
|
|
||||||
ohne = loese(geaendert(engpass, noetig))
|
|
||||||
wunsch_gratis = abs((-e.fun) - (-ohne.fun)) < 1e-6
|
|
||||||
print(f"\n Mit +{noetig} bei '{engpass}' wird der Wunsch erfuellbar:")
|
|
||||||
print(f" Deckungsbeitrag {euro(-e.fun)} "
|
|
||||||
f"({-e.fun - db_basis:+,.2f} gegenueber heute).")
|
|
||||||
if wunsch_gratis:
|
|
||||||
print(f" Und ohne den Wunsch ergaebe dieselbe Lockerung "
|
|
||||||
f"{euro(-ohne.fun)} bei")
|
|
||||||
print(f" {ohne.x[PRODUKTE.index(WUNSCH_PRODUKT)]:.0f} "
|
|
||||||
f"{WUNSCH_PRODUKT} - der Wunsch kostet also NICHTS.")
|
|
||||||
print(" Er war nie das Problem. Der Engpass war es.")
|
|
||||||
|
|
||||||
# --- 4. Der Bericht ----------------------------------------------------
|
|
||||||
# Aus denselben Zahlen, die oben in Tabellen stehen, wird hier Prosa. Kein
|
|
||||||
# Satz enthaelt eine Zahl, die nicht vorher gerechnet wurde - das ist der
|
|
||||||
# ganze Trick und zugleich die Bedingung dafuer, dass der Bericht nicht
|
|
||||||
# irgendwann leise falsch wird.
|
|
||||||
print("\n" + "=" * 78)
|
|
||||||
print(" (4) Und so steht es in der Vorlage fuer die Abteilungsleiterrunde")
|
|
||||||
print("=" * 78)
|
|
||||||
|
|
||||||
teuerste = sorted(((n, d) for n, d in bindend if d > 0), key=lambda t: -t[1])
|
|
||||||
gross = STELLSCHRAUBEN[nach_wirkung][1]
|
|
||||||
klein = STELLSCHRAUBEN[abweichler][1]
|
|
||||||
|
|
||||||
absaetze = [
|
|
||||||
f"Der Plan bringt {euro(db_basis)} Deckungsbeitrag. Begrenzt wird er von "
|
|
||||||
f"{len(bindend)} Bedingungen, von denen {len(teuerste)} tatsaechlich Geld "
|
|
||||||
f"kosten; \"{ohne_preis[0]}\" beruehrt den Plan, ohne ihn zu verteuern.",
|
|
||||||
|
|
||||||
f"Teuerste Bindung ist \"{teuerste[0][0]}\" mit "
|
|
||||||
f"{teuerste[0][1]:.2f} EUR je Einheit, gefolgt von \"{teuerste[1][0]}\" "
|
|
||||||
f"mit {teuerste[1][1]:.2f} EUR.",
|
|
||||||
|
|
||||||
f"Die groesste einzelne Verbesserung bringt {gross}: "
|
|
||||||
f"{euro(wirkung[nach_wirkung])} mehr Deckungsbeitrag. {klein[0].upper()}{klein[1:]} "
|
|
||||||
f"dagegen bringt nur {euro(wirkung[abweichler])} statt der rechnerischen "
|
|
||||||
f"{euro(preise[abweichler] * abs(delta_max))} - ab +{grenze_gueltig:.0f} "
|
|
||||||
f"Stunden begrenzt uns etwas anderes. Wer die ganze Schicht bezahlt, "
|
|
||||||
f"bezahlt die zweite Haelfte umsonst.",
|
|
||||||
]
|
|
||||||
|
|
||||||
if schuldige and noetig is not None:
|
|
||||||
namen = ", ".join(f'"{s}"' for s in schuldige)
|
|
||||||
satz = (f"Zum Wunsch des Vertriebs ({WUNSCH_MENGE:.0f} {WUNSCH_PRODUKT} statt "
|
|
||||||
f"{plan[WUNSCH_PRODUKT]:.1f}): Er ist mit der heutigen Ausstattung "
|
|
||||||
f"nicht erfuellbar - nicht aus Kostengruenden, sondern weil {namen} "
|
|
||||||
f"ihn gemeinsam blockieren. Mit +{noetig} Stunden bei "
|
|
||||||
f'"{engpass}" wird er moeglich')
|
|
||||||
satz += (", und er kostet dann nichts: Der Plan waehlt diese Menge von "
|
|
||||||
"sich aus." if wunsch_gratis else ".")
|
|
||||||
absaetze.append(satz)
|
|
||||||
|
|
||||||
for absatz in absaetze:
|
|
||||||
print()
|
|
||||||
print(textwrap.fill(absatz, width=76))
|
|
||||||
print("\n" + "=" * 78)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ausgabe:**
|
|
||||||
|
|
||||||
```
|
|
||||||
==============================================================================
|
|
||||||
CONSTRAINT ATTRIBUTION: Was kostet uns welche Bedingung?
|
|
||||||
==============================================================================
|
|
||||||
Status: Optimization terminated successfully.
|
|
||||||
Deckungsbeitrag des Plans: 3,930.00 EUR
|
|
||||||
|
|
||||||
Rahmen: 47.5 Gehaeuse: 30.0 Deckel: 97.5 Traeger: 25.0 Halter: 0.0
|
|
||||||
|
|
||||||
==============================================================================
|
|
||||||
(1) Welche Bedingungen binden - und was kostet die naechste Einheit?
|
|
||||||
==============================================================================
|
|
||||||
Bedingung genutzt Grenze Schlupf Schattenpreis
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
Kapazitaet Montage 400.0 400 0.0 9.00 <-- bindend
|
|
||||||
Kapazitaet Lackieren 210.0 210 0.0 4.00 <-- bindend
|
|
||||||
Kapazitaet Pruefung 200.0 300 100.0 0.00
|
|
||||||
Liefervertrag Rahmen 47.5 40 7.5 0.00
|
|
||||||
Liefervertrag Gehaeuse 30.0 30 0.0 2.00 <-- bindend
|
|
||||||
Liefervertrag Deckel 97.5 20 77.5 0.00
|
|
||||||
Liefervertrag Traeger 25.0 25 0.0 18.00 <-- bindend
|
|
||||||
Marktgrenze Rahmen 47.5 120 72.5 0.00
|
|
||||||
Marktgrenze Gehaeuse 30.0 90 60.0 0.00
|
|
||||||
Marktgrenze Deckel 97.5 150 52.5 0.00
|
|
||||||
Marktgrenze Halter 0.0 200 200.0 0.00
|
|
||||||
Mindestumsatz 10800.0 10000 800.0 0.00
|
|
||||||
Sortimentsbreite 122.5 100 22.5 0.00
|
|
||||||
Lackierbudget Schicht 2 55.0 55 0.0 0.00 <-- bindend
|
|
||||||
|
|
||||||
5 Bedingungen binden. Aber: Lackierbudget Schicht 2 bindet
|
|
||||||
mit einem Schattenpreis von 0,00 EUR - sie beruehrt den Plan, ohne ihn
|
|
||||||
zu verteuern. Bindend und kostenrelevant sind zwei verschiedene Dinge.
|
|
||||||
|
|
||||||
==============================================================================
|
|
||||||
(2) Was der Schattenpreis verschweigt
|
|
||||||
==============================================================================
|
|
||||||
Der Schattenpreis gilt fuer die NAECHSTE Einheit. Ein Planer kauft aber
|
|
||||||
keine Einheit, sondern eine ganze Schicht. Beides gegenuebergestellt:
|
|
||||||
|
|
||||||
Stellschraube Preis Schritt hochgerechnet gemessen Abw.
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
Kapazitaet Montage 9.00 +50 450.00 450.00 +0.00
|
|
||||||
Kapazitaet Lackieren 4.00 +30 120.00 60.00 -60.00
|
|
||||||
Kapazitaet Pruefung 0.00 +50 0.00 0.00 +0.00
|
|
||||||
Lackierbudget Schicht 2 0.00 +10 0.00 0.00 +0.00
|
|
||||||
Liefervertrag Traeger 18.00 -10 180.00 180.00 +0.00
|
|
||||||
Liefervertrag Gehaeuse 2.00 -10 20.00 18.00 -2.00
|
|
||||||
|
|
||||||
'Kapazitaet Lackieren': hochgerechnet 120.00 EUR, tatsaechlich 60.00 EUR.
|
|
||||||
Der Preis von 4.00 EUR gilt nur bis +15 Stunden. Jede weitere Stunde
|
|
||||||
dieser Sonderschicht bringt exakt nichts - der Engpass ist dann ein anderer.
|
|
||||||
|
|
||||||
Und die Ranglisten drehen sich: Nach Schattenpreis fuehrt
|
|
||||||
'Liefervertrag Traeger' (18.00 EUR je Einheit),
|
|
||||||
nach tatsaechlicher Wirkung 'Kapazitaet Montage' (450.00 EUR).
|
|
||||||
Der Schattenpreis sagt, was eine Einheit wert ist - nicht, wie viele
|
|
||||||
Einheiten zu haben sind.
|
|
||||||
|
|
||||||
==============================================================================
|
|
||||||
(3) Was-waere-wenn: mindestens 55 Rahmen statt 47.5
|
|
||||||
==============================================================================
|
|
||||||
Der Wunsch ist nicht teuer - er ist UNMOEGLICH.
|
|
||||||
|
|
||||||
Blockiert wird er von 3 Bedingungen gemeinsam. Faellt eine davon
|
|
||||||
weg, ist er erfuellbar; keine allein ist 'der' Grund:
|
|
||||||
ohne 'Kapazitaet Lackieren': moeglich, Deckungsbeitrag 3,990.00 EUR
|
|
||||||
ohne 'Liefervertrag Gehaeuse': moeglich, Deckungsbeitrag 3,948.00 EUR
|
|
||||||
ohne 'Liefervertrag Traeger': moeglich, Deckungsbeitrag 4,320.00 EUR
|
|
||||||
|
|
||||||
Mit +15 bei 'Kapazitaet Lackieren' wird der Wunsch erfuellbar:
|
|
||||||
Deckungsbeitrag 3,990.00 EUR (+60.00 gegenueber heute).
|
|
||||||
Und ohne den Wunsch ergaebe dieselbe Lockerung 3,990.00 EUR bei
|
|
||||||
55 Rahmen - der Wunsch kostet also NICHTS.
|
|
||||||
Er war nie das Problem. Der Engpass war es.
|
|
||||||
|
|
||||||
==============================================================================
|
|
||||||
(4) Und so steht es in der Vorlage fuer die Abteilungsleiterrunde
|
|
||||||
==============================================================================
|
|
||||||
|
|
||||||
Der Plan bringt 3,930.00 EUR Deckungsbeitrag. Begrenzt wird er von 5
|
|
||||||
Bedingungen, von denen 4 tatsaechlich Geld kosten; "Lackierbudget Schicht 2"
|
|
||||||
beruehrt den Plan, ohne ihn zu verteuern.
|
|
||||||
|
|
||||||
Teuerste Bindung ist "Liefervertrag Traeger" mit 18.00 EUR je Einheit,
|
|
||||||
gefolgt von "Kapazitaet Montage" mit 9.00 EUR.
|
|
||||||
|
|
||||||
Die groesste einzelne Verbesserung bringt eine Sonderschicht Montage: 450.00
|
|
||||||
EUR mehr Deckungsbeitrag. Eine Sonderschicht Lackieren dagegen bringt nur
|
|
||||||
60.00 EUR statt der rechnerischen 120.00 EUR - ab +15 Stunden begrenzt uns
|
|
||||||
etwas anderes. Wer die ganze Schicht bezahlt, bezahlt die zweite Haelfte
|
|
||||||
umsonst.
|
|
||||||
|
|
||||||
Zum Wunsch des Vertriebs (55 Rahmen statt 47.5): Er ist mit der heutigen
|
|
||||||
Ausstattung nicht erfuellbar - nicht aus Kostengruenden, sondern weil
|
|
||||||
"Kapazitaet Lackieren", "Liefervertrag Gehaeuse", "Liefervertrag Traeger"
|
|
||||||
ihn gemeinsam blockieren. Mit +15 Stunden bei "Kapazitaet Lackieren" wird er
|
|
||||||
moeglich, und er kostet dann nichts: Der Plan waehlt diese Menge von sich
|
|
||||||
aus.
|
|
||||||
|
|
||||||
==============================================================================
|
|
||||||
```
|
|
||||||
|
|
||||||
### Drei Befunde, und keiner steht im Solverergebnis
|
|
||||||
|
|
||||||
**Erstens: bindend heißt nicht teuer.** Fünf Bedingungen binden, aber „Lackierbudget
|
|
||||||
Schicht 2“ tut es mit einem Schattenpreis von **0,00 €**. Sie berührt den Plan, ohne ihn zu
|
|
||||||
verteuern — wer sie nachverhandelt, gewinnt nichts. Das ist derselbe Unterschied, den
|
|
||||||
`Erklaerbarkeit.py` oben an einer einzelnen Zuweisung zeigt, hier im Dualwert eines LP.
|
|
||||||
|
|
||||||
**Zweitens: Der Schattenpreis ist eine Momentaufnahme.** Er gilt für die **nächste**
|
|
||||||
Einheit. Ein Planer kauft aber keine Einheit, sondern eine Schicht. Für die Lackiererei
|
|
||||||
verspricht die Hochrechnung $4{,}00 \cdot 30 = 120$ €; gemessen kommen **60 €**. Der Preis
|
|
||||||
von 4,00 € gilt exakt bis **+15 Stunden**, danach begrenzt etwas anderes — **die zweite
|
|
||||||
Hälfte der Sonderschicht wäre bezahlt und wirkungslos.**
|
|
||||||
|
|
||||||
Und die Ranglisten drehen sich dabei um: Nach Schattenpreis führt der Trägervertrag mit
|
|
||||||
18,00 € je Stück, nach tatsächlicher Wirkung die Montage-Sonderschicht mit 450 €. Der
|
|
||||||
Schattenpreis sagt, was **eine** Einheit wert ist — nicht, wie viele Einheiten zu haben
|
|
||||||
sind. Für die Frage „welchen Hebel ziehen wir?“ ist das der entscheidende Unterschied.
|
|
||||||
|
|
||||||
**Drittens: Ein Wunsch kann unmöglich statt teuer sein.** Der Vertrieb hätte gern
|
|
||||||
55 Rahmen statt 47,5. Das Modell antwortet nicht mit einem Preis, sondern mit
|
|
||||||
`INFEASIBLE` — und drei Bedingungen blockieren den Wunsch **gemeinsam**: Fällt eine davon
|
|
||||||
weg, ist er erfüllbar, keine allein ist „der“ Grund. Genau die Struktur, die
|
|
||||||
{ref:anhang:fehlerdiagnose} für den unlösbaren Fall beschreibt, hier auf einen Wunsch
|
|
||||||
angewandt.
|
|
||||||
|
|
||||||
Die Pointe steht am Ende: Mit +15 Stunden Lackierkapazität wird der Wunsch erfüllbar — und
|
|
||||||
kostet dann **nichts**. Der Plan wählt diese Menge von sich aus, sobald der Engpass weg ist.
|
|
||||||
Der Konflikt bestand nie zwischen Vertrieb und Produktion. Er bestand zwischen dem Vertrieb
|
|
||||||
und der Lackiererei, und niemand wusste das.
|
|
||||||
|
|
||||||
> **🎯 Die drei Fragen, jetzt auf Modellebene**
|
|
||||||
> Die Faustregel oben stellt sie für eine einzelne Zuweisung. Constraint Attribution stellt
|
|
||||||
> dieselben drei für den **ganzen Plan**:
|
|
||||||
> 1. **Warum dieser Zielwert?** — welche Bedingungen binden, und welche davon kosten Geld.
|
|
||||||
> 2. **Warum nicht besser?** — was eine realistische Lockerung wirklich bringt, gemessen
|
|
||||||
> statt hochgerechnet.
|
|
||||||
> 3. **Was ist mit meinem Wunsch?** — erfüllbar und zu welchem Preis, oder unmöglich und
|
|
||||||
> warum.
|
|
||||||
|
|
||||||
### Vom Zahlenwerk zur Vorlage
|
|
||||||
|
|
||||||
Alle drei Antworten stehen bis hierhin in Tabellen. Tabellen liest niemand in einer
|
|
||||||
Abteilungsleiterrunde. Teil (4) des Programms baut aus **denselben** Zahlen einen Text —
|
|
||||||
und das ist weniger trivial, als es aussieht:
|
|
||||||
|
|
||||||
* **Kein Satz enthält eine Zahl, die nicht vorher gerechnet wurde.** Auch nicht die
|
|
||||||
Rangfolge, auch nicht die Namen der blockierenden Bedingungen. Sobald sich das Modell
|
|
||||||
ändert, ändert sich der Bericht mit — er kann nicht leise falsch werden.
|
|
||||||
* **Der sprechende Name jeder Bedingung ist die Voraussetzung dafür.** `A_ub[7]` ergibt
|
|
||||||
keinen Satz. Deshalb tragen die Bedingungen in beiden Programmen einen Namen, und deshalb
|
|
||||||
ist das keine Kosmetik.
|
|
||||||
|
|
||||||
> **⚠️ Was ein solcher Bericht nicht leistet**
|
|
||||||
> Er erklärt das **Modell**, nicht die **Wirklichkeit**. „Liefervertrag Träger kostet 18 €
|
|
||||||
> je Stück“ heißt: *in diesem Modell, mit diesen Deckungsbeiträgen*. Ist ein
|
|
||||||
> Deckungsbeitrag falsch geschätzt, ist der Bericht überzeugend **und** falsch — die
|
|
||||||
> gefährlichste Kombination. Ein generierter Text macht ein Modell nicht richtiger, nur
|
|
||||||
> schwerer anzuzweifeln. Deshalb gehört zu jedem solchen Bericht die Angabe, worauf er
|
|
||||||
> beruht.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architektur einer produktionsreifen OR-Plattform {#sec:praxisfallen-architektur-einer-produktionsreifen-or-plattform}
|
## Architektur einer produktionsreifen OR-Plattform {#sec:praxisfallen-architektur-einer-produktionsreifen-or-plattform}
|
||||||
|
|
||||||

|

|
||||||
|
|
@ -1666,9 +1165,9 @@ Benoetigt: numpy, pydantic, ortools, highspy (jeweils im eigenen Prozess)
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import multiprocessing
|
import subprocess
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import BaseModel, Field, model_validator
|
from pydantic import BaseModel, Field, model_validator
|
||||||
|
|
@ -1871,29 +1370,25 @@ def geoeffnete_lager(problem: Standortproblem, loesung: Loesung) -> list[str]:
|
||||||
if any(loesung.werte[problem.schluessel(i, j)] > 0.5 for j in range(m))]
|
if any(loesung.werte[problem.schluessel(i, j)] > 0.5 for j in range(m))]
|
||||||
|
|
||||||
|
|
||||||
def loese_in_eigenem_prozess(name: str, problem: Standortproblem) -> Loesung:
|
def loese_in_eigenem_prozess(name: str) -> Loesung:
|
||||||
"""Laesst genau einen Modellbauer in einem frischen Prozess rechnen.
|
"""Startet dieses Programm noch einmal - mit genau einem Solverimport."""
|
||||||
|
ergebnis = subprocess.run([sys.executable, __file__, name],
|
||||||
'spawn' statt des Linux-Standards 'fork': Der Kindprozess startet mit
|
capture_output=True, text=True, timeout=300)
|
||||||
einem leeren Interpreter und importiert nur den Solver, den SEIN
|
if ergebnis.returncode != 0:
|
||||||
Modellbauer braucht. max_tasks_per_child=1 sorgt dafuer, dass der Pool
|
raise RuntimeError(ergebnis.stderr.strip().splitlines()[-1])
|
||||||
seinen Arbeiter nicht wiederverwendet - sonst saessen beim zweiten Aufruf
|
# Das DTO als JSON - genau dafuer ist ein Datenobjekt ohne Solverbezug gut.
|
||||||
wieder beide Bibliotheken im selben Prozess.
|
return Loesung.model_validate_json(ergebnis.stdout.strip().splitlines()[-1])
|
||||||
|
|
||||||
Hin und zurueck wandert das Domaenenmodell bzw. das Loesungs-DTO. Beide
|
|
||||||
kennen keinen Solver, sind also serialisierbar - genau dafuer sind sie da.
|
|
||||||
"""
|
|
||||||
with ProcessPoolExecutor(
|
|
||||||
max_workers=1,
|
|
||||||
mp_context=multiprocessing.get_context("spawn"),
|
|
||||||
max_tasks_per_child=1) as pool:
|
|
||||||
return pool.submit(MODELLBAUER[name], problem).result(timeout=300)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
problem = beispielproblem()
|
problem = beispielproblem()
|
||||||
|
|
||||||
# --- Beide Solver anstossen und vergleichen ---------------------------
|
# --- Kindprozess: rechnen und das DTO als JSON ausgeben ---------------
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
print(MODELLBAUER[sys.argv[1]](problem).model_dump_json())
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# --- Hauptprozess: beide Solver anstossen und vergleichen -------------
|
||||||
print("=" * 82)
|
print("=" * 82)
|
||||||
print(" DERSELBE FALL, ZWEI SOLVER - UND EIN AUSWERTUNGSCODE")
|
print(" DERSELBE FALL, ZWEI SOLVER - UND EIN AUSWERTUNGSCODE")
|
||||||
print("=" * 82)
|
print("=" * 82)
|
||||||
|
|
@ -1905,7 +1400,7 @@ if __name__ == "__main__":
|
||||||
loesungen: dict[str, Loesung] = {}
|
loesungen: dict[str, Loesung] = {}
|
||||||
for name, beschriftung in [("cpsat", "OR-Tools CP-SAT"),
|
for name, beschriftung in [("cpsat", "OR-Tools CP-SAT"),
|
||||||
("highs", "HiGHS (highspy)")]:
|
("highs", "HiGHS (highspy)")]:
|
||||||
loesung = loesungen[name] = loese_in_eigenem_prozess(name, problem)
|
loesung = loesungen[name] = loese_in_eigenem_prozess(name)
|
||||||
beanstandungen = pruefe_zuordnung(problem, loesung)
|
beanstandungen = pruefe_zuordnung(problem, loesung)
|
||||||
|
|
||||||
print(f"{beschriftung}")
|
print(f"{beschriftung}")
|
||||||
|
|
@ -2112,8 +1607,7 @@ Echtzeit, (b) Wochendienstplan, freitags erstellt, (c) Jahresproduktionsplanung,
|
||||||
(d) Portfolio-Rebalancing monatlich.
|
(d) Portfolio-Rebalancing monatlich.
|
||||||
|
|
||||||
**Aufgabe ⭐⭐ — Relaxation einbauen.**
|
**Aufgabe ⭐⭐ — Relaxation einbauen.**
|
||||||
Nehmen Sie Ihr Modell aus der Aufgabe *Eigener Dienstplan*
|
Nehmen Sie Ihr Modell aus Übung 6.7 (Wochendienstplan) und machen Sie es
|
||||||
({ref:sec:cpsat-uebungsaufgaben}) und machen Sie es
|
|
||||||
`INFEASIBLE`-sicher: Schlupfvariablen für unbesetzte Schichten, gestaffelte Strafen. Testen
|
`INFEASIBLE`-sicher: Schlupfvariablen für unbesetzte Schichten, gestaffelte Strafen. Testen
|
||||||
Sie mit einem bewusst überlasteten Szenario.
|
Sie mit einem bewusst überlasteten Szenario.
|
||||||
|
|
||||||
|
|
@ -2121,17 +1615,6 @@ Sie mit einem bewusst überlasteten Szenario.
|
||||||
Erweitern Sie ein beliebiges Modell aus dem Kurs um einen `erklaere(loesung)`-Report, der
|
Erweitern Sie ein beliebiges Modell aus dem Kurs um einen `erklaere(loesung)`-Report, der
|
||||||
die drei Fragen aus {ref:sec:praxisfallen-die-fuenf-typischen-praxisfallen} beantwortet.
|
die drei Fragen aus {ref:sec:praxisfallen-die-fuenf-typischen-praxisfallen} beantwortet.
|
||||||
|
|
||||||
**Aufgabe ⭐⭐ — Den Bericht in die Irre führen.**
|
|
||||||
`Constraint_Attribution.py` erzeugt seinen Bericht aus gerechneten Zahlen — richtig ist er
|
|
||||||
deshalb noch nicht.
|
|
||||||
(a) Setzen Sie den Deckungsbeitrag von *Deckel* von 9 € auf 20 €. Wie ändern sich Plan,
|
|
||||||
Rangliste und Bericht? Welche Sätze bleiben wörtlich stehen, obwohl sie jetzt etwas
|
|
||||||
anderes bedeuten?
|
|
||||||
(b) Der Bericht nennt „Liefervertrag Träger" als teuerste Bindung. Formulieren Sie den
|
|
||||||
Satz so um, dass ein Leser erkennt, **worauf** die Aussage beruht.
|
|
||||||
(c) Welche Angabe müsste das Programm zusätzlich ausgeben, damit ein Empfänger die
|
|
||||||
Belastbarkeit selbst einschätzen kann?
|
|
||||||
|
|
||||||
**Aufgabe ⭐⭐⭐ — Gap gegen Laufzeit.**
|
**Aufgabe ⭐⭐⭐ — Gap gegen Laufzeit.**
|
||||||
Nehmen Sie das MILP-Portfolio aus {ref:kap:milp} und vergrößern Sie es auf 50 Anlagen. Messen
|
Nehmen Sie das MILP-Portfolio aus {ref:kap:milp} und vergrößern Sie es auf 50 Anlagen. Messen
|
||||||
Sie Laufzeit und Zielwert bei Gap-Vorgaben von 10 %, 5 %, 2 %, 1 %, 0,1 % und 0 %.
|
Sie Laufzeit und Zielwert bei Gap-Vorgaben von 10 %, 5 %, 2 %, 1 %, 0,1 % und 0 %.
|
||||||
|
|
@ -2154,7 +1637,7 @@ eigene Projekte ab?
|
||||||
Falle 5 — die Laufzeitexplosion — zeigt sich im Betrieb selten als Absturz. Sie zeigt
|
Falle 5 — die Laufzeitexplosion — zeigt sich im Betrieb selten als Absturz. Sie zeigt
|
||||||
sich als **gar nichts**.
|
sich als **gar nichts**.
|
||||||
|
|
||||||
> **🐛 Finde den Denkfehler: Das Modell, das seit einem Jahr nicht mehr optimiert**
|
> **🐛 Finde den Denkfehler 15.1: Das Modell, das seit einem Jahr nicht mehr optimiert**
|
||||||
>
|
>
|
||||||
> Ein Standortplanungsmodell geht 2024 in Betrieb. Es läuft jede Nacht, hat drei Sekunden
|
> Ein Standortplanungsmodell geht 2024 in Betrieb. Es läuft jede Nacht, hat drei Sekunden
|
||||||
> Zeitbudget und liefert zuverlässig die Kosten des Tagesplans. Das Betriebsteam
|
> Zeitbudget und liefert zuverlässig die Kosten des Tagesplans. Das Betriebsteam
|
||||||
|
|
@ -2441,7 +1924,7 @@ sichtbar wird, und gibt Zeit zum Handeln.
|
||||||
|
|
||||||
## Micro-Quiz {#sec:praxisfallen-quiz}
|
## Micro-Quiz {#sec:praxisfallen-quiz}
|
||||||
|
|
||||||
> **❓ Micro-Quiz: Drei Fragen zum Selbstcheck**
|
> **❓ Micro-Quiz 15: Drei Fragen zum Selbstcheck**
|
||||||
>
|
>
|
||||||
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
> Genau eine Antwort ist jeweils richtig. Auflösung in
|
||||||
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-praxisfallen).
|
> [Anhang A](90_Anhang_Loesungen.md#quiz-loesung-praxisfallen).
|
||||||
|
|
@ -2479,10 +1962,6 @@ sichtbar wird, und gibt Zeit zum Handeln.
|
||||||
3. Warum ist ein MIP-Gap von 2 % in der Praxis meist ausreichend?
|
3. Warum ist ein MIP-Gap von 2 % in der Praxis meist ausreichend?
|
||||||
4. Was ist das Snapshot-Prinzip und wozu dient es?
|
4. Was ist das Snapshot-Prinzip und wozu dient es?
|
||||||
5. Was ist der häufigste Grund, aus dem OR-Projekte scheitern?
|
5. Was ist der häufigste Grund, aus dem OR-Projekte scheitern?
|
||||||
6. Eine Bedingung bindet, ihr Schattenpreis ist 0. Was heißt das — und was folgt daraus für
|
|
||||||
eine Nachverhandlung?
|
|
||||||
7. Warum kann die Rangfolge nach Schattenpreis eine andere sein als die Rangfolge nach
|
|
||||||
tatsächlichem Nutzen einer Lockerung?
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -2492,16 +1971,6 @@ sichtbar wird, und gibt Zeit zum Handeln.
|
||||||
erhält irgendwann eine Fehlermeldung statt eines Plans.
|
erhält irgendwann eine Fehlermeldung statt eines Plans.
|
||||||
* **Erklärbarkeit entscheidet über Akzeptanz.** Schattenpreise, Kostenzerlegung und
|
* **Erklärbarkeit entscheidet über Akzeptanz.** Schattenpreise, Kostenzerlegung und
|
||||||
Constraint-Traces sind keine Kür.
|
Constraint-Traces sind keine Kür.
|
||||||
* **Bindend heißt nicht teuer.** Eine Bedingung kann den Plan berühren und trotzdem
|
|
||||||
0,00 € kosten. Nur die kostenrelevanten lohnen eine Nachverhandlung.
|
|
||||||
* **Der Schattenpreis gilt für die nächste Einheit, nicht für die nächste Schicht.**
|
|
||||||
Hochgerechnet auf einen realistischen Schritt überschätzt er den Nutzen — im Beispiel um
|
|
||||||
das Doppelte. Wer wissen will, was ein Hebel bringt, **misst** die Lockerung, statt zu
|
|
||||||
multiplizieren; die beiden Ranglisten können sich dabei umdrehen.
|
|
||||||
* **Ein Wunsch kann unmöglich statt teuer sein** — und dann ist die richtige Antwort nicht
|
|
||||||
ein Preis, sondern die Liste der Bedingungen, die ihn gemeinsam blockieren.
|
|
||||||
* **Ein generierter Bericht macht ein Modell nicht richtiger, nur schwerer anzuzweifeln.**
|
|
||||||
Er erklärt das Modell, nicht die Wirklichkeit — deshalb gehört dazu, worauf er beruht.
|
|
||||||
* **Optimieren Sie nicht genauer als Ihre Daten.** Ein Gap von 2 % ist bei ±10 %
|
* **Optimieren Sie nicht genauer als Ihre Daten.** Ein Gap von 2 % ist bei ±10 %
|
||||||
Datenunsicherheit irrelevant.
|
Datenunsicherheit irrelevant.
|
||||||
* **Der Optimierer schlägt vor, der Mensch entscheidet** — jedenfalls anfangs.
|
* **Der Optimierer schlägt vor, der Mensch entscheidet** — jedenfalls anfangs.
|
||||||
|
|
@ -2520,4 +1989,4 @@ sichtbar wird, und gibt Zeit zum Handeln.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Weiter mit:* [{ref:kap:testing} — Testen, Messen, Ausliefern](51_Testing_und_Deployment.md)
|
*Weiter mit:* [Projektwerkstatt — acht eigene Anwendungen](59_Projektwerkstatt.md)
|
||||||
|
|
|
||||||
|
|
@ -787,124 +787,113 @@ Benoetigt: numpy; in den Kindprozessen scipy, highspy, ortools, cvxpy
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import multiprocessing
|
import json
|
||||||
import resource
|
import subprocess
|
||||||
import time
|
import sys
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
import textwrap
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
GROESSEN = [(10, 10), (32, 32), (100, 100)] # (Lager, Kunden) -> 100 / 1.024 / 10.000 Variablen
|
GROESSEN = [(10, 10), (32, 32), (100, 100)] # (Lager, Kunden) -> 100 / 1.024 / 10.000 Variablen
|
||||||
|
|
||||||
|
|
||||||
# Instanz und Speichermessung stehen als gewoehnliche Funktionen hier - nicht
|
# Jeder Eintrag ist ein eigenstaendiges Programm: Instanz aufbauen, loesen,
|
||||||
# in einem String, den ein Kindprozess ausfuehrt. Jede Messfunktion baut die
|
# Ergebnis als JSON ausgeben. Die Instanz wird in jedem Kindprozess aus
|
||||||
# Instanz aus derselben Saat neu auf, damit ueber die Prozessgrenze nichts
|
# derselben Saat neu erzeugt - so reist nichts ueber die Prozessgrenze,
|
||||||
# reist, was das Ergebnis verfaelschen koennte.
|
# was das Ergebnis verfaelschen koennte.
|
||||||
|
VORSPANN = """
|
||||||
|
import json, time, resource
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
def instanz(m: int, n: int):
|
def instanz(m, n):
|
||||||
rng = np.random.default_rng(20)
|
rng = np.random.default_rng(20)
|
||||||
kosten = rng.integers(5, 95, (m, n)).astype(float)
|
kosten = rng.integers(5, 95, (m, n)).astype(float)
|
||||||
angebot = rng.integers(50, 150, m).astype(float)
|
angebot = rng.integers(50, 150, m).astype(float)
|
||||||
bedarf = angebot.sum() * rng.dirichlet(np.ones(n))
|
bedarf = angebot.sum() * rng.dirichlet(np.ones(n))
|
||||||
return kosten, angebot, bedarf
|
return kosten, angebot, bedarf
|
||||||
|
|
||||||
|
def speicher_mb():
|
||||||
def speicher_mb() -> float:
|
# ru_maxrss ist unter Linux in Kilobyte
|
||||||
# ru_maxrss ist unter Linux in Kilobyte. Gemessen wird der Kindprozess -
|
|
||||||
# deshalb muss jede Messung einen eigenen bekommen.
|
|
||||||
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
|
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
|
||||||
|
|
||||||
|
M, N = {m}, {n}
|
||||||
|
kosten, angebot, bedarf = instanz(M, N)
|
||||||
|
"""
|
||||||
|
|
||||||
def messe_scipy(m: int, n: int):
|
ANSAETZE = {
|
||||||
|
"scipy.linprog": """
|
||||||
from scipy.optimize import linprog
|
from scipy.optimize import linprog
|
||||||
kosten, angebot, bedarf = instanz(m, n)
|
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
c = kosten.reshape(-1)
|
c = kosten.reshape(-1)
|
||||||
A_ub = np.zeros((m, m * n)); A_eq = np.zeros((n, m * n))
|
A_ub = np.zeros((M, M * N)); A_eq = np.zeros((N, M * N))
|
||||||
for i in range(m):
|
for i in range(M):
|
||||||
A_ub[i, i * n:(i + 1) * n] = 1.0
|
A_ub[i, i * N:(i + 1) * N] = 1.0
|
||||||
for j in range(n):
|
for j in range(N):
|
||||||
A_eq[j, j::n] = 1.0
|
A_eq[j, j::N] = 1.0
|
||||||
aufbau = time.perf_counter() - t0
|
aufbau = time.perf_counter() - t0
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
r = linprog(c=c, A_ub=A_ub, b_ub=angebot, A_eq=A_eq, b_eq=bedarf,
|
r = linprog(c=c, A_ub=A_ub, b_ub=angebot, A_eq=A_eq, b_eq=bedarf,
|
||||||
bounds=(0, None), method="highs")
|
bounds=(0, None), method="highs")
|
||||||
loesen = time.perf_counter() - t0
|
loesen = time.perf_counter() - t0
|
||||||
return float(r.fun), aufbau, loesen, speicher_mb()
|
ausgabe = (float(r.fun), aufbau, loesen, speicher_mb())
|
||||||
|
""",
|
||||||
|
|
||||||
|
"highspy": """
|
||||||
def messe_highspy(m: int, n: int):
|
|
||||||
import highspy
|
import highspy
|
||||||
kosten, angebot, bedarf = instanz(m, n)
|
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
h = highspy.Highs(); h.setOptionValue("output_flag", False)
|
h = highspy.Highs(); h.setOptionValue("output_flag", False)
|
||||||
h.addVars(m * n, np.zeros(m * n), np.full(m * n, highspy.kHighsInf))
|
h.addVars(M * N, np.zeros(M * N), np.full(M * N, highspy.kHighsInf))
|
||||||
for k in range(m * n):
|
for k in range(M * N):
|
||||||
h.changeColCost(k, float(kosten.reshape(-1)[k]))
|
h.changeColCost(k, float(kosten.reshape(-1)[k]))
|
||||||
for i in range(m):
|
for i in range(M):
|
||||||
idx = np.arange(i * n, (i + 1) * n, dtype=np.int32)
|
idx = np.arange(i * N, (i + 1) * N, dtype=np.int32)
|
||||||
h.addRow(-highspy.kHighsInf, float(angebot[i]), n, idx, np.ones(n))
|
h.addRow(-highspy.kHighsInf, float(angebot[i]), N, idx, np.ones(N))
|
||||||
for j in range(n):
|
for j in range(N):
|
||||||
idx = np.arange(j, m * n, n, dtype=np.int32)
|
idx = np.arange(j, M * N, N, dtype=np.int32)
|
||||||
h.addRow(float(bedarf[j]), float(bedarf[j]), m, idx, np.ones(m))
|
h.addRow(float(bedarf[j]), float(bedarf[j]), M, idx, np.ones(M))
|
||||||
aufbau = time.perf_counter() - t0
|
aufbau = time.perf_counter() - t0
|
||||||
t0 = time.perf_counter(); h.run(); loesen = time.perf_counter() - t0
|
t0 = time.perf_counter(); h.run(); loesen = time.perf_counter() - t0
|
||||||
return h.getInfo().objective_function_value, aufbau, loesen, speicher_mb()
|
ausgabe = (h.getInfo().objective_function_value, aufbau, loesen, speicher_mb())
|
||||||
|
""",
|
||||||
|
|
||||||
|
"ortools/GLOP": """
|
||||||
def messe_ortools(m: int, n: int):
|
|
||||||
from ortools.linear_solver import pywraplp
|
from ortools.linear_solver import pywraplp
|
||||||
kosten, angebot, bedarf = instanz(m, n)
|
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
s = pywraplp.Solver.CreateSolver("GLOP")
|
s = pywraplp.Solver.CreateSolver("GLOP")
|
||||||
x = [[s.NumVar(0, s.infinity(), f"x{i}_{j}") for j in range(n)]
|
x = [[s.NumVar(0, s.infinity(), f"x{i}_{j}") for j in range(N)]
|
||||||
for i in range(m)]
|
for i in range(M)]
|
||||||
for i in range(m):
|
for i in range(M):
|
||||||
s.Add(sum(x[i]) <= float(angebot[i]))
|
s.Add(sum(x[i]) <= float(angebot[i]))
|
||||||
for j in range(n):
|
for j in range(N):
|
||||||
s.Add(sum(x[i][j] for i in range(m)) == float(bedarf[j]))
|
s.Add(sum(x[i][j] for i in range(M)) == float(bedarf[j]))
|
||||||
s.Minimize(sum(float(kosten[i, j]) * x[i][j]
|
s.Minimize(sum(float(kosten[i, j]) * x[i][j]
|
||||||
for i in range(m) for j in range(n)))
|
for i in range(M) for j in range(N)))
|
||||||
aufbau = time.perf_counter() - t0
|
aufbau = time.perf_counter() - t0
|
||||||
t0 = time.perf_counter(); s.Solve(); loesen = time.perf_counter() - t0
|
t0 = time.perf_counter(); s.Solve(); loesen = time.perf_counter() - t0
|
||||||
return s.Objective().Value(), aufbau, loesen, speicher_mb()
|
ausgabe = (s.Objective().Value(), aufbau, loesen, speicher_mb())
|
||||||
|
""",
|
||||||
|
|
||||||
|
"cvxpy": """
|
||||||
def messe_cvxpy(m: int, n: int):
|
|
||||||
import cvxpy as cp
|
import cvxpy as cp
|
||||||
kosten, angebot, bedarf = instanz(m, n)
|
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
x = cp.Variable((m, n), nonneg=True)
|
x = cp.Variable((M, N), nonneg=True)
|
||||||
problem = cp.Problem(cp.Minimize(cp.sum(cp.multiply(kosten, x))),
|
problem = cp.Problem(cp.Minimize(cp.sum(cp.multiply(kosten, x))),
|
||||||
[cp.sum(x, axis=1) <= angebot,
|
[cp.sum(x, axis=1) <= angebot,
|
||||||
cp.sum(x, axis=0) == bedarf])
|
cp.sum(x, axis=0) == bedarf])
|
||||||
aufbau = time.perf_counter() - t0
|
aufbau = time.perf_counter() - t0
|
||||||
t0 = time.perf_counter(); problem.solve(); loesen = time.perf_counter() - t0
|
t0 = time.perf_counter(); problem.solve(); loesen = time.perf_counter() - t0
|
||||||
return float(problem.value), aufbau, loesen, speicher_mb()
|
ausgabe = (float(problem.value), aufbau, loesen, speicher_mb())
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ANSAETZE = {"scipy.linprog": messe_scipy, "highspy": messe_highspy,
|
def messe(name: str, quelltext: str, m: int, n: int):
|
||||||
"ortools/GLOP": messe_ortools, "cvxpy": messe_cvxpy}
|
"""Fuehrt einen Ansatz in einem eigenen Prozess aus."""
|
||||||
|
programm = (VORSPANN.format(m=m, n=n) + textwrap.dedent(quelltext)
|
||||||
|
+ "\nprint(json.dumps(ausgabe))\n")
|
||||||
def messe(funktion, m: int, n: int):
|
ergebnis = subprocess.run([sys.executable, "-c", programm],
|
||||||
"""Fuehrt eine Messfunktion in einem FRISCHEN Prozess aus.
|
capture_output=True, text=True, timeout=600)
|
||||||
|
if ergebnis.returncode != 0:
|
||||||
'spawn' und max_tasks_per_child=1 zusammen garantieren, was Regel 1
|
return None, ergebnis.stderr.strip().splitlines()[-1][:60]
|
||||||
verlangt: Jede Messung sieht einen leeren Interpreter. Ohne das
|
return json.loads(ergebnis.stdout.strip().splitlines()[-1]), None
|
||||||
zweite wuerde der Pool seinen Arbeiter wiederverwenden - dann waere
|
|
||||||
der Speicherwert der zweiten Bibliothek um die erste zu hoch, und
|
|
||||||
ortools und highspy saessen im selben Prozess.
|
|
||||||
"""
|
|
||||||
with ProcessPoolExecutor(
|
|
||||||
max_workers=1,
|
|
||||||
mp_context=multiprocessing.get_context("spawn"),
|
|
||||||
max_tasks_per_child=1) as pool:
|
|
||||||
try:
|
|
||||||
return pool.submit(funktion, m, n).result(timeout=600), None
|
|
||||||
except Exception as fehler:
|
|
||||||
return None, str(fehler).strip().splitlines()[-1][:60]
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
@ -922,8 +911,8 @@ if __name__ == "__main__":
|
||||||
f"{'Loesen':>9} {'Anteil':>8} {'Speicher':>10}")
|
f"{'Loesen':>9} {'Anteil':>8} {'Speicher':>10}")
|
||||||
print(" " + "-" * 72)
|
print(" " + "-" * 72)
|
||||||
zielwerte = {}
|
zielwerte = {}
|
||||||
for name, funktion in ANSAETZE.items():
|
for name, quelltext in ANSAETZE.items():
|
||||||
werte, fehler = messe(funktion, m, n)
|
werte, fehler = messe(name, quelltext, m, n)
|
||||||
if werte is None:
|
if werte is None:
|
||||||
print(f" {name:<16} nicht verfuegbar: {fehler}")
|
print(f" {name:<16} nicht verfuegbar: {fehler}")
|
||||||
continue
|
continue
|
||||||
|
|
@ -979,28 +968,28 @@ die Zielwerte und ihr Verhaeltnis zueinander nicht.
|
||||||
--- 10 Lager x 10 Kunden = 100 Variablen ---------------------------------------------------
|
--- 10 Lager x 10 Kunden = 100 Variablen ---------------------------------------------------
|
||||||
Bibliothek Zielwert Aufbau Loesen Anteil Speicher
|
Bibliothek Zielwert Aufbau Loesen Anteil Speicher
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
scipy.linprog 13,509.48 0.000s 0.004s 1% 79 MB
|
scipy.linprog 13,509.48 0.000s 0.004s 1% 78 MB
|
||||||
highspy 13,509.48 0.001s 0.002s 36% 44 MB
|
highspy 13,509.48 0.001s 0.002s 34% 41 MB
|
||||||
ortools/GLOP 13,509.48 0.003s 0.001s 80% 56 MB
|
ortools/GLOP 13,509.48 0.002s 0.001s 80% 54 MB
|
||||||
cvxpy 13,509.48 0.001s 0.009s 9% 229 MB
|
cvxpy 13,509.48 0.001s 0.009s 9% 229 MB
|
||||||
Spannweite der Zielwerte: 1.33e-06 (relativ 9.8e-11)
|
Spannweite der Zielwerte: 1.33e-06 (relativ 9.8e-11)
|
||||||
|
|
||||||
--- 32 Lager x 32 Kunden = 1,024 Variablen -------------------------------------------------
|
--- 32 Lager x 32 Kunden = 1,024 Variablen -------------------------------------------------
|
||||||
Bibliothek Zielwert Aufbau Loesen Anteil Speicher
|
Bibliothek Zielwert Aufbau Loesen Anteil Speicher
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
scipy.linprog 35,744.25 0.000s 0.009s 3% 81 MB
|
scipy.linprog 35,744.25 0.000s 0.009s 4% 80 MB
|
||||||
highspy 35,744.25 0.004s 0.005s 44% 45 MB
|
highspy 35,744.25 0.004s 0.005s 47% 42 MB
|
||||||
ortools/GLOP 35,744.25 0.014s 0.002s 85% 58 MB
|
ortools/GLOP 35,744.25 0.015s 0.002s 85% 55 MB
|
||||||
cvxpy 35,744.25 0.001s 0.017s 5% 231 MB
|
cvxpy 35,744.25 0.001s 0.016s 5% 230 MB
|
||||||
Spannweite der Zielwerte: 9.54e-05 (relativ 2.7e-09)
|
Spannweite der Zielwerte: 9.54e-05 (relativ 2.7e-09)
|
||||||
|
|
||||||
--- 100 Lager x 100 Kunden = 10,000 Variablen ----------------------------------------------
|
--- 100 Lager x 100 Kunden = 10,000 Variablen ----------------------------------------------
|
||||||
Bibliothek Zielwert Aufbau Loesen Anteil Speicher
|
Bibliothek Zielwert Aufbau Loesen Anteil Speicher
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
scipy.linprog 72,220.34 0.004s 0.072s 5% 122 MB
|
scipy.linprog 72,220.34 0.002s 0.070s 3% 120 MB
|
||||||
highspy 72,220.34 0.026s 0.032s 45% 50 MB
|
highspy 72,220.34 0.029s 0.034s 45% 47 MB
|
||||||
ortools/GLOP 72,220.34 0.141s 0.043s 77% 68 MB
|
ortools/GLOP 72,220.34 0.123s 0.036s 77% 66 MB
|
||||||
cvxpy 72,220.34 0.001s 0.108s 1% 242 MB
|
cvxpy 72,220.34 0.001s 0.103s 1% 242 MB
|
||||||
Spannweite der Zielwerte: 3.65e-04 (relativ 5.0e-09)
|
Spannweite der Zielwerte: 3.65e-04 (relativ 5.0e-09)
|
||||||
|
|
||||||
============================================================================================
|
============================================================================================
|
||||||
|
|
@ -1647,7 +1636,3 @@ Test, der eine Instanz einreicht, die das Limit überschreitet.
|
||||||
* Ein **Optimierungsdienst** ist zweistufig: annehmen und prüfen sofort, rechnen im
|
* Ein **Optimierungsdienst** ist zweistufig: annehmen und prüfen sofort, rechnen im
|
||||||
Hintergrund, Ergebnis mit Status und Gap abholen. Die Frage Threads oder Prozesse gehört
|
Hintergrund, Ergebnis mit Status und Gap abholen. Die Frage Threads oder Prozesse gehört
|
||||||
gemessen — bei C++-Solvern genügen Threads, bei Python-Heuristiken nicht.
|
gemessen — bei C++-Solvern genügen Threads, bei Python-Heuristiken nicht.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Weiter mit:* [Synthese Teil V](52_Synthese_Praxis.md)
|
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
# Synthese Teil V — Vom rechnenden Modell zum benutzten System
|
|
||||||
|
|
||||||
Zwei Kapitel, ein Übergang: Das Modell rechnet — und muss jetzt jemand anderem übergeben
|
|
||||||
werden. Was dabei schiefgeht, hat selten mit Mathematik zu tun.
|
|
||||||
|
|
||||||
## Was schiefgeht, und was dagegen hilft
|
|
||||||
|
|
||||||
| Was in Produktion passiert | Gegenmittel | Wo |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `INFEASIBLE` um 3 Uhr nachts, niemand weiß warum | Relaxation mit gestaffelten Strafkosten (B18); Deletion Filter für die Diagnose | {ref:kap:praxisfallen}, {ref:anhang:fehlerdiagnose} |
|
|
||||||
| Der Disponent lehnt den Plan ab, weil er ihn nicht versteht | Constraint-Trace für die einzelne Zuweisung; Kostenzurechnung für den ganzen Plan | {ref:kap:praxisfallen}, {ref:sec:praxisfallen-attribution} |
|
|
||||||
| Der Solver läuft ins Zeitlimit und keiner merkt es | Status, Gap und Zeitausschöpfung protokollieren — konstante Laufzeit ist ein Warnsignal | {ref:kap:praxisfallen} |
|
|
||||||
| Ein Test wird grundlos mal rot, mal grün | auf Zielwert und Regeln prüfen, nicht auf die Gestalt der Lösung; `num_workers = 1` **und** Seed | {ref:sec:cpsat-parallele-suche}, {ref:kap:testing} |
|
|
||||||
| Ein Ergebnis lässt sich später nicht mehr nachvollziehen | Snapshot-Prinzip: unveränderlicher Datenstand mit ID je Lauf | {ref:kap:praxisfallen} |
|
|
||||||
| Das Modell prüft sich selbst | Abnahmeprüfung als eigener Baustein, ohne Solver und ohne Modellvariable | {ref:sec:praxisfallen-or-kern} |
|
|
||||||
|
|
||||||
## Was dieser Teil gemessen hat
|
|
||||||
|
|
||||||
| Behauptung | Gemessen | Wo |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| „Der Solver nennt schon den Grund für `INFEASIBLE`." | Er nennt gar nichts. Der Deletion Filter findet einen kleinsten Konflikt — dieses Modell enthält **neun** verschiedene, und welchen man sieht, steuert die Prüfreihenfolge | {ref:anhang:fehlerdiagnose} |
|
|
||||||
| „Bindende Bedingungen sind die teuren." | Von fünf bindenden Bedingungen kostet eine **0,00 €** | {ref:sec:praxisfallen-attribution} |
|
|
||||||
| „Der Schattenpreis sagt, was der Hebel bringt." | Hochgerechnet 120 €, gemessen **60 €** — er gilt nur bis +15 Stunden. Die Ranglisten nach Preis und nach Wirkung drehen sich um | {ref:sec:praxisfallen-attribution} |
|
|
||||||
| „Mehr Arbeiter sind proportional schneller." | **12,3-fach** bei acht Arbeitern — überlinear, weil verschiedene Strategien statt derselben Suche laufen | {ref:sec:cpsat-parallele-suche} |
|
|
||||||
|
|
||||||
## Drei Fehler, die dieser Teil verhindert
|
|
||||||
|
|
||||||
1. **Alles hart formulieren.** Wer keine Regel brechen lässt, bekommt irgendwann eine
|
|
||||||
Fehlermeldung statt eines Plans — und zwar nachts, im Batchlauf.
|
|
||||||
2. **Genauer optimieren als die Daten sind.** Ein Gap von 2 % ist bei ±10 %
|
|
||||||
Datenunsicherheit bedeutungslos. Die Rechenzeit dafür ist verschenkt.
|
|
||||||
3. **Einen Bericht für einen Beweis halten.** Ein generierter Text erklärt das **Modell**,
|
|
||||||
nicht die **Wirklichkeit**. Ist eine Eingabe falsch geschätzt, ist er überzeugend **und**
|
|
||||||
falsch — die gefährlichste Kombination.
|
|
||||||
|
|
||||||
## Wenn Sie nur eines mitnehmen
|
|
||||||
|
|
||||||
> **🎯** Ein Modell, das nur auf Ihrem Rechner und nur mit Ihren Daten läuft, ist ein
|
|
||||||
> Prototyp — kein System. Der Unterschied besteht aus drei Dingen, die alle nichts mit dem
|
|
||||||
> Solver zu tun haben: einer Abnahmeprüfung, einer Erklärung und einem Protokoll.
|
|
||||||
|
|
||||||
**Danach:** die Projektwerkstatt. Elf Aufträge, jeder mit Datenquellen, Modellskizze,
|
|
||||||
Abnahmekriterien und Stolperfallen — zugeschnitten auf 10 bis 25 Stunden bis zu einem
|
|
||||||
vorzeigbaren Ergebnis.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Weiter mit:* [Projektwerkstatt](59_Projektwerkstatt.md)
|
|
||||||
|
|
@ -367,4 +367,4 @@ Viel Erfolg.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Weiter mit:* [{ref:anhang:loesungen} — Lösungen zu allen Übungsaufgaben](90_Anhang_Loesungen.md)
|
*Weiter mit:* [{ref:anhang:loesungen} — Lösungen](90_Anhang_Loesungen.md)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue