operations_research/OR_HTML_04/programme/Infeasibility_Diagnose.py

112 lines
4.3 KiB
Python
Raw Normal View History

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
# 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)