operations_research/OR_HTML_04/lp.html

1612 lines
189 KiB
HTML
Raw Normal View History

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>
2026-09-08 01:20:09 +02:00
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise · Optimierte Entscheidungsfindung mit Python</title>
<script>
(function () {
try {
var t = localStorage.getItem("or-theme");
if (t) document.documentElement.setAttribute("data-theme", t);
} catch (e) {}
})();
</script>
<link rel="stylesheet" href="assets/highlight.css" />
<link rel="stylesheet" href="katex/katex.min.css" />
<script defer="" src="katex/katex.min.js"></script>
<script>document.addEventListener("DOMContentLoaded", function () {
var mathElements = document.getElementsByClassName("math");
var macros = [];
for (var i = 0; i < mathElements.length; i++) {
var texText = mathElements[i].firstChild;
if (mathElements[i].tagName == "SPAN") {
katex.render(texText.data, mathElements[i], {
displayMode: mathElements[i].classList.contains('display'),
throwOnError: false,
macros: macros,
fleqn: false
});
}}
// Der Browser springt zu einem #anker in der URL schon beim ersten Rendern
// an, BEVOR die KaTeX-Formeln oben im Text ihre finale Hoehe bekommen -
// durch den Reflow landet der Anker danach zu weit unten. Nach dem
// Formel-Rendering hier erneut zum Anker springen, das behebt es.
if (location.hash) {
var ziel = document.getElementById(decodeURIComponent(location.hash.slice(1)));
if (ziel) ziel.scrollIntoView({behavior: "instant", block: "start"});
}
});
</script>
<link rel="stylesheet" href="assets/site.css" />
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="icon-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
</symbol>
<symbol id="icon-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.2" y2="16.2"/>
</symbol>
<symbol id="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4.5"/>
<line x1="12" y1="1.5" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="22.5"/>
<line x1="1.5" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="22.5" y2="12"/>
<line x1="4.5" y1="4.5" x2="6.2" y2="6.2"/><line x1="17.8" y1="17.8" x2="19.5" y2="19.5"/>
<line x1="19.5" y1="4.5" x2="17.8" y2="6.2"/><line x1="6.2" y1="17.8" x2="4.5" y2="19.5"/>
</symbol>
<symbol id="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a6.8 6.8 0 0 0 10.5 10.5z"/>
</symbol>
<symbol id="icon-chevron-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 4 7 12 15 20"/>
</symbol>
<symbol id="icon-chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 4 17 12 9 20"/>
</symbol>
<symbol id="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 13 9.5 18.5 20 6"/>
</symbol>
<symbol id="icon-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13.5V19a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 19V7A1.5 1.5 0 0 1 5 5.5h5.5"/>
<polyline points="14.5 3.5 20.5 3.5 20.5 9.5"/><line x1="11" y1="13" x2="20" y2="4"/>
</symbol>
<symbol id="icon-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 5.5A2 2 0 0 1 6 4h6v16H6a2 2 0 0 0-2 1.5z"/>
<path d="M20 5.5A2 2 0 0 0 18 4h-6v16h6a2 2 0 0 1 2 1.5z"/>
</symbol>
<symbol id="icon-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</symbol>
<symbol id="icon-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 3v12"/><polyline points="7 10 12 15 17 10"/><path d="M4 19.5h16"/>
</symbol></svg>
<header class="site-header">
<button type="button" class="icon-btn" id="sidebar-toggle" aria-label="Menü öffnen"><svg class="icon" aria-hidden="true"><use href="#icon-menu"></use></svg></button>
<a class="brand" href="index.html"><svg class="icon" aria-hidden="true"><use href="#icon-book"></use></svg> <span>Optimierte Entscheidungsfindung mit Python</span></a>
<div class="site-search">
<input id="suche-eingabe" type="search" placeholder="Suchen …" aria-label="Suche" autocomplete="off" />
<svg class="icon such-icon" aria-hidden="true"><use href="#icon-search"></use></svg>
<div id="suche-ergebnisse" class="suche-ergebnisse" hidden></div>
</div>
<button type="button" class="icon-btn" id="theme-toggle" aria-label="Darstellung umschalten">
<svg class="icon icon-sun" aria-hidden="true"><use href="#icon-sun"></use></svg><svg class="icon icon-moon" aria-hidden="true"><use href="#icon-moon"></use></svg>
</button>
</header>
<div class="site-body">
<div class="sidebar-overlay" id="sidebar-overlay" hidden></div>
<nav class="sidebar" id="sidebar" aria-label="Kapitelnavigation"><div class="sidebar-inhalt"><details class="sidebar-gruppe"><summary>Einstieg</summary><ul><li data-kapitel="vorwort.html"><a href="vorwort.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Vorwort & Lesehilfe</span></a></li><li data-kapitel="notation.html"><a href="notation.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Notation & Abkürzungen</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Teil I: Grundlagen des Operations Research</summary><ul><li data-kapitel="einfuehrung.html"><a href="einfuehrung.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></a></li><li data-kapitel="fundament.html"><a href="fundament.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität{idx:Konvexität}</span></a></li><li data-kapitel="oekosystem.html"><a href="oekosystem.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 3: Das Python-Ökosystem für OR — Solver, Bindings und Modellierungsschichten</span></a></li><li data-kapitel="modellierung.html"><a href="modellierung.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 4: Vom Management-Wunsch zum Modell</span></a></li><li data-kapitel="synthese-grundlagen.html"><a href="synthese-grundlagen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Synthese Teil I</span></a></li></ul></details><details class="sidebar-gruppe" open><summary>Teil II: Die Kernverfahren der deterministischen Optimierung</summary><ul><li data-kapitel="lp.html" class="aktiv"><a href="lp.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></a></li><li data-kapitel="milp.html"><a href="milp.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound{idx:Branch-and-Bound}</span></a></li><li data-kapitel="cpsat.html"><a href="cpsat.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 7: Constraint Programming mit CP-SAT{idx:CP-SAT} — Logik, Scheduling und Zuweisung</span></a></li><li data-kapitel="graphen.html"><a href="graphen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</span></a></li><li data-kapitel="metaheuristiken.html"><a href="metaheuristiken.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 9: Metaheuristiken — wenn der exakte Solver aussteigt</span></a></li><li data-kapitel="dekomposition.html"><a href="dekomposition.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 10: Spaltengenerierung — das Modell umbauen statt die Lösung raten</span></a></li><li data-kapitel="synthese-kernverfahren.html"><a href="synthese-kernverfahren.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Synthese Teil II</span></a></li></ul></details><details class="sidebar-gruppe"
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>
2026-09-08 01:20:09 +02:00
<main class="content">
<nav class="breadcrumb" aria-label="Breadcrumb"><a href="index.html">Start</a> <span>Teil II</span> <span>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></nav>
<nav class="prev-next"><a class="prev-next-knopf prev-next-prev" href="synthese-grundlagen.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Synthese Teil I</span></a><a class="prev-next-knopf prev-next-next" href="milp.html"><span><small>Weiter</small>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound{idx:Branch-and-Bound}</span><svg class="icon" aria-hidden="true"><use href="#icon-chevron-right"></use></svg></a></nav>
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>
2026-09-08 01:20:09 +02:00
<article>
<h1 id="teil-kernverfahren">Teil II: Die Kernverfahren der deterministischen Optimierung</h1>
<p>Die nächsten vier Kapitel behandeln Probleme, bei denen <strong>alle Daten bekannt</strong> sind: Kosten, Kapazitäten, Zeiten stehen fest. Das klingt nach einer starken Vereinfachung, deckt aber den größten Teil der betrieblichen Planung ab — und ist die Grundlage für alles Weitere.</p>
<p>Vier Kapitel, vier Werkzeugkästen. Welcher zu Ihrem Problem passt, entscheiden Sie mit vier Fragen:</p>
<figure>
<img src="bilder_04/teil2_solverwahl.svg" alt="Entscheidungsdiagramm: Welches Werkzeug passt zu meinem Problem? (Teil II)" />
<figcaption aria-hidden="true">Entscheidungsdiagramm: Welches Werkzeug passt zu meinem Problem? (Teil II)</figcaption>
</figure>
<blockquote>
<p><strong>🎯 Merksatz zum Diagramm</strong> Die erste Frage ist die wichtigste — und sie lautet <strong>nicht</strong> „welcher <a href="anhang-glossar.md#gloss:solver" class="glossar-link">Solver</a> ist der schnellste?“, sondern „welche <strong>Struktur</strong> hat mein Problem?“. Ein <a href="anhang-glossar.md#gloss:zuordnungsproblem" class="glossar-link">Zuordnungsproblem</a> braucht keinen <a href="anhang-glossar.md#gloss:gemischt-ganzzahlige-optimierung-milp" class="glossar-link">MILP</a>-Solver, sondern eine Zeile <code>linear_sum_assignment</code>. Ein Dienstplan gehört nicht in ein <a href="anhang-glossar.md#gloss:lineare-programmierung-lp" class="glossar-link">LP</a>, sondern zu <a href="anhang-glossar.md#gloss:cp-sat" class="glossar-link">CP-SAT</a>. Wer die Struktur erkennt, hat die halbe Laufzeit gespart, bevor die erste Zeile Code geschrieben ist.</p>
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>
2026-09-08 01:20:09 +02:00
</blockquote>
<p>Fällt Ihr Problem durch das Diagramm hindurch — nichtlineare <a href="anhang-glossar.md#gloss:zielfunktion" class="glossar-link">Zielfunktion</a>, unsichere Daten, Entscheidungen über mehrere Zeitstufen —, dann ist <a href="qp-nlp.html#teil-nichtlinear">Teil III</a> der richtige Ort. Dort steht ein zweites Diagramm für genau diese Fälle.</p>
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>
2026-09-08 01:20:09 +02:00
<hr />
<h1 id="kap-lp">Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</h1>
<div class="card card-blick">
<blockquote>
<p><strong>📌 Kapitel auf einen Blick</strong></p>
<p><strong>Worum geht es?</strong> Um das Kernverfahren des Operations Research. Wir bauen den <a href="anhang-glossar.md#gloss:simplex-algorithmus" class="glossar-link">Simplex-Algorithmus</a> selbst — nicht um ihn zu verwenden, sondern um zu verstehen, woher <strong>Schattenpreise</strong> kommen. Sie sind das wertvollste Nebenprodukt jeder Optimierung.</p>
<p><strong>Voraussetzungen:</strong> <a href="fundament.html#kap-fundament">Kapitel 2</a> (Matrixform, <a href="anhang-glossar.md#gloss:polyeder" class="glossar-link">Polyeder</a>, Ecken).</p>
<p><strong>Danach können Sie:</strong> Ein LP in Standardform bringen, den Simplex von Hand rechnen, Dualwerte interpretieren, daraus wirtschaftliche Handlungsempfehlungen ableiten — und erkennen, wann ein <a href="anhang-glossar.md#gloss:schattenpreis" class="glossar-link">Schattenpreis</a> eine Entscheidung <strong>nicht</strong> tragen darf.</p>
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>
2026-09-08 01:20:09 +02:00
<p><strong>Zeitbedarf:</strong> ca. 7 Stunden — das umfangreichste Kapitel von <a href="#teil-kernverfahren">Teil II</a>.</p>
<p><strong>Programme:</strong><br />
<code>Simplex_Tableau_LP.py</code><br />
<code>Sensitivitaetsanalyse.py</code><br />
<code>Dualitaet_Nachweis.py</code><br />
<code>Toleranzen_und_Entartung.py</code></p>
Veroeffentlichung: Lizenzen, README, Colab-Entscheidung, JupyterLab im Image Sechs Dinge, alle fuer das jetzt oeffentliche Repository. LIZENZEN. LICENSE traegt die MIT-Lizenz fuer den Programmcode (alle .py, Notebooks, Dockerfile, pyproject.toml), LICENSE-TEXT.md die CC BY-SA 4.0 fuer Buchtext, PDF, Website und Diagramme. Beide Dateien nennen ausdruecklich, was sie abdecken und was nicht - ein Codeblock im Buchtext bleibt Programmcode und steht unter MIT. Vorher hatte das Repository gar keine Lizenz, womit standardmaessig "alle Rechte vorbehalten" galt und niemand die 76 Programme haette weiterverwenden duerfen. COLAB. COLAB_BASIS_URL steht jetzt auf "". Der Platzhalter zeigte auf GitHub, das Repository liegt auf einer eigenen Forgejo-Instanz - und Colab oeffnet Notebooks NUR aus GitHub, Google Drive oder einem Upload. Die URL-Form colab.research.google.com/github/... ist fest auf GitHub verdrahtet; eine selbstgehostete Adresse dort einzusetzen ergaebe keinen Link zum eigenen Server, sondern einen toten GitHub-Link. Der Kommentar im Quelltext ging von GitHub aus und war damit selbst irrefuehrend; er ist ersetzt. Verloren geht nichts: Die 25 Notebooks liegen neben der Website und bekommen einen echten Download-Link, jetzt mit dem Hinweis, was man damit tut - "herunterladen und in Jupyter oeffnen, in Colab hochladen oder mit dem Kurs-Image starten". JUPYTERLAB IM KURS-IMAGE. Neue pyproject-Gruppe [notebook] mit jupyterlab, die Notebooks werden ins Image kopiert, und ein kleiner Startbefehl macht beide Betriebsarten ohne --entrypoint moeglich: ohne Argument JupyterLab, mit Argument ein einzelnes Programm. Gebaut und geprueft - Rucksack.py laeuft, JupyterLab antwortet mit HTTP 200 und zeigt alle 25 Notebooks. Image 1,31 -> 1,46 GB. Es laeuft ohne Token, deshalb im README die Portfreigabe an 127.0.0.1 gebunden. README KOMPLETT NEU. Es war die Bau-Anleitung eines privaten Verzeichnisses und ist jetzt die Visitenkarte eines oeffentlichen Repositorys: was das Buch ist, wo man es liest, drei Wege die Beispiele auszufuehren (Container, schlanke Installation, alles auf einmal), was hier liegt, wie man baut, die Colab-Frage, die Lizenzen und wie man mitwirkt. Alle relativen Links geprueft: 0 tot. .gitattributes. Ohne die Datei entschied core.autocrlf des jeweiligen Rechners, was beim Klonen passiert - ein Windows-Leser bekam CRLF-Rauschen in jedem Diff. Jetzt: im Repository immer LF, im Arbeitsverzeichnis passend zum System, Binaerdateien ausdruecklich ausgenommen. Der Bestand war bereits sauber (git add --renormalize aendert null Dateien). Zusaetzlich sind die erzeugten Verzeichnisse als linguist-generated markiert, sonst zaehlt die Sprachstatistik das Repository als HTML-Projekt. PROGRESS.md: Remote-Repository als erledigt markiert, der Colab-Befund festgehalten. CLAUDE.md um Veroeffentlichung, Lizenzen und die neuen Dateien ergaenzt. Geprueft: --check ohne Fehler, 0 tote README-Links, 76 Programme unveraendert, 33 pytest-Tests, pyproject baut mit acht Gruppen, PDF 759 Seiten (eine weniger - die Colab-Zeile entfaellt in 25 Kapiteln). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:54:04 +02:00
<p><strong>Notebook:</strong> <a href="Notebooks_04/lp.ipynb">lp.ipynb</a> — herunterladen und in Jupyter öffnen, in Colab hochladen oder mit dem Kurs-Image starten</p>
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>
2026-09-08 01:20:09 +02:00
</blockquote>
</div>
<hr />
<h2 id="sec:lp-schnellstart">5.1 In 5 Minuten gelöst</h2>
<p>Bisher hat ein Solver Ihnen gesagt, <em>was</em> Sie tun sollen. Jetzt sagt er Ihnen zusätzlich, <em>was eine zusätzliche Stunde wert wäre</em> — und das ist die Zahl, für die Sie im Betrieb gehört werden.</p>
<div class="card card-schnellstart">
<blockquote>
<p><strong>🚀 In 5 Minuten gelöst: Wo lohnen sich Überstunden?</strong></p>
<p>Ein Zulieferer fertigt Karosserieteile und Rahmen. Drei Bereiche begrenzen ihn: der Lackierofen (100 h), die Montage (160 h) und die Qualitätsprüfung (120 h).</p>
<table>
<thead>
<tr class="header">
<th>Produkt</th>
<th style="text-align: right;">Deckungsbeitrag</th>
<th style="text-align: right;">Lackierofen</th>
<th style="text-align: right;">Montage</th>
<th style="text-align: right;">Prüfung</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Karosserieteil</td>
<td style="text-align: right;">90 €</td>
<td style="text-align: right;">1 h</td>
<td style="text-align: right;">2 h</td>
<td style="text-align: right;">1 h</td>
</tr>
<tr class="even">
<td>Rahmen</td>
<td style="text-align: right;">140 €</td>
<td style="text-align: right;">2 h</td>
<td style="text-align: right;">2 h</td>
<td style="text-align: right;">1 h</td>
</tr>
</tbody>
</table>
<p>Der Betriebsrat hat 20 Überstunden genehmigt. <strong>In welchem Bereich sollen sie eingesetzt werden?</strong></p>
<div class="sourceCode" id="cb1"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> [linprog](anhang<span class="op">-</span>glossar.md<span class="co">#gloss:linprog){.glossar-link}</span></span>
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>
2026-09-08 01:20:09 +02:00
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Deckungsbeitrag je Stueck (negiert, weil linprog minimiert)</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>c <span class="op">=</span> [<span class="op">-</span><span class="dv">90</span>, <span class="op">-</span><span class="dv">140</span>]</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>A <span class="op">=</span> [[<span class="dv">1</span>, <span class="dv">2</span>], <span class="co"># Lackierofen (Stunden je Stueck)</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a> [<span class="dv">2</span>, <span class="dv">2</span>], <span class="co"># Montage</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a> [<span class="dv">1</span>, <span class="dv">1</span>]] <span class="co"># Qualitaetspruefung</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>b <span class="op">=</span> [<span class="dv">100</span>, <span class="dv">160</span>, <span class="dv">120</span>] <span class="co"># verfuegbare Stunden</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>name <span class="op">=</span> [<span class="st">&quot;Lackierofen&quot;</span>, <span class="st">&quot;Montage&quot;</span>, <span class="st">&quot;Pruefung&quot;</span>]</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>res <span class="op">=</span> linprog(c, A_ub<span class="op">=</span>A, b_ub<span class="op">=</span>b, bounds<span class="op">=</span>(<span class="dv">0</span>, <span class="va">None</span>))</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Plan: </span><span class="sc">{</span>res<span class="sc">.</span>x[<span class="dv">0</span>]<span class="sc">:.0f}</span><span class="ss"> Karosserieteile, </span><span class="sc">{</span>res<span class="sc">.</span>x[<span class="dv">1</span>]<span class="sc">:.0f}</span><span class="ss"> Rahmen &quot;</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;-&gt; </span><span class="sc">{</span><span class="op">-</span>res<span class="sc">.</span>fun<span class="sc">:,.0f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> i, y <span class="kw">in</span> <span class="bu">enumerate</span>(<span class="op">-</span>res.ineqlin.marginals):</span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; eine Stunde </span><span class="sc">{</span>name[i]<span class="sc">:&lt;12}</span><span class="ss"> mehr waere wert: </span><span class="sc">{</span>y<span class="sc">:6.2f}</span><span class="ss"> EUR&quot;</span>)</span></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>Plan: 60 Karosserieteile, 20 Rahmen -&gt; 8,200 EUR
eine Stunde Lackierofen mehr waere wert: 50.00 EUR
eine Stunde Montage mehr waere wert: 20.00 EUR
eine Stunde Pruefung mehr waere wert: 0.00 EUR</code></pre>
</blockquote>
</div>
<p><strong>Die Antwort steht in der letzten Spalte, und sie ist eindeutig:</strong> Die 20 Überstunden gehören an den <strong>Lackierofen</strong>. Jede Stunde dort bringt 50 € zusätzlichen Deckungsbeitrag, in der Montage nur 20 € — und in der Qualitätsprüfung <strong>null</strong>, denn dort sind 40 Stunden ohnehin unbenutzt. Wer die Überstunden gleichmäßig verteilt hätte, verschenkt gegenüber der richtigen Zuteilung rund 400 €.</p>
<p>Diese drei Zahlen heißen <strong>Schattenpreise</strong> (oder <em>Dualwerte</em>). Sie sind das eigentliche Produkt einer Optimierung — wertvoller als der Plan selbst, weil sie eine Frage beantworten, die der Plan gar nicht stellt: <em>Woran hängt es, und was wäre eine Verbesserung wert?</em></p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Der Plan sagt Ihnen, was Sie heute tun sollen. Der Schattenpreis sagt Ihnen, worin Sie morgen investieren sollen. Ein Schattenpreis von 0 ist dabei genauso wertvoll wie ein hoher: Er beweist, dass diese Ressource <strong>nicht</strong> der Engpass ist — und bewahrt Sie vor einer Investition, die nichts bringt.</p>
</blockquote>
<p><strong>Warum funktioniert das?</strong> Der Schattenpreis fällt beim Lösen nebenbei ab. Er ist die Lösung eines zweiten, spiegelbildlichen Problems, das der Solver implizit mitlöst: nicht „welche Mengen produziere ich?“, sondern „was sind meine Ressourcen wert?“. Dieses Zwillingsproblem heißt <strong>duales Problem</strong>, und der Rest des Kapitels erklärt, woher es kommt, warum seine Lösung exakt denselben Zielwert hat — und in welchen zwei Fällen man Schattenpreisen <strong>nicht</strong> trauen darf.</p>
<hr />
<h2 id="sec:lp-lernziele">5.2 Lernziele</h2>
<p>Nach diesem Kapitel können Sie …</p>
<ol type="1">
<li>… Schlupfvariablen einführen und ein Ungleichungssystem in ein Gleichungssystem überführen.</li>
<li>… eine Simplex-Iteration von Hand rechnen: Pivotspalte, Pivotzeile, Basistausch.</li>
<li>… erklären, warum das Verfahren terminiert und warum es das Optimum findet.</li>
<li>… das duale Problem zu einem gegebenen primalen aufstellen.</li>
<li><strong>Schattenpreise</strong> korrekt auslesen und interpretieren — inklusive der Vorzeichenfalle, die einer der häufigsten Fehler bei der <a href="anhang-glossar.md#gloss:sensitivitaetsanalyse" class="glossar-link">Sensitivitätsanalyse</a> ist.</li>
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>
2026-09-08 01:20:09 +02:00
<li>… aus einer Sensitivitätsanalyse eine begründete Kaufentscheidung ableiten.</li>
<li>… ein Modell auf <strong><a href="anhang-glossar.md#gloss:entartung" class="glossar-link">Entartung</a></strong> prüfen und statt eines nicht eindeutigen Schattenpreises dessen belastbare Spanne berechnen.</li>
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>
2026-09-08 01:20:09 +02:00
<li>… erklären, warum Zulässigkeit immer gegen eine Toleranz und nie mit <code>== 0</code> geprüft wird.</li>
</ol>
<hr />
<h2 id="sec:lp-die-standardform-und-schlupfvariablen">5.3 Die Standardform und Schlupfvariablen</h2>
<p>Lineare Programme zeichnen sich dadurch aus, dass Zielfunktion <strong>und</strong> alle Nebenbedingungen <strong>strikt linear</strong> sind — keine Produkte von Variablen, keine Quadrate, keine Logarithmen.</p>
<p>Ein Ungleichungssystem ist rechnerisch unhandlich. Gleichungssysteme dagegen kann man mit Gauß-Elimination lösen. Die Brücke bilden <strong>Schlupfvariablen</strong> (<em>slack variables</em>) <span class="math inline">s_i \ge 0</span>:</p>
<p><span class="math display">
\sum_{j=1}^n a_{ij}x_j \le b_i
\qquad\Longleftrightarrow\qquad
\sum_{j=1}^n a_{ij}x_j + s_i = b_i,\quad s_i \ge 0
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong> Links steht „der Verbrauch ist <strong>höchstens</strong> die Kapazität“. Rechts steht „der Verbrauch <strong>plus ein Rest</strong> ist <strong>genau</strong> die Kapazität“.</p>
<p><strong>Ohne Formel gesagt:</strong> Statt zu sagen „ich verbrauche höchstens 60 GB“, sagt man „ich verbrauche 60 GB minus dem, was übrig bleibt“. Der Rest bekommt einen Namen — und wird dadurch zu einer Variablen, mit der man rechnen kann.</p>
</blockquote>
<p><strong>Die ökonomische Bedeutung von <span class="math inline">s_i</span> ist der eigentliche Gewinn dieser Umformung:</strong></p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th><span class="math inline">s_i</span></th>
<th>Bedeutung</th>
<th>Fachbegriff</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><span class="math inline">s_i = 0</span></td>
<td>Ressource <span class="math inline">i</span> ist <strong>vollständig ausgelastet</strong></td>
<td>bindende Nebenbedingung, <strong>Engpass</strong> (<em>bottleneck</em>)</td>
</tr>
<tr class="even">
<td><span class="math inline">s_i &gt; 0</span></td>
<td>Es sind <span class="math inline">s_i</span> Einheiten <strong>übrig</strong></td>
<td>nicht-bindende Nebenbedingung, Reserve</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>🎯 Merksatz</strong> Die Schlupfvariable ist kein Rechentrick, sondern eine Messgröße: Sie sagt Ihnen, welche Ressource Ihr Geschäft begrenzt. Und nur wo sie null ist, kann es sich lohnen, Kapazität nachzukaufen.</p>
</blockquote>
<hr />
<h2 id="sec:lp-der-simplex-algorithmus-schritt-fuer-schritt">5.4 Der Simplex-Algorithmus Schritt für Schritt</h2>
<p>Der von George Dantzig 1947 formulierte Simplex-Algorithmus nutzt den Fundamentalsatz aus <a href="fundament.html#kap-fundament">Kapitel 2</a>: <strong>Das Optimum liegt in einer Ecke.</strong> Statt alle Ecken aufzuzählen, wandert er von Ecke zu Ecke — und zwar immer in Richtung Verbesserung.</p>
<figure>
2026-09-09 07:37:07 +02:00
<img src="bilder_04/kap_lp_simplex_ecken.svg" alt="Abb. 5.1: Derselbe zulässige Bereich, aber der Weg der Handrechnung weiter unten: vom Ursprung über (0; 8) zum Optimum (7,5; 5). Drei der fünf Ecken werden besucht, keine zweimal — das ist der ganze Gewinn gegenüber dem Aufzählen aller Ecken. Punktiert die Niveaulinien der Zielfunktion, der Pfeil ihr Gradient \mathbf{c}. Erzeugt von bilder_04/erzeuge_polyeder.py." />
<figcaption aria-hidden="true">Abb. 5.1: Derselbe zulässige Bereich, aber der Weg der Handrechnung weiter unten: vom Ursprung über (0; 8) zum Optimum (7,5; 5). Drei der fünf Ecken werden besucht, keine zweimal — das ist der ganze Gewinn gegenüber dem Aufzählen aller Ecken. Punktiert die Niveaulinien der Zielfunktion, der Pfeil ihr <a href="anhang-glossar.md#gloss:gradient" class="glossar-link">Gradient</a> <span class="math inline">\mathbf{c}</span>. Erzeugt von <code>bilder_04/erzeuge_polyeder.py</code>.</figcaption>
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>
2026-09-08 01:20:09 +02:00
</figure>
2026-09-09 07:37:07 +02:00
<p><strong>Was Sie in der Abbildung sehen.</strong> Durch jede besuchte Ecke läuft genau eine Niveaulinie — die Linien sind nicht gewählt, sondern die Zielwerte 0 €, 2.000 € und 2.375 € der drei Ecken. Der Simplex schiebt diese Linie in Richtung des Pfeils so weit hinaus, wie das Polyeder es zulässt. Dass er dabei an einer Ecke anhält und nicht mittendrin, ist keine Eigenheit des Verfahrens, sondern der Fundamentalsatz: Weiter geht es nur noch hinaus.</p>
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>
2026-09-08 01:20:09 +02:00
<h3 id="der-ablauf">Der Ablauf</h3>
<ol type="1">
<li><strong>Initialisierung.</strong> Starte an einer bekannten Ecke — meist dem Ursprung <span class="math inline">\mathbf{x} = \mathbf{0}</span>, bei dem alle Schlupfvariablen ihre Kapazität aufnehmen: <span class="math inline">\mathbf{s} = \mathbf{b}</span>. Das ist zulässig, solange <span class="math inline">\mathbf{b} \ge \mathbf{0}</span>.</li>
<li><strong>Optimalitätsprüfung.</strong> Sieh dir die Zielfunktionszeile an. Gibt es eine Variable, deren Aufnahme in die Basis den Zielwert noch verbessern würde? → <strong>Nein:</strong> fertig, das Optimum ist erreicht.</li>
<li><strong>Pivotspalte wählen (eintretende Variable).</strong> Diejenige Variable mit dem größten Verbesserungspotenzial pro Einheit (<em>Dantzig-Regel</em>).</li>
<li><strong>Pivotzeile wählen (austretende Variable).</strong> Der <strong>minimale Quotient</strong> <span class="math inline">\min_i \{ b_i / a_{ik} \mid a_{ik} &gt; 0 \}</span>. Das ist die Bedingung, die als erste erschöpft ist, wenn man die neue Variable hochfährt.</li>
<li><strong>Basistausch (Gauß-Jordan-Schritt).</strong> Eliminiere die eintretende Variable aus allen anderen Zeilen. Weiter bei Schritt 2.</li>
</ol>
<blockquote>
<p><strong>✏️ Handrechnung 5.1: Eine vollständige Simplex-Rechnung</strong></p>
<p>Wir lösen von Hand: <span class="math display">\max\ Z = 150x_1 + 250x_2 \quad\text{u. d. N.}\quad 2x_1+5x_2 \le 40,\quad 4x_1+6x_2 \le 60,\quad x_1 \le 8,\quad x_1,x_2 \ge 0</span></p>
<p><strong>Schritt 0 — Schlupfvariablen einführen:</strong> <span class="math display">\begin{aligned} 2x_1+5x_2+s_1 &amp;= 40\\
4x_1+6x_2+s_2 &amp;= 60\\
x_1+s_3 &amp;= 8\\
Z - 150x_1 - 250x_2 &amp;= 0 \end{aligned}</span></p>
<p><strong>Starttableau</strong> (Basis: <span class="math inline">s_1, s_2, s_3</span>; also <span class="math inline">x_1 = x_2 = 0</span>, <span class="math inline">Z = 0</span>):</p>
<table>
<thead>
<tr class="header">
<th>Basis</th>
<th style="text-align: right;"><span class="math inline">x_1</span></th>
<th style="text-align: right;"><span class="math inline">x_2</span></th>
<th style="text-align: right;"><span class="math inline">s_1</span></th>
<th style="text-align: right;"><span class="math inline">s_2</span></th>
<th style="text-align: right;"><span class="math inline">s_3</span></th>
<th style="text-align: right;">RHS</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><span class="math inline">s_1</span></td>
<td style="text-align: right;">2</td>
<td style="text-align: right;"><strong>5</strong></td>
<td style="text-align: right;">1</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">40</td>
</tr>
<tr class="even">
<td><span class="math inline">s_2</span></td>
<td style="text-align: right;">4</td>
<td style="text-align: right;">6</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">1</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">60</td>
</tr>
<tr class="odd">
<td><span class="math inline">s_3</span></td>
<td style="text-align: right;">1</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">1</td>
<td style="text-align: right;">8</td>
</tr>
<tr class="even">
<td><span class="math inline">Z</span></td>
<td style="text-align: right;">150</td>
<td style="text-align: right;"><strong>250</strong></td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
</tr>
</tbody>
</table>
<p><strong>Iteration 1.</strong> <em>Pivotspalte:</em> In der <span class="math inline">Z</span>-Zeile ist <span class="math inline">-250</span> am negativsten → <span class="math inline">x_2</span> tritt ein. <em>Pivotzeile:</em> Quotienten <span class="math inline">40/5 = 8</span>, <span class="math inline">60/6 = 10</span>, <span class="math inline">8/0 = \infty</span><strong>Zeile 1</strong> (kleinster Quotient 8). Pivotelement ist die <strong>5</strong>.</p>
<p>Pivotzeile durch 5 teilen: <span class="math inline">(0{,}4;\ 1;\ 0{,}2;\ 0;\ 0 \mid 8)</span>. Dann <span class="math inline">x_2</span> aus den anderen Zeilen eliminieren: * Zeile 2 <span class="math inline">-\ 6 \cdot</span> Pivotzeile: <span class="math inline">(4-2{,}4;\ 0;\ -1{,}2;\ 1;\ 0 \mid 60-48) = (1{,}6;\ 0;\ -1{,}2;\ 1;\ 0 \mid 12)</span> * Zeile 3 bleibt (Koeffizient war 0). * <span class="math inline">Z</span>-Zeile <span class="math inline">+\ 250 \cdot</span> Pivotzeile: <span class="math inline">(-150+100;\ 0;\ 50;\ 0;\ 0 \mid 2000) = (-50;\ 0;\ 50;\ 0;\ 0 \mid 2000)</span></p>
<table>
<thead>
<tr class="header">
<th>Basis</th>
<th style="text-align: right;"><span class="math inline">x_1</span></th>
<th style="text-align: right;"><span class="math inline">x_2</span></th>
<th style="text-align: right;"><span class="math inline">s_1</span></th>
<th style="text-align: right;"><span class="math inline">s_2</span></th>
<th style="text-align: right;"><span class="math inline">s_3</span></th>
<th style="text-align: right;">RHS</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><span class="math inline">x_2</span></td>
<td style="text-align: right;">0,4</td>
<td style="text-align: right;">1</td>
<td style="text-align: right;">0,2</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">8</td>
</tr>
<tr class="even">
<td><span class="math inline">s_2</span></td>
<td style="text-align: right;"><strong>1,6</strong></td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">1,2</td>
<td style="text-align: right;">1</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">12</td>
</tr>
<tr class="odd">
<td><span class="math inline">s_3</span></td>
<td style="text-align: right;">1</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">1</td>
<td style="text-align: right;">8</td>
</tr>
<tr class="even">
<td><span class="math inline">Z</span></td>
<td style="text-align: right;"><strong>50</strong></td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">50</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">2000</td>
</tr>
</tbody>
</table>
<p>Zwischenstand: <span class="math inline">x_2 = 8</span>, <span class="math inline">Z = 2000</span> — das ist genau die naive Lösung „nur Trendfolge“ aus <a href="einfuehrung.html#kap-einfuehrung">Kapitel 1</a>. Der Simplex ist noch nicht fertig, denn in der <span class="math inline">Z</span>-Zeile steht noch <span class="math inline">-50</span>.</p>
<p><strong>Iteration 2.</strong> <em>Pivotspalte:</em> <span class="math inline">x_1</span> (einziger negativer Eintrag, <span class="math inline">-50</span>). <em>Pivotzeile:</em> Quotienten <span class="math inline">8/0{,}4 = 20</span>, <span class="math inline">12/1{,}6 = 7{,}5</span>, <span class="math inline">8/1 = 8</span><strong>Zeile 2</strong> (kleinster Quotient 7,5). Pivotelement <strong>1,6</strong>.</p>
<p>Pivotzeile durch 1,6: <span class="math inline">(1;\ 0;\ -0{,}75;\ 0{,}625;\ 0 \mid 7{,}5)</span>. Eliminieren: * Zeile 1 <span class="math inline">-\ 0{,}4\cdot</span>: <span class="math inline">(0;\ 1;\ 0{,}5;\ -0{,}25;\ 0 \mid 5)</span> * Zeile 3 <span class="math inline">-\ 1\cdot</span>: <span class="math inline">(0;\ 0;\ 0{,}75;\ -0{,}625;\ 1 \mid 0{,}5)</span> * <span class="math inline">Z</span>-Zeile <span class="math inline">+\ 50\cdot</span>: <span class="math inline">(0;\ 0;\ 12{,}5;\ 31{,}25;\ 0 \mid 2375)</span></p>
<table>
<thead>
<tr class="header">
<th>Basis</th>
<th style="text-align: right;"><span class="math inline">x_1</span></th>
<th style="text-align: right;"><span class="math inline">x_2</span></th>
<th style="text-align: right;"><span class="math inline">s_1</span></th>
<th style="text-align: right;"><span class="math inline">s_2</span></th>
<th style="text-align: right;"><span class="math inline">s_3</span></th>
<th style="text-align: right;">RHS</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><span class="math inline">x_2</span></td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">1</td>
<td style="text-align: right;">0,5</td>
<td style="text-align: right;">0,25</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">5</td>
</tr>
<tr class="even">
<td><span class="math inline">x_1</span></td>
<td style="text-align: right;">1</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0,75</td>
<td style="text-align: right;">0,625</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">7,5</td>
</tr>
<tr class="odd">
<td><span class="math inline">s_3</span></td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0,75</td>
<td style="text-align: right;">0,625</td>
<td style="text-align: right;">1</td>
<td style="text-align: right;">0,5</td>
</tr>
<tr class="even">
<td><span class="math inline">Z</span></td>
<td style="text-align: right;">0</td>
<td style="text-align: right;">0</td>
<td style="text-align: right;"><strong>12,5</strong></td>
<td style="text-align: right;"><strong>31,25</strong></td>
<td style="text-align: right;"><strong>0</strong></td>
<td style="text-align: right;"><strong>2375</strong></td>
</tr>
</tbody>
</table>
<p><strong>Alle Einträge der <span class="math inline">Z</span>-Zeile sind <span class="math inline">\ge 0</span> → Optimum erreicht.</strong></p>
<p><strong>Ablesen:</strong> <span class="math inline">x_1^* = 7{,}5</span>, <span class="math inline">x_2^* = 5</span>, <span class="math inline">Z^* = 2375</span>. Das deckt sich exakt mit der Eckenrechnung aus <a href="fundament.html#kap-fundament">Kapitel 2</a> — und wir haben statt fünf Ecken nur <strong>drei</strong> besucht.</p>
<p><strong>Und jetzt der Clou:</strong> Die Zahlen in der <span class="math inline">Z</span>-Zeile unter den Schlupfvariablen sind die <strong>Schattenpreise</strong>: * <span class="math inline">s_1</span> (vCPU): <span class="math inline">y_1^* = 12{,}5</span> €/vCPU * <span class="math inline">s_2</span> (RAM): <span class="math inline">y_2^* = 31{,}25</span> €/GB * <span class="math inline">s_3</span> (Marktlimit): <span class="math inline">y_3^* = 0</span></p>
<p>Der letzte Wert ist null, weil <span class="math inline">s_3 = 0{,}5 &gt; 0</span> — es ist noch Luft beim Arbitrage-Limit, also ist eine Erhöhung dieser Grenze nichts wert. Genau das besagt der Satz vom komplementären Schlupf, den wir in <a href="#sec:lp-dualitaet-und-schattenpreise">Abschnitt 5.6</a> formulieren.</p>
</blockquote>
<hr />
<h2 id="sec:lp-das-simplex-tableau-in-python">5.5 Das Simplex-Tableau in Python</h2>
<div class="sourceCode" id="cb3"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Simplex_Tableau_LP.py</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel LP: Vollständige Implementierung des Simplex-Algorithmus (Tableau-Methode).</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="co">GRENZEN DIESER IMPLEMENTIERUNG (bewusst, aus didaktischen Gründen):</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="co"> * nur Maximierung</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a><span class="co"> * nur &quot;&lt;=&quot;-Nebenbedingungen</span></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a><span class="co"> * alle b_i &gt;= 0 (sonst wäre der Ursprung keine zulässige Startecke und man</span></span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a><span class="co"> bräuchte eine Phase-1-Rechnung mit künstlichen Variablen)</span></span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a><span class="co">Für den produktiven Einsatz nimmt man HiGHS - dieser Code dient dem Verständnis.</span></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a><span class="co">Voraussetzungen werden geprüft statt stillschweigend angenommen;</span></span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a><span class="co">Iterationsprotokoll und Schattenpreise werden ausgegeben.</span></span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a><span class="kw">class</span> SimplexTableauSolver:</span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Maximierungs-Standardform: max c^T x u.d.N. A x &lt;= b, x &gt;= 0, b &gt;= 0.&quot;&quot;&quot;</span></span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> <span class="fu">__init__</span>(<span class="va">self</span>, c, A, b, variablennamen<span class="op">=</span><span class="va">None</span>, restriktionsnamen<span class="op">=</span><span class="va">None</span>):</span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.c <span class="op">=</span> np.asarray(c, dtype<span class="op">=</span><span class="bu">float</span>)</span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.A <span class="op">=</span> np.asarray(A, dtype<span class="op">=</span><span class="bu">float</span>)</span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.b <span class="op">=</span> np.asarray(b, dtype<span class="op">=</span><span class="bu">float</span>)</span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.n <span class="op">=</span> <span class="bu">len</span>(<span class="va">self</span>.c) <span class="co"># Anzahl Originalvariablen</span></span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.m <span class="op">=</span> <span class="bu">len</span>(<span class="va">self</span>.b) <span class="co"># Anzahl Nebenbedingungen</span></span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Voraussetzungen pruefen, statt sie stillschweigend anzunehmen ---</span></span>
<span id="cb3-32"><a href="#cb3-32" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="va">self</span>.A.shape <span class="op">!=</span> (<span class="va">self</span>.m, <span class="va">self</span>.n):</span>
<span id="cb3-33"><a href="#cb3-33" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="ss">f&quot;A hat Form </span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>A<span class="sc">.</span>shape<span class="sc">}</span><span class="ss">, erwartet (</span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>m<span class="sc">}</span><span class="ss">, </span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>n<span class="sc">}</span><span class="ss">).&quot;</span>)</span>
<span id="cb3-34"><a href="#cb3-34" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> np.<span class="bu">any</span>(<span class="va">self</span>.b <span class="op">&lt;</span> <span class="dv">0</span>):</span>
<span id="cb3-35"><a href="#cb3-35" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">ValueError</span>(</span>
<span id="cb3-36"><a href="#cb3-36" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Mindestens ein b_i ist negativ. Dann ist der Ursprung keine zulässige &quot;</span></span>
<span id="cb3-37"><a href="#cb3-37" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Startecke; dieser Solver benötigt eine Phase-1-Rechnung, die hier &quot;</span></span>
<span id="cb3-38"><a href="#cb3-38" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;bewusst nicht implementiert ist. Nutzen Sie scipy.optimize.linprog.&quot;</span></span>
<span id="cb3-39"><a href="#cb3-39" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb3-40"><a href="#cb3-40" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-41"><a href="#cb3-41" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.var_namen <span class="op">=</span> variablennamen <span class="kw">or</span> [<span class="ss">f&quot;x</span><span class="sc">{</span>j<span class="op">+</span><span class="dv">1</span><span class="sc">}</span><span class="ss">&quot;</span> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(<span class="va">self</span>.n)]</span>
<span id="cb3-42"><a href="#cb3-42" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.restr_namen <span class="op">=</span> restriktionsnamen <span class="kw">or</span> [<span class="ss">f&quot;R</span><span class="sc">{</span>i<span class="op">+</span><span class="dv">1</span><span class="sc">}</span><span class="ss">&quot;</span> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(<span class="va">self</span>.m)]</span>
<span id="cb3-43"><a href="#cb3-43" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-44"><a href="#cb3-44" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.tableau <span class="op">=</span> <span class="va">None</span></span>
<span id="cb3-45"><a href="#cb3-45" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.basis <span class="op">=</span> <span class="va">None</span> <span class="co"># welche Variable ist in welcher Zeile Basis?</span></span>
<span id="cb3-46"><a href="#cb3-46" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>._baue_starttableau()</span>
<span id="cb3-47"><a href="#cb3-47" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-48"><a href="#cb3-48" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> _baue_starttableau(<span class="va">self</span>):</span>
<span id="cb3-49"><a href="#cb3-49" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Zeilen: m Nebenbedingungen + Zielfunktionszeile.</span></span>
<span id="cb3-50"><a href="#cb3-50" aria-hidden="true" tabindex="-1"></a><span class="co"> Spalten: n Variablen + m Schlupfvariablen + rechte Seite.&quot;&quot;&quot;</span></span>
<span id="cb3-51"><a href="#cb3-51" aria-hidden="true" tabindex="-1"></a> m, n <span class="op">=</span> <span class="va">self</span>.m, <span class="va">self</span>.n</span>
<span id="cb3-52"><a href="#cb3-52" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.tableau <span class="op">=</span> np.zeros((m <span class="op">+</span> <span class="dv">1</span>, n <span class="op">+</span> m <span class="op">+</span> <span class="dv">1</span>))</span>
<span id="cb3-53"><a href="#cb3-53" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.tableau[:m, :n] <span class="op">=</span> <span class="va">self</span>.A <span class="co"># Koeffizienten</span></span>
<span id="cb3-54"><a href="#cb3-54" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.tableau[:m, n:n <span class="op">+</span> m] <span class="op">=</span> np.eye(m) <span class="co"># Schlupfvariablen</span></span>
<span id="cb3-55"><a href="#cb3-55" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.tableau[:m, <span class="op">-</span><span class="dv">1</span>] <span class="op">=</span> <span class="va">self</span>.b <span class="co"># rechte Seite</span></span>
<span id="cb3-56"><a href="#cb3-56" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.tableau[<span class="op">-</span><span class="dv">1</span>, :n] <span class="op">=</span> <span class="op">-</span><span class="va">self</span>.c <span class="co"># Zielzeile: -c (Maximierung)</span></span>
<span id="cb3-57"><a href="#cb3-57" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.basis <span class="op">=</span> <span class="bu">list</span>(<span class="bu">range</span>(n, n <span class="op">+</span> m)) <span class="co"># Start: alle Schlupf in der Basis</span></span>
<span id="cb3-58"><a href="#cb3-58" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-59"><a href="#cb3-59" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> _spaltenname(<span class="va">self</span>, index):</span>
<span id="cb3-60"><a href="#cb3-60" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">self</span>.var_namen[index] <span class="cf">if</span> index <span class="op">&lt;</span> <span class="va">self</span>.n <span class="cf">else</span> <span class="ss">f&quot;s</span><span class="sc">{</span>index <span class="op">-</span> <span class="va">self</span><span class="sc">.</span>n <span class="op">+</span> <span class="dv">1</span><span class="sc">}</span><span class="ss">&quot;</span></span>
<span id="cb3-61"><a href="#cb3-61" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-62"><a href="#cb3-62" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> solve(<span class="va">self</span>, max_iterationen<span class="op">=</span><span class="dv">100</span>, protokoll<span class="op">=</span><span class="va">True</span>):</span>
<span id="cb3-63"><a href="#cb3-63" aria-hidden="true" tabindex="-1"></a> m, n <span class="op">=</span> <span class="va">self</span>.m, <span class="va">self</span>.n</span>
<span id="cb3-64"><a href="#cb3-64" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> protokoll:</span>
<span id="cb3-65"><a href="#cb3-65" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Iter&#39;</span><span class="sc">:&gt;4}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;eintritt&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;austritt&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> | &quot;</span></span>
<span id="cb3-66"><a href="#cb3-66" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Pivot&#39;</span><span class="sc">:&gt;8}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Z&#39;</span><span class="sc">:&gt;12}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-67"><a href="#cb3-67" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">58</span>)</span>
<span id="cb3-68"><a href="#cb3-68" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-69"><a href="#cb3-69" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> iteration <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">1</span>, max_iterationen <span class="op">+</span> <span class="dv">1</span>):</span>
<span id="cb3-70"><a href="#cb3-70" aria-hidden="true" tabindex="-1"></a> zielzeile <span class="op">=</span> <span class="va">self</span>.tableau[<span class="op">-</span><span class="dv">1</span>, :<span class="op">-</span><span class="dv">1</span>]</span>
<span id="cb3-71"><a href="#cb3-71" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-72"><a href="#cb3-72" aria-hidden="true" tabindex="-1"></a> <span class="co"># 1. Optimalitätsprüfung: alle Koeffizienten &gt;= 0 ?</span></span>
<span id="cb3-73"><a href="#cb3-73" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> np.<span class="bu">all</span>(zielzeile <span class="op">&gt;=</span> <span class="op">-</span><span class="fl">1e-9</span>):</span>
<span id="cb3-74"><a href="#cb3-74" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> protokoll:</span>
<span id="cb3-75"><a href="#cb3-75" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">58</span>)</span>
<span id="cb3-76"><a href="#cb3-76" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Optimum nach </span><span class="sc">{</span>iteration <span class="op">-</span> <span class="dv">1</span><span class="sc">}</span><span class="ss"> Pivotschritten erreicht.&quot;</span>)</span>
<span id="cb3-77"><a href="#cb3-77" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">self</span>._loesung_auslesen()</span>
<span id="cb3-78"><a href="#cb3-78" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-79"><a href="#cb3-79" aria-hidden="true" tabindex="-1"></a> <span class="co"># 2. Pivotspalte: negativster Eintrag (Dantzig-Regel)</span></span>
<span id="cb3-80"><a href="#cb3-80" aria-hidden="true" tabindex="-1"></a> pivot_spalte <span class="op">=</span> <span class="bu">int</span>(np.argmin(zielzeile))</span>
<span id="cb3-81"><a href="#cb3-81" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-82"><a href="#cb3-82" aria-hidden="true" tabindex="-1"></a> <span class="co"># 3. Pivotzeile: minimaler Quotient über POSITIVE Spalteneinträge</span></span>
<span id="cb3-83"><a href="#cb3-83" aria-hidden="true" tabindex="-1"></a> spalte <span class="op">=</span> <span class="va">self</span>.tableau[:m, pivot_spalte]</span>
<span id="cb3-84"><a href="#cb3-84" aria-hidden="true" tabindex="-1"></a> rechte_seite <span class="op">=</span> <span class="va">self</span>.tableau[:m, <span class="op">-</span><span class="dv">1</span>]</span>
<span id="cb3-85"><a href="#cb3-85" aria-hidden="true" tabindex="-1"></a> quotienten <span class="op">=</span> np.where(spalte <span class="op">&gt;</span> <span class="fl">1e-9</span>, rechte_seite <span class="op">/</span> np.where(spalte <span class="op">&gt;</span> <span class="fl">1e-9</span>, spalte, <span class="dv">1</span>),</span>
<span id="cb3-86"><a href="#cb3-86" aria-hidden="true" tabindex="-1"></a> np.inf)</span>
<span id="cb3-87"><a href="#cb3-87" aria-hidden="true" tabindex="-1"></a> pivot_zeile <span class="op">=</span> <span class="bu">int</span>(np.argmin(quotienten))</span>
<span id="cb3-88"><a href="#cb3-88" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> np.isfinite(quotienten[pivot_zeile]):</span>
<span id="cb3-89"><a href="#cb3-89" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">ValueError</span>(</span>
<span id="cb3-90"><a href="#cb3-90" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Problem ist unbeschraenkt: Variable </span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>_spaltenname(pivot_spalte)<span class="sc">}</span><span class="ss"> &quot;</span></span>
<span id="cb3-91"><a href="#cb3-91" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;kann beliebig wachsen, ohne eine Bedingung zu verletzen. &quot;</span></span>
<span id="cb3-92"><a href="#cb3-92" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Meist fehlt eine Kapazitaetsbeschraenkung.&quot;</span></span>
<span id="cb3-93"><a href="#cb3-93" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb3-94"><a href="#cb3-94" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-95"><a href="#cb3-95" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> protokoll:</span>
<span id="cb3-96"><a href="#cb3-96" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>iteration<span class="sc">:&gt;4}</span><span class="ss"> | </span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>_spaltenname(pivot_spalte)<span class="sc">:&gt;9}</span><span class="ss"> | &quot;</span></span>
<span id="cb3-97"><a href="#cb3-97" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>_spaltenname(<span class="va">self</span>.basis[pivot_zeile])<span class="sc">:&gt;9}</span><span class="ss"> | &quot;</span></span>
<span id="cb3-98"><a href="#cb3-98" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>tableau[pivot_zeile, pivot_spalte]<span class="sc">:&gt;8.3f}</span><span class="ss"> | &quot;</span></span>
<span id="cb3-99"><a href="#cb3-99" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>tableau[<span class="op">-</span><span class="dv">1</span>, <span class="op">-</span><span class="dv">1</span>]<span class="sc">:&gt;12,.2f}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-100"><a href="#cb3-100" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-101"><a href="#cb3-101" aria-hidden="true" tabindex="-1"></a> <span class="co"># 4. Pivotoperation (Gauß-Jordan)</span></span>
<span id="cb3-102"><a href="#cb3-102" aria-hidden="true" tabindex="-1"></a> pivot_wert <span class="op">=</span> <span class="va">self</span>.tableau[pivot_zeile, pivot_spalte]</span>
<span id="cb3-103"><a href="#cb3-103" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.tableau[pivot_zeile, :] <span class="op">/=</span> pivot_wert</span>
<span id="cb3-104"><a href="#cb3-104" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> zeile <span class="kw">in</span> <span class="bu">range</span>(m <span class="op">+</span> <span class="dv">1</span>):</span>
<span id="cb3-105"><a href="#cb3-105" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> zeile <span class="op">!=</span> pivot_zeile:</span>
<span id="cb3-106"><a href="#cb3-106" aria-hidden="true" tabindex="-1"></a> faktor <span class="op">=</span> <span class="va">self</span>.tableau[zeile, pivot_spalte]</span>
<span id="cb3-107"><a href="#cb3-107" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.tableau[zeile, :] <span class="op">-=</span> faktor <span class="op">*</span> <span class="va">self</span>.tableau[pivot_zeile, :]</span>
<span id="cb3-108"><a href="#cb3-108" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-109"><a href="#cb3-109" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.basis[pivot_zeile] <span class="op">=</span> pivot_spalte</span>
<span id="cb3-110"><a href="#cb3-110" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-111"><a href="#cb3-111" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="st">&quot;Maximale Iterationszahl ueberschritten (moeglicherweise Zyklus).&quot;</span>)</span>
<span id="cb3-112"><a href="#cb3-112" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-113"><a href="#cb3-113" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> _loesung_auslesen(<span class="va">self</span>):</span>
<span id="cb3-114"><a href="#cb3-114" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Basisvariablen tragen den RHS-Wert ihrer Zeile, Nichtbasisvariablen sind 0.&quot;&quot;&quot;</span></span>
<span id="cb3-115"><a href="#cb3-115" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> np.zeros(<span class="va">self</span>.n <span class="op">+</span> <span class="va">self</span>.m)</span>
<span id="cb3-116"><a href="#cb3-116" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> zeile, spalte <span class="kw">in</span> <span class="bu">enumerate</span>(<span class="va">self</span>.basis):</span>
<span id="cb3-117"><a href="#cb3-117" aria-hidden="true" tabindex="-1"></a> x[spalte] <span class="op">=</span> <span class="va">self</span>.tableau[zeile, <span class="op">-</span><span class="dv">1</span>]</span>
<span id="cb3-118"><a href="#cb3-118" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> x[:<span class="va">self</span>.n], x[<span class="va">self</span>.n:], <span class="va">self</span>.tableau[<span class="op">-</span><span class="dv">1</span>, <span class="op">-</span><span class="dv">1</span>]</span>
<span id="cb3-119"><a href="#cb3-119" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-120"><a href="#cb3-120" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> schattenpreise(<span class="va">self</span>):</span>
<span id="cb3-121"><a href="#cb3-121" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Die Zielzeile unter den Schlupfspalten enthält direkt die Dualwerte.&quot;&quot;&quot;</span></span>
<span id="cb3-122"><a href="#cb3-122" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">self</span>.tableau[<span class="op">-</span><span class="dv">1</span>, <span class="va">self</span>.n:<span class="va">self</span>.n <span class="op">+</span> <span class="va">self</span>.m].copy()</span>
<span id="cb3-123"><a href="#cb3-123" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-124"><a href="#cb3-124" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-125"><a href="#cb3-125" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb3-126"><a href="#cb3-126" aria-hidden="true" tabindex="-1"></a> <span class="co"># Modell aus der Simplex-Handrechnung (Bot-Beispiel, Kapitel Einfuehrung)</span></span>
<span id="cb3-127"><a href="#cb3-127" aria-hidden="true" tabindex="-1"></a> ertraege <span class="op">=</span> [<span class="fl">150.0</span>, <span class="fl">250.0</span>]</span>
<span id="cb3-128"><a href="#cb3-128" aria-hidden="true" tabindex="-1"></a> matrix <span class="op">=</span> [[<span class="fl">2.0</span>, <span class="fl">5.0</span>],</span>
<span id="cb3-129"><a href="#cb3-129" aria-hidden="true" tabindex="-1"></a> [<span class="fl">4.0</span>, <span class="fl">6.0</span>],</span>
<span id="cb3-130"><a href="#cb3-130" aria-hidden="true" tabindex="-1"></a> [<span class="fl">1.0</span>, <span class="fl">0.0</span>]]</span>
<span id="cb3-131"><a href="#cb3-131" aria-hidden="true" tabindex="-1"></a> kapazitaeten <span class="op">=</span> [<span class="fl">40.0</span>, <span class="fl">60.0</span>, <span class="fl">8.0</span>]</span>
<span id="cb3-132"><a href="#cb3-132" aria-hidden="true" tabindex="-1"></a> var_namen <span class="op">=</span> [<span class="st">&quot;x_A&quot;</span>, <span class="st">&quot;x_B&quot;</span>]</span>
<span id="cb3-133"><a href="#cb3-133" aria-hidden="true" tabindex="-1"></a> restr_namen <span class="op">=</span> [<span class="st">&quot;vCPU&quot;</span>, <span class="st">&quot;RAM&quot;</span>, <span class="st">&quot;Marktlimit&quot;</span>]</span>
<span id="cb3-134"><a href="#cb3-134" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-135"><a href="#cb3-135" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">58</span>)</span>
<span id="cb3-136"><a href="#cb3-136" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; SIMPLEX-TABLEAU: ITERATIONSPROTOKOLL&quot;</span>)</span>
<span id="cb3-137"><a href="#cb3-137" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">58</span>)</span>
<span id="cb3-138"><a href="#cb3-138" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-139"><a href="#cb3-139" aria-hidden="true" tabindex="-1"></a> solver <span class="op">=</span> SimplexTableauSolver(ertraege, matrix, kapazitaeten, var_namen, restr_namen)</span>
<span id="cb3-140"><a href="#cb3-140" aria-hidden="true" tabindex="-1"></a> x_opt, schlupf, z_opt <span class="op">=</span> solver.solve()</span>
<span id="cb3-141"><a href="#cb3-141" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-142"><a href="#cb3-142" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">58</span>)</span>
<span id="cb3-143"><a href="#cb3-143" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; ERGEBNIS&quot;</span>)</span>
<span id="cb3-144"><a href="#cb3-144" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">58</span>)</span>
<span id="cb3-145"><a href="#cb3-145" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> name, wert <span class="kw">in</span> <span class="bu">zip</span>(var_namen, x_opt):</span>
<span id="cb3-146"><a href="#cb3-146" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>name<span class="sc">:&lt;12}</span><span class="ss"> = </span><span class="sc">{</span>wert<span class="sc">:8.4f}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-147"><a href="#cb3-147" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span><span class="st">&#39;Zielwert Z&#39;</span><span class="sc">:&lt;12}</span><span class="ss"> = </span><span class="sc">{</span>z_opt<span class="sc">:8.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb3-148"><a href="#cb3-148" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-149"><a href="#cb3-149" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st"> Ressourcenanalyse:&quot;</span>)</span>
<span id="cb3-150"><a href="#cb3-150" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span><span class="st">&#39;Ressource&#39;</span><span class="sc">:&lt;12}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Schlupf&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Status&#39;</span><span class="sc">:&gt;22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Schattenpreis&#39;</span><span class="sc">:&gt;15}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-151"><a href="#cb3-151" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; &quot;</span> <span class="op">+</span> <span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">60</span>)</span>
<span id="cb3-152"><a href="#cb3-152" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> name, s, y <span class="kw">in</span> <span class="bu">zip</span>(restr_namen, schlupf, solver.schattenpreise()):</span>
<span id="cb3-153"><a href="#cb3-153" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> <span class="st">&quot;ENGPASS (bindend)&quot;</span> <span class="cf">if</span> <span class="bu">abs</span>(s) <span class="op">&lt;</span> <span class="fl">1e-9</span> <span class="cf">else</span> <span class="st">&quot;Reserve vorhanden&quot;</span></span>
<span id="cb3-154"><a href="#cb3-154" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>name<span class="sc">:&lt;12}</span><span class="ss"> </span><span class="sc">{</span>s<span class="sc">:&gt;9.3f}</span><span class="ss"> </span><span class="sc">{</span>status<span class="sc">:&gt;22}</span><span class="ss"> </span><span class="sc">{</span>y<span class="sc">:&gt;12.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb3-155"><a href="#cb3-155" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-156"><a href="#cb3-156" aria-hidden="true" tabindex="-1"></a> <span class="co"># Selbstkontrolle: komplementaerer Schlupf muss gelten</span></span>
<span id="cb3-157"><a href="#cb3-157" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> s, y <span class="kw">in</span> <span class="bu">zip</span>(schlupf, solver.schattenpreise()):</span>
<span id="cb3-158"><a href="#cb3-158" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> <span class="bu">abs</span>(s <span class="op">*</span> y) <span class="op">&lt;</span> <span class="fl">1e-6</span>, <span class="st">&quot;Komplementaerer Schlupf verletzt - Rechenfehler!&quot;</span></span>
<span id="cb3-159"><a href="#cb3-159" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st"> Pruefung: komplementaerer Schlupf (s_i * y_i = 0) fuer alle i erfuellt.&quot;</span>)</span>
<span id="cb3-160"><a href="#cb3-160" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">58</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==========================================================
SIMPLEX-TABLEAU: ITERATIONSPROTOKOLL
==========================================================
Iter | eintritt | austritt | Pivot | Z
----------------------------------------------------------
1 | x_B | s1 | 5.000 | 0.00
2 | x_A | s2 | 1.600 | 2,000.00
----------------------------------------------------------
Optimum nach 2 Pivotschritten erreicht.
==========================================================
ERGEBNIS
==========================================================
x_A = 7.5000
x_B = 5.0000
Zielwert Z = 2375.00 EUR
Ressourcenanalyse:
Ressource Schlupf Status Schattenpreis
------------------------------------------------------------
vCPU 0.000 ENGPASS (bindend) 12.50 EUR
RAM 0.000 ENGPASS (bindend) 31.25 EUR
Marktlimit 0.500 Reserve vorhanden 0.00 EUR
Pruefung: komplementaerer Schlupf (s_i * y_i = 0) fuer alle i erfuellt.
==========================================================</code></pre>
<p>Die Ausgabe reproduziert die Simplex-Handrechnung exakt — inklusive der Schattenpreise 12,50 € und 31,25 €.</p>
<blockquote>
<p><strong>💻 Code-Durchgang</strong></p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Stelle</th>
<th>Was passiert</th>
<th>Warum</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>if np.any(self.b &lt; 0): raise</code></td>
<td>Voraussetzung geprüft</td>
<td>Ohne diese Prüfung liefert der Solver hier still ein falsches Ergebnis. Ein Solver, der seine Annahmen nicht prüft, ist gefährlich</td>
</tr>
<tr class="even">
<td><code>self.tableau[-1, :n] = -self.c</code></td>
<td>Zielzeile <strong>negativ</strong> eintragen</td>
<td>Trick, damit „alle Einträge <span class="math inline">\ge 0</span>“ das Abbruchkriterium wird</td>
</tr>
<tr class="odd">
<td><code>np.argmin(zielzeile)</code></td>
<td>Dantzig-Regel</td>
<td>Größte Verbesserung pro Einheit; andere Regeln (Bland) vermeiden Zyklen, sind aber langsamer</td>
</tr>
<tr class="even">
<td><code>quotienten = np.where(spalte &gt; 1e-9, ...)</code></td>
<td>Minimum-Quotienten-Test</td>
<td><strong>Nur positive</strong> Spalteneinträge zählen — sonst würde eine Variable negativ</td>
</tr>
<tr class="odd">
<td><code>raise ValueError("unbeschraenkt")</code></td>
<td>alle Quotienten unendlich</td>
<td>Diagnose statt Absturz: „Ihnen fehlt eine Nebenbedingung“</td>
</tr>
<tr class="even">
<td><code>self.basis[pivot_zeile] = pivot_spalte</code></td>
<td>Basiswechsel protokollieren</td>
<td>Ohne diese Buchführung kann man die Lösung am Ende nicht auslesen</td>
</tr>
<tr class="odd">
<td><code>assert abs(s*y) &lt; 1e-6</code></td>
<td>komplementärer Schlupf</td>
<td>Eine Zeile, die jeden Vorzeichenfehler sofort aufdeckt</td>
</tr>
</tbody>
</table>
</blockquote>
<blockquote>
<p><strong>⚠️ Typische Fehler</strong></p>
<ul>
<li><strong>Negative rechte Seite.</strong> <span class="math inline">-3x_1 - 2x_2 \le -12</span> (aus einer „<span class="math inline">\ge</span>“-Bedingung entstanden) macht den Ursprung unzulässig. Dann braucht man Phase 1 mit künstlichen Variablen. Dieser Solver meldet das jetzt ehrlich.</li>
<li><strong>Zyklen bei Entartung.</strong> Wenn mehrere Quotienten gleich sind, kann der Simplex theoretisch endlos kreisen. In der Praxis extrem selten; Blands Regel verhindert es beweisbar.</li>
<li><strong>Den Simplex selbst produktiv einsetzen.</strong> Diese Implementierung ist ein Lehrmodell. <a href="anhang-glossar.md#gloss:highs" class="glossar-link">HiGHS</a> ist um Größenordnungen schneller und numerisch stabiler.</li>
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>
2026-09-08 01:20:09 +02:00
</ul>
</blockquote>
<hr />
<h2 id="sec:lp-dualitaet-und-schattenpreise">5.6 Dualität und Schattenpreise</h2>
<p>Zu <strong>jedem</strong> linearen Optimierungsproblem (dem <strong>primalen Problem</strong>) existiert ein spiegelbildliches Zwillingsproblem: das <strong>duale Problem</strong>.</p>
<figure>
<img src="bilder_04/kap04_primal_dual.svg" alt="Abb. 5.2: Primales und duales Problem" />
<figcaption aria-hidden="true">Abb. 5.2: Primales und duales Problem</figcaption>
</figure>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th></th>
<th><strong>Primal</strong> (Unternehmer)</th>
<th><strong>Dual</strong> (Ressourcenbewerter)</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Fragestellung</td>
<td>„Wie viel produziere ich?“</td>
<td>„Was sind meine Ressourcen wert?“</td>
</tr>
<tr class="even">
<td>Zielfunktion</td>
<td><span class="math inline">\max\ \mathbf{c}^\top\mathbf{x}</span></td>
<td><span class="math inline">\min\ \mathbf{b}^\top\mathbf{y}</span></td>
</tr>
<tr class="odd">
<td>Nebenbedingungen</td>
<td><span class="math inline">\mathbf{A}\mathbf{x} \le \mathbf{b}</span></td>
<td><span class="math inline">\mathbf{A}^\top\mathbf{y} \ge \mathbf{c}</span></td>
</tr>
<tr class="even">
<td>Variablen</td>
<td><span class="math inline">\mathbf{x} \ge \mathbf{0}</span> (Mengen)</td>
<td><span class="math inline">\mathbf{y} \ge \mathbf{0}</span> (Preise)</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>📐 Formel-Lesehilfe zum dualen Problem</strong> * <span class="math inline">\mathbf{b}^\top\mathbf{y}</span> — der Gesamtwert aller vorhandenen Kapazitäten, bewertet mit den Preisen <span class="math inline">\mathbf{y}</span>. * <span class="math inline">\mathbf{A}^\top\mathbf{y} \ge \mathbf{c}</span> — für <strong>jedes Produkt</strong> muss gelten: Der Wert der Ressourcen, die eine Einheit verbraucht, ist mindestens so hoch wie ihr Verkaufserlös.</p>
<p><strong>Ohne Formel gesagt:</strong> Ein gedachter Käufer will Ihnen die gesamten Ressourcen abkaufen. Er will möglichst wenig zahlen (Minimierung), muss Ihnen aber pro Ressourcenbündel mindestens so viel bieten, wie Sie durch eigene Produktion verdienen würden — sonst lehnen Sie ab.</p>
</blockquote>
<h3 id="der-starke-dualitätssatz">Der starke Dualitätssatz</h3>
<blockquote>
<p><strong>Satz.</strong> Besitzt das primale Problem eine endliche Optimallösung <span class="math inline">\mathbf{x}^*</span>, so besitzt auch das duale eine Optimallösung <span class="math inline">\mathbf{y}^*</span>, und die Zielwerte sind <strong>exakt gleich</strong>: <span class="math display">\mathbf{c}^\top\mathbf{x}^* = \mathbf{b}^\top\mathbf{y}^*</span></p>
</blockquote>
<p>Prüfen wir das an unserem Beispiel: <span class="math inline">\mathbf{b}^\top\mathbf{y}^* = 40 \cdot 12{,}5 + 60 \cdot 31{,}25 + 8 \cdot 0 = 500 + 1875 + 0 = 2375</span> ✓ — identisch mit <span class="math inline">Z^* = 2375</span>.</p>
2026-09-09 07:37:07 +02:00
<figure>
<img src="bilder_04/kap_lp_dualitaets_bruecke.svg" alt="Abb. 5.3: Beide Probleme geometrisch, gerechnet mit Dualitaet_Nachweis.py. Rechts ist die Ebene y_3 = 0 gezeichnet — zulässig, weil das Marktlimit x_1 \le 8 bei x_1^* = 7{,}5 nicht bindet und deshalb y_3^* = 0 gilt. Erzeugt von bilder_04/erzeuge_dualitaets_bruecke.py." />
<figcaption aria-hidden="true">Abb. 5.3: Beide Probleme geometrisch, gerechnet mit <code>Dualitaet_Nachweis.py</code>. Rechts ist die Ebene <span class="math inline">y_3 = 0</span> gezeichnet — zulässig, weil das Marktlimit <span class="math inline">x_1 \le 8</span> bei <span class="math inline">x_1^* = 7{,}5</span> nicht bindet und deshalb <span class="math inline">y_3^* = 0</span> gilt. Erzeugt von <code>bilder_04/erzeuge_dualitaets_bruecke.py</code>.</figcaption>
</figure>
<p><strong>Was Sie in der Abbildung sehen.</strong> Die beiden Tafeln haben nichts gemeinsam: verschiedene Achsen, verschiedene Einheiten, links ein geschlossenes Polyeder, rechts ein nach oben offener Bereich. Links wird maximiert, rechts minimiert. Und doch steht an beiden Optima dieselbe Zahl — 2.375 €. Genau das behauptet der Satz, und mehr ist an ihm nicht zu sehen.</p>
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>
2026-09-08 01:20:09 +02:00
<p>Das lässt sich auch unabhängig von der Handrechnung nachweisen: Primal- und Dualproblem werden als <strong>zwei getrennte, voneinander unabhängige LPs</strong> aufgestellt und gelöst — wenn der starke Dualitätssatz stimmt, müssen beide denselben Zielwert liefern, ohne dass eines vom anderen „weiß“.</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Dualitaet_Nachweis.py</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel LP: Primales und duales Problem unabhaengig loesen und den starken</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a><span class="co">Dualitaetssatz sowie den komplementaeren Schlupf numerisch nachweisen.</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a><span class="co">Modell aus der Simplex-Handrechnung (Bot-Allokation, LP-Relaxation):</span></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a><span class="co"> max 150*x1 + 250*x2 u.d.N. 2*x1+5*x2&lt;=40, 4*x1+6*x2&lt;=60, x1&lt;=8, x&gt;=0</span></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linprog</span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Primales Problem --------------------------------------------------------</span></span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a>C_PRIMAL <span class="op">=</span> np.array([<span class="fl">150.0</span>, <span class="fl">250.0</span>])</span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a>A_PRIMAL <span class="op">=</span> np.array([[<span class="fl">2.0</span>, <span class="fl">5.0</span>], [<span class="fl">4.0</span>, <span class="fl">6.0</span>], [<span class="fl">1.0</span>, <span class="fl">0.0</span>]])</span>
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a>B_PRIMAL <span class="op">=</span> np.array([<span class="fl">40.0</span>, <span class="fl">60.0</span>, <span class="fl">8.0</span>])</span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Duales Problem: min b^T y u.d.N. A^T y &gt;= c, y &gt;= 0 ------------------</span></span>
<span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a><span class="co"># linprog kennt nur &lt;=, also A^T y &gt;= c &lt;=&gt; -A^T y &lt;= -c</span></span>
<span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a>C_DUAL <span class="op">=</span> B_PRIMAL</span>
<span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a>A_DUAL <span class="op">=</span> <span class="op">-</span>A_PRIMAL.T</span>
<span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a>B_DUAL <span class="op">=</span> <span class="op">-</span>C_PRIMAL</span>
<span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-26"><a href="#cb5-26" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-27"><a href="#cb5-27" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese():</span>
<span id="cb5-28"><a href="#cb5-28" aria-hidden="true" tabindex="-1"></a> primal <span class="op">=</span> linprog(c<span class="op">=-</span>C_PRIMAL, A_ub<span class="op">=</span>A_PRIMAL, b_ub<span class="op">=</span>B_PRIMAL,</span>
<span id="cb5-29"><a href="#cb5-29" aria-hidden="true" tabindex="-1"></a> bounds<span class="op">=</span>[(<span class="dv">0</span>, <span class="va">None</span>)] <span class="op">*</span> <span class="dv">2</span>, method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb5-30"><a href="#cb5-30" aria-hidden="true" tabindex="-1"></a> dual <span class="op">=</span> linprog(c<span class="op">=</span>C_DUAL, A_ub<span class="op">=</span>A_DUAL, b_ub<span class="op">=</span>B_DUAL,</span>
<span id="cb5-31"><a href="#cb5-31" aria-hidden="true" tabindex="-1"></a> bounds<span class="op">=</span>[(<span class="dv">0</span>, <span class="va">None</span>)] <span class="op">*</span> <span class="dv">3</span>, method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb5-32"><a href="#cb5-32" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> primal.success <span class="kw">or</span> <span class="kw">not</span> dual.success:</span>
<span id="cb5-33"><a href="#cb5-33" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">SystemExit</span>(<span class="st">&quot;Primal oder Dual nicht loesbar.&quot;</span>)</span>
<span id="cb5-34"><a href="#cb5-34" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> primal, dual</span>
<span id="cb5-35"><a href="#cb5-35" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-36"><a href="#cb5-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-37"><a href="#cb5-37" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb5-38"><a href="#cb5-38" aria-hidden="true" tabindex="-1"></a> primal, dual <span class="op">=</span> loese()</span>
<span id="cb5-39"><a href="#cb5-39" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> primal.x</span>
<span id="cb5-40"><a href="#cb5-40" aria-hidden="true" tabindex="-1"></a> y <span class="op">=</span> dual.x</span>
<span id="cb5-41"><a href="#cb5-41" aria-hidden="true" tabindex="-1"></a> z_primal <span class="op">=</span> <span class="op">-</span>primal.fun</span>
<span id="cb5-42"><a href="#cb5-42" aria-hidden="true" tabindex="-1"></a> z_dual <span class="op">=</span> dual.fun</span>
<span id="cb5-43"><a href="#cb5-43" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-44"><a href="#cb5-44" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-45"><a href="#cb5-45" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; PRIMALES PROBLEM&quot;</span>)</span>
<span id="cb5-46"><a href="#cb5-46" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-47"><a href="#cb5-47" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;x* = (</span><span class="sc">{</span>x[<span class="dv">0</span>]<span class="sc">:.4f}</span><span class="ss">, </span><span class="sc">{</span>x[<span class="dv">1</span>]<span class="sc">:.4f}</span><span class="ss">)&quot;</span>)</span>
<span id="cb5-48"><a href="#cb5-48" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Z* = </span><span class="sc">{</span>z_primal<span class="sc">:.4f}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-49"><a href="#cb5-49" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-50"><a href="#cb5-50" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-51"><a href="#cb5-51" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; DUALES PROBLEM&quot;</span>)</span>
<span id="cb5-52"><a href="#cb5-52" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-53"><a href="#cb5-53" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;y* = (</span><span class="sc">{</span>y[<span class="dv">0</span>]<span class="sc">:.4f}</span><span class="ss">, </span><span class="sc">{</span>y[<span class="dv">1</span>]<span class="sc">:.4f}</span><span class="ss">, </span><span class="sc">{</span>y[<span class="dv">2</span>]<span class="sc">:.4f}</span><span class="ss">)&quot;</span>)</span>
<span id="cb5-54"><a href="#cb5-54" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;W* = </span><span class="sc">{</span>z_dual<span class="sc">:.4f}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-55"><a href="#cb5-55" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-56"><a href="#cb5-56" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-57"><a href="#cb5-57" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; STARKER DUALITAETSSATZ: c^T x* == b^T y* ?&quot;</span>)</span>
<span id="cb5-58"><a href="#cb5-58" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-59"><a href="#cb5-59" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Primal Z* = </span><span class="sc">{</span>z_primal<span class="sc">:.6f}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-60"><a href="#cb5-60" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Dual W* = </span><span class="sc">{</span>z_dual<span class="sc">:.6f}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-61"><a href="#cb5-61" aria-hidden="true" tabindex="-1"></a> differenz <span class="op">=</span> <span class="bu">abs</span>(z_primal <span class="op">-</span> z_dual)</span>
<span id="cb5-62"><a href="#cb5-62" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Differenz = </span><span class="sc">{</span>differenz<span class="sc">:.2e}</span><span class="ss"> -&gt; &quot;</span></span>
<span id="cb5-63"><a href="#cb5-63" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;BESTAETIGT&#39;</span> <span class="cf">if</span> differenz <span class="op">&lt;</span> <span class="fl">1e-6</span> <span class="cf">else</span> <span class="st">&#39;VERLETZT!&#39;</span><span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-64"><a href="#cb5-64" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-65"><a href="#cb5-65" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-66"><a href="#cb5-66" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; KOMPLEMENTAERER SCHLUPF: s_i * y_i == 0 fuer alle i ?&quot;</span>)</span>
<span id="cb5-67"><a href="#cb5-67" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-68"><a href="#cb5-68" aria-hidden="true" tabindex="-1"></a> schlupf <span class="op">=</span> B_PRIMAL <span class="op">-</span> A_PRIMAL <span class="op">@</span> x</span>
<span id="cb5-69"><a href="#cb5-69" aria-hidden="true" tabindex="-1"></a> ressourcen <span class="op">=</span> [<span class="st">&quot;vCPU (s1)&quot;</span>, <span class="st">&quot;RAM (s2)&quot;</span>, <span class="st">&quot;Marktlimit (s3)&quot;</span>]</span>
<span id="cb5-70"><a href="#cb5-70" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> name, s, yi <span class="kw">in</span> <span class="bu">zip</span>(ressourcen, schlupf, y):</span>
<span id="cb5-71"><a href="#cb5-71" aria-hidden="true" tabindex="-1"></a> produkt <span class="op">=</span> s <span class="op">*</span> yi</span>
<span id="cb5-72"><a href="#cb5-72" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>name<span class="sc">:&lt;16}</span><span class="ss"> Schlupf s=</span><span class="sc">{</span>s<span class="sc">:6.4f}</span><span class="ss"> Schattenpreis y=</span><span class="sc">{</span>yi<span class="sc">:6.4f}</span><span class="ss"> &quot;</span></span>
<span id="cb5-73"><a href="#cb5-73" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;s*y=</span><span class="sc">{</span>produkt<span class="sc">:.2e}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;OK&#39;</span> <span class="cf">if</span> <span class="bu">abs</span>(produkt) <span class="op">&lt;</span> <span class="fl">1e-6</span> <span class="cf">else</span> <span class="st">&#39;VERLETZT!&#39;</span><span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-74"><a href="#cb5-74" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-75"><a href="#cb5-75" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">Fazit: Das dual geloeste y* stimmt exakt mit den Schattenpreisen&quot;</span>)</span>
<span id="cb5-76"><a href="#cb5-76" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;überein, die die Simplex-Rechnung von Hand in der Z-Zeile&quot;</span>)</span>
<span id="cb5-77"><a href="#cb5-77" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;ablas - unabhaengig voneinander berechnet, identisches Ergebnis.&quot;</span>)</span>
<span id="cb5-78"><a href="#cb5-78" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==============================================================================
PRIMALES PROBLEM
==============================================================================
x* = (7.5000, 5.0000)
Z* = 2375.0000
==============================================================================
DUALES PROBLEM
==============================================================================
y* = (12.5000, 31.2500, 0.0000)
W* = 2375.0000
==============================================================================
STARKER DUALITAETSSATZ: c^T x* == b^T y* ?
==============================================================================
Primal Z* = 2375.000000
Dual W* = 2375.000000
Differenz = 4.55e-13 -&gt; BESTAETIGT
==============================================================================
KOMPLEMENTAERER SCHLUPF: s_i * y_i == 0 fuer alle i ?
==============================================================================
vCPU (s1) Schlupf s=0.0000 Schattenpreis y=12.5000 s*y=0.00e+00 OK
RAM (s2) Schlupf s=0.0000 Schattenpreis y=31.2500 s*y=0.00e+00 OK
Marktlimit (s3) Schlupf s=0.5000 Schattenpreis y=0.0000 s*y=0.00e+00 OK
Fazit: Das dual geloeste y* stimmt exakt mit den Schattenpreisen
überein, die die Simplex-Rechnung von Hand in der Z-Zeile
ablas - unabhaengig voneinander berechnet, identisches Ergebnis.
==============================================================================</code></pre>
<p>Die winzige Differenz (<code>4.55e-13</code>) ist reines Gleitkommarauschen — numerisch ist das exakte Gleichheit. Wichtiger als die Zahl selbst: <strong>Primal und Dual wissen nichts voneinander</strong>, wurden als zwei separate <code>linprog</code>-Aufrufe gelöst, und landen trotzdem exakt beim selben Zielwert. Das <em>ist</em> der starke Dualitätssatz, nicht nur eine Illustration davon.</p>
<h3 id="der-schattenpreis-als-ableitung">Der Schattenpreis als Ableitung</h3>
<p>Der Dualwert <span class="math inline">y_i^*</span> ist die <strong>Grenzproduktivität</strong> der Ressource <span class="math inline">i</span>:</p>
<p><span class="math display">
y_i^* = \frac{\partial Z^*}{\partial b_i}
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong> <strong>Ohne Formel gesagt:</strong> „Um wie viele Euro steigt mein optimaler Gewinn, wenn ich von Ressource <span class="math inline">i</span> <strong>eine Einheit mehr</strong> hätte?“</p>
<p>Im Beispiel: Ein zusätzliches GB RAM bringt 31,25 € zusätzlichen Tagesgewinn. Ein zusätzlicher vCPU bringt 12,50 €. Eine Lockerung des Arbitrage-Limits bringt <strong>nichts</strong>, weil es gar nicht bindet.</p>
</blockquote>
<p><strong>Satz vom komplementären Schlupf:</strong></p>
<p><span class="math display">
s_i^* \cdot y_i^* = 0 \qquad \text{für alle } i
</span></p>
<p>Also: <strong>Entweder</strong> ist die Ressource knapp (<span class="math inline">s_i = 0</span>) und kann einen positiven Preis haben, <strong>oder</strong> sie hat Reserven (<span class="math inline">s_i &gt; 0</span>) und ihr Preis ist zwingend null. Beides gleichzeitig geht nicht.</p>
<div class="card card-formel">
<blockquote>
<p><strong>🔤 Formel-Übersetzer: das Dualitätspaket auf einen Blick</strong></p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr class="header">
<th>Mathematik</th>
<th>Alltagssprache</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><span class="math inline">\max\ \mathbf{c}^\top\mathbf{x}</span> u. d. N. <span class="math inline">\mathbf{A}\mathbf{x} \le \mathbf{b}</span></td>
<td>„Wie viel produziere ich von jedem Produkt, damit der Ertrag maximal wird und kein Vorrat überzogen wird?“</td>
</tr>
<tr class="even">
<td><span class="math inline">\min\ \mathbf{b}^\top\mathbf{y}</span> u. d. N. <span class="math inline">\mathbf{A}^\top\mathbf{y} \ge \mathbf{c}</span></td>
<td>„Was ist jede Ressourcenstunde wert? So wenig wie möglich — aber jedes Produkt muss seinen Verkaufserlös durch die verbrauchten Ressourcen gedeckt sehen.“</td>
</tr>
<tr class="odd">
<td><span class="math inline">\mathbf{c}^\top\mathbf{x}^* = \mathbf{b}^\top\mathbf{y}^*</span></td>
<td>„Der erwirtschaftete Gewinn und der Wert des Maschinenparks sind dieselbe Zahl — von zwei Seiten betrachtet.“</td>
</tr>
<tr class="even">
<td><span class="math inline">y_i^* = \partial Z^* / \partial b_i</span></td>
<td>„Um so viel Euro steigt der Gewinn, wenn ich von Ressource <span class="math inline">i</span> eine Einheit mehr hätte.“</td>
</tr>
<tr class="odd">
<td><span class="math inline">s_i^* \cdot y_i^* = 0</span></td>
<td>„Was übrig ist, ist nichts wert. Was etwas wert ist, ist restlos verbraucht.“ Nie beides zugleich.</td>
</tr>
<tr class="even">
<td><span class="math inline">s_i^* &gt; 0 \Rightarrow y_i^* = 0</span></td>
<td>„Von dieser Ressource haben Sie zu viel — kaufen Sie davon nichts nach.“</td>
</tr>
<tr class="odd">
<td><span class="math inline">y_i^* &gt; 0 \Rightarrow s_i^* = 0</span></td>
<td>„Diese Ressource ist Ihr Engpass — hier lohnt die Überstunde.“</td>
</tr>
</tbody>
</table>
<p><strong>Das Ganze in einem Satz:</strong> <em>Jede Optimierung beantwortet gleichzeitig zwei Fragen — was zu tun ist, und was die Mittel wert sind, mit denen man es tut.</em></p>
</blockquote>
</div>
<blockquote>
<p><strong>🎯 Merksatz</strong> Eine Optimierung liefert nicht nur eine Lösung, sondern auch ihre <strong>Begründung</strong>: Die Schattenpreise sagen, welcher Engpass Sie ausbremst und wie viel seine Beseitigung wert ist. Das ist in Managementgesprächen oft wertvoller als die Lösung selbst.</p>
</blockquote>
<hr />
<h2 id="sec:lp-die-vorzeichenfalle-bei-schattenpreisen">5.7 Die Vorzeichenfalle bei Schattenpreisen</h2>
<p>Diese Falle sollten Sie sich merken, weil sie in jedem Projekt wieder auftaucht.</p>
<p><strong>Das Problem:</strong> Wir wollen <strong>maximieren</strong>, aber <strong>minimiert</strong>. Also negieren wir die Zielfunktion. Damit wird auch die Ableitung negiert — und die Schattenpreise kommen mit <strong>umgekehrtem Vorzeichen</strong> heraus.</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a>res <span class="op">=</span> linprog(c<span class="op">=-</span>gewinn, A_ub<span class="op">=</span>A, b_ub<span class="op">=</span>b, method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>res.ineqlin.marginals <span class="co"># &lt;= 0 für &quot;&lt;=&quot;-Bedingungen bei Minimierung</span></span></code></pre></div>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th></th>
<th>Wert aus <code>marginals</code></th>
<th>Wirtschaftliche Bedeutung</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Ressource mit Reserve</td>
<td><span class="math inline">0</span></td>
<td>Preis 0 ✓</td>
</tr>
<tr class="even">
<td>Engpassressource</td>
<td><strong>negativ</strong>, z. B. <span class="math inline">-33{,}33</span></td>
<td>Schattenpreis ist <span class="math inline">+33{,}33</span> €!</td>
</tr>
</tbody>
</table>
<p>Eine unbedachte Abfrage <code>if sp &gt; 0:</code> wird dadurch <strong>niemals</strong> wahr — ein Programm mit diesem Fehler empfiehlt selbst bei den knappsten Ressourcen „keine Zukäufe nötig“.</p>
<blockquote>
<p><strong>🎯 Die Regel</strong> <strong>Nach jeder Negation der Zielfunktion müssen auch die Dualwerte zurücknegiert werden.</strong> Und prüfen Sie es: Der Schattenpreis einer Engpassressource muss bei einer Maximierung <strong>positiv</strong> sein. Ist er negativ, haben Sie ein Vorzeichen vergessen.</p>
</blockquote>
<hr />
<h2 id="sec:lp-praxisfall-sensitivitaetsanalyse-mit-korrekten-schattenpreisen">5.8 Praxisfall: Sensitivitätsanalyse mit korrekten Schattenpreisen</h2>
<p><strong>Szenario.</strong> Ein Fertigungsbetrieb stellt drei Produkte her und will wissen:</p>
<ul>
<li>Lohnt sich der Zukauf zusätzlicher Prüfstunden für 18 €/h?</li>
<li>Welche Ressource limitiert den Gewinn am stärksten?</li>
</ul>
<div class="sourceCode" id="cb8"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Sensitivitaetsanalyse.py</span></span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel LP: Schattenpreis- und Sensitivitätsanalyse mit SciPy und HiGHS.</span></span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a><span class="co">Achtung: Ohne Vorzeichenumkehr der Dualwerte waere die Handlungsempfehlung</span></span>
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a><span class="co">strukturell immer &quot;kein Zukauf noetig&quot; - selbst bei harten Engpaessen. Dieses</span></span>
<span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a><span class="co">Programm zeigt die korrekte Vorzeichenbehandlung.</span></span>
<span id="cb8-10"><a href="#cb8-10" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb8-11"><a href="#cb8-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-12"><a href="#cb8-12" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb8-13"><a href="#cb8-13" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linprog</span>
<span id="cb8-14"><a href="#cb8-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-15"><a href="#cb8-15" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Modell: Maximiere Deckungsbeitrag aus 3 Produkten ---------------------</span></span>
<span id="cb8-16"><a href="#cb8-16" aria-hidden="true" tabindex="-1"></a><span class="co"># max 40*x1 + 30*x2 + 50*x3</span></span>
<span id="cb8-17"><a href="#cb8-17" aria-hidden="true" tabindex="-1"></a>DECKUNGSBEITRAG <span class="op">=</span> np.array([<span class="fl">40.0</span>, <span class="fl">30.0</span>, <span class="fl">50.0</span>])</span>
<span id="cb8-18"><a href="#cb8-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-19"><a href="#cb8-19" aria-hidden="true" tabindex="-1"></a><span class="co"># Ressourcenverbrauch je Produkt (Zeile = Ressource, Spalte = Produkt)</span></span>
<span id="cb8-20"><a href="#cb8-20" aria-hidden="true" tabindex="-1"></a>VERBRAUCH <span class="op">=</span> np.array([</span>
<span id="cb8-21"><a href="#cb8-21" aria-hidden="true" tabindex="-1"></a> [<span class="fl">2.0</span>, <span class="fl">1.0</span>, <span class="fl">3.0</span>], <span class="co"># Montagezeit</span></span>
<span id="cb8-22"><a href="#cb8-22" aria-hidden="true" tabindex="-1"></a> [<span class="fl">1.0</span>, <span class="fl">2.0</span>, <span class="fl">1.0</span>], <span class="co"># Lackierzeit</span></span>
<span id="cb8-23"><a href="#cb8-23" aria-hidden="true" tabindex="-1"></a> [<span class="fl">1.0</span>, <span class="fl">0.5</span>, <span class="fl">2.0</span>], <span class="co"># Qualitätsprüfung</span></span>
<span id="cb8-24"><a href="#cb8-24" aria-hidden="true" tabindex="-1"></a>])</span>
<span id="cb8-25"><a href="#cb8-25" aria-hidden="true" tabindex="-1"></a>KAPAZITAET <span class="op">=</span> np.array([<span class="fl">120.0</span>, <span class="fl">80.0</span>, <span class="fl">50.0</span>]) <span class="co"># Stunden</span></span>
<span id="cb8-26"><a href="#cb8-26" aria-hidden="true" tabindex="-1"></a>RESSOURCEN <span class="op">=</span> [<span class="st">&quot;Montage&quot;</span>, <span class="st">&quot;Lackieren&quot;</span>, <span class="st">&quot;Qualitätsprüfung&quot;</span>]</span>
<span id="cb8-27"><a href="#cb8-27" aria-hidden="true" tabindex="-1"></a>PRODUKTE <span class="op">=</span> [<span class="st">&quot;Produkt 1&quot;</span>, <span class="st">&quot;Produkt 2&quot;</span>, <span class="st">&quot;Produkt 3&quot;</span>]</span>
<span id="cb8-28"><a href="#cb8-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-29"><a href="#cb8-29" aria-hidden="true" tabindex="-1"></a>ANGEBOTSPREIS_PRUEFSTUNDE <span class="op">=</span> <span class="fl">18.0</span> <span class="co"># EUR/h - lohnt sich der Zukauf?</span></span>
<span id="cb8-30"><a href="#cb8-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-31"><a href="#cb8-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-32"><a href="#cb8-32" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> analysiere():</span>
<span id="cb8-33"><a href="#cb8-33" aria-hidden="true" tabindex="-1"></a> <span class="co"># linprog MINIMIERT -&gt; Zielfunktion negieren</span></span>
<span id="cb8-34"><a href="#cb8-34" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> linprog(c<span class="op">=-</span>DECKUNGSBEITRAG, A_ub<span class="op">=</span>VERBRAUCH, b_ub<span class="op">=</span>KAPAZITAET,</span>
<span id="cb8-35"><a href="#cb8-35" aria-hidden="true" tabindex="-1"></a> bounds<span class="op">=</span>[(<span class="dv">0</span>, <span class="va">None</span>)] <span class="op">*</span> <span class="bu">len</span>(DECKUNGSBEITRAG), method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb8-36"><a href="#cb8-36" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> ergebnis.success:</span>
<span id="cb8-37"><a href="#cb8-37" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">SystemExit</span>(<span class="ss">f&quot;Kein Optimum gefunden: </span><span class="sc">{</span>ergebnis<span class="sc">.</span>message<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb8-38"><a href="#cb8-38" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-39"><a href="#cb8-39" aria-hidden="true" tabindex="-1"></a> max_gewinn <span class="op">=</span> <span class="op">-</span>ergebnis.fun</span>
<span id="cb8-40"><a href="#cb8-40" aria-hidden="true" tabindex="-1"></a> mengen <span class="op">=</span> ergebnis.x</span>
<span id="cb8-41"><a href="#cb8-41" aria-hidden="true" tabindex="-1"></a> schlupf <span class="op">=</span> ergebnis.slack</span>
<span id="cb8-42"><a href="#cb8-42" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-43"><a href="#cb8-43" aria-hidden="true" tabindex="-1"></a> <span class="co"># ------------------------------------------------------------------</span></span>
<span id="cb8-44"><a href="#cb8-44" aria-hidden="true" tabindex="-1"></a> <span class="co"># DER ENTSCHEIDENDE PUNKT:</span></span>
<span id="cb8-45"><a href="#cb8-45" aria-hidden="true" tabindex="-1"></a> <span class="co"># Weil wir zur Maximierung negiert haben, sind die Dualwerte aus</span></span>
<span id="cb8-46"><a href="#cb8-46" aria-hidden="true" tabindex="-1"></a> <span class="co"># linprog fuer &quot;&lt;=&quot;-Bedingungen &lt;= 0. Zurueckdrehen!</span></span>
<span id="cb8-47"><a href="#cb8-47" aria-hidden="true" tabindex="-1"></a> <span class="co"># ------------------------------------------------------------------</span></span>
<span id="cb8-48"><a href="#cb8-48" aria-hidden="true" tabindex="-1"></a> schattenpreise <span class="op">=</span> <span class="op">-</span>ergebnis.ineqlin.marginals</span>
<span id="cb8-49"><a href="#cb8-49" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-50"><a href="#cb8-50" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb8-51"><a href="#cb8-51" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; PRIMALE UND DUALE ERGEBNISANALYSE (SENSITIVITAET)&quot;</span>)</span>
<span id="cb8-52"><a href="#cb8-52" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb8-53"><a href="#cb8-53" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Maximaler Deckungsbeitrag: </span><span class="sc">{</span>max_gewinn<span class="sc">:,.2f}</span><span class="ss"> EUR</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb8-54"><a href="#cb8-54" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-55"><a href="#cb8-55" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;--- Primalloesung: optimale Produktionsmengen ---&quot;</span>)</span>
<span id="cb8-56"><a href="#cb8-56" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> name, menge <span class="kw">in</span> <span class="bu">zip</span>(PRODUKTE, mengen):</span>
<span id="cb8-57"><a href="#cb8-57" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; * </span><span class="sc">{</span>name<span class="sc">}</span><span class="ss">: </span><span class="sc">{</span>menge<span class="sc">:8.2f}</span><span class="ss"> Stueck&quot;</span>)</span>
<span id="cb8-58"><a href="#cb8-58" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-59"><a href="#cb8-59" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">--- Duale Analyse: Schattenpreise und Auslastung ---&quot;</span>)</span>
<span id="cb8-60"><a href="#cb8-60" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i, name <span class="kw">in</span> <span class="bu">enumerate</span>(RESSOURCEN):</span>
<span id="cb8-61"><a href="#cb8-61" aria-hidden="true" tabindex="-1"></a> kapazitaet <span class="op">=</span> KAPAZITAET[i]</span>
<span id="cb8-62"><a href="#cb8-62" aria-hidden="true" tabindex="-1"></a> genutzt <span class="op">=</span> kapazitaet <span class="op">-</span> schlupf[i]</span>
<span id="cb8-63"><a href="#cb8-63" aria-hidden="true" tabindex="-1"></a> auslastung <span class="op">=</span> genutzt <span class="op">/</span> kapazitaet <span class="op">*</span> <span class="dv">100</span></span>
<span id="cb8-64"><a href="#cb8-64" aria-hidden="true" tabindex="-1"></a> preis <span class="op">=</span> schattenpreise[i]</span>
<span id="cb8-65"><a href="#cb8-65" aria-hidden="true" tabindex="-1"></a> bindend <span class="op">=</span> <span class="bu">abs</span>(schlupf[i]) <span class="op">&lt;</span> <span class="fl">1e-9</span></span>
<span id="cb8-66"><a href="#cb8-66" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-67"><a href="#cb8-67" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss">Ressource &#39;</span><span class="sc">{</span>name<span class="sc">}</span><span class="ss">&#39;:&quot;</span>)</span>
<span id="cb8-68"><a href="#cb8-68" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Auslastung: </span><span class="sc">{</span>genutzt<span class="sc">:6.1f}</span><span class="ss"> / </span><span class="sc">{</span>kapazitaet<span class="sc">:6.1f}</span><span class="ss"> h (</span><span class="sc">{</span>auslastung<span class="sc">:5.1f}</span><span class="ss"> %)&quot;</span></span>
<span id="cb8-69"><a href="#cb8-69" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot; -&gt; </span><span class="sc">{</span><span class="st">&#39;ENGPASS&#39;</span> <span class="cf">if</span> bindend <span class="cf">else</span> <span class="st">&#39;Reserve: </span><span class="sc">%.1f</span><span class="st"> h&#39;</span> <span class="op">%</span> schlupf[i]<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb8-70"><a href="#cb8-70" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Schattenpreis: </span><span class="sc">{</span>preis<span class="sc">:6.2f}</span><span class="ss"> EUR je zusaetzlicher Stunde&quot;</span>)</span>
<span id="cb8-71"><a href="#cb8-71" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-72"><a href="#cb8-72" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> preis <span class="op">&gt;</span> <span class="fl">1e-9</span>:</span>
<span id="cb8-73"><a href="#cb8-73" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; &gt;&gt; Zusaetzliche Stunden lohnen sich bis zu einem Preis von &quot;</span></span>
<span id="cb8-74"><a href="#cb8-74" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>preis<span class="sc">:.2f}</span><span class="ss"> EUR/h.&quot;</span>)</span>
<span id="cb8-75"><a href="#cb8-75" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb8-76"><a href="#cb8-76" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; &gt;&gt; Kein Zukauf noetig - die Kapazitaet ist nicht erschoepft.&quot;</span>)</span>
<span id="cb8-77"><a href="#cb8-77" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-78"><a href="#cb8-78" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Konkrete Kaufentscheidung ---------------------------------------</span></span>
<span id="cb8-79"><a href="#cb8-79" aria-hidden="true" tabindex="-1"></a> preis_pruefung <span class="op">=</span> schattenpreise[RESSOURCEN.index(<span class="st">&quot;Qualitätsprüfung&quot;</span>)]</span>
<span id="cb8-80"><a href="#cb8-80" aria-hidden="true" tabindex="-1"></a> marge <span class="op">=</span> preis_pruefung <span class="op">-</span> ANGEBOTSPREIS_PRUEFSTUNDE</span>
<span id="cb8-81"><a href="#cb8-81" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb8-82"><a href="#cb8-82" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;ENTSCHEIDUNG: Pruefstunden werden fuer &quot;</span></span>
<span id="cb8-83"><a href="#cb8-83" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>ANGEBOTSPREIS_PRUEFSTUNDE<span class="sc">:.2f}</span><span class="ss"> EUR/h angeboten.&quot;</span>)</span>
<span id="cb8-84"><a href="#cb8-84" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Schattenpreis: </span><span class="sc">{</span>preis_pruefung<span class="sc">:6.2f}</span><span class="ss"> EUR/h&quot;</span>)</span>
<span id="cb8-85"><a href="#cb8-85" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Angebotspreis: </span><span class="sc">{</span>ANGEBOTSPREIS_PRUEFSTUNDE<span class="sc">:6.2f}</span><span class="ss"> EUR/h&quot;</span>)</span>
<span id="cb8-86"><a href="#cb8-86" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Marge: </span><span class="sc">{</span>marge<span class="sc">:+6.2f}</span><span class="ss"> EUR je zugekaufter Stunde&quot;</span>)</span>
<span id="cb8-87"><a href="#cb8-87" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; &gt;&gt; </span><span class="sc">{</span><span class="st">&#39;ZUKAUFEN&#39;</span> <span class="cf">if</span> marge <span class="op">&gt;</span> <span class="dv">0</span> <span class="cf">else</span> <span class="st">&#39;NICHT ZUKAUFEN&#39;</span><span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb8-88"><a href="#cb8-88" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-89"><a href="#cb8-89" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Numerische Gegenprobe: Kapazitaet wirklich um 1 erhoehen --------</span></span>
<span id="cb8-90"><a href="#cb8-90" aria-hidden="true" tabindex="-1"></a> kapazitaet_plus <span class="op">=</span> KAPAZITAET.copy()</span>
<span id="cb8-91"><a href="#cb8-91" aria-hidden="true" tabindex="-1"></a> kapazitaet_plus[RESSOURCEN.index(<span class="st">&quot;Qualitätsprüfung&quot;</span>)] <span class="op">+=</span> <span class="fl">1.0</span></span>
<span id="cb8-92"><a href="#cb8-92" aria-hidden="true" tabindex="-1"></a> gegenprobe <span class="op">=</span> linprog(c<span class="op">=-</span>DECKUNGSBEITRAG, A_ub<span class="op">=</span>VERBRAUCH, b_ub<span class="op">=</span>kapazitaet_plus,</span>
<span id="cb8-93"><a href="#cb8-93" aria-hidden="true" tabindex="-1"></a> bounds<span class="op">=</span>[(<span class="dv">0</span>, <span class="va">None</span>)] <span class="op">*</span> <span class="dv">3</span>, method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb8-94"><a href="#cb8-94" aria-hidden="true" tabindex="-1"></a> tatsaechlicher_zuwachs <span class="op">=</span> <span class="op">-</span>gegenprobe.fun <span class="op">-</span> max_gewinn</span>
<span id="cb8-95"><a href="#cb8-95" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">--- Gegenprobe: Modell mit +1 Pruefstunde neu geloest ---&quot;</span>)</span>
<span id="cb8-96"><a href="#cb8-96" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Vorhergesagt (Schattenpreis): </span><span class="sc">{</span>preis_pruefung<span class="sc">:8.4f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb8-97"><a href="#cb8-97" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Tatsaechlich gemessen: </span><span class="sc">{</span>tatsaechlicher_zuwachs<span class="sc">:8.4f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb8-98"><a href="#cb8-98" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> <span class="bu">abs</span>(tatsaechlicher_zuwachs <span class="op">-</span> preis_pruefung) <span class="op">&lt;</span> <span class="fl">1e-6</span>, <span class="op">\</span></span>
<span id="cb8-99"><a href="#cb8-99" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Schattenpreis stimmt nicht mit der Messung ueberein!&quot;</span></span>
<span id="cb8-100"><a href="#cb8-100" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; -&gt; Der Schattenpreis ist bestaetigt.&quot;</span>)</span>
<span id="cb8-101"><a href="#cb8-101" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-102"><a href="#cb8-102" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Komplementaerer Schlupf pruefen --------------------------------</span></span>
<span id="cb8-103"><a href="#cb8-103" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> s, y <span class="kw">in</span> <span class="bu">zip</span>(schlupf, schattenpreise):</span>
<span id="cb8-104"><a href="#cb8-104" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> <span class="bu">abs</span>(s <span class="op">*</span> y) <span class="op">&lt;</span> <span class="fl">1e-6</span>, <span class="st">&quot;Komplementaerer Schlupf verletzt!&quot;</span></span>
<span id="cb8-105"><a href="#cb8-105" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">Pruefung: komplementaerer Schlupf fuer alle Ressourcen erfuellt.&quot;</span>)</span>
<span id="cb8-106"><a href="#cb8-106" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb8-107"><a href="#cb8-107" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-108"><a href="#cb8-108" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-109"><a href="#cb8-109" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb8-110"><a href="#cb8-110" aria-hidden="true" tabindex="-1"></a> analysiere()</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==========================================================================
PRIMALE UND DUALE ERGEBNISANALYSE (SENSITIVITAET)
==========================================================================
Maximaler Deckungsbeitrag: 2,200.00 EUR
--- Primalloesung: optimale Produktionsmengen ---
* Produkt 1: 40.00 Stueck
* Produkt 2: 20.00 Stueck
* Produkt 3: 0.00 Stueck
--- Duale Analyse: Schattenpreise und Auslastung ---
Ressource &#39;Montage&#39;:
Auslastung: 100.0 / 120.0 h ( 83.3 %) -&gt; Reserve: 20.0 h
Schattenpreis: 0.00 EUR je zusaetzlicher Stunde
&gt;&gt; Kein Zukauf noetig - die Kapazitaet ist nicht erschoepft.
Ressource &#39;Lackieren&#39;:
Auslastung: 80.0 / 80.0 h (100.0 %) -&gt; ENGPASS
Schattenpreis: 6.67 EUR je zusaetzlicher Stunde
&gt;&gt; Zusaetzliche Stunden lohnen sich bis zu einem Preis von 6.67 EUR/h.
Ressource &#39;Qualitätsprüfung&#39;:
Auslastung: 50.0 / 50.0 h (100.0 %) -&gt; ENGPASS
Schattenpreis: 33.33 EUR je zusaetzlicher Stunde
&gt;&gt; Zusaetzliche Stunden lohnen sich bis zu einem Preis von 33.33 EUR/h.
--------------------------------------------------------------------------
ENTSCHEIDUNG: Pruefstunden werden fuer 18.00 EUR/h angeboten.
Schattenpreis: 33.33 EUR/h
Angebotspreis: 18.00 EUR/h
Marge: +15.33 EUR je zugekaufter Stunde
&gt;&gt; ZUKAUFEN
--- Gegenprobe: Modell mit +1 Pruefstunde neu geloest ---
Vorhergesagt (Schattenpreis): 33.3333 EUR
Tatsaechlich gemessen: 33.3333 EUR
-&gt; Der Schattenpreis ist bestaetigt.
Pruefung: komplementaerer Schlupf fuer alle Ressourcen erfuellt.
==========================================================================</code></pre>
<p>Der Zukauf für 18 €/h ist hochprofitabel: Jede zusätzliche Prüfstunde bringt 33,33 € Deckungsbeitrag, also 15,33 € Reingewinn.</p>
<blockquote>
<p><strong>💻 Code-Durchgang: die drei Sicherungen</strong></p>
<p>Dieses Programm enthält drei Prüfungen, die genau diese Vorzeichenfalle zuverlässig aufdecken würden:</p>
<ol type="1">
<li><strong><code>schattenpreise = -ergebnis.ineqlin.marginals</code></strong> — die eigentliche Korrektur.</li>
<li><strong>Numerische Gegenprobe:</strong> Das Modell wird mit <span class="math inline">b_3 + 1</span> neu gelöst; der gemessene Zuwachs muss dem Schattenpreis entsprechen. Diese Prüfung ist unabhängig von jeder Vorzeichenkonvention und deshalb der zuverlässigste Test überhaupt.</li>
<li><strong><a href="anhang-glossar.md#gloss:komplementaerer-schlupf" class="glossar-link">Komplementärer Schlupf</a>:</strong> <span class="math inline">s_i \cdot y_i = 0</span> muss für alle <span class="math inline">i</span> gelten.</li>
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>
2026-09-08 01:20:09 +02:00
</ol>
<p><strong>Übernehmen Sie dieses Muster in eigene Projekte.</strong> Der Aufwand ist eine Handvoll Zeilen, der Nutzen ist die Gewissheit, dass Ihre Handlungsempfehlung nicht das Gegenteil des Richtigen sagt.</p>
</blockquote>
<blockquote>
<p><strong>⚠️ Grenzen der Schattenpreis-Aussage</strong></p>
Achter Fund (Uebung-Verweise) und Phase 8.3: Dockerfile Zwei Dinge in einem Commit, weil beide den Plan abschliessen. ACHTER FUND: sieben Saetze der Bauart "Uebung 8.5 laesst Sie diese Balance untersuchen" - und sechs davon standen in alter Zaehlung. "8" war in Version 03 das QP/NLP-Kapitel, heute ist es Kapitel 11. Dieselbe Familie wie die Denkfehler-Verweise aus 6.1a, nur mit einem Wort, das keine der bestehenden Pruefungen kannte. Aufgaben haben kein eigenes Label, ein {ref:} auf eine einzelne Aufgabe ist also nicht moeglich. Verwiesen wird stattdessen auf Abschnitt plus Aufgabentitel - und der ist stabil. Jedes Ziel wurde einzeln ueber die alte Zaehlung bestimmt und am Zusammenhang geprueft. Ein Fall war knifflig: "Uebung 6.7 (Wochendienstplan)" meinte die siebte CP-SAT-Aufgabe der alten Zaehlung, also "Eigener Dienstplan" - die heute an achter Stelle steht, weil in Phase 6.3 eine Aufgabe davor eingefuegt wurde. Wer nur die Kapitelnummer angepasst haette, waere bei der falschen Aufgabe gelandet. --check kennt jetzt auch "Uebung"/"Übung". Gegengetestet. PHASE 8.3: Dockerfile, zweistufig. Die erste Stufe uebersetzt die Abhaengigkeiten in eine virtuelle Umgebung und braucht dafuer einen Compiler, die zweite kopiert nur /opt/venv. Installiert werden die Gruppen finance, large-scale, api und dev aus pyproject.toml; figures fehlt bewusst, weil es zusaetzlich Graphviz verlangt. Es wurde nicht behauptet, sondern gebaut. Ergebnis: 1,31 GB, und darin der Installationstest mit allen drei Solver-Funktionstests bestanden, die 33 pytest-Tests bestanden und alle 69 netzfreien Programme fehlerfrei - einschliesslich der drei aus 8.2, deren spawn-Isolation im Container ebenso traegt wie ausserhalb. Zwei Dinge, die der Bau gelehrt hat: libgomp1 fehlt im python:3.12-slim-Image und wird von OR-Tools und HiGHS zur Laufzeit gebraucht (sonst "libgomp.so.1: cannot open shared object file"). Und ein eigener Fehler: USER kurs stand vor dem mkdir /buch/output, /buch gehoert root, der Bau brach in der letzten Zeile ab. Beides steht jetzt als Kommentar im Dockerfile. Das Image fuehrt die Programme aus und baut das Buch nicht. Ein .dockerignore haelt Website, PDF und Notebooks aus dem Build-Kontext. Und es enthaelt ortools UND highspy, obwohl sie sich nicht gemeinsam importieren lassen - der Konflikt wird zur Laufzeit durch getrennte Prozesse geloest, nicht durch Weglassen. Damit ist Phase 8 abgeschlossen und der Plan abgearbeitet. Stand: 825 Querverweise, 76 Programme, 33 pytest-Tests, PDF 760 Seiten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 13:18:05 +02:00
<p>Der Schattenpreis gilt nur <strong>lokal</strong>, in einem begrenzten Intervall um die aktuelle Kapazität. Kauft man 200 Prüfstunden zu, wird irgendwann eine andere Ressource zum Engpass, und der Schattenpreis springt auf einen neuen Wert (oder auf null). Wer große Kapazitätsänderungen bewerten will, muss das Modell <strong>neu rechnen</strong> — nicht linear hochrechnen. Die Aufgabe <em>Gültigkeitsbereich des Schattenpreises</em> (<a href="#sec:lp-uebungsaufgaben">Abschnitt 5.10</a>) macht diesen Effekt sichtbar.</p>
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>
2026-09-08 01:20:09 +02:00
</blockquote>
<hr />
<h2 id="sec:lp-entartung">5.9 Wann Schattenpreise lügen: Entartung und Toleranzen</h2>
<p>Der vorige Abschnitt hat gezeigt, wie wertvoll Schattenpreise sind. Dieser zeigt die zwei Fälle, in denen sie eine Entscheidung <strong>nicht</strong> tragen — und wie man beide erkennt, bevor jemand auf ihrer Grundlage eine Maschine kauft.</p>
Grafik-Upgrade Stufe 1: elf neue Diagramme Grafik_Upgrade.md verlangt "37 didaktisch wertvolle Visualisierungen". Es sind 19: Die Abschnitte "Modul 1-4" wiederholen die Grafiken 1-18 unter den Nummern 19-36, nur Grafik 37 ist zusaetzlich. Belegt an den Skriptnamen - 38 Nennungen, 19 verschiedene Dateien, erzeuge_kombinatorik_wand.py steht dreimal darin. Von den 19 betreffen neun bereits vorhandene Diagramme. Umgesetzt sind die zehn wirklich neuen plus eine elfte, die aus einem Sachfehler des Auftrags entstand: Grafik 14 sollte in Abschnitt 12.3 stehen, nannte aber Zahlen aus Kapitel 17. Beide Stellen haben jetzt ihre eigene Grafik mit ihren eigenen Zahlen. Vier Vorgaben des Auftrags wurden bewusst nicht befolgt: PNG bei 300 dpi (das Buch kennt nur SVG - die 33 verwaisten PNGs waren gerade erst geloescht worden), Matplotlib-Standardfarben (die Buchpalette steht in 33 Diagrammen), harte Kapitelnummern im Fliesstext ({ref:sec:...} stattdessen) und die fehlende Byte-Reproduzierbarkeit. Der eigentliche Ertrag waren die Selbsttests. Jeder Generator prueft seine Zahlen gegen das, was im Kapitel steht, und bricht bei Abweichung ab. Vier haben angeschlagen: * Kombinatorik-Wand: 25! ergibt 491,5 Mio. Jahre, die Tabelle nennt 490 Mio. - Rundung auf zwei signifikante Stellen, nicht Fehler des Buchs. * Laufzeit-Diagramm: Die abgedruckte Prozentspalte laesst sich aus den abgedruckten Zeiten nicht exakt nachrechnen (0,001/0,008 ergibt 12 %, gedruckt sind 14 %) - die Zeiten sind gerundet, die Prozente nicht. * Predict-then-Optimize: geratene Datenaufteilung ergab 197,35 statt 200,95 MSE. Mit TRAINING aus dem Buchprogramm stimmen alle vier Kostenwerte. * Almgren-Chriss: ein echter Fehler im Buchprogramm. DER FUND: analytische_loesung() minimiert die eigene Kostenfunktion nicht. Ihr Pfad kostet 10.860,40 EUR, der DP-Pfad 10.266,24 EUR - ein Gitterverfahren kann das kontinuierliche Optimum aber nicht unterbieten. periodenkosten() rechnet das Risiko mit lambda/2, die geschlossene Formel setzt kappa~^2 = lambda*sigma^2*P0^2/eta ohne dieses Halbe. Mit lambda/2 liefert die Formel 10.264,85 EUR und denselben Pfad wie die DP, knapp UNTER der Gitterloesung. Die Grafik zeigt bis zur Entscheidung darueber den DP-Pfad; er ist der, den das Kapitel abdruckt. Nachweise: Alle elf Diagramme sind ueber zwei Laeufe byte-identisch. Kein vorhandenes Diagramm wurde veraendert. Die Lastabwurf-Grafik reproduziert die Befundtabelle exakt (874.870/2.504.154/28 von 40 gegen 351.356/415.924/0). Zwei Abbildungen landeten beim automatischen Einfuegen im Codeblock (ihre Anker standen in abgedruckten Ausgaben) und wurden dahinter verschoben; eine Pruefung ueber alle elf zeigt null Fehlplatzierungen. Neu: bilder_04/stil_04.py buendelt Palette, rcParams und speichere(). networkx wurde entgegen der Planung nicht gebraucht - pyproject.toml bleibt unveraendert. PDF von 781 auf 788 Seiten, 44 Diagramme, 27 Generatoren. Veroeffentlicht: 25 Dateien. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 01:11:33 +02:00
<figure>
2026-09-09 07:37:07 +02:00
<img src="bilder_04/kap_lp_entartung.svg" alt="Abb. 5.4: Links laufen drei Geraden durch dieselbe Ecke — eine mehr, als zwei Dimensionen brauchen. Rechts die Folge: Der Schattenpreis ist keine Zahl mehr, sondern eine Spanne. Erzeugt von bilder_04/erzeuge_entartung_polyeder.py." />
<figcaption aria-hidden="true">Abb. 5.4: Links laufen drei Geraden durch dieselbe Ecke — eine mehr, als zwei Dimensionen brauchen. Rechts die Folge: Der Schattenpreis ist keine Zahl mehr, sondern eine Spanne. Erzeugt von <code>bilder_04/erzeuge_entartung_polyeder.py</code>.</figcaption>
Grafik-Upgrade Stufe 1: elf neue Diagramme Grafik_Upgrade.md verlangt "37 didaktisch wertvolle Visualisierungen". Es sind 19: Die Abschnitte "Modul 1-4" wiederholen die Grafiken 1-18 unter den Nummern 19-36, nur Grafik 37 ist zusaetzlich. Belegt an den Skriptnamen - 38 Nennungen, 19 verschiedene Dateien, erzeuge_kombinatorik_wand.py steht dreimal darin. Von den 19 betreffen neun bereits vorhandene Diagramme. Umgesetzt sind die zehn wirklich neuen plus eine elfte, die aus einem Sachfehler des Auftrags entstand: Grafik 14 sollte in Abschnitt 12.3 stehen, nannte aber Zahlen aus Kapitel 17. Beide Stellen haben jetzt ihre eigene Grafik mit ihren eigenen Zahlen. Vier Vorgaben des Auftrags wurden bewusst nicht befolgt: PNG bei 300 dpi (das Buch kennt nur SVG - die 33 verwaisten PNGs waren gerade erst geloescht worden), Matplotlib-Standardfarben (die Buchpalette steht in 33 Diagrammen), harte Kapitelnummern im Fliesstext ({ref:sec:...} stattdessen) und die fehlende Byte-Reproduzierbarkeit. Der eigentliche Ertrag waren die Selbsttests. Jeder Generator prueft seine Zahlen gegen das, was im Kapitel steht, und bricht bei Abweichung ab. Vier haben angeschlagen: * Kombinatorik-Wand: 25! ergibt 491,5 Mio. Jahre, die Tabelle nennt 490 Mio. - Rundung auf zwei signifikante Stellen, nicht Fehler des Buchs. * Laufzeit-Diagramm: Die abgedruckte Prozentspalte laesst sich aus den abgedruckten Zeiten nicht exakt nachrechnen (0,001/0,008 ergibt 12 %, gedruckt sind 14 %) - die Zeiten sind gerundet, die Prozente nicht. * Predict-then-Optimize: geratene Datenaufteilung ergab 197,35 statt 200,95 MSE. Mit TRAINING aus dem Buchprogramm stimmen alle vier Kostenwerte. * Almgren-Chriss: ein echter Fehler im Buchprogramm. DER FUND: analytische_loesung() minimiert die eigene Kostenfunktion nicht. Ihr Pfad kostet 10.860,40 EUR, der DP-Pfad 10.266,24 EUR - ein Gitterverfahren kann das kontinuierliche Optimum aber nicht unterbieten. periodenkosten() rechnet das Risiko mit lambda/2, die geschlossene Formel setzt kappa~^2 = lambda*sigma^2*P0^2/eta ohne dieses Halbe. Mit lambda/2 liefert die Formel 10.264,85 EUR und denselben Pfad wie die DP, knapp UNTER der Gitterloesung. Die Grafik zeigt bis zur Entscheidung darueber den DP-Pfad; er ist der, den das Kapitel abdruckt. Nachweise: Alle elf Diagramme sind ueber zwei Laeufe byte-identisch. Kein vorhandenes Diagramm wurde veraendert. Die Lastabwurf-Grafik reproduziert die Befundtabelle exakt (874.870/2.504.154/28 von 40 gegen 351.356/415.924/0). Zwei Abbildungen landeten beim automatischen Einfuegen im Codeblock (ihre Anker standen in abgedruckten Ausgaben) und wurden dahinter verschoben; eine Pruefung ueber alle elf zeigt null Fehlplatzierungen. Neu: bilder_04/stil_04.py buendelt Palette, rcParams und speichere(). networkx wurde entgegen der Planung nicht gebraucht - pyproject.toml bleibt unveraendert. PDF von 781 auf 788 Seiten, 44 Diagramme, 27 Generatoren. Veroeffentlicht: 25 Dateien. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 01:11:33 +02:00
</figure>
<p><strong>Was Sie in der Abbildung sehen.</strong> Jeder Wert innerhalb der farbigen Balken erfüllt die Optimalitätsbedingungen. Zwei Solver dürfen also verschiedene Schattenpreise melden und haben beide recht — die Frage „was ist eine Stunde wert?” hat hier schlicht keine eindeutige Antwort.</p>
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>
2026-09-08 01:20:09 +02:00
<h3 id="fall-1-entartung">Fall 1: Entartung</h3>
<p>Erinnern Sie sich an <a href="fundament.html#sec:fundament-der-zulaessige-loesungsraum-und-das-polyeder">Abschnitt 2.4</a>: In zwei Dimensionen legen <strong>zwei</strong> sich schneidende Geraden eine Ecke fest, in drei Dimensionen drei Ebenen, allgemein <span class="math inline">n</span> Restriktionen bei <span class="math inline">n</span> Variablen. Was passiert, wenn in einer Ecke <strong>mehr</strong> Nebenbedingungen aktiv sind als das Problem Variablen hat?</p>
<p>Geometrisch: Drei Geraden laufen zufällig durch denselben Punkt. Die Ecke ist immer noch eine Ecke — aber sie ist <strong>überbestimmt</strong>. Man nennt das <strong>Entartung</strong> (<em>Degeneriertheit</em>), und sie ist in der Praxis nicht die Ausnahme, sondern die Regel: Sie entsteht überall dort, wo Kapazitäten aus derselben Planung stammen und deshalb glatt aufeinander passen — Schichtlängen, Chargengrößen, runde Vertragsmengen.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Bei einer entarteten Ecke ist der Plan eindeutig, der <strong>Schattenpreis aber nicht</strong>. Es gibt dann viele gleichermaßen korrekte Dualvektoren, und welchen Sie sehen, hängt davon ab, welchen Algorithmus der Solver zufällig benutzt hat.</p>
</blockquote>
<p>Das ist keine theoretische Sorge. Dasselbe Modell, zwei Verfahren desselben Solvers:</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Verfahren</th>
<th>Schattenpreise</th>
<th>Was das Management daraus liest</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><a href="anhang-glossar.md#gloss:dual-simplex" class="glossar-link">Dual Simplex</a></td>
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>
2026-09-08 01:20:09 +02:00
<td><span class="math inline">(0{,}33;\ 0{,}33;\ 0)</span></td>
<td>„Fräse und Schleiferei sind die Engpässe, die Prüfung ist wertlos.“</td>
</tr>
<tr class="even">
<td>Innere-Punkte-Verfahren</td>
<td><span class="math inline">(0;\ 0;\ 1{,}00)</span></td>
<td>„Die Prüfung ist der Engpass, Fräse und Schleiferei sind wertlos.“</td>
</tr>
</tbody>
</table>
<p><strong>Beide Zeilen sind mathematisch korrekt.</strong> Sie widersprechen sich trotzdem vollständig.</p>
<h3 id="fall-2-toleranzen">Fall 2: Toleranzen</h3>
<p>Der zweite Fall ist unscheinbarer und deshalb häufiger. Wie stellt man fest, ob eine Ressource ausgelastet ist? Naheliegend wäre <code>if schlupf == 0</code>. Genau das ist falsch.</p>
<p>Solver rechnen mit endlicher Genauigkeit und brechen ab, sobald ihre eigene Toleranz erreicht ist (<a href="fundament.html#sec:fundament-kondition">Abschnitt 2.7</a>). Ein voll ausgelasteter Engpass meldet dann einen Schlupf von <span class="math inline">-4{,}44 \cdot 10^{-16}</span> statt exakt <span class="math inline">0</span> — rechnerisch null, aber eben nicht <code>== 0.0</code>. Wer exakt vergleicht, übersieht ausgerechnet die Engpässe, die er sucht.</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Toleranz</th>
<th>Was sie steuert</th>
<th>Typischer Standardwert</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Primale Zulässigkeit</strong></td>
<td>Wie weit darf <span class="math inline">\mathbf{A}\mathbf{x}</span> die Schranke <span class="math inline">\mathbf{b}</span> überschreiten?</td>
<td><span class="math inline">10^{-7}</span> (HiGHS), <span class="math inline">10^{-6}</span> (<a href="anhang-glossar.md#gloss:constraint-programming-cp" class="glossar-link">CP</a>-<a href="anhang-glossar.md#gloss:sat-boolean-satisfiability" class="glossar-link">SAT</a>)</td>
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>
2026-09-08 01:20:09 +02:00
</tr>
<tr class="even">
<td><strong>Duale Zulässigkeit</strong> (Optimalität)</td>
<td>Wie weit dürfen die reduzierten Kosten das falsche Vorzeichen haben?</td>
<td><span class="math inline">10^{-7}</span></td>
</tr>
<tr class="odd">
<td><strong>Ganzzahligkeit</strong></td>
<td>Wie weit darf eine Ganzzahlvariable von der nächsten ganzen Zahl abweichen?</td>
<td><span class="math inline">10^{-6}</span> (<a href="milp.html#kap-milp">Kapitel 6</a>)</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>⚠️ Toleranzen kleiner zu drehen ist selten die Lösung.</strong> Wer <code>1e-12</code> verlangt, bekommt meist keinen genaueren Solver, sondern einen, der <code>INFEASIBLE</code> meldet oder nicht konvergiert. Die Ursache steckt fast immer in der Skalierung des Modells, nicht in der Einstellung — deshalb steht der Konditionsabschnitt in <a href="fundament.html#kap-fundament">Kapitel 2</a> vor diesem hier.</p>
</blockquote>
<h3 id="die-ehrliche-auskunft-schattenpreis-spannen">Die ehrliche Auskunft: Schattenpreis-Spannen</h3>
<p>Was tut man also, wenn das Modell entartet ist? Man meldet keinen Einzelwert, sondern die <strong>Spanne</strong>. Die Menge aller optimalen Dualvektoren ist selbst ein Polyeder:</p>
<p><span class="math display">
\mathbf{A}^\top\mathbf{y} \ge \mathbf{c},\qquad \mathbf{y} \ge \mathbf{0},
\qquad \mathbf{b}^\top\mathbf{y} = Z^*
</span></p>
<div class="card card-formel">
<blockquote>
<p><strong>🔤 Formel-Übersetzer</strong></p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr class="header">
<th>Mathematik</th>
<th>Alltagssprache</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><span class="math inline">\mathbf{A}^\top\mathbf{y} \ge \mathbf{c}</span></td>
<td>„Die Preise müssen jedes Produkt decken“ — Dualzulässigkeit.</td>
</tr>
<tr class="even">
<td><span class="math inline">\mathbf{b}^\top\mathbf{y} = Z^*</span></td>
<td>„Und der Gesamtwert muss genau dem erreichten Gewinn entsprechen“ — starker Dualitätssatz als Gleichung.</td>
</tr>
<tr class="odd">
<td><span class="math inline">\min / \max\ y_i</span> darauf</td>
<td>„Wie klein und wie groß darf der Preis von Ressource <span class="math inline">i</span> sein, ohne diese Regeln zu verletzen?“</td>
</tr>
</tbody>
</table>
<p>Zwei zusätzliche LPs je Ressource — und Sie haben statt einer Zufallszahl ein belastbares Intervall.</p>
</blockquote>
</div>
<div class="sourceCode" id="cb10"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Toleranzen_und_Entartung.py</span></span>
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel LP: Zwei Faelle, in denen man Schattenpreisen NICHT trauen darf.</span></span>
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a><span class="co"> 1. Entartung (Degeneriertheit): Mehr Nebenbedingungen sind aktiv, als das</span></span>
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a><span class="co"> Problem Variablen hat. Dann ist der Schattenpreis nicht eindeutig - zwei</span></span>
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a><span class="co"> korrekte Solver liefern voellig verschiedene Werte fuer dasselbe Optimum.</span></span>
<span id="cb10-10"><a href="#cb10-10" aria-hidden="true" tabindex="-1"></a><span class="co"> 2. Toleranzen: &quot;ausgelastet&quot; heisst nie &#39;schlupf == 0&#39;, sondern immer</span></span>
<span id="cb10-11"><a href="#cb10-11" aria-hidden="true" tabindex="-1"></a><span class="co"> &#39;schlupf &lt; toleranz&#39;. Wer auf exakte Gleichheit prueft, baut Berichte,</span></span>
<span id="cb10-12"><a href="#cb10-12" aria-hidden="true" tabindex="-1"></a><span class="co"> die zufaellig mal stimmen und mal nicht.</span></span>
<span id="cb10-13"><a href="#cb10-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-14"><a href="#cb10-14" aria-hidden="true" tabindex="-1"></a><span class="co">Teil 3 zeigt die professionelle Antwort auf Fall 1: Statt EINEN Schattenpreis</span></span>
<span id="cb10-15"><a href="#cb10-15" aria-hidden="true" tabindex="-1"></a><span class="co">zu melden, berechnet man seine SPANNE ueber alle optimalen Dualloesungen.</span></span>
<span id="cb10-16"><a href="#cb10-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-17"><a href="#cb10-17" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, scipy</span></span>
<span id="cb10-18"><a href="#cb10-18" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb10-19"><a href="#cb10-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-20"><a href="#cb10-20" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb10-21"><a href="#cb10-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-22"><a href="#cb10-22" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb10-23"><a href="#cb10-23" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linprog</span>
<span id="cb10-24"><a href="#cb10-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-25"><a href="#cb10-25" aria-hidden="true" tabindex="-1"></a><span class="co"># Ein bewusst entartetes Beispiel: drei Geraden schneiden sich in EINEM Punkt.</span></span>
<span id="cb10-26"><a href="#cb10-26" aria-hidden="true" tabindex="-1"></a><span class="co"># max x1 + x2</span></span>
<span id="cb10-27"><a href="#cb10-27" aria-hidden="true" tabindex="-1"></a><span class="co"># u.d.N. x1 + 2*x2 &lt;= 4 (A)</span></span>
<span id="cb10-28"><a href="#cb10-28" aria-hidden="true" tabindex="-1"></a><span class="co"># 2*x1 + x2 &lt;= 4 (B)</span></span>
<span id="cb10-29"><a href="#cb10-29" aria-hidden="true" tabindex="-1"></a><span class="co"># x1 + x2 &lt;= 8/3 (C) - laeuft genau durch die Ecke (4/3, 4/3)</span></span>
<span id="cb10-30"><a href="#cb10-30" aria-hidden="true" tabindex="-1"></a><span class="co"># In zwei Dimensionen legen schon zwei Geraden eine Ecke fest. Hier sind drei</span></span>
<span id="cb10-31"><a href="#cb10-31" aria-hidden="true" tabindex="-1"></a><span class="co"># aktiv - eine zu viel. Genau das ist Entartung.</span></span>
<span id="cb10-32"><a href="#cb10-32" aria-hidden="true" tabindex="-1"></a>C_ZIEL <span class="op">=</span> np.array([<span class="op">-</span><span class="fl">1.0</span>, <span class="op">-</span><span class="fl">1.0</span>]) <span class="co"># linprog minimiert -&gt; negiert</span></span>
<span id="cb10-33"><a href="#cb10-33" aria-hidden="true" tabindex="-1"></a>A_UB <span class="op">=</span> np.array([[<span class="fl">1.0</span>, <span class="fl">2.0</span>],</span>
<span id="cb10-34"><a href="#cb10-34" aria-hidden="true" tabindex="-1"></a> [<span class="fl">2.0</span>, <span class="fl">1.0</span>],</span>
<span id="cb10-35"><a href="#cb10-35" aria-hidden="true" tabindex="-1"></a> [<span class="fl">1.0</span>, <span class="fl">1.0</span>]])</span>
<span id="cb10-36"><a href="#cb10-36" aria-hidden="true" tabindex="-1"></a>B_UB <span class="op">=</span> np.array([<span class="fl">4.0</span>, <span class="fl">4.0</span>, <span class="fl">8.0</span> <span class="op">/</span> <span class="fl">3.0</span>])</span>
<span id="cb10-37"><a href="#cb10-37" aria-hidden="true" tabindex="-1"></a>NAMEN <span class="op">=</span> [<span class="st">&quot;A: Fraeszeit&quot;</span>, <span class="st">&quot;B: Schleifzeit&quot;</span>, <span class="st">&quot;C: Pruefzeit&quot;</span>]</span>
<span id="cb10-38"><a href="#cb10-38" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-39"><a href="#cb10-39" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-40"><a href="#cb10-40" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zeige_entartung() <span class="op">-&gt;</span> <span class="bu">float</span>:</span>
<span id="cb10-41"><a href="#cb10-41" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Loest dasselbe LP mit zwei Verfahren und vergleicht die Dualwerte.&quot;&quot;&quot;</span></span>
<span id="cb10-42"><a href="#cb10-42" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-43"><a href="#cb10-43" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; 1. ENTARTUNG: DERSELBE PLAN, GEGENSAETZLICHE SCHATTENPREISE&quot;</span>)</span>
<span id="cb10-44"><a href="#cb10-44" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-45"><a href="#cb10-45" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-46"><a href="#cb10-46" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Verfahren&#39;</span><span class="sc">:&lt;26}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;x1&#39;</span><span class="sc">:&gt;7}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;x2&#39;</span><span class="sc">:&gt;7}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Z*&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> Schattenpreise&quot;</span>)</span>
<span id="cb10-47"><a href="#cb10-47" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-48"><a href="#cb10-48" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-49"><a href="#cb10-49" aria-hidden="true" tabindex="-1"></a> dualwerte <span class="op">=</span> {}</span>
<span id="cb10-50"><a href="#cb10-50" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> verfahren, beschreibung <span class="kw">in</span> [(<span class="st">&quot;highs-ds&quot;</span>, <span class="st">&quot;Dual Simplex&quot;</span>),</span>
<span id="cb10-51"><a href="#cb10-51" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;highs-ipm&quot;</span>, <span class="st">&quot;Innere-Punkte-Verfahren&quot;</span>)]:</span>
<span id="cb10-52"><a href="#cb10-52" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> linprog(C_ZIEL, A_ub<span class="op">=</span>A_UB, b_ub<span class="op">=</span>B_UB, bounds<span class="op">=</span>(<span class="dv">0</span>, <span class="va">None</span>),</span>
<span id="cb10-53"><a href="#cb10-53" aria-hidden="true" tabindex="-1"></a> method<span class="op">=</span>verfahren)</span>
<span id="cb10-54"><a href="#cb10-54" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> ergebnis.success:</span>
<span id="cb10-55"><a href="#cb10-55" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="ss">f&quot;</span><span class="sc">{</span>verfahren<span class="sc">}</span><span class="ss">: </span><span class="sc">{</span>ergebnis<span class="sc">.</span>message<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-56"><a href="#cb10-56" aria-hidden="true" tabindex="-1"></a> y <span class="op">=</span> <span class="op">-</span>ergebnis.ineqlin.marginals</span>
<span id="cb10-57"><a href="#cb10-57" aria-hidden="true" tabindex="-1"></a> dualwerte[verfahren] <span class="op">=</span> y</span>
<span id="cb10-58"><a href="#cb10-58" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>beschreibung<span class="sc">:&lt;26}</span><span class="ss"> </span><span class="sc">{</span>ergebnis<span class="sc">.</span>x[<span class="dv">0</span>]<span class="sc">:&gt;7.3f}</span><span class="ss"> </span><span class="sc">{</span>ergebnis<span class="sc">.</span>x[<span class="dv">1</span>]<span class="sc">:&gt;7.3f}</span><span class="ss"> &quot;</span></span>
<span id="cb10-59"><a href="#cb10-59" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="op">-</span>ergebnis<span class="sc">.</span>fun<span class="sc">:&gt;9.4f}</span><span class="ss"> </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">round</span>(y, <span class="dv">4</span>)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-60"><a href="#cb10-60" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-61"><a href="#cb10-61" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-62"><a href="#cb10-62" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Beide Zeilen sind RICHTIG: gleicher Plan, gleicher Zielwert, und beide&quot;</span>)</span>
<span id="cb10-63"><a href="#cb10-63" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Dualvektoren erfuellen die Optimalitaetsbedingungen. Trotzdem sagen sie&quot;</span>)</span>
<span id="cb10-64"><a href="#cb10-64" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;das Gegenteil:&quot;</span>)</span>
<span id="cb10-65"><a href="#cb10-65" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Dual Simplex : </span><span class="sc">{</span>NAMEN[<span class="dv">2</span>]<span class="sc">}</span><span class="ss"> ist wertlos, A und B sind je 0,33 EUR wert.&quot;</span>)</span>
<span id="cb10-66"><a href="#cb10-66" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Innere Punkte: </span><span class="sc">{</span>NAMEN[<span class="dv">0</span>]<span class="sc">}</span><span class="ss"> und </span><span class="sc">{</span>NAMEN[<span class="dv">1</span>]<span class="sc">}</span><span class="ss"> sind wertlos, C ist 1,00 EUR wert.&quot;</span>)</span>
<span id="cb10-67"><a href="#cb10-67" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb10-68"><a href="#cb10-68" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Wer auf dieser Grundlage eine Maschine kauft, hat eine 50:50-Chance -&quot;</span>)</span>
<span id="cb10-69"><a href="#cb10-69" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;abhaengig davon, welches Verfahren der Solver zufaellig gewaehlt hat.&quot;</span>)</span>
<span id="cb10-70"><a href="#cb10-70" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-71"><a href="#cb10-71" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="bu">float</span>(<span class="op">-</span>linprog(C_ZIEL, A_ub<span class="op">=</span>A_UB, b_ub<span class="op">=</span>B_UB, bounds<span class="op">=</span>(<span class="dv">0</span>, <span class="va">None</span>)).fun)</span>
<span id="cb10-72"><a href="#cb10-72" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-73"><a href="#cb10-73" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-74"><a href="#cb10-74" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> entartung_erkennen() <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb10-75"><a href="#cb10-75" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Der Test, der in jedes Auswertungsskript gehoert.&quot;&quot;&quot;</span></span>
<span id="cb10-76"><a href="#cb10-76" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-77"><a href="#cb10-77" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; 2. ENTARTUNG ERKENNEN - UND WARUM &#39;== 0&#39; DABEI VERSAGT&quot;</span>)</span>
<span id="cb10-78"><a href="#cb10-78" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-79"><a href="#cb10-79" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-80"><a href="#cb10-80" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> linprog(C_ZIEL, A_ub<span class="op">=</span>A_UB, b_ub<span class="op">=</span>B_UB, bounds<span class="op">=</span>(<span class="dv">0</span>, <span class="va">None</span>))</span>
<span id="cb10-81"><a href="#cb10-81" aria-hidden="true" tabindex="-1"></a> schlupf <span class="op">=</span> B_UB <span class="op">-</span> A_UB <span class="op">@</span> ergebnis.x</span>
<span id="cb10-82"><a href="#cb10-82" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-83"><a href="#cb10-83" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Nebenbedingung&#39;</span><span class="sc">:&lt;18}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Schlupf&#39;</span><span class="sc">:&gt;16}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;== 0 ?&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> &quot;</span></span>
<span id="cb10-84"><a href="#cb10-84" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;&lt; 1e-7 ?&#39;</span><span class="sc">:&gt;10}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-85"><a href="#cb10-85" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-86"><a href="#cb10-86" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> name, s <span class="kw">in</span> <span class="bu">zip</span>(NAMEN, schlupf):</span>
<span id="cb10-87"><a href="#cb10-87" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">:&lt;18}</span><span class="ss"> </span><span class="sc">{</span>s<span class="sc">:&gt;16.3e}</span><span class="ss"> </span><span class="sc">{</span><span class="bu">str</span>(s <span class="op">==</span> <span class="fl">0.0</span>)<span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="bu">str</span>(<span class="bu">abs</span>(s) <span class="op">&lt;</span> <span class="fl">1e-7</span>)<span class="sc">:&gt;10}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-88"><a href="#cb10-88" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-89"><a href="#cb10-89" aria-hidden="true" tabindex="-1"></a> aktiv <span class="op">=</span> <span class="bu">int</span>((np.<span class="bu">abs</span>(schlupf) <span class="op">&lt;</span> <span class="fl">1e-7</span>).<span class="bu">sum</span>())</span>
<span id="cb10-90"><a href="#cb10-90" aria-hidden="true" tabindex="-1"></a> variablen <span class="op">=</span> A_UB.shape[<span class="dv">1</span>]</span>
<span id="cb10-91"><a href="#cb10-91" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-92"><a href="#cb10-92" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Aktive Nebenbedingungen: </span><span class="sc">{</span>aktiv<span class="sc">}</span><span class="ss">, Variablen: </span><span class="sc">{</span>variablen<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-93"><a href="#cb10-93" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> aktiv <span class="op">&gt;</span> variablen:</span>
<span id="cb10-94"><a href="#cb10-94" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;=&gt; ENTARTET. </span><span class="sc">{</span>aktiv<span class="sc">}</span><span class="ss"> aktive Restriktionen bei nur </span><span class="sc">{</span>variablen<span class="sc">}</span><span class="ss"> &quot;</span></span>
<span id="cb10-95"><a href="#cb10-95" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Variablen bedeuten:&quot;</span>)</span>
<span id="cb10-96"><a href="#cb10-96" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Der Schattenpreis ist nicht eindeutig. Melden Sie eine Spanne,&quot;</span>)</span>
<span id="cb10-97"><a href="#cb10-97" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; keinen Einzelwert (siehe Teil 3).&quot;</span>)</span>
<span id="cb10-98"><a href="#cb10-98" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb10-99"><a href="#cb10-99" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&gt; nicht entartet, die Dualwerte sind eindeutig.&quot;</span>)</span>
<span id="cb10-100"><a href="#cb10-100" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-101"><a href="#cb10-101" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">Beachten Sie die Spalte &#39;== 0&#39;: Ein Schlupf von 4.44e-16 ist&quot;</span>)</span>
<span id="cb10-102"><a href="#cb10-102" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;rechnerisch null, aber nicht gleich 0.0. Wer mit &#39;==&#39; prueft,&quot;</span>)</span>
<span id="cb10-103"><a href="#cb10-103" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;uebersieht genau die Engpaesse, die er sucht.&quot;</span>)</span>
<span id="cb10-104"><a href="#cb10-104" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-105"><a href="#cb10-105" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-106"><a href="#cb10-106" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> schattenpreis_spanne(zielwert: <span class="bu">float</span>, toleranz: <span class="bu">float</span> <span class="op">=</span> <span class="fl">1e-9</span></span>
<span id="cb10-107"><a href="#cb10-107" aria-hidden="true" tabindex="-1"></a> ) <span class="op">-&gt;</span> <span class="bu">list</span>[<span class="bu">tuple</span>[<span class="bu">float</span>, <span class="bu">float</span>]]:</span>
<span id="cb10-108"><a href="#cb10-108" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Berechnet fuer jede Nebenbedingung die Spanne ihres Schattenpreises</span></span>
<span id="cb10-109"><a href="#cb10-109" aria-hidden="true" tabindex="-1"></a><span class="co"> ueber ALLE optimalen Dualloesungen.</span></span>
<span id="cb10-110"><a href="#cb10-110" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-111"><a href="#cb10-111" aria-hidden="true" tabindex="-1"></a><span class="co"> Die Menge der optimalen Dualloesungen ist selbst ein Polyeder:</span></span>
<span id="cb10-112"><a href="#cb10-112" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-113"><a href="#cb10-113" aria-hidden="true" tabindex="-1"></a><span class="co"> A^T y &gt;= c, y &gt;= 0, b^T y = Z*</span></span>
<span id="cb10-114"><a href="#cb10-114" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-115"><a href="#cb10-115" aria-hidden="true" tabindex="-1"></a><span class="co"> (Dualzulaessigkeit plus starker Dualitaetssatz.) Minimiert und maximiert</span></span>
<span id="cb10-116"><a href="#cb10-116" aria-hidden="true" tabindex="-1"></a><span class="co"> man darauf y_i, erhaelt man die exakten Grenzen. Das ist die ehrliche</span></span>
<span id="cb10-117"><a href="#cb10-117" aria-hidden="true" tabindex="-1"></a><span class="co"> Auskunft an das Management: nicht &#39;die Stunde ist 0,33 EUR wert&#39;, sondern</span></span>
<span id="cb10-118"><a href="#cb10-118" aria-hidden="true" tabindex="-1"></a><span class="co"> &#39;zwischen 0,00 und 0,33 EUR - der Wert ist aus dem Modell nicht bestimmbar&#39;.</span></span>
<span id="cb10-119"><a href="#cb10-119" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb10-120"><a href="#cb10-120" aria-hidden="true" tabindex="-1"></a> m <span class="op">=</span> A_UB.shape[<span class="dv">0</span>]</span>
<span id="cb10-121"><a href="#cb10-121" aria-hidden="true" tabindex="-1"></a> <span class="co"># A^T y &gt;= c &lt;=&gt; -A^T y &lt;= -c ; Ziel war max c^T x, in linprog-Notation</span></span>
<span id="cb10-122"><a href="#cb10-122" aria-hidden="true" tabindex="-1"></a> <span class="co"># steckt c mit negativem Vorzeichen in C_ZIEL.</span></span>
<span id="cb10-123"><a href="#cb10-123" aria-hidden="true" tabindex="-1"></a> c_original <span class="op">=</span> <span class="op">-</span>C_ZIEL</span>
<span id="cb10-124"><a href="#cb10-124" aria-hidden="true" tabindex="-1"></a> A_dual_ub <span class="op">=</span> <span class="op">-</span>A_UB.T</span>
<span id="cb10-125"><a href="#cb10-125" aria-hidden="true" tabindex="-1"></a> b_dual_ub <span class="op">=</span> <span class="op">-</span>c_original</span>
<span id="cb10-126"><a href="#cb10-126" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-127"><a href="#cb10-127" aria-hidden="true" tabindex="-1"></a> spannen <span class="op">=</span> []</span>
<span id="cb10-128"><a href="#cb10-128" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(m):</span>
<span id="cb10-129"><a href="#cb10-129" aria-hidden="true" tabindex="-1"></a> richtung <span class="op">=</span> np.zeros(m)</span>
<span id="cb10-130"><a href="#cb10-130" aria-hidden="true" tabindex="-1"></a> richtung[i] <span class="op">=</span> <span class="fl">1.0</span></span>
<span id="cb10-131"><a href="#cb10-131" aria-hidden="true" tabindex="-1"></a> grenzen <span class="op">=</span> []</span>
<span id="cb10-132"><a href="#cb10-132" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> vorzeichen <span class="kw">in</span> (<span class="fl">1.0</span>, <span class="op">-</span><span class="fl">1.0</span>): <span class="co"># 1 = minimieren, -1 = maximieren</span></span>
<span id="cb10-133"><a href="#cb10-133" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> linprog(</span>
<span id="cb10-134"><a href="#cb10-134" aria-hidden="true" tabindex="-1"></a> vorzeichen <span class="op">*</span> richtung,</span>
<span id="cb10-135"><a href="#cb10-135" aria-hidden="true" tabindex="-1"></a> A_ub<span class="op">=</span>A_dual_ub, b_ub<span class="op">=</span>b_dual_ub,</span>
<span id="cb10-136"><a href="#cb10-136" aria-hidden="true" tabindex="-1"></a> A_eq<span class="op">=</span>B_UB.reshape(<span class="dv">1</span>, <span class="op">-</span><span class="dv">1</span>), b_eq<span class="op">=</span>[zielwert],</span>
<span id="cb10-137"><a href="#cb10-137" aria-hidden="true" tabindex="-1"></a> bounds<span class="op">=</span>(<span class="dv">0</span>, <span class="va">None</span>))</span>
<span id="cb10-138"><a href="#cb10-138" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> ergebnis.success:</span>
<span id="cb10-139"><a href="#cb10-139" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="ss">f&quot;Spannenberechnung fehlgeschlagen: &quot;</span></span>
<span id="cb10-140"><a href="#cb10-140" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>ergebnis<span class="sc">.</span>message<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-141"><a href="#cb10-141" aria-hidden="true" tabindex="-1"></a> grenzen.append(<span class="bu">float</span>(ergebnis.x[i]))</span>
<span id="cb10-142"><a href="#cb10-142" aria-hidden="true" tabindex="-1"></a> spannen.append((<span class="bu">min</span>(grenzen), <span class="bu">max</span>(grenzen)))</span>
<span id="cb10-143"><a href="#cb10-143" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> spannen</span>
<span id="cb10-144"><a href="#cb10-144" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-145"><a href="#cb10-145" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-146"><a href="#cb10-146" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zeige_spanne(zielwert: <span class="bu">float</span>) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb10-147"><a href="#cb10-147" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-148"><a href="#cb10-148" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; 3. DIE EHRLICHE AUSKUNFT: SCHATTENPREIS-SPANNEN&quot;</span>)</span>
<span id="cb10-149"><a href="#cb10-149" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-150"><a href="#cb10-150" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-151"><a href="#cb10-151" aria-hidden="true" tabindex="-1"></a> spannen <span class="op">=</span> schattenpreis_spanne(zielwert)</span>
<span id="cb10-152"><a href="#cb10-152" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Nebenbedingung&#39;</span><span class="sc">:&lt;18}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;von&#39;</span><span class="sc">:&gt;10}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;bis&#39;</span><span class="sc">:&gt;10}</span><span class="ss"> Aussage&quot;</span>)</span>
<span id="cb10-153"><a href="#cb10-153" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-154"><a href="#cb10-154" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> name, (unten, oben) <span class="kw">in</span> <span class="bu">zip</span>(NAMEN, spannen):</span>
<span id="cb10-155"><a href="#cb10-155" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> oben <span class="op">-</span> unten <span class="op">&lt;</span> <span class="fl">1e-7</span>:</span>
<span id="cb10-156"><a href="#cb10-156" aria-hidden="true" tabindex="-1"></a> aussage <span class="op">=</span> <span class="ss">f&quot;eindeutig </span><span class="sc">{</span>oben<span class="sc">:.2f}</span><span class="ss"> EUR&quot;</span></span>
<span id="cb10-157"><a href="#cb10-157" aria-hidden="true" tabindex="-1"></a> <span class="cf">elif</span> oben <span class="op">&lt;</span> <span class="fl">1e-7</span>:</span>
<span id="cb10-158"><a href="#cb10-158" aria-hidden="true" tabindex="-1"></a> aussage <span class="op">=</span> <span class="st">&quot;sicher wertlos (kein Engpass)&quot;</span></span>
<span id="cb10-159"><a href="#cb10-159" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb10-160"><a href="#cb10-160" aria-hidden="true" tabindex="-1"></a> aussage <span class="op">=</span> <span class="st">&quot;NICHT bestimmbar - Spanne melden!&quot;</span></span>
<span id="cb10-161"><a href="#cb10-161" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">:&lt;18}</span><span class="ss"> </span><span class="sc">{</span>unten<span class="sc">:&gt;10.4f}</span><span class="ss"> </span><span class="sc">{</span>oben<span class="sc">:&gt;10.4f}</span><span class="ss"> </span><span class="sc">{</span>aussage<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-162"><a href="#cb10-162" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-163"><a href="#cb10-163" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-164"><a href="#cb10-164" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;So berichtet man an Entscheider: &#39;Eine zusaetzliche Fraesstunde ist&quot;</span>)</span>
<span id="cb10-165"><a href="#cb10-165" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;zwischen 0,00 und 0,33 EUR wert - das Modell kann es nicht genauer&quot;</span>)</span>
<span id="cb10-166"><a href="#cb10-166" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;sagen, weil drei Engpaesse exakt gleichzeitig binden.&#39; Das ist eine&quot;</span>)</span>
<span id="cb10-167"><a href="#cb10-167" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;brauchbare Aussage. Ein erfundener Einzelwert ist es nicht.&quot;</span>)</span>
<span id="cb10-168"><a href="#cb10-168" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-169"><a href="#cb10-169" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-170"><a href="#cb10-170" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb10-171"><a href="#cb10-171" aria-hidden="true" tabindex="-1"></a> zielwert <span class="op">=</span> zeige_entartung()</span>
<span id="cb10-172"><a href="#cb10-172" aria-hidden="true" tabindex="-1"></a> entartung_erkennen()</span>
<span id="cb10-173"><a href="#cb10-173" aria-hidden="true" tabindex="-1"></a> zeige_spanne(zielwert)</span>
<span id="cb10-174"><a href="#cb10-174" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-175"><a href="#cb10-175" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-176"><a href="#cb10-176" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Merksatz: Pruefen Sie VOR jeder Sensitivitaetsaussage auf Entartung -&quot;</span>)</span>
<span id="cb10-177"><a href="#cb10-177" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;und vergleichen Sie Schlupfwerte nie mit &#39;== 0&#39;, sondern mit einer&quot;</span>)</span>
<span id="cb10-178"><a href="#cb10-178" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Toleranz.&quot;</span>)</span>
<span id="cb10-179"><a href="#cb10-179" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==============================================================================
1. ENTARTUNG: DERSELBE PLAN, GEGENSAETZLICHE SCHATTENPREISE
==============================================================================
Verfahren x1 x2 Z* Schattenpreise
------------------------------------------------------------------------------
Dual Simplex 1.333 1.333 2.6667 [0.3333 0.3333 0. ]
Innere-Punkte-Verfahren 1.333 1.333 2.6667 [-0. -0. 1.]
------------------------------------------------------------------------------
Beide Zeilen sind RICHTIG: gleicher Plan, gleicher Zielwert, und beide
Dualvektoren erfuellen die Optimalitaetsbedingungen. Trotzdem sagen sie
das Gegenteil:
Dual Simplex : C: Pruefzeit ist wertlos, A und B sind je 0,33 EUR wert.
Innere Punkte: A: Fraeszeit und B: Schleifzeit sind wertlos, C ist 1,00 EUR wert.
Wer auf dieser Grundlage eine Maschine kauft, hat eine 50:50-Chance -
abhaengig davon, welches Verfahren der Solver zufaellig gewaehlt hat.
==============================================================================
2. ENTARTUNG ERKENNEN - UND WARUM &#39;== 0&#39; DABEI VERSAGT
==============================================================================
Nebenbedingung Schlupf == 0 ? &lt; 1e-7 ?
------------------------------------------------------------------------------
A: Fraeszeit 0.000e+00 True True
B: Schleifzeit 0.000e+00 True True
C: Pruefzeit -4.441e-16 False True
------------------------------------------------------------------------------
Aktive Nebenbedingungen: 3, Variablen: 2
=&gt; ENTARTET. 3 aktive Restriktionen bei nur 2 Variablen bedeuten:
Der Schattenpreis ist nicht eindeutig. Melden Sie eine Spanne,
keinen Einzelwert (siehe Teil 3).
Beachten Sie die Spalte &#39;== 0&#39;: Ein Schlupf von 4.44e-16 ist
rechnerisch null, aber nicht gleich 0.0. Wer mit &#39;==&#39; prueft,
uebersieht genau die Engpaesse, die er sucht.
==============================================================================
3. DIE EHRLICHE AUSKUNFT: SCHATTENPREIS-SPANNEN
==============================================================================
Nebenbedingung von bis Aussage
------------------------------------------------------------------------------
A: Fraeszeit 0.0000 0.3333 NICHT bestimmbar - Spanne melden!
B: Schleifzeit 0.0000 0.3333 NICHT bestimmbar - Spanne melden!
C: Pruefzeit 0.0000 1.0000 NICHT bestimmbar - Spanne melden!
------------------------------------------------------------------------------
So berichtet man an Entscheider: &#39;Eine zusaetzliche Fraesstunde ist
zwischen 0,00 und 0,33 EUR wert - das Modell kann es nicht genauer
sagen, weil drei Engpaesse exakt gleichzeitig binden.&#39; Das ist eine
brauchbare Aussage. Ein erfundener Einzelwert ist es nicht.</code></pre>
<blockquote>
<p><strong>💻 Code-Durchgang</strong></p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Stelle</th>
<th>Was passiert</th>
<th>Warum es zählt</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>method="highs-ds"</code> vs. <code>"highs-ipm"</code></td>
<td>zwei Verfahren desselben Solvers</td>
<td>Der Widerspruch entsteht <strong>nicht</strong> durch einen Fehler oder unterschiedliche Bibliotheken, sondern durch verschiedene, gleich gültige Wege zur selben Ecke.</td>
</tr>
<tr class="even">
<td><code>aktiv &gt; variablen</code></td>
<td>der Entartungstest</td>
<td>Drei Zeilen Code. Sie gehören in jedes Skript, das Schattenpreise ausgibt — und fehlen in fast allen.</td>
</tr>
<tr class="odd">
<td>Spalte <code>== 0 ?</code></td>
<td>zeigt <code>False</code> bei <span class="math inline">-4{,}44 \cdot 10^{-16}</span></td>
<td>Der Beleg, warum Zulässigkeitsprüfungen immer eine Toleranz brauchen. Die Zahl ist echtes Solver-Rauschen, kein konstruiertes Beispiel.</td>
</tr>
<tr class="even">
<td><code>A_eq=B_UB.reshape(1, -1), b_eq=[zielwert]</code></td>
<td>der starke Dualitätssatz <strong>als Nebenbedingung</strong></td>
<td>Das ist der ganze Trick der Spannenrechnung: Wir suchen unter allen Dualvektoren, die denselben Zielwert erzeugen.</td>
</tr>
<tr class="odd">
<td>zwei <code>linprog</code>-Läufe je Ressource</td>
<td>Minimum und Maximum von <span class="math inline">y_i</span></td>
<td>Bei 50 Ressourcen sind das 100 kleine LPs — Sekunden. Verglichen mit einer Fehlinvestition ist das billig.</td>
</tr>
</tbody>
</table>
</blockquote>
<blockquote>
<p><strong>⚠️ Typische Fehler</strong></p>
<ul>
<li><strong>Schattenpreis als „der Wert“ berichten, ohne auf Entartung zu prüfen.</strong> Der häufigste Weg, wie eine formal korrekte Optimierung zu einer falschen Investitionsentscheidung führt.</li>
<li><strong>Schattenpreis weit extrapolieren.</strong> <span class="math inline">y_i</span> ist eine <em>Ableitung</em>: Sie gilt lokal. „Eine Stunde mehr bringt 50 €“ heißt nicht „200 Stunden mehr bringen 10 000 €“ — ab einem gewissen Punkt bindet eine andere Ressource, und der Wert bricht ein.</li>
<li><strong><code>if schlupf == 0</code>.</strong> Siehe oben. Richtig ist <code>abs(schlupf) &lt; 1e-7</code>.</li>
<li><strong>Toleranzen herunterdrehen statt das Modell zu skalieren.</strong> Behandelt das Symptom und erzeugt neue Probleme (<a href="fundament.html#sec:fundament-kondition">Abschnitt 2.7</a>).</li>
</ul>
</blockquote>
<hr />
<h2 id="sec:lp-uebungsaufgaben">5.10 Übungsaufgaben</h2>
<blockquote>
<p>Lösungen: <a href="anhang-loesungen.html#sec:loesungen-lp">Abschnitt A.5</a>.</p>
</blockquote>
<p><strong>Aufgabe 5.1 ⭐ — Schlupf deuten.</strong> Ein LP liefert <span class="math inline">s = (0;\ 12{,}5;\ 0;\ 3)</span> und <span class="math inline">y = (4{,}2;\ 0;\ 9{,}8;\ 0)</span>. (a) Welche Ressourcen sind Engpässe? (b) Ist die Lösung mit dem Satz vom komplementären Schlupf verträglich? (c) In welche Ressource würden Sie zuerst investieren?</p>
<p><strong>Aufgabe 5.2 ⭐ — Vorzeichen prüfen.</strong> Ein Kollege maximiert Gewinn mit <code>linprog</code> und meldet: „Der Schattenpreis der Engpassmaschine ist <span class="math inline">-45</span> €.“ Was ist passiert, und wie lautet der korrekte Wert?</p>
<p><strong>Aufgabe 5.3 ⭐⭐ — Simplex von Hand.</strong> Lösen Sie mit dem Tableau-Verfahren vollständig von Hand: <span class="math display">\max\ 5x_1 + 4x_2 \quad \text{u. d. N.}\quad 6x_1+4x_2 \le 24,\quad x_1+2x_2 \le 6,\quad x_1,x_2\ge0</span> Geben Sie jedes Zwischentableau an und lesen Sie am Ende Lösung <strong>und</strong> Schattenpreise ab. Prüfen Sie mit <code>scipy.optimize.linprog</code>.</p>
<p><strong>Aufgabe 5.4 ⭐⭐ — <a href="anhang-glossar.md#gloss:duales-problem" class="glossar-link">Duales Problem</a> aufstellen.</strong> Stellen Sie zum Modell aus der Aufgabe <em>Simplex von Hand</em> das duale Problem auf, lösen Sie es mit <code>linprog</code> und weisen Sie den starken Dualitätssatz numerisch nach.</p>
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>
2026-09-08 01:20:09 +02:00
<p><strong>Aufgabe 5.5 ⭐⭐ — Unbeschränktheit erkennen.</strong> Was liefert <code>SimplexTableauSolver</code> für <span class="math inline">\max x_1 + x_2</span> u. d. N. <span class="math inline">x_1 - x_2 \le 5</span>, <span class="math inline">x_1, x_2 \ge 0</span>? Erklären Sie die Fehlermeldung geometrisch.</p>
<p><strong>Aufgabe 5.6 ⭐⭐⭐ — Gültigkeitsbereich des Schattenpreises.</strong> Erweitern Sie <code>Sensitivitaetsanalyse.py</code>: Lösen Sie das Modell für Prüfkapazitäten von 30 bis 120 Stunden (Schrittweite 5) und tragen Sie Gewinn und Schattenpreis gegen die Kapazität auf. (a) Ab welcher Kapazität fällt der Schattenpreis auf einen niedrigeren Wert? Warum? (b) Wie viele Stunden sollte der Betrieb bei einem Angebotspreis von 18 €/h <strong>maximal</strong> zukaufen? (c) Zeichnen Sie den Gewinnverlauf. Was für eine Kurvenform ergibt sich, und warum?</p>
<p><strong>Aufgabe 5.7 ⭐⭐⭐ — Phase 1 ergänzen.</strong> Erweitern Sie den <code>SimplexTableauSolver</code> um eine Phase-1-Rechnung mit künstlichen Variablen, sodass auch <span class="math inline">b_i &lt; 0</span> verarbeitet werden kann. Testen Sie an: <span class="math display">\max\ 3x_1+2x_2 \quad\text{u. d. N.}\quad x_1+x_2 \ge 4,\quad x_1+3x_2 \le 12,\quad x_1,x_2\ge0</span></p>
<hr />
<h2 id="sec:lp-denkfehler">5.11 Finde den Denkfehler</h2>
<div class="card card-denkfehler">
<blockquote>
Sechster und siebter Fund: die letzten handgeschriebenen Nummern Der Loesungsanhang trug 98 Marken der Bauart "**9.2 — Ansatz waehlen.**", saemtlich aus Version 03, wo Unsicherheit Kapitel 9 war. Heute ist es Kapitel 12, und die Aufgabe wird korrekt als "Aufgabe 12.2" gesetzt. Das war keine Schoenheitsfrage: "Aufgabe 9.2" existiert wirklich - sie gehoert zum Kapitel Metaheuristiken ("Zuggroesse und Temperatur"). Wer die Loesung zu 9.2 nachschlug, landete beim falschen Thema. Beide bisherigen Pruefungen liefen vorbei: Vor der Zahl steht kein Schluesselwort, und es ist keine Tabellenzelle. Zwei Messungen machten die Reparatur billig. In allen 15 nummerierten Abschnitten gab es exakt so viele Loesungen wie Aufgaben, lueckenlos 1..n - nur der Kapitelteil war falsch. Und die acht in Phase 3 ergaenzten Kapitel machten es laengst richtig (**Titel.** ohne Nummer); der Anhang wurde also nicht auf etwas Neues umgestellt, sondern auf das, was seine neueren Teile schon taten. nummeriere_marken() vergibt die Marken jetzt selbst. Der Praefix kommt NICHT aus "# Anhang A:" - der Anhang ist eine Ueberschrift, seine Abschnitte gehoeren aber zu 23 verschiedenen Kapiteln. Er kommt aus {#sec:loesungen-X} -> {#kap:X}; diese Zuordnung gilt geprueft fuer alle 23. Im Quelltext steht "**{loesung} — Titel.**". Siebter Fund unterwegs: neun Verweise auf Denkfehler-Nummern. Saetze wie "siehe Denkfehler 8.1" standen in alter Zaehlung - "8" war in Version 03 das QP/NLP-Kapitel, heute ist es Graphen. Eine blosse Umnummerierung haette sie stillschweigend woanders hin zeigen lassen; das Ziel wurde deshalb fuer jeden einzelnen aus dem Zusammenhang bestimmt und auf {ref:sec:<kapitel>-denkfehler} umgestellt. Einer steckte in einem Programm-Docstring und bekam nach Regel 12 den Kapitelnamen statt eines Verweises. Eine bewusste Abweichung vom Plan: Der Denkfehler bekommt GAR KEINE Nummer. Es gibt je Kapitel genau einen, die zweite Stelle waere immer .1, und er steht ohnehin unter einer nummerierten Ueberschrift. Eine Nummer, die nie variiert, holte nur die Fragilitaet zurueck, die hier beseitigt wird. Auch das folgt den acht neuen Kapiteln, die ihren Denkfehler schon vorher nur mit dem Titel des Raetsels ankuendigen. --check bewacht jetzt alle Familien (Denkfehler N.M, Micro-Quiz N, **N.M — im Anhang) und zaehlt zusaetzlich ab, dass jeder Loesungsabschnitt so viele {loesung}-Marken hat wie sein Kapitel Aufgaben - das faengt eine vergessene oder doppelte Loesung, was keine Textsuche leisten kann. Gegengetestet mit vier kuenstlichen Fehlern auf einmal: alle vier gemeldet, nach dem Rueckbau null. Gegenprobe der Umstellung: Gesamtdokument vorher gesichert, nachher verglichen. 168 geaenderte Zeilen, restlos einer Familie zuzuordnen - 118 Loesungsmarken (138 gesamt minus 20, die in den Kapiteln 1 bis 3 zufaellig schon stimmten), 15 Anhang-Ueberschriften, 15 Kapitel-Callouts, 12 Micro-Quiz, 7 Zeilen mit Verweisen, 1 Docstring. Keine verschobene oder inhaltlich veraenderte Zeile. Ergebnis: 138 Aufgaben, 138 Loesungen, keine ohne Gegenstueck. Micro-Quiz tragen die echten Kapitelnummern. 293 Abschnitte, 720 Querverweise, 327 Indexmarken, PDF unveraendert 725 Seiten, 33 pytest-Tests, 67 netzfreie Programme fehlerfrei (die vier yfinance-Programme scheiterten am Rate-Limit des Anbieters, nicht an dieser Aenderung). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 09:52:41 +02:00
<p><strong>🐛 Finde den Denkfehler: Die 380 000-Euro-Maschine</strong></p>
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>
2026-09-08 01:20:09 +02:00
<p>Ein Werksleiter lässt die Engpässe seiner Fertigung analysieren. Das Skript liefert für den Lackierofen einen Schattenpreis von <strong>50 €/Stunde</strong>. Er rechnet:</p>
<blockquote>
<p><em>„Ein zweiter Lackierofen bringt 2 000 zusätzliche Stunden im Jahr. Bei 50 € &gt; je Stunde sind das 100 000 € Deckungsbeitrag pro Jahr. Die Maschine kostet 380 000 € &gt; und amortisiert sich in 3,8 Jahren. Wir kaufen.“</em></p>
</blockquote>
<p>Das Skript, das die Zahl geliefert hat:</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a>res <span class="op">=</span> linprog(c, A_ub<span class="op">=</span>A, b_ub<span class="op">=</span>b, bounds<span class="op">=</span>(<span class="dv">0</span>, <span class="va">None</span>))</span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> i, y <span class="kw">in</span> <span class="bu">enumerate</span>(<span class="op">-</span>res.ineqlin.marginals):</span>
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> y <span class="op">&gt;</span> <span class="dv">0</span>:</span>
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Engpass </span><span class="sc">{</span>name[i]<span class="sc">}</span><span class="ss">: </span><span class="sc">{</span>y<span class="sc">:.2f}</span><span class="ss"> EUR je zusaetzlicher Stunde&quot;</span>)</span></code></pre></div>
<p><strong>Ihre Aufgabe:</strong></p>
<ol type="a">
<li><p>Der Schattenpreis ist <span class="math inline">y_i = \partial Z^* / \partial b_i</span>. Welche Eigenschaft einer Ableitung übersieht die Hochrechnung auf 2 000 Stunden?</p></li>
<li><p>Skizzieren Sie, wie der zusätzliche Deckungsbeitrag als Funktion der zusätzlichen Ofenstunden tatsächlich verläuft. Warum ist er nicht linear — und in welche Richtung liegt der Fehler?</p></li>
<li><p>Zwei Prüfungen fehlen im Skript, bevor die Zahl 50 € überhaupt berichtet werden darf. Welche?</p></li>
<li><p>Wie ermitteln Sie den <strong>tatsächlichen</strong> Nutzen von 2 000 Zusatzstunden — ohne jede Extrapolation? Schreiben Sie die drei Zeilen hin.</p></li>
</ol>
<p><em>Auflösung: <a href="anhang-loesungen.html#sec:loesungen-lp">Abschnitt A.5</a>.</em></p>
</blockquote>
</div>
<hr />
<h2 id="sec:lp-quiz">5.12 Micro-Quiz</h2>
<div class="card card-quiz">
<blockquote>
Sechster und siebter Fund: die letzten handgeschriebenen Nummern Der Loesungsanhang trug 98 Marken der Bauart "**9.2 — Ansatz waehlen.**", saemtlich aus Version 03, wo Unsicherheit Kapitel 9 war. Heute ist es Kapitel 12, und die Aufgabe wird korrekt als "Aufgabe 12.2" gesetzt. Das war keine Schoenheitsfrage: "Aufgabe 9.2" existiert wirklich - sie gehoert zum Kapitel Metaheuristiken ("Zuggroesse und Temperatur"). Wer die Loesung zu 9.2 nachschlug, landete beim falschen Thema. Beide bisherigen Pruefungen liefen vorbei: Vor der Zahl steht kein Schluesselwort, und es ist keine Tabellenzelle. Zwei Messungen machten die Reparatur billig. In allen 15 nummerierten Abschnitten gab es exakt so viele Loesungen wie Aufgaben, lueckenlos 1..n - nur der Kapitelteil war falsch. Und die acht in Phase 3 ergaenzten Kapitel machten es laengst richtig (**Titel.** ohne Nummer); der Anhang wurde also nicht auf etwas Neues umgestellt, sondern auf das, was seine neueren Teile schon taten. nummeriere_marken() vergibt die Marken jetzt selbst. Der Praefix kommt NICHT aus "# Anhang A:" - der Anhang ist eine Ueberschrift, seine Abschnitte gehoeren aber zu 23 verschiedenen Kapiteln. Er kommt aus {#sec:loesungen-X} -> {#kap:X}; diese Zuordnung gilt geprueft fuer alle 23. Im Quelltext steht "**{loesung} — Titel.**". Siebter Fund unterwegs: neun Verweise auf Denkfehler-Nummern. Saetze wie "siehe Denkfehler 8.1" standen in alter Zaehlung - "8" war in Version 03 das QP/NLP-Kapitel, heute ist es Graphen. Eine blosse Umnummerierung haette sie stillschweigend woanders hin zeigen lassen; das Ziel wurde deshalb fuer jeden einzelnen aus dem Zusammenhang bestimmt und auf {ref:sec:<kapitel>-denkfehler} umgestellt. Einer steckte in einem Programm-Docstring und bekam nach Regel 12 den Kapitelnamen statt eines Verweises. Eine bewusste Abweichung vom Plan: Der Denkfehler bekommt GAR KEINE Nummer. Es gibt je Kapitel genau einen, die zweite Stelle waere immer .1, und er steht ohnehin unter einer nummerierten Ueberschrift. Eine Nummer, die nie variiert, holte nur die Fragilitaet zurueck, die hier beseitigt wird. Auch das folgt den acht neuen Kapiteln, die ihren Denkfehler schon vorher nur mit dem Titel des Raetsels ankuendigen. --check bewacht jetzt alle Familien (Denkfehler N.M, Micro-Quiz N, **N.M — im Anhang) und zaehlt zusaetzlich ab, dass jeder Loesungsabschnitt so viele {loesung}-Marken hat wie sein Kapitel Aufgaben - das faengt eine vergessene oder doppelte Loesung, was keine Textsuche leisten kann. Gegengetestet mit vier kuenstlichen Fehlern auf einmal: alle vier gemeldet, nach dem Rueckbau null. Gegenprobe der Umstellung: Gesamtdokument vorher gesichert, nachher verglichen. 168 geaenderte Zeilen, restlos einer Familie zuzuordnen - 118 Loesungsmarken (138 gesamt minus 20, die in den Kapiteln 1 bis 3 zufaellig schon stimmten), 15 Anhang-Ueberschriften, 15 Kapitel-Callouts, 12 Micro-Quiz, 7 Zeilen mit Verweisen, 1 Docstring. Keine verschobene oder inhaltlich veraenderte Zeile. Ergebnis: 138 Aufgaben, 138 Loesungen, keine ohne Gegenstueck. Micro-Quiz tragen die echten Kapitelnummern. 293 Abschnitte, 720 Querverweise, 327 Indexmarken, PDF unveraendert 725 Seiten, 33 pytest-Tests, 67 netzfreie Programme fehlerfrei (die vier yfinance-Programme scheiterten am Rate-Limit des Anbieters, nicht an dieser Aenderung). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 09:52:41 +02:00
<p><strong>❓ Micro-Quiz 5: Drei Fragen zum Selbstcheck</strong></p>
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>
2026-09-08 01:20:09 +02:00
<p>Genau eine Antwort ist jeweils richtig. Auflösung in <a href="anhang-loesungen.html#quiz-loesung-lp">Anhang A</a>.</p>
<p><strong>1. Eine Nebenbedingung hat den Schlupf <span class="math inline">s_i = 12</span> und den Schattenpreis <span class="math inline">y_i = 0</span>. Was folgt daraus?</strong> (a) Der Solver hat einen Fehler gemacht — bei positivem Schlupf muss der Dualwert ebenfalls positiv sein. (b) Von dieser Ressource sind 12 Einheiten übrig; sie ist kein Engpass, und Zukauf bringt nichts. Das ist genau der komplementäre Schlupf. (c) Die Ressource ist voll ausgelastet, wird aber nicht benötigt.</p>
<p><strong>2. Ihr Modell hat 5 Variablen. In der optimalen Ecke sind 7 Nebenbedingungen aktiv. Was bedeutet das für Ihren Bericht ans Management?</strong> (a) Nichts Besonderes — mehr aktive Bedingungen heißt nur, dass das Modell gut ausgelastet ist. (b) Das Modell ist entartet: Der Plan ist eindeutig, die Schattenpreise sind es nicht. Statt Einzelwerten gehören Spannen in den Bericht. (c) Das Modell ist unlösbar, weil mehr Bedingungen als Variablen aktiv sind.</p>
<p><strong>3. Ein Kollege prüft mit <code>if schlupf == 0.0</code>, welche Ressourcen Engpässe sind. Der Bericht weist einen offensichtlichen Engpass nicht aus. Warum?</strong> (a) Der Solver hat den Engpass übersehen. (b) Der Schlupf beträgt <span class="math inline">-4{,}4 \cdot 10^{-16}</span> statt exakt <span class="math inline">0</span> — rechnerisch null, aber nicht gleich <code>0.0</code>. Richtig ist ein Vergleich gegen eine Toleranz. (c) Schlupfwerte sind bei Maximierungsproblemen immer negativ und müssen erst umgerechnet werden.</p>
</blockquote>
</div>
<hr />
<h2 id="sec:lp-selbsttest">5.13 Selbsttest</h2>
<blockquote>
<p>Antworten: <a href="anhang-loesungen.html#selbsttest-loesung-lp">Anhang A</a>.</p>
</blockquote>
<ol type="1">
<li>Was misst eine Schlupfvariable, und was bedeutet der Wert 0?</li>
<li>Nach welchem Kriterium wählt der Simplex die Pivotzeile — und was würde passieren, wenn man stattdessen einfach die erste Zeile nähme?</li>
<li>Formulieren Sie den Satz vom komplementären Schlupf und erklären Sie ihn wirtschaftlich.</li>
<li>Warum sind die Dualwerte aus <code>scipy.optimize.linprog</code> bei einem Maximierungsproblem negativ?</li>
<li>Ihr Modell meldet „unbeschränkt“. Was ist die wahrscheinlichste Ursache?</li>
</ol>
<hr />
<h2 id="sec:lp-zusammenfassung">5.14 Zusammenfassung</h2>
<ul>
<li><strong>Schlupfvariablen</strong> verwandeln Ungleichungen in Gleichungen — und messen nebenbei ungenutzte Kapazität.</li>
<li><strong>Der Simplex</strong> wandert von Ecke zu Ecke, immer bergauf, und stoppt, wenn keine Verbesserung mehr möglich ist. Im Beispiel genügten 2 Schritte statt 5 Eckenprüfungen.</li>
<li><strong>Dualität</strong> verwandelt „Wie viel produziere ich?“ in „Was sind meine Ressourcen wert?“ — beide Probleme haben denselben Optimalwert.</li>
<li><strong>Schattenpreise</strong> beantworten die Managementfrage schlechthin: Wo ist der Engpass, und was ist seine Beseitigung wert?</li>
<li><strong>Vorzeichen prüfen!</strong> Nach einer Negation der Zielfunktion müssen die Dualwerte zurücknegiert werden. Die numerische Gegenprobe („Kapazität um 1 erhöhen und neu lösen“) ist die sicherste Kontrolle.</li>
</ul>
<p><strong>Ausblick.</strong> <a href="milp.html#kap-milp">Kapitel 6</a> bricht mit der Annahme beliebiger Teilbarkeit: Was, wenn man nur ganze Maschinen kaufen kann? Wir werden sehen, dass Runden der LP-Lösung nicht nur ungenau, sondern grundsätzlich falsch ist — und lernen <a href="anhang-glossar.md#gloss:branch-and-bound" class="glossar-link">Branch-and-Bound</a> kennen.</p>
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>
2026-09-08 01:20:09 +02:00
</article>
<button type="button" class="fortschritt-knopf" data-kapitel="lp.html"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg> <span>Als gelesen markieren</span></button>
<nav class="prev-next"><a class="prev-next-knopf prev-next-prev" href="synthese-grundlagen.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Synthese Teil I</span></a><a class="prev-next-knopf prev-next-next" href="milp.html"><span><small>Weiter</small>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound{idx:Branch-and-Bound}</span><svg class="icon" aria-hidden="true"><use href="#icon-chevron-right"></use></svg></a></nav>
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>
2026-09-08 01:20:09 +02:00
</main>
</div>
<footer class="site-footer">
<p>© Dieter Schlüter · <a href="gesamtdokument.html">Gesamtdokument</a> ·
<a href="programme.html">Beispielprogramme</a></p>
</footer>
<script defer src="assets/search-index.js"></script>
<script defer src="assets/programme.js"></script>
<script defer src="assets/site.js"></script>
</body>
</html>