85 lines
3.7 KiB
Python
85 lines
3.7 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
|
||
|
|
# Erklaerbarkeit.py
|
||
|
|
"""
|
||
|
|
Kapitel Praxisfallen: Ein Constraint-Trace, der die drei Fragen der Faustregel fuer
|
||
|
|
Erklaerbarkeit (Abschnitt 'Die fuenf typischen Praxisfallen') fuer eine
|
||
|
|
einzelne Zuweisung beantwortet:
|
||
|
|
Warum diese Loesung? Warum nicht die Alternative? Was wuerde sie verbessern?
|
||
|
|
"""
|
||
|
|
|
||
|
|
from ortools.sat.python import cp_model
|
||
|
|
|
||
|
|
PERSONAL = ["Anna", "Ben", "Clara"]
|
||
|
|
BEREITS_STUNDEN = {"Anna": 1, "Ben": 2, "Clara": 0} # diese Woche schon geleistet
|
||
|
|
MAX_STUNDEN = 2 # gesetzliche Obergrenze
|
||
|
|
STRAFE_VORBELASTUNG = 50 # pro bereits geleisteter Stunde
|
||
|
|
STRAFKOSTEN_WUNSCH = {"Anna": 0, "Ben": 30, "Clara": 100}
|
||
|
|
|
||
|
|
|
||
|
|
def loese(max_stunden: int):
|
||
|
|
modell = cp_model.CpModel()
|
||
|
|
x = {p: modell.NewBoolVar(f"x_{p}") for p in PERSONAL}
|
||
|
|
modell.AddExactlyOne(x.values())
|
||
|
|
|
||
|
|
kosten = []
|
||
|
|
for p in PERSONAL:
|
||
|
|
if BEREITS_STUNDEN[p] + 1 > max_stunden:
|
||
|
|
modell.Add(x[p] == 0) # hartes Ausschlusskriterium
|
||
|
|
strafe = BEREITS_STUNDEN[p] * STRAFE_VORBELASTUNG + STRAFKOSTEN_WUNSCH[p]
|
||
|
|
kosten.append(strafe * x[p])
|
||
|
|
|
||
|
|
modell.Minimize(sum(kosten))
|
||
|
|
loeser = cp_model.CpSolver()
|
||
|
|
status = loeser.Solve(modell)
|
||
|
|
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
|
||
|
|
return None
|
||
|
|
gewinner = next(p for p in PERSONAL if loeser.Value(x[p]))
|
||
|
|
return gewinner, loeser.ObjectiveValue()
|
||
|
|
|
||
|
|
|
||
|
|
def gesamtkosten(p: str) -> int:
|
||
|
|
return BEREITS_STUNDEN[p] * STRAFE_VORBELASTUNG + STRAFKOSTEN_WUNSCH[p]
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
gewinner, kosten_opt = loese(MAX_STUNDEN)
|
||
|
|
|
||
|
|
print("=" * 78)
|
||
|
|
print(" CONSTRAINT-TRACE: Warum bekommt", gewinner, "die Vertretungsstunde?")
|
||
|
|
print("=" * 78)
|
||
|
|
|
||
|
|
print("\n1. WARUM DIESE LOESUNG?")
|
||
|
|
v, w = BEREITS_STUNDEN[gewinner] * STRAFE_VORBELASTUNG, STRAFKOSTEN_WUNSCH[gewinner]
|
||
|
|
print(f" {gewinner}: Vorbelastung {v} Strafpunkte + Wunsch {w} Strafpunkte "
|
||
|
|
f"= {v + w} Strafpunkte insgesamt (Minimum).")
|
||
|
|
|
||
|
|
print("\n2. WARUM NICHT DIE NAHELIEGENDEN ALTERNATIVEN?")
|
||
|
|
for p in PERSONAL:
|
||
|
|
if p == gewinner:
|
||
|
|
continue
|
||
|
|
if BEREITS_STUNDEN[p] + 1 > MAX_STUNDEN:
|
||
|
|
print(f" {p}: AUSGESCHLOSSEN - hat bereits {BEREITS_STUNDEN[p]} Stunden, "
|
||
|
|
f"eine dritte würde die Obergrenze von {MAX_STUNDEN} Stunden verletzen "
|
||
|
|
f"(harte Bedingung, nicht verhandelbar).")
|
||
|
|
else:
|
||
|
|
differenz = gesamtkosten(p) - kosten_opt
|
||
|
|
print(f" {p}: zulässig, aber {differenz:.0f} Strafpunkte teurer als "
|
||
|
|
f"{gewinner} ({gesamtkosten(p):.0f} statt {kosten_opt:.0f}).")
|
||
|
|
|
||
|
|
print("\n3. WAS WUERDE DIE LOESUNG VERBESSERN?")
|
||
|
|
gewinner_gelockert, kosten_gelockert = loese(MAX_STUNDEN + 1)
|
||
|
|
if kosten_gelockert < kosten_opt:
|
||
|
|
print(f" Obergrenze auf {MAX_STUNDEN + 1} Stunden gelockert: {gewinner_gelockert} "
|
||
|
|
f"uebernimmt jetzt zu {kosten_gelockert:.0f} Strafpunkten "
|
||
|
|
f"({kosten_opt - kosten_gelockert:.0f} weniger als heute).")
|
||
|
|
print(" -> Diese Bedingung hat tatsaechlich einen Preis.")
|
||
|
|
else:
|
||
|
|
print(f" Obergrenze auf {MAX_STUNDEN + 1} Stunden gelockert: Ergebnis bleibt bei "
|
||
|
|
f"{gewinner} mit {kosten_gelockert:.0f} Strafpunkten (unveraendert).")
|
||
|
|
blockiert = ", ".join(p for p in PERSONAL
|
||
|
|
if BEREITS_STUNDEN[p] + 1 > MAX_STUNDEN and p != gewinner)
|
||
|
|
print(f" -> Die Obergrenze blockiert zwar {blockiert}, kostet aber gerade NICHTS:")
|
||
|
|
print(" Selbst ohne sie waere die Loesung dieselbe.")
|
||
|
|
print(" Blockierend und kostenrelevant sind zwei verschiedene Dinge.")
|
||
|
|
print("=" * 78)
|