Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
|
|
# Warmstart_Effekt.py
|
|
|
|
|
"""
|
|
|
|
|
Kapitel MILP: Wann ein Warm-Start wirklich etwas bringt.
|
|
|
|
|
|
|
|
|
|
Solverstatus_und_Gap.py zeigt einen Fall, in dem ein Startwert NICHTS
|
|
|
|
|
bringt - HiGHS findet von allein schneller etwas Besseres. Hier der
|
|
|
|
|
Gegenfall: eine Heuristik, die dem Solver echtes Domaenenwissen liefert.
|
|
|
|
|
|
|
|
|
|
Problem: Lastverteilung. n Auftraege mit bekannter Dauer sind auf m
|
|
|
|
|
gleichartige Maschinen zu verteilen, sodass die zuletzt fertige Maschine
|
|
|
|
|
so frueh wie moeglich fertig wird (Makespan-Minimierung).
|
|
|
|
|
|
|
|
|
|
Die Heuristik: LPT (Longest Processing Time first) - laengste Auftraege
|
|
|
|
|
zuerst, jeder auf die momentan am wenigsten belastete Maschine. Sie ist
|
|
|
|
|
Jahrzehnte alt, in zwei Zeilen geschrieben und beweisbar nie schlechter
|
|
|
|
|
als 4/3 des Optimums.
|
|
|
|
|
|
|
|
|
|
Ueber model.AddHint() bekommt CP-SAT diese Loesung als Startpunkt.
|
|
|
|
|
|
|
|
|
|
WICHTIG: highspy wird hier bewusst NICHT importiert - es vertraegt sich
|
|
|
|
|
nicht mit ortools im selben Prozess (siehe Kapitel Oekosystem).
|
|
|
|
|
|
|
|
|
|
Benoetigt: numpy, ortools
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
from ortools.sat.python import cp_model
|
|
|
|
|
|
|
|
|
|
RNG = np.random.default_rng(4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def lpt_heuristik(dauer: np.ndarray, n_maschinen: int) -> tuple[np.ndarray, int]:
|
|
|
|
|
"""Longest Processing Time first.
|
|
|
|
|
|
|
|
|
|
Laengste Auftraege zuerst auf die jeweils freieste Maschine legen. Zwei
|
|
|
|
|
Zeilen, keine Bibliothek, Ergebnis in Mikrosekunden - und erstaunlich
|
|
|
|
|
nah am Optimum.
|
|
|
|
|
"""
|
|
|
|
|
zuordnung = np.zeros(len(dauer), dtype=int)
|
|
|
|
|
belegung = np.zeros(n_maschinen)
|
|
|
|
|
for auftrag in np.argsort(-dauer): # laengster zuerst
|
|
|
|
|
maschine = int(np.argmin(belegung)) # freieste Maschine
|
|
|
|
|
zuordnung[auftrag] = maschine
|
|
|
|
|
belegung[maschine] += dauer[auftrag]
|
|
|
|
|
return zuordnung, int(belegung.max())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def loese(dauer: np.ndarray, n_maschinen: int, zeitlimit: float,
|
|
|
|
|
hinweis: np.ndarray | None = None) -> tuple[str, int, float]:
|
|
|
|
|
"""Exaktes Modell mit CP-SAT, optional mit Startloesung als Hinweis."""
|
|
|
|
|
n_auftraege = len(dauer)
|
|
|
|
|
obergrenze = int(dauer.sum())
|
|
|
|
|
|
|
|
|
|
modell = cp_model.CpModel()
|
|
|
|
|
# x[i][k] = 1 <=> Auftrag i laeuft auf Maschine k
|
|
|
|
|
x = [[modell.NewBoolVar(f"x_{i}_{k}") for k in range(n_maschinen)]
|
|
|
|
|
for i in range(n_auftraege)]
|
|
|
|
|
for i in range(n_auftraege):
|
|
|
|
|
modell.AddExactlyOne(x[i]) # jeder Auftrag genau einmal
|
|
|
|
|
|
|
|
|
|
belegung = [modell.NewIntVar(0, obergrenze, f"last_{k}")
|
|
|
|
|
for k in range(n_maschinen)]
|
|
|
|
|
for k in range(n_maschinen):
|
|
|
|
|
modell.Add(belegung[k] == sum(int(dauer[i]) * x[i][k]
|
|
|
|
|
for i in range(n_auftraege)))
|
|
|
|
|
|
|
|
|
|
makespan = modell.NewIntVar(0, obergrenze, "makespan")
|
|
|
|
|
modell.AddMaxEquality(makespan, belegung) # das Maximum ueber alle Maschinen
|
|
|
|
|
modell.Minimize(makespan)
|
|
|
|
|
|
|
|
|
|
# Der Warm-Start: ein Hinweis pro Variable. CP-SAT muss ihn nicht
|
|
|
|
|
# befolgen - er nutzt ihn als erste Loesung, wenn er zulaessig ist.
|
|
|
|
|
if hinweis is not None:
|
|
|
|
|
for i in range(n_auftraege):
|
|
|
|
|
for k in range(n_maschinen):
|
|
|
|
|
modell.AddHint(x[i][k], 1 if hinweis[i] == k else 0)
|
|
|
|
|
|
|
|
|
|
loeser = cp_model.CpSolver()
|
|
|
|
|
loeser.parameters.max_time_in_seconds = zeitlimit
|
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>
2026-09-08 11:19:53 +02:00
|
|
|
# Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist -
|
|
|
|
|
# der Seed allein genuegt dafuer NICHT (Kapitel Constraint Programming).
|
Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
|
|
|
# Im Produktivbetrieb laesst man beides auf den Standardwerten.
|
|
|
|
|
loeser.parameters.num_workers = 1
|
|
|
|
|
loeser.parameters.random_seed = 1
|
|
|
|
|
|
|
|
|
|
t0 = time.perf_counter()
|
|
|
|
|
status = loeser.Solve(modell)
|
|
|
|
|
dauer_s = time.perf_counter() - t0
|
|
|
|
|
|
|
|
|
|
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
|
|
|
|
|
raise RuntimeError(f"Kein Plan gefunden: {loeser.StatusName(status)}")
|
|
|
|
|
return loeser.StatusName(status), int(loeser.ObjectiveValue()), dauer_s
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
print("=" * 78)
|
|
|
|
|
print(" WARM-START: WENN DIE HEURISTIK MEHR WEISS ALS DER SOLVER")
|
|
|
|
|
print("=" * 78)
|
|
|
|
|
print("Lastverteilung: Auftraege auf gleichartige Maschinen verteilen,")
|
|
|
|
|
print("sodass die letzte Maschine so frueh wie moeglich fertig wird.\n")
|
|
|
|
|
|
|
|
|
|
print(f"{'Instanz':<22} {'Variante':<22} {'Makespan':>9} {'Zeit':>9} "
|
|
|
|
|
f"{'Faktor':>8}")
|
|
|
|
|
print("-" * 78)
|
|
|
|
|
|
|
|
|
|
for n_auftraege, n_maschinen in [(60, 7), (80, 9)]:
|
|
|
|
|
dauer = RNG.integers(10, 90, n_auftraege)
|
|
|
|
|
start, lpt_wert = lpt_heuristik(dauer, n_maschinen)
|
|
|
|
|
untere_schranke = dauer.sum() / n_maschinen
|
|
|
|
|
|
|
|
|
|
instanz = f"{n_auftraege} Auftr., {n_maschinen} Masch."
|
|
|
|
|
print(f"{instanz:<22} {'LPT-Heuristik':<22} {lpt_wert:>9} "
|
|
|
|
|
f"{'< 0.001s':>9} {'':>8}")
|
|
|
|
|
|
|
|
|
|
_, ziel_kalt, zeit_kalt = loese(dauer, n_maschinen, 60.0)
|
|
|
|
|
print(f"{'':<22} {'CP-SAT kalt':<22} {ziel_kalt:>9} "
|
|
|
|
|
f"{zeit_kalt:>8.3f}s {'1,0x':>8}")
|
|
|
|
|
|
|
|
|
|
_, ziel_warm, zeit_warm = loese(dauer, n_maschinen, 60.0, hinweis=start)
|
|
|
|
|
print(f"{'':<22} {'CP-SAT + LPT-Hinweis':<22} {ziel_warm:>9} "
|
|
|
|
|
f"{zeit_warm:>8.3f}s {zeit_kalt / zeit_warm:>7.1f}x")
|
|
|
|
|
|
|
|
|
|
assert ziel_kalt == ziel_warm, \
|
|
|
|
|
"Der Hinweis darf das Optimum nicht veraendern - nur den Weg dorthin!"
|
|
|
|
|
print(f"{'':<22} {'untere Schranke':<22} {untere_schranke:>9.1f}")
|
|
|
|
|
print("-" * 78)
|
|
|
|
|
|
|
|
|
|
print("\nDrei Beobachtungen:")
|
|
|
|
|
print("1. Der Hinweis aendert das ERGEBNIS nicht - beide Laeufe finden")
|
|
|
|
|
print(" dasselbe Optimum. Er aendert nur, wie lange der Beweis dauert.")
|
|
|
|
|
print(" Genau deshalb ist ein Warm-Start ungefaehrlich: Ein schlechter")
|
|
|
|
|
print(" Hinweis kostet Zeit, er verfaelscht aber nie die Loesung.")
|
|
|
|
|
print("2. Die LPT-Heuristik liegt schon sehr nah am Optimum. Ihr Wert fuer")
|
|
|
|
|
print(" den Solver liegt weniger in der Qualitaet als darin, dass sie")
|
|
|
|
|
print(" SOFORT da ist - der Solver kann von Beginn an alles verwerfen,")
|
|
|
|
|
print(" was schlechter ist.")
|
|
|
|
|
print("3. Der Faktor schwankt von Instanz zu Instanz - oben 2,2x und 1,4x -")
|
|
|
|
|
print(" und laesst sich NICHT aus der Problemgroesse ableiten. Er haengt")
|
|
|
|
|
print(" davon ab, wie schnell der Solver von allein eine vergleichbar gute")
|
|
|
|
|
print(" Loesung findet. Das ist die eigentliche Lehre: Ein Warm-Start ist")
|
|
|
|
|
print(" eine Messung wert, keine Glaubensfrage.")
|
|
|
|
|
print("=" * 78)
|