Phase 6.3: Parallelitaet und Determinismus - gemessen statt behauptet
Neuer Abschnitt im CP-SAT-Kapitel plus Parallele_Suche.py (76. Programm). Setzt Paket 2 aus Verbesserungen_02.md um. Was fehlte, war nicht die Regel, sondern die Messung: Das Buch setzt an acht Stellen num_workers = 1 mit dem Kommentar "fuer eine reproduzierbare Ausgabe", nachgeprueft hatte es nie jemand. Gerechnet wird auf demselben Job-Shop wie das Intervallvariablen-Kapitel, nur gross genug, dass die Suche arbeitet: 12 Auftraege, 10 Maschinen, 120 Arbeitsgaenge. Zwei Befunde, beide staerker als die Behauptung: * Die Beschleunigung ist ueberlinear. Acht Arbeiter waren im abgedruckten Lauf nicht achtmal, sondern 12,3-mal schneller als einer. Kein Messfehler: CP-SAT vervielfacht nicht dieselbe Suche, sondern laesst verschiedene Strategien nebeneinander laufen, die einander ihre Schranken mitteilen. * Der Seed genuegt nicht - und zwar schon ab ZWEI Arbeitern. Ein Arbeiter: 1 Plan aus 4 Laeufen. Zwei Arbeiter: 3 verschiedene Plaene aus 4 Laeufen, bei identischem random_seed und identischem Zielwert 183. Die vollstaendige Antwort kam erst ueber die Uebungsaufgabe: Mit num_workers = 1, aber fuenf verschiedenen Seeds ergeben sich ebenfalls fuenf verschiedene Plaene. Keiner der beiden Parameter sichert die Reproduzierbarkeit allein - erst die Kombination traegt. Weiter fuer die Aufgabe gemessen: Der Gewinn kehrt sich um (auf 24 Kernen Faktor 10,2 bei 8 Arbeitern, 12,3 bei 16, 9,0 bei 24) - "so viele Arbeiter wie Kerne" ist damit widerlegt. Und bei 15 Auftraegen laeuft ein Arbeiter ins 60-s-Limit (FEASIBLE, Makespan 200), waehrend acht OPTIMAL mit demselben Makespan 200 nach 26,4 s melden: Der Unterschied liegt nicht in der Loesung, sondern im Beweis, dass es keine bessere gibt. Die abgedruckte Ausgabe traegt die Kennzeichnung "Laufzeiten und die Zahl der verschiedenen Plaene sind hardwareabhaengig" - nach dem Muster, das das Testing-Kapitel fuer Benchmark_Skalierung.py schon verwendet. Der Vergleich des extrahierten Programms mit dem Abdruck weicht denn auch in genau einer Zelle ab (4 statt 3 verschiedene Plaene bei 4 Arbeitern); Zielwert und Struktur sind identisch. Hier ist die Nichtreproduzierbarkeit der abgedruckten Zahl die Aussage selbst. Mitgezogen: Kapitelkopf, Lernziele, Selbsttest, Zusammenfassung, Vorwort-Programmverzeichnis, Uebungsaufgabe und Loesung in Anhang A, ein Verweis aus dem bestehenden Callout zu mehrdeutigen Optima und einer aus Warmstart_Effekt.py im MILP-Kapitel (dort nach Regel 12 der Kapitelname). Stand: 295 Abschnitte, 730 Querverweise, 328 Indexmarken, 76 Programme, 140 Aufgaben mit 140 Loesungen, 33 pytest-Tests, PDF 744 Seiten, 69 netzfreie Programme fehlerfrei. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
dd4acda129
commit
293370b7a1
30 changed files with 12643 additions and 10385 deletions
|
|
@ -496,6 +496,218 @@
|
||||||
" 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": {},
|
||||||
|
|
|
||||||
|
|
@ -757,7 +757,8 @@
|
||||||
"\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",
|
||||||
|
|
|
||||||
|
|
@ -496,6 +496,218 @@
|
||||||
" 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": {},
|
||||||
|
|
|
||||||
|
|
@ -757,7 +757,8 @@
|
||||||
"\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",
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -484,8 +484,31 @@
|
||||||
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">9</span>):</span>
|
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">9</span>):</span>
|
||||||
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> vorgabe[i][j]: m.Add(x[i][j] <span class="op">==</span> vorgabe[i][j])</span></code></pre></div>
|
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> vorgabe[i][j]: m.Add(x[i][j] <span class="op">==</span> vorgabe[i][j])</span></code></pre></div>
|
||||||
<p><strong>Etwa 10 Zeilen Modellcode</strong> — und der Solver löst jedes Sudoku in Millisekunden. Das ist die Stärke globaler Constraints.</p>
|
<p><strong>Etwa 10 Zeilen Modellcode</strong> — und der Solver löst jedes Sudoku in Millisekunden. Das ist die Stärke globaler Constraints.</p>
|
||||||
<p><strong>7.6 — Job-Shop erweitern.</strong> (a) Rüstzeiten: <code>AddNoOverlap</code> durch paarweise Disjunktionen mit Übergangszeit ersetzen, oder <code>AddCircuit</code> je Maschine mit Übergangsmatrix. (b) Verspätung: <code>tardiness = MaxEquality(0, ende - faellig)</code>, in die Zielfunktion. (c) <code>AddCumulative(intervalle, [1]*n, 2)</code> statt <code>AddNoOverlap</code> für die Doppelmaschine.</p>
|
<p><strong>7.6 — Die Grenze der Parallelität finden.</strong> (a) Der Gewinn flacht ab und kehrt sich um. Auf einem Rechner mit 24 Kernen gemessen (Faktor gegenüber einem Arbeiter): 8 Arbeiter <strong>10,2</strong>, 16 Arbeiter <strong>12,3</strong>, 24 Arbeiter <strong>9,0</strong>. Wer alle Kerne belegt, ist langsamer als mit zwei Dritteln — dem Betriebssystem, dem Speicherbus und den Suchsträngen untereinander bleibt zu wenig Luft. Die Faustregel „so viele Arbeiter wie Kerne” ist also falsch; die richtige Zahl ist eine Messung, keine Einstellung. (b) <strong>Fünf verschiedene Pläne aus fünf Seeds</strong> — bei <code>num_workers = 1</code> und identischem Zielwert 183. Das ist die andere Hälfte der Antwort: Der Seed <em>wirkt</em>, aber nur auf den einzelnen Suchstrang. Es sichert also <strong>keiner der beiden Parameter allein</strong> die Reproduzierbarkeit:</p>
|
||||||
<p><strong>7.7 — Wochendienstplan.</strong> Siehe Projekt P2. Kernpunkte: Nachtschicht-Folgeregel über <code>AddImplication</code>; „höchstens 5 Tage in Folge“ über gleitende Fenster (<code>sum(x[p,t..t+5]) <= 5</code>).</p>
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="header">
|
||||||
|
<th></th>
|
||||||
|
<th>Seed fest</th>
|
||||||
|
<th>Seed variabel</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="odd">
|
||||||
|
<td><strong>1 Arbeiter</strong></td>
|
||||||
|
<td>reproduzierbar</td>
|
||||||
|
<td>5 Pläne aus 5 Läufen</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="even">
|
||||||
|
<td><strong>mehrere Arbeiter</strong></td>
|
||||||
|
<td>3 Pläne aus 4 Läufen</td>
|
||||||
|
<td>erst recht nicht</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>Nur die Kombination trägt. (c) Bei <strong>15 Aufträgen</strong> (60 s Zeitlimit): Ein Arbeiter läuft ins Limit und meldet <code>FEASIBLE</code> mit Makespan 200; acht Arbeiter melden <code>OPTIMAL</code> mit <strong>demselben</strong> Makespan 200 nach 26,4 s. Bei 13 Aufträgen sind es 11,2 s gegen 2,8 s, bei 14 schon 21,4 s gegen 3,7 s. Bemerkenswert ist, dass beide denselben Wert finden — der Unterschied liegt nicht in der Lösung, sondern im <strong>Beweis</strong>, dass es keine bessere gibt. Genau diese Arbeit teilen sich die parallelen Stränge auf.</p>
|
||||||
|
<p><strong>7.7 — Job-Shop erweitern.</strong> (a) Rüstzeiten: <code>AddNoOverlap</code> durch paarweise Disjunktionen mit Übergangszeit ersetzen, oder <code>AddCircuit</code> je Maschine mit Übergangsmatrix. (b) Verspätung: <code>tardiness = MaxEquality(0, ende - faellig)</code>, in die Zielfunktion. (c) <code>AddCumulative(intervalle, [1]*n, 2)</code> statt <code>AddNoOverlap</code> für die Doppelmaschine.</p>
|
||||||
|
<p><strong>7.8 — Wochendienstplan.</strong> Siehe Projekt P2. Kernpunkte: Nachtschicht-Folgeregel über <code>AddImplication</code>; „höchstens 5 Tage in Folge“ über gleitende Fenster (<code>sum(x[p,t..t+5]) <= 5</code>).</p>
|
||||||
<h3 id="finde-den-denkfehler-die-betriebsvereinbarung-die-niemanden-interessiert">Finde den Denkfehler — Die Betriebsvereinbarung, die niemanden interessiert</h3>
|
<h3 id="finde-den-denkfehler-die-betriebsvereinbarung-die-niemanden-interessiert">Finde den Denkfehler — Die Betriebsvereinbarung, die niemanden interessiert</h3>
|
||||||
<ol type="a">
|
<ol type="a">
|
||||||
<li><p><strong>Warum die Regel verletzt wird.</strong> Weil sie einen <strong>Preis</strong> hat. Eine weiche Nebenbedingung verbietet nichts — sie verteuert. Der Solver vergleicht in jedem Schritt, was ihn eine Verletzung kostet und was sie ihm einbringt. Bei einem Gewicht von 1 gegen 10 lohnt sich eine Drei-Tage-Serie, sobald sie auch nur ein Zehntel eines Wunsches rettet. Der Solver hat genau das getan, was im Modell steht — er hat nur nicht getan, was gemeint war.</p></li>
|
<li><p><strong>Warum die Regel verletzt wird.</strong> Weil sie einen <strong>Preis</strong> hat. Eine weiche Nebenbedingung verbietet nichts — sie verteuert. Der Solver vergleicht in jedem Schritt, was ihn eine Verletzung kostet und was sie ihm einbringt. Bei einem Gewicht von 1 gegen 10 lohnt sich eine Drei-Tage-Serie, sobald sie auch nur ein Zehntel eines Wunsches rettet. Der Solver hat genau das getan, was im Modell steht — er hat nur nicht getan, was gemeint war.</p></li>
|
||||||
|
|
@ -563,6 +586,8 @@
|
||||||
<li>Eine Variable mit Start, Dauer und Ende; sie garantiert automatisch <code>start + dauer == ende</code>.</li>
|
<li>Eine Variable mit Start, Dauer und Ende; sie garantiert automatisch <code>start + dauer == ende</code>.</li>
|
||||||
<li>Weil jede zusätzliche harte Bedingung das Risiko von <code>INFEASIBLE</code> erhöht — und eine Fehlermeldung dem Anwender nicht hilft.</li>
|
<li>Weil jede zusätzliche harte Bedingung das Risiko von <code>INFEASIBLE</code> erhöht — und eine Fehlermeldung dem Anwender nicht hilft.</li>
|
||||||
<li>Weil Anwender die Frage „Warum ich?“ stellen. Ohne Antwort wird das System umgangen.</li>
|
<li>Weil Anwender die Frage „Warum ich?“ stellen. Ohne Antwort wird das System umgangen.</li>
|
||||||
|
<li>Weil der Seed nur festlegt, wie ein <strong>einzelner</strong> Suchstrang würfelt — nicht, welcher von mehreren parallelen zuerst fertig wird. Das entscheidet die Ausführungsreihenfolge, also die Uhr. Abhilfe: zusätzlich <code>num_workers = 1</code>. Gemessen: schon zwei Arbeiter liefern bei identischem Seed drei verschiedene Pläne zum selben Zielwert.</li>
|
||||||
|
<li>Weil CP-SAT nicht dieselbe Suche vervielfacht, sondern <strong>verschiedene Strategien</strong> nebeneinander laufen lässt (LP-gestützt, andere Verzweigungsregeln, lokale Suche), die einander ihre Schranken mitteilen. Ein zusätzlicher Arbeiter ist deshalb nicht ein weiterer Kern für dieselbe Arbeit, sondern ein anderes Verfahren — und wenn eines davon früh eine gute Schranke findet, profitieren alle.</li>
|
||||||
</ol>
|
</ol>
|
||||||
<hr />
|
<hr />
|
||||||
<h2 id="sec:loesungen-graphen">A.8 Lösungen zu Kapitel „Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP“</h2>
|
<h2 id="sec:loesungen-graphen">A.8 Lösungen zu Kapitel „Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP“</h2>
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
window.OR_PROGRAMME = ["Backtest_Fallen.py", "Bausteine_Vorlage.py", "Bellman_Minimalbeispiel.py", "Benchmark_Skalierung.py", "Betriebsueberwachung.py", "Big_M_Falle.py", "Bot_Allokation.py", "Brute_Force_Vergleich.py", "CP_SAT_Statusfaelle.py", "CP_SAT_Vertretungssystem.py", "CVaR_Portfolio.py", "Chance_Constraints.py", "Constraint_Attribution.py", "Data_Snooping.py", "Diversifikation_Demo.py", "Dualitaet_Nachweis.py", "Ein_System_Vier_Ansaetze.py", "Entropie_Maximierte_Allokation.py", "Erklaerbarkeit.py", "Excel_Bruecke.py", "Finanzdaten_Ledoit_Wolf.py", "Fluch_des_Durchschnitts.py", "Infeasibility_Diagnose.py", "Installationstest.py", "JobShop_Intervalle.py", "KKT_Nachweis.py", "Konfliktsuche.py", "Konvexitaet_Demo.py", "Kovarianz_Falle.py", "Kraftwerkseinsatz.py", "Large_Neighborhood_Search.py", "Lokale_Optima_Multistart.py", "MILP_Portfolio_Fixgebuehren.py", "Markowitz_CVXPY.py", "Matrixform.py", "Mehrperiodige_Order_Execution.py", "Mehrziel_Pareto.py", "Metaheuristik_vs_Exakt.py", "Min_Cost_Flow.py", "Modellierungsschichten.py", "Monte_Carlo.py", "Mutationstest.py", "Optimierungsdienst.py", "Predict_then_Optimize.py", "Propagation_Demo.py", "QP_Grundlagen.py", "QuantitativeTradingEngine.py", "Renditen_Vergleich.py", "Renditeschaetzung_Falle.py", "Robuste_Optimierung.py", "Rucksack.py", "Runden_Gegenbeispiel.py", "Schaetzrauschen_Demo.py", "Sensitivitaetsanalyse.py", "Simplex_Tableau_LP.py", "Simulated_Annealing.py", "Skalierung_Kondition.py", "Solver_Wahl.py", "Solverstatus_und_Gap.py", "Solverwechsel_CPSAT_HiGHS.py", "Spaltengenerierung.py", "Stochastische_Optimierung.py", "Strafgewichte.py", "Strukturbruecke.py", "Toleranzen_und_Entartung.py", "VRP_Flotten_Routing.py", "VRP_Kapazitaetsfalle.py", "VaR_CVaR_Demo.py", "Vektorisierte_Modellgenerierung.py", "Visualisierung_Loesungsraum.py", "Vom_Wunsch_zum_Modell.py", "Warmstart_Effekt.py", "Zuordnung_Ungarisch.py", "or_kern.py", "test_or_kern.py"];
|
window.OR_PROGRAMME = ["Backtest_Fallen.py", "Bausteine_Vorlage.py", "Bellman_Minimalbeispiel.py", "Benchmark_Skalierung.py", "Betriebsueberwachung.py", "Big_M_Falle.py", "Bot_Allokation.py", "Brute_Force_Vergleich.py", "CP_SAT_Statusfaelle.py", "CP_SAT_Vertretungssystem.py", "CVaR_Portfolio.py", "Chance_Constraints.py", "Constraint_Attribution.py", "Data_Snooping.py", "Diversifikation_Demo.py", "Dualitaet_Nachweis.py", "Ein_System_Vier_Ansaetze.py", "Entropie_Maximierte_Allokation.py", "Erklaerbarkeit.py", "Excel_Bruecke.py", "Finanzdaten_Ledoit_Wolf.py", "Fluch_des_Durchschnitts.py", "Infeasibility_Diagnose.py", "Installationstest.py", "JobShop_Intervalle.py", "KKT_Nachweis.py", "Konfliktsuche.py", "Konvexitaet_Demo.py", "Kovarianz_Falle.py", "Kraftwerkseinsatz.py", "Large_Neighborhood_Search.py", "Lokale_Optima_Multistart.py", "MILP_Portfolio_Fixgebuehren.py", "Markowitz_CVXPY.py", "Matrixform.py", "Mehrperiodige_Order_Execution.py", "Mehrziel_Pareto.py", "Metaheuristik_vs_Exakt.py", "Min_Cost_Flow.py", "Modellierungsschichten.py", "Monte_Carlo.py", "Mutationstest.py", "Optimierungsdienst.py", "Parallele_Suche.py", "Predict_then_Optimize.py", "Propagation_Demo.py", "QP_Grundlagen.py", "QuantitativeTradingEngine.py", "Renditen_Vergleich.py", "Renditeschaetzung_Falle.py", "Robuste_Optimierung.py", "Rucksack.py", "Runden_Gegenbeispiel.py", "Schaetzrauschen_Demo.py", "Sensitivitaetsanalyse.py", "Simplex_Tableau_LP.py", "Simulated_Annealing.py", "Skalierung_Kondition.py", "Solver_Wahl.py", "Solverstatus_und_Gap.py", "Solverwechsel_CPSAT_HiGHS.py", "Spaltengenerierung.py", "Stochastische_Optimierung.py", "Strafgewichte.py", "Strukturbruecke.py", "Toleranzen_und_Entartung.py", "VRP_Flotten_Routing.py", "VRP_Kapazitaetsfalle.py", "VaR_CVaR_Demo.py", "Vektorisierte_Modellgenerierung.py", "Visualisierung_Loesungsraum.py", "Vom_Wunsch_zum_Modell.py", "Warmstart_Effekt.py", "Zuordnung_Ungarisch.py", "or_kern.py", "test_or_kern.py"];
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -252,7 +252,7 @@ f_j(x) < f_j(y)\ \text{ für mindestens ein } j
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="odd">
|
||||||
<td><span class="math inline">w</span></td>
|
<td><span class="math inline">w</span></td>
|
||||||
<td>„So viele Euro ist mir ein Kilogramm CO₂ wert.“ Ein <strong>Wechselkurs</strong>, kein Wichtigkeitsregler (<a href="cpsat.html#sec:cpsat-denkfehler">Abschnitt 7.9</a>).</td>
|
<td>„So viele Euro ist mir ein Kilogramm CO₂ wert.“ Ein <strong>Wechselkurs</strong>, kein Wichtigkeitsregler (<a href="cpsat.html#sec:cpsat-denkfehler">Abschnitt 7.10</a>).</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="even">
|
||||||
<td><span class="math inline">\min_{x \in X}</span></td>
|
<td><span class="math inline">\min_{x \in X}</span></td>
|
||||||
|
|
|
||||||
|
|
@ -1342,68 +1342,69 @@ mit Warm-Start 8.44s 150 69,134.14</code></pre>
|
||||||
<span id="cb12-83"><a href="#cb12-83" aria-hidden="true" tabindex="-1"></a></span>
|
<span id="cb12-83"><a href="#cb12-83" aria-hidden="true" tabindex="-1"></a></span>
|
||||||
<span id="cb12-84"><a href="#cb12-84" aria-hidden="true" tabindex="-1"></a> loeser <span class="op">=</span> cp_model.CpSolver()</span>
|
<span id="cb12-84"><a href="#cb12-84" aria-hidden="true" tabindex="-1"></a> loeser <span class="op">=</span> cp_model.CpSolver()</span>
|
||||||
<span id="cb12-85"><a href="#cb12-85" aria-hidden="true" tabindex="-1"></a> loeser.parameters.max_time_in_seconds <span class="op">=</span> zeitlimit</span>
|
<span id="cb12-85"><a href="#cb12-85" aria-hidden="true" tabindex="-1"></a> loeser.parameters.max_time_in_seconds <span class="op">=</span> zeitlimit</span>
|
||||||
<span id="cb12-86"><a href="#cb12-86" aria-hidden="true" tabindex="-1"></a> <span class="co"># Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist.</span></span>
|
<span id="cb12-86"><a href="#cb12-86" aria-hidden="true" tabindex="-1"></a> <span class="co"># Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist -</span></span>
|
||||||
<span id="cb12-87"><a href="#cb12-87" aria-hidden="true" tabindex="-1"></a> <span class="co"># Im Produktivbetrieb laesst man beides auf den Standardwerten.</span></span>
|
<span id="cb12-87"><a href="#cb12-87" aria-hidden="true" tabindex="-1"></a> <span class="co"># der Seed allein genuegt dafuer NICHT (Kapitel Constraint Programming).</span></span>
|
||||||
<span id="cb12-88"><a href="#cb12-88" aria-hidden="true" tabindex="-1"></a> loeser.parameters.num_workers <span class="op">=</span> <span class="dv">1</span></span>
|
<span id="cb12-88"><a href="#cb12-88" aria-hidden="true" tabindex="-1"></a> <span class="co"># Im Produktivbetrieb laesst man beides auf den Standardwerten.</span></span>
|
||||||
<span id="cb12-89"><a href="#cb12-89" aria-hidden="true" tabindex="-1"></a> loeser.parameters.random_seed <span class="op">=</span> <span class="dv">1</span></span>
|
<span id="cb12-89"><a href="#cb12-89" aria-hidden="true" tabindex="-1"></a> loeser.parameters.num_workers <span class="op">=</span> <span class="dv">1</span></span>
|
||||||
<span id="cb12-90"><a href="#cb12-90" aria-hidden="true" tabindex="-1"></a></span>
|
<span id="cb12-90"><a href="#cb12-90" aria-hidden="true" tabindex="-1"></a> loeser.parameters.random_seed <span class="op">=</span> <span class="dv">1</span></span>
|
||||||
<span id="cb12-91"><a href="#cb12-91" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()</span>
|
<span id="cb12-91"><a href="#cb12-91" aria-hidden="true" tabindex="-1"></a></span>
|
||||||
<span id="cb12-92"><a href="#cb12-92" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> loeser.Solve(modell)</span>
|
<span id="cb12-92"><a href="#cb12-92" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()</span>
|
||||||
<span id="cb12-93"><a href="#cb12-93" aria-hidden="true" tabindex="-1"></a> dauer_s <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
|
<span id="cb12-93"><a href="#cb12-93" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> loeser.Solve(modell)</span>
|
||||||
<span id="cb12-94"><a href="#cb12-94" aria-hidden="true" tabindex="-1"></a></span>
|
<span id="cb12-94"><a href="#cb12-94" aria-hidden="true" tabindex="-1"></a> dauer_s <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
|
||||||
<span id="cb12-95"><a href="#cb12-95" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> status <span class="kw">not</span> <span class="kw">in</span> (cp_model.OPTIMAL, cp_model.FEASIBLE):</span>
|
<span id="cb12-95"><a href="#cb12-95" aria-hidden="true" tabindex="-1"></a></span>
|
||||||
<span id="cb12-96"><a href="#cb12-96" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="ss">f"Kein Plan gefunden: </span><span class="sc">{</span>loeser<span class="sc">.</span>StatusName(status)<span class="sc">}</span><span class="ss">"</span>)</span>
|
<span id="cb12-96"><a href="#cb12-96" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> status <span class="kw">not</span> <span class="kw">in</span> (cp_model.OPTIMAL, cp_model.FEASIBLE):</span>
|
||||||
<span id="cb12-97"><a href="#cb12-97" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> loeser.StatusName(status), <span class="bu">int</span>(loeser.ObjectiveValue()), dauer_s</span>
|
<span id="cb12-97"><a href="#cb12-97" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="ss">f"Kein Plan gefunden: </span><span class="sc">{</span>loeser<span class="sc">.</span>StatusName(status)<span class="sc">}</span><span class="ss">"</span>)</span>
|
||||||
<span id="cb12-98"><a href="#cb12-98" aria-hidden="true" tabindex="-1"></a></span>
|
<span id="cb12-98"><a href="#cb12-98" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> loeser.StatusName(status), <span class="bu">int</span>(loeser.ObjectiveValue()), dauer_s</span>
|
||||||
<span id="cb12-99"><a href="#cb12-99" aria-hidden="true" tabindex="-1"></a></span>
|
<span id="cb12-99"><a href="#cb12-99" aria-hidden="true" tabindex="-1"></a></span>
|
||||||
<span id="cb12-100"><a href="#cb12-100" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">"__main__"</span>:</span>
|
<span id="cb12-100"><a href="#cb12-100" aria-hidden="true" tabindex="-1"></a></span>
|
||||||
<span id="cb12-101"><a href="#cb12-101" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"="</span> <span class="op">*</span> <span class="dv">78</span>)</span>
|
<span id="cb12-101"><a href="#cb12-101" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">"__main__"</span>:</span>
|
||||||
<span id="cb12-102"><a href="#cb12-102" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" WARM-START: WENN DIE HEURISTIK MEHR WEISS ALS DER SOLVER"</span>)</span>
|
<span id="cb12-102"><a href="#cb12-102" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"="</span> <span class="op">*</span> <span class="dv">78</span>)</span>
|
||||||
<span id="cb12-103"><a href="#cb12-103" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"="</span> <span class="op">*</span> <span class="dv">78</span>)</span>
|
<span id="cb12-103"><a href="#cb12-103" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" WARM-START: WENN DIE HEURISTIK MEHR WEISS ALS DER SOLVER"</span>)</span>
|
||||||
<span id="cb12-104"><a href="#cb12-104" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"Lastverteilung: Auftraege auf gleichartige Maschinen verteilen,"</span>)</span>
|
<span id="cb12-104"><a href="#cb12-104" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"="</span> <span class="op">*</span> <span class="dv">78</span>)</span>
|
||||||
<span id="cb12-105"><a href="#cb12-105" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"sodass die letzte Maschine so frueh wie moeglich fertig wird.</span><span class="ch">\n</span><span class="st">"</span>)</span>
|
<span id="cb12-105"><a href="#cb12-105" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"Lastverteilung: Auftraege auf gleichartige Maschinen verteilen,"</span>)</span>
|
||||||
<span id="cb12-106"><a href="#cb12-106" aria-hidden="true" tabindex="-1"></a></span>
|
<span id="cb12-106"><a href="#cb12-106" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"sodass die letzte Maschine so frueh wie moeglich fertig wird.</span><span class="ch">\n</span><span class="st">"</span>)</span>
|
||||||
<span id="cb12-107"><a href="#cb12-107" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f"</span><span class="sc">{</span><span class="st">'Instanz'</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">'Variante'</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">'Makespan'</span><span class="sc">:>9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">'Zeit'</span><span class="sc">:>9}</span><span class="ss"> "</span></span>
|
<span id="cb12-107"><a href="#cb12-107" aria-hidden="true" tabindex="-1"></a></span>
|
||||||
<span id="cb12-108"><a href="#cb12-108" aria-hidden="true" tabindex="-1"></a> <span class="ss">f"</span><span class="sc">{</span><span class="st">'Faktor'</span><span class="sc">:>8}</span><span class="ss">"</span>)</span>
|
<span id="cb12-108"><a href="#cb12-108" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f"</span><span class="sc">{</span><span class="st">'Instanz'</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">'Variante'</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">'Makespan'</span><span class="sc">:>9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">'Zeit'</span><span class="sc">:>9}</span><span class="ss"> "</span></span>
|
||||||
<span id="cb12-109"><a href="#cb12-109" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"-"</span> <span class="op">*</span> <span class="dv">78</span>)</span>
|
<span id="cb12-109"><a href="#cb12-109" aria-hidden="true" tabindex="-1"></a> <span class="ss">f"</span><span class="sc">{</span><span class="st">'Faktor'</span><span class="sc">:>8}</span><span class="ss">"</span>)</span>
|
||||||
<span id="cb12-110"><a href="#cb12-110" aria-hidden="true" tabindex="-1"></a></span>
|
<span id="cb12-110"><a href="#cb12-110" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"-"</span> <span class="op">*</span> <span class="dv">78</span>)</span>
|
||||||
<span id="cb12-111"><a href="#cb12-111" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> n_auftraege, n_maschinen <span class="kw">in</span> [(<span class="dv">60</span>, <span class="dv">7</span>), (<span class="dv">80</span>, <span class="dv">9</span>)]:</span>
|
<span id="cb12-111"><a href="#cb12-111" aria-hidden="true" tabindex="-1"></a></span>
|
||||||
<span id="cb12-112"><a href="#cb12-112" aria-hidden="true" tabindex="-1"></a> dauer <span class="op">=</span> RNG.integers(<span class="dv">10</span>, <span class="dv">90</span>, n_auftraege)</span>
|
<span id="cb12-112"><a href="#cb12-112" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> n_auftraege, n_maschinen <span class="kw">in</span> [(<span class="dv">60</span>, <span class="dv">7</span>), (<span class="dv">80</span>, <span class="dv">9</span>)]:</span>
|
||||||
<span id="cb12-113"><a href="#cb12-113" aria-hidden="true" tabindex="-1"></a> start, lpt_wert <span class="op">=</span> lpt_heuristik(dauer, n_maschinen)</span>
|
<span id="cb12-113"><a href="#cb12-113" aria-hidden="true" tabindex="-1"></a> dauer <span class="op">=</span> RNG.integers(<span class="dv">10</span>, <span class="dv">90</span>, n_auftraege)</span>
|
||||||
<span id="cb12-114"><a href="#cb12-114" aria-hidden="true" tabindex="-1"></a> untere_schranke <span class="op">=</span> dauer.<span class="bu">sum</span>() <span class="op">/</span> n_maschinen</span>
|
<span id="cb12-114"><a href="#cb12-114" aria-hidden="true" tabindex="-1"></a> start, lpt_wert <span class="op">=</span> lpt_heuristik(dauer, n_maschinen)</span>
|
||||||
<span id="cb12-115"><a href="#cb12-115" aria-hidden="true" tabindex="-1"></a></span>
|
<span id="cb12-115"><a href="#cb12-115" aria-hidden="true" tabindex="-1"></a> untere_schranke <span class="op">=</span> dauer.<span class="bu">sum</span>() <span class="op">/</span> n_maschinen</span>
|
||||||
<span id="cb12-116"><a href="#cb12-116" aria-hidden="true" tabindex="-1"></a> instanz <span class="op">=</span> <span class="ss">f"</span><span class="sc">{</span>n_auftraege<span class="sc">}</span><span class="ss"> Auftr., </span><span class="sc">{</span>n_maschinen<span class="sc">}</span><span class="ss"> Masch."</span></span>
|
<span id="cb12-116"><a href="#cb12-116" aria-hidden="true" tabindex="-1"></a></span>
|
||||||
<span id="cb12-117"><a href="#cb12-117" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f"</span><span class="sc">{</span>instanz<span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">'LPT-Heuristik'</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span>lpt_wert<span class="sc">:>9}</span><span class="ss"> "</span></span>
|
<span id="cb12-117"><a href="#cb12-117" aria-hidden="true" tabindex="-1"></a> instanz <span class="op">=</span> <span class="ss">f"</span><span class="sc">{</span>n_auftraege<span class="sc">}</span><span class="ss"> Auftr., </span><span class="sc">{</span>n_maschinen<span class="sc">}</span><span class="ss"> Masch."</span></span>
|
||||||
<span id="cb12-118"><a href="#cb12-118" aria-hidden="true" tabindex="-1"></a> <span class="ss">f"</span><span class="sc">{</span><span class="st">'< 0.001s'</span><span class="sc">:>9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">''</span><span class="sc">:>8}</span><span class="ss">"</span>)</span>
|
<span id="cb12-118"><a href="#cb12-118" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f"</span><span class="sc">{</span>instanz<span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">'LPT-Heuristik'</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span>lpt_wert<span class="sc">:>9}</span><span class="ss"> "</span></span>
|
||||||
<span id="cb12-119"><a href="#cb12-119" aria-hidden="true" tabindex="-1"></a></span>
|
<span id="cb12-119"><a href="#cb12-119" aria-hidden="true" tabindex="-1"></a> <span class="ss">f"</span><span class="sc">{</span><span class="st">'< 0.001s'</span><span class="sc">:>9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">''</span><span class="sc">:>8}</span><span class="ss">"</span>)</span>
|
||||||
<span id="cb12-120"><a href="#cb12-120" aria-hidden="true" tabindex="-1"></a> _, ziel_kalt, zeit_kalt <span class="op">=</span> loese(dauer, n_maschinen, <span class="fl">60.0</span>)</span>
|
<span id="cb12-120"><a href="#cb12-120" aria-hidden="true" tabindex="-1"></a></span>
|
||||||
<span id="cb12-121"><a href="#cb12-121" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f"</span><span class="sc">{</span><span class="st">''</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">'CP-SAT kalt'</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span>ziel_kalt<span class="sc">:>9}</span><span class="ss"> "</span></span>
|
<span id="cb12-121"><a href="#cb12-121" aria-hidden="true" tabindex="-1"></a> _, ziel_kalt, zeit_kalt <span class="op">=</span> loese(dauer, n_maschinen, <span class="fl">60.0</span>)</span>
|
||||||
<span id="cb12-122"><a href="#cb12-122" aria-hidden="true" tabindex="-1"></a> <span class="ss">f"</span><span class="sc">{</span>zeit_kalt<span class="sc">:>8.3f}</span><span class="ss">s </span><span class="sc">{</span><span class="st">'1,0x'</span><span class="sc">:>8}</span><span class="ss">"</span>)</span>
|
<span id="cb12-122"><a href="#cb12-122" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f"</span><span class="sc">{</span><span class="st">''</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">'CP-SAT kalt'</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span>ziel_kalt<span class="sc">:>9}</span><span class="ss"> "</span></span>
|
||||||
<span id="cb12-123"><a href="#cb12-123" aria-hidden="true" tabindex="-1"></a></span>
|
<span id="cb12-123"><a href="#cb12-123" aria-hidden="true" tabindex="-1"></a> <span class="ss">f"</span><span class="sc">{</span>zeit_kalt<span class="sc">:>8.3f}</span><span class="ss">s </span><span class="sc">{</span><span class="st">'1,0x'</span><span class="sc">:>8}</span><span class="ss">"</span>)</span>
|
||||||
<span id="cb12-124"><a href="#cb12-124" aria-hidden="true" tabindex="-1"></a> _, ziel_warm, zeit_warm <span class="op">=</span> loese(dauer, n_maschinen, <span class="fl">60.0</span>, hinweis<span class="op">=</span>start)</span>
|
<span id="cb12-124"><a href="#cb12-124" aria-hidden="true" tabindex="-1"></a></span>
|
||||||
<span id="cb12-125"><a href="#cb12-125" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f"</span><span class="sc">{</span><span class="st">''</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">'CP-SAT + LPT-Hinweis'</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span>ziel_warm<span class="sc">:>9}</span><span class="ss"> "</span></span>
|
<span id="cb12-125"><a href="#cb12-125" aria-hidden="true" tabindex="-1"></a> _, ziel_warm, zeit_warm <span class="op">=</span> loese(dauer, n_maschinen, <span class="fl">60.0</span>, hinweis<span class="op">=</span>start)</span>
|
||||||
<span id="cb12-126"><a href="#cb12-126" aria-hidden="true" tabindex="-1"></a> <span class="ss">f"</span><span class="sc">{</span>zeit_warm<span class="sc">:>8.3f}</span><span class="ss">s </span><span class="sc">{</span>zeit_kalt <span class="op">/</span> zeit_warm<span class="sc">:>7.1f}</span><span class="ss">x"</span>)</span>
|
<span id="cb12-126"><a href="#cb12-126" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f"</span><span class="sc">{</span><span class="st">''</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">'CP-SAT + LPT-Hinweis'</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span>ziel_warm<span class="sc">:>9}</span><span class="ss"> "</span></span>
|
||||||
<span id="cb12-127"><a href="#cb12-127" aria-hidden="true" tabindex="-1"></a></span>
|
<span id="cb12-127"><a href="#cb12-127" aria-hidden="true" tabindex="-1"></a> <span class="ss">f"</span><span class="sc">{</span>zeit_warm<span class="sc">:>8.3f}</span><span class="ss">s </span><span class="sc">{</span>zeit_kalt <span class="op">/</span> zeit_warm<span class="sc">:>7.1f}</span><span class="ss">x"</span>)</span>
|
||||||
<span id="cb12-128"><a href="#cb12-128" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> ziel_kalt <span class="op">==</span> ziel_warm, <span class="op">\</span></span>
|
<span id="cb12-128"><a href="#cb12-128" aria-hidden="true" tabindex="-1"></a></span>
|
||||||
<span id="cb12-129"><a href="#cb12-129" aria-hidden="true" tabindex="-1"></a> <span class="st">"Der Hinweis darf das Optimum nicht veraendern - nur den Weg dorthin!"</span></span>
|
<span id="cb12-129"><a href="#cb12-129" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> ziel_kalt <span class="op">==</span> ziel_warm, <span class="op">\</span></span>
|
||||||
<span id="cb12-130"><a href="#cb12-130" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f"</span><span class="sc">{</span><span class="st">''</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">'untere Schranke'</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span>untere_schranke<span class="sc">:>9.1f}</span><span class="ss">"</span>)</span>
|
<span id="cb12-130"><a href="#cb12-130" aria-hidden="true" tabindex="-1"></a> <span class="st">"Der Hinweis darf das Optimum nicht veraendern - nur den Weg dorthin!"</span></span>
|
||||||
<span id="cb12-131"><a href="#cb12-131" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"-"</span> <span class="op">*</span> <span class="dv">78</span>)</span>
|
<span id="cb12-131"><a href="#cb12-131" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f"</span><span class="sc">{</span><span class="st">''</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">'untere Schranke'</span><span class="sc">:<22}</span><span class="ss"> </span><span class="sc">{</span>untere_schranke<span class="sc">:>9.1f}</span><span class="ss">"</span>)</span>
|
||||||
<span id="cb12-132"><a href="#cb12-132" aria-hidden="true" tabindex="-1"></a></span>
|
<span id="cb12-132"><a href="#cb12-132" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"-"</span> <span class="op">*</span> <span class="dv">78</span>)</span>
|
||||||
<span id="cb12-133"><a href="#cb12-133" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"</span><span class="ch">\n</span><span class="st">Drei Beobachtungen:"</span>)</span>
|
<span id="cb12-133"><a href="#cb12-133" aria-hidden="true" tabindex="-1"></a></span>
|
||||||
<span id="cb12-134"><a href="#cb12-134" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"1. Der Hinweis aendert das ERGEBNIS nicht - beide Laeufe finden"</span>)</span>
|
<span id="cb12-134"><a href="#cb12-134" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"</span><span class="ch">\n</span><span class="st">Drei Beobachtungen:"</span>)</span>
|
||||||
<span id="cb12-135"><a href="#cb12-135" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" dasselbe Optimum. Er aendert nur, wie lange der Beweis dauert."</span>)</span>
|
<span id="cb12-135"><a href="#cb12-135" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"1. Der Hinweis aendert das ERGEBNIS nicht - beide Laeufe finden"</span>)</span>
|
||||||
<span id="cb12-136"><a href="#cb12-136" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" Genau deshalb ist ein Warm-Start ungefaehrlich: Ein schlechter"</span>)</span>
|
<span id="cb12-136"><a href="#cb12-136" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" dasselbe Optimum. Er aendert nur, wie lange der Beweis dauert."</span>)</span>
|
||||||
<span id="cb12-137"><a href="#cb12-137" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" Hinweis kostet Zeit, er verfaelscht aber nie die Loesung."</span>)</span>
|
<span id="cb12-137"><a href="#cb12-137" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" Genau deshalb ist ein Warm-Start ungefaehrlich: Ein schlechter"</span>)</span>
|
||||||
<span id="cb12-138"><a href="#cb12-138" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"2. Die LPT-Heuristik liegt schon sehr nah am Optimum. Ihr Wert fuer"</span>)</span>
|
<span id="cb12-138"><a href="#cb12-138" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" Hinweis kostet Zeit, er verfaelscht aber nie die Loesung."</span>)</span>
|
||||||
<span id="cb12-139"><a href="#cb12-139" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" den Solver liegt weniger in der Qualitaet als darin, dass sie"</span>)</span>
|
<span id="cb12-139"><a href="#cb12-139" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"2. Die LPT-Heuristik liegt schon sehr nah am Optimum. Ihr Wert fuer"</span>)</span>
|
||||||
<span id="cb12-140"><a href="#cb12-140" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" SOFORT da ist - der Solver kann von Beginn an alles verwerfen,"</span>)</span>
|
<span id="cb12-140"><a href="#cb12-140" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" den Solver liegt weniger in der Qualitaet als darin, dass sie"</span>)</span>
|
||||||
<span id="cb12-141"><a href="#cb12-141" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" was schlechter ist."</span>)</span>
|
<span id="cb12-141"><a href="#cb12-141" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" SOFORT da ist - der Solver kann von Beginn an alles verwerfen,"</span>)</span>
|
||||||
<span id="cb12-142"><a href="#cb12-142" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"3. Der Faktor schwankt von Instanz zu Instanz - oben 2,2x und 1,4x -"</span>)</span>
|
<span id="cb12-142"><a href="#cb12-142" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" was schlechter ist."</span>)</span>
|
||||||
<span id="cb12-143"><a href="#cb12-143" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" und laesst sich NICHT aus der Problemgroesse ableiten. Er haengt"</span>)</span>
|
<span id="cb12-143"><a href="#cb12-143" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"3. Der Faktor schwankt von Instanz zu Instanz - oben 2,2x und 1,4x -"</span>)</span>
|
||||||
<span id="cb12-144"><a href="#cb12-144" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" davon ab, wie schnell der Solver von allein eine vergleichbar gute"</span>)</span>
|
<span id="cb12-144"><a href="#cb12-144" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" und laesst sich NICHT aus der Problemgroesse ableiten. Er haengt"</span>)</span>
|
||||||
<span id="cb12-145"><a href="#cb12-145" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" Loesung findet. Das ist die eigentliche Lehre: Ein Warm-Start ist"</span>)</span>
|
<span id="cb12-145"><a href="#cb12-145" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" davon ab, wie schnell der Solver von allein eine vergleichbar gute"</span>)</span>
|
||||||
<span id="cb12-146"><a href="#cb12-146" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" eine Messung wert, keine Glaubensfrage."</span>)</span>
|
<span id="cb12-146"><a href="#cb12-146" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" Loesung findet. Das ist die eigentliche Lehre: Ein Warm-Start ist"</span>)</span>
|
||||||
<span id="cb12-147"><a href="#cb12-147" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"="</span> <span class="op">*</span> <span class="dv">78</span>)</span></code></pre></div>
|
<span id="cb12-147"><a href="#cb12-147" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">" eine Messung wert, keine Glaubensfrage."</span>)</span>
|
||||||
|
<span id="cb12-148"><a href="#cb12-148" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">"="</span> <span class="op">*</span> <span class="dv">78</span>)</span></code></pre></div>
|
||||||
<p><strong>Erwartete Ausgabe (Zeiten hardwareabhängig):</strong></p>
|
<p><strong>Erwartete Ausgabe (Zeiten hardwareabhängig):</strong></p>
|
||||||
<pre><code>==============================================================================
|
<pre><code>==============================================================================
|
||||||
WARM-START: WENN DIE HEURISTIK MEHR WEISS ALS DER SOLVER
|
WARM-START: WENN DIE HEURISTIK MEHR WEISS ALS DER SOLVER
|
||||||
|
|
|
||||||
|
|
@ -706,7 +706,7 @@ Drei Dinge, die deshalb vor dem ersten Modell geklaert gehoeren:
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p>Bei 3 000 € ändert sich <strong>nichts</strong>: Die Strafe liegt unter dem Deckungsbeitrag, den die Stammkundenaufträge verdrängen würden. Bei 10 000 € kommt genau Lesart A heraus.</p>
|
<p>Bei 3 000 € ändert sich <strong>nichts</strong>: Die Strafe liegt unter dem Deckungsbeitrag, den die Stammkundenaufträge verdrängen würden. Bei 10 000 € kommt genau Lesart A heraus.</p>
|
||||||
<p>Daraus folgt eine Einsicht, die das Modellieren erleichtert: <strong>Eine harte Regel ist eine weiche mit hinreichend großer Strafe.</strong> Man kann also immer weich anfangen und die Strafe hochdrehen, bis die Regel greift — und sieht dabei, was sie kostet. Was ein Strafgewicht darüber hinaus bedeutet und warum zwei Gewichte einen <em>Wechselkurs</em> festlegen und keine Wichtigkeiten, steht in <a href="cpsat.html#sec:cpsat-denkfehler">Abschnitt 7.9</a>.</p>
|
<p>Daraus folgt eine Einsicht, die das Modellieren erleichtert: <strong>Eine harte Regel ist eine weiche mit hinreichend großer Strafe.</strong> Man kann also immer weich anfangen und die Strafe hochdrehen, bis die Regel greift — und sieht dabei, was sie kostet. Was ein Strafgewicht darüber hinaus bedeutet und warum zwei Gewichte einen <em>Wechselkurs</em> festlegen und keine Wichtigkeiten, steht in <a href="cpsat.html#sec:cpsat-denkfehler">Abschnitt 7.10</a>.</p>
|
||||||
<h3 id="teil-4-ein-quartal-später">Teil 4: Ein Quartal später</h3>
|
<h3 id="teil-4-ein-quartal-später">Teil 4: Ein Quartal später</h3>
|
||||||
<p>Die harte Lesart A wurde eingebaut, alles lief drei Monate lang. Dann fragt Weber einen Auftrag über 320 Maschinenstunden an — mehr, als der ganze Zeitraum hergibt.</p>
|
<p>Die harte Lesart A wurde eingebaut, alles lief drei Monate lang. Dann fragt Weber einen Auftrag über 320 Maschinenstunden an — mehr, als der ganze Zeitraum hergibt.</p>
|
||||||
<pre><code>Lesart A (hart): UNZULAESSIG - der Nachtlauf bricht ab
|
<pre><code>Lesart A (hart): UNZULAESSIG - der Nachtlauf bricht ab
|
||||||
|
|
|
||||||
|
|
@ -1664,7 +1664,7 @@ Domaenenschicht.
|
||||||
<tr class="odd">
|
<tr class="odd">
|
||||||
<td><code>SolverStatus.modellfehler</code></td>
|
<td><code>SolverStatus.modellfehler</code></td>
|
||||||
<td>trennt Modell- von Zeitproblemen</td>
|
<td>trennt Modell- von Zeitproblemen</td>
|
||||||
<td>Genau die Unterscheidung, die <a href="cpsat.html#sec:cpsat-statusfaelle">Abschnitt 7.7</a> als häufigste Verwechslung benennt.</td>
|
<td>Genau die Unterscheidung, die <a href="cpsat.html#sec:cpsat-statusfaelle">Abschnitt 7.8</a> als häufigste Verwechslung benennt.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="even">
|
||||||
<td><code>model_validator(mode="after")</code></td>
|
<td><code>model_validator(mode="after")</code></td>
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
195
OR_HTML_04/programme/Parallele_Suche.py
Normal file
195
OR_HTML_04/programme/Parallele_Suche.py
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Parallele_Suche.py
|
||||||
|
"""
|
||||||
|
Kapitel CP-SAT: Was mehrere Arbeiter wirklich bringen - und was sie kosten.
|
||||||
|
|
||||||
|
Das Buch setzt an mehreren Stellen 'num_workers = 1', damit die abgedruckte
|
||||||
|
Ausgabe reproduzierbar ist. Behauptet wird dabei zweierlei: dass parallele
|
||||||
|
Suche schneller ist, und dass sie von Lauf zu Lauf verschiedene, gleich gute
|
||||||
|
Loesungen findet. Beides wird hier gemessen statt geglaubt.
|
||||||
|
|
||||||
|
Gerechnet wird auf einem Job-Shop wie in JobShop_Intervalle.py, nur gross
|
||||||
|
genug, dass die Suche wirklich arbeitet: 12 Auftraege auf 10 Maschinen,
|
||||||
|
jeder Auftrag besucht jede Maschine.
|
||||||
|
|
||||||
|
Zwei Messungen:
|
||||||
|
1. Dieselbe Aufgabe mit 1, 2, 4 und 8 Arbeitern - Laufzeit und Ergebnis.
|
||||||
|
2. Dieselbe Konfiguration mehrfach, bei FESTEM random_seed - wie oft kommt
|
||||||
|
derselbe Plan heraus?
|
||||||
|
|
||||||
|
Die zweite ist die wichtigere: Sie entscheidet, wie man Tests schreibt.
|
||||||
|
|
||||||
|
Achtung, Laufzeiten sind hardwareabhaengig. Die ZAHL der verschiedenen
|
||||||
|
Plaene ist es auch - und genau das ist die Aussage.
|
||||||
|
|
||||||
|
Benoetigt: numpy, ortools
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from ortools.sat.python import cp_model
|
||||||
|
|
||||||
|
AUFTRAEGE = 12
|
||||||
|
MASCHINEN = 10
|
||||||
|
INSTANZ_SEED = 20260908
|
||||||
|
SOLVER_SEED = 1 # bleibt ueber ALLE Laeufe gleich - das ist der Punkt
|
||||||
|
ARBEITERZAHLEN = (1, 2, 4, 8)
|
||||||
|
WIEDERHOLUNGEN = 4
|
||||||
|
ZEITLIMIT = 120.0
|
||||||
|
|
||||||
|
|
||||||
|
def baue_instanz() -> list[list[tuple[int, int]]]:
|
||||||
|
"""Klassischer Job-Shop: Jeder Auftrag besucht jede Maschine genau einmal,
|
||||||
|
in einer eigenen zufaelligen Reihenfolge."""
|
||||||
|
rng = np.random.default_rng(INSTANZ_SEED)
|
||||||
|
return [[(int(m), int(rng.integers(2, 20))) for m in rng.permutation(MASCHINEN)]
|
||||||
|
for _ in range(AUFTRAEGE)]
|
||||||
|
|
||||||
|
|
||||||
|
def loese(auftraege, arbeiter: int):
|
||||||
|
"""Minimiert den Makespan. Gibt Status, Zielwert, Laufzeit und den Plan
|
||||||
|
zurueck - den Plan als Tupel aller Startzeiten, damit sich zwei Laeufe
|
||||||
|
vergleichen lassen."""
|
||||||
|
horizont = sum(dauer for auftrag in auftraege for _, dauer in auftrag)
|
||||||
|
modell = cp_model.CpModel()
|
||||||
|
Gang = collections.namedtuple("Gang", "start ende intervall")
|
||||||
|
plaene: dict[tuple[int, int], Gang] = {}
|
||||||
|
je_maschine = collections.defaultdict(list)
|
||||||
|
|
||||||
|
for a, auftrag in enumerate(auftraege):
|
||||||
|
for g, (maschine, dauer) in enumerate(auftrag):
|
||||||
|
start = modell.NewIntVar(0, horizont, f"start_{a}_{g}")
|
||||||
|
ende = modell.NewIntVar(0, horizont, f"ende_{a}_{g}")
|
||||||
|
intervall = modell.NewIntervalVar(start, dauer, ende, f"iv_{a}_{g}")
|
||||||
|
plaene[a, g] = Gang(start, ende, intervall)
|
||||||
|
je_maschine[maschine].append(intervall)
|
||||||
|
|
||||||
|
for maschine in range(MASCHINEN):
|
||||||
|
modell.AddNoOverlap(je_maschine[maschine])
|
||||||
|
for a, auftrag in enumerate(auftraege):
|
||||||
|
for g in range(len(auftrag) - 1):
|
||||||
|
modell.Add(plaene[a, g + 1].start >= plaene[a, g].ende)
|
||||||
|
|
||||||
|
makespan = modell.NewIntVar(0, horizont, "makespan")
|
||||||
|
modell.AddMaxEquality(
|
||||||
|
makespan, [plaene[a, len(auftrag) - 1].ende
|
||||||
|
for a, auftrag in enumerate(auftraege)])
|
||||||
|
modell.Minimize(makespan)
|
||||||
|
|
||||||
|
loeser = cp_model.CpSolver()
|
||||||
|
loeser.parameters.num_workers = arbeiter
|
||||||
|
loeser.parameters.random_seed = SOLVER_SEED
|
||||||
|
loeser.parameters.max_time_in_seconds = ZEITLIMIT
|
||||||
|
|
||||||
|
beginn = time.perf_counter()
|
||||||
|
status = loeser.Solve(modell)
|
||||||
|
dauer = time.perf_counter() - beginn
|
||||||
|
|
||||||
|
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
|
||||||
|
return loeser.StatusName(status), None, dauer, None
|
||||||
|
plan = tuple(loeser.Value(plaene[a, g].start)
|
||||||
|
for a, auftrag in enumerate(auftraege)
|
||||||
|
for g in range(len(auftrag)))
|
||||||
|
return loeser.StatusName(status), int(loeser.ObjectiveValue()), dauer, plan
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
auftraege = baue_instanz()
|
||||||
|
gaenge = sum(len(a) for a in auftraege)
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print(" WAS MEHRERE ARBEITER WIRKLICH BRINGEN")
|
||||||
|
print("=" * 78)
|
||||||
|
print(f"Job-Shop: {AUFTRAEGE} Auftraege, {MASCHINEN} Maschinen, "
|
||||||
|
f"{gaenge} Arbeitsgaenge.")
|
||||||
|
print(f"random_seed = {SOLVER_SEED} in JEDEM Lauf, "
|
||||||
|
f"{WIEDERHOLUNGEN} Wiederholungen je Arbeiterzahl.\n")
|
||||||
|
|
||||||
|
# Jede Konfiguration mehrfach - ein einzelner Zeitwert sagt nichts.
|
||||||
|
ergebnisse: dict[int, list[tuple]] = {}
|
||||||
|
for arbeiter in ARBEITERZAHLEN:
|
||||||
|
ergebnisse[arbeiter] = [loese(auftraege, arbeiter)
|
||||||
|
for _ in range(WIEDERHOLUNGEN)]
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print(" (1) Laufzeit: was die Arbeiter bringen")
|
||||||
|
print("=" * 78)
|
||||||
|
print(f"{'Arbeiter':>9} {'Status':>9} {'Makespan':>9} "
|
||||||
|
f"{'schnellste':>11} {'langsamste':>11} {'Beschleunigung':>15}")
|
||||||
|
print("-" * 78)
|
||||||
|
basis_zeit = min(d for _, _, d, _ in ergebnisse[1])
|
||||||
|
for arbeiter in ARBEITERZAHLEN:
|
||||||
|
laeufe = ergebnisse[arbeiter]
|
||||||
|
zeiten = [d for _, _, d, _ in laeufe]
|
||||||
|
ziele = {o for _, o, _, _ in laeufe}
|
||||||
|
status = {s for s, _, _, _ in laeufe}
|
||||||
|
print(f"{arbeiter:>9} {'/'.join(sorted(status)):>9} "
|
||||||
|
f"{'/'.join(str(z) for z in sorted(ziele)):>9} "
|
||||||
|
f"{min(zeiten):>10.2f}s {max(zeiten):>10.2f}s "
|
||||||
|
f"{basis_zeit / min(zeiten):>14.1f}x")
|
||||||
|
|
||||||
|
beste = min(ARBEITERZAHLEN, key=lambda w: min(d for _, _, d, _ in ergebnisse[w]))
|
||||||
|
faktor = basis_zeit / min(d for _, _, d, _ in ergebnisse[beste])
|
||||||
|
print(f"\n Am schnellsten sind {beste} Arbeiter: Faktor {faktor:.1f} gegenueber")
|
||||||
|
print(f" einem einzigen - also MEHR, als {beste} Kerne hergeben sollten.")
|
||||||
|
print(f" Das ist kein Messfehler. CP-SAT laesst nicht {beste}-mal dieselbe Suche")
|
||||||
|
print(" laufen, sondern verschiedene Strategien nebeneinander, die einander")
|
||||||
|
print(" ihre Schranken zurufen. Der zweite Arbeiter ist deshalb nicht der")
|
||||||
|
print(" 'zweite Kern', sondern ein anderes Verfahren.")
|
||||||
|
letzte = ARBEITERZAHLEN[-1]
|
||||||
|
if beste != letzte:
|
||||||
|
zeit_letzte = min(d for _, _, d, _ in ergebnisse[letzte])
|
||||||
|
print(f"\n Und mehr ist nicht immer besser: {letzte} Arbeiter brauchen "
|
||||||
|
f"{zeit_letzte:.2f}s")
|
||||||
|
print(f" gegenueber {min(d for _, _, d, _ in ergebnisse[beste]):.2f}s bei "
|
||||||
|
f"{beste}. Ab einer gewissen Zahl kosten Abstimmung")
|
||||||
|
print(" und Speicherbandbreite mehr, als ein weiterer Suchstrang einbringt.")
|
||||||
|
|
||||||
|
print("\n" + "=" * 78)
|
||||||
|
print(" (2) Reproduzierbarkeit: derselbe Seed, derselbe Plan?")
|
||||||
|
print("=" * 78)
|
||||||
|
print(f"{'Arbeiter':>9} {'Laeufe':>7} {'verschiedene Plaene':>21} "
|
||||||
|
f"{'Makespan':>10}")
|
||||||
|
print("-" * 78)
|
||||||
|
for arbeiter in ARBEITERZAHLEN:
|
||||||
|
plaene = {p for _, _, _, p in ergebnisse[arbeiter] if p is not None}
|
||||||
|
ziele = {o for _, o, _, _ in ergebnisse[arbeiter]}
|
||||||
|
print(f"{arbeiter:>9} {WIEDERHOLUNGEN:>7} {len(plaene):>21} "
|
||||||
|
f"{'/'.join(str(z) for z in sorted(ziele)):>10}")
|
||||||
|
|
||||||
|
einer = len({p for _, _, _, p in ergebnisse[1]})
|
||||||
|
# Ab welcher Arbeiterzahl bricht die Reproduzierbarkeit? Das ist die
|
||||||
|
# eigentliche Zahl - nicht, was acht Arbeiter anrichten.
|
||||||
|
ab = next((w for w in ARBEITERZAHLEN
|
||||||
|
if len({p for _, _, _, p in ergebnisse[w]}) > 1), None)
|
||||||
|
print(f"\n Mit einem Arbeiter: {einer} Plan aus {WIEDERHOLUNGEN} Laeufen.")
|
||||||
|
if ab is not None:
|
||||||
|
viele = len({p for _, _, _, p in ergebnisse[ab]})
|
||||||
|
print(f" Schon mit {ab} Arbeitern: {viele} verschiedene Plaene aus "
|
||||||
|
f"{WIEDERHOLUNGEN} Laeufen -")
|
||||||
|
print(" bei identischem random_seed und identischem Zielwert.")
|
||||||
|
print(f" Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit")
|
||||||
|
print(f" zu verlieren. {ab} genuegen.")
|
||||||
|
print("\n Der Seed allein macht einen Lauf also NICHT reproduzierbar. Er legt")
|
||||||
|
print(" fest, wie ein einzelner Suchstrang wuerfelt - nicht, welcher von")
|
||||||
|
print(" mehreren zuerst fertig wird. Das entscheidet die Uhr.")
|
||||||
|
|
||||||
|
print("\n" + "=" * 78)
|
||||||
|
print(" (3) Was daraus fuer Tests folgt")
|
||||||
|
print("=" * 78)
|
||||||
|
print(" Ein Test der Form assert plan == erwarteter_plan besteht mal und")
|
||||||
|
print(" scheitert mal, ohne dass sich am Code etwas geaendert haette.")
|
||||||
|
print(" Zu pruefen sind stattdessen:")
|
||||||
|
print(" * der ZIELWERT (hier in allen Laeufen gleich),")
|
||||||
|
print(" * die Einhaltung aller Regeln (Abnahmepruefung, Kapitel Praxisfallen),")
|
||||||
|
print(" * und der Status - nicht die Gestalt der Loesung.")
|
||||||
|
print("\n Wer doch einen bestimmten Plan braucht - fuer eine abgedruckte")
|
||||||
|
print(" Ausgabe, fuer einen Regressionstest -, setzt num_workers = 1.")
|
||||||
|
print(f" Das kostet hier den Faktor {faktor:.1f} an Laufzeit und ist genau")
|
||||||
|
print(" deshalb eine Entscheidung fuer den Test, nicht fuer den Betrieb.")
|
||||||
|
print("=" * 78)
|
||||||
|
|
@ -83,7 +83,8 @@ def loese(dauer: np.ndarray, n_maschinen: int, zeitlimit: float,
|
||||||
|
|
||||||
loeser = cp_model.CpSolver()
|
loeser = cp_model.CpSolver()
|
||||||
loeser.parameters.max_time_in_seconds = zeitlimit
|
loeser.parameters.max_time_in_seconds = zeitlimit
|
||||||
# Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist.
|
# Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist -
|
||||||
|
# der Seed allein genuegt dafuer NICHT (Kapitel Constraint Programming).
|
||||||
# Im Produktivbetrieb laesst man beides auf den Standardwerten.
|
# Im Produktivbetrieb laesst man beides auf den Standardwerten.
|
||||||
loeser.parameters.num_workers = 1
|
loeser.parameters.num_workers = 1
|
||||||
loeser.parameters.random_seed = 1
|
loeser.parameters.random_seed = 1
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -519,241 +519,246 @@ Alles bereit — Sie können mit dem ersten Kapitel beginnen.</code></pre>
|
||||||
<td><a href="cpsat.html#kap-cpsat">Kapitel 7</a></td>
|
<td><a href="cpsat.html#kap-cpsat">Kapitel 7</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="even">
|
||||||
|
<td><code>Parallele_Suche.py</code></td>
|
||||||
|
<td>num_workers: Tempo gegen Reproduzierbarkeit</td>
|
||||||
|
<td><a href="cpsat.html#kap-cpsat">Kapitel 7</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="odd">
|
||||||
<td><code>CP_SAT_Statusfaelle.py</code></td>
|
<td><code>CP_SAT_Statusfaelle.py</code></td>
|
||||||
<td>Die fünf Solver-Antworten</td>
|
<td>Die fünf Solver-Antworten</td>
|
||||||
<td><a href="cpsat.html#kap-cpsat">Kapitel 7</a></td>
|
<td><a href="cpsat.html#kap-cpsat">Kapitel 7</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Strafgewichte.py</code></td>
|
<td><code>Strafgewichte.py</code></td>
|
||||||
<td>Gewichte als Wechselkurse</td>
|
<td>Gewichte als Wechselkurse</td>
|
||||||
<td><a href="cpsat.html#kap-cpsat">Kapitel 7</a></td>
|
<td><a href="cpsat.html#kap-cpsat">Kapitel 7</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Min_Cost_Flow.py</code></td>
|
<td><code>Min_Cost_Flow.py</code></td>
|
||||||
<td>Netzwerkfluss</td>
|
<td>Netzwerkfluss</td>
|
||||||
<td><a href="graphen.html#kap-graphen">Kapitel 8</a></td>
|
<td><a href="graphen.html#kap-graphen">Kapitel 8</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Zuordnung_Ungarisch.py</code></td>
|
<td><code>Zuordnung_Ungarisch.py</code></td>
|
||||||
<td>Zuordnung, Unimodularität</td>
|
<td>Zuordnung, Unimodularität</td>
|
||||||
<td><a href="graphen.html#kap-graphen">Kapitel 8</a></td>
|
<td><a href="graphen.html#kap-graphen">Kapitel 8</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>VRP_Flotten_Routing.py</code></td>
|
<td><code>VRP_Flotten_Routing.py</code></td>
|
||||||
<td>CVRPTW</td>
|
<td>CVRPTW</td>
|
||||||
<td><a href="graphen.html#kap-graphen">Kapitel 8</a></td>
|
<td><a href="graphen.html#kap-graphen">Kapitel 8</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>VRP_Kapazitaetsfalle.py</code></td>
|
<td><code>VRP_Kapazitaetsfalle.py</code></td>
|
||||||
<td>Vergessene Dimension im Routing</td>
|
<td>Vergessene Dimension im Routing</td>
|
||||||
<td><a href="graphen.html#kap-graphen">Kapitel 8</a></td>
|
<td><a href="graphen.html#kap-graphen">Kapitel 8</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Simulated_Annealing.py</code></td>
|
<td><code>Simulated_Annealing.py</code></td>
|
||||||
<td>Lokale Suche, Temperatur kalibrieren</td>
|
<td>Lokale Suche, Temperatur kalibrieren</td>
|
||||||
<td><a href="metaheuristiken.html#kap-metaheuristiken">Kapitel 9</a></td>
|
<td><a href="metaheuristiken.html#kap-metaheuristiken">Kapitel 9</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Metaheuristik_vs_Exakt.py</code></td>
|
<td><code>Metaheuristik_vs_Exakt.py</code></td>
|
||||||
<td>Der Umschlagpunkt, und die Schranke</td>
|
<td>Der Umschlagpunkt, und die Schranke</td>
|
||||||
<td><a href="metaheuristiken.html#kap-metaheuristiken">Kapitel 9</a></td>
|
<td><a href="metaheuristiken.html#kap-metaheuristiken">Kapitel 9</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Large_Neighborhood_Search.py</code></td>
|
<td><code>Large_Neighborhood_Search.py</code></td>
|
||||||
<td>Zerstören und exakt reparieren</td>
|
<td>Zerstören und exakt reparieren</td>
|
||||||
<td><a href="metaheuristiken.html#kap-metaheuristiken">Kapitel 9</a></td>
|
<td><a href="metaheuristiken.html#kap-metaheuristiken">Kapitel 9</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Spaltengenerierung.py</code></td>
|
<td><code>Spaltengenerierung.py</code></td>
|
||||||
<td>Muster statt Stücke, Master und Pricing</td>
|
<td>Muster statt Stücke, Master und Pricing</td>
|
||||||
<td><a href="dekomposition.html#kap-dekomposition">Kapitel 10</a></td>
|
<td><a href="dekomposition.html#kap-dekomposition">Kapitel 10</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>QP_Grundlagen.py</code></td>
|
<td><code>QP_Grundlagen.py</code></td>
|
||||||
<td>Konvexität, DCP-Check</td>
|
<td>Konvexität, DCP-Check</td>
|
||||||
<td><a href="qp-nlp.html#kap-qp-nlp">Kapitel 11</a></td>
|
<td><a href="qp-nlp.html#kap-qp-nlp">Kapitel 11</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>KKT_Nachweis.py</code></td>
|
<td><code>KKT_Nachweis.py</code></td>
|
||||||
<td>KKT numerisch prüfen</td>
|
<td>KKT numerisch prüfen</td>
|
||||||
<td><a href="qp-nlp.html#kap-qp-nlp">Kapitel 11</a></td>
|
<td><a href="qp-nlp.html#kap-qp-nlp">Kapitel 11</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Entropie_Maximierte_Allokation.py</code></td>
|
<td><code>Entropie_Maximierte_Allokation.py</code></td>
|
||||||
<td>NLP mit Entropie</td>
|
<td>NLP mit Entropie</td>
|
||||||
<td><a href="qp-nlp.html#kap-qp-nlp">Kapitel 11</a></td>
|
<td><a href="qp-nlp.html#kap-qp-nlp">Kapitel 11</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Lokale_Optima_Multistart.py</code></td>
|
<td><code>Lokale_Optima_Multistart.py</code></td>
|
||||||
<td>Lokale Optima, Multistart</td>
|
<td>Lokale Optima, Multistart</td>
|
||||||
<td><a href="qp-nlp.html#kap-qp-nlp">Kapitel 11</a></td>
|
<td><a href="qp-nlp.html#kap-qp-nlp">Kapitel 11</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Fluch_des_Durchschnitts.py</code></td>
|
<td><code>Fluch_des_Durchschnitts.py</code></td>
|
||||||
<td>Optimum ≠ Mittelwert</td>
|
<td>Optimum ≠ Mittelwert</td>
|
||||||
<td><a href="unsicherheit.html#kap-unsicherheit">Kapitel 12</a></td>
|
<td><a href="unsicherheit.html#kap-unsicherheit">Kapitel 12</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Monte_Carlo.py</code></td>
|
<td><code>Monte_Carlo.py</code></td>
|
||||||
<td>Monte-Carlo-Bewertung</td>
|
<td>Monte-Carlo-Bewertung</td>
|
||||||
<td><a href="unsicherheit.html#kap-unsicherheit">Kapitel 12</a></td>
|
<td><a href="unsicherheit.html#kap-unsicherheit">Kapitel 12</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Stochastische_Optimierung.py</code></td>
|
<td><code>Stochastische_Optimierung.py</code></td>
|
||||||
<td>Two-Stage mit Recourse</td>
|
<td>Two-Stage mit Recourse</td>
|
||||||
<td><a href="unsicherheit.html#kap-unsicherheit">Kapitel 12</a></td>
|
<td><a href="unsicherheit.html#kap-unsicherheit">Kapitel 12</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Robuste_Optimierung.py</code></td>
|
<td><code>Robuste_Optimierung.py</code></td>
|
||||||
<td>Worst-Case-Absicherung</td>
|
<td>Worst-Case-Absicherung</td>
|
||||||
<td><a href="unsicherheit.html#kap-unsicherheit">Kapitel 12</a></td>
|
<td><a href="unsicherheit.html#kap-unsicherheit">Kapitel 12</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Chance_Constraints.py</code></td>
|
<td><code>Chance_Constraints.py</code></td>
|
||||||
<td>Zusage „mit 95 % Sicherheit“, SOC und Big-M</td>
|
<td>Zusage „mit 95 % Sicherheit“, SOC und Big-M</td>
|
||||||
<td><a href="unsicherheit.html#kap-unsicherheit">Kapitel 12</a></td>
|
<td><a href="unsicherheit.html#kap-unsicherheit">Kapitel 12</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Bellman_Minimalbeispiel.py</code></td>
|
<td><code>Bellman_Minimalbeispiel.py</code></td>
|
||||||
<td>Rückwärtsinduktion</td>
|
<td>Rückwärtsinduktion</td>
|
||||||
<td><a href="dynamische-programmierung.html#kap-dynamische-programmierung">Kapitel 13</a></td>
|
<td><a href="dynamische-programmierung.html#kap-dynamische-programmierung">Kapitel 13</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Mehrziel_Pareto.py</code></td>
|
<td><code>Mehrziel_Pareto.py</code></td>
|
||||||
<td>Pareto-Front, ε-Constraint, Gewichtslücke</td>
|
<td>Pareto-Front, ε-Constraint, Gewichtslücke</td>
|
||||||
<td><a href="mehrziel.html#kap-mehrziel">Kapitel 14</a></td>
|
<td><a href="mehrziel.html#kap-mehrziel">Kapitel 14</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Predict_then_Optimize.py</code></td>
|
<td><code>Predict_then_Optimize.py</code></td>
|
||||||
<td>MSE gegen Entscheidungskosten</td>
|
<td>MSE gegen Entscheidungskosten</td>
|
||||||
<td><a href="prognose.html#kap-prognose">Kapitel 15</a></td>
|
<td><a href="prognose.html#kap-prognose">Kapitel 15</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Strukturbruecke.py</code></td>
|
<td><code>Strukturbruecke.py</code></td>
|
||||||
<td>Derselbe Code über Werkstatt und Depot</td>
|
<td>Derselbe Code über Werkstatt und Depot</td>
|
||||||
<td><a href="bruecke.html#kap-bruecke">Kapitel 16</a></td>
|
<td><a href="bruecke.html#kap-bruecke">Kapitel 16</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Kraftwerkseinsatz.py</code></td>
|
<td><code>Kraftwerkseinsatz.py</code></td>
|
||||||
<td>Unit Commitment unter Windunsicherheit</td>
|
<td>Unit Commitment unter Windunsicherheit</td>
|
||||||
<td><a href="supplychain.html#kap-supplychain">Kapitel 17</a></td>
|
<td><a href="supplychain.html#kap-supplychain">Kapitel 17</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Mehrperiodige_Order_Execution.py</code></td>
|
<td><code>Mehrperiodige_Order_Execution.py</code></td>
|
||||||
<td>Almgren-Chriss</td>
|
<td>Almgren-Chriss</td>
|
||||||
<td><a href="dynamische-programmierung.html#kap-dynamische-programmierung">Kapitel 13</a></td>
|
<td><a href="dynamische-programmierung.html#kap-dynamische-programmierung">Kapitel 13</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Renditen_Vergleich.py</code></td>
|
<td><code>Renditen_Vergleich.py</code></td>
|
||||||
<td>Diskret vs. logarithmisch</td>
|
<td>Diskret vs. logarithmisch</td>
|
||||||
<td><a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a></td>
|
<td><a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Schaetzrauschen_Demo.py</code></td>
|
<td><code>Schaetzrauschen_Demo.py</code></td>
|
||||||
<td>Error-Maximizer messen</td>
|
<td>Error-Maximizer messen</td>
|
||||||
<td><a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a></td>
|
<td><a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Finanzdaten_Ledoit_Wolf.py</code></td>
|
<td><code>Finanzdaten_Ledoit_Wolf.py</code></td>
|
||||||
<td>Datenpipeline, Shrinkage</td>
|
<td>Datenpipeline, Shrinkage</td>
|
||||||
<td><a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a></td>
|
<td><a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Kovarianz_Falle.py</code></td>
|
<td><code>Kovarianz_Falle.py</code></td>
|
||||||
<td>Singuläre Kovarianz, Error-Maximizer</td>
|
<td>Singuläre Kovarianz, Error-Maximizer</td>
|
||||||
<td><a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a></td>
|
<td><a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Markowitz_CVXPY.py</code></td>
|
<td><code>Markowitz_CVXPY.py</code></td>
|
||||||
<td>GMV, Max Sharpe, Frontier</td>
|
<td>GMV, Max Sharpe, Frontier</td>
|
||||||
<td><a href="markowitz.html#kap-markowitz">Kapitel 19</a></td>
|
<td><a href="markowitz.html#kap-markowitz">Kapitel 19</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Diversifikation_Demo.py</code></td>
|
<td><code>Diversifikation_Demo.py</code></td>
|
||||||
<td>Korrelation und Portfoliorisiko</td>
|
<td>Korrelation und Portfoliorisiko</td>
|
||||||
<td><a href="markowitz.html#kap-markowitz">Kapitel 19</a></td>
|
<td><a href="markowitz.html#kap-markowitz">Kapitel 19</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Renditeschaetzung_Falle.py</code></td>
|
<td><code>Renditeschaetzung_Falle.py</code></td>
|
||||||
<td>Schätzfehler in erwarteten Renditen</td>
|
<td>Schätzfehler in erwarteten Renditen</td>
|
||||||
<td><a href="markowitz.html#kap-markowitz">Kapitel 19</a></td>
|
<td><a href="markowitz.html#kap-markowitz">Kapitel 19</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>VaR_CVaR_Demo.py</code></td>
|
<td><code>VaR_CVaR_Demo.py</code></td>
|
||||||
<td>Fat Tails, Subadditivität</td>
|
<td>Fat Tails, Subadditivität</td>
|
||||||
<td><a href="cvar.html#kap-cvar">Kapitel 20</a></td>
|
<td><a href="cvar.html#kap-cvar">Kapitel 20</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>CVaR_Portfolio.py</code></td>
|
<td><code>CVaR_Portfolio.py</code></td>
|
||||||
<td>CVaR mit Reibung</td>
|
<td>CVaR mit Reibung</td>
|
||||||
<td><a href="cvar.html#kap-cvar">Kapitel 20</a></td>
|
<td><a href="cvar.html#kap-cvar">Kapitel 20</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>QuantitativeTradingEngine.py</code></td>
|
<td><code>QuantitativeTradingEngine.py</code></td>
|
||||||
<td>Walk-Forward-Backtest</td>
|
<td>Walk-Forward-Backtest</td>
|
||||||
<td><a href="handelsmaschine.html#kap-handelsmaschine">Kapitel 21</a></td>
|
<td><a href="handelsmaschine.html#kap-handelsmaschine">Kapitel 21</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Backtest_Fallen.py</code></td>
|
<td><code>Backtest_Fallen.py</code></td>
|
||||||
<td>Fünf Selbsttäuschungen</td>
|
<td>Fünf Selbsttäuschungen</td>
|
||||||
<td><a href="handelsmaschine.html#kap-handelsmaschine">Kapitel 21</a></td>
|
<td><a href="handelsmaschine.html#kap-handelsmaschine">Kapitel 21</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Data_Snooping.py</code></td>
|
<td><code>Data_Snooping.py</code></td>
|
||||||
<td>Bestes aus N Versuchen auf Rauschen</td>
|
<td>Bestes aus N Versuchen auf Rauschen</td>
|
||||||
<td><a href="handelsmaschine.html#kap-handelsmaschine">Kapitel 21</a></td>
|
<td><a href="handelsmaschine.html#kap-handelsmaschine">Kapitel 21</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Infeasibility_Diagnose.py</code></td>
|
<td><code>Infeasibility_Diagnose.py</code></td>
|
||||||
<td>Notfallplan statt Fehler</td>
|
<td>Notfallplan statt Fehler</td>
|
||||||
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
|
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Erklaerbarkeit.py</code></td>
|
<td><code>Erklaerbarkeit.py</code></td>
|
||||||
<td>Constraint-Trace, Was-wäre-wenn</td>
|
<td>Constraint-Trace, Was-wäre-wenn</td>
|
||||||
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
|
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Constraint_Attribution.py</code></td>
|
<td><code>Constraint_Attribution.py</code></td>
|
||||||
<td>Was kostet welche Bedingung? Managementbericht</td>
|
<td>Was kostet welche Bedingung? Managementbericht</td>
|
||||||
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
|
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Betriebsueberwachung.py</code></td>
|
<td><code>Betriebsueberwachung.py</code></td>
|
||||||
<td>Status, Gap und Zeitausschöpfung</td>
|
<td>Status, Gap und Zeitausschöpfung</td>
|
||||||
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
|
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>or_kern.py</code></td>
|
<td><code>or_kern.py</code></td>
|
||||||
<td>Gemeinsamer Unterbau: Domäne, Status, Prüfung</td>
|
<td>Gemeinsamer Unterbau: Domäne, Status, Prüfung</td>
|
||||||
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
|
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Solverwechsel_CPSAT_HiGHS.py</code></td>
|
<td><code>Solverwechsel_CPSAT_HiGHS.py</code></td>
|
||||||
<td>Derselbe Fall in zwei Solvern</td>
|
<td>Derselbe Fall in zwei Solvern</td>
|
||||||
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
|
<td><a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>test_or_kern.py</code></td>
|
<td><code>test_or_kern.py</code></td>
|
||||||
<td>Testsuite für ein Optimierungsmodell</td>
|
<td>Testsuite für ein Optimierungsmodell</td>
|
||||||
<td><a href="testing.html#kap-testing">Kapitel 23</a></td>
|
<td><a href="testing.html#kap-testing">Kapitel 23</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Mutationstest.py</code></td>
|
<td><code>Mutationstest.py</code></td>
|
||||||
<td>Findet die Lücken der eigenen Tests</td>
|
<td>Findet die Lücken der eigenen Tests</td>
|
||||||
<td><a href="testing.html#kap-testing">Kapitel 23</a></td>
|
<td><a href="testing.html#kap-testing">Kapitel 23</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Benchmark_Skalierung.py</code></td>
|
<td><code>Benchmark_Skalierung.py</code></td>
|
||||||
<td>Aufbau gegen Lösen, vier Bibliotheken</td>
|
<td>Aufbau gegen Lösen, vier Bibliotheken</td>
|
||||||
<td><a href="testing.html#kap-testing">Kapitel 23</a></td>
|
<td><a href="testing.html#kap-testing">Kapitel 23</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="even">
|
<tr class="odd">
|
||||||
<td><code>Optimierungsdienst.py</code></td>
|
<td><code>Optimierungsdienst.py</code></td>
|
||||||
<td>Das Modell als HTTP-Dienst</td>
|
<td>Das Modell als HTTP-Dienst</td>
|
||||||
<td><a href="testing.html#kap-testing">Kapitel 23</a></td>
|
<td><a href="testing.html#kap-testing">Kapitel 23</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="odd">
|
<tr class="even">
|
||||||
<td><code>Konfliktsuche.py</code></td>
|
<td><code>Konfliktsuche.py</code></td>
|
||||||
<td>Deletion Filter: den Widerspruch einkreisen</td>
|
<td>Deletion Filter: den Widerspruch einkreisen</td>
|
||||||
<td><a href="anhang-fehlerdiagnose.html#anhang-fehlerdiagnose">Anhang C</a></td>
|
<td><a href="anhang-fehlerdiagnose.html#anhang-fehlerdiagnose">Anhang C</a></td>
|
||||||
|
|
|
||||||
|
|
@ -341,6 +341,7 @@ Alle Beispielprogramme dieses Buchs im Überblick, sortiert nach Kapitel — pra
|
||||||
| `Propagation_Demo.py` | Propagation messbar | [Kapitel 7](#kap-cpsat) |
|
| `Propagation_Demo.py` | Propagation messbar | [Kapitel 7](#kap-cpsat) |
|
||||||
| `CP_SAT_Vertretungssystem.py` | Vertretungsplan | [Kapitel 7](#kap-cpsat) |
|
| `CP_SAT_Vertretungssystem.py` | Vertretungsplan | [Kapitel 7](#kap-cpsat) |
|
||||||
| `JobShop_Intervalle.py` | Job-Shop-Scheduling | [Kapitel 7](#kap-cpsat) |
|
| `JobShop_Intervalle.py` | Job-Shop-Scheduling | [Kapitel 7](#kap-cpsat) |
|
||||||
|
| `Parallele_Suche.py` | num_workers: Tempo gegen Reproduzierbarkeit | [Kapitel 7](#kap-cpsat) |
|
||||||
| `CP_SAT_Statusfaelle.py` | Die fünf Solver-Antworten | [Kapitel 7](#kap-cpsat) |
|
| `CP_SAT_Statusfaelle.py` | Die fünf Solver-Antworten | [Kapitel 7](#kap-cpsat) |
|
||||||
| `Strafgewichte.py` | Gewichte als Wechselkurse | [Kapitel 7](#kap-cpsat) |
|
| `Strafgewichte.py` | Gewichte als Wechselkurse | [Kapitel 7](#kap-cpsat) |
|
||||||
| `Min_Cost_Flow.py` | Netzwerkfluss | [Kapitel 8](#kap-graphen) |
|
| `Min_Cost_Flow.py` | Netzwerkfluss | [Kapitel 8](#kap-graphen) |
|
||||||
|
|
@ -4412,7 +4413,7 @@ Dieselbe Regel als Strafkosten je abgelehntem Stammkundenauftrag:
|
||||||
|
|
||||||
Bei 3 000 € ändert sich **nichts**: Die Strafe liegt unter dem Deckungsbeitrag, den die Stammkundenaufträge verdrängen würden. Bei 10 000 € kommt genau Lesart A heraus.
|
Bei 3 000 € ändert sich **nichts**: Die Strafe liegt unter dem Deckungsbeitrag, den die Stammkundenaufträge verdrängen würden. Bei 10 000 € kommt genau Lesart A heraus.
|
||||||
|
|
||||||
Daraus folgt eine Einsicht, die das Modellieren erleichtert: **Eine harte Regel ist eine weiche mit hinreichend großer Strafe.** Man kann also immer weich anfangen und die Strafe hochdrehen, bis die Regel greift — und sieht dabei, was sie kostet. Was ein Strafgewicht darüber hinaus bedeutet und warum zwei Gewichte einen *Wechselkurs* festlegen und keine Wichtigkeiten, steht in [Abschnitt 7.9](#sec:cpsat-denkfehler).
|
Daraus folgt eine Einsicht, die das Modellieren erleichtert: **Eine harte Regel ist eine weiche mit hinreichend großer Strafe.** Man kann also immer weich anfangen und die Strafe hochdrehen, bis die Regel greift — und sieht dabei, was sie kostet. Was ein Strafgewicht darüber hinaus bedeutet und warum zwei Gewichte einen *Wechselkurs* festlegen und keine Wichtigkeiten, steht in [Abschnitt 7.10](#sec:cpsat-denkfehler).
|
||||||
|
|
||||||
### Teil 4: Ein Quartal später
|
### Teil 4: Ein Quartal später
|
||||||
|
|
||||||
|
|
@ -6909,7 +6910,8 @@ def loese(dauer: np.ndarray, n_maschinen: int, zeitlimit: float,
|
||||||
|
|
||||||
loeser = cp_model.CpSolver()
|
loeser = cp_model.CpSolver()
|
||||||
loeser.parameters.max_time_in_seconds = zeitlimit
|
loeser.parameters.max_time_in_seconds = zeitlimit
|
||||||
# Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist.
|
# Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist -
|
||||||
|
# der Seed allein genuegt dafuer NICHT (Kapitel Constraint Programming).
|
||||||
# Im Produktivbetrieb laesst man beides auf den Standardwerten.
|
# Im Produktivbetrieb laesst man beides auf den Standardwerten.
|
||||||
loeser.parameters.num_workers = 1
|
loeser.parameters.num_workers = 1
|
||||||
loeser.parameters.random_seed = 1
|
loeser.parameters.random_seed = 1
|
||||||
|
|
@ -7366,6 +7368,7 @@ funktioniert immer.
|
||||||
> `Propagation_Demo.py`\
|
> `Propagation_Demo.py`\
|
||||||
> `CP_SAT_Vertretungssystem.py`\
|
> `CP_SAT_Vertretungssystem.py`\
|
||||||
> `JobShop_Intervalle.py`\
|
> `JobShop_Intervalle.py`\
|
||||||
|
> `Parallele_Suche.py`\
|
||||||
> `CP_SAT_Statusfaelle.py`\
|
> `CP_SAT_Statusfaelle.py`\
|
||||||
> `Strafgewichte.py`
|
> `Strafgewichte.py`
|
||||||
>
|
>
|
||||||
|
|
@ -7419,7 +7422,7 @@ funktioniert immer.
|
||||||
|
|
||||||
Nein — und das ist die wichtigste Lektion dieses Kapitels. Der Plan erfüllt **jede** Regel, die wir aufgeschrieben haben. Wir haben nur nie nach Fairness gefragt, und wir haben nie gesagt, dass Dienste nicht aufeinanderfolgen sollen. Ein Solver liefert exakt das Bestellte — nicht das Gemeinte.
|
Nein — und das ist die wichtigste Lektion dieses Kapitels. Der Plan erfüllt **jede** Regel, die wir aufgeschrieben haben. Wir haben nur nie nach Fairness gefragt, und wir haben nie gesagt, dass Dienste nicht aufeinanderfolgen sollen. Ein Solver liefert exakt das Bestellte — nicht das Gemeinte.
|
||||||
|
|
||||||
> **🎯 Merksatz** Ein Modell, das durchläuft, ist noch kein richtiges Modell. Die eigentliche Arbeit im Constraint Programming ist nicht das Lösen, sondern das vollständige Aufschreiben dessen, was „ein guter Plan“ überhaupt heißt. Wie man Wünsche wie Fairness als **weiche** Bedingungen mit Strafkosten formuliert, zeigt [Abschnitt 7.5](#sec:cpsat-praxisfall-dynamisches-vertretungssystem) — und [Abschnitt 7.9](#sec:cpsat-denkfehler) zeigt, was passiert, wenn man ihr Gewicht falsch wählt.
|
> **🎯 Merksatz** Ein Modell, das durchläuft, ist noch kein richtiges Modell. Die eigentliche Arbeit im Constraint Programming ist nicht das Lösen, sondern das vollständige Aufschreiben dessen, was „ein guter Plan“ überhaupt heißt. Wie man Wünsche wie Fairness als **weiche** Bedingungen mit Strafkosten formuliert, zeigt [Abschnitt 7.5](#sec:cpsat-praxisfall-dynamisches-vertretungssystem) — und [Abschnitt 7.10](#sec:cpsat-denkfehler) zeigt, was passiert, wenn man ihr Gewicht falsch wählt.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -7433,7 +7436,8 @@ Nach diesem Kapitel können Sie …
|
||||||
4. … mit **Intervallvariablen**`\index{Intervallvariable}`{=latex} ein Maschinenbelegungsproblem modellieren.
|
4. … mit **Intervallvariablen**`\index{Intervallvariable}`{=latex} ein Maschinenbelegungsproblem modellieren.
|
||||||
5. … harte Regeln und weiche Ziele in einem Modell kombinieren und gewichten.
|
5. … harte Regeln und weiche Ziele in einem Modell kombinieren und gewichten.
|
||||||
6. … die fünf CP-SAT-Statusfälle unterscheiden — insbesondere `INFEASIBLE` (es gibt keine Lösung) von `UNKNOWN` (es wurde keine gefunden) und `MODEL_INVALID` (Ihr Code ist fehlerhaft).
|
6. … die fünf CP-SAT-Statusfälle unterscheiden — insbesondere `INFEASIBLE` (es gibt keine Lösung) von `UNKNOWN` (es wurde keine gefunden) und `MODEL_INVALID` (Ihr Code ist fehlerhaft).
|
||||||
7. … begründen, warum ein Strafgewicht ein **Wechselkurs** ist, und welche Regeln deshalb niemals in die Zielfunktion gehören.
|
7. … begründen, warum `num_workers = 1` für einen reproduzierbaren Lauf nötig ist und ein fester `random_seed` allein nicht genügt — und was ein Test deshalb prüfen darf.
|
||||||
|
8. … begründen, warum ein Strafgewicht ein **Wechselkurs** ist, und welche Regeln deshalb niemals in die Zielfunktion gehören.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -8064,6 +8068,8 @@ Erreichter Makespan: 11
|
||||||
>
|
>
|
||||||
> Deshalb setzt das Programm oben `num_workers = 1` und `random_seed = 1`: nur so ist die abgedruckte Ausgabe reproduzierbar. **Im Produktivbetrieb lässt man beides weg** — mehrere Arbeiter sind deutlich schneller.
|
> Deshalb setzt das Programm oben `num_workers = 1` und `random_seed = 1`: nur so ist die abgedruckte Ausgabe reproduzierbar. **Im Produktivbetrieb lässt man beides weg** — mehrere Arbeiter sind deutlich schneller.
|
||||||
>
|
>
|
||||||
|
> Wie viel schneller, und warum der Seed allein **nicht** genügt, misst [Abschnitt 7.7](#sec:cpsat-parallele-suche) an genau diesem Modell.
|
||||||
|
>
|
||||||
> Die Lehre für Ihre eigenen Tests: Prüfen Sie bei mehrdeutigen Optima nie auf einen **bestimmten Plan**, sondern immer auf den **Zielwert** und auf die Einhaltung aller Regeln. Ein Test wie `assert plan == erwarteter_plan` besteht sonst mal und scheitert mal, ohne dass sich am Code etwas geändert hätte.
|
> Die Lehre für Ihre eigenen Tests: Prüfen Sie bei mehrdeutigen Optima nie auf einen **bestimmten Plan**, sondern immer auf den **Zielwert** und auf die Einhaltung aller Regeln. Ein Test wie `assert plan == erwarteter_plan` besteht sonst mal und scheitert mal, ohne dass sich am Code etwas geändert hätte.
|
||||||
|
|
||||||
Bemerkenswert ist die letzte Zeile: Die untere Schranke liegt bei 10, erreicht wurden 11. Die Schranke ist also **nicht erreichbar** — kein Plan kann 10 schaffen, obwohl weder ein einzelner Auftrag noch eine einzelne Maschine mehr als 10 Zeiteinheiten braucht. Der Grund sind die Wechselwirkungen. Das ist typisch für Scheduling: **Einfache Schranken sind selten scharf.** Trotzdem sind sie nützlich — sie sagten uns hier, dass wir höchstens 10 % vom theoretischen Ideal entfernt sein können, noch bevor der Solver fertig war.
|
Bemerkenswert ist die letzte Zeile: Die untere Schranke liegt bei 10, erreicht wurden 11. Die Schranke ist also **nicht erreichbar** — kein Plan kann 10 schaffen, obwohl weder ein einzelner Auftrag noch eine einzelne Maschine mehr als 10 Zeiteinheiten braucht. Der Grund sind die Wechselwirkungen. Das ist typisch für Scheduling: **Einfache Schranken sind selten scharf.** Trotzdem sind sie nützlich — sie sagten uns hier, dass wir höchstens 10 % vom theoretischen Ideal entfernt sein können, noch bevor der Solver fertig war.
|
||||||
|
|
@ -8074,7 +8080,302 @@ Bemerkenswert ist die letzte Zeile: Die untere Schranke liegt bei 10, erreicht w
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7.7 Die fünf Antworten von CP-SAT {#sec:cpsat-statusfaelle}
|
## 7.7 Parallele Suche: was `num_workers` wirklich bewirkt {#sec:cpsat-parallele-suche}
|
||||||
|
|
||||||
|
An mehreren Stellen dieses Buchs steht `loeser.parameters.num_workers = 1`, mit dem Kommentar „für eine reproduzierbare Ausgabe“. Darin stecken zwei Behauptungen: dass parallele Suche **schneller** ist, und dass sie **von Lauf zu Lauf andere, gleich gute** Lösungen liefert. Beides lässt sich messen — und beim Messen kommt mehr heraus als erwartet.
|
||||||
|
|
||||||
|
Gerechnet wird auf demselben Modell wie [Abschnitt 7.6](#sec:cpsat-intervallvariablen-job-shop-scheduling), nur groß genug, dass die Suche wirklich arbeitet: 12 Aufträge auf 10 Maschinen, jeder Auftrag besucht jede Maschine. Entscheidend ist, dass `random_seed` in **jedem** Lauf denselben Wert hat.
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Parallele_Suche.py
|
||||||
|
"""
|
||||||
|
Kapitel CP-SAT: Was mehrere Arbeiter wirklich bringen - und was sie kosten.
|
||||||
|
|
||||||
|
Das Buch setzt an mehreren Stellen 'num_workers = 1', damit die abgedruckte
|
||||||
|
Ausgabe reproduzierbar ist. Behauptet wird dabei zweierlei: dass parallele
|
||||||
|
Suche schneller ist, und dass sie von Lauf zu Lauf verschiedene, gleich gute
|
||||||
|
Loesungen findet. Beides wird hier gemessen statt geglaubt.
|
||||||
|
|
||||||
|
Gerechnet wird auf einem Job-Shop wie in JobShop_Intervalle.py, nur gross
|
||||||
|
genug, dass die Suche wirklich arbeitet: 12 Auftraege auf 10 Maschinen,
|
||||||
|
jeder Auftrag besucht jede Maschine.
|
||||||
|
|
||||||
|
Zwei Messungen:
|
||||||
|
1. Dieselbe Aufgabe mit 1, 2, 4 und 8 Arbeitern - Laufzeit und Ergebnis.
|
||||||
|
2. Dieselbe Konfiguration mehrfach, bei FESTEM random_seed - wie oft kommt
|
||||||
|
derselbe Plan heraus?
|
||||||
|
|
||||||
|
Die zweite ist die wichtigere: Sie entscheidet, wie man Tests schreibt.
|
||||||
|
|
||||||
|
Achtung, Laufzeiten sind hardwareabhaengig. Die ZAHL der verschiedenen
|
||||||
|
Plaene ist es auch - und genau das ist die Aussage.
|
||||||
|
|
||||||
|
Benoetigt: numpy, ortools
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from ortools.sat.python import cp_model
|
||||||
|
|
||||||
|
AUFTRAEGE = 12
|
||||||
|
MASCHINEN = 10
|
||||||
|
INSTANZ_SEED = 20260908
|
||||||
|
SOLVER_SEED = 1 # bleibt ueber ALLE Laeufe gleich - das ist der Punkt
|
||||||
|
ARBEITERZAHLEN = (1, 2, 4, 8)
|
||||||
|
WIEDERHOLUNGEN = 4
|
||||||
|
ZEITLIMIT = 120.0
|
||||||
|
|
||||||
|
|
||||||
|
def baue_instanz() -> list[list[tuple[int, int]]]:
|
||||||
|
"""Klassischer Job-Shop: Jeder Auftrag besucht jede Maschine genau einmal,
|
||||||
|
in einer eigenen zufaelligen Reihenfolge."""
|
||||||
|
rng = np.random.default_rng(INSTANZ_SEED)
|
||||||
|
return [[(int(m), int(rng.integers(2, 20))) for m in rng.permutation(MASCHINEN)]
|
||||||
|
for _ in range(AUFTRAEGE)]
|
||||||
|
|
||||||
|
|
||||||
|
def loese(auftraege, arbeiter: int):
|
||||||
|
"""Minimiert den Makespan. Gibt Status, Zielwert, Laufzeit und den Plan
|
||||||
|
zurueck - den Plan als Tupel aller Startzeiten, damit sich zwei Laeufe
|
||||||
|
vergleichen lassen."""
|
||||||
|
horizont = sum(dauer for auftrag in auftraege for _, dauer in auftrag)
|
||||||
|
modell = cp_model.CpModel()
|
||||||
|
Gang = collections.namedtuple("Gang", "start ende intervall")
|
||||||
|
plaene: dict[tuple[int, int], Gang] = {}
|
||||||
|
je_maschine = collections.defaultdict(list)
|
||||||
|
|
||||||
|
for a, auftrag in enumerate(auftraege):
|
||||||
|
for g, (maschine, dauer) in enumerate(auftrag):
|
||||||
|
start = modell.NewIntVar(0, horizont, f"start_{a}_{g}")
|
||||||
|
ende = modell.NewIntVar(0, horizont, f"ende_{a}_{g}")
|
||||||
|
intervall = modell.NewIntervalVar(start, dauer, ende, f"iv_{a}_{g}")
|
||||||
|
plaene[a, g] = Gang(start, ende, intervall)
|
||||||
|
je_maschine[maschine].append(intervall)
|
||||||
|
|
||||||
|
for maschine in range(MASCHINEN):
|
||||||
|
modell.AddNoOverlap(je_maschine[maschine])
|
||||||
|
for a, auftrag in enumerate(auftraege):
|
||||||
|
for g in range(len(auftrag) - 1):
|
||||||
|
modell.Add(plaene[a, g + 1].start >= plaene[a, g].ende)
|
||||||
|
|
||||||
|
makespan = modell.NewIntVar(0, horizont, "makespan")
|
||||||
|
modell.AddMaxEquality(
|
||||||
|
makespan, [plaene[a, len(auftrag) - 1].ende
|
||||||
|
for a, auftrag in enumerate(auftraege)])
|
||||||
|
modell.Minimize(makespan)
|
||||||
|
|
||||||
|
loeser = cp_model.CpSolver()
|
||||||
|
loeser.parameters.num_workers = arbeiter
|
||||||
|
loeser.parameters.random_seed = SOLVER_SEED
|
||||||
|
loeser.parameters.max_time_in_seconds = ZEITLIMIT
|
||||||
|
|
||||||
|
beginn = time.perf_counter()
|
||||||
|
status = loeser.Solve(modell)
|
||||||
|
dauer = time.perf_counter() - beginn
|
||||||
|
|
||||||
|
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
|
||||||
|
return loeser.StatusName(status), None, dauer, None
|
||||||
|
plan = tuple(loeser.Value(plaene[a, g].start)
|
||||||
|
for a, auftrag in enumerate(auftraege)
|
||||||
|
for g in range(len(auftrag)))
|
||||||
|
return loeser.StatusName(status), int(loeser.ObjectiveValue()), dauer, plan
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
auftraege = baue_instanz()
|
||||||
|
gaenge = sum(len(a) for a in auftraege)
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print(" WAS MEHRERE ARBEITER WIRKLICH BRINGEN")
|
||||||
|
print("=" * 78)
|
||||||
|
print(f"Job-Shop: {AUFTRAEGE} Auftraege, {MASCHINEN} Maschinen, "
|
||||||
|
f"{gaenge} Arbeitsgaenge.")
|
||||||
|
print(f"random_seed = {SOLVER_SEED} in JEDEM Lauf, "
|
||||||
|
f"{WIEDERHOLUNGEN} Wiederholungen je Arbeiterzahl.\n")
|
||||||
|
|
||||||
|
# Jede Konfiguration mehrfach - ein einzelner Zeitwert sagt nichts.
|
||||||
|
ergebnisse: dict[int, list[tuple]] = {}
|
||||||
|
for arbeiter in ARBEITERZAHLEN:
|
||||||
|
ergebnisse[arbeiter] = [loese(auftraege, arbeiter)
|
||||||
|
for _ in range(WIEDERHOLUNGEN)]
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print(" (1) Laufzeit: was die Arbeiter bringen")
|
||||||
|
print("=" * 78)
|
||||||
|
print(f"{'Arbeiter':>9} {'Status':>9} {'Makespan':>9} "
|
||||||
|
f"{'schnellste':>11} {'langsamste':>11} {'Beschleunigung':>15}")
|
||||||
|
print("-" * 78)
|
||||||
|
basis_zeit = min(d for _, _, d, _ in ergebnisse[1])
|
||||||
|
for arbeiter in ARBEITERZAHLEN:
|
||||||
|
laeufe = ergebnisse[arbeiter]
|
||||||
|
zeiten = [d for _, _, d, _ in laeufe]
|
||||||
|
ziele = {o for _, o, _, _ in laeufe}
|
||||||
|
status = {s for s, _, _, _ in laeufe}
|
||||||
|
print(f"{arbeiter:>9} {'/'.join(sorted(status)):>9} "
|
||||||
|
f"{'/'.join(str(z) for z in sorted(ziele)):>9} "
|
||||||
|
f"{min(zeiten):>10.2f}s {max(zeiten):>10.2f}s "
|
||||||
|
f"{basis_zeit / min(zeiten):>14.1f}x")
|
||||||
|
|
||||||
|
beste = min(ARBEITERZAHLEN, key=lambda w: min(d for _, _, d, _ in ergebnisse[w]))
|
||||||
|
faktor = basis_zeit / min(d for _, _, d, _ in ergebnisse[beste])
|
||||||
|
print(f"\n Am schnellsten sind {beste} Arbeiter: Faktor {faktor:.1f} gegenueber")
|
||||||
|
print(f" einem einzigen - also MEHR, als {beste} Kerne hergeben sollten.")
|
||||||
|
print(f" Das ist kein Messfehler. CP-SAT laesst nicht {beste}-mal dieselbe Suche")
|
||||||
|
print(" laufen, sondern verschiedene Strategien nebeneinander, die einander")
|
||||||
|
print(" ihre Schranken zurufen. Der zweite Arbeiter ist deshalb nicht der")
|
||||||
|
print(" 'zweite Kern', sondern ein anderes Verfahren.")
|
||||||
|
letzte = ARBEITERZAHLEN[-1]
|
||||||
|
if beste != letzte:
|
||||||
|
zeit_letzte = min(d for _, _, d, _ in ergebnisse[letzte])
|
||||||
|
print(f"\n Und mehr ist nicht immer besser: {letzte} Arbeiter brauchen "
|
||||||
|
f"{zeit_letzte:.2f}s")
|
||||||
|
print(f" gegenueber {min(d for _, _, d, _ in ergebnisse[beste]):.2f}s bei "
|
||||||
|
f"{beste}. Ab einer gewissen Zahl kosten Abstimmung")
|
||||||
|
print(" und Speicherbandbreite mehr, als ein weiterer Suchstrang einbringt.")
|
||||||
|
|
||||||
|
print("\n" + "=" * 78)
|
||||||
|
print(" (2) Reproduzierbarkeit: derselbe Seed, derselbe Plan?")
|
||||||
|
print("=" * 78)
|
||||||
|
print(f"{'Arbeiter':>9} {'Laeufe':>7} {'verschiedene Plaene':>21} "
|
||||||
|
f"{'Makespan':>10}")
|
||||||
|
print("-" * 78)
|
||||||
|
for arbeiter in ARBEITERZAHLEN:
|
||||||
|
plaene = {p for _, _, _, p in ergebnisse[arbeiter] if p is not None}
|
||||||
|
ziele = {o for _, o, _, _ in ergebnisse[arbeiter]}
|
||||||
|
print(f"{arbeiter:>9} {WIEDERHOLUNGEN:>7} {len(plaene):>21} "
|
||||||
|
f"{'/'.join(str(z) for z in sorted(ziele)):>10}")
|
||||||
|
|
||||||
|
einer = len({p for _, _, _, p in ergebnisse[1]})
|
||||||
|
# Ab welcher Arbeiterzahl bricht die Reproduzierbarkeit? Das ist die
|
||||||
|
# eigentliche Zahl - nicht, was acht Arbeiter anrichten.
|
||||||
|
ab = next((w for w in ARBEITERZAHLEN
|
||||||
|
if len({p for _, _, _, p in ergebnisse[w]}) > 1), None)
|
||||||
|
print(f"\n Mit einem Arbeiter: {einer} Plan aus {WIEDERHOLUNGEN} Laeufen.")
|
||||||
|
if ab is not None:
|
||||||
|
viele = len({p for _, _, _, p in ergebnisse[ab]})
|
||||||
|
print(f" Schon mit {ab} Arbeitern: {viele} verschiedene Plaene aus "
|
||||||
|
f"{WIEDERHOLUNGEN} Laeufen -")
|
||||||
|
print(" bei identischem random_seed und identischem Zielwert.")
|
||||||
|
print(f" Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit")
|
||||||
|
print(f" zu verlieren. {ab} genuegen.")
|
||||||
|
print("\n Der Seed allein macht einen Lauf also NICHT reproduzierbar. Er legt")
|
||||||
|
print(" fest, wie ein einzelner Suchstrang wuerfelt - nicht, welcher von")
|
||||||
|
print(" mehreren zuerst fertig wird. Das entscheidet die Uhr.")
|
||||||
|
|
||||||
|
print("\n" + "=" * 78)
|
||||||
|
print(" (3) Was daraus fuer Tests folgt")
|
||||||
|
print("=" * 78)
|
||||||
|
print(" Ein Test der Form assert plan == erwarteter_plan besteht mal und")
|
||||||
|
print(" scheitert mal, ohne dass sich am Code etwas geaendert haette.")
|
||||||
|
print(" Zu pruefen sind stattdessen:")
|
||||||
|
print(" * der ZIELWERT (hier in allen Laeufen gleich),")
|
||||||
|
print(" * die Einhaltung aller Regeln (Abnahmepruefung, Kapitel Praxisfallen),")
|
||||||
|
print(" * und der Status - nicht die Gestalt der Loesung.")
|
||||||
|
print("\n Wer doch einen bestimmten Plan braucht - fuer eine abgedruckte")
|
||||||
|
print(" Ausgabe, fuer einen Regressionstest -, setzt num_workers = 1.")
|
||||||
|
print(f" Das kostet hier den Faktor {faktor:.1f} an Laufzeit und ist genau")
|
||||||
|
print(" deshalb eine Entscheidung fuer den Test, nicht fuer den Betrieb.")
|
||||||
|
print("=" * 78)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erwartete Ausgabe** (Laufzeiten *und* die Zahl der verschiedenen Pläne sind hardwareabhängig — genau das ist die Aussage; der Zielwert ist es nicht):
|
||||||
|
|
||||||
|
```
|
||||||
|
==============================================================================
|
||||||
|
WAS MEHRERE ARBEITER WIRKLICH BRINGEN
|
||||||
|
==============================================================================
|
||||||
|
Job-Shop: 12 Auftraege, 10 Maschinen, 120 Arbeitsgaenge.
|
||||||
|
random_seed = 1 in JEDEM Lauf, 4 Wiederholungen je Arbeiterzahl.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
(1) Laufzeit: was die Arbeiter bringen
|
||||||
|
==============================================================================
|
||||||
|
Arbeiter Status Makespan schnellste langsamste Beschleunigung
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
1 OPTIMAL 183 5.62s 5.82s 1.0x
|
||||||
|
2 OPTIMAL 183 1.43s 2.04s 3.9x
|
||||||
|
4 OPTIMAL 183 0.60s 0.66s 9.4x
|
||||||
|
8 OPTIMAL 183 0.46s 0.65s 12.3x
|
||||||
|
|
||||||
|
Am schnellsten sind 8 Arbeiter: Faktor 12.3 gegenueber
|
||||||
|
einem einzigen - also MEHR, als 8 Kerne hergeben sollten.
|
||||||
|
Das ist kein Messfehler. CP-SAT laesst nicht 8-mal dieselbe Suche
|
||||||
|
laufen, sondern verschiedene Strategien nebeneinander, die einander
|
||||||
|
ihre Schranken zurufen. Der zweite Arbeiter ist deshalb nicht der
|
||||||
|
'zweite Kern', sondern ein anderes Verfahren.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
(2) Reproduzierbarkeit: derselbe Seed, derselbe Plan?
|
||||||
|
==============================================================================
|
||||||
|
Arbeiter Laeufe verschiedene Plaene Makespan
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
1 4 1 183
|
||||||
|
2 4 3 183
|
||||||
|
4 4 3 183
|
||||||
|
8 4 4 183
|
||||||
|
|
||||||
|
Mit einem Arbeiter: 1 Plan aus 4 Laeufen.
|
||||||
|
Schon mit 2 Arbeitern: 3 verschiedene Plaene aus 4 Laeufen -
|
||||||
|
bei identischem random_seed und identischem Zielwert.
|
||||||
|
Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit
|
||||||
|
zu verlieren. 2 genuegen.
|
||||||
|
|
||||||
|
Der Seed allein macht einen Lauf also NICHT reproduzierbar. Er legt
|
||||||
|
fest, wie ein einzelner Suchstrang wuerfelt - nicht, welcher von
|
||||||
|
mehreren zuerst fertig wird. Das entscheidet die Uhr.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
(3) Was daraus fuer Tests folgt
|
||||||
|
==============================================================================
|
||||||
|
Ein Test der Form assert plan == erwarteter_plan besteht mal und
|
||||||
|
scheitert mal, ohne dass sich am Code etwas geaendert haette.
|
||||||
|
Zu pruefen sind stattdessen:
|
||||||
|
* der ZIELWERT (hier in allen Laeufen gleich),
|
||||||
|
* die Einhaltung aller Regeln (Abnahmepruefung, Kapitel Praxisfallen),
|
||||||
|
* und der Status - nicht die Gestalt der Loesung.
|
||||||
|
|
||||||
|
Wer doch einen bestimmten Plan braucht - fuer eine abgedruckte
|
||||||
|
Ausgabe, fuer einen Regressionstest -, setzt num_workers = 1.
|
||||||
|
Das kostet hier den Faktor 12.3 an Laufzeit und ist genau
|
||||||
|
deshalb eine Entscheidung fuer den Test, nicht fuer den Betrieb.
|
||||||
|
==============================================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
### Was die Messung zeigt
|
||||||
|
|
||||||
|
**Erstens: Die Beschleunigung ist überlinear.** Acht Arbeiter sind im abgedruckten Lauf nicht achtmal, sondern **gut zwölfmal** schneller als einer. Das sieht nach Messfehler aus, ist aber die korrekte Beschreibung dessen, was CP-SAT tut: Es lässt **nicht** achtmal dieselbe Suche auf verschiedenen Kernen laufen, sondern **verschiedene Strategien nebeneinander** — Suche mit LP-Relaxation, Suche mit anderen Verzweigungsregeln, lokale Suche —, und die teilen einander ihre Schranken mit. Der zweite Arbeiter ist deshalb nicht „der zweite Kern“, sondern ein anderes Verfahren, das zufällig auch rechnet.
|
||||||
|
|
||||||
|
Daraus folgt zweierlei. Ein Speedup über der Kernzahl ist bei CP-SAT normal und kein Grund zum Misstrauen. Und der Zugewinn flacht ab: Zwischen vier und acht Arbeitern lag in unseren Wiederholungen mal das eine, mal das andere vorn — ab einer gewissen Zahl kosten Abstimmung und Speicherbandbreite mehr, als ein weiterer Suchstrang einbringt. Auch hier gilt: messen, nicht schätzen.
|
||||||
|
|
||||||
|
**Zweitens — und das ist der Befund, der die Testpraxis bestimmt: Der Seed genügt nicht.** Bei einem Arbeiter liefern vier Läufe **einen** Plan. Schon bei **zwei** Arbeitern liefern vier Läufe **drei verschiedene** — bei identischem `random_seed` und identischem Zielwert 183.
|
||||||
|
|
||||||
|
Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit zu verlieren. Zwei genügen. Der Grund ist einfach, sobald man ihn ausspricht: Der Seed legt fest, wie ein **einzelner** Suchstrang würfelt. Er legt nicht fest, **welcher von mehreren zuerst fertig wird** — das entscheidet die Uhr, und die ist bei jedem Lauf anders.
|
||||||
|
|
||||||
|
> **⚠️ `random_seed` allein macht nichts reproduzierbar** Ein häufiger Irrtum: „Ich habe doch einen Seed gesetzt.“ Für einen Zufallsgenerator stimmt das. Für einen **parallelen** Solver nicht — dort ist die zweite Zufallsquelle die Ausführungsreihenfolge, und die lässt sich nicht mit einem Parameter festnageln. Reproduzierbar wird der Lauf erst mit `num_workers = 1` **und** Seed. Wer nur eines von beidem setzt, hat einen Test, der gelegentlich grundlos rot wird.
|
||||||
|
|
||||||
|
### Was daraus für Tests folgt
|
||||||
|
|
||||||
|
Der Zielwert war in **allen** sechzehn Läufen derselbe: 183. Die Gestalt der Lösung war es nicht. Genau daran entscheidet sich, was ein Test prüfen darf:
|
||||||
|
|
||||||
|
| Prüfen | Nicht prüfen |
|
||||||
|
| --- | --- |
|
||||||
|
| den **Zielwert** — er ist eindeutig, wenn das Modell optimal gelöst wurde | die **Gestalt** des Plans (`assert plan == erwarteter_plan`) |
|
||||||
|
| die **Einhaltung aller Regeln** über eine Abnahmeprüfung ([Abschnitt 22.6](#sec:praxisfallen-or-kern)) | die Reihenfolge, in der gleichwertige Aufträge zugewiesen wurden |
|
||||||
|
| den **Status** (`OPTIMAL`, `FEASIBLE`, `INFEASIBLE`) | die Laufzeit als feste Zahl |
|
||||||
|
|
||||||
|
Wer doch einen bestimmten Plan braucht — für eine abgedruckte Ausgabe, für einen Regressionstest —, setzt `num_workers = 1`. Das kostet hier den Faktor 12 an Laufzeit, und genau deshalb ist es eine Entscheidung **für den Test**, nicht für den Betrieb. In Produktion lässt man beide Parameter auf ihren Standardwerten.
|
||||||
|
|
||||||
|
> **🎯 Merksatz** Ein paralleler Solver hat zwei Zufallsquellen: den Seed und die Uhr. Nur die erste lässt sich setzen. Wer Reproduzierbarkeit braucht, muss die zweite abschalten — und das heißt `num_workers = 1`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7.8 Die fünf Antworten von CP-SAT {#sec:cpsat-statusfaelle}
|
||||||
|
|
||||||
[Abschnitt 6.8](#sec:milp-gap) hat gezeigt, warum `OPTIMAL` bei ganzzahligen Problemen nicht der Normalfall ist. Bei CP-SAT kommt eine Besonderheit hinzu: Es gibt **fünf** verschiedene Antworten, und drei davon werden regelmäßig verwechselt.
|
[Abschnitt 6.8](#sec:milp-gap) hat gezeigt, warum `OPTIMAL` bei ganzzahligen Problemen nicht der Normalfall ist. Bei CP-SAT kommt eine Besonderheit hinzu: Es gibt **fünf** verschiedene Antworten, und drei davon werden regelmäßig verwechselt.
|
||||||
|
|
||||||
|
|
@ -8391,7 +8692,7 @@ genau die Information, die zur Fehlersuche noetig waere.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7.8 Übungsaufgaben {#sec:cpsat-uebungsaufgaben}
|
## 7.9 Übungsaufgaben {#sec:cpsat-uebungsaufgaben}
|
||||||
|
|
||||||
> Lösungen: [Abschnitt A.7](#sec:loesungen-cpsat).
|
> Lösungen: [Abschnitt A.7](#sec:loesungen-cpsat).
|
||||||
|
|
||||||
|
|
@ -8405,13 +8706,15 @@ genau die Information, die zur Fehlersuche noetig waere.
|
||||||
|
|
||||||
**Aufgabe 7.5 ⭐⭐ — Sudoku.** Lösen Sie ein 9×9-Sudoku mit CP-SAT. Sie brauchen nur `NewIntVar(1, 9, ...)` und `AddAllDifferent` für Zeilen, Spalten und die neun 3×3-Blöcke. Wie viele Zeilen Modellcode benötigen Sie?
|
**Aufgabe 7.5 ⭐⭐ — Sudoku.** Lösen Sie ein 9×9-Sudoku mit CP-SAT. Sie brauchen nur `NewIntVar(1, 9, ...)` und `AddAllDifferent` für Zeilen, Spalten und die neun 3×3-Blöcke. Wie viele Zeilen Modellcode benötigen Sie?
|
||||||
|
|
||||||
**Aufgabe 7.6 ⭐⭐⭐ — Job-Shop erweitern.** Ergänzen Sie `JobShop_Intervalle.py` um: (a) Rüstzeiten von 1 Zeiteinheit zwischen zwei Aufträgen auf derselben Maschine. (b) Fälligkeitstermine je Auftrag mit Strafkosten für Verspätung. (c) Eine zweite Fräse (Kapazität 2 statt 1) mit `AddCumulative`.
|
**Aufgabe 7.6 ⭐⭐ — Die Grenze der Parallelität finden.** `Parallele_Suche.py` misst 1, 2, 4 und 8 Arbeiter. (a) Ergänzen Sie 16 und 24 (bzw. die Kernzahl Ihres Rechners). Ab wo bringt ein weiterer Arbeiter nichts mehr — und wird es irgendwann wieder schlechter? (b) Setzen Sie `random_seed` bei jedem Lauf auf einen **anderen** Wert, aber `num_workers = 1`. Wie viele verschiedene Pläne kommen jetzt heraus? Was folgt daraus für die Frage, welcher der beiden Parameter die Reproduzierbarkeit sichert? (c) Vergrößern Sie die Instanz auf 15 Aufträge. Ab welcher Größe erreicht der Lauf mit einem Arbeiter das Zeitlimit, während acht noch `OPTIMAL` melden?
|
||||||
|
|
||||||
**Aufgabe 7.7 ⭐⭐⭐ — Eigener Dienstplan.** Modellieren Sie einen Wochendienstplan: 7 Tage, 3 Schichten (früh/spät/nacht), 8 Mitarbeitende. Regeln: Jede Schicht 2 Personen; nach Nachtschicht mindestens 2 Tage frei; höchstens 5 Arbeitstage in Folge; jede Person mindestens 2 freie Tage pro Woche; Wünsche als weiche Ziele. Geben Sie den Plan als Wochentabelle aus.
|
**Aufgabe 7.7 ⭐⭐⭐ — Job-Shop erweitern.** Ergänzen Sie `JobShop_Intervalle.py` um: (a) Rüstzeiten von 1 Zeiteinheit zwischen zwei Aufträgen auf derselben Maschine. (b) Fälligkeitstermine je Auftrag mit Strafkosten für Verspätung. (c) Eine zweite Fräse (Kapazität 2 statt 1) mit `AddCumulative`.
|
||||||
|
|
||||||
|
**Aufgabe 7.8 ⭐⭐⭐ — Eigener Dienstplan.** Modellieren Sie einen Wochendienstplan: 7 Tage, 3 Schichten (früh/spät/nacht), 8 Mitarbeitende. Regeln: Jede Schicht 2 Personen; nach Nachtschicht mindestens 2 Tage frei; höchstens 5 Arbeitstage in Folge; jede Person mindestens 2 freie Tage pro Woche; Wünsche als weiche Ziele. Geben Sie den Plan als Wochentabelle aus.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7.9 Finde den Denkfehler {#sec:cpsat-denkfehler}
|
## 7.10 Finde den Denkfehler {#sec:cpsat-denkfehler}
|
||||||
|
|
||||||
> **🐛 Finde den Denkfehler: Die Betriebsvereinbarung, die niemanden interessiert**
|
> **🐛 Finde den Denkfehler: Die Betriebsvereinbarung, die niemanden interessiert**
|
||||||
>
|
>
|
||||||
|
|
@ -8619,7 +8922,7 @@ Nebenbedingungen. Alles, was einen Preis hat, wird irgendwann gekauft.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7.10 Micro-Quiz {#sec:cpsat-quiz}
|
## 7.11 Micro-Quiz {#sec:cpsat-quiz}
|
||||||
|
|
||||||
> **❓ Micro-Quiz 7: Drei Fragen zum Selbstcheck**
|
> **❓ Micro-Quiz 7: Drei Fragen zum Selbstcheck**
|
||||||
>
|
>
|
||||||
|
|
@ -8633,7 +8936,7 @@ Nebenbedingungen. Alles, was einen Preis hat, wird irgendwann gekauft.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7.11 Selbsttest {#sec:cpsat-selbsttest}
|
## 7.12 Selbsttest {#sec:cpsat-selbsttest}
|
||||||
|
|
||||||
> Antworten: [Anhang A](#selbsttest-loesung-cpsat).
|
> Antworten: [Anhang A](#selbsttest-loesung-cpsat).
|
||||||
|
|
||||||
|
|
@ -8641,11 +8944,13 @@ Nebenbedingungen. Alles, was einen Preis hat, wird irgendwann gekauft.
|
||||||
2. Welchen Vorteil hat `AddAllDifferent(x)` gegenüber $\binom{n}{2}$ Einzelbedingungen?
|
2. Welchen Vorteil hat `AddAllDifferent(x)` gegenüber $\binom{n}{2}$ Einzelbedingungen?
|
||||||
3. Was ist eine Intervallvariable, und welche Bedingung garantiert sie automatisch?
|
3. Was ist eine Intervallvariable, und welche Bedingung garantiert sie automatisch?
|
||||||
4. Warum sollten möglichst wenige Bedingungen hart formuliert werden?
|
4. Warum sollten möglichst wenige Bedingungen hart formuliert werden?
|
||||||
5. Warum ist die Aufschlüsselung der Strafkosten in der Praxis wichtiger als der Zielwert selbst?
|
5. Warum ist die Aufschlüsselung der Strafkosten in der Praxis wichtiger als der Zielwert
|
||||||
|
6. Sie setzen `random_seed = 1`, bekommen aber trotzdem bei jedem Lauf einen anderen Plan. Woran liegt das, und was hilft?
|
||||||
|
7. Warum kann ein CP-SAT-Lauf mit acht Arbeitern **mehr** als achtmal schneller sein als mit einem? selbst?
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7.12 Zusammenfassung {#sec:cpsat-zusammenfassung}
|
## 7.13 Zusammenfassung {#sec:cpsat-zusammenfassung}
|
||||||
|
|
||||||
* **CP denkt in Wertebereichen und Regeln**, nicht in Ungleichungen und Schranken.
|
* **CP denkt in Wertebereichen und Regeln**, nicht in Ungleichungen und Schranken.
|
||||||
* **Propagation** streicht unmögliche Werte, bevor gesucht wird; **Conflict Learning**`\index{CDCL!Conflict Learning}`{=latex} verhindert, dieselbe Sackgasse zweimal zu betreten.
|
* **Propagation** streicht unmögliche Werte, bevor gesucht wird; **Conflict Learning**`\index{CDCL!Conflict Learning}`{=latex} verhindert, dieselbe Sackgasse zweimal zu betreten.
|
||||||
|
|
@ -8656,7 +8961,8 @@ Nebenbedingungen. Alles, was einen Preis hat, wird irgendwann gekauft.
|
||||||
* **Fünf Antworten, nicht zwei.** `INFEASIBLE` (es gibt keine Lösung), `UNKNOWN` (es wurde keine gefunden) und `MODEL_INVALID` (Ihr Code ist fehlerhaft) bedeuten Grundverschiedenes. Wer sie in ein `else` zusammenfasst, verliert die Information zur Fehlersuche.
|
* **Fünf Antworten, nicht zwei.** `INFEASIBLE` (es gibt keine Lösung), `UNKNOWN` (es wurde keine gefunden) und `MODEL_INVALID` (Ihr Code ist fehlerhaft) bedeuten Grundverschiedenes. Wer sie in ein `else` zusammenfasst, verliert die Information zur Fehlersuche.
|
||||||
* **Bei mehrdeutigen Optima nie auf einen bestimmten Plan testen** — nur auf den Zielwert und die Einhaltung aller Regeln.
|
* **Bei mehrdeutigen Optima nie auf einen bestimmten Plan testen** — nur auf den Zielwert und die Einhaltung aller Regeln.
|
||||||
|
|
||||||
**Ausblick.** [Kapitel 8](#kap-graphen) behandelt Probleme, die sich am besten als **Graph** begreifen lassen: Flüsse durch Netzwerke, Zuordnungen und die Tourenplanung mit Fahrzeugen und Zeitfenstern.
|
* **Ein paralleler Solver hat zwei Zufallsquellen: den Seed und die Uhr.** Nur die erste lässt sich setzen. Reproduzierbar wird ein Lauf erst mit `num_workers = 1` **und** Seed — gemessen in [Abschnitt 7.7](#sec:cpsat-parallele-suche): schon zwei Arbeiter liefern bei identischem Seed drei verschiedene Pläne zum selben Zielwert.
|
||||||
|
* **Parallele Suche kann überlinear beschleunigen**, weil CP-SAT nicht dieselbe Suche vervielfacht, sondern verschiedene Strategien nebeneinander laufen lässt, die einander ihre Schranken mitteilen. **Ausblick.** [Kapitel 8](#kap-graphen) behandelt Probleme, die sich am besten als **Graph** begreifen lassen: Flüsse durch Netzwerke, Zuordnungen und die Tourenplanung mit Fahrzeugen und Zeitfenstern.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -14808,7 +15114,7 @@ $$
|
||||||
> | --- | --- |
|
> | --- | --- |
|
||||||
> | $c(x)$ | Transportkosten des Plans $x$, in Euro. |
|
> | $c(x)$ | Transportkosten des Plans $x$, in Euro. |
|
||||||
> | $e(x)$ | CO₂-Ausstoß desselben Plans, in Kilogramm. |
|
> | $e(x)$ | CO₂-Ausstoß desselben Plans, in Kilogramm. |
|
||||||
> | $w$ | „So viele Euro ist mir ein Kilogramm CO₂ wert.“ Ein **Wechselkurs**, kein Wichtigkeitsregler ([Abschnitt 7.9](#sec:cpsat-denkfehler)). |
|
> | $w$ | „So viele Euro ist mir ein Kilogramm CO₂ wert.“ Ein **Wechselkurs**, kein Wichtigkeitsregler ([Abschnitt 7.10](#sec:cpsat-denkfehler)). |
|
||||||
> | $\min_{x \in X}$ | über alle zulässigen Pläne. |
|
> | $\min_{x \in X}$ | über alle zulässigen Pläne. |
|
||||||
>
|
>
|
||||||
> Der Ausdruck hat eine Einheit: Euro. Damit ist $w$ keine freie Zahl zwischen 0 und 1, sondern ein CO₂-Preis in €/kg — und den kann man tatsächlich beziffern.
|
> Der Ausdruck hat eine Einheit: Euro. Damit ist $w$ keine freie Zahl zwischen 0 und 1, sondern ein CO₂-Preis in €/kg — und den kann man tatsächlich beziffern.
|
||||||
|
|
@ -21993,7 +22299,7 @@ Domaenenschicht.
|
||||||
> | --- | --- | --- |
|
> | --- | --- | --- |
|
||||||
> | `PositiveZahl = Annotated[float, Field(gt=0)]` | Prüfung im **Typ**, nicht im Code | Sie gilt damit überall, wo der Typ verwendet wird — man kann sie nicht vergessen. |
|
> | `PositiveZahl = Annotated[float, Field(gt=0)]` | Prüfung im **Typ**, nicht im Code | Sie gilt damit überall, wo der Typ verwendet wird — man kann sie nicht vergessen. |
|
||||||
> | `SolverStatus.brauchbar` | fragt „habe ich etwas in der Hand?“ | Nicht „war der Solver erfolgreich?“. `ZULAESSIG` ist im Betrieb meist völlig ausreichend ([Abschnitt 6.8](#sec:milp-gap)). |
|
> | `SolverStatus.brauchbar` | fragt „habe ich etwas in der Hand?“ | Nicht „war der Solver erfolgreich?“. `ZULAESSIG` ist im Betrieb meist völlig ausreichend ([Abschnitt 6.8](#sec:milp-gap)). |
|
||||||
> | `SolverStatus.modellfehler` | trennt Modell- von Zeitproblemen | Genau die Unterscheidung, die [Abschnitt 7.7](#sec:cpsat-statusfaelle) als häufigste Verwechslung benennt. |
|
> | `SolverStatus.modellfehler` | trennt Modell- von Zeitproblemen | Genau die Unterscheidung, die [Abschnitt 7.8](#sec:cpsat-statusfaelle) als häufigste Verwechslung benennt. |
|
||||||
> | `model_validator(mode="after")` | prüft Zusammenhänge **zwischen** Feldern | Einzelne Felder prüft Pydantic selbst; dass jedes Produkt nur bekannte Ressourcen verbraucht, muss man sagen. |
|
> | `model_validator(mode="after")` | prüft Zusammenhänge **zwischen** Feldern | Einzelne Felder prüft Pydantic selbst; dass jedes Produkt nur bekannte Ressourcen verbraucht, muss man sagen. |
|
||||||
> | `ressourcen` als sortierte Liste | feste Spaltenreihenfolge | Die Vertauschungsfalle aus [Kapitel 18](#kap-finanzdaten) wird hier strukturell unmöglich. |
|
> | `ressourcen` als sortierte Liste | feste Spaltenreihenfolge | Die Vertauschungsfalle aus [Kapitel 18](#kap-finanzdaten) wird hier strukturell unmöglich. |
|
||||||
> | `loese_mit_glop` / `loese_mit_scipy` | zwei Modellbauer, ein Auswertungscode | Der Solverwechsel betrifft **eine** Funktion. Alles danach — Bericht, Prüfung, Test — bleibt unverändert. |
|
> | `loese_mit_glop` / `loese_mit_scipy` | zwei Modellbauer, ein Auswertungscode | Der Solverwechsel betrifft **eine** Funktion. Alles danach — Bericht, Prüfung, Test — bleibt unverändert. |
|
||||||
|
|
@ -25087,9 +25393,18 @@ for i in range(9):
|
||||||
```
|
```
|
||||||
**Etwa 10 Zeilen Modellcode** — und der Solver löst jedes Sudoku in Millisekunden. Das ist die Stärke globaler Constraints.
|
**Etwa 10 Zeilen Modellcode** — und der Solver löst jedes Sudoku in Millisekunden. Das ist die Stärke globaler Constraints.
|
||||||
|
|
||||||
**7.6 — Job-Shop erweitern.** (a) Rüstzeiten: `AddNoOverlap` durch paarweise Disjunktionen mit Übergangszeit ersetzen, oder `AddCircuit` je Maschine mit Übergangsmatrix. (b) Verspätung: `tardiness = MaxEquality(0, ende - faellig)`, in die Zielfunktion. (c) `AddCumulative(intervalle, [1]*n, 2)` statt `AddNoOverlap` für die Doppelmaschine.
|
**7.6 — Die Grenze der Parallelität finden.** (a) Der Gewinn flacht ab und kehrt sich um. Auf einem Rechner mit 24 Kernen gemessen (Faktor gegenüber einem Arbeiter): 8 Arbeiter **10,2**, 16 Arbeiter **12,3**, 24 Arbeiter **9,0**. Wer alle Kerne belegt, ist langsamer als mit zwei Dritteln — dem Betriebssystem, dem Speicherbus und den Suchsträngen untereinander bleibt zu wenig Luft. Die Faustregel „so viele Arbeiter wie Kerne" ist also falsch; die richtige Zahl ist eine Messung, keine Einstellung. (b) **Fünf verschiedene Pläne aus fünf Seeds** — bei `num_workers = 1` und identischem Zielwert 183. Das ist die andere Hälfte der Antwort: Der Seed *wirkt*, aber nur auf den einzelnen Suchstrang. Es sichert also **keiner der beiden Parameter allein** die Reproduzierbarkeit:
|
||||||
|
|
||||||
**7.7 — Wochendienstplan.** Siehe Projekt P2. Kernpunkte: Nachtschicht-Folgeregel über `AddImplication`; „höchstens 5 Tage in Folge“ über gleitende Fenster (`sum(x[p,t..t+5]) <= 5`).
|
| | Seed fest | Seed variabel |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **1 Arbeiter** | reproduzierbar | 5 Pläne aus 5 Läufen |
|
||||||
|
| **mehrere Arbeiter** | 3 Pläne aus 4 Läufen | erst recht nicht |
|
||||||
|
|
||||||
|
Nur die Kombination trägt. (c) Bei **15 Aufträgen** (60 s Zeitlimit): Ein Arbeiter läuft ins Limit und meldet `FEASIBLE` mit Makespan 200; acht Arbeiter melden `OPTIMAL` mit **demselben** Makespan 200 nach 26,4 s. Bei 13 Aufträgen sind es 11,2 s gegen 2,8 s, bei 14 schon 21,4 s gegen 3,7 s. Bemerkenswert ist, dass beide denselben Wert finden — der Unterschied liegt nicht in der Lösung, sondern im **Beweis**, dass es keine bessere gibt. Genau diese Arbeit teilen sich die parallelen Stränge auf.
|
||||||
|
|
||||||
|
**7.7 — Job-Shop erweitern.** (a) Rüstzeiten: `AddNoOverlap` durch paarweise Disjunktionen mit Übergangszeit ersetzen, oder `AddCircuit` je Maschine mit Übergangsmatrix. (b) Verspätung: `tardiness = MaxEquality(0, ende - faellig)`, in die Zielfunktion. (c) `AddCumulative(intervalle, [1]*n, 2)` statt `AddNoOverlap` für die Doppelmaschine.
|
||||||
|
|
||||||
|
**7.8 — Wochendienstplan.** Siehe Projekt P2. Kernpunkte: Nachtschicht-Folgeregel über `AddImplication`; „höchstens 5 Tage in Folge“ über gleitende Fenster (`sum(x[p,t..t+5]) <= 5`).
|
||||||
|
|
||||||
### Finde den Denkfehler — Die Betriebsvereinbarung, die niemanden interessiert
|
### Finde den Denkfehler — Die Betriebsvereinbarung, die niemanden interessiert
|
||||||
|
|
||||||
|
|
@ -25140,6 +25455,8 @@ Das ist eine Frage, die eine Zahl als Antwort hat. Und wenn der Auftraggeber sie
|
||||||
3. Eine Variable mit Start, Dauer und Ende; sie garantiert automatisch `start + dauer == ende`.
|
3. Eine Variable mit Start, Dauer und Ende; sie garantiert automatisch `start + dauer == ende`.
|
||||||
4. Weil jede zusätzliche harte Bedingung das Risiko von `INFEASIBLE` erhöht — und eine Fehlermeldung dem Anwender nicht hilft.
|
4. Weil jede zusätzliche harte Bedingung das Risiko von `INFEASIBLE` erhöht — und eine Fehlermeldung dem Anwender nicht hilft.
|
||||||
5. Weil Anwender die Frage „Warum ich?“ stellen. Ohne Antwort wird das System umgangen.
|
5. Weil Anwender die Frage „Warum ich?“ stellen. Ohne Antwort wird das System umgangen.
|
||||||
|
6. Weil der Seed nur festlegt, wie ein **einzelner** Suchstrang würfelt — nicht, welcher von mehreren parallelen zuerst fertig wird. Das entscheidet die Ausführungsreihenfolge, also die Uhr. Abhilfe: zusätzlich `num_workers = 1`. Gemessen: schon zwei Arbeiter liefern bei identischem Seed drei verschiedene Pläne zum selben Zielwert.
|
||||||
|
7. Weil CP-SAT nicht dieselbe Suche vervielfacht, sondern **verschiedene Strategien** nebeneinander laufen lässt (LP-gestützt, andere Verzweigungsregeln, lokale Suche), die einander ihre Schranken mitteilen. Ein zusätzlicher Arbeiter ist deshalb nicht ein weiterer Kern für dieselbe Arbeit, sondern ein anderes Verfahren — und wenn eines davon früh eine gute Schranke findet, profitieren alle.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -431,6 +431,7 @@ Pfad E oder um gezielt nach einem Thema zu suchen:
|
||||||
| `Propagation_Demo.py` | Propagation messbar | {ref:kap:cpsat} |
|
| `Propagation_Demo.py` | Propagation messbar | {ref:kap:cpsat} |
|
||||||
| `CP_SAT_Vertretungssystem.py` | Vertretungsplan | {ref:kap:cpsat} |
|
| `CP_SAT_Vertretungssystem.py` | Vertretungsplan | {ref:kap:cpsat} |
|
||||||
| `JobShop_Intervalle.py` | Job-Shop-Scheduling | {ref:kap:cpsat} |
|
| `JobShop_Intervalle.py` | Job-Shop-Scheduling | {ref:kap:cpsat} |
|
||||||
|
| `Parallele_Suche.py` | num_workers: Tempo gegen Reproduzierbarkeit | {ref:kap:cpsat} |
|
||||||
| `CP_SAT_Statusfaelle.py` | Die fünf Solver-Antworten | {ref:kap:cpsat} |
|
| `CP_SAT_Statusfaelle.py` | Die fünf Solver-Antworten | {ref:kap:cpsat} |
|
||||||
| `Strafgewichte.py` | Gewichte als Wechselkurse | {ref:kap:cpsat} |
|
| `Strafgewichte.py` | Gewichte als Wechselkurse | {ref:kap:cpsat} |
|
||||||
| `Min_Cost_Flow.py` | Netzwerkfluss | {ref:kap:graphen} |
|
| `Min_Cost_Flow.py` | Netzwerkfluss | {ref:kap:graphen} |
|
||||||
|
|
|
||||||
|
|
@ -1308,7 +1308,8 @@ def loese(dauer: np.ndarray, n_maschinen: int, zeitlimit: float,
|
||||||
|
|
||||||
loeser = cp_model.CpSolver()
|
loeser = cp_model.CpSolver()
|
||||||
loeser.parameters.max_time_in_seconds = zeitlimit
|
loeser.parameters.max_time_in_seconds = zeitlimit
|
||||||
# Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist.
|
# Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist -
|
||||||
|
# der Seed allein genuegt dafuer NICHT (Kapitel Constraint Programming).
|
||||||
# Im Produktivbetrieb laesst man beides auf den Standardwerten.
|
# Im Produktivbetrieb laesst man beides auf den Standardwerten.
|
||||||
loeser.parameters.num_workers = 1
|
loeser.parameters.num_workers = 1
|
||||||
loeser.parameters.random_seed = 1
|
loeser.parameters.random_seed = 1
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
> `Propagation_Demo.py`\
|
> `Propagation_Demo.py`\
|
||||||
> `CP_SAT_Vertretungssystem.py`\
|
> `CP_SAT_Vertretungssystem.py`\
|
||||||
> `JobShop_Intervalle.py`\
|
> `JobShop_Intervalle.py`\
|
||||||
|
> `Parallele_Suche.py`\
|
||||||
> `CP_SAT_Statusfaelle.py`\
|
> `CP_SAT_Statusfaelle.py`\
|
||||||
> `Strafgewichte.py`
|
> `Strafgewichte.py`
|
||||||
|
|
||||||
|
|
@ -101,7 +102,9 @@ Nach diesem Kapitel können Sie …
|
||||||
6. … die fünf CP-SAT-Statusfälle unterscheiden — insbesondere `INFEASIBLE` (es gibt keine
|
6. … die fünf CP-SAT-Statusfälle unterscheiden — insbesondere `INFEASIBLE` (es gibt keine
|
||||||
Lösung) von `UNKNOWN` (es wurde keine gefunden) und `MODEL_INVALID` (Ihr Code ist
|
Lösung) von `UNKNOWN` (es wurde keine gefunden) und `MODEL_INVALID` (Ihr Code ist
|
||||||
fehlerhaft).
|
fehlerhaft).
|
||||||
7. … begründen, warum ein Strafgewicht ein **Wechselkurs** ist, und welche Regeln deshalb
|
7. … begründen, warum `num_workers = 1` für einen reproduzierbaren Lauf nötig ist und ein
|
||||||
|
fester `random_seed` allein nicht genügt — und was ein Test deshalb prüfen darf.
|
||||||
|
8. … begründen, warum ein Strafgewicht ein **Wechselkurs** ist, und welche Regeln deshalb
|
||||||
niemals in die Zielfunktion gehören.
|
niemals in die Zielfunktion gehören.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -794,6 +797,9 @@ Zusammenspiel von Reihenfolge- und Maschinenbedingungen, nicht aus einer schlech
|
||||||
> abgedruckte Ausgabe reproduzierbar. **Im Produktivbetrieb lässt man beides weg** — mehrere
|
> abgedruckte Ausgabe reproduzierbar. **Im Produktivbetrieb lässt man beides weg** — mehrere
|
||||||
> Arbeiter sind deutlich schneller.
|
> Arbeiter sind deutlich schneller.
|
||||||
>
|
>
|
||||||
|
> Wie viel schneller, und warum der Seed allein **nicht** genügt, misst
|
||||||
|
> {ref:sec:cpsat-parallele-suche} an genau diesem Modell.
|
||||||
|
>
|
||||||
> Die Lehre für Ihre eigenen Tests: Prüfen Sie bei mehrdeutigen Optima nie auf einen
|
> Die Lehre für Ihre eigenen Tests: Prüfen Sie bei mehrdeutigen Optima nie auf einen
|
||||||
> **bestimmten Plan**, sondern immer auf den **Zielwert** und auf die Einhaltung aller
|
> **bestimmten Plan**, sondern immer auf den **Zielwert** und auf die Einhaltung aller
|
||||||
> Regeln. Ein Test wie `assert plan == erwarteter_plan` besteht sonst mal und scheitert mal,
|
> Regeln. Ein Test wie `assert plan == erwarteter_plan` besteht sonst mal und scheitert mal,
|
||||||
|
|
@ -817,6 +823,336 @@ theoretischen Ideal entfernt sein können, noch bevor der Solver fertig war.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Parallele Suche: was `num_workers` wirklich bewirkt {#sec:cpsat-parallele-suche}
|
||||||
|
|
||||||
|
An mehreren Stellen dieses Buchs steht `loeser.parameters.num_workers = 1`, mit dem
|
||||||
|
Kommentar „für eine reproduzierbare Ausgabe“. Darin stecken zwei Behauptungen: dass
|
||||||
|
parallele Suche **schneller** ist, und dass sie **von Lauf zu Lauf andere, gleich gute**
|
||||||
|
Lösungen liefert. Beides lässt sich messen — und beim Messen kommt mehr heraus als erwartet.
|
||||||
|
|
||||||
|
Gerechnet wird auf demselben Modell wie
|
||||||
|
{ref:sec:cpsat-intervallvariablen-job-shop-scheduling}, nur groß genug, dass die Suche
|
||||||
|
wirklich arbeitet: 12 Aufträge auf 10 Maschinen, jeder Auftrag besucht jede Maschine.
|
||||||
|
Entscheidend ist, dass `random_seed` in **jedem** Lauf denselben Wert hat.
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Parallele_Suche.py
|
||||||
|
"""
|
||||||
|
Kapitel CP-SAT: Was mehrere Arbeiter wirklich bringen - und was sie kosten.
|
||||||
|
|
||||||
|
Das Buch setzt an mehreren Stellen 'num_workers = 1', damit die abgedruckte
|
||||||
|
Ausgabe reproduzierbar ist. Behauptet wird dabei zweierlei: dass parallele
|
||||||
|
Suche schneller ist, und dass sie von Lauf zu Lauf verschiedene, gleich gute
|
||||||
|
Loesungen findet. Beides wird hier gemessen statt geglaubt.
|
||||||
|
|
||||||
|
Gerechnet wird auf einem Job-Shop wie in JobShop_Intervalle.py, nur gross
|
||||||
|
genug, dass die Suche wirklich arbeitet: 12 Auftraege auf 10 Maschinen,
|
||||||
|
jeder Auftrag besucht jede Maschine.
|
||||||
|
|
||||||
|
Zwei Messungen:
|
||||||
|
1. Dieselbe Aufgabe mit 1, 2, 4 und 8 Arbeitern - Laufzeit und Ergebnis.
|
||||||
|
2. Dieselbe Konfiguration mehrfach, bei FESTEM random_seed - wie oft kommt
|
||||||
|
derselbe Plan heraus?
|
||||||
|
|
||||||
|
Die zweite ist die wichtigere: Sie entscheidet, wie man Tests schreibt.
|
||||||
|
|
||||||
|
Achtung, Laufzeiten sind hardwareabhaengig. Die ZAHL der verschiedenen
|
||||||
|
Plaene ist es auch - und genau das ist die Aussage.
|
||||||
|
|
||||||
|
Benoetigt: numpy, ortools
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from ortools.sat.python import cp_model
|
||||||
|
|
||||||
|
AUFTRAEGE = 12
|
||||||
|
MASCHINEN = 10
|
||||||
|
INSTANZ_SEED = 20260908
|
||||||
|
SOLVER_SEED = 1 # bleibt ueber ALLE Laeufe gleich - das ist der Punkt
|
||||||
|
ARBEITERZAHLEN = (1, 2, 4, 8)
|
||||||
|
WIEDERHOLUNGEN = 4
|
||||||
|
ZEITLIMIT = 120.0
|
||||||
|
|
||||||
|
|
||||||
|
def baue_instanz() -> list[list[tuple[int, int]]]:
|
||||||
|
"""Klassischer Job-Shop: Jeder Auftrag besucht jede Maschine genau einmal,
|
||||||
|
in einer eigenen zufaelligen Reihenfolge."""
|
||||||
|
rng = np.random.default_rng(INSTANZ_SEED)
|
||||||
|
return [[(int(m), int(rng.integers(2, 20))) for m in rng.permutation(MASCHINEN)]
|
||||||
|
for _ in range(AUFTRAEGE)]
|
||||||
|
|
||||||
|
|
||||||
|
def loese(auftraege, arbeiter: int):
|
||||||
|
"""Minimiert den Makespan. Gibt Status, Zielwert, Laufzeit und den Plan
|
||||||
|
zurueck - den Plan als Tupel aller Startzeiten, damit sich zwei Laeufe
|
||||||
|
vergleichen lassen."""
|
||||||
|
horizont = sum(dauer for auftrag in auftraege for _, dauer in auftrag)
|
||||||
|
modell = cp_model.CpModel()
|
||||||
|
Gang = collections.namedtuple("Gang", "start ende intervall")
|
||||||
|
plaene: dict[tuple[int, int], Gang] = {}
|
||||||
|
je_maschine = collections.defaultdict(list)
|
||||||
|
|
||||||
|
for a, auftrag in enumerate(auftraege):
|
||||||
|
for g, (maschine, dauer) in enumerate(auftrag):
|
||||||
|
start = modell.NewIntVar(0, horizont, f"start_{a}_{g}")
|
||||||
|
ende = modell.NewIntVar(0, horizont, f"ende_{a}_{g}")
|
||||||
|
intervall = modell.NewIntervalVar(start, dauer, ende, f"iv_{a}_{g}")
|
||||||
|
plaene[a, g] = Gang(start, ende, intervall)
|
||||||
|
je_maschine[maschine].append(intervall)
|
||||||
|
|
||||||
|
for maschine in range(MASCHINEN):
|
||||||
|
modell.AddNoOverlap(je_maschine[maschine])
|
||||||
|
for a, auftrag in enumerate(auftraege):
|
||||||
|
for g in range(len(auftrag) - 1):
|
||||||
|
modell.Add(plaene[a, g + 1].start >= plaene[a, g].ende)
|
||||||
|
|
||||||
|
makespan = modell.NewIntVar(0, horizont, "makespan")
|
||||||
|
modell.AddMaxEquality(
|
||||||
|
makespan, [plaene[a, len(auftrag) - 1].ende
|
||||||
|
for a, auftrag in enumerate(auftraege)])
|
||||||
|
modell.Minimize(makespan)
|
||||||
|
|
||||||
|
loeser = cp_model.CpSolver()
|
||||||
|
loeser.parameters.num_workers = arbeiter
|
||||||
|
loeser.parameters.random_seed = SOLVER_SEED
|
||||||
|
loeser.parameters.max_time_in_seconds = ZEITLIMIT
|
||||||
|
|
||||||
|
beginn = time.perf_counter()
|
||||||
|
status = loeser.Solve(modell)
|
||||||
|
dauer = time.perf_counter() - beginn
|
||||||
|
|
||||||
|
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
|
||||||
|
return loeser.StatusName(status), None, dauer, None
|
||||||
|
plan = tuple(loeser.Value(plaene[a, g].start)
|
||||||
|
for a, auftrag in enumerate(auftraege)
|
||||||
|
for g in range(len(auftrag)))
|
||||||
|
return loeser.StatusName(status), int(loeser.ObjectiveValue()), dauer, plan
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
auftraege = baue_instanz()
|
||||||
|
gaenge = sum(len(a) for a in auftraege)
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print(" WAS MEHRERE ARBEITER WIRKLICH BRINGEN")
|
||||||
|
print("=" * 78)
|
||||||
|
print(f"Job-Shop: {AUFTRAEGE} Auftraege, {MASCHINEN} Maschinen, "
|
||||||
|
f"{gaenge} Arbeitsgaenge.")
|
||||||
|
print(f"random_seed = {SOLVER_SEED} in JEDEM Lauf, "
|
||||||
|
f"{WIEDERHOLUNGEN} Wiederholungen je Arbeiterzahl.\n")
|
||||||
|
|
||||||
|
# Jede Konfiguration mehrfach - ein einzelner Zeitwert sagt nichts.
|
||||||
|
ergebnisse: dict[int, list[tuple]] = {}
|
||||||
|
for arbeiter in ARBEITERZAHLEN:
|
||||||
|
ergebnisse[arbeiter] = [loese(auftraege, arbeiter)
|
||||||
|
for _ in range(WIEDERHOLUNGEN)]
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print(" (1) Laufzeit: was die Arbeiter bringen")
|
||||||
|
print("=" * 78)
|
||||||
|
print(f"{'Arbeiter':>9} {'Status':>9} {'Makespan':>9} "
|
||||||
|
f"{'schnellste':>11} {'langsamste':>11} {'Beschleunigung':>15}")
|
||||||
|
print("-" * 78)
|
||||||
|
basis_zeit = min(d for _, _, d, _ in ergebnisse[1])
|
||||||
|
for arbeiter in ARBEITERZAHLEN:
|
||||||
|
laeufe = ergebnisse[arbeiter]
|
||||||
|
zeiten = [d for _, _, d, _ in laeufe]
|
||||||
|
ziele = {o for _, o, _, _ in laeufe}
|
||||||
|
status = {s for s, _, _, _ in laeufe}
|
||||||
|
print(f"{arbeiter:>9} {'/'.join(sorted(status)):>9} "
|
||||||
|
f"{'/'.join(str(z) for z in sorted(ziele)):>9} "
|
||||||
|
f"{min(zeiten):>10.2f}s {max(zeiten):>10.2f}s "
|
||||||
|
f"{basis_zeit / min(zeiten):>14.1f}x")
|
||||||
|
|
||||||
|
beste = min(ARBEITERZAHLEN, key=lambda w: min(d for _, _, d, _ in ergebnisse[w]))
|
||||||
|
faktor = basis_zeit / min(d for _, _, d, _ in ergebnisse[beste])
|
||||||
|
print(f"\n Am schnellsten sind {beste} Arbeiter: Faktor {faktor:.1f} gegenueber")
|
||||||
|
print(f" einem einzigen - also MEHR, als {beste} Kerne hergeben sollten.")
|
||||||
|
print(f" Das ist kein Messfehler. CP-SAT laesst nicht {beste}-mal dieselbe Suche")
|
||||||
|
print(" laufen, sondern verschiedene Strategien nebeneinander, die einander")
|
||||||
|
print(" ihre Schranken zurufen. Der zweite Arbeiter ist deshalb nicht der")
|
||||||
|
print(" 'zweite Kern', sondern ein anderes Verfahren.")
|
||||||
|
letzte = ARBEITERZAHLEN[-1]
|
||||||
|
if beste != letzte:
|
||||||
|
zeit_letzte = min(d for _, _, d, _ in ergebnisse[letzte])
|
||||||
|
print(f"\n Und mehr ist nicht immer besser: {letzte} Arbeiter brauchen "
|
||||||
|
f"{zeit_letzte:.2f}s")
|
||||||
|
print(f" gegenueber {min(d for _, _, d, _ in ergebnisse[beste]):.2f}s bei "
|
||||||
|
f"{beste}. Ab einer gewissen Zahl kosten Abstimmung")
|
||||||
|
print(" und Speicherbandbreite mehr, als ein weiterer Suchstrang einbringt.")
|
||||||
|
|
||||||
|
print("\n" + "=" * 78)
|
||||||
|
print(" (2) Reproduzierbarkeit: derselbe Seed, derselbe Plan?")
|
||||||
|
print("=" * 78)
|
||||||
|
print(f"{'Arbeiter':>9} {'Laeufe':>7} {'verschiedene Plaene':>21} "
|
||||||
|
f"{'Makespan':>10}")
|
||||||
|
print("-" * 78)
|
||||||
|
for arbeiter in ARBEITERZAHLEN:
|
||||||
|
plaene = {p for _, _, _, p in ergebnisse[arbeiter] if p is not None}
|
||||||
|
ziele = {o for _, o, _, _ in ergebnisse[arbeiter]}
|
||||||
|
print(f"{arbeiter:>9} {WIEDERHOLUNGEN:>7} {len(plaene):>21} "
|
||||||
|
f"{'/'.join(str(z) for z in sorted(ziele)):>10}")
|
||||||
|
|
||||||
|
einer = len({p for _, _, _, p in ergebnisse[1]})
|
||||||
|
# Ab welcher Arbeiterzahl bricht die Reproduzierbarkeit? Das ist die
|
||||||
|
# eigentliche Zahl - nicht, was acht Arbeiter anrichten.
|
||||||
|
ab = next((w for w in ARBEITERZAHLEN
|
||||||
|
if len({p for _, _, _, p in ergebnisse[w]}) > 1), None)
|
||||||
|
print(f"\n Mit einem Arbeiter: {einer} Plan aus {WIEDERHOLUNGEN} Laeufen.")
|
||||||
|
if ab is not None:
|
||||||
|
viele = len({p for _, _, _, p in ergebnisse[ab]})
|
||||||
|
print(f" Schon mit {ab} Arbeitern: {viele} verschiedene Plaene aus "
|
||||||
|
f"{WIEDERHOLUNGEN} Laeufen -")
|
||||||
|
print(" bei identischem random_seed und identischem Zielwert.")
|
||||||
|
print(f" Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit")
|
||||||
|
print(f" zu verlieren. {ab} genuegen.")
|
||||||
|
print("\n Der Seed allein macht einen Lauf also NICHT reproduzierbar. Er legt")
|
||||||
|
print(" fest, wie ein einzelner Suchstrang wuerfelt - nicht, welcher von")
|
||||||
|
print(" mehreren zuerst fertig wird. Das entscheidet die Uhr.")
|
||||||
|
|
||||||
|
print("\n" + "=" * 78)
|
||||||
|
print(" (3) Was daraus fuer Tests folgt")
|
||||||
|
print("=" * 78)
|
||||||
|
print(" Ein Test der Form assert plan == erwarteter_plan besteht mal und")
|
||||||
|
print(" scheitert mal, ohne dass sich am Code etwas geaendert haette.")
|
||||||
|
print(" Zu pruefen sind stattdessen:")
|
||||||
|
print(" * der ZIELWERT (hier in allen Laeufen gleich),")
|
||||||
|
print(" * die Einhaltung aller Regeln (Abnahmepruefung, Kapitel Praxisfallen),")
|
||||||
|
print(" * und der Status - nicht die Gestalt der Loesung.")
|
||||||
|
print("\n Wer doch einen bestimmten Plan braucht - fuer eine abgedruckte")
|
||||||
|
print(" Ausgabe, fuer einen Regressionstest -, setzt num_workers = 1.")
|
||||||
|
print(f" Das kostet hier den Faktor {faktor:.1f} an Laufzeit und ist genau")
|
||||||
|
print(" deshalb eine Entscheidung fuer den Test, nicht fuer den Betrieb.")
|
||||||
|
print("=" * 78)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erwartete Ausgabe** (Laufzeiten *und* die Zahl der verschiedenen Pläne sind
|
||||||
|
hardwareabhängig — genau das ist die Aussage; der Zielwert ist es nicht):
|
||||||
|
|
||||||
|
```
|
||||||
|
==============================================================================
|
||||||
|
WAS MEHRERE ARBEITER WIRKLICH BRINGEN
|
||||||
|
==============================================================================
|
||||||
|
Job-Shop: 12 Auftraege, 10 Maschinen, 120 Arbeitsgaenge.
|
||||||
|
random_seed = 1 in JEDEM Lauf, 4 Wiederholungen je Arbeiterzahl.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
(1) Laufzeit: was die Arbeiter bringen
|
||||||
|
==============================================================================
|
||||||
|
Arbeiter Status Makespan schnellste langsamste Beschleunigung
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
1 OPTIMAL 183 5.62s 5.82s 1.0x
|
||||||
|
2 OPTIMAL 183 1.43s 2.04s 3.9x
|
||||||
|
4 OPTIMAL 183 0.60s 0.66s 9.4x
|
||||||
|
8 OPTIMAL 183 0.46s 0.65s 12.3x
|
||||||
|
|
||||||
|
Am schnellsten sind 8 Arbeiter: Faktor 12.3 gegenueber
|
||||||
|
einem einzigen - also MEHR, als 8 Kerne hergeben sollten.
|
||||||
|
Das ist kein Messfehler. CP-SAT laesst nicht 8-mal dieselbe Suche
|
||||||
|
laufen, sondern verschiedene Strategien nebeneinander, die einander
|
||||||
|
ihre Schranken zurufen. Der zweite Arbeiter ist deshalb nicht der
|
||||||
|
'zweite Kern', sondern ein anderes Verfahren.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
(2) Reproduzierbarkeit: derselbe Seed, derselbe Plan?
|
||||||
|
==============================================================================
|
||||||
|
Arbeiter Laeufe verschiedene Plaene Makespan
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
1 4 1 183
|
||||||
|
2 4 3 183
|
||||||
|
4 4 3 183
|
||||||
|
8 4 4 183
|
||||||
|
|
||||||
|
Mit einem Arbeiter: 1 Plan aus 4 Laeufen.
|
||||||
|
Schon mit 2 Arbeitern: 3 verschiedene Plaene aus 4 Laeufen -
|
||||||
|
bei identischem random_seed und identischem Zielwert.
|
||||||
|
Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit
|
||||||
|
zu verlieren. 2 genuegen.
|
||||||
|
|
||||||
|
Der Seed allein macht einen Lauf also NICHT reproduzierbar. Er legt
|
||||||
|
fest, wie ein einzelner Suchstrang wuerfelt - nicht, welcher von
|
||||||
|
mehreren zuerst fertig wird. Das entscheidet die Uhr.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
(3) Was daraus fuer Tests folgt
|
||||||
|
==============================================================================
|
||||||
|
Ein Test der Form assert plan == erwarteter_plan besteht mal und
|
||||||
|
scheitert mal, ohne dass sich am Code etwas geaendert haette.
|
||||||
|
Zu pruefen sind stattdessen:
|
||||||
|
* der ZIELWERT (hier in allen Laeufen gleich),
|
||||||
|
* die Einhaltung aller Regeln (Abnahmepruefung, Kapitel Praxisfallen),
|
||||||
|
* und der Status - nicht die Gestalt der Loesung.
|
||||||
|
|
||||||
|
Wer doch einen bestimmten Plan braucht - fuer eine abgedruckte
|
||||||
|
Ausgabe, fuer einen Regressionstest -, setzt num_workers = 1.
|
||||||
|
Das kostet hier den Faktor 12.3 an Laufzeit und ist genau
|
||||||
|
deshalb eine Entscheidung fuer den Test, nicht fuer den Betrieb.
|
||||||
|
==============================================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
### Was die Messung zeigt
|
||||||
|
|
||||||
|
**Erstens: Die Beschleunigung ist überlinear.** Acht Arbeiter sind im abgedruckten Lauf
|
||||||
|
nicht achtmal, sondern **gut zwölfmal** schneller als einer. Das sieht nach Messfehler aus, ist aber die
|
||||||
|
korrekte Beschreibung dessen, was CP-SAT tut: Es lässt **nicht** achtmal dieselbe Suche auf
|
||||||
|
verschiedenen Kernen laufen, sondern **verschiedene Strategien nebeneinander** — Suche mit
|
||||||
|
LP-Relaxation, Suche mit anderen Verzweigungsregeln, lokale Suche —, und die teilen
|
||||||
|
einander ihre Schranken mit. Der zweite Arbeiter ist deshalb nicht „der zweite Kern“,
|
||||||
|
sondern ein anderes Verfahren, das zufällig auch rechnet.
|
||||||
|
|
||||||
|
Daraus folgt zweierlei. Ein Speedup über der Kernzahl ist bei CP-SAT normal und kein Grund
|
||||||
|
zum Misstrauen. Und der Zugewinn flacht ab: Zwischen vier und acht Arbeitern lag in unseren
|
||||||
|
Wiederholungen mal das eine, mal das andere vorn — ab einer gewissen Zahl kosten Abstimmung
|
||||||
|
und Speicherbandbreite mehr, als ein weiterer Suchstrang einbringt. Auch hier gilt: messen,
|
||||||
|
nicht schätzen.
|
||||||
|
|
||||||
|
**Zweitens — und das ist der Befund, der die Testpraxis bestimmt: Der Seed genügt nicht.**
|
||||||
|
Bei einem Arbeiter liefern vier Läufe **einen** Plan. Schon bei **zwei** Arbeitern liefern
|
||||||
|
vier Läufe **drei verschiedene** — bei identischem `random_seed` und identischem Zielwert
|
||||||
|
183.
|
||||||
|
|
||||||
|
Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit zu verlieren. Zwei genügen.
|
||||||
|
Der Grund ist einfach, sobald man ihn ausspricht: Der Seed legt fest, wie ein **einzelner**
|
||||||
|
Suchstrang würfelt. Er legt nicht fest, **welcher von mehreren zuerst fertig wird** — das
|
||||||
|
entscheidet die Uhr, und die ist bei jedem Lauf anders.
|
||||||
|
|
||||||
|
> **⚠️ `random_seed` allein macht nichts reproduzierbar**
|
||||||
|
> Ein häufiger Irrtum: „Ich habe doch einen Seed gesetzt.“ Für einen Zufallsgenerator
|
||||||
|
> stimmt das. Für einen **parallelen** Solver nicht — dort ist die zweite Zufallsquelle die
|
||||||
|
> Ausführungsreihenfolge, und die lässt sich nicht mit einem Parameter festnageln.
|
||||||
|
> Reproduzierbar wird der Lauf erst mit `num_workers = 1` **und** Seed. Wer nur eines von
|
||||||
|
> beidem setzt, hat einen Test, der gelegentlich grundlos rot wird.
|
||||||
|
|
||||||
|
### Was daraus für Tests folgt
|
||||||
|
|
||||||
|
Der Zielwert war in **allen** sechzehn Läufen derselbe: 183. Die Gestalt der Lösung war es
|
||||||
|
nicht. Genau daran entscheidet sich, was ein Test prüfen darf:
|
||||||
|
|
||||||
|
| Prüfen | Nicht prüfen |
|
||||||
|
| --- | --- |
|
||||||
|
| den **Zielwert** — er ist eindeutig, wenn das Modell optimal gelöst wurde | die **Gestalt** des Plans (`assert plan == erwarteter_plan`) |
|
||||||
|
| die **Einhaltung aller Regeln** über eine Abnahmeprüfung ({ref:sec:praxisfallen-or-kern}) | die Reihenfolge, in der gleichwertige Aufträge zugewiesen wurden |
|
||||||
|
| den **Status** (`OPTIMAL`, `FEASIBLE`, `INFEASIBLE`) | die Laufzeit als feste Zahl |
|
||||||
|
|
||||||
|
Wer doch einen bestimmten Plan braucht — für eine abgedruckte Ausgabe, für einen
|
||||||
|
Regressionstest —, setzt `num_workers = 1`. Das kostet hier den Faktor 12 an Laufzeit, und
|
||||||
|
genau deshalb ist es eine Entscheidung **für den Test**, nicht für den Betrieb. In
|
||||||
|
Produktion lässt man beide Parameter auf ihren Standardwerten.
|
||||||
|
|
||||||
|
> **🎯 Merksatz**
|
||||||
|
> Ein paralleler Solver hat zwei Zufallsquellen: den Seed und die Uhr. Nur die erste lässt
|
||||||
|
> sich setzen. Wer Reproduzierbarkeit braucht, muss die zweite abschalten — und das heißt
|
||||||
|
> `num_workers = 1`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Die fünf Antworten von CP-SAT {#sec:cpsat-statusfaelle}
|
## Die fünf Antworten von CP-SAT {#sec:cpsat-statusfaelle}
|
||||||
|
|
||||||
{ref:sec:milp-gap} hat gezeigt, warum `OPTIMAL` bei ganzzahligen Problemen nicht der
|
{ref:sec:milp-gap} hat gezeigt, warum `OPTIMAL` bei ganzzahligen Problemen nicht der
|
||||||
|
|
@ -1180,6 +1516,16 @@ Lösen Sie ein 9×9-Sudoku mit CP-SAT. Sie brauchen nur `NewIntVar(1, 9, ...)` u
|
||||||
`AddAllDifferent` für Zeilen, Spalten und die neun 3×3-Blöcke. Wie viele Zeilen Modellcode
|
`AddAllDifferent` für Zeilen, Spalten und die neun 3×3-Blöcke. Wie viele Zeilen Modellcode
|
||||||
benötigen Sie?
|
benötigen Sie?
|
||||||
|
|
||||||
|
**Aufgabe ⭐⭐ — Die Grenze der Parallelität finden.**
|
||||||
|
`Parallele_Suche.py` misst 1, 2, 4 und 8 Arbeiter.
|
||||||
|
(a) Ergänzen Sie 16 und 24 (bzw. die Kernzahl Ihres Rechners). Ab wo bringt ein weiterer
|
||||||
|
Arbeiter nichts mehr — und wird es irgendwann wieder schlechter?
|
||||||
|
(b) Setzen Sie `random_seed` bei jedem Lauf auf einen **anderen** Wert, aber
|
||||||
|
`num_workers = 1`. Wie viele verschiedene Pläne kommen jetzt heraus? Was folgt daraus
|
||||||
|
für die Frage, welcher der beiden Parameter die Reproduzierbarkeit sichert?
|
||||||
|
(c) Vergrößern Sie die Instanz auf 15 Aufträge. Ab welcher Größe erreicht der Lauf mit
|
||||||
|
einem Arbeiter das Zeitlimit, während acht noch `OPTIMAL` melden?
|
||||||
|
|
||||||
**Aufgabe ⭐⭐⭐ — Job-Shop erweitern.**
|
**Aufgabe ⭐⭐⭐ — Job-Shop erweitern.**
|
||||||
Ergänzen Sie `JobShop_Intervalle.py` um:
|
Ergänzen Sie `JobShop_Intervalle.py` um:
|
||||||
(a) Rüstzeiten von 1 Zeiteinheit zwischen zwei Aufträgen auf derselben Maschine.
|
(a) Rüstzeiten von 1 Zeiteinheit zwischen zwei Aufträgen auf derselben Maschine.
|
||||||
|
|
@ -1466,6 +1812,10 @@ Nebenbedingungen. Alles, was einen Preis hat, wird irgendwann gekauft.
|
||||||
3. Was ist eine Intervallvariable, und welche Bedingung garantiert sie automatisch?
|
3. Was ist eine Intervallvariable, und welche Bedingung garantiert sie automatisch?
|
||||||
4. Warum sollten möglichst wenige Bedingungen hart formuliert werden?
|
4. Warum sollten möglichst wenige Bedingungen hart formuliert werden?
|
||||||
5. Warum ist die Aufschlüsselung der Strafkosten in der Praxis wichtiger als der Zielwert
|
5. Warum ist die Aufschlüsselung der Strafkosten in der Praxis wichtiger als der Zielwert
|
||||||
|
6. Sie setzen `random_seed = 1`, bekommen aber trotzdem bei jedem Lauf einen anderen Plan.
|
||||||
|
Woran liegt das, und was hilft?
|
||||||
|
7. Warum kann ein CP-SAT-Lauf mit acht Arbeitern **mehr** als achtmal schneller sein als
|
||||||
|
mit einem?
|
||||||
selbst?
|
selbst?
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -1491,6 +1841,13 @@ Nebenbedingungen. Alles, was einen Preis hat, wird irgendwann gekauft.
|
||||||
* **Bei mehrdeutigen Optima nie auf einen bestimmten Plan testen** — nur auf den Zielwert
|
* **Bei mehrdeutigen Optima nie auf einen bestimmten Plan testen** — nur auf den Zielwert
|
||||||
und die Einhaltung aller Regeln.
|
und die Einhaltung aller Regeln.
|
||||||
|
|
||||||
|
* **Ein paralleler Solver hat zwei Zufallsquellen: den Seed und die Uhr.** Nur die erste
|
||||||
|
lässt sich setzen. Reproduzierbar wird ein Lauf erst mit `num_workers = 1` **und** Seed —
|
||||||
|
gemessen in {ref:sec:cpsat-parallele-suche}: schon zwei Arbeiter liefern bei identischem
|
||||||
|
Seed drei verschiedene Pläne zum selben Zielwert.
|
||||||
|
* **Parallele Suche kann überlinear beschleunigen**, weil CP-SAT nicht dieselbe Suche
|
||||||
|
vervielfacht, sondern verschiedene Strategien nebeneinander laufen lässt, die einander
|
||||||
|
ihre Schranken mitteilen.
|
||||||
**Ausblick.** {ref:kap:graphen} behandelt Probleme, die sich am besten als **Graph** begreifen
|
**Ausblick.** {ref:kap:graphen} behandelt Probleme, die sich am besten als **Graph** begreifen
|
||||||
lassen: Flüsse durch Netzwerke, Zuordnungen und die Tourenplanung mit Fahrzeugen und
|
lassen: Flüsse durch Netzwerke, Zuordnungen und die Tourenplanung mit Fahrzeugen und
|
||||||
Zeitfenstern.
|
Zeitfenstern.
|
||||||
|
|
|
||||||
|
|
@ -850,6 +850,31 @@ for i in range(9):
|
||||||
**Etwa 10 Zeilen Modellcode** — und der Solver löst jedes Sudoku in Millisekunden. Das ist
|
**Etwa 10 Zeilen Modellcode** — und der Solver löst jedes Sudoku in Millisekunden. Das ist
|
||||||
die Stärke globaler Constraints.
|
die Stärke globaler Constraints.
|
||||||
|
|
||||||
|
**{loesung} — Die Grenze der Parallelität finden.**
|
||||||
|
(a) Der Gewinn flacht ab und kehrt sich um. Auf einem Rechner mit 24 Kernen gemessen
|
||||||
|
(Faktor gegenüber einem Arbeiter): 8 Arbeiter **10,2**, 16 Arbeiter **12,3**,
|
||||||
|
24 Arbeiter **9,0**. Wer alle Kerne belegt, ist langsamer als mit zwei Dritteln — dem
|
||||||
|
Betriebssystem, dem Speicherbus und den Suchsträngen untereinander bleibt zu wenig Luft.
|
||||||
|
Die Faustregel „so viele Arbeiter wie Kerne" ist also falsch; die richtige Zahl ist eine
|
||||||
|
Messung, keine Einstellung.
|
||||||
|
(b) **Fünf verschiedene Pläne aus fünf Seeds** — bei `num_workers = 1` und identischem
|
||||||
|
Zielwert 183. Das ist die andere Hälfte der Antwort: Der Seed *wirkt*, aber nur auf den
|
||||||
|
einzelnen Suchstrang. Es sichert also **keiner der beiden Parameter allein** die
|
||||||
|
Reproduzierbarkeit:
|
||||||
|
|
||||||
|
| | Seed fest | Seed variabel |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **1 Arbeiter** | reproduzierbar | 5 Pläne aus 5 Läufen |
|
||||||
|
| **mehrere Arbeiter** | 3 Pläne aus 4 Läufen | erst recht nicht |
|
||||||
|
|
||||||
|
Nur die Kombination trägt.
|
||||||
|
(c) Bei **15 Aufträgen** (60 s Zeitlimit): Ein Arbeiter läuft ins Limit und meldet
|
||||||
|
`FEASIBLE` mit Makespan 200; acht Arbeiter melden `OPTIMAL` mit **demselben** Makespan 200
|
||||||
|
nach 26,4 s. Bei 13 Aufträgen sind es 11,2 s gegen 2,8 s, bei 14 schon 21,4 s gegen 3,7 s.
|
||||||
|
Bemerkenswert ist, dass beide denselben Wert finden — der Unterschied liegt nicht in der
|
||||||
|
Lösung, sondern im **Beweis**, dass es keine bessere gibt. Genau diese Arbeit teilen sich
|
||||||
|
die parallelen Stränge auf.
|
||||||
|
|
||||||
**{loesung} — Job-Shop erweitern.**
|
**{loesung} — Job-Shop erweitern.**
|
||||||
(a) Rüstzeiten: `AddNoOverlap` durch paarweise Disjunktionen mit Übergangszeit ersetzen,
|
(a) Rüstzeiten: `AddNoOverlap` durch paarweise Disjunktionen mit Übergangszeit ersetzen,
|
||||||
oder `AddCircuit` je Maschine mit Übergangsmatrix.
|
oder `AddCircuit` je Maschine mit Übergangsmatrix.
|
||||||
|
|
@ -950,6 +975,15 @@ Regeln. Wer eine reproduzierbare Ausgabe braucht — etwa für ein Buch —, fix
|
||||||
4. Weil jede zusätzliche harte Bedingung das Risiko von `INFEASIBLE` erhöht — und eine
|
4. Weil jede zusätzliche harte Bedingung das Risiko von `INFEASIBLE` erhöht — und eine
|
||||||
Fehlermeldung dem Anwender nicht hilft.
|
Fehlermeldung dem Anwender nicht hilft.
|
||||||
5. Weil Anwender die Frage „Warum ich?“ stellen. Ohne Antwort wird das System umgangen.
|
5. Weil Anwender die Frage „Warum ich?“ stellen. Ohne Antwort wird das System umgangen.
|
||||||
|
6. Weil der Seed nur festlegt, wie ein **einzelner** Suchstrang würfelt — nicht, welcher
|
||||||
|
von mehreren parallelen zuerst fertig wird. Das entscheidet die Ausführungsreihenfolge,
|
||||||
|
also die Uhr. Abhilfe: zusätzlich `num_workers = 1`. Gemessen: schon zwei Arbeiter
|
||||||
|
liefern bei identischem Seed drei verschiedene Pläne zum selben Zielwert.
|
||||||
|
7. Weil CP-SAT nicht dieselbe Suche vervielfacht, sondern **verschiedene Strategien**
|
||||||
|
nebeneinander laufen lässt (LP-gestützt, andere Verzweigungsregeln, lokale Suche), die
|
||||||
|
einander ihre Schranken mitteilen. Ein zusätzlicher Arbeiter ist deshalb nicht ein
|
||||||
|
weiterer Kern für dieselbe Arbeit, sondern ein anderes Verfahren — und wenn eines davon
|
||||||
|
früh eine gute Schranke findet, profitieren alle.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Parallele_Suche.py
|
||||||
|
"""
|
||||||
|
Kapitel CP-SAT: Was mehrere Arbeiter wirklich bringen - und was sie kosten.
|
||||||
|
|
||||||
|
Das Buch setzt an mehreren Stellen 'num_workers = 1', damit die abgedruckte
|
||||||
|
Ausgabe reproduzierbar ist. Behauptet wird dabei zweierlei: dass parallele
|
||||||
|
Suche schneller ist, und dass sie von Lauf zu Lauf verschiedene, gleich gute
|
||||||
|
Loesungen findet. Beides wird hier gemessen statt geglaubt.
|
||||||
|
|
||||||
|
Gerechnet wird auf einem Job-Shop wie in JobShop_Intervalle.py, nur gross
|
||||||
|
genug, dass die Suche wirklich arbeitet: 12 Auftraege auf 10 Maschinen,
|
||||||
|
jeder Auftrag besucht jede Maschine.
|
||||||
|
|
||||||
|
Zwei Messungen:
|
||||||
|
1. Dieselbe Aufgabe mit 1, 2, 4 und 8 Arbeitern - Laufzeit und Ergebnis.
|
||||||
|
2. Dieselbe Konfiguration mehrfach, bei FESTEM random_seed - wie oft kommt
|
||||||
|
derselbe Plan heraus?
|
||||||
|
|
||||||
|
Die zweite ist die wichtigere: Sie entscheidet, wie man Tests schreibt.
|
||||||
|
|
||||||
|
Achtung, Laufzeiten sind hardwareabhaengig. Die ZAHL der verschiedenen
|
||||||
|
Plaene ist es auch - und genau das ist die Aussage.
|
||||||
|
|
||||||
|
Benoetigt: numpy, ortools
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from ortools.sat.python import cp_model
|
||||||
|
|
||||||
|
AUFTRAEGE = 12
|
||||||
|
MASCHINEN = 10
|
||||||
|
INSTANZ_SEED = 20260908
|
||||||
|
SOLVER_SEED = 1 # bleibt ueber ALLE Laeufe gleich - das ist der Punkt
|
||||||
|
ARBEITERZAHLEN = (1, 2, 4, 8)
|
||||||
|
WIEDERHOLUNGEN = 4
|
||||||
|
ZEITLIMIT = 120.0
|
||||||
|
|
||||||
|
|
||||||
|
def baue_instanz() -> list[list[tuple[int, int]]]:
|
||||||
|
"""Klassischer Job-Shop: Jeder Auftrag besucht jede Maschine genau einmal,
|
||||||
|
in einer eigenen zufaelligen Reihenfolge."""
|
||||||
|
rng = np.random.default_rng(INSTANZ_SEED)
|
||||||
|
return [[(int(m), int(rng.integers(2, 20))) for m in rng.permutation(MASCHINEN)]
|
||||||
|
for _ in range(AUFTRAEGE)]
|
||||||
|
|
||||||
|
|
||||||
|
def loese(auftraege, arbeiter: int):
|
||||||
|
"""Minimiert den Makespan. Gibt Status, Zielwert, Laufzeit und den Plan
|
||||||
|
zurueck - den Plan als Tupel aller Startzeiten, damit sich zwei Laeufe
|
||||||
|
vergleichen lassen."""
|
||||||
|
horizont = sum(dauer for auftrag in auftraege for _, dauer in auftrag)
|
||||||
|
modell = cp_model.CpModel()
|
||||||
|
Gang = collections.namedtuple("Gang", "start ende intervall")
|
||||||
|
plaene: dict[tuple[int, int], Gang] = {}
|
||||||
|
je_maschine = collections.defaultdict(list)
|
||||||
|
|
||||||
|
for a, auftrag in enumerate(auftraege):
|
||||||
|
for g, (maschine, dauer) in enumerate(auftrag):
|
||||||
|
start = modell.NewIntVar(0, horizont, f"start_{a}_{g}")
|
||||||
|
ende = modell.NewIntVar(0, horizont, f"ende_{a}_{g}")
|
||||||
|
intervall = modell.NewIntervalVar(start, dauer, ende, f"iv_{a}_{g}")
|
||||||
|
plaene[a, g] = Gang(start, ende, intervall)
|
||||||
|
je_maschine[maschine].append(intervall)
|
||||||
|
|
||||||
|
for maschine in range(MASCHINEN):
|
||||||
|
modell.AddNoOverlap(je_maschine[maschine])
|
||||||
|
for a, auftrag in enumerate(auftraege):
|
||||||
|
for g in range(len(auftrag) - 1):
|
||||||
|
modell.Add(plaene[a, g + 1].start >= plaene[a, g].ende)
|
||||||
|
|
||||||
|
makespan = modell.NewIntVar(0, horizont, "makespan")
|
||||||
|
modell.AddMaxEquality(
|
||||||
|
makespan, [plaene[a, len(auftrag) - 1].ende
|
||||||
|
for a, auftrag in enumerate(auftraege)])
|
||||||
|
modell.Minimize(makespan)
|
||||||
|
|
||||||
|
loeser = cp_model.CpSolver()
|
||||||
|
loeser.parameters.num_workers = arbeiter
|
||||||
|
loeser.parameters.random_seed = SOLVER_SEED
|
||||||
|
loeser.parameters.max_time_in_seconds = ZEITLIMIT
|
||||||
|
|
||||||
|
beginn = time.perf_counter()
|
||||||
|
status = loeser.Solve(modell)
|
||||||
|
dauer = time.perf_counter() - beginn
|
||||||
|
|
||||||
|
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
|
||||||
|
return loeser.StatusName(status), None, dauer, None
|
||||||
|
plan = tuple(loeser.Value(plaene[a, g].start)
|
||||||
|
for a, auftrag in enumerate(auftraege)
|
||||||
|
for g in range(len(auftrag)))
|
||||||
|
return loeser.StatusName(status), int(loeser.ObjectiveValue()), dauer, plan
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
auftraege = baue_instanz()
|
||||||
|
gaenge = sum(len(a) for a in auftraege)
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print(" WAS MEHRERE ARBEITER WIRKLICH BRINGEN")
|
||||||
|
print("=" * 78)
|
||||||
|
print(f"Job-Shop: {AUFTRAEGE} Auftraege, {MASCHINEN} Maschinen, "
|
||||||
|
f"{gaenge} Arbeitsgaenge.")
|
||||||
|
print(f"random_seed = {SOLVER_SEED} in JEDEM Lauf, "
|
||||||
|
f"{WIEDERHOLUNGEN} Wiederholungen je Arbeiterzahl.\n")
|
||||||
|
|
||||||
|
# Jede Konfiguration mehrfach - ein einzelner Zeitwert sagt nichts.
|
||||||
|
ergebnisse: dict[int, list[tuple]] = {}
|
||||||
|
for arbeiter in ARBEITERZAHLEN:
|
||||||
|
ergebnisse[arbeiter] = [loese(auftraege, arbeiter)
|
||||||
|
for _ in range(WIEDERHOLUNGEN)]
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print(" (1) Laufzeit: was die Arbeiter bringen")
|
||||||
|
print("=" * 78)
|
||||||
|
print(f"{'Arbeiter':>9} {'Status':>9} {'Makespan':>9} "
|
||||||
|
f"{'schnellste':>11} {'langsamste':>11} {'Beschleunigung':>15}")
|
||||||
|
print("-" * 78)
|
||||||
|
basis_zeit = min(d for _, _, d, _ in ergebnisse[1])
|
||||||
|
for arbeiter in ARBEITERZAHLEN:
|
||||||
|
laeufe = ergebnisse[arbeiter]
|
||||||
|
zeiten = [d for _, _, d, _ in laeufe]
|
||||||
|
ziele = {o for _, o, _, _ in laeufe}
|
||||||
|
status = {s for s, _, _, _ in laeufe}
|
||||||
|
print(f"{arbeiter:>9} {'/'.join(sorted(status)):>9} "
|
||||||
|
f"{'/'.join(str(z) for z in sorted(ziele)):>9} "
|
||||||
|
f"{min(zeiten):>10.2f}s {max(zeiten):>10.2f}s "
|
||||||
|
f"{basis_zeit / min(zeiten):>14.1f}x")
|
||||||
|
|
||||||
|
beste = min(ARBEITERZAHLEN, key=lambda w: min(d for _, _, d, _ in ergebnisse[w]))
|
||||||
|
faktor = basis_zeit / min(d for _, _, d, _ in ergebnisse[beste])
|
||||||
|
print(f"\n Am schnellsten sind {beste} Arbeiter: Faktor {faktor:.1f} gegenueber")
|
||||||
|
print(f" einem einzigen - also MEHR, als {beste} Kerne hergeben sollten.")
|
||||||
|
print(f" Das ist kein Messfehler. CP-SAT laesst nicht {beste}-mal dieselbe Suche")
|
||||||
|
print(" laufen, sondern verschiedene Strategien nebeneinander, die einander")
|
||||||
|
print(" ihre Schranken zurufen. Der zweite Arbeiter ist deshalb nicht der")
|
||||||
|
print(" 'zweite Kern', sondern ein anderes Verfahren.")
|
||||||
|
letzte = ARBEITERZAHLEN[-1]
|
||||||
|
if beste != letzte:
|
||||||
|
zeit_letzte = min(d for _, _, d, _ in ergebnisse[letzte])
|
||||||
|
print(f"\n Und mehr ist nicht immer besser: {letzte} Arbeiter brauchen "
|
||||||
|
f"{zeit_letzte:.2f}s")
|
||||||
|
print(f" gegenueber {min(d for _, _, d, _ in ergebnisse[beste]):.2f}s bei "
|
||||||
|
f"{beste}. Ab einer gewissen Zahl kosten Abstimmung")
|
||||||
|
print(" und Speicherbandbreite mehr, als ein weiterer Suchstrang einbringt.")
|
||||||
|
|
||||||
|
print("\n" + "=" * 78)
|
||||||
|
print(" (2) Reproduzierbarkeit: derselbe Seed, derselbe Plan?")
|
||||||
|
print("=" * 78)
|
||||||
|
print(f"{'Arbeiter':>9} {'Laeufe':>7} {'verschiedene Plaene':>21} "
|
||||||
|
f"{'Makespan':>10}")
|
||||||
|
print("-" * 78)
|
||||||
|
for arbeiter in ARBEITERZAHLEN:
|
||||||
|
plaene = {p for _, _, _, p in ergebnisse[arbeiter] if p is not None}
|
||||||
|
ziele = {o for _, o, _, _ in ergebnisse[arbeiter]}
|
||||||
|
print(f"{arbeiter:>9} {WIEDERHOLUNGEN:>7} {len(plaene):>21} "
|
||||||
|
f"{'/'.join(str(z) for z in sorted(ziele)):>10}")
|
||||||
|
|
||||||
|
einer = len({p for _, _, _, p in ergebnisse[1]})
|
||||||
|
# Ab welcher Arbeiterzahl bricht die Reproduzierbarkeit? Das ist die
|
||||||
|
# eigentliche Zahl - nicht, was acht Arbeiter anrichten.
|
||||||
|
ab = next((w for w in ARBEITERZAHLEN
|
||||||
|
if len({p for _, _, _, p in ergebnisse[w]}) > 1), None)
|
||||||
|
print(f"\n Mit einem Arbeiter: {einer} Plan aus {WIEDERHOLUNGEN} Laeufen.")
|
||||||
|
if ab is not None:
|
||||||
|
viele = len({p for _, _, _, p in ergebnisse[ab]})
|
||||||
|
print(f" Schon mit {ab} Arbeitern: {viele} verschiedene Plaene aus "
|
||||||
|
f"{WIEDERHOLUNGEN} Laeufen -")
|
||||||
|
print(" bei identischem random_seed und identischem Zielwert.")
|
||||||
|
print(f" Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit")
|
||||||
|
print(f" zu verlieren. {ab} genuegen.")
|
||||||
|
print("\n Der Seed allein macht einen Lauf also NICHT reproduzierbar. Er legt")
|
||||||
|
print(" fest, wie ein einzelner Suchstrang wuerfelt - nicht, welcher von")
|
||||||
|
print(" mehreren zuerst fertig wird. Das entscheidet die Uhr.")
|
||||||
|
|
||||||
|
print("\n" + "=" * 78)
|
||||||
|
print(" (3) Was daraus fuer Tests folgt")
|
||||||
|
print("=" * 78)
|
||||||
|
print(" Ein Test der Form assert plan == erwarteter_plan besteht mal und")
|
||||||
|
print(" scheitert mal, ohne dass sich am Code etwas geaendert haette.")
|
||||||
|
print(" Zu pruefen sind stattdessen:")
|
||||||
|
print(" * der ZIELWERT (hier in allen Laeufen gleich),")
|
||||||
|
print(" * die Einhaltung aller Regeln (Abnahmepruefung, Kapitel Praxisfallen),")
|
||||||
|
print(" * und der Status - nicht die Gestalt der Loesung.")
|
||||||
|
print("\n Wer doch einen bestimmten Plan braucht - fuer eine abgedruckte")
|
||||||
|
print(" Ausgabe, fuer einen Regressionstest -, setzt num_workers = 1.")
|
||||||
|
print(f" Das kostet hier den Faktor {faktor:.1f} an Laufzeit und ist genau")
|
||||||
|
print(" deshalb eine Entscheidung fuer den Test, nicht fuer den Betrieb.")
|
||||||
|
print("=" * 78)
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Beispielprogramme zu „Optimierte Entscheidungsfindung mit Python" (Version 04)
|
# Beispielprogramme zu „Optimierte Entscheidungsfindung mit Python" (Version 04)
|
||||||
|
|
||||||
Diese 75 Dateien sind automatisch aus den Kapiteldateien in
|
Diese 76 Dateien sind automatisch aus den Kapiteldateien in
|
||||||
`../Operations_Research_mit_Python_Version_04/` extrahiert — jede Datei entspricht
|
`../Operations_Research_mit_Python_Version_04/` extrahiert — jede Datei entspricht
|
||||||
genau einem im Buch vollständig abgedruckten Programm. Sie sind hier zusätzlich als
|
genau einem im Buch vollständig abgedruckten Programm. Sie sind hier zusätzlich als
|
||||||
eigenständige, direkt ausführbare `.py`-Dateien verfügbar, damit man sie ohne
|
eigenständige, direkt ausführbare `.py`-Dateien verfügbar, damit man sie ohne
|
||||||
|
|
@ -15,7 +15,7 @@ python3 Operations_Research_mit_Python_Version_04/extract_programme_04.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Das Verzeichnis „Verzeichnis der Beispielprogramme" im Vorwort des Buchs
|
Das Verzeichnis „Verzeichnis der Beispielprogramme" im Vorwort des Buchs
|
||||||
(`00_Vorwort_und_Lesehilfe.md`) listet dieselben 75 Programme mit Kurzbeschreibung.
|
(`00_Vorwort_und_Lesehilfe.md`) listet dieselben 76 Programme mit Kurzbeschreibung.
|
||||||
|
|
||||||
Auch dieses README ist selbst generiert: `extract_programme.py` erzeugt es bei jedem
|
Auch dieses README ist selbst generiert: `extract_programme.py` erzeugt es bei jedem
|
||||||
Lauf aus der Vorlage `README_Programme.md` im Quellverzeichnis und setzt die
|
Lauf aus der Vorlage `README_Programme.md` im Quellverzeichnis und setzt die
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,8 @@ def loese(dauer: np.ndarray, n_maschinen: int, zeitlimit: float,
|
||||||
|
|
||||||
loeser = cp_model.CpSolver()
|
loeser = cp_model.CpSolver()
|
||||||
loeser.parameters.max_time_in_seconds = zeitlimit
|
loeser.parameters.max_time_in_seconds = zeitlimit
|
||||||
# Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist.
|
# Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist -
|
||||||
|
# der Seed allein genuegt dafuer NICHT (Kapitel Constraint Programming).
|
||||||
# Im Produktivbetrieb laesst man beides auf den Standardwerten.
|
# Im Produktivbetrieb laesst man beides auf den Standardwerten.
|
||||||
loeser.parameters.num_workers = 1
|
loeser.parameters.num_workers = 1
|
||||||
loeser.parameters.random_seed = 1
|
loeser.parameters.random_seed = 1
|
||||||
|
|
|
||||||
67
PROGRESS.md
67
PROGRESS.md
|
|
@ -77,14 +77,14 @@ sie unerwartet ab, ist etwas kaputtgegangen.
|
||||||
| Kapiteldateien | 22 | **31** |
|
| Kapiteldateien | 22 | **31** |
|
||||||
| Kapitel | 15 | **23** |
|
| Kapitel | 15 | **23** |
|
||||||
| Anhänge | 4 | **5** |
|
| Anhänge | 4 | **5** |
|
||||||
| Zeilen im Gesamtdokument | 11 082 | **27 868** |
|
| Zeilen im Gesamtdokument | 11 082 | **28 185** |
|
||||||
| Größe des Gesamtdokuments | 606 KB | **1 565 KB** |
|
| Größe des Gesamtdokuments | 606 KB | **1 585 KB** |
|
||||||
| Hauptüberschriften | 131 | **297** |
|
| Hauptüberschriften | 131 | **298** |
|
||||||
| registrierte Abschnitte | 122 | **294** |
|
| registrierte Abschnitte | 122 | **295** |
|
||||||
| aufgelöste Querverweise | 314 | **725** (0 unaufgelöst) |
|
| aufgelöste Querverweise | 314 | **730** (0 unaufgelöst) |
|
||||||
| Indexmarken | 295 | **328** |
|
| Indexmarken | 295 | **328** |
|
||||||
| Beispielprogramme | 41 | **75** (alle lauffähig) |
|
| Beispielprogramme | 41 | **76** (alle lauffähig) |
|
||||||
| PDF-Seiten | — | **737** |
|
| PDF-Seiten | — | **744** |
|
||||||
| Notebooks | — | **25** |
|
| Notebooks | — | **25** |
|
||||||
| Plotly-Figuren | — | **4** |
|
| Plotly-Figuren | — | **4** |
|
||||||
| Diagramme (SVG) | 26 | **32**, davon **18** mit Generatorskript (15 Skripte) |
|
| Diagramme (SVG) | 26 | **32**, davon **18** mit Generatorskript (15 Skripte) |
|
||||||
|
|
@ -1565,6 +1565,59 @@ war. Genau der Fall, für den sie gebaut wurde.
|
||||||
Stand danach: 294 Abschnitte, 725 Querverweise, 328 Indexmarken, **75** Programme,
|
Stand danach: 294 Abschnitte, 725 Querverweise, 328 Indexmarken, **75** Programme,
|
||||||
139 Aufgaben mit 139 Lösungen, PDF **737** Seiten.
|
139 Aufgaben mit 139 Lösungen, PDF **737** Seiten.
|
||||||
|
|
||||||
|
### ✅ 6.3 Parallelität und Determinismus ({ref:kap:cpsat})
|
||||||
|
|
||||||
|
Neuer Abschnitt {ref:sec:cpsat-parallele-suche} plus `Parallele_Suche.py` (76. Programm).
|
||||||
|
Setzt Paket 2 aus `Verbesserungen_02.md` um.
|
||||||
|
|
||||||
|
**Was fehlte, war nicht die Regel, sondern die Messung.** Das Buch setzt an acht Stellen
|
||||||
|
`num_workers = 1` mit dem Kommentar „für eine reproduzierbare Ausgabe", und der Spickzettel
|
||||||
|
nennt den Grund. Nachgeprüft hatte es nie jemand. Gerechnet wird auf demselben Job-Shop wie
|
||||||
|
{ref:sec:cpsat-intervallvariablen-job-shop-scheduling}, nur groß genug, dass die Suche
|
||||||
|
arbeitet: 12 Aufträge, 10 Maschinen, 120 Arbeitsgänge.
|
||||||
|
|
||||||
|
**Zwei Befunde, beide stärker als die Behauptung:**
|
||||||
|
|
||||||
|
1. **Die Beschleunigung ist überlinear.** Acht Arbeiter waren im abgedruckten Lauf nicht
|
||||||
|
achtmal, sondern **12,3-mal** schneller als einer. Kein Messfehler: CP-SAT vervielfacht
|
||||||
|
nicht dieselbe Suche, sondern lässt **verschiedene Strategien** nebeneinander laufen, die
|
||||||
|
einander ihre Schranken mitteilen. Der zweite Arbeiter ist nicht „der zweite Kern",
|
||||||
|
sondern ein anderes Verfahren.
|
||||||
|
2. **Der Seed genügt nicht — und zwar schon ab zwei Arbeitern.** Ein Arbeiter: 1 Plan aus
|
||||||
|
4 Läufen. **Zwei** Arbeiter: 3 verschiedene Pläne aus 4 Läufen, bei identischem
|
||||||
|
`random_seed` und identischem Zielwert 183. Es braucht also keine acht Arbeiter, um die
|
||||||
|
Reproduzierbarkeit zu verlieren.
|
||||||
|
|
||||||
|
**Die vollständige Antwort kam erst über die Übungsaufgabe**, und sie ist besser als der
|
||||||
|
Abschnitt allein: Mit `num_workers = 1`, aber **fünf verschiedenen Seeds**, ergeben sich
|
||||||
|
ebenfalls **fünf verschiedene Pläne**. Keiner der beiden Parameter sichert die
|
||||||
|
Reproduzierbarkeit allein:
|
||||||
|
|
||||||
|
| | Seed fest | Seed variabel |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **1 Arbeiter** | reproduzierbar | 5 Pläne aus 5 Läufen |
|
||||||
|
| **mehrere Arbeiter** | 3 Pläne aus 4 Läufen | erst recht nicht |
|
||||||
|
|
||||||
|
Weiter gemessen für die Aufgabe: Der Gewinn kehrt sich um — auf 24 Kernen Faktor **10,2**
|
||||||
|
(8 Arbeiter), **12,3** (16), **9,0** (24). „So viele Arbeiter wie Kerne" ist damit
|
||||||
|
widerlegt. Und bei 15 Aufträgen läuft ein Arbeiter ins 60-s-Limit (`FEASIBLE`, Makespan
|
||||||
|
200), während acht `OPTIMAL` mit **demselben** Makespan 200 nach 26,4 s melden — der
|
||||||
|
Unterschied liegt nicht in der Lösung, sondern im **Beweis**, dass es keine bessere gibt.
|
||||||
|
|
||||||
|
**Zur abgedruckten Ausgabe:** Sie trägt die Kennzeichnung „Laufzeiten *und* die Zahl der
|
||||||
|
verschiedenen Pläne sind hardwareabhängig" — nach dem Muster, das
|
||||||
|
{ref:kap:testing} für `Benchmark_Skalierung.py` schon verwendet. Der Vergleich des
|
||||||
|
extrahierten Programms mit dem Abdruck weicht denn auch in genau einer Zelle ab (4 statt 3
|
||||||
|
verschiedene Pläne bei 4 Arbeitern); Zielwert und Struktur sind identisch. Hier ist die
|
||||||
|
Nichtreproduzierbarkeit der abgedruckten Zahl **die Aussage selbst** — der einzige Fall im
|
||||||
|
Buch, in dem das so ist.
|
||||||
|
|
||||||
|
Im MILP-Kapitel bekam die entsprechende Stelle in `Warmstart_Effekt.py` einen Verweis;
|
||||||
|
dort steht nach Regel 12 der Kapitel**name**, keine Nummer.
|
||||||
|
|
||||||
|
Stand danach: 295 Abschnitte, 730 Querverweise, **76** Programme, 140 Aufgaben mit
|
||||||
|
140 Lösungen, PDF **744** Seiten.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Commit-Historie des V04-Strangs
|
## 8. Commit-Historie des V04-Strangs
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ daraus ab, und alle Befehle unten werden **hier** ausgeführt.
|
||||||
| `Operations_Research_mit_Python_Version_04/` | **Quelle**: 31 Kapiteldateien + Build-Skripte |
|
| `Operations_Research_mit_Python_Version_04/` | **Quelle**: 31 Kapiteldateien + Build-Skripte |
|
||||||
| `bilder_04/` | **Quelle**: Diagramme (SVG/PNG) + `erzeuge_*.py`-Generatoren |
|
| `bilder_04/` | **Quelle**: Diagramme (SVG/PNG) + `erzeuge_*.py`-Generatoren |
|
||||||
| `Operations_Research_mit_Python_Version_04.md` | generiert: Gesamtdokument (Pandoc-Eingabe) |
|
| `Operations_Research_mit_Python_Version_04.md` | generiert: Gesamtdokument (Pandoc-Eingabe) |
|
||||||
| `Operations_Research_mit_Python_Version_04.pdf` | generiert: PDF, 737 Seiten |
|
| `Operations_Research_mit_Python_Version_04.pdf` | generiert: PDF, 744 Seiten |
|
||||||
| `OR_HTML_04/` | generiert: **Mehrseiten-Website** — dieser Ordner wird veröffentlicht |
|
| `OR_HTML_04/` | generiert: **Mehrseiten-Website** — dieser Ordner wird veröffentlicht |
|
||||||
| `Operations_Research_mit_Python_Version_04_Programme/` | generiert: 75 lauffähige Beispielprogramme |
|
| `Operations_Research_mit_Python_Version_04_Programme/` | generiert: 76 lauffähige Beispielprogramme |
|
||||||
| `Notebooks_04/` | generiert: ein Jupyter-Notebook je Kapitel |
|
| `Notebooks_04/` | generiert: ein Jupyter-Notebook je Kapitel |
|
||||||
| `PLAN.md` / `PROGRESS.md` | das Ziel des Umbaus und der erreichte Stand |
|
| `PLAN.md` / `PROGRESS.md` | das Ziel des Umbaus und der erreichte Stand |
|
||||||
| `pyproject.toml` | Abhängigkeiten in Gruppen (`finance`, `large-scale`, `api`, …) |
|
| `pyproject.toml` | Abhängigkeiten in Gruppen (`finance`, `large-scale`, `api`, …) |
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue