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>
112 lines
4.3 KiB
Python
Generated
112 lines
4.3 KiB
Python
Generated
#!/usr/bin/env python3
|
|
|
|
# Infeasibility_Diagnose.py
|
|
"""
|
|
Kapitel Praxisfallen: Aus INFEASIBLE einen Notfallplan machen.
|
|
|
|
Demonstriert die hierarchische Relaxation an einem Dienstplan, der in der
|
|
harten Fassung unloesbar ist, und zeigt, wie der Solver die Ursache benennt.
|
|
"""
|
|
|
|
from ortools.sat.python import cp_model
|
|
|
|
SLOTS = ["Mo frueh", "Mo spaet", "Di frueh", "Di spaet"]
|
|
FAECHER = ["Mathematik", "Physik", "Mathematik", "Chemie"] # Chemie kann niemand!
|
|
PERSONAL = ["Alice", "Bob", "Carla"]
|
|
QUALIFIKATION = {
|
|
"Alice": {"Mathematik", "Physik"},
|
|
"Bob": {"Mathematik"},
|
|
"Carla": {"Physik", "Mathematik"},
|
|
}
|
|
MAX_PRO_PERSON = 2
|
|
STRAFE_UNBESETZT = 10_000 # sehr teuer, aber nicht unmoeglich
|
|
STRAFE_UEBERLAST = 500 # teuer, aber billiger als Ausfall
|
|
|
|
|
|
def plane(harte_fassung: bool):
|
|
"""harte_fassung=True: klassisch (kann INFEASIBLE werden).
|
|
harte_fassung=False: mit Schlupfvariablen (immer loesbar)."""
|
|
modell = cp_model.CpModel()
|
|
x = {(p, s): modell.NewBoolVar(f"x_{p}_{s}")
|
|
for p in PERSONAL for s in range(len(SLOTS))}
|
|
|
|
strafen = []
|
|
unbesetzt = {}
|
|
ueberlast = {}
|
|
|
|
for s in range(len(SLOTS)):
|
|
if harte_fassung:
|
|
modell.AddExactlyOne(x[p, s] for p in PERSONAL)
|
|
else:
|
|
# Schlupfvariable: der Slot DARF unbesetzt bleiben - gegen hohe Strafe
|
|
unbesetzt[s] = modell.NewBoolVar(f"unbesetzt_{s}")
|
|
modell.AddExactlyOne([x[p, s] for p in PERSONAL] + [unbesetzt[s]])
|
|
strafen.append(unbesetzt[s] * STRAFE_UNBESETZT)
|
|
|
|
# Qualifikation bleibt IMMER hart - fachfremder Unterricht ist keine Option
|
|
for s, fach in enumerate(FAECHER):
|
|
for p in PERSONAL:
|
|
if fach not in QUALIFIKATION[p]:
|
|
modell.Add(x[p, s] == 0)
|
|
|
|
for p in PERSONAL:
|
|
last = sum(x[p, s] for s in range(len(SLOTS)))
|
|
if harte_fassung:
|
|
modell.Add(last <= MAX_PRO_PERSON)
|
|
else:
|
|
# Ueberlast erlaubt - aber teuer
|
|
ueberlast[p] = modell.NewIntVar(0, len(SLOTS), f"ueberlast_{p}")
|
|
modell.Add(last <= MAX_PRO_PERSON + ueberlast[p])
|
|
strafen.append(ueberlast[p] * STRAFE_UEBERLAST)
|
|
|
|
if strafen:
|
|
modell.Minimize(sum(strafen))
|
|
|
|
loeser = cp_model.CpSolver()
|
|
loeser.parameters.max_time_in_seconds = 5.0
|
|
status = loeser.Solve(modell)
|
|
return loeser, status, x, unbesetzt, ueberlast
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("=" * 78)
|
|
print(" VARIANTE A: KLASSISCH MIT LAUTER HARTEN BEDINGUNGEN")
|
|
print("=" * 78)
|
|
loeser, status, *_ = plane(harte_fassung=True)
|
|
print(f"Solver-Status: {loeser.StatusName(status)}")
|
|
if status == cp_model.INFEASIBLE:
|
|
print("Das System kann keinen Plan liefern. Der Anwender erfaehrt NICHT,")
|
|
print("welche Regel das Problem verursacht - nur, dass es nicht geht.\n")
|
|
|
|
print("=" * 78)
|
|
print(" VARIANTE B: MIT HIERARCHISCHER RELAXATION")
|
|
print("=" * 78)
|
|
loeser, status, x, unbesetzt, ueberlast = plane(harte_fassung=False)
|
|
print(f"Solver-Status: {loeser.StatusName(status)} | "
|
|
f"Strafkosten: {loeser.ObjectiveValue():.0f}\n")
|
|
|
|
print(f"{'Slot':<12} {'Fach':<12} {'Zuweisung':<18} {'Bemerkung'}")
|
|
print("-" * 78)
|
|
for s, slot in enumerate(SLOTS):
|
|
if loeser.Value(unbesetzt[s]):
|
|
print(f"{slot:<12} {FAECHER[s]:<12} {'-- UNBESETZT --':<18} "
|
|
f"Kein qualifiziertes Personal verfuegbar")
|
|
else:
|
|
person = next(p for p in PERSONAL if loeser.Value(x[p, s]))
|
|
print(f"{slot:<12} {FAECHER[s]:<12} {person:<18}")
|
|
|
|
print("\n--- Diagnose ---")
|
|
for s, slot in enumerate(SLOTS):
|
|
if loeser.Value(unbesetzt[s]):
|
|
fach = FAECHER[s]
|
|
qualifiziert = [p for p in PERSONAL if fach in QUALIFIKATION[p]]
|
|
print(f" '{slot}' ({fach}): {len(qualifiziert)} qualifizierte Personen "
|
|
f"{qualifiziert if qualifiziert else '-> URSACHE: niemand kann dieses Fach'}")
|
|
for p in PERSONAL:
|
|
if loeser.Value(ueberlast[p]):
|
|
print(f" {p} arbeitet {loeser.Value(ueberlast[p])} Stunden ueber der Grenze.")
|
|
|
|
print("\nDer Anwender bekommt jetzt einen Plan PLUS eine konkrete Ursache -")
|
|
print("und kann handeln: Vertretung von aussen holen, Stunde verlegen,")
|
|
print("oder die Klasse zusammenlegen.")
|
|
print("=" * 78)
|