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
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue