operations_research/OR_HTML_04/milp.html

1807 lines
252 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 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound{idx:Branch-and-Bound} · Optimierte Entscheidungsfindung mit Python</title>
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
<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"><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" class="aktiv"><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 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound{idx:Branch-and-Bound}</span></nav>
<nav class="prev-next"><a class="prev-next-knopf prev-next-prev" href="lp.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></a><a class="prev-next-knopf prev-next-next" href="cpsat.html"><span><small>Weiter</small>Kapitel 7: Constraint Programming mit CP-SAT{idx:CP-SAT} — Logik, Scheduling und Zuweisung</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="kap-milp">Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound</h1>
<div class="card card-blick">
<blockquote>
<p><strong>📌 Kapitel auf einen Blick</strong></p>
<p><strong>Worum geht es?</strong> Um Entscheidungen, die nicht teilbar sind: ganze Maschinen, Ja/Nein, „höchstens fünf davon“. Wir zeigen mit einem konkreten Gegenbeispiel, warum Runden <strong>falsch</strong> ist, und lernen die Modellierungstricks, mit denen sich logische Regeln in lineare Ungleichungen übersetzen lassen.</p>
<p><strong>Voraussetzungen:</strong> <a href="lp.html#kap-lp">Kapitel 5</a> (<a href="anhang-glossar.md#gloss:lineare-programmierung-lp" class="glossar-link">LP (Lineare Programmierung)</a>, <a href="anhang-glossar.md#gloss:relaxation" class="glossar-link">Relaxation</a>, Schranken).</p>
<p><strong>Danach können Sie:</strong> Fixkosten, Entweder-Oder, Wenn-Dann und Kardinalitätsgrenzen modellieren, ein <a href="anhang-glossar.md#gloss:gemischt-ganzzahlige-optimierung-milp" class="glossar-link">MILP (Gemischt-ganzzahlige Optimierung)</a> mit <a href="anhang-glossar.md#gloss:cp-sat" class="glossar-link">CP-SAT</a> oder <a href="anhang-glossar.md#gloss:highs" class="glossar-link">HiGHS</a> lösen, den <a href="anhang-glossar.md#gloss:mip-gap" class="glossar-link">MIP-Gap</a> als Zusage an den Auftraggeber lesen — und ein Ergebnis erkennen, das nur deshalb gut aussieht, weil das Big-M zu groß gewählt war.</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. 6,5 Stunden.</p>
<p><strong>Programme:</strong><br />
<code>Runden_Gegenbeispiel.py</code><br />
<code>Rucksack.py</code><br />
<code>MILP_Portfolio_Fixgebuehren.py</code><br />
<code>Solverstatus_und_Gap.py</code><br />
<code>Warmstart_Effekt.py</code><br />
<code>Big_M_Falle.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/milp.ipynb">milp.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:milp-schnellstart">6.1 In 5 Minuten gelöst</h2>
<div class="card card-schnellstart">
<blockquote>
<p><strong>🚀 In 5 Minuten gelöst: Welches Lager eröffnen wir?</strong></p>
<p>Ein Händler muss 100 Paletten pro Woche ausliefern und kann dafür bis zu drei Lager eröffnen. Jedes verursacht Fixkosten, egal wie viel darüber läuft.</p>
<table>
<thead>
<tr class="header">
<th>Lager</th>
<th style="text-align: right;">Fixkosten/Woche</th>
<th style="text-align: right;">Kapazität</th>
<th style="text-align: right;">Kosten je Palette</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Nord</td>
<td style="text-align: right;">180 €</td>
<td style="text-align: right;">40</td>
<td style="text-align: right;">6,00 €</td>
</tr>
<tr class="even">
<td>Mitte</td>
<td style="text-align: right;">260 €</td>
<td style="text-align: right;">70</td>
<td style="text-align: right;">4,50 €</td>
</tr>
<tr class="odd">
<td>Süd</td>
<td style="text-align: right;">300 €</td>
<td style="text-align: right;">90</td>
<td style="text-align: right;">3,80 €</td>
</tr>
</tbody>
</table>
<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">import</span> numpy <span class="im">as</span> np</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> milp, LinearConstraint, Bounds</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>fix <span class="op">=</span> np.array([<span class="fl">180.0</span>, <span class="fl">260.0</span>, <span class="fl">300.0</span>]) <span class="co"># Fixkosten je Lager</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>kap <span class="op">=</span> np.array([<span class="fl">40.0</span>, <span class="fl">70.0</span>, <span class="fl">90.0</span>]) <span class="co"># Kapazitaet je Lager</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>var <span class="op">=</span> np.array([<span class="fl">6.0</span>, <span class="fl">4.5</span>, <span class="fl">3.8</span>]) <span class="co"># variable Kosten je Palette</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>A <span class="op">=</span> np.zeros((<span class="dv">4</span>, <span class="dv">6</span>)) <span class="co"># Variablen: 3x Menge, 3x Schalter</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>A[<span class="dv">0</span>, :<span class="dv">3</span>] <span class="op">=</span> <span class="fl">1.0</span> <span class="co"># Summe der Mengen = 100</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">3</span>):</span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a> A[i <span class="op">+</span> <span class="dv">1</span>, i], A[i <span class="op">+</span> <span class="dv">1</span>, <span class="dv">3</span> <span class="op">+</span> i] <span class="op">=</span> <span class="fl">1.0</span>, <span class="op">-</span>kap[i] <span class="co"># Menge &lt;= Kapazitaet * Schalter</span></span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a>nb <span class="op">=</span> LinearConstraint(A, [<span class="dv">100</span>, <span class="op">-</span>np.inf, <span class="op">-</span>np.inf, <span class="op">-</span>np.inf], [<span class="dv">100</span>, <span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">0</span>])</span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a>grenzen <span class="op">=</span> Bounds([<span class="dv">0</span>] <span class="op">*</span> <span class="dv">6</span>, <span class="bu">list</span>(kap) <span class="op">+</span> [<span class="dv">1</span>, <span class="dv">1</span>, <span class="dv">1</span>])</span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> name, ganzzahlig <span class="kw">in</span> [(<span class="st">&quot;[LP-Relaxation](anhang-glossar.md#gloss:lp-relaxation){.glossar-link}&quot;</span>, [<span class="dv">0</span>] <span class="op">*</span> <span class="dv">6</span>), (<span class="st">&quot;MILP&quot;</span>, [<span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">1</span>, <span class="dv">1</span>, <span class="dv">1</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-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a> r <span class="op">=</span> milp(c<span class="op">=</span>np.concatenate([var, fix]), constraints<span class="op">=</span>[nb],</span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a> integrality<span class="op">=</span>ganzzahlig, bounds<span class="op">=</span>grenzen)</span>
<span id="cb1-18"><a href="#cb1-18" 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">:14}</span><span class="ss"> </span><span class="sc">{</span>r<span class="sc">.</span>fun<span class="sc">:7.2f}</span><span class="ss"> EUR Mengen </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">round</span>(r.x[:<span class="dv">3</span>], <span class="dv">1</span>)<span class="sc">}</span><span class="ss"> &quot;</span></span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Schalter </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">round</span>(r.x[<span class="dv">3</span>:], <span class="dv">3</span>)<span class="sc">}</span><span class="ss">&quot;</span>)</span></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>LP-Relaxation 724.14 EUR Mengen [ 0. 10. 90.] Schalter [-0. 0.143 1. ]
MILP 882.00 EUR Mengen [10. 0. 90.] Schalter [ 1. -0. 1. ]</code></pre>
</blockquote>
</div>
<p><strong>Sehen Sie sich die Schalter der ersten Zeile an: 0,143.</strong> Die LP-Relaxation eröffnet Lager Mitte zu 14,3 % — und zahlt dafür nur 14,3 % der Fixkosten. So etwas gibt es in der Wirklichkeit nicht. Ein Lager ist offen oder zu.</p>
<p>Und jetzt der Punkt, um den es in diesem ganzen Kapitel geht — <strong>was passiert, wenn man diese 0,143 rundet?</strong></p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr class="header">
<th>Vorgehen</th>
<th>Ergebnis</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Abrunden auf <span class="math inline">y = (0, 0, 1)</span></td>
<td>Nur Süd offen: 90 Paletten Kapazität für 100 Paletten Bedarf → <strong>unzulässig</strong></td>
</tr>
<tr class="even">
<td>Aufrunden auf <span class="math inline">y = (0, 1, 1)</span></td>
<td>Zulässig, aber <strong>947 €</strong> — 65 € teurer als nötig</td>
</tr>
<tr class="odd">
<td><strong>MILP-Optimum <span class="math inline">y = (1, 0, 1)</span></strong></td>
<td><strong>882 €</strong> — und es öffnet <strong>Nord</strong>, ein Lager, das die Relaxation gar nicht vorgeschlagen hatte</td>
</tr>
</tbody>
</table>
<p>Runden führt hier also in beide Richtungen in die Irre: einmal in die Unzulässigkeit, einmal in unnötige Kosten. Und der entscheidende Punkt ist der dritte: Die ganzzahlige Lösung ist <strong>strukturell anders</strong>. Sie öffnet ein anderes Lager. Keine noch so geschickte Rundung der LP-Lösung hätte darauf kommen können.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Die LP-Relaxation ist keine ungefähre Antwort, die man nur noch glattziehen muss. Sie ist eine Antwort auf eine <strong>andere Frage</strong> — nämlich die, bei der man Lager auch zu 14,3 % eröffnen darf. Ihr Wert liegt nicht in ihrer Lösung, sondern in ihrer <strong>Schranke</strong>: Weniger als 724,14 € kann die richtige Antwort nicht kosten.</p>
</blockquote>
<p>Genau diese Schranke ist der Hebel, mit dem <a href="anhang-glossar.md#gloss:branch-and-bound" class="glossar-link">Branch-and-Bound</a> arbeitet. Wie, zeigt der Abschnitt <a href="#sec:milp-branch-and-bound">Abschnitt 6.4</a>.</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 />
<h2 id="sec:milp-lernziele">6.2 Lernziele</h2>
<p>Nach diesem Kapitel können Sie …</p>
<ol type="1">
<li>… an einem Zahlenbeispiel belegen, warum Runden einer LP-Lösung scheitert.</li>
<li>… den Ablauf von Branch-and-Bound erklären und einen kleinen Suchbaum von Hand zeichnen.</li>
<li>… die vier klassischen Big-M-Muster anwenden (Fixkosten, Entweder-Oder, Wenn-Dann, Kardinalität).</li>
<li>… begründen, warum <span class="math inline">M</span> so klein wie möglich gewählt werden muss.</li>
<li>… ein Portfolioproblem mit Ordergebühren und Höchstzahl an Positionen lösen.</li>
<li><strong>Incumbent, Schranke und MIP-Gap</strong> unterscheiden und aus einem abgebrochenen Solverlauf eine belastbare Aussage ableiten.</li>
<li>… alle <a href="anhang-glossar.md#gloss:solver" class="glossar-link">Solver</a>-Statusfälle explizit behandeln, statt <code>OPTIMAL</code> vorauszusetzen.</li>
<li>… einen <strong><a href="anhang-glossar.md#gloss:warm-start" class="glossar-link">Warm-Start</a></strong> setzen — und messen, ob er auf Ihrer Problemklasse überhaupt etwas bringt.</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>
<hr />
<h2 id="sec:milp-warum-runden-fundamental-scheitert">6.3 Warum Runden fundamental scheitert</h2>
<p>In der realen Welt sind viele Entscheidungen nicht teilbar. Man kann nicht 0,47 Flugzeuge kaufen, keine halbe Lagerhalle bauen, keinen Mitarbeiter zu 38 % einstellen. Noch wichtiger: Logische Schalter — <em>„Wenn Fabrik A gebaut wird, muss auch Lager B gebaut werden“</em> — brauchen diskrete Zustände.</p>
<p><strong>Mixed-Integer Linear Programming (MILP)</strong>, deutsch <em>gemischt-ganzzahlige lineare Optimierung</em>, erweitert das LP um ganzzahlige (<span class="math inline">\mathbb{Z}</span>) und binäre (<span class="math inline">\{0,1\}</span>) Variablen.</p>
<p>Der verlockende Gedanke lautet: <em>„Wir lösen das Problem kontinuierlich und runden.“</em> Eine verbreitete, aber unbelegte Behauptung dazu lautet, man verliere dadurch „oft 2050 % des Gewinns“. Rechnen wir es nach, statt es zu behaupten.</p>
<figure>
<img src="bilder_04/kap_milp_runden.svg" alt="Abb. 6.1: Links die Handrechnung von unten als Bild: Die Zielfunktion läuft parallel zur Restriktion, also ist die ganze Kante optimal. Aufrunden auf (1;1) führt aus dem zulässigen Bereich heraus, Abrunden auf (0;0) auf den Wert null. Rechts die Messung aus Runden_Gegenbeispiel.py über je 200 Zufallsinstanzen. Erzeugt von bilder_04/erzeuge_runden.py." />
<figcaption aria-hidden="true">Abb. 6.1: Links die Handrechnung von unten als Bild: Die <a href="anhang-glossar.md#gloss:zielfunktion" class="glossar-link">Zielfunktion</a> läuft parallel zur Restriktion, also ist die ganze Kante optimal. Aufrunden auf <span class="math inline">(1;1)</span> führt aus dem zulässigen Bereich heraus, Abrunden auf <span class="math inline">(0;0)</span> auf den Wert null. Rechts die Messung aus <code>Runden_Gegenbeispiel.py</code> über je 200 Zufallsinstanzen. Erzeugt von <code>bilder_04/erzeuge_runden.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>
<blockquote>
<p><strong>✏️ Handrechnung 6.1: Ein Gegenbeispiel, das Sie im Kopf prüfen können</strong></p>
<p><span class="math display">\max\ Z = x_1 + x_2 \quad\text{u. d. N.}\quad 2x_1 + 2x_2 \le 3,\quad x_1,x_2 \in \{0,1,2,\dots\}</span></p>
<p><strong>LP-Relaxation</strong> (Ganzzahligkeit weggelassen): Jede Kombination mit <span class="math inline">x_1 + x_2 = 1{,}5</span> ist optimal, z. B. <span class="math inline">x_1 = x_2 = 0{,}75</span> mit <span class="math inline">Z_{LP} = 1{,}5</span>.</p>
<p><strong>Aufrunden</strong> auf <span class="math inline">(1, 1)</span>: <span class="math inline">2\cdot1 + 2\cdot1 = 4 &gt; 3</span><strong>unzulässig</strong>. <strong>Abrunden</strong> auf <span class="math inline">(0, 0)</span>: zulässig, aber <span class="math inline">Z = 0</span>. <strong>Wahres ganzzahliges Optimum:</strong> <span class="math inline">(1, 0)</span> oder <span class="math inline">(0, 1)</span> mit <span class="math inline">Z_{IP} = 1</span>.</p>
<p>Das Abrunden verliert hier <strong>100 %</strong> des erreichbaren Werts. Das ist kein exotischer Sonderfall, sondern typisch, sobald die Zahlen klein sind — und bei Ja/Nein-Entscheidungen sind sie immer klein.</p>
</blockquote>
<p>Die drei Gründe systematisch:</p>
<ol type="1">
<li><strong>Verletzung von Nebenbedingungen.</strong> Aufrunden überschreitet Kapazitätsgrenzen — die Lösung wird unzulässig.</li>
<li><strong>Suboptimalität.</strong> Abrunden verschenkt Kapazität. Der Verlust ist umso größer, je kleiner die Zahlen sind.</li>
<li><strong>Distanz im Raum.</strong> In hohen Dimensionen liegt der beste ganzzahlige Punkt oft <strong>weit entfernt</strong> von der LP-Lösung — er ist durch Runden gar nicht erreichbar.</li>
</ol>
<p>Das folgende Programm quantifiziert den Effekt über viele Zufallsinstanzen.</p>
<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"># Runden_Gegenbeispiel.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 MILP: Wie schlecht ist Runden wirklich?</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">Ersetzt die unbelegte Behauptung &quot;20-50 % Verlust&quot; durch eine Messung ueber</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="co">viele Zufallsinstanzen.</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linprog</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>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> erzeuge_instanz(n, m, rng):</span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Zufaelliges Rucksack-aehnliches MILP mit kleinen Zahlen.&quot;&quot;&quot;</span></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a> c <span class="op">=</span> rng.integers(<span class="dv">3</span>, <span class="dv">20</span>, size<span class="op">=</span>n).astype(<span class="bu">float</span>) <span class="co"># Ertraege</span></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a> A <span class="op">=</span> rng.integers(<span class="dv">1</span>, <span class="dv">9</span>, size<span class="op">=</span>(m, n)).astype(<span class="bu">float</span>) <span class="co"># Verbraeuche</span></span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a> b <span class="op">=</span> (A.<span class="bu">sum</span>(axis<span class="op">=</span><span class="dv">1</span>) <span class="op">*</span> rng.uniform(<span class="fl">0.25</span>, <span class="fl">0.45</span>)).<span class="bu">round</span>() <span class="co"># knappe Kapazitaeten</span></span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> c, A, b</span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese_lp(c, A, b, ganzzahlig<span class="op">=</span><span class="va">False</span>):</span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;LP-Relaxation oder exaktes MILP ueber HiGHS.&quot;&quot;&quot;</span></span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a> n <span class="op">=</span> <span class="bu">len</span>(c)</span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> linprog(</span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a> c<span class="op">=-</span>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 class="op">*</span> n,</span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a> integrality<span class="op">=</span>np.ones(n) <span class="cf">if</span> ganzzahlig <span class="cf">else</span> <span class="va">None</span>,</span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a> method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> (<span class="op">-</span>ergebnis.fun, ergebnis.x) <span class="cf">if</span> ergebnis.success <span class="cf">else</span> (<span class="va">None</span>, <span class="va">None</span>)</span>
<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-32"><a href="#cb3-32" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-33"><a href="#cb3-33" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> abrunden_und_reparieren(x_lp, c, A, b):</span>
<span id="cb3-34"><a href="#cb3-34" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Naive Strategie: abrunden, dann gierig auffuellen, solange zulaessig.&quot;&quot;&quot;</span></span>
<span id="cb3-35"><a href="#cb3-35" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> np.floor(x_lp <span class="op">+</span> <span class="fl">1e-9</span>)</span>
<span id="cb3-36"><a href="#cb3-36" aria-hidden="true" tabindex="-1"></a> verbessert <span class="op">=</span> <span class="va">True</span></span>
<span id="cb3-37"><a href="#cb3-37" aria-hidden="true" tabindex="-1"></a> <span class="cf">while</span> verbessert: <span class="co"># gierig auffuellen</span></span>
<span id="cb3-38"><a href="#cb3-38" aria-hidden="true" tabindex="-1"></a> verbessert <span class="op">=</span> <span class="va">False</span></span>
<span id="cb3-39"><a href="#cb3-39" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> np.argsort(<span class="op">-</span>c): <span class="co"># bester Ertrag zuerst</span></span>
<span id="cb3-40"><a href="#cb3-40" aria-hidden="true" tabindex="-1"></a> kandidat <span class="op">=</span> x.copy()</span>
<span id="cb3-41"><a href="#cb3-41" aria-hidden="true" tabindex="-1"></a> kandidat[j] <span class="op">+=</span> <span class="dv">1</span></span>
<span id="cb3-42"><a href="#cb3-42" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> np.<span class="bu">all</span>(A <span class="op">@</span> kandidat <span class="op">&lt;=</span> b <span class="op">+</span> <span class="fl">1e-9</span>):</span>
<span id="cb3-43"><a href="#cb3-43" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> kandidat</span>
<span id="cb3-44"><a href="#cb3-44" aria-hidden="true" tabindex="-1"></a> verbessert <span class="op">=</span> <span class="va">True</span></span>
<span id="cb3-45"><a href="#cb3-45" aria-hidden="true" tabindex="-1"></a> <span class="cf">break</span></span>
<span id="cb3-46"><a href="#cb3-46" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> c <span class="op">@</span> x, x</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>
<span id="cb3-49"><a href="#cb3-49" 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-50"><a href="#cb3-50" aria-hidden="true" tabindex="-1"></a> rng <span class="op">=</span> np.random.default_rng(<span class="dv">2026</span>)</span>
<span id="cb3-51"><a href="#cb3-51" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">82</span>)</span>
<span id="cb3-52"><a href="#cb3-52" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; WIE TEUER IST RUNDEN? (200 Zufallsinstanzen je Groesse)&quot;</span>)</span>
<span id="cb3-53"><a href="#cb3-53" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">82</span>)</span>
<span id="cb3-54"><a href="#cb3-54" 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;n x m&#39;</span><span class="sc">:&gt;8}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Aufrunden unzul.&#39;</span><span class="sc">:&gt;17}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Abrunden: mittl.&#39;</span><span class="sc">:&gt;17}</span><span class="ss"> | &quot;</span></span>
<span id="cb3-55"><a href="#cb3-55" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;schlimmster&#39;</span><span class="sc">:&gt;12}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;gierig&#39;</span><span class="sc">:&gt;8}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-56"><a href="#cb3-56" 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;&#39;</span><span class="sc">:&gt;8}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&gt;17}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Verlust&#39;</span><span class="sc">:&gt;17}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Fall&#39;</span><span class="sc">:&gt;12}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Verlust&#39;</span><span class="sc">:&gt;8}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-57"><a href="#cb3-57" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">82</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="cf">for</span> n, m <span class="kw">in</span> [(<span class="dv">5</span>, <span class="dv">2</span>), (<span class="dv">10</span>, <span class="dv">3</span>), (<span class="dv">20</span>, <span class="dv">5</span>), (<span class="dv">40</span>, <span class="dv">8</span>)]:</span>
<span id="cb3-60"><a href="#cb3-60" aria-hidden="true" tabindex="-1"></a> unzulaessig <span class="op">=</span> <span class="dv">0</span></span>
<span id="cb3-61"><a href="#cb3-61" aria-hidden="true" tabindex="-1"></a> verluste_ab, verluste_gierig <span class="op">=</span> [], []</span>
<span id="cb3-62"><a href="#cb3-62" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-63"><a href="#cb3-63" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> _ <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">200</span>):</span>
<span id="cb3-64"><a href="#cb3-64" aria-hidden="true" tabindex="-1"></a> c, A, b <span class="op">=</span> erzeuge_instanz(n, m, rng)</span>
<span id="cb3-65"><a href="#cb3-65" aria-hidden="true" tabindex="-1"></a> z_lp, x_lp <span class="op">=</span> loese_lp(c, A, b, ganzzahlig<span class="op">=</span><span class="va">False</span>)</span>
<span id="cb3-66"><a href="#cb3-66" aria-hidden="true" tabindex="-1"></a> z_ip, _ <span class="op">=</span> loese_lp(c, A, b, ganzzahlig<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb3-67"><a href="#cb3-67" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> z_lp <span class="kw">is</span> <span class="va">None</span> <span class="kw">or</span> z_ip <span class="kw">is</span> <span class="va">None</span> <span class="kw">or</span> z_ip <span class="op">&lt;=</span> <span class="dv">0</span>:</span>
<span id="cb3-68"><a href="#cb3-68" aria-hidden="true" tabindex="-1"></a> <span class="cf">continue</span></span>
<span id="cb3-69"><a href="#cb3-69" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-70"><a href="#cb3-70" aria-hidden="true" tabindex="-1"></a> <span class="co"># Variante 1: aufrunden</span></span>
<span id="cb3-71"><a href="#cb3-71" aria-hidden="true" tabindex="-1"></a> x_auf <span class="op">=</span> np.ceil(x_lp <span class="op">-</span> <span class="fl">1e-9</span>)</span>
<span id="cb3-72"><a href="#cb3-72" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> np.<span class="bu">any</span>(A <span class="op">@</span> x_auf <span class="op">&gt;</span> b <span class="op">+</span> <span class="fl">1e-9</span>):</span>
<span id="cb3-73"><a href="#cb3-73" aria-hidden="true" tabindex="-1"></a> unzulaessig <span class="op">+=</span> <span class="dv">1</span></span>
<span id="cb3-74"><a href="#cb3-74" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-75"><a href="#cb3-75" aria-hidden="true" tabindex="-1"></a> <span class="co"># Variante 2: abrunden</span></span>
<span id="cb3-76"><a href="#cb3-76" aria-hidden="true" tabindex="-1"></a> x_ab <span class="op">=</span> np.floor(x_lp <span class="op">+</span> <span class="fl">1e-9</span>)</span>
<span id="cb3-77"><a href="#cb3-77" aria-hidden="true" tabindex="-1"></a> verluste_ab.append(<span class="fl">1.0</span> <span class="op">-</span> (c <span class="op">@</span> x_ab) <span class="op">/</span> z_ip)</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"># Variante 3: abrunden + gierig auffuellen</span></span>
<span id="cb3-80"><a href="#cb3-80" aria-hidden="true" tabindex="-1"></a> z_gierig, _ <span class="op">=</span> abrunden_und_reparieren(x_lp, c, A, b)</span>
<span id="cb3-81"><a href="#cb3-81" aria-hidden="true" tabindex="-1"></a> verluste_gierig.append(<span class="fl">1.0</span> <span class="op">-</span> z_gierig <span class="op">/</span> z_ip)</span>
<span id="cb3-82"><a href="#cb3-82" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-83"><a href="#cb3-83" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>n<span class="sc">:&gt;3}</span><span class="ss"> x </span><span class="sc">{</span>m<span class="sc">:&lt;3}</span><span class="ss"> | </span><span class="sc">{</span>unzulaessig<span class="op">/</span><span class="dv">2</span><span class="sc">:&gt;15.1f}</span><span class="ss"> % | &quot;</span></span>
<span id="cb3-84"><a href="#cb3-84" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>np<span class="sc">.</span>mean(verluste_ab)<span class="op">*</span><span class="dv">100</span><span class="sc">:&gt;15.1f}</span><span class="ss"> % | &quot;</span></span>
<span id="cb3-85"><a href="#cb3-85" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">max</span>(verluste_ab)<span class="op">*</span><span class="dv">100</span><span class="sc">:&gt;10.1f}</span><span class="ss"> % | &quot;</span></span>
<span id="cb3-86"><a href="#cb3-86" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>np<span class="sc">.</span>mean(verluste_gierig)<span class="op">*</span><span class="dv">100</span><span class="sc">:&gt;6.1f}</span><span class="ss"> %&quot;</span>)</span>
<span id="cb3-87"><a href="#cb3-87" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-88"><a href="#cb3-88" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">82</span>)</span>
<span id="cb3-89"><a href="#cb3-89" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Lesart: &#39;Aufrunden unzul.&#39; = Anteil der Faelle, in denen die aufgerundete&quot;</span>)</span>
<span id="cb3-90"><a href="#cb3-90" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Loesung eine Nebenbedingung verletzt. &#39;Verlust&#39; = Abstand zum exakten Optimum.&quot;</span>)</span>
<span id="cb3-91"><a href="#cb3-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">82</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==================================================================================
WIE TEUER IST RUNDEN? (200 Zufallsinstanzen je Groesse)
==================================================================================
n x m | Aufrunden unzul. | Abrunden: mittl. | schlimmster | gierig
| | Verlust | Fall | Verlust
----------------------------------------------------------------------------------
5 x 2 | 82.0 % | 17.9 % | 100.0 % | 3.1 %
10 x 3 | 90.0 % | 17.2 % | 100.0 % | 3.0 %
20 x 5 | 97.5 % | 17.8 % | 50.0 % | 2.9 %
40 x 8 | 100.0 % | 15.6 % | 35.7 % | 3.6 %
----------------------------------------------------------------------------------
Lesart: &#39;Aufrunden unzul.&#39; = Anteil der Faelle, in denen die aufgerundete
Loesung eine Nebenbedingung verletzt. &#39;Verlust&#39; = Abstand zum exakten Optimum.
==================================================================================</code></pre>
<p><strong>Was die Zahlen sagen:</strong></p>
<ul>
<li><strong>Aufrunden ist fast immer unzulässig</strong> (82 % bei kleinen, 100 % bei größeren Instanzen). Es ist keine Strategie, sondern ein Fehler — und zwar einer, der mit der Problemgröße <em>zunimmt</em>, weil mehr Variablen mehr Gelegenheiten bieten, eine Kapazität zu sprengen.</li>
<li><strong>Abrunden verliert im Mittel 1618 %</strong>, in den schlimmsten gemessenen Fällen <strong>100 %</strong> (dann bleibt das Ergebnis bei null). Die eingangs zitierte Behauptung „2050 %“ lag zu hoch; der gemessene Mittelwert liegt darunter, die Extremfälle darüber. Beides ist erst durch die Messung sichtbar.</li>
<li><strong>Abrunden plus gieriges Auffüllen</strong> ist deutlich besser (rund 3 % Verlust) — aber weiterhin ohne jede Garantie und mit zusätzlichem Implementierungsaufwand. Ein exakter MILP-Solver liefert die Optimalität gratis und dazu einen <strong>Beweis</strong> dafür.</li>
</ul>
<blockquote>
<p><strong>⚠️ Zur Ehrlichkeit von Messungen</strong> Diese Zahlen gelten für <strong>diese</strong> Instanzfamilie (kleine ganzzahlige Erträge, knappe Kapazitäten). Bei großen Stückzahlen — etwa 4 700 statt 4,7 produzierten Einheiten — ist der relative Rundungsfehler viel kleiner, und Runden wird zu einer brauchbaren Heuristik. <strong>Die Faustregel lautet deshalb nicht „Runden ist immer schlecht“, sondern: Je kleiner die Zahlen und je knapper die Kapazitäten, desto teurer das Runden.</strong> Bei Ja/Nein-Variablen — dem häufigsten Fall — sind die Zahlen maximal klein, und Runden ist sinnlos.</p>
</blockquote>
<blockquote>
<p><strong>🎯 Merksatz</strong> Runden ist kein Näherungsverfahren mit kontrollierbarem Fehler, sondern ein Verfahren ohne jede Garantie. Wenn Ganzzahligkeit zum Problem gehört, gehört sie ins Modell.</p>
</blockquote>
<hr />
<h2 id="sec:milp-branch-and-bound">6.4 Branch-and-Bound</h2>
<p>MILP-Probleme sind <strong><a href="anhang-glossar.md#gloss:np-schwer" class="glossar-link">NP-schwer</a></strong>. Der Standardansatz moderner Solver (HiGHS, <a href="anhang-glossar.md#gloss:scip" class="glossar-link">SCIP</a>, <a href="anhang-glossar.md#gloss:gurobi" class="glossar-link">Gurobi</a>) heißt <strong><a href="anhang-glossar.md#gloss:branch-and-cut" class="glossar-link">Branch-and-Cut</a></strong> — Branch-and-Bound plus Schnittebenen.</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
<figure>
<img src="bilder_04/kap_milp_suchbaum.svg" alt="Abb. 6.2: Der Suchbaum der Handrechnung weiter unten — nicht gezeichnet, sondern gerechnet: Das Skript löst die LP-Relaxationen und kappt nach denselben drei Regeln, die oben stehen. Ast B wird verworfen, weil sein Wert 18 den Incumbent 20 aus Ast A nicht schlägt. Erzeugt von bilder_04/erzeuge_branch_and_bound.py." />
<figcaption aria-hidden="true">Abb. 6.2: Der Suchbaum der Handrechnung weiter unten — nicht gezeichnet, sondern gerechnet: Das Skript löst die LP-Relaxationen und kappt nach denselben drei Regeln, die oben stehen. Ast B wird verworfen, weil sein Wert 18 den Incumbent 20 aus Ast A nicht schlägt. Erzeugt von <code>bilder_04/erzeuge_branch_and_bound.py</code>.</figcaption>
</figure>
<h3 id="die-drei-phasen">Die drei Phasen</h3>
<ol type="1">
<li><strong>LP-Relaxation.</strong> Ganzzahligkeit vorübergehend weglassen und das LP lösen. Der Wert ist eine <strong>obere Schranke</strong> (bei Maximierung): Besser als das kann kein ganzzahliger Punkt sein, denn jeder ganzzahlige Punkt ist auch ein zulässiger LP-Punkt.</li>
<li><strong>Branching.</strong> Hat eine ganzzahlige Variable einen gebrochenen Wert, etwa <span class="math inline">x_1 = 2{,}7</span>, wird das Problem in zwei disjunkte Teilprobleme zerlegt: <span class="math display">\text{Ast 1: } x_1 \le 2 \qquad\text{und}\qquad \text{Ast 2: } x_1 \ge 3</span> Der Wert <span class="math inline">2{,}7</span> selbst wird dadurch ausgeschlossen — kein ganzzahliger Punkt geht verloren.</li>
<li><strong>Bounding und Pruning.</strong> Ein Ast wird abgeschnitten (<em>pruned</em>), wenn:
<ul>
<li>das Teilproblem unzulässig ist,</li>
<li>seine LP-Schranke schlechter ist als die beste bereits gefundene ganzzahlige Lösung (der <strong>Incumbent</strong>),</li>
<li>oder die LP-Lösung bereits ganzzahlig ist (dann ist dieser Ast fertig).</li>
</ul></li>
</ol>
2026-09-09 07:37:07 +02:00
<figure>
<img src="bilder_04/kap_milp_bnb_geometrie.svg" alt="Abb. 6.3: Was der erste Verzweigungsschritt des Suchbaums geometrisch bedeutet. Graue Punkte sind die 13 ganzzahligen zulässigen Punkte. Erzeugt von bilder_04/erzeuge_bnb_geometrie.py." />
<figcaption aria-hidden="true">Abb. 6.3: Was der erste Verzweigungsschritt des Suchbaums geometrisch bedeutet. Graue Punkte sind die 13 ganzzahligen zulässigen Punkte. Erzeugt von <code>bilder_04/erzeuge_bnb_geometrie.py</code>.</figcaption>
</figure>
<p><strong>Was Sie in der Abbildung sehen.</strong> Der rote Streifen ist genau das, was Phase 2 wegnimmt — und in ihm liegt kein einziger grauer Punkt. Deshalb ist Verzweigen verlustfrei. Weggenommen wird trotzdem etwas Wertvolles: das LP-Optimum (3; 1,5) selbst. Genau darum sinkt die Schranke von 21 auf 20,67, und genau dieses Sinken ist der Fortschritt, den der Suchbaum Knoten für Knoten protokolliert.</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>Zusätzlich fügen Solver <strong>Schnittebenen</strong> (<em>cutting planes</em>, z. B. Gomory-Cuts) hinzu: gültige Ungleichungen, die gebrochene Bereiche wegschneiden, ohne einen einzigen zulässigen ganzzahligen Punkt zu entfernen.</p>
<blockquote>
<p><strong>✏️ Handrechnung 6.2: Ein Suchbaum von Hand</strong></p>
<p><span class="math display">\max\ 5x_1 + 4x_2 \quad\text{u. d. N.}\quad 6x_1+4x_2\le24,\quad x_1+2x_2\le6,\quad x_1,x_2\in\mathbb{Z}_{\ge0}</span></p>
<p><strong>Wurzel <span class="math inline">P_0</span>:</strong> LP-Lösung <span class="math inline">x = (3;\ 1{,}5)</span>, <span class="math inline">Z_{LP} = 21</span>. → obere Schranke 21. <span class="math inline">x_2</span> ist gebrochen → verzweigen über <span class="math inline">x_2</span>.</p>
<p><strong>Ast A (<span class="math inline">x_2 \le 1</span>):</strong> LP liefert <span class="math inline">x = (3{,}33;\ 1)</span>, <span class="math inline">Z = 20{,}67</span>. Noch gebrochen (<span class="math inline">x_1</span>) → weiter verzweigen. * <strong>A1 (<span class="math inline">x_2\le1</span>, <span class="math inline">x_1\le3</span>):</strong> <span class="math inline">x = (3;\ 1)</span>, <span class="math inline">Z = 19</span><strong>ganzzahlig!</strong> Incumbent <span class="math inline">= 19</span>. * <strong>A2 (<span class="math inline">x_2\le1</span>, <span class="math inline">x_1\ge4</span>):</strong> <span class="math inline">x = (4;\ 0)</span>, <span class="math inline">Z = 20</span><strong>ganzzahlig!</strong> Incumbent verbessert auf <span class="math inline">20</span>.</p>
<p><strong>Ast B (<span class="math inline">x_2 \ge 2</span>):</strong> LP liefert <span class="math inline">x = (2{,}67;\ 2)</span>, <span class="math inline">Z = 21{,}33</span>… halt: Prüfen wir <span class="math inline">6x_1+4x_2\le24</span> mit <span class="math inline">x_2=2</span>: <span class="math inline">6x_1 \le 16</span>, also <span class="math inline">x_1 \le 2{,}67</span>; und <span class="math inline">x_1+2x_2\le6</span> gibt <span class="math inline">x_1 \le 2</span>. Also <span class="math inline">x_1 = 2</span>, <span class="math inline">Z = 5\cdot2+4\cdot2 = 18</span> → ganzzahlig, aber <strong>schlechter</strong> als der Incumbent 20 → verworfen.</p>
<p><strong>Ergebnis:</strong> <span class="math inline">x^* = (4;\ 0)</span>, <span class="math inline">Z^* = 20</span>. Wir haben <strong>fünf</strong> LPs gelöst — <span class="math inline">P_0</span>, A, A1, A2 und B — statt alle ganzzahligen Punkte aufzuzählen. Das Skript <code>bilder_04/erzeuge_branch_and_bound.py</code> rechnet genau diesen Baum nach und zählt mit.</p>
<p>Beachten Sie: Der Wert <span class="math inline">Z_{LP} = 21</span> der Wurzel ist die <strong>Optimalitätslücke</strong>-Referenz. Nach dem Finden von 20 weiß der Solver: Das Optimum liegt zwischen 20 und 21 — eine Lücke von 5 %. In der Praxis bricht man oft genau hier ab (siehe <a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a>).</p>
</blockquote>
<hr />
<h2 id="sec:milp-modellierungstricks-big-m-und-logische-bedingungen">6.5 Modellierungstricks: Big-M und logische Bedingungen</h2>
<p>Hier kommt das Handwerkszeug, das MILP so mächtig macht: Man übersetzt Geschäftsregeln in lineare Ungleichungen — mithilfe binärer Hilfsvariablen <span class="math inline">y \in \{0,1\}</span> und einer hinreichend großen Konstante <span class="math inline">M</span>.</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_milp_big_m.svg" alt="Abb. 6.4: Dieselbe Bedingung x \le M\,y, zwei Zahlen für M. Gezeigt ist die LP-Relaxation — also das, was der Solver sieht, bevor y ganzzahlig wird. Erzeugt von bilder_04/erzeuge_big_m_vergleich.py." />
<figcaption aria-hidden="true">Abb. 6.4: Dieselbe Bedingung <span class="math inline">x \le M\,y</span>, zwei Zahlen für <span class="math inline">M</span>. Gezeigt ist die LP-Relaxation — also das, was der Solver sieht, bevor <span class="math inline">y</span> ganzzahlig wird. Erzeugt von <code>bilder_04/erzeuge_big_m_vergleich.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> Für <span class="math inline">y \in \{0,1\}</span> sind beide Modelle gleichwertig; die beiden blauen Punkte stimmen überein. Verschieden ist nur, was dazwischen erlaubt ist — und rechts genügt bereits <span class="math inline">y = 0{,}08</span>, um die volle Kapazität freizugeben.</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="muster-1-fixkosten-aktivierungsschalter">Muster 1 — Fixkosten / Aktivierungsschalter</h3>
<p><em>Regel:</em> Wird Maschine <span class="math inline">j</span> genutzt (<span class="math inline">x_j &gt; 0</span>), fallen Rüstkosten <span class="math inline">F_j</span> an.</p>
<p><span class="math display">
x_j \le M \cdot y_j, \qquad y_j \in \{0,1\},\ x_j \ge 0
</span></p>
<ul>
<li>Ist <span class="math inline">y_j = 0</span>: erzwingt <span class="math inline">x_j \le 0</span>, zusammen mit <span class="math inline">x_j \ge 0</span> also <span class="math inline">x_j = 0</span>.</li>
<li>Ist <span class="math inline">y_j = 1</span>: gilt nur <span class="math inline">x_j \le M</span> — die Kapazität ist freigegeben.</li>
</ul>
<p>In der Zielfunktion erscheint dann <span class="math inline">+F_j y_j</span> als Kostenterm.</p>
<h3 id="muster-2-entweder-oder-disjunctive-constraints">Muster 2 — Entweder-Oder (<em>disjunctive constraints</em>)</h3>
<p><em>Regel:</em> Es muss <strong>entweder</strong> <span class="math inline">f(\mathbf{x}) \le b_1</span> <strong>oder</strong> <span class="math inline">g(\mathbf{x}) \le b_2</span> gelten.</p>
<p><span class="math display">
f(\mathbf{x}) \le b_1 + M(1-y), \qquad g(\mathbf{x}) \le b_2 + M y, \qquad y\in\{0,1\}
</span></p>
<ul>
<li><span class="math inline">y=1</span>: erste Bedingung aktiv, zweite durch <span class="math inline">+M</span> praktisch außer Kraft.</li>
<li><span class="math inline">y=0</span>: umgekehrt.</li>
</ul>
<h3 id="muster-3-wenn-dann-implikation">Muster 3 — Wenn-Dann (Implikation)</h3>
<p><em>Regel:</em> Wenn <span class="math inline">y_1 = 1</span>, dann muss auch <span class="math inline">y_2 = 1</span> sein.</p>
<p><span class="math display">
y_1 \le y_2
</span></p>
<p>Kein Big-M nötig — das ist die eleganteste Formulierung überhaupt. Prüfen Sie die vier Fälle: <span class="math inline">(0,0)</span> ✓, <span class="math inline">(0,1)</span> ✓, <span class="math inline">(1,1)</span> ✓, <span class="math inline">(1,0)</span> ✗ — genau die verbotene Kombination wird ausgeschlossen.</p>
<h3 id="muster-4-kardinalität-höchstens-k-aus-n">Muster 4 — Kardinalität („höchstens <span class="math inline">K</span> aus <span class="math inline">N</span>“)</h3>
<p><span class="math display">
\sum_{j=1}^N y_j \le K
</span></p>
<h3 id="muster-5-semikontinuierlich-entweder-0-oder-mindestens-l">Muster 5 — Semikontinuierlich („entweder 0 oder mindestens <span class="math inline">L</span>“)</h3>
<p><em>Regel:</em> Eine Position ist entweder gar nicht besetzt oder mit mindestens <span class="math inline">L</span> Euro.</p>
<p><span class="math display">
L\,y_j \le x_j \le U\,y_j
</span></p>
<p>Dieses Muster brauchen wir gleich für die Mindestordergröße.</p>
<blockquote>
<p><strong>⚠️ Die Big-M-Falle: <span class="math inline">M</span> so klein wie möglich!</strong></p>
<p><span class="math inline">M</span> muss groß genug sein, um die Bedingung wirklich außer Kraft zu setzen — aber <strong>jedes Übermaß kostet Laufzeit</strong>. Der Grund liegt in der LP-Relaxation: Mit <span class="math inline">M = 10^9</span> und <span class="math inline">x_j \le 10^9 y_j</span> genügt schon <span class="math inline">y_j = 10^{-9} \cdot x_j</span>, um die Bedingung zu erfüllen. Die Relaxation ist dann extrem schwach, die obere Schranke nutzlos, und Branch-and-Bound muss praktisch alles durchsuchen.</p>
<p><strong>Regel:</strong> Wählen Sie <span class="math inline">M</span> als kleinste Zahl, die nachweislich nie bindet — meist eine ohnehin vorhandene Kapazitätsgrenze. Im Portfoliobeispiel unten ist das die Obergrenze pro Position (40 000 €), <strong>nicht</strong> eine willkürliche Million.</p>
<p>Gemessen an der Standortplanung aus <code>Big_M_Falle.py</code> (12 Lager, 40 Kunden, HiGHS):</p>
<table>
<thead>
<tr class="header">
<th><span class="math inline">M</span></th>
<th style="text-align: right;">Knoten im Suchbaum</th>
<th>Zielwert</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>= Lagerkapazität (knappstmöglich)</td>
<td style="text-align: right;"><strong>13</strong></td>
<td>richtig</td>
</tr>
<tr class="even">
<td><span class="math inline">10\times</span> zu groß</td>
<td style="text-align: right;">51</td>
<td>richtig</td>
</tr>
<tr class="odd">
<td><span class="math inline">10^4\times</span> zu groß</td>
<td style="text-align: right;">51</td>
<td>richtig</td>
</tr>
<tr class="even">
<td><span class="math inline">10^7\times</span> zu groß</td>
<td style="text-align: right;">51</td>
<td>richtig</td>
</tr>
</tbody>
</table>
<p>Zwei Dinge sind daran bemerkenswert. Erstens: Das knappe <span class="math inline">M</span> braucht <strong>viermal weniger Knoten</strong> — der Effekt ist real. Zweitens: Ab dem Zehnfachen wird es nicht mehr schlimmer, weil HiGHS <em>Presolve</em> das übergroße <span class="math inline">M</span> selbst auf die implizit vorhandene Schranke zurechtstutzt. Verlassen Sie sich darauf <strong>nicht</strong>: Presolve kann das nur, wenn eine solche Schranke im Modell überhaupt herleitbar ist. Und wenn nicht, wird es richtig unangenehm — siehe <a href="#sec:milp-denkfehler">Abschnitt 6.10</a>.</p>
</blockquote>
<hr />
<h2 id="sec:milp-beispiel-das-rucksackproblem">6.6 Beispiel: Das Rucksackproblem</h2>
<p>Bevor wir zum Portfolio kommen, das klassische Einstiegsproblem — kurz, verständlich und überall wiederzuerkennen.</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"># Rucksack.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 MILP: Das Rucksackproblem (Knapsack).</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a><span class="co">Zeigt LP-Relaxation, Branch-and-Bound-Ergebnis und den Preis der Ganzzahligkeit</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a><span class="co">an einem Beispiel, das man vollstaendig im Kopf nachvollziehen kann.</span></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linprog</span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a>GEGENSTAENDE <span class="op">=</span> [<span class="st">&quot;Zelt&quot;</span>, <span class="st">&quot;Schlafsack&quot;</span>, <span class="st">&quot;Kocher&quot;</span>, <span class="st">&quot;Kamera&quot;</span>, <span class="st">&quot;Buch&quot;</span>, <span class="st">&quot;Wasserfilter&quot;</span>, <span class="st">&quot;Seil&quot;</span>]</span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a>NUTZEN <span class="op">=</span> np.array([<span class="fl">40.0</span>, <span class="fl">35.0</span>, <span class="fl">20.0</span>, <span class="fl">30.0</span>, <span class="fl">8.0</span>, <span class="fl">25.0</span>, <span class="fl">12.0</span>])</span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a>GEWICHT <span class="op">=</span> np.array([ <span class="fl">6.0</span>, <span class="fl">4.0</span>, <span class="fl">3.0</span>, <span class="fl">2.0</span>, <span class="fl">1.0</span>, <span class="fl">2.0</span>, <span class="fl">3.0</span>])</span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a>KAPAZITAET <span class="op">=</span> <span class="fl">11.0</span> <span class="co"># kg</span></span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese(ganzzahlig: <span class="bu">bool</span>):</span>
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a> n <span class="op">=</span> <span class="bu">len</span>(NUTZEN)</span>
<span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> linprog(</span>
<span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a> c<span class="op">=-</span>NUTZEN, A_ub<span class="op">=</span>[GEWICHT], b_ub<span class="op">=</span>[KAPAZITAET],</span>
<span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a> bounds<span class="op">=</span>[(<span class="dv">0</span>, <span class="dv">1</span>)] <span class="op">*</span> n, <span class="co"># jedes Teil hoechstens einmal</span></span>
<span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a> integrality<span class="op">=</span>np.ones(n) <span class="cf">if</span> ganzzahlig <span class="cf">else</span> <span class="va">None</span>,</span>
<span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a> method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb5-26"><a href="#cb5-26" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="op">-</span>ergebnis.fun, ergebnis.x</span>
<span id="cb5-27"><a href="#cb5-27" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-28"><a href="#cb5-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-29"><a href="#cb5-29" 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-30"><a href="#cb5-30" aria-hidden="true" tabindex="-1"></a> z_lp, x_lp <span class="op">=</span> loese(ganzzahlig<span class="op">=</span><span class="va">False</span>)</span>
<span id="cb5-31"><a href="#cb5-31" aria-hidden="true" tabindex="-1"></a> z_ip, x_ip <span class="op">=</span> loese(ganzzahlig<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb5-32"><a href="#cb5-32" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-33"><a href="#cb5-33" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">72</span>)</span>
<span id="cb5-34"><a href="#cb5-34" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; RUCKSACKPROBLEM (Kapazitaet </span><span class="sc">{</span>KAPAZITAET<span class="sc">:.0f}</span><span class="ss"> kg)&quot;</span>)</span>
<span id="cb5-35"><a href="#cb5-35" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">72</span>)</span>
<span id="cb5-36"><a href="#cb5-36" 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;Gegenstand&#39;</span><span class="sc">:&lt;14}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Nutzen&#39;</span><span class="sc">:&gt;7}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;kg&#39;</span><span class="sc">:&gt;5}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Nutzen/kg&#39;</span><span class="sc">:&gt;10}</span><span class="ss"> &quot;</span></span>
<span id="cb5-37"><a href="#cb5-37" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;LP&#39;</span><span class="sc">:&gt;7}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;MILP&#39;</span><span class="sc">:&gt;6}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-38"><a href="#cb5-38" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">72</span>)</span>
<span id="cb5-39"><a href="#cb5-39" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i, name <span class="kw">in</span> <span class="bu">enumerate</span>(GEGENSTAENDE):</span>
<span id="cb5-40"><a href="#cb5-40" aria-hidden="true" tabindex="-1"></a> <span class="co"># int(round(...)) statt Format &quot;%.0f&quot;: vermeidet die Ausgabe &quot;-0&quot;</span></span>
<span id="cb5-41"><a href="#cb5-41" 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;14}</span><span class="ss"> </span><span class="sc">{</span>NUTZEN[i]<span class="sc">:&gt;7.0f}</span><span class="ss"> </span><span class="sc">{</span>GEWICHT[i]<span class="sc">:&gt;5.0f}</span><span class="ss"> &quot;</span></span>
<span id="cb5-42"><a href="#cb5-42" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>NUTZEN[i]<span class="op">/</span>GEWICHT[i]<span class="sc">:&gt;10.2f}</span><span class="ss"> </span><span class="sc">{</span>x_lp[i]<span class="sc">:&gt;7.2f}</span><span class="ss"> </span><span class="sc">{</span><span class="bu">int</span>(<span class="bu">round</span>(x_ip[i]))<span class="sc">:&gt;6d}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-43"><a href="#cb5-43" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">72</span>)</span>
<span id="cb5-44"><a href="#cb5-44" 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;Gesamtnutzen&#39;</span><span class="sc">:&lt;14}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;7}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;5}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;10}</span><span class="ss"> </span><span class="sc">{</span>z_lp<span class="sc">:&gt;7.2f}</span><span class="ss"> </span><span class="sc">{</span>z_ip<span class="sc">:&gt;6.0f}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-45"><a href="#cb5-45" 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;Gesamtgewicht&#39;</span><span class="sc">:&lt;14}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;7}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;5}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;10}</span><span class="ss"> &quot;</span></span>
<span id="cb5-46"><a href="#cb5-46" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>GEWICHT <span class="op">@</span> x_lp<span class="sc">:&gt;7.2f}</span><span class="ss"> </span><span class="sc">{</span>GEWICHT <span class="op">@</span> x_ip<span class="sc">:&gt;6.0f}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-47"><a href="#cb5-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">72</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;Obere Schranke aus der LP-Relaxation: </span><span class="sc">{</span>z_lp<span class="sc">:.2f}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-49"><a href="#cb5-49" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Bestes ganzzahliges Ergebnis: </span><span class="sc">{</span>z_ip<span class="sc">:.0f}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-50"><a href="#cb5-50" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Preis der Ganzzahligkeit: </span><span class="sc">{</span>z_lp <span class="op">-</span> z_ip<span class="sc">:.2f}</span><span class="ss"> &quot;</span></span>
<span id="cb5-51"><a href="#cb5-51" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span>(<span class="dv">1</span> <span class="op">-</span> z_ip<span class="op">/</span>z_lp)<span class="op">*</span><span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> %)&quot;</span>)</span>
<span id="cb5-52"><a href="#cb5-52" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-53"><a href="#cb5-53" aria-hidden="true" tabindex="-1"></a> gebrochen <span class="op">=</span> [GEGENSTAENDE[i] <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(NUTZEN)) <span class="cf">if</span> <span class="fl">1e-6</span> <span class="op">&lt;</span> x_lp[i] <span class="op">&lt;</span> <span class="dv">1</span> <span class="op">-</span> <span class="fl">1e-6</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;</span><span class="ch">\n</span><span class="ss">In der LP-Loesung nur teilweise eingepackt: </span><span class="sc">{</span>gebrochen<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-55"><a href="#cb5-55" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Genau hier wuerde Branch-and-Bound verzweigen:&quot;</span>)</span>
<span id="cb5-56"><a href="#cb5-56" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Ast 1: </span><span class="sc">{</span>gebrochen[<span class="dv">0</span>]<span class="sc">}</span><span class="ss"> bleibt ganz zuhause (x=0)&quot;</span>)</span>
<span id="cb5-57"><a href="#cb5-57" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Ast 2: </span><span class="sc">{</span>gebrochen[<span class="dv">0</span>]<span class="sc">}</span><span class="ss"> kommt ganz mit (x=1)&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">72</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>========================================================================
RUCKSACKPROBLEM (Kapazitaet 11 kg)
========================================================================
Gegenstand Nutzen kg Nutzen/kg LP MILP
------------------------------------------------------------------------
Zelt 40 6 6.67 0.00 0
Schlafsack 35 4 8.75 1.00 1
Kocher 20 3 6.67 0.67 1
Kamera 30 2 15.00 1.00 1
Buch 8 1 8.00 1.00 0
Wasserfilter 25 2 12.50 1.00 1
Seil 12 3 4.00 0.00 0
------------------------------------------------------------------------
Gesamtnutzen 111.33 110
Gesamtgewicht 11.00 11
------------------------------------------------------------------------
Obere Schranke aus der LP-Relaxation: 111.33
Bestes ganzzahliges Ergebnis: 110
Preis der Ganzzahligkeit: 1.33 (1.2 %)
In der LP-Loesung nur teilweise eingepackt: [&#39;Kocher&#39;]
Genau hier wuerde Branch-and-Bound verzweigen:
Ast 1: Kocher bleibt ganz zuhause (x=0)
Ast 2: Kocher kommt ganz mit (x=1)
========================================================================</code></pre>
<p>Drei Beobachtungen, die den Kern des Kapitels illustrieren:</p>
<ol type="1">
<li><strong>Die LP-Relaxation packt zwei Drittel Kocher ein</strong> — physikalisch unsinnig, als <strong>Schranke</strong> aber wertvoll: „Mehr als 111,33 ist unmöglich.“</li>
<li><strong>Runden hätte hier versagt.</strong> Abrunden des Kochers auf 0 ergäbe <span class="math inline">35+30+8+25 = 98</span> bei nur 9 kg — zwölf Punkte schlechter als das Optimum, und drei Kilo Kapazität bleiben ungenutzt. Aufrunden auf 1 ergäbe 12 kg und wäre <strong>unzulässig</strong>. Das exakte Optimum tauscht stattdessen das <em>Buch</em> gegen den <em>Kocher</em> — eine Umschichtung, auf die kein Rundungsverfahren kommt, weil sie eine bereits „fertige“ Variable wieder verändert.</li>
<li><strong>Die Lücke ist klein</strong> (1,2 %). Ein Solver, der frühzeitig bei 110 abbricht, kann beweisen, höchstens 1,2 % vom Optimum entfernt zu sein — ohne alle Äste zu durchsuchen. Genau das ist der MIP-Gap aus <a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a>.</li>
</ol>
<hr />
<h2 id="sec:milp-praxisfall-portfolio-mit-ordergebuehren-und-kardinalitaetsgrenze">6.7 Praxisfall: Portfolio mit Ordergebühren und Kardinalitätsgrenze</h2>
<p><strong>Szenario.</strong> Ein Investor verteilt <span class="math inline">100\,000\,\text{€}</span> auf sechs Anlageklassen.</p>
<ul>
<li>Jede Transaktion kostet <strong>50 € Fixgebühr</strong>, unabhängig vom Betrag.</li>
<li>Höchstens <strong>3 Positionen</strong> gleichzeitig (<em><a href="anhang-glossar.md#gloss:kardinalitaetsbeschraenkung" class="glossar-link">Kardinalitätsbeschränkung</a></em>).</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>Wird eine Position eröffnet, dann mit <strong>mindestens 10 000 €</strong> (Mindestordergröße).</li>
<li>Höchstens <strong>40 000 €</strong> pro Position.</li>
</ul>
<p><strong>Modell.</strong> Für jede Anlage <span class="math inline">i</span>: <span class="math inline">x_i \ge 0</span> (Euro-Betrag) und <span class="math inline">y_i \in \{0,1\}</span> (Position aktiv).</p>
<p><span class="math display">
\max \sum_{i=1}^n \mu_i x_i - F\sum_{i=1}^n y_i
</span></p>
<p><span class="math display">
\begin{aligned}
\sum_i x_i &amp;= B &amp;&amp; \text{(Budget vollständig investiert)}\\
\sum_i y_i &amp;\le K &amp;&amp; \text{(höchstens $K$ Positionen)}\\
L\,y_i \le x_i &amp;\le U\,y_i &amp;&amp; \forall i \quad \text{(semikontinuierlich, Muster 5)}
\end{aligned}
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong> * <span class="math inline">\mu_i</span> — erwartete Jahresrendite der Anlage <span class="math inline">i</span> (z. B. 0,11 = 11 %). * <span class="math inline">\mu_i x_i</span> — erwarteter Ertrag in Euro. * <span class="math inline">F\sum y_i</span> — Summe der Ordergebühren: 50 € je aktivierter Position. * <span class="math inline">L y_i \le x_i \le U y_i</span> — die Doppelungleichung erledigt <strong>beides</strong> auf einmal: Ist <span class="math inline">y_i=0</span>, folgt <span class="math inline">0 \le x_i \le 0</span>, also <span class="math inline">x_i=0</span>. Ist <span class="math inline">y_i=1</span>, folgt <span class="math inline">10\,000 \le x_i \le 40\,000</span>.</p>
<p><strong>Ohne Formel gesagt:</strong> „Investiere das ganze Budget in höchstens drei Töpfe, jeweils zwischen 10 000 und 40 000 Euro, und ziehe für jeden benutzten Topf 50 Euro Gebühr ab.“</p>
</blockquote>
<blockquote>
<p><strong>⚠️ Eine ökonomische Unsauberkeit, die Sie kennen sollten</strong></p>
<p>Das Modell verrechnet einen <strong>einmaligen</strong> Gebührenbetrag (50 €) mit einem <strong>jährlichen</strong> Ertrag (<span class="math inline">\mu_i x_i</span>). Streng genommen mischt das Einheiten — bei einer Haltedauer von einem Jahr geht es auf, bei zehn Jahren wären die Gebühren zehnfach zu schwer gewichtet. Für das Beispiel ist der Effekt klein (150 € gegen ~11 000 € Ertrag), aber in einem echten Modell würde man entweder die Gebühr annualisieren oder mit Barwerten rechnen.</p>
</blockquote>
<blockquote>
<p><strong>📎 Zwei Bausteine aus <code>or_kern.py</code></strong></p>
Phase 6.2: Constraint Attribution und Managementbericht Neuer Abschnitt im Kapitel Praxisfallen plus Constraint_Attribution.py (75. Programm). Setzt den fehlenden Teil von Paket 3 aus Verbesserungen_02.md um. Die Luecke, die es schliesst: Erklaerbarkeit.py beantwortet "warum DIESE Zuweisung?", Konfliktsuche.py "warum geht es GAR NICHT?". Der haeufigste Fall der Praxis lag dazwischen und hatte kein Werkzeug - das Modell rechnet, der Plan ist zulaessig, und trotzdem enttaeuschend, nur weiss niemand woran es liegt. Gerechnet wird auf derselben Fabrik wie die Konfliktsuche, nach der Reparatur: Dort war die Lackierkapazitaet von 150 Stunden der Kern des Widerspruchs (2*40 + 3*30 = 170 > 150), eine zweite Schicht bringt sie auf 210. Damit laesst sich dieselbe Instanz ueber zwei Kapitel verfolgen - erst "warum nicht?", dann "was kostet was?". Drei gemessene Befunde: * Bindend heisst nicht teuer: Fuenf Bedingungen binden, "Lackierbudget Schicht 2" mit einem Schattenpreis von 0,00 EUR. * Der Schattenpreis ist eine Momentaufnahme: Fuer die Lackiererei verspricht die Hochrechnung 4,00 * 30 = 120 EUR, gemessen kommen 60 EUR; der Preis gilt exakt bis +15 Stunden. Die Ranglisten drehen sich dabei um - nach Schattenpreis fuehrt der Traegervertrag (18 EUR/Stueck), nach tatsaechlicher Wirkung die Montage-Sonderschicht (450 EUR). * Ein Wunsch kann unmoeglich statt teuer sein: "mindestens 55 Rahmen" ist INFEASIBLE, blockiert von drei Bedingungen gemeinsam. Mit +15 Stunden Lackierkapazitaet wird er erfuellbar und kostet dann nichts - der Plan waehlt diese Menge von sich aus. Der Konflikt bestand nie zwischen Vertrieb und Produktion. Der Managementbericht entsteht aus genau diesen Zahlen; kein Satz enthaelt eine Zahl, die nicht vorher gerechnet wurde. Voraussetzung ist der sprechende Name jeder Bedingung - aus A_ub[7] wird kein Satz. Dazu die Grenze, als Warnkasten und als Uebungsaufgabe: Der Bericht erklaert das Modell, nicht die Wirklichkeit. Steigt der Deckungsbeitrag von Deckel von 9 auf 20 EUR, kippt der Plan (Rahmen auf das Vertragsminimum 40, Deckel auf die Marktgrenze 120), "Kapazitaet Lackieren" hoert auf zu binden, "Liefervertrag Rahmen" faengt damit an - und der Satz "Teuerste Bindung ist Liefervertrag Traeger" steht woertlich unveraendert im Bericht, waehrend die Zahl dahinter von 18 auf 58 EUR springt. Die Zaehlprobe aus 6.1a hat sich sofort bewaehrt: Nach dem Einbau der Uebungsaufgabe meldete --check "6 Loesungen, das Kapitel aber 7 Aufgaben", bevor die Loesung geschrieben war. Mitgezogen: Kapitelkopf, Lernziele, Selbsttest, Zusammenfassung, Vorwort-Programmverzeichnis, Vorwaertsverweis aus Falle 2, Uebungsaufgabe und Loesung in Anhang A. Stand: 294 Abschnitte, 725 Querverweise, 328 Indexmarken, 75 Programme, 139 Aufgaben mit 139 Loesungen, 33 pytest-Tests, PDF 737 Seiten, 68 netzfreie Programme fehlerfrei. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 10:30:33 +02:00
<p>Das Programm wertet den Solver nicht über <code>ergebnis.success</code> aus, sondern über <code>status_von_scipy()</code> und das <code>Loesung</code>-Objekt aus dem gemeinsamen Unterbau (<a href="praxisfallen.html#sec:praxisfallen-or-kern">Abschnitt 22.6</a>). Der Unterschied ist kein Schönheitsfehler:</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
<ul>
<li><code>success</code> ist <strong>ein Bit</strong>. Es unterscheidet nicht zwischen „es gibt keine Lösung“ (Modellfehler — das Modell muss geändert werden) und „die Zeit war um“ (Rechenproblem — mehr Zeit oder ein besserer Startwert hilft). Das sind zwei völlig verschiedene Nachrichten an völlig verschiedene Adressaten.</li>
<li>Das <code>Loesung</code>-Objekt führt neben dem Zielwert die <strong>Schranke</strong> mit und rechnet daraus den Gap aus. Erst dadurch steht in der Ausgabe <code>Gap: 0.00%</code> — die Zusage, dass hier wirklich das Optimum gefunden <em>und bewiesen</em> wurde und nicht bloß irgendetwas.</li>
</ul>
<p>Genau die Unterscheidung, um die es im nächsten Abschnitt geht — hier schon einmal angewandt.</p>
</blockquote>
<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><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a><span class="co"># MILP_Portfolio_Fixgebuehren.py</span></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel MILP: MILP-Portfolio-Selektion mit Fixkosten und Kardinalitaet.</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a><span class="co">CP-SAT-freie, gut lesbare Formulierung ueber scipy/HiGHS (kein manueller</span></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a><span class="co">CSR-Matrixaufbau, deutlich leichter nachvollziehbar), mit Vergleich gegen die</span></span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a><span class="co">Loesung OHNE Restriktionen.</span></span>
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a><span class="co">Die Auswertung laeuft ueber SolverStatus und das Loesung-Objekt aus</span></span>
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a><span class="co">or_kern.py: Alle Statusfaelle werden behandelt, und der MIP-Gap steht im</span></span>
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a><span class="co">Bericht - statt eines blossen &#39;success&#39;, das nicht verraet, ob der Solver</span></span>
<span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a><span class="co">fertig geworden ist oder nur aufgegeben hat.</span></span>
<span id="cb7-15"><a href="#cb7-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-16"><a href="#cb7-16" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, pandas, scipy, pydantic (ueber or_kern)</span></span>
<span id="cb7-17"><a href="#cb7-17" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb7-18"><a href="#cb7-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-19"><a href="#cb7-19" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb7-20"><a href="#cb7-20" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-21"><a href="#cb7-21" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> time</span>
<span id="cb7-22"><a href="#cb7-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-23"><a href="#cb7-23" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb7-24"><a href="#cb7-24" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> pandas <span class="im">as</span> pd</span>
<span id="cb7-25"><a href="#cb7-25" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linprog</span>
<span id="cb7-26"><a href="#cb7-26" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-27"><a href="#cb7-27" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> or_kern <span class="im">import</span> Loesung, SolverStatus, status_von_scipy</span>
<span id="cb7-28"><a href="#cb7-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-29"><a href="#cb7-29" aria-hidden="true" tabindex="-1"></a>ANLAGEN <span class="op">=</span> [<span class="st">&quot;US-Aktien&quot;</span>, <span class="st">&quot;EU-Aktien&quot;</span>, <span class="st">&quot;Emerging-Markets&quot;</span>,</span>
<span id="cb7-30"><a href="#cb7-30" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Staatsanleihen&quot;</span>, <span class="st">&quot;Unternehmensanleihen&quot;</span>, <span class="st">&quot;Rohstoffe&quot;</span>]</span>
<span id="cb7-31"><a href="#cb7-31" aria-hidden="true" tabindex="-1"></a>RENDITE <span class="op">=</span> np.array([<span class="fl">0.11</span>, <span class="fl">0.08</span>, <span class="fl">0.13</span>, <span class="fl">0.03</span>, <span class="fl">0.05</span>, <span class="fl">0.07</span>]) <span class="co"># erwartet, p.a.</span></span>
<span id="cb7-32"><a href="#cb7-32" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-33"><a href="#cb7-33" aria-hidden="true" tabindex="-1"></a>BUDGET <span class="op">=</span> <span class="fl">100_000.0</span></span>
<span id="cb7-34"><a href="#cb7-34" aria-hidden="true" tabindex="-1"></a>MIN_POSITION <span class="op">=</span> <span class="fl">10_000.0</span> <span class="co"># L</span></span>
<span id="cb7-35"><a href="#cb7-35" aria-hidden="true" tabindex="-1"></a>MAX_POSITION <span class="op">=</span> <span class="fl">40_000.0</span> <span class="co"># U (dient zugleich als Big-M!)</span></span>
<span id="cb7-36"><a href="#cb7-36" aria-hidden="true" tabindex="-1"></a>GEBUEHR <span class="op">=</span> <span class="fl">50.0</span> <span class="co"># F, je aktivierter Position</span></span>
<span id="cb7-37"><a href="#cb7-37" aria-hidden="true" tabindex="-1"></a>MAX_POSITIONEN <span class="op">=</span> <span class="dv">3</span> <span class="co"># K</span></span>
<span id="cb7-38"><a href="#cb7-38" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-39"><a href="#cb7-39" aria-hidden="true" tabindex="-1"></a>N <span class="op">=</span> <span class="bu">len</span>(ANLAGEN)</span>
<span id="cb7-40"><a href="#cb7-40" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-41"><a href="#cb7-41" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-42"><a href="#cb7-42" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> baue_und_loese(zeitlimit: <span class="bu">float</span> <span class="op">|</span> <span class="va">None</span> <span class="op">=</span> <span class="va">None</span>) <span class="op">-&gt;</span> Loesung:</span>
<span id="cb7-43"><a href="#cb7-43" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;</span></span>
<span id="cb7-44"><a href="#cb7-44" aria-hidden="true" tabindex="-1"></a><span class="co"> Variablenreihenfolge: [x_0..x_{N-1}, y_0..y_{N-1}]</span></span>
<span id="cb7-45"><a href="#cb7-45" aria-hidden="true" tabindex="-1"></a><span class="co"> Zielfunktion (Maximierung -&gt; fuer linprog negiert):</span></span>
<span id="cb7-46"><a href="#cb7-46" aria-hidden="true" tabindex="-1"></a><span class="co"> max sum(rendite_i * x_i) - GEBUEHR * sum(y_i)</span></span>
<span id="cb7-47"><a href="#cb7-47" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb7-48"><a href="#cb7-48" aria-hidden="true" tabindex="-1"></a> c <span class="op">=</span> np.concatenate([<span class="op">-</span>RENDITE, np.full(N, GEBUEHR)]) <span class="co"># negiert = Minimierung</span></span>
<span id="cb7-49"><a href="#cb7-49" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-50"><a href="#cb7-50" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Gleichungsnebenbedingung: Budget vollstaendig investiert -----------</span></span>
<span id="cb7-51"><a href="#cb7-51" aria-hidden="true" tabindex="-1"></a> A_eq <span class="op">=</span> np.zeros((<span class="dv">1</span>, <span class="dv">2</span> <span class="op">*</span> N))</span>
<span id="cb7-52"><a href="#cb7-52" aria-hidden="true" tabindex="-1"></a> A_eq[<span class="dv">0</span>, :N] <span class="op">=</span> <span class="fl">1.0</span></span>
<span id="cb7-53"><a href="#cb7-53" aria-hidden="true" tabindex="-1"></a> b_eq <span class="op">=</span> np.array([BUDGET])</span>
<span id="cb7-54"><a href="#cb7-54" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-55"><a href="#cb7-55" aria-hidden="true" tabindex="-1"></a> zeilen, grenzen <span class="op">=</span> [], []</span>
<span id="cb7-56"><a href="#cb7-56" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-57"><a href="#cb7-57" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Kardinalitaet: sum(y_i) &lt;= K --------------------------------------</span></span>
<span id="cb7-58"><a href="#cb7-58" aria-hidden="true" tabindex="-1"></a> zeile <span class="op">=</span> np.zeros(<span class="dv">2</span> <span class="op">*</span> N)</span>
<span id="cb7-59"><a href="#cb7-59" aria-hidden="true" tabindex="-1"></a> zeile[N:] <span class="op">=</span> <span class="fl">1.0</span></span>
<span id="cb7-60"><a href="#cb7-60" aria-hidden="true" tabindex="-1"></a> zeilen.append(zeile)</span>
<span id="cb7-61"><a href="#cb7-61" aria-hidden="true" tabindex="-1"></a> grenzen.append(MAX_POSITIONEN)</span>
<span id="cb7-62"><a href="#cb7-62" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-63"><a href="#cb7-63" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Obergrenze (Big-M): x_i - U*y_i &lt;= 0 -----------------------------</span></span>
<span id="cb7-64"><a href="#cb7-64" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N):</span>
<span id="cb7-65"><a href="#cb7-65" aria-hidden="true" tabindex="-1"></a> zeile <span class="op">=</span> np.zeros(<span class="dv">2</span> <span class="op">*</span> N)</span>
<span id="cb7-66"><a href="#cb7-66" aria-hidden="true" tabindex="-1"></a> zeile[i] <span class="op">=</span> <span class="fl">1.0</span></span>
<span id="cb7-67"><a href="#cb7-67" aria-hidden="true" tabindex="-1"></a> zeile[N <span class="op">+</span> i] <span class="op">=</span> <span class="op">-</span>MAX_POSITION</span>
<span id="cb7-68"><a href="#cb7-68" aria-hidden="true" tabindex="-1"></a> zeilen.append(zeile)</span>
<span id="cb7-69"><a href="#cb7-69" aria-hidden="true" tabindex="-1"></a> grenzen.append(<span class="fl">0.0</span>)</span>
<span id="cb7-70"><a href="#cb7-70" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-71"><a href="#cb7-71" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Untergrenze: L*y_i - x_i &lt;= 0 (entspricht x_i &gt;= L*y_i) --------</span></span>
<span id="cb7-72"><a href="#cb7-72" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N):</span>
<span id="cb7-73"><a href="#cb7-73" aria-hidden="true" tabindex="-1"></a> zeile <span class="op">=</span> np.zeros(<span class="dv">2</span> <span class="op">*</span> N)</span>
<span id="cb7-74"><a href="#cb7-74" aria-hidden="true" tabindex="-1"></a> zeile[i] <span class="op">=</span> <span class="op">-</span><span class="fl">1.0</span></span>
<span id="cb7-75"><a href="#cb7-75" aria-hidden="true" tabindex="-1"></a> zeile[N <span class="op">+</span> i] <span class="op">=</span> MIN_POSITION</span>
<span id="cb7-76"><a href="#cb7-76" aria-hidden="true" tabindex="-1"></a> zeilen.append(zeile)</span>
<span id="cb7-77"><a href="#cb7-77" aria-hidden="true" tabindex="-1"></a> grenzen.append(<span class="fl">0.0</span>)</span>
<span id="cb7-78"><a href="#cb7-78" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-79"><a href="#cb7-79" aria-hidden="true" tabindex="-1"></a> A_ub <span class="op">=</span> np.array(zeilen)</span>
<span id="cb7-80"><a href="#cb7-80" aria-hidden="true" tabindex="-1"></a> b_ub <span class="op">=</span> np.array(grenzen)</span>
<span id="cb7-81"><a href="#cb7-81" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-82"><a href="#cb7-82" aria-hidden="true" tabindex="-1"></a> schranken <span class="op">=</span> [(<span class="fl">0.0</span>, MAX_POSITION)] <span class="op">*</span> N <span class="op">+</span> [(<span class="fl">0.0</span>, <span class="fl">1.0</span>)] <span class="op">*</span> N</span>
<span id="cb7-83"><a href="#cb7-83" aria-hidden="true" tabindex="-1"></a> ganzzahligkeit <span class="op">=</span> np.concatenate([np.zeros(N), np.ones(N)]) <span class="co"># y binaer</span></span>
<span id="cb7-84"><a href="#cb7-84" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-85"><a href="#cb7-85" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()</span>
<span id="cb7-86"><a href="#cb7-86" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> linprog(c<span class="op">=</span>c, A_ub<span class="op">=</span>A_ub, b_ub<span class="op">=</span>b_ub, A_eq<span class="op">=</span>A_eq, b_eq<span class="op">=</span>b_eq,</span>
<span id="cb7-87"><a href="#cb7-87" aria-hidden="true" tabindex="-1"></a> bounds<span class="op">=</span>schranken, integrality<span class="op">=</span>ganzzahligkeit,</span>
<span id="cb7-88"><a href="#cb7-88" aria-hidden="true" tabindex="-1"></a> method<span class="op">=</span><span class="st">&quot;highs&quot;</span>,</span>
<span id="cb7-89"><a href="#cb7-89" aria-hidden="true" tabindex="-1"></a> options<span class="op">=</span>{<span class="st">&quot;time_limit&quot;</span>: zeitlimit} <span class="cf">if</span> zeitlimit <span class="cf">else</span> <span class="va">None</span>)</span>
<span id="cb7-90"><a href="#cb7-90" aria-hidden="true" tabindex="-1"></a> laufzeit <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
<span id="cb7-91"><a href="#cb7-91" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-92"><a href="#cb7-92" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> status_von_scipy(ergebnis)</span>
<span id="cb7-93"><a href="#cb7-93" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> status.brauchbar:</span>
<span id="cb7-94"><a href="#cb7-94" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Loesung(status<span class="op">=</span>status, laufzeit<span class="op">=</span>laufzeit)</span>
<span id="cb7-95"><a href="#cb7-95" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-96"><a href="#cb7-96" aria-hidden="true" tabindex="-1"></a> <span class="co"># Zurueck in die Maximierungswelt: Zielwert UND Schranke negieren.</span></span>
<span id="cb7-97"><a href="#cb7-97" aria-hidden="true" tabindex="-1"></a> <span class="co"># Aus beiden zusammen rechnet das Loesung-Objekt den MIP-Gap aus.</span></span>
<span id="cb7-98"><a href="#cb7-98" aria-hidden="true" tabindex="-1"></a> x, y <span class="op">=</span> ergebnis.x[:N], np.<span class="bu">round</span>(ergebnis.x[N:])</span>
<span id="cb7-99"><a href="#cb7-99" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Loesung(</span>
<span id="cb7-100"><a href="#cb7-100" aria-hidden="true" tabindex="-1"></a> status<span class="op">=</span>status,</span>
<span id="cb7-101"><a href="#cb7-101" aria-hidden="true" tabindex="-1"></a> werte<span class="op">=</span>{<span class="op">**</span>{name: <span class="bu">float</span>(w) <span class="cf">for</span> name, w <span class="kw">in</span> <span class="bu">zip</span>(ANLAGEN, x)},</span>
<span id="cb7-102"><a href="#cb7-102" aria-hidden="true" tabindex="-1"></a> <span class="op">**</span>{<span class="ss">f&quot;aktiv:</span><span class="sc">{</span>name<span class="sc">}</span><span class="ss">&quot;</span>: <span class="bu">float</span>(w) <span class="cf">for</span> name, w <span class="kw">in</span> <span class="bu">zip</span>(ANLAGEN, y)}},</span>
<span id="cb7-103"><a href="#cb7-103" aria-hidden="true" tabindex="-1"></a> zielwert<span class="op">=</span><span class="bu">float</span>(<span class="op">-</span>ergebnis.fun),</span>
<span id="cb7-104"><a href="#cb7-104" aria-hidden="true" tabindex="-1"></a> schranke<span class="op">=</span><span class="bu">float</span>(<span class="op">-</span>ergebnis.mip_dual_bound),</span>
<span id="cb7-105"><a href="#cb7-105" aria-hidden="true" tabindex="-1"></a> laufzeit<span class="op">=</span>laufzeit)</span>
<span id="cb7-106"><a href="#cb7-106" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-107"><a href="#cb7-107" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-108"><a href="#cb7-108" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> ohne_restriktionen() <span class="op">-&gt;</span> <span class="bu">tuple</span>[<span class="bu">float</span>, np.ndarray]:</span>
<span id="cb7-109"><a href="#cb7-109" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Vergleichsfall: nur Budget, keine Gebuehren/Kardinalitaet/Mindestgroesse.&quot;&quot;&quot;</span></span>
<span id="cb7-110"><a href="#cb7-110" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> linprog(c<span class="op">=-</span>RENDITE, A_eq<span class="op">=</span>[np.ones(N)], b_eq<span class="op">=</span>[BUDGET],</span>
<span id="cb7-111"><a href="#cb7-111" 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> N, method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb7-112"><a href="#cb7-112" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> status_von_scipy(ergebnis)</span>
<span id="cb7-113"><a href="#cb7-113" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> status.brauchbar:</span>
<span id="cb7-114"><a href="#cb7-114" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">SystemExit</span>(<span class="ss">f&quot;Vergleichsfall nicht loesbar: </span><span class="sc">{</span>status<span class="sc">.</span>value<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-115"><a href="#cb7-115" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="op">-</span>ergebnis.fun, ergebnis.x</span>
<span id="cb7-116"><a href="#cb7-116" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-117"><a href="#cb7-117" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-118"><a href="#cb7-118" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> pruefe_portfolio(loesung: Loesung, toleranz: <span class="bu">float</span> <span class="op">=</span> <span class="fl">1e-6</span>) <span class="op">-&gt;</span> <span class="bu">list</span>[<span class="bu">str</span>]:</span>
<span id="cb7-119"><a href="#cb7-119" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Prueft die Loesung gegen die Anforderungen - ohne den Solver zu fragen.</span></span>
<span id="cb7-120"><a href="#cb7-120" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-121"><a href="#cb7-121" aria-hidden="true" tabindex="-1"></a><span class="co"> Bewusst kein assert: Eine Beanstandungsliste laesst sich protokollieren,</span></span>
<span id="cb7-122"><a href="#cb7-122" aria-hidden="true" tabindex="-1"></a><span class="co"> weiterreichen und testen. Ein assert verschwindet ausserdem, sobald</span></span>
<span id="cb7-123"><a href="#cb7-123" aria-hidden="true" tabindex="-1"></a><span class="co"> jemand Python mit -O startet.</span></span>
<span id="cb7-124"><a href="#cb7-124" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb7-125"><a href="#cb7-125" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> np.array([loesung.werte[name] <span class="cf">for</span> name <span class="kw">in</span> ANLAGEN])</span>
<span id="cb7-126"><a href="#cb7-126" aria-hidden="true" tabindex="-1"></a> y <span class="op">=</span> np.array([loesung.werte[<span class="ss">f&quot;aktiv:</span><span class="sc">{</span>name<span class="sc">}</span><span class="ss">&quot;</span>] <span class="cf">for</span> name <span class="kw">in</span> ANLAGEN])</span>
<span id="cb7-127"><a href="#cb7-127" aria-hidden="true" tabindex="-1"></a> beanstandungen: <span class="bu">list</span>[<span class="bu">str</span>] <span class="op">=</span> []</span>
<span id="cb7-128"><a href="#cb7-128" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-129"><a href="#cb7-129" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(x.<span class="bu">sum</span>() <span class="op">-</span> BUDGET) <span class="op">&gt;</span> <span class="fl">1e-4</span>:</span>
<span id="cb7-130"><a href="#cb7-130" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;Budget nicht exakt investiert: </span><span class="sc">{</span>x<span class="sc">.</span><span class="bu">sum</span>()<span class="sc">:,.2f}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-131"><a href="#cb7-131" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> y.<span class="bu">sum</span>() <span class="op">&gt;</span> MAX_POSITIONEN <span class="op">+</span> toleranz:</span>
<span id="cb7-132"><a href="#cb7-132" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;</span><span class="sc">{</span>y<span class="sc">.</span><span class="bu">sum</span>()<span class="sc">:.0f}</span><span class="ss"> Positionen statt hoechstens &quot;</span></span>
<span id="cb7-133"><a href="#cb7-133" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>MAX_POSITIONEN<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-134"><a href="#cb7-134" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i, name <span class="kw">in</span> <span class="bu">enumerate</span>(ANLAGEN):</span>
<span id="cb7-135"><a href="#cb7-135" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(y[i] <span class="op">-</span> <span class="bu">round</span>(y[i])) <span class="op">&gt;</span> toleranz:</span>
<span id="cb7-136"><a href="#cb7-136" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">}</span><span class="ss">: y = </span><span class="sc">{</span>y[i]<span class="sc">!r}</span><span class="ss"> ist nicht ganzzahlig&quot;</span>)</span>
<span id="cb7-137"><a href="#cb7-137" aria-hidden="true" tabindex="-1"></a> <span class="cf">elif</span> y[i] <span class="kw">and</span> <span class="kw">not</span> (MIN_POSITION <span class="op">-</span> toleranz <span class="op">&lt;=</span> x[i]</span>
<span id="cb7-138"><a href="#cb7-138" aria-hidden="true" tabindex="-1"></a> <span class="op">&lt;=</span> MAX_POSITION <span class="op">+</span> toleranz):</span>
<span id="cb7-139"><a href="#cb7-139" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">}</span><span class="ss">: </span><span class="sc">{</span>x[i]<span class="sc">:,.2f}</span><span class="ss"> EUR verletzt die &quot;</span></span>
<span id="cb7-140"><a href="#cb7-140" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Groessengrenzen&quot;</span>)</span>
<span id="cb7-141"><a href="#cb7-141" aria-hidden="true" tabindex="-1"></a> <span class="cf">elif</span> <span class="kw">not</span> y[i] <span class="kw">and</span> x[i] <span class="op">&gt;</span> toleranz:</span>
<span id="cb7-142"><a href="#cb7-142" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">}</span><span class="ss">: inaktiv, aber </span><span class="sc">{</span>x[i]<span class="sc">:,.2f}</span><span class="ss"> EUR &quot;</span></span>
<span id="cb7-143"><a href="#cb7-143" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;investiert&quot;</span>)</span>
<span id="cb7-144"><a href="#cb7-144" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> beanstandungen</span>
<span id="cb7-145"><a href="#cb7-145" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-146"><a href="#cb7-146" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-147"><a href="#cb7-147" 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="cb7-148"><a href="#cb7-148" aria-hidden="true" tabindex="-1"></a> loesung <span class="op">=</span> baue_und_loese()</span>
<span id="cb7-149"><a href="#cb7-149" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-150"><a href="#cb7-150" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">88</span>)</span>
<span id="cb7-151"><a href="#cb7-151" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; OPTIMALE MILP-PORTFOLIO-ALLOKATION MIT FIXGEBUEHREN&quot;</span>)</span>
<span id="cb7-152"><a href="#cb7-152" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">88</span>)</span>
<span id="cb7-153"><a href="#cb7-153" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Budget: </span><span class="sc">{</span>BUDGET<span class="sc">:,.0f}</span><span class="ss"> EUR | max. </span><span class="sc">{</span>MAX_POSITIONEN<span class="sc">}</span><span class="ss"> Positionen | &quot;</span></span>
<span id="cb7-154"><a href="#cb7-154" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;je </span><span class="sc">{</span>MIN_POSITION<span class="sc">:,.0f}</span><span class="ss">-</span><span class="sc">{</span>MAX_POSITION<span class="sc">:,.0f}</span><span class="ss"> EUR | Gebuehr </span><span class="sc">{</span>GEBUEHR<span class="sc">:.0f}</span><span class="ss"> EUR</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb7-155"><a href="#cb7-155" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-156"><a href="#cb7-156" aria-hidden="true" tabindex="-1"></a> <span class="co"># Zuerst der Status - erst danach interessieren die Zahlen.</span></span>
<span id="cb7-157"><a href="#cb7-157" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> loesung.status.modellfehler:</span>
<span id="cb7-158"><a href="#cb7-158" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">SystemExit</span>(<span class="ss">f&quot;Das Modell ist nicht loesbar (</span><span class="sc">{</span>loesung<span class="sc">.</span>status<span class="sc">.</span>value<span class="sc">}</span><span class="ss">). &quot;</span></span>
<span id="cb7-159"><a href="#cb7-159" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Naechster Schritt: Anhang Fehlerdiagnose.&quot;</span>)</span>
<span id="cb7-160"><a href="#cb7-160" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> loesung.status.brauchbar:</span>
<span id="cb7-161"><a href="#cb7-161" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">SystemExit</span>(<span class="ss">f&quot;Keine Loesung erhalten (</span><span class="sc">{</span>loesung<span class="sc">.</span>status<span class="sc">.</span>value<span class="sc">}</span><span class="ss">). &quot;</span></span>
<span id="cb7-162"><a href="#cb7-162" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Zeitlimit erhoehen oder Modell vereinfachen.&quot;</span>)</span>
<span id="cb7-163"><a href="#cb7-163" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> loesung.status <span class="kw">is</span> SolverStatus.ZULAESSIG:</span>
<span id="cb7-164"><a href="#cb7-164" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;ACHTUNG: nicht beweisbar optimal - Gap </span><span class="sc">{</span>loesung<span class="sc">.</span>gap<span class="sc">:.2%}</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb7-165"><a href="#cb7-165" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-166"><a href="#cb7-166" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> np.array([loesung.werte[name] <span class="cf">for</span> name <span class="kw">in</span> ANLAGEN])</span>
<span id="cb7-167"><a href="#cb7-167" aria-hidden="true" tabindex="-1"></a> y <span class="op">=</span> np.array([loesung.werte[<span class="ss">f&quot;aktiv:</span><span class="sc">{</span>name<span class="sc">}</span><span class="ss">&quot;</span>] <span class="cf">for</span> name <span class="kw">in</span> ANLAGEN], dtype<span class="op">=</span><span class="bu">int</span>)</span>
<span id="cb7-168"><a href="#cb7-168" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-169"><a href="#cb7-169" aria-hidden="true" tabindex="-1"></a> tabelle <span class="op">=</span> pd.DataFrame({</span>
<span id="cb7-170"><a href="#cb7-170" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Anlage&quot;</span>: ANLAGEN,</span>
<span id="cb7-171"><a href="#cb7-171" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Aktiv&quot;</span>: [<span class="st">&quot;JA&quot;</span> <span class="cf">if</span> y[i] <span class="cf">else</span> <span class="st">&quot;-&quot;</span> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N)],</span>
<span id="cb7-172"><a href="#cb7-172" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Investition (EUR)&quot;</span>: [<span class="ss">f&quot;</span><span class="sc">{</span>x[i]<span class="sc">:,.0f}</span><span class="ss">&quot;</span> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N)],</span>
<span id="cb7-173"><a href="#cb7-173" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Anteil&quot;</span>: [<span class="ss">f&quot;</span><span class="sc">{</span>x[i]<span class="op">/</span>BUDGET<span class="op">*</span><span class="dv">100</span><span class="sc">:5.1f}</span><span class="ss"> %&quot;</span> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N)],</span>
<span id="cb7-174"><a href="#cb7-174" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Erw. Rendite&quot;</span>: [<span class="ss">f&quot;</span><span class="sc">{</span>RENDITE[i]<span class="op">*</span><span class="dv">100</span><span class="sc">:4.1f}</span><span class="ss"> %&quot;</span> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N)],</span>
<span id="cb7-175"><a href="#cb7-175" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Erw. Ertrag (EUR)&quot;</span>: [<span class="ss">f&quot;</span><span class="sc">{</span>x[i]<span class="op">*</span>RENDITE[i]<span class="sc">:,.0f}</span><span class="ss">&quot;</span> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N)],</span>
<span id="cb7-176"><a href="#cb7-176" aria-hidden="true" tabindex="-1"></a> })</span>
<span id="cb7-177"><a href="#cb7-177" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(tabelle.to_string(index<span class="op">=</span><span class="va">False</span>))</span>
<span id="cb7-178"><a href="#cb7-178" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-179"><a href="#cb7-179" aria-hidden="true" tabindex="-1"></a> brutto <span class="op">=</span> <span class="bu">float</span>(RENDITE <span class="op">@</span> x)</span>
<span id="cb7-180"><a href="#cb7-180" aria-hidden="true" tabindex="-1"></a> gebuehren <span class="op">=</span> <span class="bu">float</span>(GEBUEHR <span class="op">*</span> y.<span class="bu">sum</span>())</span>
<span id="cb7-181"><a href="#cb7-181" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">88</span>)</span>
<span id="cb7-182"><a href="#cb7-182" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Erwarteter Bruttoertrag: </span><span class="sc">{</span>brutto<span class="sc">:&gt;12,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb7-183"><a href="#cb7-183" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Ordergebuehren: </span><span class="sc">{</span><span class="op">-</span>gebuehren<span class="sc">:&gt;12,.2f}</span><span class="ss"> EUR (</span><span class="sc">{</span>y<span class="sc">.</span><span class="bu">sum</span>()<span class="sc">}</span><span class="ss"> Positionen)&quot;</span>)</span>
<span id="cb7-184"><a href="#cb7-184" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Netto-Erwartungswert: </span><span class="sc">{</span>loesung<span class="sc">.</span>zielwert<span class="sc">:&gt;12,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb7-185"><a href="#cb7-185" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="sc">{</span>loesung<span class="sc">.</span>als_bericht()<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-186"><a href="#cb7-186" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-187"><a href="#cb7-187" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Vergleich mit dem unbeschraenkten Fall ---------------------------</span></span>
<span id="cb7-188"><a href="#cb7-188" aria-hidden="true" tabindex="-1"></a> z_frei, x_frei <span class="op">=</span> ohne_restriktionen()</span>
<span id="cb7-189"><a href="#cb7-189" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">88</span>)</span>
<span id="cb7-190"><a href="#cb7-190" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Zum Vergleich ohne jede Restriktion (alles in den Bestwert): &quot;</span></span>
<span id="cb7-191"><a href="#cb7-191" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>z_frei<span class="sc">:,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb7-192"><a href="#cb7-192" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Kosten der Realitaet (Gebuehren, Streuung, Mindestgroessen): &quot;</span></span>
<span id="cb7-193"><a href="#cb7-193" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>z_frei <span class="op">-</span> loesung<span class="sc">.</span>zielwert<span class="sc">:,.2f}</span><span class="ss"> EUR &quot;</span></span>
<span id="cb7-194"><a href="#cb7-194" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span>(<span class="dv">1</span> <span class="op">-</span> loesung.zielwert<span class="op">/</span>z_frei)<span class="op">*</span><span class="dv">100</span><span class="sc">:.2f}</span><span class="ss"> %)&quot;</span>)</span>
<span id="cb7-195"><a href="#cb7-195" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-196"><a href="#cb7-196" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Alle Nebenbedingungen nachpruefen --------------------------------</span></span>
<span id="cb7-197"><a href="#cb7-197" aria-hidden="true" tabindex="-1"></a> beanstandungen <span class="op">=</span> pruefe_portfolio(loesung)</span>
<span id="cb7-198"><a href="#cb7-198" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">88</span>)</span>
<span id="cb7-199"><a href="#cb7-199" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> beanstandungen:</span>
<span id="cb7-200"><a href="#cb7-200" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">SystemExit</span>(<span class="st">&quot;Abnahmepruefung fehlgeschlagen:</span><span class="ch">\n</span><span class="st"> - &quot;</span></span>
<span id="cb7-201"><a href="#cb7-201" aria-hidden="true" tabindex="-1"></a> <span class="op">+</span> <span class="st">&quot;</span><span class="ch">\n</span><span class="st"> - &quot;</span>.join(beanstandungen))</span>
<span id="cb7-202"><a href="#cb7-202" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Abnahmepruefung: alle Nebenbedingungen geprueft und eingehalten.&quot;</span>)</span>
<span id="cb7-203"><a href="#cb7-203" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">88</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>========================================================================================
OPTIMALE MILP-PORTFOLIO-ALLOKATION MIT FIXGEBUEHREN
========================================================================================
Budget: 100,000 EUR | max. 3 Positionen | je 10,000-40,000 EUR | Gebuehr 50 EUR
Anlage Aktiv Investition (EUR) Anteil Erw. Rendite Erw. Ertrag (EUR)
US-Aktien JA 40,000 40.0 % 11.0 % 4,400
EU-Aktien JA 20,000 20.0 % 8.0 % 1,600
Emerging-Markets JA 40,000 40.0 % 13.0 % 5,200
Staatsanleihen - 0 0.0 % 3.0 % 0
Unternehmensanleihen - 0 0.0 % 5.0 % 0
Rohstoffe - 0 0.0 % 7.0 % 0
----------------------------------------------------------------------------------------
Erwarteter Bruttoertrag: 11,200.00 EUR
Ordergebuehren: -150.00 EUR (3 Positionen)
Netto-Erwartungswert: 11,050.00 EUR
Status: optimal | Zielwert: 11,050.00 | Gap: 0.00% | Zeit: 0.01s
----------------------------------------------------------------------------------------
Zum Vergleich ohne jede Restriktion (alles in den Bestwert): 13,000.00 EUR
Kosten der Realitaet (Gebuehren, Streuung, Mindestgroessen): 1,950.00 EUR (15.00 %)
----------------------------------------------------------------------------------------
Abnahmepruefung: alle Nebenbedingungen geprueft und eingehalten.
========================================================================================</code></pre>
<blockquote>
<p><strong>💻 Code-Durchgang</strong></p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr class="header">
<th>Stelle</th>
<th>Was passiert</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>c = concat([-RENDITE, full(N, GEBUEHR)])</code></td>
<td>Zielfunktion über <strong>beide</strong> Variablenblöcke: Erträge negativ (weil <code>linprog</code> minimiert), Gebühren positiv (sie sollen ja gedrückt werden)</td>
</tr>
<tr class="even">
<td><code>A_eq[0, :N] = 1.0</code></td>
<td>Budgetgleichung betrifft nur die <span class="math inline">x</span>-Variablen</td>
</tr>
<tr class="odd">
<td><code>zeile[i]=1; zeile[N+i]=-MAX_POSITION</code></td>
<td>Muster 1: <span class="math inline">x_i - U y_i \le 0</span>. <strong><span class="math inline">M = U = 40\,000</span></strong> — die kleinstmögliche gültige Wahl</td>
</tr>
<tr class="even">
<td><code>zeile[i]=-1; zeile[N+i]=MIN_POSITION</code></td>
<td>Muster 5 unten: <span class="math inline">L y_i - x_i \le 0</span></td>
</tr>
<tr class="odd">
<td><code>integrality=concat([zeros(N), ones(N)])</code></td>
<td>nur die <span class="math inline">y</span> sind ganzzahlig — das ist das „mixed“ in MILP</td>
</tr>
<tr class="even">
<td><code>status = status_von_scipy(ergebnis)</code></td>
<td>übersetzt den <a href="anhang-glossar.md#gloss:scipy" class="glossar-link">SciPy</a>-Rückgabewert in die gemeinsame Sprache aus <a href="praxisfallen.html#sec:praxisfallen-or-kern">Abschnitt 22.6</a> — die drei <code>if</code>-Zweige darunter behandeln Modellfehler, Abbruch ohne Lösung und „zulässig, aber unbewiesen“ getrennt</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="odd">
<td><code>schranke=-ergebnis.mip_dual_bound</code></td>
<td>die zweite Zahl aus <a href="#sec:milp-gap">Abschnitt 6.8</a>. Erst mit ihr kann das <code>Loesung</code>-Objekt den Gap ausrechnen — und der Bericht sagt <code>Gap: 0.00%</code>, also <em>beweisbar</em> optimal</td>
</tr>
<tr class="even">
<td><code>pruefe_portfolio(...)</code></td>
<td>prüft <strong>jede</strong> modellierte Regel einzeln nach und liefert eine Liste von Beanstandungen</td>
</tr>
</tbody>
</table>
<p><strong>Warum das Ergebnis wirtschaftlich Sinn ergibt:</strong> Der Solver wählt die drei renditestärksten Anlagen (13 %, 11 %, 8 %) und füllt sie in dieser Reihenfolge bis zur Obergrenze von 40 000 €. Die drittbeste Position (EU-Aktien) erhält nur den Rest von 20 000 € — sie liegt über der Mindestordergröße von 10 000 €, ist also zulässig. Ohne Kardinalitätsgrenze läge alles im Bestwert (13 %); die Restriktionen kosten 15 % des theoretischen Ertrags. Genau diese Zahl braucht man, wenn man mit dem Risikomanagement über die Sinnhaftigkeit einer Regel diskutiert: „Die Obergrenze von 40 % je Position kostet uns 1 950 € pro Jahr — ist uns die <a href="anhang-glossar.md#gloss:diversifikation" class="glossar-link">Diversifikation</a> das wert?“ Das ist eine beantwortbare Frage; „wir sollten breiter streuen“ ist es nicht.</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:milp-gap">6.8 Wenn der Solver nicht fertig wird: Gap, Zeitlimit und Warm-Start</h2>
<p>Bei einem LP gibt es zwei Ausgänge: eine optimale Lösung oder eine klare Absage. Bei einem MILP gibt es einen dritten, und im Betrieb ist er der häufigste:</p>
<blockquote>
<p><em>„Ich habe eine Lösung. Ob sie die beste ist, weiß ich nicht. Die Zeit ist um.“</em></p>
</blockquote>
<p>Dieser Abschnitt handelt davon, wie man damit professionell umgeht — statt so zu tun, als käme immer <code>OPTIMAL</code> zurück.</p>
<h3 id="die-zwei-zahlen-die-zählen">Die zwei Zahlen, die zählen</h3>
<p>Branch-and-Bound führt zu jedem Zeitpunkt zwei Werte mit:</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Begriff</th>
<th>Was er bedeutet</th>
<th>Wie er sich entwickelt</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Incumbent</strong> (beste gefundene Lösung)</td>
<td>ein Plan, den man tatsächlich ausführen könnte</td>
<td>wird im Lauf der Suche immer besser</td>
</tr>
<tr class="even">
<td><strong>Schranke</strong> (<em>dual bound</em>)</td>
<td>der beste Wert, den es überhaupt geben <em>könnte</em></td>
<td>wird im Lauf der Suche immer schlechter</td>
</tr>
</tbody>
</table>
<p>Beide laufen aufeinander zu. Ihr Abstand ist der <strong>MIP-Gap</strong>:</p>
<p><span class="math display">
\text{Gap} = \frac{\lvert\, z_{\text{incumbent}} - z_{\text{schranke}}\,\rvert}
{\lvert\, z_{\text{incumbent}}\,\rvert}
</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">z_{\text{incumbent}}</span></td>
<td>„Das ist der beste Plan, den ich bisher tatsächlich in der Hand habe.“</td>
</tr>
<tr class="even">
<td><span class="math inline">z_{\text{schranke}}</span></td>
<td>„Besser als das kann es rechnerisch nicht werden — bewiesen.“</td>
</tr>
<tr class="odd">
<td>Gap <span class="math inline">= 0</span></td>
<td>„Beide sind gleich: Der Plan ist beweisbar optimal.“</td>
</tr>
<tr class="even">
<td>Gap <span class="math inline">= 0{,}09</span></td>
<td>„Mein Plan ist höchstens 9 % schlechter als das theoretische Beste. <strong>Garantiert.</strong></td>
</tr>
</tbody>
</table>
<p>Der Gap ist damit keine Schätzung und keine Fehlerbalken-Angabe, sondern eine <strong>Zusage</strong>. Das macht ihn zu der Zahl, die man ins Managementgespräch mitnimmt: „Wir liegen höchstens 2 % vom Optimum entfernt“ ist eine belastbare Aussage. „Der Solver hat lange gerechnet“ ist keine.</p>
</blockquote>
</div>
<h3 id="alle-statusfälle-behandeln">Alle Statusfälle behandeln</h3>
<p>Der zweite Teil der Professionalität ist unspektakulär, aber entscheidend: <strong>jeden</strong> Rückgabewert auswerten, nicht nur <code>OPTIMAL</code>.</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Status</th>
<th>Bedeutung</th>
<th>Was Sie tun</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>Optimal</code></td>
<td>beweisbar bestmöglich</td>
<td>ausführen</td>
</tr>
<tr class="even">
<td><code>Time limit reached</code> (mit Lösung)</td>
<td>zulässig, nicht bewiesen optimal</td>
<td>Gap prüfen, dann entscheiden</td>
</tr>
<tr class="odd">
<td><code>Time limit reached</code> (ohne Lösung)</td>
<td>nichts gefunden</td>
<td>Modell vereinfachen, Heuristik als Startwert, Limit erhöhen</td>
</tr>
<tr class="even">
<td><code>Infeasible</code></td>
<td>kein zulässiger Plan existiert</td>
<td>harte Bedingungen prüfen (<a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a>, Anhang C)</td>
</tr>
<tr class="odd">
<td><code>Unbounded</code></td>
<td>Ziel wächst unbegrenzt</td>
<td>fehlende Schranke — fast immer ein Modellierungsfehler</td>
</tr>
</tbody>
</table>
<div class="sourceCode" id="cb9"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Solverstatus_und_Gap.py</span></span>
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel MILP: Was tun, wenn der Solver nicht fertig wird?</span></span>
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a><span class="co">Bei einem LP kommt entweder eine optimale Loesung oder eine klare Absage.</span></span>
<span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a><span class="co">Bei einem MILP ist der haeufigste Ausgang im Betrieb ein dritter: &quot;Ich habe</span></span>
<span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a><span class="co">eine Loesung, ich weiss aber nicht, ob sie die beste ist - und die Zeit ist</span></span>
<span id="cb9-10"><a href="#cb9-10" aria-hidden="true" tabindex="-1"></a><span class="co">um.&quot; Dieses Programm zeigt, wie man mit diesem Fall umgeht.</span></span>
<span id="cb9-11"><a href="#cb9-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-12"><a href="#cb9-12" aria-hidden="true" tabindex="-1"></a><span class="co"> 1. Alle Statusfaelle explizit behandeln, statt OPTIMAL vorauszusetzen.</span></span>
<span id="cb9-13"><a href="#cb9-13" aria-hidden="true" tabindex="-1"></a><span class="co"> 2. Den MIP-Gap lesen: Wie weit kann ich hoechstens danebenliegen?</span></span>
<span id="cb9-14"><a href="#cb9-14" aria-hidden="true" tabindex="-1"></a><span class="co"> 3. Messen, was zusaetzliche Rechenzeit ueberhaupt noch bringt.</span></span>
<span id="cb9-15"><a href="#cb9-15" aria-hidden="true" tabindex="-1"></a><span class="co"> 4. Warm-Start ausprobieren - und ehrlich messen, ob er etwas bringt.</span></span>
<span id="cb9-16"><a href="#cb9-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-17"><a href="#cb9-17" aria-hidden="true" tabindex="-1"></a><span class="co">Beispiel: Standortplanung, 45 moegliche Lager, 120 Kunden (5445 Variablen,</span></span>
<span id="cb9-18"><a href="#cb9-18" aria-hidden="true" tabindex="-1"></a><span class="co">davon 45 binaer).</span></span>
<span id="cb9-19"><a href="#cb9-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-20"><a href="#cb9-20" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, highspy</span></span>
<span id="cb9-21"><a href="#cb9-21" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb9-22"><a href="#cb9-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-23"><a href="#cb9-23" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb9-24"><a href="#cb9-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-25"><a href="#cb9-25" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> time</span>
<span id="cb9-26"><a href="#cb9-26" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> dataclasses <span class="im">import</span> dataclass</span>
<span id="cb9-27"><a href="#cb9-27" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-28"><a href="#cb9-28" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb9-29"><a href="#cb9-29" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> highspy</span>
<span id="cb9-30"><a href="#cb9-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-31"><a href="#cb9-31" aria-hidden="true" tabindex="-1"></a>RNG <span class="op">=</span> np.random.default_rng(<span class="dv">7</span>)</span>
<span id="cb9-32"><a href="#cb9-32" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-33"><a href="#cb9-33" aria-hidden="true" tabindex="-1"></a>N_LAGER, N_KUNDE <span class="op">=</span> <span class="dv">45</span>, <span class="dv">120</span></span>
<span id="cb9-34"><a href="#cb9-34" aria-hidden="true" tabindex="-1"></a>FIXKOSTEN <span class="op">=</span> RNG.uniform(<span class="dv">3000</span>, <span class="dv">9000</span>, N_LAGER)</span>
<span id="cb9-35"><a href="#cb9-35" aria-hidden="true" tabindex="-1"></a>TRANSPORT <span class="op">=</span> RNG.uniform(<span class="dv">5</span>, <span class="dv">60</span>, (N_LAGER, N_KUNDE))</span>
<span id="cb9-36"><a href="#cb9-36" aria-hidden="true" tabindex="-1"></a>BEDARF <span class="op">=</span> RNG.uniform(<span class="dv">10</span>, <span class="dv">60</span>, N_KUNDE)</span>
<span id="cb9-37"><a href="#cb9-37" aria-hidden="true" tabindex="-1"></a>KAPAZITAET <span class="op">=</span> np.full(N_LAGER, BEDARF.<span class="bu">sum</span>() <span class="op">*</span> <span class="fl">0.22</span>)</span>
<span id="cb9-38"><a href="#cb9-38" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-39"><a href="#cb9-39" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-40"><a href="#cb9-40" aria-hidden="true" tabindex="-1"></a><span class="at">@dataclass</span></span>
<span id="cb9-41"><a href="#cb9-41" aria-hidden="true" tabindex="-1"></a><span class="kw">class</span> Ergebnis:</span>
<span id="cb9-42"><a href="#cb9-42" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Alles, was nach einem Solverlauf ausgewertet werden muss - nicht nur</span></span>
<span id="cb9-43"><a href="#cb9-43" aria-hidden="true" tabindex="-1"></a><span class="co"> der Zielwert.&quot;&quot;&quot;</span></span>
<span id="cb9-44"><a href="#cb9-44" aria-hidden="true" tabindex="-1"></a> status: <span class="bu">str</span></span>
<span id="cb9-45"><a href="#cb9-45" aria-hidden="true" tabindex="-1"></a> brauchbar: <span class="bu">bool</span> <span class="co"># Gibt es ueberhaupt eine zulaessige Loesung?</span></span>
<span id="cb9-46"><a href="#cb9-46" aria-hidden="true" tabindex="-1"></a> beweisbar_optimal: <span class="bu">bool</span></span>
<span id="cb9-47"><a href="#cb9-47" aria-hidden="true" tabindex="-1"></a> zielwert: <span class="bu">float</span> <span class="co"># bester gefundener Wert (Incumbent)</span></span>
<span id="cb9-48"><a href="#cb9-48" aria-hidden="true" tabindex="-1"></a> schranke: <span class="bu">float</span> <span class="co"># beste bewiesene Schranke (Dual Bound)</span></span>
<span id="cb9-49"><a href="#cb9-49" aria-hidden="true" tabindex="-1"></a> gap: <span class="bu">float</span> <span class="co"># relativer Abstand zwischen beiden</span></span>
<span id="cb9-50"><a href="#cb9-50" aria-hidden="true" tabindex="-1"></a> knoten: <span class="bu">int</span></span>
<span id="cb9-51"><a href="#cb9-51" aria-hidden="true" tabindex="-1"></a> dauer: <span class="bu">float</span></span>
<span id="cb9-52"><a href="#cb9-52" aria-hidden="true" tabindex="-1"></a> loesung: np.ndarray</span>
<span id="cb9-53"><a href="#cb9-53" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-54"><a href="#cb9-54" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-55"><a href="#cb9-55" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese(zeitlimit: <span class="bu">float</span>, startloesung: np.ndarray <span class="op">|</span> <span class="va">None</span> <span class="op">=</span> <span class="va">None</span>) <span class="op">-&gt;</span> Ergebnis:</span>
<span id="cb9-56"><a href="#cb9-56" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Loest das Standortmodell mit Zeitlimit und wertet ALLE Statusfaelle aus.&quot;&quot;&quot;</span></span>
<span id="cb9-57"><a href="#cb9-57" aria-hidden="true" tabindex="-1"></a> hochschule <span class="op">=</span> highspy.Highs()</span>
<span id="cb9-58"><a href="#cb9-58" aria-hidden="true" tabindex="-1"></a> hochschule.setOptionValue(<span class="st">&quot;output_flag&quot;</span>, <span class="va">False</span>)</span>
<span id="cb9-59"><a href="#cb9-59" aria-hidden="true" tabindex="-1"></a> hochschule.setOptionValue(<span class="st">&quot;time_limit&quot;</span>, zeitlimit)</span>
<span id="cb9-60"><a href="#cb9-60" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-61"><a href="#cb9-61" aria-hidden="true" tabindex="-1"></a> anzahl_x <span class="op">=</span> N_LAGER <span class="op">*</span> N_KUNDE</span>
<span id="cb9-62"><a href="#cb9-62" aria-hidden="true" tabindex="-1"></a> unendlich <span class="op">=</span> highspy.kHighsInf</span>
<span id="cb9-63"><a href="#cb9-63" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-64"><a href="#cb9-64" aria-hidden="true" tabindex="-1"></a> hochschule.addVars(anzahl_x, np.zeros(anzahl_x), np.full(anzahl_x, unendlich))</span>
<span id="cb9-65"><a href="#cb9-65" aria-hidden="true" tabindex="-1"></a> hochschule.addVars(N_LAGER, np.zeros(N_LAGER), np.ones(N_LAGER))</span>
<span id="cb9-66"><a href="#cb9-66" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N_LAGER):</span>
<span id="cb9-67"><a href="#cb9-67" aria-hidden="true" tabindex="-1"></a> hochschule.changeColIntegrality(anzahl_x <span class="op">+</span> i, highspy.HighsVarType.kInteger)</span>
<span id="cb9-68"><a href="#cb9-68" aria-hidden="true" tabindex="-1"></a> hochschule.changeColCost(anzahl_x <span class="op">+</span> i, FIXKOSTEN[i])</span>
<span id="cb9-69"><a href="#cb9-69" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(N_KUNDE):</span>
<span id="cb9-70"><a href="#cb9-70" aria-hidden="true" tabindex="-1"></a> hochschule.changeColCost(i <span class="op">*</span> N_KUNDE <span class="op">+</span> j, TRANSPORT[i, j])</span>
<span id="cb9-71"><a href="#cb9-71" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-72"><a href="#cb9-72" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(N_KUNDE):</span>
<span id="cb9-73"><a href="#cb9-73" aria-hidden="true" tabindex="-1"></a> index <span class="op">=</span> np.array([i <span class="op">*</span> N_KUNDE <span class="op">+</span> j <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N_LAGER)], dtype<span class="op">=</span>np.int32)</span>
<span id="cb9-74"><a href="#cb9-74" aria-hidden="true" tabindex="-1"></a> hochschule.addRow(BEDARF[j], BEDARF[j], <span class="bu">len</span>(index), index, np.ones(<span class="bu">len</span>(index)))</span>
<span id="cb9-75"><a href="#cb9-75" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-76"><a href="#cb9-76" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N_LAGER):</span>
<span id="cb9-77"><a href="#cb9-77" aria-hidden="true" tabindex="-1"></a> index <span class="op">=</span> np.array([i <span class="op">*</span> N_KUNDE <span class="op">+</span> j <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(N_KUNDE)] <span class="op">+</span> [anzahl_x <span class="op">+</span> i],</span>
<span id="cb9-78"><a href="#cb9-78" aria-hidden="true" tabindex="-1"></a> dtype<span class="op">=</span>np.int32)</span>
<span id="cb9-79"><a href="#cb9-79" aria-hidden="true" tabindex="-1"></a> werte <span class="op">=</span> np.concatenate([np.ones(N_KUNDE), [<span class="op">-</span>KAPAZITAET[i]]])</span>
<span id="cb9-80"><a href="#cb9-80" aria-hidden="true" tabindex="-1"></a> hochschule.addRow(<span class="op">-</span>unendlich, <span class="fl">0.0</span>, <span class="bu">len</span>(index), index, werte)</span>
<span id="cb9-81"><a href="#cb9-81" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-82"><a href="#cb9-82" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> startloesung <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span>
<span id="cb9-83"><a href="#cb9-83" aria-hidden="true" tabindex="-1"></a> hochschule.setSolution(<span class="bu">len</span>(startloesung),</span>
<span id="cb9-84"><a href="#cb9-84" aria-hidden="true" tabindex="-1"></a> np.arange(<span class="bu">len</span>(startloesung), dtype<span class="op">=</span>np.int32),</span>
<span id="cb9-85"><a href="#cb9-85" aria-hidden="true" tabindex="-1"></a> startloesung)</span>
<span id="cb9-86"><a href="#cb9-86" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-87"><a href="#cb9-87" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()</span>
<span id="cb9-88"><a href="#cb9-88" aria-hidden="true" tabindex="-1"></a> hochschule.run()</span>
<span id="cb9-89"><a href="#cb9-89" aria-hidden="true" tabindex="-1"></a> dauer <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
<span id="cb9-90"><a href="#cb9-90" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-91"><a href="#cb9-91" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> hochschule.modelStatusToString(hochschule.getModelStatus())</span>
<span id="cb9-92"><a href="#cb9-92" aria-hidden="true" tabindex="-1"></a> info <span class="op">=</span> hochschule.getInfo()</span>
<span id="cb9-93"><a href="#cb9-93" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-94"><a href="#cb9-94" aria-hidden="true" tabindex="-1"></a> <span class="co"># Der Kern der Sache: Aus dem Status folgt, WAS man mit dem Ergebnis</span></span>
<span id="cb9-95"><a href="#cb9-95" aria-hidden="true" tabindex="-1"></a> <span class="co"># ueberhaupt anfangen darf.</span></span>
<span id="cb9-96"><a href="#cb9-96" aria-hidden="true" tabindex="-1"></a> brauchbar <span class="op">=</span> status <span class="kw">in</span> (<span class="st">&quot;Optimal&quot;</span>, <span class="st">&quot;Time limit reached&quot;</span>, <span class="st">&quot;Solution limit reached&quot;</span>)</span>
<span id="cb9-97"><a href="#cb9-97" aria-hidden="true" tabindex="-1"></a> beweisbar_optimal <span class="op">=</span> status <span class="op">==</span> <span class="st">&quot;Optimal&quot;</span></span>
<span id="cb9-98"><a href="#cb9-98" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> status <span class="kw">in</span> (<span class="st">&quot;Infeasible&quot;</span>, <span class="st">&quot;Unbounded&quot;</span>, <span class="st">&quot;Primal infeasible or unbounded&quot;</span>):</span>
<span id="cb9-99"><a href="#cb9-99" aria-hidden="true" tabindex="-1"></a> brauchbar <span class="op">=</span> <span class="va">False</span></span>
<span id="cb9-100"><a href="#cb9-100" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-101"><a href="#cb9-101" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Ergebnis(</span>
<span id="cb9-102"><a href="#cb9-102" aria-hidden="true" tabindex="-1"></a> status<span class="op">=</span>status,</span>
<span id="cb9-103"><a href="#cb9-103" aria-hidden="true" tabindex="-1"></a> brauchbar<span class="op">=</span>brauchbar <span class="kw">and</span> info.objective_function_value <span class="op">&lt;</span> unendlich,</span>
<span id="cb9-104"><a href="#cb9-104" aria-hidden="true" tabindex="-1"></a> beweisbar_optimal<span class="op">=</span>beweisbar_optimal,</span>
<span id="cb9-105"><a href="#cb9-105" aria-hidden="true" tabindex="-1"></a> zielwert<span class="op">=</span>info.objective_function_value,</span>
<span id="cb9-106"><a href="#cb9-106" aria-hidden="true" tabindex="-1"></a> schranke<span class="op">=</span>info.mip_dual_bound,</span>
<span id="cb9-107"><a href="#cb9-107" aria-hidden="true" tabindex="-1"></a> gap<span class="op">=</span>info.mip_gap,</span>
<span id="cb9-108"><a href="#cb9-108" aria-hidden="true" tabindex="-1"></a> knoten<span class="op">=</span>info.mip_node_count,</span>
<span id="cb9-109"><a href="#cb9-109" aria-hidden="true" tabindex="-1"></a> dauer<span class="op">=</span>dauer,</span>
<span id="cb9-110"><a href="#cb9-110" aria-hidden="true" tabindex="-1"></a> loesung<span class="op">=</span>np.array(hochschule.getSolution().col_value),</span>
<span id="cb9-111"><a href="#cb9-111" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb9-112"><a href="#cb9-112" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-113"><a href="#cb9-113" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-114"><a href="#cb9-114" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> gieriger_startplan() <span class="op">-&gt;</span> <span class="bu">tuple</span>[np.ndarray, <span class="bu">float</span>]:</span>
<span id="cb9-115"><a href="#cb9-115" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Eine Faustregel-Loesung, wie sie ein Disponent von Hand erstellen wuerde:</span></span>
<span id="cb9-116"><a href="#cb9-116" aria-hidden="true" tabindex="-1"></a><span class="co"> die guenstigsten Lager oeffnen (Fixkosten je Kapazitaetseinheit), dann</span></span>
<span id="cb9-117"><a href="#cb9-117" aria-hidden="true" tabindex="-1"></a><span class="co"> jeden Kunden dem naechstgelegenen offenen Lager mit Restkapazitaet</span></span>
<span id="cb9-118"><a href="#cb9-118" aria-hidden="true" tabindex="-1"></a><span class="co"> zuordnen. Kein Solver noetig - und in Sekunden fertig.</span></span>
<span id="cb9-119"><a href="#cb9-119" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb9-120"><a href="#cb9-120" aria-hidden="true" tabindex="-1"></a> reihenfolge <span class="op">=</span> np.argsort(FIXKOSTEN <span class="op">/</span> KAPAZITAET)</span>
<span id="cb9-121"><a href="#cb9-121" aria-hidden="true" tabindex="-1"></a> offen: <span class="bu">list</span>[<span class="bu">int</span>] <span class="op">=</span> []</span>
<span id="cb9-122"><a href="#cb9-122" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> reihenfolge:</span>
<span id="cb9-123"><a href="#cb9-123" aria-hidden="true" tabindex="-1"></a> offen.append(<span class="bu">int</span>(i))</span>
<span id="cb9-124"><a href="#cb9-124" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> KAPAZITAET[offen].<span class="bu">sum</span>() <span class="op">&gt;=</span> BEDARF.<span class="bu">sum</span>() <span class="op">*</span> <span class="fl">1.05</span>:</span>
<span id="cb9-125"><a href="#cb9-125" aria-hidden="true" tabindex="-1"></a> <span class="cf">break</span></span>
<span id="cb9-126"><a href="#cb9-126" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-127"><a href="#cb9-127" aria-hidden="true" tabindex="-1"></a> rest <span class="op">=</span> KAPAZITAET.copy()</span>
<span id="cb9-128"><a href="#cb9-128" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> np.zeros((N_LAGER, N_KUNDE))</span>
<span id="cb9-129"><a href="#cb9-129" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> np.argsort(<span class="op">-</span>BEDARF): <span class="co"># groesste Kunden zuerst</span></span>
<span id="cb9-130"><a href="#cb9-130" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">sorted</span>(offen, key<span class="op">=</span><span class="kw">lambda</span> i: TRANSPORT[i, j]):</span>
<span id="cb9-131"><a href="#cb9-131" aria-hidden="true" tabindex="-1"></a> menge <span class="op">=</span> <span class="bu">min</span>(rest[i], BEDARF[j] <span class="op">-</span> x[:, j].<span class="bu">sum</span>())</span>
<span id="cb9-132"><a href="#cb9-132" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> menge <span class="op">&gt;</span> <span class="fl">1e-9</span>:</span>
<span id="cb9-133"><a href="#cb9-133" aria-hidden="true" tabindex="-1"></a> x[i, j] <span class="op">+=</span> menge</span>
<span id="cb9-134"><a href="#cb9-134" aria-hidden="true" tabindex="-1"></a> rest[i] <span class="op">-=</span> menge</span>
<span id="cb9-135"><a href="#cb9-135" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(x[:, j].<span class="bu">sum</span>() <span class="op">-</span> BEDARF[j]) <span class="op">&lt;</span> <span class="fl">1e-9</span>:</span>
<span id="cb9-136"><a href="#cb9-136" aria-hidden="true" tabindex="-1"></a> <span class="cf">break</span></span>
<span id="cb9-137"><a href="#cb9-137" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-138"><a href="#cb9-138" aria-hidden="true" tabindex="-1"></a> y <span class="op">=</span> np.zeros(N_LAGER)</span>
<span id="cb9-139"><a href="#cb9-139" aria-hidden="true" tabindex="-1"></a> y[offen] <span class="op">=</span> <span class="fl">1.0</span></span>
<span id="cb9-140"><a href="#cb9-140" aria-hidden="true" tabindex="-1"></a> kosten <span class="op">=</span> <span class="bu">float</span>((FIXKOSTEN <span class="op">*</span> y).<span class="bu">sum</span>() <span class="op">+</span> (TRANSPORT <span class="op">*</span> x).<span class="bu">sum</span>())</span>
<span id="cb9-141"><a href="#cb9-141" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> np.concatenate([x.ravel(), y]), kosten</span>
<span id="cb9-142"><a href="#cb9-142" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-143"><a href="#cb9-143" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-144"><a href="#cb9-144" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zeige(titel: <span class="bu">str</span>, e: Ergebnis) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb9-145"><a href="#cb9-145" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="sc">{</span>titel<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-146"><a href="#cb9-146" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Status </span><span class="sc">{</span>e<span class="sc">.</span>status<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-147"><a href="#cb9-147" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> e.brauchbar:</span>
<span id="cb9-148"><a href="#cb9-148" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; -&gt; KEINE verwertbare Loesung. Nicht weiterrechnen!&quot;</span>)</span>
<span id="cb9-149"><a href="#cb9-149" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span></span>
<span id="cb9-150"><a href="#cb9-150" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; bester Plan (Incumbent) </span><span class="sc">{</span>e<span class="sc">.</span>zielwert<span class="sc">:&gt;12,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb9-151"><a href="#cb9-151" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; bewiesene Schranke </span><span class="sc">{</span>e<span class="sc">.</span>schranke<span class="sc">:&gt;12,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb9-152"><a href="#cb9-152" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; MIP-Gap </span><span class="sc">{</span>e<span class="sc">.</span>gap <span class="op">*</span> <span class="dv">100</span><span class="sc">:&gt;12.3f}</span><span class="ss"> %&quot;</span>)</span>
<span id="cb9-153"><a href="#cb9-153" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Knoten / Zeit </span><span class="sc">{</span>e<span class="sc">.</span>knoten<span class="sc">:&gt;12,}</span><span class="ss"> / </span><span class="sc">{</span>e<span class="sc">.</span>dauer<span class="sc">:.2f}</span><span class="ss"> s&quot;</span>)</span>
<span id="cb9-154"><a href="#cb9-154" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> e.beweisbar_optimal:</span>
<span id="cb9-155"><a href="#cb9-155" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; -&gt; beweisbar optimal&quot;</span>)</span>
<span id="cb9-156"><a href="#cb9-156" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb9-157"><a href="#cb9-157" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; -&gt; zulaessig, aber nicht bewiesen optimal. Der wahre Bestwert&quot;</span>)</span>
<span id="cb9-158"><a href="#cb9-158" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; liegt zwischen </span><span class="sc">{</span>e<span class="sc">.</span>schranke<span class="sc">:,.2f}</span><span class="ss"> und </span><span class="sc">{</span>e<span class="sc">.</span>zielwert<span class="sc">:,.2f}</span><span class="ss"> EUR.&quot;</span>)</span>
<span id="cb9-159"><a href="#cb9-159" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-160"><a href="#cb9-160" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-161"><a href="#cb9-161" 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="cb9-162"><a href="#cb9-162" 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="cb9-163"><a href="#cb9-163" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; MIP-GAP UND ZEITLIMIT: STANDORTPLANUNG, 45 LAGER, 120 KUNDEN&quot;</span>)</span>
<span id="cb9-164"><a href="#cb9-164" 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="cb9-165"><a href="#cb9-165" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>N_LAGER <span class="op">*</span> N_KUNDE <span class="op">+</span> N_LAGER<span class="sc">:,}</span><span class="ss"> Variablen, davon </span><span class="sc">{</span>N_LAGER<span class="sc">}</span><span class="ss"> binaer.&quot;</span>)</span>
<span id="cb9-166"><a href="#cb9-166" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-167"><a href="#cb9-167" aria-hidden="true" tabindex="-1"></a> kurz <span class="op">=</span> loese(<span class="fl">2.0</span>)</span>
<span id="cb9-168"><a href="#cb9-168" aria-hidden="true" tabindex="-1"></a> zeige(<span class="st">&quot;[1] Zeitlimit 2 Sekunden&quot;</span>, kurz)</span>
<span id="cb9-169"><a href="#cb9-169" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-170"><a href="#cb9-170" aria-hidden="true" tabindex="-1"></a> lang <span class="op">=</span> loese(<span class="fl">60.0</span>)</span>
<span id="cb9-171"><a href="#cb9-171" aria-hidden="true" tabindex="-1"></a> zeige(<span class="st">&quot;[2] Zeitlimit 60 Sekunden&quot;</span>, lang)</span>
<span id="cb9-172"><a href="#cb9-172" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-173"><a href="#cb9-173" 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="cb9-174"><a href="#cb9-174" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; WAS BRINGT MEHR RECHENZEIT?&quot;</span>)</span>
<span id="cb9-175"><a href="#cb9-175" 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="cb9-176"><a href="#cb9-176" aria-hidden="true" tabindex="-1"></a> verbesserung <span class="op">=</span> kurz.zielwert <span class="op">-</span> lang.zielwert</span>
<span id="cb9-177"><a href="#cb9-177" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Nach 2 Sekunden: </span><span class="sc">{</span>kurz<span class="sc">.</span>zielwert<span class="sc">:,.2f}</span><span class="ss"> EUR, Gap </span><span class="sc">{</span>kurz<span class="sc">.</span>gap <span class="op">*</span> <span class="dv">100</span><span class="sc">:.2f}</span><span class="ss"> %&quot;</span>)</span>
<span id="cb9-178"><a href="#cb9-178" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Nach </span><span class="sc">{</span>lang<span class="sc">.</span>dauer<span class="sc">:.1f}</span><span class="ss"> Sekunden: </span><span class="sc">{</span>lang<span class="sc">.</span>zielwert<span class="sc">:,.2f}</span><span class="ss"> EUR, bewiesen optimal&quot;</span>)</span>
<span id="cb9-179"><a href="#cb9-179" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Gewinn durch </span><span class="sc">{</span>lang<span class="sc">.</span>dauer <span class="op">-</span> kurz<span class="sc">.</span>dauer<span class="sc">:.1f}</span><span class="ss"> Sekunden mehr Rechenzeit: &quot;</span></span>
<span id="cb9-180"><a href="#cb9-180" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>verbesserung<span class="sc">:,.2f}</span><span class="ss"> EUR &quot;</span></span>
<span id="cb9-181"><a href="#cb9-181" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span>verbesserung <span class="op">/</span> kurz<span class="sc">.</span>zielwert <span class="op">*</span> <span class="dv">100</span><span class="sc">:.2f}</span><span class="ss"> %)&quot;</span>)</span>
<span id="cb9-182"><a href="#cb9-182" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb9-183"><a href="#cb9-183" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Das ist die Frage, die im Betrieb wirklich zaehlt: Der Gap von&quot;</span>)</span>
<span id="cb9-184"><a href="#cb9-184" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>kurz<span class="sc">.</span>gap <span class="op">*</span> <span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> % nach 2 Sekunden ist eine GARANTIE - schlechter als&quot;</span>)</span>
<span id="cb9-185"><a href="#cb9-185" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;dieser Wert kann die Loesung nicht sein. Ob sich die restliche&quot;</span>)</span>
<span id="cb9-186"><a href="#cb9-186" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Rechenzeit lohnt, entscheidet nicht der Solver, sondern die Anwendung:&quot;</span>)</span>
<span id="cb9-187"><a href="#cb9-187" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Ein naechtlicher Tourenplan darf eine Stunde rechnen, eine Umplanung&quot;</span>)</span>
<span id="cb9-188"><a href="#cb9-188" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;bei Maschinenausfall hat 30 Sekunden.&quot;</span>)</span>
<span id="cb9-189"><a href="#cb9-189" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-190"><a href="#cb9-190" 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="cb9-191"><a href="#cb9-191" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; BRINGT EIN WARM-START ETWAS?&quot;</span>)</span>
<span id="cb9-192"><a href="#cb9-192" 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="cb9-193"><a href="#cb9-193" aria-hidden="true" tabindex="-1"></a> start, start_kosten <span class="op">=</span> gieriger_startplan()</span>
<span id="cb9-194"><a href="#cb9-194" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Faustregel-Startplan (ohne Solver): </span><span class="sc">{</span>start_kosten<span class="sc">:,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb9-195"><a href="#cb9-195" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Das sind </span><span class="sc">{</span>(start_kosten <span class="op">/</span> lang.zielwert <span class="op">-</span> <span class="dv">1</span>) <span class="op">*</span> <span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> % ueber dem Optimum.</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb9-196"><a href="#cb9-196" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-197"><a href="#cb9-197" aria-hidden="true" tabindex="-1"></a> warm <span class="op">=</span> loese(<span class="fl">60.0</span>, startloesung<span class="op">=</span>start)</span>
<span id="cb9-198"><a href="#cb9-198" 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;&#39;</span><span class="sc">:26}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Zeit&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Knoten&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Ziel&#39;</span><span class="sc">:&gt;13}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-199"><a href="#cb9-199" 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="cb9-200"><a href="#cb9-200" 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;ohne Warm-Start&#39;</span><span class="sc">:&lt;26}</span><span class="ss"> </span><span class="sc">{</span>lang<span class="sc">.</span>dauer<span class="sc">:&gt;8.2f}</span><span class="ss">s </span><span class="sc">{</span>lang<span class="sc">.</span>knoten<span class="sc">:&gt;9,}</span><span class="ss"> &quot;</span></span>
<span id="cb9-201"><a href="#cb9-201" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>lang<span class="sc">.</span>zielwert<span class="sc">:&gt;13,.2f}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-202"><a href="#cb9-202" 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;mit Warm-Start&#39;</span><span class="sc">:&lt;26}</span><span class="ss"> </span><span class="sc">{</span>warm<span class="sc">.</span>dauer<span class="sc">:&gt;8.2f}</span><span class="ss">s </span><span class="sc">{</span>warm<span class="sc">.</span>knoten<span class="sc">:&gt;9,}</span><span class="ss"> &quot;</span></span>
<span id="cb9-203"><a href="#cb9-203" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>warm<span class="sc">.</span>zielwert<span class="sc">:&gt;13,.2f}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-204"><a href="#cb9-204" 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="cb9-205"><a href="#cb9-205" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Ergebnis: praktisch kein Unterschied. Der Grund ist nicht, dass&quot;</span>)</span>
<span id="cb9-206"><a href="#cb9-206" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Warm-Starts nichts taugen - sondern dass HiGHS&#39; eigene Heuristiken&quot;</span>)</span>
<span id="cb9-207"><a href="#cb9-207" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;innerhalb der ersten Sekunde bereits eine BESSERE Loesung finden als&quot;</span>)</span>
<span id="cb9-208"><a href="#cb9-208" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;unsere Faustregel. Ein Startwert hilft nur, wenn er besser ist als&quot;</span>)</span>
<span id="cb9-209"><a href="#cb9-209" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;das, was der Solver von allein in derselben Zeit findet.&quot;</span>)</span>
<span id="cb9-210"><a href="#cb9-210" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb9-211"><a href="#cb9-211" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Warm-Starts lohnen sich damit vor allem in zwei Faellen:&quot;</span>)</span>
<span id="cb9-212"><a href="#cb9-212" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; * Sie haben Domaenenwissen, das der Solver nicht hat (siehe&quot;</span>)</span>
<span id="cb9-213"><a href="#cb9-213" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Warmstart_Effekt.py - dort halbiert ein Heuristik-Hinweis die Zeit).&quot;</span>)</span>
<span id="cb9-214"><a href="#cb9-214" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; * Sie planen laufend neu und der gestrige Plan ist fast noch gueltig.&quot;</span>)</span>
<span id="cb9-215"><a href="#cb9-215" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;In beiden Faellen gilt: MESSEN, nicht glauben.&quot;</span>)</span>
<span id="cb9-216"><a href="#cb9-216" 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 (Zeiten hardwareabhängig):</strong></p>
<pre><code>==============================================================================
MIP-GAP UND ZEITLIMIT: STANDORTPLANUNG, 45 LAGER, 120 KUNDEN
==============================================================================
5,445 Variablen, davon 45 binaer.
[1] Zeitlimit 2 Sekunden
Status Time limit reached
bester Plan (Incumbent) 69,255.12 EUR
bewiesene Schranke 62,982.37 EUR
MIP-Gap 9.057 %
Knoten / Zeit 2 / 2.01 s
-&gt; zulaessig, aber nicht bewiesen optimal. Der wahre Bestwert
liegt zwischen 62,982.37 und 69,255.12 EUR.
[2] Zeitlimit 60 Sekunden
Status Optimal
bester Plan (Incumbent) 69,134.14 EUR
bewiesene Schranke 69,128.19 EUR
MIP-Gap 0.009 %
Knoten / Zeit 150 / 8.98 s
-&gt; beweisbar optimal
==============================================================================
WAS BRINGT MEHR RECHENZEIT?
==============================================================================
Nach 2 Sekunden: 69,255.12 EUR, Gap 9.06 %
Nach 9.0 Sekunden: 69,134.14 EUR, bewiesen optimal
Gewinn durch 7.0 Sekunden mehr Rechenzeit: 120.98 EUR (0.17 %)</code></pre>
<p><strong>Diese letzte Zeile ist die betriebswirtschaftlich interessante.</strong> Nach 2 Sekunden liegt ein Plan vor, der garantiert höchstens 9 % vom Optimum entfernt ist. Die restlichen 7 Sekunden Rechenzeit verbessern ihn um <strong>0,17 %</strong> — sie werden fast vollständig dafür verbraucht, die <em>Optimalität zu beweisen</em>, nicht den Plan zu verbessern.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Bei einem MILP kostet der Beweis der Optimalität meist ein Vielfaches dessen, was das Finden der optimalen Lösung kostet. Fragen Sie deshalb nicht „wie lange bis optimal?“, sondern „welchen Gap kann ich mir leisten?“. Ein nächtlicher Tourenplan darf eine Stunde rechnen; die Umplanung bei einem Maschinenausfall hat dreißig Sekunden.</p>
</blockquote>
<h3 id="warm-starts-was-sie-können-und-was-nicht">Warm-Starts: was sie können und was nicht</h3>
<p>Ein <strong>Warm-Start</strong> gibt dem Solver eine bekannte Lösung als Startpunkt mit. Die Idee ist verlockend: Wer schon eine brauchbare Lösung hat, muss nicht bei null anfangen.</p>
<p>In der Literatur wird das oft als sicherer Beschleuniger dargestellt. Das ist es <strong>nicht</strong> — und der ehrlichste Weg, das zu zeigen, ist eine Messung, die beide Seiten enthält. Der letzte Teil von <code>Solverstatus_und_Gap.py</code> füttert HiGHS mit einem Faustregel-Startplan:</p>
<pre><code>Faustregel-Startplan (ohne Solver): 75,091.59 EUR
Das sind 8.6 % ueber dem Optimum.
Zeit Knoten Ziel
------------------------------------------------------------------------------
ohne Warm-Start 8.98s 150 69,134.14
mit Warm-Start 8.44s 150 69,134.14</code></pre>
<p><strong>Kein nennenswerter Unterschied.</strong> Der Grund ist nicht, dass Warm-Starts nichts taugen, sondern dass HiGHS eigene Heuristiken innerhalb der ersten Sekunde bereits eine <em>bessere</em> Lösung finden als unsere Faustregel. Ein Startwert hilft nur, wenn er besser ist als das, was der Solver in derselben Zeit von allein findet.</p>
<p>Der Gegenfall — eine Heuristik mit echtem Domänenwissen:</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><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Warmstart_Effekt.py</span></span>
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb12-5"><a href="#cb12-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel MILP: Wann ein Warm-Start wirklich etwas bringt.</span></span>
<span id="cb12-6"><a href="#cb12-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-7"><a href="#cb12-7" aria-hidden="true" tabindex="-1"></a><span class="co">Solverstatus_und_Gap.py zeigt einen Fall, in dem ein Startwert NICHTS</span></span>
<span id="cb12-8"><a href="#cb12-8" aria-hidden="true" tabindex="-1"></a><span class="co">bringt - HiGHS findet von allein schneller etwas Besseres. Hier der</span></span>
<span id="cb12-9"><a href="#cb12-9" aria-hidden="true" tabindex="-1"></a><span class="co">Gegenfall: eine Heuristik, die dem Solver echtes Domaenenwissen liefert.</span></span>
<span id="cb12-10"><a href="#cb12-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-11"><a href="#cb12-11" aria-hidden="true" tabindex="-1"></a><span class="co">Problem: Lastverteilung. n Auftraege mit bekannter Dauer sind auf m</span></span>
<span id="cb12-12"><a href="#cb12-12" aria-hidden="true" tabindex="-1"></a><span class="co">gleichartige Maschinen zu verteilen, sodass die zuletzt fertige Maschine</span></span>
<span id="cb12-13"><a href="#cb12-13" aria-hidden="true" tabindex="-1"></a><span class="co">so frueh wie moeglich fertig wird (Makespan-Minimierung).</span></span>
<span id="cb12-14"><a href="#cb12-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-15"><a href="#cb12-15" aria-hidden="true" tabindex="-1"></a><span class="co">Die Heuristik: LPT (Longest Processing Time first) - laengste Auftraege</span></span>
<span id="cb12-16"><a href="#cb12-16" aria-hidden="true" tabindex="-1"></a><span class="co">zuerst, jeder auf die momentan am wenigsten belastete Maschine. Sie ist</span></span>
<span id="cb12-17"><a href="#cb12-17" aria-hidden="true" tabindex="-1"></a><span class="co">Jahrzehnte alt, in zwei Zeilen geschrieben und beweisbar nie schlechter</span></span>
<span id="cb12-18"><a href="#cb12-18" aria-hidden="true" tabindex="-1"></a><span class="co">als 4/3 des Optimums.</span></span>
<span id="cb12-19"><a href="#cb12-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-20"><a href="#cb12-20" aria-hidden="true" tabindex="-1"></a><span class="co">Ueber model.AddHint() bekommt CP-SAT diese Loesung als Startpunkt.</span></span>
<span id="cb12-21"><a href="#cb12-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-22"><a href="#cb12-22" aria-hidden="true" tabindex="-1"></a><span class="co">WICHTIG: highspy wird hier bewusst NICHT importiert - es vertraegt sich</span></span>
<span id="cb12-23"><a href="#cb12-23" aria-hidden="true" tabindex="-1"></a><span class="co">nicht mit ortools im selben Prozess (siehe Kapitel Oekosystem).</span></span>
<span id="cb12-24"><a href="#cb12-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-25"><a href="#cb12-25" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, ortools</span></span>
<span id="cb12-26"><a href="#cb12-26" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb12-27"><a href="#cb12-27" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-28"><a href="#cb12-28" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb12-29"><a href="#cb12-29" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-30"><a href="#cb12-30" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> time</span>
<span id="cb12-31"><a href="#cb12-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-32"><a href="#cb12-32" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb12-33"><a href="#cb12-33" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> ortools.sat.python <span class="im">import</span> cp_model</span>
<span id="cb12-34"><a href="#cb12-34" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-35"><a href="#cb12-35" aria-hidden="true" tabindex="-1"></a>RNG <span class="op">=</span> np.random.default_rng(<span class="dv">4</span>)</span>
<span id="cb12-36"><a href="#cb12-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-37"><a href="#cb12-37" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-38"><a href="#cb12-38" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> lpt_heuristik(dauer: np.ndarray, n_maschinen: <span class="bu">int</span>) <span class="op">-&gt;</span> <span class="bu">tuple</span>[np.ndarray, <span class="bu">int</span>]:</span>
<span id="cb12-39"><a href="#cb12-39" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Longest Processing Time first.</span></span>
<span id="cb12-40"><a href="#cb12-40" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-41"><a href="#cb12-41" aria-hidden="true" tabindex="-1"></a><span class="co"> Laengste Auftraege zuerst auf die jeweils freieste Maschine legen. Zwei</span></span>
<span id="cb12-42"><a href="#cb12-42" aria-hidden="true" tabindex="-1"></a><span class="co"> Zeilen, keine Bibliothek, Ergebnis in Mikrosekunden - und erstaunlich</span></span>
<span id="cb12-43"><a href="#cb12-43" aria-hidden="true" tabindex="-1"></a><span class="co"> nah am Optimum.</span></span>
<span id="cb12-44"><a href="#cb12-44" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb12-45"><a href="#cb12-45" aria-hidden="true" tabindex="-1"></a> zuordnung <span class="op">=</span> np.zeros(<span class="bu">len</span>(dauer), dtype<span class="op">=</span><span class="bu">int</span>)</span>
<span id="cb12-46"><a href="#cb12-46" aria-hidden="true" tabindex="-1"></a> belegung <span class="op">=</span> np.zeros(n_maschinen)</span>
<span id="cb12-47"><a href="#cb12-47" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> auftrag <span class="kw">in</span> np.argsort(<span class="op">-</span>dauer): <span class="co"># laengster zuerst</span></span>
<span id="cb12-48"><a href="#cb12-48" aria-hidden="true" tabindex="-1"></a> maschine <span class="op">=</span> <span class="bu">int</span>(np.argmin(belegung)) <span class="co"># freieste Maschine</span></span>
<span id="cb12-49"><a href="#cb12-49" aria-hidden="true" tabindex="-1"></a> zuordnung[auftrag] <span class="op">=</span> maschine</span>
<span id="cb12-50"><a href="#cb12-50" aria-hidden="true" tabindex="-1"></a> belegung[maschine] <span class="op">+=</span> dauer[auftrag]</span>
<span id="cb12-51"><a href="#cb12-51" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> zuordnung, <span class="bu">int</span>(belegung.<span class="bu">max</span>())</span>
<span id="cb12-52"><a href="#cb12-52" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-53"><a href="#cb12-53" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-54"><a href="#cb12-54" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese(dauer: np.ndarray, n_maschinen: <span class="bu">int</span>, zeitlimit: <span class="bu">float</span>,</span>
<span id="cb12-55"><a href="#cb12-55" aria-hidden="true" tabindex="-1"></a> hinweis: np.ndarray <span class="op">|</span> <span class="va">None</span> <span class="op">=</span> <span class="va">None</span>) <span class="op">-&gt;</span> <span class="bu">tuple</span>[<span class="bu">str</span>, <span class="bu">int</span>, <span class="bu">float</span>]:</span>
<span id="cb12-56"><a href="#cb12-56" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Exaktes Modell mit CP-SAT, optional mit Startloesung als Hinweis.&quot;&quot;&quot;</span></span>
<span id="cb12-57"><a href="#cb12-57" aria-hidden="true" tabindex="-1"></a> n_auftraege <span class="op">=</span> <span class="bu">len</span>(dauer)</span>
<span id="cb12-58"><a href="#cb12-58" aria-hidden="true" tabindex="-1"></a> obergrenze <span class="op">=</span> <span class="bu">int</span>(dauer.<span class="bu">sum</span>())</span>
<span id="cb12-59"><a href="#cb12-59" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-60"><a href="#cb12-60" aria-hidden="true" tabindex="-1"></a> modell <span class="op">=</span> cp_model.CpModel()</span>
<span id="cb12-61"><a href="#cb12-61" aria-hidden="true" tabindex="-1"></a> <span class="co"># x[i][k] = 1 &lt;=&gt; Auftrag i laeuft auf Maschine k</span></span>
<span id="cb12-62"><a href="#cb12-62" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> [[modell.NewBoolVar(<span class="ss">f&quot;x_</span><span class="sc">{</span>i<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>k<span class="sc">}</span><span class="ss">&quot;</span>) <span class="cf">for</span> k <span class="kw">in</span> <span class="bu">range</span>(n_maschinen)]</span>
<span id="cb12-63"><a href="#cb12-63" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n_auftraege)]</span>
<span id="cb12-64"><a href="#cb12-64" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n_auftraege):</span>
<span id="cb12-65"><a href="#cb12-65" aria-hidden="true" tabindex="-1"></a> modell.AddExactlyOne(x[i]) <span class="co"># jeder Auftrag genau einmal</span></span>
<span id="cb12-66"><a href="#cb12-66" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-67"><a href="#cb12-67" aria-hidden="true" tabindex="-1"></a> belegung <span class="op">=</span> [modell.NewIntVar(<span class="dv">0</span>, obergrenze, <span class="ss">f&quot;last_</span><span class="sc">{</span>k<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-68"><a href="#cb12-68" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> k <span class="kw">in</span> <span class="bu">range</span>(n_maschinen)]</span>
<span id="cb12-69"><a href="#cb12-69" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> k <span class="kw">in</span> <span class="bu">range</span>(n_maschinen):</span>
<span id="cb12-70"><a href="#cb12-70" aria-hidden="true" tabindex="-1"></a> modell.Add(belegung[k] <span class="op">==</span> <span class="bu">sum</span>(<span class="bu">int</span>(dauer[i]) <span class="op">*</span> x[i][k]</span>
<span id="cb12-71"><a href="#cb12-71" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n_auftraege)))</span>
<span id="cb12-72"><a href="#cb12-72" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-73"><a href="#cb12-73" aria-hidden="true" tabindex="-1"></a> makespan <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, obergrenze, <span class="st">&quot;makespan&quot;</span>)</span>
<span id="cb12-74"><a href="#cb12-74" aria-hidden="true" tabindex="-1"></a> modell.AddMaxEquality(makespan, belegung) <span class="co"># das Maximum ueber alle Maschinen</span></span>
<span id="cb12-75"><a href="#cb12-75" aria-hidden="true" tabindex="-1"></a> modell.Minimize(makespan)</span>
<span id="cb12-76"><a href="#cb12-76" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-77"><a href="#cb12-77" aria-hidden="true" tabindex="-1"></a> <span class="co"># Der Warm-Start: ein Hinweis pro Variable. CP-SAT muss ihn nicht</span></span>
<span id="cb12-78"><a href="#cb12-78" aria-hidden="true" tabindex="-1"></a> <span class="co"># befolgen - er nutzt ihn als erste Loesung, wenn er zulaessig ist.</span></span>
<span id="cb12-79"><a href="#cb12-79" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> hinweis <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span>
<span id="cb12-80"><a href="#cb12-80" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n_auftraege):</span>
<span id="cb12-81"><a href="#cb12-81" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> k <span class="kw">in</span> <span class="bu">range</span>(n_maschinen):</span>
<span id="cb12-82"><a href="#cb12-82" aria-hidden="true" tabindex="-1"></a> modell.AddHint(x[i][k], <span class="dv">1</span> <span class="cf">if</span> hinweis[i] <span class="op">==</span> k <span class="cf">else</span> <span class="dv">0</span>)</span>
<span id="cb12-83"><a href="#cb12-83" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-84"><a href="#cb12-84" aria-hidden="true" tabindex="-1"></a> loeser <span class="op">=</span> cp_model.CpSolver()</span>
<span id="cb12-85"><a href="#cb12-85" aria-hidden="true" tabindex="-1"></a> loeser.parameters.max_time_in_seconds <span class="op">=</span> zeitlimit</span>
Phase 6.3: Parallelitaet und Determinismus - gemessen statt behauptet Neuer Abschnitt im CP-SAT-Kapitel plus Parallele_Suche.py (76. Programm). Setzt Paket 2 aus Verbesserungen_02.md um. Was fehlte, war nicht die Regel, sondern die Messung: Das Buch setzt an acht Stellen num_workers = 1 mit dem Kommentar "fuer eine reproduzierbare Ausgabe", nachgeprueft hatte es nie jemand. Gerechnet wird auf demselben Job-Shop wie das Intervallvariablen-Kapitel, nur gross genug, dass die Suche arbeitet: 12 Auftraege, 10 Maschinen, 120 Arbeitsgaenge. Zwei Befunde, beide staerker als die Behauptung: * Die Beschleunigung ist ueberlinear. Acht Arbeiter waren im abgedruckten Lauf nicht achtmal, sondern 12,3-mal schneller als einer. Kein Messfehler: CP-SAT vervielfacht nicht dieselbe Suche, sondern laesst verschiedene Strategien nebeneinander laufen, die einander ihre Schranken mitteilen. * Der Seed genuegt nicht - und zwar schon ab ZWEI Arbeitern. Ein Arbeiter: 1 Plan aus 4 Laeufen. Zwei Arbeiter: 3 verschiedene Plaene aus 4 Laeufen, bei identischem random_seed und identischem Zielwert 183. Die vollstaendige Antwort kam erst ueber die Uebungsaufgabe: Mit num_workers = 1, aber fuenf verschiedenen Seeds ergeben sich ebenfalls fuenf verschiedene Plaene. Keiner der beiden Parameter sichert die Reproduzierbarkeit allein - erst die Kombination traegt. Weiter fuer die Aufgabe gemessen: Der Gewinn kehrt sich um (auf 24 Kernen Faktor 10,2 bei 8 Arbeitern, 12,3 bei 16, 9,0 bei 24) - "so viele Arbeiter wie Kerne" ist damit widerlegt. Und bei 15 Auftraegen laeuft ein Arbeiter ins 60-s-Limit (FEASIBLE, Makespan 200), waehrend acht OPTIMAL mit demselben Makespan 200 nach 26,4 s melden: Der Unterschied liegt nicht in der Loesung, sondern im Beweis, dass es keine bessere gibt. Die abgedruckte Ausgabe traegt die Kennzeichnung "Laufzeiten und die Zahl der verschiedenen Plaene sind hardwareabhaengig" - nach dem Muster, das das Testing-Kapitel fuer Benchmark_Skalierung.py schon verwendet. Der Vergleich des extrahierten Programms mit dem Abdruck weicht denn auch in genau einer Zelle ab (4 statt 3 verschiedene Plaene bei 4 Arbeitern); Zielwert und Struktur sind identisch. Hier ist die Nichtreproduzierbarkeit der abgedruckten Zahl die Aussage selbst. Mitgezogen: Kapitelkopf, Lernziele, Selbsttest, Zusammenfassung, Vorwort-Programmverzeichnis, Uebungsaufgabe und Loesung in Anhang A, ein Verweis aus dem bestehenden Callout zu mehrdeutigen Optima und einer aus Warmstart_Effekt.py im MILP-Kapitel (dort nach Regel 12 der Kapitelname). Stand: 295 Abschnitte, 730 Querverweise, 328 Indexmarken, 76 Programme, 140 Aufgaben mit 140 Loesungen, 33 pytest-Tests, PDF 744 Seiten, 69 netzfreie Programme fehlerfrei. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:19:53 +02:00
<span id="cb12-86"><a href="#cb12-86" aria-hidden="true" tabindex="-1"></a> <span class="co"># Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist -</span></span>
<span id="cb12-87"><a href="#cb12-87" aria-hidden="true" tabindex="-1"></a> <span class="co"># der Seed allein genuegt dafuer NICHT (Kapitel Constraint Programming).</span></span>
<span id="cb12-88"><a href="#cb12-88" aria-hidden="true" tabindex="-1"></a> <span class="co"># Im Produktivbetrieb laesst man beides auf den Standardwerten.</span></span>
<span id="cb12-89"><a href="#cb12-89" aria-hidden="true" tabindex="-1"></a> loeser.parameters.num_workers <span class="op">=</span> <span class="dv">1</span></span>
<span id="cb12-90"><a href="#cb12-90" aria-hidden="true" tabindex="-1"></a> loeser.parameters.random_seed <span class="op">=</span> <span class="dv">1</span></span>
<span id="cb12-91"><a href="#cb12-91" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-92"><a href="#cb12-92" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()</span>
<span id="cb12-93"><a href="#cb12-93" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> loeser.Solve(modell)</span>
<span id="cb12-94"><a href="#cb12-94" aria-hidden="true" tabindex="-1"></a> dauer_s <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
<span id="cb12-95"><a href="#cb12-95" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-96"><a href="#cb12-96" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> status <span class="kw">not</span> <span class="kw">in</span> (cp_model.OPTIMAL, cp_model.FEASIBLE):</span>
<span id="cb12-97"><a href="#cb12-97" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="ss">f&quot;Kein Plan gefunden: </span><span class="sc">{</span>loeser<span class="sc">.</span>StatusName(status)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-98"><a href="#cb12-98" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> loeser.StatusName(status), <span class="bu">int</span>(loeser.ObjectiveValue()), dauer_s</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="cb12-99"><a href="#cb12-99" aria-hidden="true" tabindex="-1"></a></span>
Phase 6.3: Parallelitaet und Determinismus - gemessen statt behauptet Neuer Abschnitt im CP-SAT-Kapitel plus Parallele_Suche.py (76. Programm). Setzt Paket 2 aus Verbesserungen_02.md um. Was fehlte, war nicht die Regel, sondern die Messung: Das Buch setzt an acht Stellen num_workers = 1 mit dem Kommentar "fuer eine reproduzierbare Ausgabe", nachgeprueft hatte es nie jemand. Gerechnet wird auf demselben Job-Shop wie das Intervallvariablen-Kapitel, nur gross genug, dass die Suche arbeitet: 12 Auftraege, 10 Maschinen, 120 Arbeitsgaenge. Zwei Befunde, beide staerker als die Behauptung: * Die Beschleunigung ist ueberlinear. Acht Arbeiter waren im abgedruckten Lauf nicht achtmal, sondern 12,3-mal schneller als einer. Kein Messfehler: CP-SAT vervielfacht nicht dieselbe Suche, sondern laesst verschiedene Strategien nebeneinander laufen, die einander ihre Schranken mitteilen. * Der Seed genuegt nicht - und zwar schon ab ZWEI Arbeitern. Ein Arbeiter: 1 Plan aus 4 Laeufen. Zwei Arbeiter: 3 verschiedene Plaene aus 4 Laeufen, bei identischem random_seed und identischem Zielwert 183. Die vollstaendige Antwort kam erst ueber die Uebungsaufgabe: Mit num_workers = 1, aber fuenf verschiedenen Seeds ergeben sich ebenfalls fuenf verschiedene Plaene. Keiner der beiden Parameter sichert die Reproduzierbarkeit allein - erst die Kombination traegt. Weiter fuer die Aufgabe gemessen: Der Gewinn kehrt sich um (auf 24 Kernen Faktor 10,2 bei 8 Arbeitern, 12,3 bei 16, 9,0 bei 24) - "so viele Arbeiter wie Kerne" ist damit widerlegt. Und bei 15 Auftraegen laeuft ein Arbeiter ins 60-s-Limit (FEASIBLE, Makespan 200), waehrend acht OPTIMAL mit demselben Makespan 200 nach 26,4 s melden: Der Unterschied liegt nicht in der Loesung, sondern im Beweis, dass es keine bessere gibt. Die abgedruckte Ausgabe traegt die Kennzeichnung "Laufzeiten und die Zahl der verschiedenen Plaene sind hardwareabhaengig" - nach dem Muster, das das Testing-Kapitel fuer Benchmark_Skalierung.py schon verwendet. Der Vergleich des extrahierten Programms mit dem Abdruck weicht denn auch in genau einer Zelle ab (4 statt 3 verschiedene Plaene bei 4 Arbeitern); Zielwert und Struktur sind identisch. Hier ist die Nichtreproduzierbarkeit der abgedruckten Zahl die Aussage selbst. Mitgezogen: Kapitelkopf, Lernziele, Selbsttest, Zusammenfassung, Vorwort-Programmverzeichnis, Uebungsaufgabe und Loesung in Anhang A, ein Verweis aus dem bestehenden Callout zu mehrdeutigen Optima und einer aus Warmstart_Effekt.py im MILP-Kapitel (dort nach Regel 12 der Kapitelname). Stand: 295 Abschnitte, 730 Querverweise, 328 Indexmarken, 76 Programme, 140 Aufgaben mit 140 Loesungen, 33 pytest-Tests, PDF 744 Seiten, 69 netzfreie Programme fehlerfrei. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:19:53 +02:00
<span id="cb12-100"><a href="#cb12-100" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-101"><a href="#cb12-101" 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="cb12-102"><a href="#cb12-102" 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="cb12-103"><a href="#cb12-103" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; WARM-START: WENN DIE HEURISTIK MEHR WEISS ALS DER SOLVER&quot;</span>)</span>
<span id="cb12-104"><a href="#cb12-104" 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="cb12-105"><a href="#cb12-105" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Lastverteilung: Auftraege auf gleichartige Maschinen verteilen,&quot;</span>)</span>
<span id="cb12-106"><a href="#cb12-106" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;sodass die letzte Maschine so frueh wie moeglich fertig wird.</span><span class="ch">\n</span><span class="st">&quot;</span>)</span>
<span id="cb12-107"><a href="#cb12-107" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-108"><a href="#cb12-108" 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;Instanz&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Variante&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Makespan&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Zeit&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> &quot;</span></span>
<span id="cb12-109"><a href="#cb12-109" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Faktor&#39;</span><span class="sc">:&gt;8}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-110"><a href="#cb12-110" 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="cb12-111"><a href="#cb12-111" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-112"><a href="#cb12-112" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> n_auftraege, n_maschinen <span class="kw">in</span> [(<span class="dv">60</span>, <span class="dv">7</span>), (<span class="dv">80</span>, <span class="dv">9</span>)]:</span>
<span id="cb12-113"><a href="#cb12-113" aria-hidden="true" tabindex="-1"></a> dauer <span class="op">=</span> RNG.integers(<span class="dv">10</span>, <span class="dv">90</span>, n_auftraege)</span>
<span id="cb12-114"><a href="#cb12-114" aria-hidden="true" tabindex="-1"></a> start, lpt_wert <span class="op">=</span> lpt_heuristik(dauer, n_maschinen)</span>
<span id="cb12-115"><a href="#cb12-115" aria-hidden="true" tabindex="-1"></a> untere_schranke <span class="op">=</span> dauer.<span class="bu">sum</span>() <span class="op">/</span> n_maschinen</span>
<span id="cb12-116"><a href="#cb12-116" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-117"><a href="#cb12-117" aria-hidden="true" tabindex="-1"></a> instanz <span class="op">=</span> <span class="ss">f&quot;</span><span class="sc">{</span>n_auftraege<span class="sc">}</span><span class="ss"> Auftr., </span><span class="sc">{</span>n_maschinen<span class="sc">}</span><span class="ss"> Masch.&quot;</span></span>
<span id="cb12-118"><a href="#cb12-118" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>instanz<span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;LPT-Heuristik&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span>lpt_wert<span class="sc">:&gt;9}</span><span class="ss"> &quot;</span></span>
<span id="cb12-119"><a href="#cb12-119" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;&lt; 0.001s&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&gt;8}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-120"><a href="#cb12-120" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-121"><a href="#cb12-121" aria-hidden="true" tabindex="-1"></a> _, ziel_kalt, zeit_kalt <span class="op">=</span> loese(dauer, n_maschinen, <span class="fl">60.0</span>)</span>
<span id="cb12-122"><a href="#cb12-122" 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;&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;CP-SAT kalt&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span>ziel_kalt<span class="sc">:&gt;9}</span><span class="ss"> &quot;</span></span>
<span id="cb12-123"><a href="#cb12-123" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>zeit_kalt<span class="sc">:&gt;8.3f}</span><span class="ss">s </span><span class="sc">{</span><span class="st">&#39;1,0x&#39;</span><span class="sc">:&gt;8}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-124"><a href="#cb12-124" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-125"><a href="#cb12-125" aria-hidden="true" tabindex="-1"></a> _, ziel_warm, zeit_warm <span class="op">=</span> loese(dauer, n_maschinen, <span class="fl">60.0</span>, hinweis<span class="op">=</span>start)</span>
<span id="cb12-126"><a href="#cb12-126" 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;&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;CP-SAT + LPT-Hinweis&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span>ziel_warm<span class="sc">:&gt;9}</span><span class="ss"> &quot;</span></span>
<span id="cb12-127"><a href="#cb12-127" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>zeit_warm<span class="sc">:&gt;8.3f}</span><span class="ss">s </span><span class="sc">{</span>zeit_kalt <span class="op">/</span> zeit_warm<span class="sc">:&gt;7.1f}</span><span class="ss">x&quot;</span>)</span>
<span id="cb12-128"><a href="#cb12-128" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-129"><a href="#cb12-129" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> ziel_kalt <span class="op">==</span> ziel_warm, <span class="op">\</span></span>
<span id="cb12-130"><a href="#cb12-130" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Der Hinweis darf das Optimum nicht veraendern - nur den Weg dorthin!&quot;</span></span>
<span id="cb12-131"><a href="#cb12-131" 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;&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;untere Schranke&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span>untere_schranke<span class="sc">:&gt;9.1f}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-132"><a href="#cb12-132" 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="cb12-133"><a href="#cb12-133" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-134"><a href="#cb12-134" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">Drei Beobachtungen:&quot;</span>)</span>
<span id="cb12-135"><a href="#cb12-135" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;1. Der Hinweis aendert das ERGEBNIS nicht - beide Laeufe finden&quot;</span>)</span>
<span id="cb12-136"><a href="#cb12-136" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; dasselbe Optimum. Er aendert nur, wie lange der Beweis dauert.&quot;</span>)</span>
<span id="cb12-137"><a href="#cb12-137" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Genau deshalb ist ein Warm-Start ungefaehrlich: Ein schlechter&quot;</span>)</span>
<span id="cb12-138"><a href="#cb12-138" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Hinweis kostet Zeit, er verfaelscht aber nie die Loesung.&quot;</span>)</span>
<span id="cb12-139"><a href="#cb12-139" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;2. Die LPT-Heuristik liegt schon sehr nah am Optimum. Ihr Wert fuer&quot;</span>)</span>
<span id="cb12-140"><a href="#cb12-140" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; den Solver liegt weniger in der Qualitaet als darin, dass sie&quot;</span>)</span>
<span id="cb12-141"><a href="#cb12-141" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; SOFORT da ist - der Solver kann von Beginn an alles verwerfen,&quot;</span>)</span>
<span id="cb12-142"><a href="#cb12-142" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; was schlechter ist.&quot;</span>)</span>
<span id="cb12-143"><a href="#cb12-143" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;3. Der Faktor schwankt von Instanz zu Instanz - oben 2,2x und 1,4x -&quot;</span>)</span>
<span id="cb12-144"><a href="#cb12-144" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; und laesst sich NICHT aus der Problemgroesse ableiten. Er haengt&quot;</span>)</span>
<span id="cb12-145"><a href="#cb12-145" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; davon ab, wie schnell der Solver von allein eine vergleichbar gute&quot;</span>)</span>
<span id="cb12-146"><a href="#cb12-146" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Loesung findet. Das ist die eigentliche Lehre: Ein Warm-Start ist&quot;</span>)</span>
<span id="cb12-147"><a href="#cb12-147" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; eine Messung wert, keine Glaubensfrage.&quot;</span>)</span>
<span id="cb12-148"><a href="#cb12-148" 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>
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>Erwartete Ausgabe (Zeiten hardwareabhängig):</strong></p>
<pre><code>==============================================================================
WARM-START: WENN DIE HEURISTIK MEHR WEISS ALS DER SOLVER
==============================================================================
Lastverteilung: Auftraege auf gleichartige Maschinen verteilen,
sodass die letzte Maschine so frueh wie moeglich fertig wird.
Instanz Variante Makespan Zeit Faktor
------------------------------------------------------------------------------
60 Auftr., 7 Masch. LPT-Heuristik 483 &lt; 0.001s
CP-SAT kalt 475 0.627s 1,0x
CP-SAT + LPT-Hinweis 475 0.292s 2.2x
untere Schranke 474.7
------------------------------------------------------------------------------
80 Auftr., 9 Masch. LPT-Heuristik 445 &lt; 0.001s
CP-SAT kalt 442 3.275s 1,0x
CP-SAT + LPT-Hinweis 442 2.420s 1.4x
untere Schranke 441.8
------------------------------------------------------------------------------</code></pre>
<p>Hier wirkt der Hinweis: Faktor 2,2 bzw. 1,4. Beachten Sie aber, dass der Faktor <strong>schwankt</strong> und sich nicht aus der Problemgröße ableiten lässt — bei der größeren Instanz ist er sogar kleiner. Es gibt keine Regel, es gibt nur die Messung.</p>
<blockquote>
<p><strong>⚠️ Typische Fehler</strong></p>
<ul>
<li><strong><code>if status == OPTIMAL: ... else: return None</code>.</strong> Wirft eine Lösung weg, die 9 % vom Optimum entfernt und damit vollkommen brauchbar ist. Werten Sie den Gap aus, nicht nur den Status.</li>
<li><strong>Zeitlimit ohne Gap-Auswertung.</strong> Ein Limit erzwingt ein Ende, sagt aber nichts über die Qualität. Ohne den Gap wissen Sie nicht, ob Sie 0,1 % oder 60 % danebenliegen.</li>
<li><strong>Warm-Start als Selbstverständlichkeit.</strong> Messen Sie ihn — auf Ihrer Instanzklasse, gegen den kalten Lauf. In der Hälfte der Fälle bringt er nichts.</li>
<li><strong>Einen Hinweis für eine unzulässige Lösung geben.</strong> <a href="anhang-glossar.md#gloss:constraint-programming-cp" class="glossar-link">CP (Constraint Programming)</a>-<a href="anhang-glossar.md#gloss:sat-boolean-satisfiability" class="glossar-link">SAT (Boolean Satisfiability)</a> und HiGHS verwerfen ihn dann stillschweigend. Prüfen Sie Ihre Startlösung vorher auf Zulässigkeit — sonst messen Sie einen Effekt, den es gar nicht gibt.</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>
<blockquote>
<p><strong>🎯 Merksatz</strong> Ein Warm-Start kann das Ergebnis nie verfälschen — im schlimmsten Fall kostet er Zeit. Genau deshalb darf man ihn ausprobieren. Aber man muss ihn auch <strong>messen</strong>, statt an ihn zu glauben.</p>
</blockquote>
<hr />
<h2 id="sec:milp-uebungsaufgaben">6.9 Übungsaufgaben</h2>
<blockquote>
<p>Lösungen: <a href="anhang-loesungen.html#sec:loesungen-milp">Abschnitt A.6</a>.</p>
</blockquote>
<p><strong>Aufgabe 6.1 ⭐ — Runden widerlegen.</strong> Konstruieren Sie selbst ein Beispiel mit zwei Variablen, bei dem Abrunden der LP-Lösung mindestens 50 % des optimalen Zielwerts verliert. Begründen Sie.</p>
<p><strong>Aufgabe 6.2 ⭐ — Big-M wählen.</strong> Ein Modell enthält <span class="math inline">x_j \le M y_j</span> mit <span class="math inline">x_j \le 250</span> als bekannter Kapazität. Welchen Wert sollte <span class="math inline">M</span> haben? Was passiert bei <span class="math inline">M = 10^6</span>?</p>
<p><strong>Aufgabe 6.3 ⭐⭐ — Regeln in Ungleichungen übersetzen.</strong> Formulieren Sie mit Binärvariablen: (a) „Wenn Produkt A produziert wird, darf Produkt B nicht produziert werden.“ (b) „Mindestens zwei der vier Standorte müssen eröffnet werden.“ (c) „Wenn Standort 1 <strong>und</strong> Standort 2 eröffnet werden, muss auch das Zentrallager gebaut werden.“ (d) „Die Produktionsmenge ist entweder 0 oder liegt zwischen 500 und 2000.“ (e) „Genau eine der drei Maschinen wird eingesetzt.“</p>
<p><strong>Aufgabe 6.4 ⭐⭐ — Branch-and-Bound von Hand.</strong> Lösen Sie mit Branch-and-Bound und zeichnen Sie den Suchbaum: <span class="math display">\max\ 8x_1+11x_2+6x_3+4x_4 \quad\text{u. d. N.}\quad 5x_1+7x_2+4x_3+3x_4\le14,\quad x_i\in\{0,1\}</span> Geben Sie für jeden Knoten die LP-Schranke an und markieren Sie, wo gekappt wird.</p>
<p><strong>Aufgabe 6.5 ⭐⭐⭐ — Kardinalität variieren.</strong> Erweitern Sie <code>MILP_Portfolio_Fixgebuehren.py</code> so, dass es <span class="math inline">K = 1, 2, \dots, 6</span> durchläuft und Netto-Ertrag sowie Rechenzeit tabelliert. (a) Ab welchem <span class="math inline">K</span> steigt der Ertrag nicht mehr? Warum? (b) Wie verhält sich die Rechenzeit? (c) Was wäre der „faire Preis“ für die Erlaubnis, eine vierte Position zu eröffnen?</p>
<p><strong>Aufgabe 6.6 ⭐⭐⭐ — Big-M-Effekt messen.</strong> Ersetzen Sie im Portfoliomodell <code>MAX_POSITION</code> in der Big-M-Zeile durch <span class="math inline">10^4</span>, <span class="math inline">10^6</span> und <span class="math inline">10^9</span> (die echte Obergrenze bleibt in den <code>bounds</code>). Messen Sie jeweils Laufzeit und Zahl der Branch-and-Bound-Knoten (<code>ergebnis.mip_node_count</code>). Stellen Sie die Ergebnisse dar und erklären Sie sie.</p>
<p><strong>Aufgabe 6.7 ⭐⭐⭐ — Standortplanung.</strong> Modellieren und lösen Sie: Fünf mögliche Lagerstandorte mit Fixkosten <span class="math inline">(80, 60, 90, 70, 50)</span> Tsd. € versorgen vier Regionen mit Bedarf <span class="math inline">(30, 45, 25, 40)</span> Einheiten. Transportkosten je Einheit stehen in einer <span class="math inline">5\times4</span>-Matrix Ihrer Wahl. Jedes eröffnete Lager hat Kapazität 80. Minimieren Sie Fix- plus Transportkosten.</p>
<hr />
<h2 id="sec:milp-denkfehler">6.10 Finde den Denkfehler</h2>
<p>Die Warnung „<span class="math inline">M</span> so klein wie möglich“ wird meist mit der Laufzeit begründet: schwache Relaxation, mehr Knoten. Das stimmt — und ist die <strong>harmlosere</strong> Hälfte der Wahrheit.</p>
<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: Elf Lager, die keine Fixkosten kosten</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 Team plant Standorte: 12 mögliche Lager, 40 Kunden, Fixkosten je eröffnetem Lager. Für das Big-M in der Kopplung</p>
<p><span class="math display">\sum_j x_{ij} \le M \cdot y_i</span></p>
<p>setzt jemand „sicherheitshalber“ eine sehr große Zahl ein. Das Modell läuft durch und meldet Gesamtkosten von <strong>12 441,96 €</strong>. Mit einem knapp gewählten <span class="math inline">M</span> hatte dasselbe Modell zuvor 26 525,28 € gemeldet. Das Team freut sich über die Einsparung.</p>
<p><strong>Ihre Aufgabe:</strong> (a) Warum ist die zweite Zahl <em>kleiner</em>, obwohl sich am Problem nichts geändert hat? Was müsste für ein größeres <span class="math inline">M</span> mathematisch gelten? (b) Sehen Sie sich unten die Werte der Binärvariablen an — was fällt auf, und was hat das mit der Ganzzahltoleranz des Solvers zu tun? (c) Wie viele Lager liefern in dieser „Lösung“ tatsächlich Ware, und wie viele Fixkosten werden dafür verbucht? (d) Formulieren Sie die Prüfung, die diesen Fehler in jedem MILP-Skript auffliegen ließe.</p>
<p><em>Auflösung: <a href="anhang-loesungen.html#sec:loesungen-milp">Abschnitt A.6</a>.</em></p>
</blockquote>
</div>
<p>Das folgende Programm führt beide Läufe nebeneinander aus und wendet auf beide dieselbe Prüfung an.</p>
<div class="sourceCode" id="cb14"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Big_M_Falle.py</span></span>
<span id="cb14-4"><a href="#cb14-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb14-5"><a href="#cb14-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel MILP: Was ein zu grosses Big-M wirklich anrichtet.</span></span>
<span id="cb14-6"><a href="#cb14-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-7"><a href="#cb14-7" aria-hidden="true" tabindex="-1"></a><span class="co">Lehrbuecher warnen vor grossem M mit dem Hinweis &quot;die Relaxation wird</span></span>
<span id="cb14-8"><a href="#cb14-8" aria-hidden="true" tabindex="-1"></a><span class="co">schwach, der Solver braucht mehr Knoten&quot;. Das stimmt - ist aber die</span></span>
<span id="cb14-9"><a href="#cb14-9" aria-hidden="true" tabindex="-1"></a><span class="co">harmlosere Haelfte der Wahrheit. Die gefaehrlichere: Bei sehr grossem M</span></span>
<span id="cb14-10"><a href="#cb14-10" aria-hidden="true" tabindex="-1"></a><span class="co">kann der Solver eine Loesung als ganzzahlig ANNEHMEN, in der die</span></span>
<span id="cb14-11"><a href="#cb14-11" aria-hidden="true" tabindex="-1"></a><span class="co">Binaervariablen bei 1e-8 stehen. Dann liefern zugeschaltete Anlagen Ware</span></span>
<span id="cb14-12"><a href="#cb14-12" aria-hidden="true" tabindex="-1"></a><span class="co">aus, waehrend das Modell ihre Fixkosten mit 0 verbucht.</span></span>
<span id="cb14-13"><a href="#cb14-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-14"><a href="#cb14-14" aria-hidden="true" tabindex="-1"></a><span class="co">Beispiel: Standortplanung, 12 moegliche Lager, 40 Kunden.</span></span>
<span id="cb14-15"><a href="#cb14-15" aria-hidden="true" tabindex="-1"></a><span class="co"> min sum_i fix_i * y_i + sum_ij kosten_ij * x_ij</span></span>
<span id="cb14-16"><a href="#cb14-16" aria-hidden="true" tabindex="-1"></a><span class="co"> u.d.N. sum_i x_ij = bedarf_j (jeder Kunde beliefert)</span></span>
<span id="cb14-17"><a href="#cb14-17" aria-hidden="true" tabindex="-1"></a><span class="co"> sum_j x_ij &lt;= M_i * y_i (Lager offen, wenn es liefert)</span></span>
<span id="cb14-18"><a href="#cb14-18" aria-hidden="true" tabindex="-1"></a><span class="co"> y_i binaer, x_ij &gt;= 0</span></span>
<span id="cb14-19"><a href="#cb14-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-20"><a href="#cb14-20" aria-hidden="true" tabindex="-1"></a><span class="co">Zwei Laeufe:</span></span>
<span id="cb14-21"><a href="#cb14-21" aria-hidden="true" tabindex="-1"></a><span class="co"> 1. M knapp gewaehlt (= tatsaechliche Lagerkapazitaet) -&gt; richtig</span></span>
<span id="cb14-22"><a href="#cb14-22" aria-hidden="true" tabindex="-1"></a><span class="co"> 2. M = 1e7-fach zu gross, Presolve abgeschaltet -&gt; stilles Desaster</span></span>
<span id="cb14-23"><a href="#cb14-23" aria-hidden="true" tabindex="-1"></a><span class="co">Beide werden mit derselben Pruefung kontrolliert, die den Fall auffliegen laesst.</span></span>
<span id="cb14-24"><a href="#cb14-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-25"><a href="#cb14-25" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, highspy</span></span>
<span id="cb14-26"><a href="#cb14-26" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb14-27"><a href="#cb14-27" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-28"><a href="#cb14-28" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb14-29"><a href="#cb14-29" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-30"><a href="#cb14-30" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> time</span>
<span id="cb14-31"><a href="#cb14-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-32"><a href="#cb14-32" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb14-33"><a href="#cb14-33" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> highspy</span>
<span id="cb14-34"><a href="#cb14-34" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-35"><a href="#cb14-35" aria-hidden="true" tabindex="-1"></a>RNG <span class="op">=</span> np.random.default_rng(<span class="dv">11</span>)</span>
<span id="cb14-36"><a href="#cb14-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-37"><a href="#cb14-37" aria-hidden="true" tabindex="-1"></a>N_LAGER, N_KUNDE <span class="op">=</span> <span class="dv">12</span>, <span class="dv">40</span></span>
<span id="cb14-38"><a href="#cb14-38" aria-hidden="true" tabindex="-1"></a>FIXKOSTEN <span class="op">=</span> RNG.uniform(<span class="dv">2000</span>, <span class="dv">5000</span>, N_LAGER)</span>
<span id="cb14-39"><a href="#cb14-39" aria-hidden="true" tabindex="-1"></a>TRANSPORT <span class="op">=</span> RNG.uniform(<span class="dv">5</span>, <span class="dv">40</span>, (N_LAGER, N_KUNDE))</span>
<span id="cb14-40"><a href="#cb14-40" aria-hidden="true" tabindex="-1"></a>BEDARF <span class="op">=</span> RNG.uniform(<span class="dv">10</span>, <span class="dv">60</span>, N_KUNDE)</span>
<span id="cb14-41"><a href="#cb14-41" aria-hidden="true" tabindex="-1"></a>KAPAZITAET <span class="op">=</span> BEDARF.<span class="bu">sum</span>() <span class="op">*</span> <span class="fl">0.45</span> <span class="co"># jedes Lager schafft 45 % des Gesamtbedarfs</span></span>
<span id="cb14-42"><a href="#cb14-42" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-43"><a href="#cb14-43" aria-hidden="true" tabindex="-1"></a><span class="co"># Toleranz, ab der ein Solver eine Variable als ganzzahlig durchgehen laesst.</span></span>
<span id="cb14-44"><a href="#cb14-44" aria-hidden="true" tabindex="-1"></a><span class="co"># HiGHS und die meisten anderen verwenden 1e-6 als Standard.</span></span>
<span id="cb14-45"><a href="#cb14-45" aria-hidden="true" tabindex="-1"></a>GANZZAHL_TOLERANZ <span class="op">=</span> <span class="fl">1e-6</span></span>
<span id="cb14-46"><a href="#cb14-46" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-47"><a href="#cb14-47" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-48"><a href="#cb14-48" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese(big_m: <span class="bu">float</span>, presolve: <span class="bu">str</span> <span class="op">=</span> <span class="st">&quot;on&quot;</span>) <span class="op">-&gt;</span> <span class="bu">dict</span>:</span>
<span id="cb14-49"><a href="#cb14-49" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Baut und loest das Standortmodell. Liefert Loesung und Solverkennzahlen.&quot;&quot;&quot;</span></span>
<span id="cb14-50"><a href="#cb14-50" aria-hidden="true" tabindex="-1"></a> hochschule <span class="op">=</span> highspy.Highs()</span>
<span id="cb14-51"><a href="#cb14-51" aria-hidden="true" tabindex="-1"></a> hochschule.setOptionValue(<span class="st">&quot;output_flag&quot;</span>, <span class="va">False</span>)</span>
<span id="cb14-52"><a href="#cb14-52" aria-hidden="true" tabindex="-1"></a> hochschule.setOptionValue(<span class="st">&quot;presolve&quot;</span>, presolve)</span>
<span id="cb14-53"><a href="#cb14-53" aria-hidden="true" tabindex="-1"></a> hochschule.setOptionValue(<span class="st">&quot;time_limit&quot;</span>, <span class="fl">300.0</span>)</span>
<span id="cb14-54"><a href="#cb14-54" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-55"><a href="#cb14-55" aria-hidden="true" tabindex="-1"></a> anzahl_x <span class="op">=</span> N_LAGER <span class="op">*</span> N_KUNDE</span>
<span id="cb14-56"><a href="#cb14-56" aria-hidden="true" tabindex="-1"></a> unendlich <span class="op">=</span> highspy.kHighsInf</span>
<span id="cb14-57"><a href="#cb14-57" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-58"><a href="#cb14-58" aria-hidden="true" tabindex="-1"></a> <span class="co"># Spalten: erst alle x_ij, dann die y_i</span></span>
<span id="cb14-59"><a href="#cb14-59" aria-hidden="true" tabindex="-1"></a> hochschule.addVars(anzahl_x, np.zeros(anzahl_x), np.full(anzahl_x, unendlich))</span>
<span id="cb14-60"><a href="#cb14-60" aria-hidden="true" tabindex="-1"></a> hochschule.addVars(N_LAGER, np.zeros(N_LAGER), np.ones(N_LAGER))</span>
<span id="cb14-61"><a href="#cb14-61" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N_LAGER):</span>
<span id="cb14-62"><a href="#cb14-62" aria-hidden="true" tabindex="-1"></a> hochschule.changeColIntegrality(anzahl_x <span class="op">+</span> i,</span>
<span id="cb14-63"><a href="#cb14-63" aria-hidden="true" tabindex="-1"></a> highspy.HighsVarType.kInteger)</span>
<span id="cb14-64"><a href="#cb14-64" aria-hidden="true" tabindex="-1"></a> hochschule.changeColCost(anzahl_x <span class="op">+</span> i, FIXKOSTEN[i])</span>
<span id="cb14-65"><a href="#cb14-65" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(N_KUNDE):</span>
<span id="cb14-66"><a href="#cb14-66" aria-hidden="true" tabindex="-1"></a> hochschule.changeColCost(i <span class="op">*</span> N_KUNDE <span class="op">+</span> j, TRANSPORT[i, j])</span>
<span id="cb14-67"><a href="#cb14-67" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-68"><a href="#cb14-68" aria-hidden="true" tabindex="-1"></a> <span class="co"># Jeder Kunde wird genau beliefert</span></span>
<span id="cb14-69"><a href="#cb14-69" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(N_KUNDE):</span>
<span id="cb14-70"><a href="#cb14-70" aria-hidden="true" tabindex="-1"></a> index <span class="op">=</span> np.array([i <span class="op">*</span> N_KUNDE <span class="op">+</span> j <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N_LAGER)], dtype<span class="op">=</span>np.int32)</span>
<span id="cb14-71"><a href="#cb14-71" aria-hidden="true" tabindex="-1"></a> hochschule.addRow(BEDARF[j], BEDARF[j], <span class="bu">len</span>(index), index,</span>
<span id="cb14-72"><a href="#cb14-72" aria-hidden="true" tabindex="-1"></a> np.ones(<span class="bu">len</span>(index)))</span>
<span id="cb14-73"><a href="#cb14-73" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-74"><a href="#cb14-74" aria-hidden="true" tabindex="-1"></a> <span class="co"># Die Kopplung: sum_j x_ij - M * y_i &lt;= 0</span></span>
<span id="cb14-75"><a href="#cb14-75" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N_LAGER):</span>
<span id="cb14-76"><a href="#cb14-76" aria-hidden="true" tabindex="-1"></a> index <span class="op">=</span> np.array([i <span class="op">*</span> N_KUNDE <span class="op">+</span> j <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(N_KUNDE)] <span class="op">+</span> [anzahl_x <span class="op">+</span> i],</span>
<span id="cb14-77"><a href="#cb14-77" aria-hidden="true" tabindex="-1"></a> dtype<span class="op">=</span>np.int32)</span>
<span id="cb14-78"><a href="#cb14-78" aria-hidden="true" tabindex="-1"></a> werte <span class="op">=</span> np.concatenate([np.ones(N_KUNDE), [<span class="op">-</span>big_m]])</span>
<span id="cb14-79"><a href="#cb14-79" aria-hidden="true" tabindex="-1"></a> hochschule.addRow(<span class="op">-</span>unendlich, <span class="fl">0.0</span>, <span class="bu">len</span>(index), index, werte)</span>
<span id="cb14-80"><a href="#cb14-80" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-81"><a href="#cb14-81" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()</span>
<span id="cb14-82"><a href="#cb14-82" aria-hidden="true" tabindex="-1"></a> hochschule.run()</span>
<span id="cb14-83"><a href="#cb14-83" aria-hidden="true" tabindex="-1"></a> dauer <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
<span id="cb14-84"><a href="#cb14-84" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-85"><a href="#cb14-85" aria-hidden="true" tabindex="-1"></a> loesung <span class="op">=</span> np.array(hochschule.getSolution().col_value)</span>
<span id="cb14-86"><a href="#cb14-86" aria-hidden="true" tabindex="-1"></a> info <span class="op">=</span> hochschule.getInfo()</span>
<span id="cb14-87"><a href="#cb14-87" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> {</span>
<span id="cb14-88"><a href="#cb14-88" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;x&quot;</span>: loesung[:anzahl_x].reshape(N_LAGER, N_KUNDE),</span>
<span id="cb14-89"><a href="#cb14-89" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;y&quot;</span>: loesung[anzahl_x:],</span>
<span id="cb14-90"><a href="#cb14-90" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;zielwert&quot;</span>: info.objective_function_value,</span>
<span id="cb14-91"><a href="#cb14-91" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;knoten&quot;</span>: info.mip_node_count,</span>
<span id="cb14-92"><a href="#cb14-92" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;dauer&quot;</span>: dauer,</span>
<span id="cb14-93"><a href="#cb14-93" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb14-94"><a href="#cb14-94" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-95"><a href="#cb14-95" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-96"><a href="#cb14-96" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> pruefe(ergebnis: <span class="bu">dict</span>) <span class="op">-&gt;</span> <span class="bu">tuple</span>[<span class="bu">bool</span>, <span class="bu">list</span>[<span class="bu">str</span>]]:</span>
<span id="cb14-97"><a href="#cb14-97" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Die Pruefung, die in jedes MILP-Auswertungsskript gehoert.</span></span>
<span id="cb14-98"><a href="#cb14-98" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-99"><a href="#cb14-99" aria-hidden="true" tabindex="-1"></a><span class="co"> Sie rechnet die Kosten AUS DER LOESUNG neu aus, statt dem Zielwert des</span></span>
<span id="cb14-100"><a href="#cb14-100" aria-hidden="true" tabindex="-1"></a><span class="co"> Solvers zu glauben - und vergleicht beide. Genau diese Gegenrechnung</span></span>
<span id="cb14-101"><a href="#cb14-101" aria-hidden="true" tabindex="-1"></a><span class="co"> entlarvt eine Loesung, in der Binaervariablen bei 1e-8 haengengeblieben</span></span>
<span id="cb14-102"><a href="#cb14-102" aria-hidden="true" tabindex="-1"></a><span class="co"> sind.</span></span>
<span id="cb14-103"><a href="#cb14-103" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb14-104"><a href="#cb14-104" aria-hidden="true" tabindex="-1"></a> beanstandungen <span class="op">=</span> []</span>
<span id="cb14-105"><a href="#cb14-105" aria-hidden="true" tabindex="-1"></a> x, y <span class="op">=</span> ergebnis[<span class="st">&quot;x&quot;</span>], ergebnis[<span class="st">&quot;y&quot;</span>]</span>
<span id="cb14-106"><a href="#cb14-106" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-107"><a href="#cb14-107" aria-hidden="true" tabindex="-1"></a> <span class="co"># 1. Sind die Binaervariablen wirklich binaer?</span></span>
<span id="cb14-108"><a href="#cb14-108" aria-hidden="true" tabindex="-1"></a> abstand <span class="op">=</span> np.<span class="bu">abs</span>(y <span class="op">-</span> np.<span class="bu">round</span>(y))</span>
<span id="cb14-109"><a href="#cb14-109" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> abstand.<span class="bu">max</span>() <span class="op">&gt;</span> GANZZAHL_TOLERANZ:</span>
<span id="cb14-110"><a href="#cb14-110" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(</span>
<span id="cb14-111"><a href="#cb14-111" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;y ist nicht ganzzahlig: groesster Abstand </span><span class="sc">{</span>abstand<span class="sc">.</span><span class="bu">max</span>()<span class="sc">:.2e}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-112"><a href="#cb14-112" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-113"><a href="#cb14-113" aria-hidden="true" tabindex="-1"></a> <span class="co"># 2. Liefert ein Lager, dessen Schalter aus ist?</span></span>
<span id="cb14-114"><a href="#cb14-114" aria-hidden="true" tabindex="-1"></a> liefert <span class="op">=</span> x.<span class="bu">sum</span>(axis<span class="op">=</span><span class="dv">1</span>) <span class="op">&gt;</span> <span class="fl">1e-6</span></span>
<span id="cb14-115"><a href="#cb14-115" aria-hidden="true" tabindex="-1"></a> geschlossen_aber_aktiv <span class="op">=</span> np.where(liefert <span class="op">&amp;</span> (y <span class="op">&lt;</span> <span class="fl">0.5</span>))[<span class="dv">0</span>]</span>
<span id="cb14-116"><a href="#cb14-116" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> geschlossen_aber_aktiv.size:</span>
<span id="cb14-117"><a href="#cb14-117" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(</span>
<span id="cb14-118"><a href="#cb14-118" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Lager </span><span class="sc">{</span>geschlossen_aber_aktiv<span class="sc">.</span>tolist()<span class="sc">}</span><span class="ss"> liefern Ware, &quot;</span></span>
<span id="cb14-119"><a href="#cb14-119" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;gelten im Modell aber als geschlossen&quot;</span>)</span>
<span id="cb14-120"><a href="#cb14-120" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-121"><a href="#cb14-121" aria-hidden="true" tabindex="-1"></a> <span class="co"># 3. Stimmt der Zielwert mit den echten Kosten ueberein?</span></span>
<span id="cb14-122"><a href="#cb14-122" aria-hidden="true" tabindex="-1"></a> echte_fixkosten <span class="op">=</span> FIXKOSTEN[liefert].<span class="bu">sum</span>()</span>
<span id="cb14-123"><a href="#cb14-123" aria-hidden="true" tabindex="-1"></a> echte_transportkosten <span class="op">=</span> <span class="bu">float</span>((TRANSPORT <span class="op">*</span> x).<span class="bu">sum</span>())</span>
<span id="cb14-124"><a href="#cb14-124" aria-hidden="true" tabindex="-1"></a> echte_kosten <span class="op">=</span> echte_fixkosten <span class="op">+</span> echte_transportkosten</span>
<span id="cb14-125"><a href="#cb14-125" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(echte_kosten <span class="op">-</span> ergebnis[<span class="st">&quot;zielwert&quot;</span>]) <span class="op">&gt;</span> <span class="fl">1e-4</span> <span class="op">*</span> <span class="bu">max</span>(<span class="fl">1.0</span>, echte_kosten):</span>
<span id="cb14-126"><a href="#cb14-126" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(</span>
<span id="cb14-127"><a href="#cb14-127" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Zielwert </span><span class="sc">{</span>ergebnis[<span class="st">&#39;zielwert&#39;</span>]<span class="sc">:,.2f}</span><span class="ss"> weicht von den echten Kosten &quot;</span></span>
<span id="cb14-128"><a href="#cb14-128" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>echte_kosten<span class="sc">:,.2f}</span><span class="ss"> ab (Differenz </span><span class="sc">{</span>echte_kosten <span class="op">-</span> ergebnis[<span class="st">&#39;zielwert&#39;</span>]<span class="sc">:,.2f}</span><span class="ss">)&quot;</span>)</span>
<span id="cb14-129"><a href="#cb14-129" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-130"><a href="#cb14-130" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="kw">not</span> beanstandungen, beanstandungen</span>
<span id="cb14-131"><a href="#cb14-131" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-132"><a href="#cb14-132" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-133"><a href="#cb14-133" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zeige(titel: <span class="bu">str</span>, ergebnis: <span class="bu">dict</span>) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb14-134"><a href="#cb14-134" aria-hidden="true" tabindex="-1"></a> x, y <span class="op">=</span> ergebnis[<span class="st">&quot;x&quot;</span>], ergebnis[<span class="st">&quot;y&quot;</span>]</span>
<span id="cb14-135"><a href="#cb14-135" aria-hidden="true" tabindex="-1"></a> liefert <span class="op">=</span> x.<span class="bu">sum</span>(axis<span class="op">=</span><span class="dv">1</span>) <span class="op">&gt;</span> <span class="fl">1e-6</span></span>
<span id="cb14-136"><a href="#cb14-136" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="sc">{</span>titel<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-137"><a href="#cb14-137" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Zielwert laut Solver </span><span class="sc">{</span>ergebnis[<span class="st">&#39;zielwert&#39;</span>]<span class="sc">:&gt;14,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb14-138"><a href="#cb14-138" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Knoten / Zeit </span><span class="sc">{</span>ergebnis[<span class="st">&#39;knoten&#39;</span>]<span class="sc">:&gt;14,}</span><span class="ss"> &quot;</span></span>
<span id="cb14-139"><a href="#cb14-139" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;/ </span><span class="sc">{</span>ergebnis[<span class="st">&#39;dauer&#39;</span>]<span class="sc">:.3f}</span><span class="ss"> s&quot;</span>)</span>
<span id="cb14-140"><a href="#cb14-140" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Lager mit y = 1 </span><span class="sc">{</span><span class="bu">int</span>((y <span class="op">&gt;</span> <span class="fl">0.5</span>).<span class="bu">sum</span>())<span class="sc">:&gt;14}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-141"><a href="#cb14-141" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Lager, die tatsaechlich liefern </span><span class="sc">{</span><span class="bu">int</span>(liefert.<span class="bu">sum</span>())<span class="sc">:&gt;10}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-142"><a href="#cb14-142" aria-hidden="true" tabindex="-1"></a> unter <span class="op">=</span> y[y <span class="op">&lt;</span> <span class="fl">0.5</span>]</span>
<span id="cb14-143"><a href="#cb14-143" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; groesster y-Wert unter 0.5 &quot;</span></span>
<span id="cb14-144"><a href="#cb14-144" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>(unter.<span class="bu">max</span>() <span class="cf">if</span> unter.size <span class="cf">else</span> <span class="fl">0.0</span>)<span class="sc">:&gt;14.3e}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-145"><a href="#cb14-145" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Fixkosten real / verbucht </span><span class="sc">{</span>FIXKOSTEN[liefert]<span class="sc">.</span><span class="bu">sum</span>()<span class="sc">:&gt;14,.2f}</span><span class="ss"> &quot;</span></span>
<span id="cb14-146"><a href="#cb14-146" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;/ </span><span class="sc">{</span><span class="bu">float</span>((FIXKOSTEN <span class="op">*</span> y).<span class="bu">sum</span>())<span class="sc">:,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb14-147"><a href="#cb14-147" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-148"><a href="#cb14-148" aria-hidden="true" tabindex="-1"></a> in_ordnung, beanstandungen <span class="op">=</span> pruefe(ergebnis)</span>
<span id="cb14-149"><a href="#cb14-149" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> in_ordnung:</span>
<span id="cb14-150"><a href="#cb14-150" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; PRUEFUNG: bestanden&quot;</span>)</span>
<span id="cb14-151"><a href="#cb14-151" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb14-152"><a href="#cb14-152" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; PRUEFUNG: DURCHGEFALLEN&quot;</span>)</span>
<span id="cb14-153"><a href="#cb14-153" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> text <span class="kw">in</span> beanstandungen:</span>
<span id="cb14-154"><a href="#cb14-154" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; - </span><span class="sc">{</span>text<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-155"><a href="#cb14-155" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-156"><a href="#cb14-156" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-157"><a href="#cb14-157" 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="cb14-158"><a href="#cb14-158" 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="cb14-159"><a href="#cb14-159" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; DIE BIG-M-FALLE: STANDORTPLANUNG, 12 LAGER, 40 KUNDEN&quot;</span>)</span>
<span id="cb14-160"><a href="#cb14-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">78</span>)</span>
<span id="cb14-161"><a href="#cb14-161" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Tatsaechliche Lagerkapazitaet: </span><span class="sc">{</span>KAPAZITAET<span class="sc">:,.1f}</span><span class="ss"> Einheiten.&quot;</span>)</span>
<span id="cb14-162"><a href="#cb14-162" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Genau das ist das kleinstmoegliche gueltige M - mehr kann ein Lager&quot;</span>)</span>
<span id="cb14-163"><a href="#cb14-163" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;ohnehin nicht ausliefern.&quot;</span>)</span>
<span id="cb14-164"><a href="#cb14-164" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-165"><a href="#cb14-165" aria-hidden="true" tabindex="-1"></a> knapp <span class="op">=</span> loese(KAPAZITAET)</span>
<span id="cb14-166"><a href="#cb14-166" aria-hidden="true" tabindex="-1"></a> zeige(<span class="st">&quot;[1] M = Kapazitaet (richtig gewaehlt)&quot;</span>, knapp)</span>
<span id="cb14-167"><a href="#cb14-167" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-168"><a href="#cb14-168" aria-hidden="true" tabindex="-1"></a> gross <span class="op">=</span> loese(<span class="fl">1e7</span> <span class="op">*</span> KAPAZITAET, presolve<span class="op">=</span><span class="st">&quot;off&quot;</span>)</span>
<span id="cb14-169"><a href="#cb14-169" aria-hidden="true" tabindex="-1"></a> zeige(<span class="st">&quot;[2] M = 10 Millionen mal Kapazitaet, Presolve abgeschaltet&quot;</span>, gross)</span>
<span id="cb14-170"><a href="#cb14-170" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-171"><a href="#cb14-171" 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="cb14-172"><a href="#cb14-172" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; WAS DA PASSIERT IST&quot;</span>)</span>
<span id="cb14-173"><a href="#cb14-173" 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="cb14-174"><a href="#cb14-174" aria-hidden="true" tabindex="-1"></a> fehlbetrag <span class="op">=</span> knapp[<span class="st">&quot;zielwert&quot;</span>] <span class="op">-</span> gross[<span class="st">&quot;zielwert&quot;</span>]</span>
<span id="cb14-175"><a href="#cb14-175" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Lauf [2] meldet </span><span class="sc">{</span>gross[<span class="st">&#39;zielwert&#39;</span>]<span class="sc">:,.2f}</span><span class="ss"> EUR und sieht damit um&quot;</span>)</span>
<span id="cb14-176"><a href="#cb14-176" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>fehlbetrag<span class="sc">:,.2f}</span><span class="ss"> EUR BESSER aus als die richtige Loesung - ein Ergebnis,&quot;</span>)</span>
<span id="cb14-177"><a href="#cb14-177" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;ueber das sich jeder Auftraggeber freuen wuerde.&quot;</span>)</span>
<span id="cb14-178"><a href="#cb14-178" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb14-179"><a href="#cb14-179" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Der Grund steht in der Zeile &#39;groesster y-Wert unter 0.5&#39;: Die&quot;</span>)</span>
<span id="cb14-180"><a href="#cb14-180" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Schaltervariablen stehen bei rund &quot;</span></span>
<span id="cb14-181"><a href="#cb14-181" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>gross[<span class="st">&#39;y&#39;</span>][gross[<span class="st">&#39;y&#39;</span>] <span class="op">&lt;</span> <span class="fl">0.5</span>]<span class="sc">.</span><span class="bu">max</span>()<span class="sc">:.0e}</span><span class="ss">.&quot;</span>)</span>
<span id="cb14-182"><a href="#cb14-182" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Das ist kleiner als die Ganzzahltoleranz </span><span class="sc">{</span>GANZZAHL_TOLERANZ<span class="sc">:.0e}</span><span class="ss">, also gilt&quot;</span>)</span>
<span id="cb14-183"><a href="#cb14-183" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;y = 0 - &#39;Lager geschlossen&#39;. Zugleich ist M so gross, dass&quot;</span>)</span>
<span id="cb14-184"><a href="#cb14-184" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; sum_j x_ij &lt;= M * 1e-8&quot;</span>)</span>
<span id="cb14-185"><a href="#cb14-185" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;immer noch reichlich Liefermenge erlaubt. Die Lager liefern also,&quot;</span>)</span>
<span id="cb14-186"><a href="#cb14-186" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;ohne dass ihre Fixkosten je bezahlt werden. Der Fachbegriff dafuer&quot;</span>)</span>
<span id="cb14-187"><a href="#cb14-187" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;ist &#39;trickle flow&#39;.&quot;</span>)</span>
<span id="cb14-188"><a href="#cb14-188" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb14-189"><a href="#cb14-189" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;WICHTIG: Mit eingeschaltetem Presolve (Standard) faellt HiGHS hier&quot;</span>)</span>
<span id="cb14-190"><a href="#cb14-190" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;nicht darauf herein - es zieht M selbst zurecht. Verlassen Sie sich&quot;</span>)</span>
<span id="cb14-191"><a href="#cb14-191" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;nicht darauf: Presolve kann das nur, wenn eine implizite Schranke&quot;</span>)</span>
<span id="cb14-192"><a href="#cb14-192" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;herleitbar ist. Die Pruefung aus pruefe() kostet Millisekunden und&quot;</span>)</span>
<span id="cb14-193"><a href="#cb14-193" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;funktioniert immer.&quot;</span>)</span>
<span id="cb14-194"><a href="#cb14-194" 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>==============================================================================
DIE BIG-M-FALLE: STANDORTPLANUNG, 12 LAGER, 40 KUNDEN
==============================================================================
Tatsaechliche Lagerkapazitaet: 722.4 Einheiten.
Genau das ist das kleinstmoegliche gueltige M - mehr kann ein Lager
ohnehin nicht ausliefern.
[1] M = Kapazitaet (richtig gewaehlt)
Zielwert laut Solver 26,525.28 EUR
Knoten / Zeit 13 / 0.075 s
Lager mit y = 1 3
Lager, die tatsaechlich liefern 3
groesster y-Wert unter 0.5 0.000e+00
Fixkosten real / verbucht 10,671.79 / 10,671.79 EUR
PRUEFUNG: bestanden
[2] M = 10 Millionen mal Kapazitaet, Presolve abgeschaltet
Zielwert laut Solver 12,441.96 EUR
Knoten / Zeit 1 / 0.006 s
Lager mit y = 1 0
Lager, die tatsaechlich liefern 11
groesster y-Wert unter 0.5 4.074e-08
Fixkosten real / verbucht 35,847.91 / 0.00 EUR
PRUEFUNG: DURCHGEFALLEN
- Lager [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11] liefern Ware, gelten im Modell aber als geschlossen
- Zielwert 12,441.96 weicht von den echten Kosten 48,289.86 ab (Differenz 35,847.91)
==============================================================================
WAS DA PASSIERT IST
==============================================================================
Lauf [2] meldet 12,441.96 EUR und sieht damit um
14,083.32 EUR BESSER aus als die richtige Loesung - ein Ergebnis,
ueber das sich jeder Auftraggeber freuen wuerde.
Der Grund steht in der Zeile &#39;groesster y-Wert unter 0.5&#39;: Die
Schaltervariablen stehen bei rund 4e-08.
Das ist kleiner als die Ganzzahltoleranz 1e-06, also gilt
y = 0 - &#39;Lager geschlossen&#39;. Zugleich ist M so gross, dass
sum_j x_ij &lt;= M * 1e-8
immer noch reichlich Liefermenge erlaubt. Die Lager liefern also,
ohne dass ihre Fixkosten je bezahlt werden. Der Fachbegriff dafuer
ist &#39;trickle flow&#39;.
WICHTIG: Mit eingeschaltetem Presolve (Standard) faellt HiGHS hier
nicht darauf herein - es zieht M selbst zurecht. Verlassen Sie sich
nicht darauf: Presolve kann das nur, wenn eine implizite Schranke
herleitbar ist. Die Pruefung aus pruefe() kostet Millisekunden und
funktioniert immer.
==============================================================================</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>presolve="off"</code></td>
<td>schaltet HiGHS Vorverarbeitung ab</td>
<td>Mit Standardeinstellung fällt HiGHS <strong>nicht</strong> herein: Presolve zieht das übergroße <span class="math inline">M</span> selbst zurecht. Der Fehler ist trotzdem real — Presolve kann das nur, wenn eine implizite Schranke im Modell steckt. Verlassen Sie sich nicht auf eine Rettung, die Sie nicht kontrollieren.</td>
</tr>
<tr class="even">
<td><code>np.abs(y - np.round(y))</code></td>
<td>prüft, ob Binärvariablen wirklich binär sind</td>
<td>Ein Wert von <span class="math inline">4 \cdot 10^{-8}</span> liegt unter der Toleranz <span class="math inline">10^{-6}</span> und gilt dem Solver als 0. Für Ihr Geschäft ist er es nicht.</td>
</tr>
<tr class="odd">
<td><code>liefert &amp; (y &lt; 0.5)</code></td>
<td>sucht Lager, die liefern, obwohl der Schalter aus ist</td>
<td>Die inhaltliche Prüfung: Widerspricht die Lösung sich selbst?</td>
</tr>
<tr class="even">
<td>Kosten <strong>aus der Lösung</strong> neu berechnen</td>
<td>statt dem Zielwert zu glauben</td>
<td>Der wichtigste Test überhaupt. Er vergleicht nicht Modell mit Modell, sondern Modell mit Wirklichkeit — und deckt jeden Fehler dieser Art auf, egal wodurch er entstanden ist.</td>
</tr>
</tbody>
</table>
</blockquote>
<hr />
<h2 id="sec:milp-quiz">6.11 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 6: 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-milp">Anhang A</a>.</p>
<p><strong>1. Ihr Solver meldet nach dem Zeitlimit: Incumbent 48 200 €, Schranke 47 100 €. Was berichten Sie dem Auftraggeber?</strong> (a) „Der Solver ist nicht fertig geworden, das Ergebnis ist unbrauchbar.“ (b) „Wir haben einen Plan für 48 200 €. Er ist garantiert höchstens 2,3 % teurer als das theoretisch bestmögliche.“ (c) „Der optimale Wert liegt bei 47 100 €, wir müssen den Plan nur noch dorthin verbessern.“</p>
<p><strong>2. In Ihrem Fixkostenmodell ist <span class="math inline">M = 10^9</span>, die tatsächliche Anlagenkapazität beträgt 500. Welche Gefahr ist die größere?</strong> (a) Der Solver braucht mehr Knoten und wird langsamer. (b) Binärvariablen können bei Werten um <span class="math inline">10^{-8}</span> hängenbleiben, gelten dem Solver als 0 — und Anlagen produzieren, ohne dass ihre Fixkosten je verbucht werden. (c) Die Zielfunktion wird numerisch instabil und liefert negative Kosten.</p>
<p><strong>3. Sie geben CP-SAT über <code>AddHint()</code> eine Startlösung mit, die eine Nebenbedingung verletzt. Was passiert?</strong> (a) Der Solver meldet <code>INFEASIBLE</code>, weil der Hinweis unzulässig ist. (b) Der Solver übernimmt den Hinweis und liefert eine unzulässige Lösung. (c) Der Hinweis wird verworfen; das Ergebnis bleibt korrekt, der erhoffte Zeitgewinn bleibt aber aus.</p>
</blockquote>
</div>
<hr />
<h2 id="sec:milp-selbsttest">6.12 Selbsttest</h2>
<blockquote>
<p>Antworten: <a href="anhang-loesungen.html#selbsttest-loesung-milp">Anhang A</a>.</p>
</blockquote>
<ol type="1">
<li>Warum liefert die LP-Relaxation bei Maximierung eine <strong>obere</strong> Schranke?</li>
<li>Nennen Sie die drei Gründe, aus denen ein Branch-and-Bound-Ast gekappt werden darf.</li>
<li>Wie modelliert man „<span class="math inline">x</span> ist entweder 0 oder mindestens 500“?</li>
<li>Warum verschlechtert ein zu großes <span class="math inline">M</span> die Laufzeit, obwohl das Modell korrekt bleibt?</li>
<li>Was bedeutet ein MIP-Gap von 3 %?</li>
</ol>
<hr />
<h2 id="sec:milp-zusammenfassung">6.13 Zusammenfassung</h2>
<ul>
<li><strong>Runden ist keine Lösung:</strong> Aufrunden ist fast immer unzulässig (98100 % der Fälle), Abrunden verliert im Mittel 1026 %, im Extremfall alles.</li>
<li><strong>Branch-and-Bound</strong> löst LP-Relaxationen, verzweigt an gebrochenen Variablen und kappt Äste anhand von Schranken. Der Preis: NP-Schwere.</li>
<li><strong>Fünf Modellierungsmuster</strong> decken die meisten Praxisfälle ab: Fixkosten, Entweder-Oder, Wenn-Dann, Kardinalität, semikontinuierlich.</li>
<li><strong><span class="math inline">M</span> so klein wie möglich</strong> — nicht nur wegen der Laufzeit. Ein übergroßes <span class="math inline">M</span> kann Binärvariablen bei <span class="math inline">10^{-8}</span> hängen lassen, wo sie dem Solver als 0 gelten: Anlagen produzieren, ohne dass ihre Fixkosten je verbucht werden (<em><a href="anhang-glossar.md#gloss:trickle-flow" class="glossar-link">Trickle Flow</a></em>).</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><strong><code>OPTIMAL</code> ist nicht der Normalfall.</strong> Werten Sie jeden Status aus und lesen Sie den <strong>MIP-Gap</strong> — er ist eine Garantie („höchstens 2,3 % vom Optimum“), keine Schätzung. Der Beweis der Optimalität kostet meist ein Vielfaches dessen, was das Finden der optimalen Lösung kostet.</li>
<li><strong>Warm-Starts können nichts verfälschen, aber auch nichts versprechen.</strong> Ob ein Startwert hilft, hängt davon ab, ob er besser ist als das, was der Solver in derselben Zeit von allein findet — im Buch gemessen: einmal Faktor 2,2, einmal gar nichts.</li>
<li><strong>Prüfen Sie jede modellierte Regel</strong> nach dem Lösen mit einer <code>assert</code>-Zeile — und rechnen Sie den Zielwert aus der Lösung nach, statt ihm zu glauben.</li>
</ul>
<p><strong>Ausblick.</strong> <a href="cpsat.html#kap-cpsat">Kapitel 7</a> wechselt das Paradigma: Bei Dienstplänen, Zuordnungen und Reihenfolgen ist die algebraische MILP-Sicht oft unhandlich. Constraint Programming denkt stattdessen in Wertebereichen und logischen Regeln — und löst solche Probleme um Größenordnungen schneller.</p>
</article>
<button type="button" class="fortschritt-knopf" data-kapitel="milp.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="lp.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></a><a class="prev-next-knopf prev-next-next" href="cpsat.html"><span><small>Weiter</small>Kapitel 7: Constraint Programming mit CP-SAT{idx:CP-SAT} — Logik, Scheduling und Zuweisung</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>