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
|
|
@ -341,6 +341,7 @@ Alle Beispielprogramme dieses Buchs im Überblick, sortiert nach Kapitel — pra
|
|||
| `Propagation_Demo.py` | Propagation messbar | [Kapitel 7](#kap-cpsat) |
|
||||
| `CP_SAT_Vertretungssystem.py` | Vertretungsplan | [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) |
|
||||
| `Strafgewichte.py` | Gewichte als Wechselkurse | [Kapitel 7](#kap-cpsat) |
|
||||
| `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.
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -6909,7 +6910,8 @@ def loese(dauer: np.ndarray, n_maschinen: int, zeitlimit: float,
|
|||
|
||||
loeser = cp_model.CpSolver()
|
||||
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.
|
||||
loeser.parameters.num_workers = 1
|
||||
loeser.parameters.random_seed = 1
|
||||
|
|
@ -7366,6 +7368,7 @@ funktioniert immer.
|
|||
> `Propagation_Demo.py`\
|
||||
> `CP_SAT_Vertretungssystem.py`\
|
||||
> `JobShop_Intervalle.py`\
|
||||
> `Parallele_Suche.py`\
|
||||
> `CP_SAT_Statusfaelle.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.
|
||||
|
||||
> **🎯 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.
|
||||
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).
|
||||
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.
|
||||
>
|
||||
> 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.
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -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).
|
||||
|
||||
|
|
@ -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.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**
|
||||
>
|
||||
|
|
@ -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**
|
||||
>
|
||||
|
|
@ -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).
|
||||
|
||||
|
|
@ -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?
|
||||
3. Was ist eine Intervallvariable, und welche Bedingung garantiert sie automatisch?
|
||||
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.
|
||||
* **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.
|
||||
* **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. |
|
||||
> | $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. |
|
||||
>
|
||||
> 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. |
|
||||
> | `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. |
|
||||
> | `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. |
|
||||
|
|
@ -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.
|
||||
|
||||
**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
|
||||
|
||||
|
|
@ -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`.
|
||||
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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue