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>
This commit is contained in:
dschlueter 2026-09-08 01:20:09 +02:00
commit b7af2f1d9a
468 changed files with 262141 additions and 0 deletions

View file

@ -0,0 +1,112 @@
#!/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)