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,225 @@
#!/usr/bin/env python3
# Konfliktsuche.py
"""
Anhang Fehlerdiagnose: Den Konflikt finden, der INFEASIBLE verursacht.
Ein Deletion Filter (auch: IIS, Irreducible Infeasible Subset) isoliert aus
einem unloesbaren Modell eine kleinste widerspruechliche Teilmenge von
Bedingungen: Nimmt man aus ihr auch nur eine einzige Bedingung heraus, ist
der Rest wieder loesbar.
Das Programm zeigt sechs Dinge:
1. Der Solver meldet INFEASIBLE - und sonst nichts.
2. Die naive Suche ("jede Bedingung einmal weglassen") findet hier gar nichts.
3. Der Deletion Filter findet einen Konflikt in n Solveraufrufen.
4. Ein Konflikt ist selten einer: nach der Reparatur folgt der naechste.
5. "Den" kleinsten Konflikt gibt es nicht - dieses Modell enthaelt neun.
6. Welchen davon man zu sehen bekommt, steuert die Pruefreihenfolge.
Abgrenzung zu Infeasibility_Diagnose.py (Kapitel Praxisfallen): Dort geht es
um die Relaxation - das Modell soll trotz Widerspruch eine brauchbare Antwort
liefern. Hier geht es um die Diagnose - welche Bedingungen widersprechen sich
ueberhaupt. Beides zusammen ergibt den Umgang mit INFEASIBLE in Produktion.
Benoetigt: numpy, scipy
"""
from __future__ import annotations
import numpy as np
from scipy.optimize import linprog
PRODUKTE = ["Rahmen", "Gehaeuse", "Deckel", "Traeger", "Halter"]
# Jede Bedingung traegt einen sprechenden Namen - das ist keine Kosmetik,
# sondern die Voraussetzung dafuer, dass der Befund lesbar wird.
# (Name, Koeffizienten je Produkt, Richtung, rechte Seite)
BEDINGUNGEN: list[tuple[str, list[float], str, float]] = [
("Kapazitaet Montage", [3, 2, 1, 4, 2], "<=", 400),
("Kapazitaet Lackieren", [2, 3, 0, 1, 0], "<=", 150),
("Kapazitaet Pruefung", [1, 1, 1, 1, 1], "<=", 300),
("Liefervertrag Rahmen", [1, 0, 0, 0, 0], ">=", 40),
("Liefervertrag Gehaeuse", [0, 1, 0, 0, 0], ">=", 30),
("Liefervertrag Deckel", [0, 0, 1, 0, 0], ">=", 20),
("Liefervertrag Traeger", [0, 0, 0, 1, 0], ">=", 25),
("Marktgrenze Rahmen", [1, 0, 0, 0, 0], "<=", 120),
("Marktgrenze Gehaeuse", [0, 1, 0, 0, 0], "<=", 90),
("Marktgrenze Deckel", [0, 0, 1, 0, 0], "<=", 150),
("Marktgrenze Halter", [0, 0, 0, 0, 1], "<=", 200),
("Mindestumsatz", [90, 70, 30, 60, 20], ">=", 12000),
("Sortimentsbreite", [0, 0, 1, 1, 1], ">=", 100),
("Lackierbudget Schicht 2", [0, 1, 0, 1, 0], "<=", 55),
]
# Welche Bedingungen koennte ein Planer im Ernstfall wirklich veraendern?
# Kapazitaeten lassen sich durch Sonderschichten dehnen, Liefervertraege und
# Marktgrenzen nicht.
VERHANDELBAR = {"Kapazitaet Montage", "Kapazitaet Lackieren",
"Kapazitaet Pruefung", "Lackierbudget Schicht 2"}
aufrufe = 0 # zaehlt jeden Solveraufruf mit - der Preis des Verfahrens
def ist_loesbar(auswahl: list[int]) -> bool:
"""Gibt es einen Punkt, der ALLE Bedingungen aus 'auswahl' erfuellt?
Es wird nur Zulaessigkeit geprueft, keine Zielfunktion optimiert: die
Zielfunktion ist konstant null. INFEASIBLE haengt nie an der Zielfunktion.
"""
global aufrufe
aufrufe += 1
matrix, rechte_seite = [], []
for i in auswahl:
_, koeffizienten, richtung, grenze = BEDINGUNGEN[i]
zeile = np.array(koeffizienten, dtype=float)
# linprog kennt nur "<=", also ">=" durch Negation umdrehen
matrix.append(zeile if richtung == "<=" else -zeile)
rechte_seite.append(grenze if richtung == "<=" else -grenze)
ergebnis = linprog(np.zeros(len(PRODUKTE)),
A_ub=np.array(matrix), b_ub=np.array(rechte_seite),
bounds=(0, None), method="highs")
return ergebnis.status == 0
def deletion_filter(auswahl: list[int]) -> list[int]:
"""Verkleinert eine unloesbare Menge zu einer kleinsten unloesbaren Menge.
Der Kern des Verfahrens ist eine einzige Regel: Nimm eine Bedingung
versuchsweise heraus. Bleibt der Rest unloesbar, wurde sie fuer den
Widerspruch nicht gebraucht - sie darf endgueltig weg. Wird der Rest
loesbar, war sie beteiligt und muss bleiben.
Nach genau einem Durchlauf ueber alle Bedingungen ist das Ergebnis
unreduzierbar: n Solveraufrufe statt 2^n Teilmengen.
"""
rest = list(auswahl)
for i in list(auswahl):
probe = [j for j in rest if j != i]
if not ist_loesbar(probe):
rest = probe
return rest
def namen(auswahl: list[int]) -> list[str]:
return [BEDINGUNGEN[i][0] for i in auswahl]
def teil_1_der_befund(alle: list[int]) -> None:
print("=" * 68)
print("1. Was der Solver sagt")
print("=" * 68)
print(f"Modell: {len(PRODUKTE)} Variablen, {len(BEDINGUNGEN)} Bedingungen")
status = "OPTIMAL" if ist_loesbar(alle) else "INFEASIBLE"
print(f"Status: {status}")
print("\nMehr ist es nicht. Der Solver nennt keine Ursache, weil es die eine")
print("Ursache nicht gibt: Unloesbarkeit ist eine Eigenschaft von Mengen von")
print("Bedingungen, nicht von einzelnen Bedingungen.")
def teil_2_die_naive_suche(alle: list[int]) -> None:
print("\n" + "=" * 68)
print("2. Die naheliegende Idee - und warum sie hier scheitert")
print("=" * 68)
print("Jede Bedingung einmal weglassen und schauen, ob es dann geht:\n")
treffer = [i for i in alle if ist_loesbar([j for j in alle if j != i])]
for i in alle:
print(f" ohne {BEDINGUNGEN[i][0]:<26} {'loesbar' if i in treffer else 'weiter unloesbar'}")
print(f"\nGefundene Schuldige: {len(treffer)}")
print("Keine einzelne Bedingung ist schuld. Genau das ist der Normalfall -")
print("und der Grund, warum diese Suche in der Praxis so oft im Nichts endet.")
def teil_3_der_filter(alle: list[int]) -> list[int]:
global aufrufe
print("\n" + "=" * 68)
print("3. Der Deletion Filter")
print("=" * 68)
vorher = aufrufe
konflikt = deletion_filter(alle)
kosten = aufrufe - vorher
print(f"{kosten} Solveraufrufe -> Konflikt aus {len(konflikt)} von "
f"{len(BEDINGUNGEN)} Bedingungen:\n")
for name in namen(konflikt):
print(f" * {name}")
print("\nProbe auf Unreduzierbarkeit - jede einzelne davon weglassen:")
for i in konflikt:
rest_loesbar = ist_loesbar([j for j in konflikt if j != i])
print(f" ohne {BEDINGUNGEN[i][0]:<26} {'loesbar' if rest_loesbar else 'UNLOESBAR - nicht minimal!'}")
print(f"\nAufwand: {kosten} Aufrufe. Alle Teilmengen durchzuprobieren waeren "
f"2^{len(BEDINGUNGEN)} = {2 ** len(BEDINGUNGEN):,} gewesen.".replace(",", "."))
return konflikt
def teil_4_der_naechste_konflikt(alle: list[int], konflikt: list[int]) -> None:
print("\n" + "=" * 68)
print("4. Ein Konflikt ist selten einer")
print("=" * 68)
entfernt = konflikt[0]
rest = [i for i in alle if i != entfernt]
print(f"Angenommen, '{BEDINGUNGEN[entfernt][0]}' laesst sich verhandeln")
print("und wird aus dem Modell genommen. Dann ist das Modell ...")
if ist_loesbar(rest):
print("... loesbar. Fertig.")
return
print("... immer noch unloesbar. Der Filter erneut:\n")
zweiter = deletion_filter(rest)
for name in namen(zweiter):
print(f" * {name}")
gemeinsam = set(zweiter) & set(konflikt)
print(f"\nUeberschneidung mit dem ersten Konflikt: {len(gemeinsam)} Bedingungen")
print("Ein zweiter, unabhaengiger Widerspruch, den der erste verdeckt hat.")
print("Deshalb ist die Konfliktsuche eine Schleife, kein einzelner Aufruf:")
print("reparieren, neu suchen, bis das Modell loesbar ist.")
def teil_5_die_reihenfolge(alle: list[int]) -> None:
print("\n" + "=" * 68)
print("5. Es gibt nicht DEN Konflikt")
print("=" * 68)
print("Derselbe Filter, nur eine andere Pruefreihenfolge - 200-mal gewuerfelt:\n")
zufall = np.random.default_rng(0)
haeufigkeit: dict[tuple[int, ...], int] = {}
for _ in range(200):
ordnung = [int(i) for i in zufall.permutation(alle)]
schluessel = tuple(sorted(deletion_filter(ordnung)))
haeufigkeit[schluessel] = haeufigkeit.get(schluessel, 0) + 1
for schluessel, anzahl in sorted(haeufigkeit.items(), key=lambda p: -p[1]):
print(f" {anzahl:3d}x {len(schluessel)} Bedingungen: "
f"{', '.join(namen(list(schluessel)))}")
print(f"\n{len(haeufigkeit)} verschiedene minimale Konflikte in EINEM Modell.")
print("Der Filter liefert *einen* kleinsten Konflikt, nicht *den* kleinsten -")
print("den gibt es nicht. Alle oben sind gleichermassen korrekt.")
def teil_6_den_befund_steuern(alle: list[int]) -> None:
print("\n" + "=" * 68)
print("6. Den Befund brauchbar machen")
print("=" * 68)
print("Der Filter wirft heraus, was er zuerst in die Hand bekommt. Wer die")
print("unveraenderlichen Bedingungen zuerst pruefen laesst, bekommt sie eher")
print("aus dem Befund heraus - und sieht dafuer mehr Stellschrauben.\n")
fest = [i for i in alle if BEDINGUNGEN[i][0] not in VERHANDELBAR]
frei = [i for i in alle if BEDINGUNGEN[i][0] in VERHANDELBAR]
for titel, ordnung in (("unveraenderliche zuerst", fest + frei),
("veraenderliche zuerst ", frei + fest)):
konflikt = sorted(deletion_filter(ordnung))
stellschrauben = [i for i in konflikt if BEDINGUNGEN[i][0] in VERHANDELBAR]
print(f" {titel} -> {len(konflikt)} Bedingungen, davon "
f"{len(stellschrauben)} veraenderlich")
print(f" {', '.join(namen(konflikt))}")
print("\nBeide Befunde sind wahr. Nur einer davon nennt dem Planer etwas,")
print("das er tatsaechlich tun kann.")
if __name__ == "__main__":
alle = list(range(len(BEDINGUNGEN)))
teil_1_der_befund(alle)
teil_2_die_naive_suche(alle)
konflikt = teil_3_der_filter(alle)
teil_4_der_naechste_konflikt(alle, konflikt)
teil_5_die_reihenfolge(alle)
teil_6_den_befund_steuern(alle)
print("\n" + "=" * 68)
print(f"Insgesamt {aufrufe} Solveraufrufe fuer die gesamte Diagnose.")
print("=" * 68)