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>
280 lines
13 KiB
Text
Generated
280 lines
13 KiB
Text
Generated
{
|
|
"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
|
|
}
|