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