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