112 lines
4.3 KiB
Python
112 lines
4.3 KiB
Python
|
|
#!/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)
|