operations_research/OR_HTML_04/praxisfallen.html
dschlueter 5c49725d5c Notebook-Uebersichtsseite - das fehlende Gegenstueck zu programme.html
Der 403 auf /OR/Notebooks_04/ hatte eine andere Ursache als vermutet: Beim
Eintragen der Website-Adresse ins README hatte ich in der Lesen-Tabelle das
VERZEICHNIS verlinkt. Keine Seite der Website tut das - die Kapitelseiten
zeigen direkt auf Notebooks_04/<name>.ipynb, und das funktioniert. Der
Webserver verweigert Directory-Listing, zu Recht.

Die Frage "gibt es so etwas nicht schon?" war aber berechtigt und hat die
eigentliche Luecke sichtbar gemacht: baue_programme_seite() erzeugt seit
jeher programme.html, eine gestaltete Uebersicht der 76 Programme. Fuer die
25 Notebooks gab es kein Gegenstueck - sie wurden ausgeliefert, aber keine
Seite listete sie.

Neu ist baue_notebooks_seite(), nach demselben Muster und mit denselben
Bausteinen: die Zuordnung Seite -> Notebook kommt fertig aus
baue_notebooks(), Rahmen und Navigation aus baue_seiten_schablone() und
baue_sidebar_html(). Gruppiert nach Vorspann, Kapiteln und Anhaengen; der
einleitende Absatz nennt die drei Wege, ein Notebook auszufuehren.

Dazu ein ZIP mit allen 25 Notebooks (208 KB), byteidentisch ueber zwei
Laeufe. Ein ZIP speichert je Eintrag die Aenderungszeit; ohne festen Wert
entstuende bei jedem Bau eine andere Datei, und da OR_HTML_04/ versioniert
ist, wuechse das Repository bei jedem Lauf. Die Eintraege werden deshalb
sortiert und mit ZipInfo(date_time=(1980,1,1,0,0,0)) geschrieben - dieselbe
Sorgfalt wie svg.hashsalt bei den Diagrammen.

Gegen kuenftige 403 schreiben spiegle_notebooks() und baue_programme_seite()
je eine dreizeilige index.html mit meta refresh in ihr Downloadverzeichnis.
Das behebt zugleich denselben latenten Fall bei programme/.

Ein eigener Fehler, zum zweiten Mal derselbe: Das .replace(",", ".") fuer
deutsche Tausenderpunkte hing am Ende eines mehrzeiligen f-Strings - und in
Python bindet die Methode an die GESAMTE zusammengesetzte Zeichenkette. Aus
"Drei Wege, sie auszufuehren" wurde "Drei Wege. sie auszufuehren". Exakt der
Fehler, vor dem ich in erzeuge_wirkung.py selbst einen Kommentar hinterlassen
hatte. Die Zahl wird jetzt getrennt formatiert.

Geprueft: 25 gelistete Notebooks, 0 tote Links auf der Seite, ZIP mit 25
Eintraegen und identischen Zeitstempeln, Seitenleisteneintrag auf allen 40
Seiten mit Seitenleiste, --check fehlerfrei, PDF unveraendert 759 Seiten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:20:06 +02:00

2546 lines
360 KiB
HTML
Generated
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Kapitel 22: Praxisfallen und der Weg zum produktiven Einsatz · Optimierte Entscheidungsfindung mit Python</title>
<script>
(function () {
try {
var t = localStorage.getItem("or-theme");
if (t) document.documentElement.setAttribute("data-theme", t);
} catch (e) {}
})();
</script>
<link rel="stylesheet" href="assets/highlight.css" />
<link rel="stylesheet" href="katex/katex.min.css" />
<script defer="" src="katex/katex.min.js"></script>
<script>document.addEventListener("DOMContentLoaded", function () {
var mathElements = document.getElementsByClassName("math");
var macros = [];
for (var i = 0; i < mathElements.length; i++) {
var texText = mathElements[i].firstChild;
if (mathElements[i].tagName == "SPAN") {
katex.render(texText.data, mathElements[i], {
displayMode: mathElements[i].classList.contains('display'),
throwOnError: false,
macros: macros,
fleqn: false
});
}}
// Der Browser springt zu einem #anker in der URL schon beim ersten Rendern
// an, BEVOR die KaTeX-Formeln oben im Text ihre finale Hoehe bekommen -
// durch den Reflow landet der Anker danach zu weit unten. Nach dem
// Formel-Rendering hier erneut zum Anker springen, das behebt es.
if (location.hash) {
var ziel = document.getElementById(decodeURIComponent(location.hash.slice(1)));
if (ziel) ziel.scrollIntoView({behavior: "instant", block: "start"});
}
});
</script>
<link rel="stylesheet" href="assets/site.css" />
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="icon-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
</symbol>
<symbol id="icon-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.2" y2="16.2"/>
</symbol>
<symbol id="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4.5"/>
<line x1="12" y1="1.5" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="22.5"/>
<line x1="1.5" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="22.5" y2="12"/>
<line x1="4.5" y1="4.5" x2="6.2" y2="6.2"/><line x1="17.8" y1="17.8" x2="19.5" y2="19.5"/>
<line x1="19.5" y1="4.5" x2="17.8" y2="6.2"/><line x1="6.2" y1="17.8" x2="4.5" y2="19.5"/>
</symbol>
<symbol id="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a6.8 6.8 0 0 0 10.5 10.5z"/>
</symbol>
<symbol id="icon-chevron-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 4 7 12 15 20"/>
</symbol>
<symbol id="icon-chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 4 17 12 9 20"/>
</symbol>
<symbol id="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 13 9.5 18.5 20 6"/>
</symbol>
<symbol id="icon-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13.5V19a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 19V7A1.5 1.5 0 0 1 5 5.5h5.5"/>
<polyline points="14.5 3.5 20.5 3.5 20.5 9.5"/><line x1="11" y1="13" x2="20" y2="4"/>
</symbol>
<symbol id="icon-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 5.5A2 2 0 0 1 6 4h6v16H6a2 2 0 0 0-2 1.5z"/>
<path d="M20 5.5A2 2 0 0 0 18 4h-6v16h6a2 2 0 0 1 2 1.5z"/>
</symbol>
<symbol id="icon-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</symbol>
<symbol id="icon-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 3v12"/><polyline points="7 10 12 15 17 10"/><path d="M4 19.5h16"/>
</symbol></svg>
<header class="site-header">
<button type="button" class="icon-btn" id="sidebar-toggle" aria-label="Menü öffnen"><svg class="icon" aria-hidden="true"><use href="#icon-menu"></use></svg></button>
<a class="brand" href="index.html"><svg class="icon" aria-hidden="true"><use href="#icon-book"></use></svg> <span>Optimierte Entscheidungsfindung mit Python</span></a>
<div class="site-search">
<input id="suche-eingabe" type="search" placeholder="Suchen …" aria-label="Suche" autocomplete="off" />
<svg class="icon such-icon" aria-hidden="true"><use href="#icon-search"></use></svg>
<div id="suche-ergebnisse" class="suche-ergebnisse" hidden></div>
</div>
<button type="button" class="icon-btn" id="theme-toggle" aria-label="Darstellung umschalten">
<svg class="icon icon-sun" aria-hidden="true"><use href="#icon-sun"></use></svg><svg class="icon icon-moon" aria-hidden="true"><use href="#icon-moon"></use></svg>
</button>
</header>
<div class="site-body">
<div class="sidebar-overlay" id="sidebar-overlay" hidden></div>
<nav class="sidebar" id="sidebar" aria-label="Kapitelnavigation"><div class="sidebar-inhalt"><details class="sidebar-gruppe"><summary>Einstieg</summary><ul><li data-kapitel="vorwort.html"><a href="vorwort.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Vorwort & Lesehilfe</span></a></li><li data-kapitel="notation.html"><a href="notation.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Notation & Abkürzungen</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Teil I: Grundlagen des Operations Research</summary><ul><li data-kapitel="einfuehrung.html"><a href="einfuehrung.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></a></li><li data-kapitel="fundament.html"><a href="fundament.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</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"><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"><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</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 — 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"><summary>Teil III: Nichtlinearität, Unsicherheit und mehrperiodige Dynamik</summary><ul><li data-kapitel="qp-nlp.html"><a href="qp-nlp.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 11: Quadratische und nichtlineare Optimierung — KKT, Lagrange, Konvexität</span></a></li><li data-kapitel="unsicherheit.html"><a href="unsicherheit.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 12: Optimierung unter Unsicherheit — Monte-Carlo, Stochastik, Robustheit</span></a></li><li data-kapitel="dynamische-programmierung.html"><a href="dynamische-programmierung.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 13: Dynamische Programmierung — Die Bellman-Gleichung und Order-Execution</span></a></li><li data-kapitel="mehrziel.html"><a href="mehrziel.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 14: Mehrere Ziele — Pareto-Fronten statt Gewichte</span></a></li><li data-kapitel="prognose.html"><a href="prognose.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 15: Predict-then-Optimize — die bessere Prognose, die schlechtere Entscheidung</span></a></li><li data-kapitel="synthese-nichtlinear.html"><a href="synthese-nichtlinear.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Synthese Teil III</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Teil IV: Anwendungen — Energiewirtschaft und Finanzmärkte</summary><ul><li data-kapitel="bruecke.html"><a href="bruecke.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 16: Die Strukturbrücke — dieselbe Mathematik, zwei Welten</span></a></li><li data-kapitel="supplychain.html"><a href="supplychain.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 17: Supply-Chain und Energieeinsatz unter Unsicherheit</span></a></li><li data-kapitel="finanzdaten.html"><a href="finanzdaten.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 18: Finanzdaten-Modellierung — Renditen, Kovarianz und Shrinkage</span></a></li><li data-kapitel="markowitz.html"><a href="markowitz.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 19: Die moderne Portfoliotheorie nach Markowitz</span></a></li><li data-kapitel="cvar.html"><a href="cvar.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 20: Tail-Risiko, CVaR und Transaktionskosten</span></a></li><li data-kapitel="handelsmaschine.html"><a href="handelsmaschine.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 21: Die vollständige quantitative Handelsmaschine</span></a></li><li data-kapitel="synthese-anwendungen.html"><a href="synthese-anwendungen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Synthese Teil IV</span></a></li></ul></details><details class="sidebar-gruppe" open><summary>Teil V: Praxis</summary><ul><li data-kapitel="praxisfallen.html" class="aktiv"><a href="praxisfallen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 22: Praxisfallen und der Weg zum produktiven Einsatz</span></a></li><li data-kapitel="testing.html"><a href="testing.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 23: Testen, Messen, Ausliefern</span></a></li><li data-kapitel="synthese-praxis.html"><a href="synthese-praxis.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Synthese Teil V</span></a></li><li data-kapitel="projektwerkstatt.html"><a href="projektwerkstatt.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Projektwerkstatt</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Anhänge</summary><ul><li data-kapitel="anhang-loesungen.html"><a href="anhang-loesungen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Anhang A: Lösungen zu allen Übungsaufgaben</span></a></li><li data-kapitel="anhang-modellierungsmuster.html"><a href="anhang-modellierungsmuster.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Anhang B: Katalog der Modellierungsmuster</span></a></li><li data-kapitel="anhang-fehlerdiagnose.html"><a href="anhang-fehlerdiagnose.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Anhang C: Fehlerdiagnose-Handbuch</span></a></li><li data-kapitel="anhang-spickzettel.html"><a href="anhang-spickzettel.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Anhang D: Spickzettel der Solver</span></a></li><li data-kapitel="anhang-glossar-literatur.html"><a href="anhang-glossar-literatur.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Anhang E: Glossar und Literatur</span></a></li></ul></details><ul class="sidebar-extra"><li><a href="programme.html"><svg class="icon" aria-hidden="true"><use href="#icon-book"></use></svg> Beispielprogramme</a></li><li><a href="notebooks.html"><svg class="icon" aria-hidden="true"><use href="#icon-book"></use></svg> Notebooks</a></li><li><a href="stichwortverzeichnis.html"><svg class="icon" aria-hidden="true"><use href="#icon-search"></use></svg> Stichwortverzeichnis</a></li><li><a href="gesamtdokument.html"><svg class="icon" aria-hidden="true"><use href="#icon-external-link"></use></svg> Gesamtdokument (eine Seite)</a></li><li><a href="Operations_Research_mit_Python_Version_04.pdf" download><svg class="icon" aria-hidden="true"><use href="#icon-download"></use></svg> Download als PDF</a></li></ul></div></nav>
<main class="content">
<nav class="breadcrumb" aria-label="Breadcrumb"><a href="index.html">Start</a> <span>Teil V</span> <span>Kapitel 22: Praxisfallen und der Weg zum produktiven Einsatz</span></nav>
<nav class="prev-next"><a class="prev-next-knopf prev-next-prev" href="synthese-anwendungen.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Synthese Teil IV</span></a><a class="prev-next-knopf prev-next-next" href="testing.html"><span><small>Weiter</small>Kapitel 23: Testen, Messen, Ausliefern</span><svg class="icon" aria-hidden="true"><use href="#icon-chevron-right"></use></svg></a></nav>
<article>
<h1 id="teil-praxis">Teil V: Praxis</h1>
<hr />
<h1 id="kap-praxisfallen">Kapitel 22: Praxisfallen und der Weg zum produktiven Einsatz</h1>
<div class="card card-blick">
<blockquote>
<p><strong>📌 Kapitel auf einen Blick</strong></p>
<p><strong>Worum geht es?</strong> Um alles, was zwischen einem funktionierenden Notebook und einem System steht, das Menschen tatsächlich benutzen.</p>
<p><strong>Voraussetzungen:</strong> Die vorangegangenen Kapitel — dieses Kapitel zieht die Summe.</p>
<p><strong>Danach können Sie:</strong> Unlösbarkeit systematisch diagnostizieren und auffangen, Ergebnisse erklärbar machen, Laufzeiten begrenzen und ein OR-System betriebsfähig aufsetzen.</p>
<p><strong>Zeitbedarf:</strong> ca. 4 Stunden.</p>
<p><strong>Programme:</strong><br />
<code>Infeasibility_Diagnose.py</code><br />
<code>Erklaerbarkeit.py</code><br />
<code>Constraint_Attribution.py</code><br />
<code>Betriebsueberwachung.py</code><br />
<code>or_kern.py</code><br />
<code>Solverwechsel_CPSAT_HiGHS.py</code></p>
<p><strong>Notebook:</strong> <a href="Notebooks_04/praxisfallen.ipynb">praxisfallen.ipynb</a> — herunterladen und in Jupyter öffnen, in Colab hochladen oder mit dem Kurs-Image starten</p>
</blockquote>
</div>
<hr />
<h2 id="sec:praxisfallen-schnellstart">22.1 In 5 Minuten gelöst</h2>
<p>Die vorangegangenen vierzehn Kapitel enden fast alle mit derselben Empfehlung: <em>Prüfen Sie das Ergebnis gegen die Wirklichkeit.</em> Dieses Kapitel beginnt damit, diese Prüfung als wiederverwendbaren Baustein hinzuschreiben.</p>
<div class="card card-schnellstart">
<blockquote>
<p><strong>🚀 In 5 Minuten gelöst: Die Abnahmeprüfung, die in jedes Modell gehört</strong></p>
<div class="sourceCode" id="cb1"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> pruefe_loesung(x, A, b, kosten, zielwert, ganzzahlig<span class="op">=</span>(), toleranz<span class="op">=</span><span class="fl">1e-6</span>):</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Prueft eine Loesung gegen die Anforderungen - OHNE den Solver zu fragen.&quot;&quot;&quot;</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a> beanstandungen <span class="op">=</span> []</span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a> verbrauch <span class="op">=</span> A <span class="op">@</span> x <span class="co"># 1. Werden Grenzen eingehalten?</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i, (ist, grenze) <span class="kw">in</span> <span class="bu">enumerate</span>(<span class="bu">zip</span>(verbrauch, b)):</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> ist <span class="op">&gt;</span> grenze <span class="op">+</span> toleranz:</span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;Bedingung </span><span class="sc">{</span>i<span class="sc">}</span><span class="ss">: </span><span class="sc">{</span>ist<span class="sc">:.4f}</span><span class="ss"> &gt; </span><span class="sc">{</span>grenze<span class="sc">:.4f}</span><span class="ss">&quot;</span>)</span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> ganzzahlig: <span class="co"># 2. Ist ganzzahlig auch ganzzahlig?</span></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(x[j] <span class="op">-</span> <span class="bu">round</span>(x[j])) <span class="op">&gt;</span> toleranz:</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;x[</span><span class="sc">{</span>j<span class="sc">}</span><span class="ss">] = </span><span class="sc">{</span>x[j]<span class="sc">!r}</span><span class="ss"> ist nicht ganzzahlig&quot;</span>)</span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a> nachgerechnet <span class="op">=</span> <span class="bu">float</span>(kosten <span class="op">@</span> x) <span class="co"># 3. Stimmt der Zielwert?</span></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(nachgerechnet <span class="op">-</span> zielwert) <span class="op">&gt;</span> toleranz <span class="op">*</span> <span class="bu">max</span>(<span class="fl">1.0</span>, <span class="bu">abs</span>(nachgerechnet)):</span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(</span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Zielwert </span><span class="sc">{</span>zielwert<span class="sc">:.4f}</span><span class="ss"> passt nicht zu den Daten (</span><span class="sc">{</span>nachgerechnet<span class="sc">:.4f}</span><span class="ss">)&quot;</span>)</span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> beanstandungen</span></code></pre></div>
</blockquote>
</div>
<p><strong>Zwölf Zeilen, drei Prüfungen — und jede davon hätte einen der Denkfehler dieses Buches gefunden:</strong></p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Prüfung</th>
<th>fängt</th>
<th>aus</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Grenzen einhalten</td>
<td>die pro Produkt statt insgesamt formulierte Kapazität</td>
<td><a href="einfuehrung.html#sec:einfuehrung-denkfehler">Abschnitt 1.10</a></td>
</tr>
<tr class="even">
<td>Grenzen einhalten</td>
<td>die vergessene Ladungsdimension im Routing</td>
<td><a href="graphen.html#sec:graphen-denkfehler">Abschnitt 8.7</a></td>
</tr>
<tr class="odd">
<td>Ganzzahligkeit</td>
<td>Binärvariablen, die bei <span class="math inline">10^{-8}</span> hängen (Big-M)</td>
<td><a href="milp.html#sec:milp-denkfehler">Abschnitt 6.10</a></td>
</tr>
<tr class="even">
<td>Zielwert nachrechnen</td>
<td>Fixkosten, die im Modell nicht verbucht wurden</td>
<td><a href="milp.html#sec:milp-denkfehler">Abschnitt 6.10</a></td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>🎯 Merksatz</strong> Entscheidend ist, was diese Funktion <strong>nicht</strong> benutzt: kein Solver-Objekt, keine Modellvariable, keine Dimension aus der Bibliothek. Sie bekommt nur die ausgegebene Lösung und die Anforderungen — und rechnet unabhängig nach. Eine Prüfung, die dieselben Bausteine verwendet wie das Modell, prüft das Modell gegen sich selbst und findet nichts.</p>
</blockquote>
<p><strong>Und was sie nicht kann.</strong> Sie prüft, ob die gefundene Lösung <strong>zulässig</strong> ist. Sie kann nicht prüfen, ob es eine <strong>bessere</strong> gäbe — genau daran ist die Prüfung in <a href="dynamische-programmierung.html#sec:dynamische-programmierung-denkfehler">Abschnitt 13.7</a> gescheitert, wo das Modell einen korrekten Kostenbetrag für einen unnötig teuren Plan meldete. Dafür braucht es ein zweites, unabhängiges Verfahren oder eine bekannte Schranke. Die restlichen Abschnitte dieses Kapitels handeln davon, was im Betrieb sonst noch schiefgeht — und wie man es bemerkt.</p>
<hr />
<h2 id="sec:praxisfallen-lernziele">22.2 Lernziele</h2>
<p>Nach diesem Kapitel können Sie …</p>
<ol type="1">
<li>… die fünf typischen Praxisfallen benennen und je ein Gegenmittel nennen.</li>
<li>… ein unlösbares Modell in einen „am wenigsten schlechten“ Plan verwandeln.</li>
<li>… Solver-Ergebnisse erklärbar machen (<em>Explainable OR</em>).</li>
<li>… für jede Nebenbedingung beziffern, was sie den Plan kostet — und begründen, warum der Schattenpreis diese Frage nur für die <em>nächste</em> Einheit beantwortet.</li>
<li>… Zeitlimits und Optimalitätslücken sinnvoll setzen.</li>
<li>… die Architektur einer produktionsreifen OR-Plattform skizzieren.</li>
<li>… eine <strong>modellunabhängige Abnahmeprüfung</strong> schreiben, die eine Lösung gegen die Anforderungen prüft statt gegen das Modell.</li>
<li>… Status, Gap und Zeitausschöpfung protokollieren und begründen, warum eine konstante Laufzeit bei Zeitlimit ein Warnsignal ist.</li>
<li>… ein Modell in Domänenschicht, Modellbauer und Lösungs-DTO trennen — und begründen, warum sich das nicht wegen des Solverwechsels lohnt, sondern wegen der Prüfbarkeit.</li>
<li>… die Statuswerte verschiedener Solverbibliotheken auf eine gemeinsame Sprache abbilden.</li>
</ol>
<hr />
<h2 id="sec:praxisfallen-die-fuenf-typischen-praxisfallen">22.3 Die fünf typischen Praxisfallen</h2>
<figure>
<img src="bilder_04/kap15_praxisfallen.svg" alt="Abb. 22.1: Die fünf typischen Praxisfallen" />
<figcaption aria-hidden="true">Abb. 22.1: Die fünf typischen Praxisfallen</figcaption>
</figure>
<h3 id="falle-1-infeasibility">Falle 1 — Infeasibility</h3>
<p>Wenn Parameter unglücklich zusammentreffen — drei Mitarbeitende gleichzeitig krank, kein qualifizierter Ersatz —, meldet der Solver schlicht <code>INFEASIBLE</code>.</p>
<ul>
<li><strong>Falsch:</strong> Das System stürzt ab oder zeigt „Fehler“. Der Disponent steht ohne Plan da.</li>
<li><strong>Richtig:</strong> <strong>Hierarchische Relaxation.</strong> Jede harte Bedingung, die im Notfall gebrochen werden darf, erhält eine teure Schlupfvariable. So liefert der Solver immer einen Plan — und zeigt zugleich präzise, <strong>wo</strong> es klemmt.</li>
</ul>
<div class="sourceCode" id="cb2"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Infeasibility_Diagnose.py</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel Praxisfallen: Aus INFEASIBLE einen Notfallplan machen.</span></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a><span class="co">Demonstriert die hierarchische Relaxation an einem Dienstplan, der in der</span></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a><span class="co">harten Fassung unloesbar ist, und zeigt, wie der Solver die Ursache benennt.</span></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> ortools.sat.python <span class="im">import</span> cp_model</span>
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a>SLOTS <span class="op">=</span> [<span class="st">&quot;Mo frueh&quot;</span>, <span class="st">&quot;Mo spaet&quot;</span>, <span class="st">&quot;Di frueh&quot;</span>, <span class="st">&quot;Di spaet&quot;</span>]</span>
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a>FAECHER <span class="op">=</span> [<span class="st">&quot;Mathematik&quot;</span>, <span class="st">&quot;Physik&quot;</span>, <span class="st">&quot;Mathematik&quot;</span>, <span class="st">&quot;Chemie&quot;</span>] <span class="co"># Chemie kann niemand!</span></span>
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a>PERSONAL <span class="op">=</span> [<span class="st">&quot;Alice&quot;</span>, <span class="st">&quot;Bob&quot;</span>, <span class="st">&quot;Carla&quot;</span>]</span>
<span id="cb2-16"><a href="#cb2-16" aria-hidden="true" tabindex="-1"></a>QUALIFIKATION <span class="op">=</span> {</span>
<span id="cb2-17"><a href="#cb2-17" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Alice&quot;</span>: {<span class="st">&quot;Mathematik&quot;</span>, <span class="st">&quot;Physik&quot;</span>},</span>
<span id="cb2-18"><a href="#cb2-18" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Bob&quot;</span>: {<span class="st">&quot;Mathematik&quot;</span>},</span>
<span id="cb2-19"><a href="#cb2-19" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Carla&quot;</span>: {<span class="st">&quot;Physik&quot;</span>, <span class="st">&quot;Mathematik&quot;</span>},</span>
<span id="cb2-20"><a href="#cb2-20" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb2-21"><a href="#cb2-21" aria-hidden="true" tabindex="-1"></a>MAX_PRO_PERSON <span class="op">=</span> <span class="dv">2</span></span>
<span id="cb2-22"><a href="#cb2-22" aria-hidden="true" tabindex="-1"></a>STRAFE_UNBESETZT <span class="op">=</span> <span class="dv">10_000</span> <span class="co"># sehr teuer, aber nicht unmoeglich</span></span>
<span id="cb2-23"><a href="#cb2-23" aria-hidden="true" tabindex="-1"></a>STRAFE_UEBERLAST <span class="op">=</span> <span class="dv">500</span> <span class="co"># teuer, aber billiger als Ausfall</span></span>
<span id="cb2-24"><a href="#cb2-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-25"><a href="#cb2-25" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-26"><a href="#cb2-26" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> plane(harte_fassung: <span class="bu">bool</span>):</span>
<span id="cb2-27"><a href="#cb2-27" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;harte_fassung=True: klassisch (kann INFEASIBLE werden).</span></span>
<span id="cb2-28"><a href="#cb2-28" aria-hidden="true" tabindex="-1"></a><span class="co"> harte_fassung=False: mit Schlupfvariablen (immer loesbar).&quot;&quot;&quot;</span></span>
<span id="cb2-29"><a href="#cb2-29" aria-hidden="true" tabindex="-1"></a> modell <span class="op">=</span> cp_model.CpModel()</span>
<span id="cb2-30"><a href="#cb2-30" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> {(p, s): modell.NewBoolVar(<span class="ss">f&quot;x_</span><span class="sc">{</span>p<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>s<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb2-31"><a href="#cb2-31" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> PERSONAL <span class="cf">for</span> s <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(SLOTS))}</span>
<span id="cb2-32"><a href="#cb2-32" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-33"><a href="#cb2-33" aria-hidden="true" tabindex="-1"></a> strafen <span class="op">=</span> []</span>
<span id="cb2-34"><a href="#cb2-34" aria-hidden="true" tabindex="-1"></a> unbesetzt <span class="op">=</span> {}</span>
<span id="cb2-35"><a href="#cb2-35" aria-hidden="true" tabindex="-1"></a> ueberlast <span class="op">=</span> {}</span>
<span id="cb2-36"><a href="#cb2-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-37"><a href="#cb2-37" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> s <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(SLOTS)):</span>
<span id="cb2-38"><a href="#cb2-38" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> harte_fassung:</span>
<span id="cb2-39"><a href="#cb2-39" aria-hidden="true" tabindex="-1"></a> modell.AddExactlyOne(x[p, s] <span class="cf">for</span> p <span class="kw">in</span> PERSONAL)</span>
<span id="cb2-40"><a href="#cb2-40" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb2-41"><a href="#cb2-41" aria-hidden="true" tabindex="-1"></a> <span class="co"># Schlupfvariable: der Slot DARF unbesetzt bleiben - gegen hohe Strafe</span></span>
<span id="cb2-42"><a href="#cb2-42" aria-hidden="true" tabindex="-1"></a> unbesetzt[s] <span class="op">=</span> modell.NewBoolVar(<span class="ss">f&quot;unbesetzt_</span><span class="sc">{</span>s<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb2-43"><a href="#cb2-43" aria-hidden="true" tabindex="-1"></a> modell.AddExactlyOne([x[p, s] <span class="cf">for</span> p <span class="kw">in</span> PERSONAL] <span class="op">+</span> [unbesetzt[s]])</span>
<span id="cb2-44"><a href="#cb2-44" aria-hidden="true" tabindex="-1"></a> strafen.append(unbesetzt[s] <span class="op">*</span> STRAFE_UNBESETZT)</span>
<span id="cb2-45"><a href="#cb2-45" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-46"><a href="#cb2-46" aria-hidden="true" tabindex="-1"></a> <span class="co"># Qualifikation bleibt IMMER hart - fachfremder Unterricht ist keine Option</span></span>
<span id="cb2-47"><a href="#cb2-47" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> s, fach <span class="kw">in</span> <span class="bu">enumerate</span>(FAECHER):</span>
<span id="cb2-48"><a href="#cb2-48" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> PERSONAL:</span>
<span id="cb2-49"><a href="#cb2-49" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> fach <span class="kw">not</span> <span class="kw">in</span> QUALIFIKATION[p]:</span>
<span id="cb2-50"><a href="#cb2-50" aria-hidden="true" tabindex="-1"></a> modell.Add(x[p, s] <span class="op">==</span> <span class="dv">0</span>)</span>
<span id="cb2-51"><a href="#cb2-51" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-52"><a href="#cb2-52" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> PERSONAL:</span>
<span id="cb2-53"><a href="#cb2-53" aria-hidden="true" tabindex="-1"></a> last <span class="op">=</span> <span class="bu">sum</span>(x[p, s] <span class="cf">for</span> s <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(SLOTS)))</span>
<span id="cb2-54"><a href="#cb2-54" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> harte_fassung:</span>
<span id="cb2-55"><a href="#cb2-55" aria-hidden="true" tabindex="-1"></a> modell.Add(last <span class="op">&lt;=</span> MAX_PRO_PERSON)</span>
<span id="cb2-56"><a href="#cb2-56" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb2-57"><a href="#cb2-57" aria-hidden="true" tabindex="-1"></a> <span class="co"># Ueberlast erlaubt - aber teuer</span></span>
<span id="cb2-58"><a href="#cb2-58" aria-hidden="true" tabindex="-1"></a> ueberlast[p] <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, <span class="bu">len</span>(SLOTS), <span class="ss">f&quot;ueberlast_</span><span class="sc">{</span>p<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb2-59"><a href="#cb2-59" aria-hidden="true" tabindex="-1"></a> modell.Add(last <span class="op">&lt;=</span> MAX_PRO_PERSON <span class="op">+</span> ueberlast[p])</span>
<span id="cb2-60"><a href="#cb2-60" aria-hidden="true" tabindex="-1"></a> strafen.append(ueberlast[p] <span class="op">*</span> STRAFE_UEBERLAST)</span>
<span id="cb2-61"><a href="#cb2-61" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-62"><a href="#cb2-62" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> strafen:</span>
<span id="cb2-63"><a href="#cb2-63" aria-hidden="true" tabindex="-1"></a> modell.Minimize(<span class="bu">sum</span>(strafen))</span>
<span id="cb2-64"><a href="#cb2-64" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-65"><a href="#cb2-65" aria-hidden="true" tabindex="-1"></a> loeser <span class="op">=</span> cp_model.CpSolver()</span>
<span id="cb2-66"><a href="#cb2-66" aria-hidden="true" tabindex="-1"></a> loeser.parameters.max_time_in_seconds <span class="op">=</span> <span class="fl">5.0</span></span>
<span id="cb2-67"><a href="#cb2-67" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> loeser.Solve(modell)</span>
<span id="cb2-68"><a href="#cb2-68" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> loeser, status, x, unbesetzt, ueberlast</span>
<span id="cb2-69"><a href="#cb2-69" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-70"><a href="#cb2-70" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-71"><a href="#cb2-71" 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="cb2-72"><a href="#cb2-72" 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="cb2-73"><a href="#cb2-73" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; VARIANTE A: KLASSISCH MIT LAUTER HARTEN BEDINGUNGEN&quot;</span>)</span>
<span id="cb2-74"><a href="#cb2-74" 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="cb2-75"><a href="#cb2-75" aria-hidden="true" tabindex="-1"></a> loeser, status, <span class="op">*</span>_ <span class="op">=</span> plane(harte_fassung<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb2-76"><a href="#cb2-76" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Solver-Status: </span><span class="sc">{</span>loeser<span class="sc">.</span>StatusName(status)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb2-77"><a href="#cb2-77" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> status <span class="op">==</span> cp_model.INFEASIBLE:</span>
<span id="cb2-78"><a href="#cb2-78" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Das System kann keinen Plan liefern. Der Anwender erfaehrt NICHT,&quot;</span>)</span>
<span id="cb2-79"><a href="#cb2-79" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;welche Regel das Problem verursacht - nur, dass es nicht geht.</span><span class="ch">\n</span><span class="st">&quot;</span>)</span>
<span id="cb2-80"><a href="#cb2-80" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-81"><a href="#cb2-81" 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="cb2-82"><a href="#cb2-82" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; VARIANTE B: MIT HIERARCHISCHER RELAXATION&quot;</span>)</span>
<span id="cb2-83"><a href="#cb2-83" 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="cb2-84"><a href="#cb2-84" aria-hidden="true" tabindex="-1"></a> loeser, status, x, unbesetzt, ueberlast <span class="op">=</span> plane(harte_fassung<span class="op">=</span><span class="va">False</span>)</span>
<span id="cb2-85"><a href="#cb2-85" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Solver-Status: </span><span class="sc">{</span>loeser<span class="sc">.</span>StatusName(status)<span class="sc">}</span><span class="ss"> | &quot;</span></span>
<span id="cb2-86"><a href="#cb2-86" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Strafkosten: </span><span class="sc">{</span>loeser<span class="sc">.</span>ObjectiveValue()<span class="sc">:.0f}</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb2-87"><a href="#cb2-87" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-88"><a href="#cb2-88" 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;Slot&#39;</span><span class="sc">:&lt;12}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Fach&#39;</span><span class="sc">:&lt;12}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Zuweisung&#39;</span><span class="sc">:&lt;18}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Bemerkung&#39;</span><span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb2-89"><a href="#cb2-89" 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="cb2-90"><a href="#cb2-90" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> s, slot <span class="kw">in</span> <span class="bu">enumerate</span>(SLOTS):</span>
<span id="cb2-91"><a href="#cb2-91" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> loeser.Value(unbesetzt[s]):</span>
<span id="cb2-92"><a href="#cb2-92" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>slot<span class="sc">:&lt;12}</span><span class="ss"> </span><span class="sc">{</span>FAECHER[s]<span class="sc">:&lt;12}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;-- UNBESETZT --&#39;</span><span class="sc">:&lt;18}</span><span class="ss"> &quot;</span></span>
<span id="cb2-93"><a href="#cb2-93" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Kein qualifiziertes Personal verfuegbar&quot;</span>)</span>
<span id="cb2-94"><a href="#cb2-94" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb2-95"><a href="#cb2-95" aria-hidden="true" tabindex="-1"></a> person <span class="op">=</span> <span class="bu">next</span>(p <span class="cf">for</span> p <span class="kw">in</span> PERSONAL <span class="cf">if</span> loeser.Value(x[p, s]))</span>
<span id="cb2-96"><a href="#cb2-96" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>slot<span class="sc">:&lt;12}</span><span class="ss"> </span><span class="sc">{</span>FAECHER[s]<span class="sc">:&lt;12}</span><span class="ss"> </span><span class="sc">{</span>person<span class="sc">:&lt;18}</span><span class="ss">&quot;</span>)</span>
<span id="cb2-97"><a href="#cb2-97" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-98"><a href="#cb2-98" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">--- Diagnose ---&quot;</span>)</span>
<span id="cb2-99"><a href="#cb2-99" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> s, slot <span class="kw">in</span> <span class="bu">enumerate</span>(SLOTS):</span>
<span id="cb2-100"><a href="#cb2-100" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> loeser.Value(unbesetzt[s]):</span>
<span id="cb2-101"><a href="#cb2-101" aria-hidden="true" tabindex="-1"></a> fach <span class="op">=</span> FAECHER[s]</span>
<span id="cb2-102"><a href="#cb2-102" aria-hidden="true" tabindex="-1"></a> qualifiziert <span class="op">=</span> [p <span class="cf">for</span> p <span class="kw">in</span> PERSONAL <span class="cf">if</span> fach <span class="kw">in</span> QUALIFIKATION[p]]</span>
<span id="cb2-103"><a href="#cb2-103" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; &#39;</span><span class="sc">{</span>slot<span class="sc">}</span><span class="ss">&#39; (</span><span class="sc">{</span>fach<span class="sc">}</span><span class="ss">): </span><span class="sc">{</span><span class="bu">len</span>(qualifiziert)<span class="sc">}</span><span class="ss"> qualifizierte Personen &quot;</span></span>
<span id="cb2-104"><a href="#cb2-104" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>qualifiziert <span class="cf">if</span> qualifiziert <span class="cf">else</span> <span class="st">&#39;-&gt; URSACHE: niemand kann dieses Fach&#39;</span><span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb2-105"><a href="#cb2-105" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> PERSONAL:</span>
<span id="cb2-106"><a href="#cb2-106" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> loeser.Value(ueberlast[p]):</span>
<span id="cb2-107"><a href="#cb2-107" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>p<span class="sc">}</span><span class="ss"> arbeitet </span><span class="sc">{</span>loeser<span class="sc">.</span>Value(ueberlast[p])<span class="sc">}</span><span class="ss"> Stunden ueber der Grenze.&quot;</span>)</span>
<span id="cb2-108"><a href="#cb2-108" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-109"><a href="#cb2-109" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">Der Anwender bekommt jetzt einen Plan PLUS eine konkrete Ursache -&quot;</span>)</span>
<span id="cb2-110"><a href="#cb2-110" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;und kann handeln: Vertretung von aussen holen, Stunde verlegen,&quot;</span>)</span>
<span id="cb2-111"><a href="#cb2-111" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;oder die Klasse zusammenlegen.&quot;</span>)</span>
<span id="cb2-112"><a href="#cb2-112" 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>
<h3 id="falle-2-der-black-box-effekt">Falle 2 — Der Black-Box-Effekt</h3>
<p>Ein Planer oder Trader lehnt ein System ab, wenn er die Zuweisungen nicht nachvollziehen kann. <strong>Akzeptanz ist keine Nebensache, sondern Voraussetzung für den Betrieb.</strong></p>
<p>Jede Entscheidung braucht einen <strong>Constraint-Trace</strong>:</p>
<blockquote>
<p><em>„Warum bekommt Person A die Stunde und nicht Person B?“</em><em>„Weil B heute bereits zwei Vertretungsstunden hat und bei einer dritten die gesetzliche Ruhezeit von 11 Stunden unterschritten würde. A hat 80 Strafpunkte weniger.“</em></p>
</blockquote>
<p>Bei Portfolioentscheidungen: <strong>Kostenzerlegung</strong> ausweisen — Alpha-Ertrag, Risikoprämie, Transaktionskostenstrafe (siehe das Muster in <a href="cpsat.html#kap-cpsat">Kapitel 7</a> und <a href="cvar.html#kap-cvar">Kapitel 20</a>).</p>
<blockquote>
<p><strong>🎯 Die Faustregel für Erklärbarkeit</strong> Für jede Entscheidung sollten Sie drei Fragen beantworten können: 1. <strong>Warum diese Lösung?</strong> — Zielfunktionswert und seine Bestandteile. 2. <strong>Warum nicht die naheliegende Alternative?</strong> — Welche Bedingung verhindert sie? 3. <strong>Was würde die Lösung verbessern?</strong> — Schattenpreise: welcher Engpass, welcher Wert.</p>
<p><a href="lp.html#kap-lp">Kapitel 5</a> liefert für Frage 3 das Werkzeug. Nutzen Sie es — Schattenpreise sind in Managementgesprächen oft wertvoller als die Lösung selbst.</p>
</blockquote>
<p>Ein vollständiger Constraint-Trace beantwortet alle drei Fragen der Faustregel für eine einzelne Zuweisung — inklusive einer Unterscheidung, die in der Praxis oft übersehen wird: eine Bedingung kann <strong>blockieren</strong>, ohne <strong>etwas zu kosten</strong>.</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"># Erklaerbarkeit.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 Praxisfallen: Ein Constraint-Trace, der die drei Fragen der Faustregel fuer</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="co">Erklaerbarkeit (Abschnitt &#39;Die fuenf typischen Praxisfallen&#39;) fuer eine</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="co">einzelne Zuweisung beantwortet:</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="co">Warum diese Loesung? Warum nicht die Alternative? Was wuerde sie verbessern?</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">from</span> ortools.sat.python <span class="im">import</span> cp_model</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>PERSONAL <span class="op">=</span> [<span class="st">&quot;Anna&quot;</span>, <span class="st">&quot;Ben&quot;</span>, <span class="st">&quot;Clara&quot;</span>]</span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>BEREITS_STUNDEN <span class="op">=</span> {<span class="st">&quot;Anna&quot;</span>: <span class="dv">1</span>, <span class="st">&quot;Ben&quot;</span>: <span class="dv">2</span>, <span class="st">&quot;Clara&quot;</span>: <span class="dv">0</span>} <span class="co"># diese Woche schon geleistet</span></span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a>MAX_STUNDEN <span class="op">=</span> <span class="dv">2</span> <span class="co"># gesetzliche Obergrenze</span></span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a>STRAFE_VORBELASTUNG <span class="op">=</span> <span class="dv">50</span> <span class="co"># pro bereits geleisteter Stunde</span></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a>STRAFKOSTEN_WUNSCH <span class="op">=</span> {<span class="st">&quot;Anna&quot;</span>: <span class="dv">0</span>, <span class="st">&quot;Ben&quot;</span>: <span class="dv">30</span>, <span class="st">&quot;Clara&quot;</span>: <span class="dv">100</span>}</span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese(max_stunden: <span class="bu">int</span>):</span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a> modell <span class="op">=</span> cp_model.CpModel()</span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> {p: modell.NewBoolVar(<span class="ss">f&quot;x_</span><span class="sc">{</span>p<span class="sc">}</span><span class="ss">&quot;</span>) <span class="cf">for</span> p <span class="kw">in</span> PERSONAL}</span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a> modell.AddExactlyOne(x.values())</span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a> kosten <span class="op">=</span> []</span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> PERSONAL:</span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> BEREITS_STUNDEN[p] <span class="op">+</span> <span class="dv">1</span> <span class="op">&gt;</span> max_stunden:</span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a> modell.Add(x[p] <span class="op">==</span> <span class="dv">0</span>) <span class="co"># hartes Ausschlusskriterium</span></span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a> strafe <span class="op">=</span> BEREITS_STUNDEN[p] <span class="op">*</span> STRAFE_VORBELASTUNG <span class="op">+</span> STRAFKOSTEN_WUNSCH[p]</span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a> kosten.append(strafe <span class="op">*</span> x[p])</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> modell.Minimize(<span class="bu">sum</span>(kosten))</span>
<span id="cb3-33"><a href="#cb3-33" aria-hidden="true" tabindex="-1"></a> loeser <span class="op">=</span> cp_model.CpSolver()</span>
<span id="cb3-34"><a href="#cb3-34" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> loeser.Solve(modell)</span>
<span id="cb3-35"><a href="#cb3-35" 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="cb3-36"><a href="#cb3-36" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">None</span></span>
<span id="cb3-37"><a href="#cb3-37" aria-hidden="true" tabindex="-1"></a> gewinner <span class="op">=</span> <span class="bu">next</span>(p <span class="cf">for</span> p <span class="kw">in</span> PERSONAL <span class="cf">if</span> loeser.Value(x[p]))</span>
<span id="cb3-38"><a href="#cb3-38" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> gewinner, loeser.ObjectiveValue()</span>
<span id="cb3-39"><a href="#cb3-39" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-40"><a href="#cb3-40" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-41"><a href="#cb3-41" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> gesamtkosten(p: <span class="bu">str</span>) <span class="op">-&gt;</span> <span class="bu">int</span>:</span>
<span id="cb3-42"><a href="#cb3-42" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> BEREITS_STUNDEN[p] <span class="op">*</span> STRAFE_VORBELASTUNG <span class="op">+</span> STRAFKOSTEN_WUNSCH[p]</span>
<span id="cb3-43"><a href="#cb3-43" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-44"><a href="#cb3-44" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-45"><a href="#cb3-45" 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-46"><a href="#cb3-46" aria-hidden="true" tabindex="-1"></a> gewinner, kosten_opt <span class="op">=</span> loese(MAX_STUNDEN)</span>
<span id="cb3-47"><a href="#cb3-47" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-48"><a href="#cb3-48" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb3-49"><a href="#cb3-49" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; CONSTRAINT-TRACE: Warum bekommt&quot;</span>, gewinner, <span class="st">&quot;die Vertretungsstunde?&quot;</span>)</span>
<span id="cb3-50"><a href="#cb3-50" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb3-51"><a href="#cb3-51" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-52"><a href="#cb3-52" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">1. WARUM DIESE LOESUNG?&quot;</span>)</span>
<span id="cb3-53"><a href="#cb3-53" aria-hidden="true" tabindex="-1"></a> v, w <span class="op">=</span> BEREITS_STUNDEN[gewinner] <span class="op">*</span> STRAFE_VORBELASTUNG, STRAFKOSTEN_WUNSCH[gewinner]</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>gewinner<span class="sc">}</span><span class="ss">: Vorbelastung </span><span class="sc">{</span>v<span class="sc">}</span><span class="ss"> Strafpunkte + Wunsch </span><span class="sc">{</span>w<span class="sc">}</span><span class="ss"> Strafpunkte &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>v <span class="op">+</span> w<span class="sc">}</span><span class="ss"> Strafpunkte insgesamt (Minimum).&quot;</span>)</span>
<span id="cb3-56"><a href="#cb3-56" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-57"><a href="#cb3-57" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">2. WARUM NICHT DIE NAHELIEGENDEN ALTERNATIVEN?&quot;</span>)</span>
<span id="cb3-58"><a href="#cb3-58" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> PERSONAL:</span>
<span id="cb3-59"><a href="#cb3-59" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> p <span class="op">==</span> gewinner:</span>
<span id="cb3-60"><a href="#cb3-60" aria-hidden="true" tabindex="-1"></a> <span class="cf">continue</span></span>
<span id="cb3-61"><a href="#cb3-61" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> BEREITS_STUNDEN[p] <span class="op">+</span> <span class="dv">1</span> <span class="op">&gt;</span> MAX_STUNDEN:</span>
<span id="cb3-62"><a href="#cb3-62" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>p<span class="sc">}</span><span class="ss">: AUSGESCHLOSSEN - hat bereits </span><span class="sc">{</span>BEREITS_STUNDEN[p]<span class="sc">}</span><span class="ss"> Stunden, &quot;</span></span>
<span id="cb3-63"><a href="#cb3-63" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;eine dritte würde die Obergrenze von </span><span class="sc">{</span>MAX_STUNDEN<span class="sc">}</span><span class="ss"> Stunden verletzen &quot;</span></span>
<span id="cb3-64"><a href="#cb3-64" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(harte Bedingung, nicht verhandelbar).&quot;</span>)</span>
<span id="cb3-65"><a href="#cb3-65" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb3-66"><a href="#cb3-66" aria-hidden="true" tabindex="-1"></a> differenz <span class="op">=</span> gesamtkosten(p) <span class="op">-</span> kosten_opt</span>
<span id="cb3-67"><a href="#cb3-67" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>p<span class="sc">}</span><span class="ss">: zulässig, aber </span><span class="sc">{</span>differenz<span class="sc">:.0f}</span><span class="ss"> Strafpunkte teurer als &quot;</span></span>
<span id="cb3-68"><a href="#cb3-68" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>gewinner<span class="sc">}</span><span class="ss"> (</span><span class="sc">{</span>gesamtkosten(p)<span class="sc">:.0f}</span><span class="ss"> statt </span><span class="sc">{</span>kosten_opt<span class="sc">:.0f}</span><span class="ss">).&quot;</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="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">3. WAS WUERDE DIE LOESUNG VERBESSERN?&quot;</span>)</span>
<span id="cb3-71"><a href="#cb3-71" aria-hidden="true" tabindex="-1"></a> gewinner_gelockert, kosten_gelockert <span class="op">=</span> loese(MAX_STUNDEN <span class="op">+</span> <span class="dv">1</span>)</span>
<span id="cb3-72"><a href="#cb3-72" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> kosten_gelockert <span class="op">&lt;</span> kosten_opt:</span>
<span id="cb3-73"><a href="#cb3-73" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Obergrenze auf </span><span class="sc">{</span>MAX_STUNDEN <span class="op">+</span> <span class="dv">1</span><span class="sc">}</span><span class="ss"> Stunden gelockert: </span><span class="sc">{</span>gewinner_gelockert<span class="sc">}</span><span class="ss"> &quot;</span></span>
<span id="cb3-74"><a href="#cb3-74" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;uebernimmt jetzt zu </span><span class="sc">{</span>kosten_gelockert<span class="sc">:.0f}</span><span class="ss"> Strafpunkten &quot;</span></span>
<span id="cb3-75"><a href="#cb3-75" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span>kosten_opt <span class="op">-</span> kosten_gelockert<span class="sc">:.0f}</span><span class="ss"> weniger als heute).&quot;</span>)</span>
<span id="cb3-76"><a href="#cb3-76" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; -&gt; Diese Bedingung hat tatsaechlich einen Preis.&quot;</span>)</span>
<span id="cb3-77"><a href="#cb3-77" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb3-78"><a href="#cb3-78" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Obergrenze auf </span><span class="sc">{</span>MAX_STUNDEN <span class="op">+</span> <span class="dv">1</span><span class="sc">}</span><span class="ss"> Stunden gelockert: Ergebnis bleibt bei &quot;</span></span>
<span id="cb3-79"><a href="#cb3-79" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>gewinner<span class="sc">}</span><span class="ss"> mit </span><span class="sc">{</span>kosten_gelockert<span class="sc">:.0f}</span><span class="ss"> Strafpunkten (unveraendert).&quot;</span>)</span>
<span id="cb3-80"><a href="#cb3-80" aria-hidden="true" tabindex="-1"></a> blockiert <span class="op">=</span> <span class="st">&quot;, &quot;</span>.join(p <span class="cf">for</span> p <span class="kw">in</span> PERSONAL</span>
<span id="cb3-81"><a href="#cb3-81" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> BEREITS_STUNDEN[p] <span class="op">+</span> <span class="dv">1</span> <span class="op">&gt;</span> MAX_STUNDEN <span class="kw">and</span> p <span class="op">!=</span> gewinner)</span>
<span id="cb3-82"><a href="#cb3-82" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; -&gt; Die Obergrenze blockiert zwar </span><span class="sc">{</span>blockiert<span class="sc">}</span><span class="ss">, kostet aber gerade NICHTS:&quot;</span>)</span>
<span id="cb3-83"><a href="#cb3-83" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Selbst ohne sie waere die Loesung dieselbe.&quot;</span>)</span>
<span id="cb3-84"><a href="#cb3-84" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Blockierend und kostenrelevant sind zwei verschiedene Dinge.&quot;</span>)</span>
<span id="cb3-85"><a href="#cb3-85" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==============================================================================
CONSTRAINT-TRACE: Warum bekommt Anna die Vertretungsstunde?
==============================================================================
1. WARUM DIESE LOESUNG?
Anna: Vorbelastung 50 Strafpunkte + Wunsch 0 Strafpunkte = 50 Strafpunkte insgesamt (Minimum).
2. WARUM NICHT DIE NAHELIEGENDEN ALTERNATIVEN?
Ben: AUSGESCHLOSSEN - hat bereits 2 Stunden, eine dritte würde die Obergrenze von 2 Stunden verletzen (harte Bedingung, nicht verhandelbar).
Clara: zulässig, aber 50 Strafpunkte teurer als Anna (100 statt 50).
3. WAS WUERDE DIE LOESUNG VERBESSERN?
Obergrenze auf 3 Stunden gelockert: Ergebnis bleibt bei Anna mit 50 Strafpunkten (unveraendert).
-&gt; Die Obergrenze blockiert zwar Ben, kostet aber gerade NICHTS:
Selbst ohne sie waere die Loesung dieselbe.
Blockierend und kostenrelevant sind zwei verschiedene Dinge.
==============================================================================</code></pre>
<p>Der letzte Punkt ist der eigentliche Lerneffekt: Die Obergrenze schließt Ben zwar technisch aus, aber selbst wenn er dürfte, wäre er wegen seiner Vorbelastung immer noch teurer als Anna. <strong>Eine bindende Bedingung zu lockern hilft nur, wenn sie tatsächlich die kostenrelevante ist</strong> — sonst ist die Mühe umsonst.</p>
<p>Derselbe Unterschied kehrt auf Modellebene wieder, und dort lässt er sich in Euro beziffern: <a href="#sec:praxisfallen-attribution">Abschnitt 22.4</a> misst für jede Bedingung, was sie den ganzen Plan kostet — und baut daraus einen Bericht, den man vorlegen kann.</p>
<h3 id="falle-3-regimewechsel-und-schätzfehler">Falle 3 — Regimewechsel und Schätzfehler</h3>
<p>Ein Modell, das auf einer mehrjährigen Aufwärtsphase kalibriert wurde, kennt keine Liquiditätskrise und keine Zinswende.</p>
<ul>
<li><strong>Robuste Schätzung</strong> statt roher Historie: Ledoit-Wolf-Shrinkage (<a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a>), Szenarien (<a href="unsicherheit.html#kap-unsicherheit">Kapitel 12</a>).</li>
<li><strong>Harte Obergrenzen</strong> für Einzelpositionen und Sektoren — auch dann, wenn der Solver rechnerisch alles in einen Titel legen möchte. Die Grenze kostet Ertrag im Normalfall und rettet im Ernstfall.</li>
<li><strong>Rollierende Neuschätzung</strong> statt einmaliger Kalibrierung.</li>
</ul>
<h3 id="falle-4-lookahead--und-survivorship-bias">Falle 4 — Lookahead- und Survivorship-Bias</h3>
<p>Ausführlich in <a href="handelsmaschine.html#kap-handelsmaschine">Kapitel 21</a> behandelt. Die Kurzfassung: Beide Fehler machen Backtests systematisch zu gut, ohne eine Fehlermeldung zu erzeugen. Der Lookahead-Selbsttest aus <a href="handelsmaschine.html#sec:handelsmaschine-die-engine">Abschnitt 21.5</a> findet den ersten automatisch.</p>
<h3 id="falle-5-laufzeitexplosion">Falle 5 — Laufzeitexplosion</h3>
<p>Exakte Optimalität kann bei MILP oder CP-SAT Minuten bis Stunden dauern — und die Laufzeit wächst nicht linear mit der Problemgröße.</p>
<table>
<thead>
<tr class="header">
<th>Einsatzszenario</th>
<th>Empfohlenes Zeitlimit</th>
<th>Akzeptabler MIP-Gap</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Interaktive Oberfläche</td>
<td>510 Sekunden</td>
<td>25 %</td>
</tr>
<tr class="even">
<td>Batch tagsüber</td>
<td>15 Minuten</td>
<td>12 %</td>
</tr>
<tr class="odd">
<td>Nachtlauf</td>
<td>3060 Minuten</td>
<td>0,11 %</td>
</tr>
<tr class="even">
<td>Strategische Planung</td>
<td>Stunden</td>
<td>exakt anstreben</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>💡 Warum ein Gap von 2 % fast immer genügt</strong> Der MIP-Gap misst den Abstand zwischen der besten gefundenen Lösung und der besten <strong>beweisbaren Schranke</strong>. Ein Gap von 2 % heißt: „Diese Lösung ist höchstens 2 % vom theoretischen Optimum entfernt.“</p>
<p>Dem gegenüber steht die <strong>Datenunsicherheit</strong>: Ihre Nachfrageprognose ist um ±10 % genau, Ihre Fahrzeiten um ±15 %. Die letzten 2 % Optimalität in einem Modell zu erkämpfen, dessen Eingangsdaten um 10 % schwanken, ist verlorene Zeit. <strong>Optimieren Sie nicht genauer, als Ihre Daten sind.</strong></p>
</blockquote>
<hr />
<h2 id="sec:praxisfallen-attribution">22.4 Constraint Attribution: welche Bedingung kostet wie viel?</h2>
<p>Der Constraint-Trace beantwortet „warum <strong>diese</strong> Zuweisung?“. Der Deletion Filter in <a href="anhang-fehlerdiagnose.html#anhang-fehlerdiagnose">Anhang C</a> beantwortet „warum geht es <strong>gar nicht</strong>?“. Der häufigste Fall in der Praxis liegt dazwischen, und für ihn hat bisher niemand ein Werkzeug bekommen:</p>
<blockquote>
<p><em>Das Modell rechnet. Der Plan ist zulässig. Und trotzdem ist er enttäuschend — nur weiß niemand, woran es liegt.</em></p>
</blockquote>
<p>Die Antwort heißt <strong>Constraint Attribution</strong>: Jede Bedingungsgruppe wird einzeln gelockert, und gemessen wird, was sich am Zielwert ändert. Das ist dasselbe Verfahren wie der Deletion Filter — nur mit <strong>Kosten</strong> statt Zulässigkeit als Kriterium.</p>
<p>Gerechnet wird auf derselben Fabrik wie die Konfliktsuche in <a href="anhang-fehlerdiagnose.html#anhang-fehlerdiagnose">Anhang C</a>, aber <strong>nach der Reparatur</strong>: Dort war die Lackierkapazität von 150 Stunden der Kern des Widerspruchs (<span class="math inline">2 \cdot 40 + 3 \cdot 30 = 170 &gt; 150</span>). Eine zweite Schicht hat sie auf 210 gebracht, der Mindestumsatz wurde auf 10 000 € gesenkt. Jetzt ist das Modell lösbar — und die Frage lautet nicht mehr „warum nicht?“, sondern „was kostet uns was?“.</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"># Constraint_Attribution.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 Praxisfallen: Welche Bedingung kostet wie viel - und was sagt man</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a><span class="co">dem Management?</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a><span class="co">Zwischen den beiden bekannten Faellen klafft eine Luecke:</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="co"> * Erklaerbarkeit.py beantwortet &quot;warum diese eine Zuweisung?&quot;</span></span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a><span class="co"> * Konfliktsuche.py (Anhang Fehlerdiagnose) beantwortet &quot;warum geht es gar</span></span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a><span class="co"> nicht?&quot;</span></span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a><span class="co">Der haeufigste Fall in der Praxis liegt dazwischen: Das Modell rechnet, der</span></span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a><span class="co">Plan ist zulaessig - und trotzdem unbefriedigend. Niemand weiss, WORAN es</span></span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a><span class="co">liegt. Genau das beantwortet Constraint Attribution.</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 class="co">Gerechnet wird auf derselben Fabrik wie in Konfliktsuche.py, nur nach der</span></span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a><span class="co">Reparatur: Dort war die Lackierkapazitaet von 150 Stunden der Kern des</span></span>
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a><span class="co">Widerspruchs (2*40 + 3*30 = 170 &gt; 150). Eine zweite Schicht hat sie auf 210</span></span>
<span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a><span class="co">gebracht, und der Mindestumsatz wurde auf 10.000 EUR gesenkt. Jetzt ist das</span></span>
<span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a><span class="co">Modell loesbar - und die Frage lautet nicht mehr &quot;warum nicht?&quot;, sondern</span></span>
<span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;was kostet uns was?&quot;.</span></span>
<span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a><span class="co">Gezeigt werden vier Dinge:</span></span>
<span id="cb5-26"><a href="#cb5-26" aria-hidden="true" tabindex="-1"></a><span class="co"> 1. Welche Bedingungen binden - und dass &quot;bindend&quot; nicht &quot;teuer&quot; heisst.</span></span>
<span id="cb5-27"><a href="#cb5-27" aria-hidden="true" tabindex="-1"></a><span class="co"> 2. Dass der Schattenpreis eine MOMENTAUFNAHME ist: hochgerechnet auf eine</span></span>
<span id="cb5-28"><a href="#cb5-28" aria-hidden="true" tabindex="-1"></a><span class="co"> realistische Sonderschicht verspricht er hier das Doppelte des</span></span>
<span id="cb5-29"><a href="#cb5-29" aria-hidden="true" tabindex="-1"></a><span class="co"> tatsaechlichen Nutzens.</span></span>
<span id="cb5-30"><a href="#cb5-30" aria-hidden="true" tabindex="-1"></a><span class="co"> 3. Dass ein Wunsch aus dem Vertrieb nicht teuer, sondern unmoeglich sein</span></span>
<span id="cb5-31"><a href="#cb5-31" aria-hidden="true" tabindex="-1"></a><span class="co"> kann - und welche Bedingungen ihn gemeinsam blockieren.</span></span>
<span id="cb5-32"><a href="#cb5-32" aria-hidden="true" tabindex="-1"></a><span class="co"> 4. Wie aus alledem ein Bericht in Alltagssprache wird.</span></span>
<span id="cb5-33"><a href="#cb5-33" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-34"><a href="#cb5-34" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, scipy</span></span>
<span id="cb5-35"><a href="#cb5-35" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb5-36"><a href="#cb5-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-37"><a href="#cb5-37" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb5-38"><a href="#cb5-38" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-39"><a href="#cb5-39" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> textwrap</span>
<span id="cb5-40"><a href="#cb5-40" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-41"><a href="#cb5-41" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb5-42"><a href="#cb5-42" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linprog</span>
<span id="cb5-43"><a href="#cb5-43" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-44"><a href="#cb5-44" aria-hidden="true" tabindex="-1"></a>PRODUKTE <span class="op">=</span> [<span class="st">&quot;Rahmen&quot;</span>, <span class="st">&quot;Gehaeuse&quot;</span>, <span class="st">&quot;Deckel&quot;</span>, <span class="st">&quot;Traeger&quot;</span>, <span class="st">&quot;Halter&quot;</span>]</span>
<span id="cb5-45"><a href="#cb5-45" aria-hidden="true" tabindex="-1"></a>DECKUNGSBEITRAG <span class="op">=</span> np.array([<span class="fl">35.0</span>, <span class="fl">28.0</span>, <span class="fl">9.0</span>, <span class="fl">22.0</span>, <span class="fl">6.0</span>]) <span class="co"># EUR je Stueck</span></span>
<span id="cb5-46"><a href="#cb5-46" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-47"><a href="#cb5-47" aria-hidden="true" tabindex="-1"></a><span class="co"># Dieselben Bedingungen wie in Konfliktsuche.py, mit den beiden Reparaturen.</span></span>
<span id="cb5-48"><a href="#cb5-48" aria-hidden="true" tabindex="-1"></a><span class="co"># Der sprechende Name ist keine Kosmetik: Ohne ihn ist kein Bericht moeglich,</span></span>
<span id="cb5-49"><a href="#cb5-49" aria-hidden="true" tabindex="-1"></a><span class="co"># der jemandem ausserhalb der Modellierung etwas sagt.</span></span>
<span id="cb5-50"><a href="#cb5-50" aria-hidden="true" tabindex="-1"></a>BEDINGUNGEN: <span class="bu">list</span>[<span class="bu">tuple</span>[<span class="bu">str</span>, <span class="bu">list</span>[<span class="bu">float</span>], <span class="bu">str</span>, <span class="bu">float</span>]] <span class="op">=</span> [</span>
<span id="cb5-51"><a href="#cb5-51" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Kapazitaet Montage&quot;</span>, [<span class="dv">3</span>, <span class="dv">2</span>, <span class="dv">1</span>, <span class="dv">4</span>, <span class="dv">2</span>], <span class="st">&quot;&lt;=&quot;</span>, <span class="dv">400</span>),</span>
<span id="cb5-52"><a href="#cb5-52" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Kapazitaet Lackieren&quot;</span>, [<span class="dv">2</span>, <span class="dv">3</span>, <span class="dv">0</span>, <span class="dv">1</span>, <span class="dv">0</span>], <span class="st">&quot;&lt;=&quot;</span>, <span class="dv">210</span>),</span>
<span id="cb5-53"><a href="#cb5-53" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Kapazitaet Pruefung&quot;</span>, [<span class="dv">1</span>, <span class="dv">1</span>, <span class="dv">1</span>, <span class="dv">1</span>, <span class="dv">1</span>], <span class="st">&quot;&lt;=&quot;</span>, <span class="dv">300</span>),</span>
<span id="cb5-54"><a href="#cb5-54" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Liefervertrag Rahmen&quot;</span>, [<span class="dv">1</span>, <span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">0</span>], <span class="st">&quot;&gt;=&quot;</span>, <span class="dv">40</span>),</span>
<span id="cb5-55"><a href="#cb5-55" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Liefervertrag Gehaeuse&quot;</span>, [<span class="dv">0</span>, <span class="dv">1</span>, <span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">0</span>], <span class="st">&quot;&gt;=&quot;</span>, <span class="dv">30</span>),</span>
<span id="cb5-56"><a href="#cb5-56" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Liefervertrag Deckel&quot;</span>, [<span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">1</span>, <span class="dv">0</span>, <span class="dv">0</span>], <span class="st">&quot;&gt;=&quot;</span>, <span class="dv">20</span>),</span>
<span id="cb5-57"><a href="#cb5-57" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Liefervertrag Traeger&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">0</span>], <span class="st">&quot;&gt;=&quot;</span>, <span class="dv">25</span>),</span>
<span id="cb5-58"><a href="#cb5-58" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Marktgrenze Rahmen&quot;</span>, [<span class="dv">1</span>, <span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">0</span>], <span class="st">&quot;&lt;=&quot;</span>, <span class="dv">120</span>),</span>
<span id="cb5-59"><a href="#cb5-59" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Marktgrenze Gehaeuse&quot;</span>, [<span class="dv">0</span>, <span class="dv">1</span>, <span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">0</span>], <span class="st">&quot;&lt;=&quot;</span>, <span class="dv">90</span>),</span>
<span id="cb5-60"><a href="#cb5-60" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Marktgrenze Deckel&quot;</span>, [<span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">1</span>, <span class="dv">0</span>, <span class="dv">0</span>], <span class="st">&quot;&lt;=&quot;</span>, <span class="dv">150</span>),</span>
<span id="cb5-61"><a href="#cb5-61" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Marktgrenze Halter&quot;</span>, [<span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">1</span>], <span class="st">&quot;&lt;=&quot;</span>, <span class="dv">200</span>),</span>
<span id="cb5-62"><a href="#cb5-62" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Mindestumsatz&quot;</span>, [<span class="dv">90</span>, <span class="dv">70</span>, <span class="dv">30</span>, <span class="dv">60</span>, <span class="dv">20</span>], <span class="st">&quot;&gt;=&quot;</span>, <span class="dv">10000</span>),</span>
<span id="cb5-63"><a href="#cb5-63" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Sortimentsbreite&quot;</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 class="st">&quot;&gt;=&quot;</span>, <span class="dv">100</span>),</span>
<span id="cb5-64"><a href="#cb5-64" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Lackierbudget Schicht 2&quot;</span>, [<span class="dv">0</span>, <span class="dv">1</span>, <span class="dv">0</span>, <span class="dv">1</span>, <span class="dv">0</span>], <span class="st">&quot;&lt;=&quot;</span>, <span class="dv">55</span>),</span>
<span id="cb5-65"><a href="#cb5-65" aria-hidden="true" tabindex="-1"></a>]</span>
<span id="cb5-66"><a href="#cb5-66" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-67"><a href="#cb5-67" aria-hidden="true" tabindex="-1"></a><span class="co"># Was liesse sich im Ernstfall wirklich bewegen - und um wie viel? Nicht &quot;eine</span></span>
<span id="cb5-68"><a href="#cb5-68" aria-hidden="true" tabindex="-1"></a><span class="co"># Einheit&quot;, sondern der Schritt, den ein Planer tatsaechlich gehen kann: eine</span></span>
<span id="cb5-69"><a href="#cb5-69" aria-hidden="true" tabindex="-1"></a><span class="co"># Sonderschicht, ein nachverhandelter Vertrag. Genau an dieser Groesse</span></span>
<span id="cb5-70"><a href="#cb5-70" aria-hidden="true" tabindex="-1"></a><span class="co"># scheitert das blosse Hochrechnen des Schattenpreises.</span></span>
<span id="cb5-71"><a href="#cb5-71" aria-hidden="true" tabindex="-1"></a>STELLSCHRAUBEN: <span class="bu">dict</span>[<span class="bu">str</span>, <span class="bu">tuple</span>[<span class="bu">float</span>, <span class="bu">str</span>]] <span class="op">=</span> {</span>
<span id="cb5-72"><a href="#cb5-72" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Kapazitaet Montage&quot;</span>: (<span class="op">+</span><span class="dv">50</span>, <span class="st">&quot;eine Sonderschicht Montage&quot;</span>),</span>
<span id="cb5-73"><a href="#cb5-73" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Kapazitaet Lackieren&quot;</span>: (<span class="op">+</span><span class="dv">30</span>, <span class="st">&quot;eine Sonderschicht Lackieren&quot;</span>),</span>
<span id="cb5-74"><a href="#cb5-74" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Kapazitaet Pruefung&quot;</span>: (<span class="op">+</span><span class="dv">50</span>, <span class="st">&quot;eine Sonderschicht Pruefung&quot;</span>),</span>
<span id="cb5-75"><a href="#cb5-75" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Lackierbudget Schicht 2&quot;</span>: (<span class="op">+</span><span class="dv">10</span>, <span class="st">&quot;10 Stunden mehr Budget in Schicht 2&quot;</span>),</span>
<span id="cb5-76"><a href="#cb5-76" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Liefervertrag Traeger&quot;</span>: (<span class="op">-</span><span class="dv">10</span>, <span class="st">&quot;10 Stueck weniger Abnahmepflicht Traeger&quot;</span>),</span>
<span id="cb5-77"><a href="#cb5-77" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Liefervertrag Gehaeuse&quot;</span>: (<span class="op">-</span><span class="dv">10</span>, <span class="st">&quot;10 Stueck weniger Abnahmepflicht Gehaeuse&quot;</span>),</span>
<span id="cb5-78"><a href="#cb5-78" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb5-79"><a href="#cb5-79" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-80"><a href="#cb5-80" aria-hidden="true" tabindex="-1"></a>WUNSCH_MENGE <span class="op">=</span> <span class="fl">55.0</span> <span class="co"># der Vertrieb haette gern mehr Rahmen</span></span>
<span id="cb5-81"><a href="#cb5-81" aria-hidden="true" tabindex="-1"></a>WUNSCH_PRODUKT <span class="op">=</span> <span class="st">&quot;Rahmen&quot;</span></span>
<span id="cb5-82"><a href="#cb5-82" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-83"><a href="#cb5-83" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-84"><a href="#cb5-84" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese(bedingungen: <span class="bu">list</span>[<span class="bu">tuple</span>[<span class="bu">str</span>, <span class="bu">list</span>[<span class="bu">float</span>], <span class="bu">str</span>, <span class="bu">float</span>]]):</span>
<span id="cb5-85"><a href="#cb5-85" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Maximiert den Deckungsbeitrag. linprog minimiert, daher das Vorzeichen.&quot;&quot;&quot;</span></span>
<span id="cb5-86"><a href="#cb5-86" aria-hidden="true" tabindex="-1"></a> matrix, rechte_seite <span class="op">=</span> [], []</span>
<span id="cb5-87"><a href="#cb5-87" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> _, koeffizienten, richtung, grenze <span class="kw">in</span> bedingungen:</span>
<span id="cb5-88"><a href="#cb5-88" aria-hidden="true" tabindex="-1"></a> zeile <span class="op">=</span> np.array(koeffizienten, dtype<span class="op">=</span><span class="bu">float</span>)</span>
<span id="cb5-89"><a href="#cb5-89" aria-hidden="true" tabindex="-1"></a> matrix.append(zeile <span class="cf">if</span> richtung <span class="op">==</span> <span class="st">&quot;&lt;=&quot;</span> <span class="cf">else</span> <span class="op">-</span>zeile)</span>
<span id="cb5-90"><a href="#cb5-90" aria-hidden="true" tabindex="-1"></a> rechte_seite.append(grenze <span class="cf">if</span> richtung <span class="op">==</span> <span class="st">&quot;&lt;=&quot;</span> <span class="cf">else</span> <span class="op">-</span>grenze)</span>
<span id="cb5-91"><a href="#cb5-91" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> linprog(<span class="op">-</span>DECKUNGSBEITRAG,</span>
<span id="cb5-92"><a href="#cb5-92" aria-hidden="true" tabindex="-1"></a> A_ub<span class="op">=</span>np.array(matrix), b_ub<span class="op">=</span>np.array(rechte_seite),</span>
<span id="cb5-93"><a href="#cb5-93" aria-hidden="true" tabindex="-1"></a> bounds<span class="op">=</span>[(<span class="dv">0</span>, <span class="va">None</span>)] <span class="op">*</span> <span class="bu">len</span>(PRODUKTE), method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb5-94"><a href="#cb5-94" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-95"><a href="#cb5-95" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-96"><a href="#cb5-96" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> geaendert(name: <span class="bu">str</span>, delta: <span class="bu">float</span>,</span>
<span id="cb5-97"><a href="#cb5-97" aria-hidden="true" tabindex="-1"></a> basis<span class="op">=</span><span class="va">None</span>) <span class="op">-&gt;</span> <span class="bu">list</span>[<span class="bu">tuple</span>[<span class="bu">str</span>, <span class="bu">list</span>[<span class="bu">float</span>], <span class="bu">str</span>, <span class="bu">float</span>]]:</span>
<span id="cb5-98"><a href="#cb5-98" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Dieselben Bedingungen, eine davon um delta verschoben.&quot;&quot;&quot;</span></span>
<span id="cb5-99"><a href="#cb5-99" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> [(n, k, r, g <span class="op">+</span> delta <span class="cf">if</span> n <span class="op">==</span> name <span class="cf">else</span> g)</span>
<span id="cb5-100"><a href="#cb5-100" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> n, k, r, g <span class="kw">in</span> (basis <span class="kw">or</span> BEDINGUNGEN)]</span>
<span id="cb5-101"><a href="#cb5-101" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-102"><a href="#cb5-102" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-103"><a href="#cb5-103" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> auswertung(ergebnis, bedingungen<span class="op">=</span><span class="va">None</span>):</span>
<span id="cb5-104"><a href="#cb5-104" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Schlupf und Schattenpreis je Bedingung.&quot;&quot;&quot;</span></span>
<span id="cb5-105"><a href="#cb5-105" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> ergebnis.x</span>
<span id="cb5-106"><a href="#cb5-106" aria-hidden="true" tabindex="-1"></a> dual <span class="op">=</span> ergebnis.ineqlin.marginals</span>
<span id="cb5-107"><a href="#cb5-107" aria-hidden="true" tabindex="-1"></a> zeilen <span class="op">=</span> []</span>
<span id="cb5-108"><a href="#cb5-108" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i, (name, koeff, richtung, grenze) <span class="kw">in</span> <span class="bu">enumerate</span>(bedingungen <span class="kw">or</span> BEDINGUNGEN):</span>
<span id="cb5-109"><a href="#cb5-109" aria-hidden="true" tabindex="-1"></a> links <span class="op">=</span> <span class="bu">float</span>(np.array(koeff, dtype<span class="op">=</span><span class="bu">float</span>) <span class="op">@</span> x)</span>
<span id="cb5-110"><a href="#cb5-110" aria-hidden="true" tabindex="-1"></a> schlupf <span class="op">=</span> (grenze <span class="op">-</span> links) <span class="cf">if</span> richtung <span class="op">==</span> <span class="st">&quot;&lt;=&quot;</span> <span class="cf">else</span> (links <span class="op">-</span> grenze)</span>
<span id="cb5-111"><a href="#cb5-111" aria-hidden="true" tabindex="-1"></a> zeilen.append((name, links, richtung, grenze, schlupf, <span class="bu">abs</span>(dual[i])))</span>
<span id="cb5-112"><a href="#cb5-112" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> zeilen</span>
<span id="cb5-113"><a href="#cb5-113" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-114"><a href="#cb5-114" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-115"><a href="#cb5-115" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> blockierer(bedingungen, zusatz) <span class="op">-&gt;</span> <span class="bu">list</span>[<span class="bu">str</span>]:</span>
<span id="cb5-116"><a href="#cb5-116" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Welche Bedingungen verhindern &#39;zusatz&#39; - einzeln weggelassen.</span></span>
<span id="cb5-117"><a href="#cb5-117" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-118"><a href="#cb5-118" aria-hidden="true" tabindex="-1"></a><span class="co"> Dasselbe Vorgehen wie der Deletion Filter in Konfliktsuche.py, nur mit</span></span>
<span id="cb5-119"><a href="#cb5-119" aria-hidden="true" tabindex="-1"></a><span class="co"> umgekehrtem Vorzeichen: Dort wird gesucht, was den Widerspruch AUSMACHT,</span></span>
<span id="cb5-120"><a href="#cb5-120" aria-hidden="true" tabindex="-1"></a><span class="co"> hier, was einen Wunsch VERHINDERT. Wird das Modell ohne eine Bedingung</span></span>
<span id="cb5-121"><a href="#cb5-121" aria-hidden="true" tabindex="-1"></a><span class="co"> loesbar, gehoert sie zum Konflikt.</span></span>
<span id="cb5-122"><a href="#cb5-122" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb5-123"><a href="#cb5-123" aria-hidden="true" tabindex="-1"></a> gefunden <span class="op">=</span> []</span>
<span id="cb5-124"><a href="#cb5-124" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i, (name, _, _, _) <span class="kw">in</span> <span class="bu">enumerate</span>(bedingungen):</span>
<span id="cb5-125"><a href="#cb5-125" aria-hidden="true" tabindex="-1"></a> rest <span class="op">=</span> [c <span class="cf">for</span> j, c <span class="kw">in</span> <span class="bu">enumerate</span>(bedingungen) <span class="cf">if</span> j <span class="op">!=</span> i] <span class="op">+</span> [zusatz]</span>
<span id="cb5-126"><a href="#cb5-126" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> loese(rest).success:</span>
<span id="cb5-127"><a href="#cb5-127" aria-hidden="true" tabindex="-1"></a> gefunden.append(name)</span>
<span id="cb5-128"><a href="#cb5-128" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> gefunden</span>
<span id="cb5-129"><a href="#cb5-129" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-130"><a href="#cb5-130" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-131"><a href="#cb5-131" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> euro(betrag: <span class="bu">float</span>) <span class="op">-&gt;</span> <span class="bu">str</span>:</span>
<span id="cb5-132"><a href="#cb5-132" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="ss">f&quot;</span><span class="sc">{</span>betrag<span class="sc">:,.2f}</span><span class="ss"> EUR&quot;</span></span>
<span id="cb5-133"><a href="#cb5-133" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-134"><a href="#cb5-134" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-135"><a href="#cb5-135" 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-136"><a href="#cb5-136" aria-hidden="true" tabindex="-1"></a> basis <span class="op">=</span> loese(BEDINGUNGEN)</span>
<span id="cb5-137"><a href="#cb5-137" aria-hidden="true" tabindex="-1"></a> db_basis <span class="op">=</span> <span class="op">-</span>basis.fun</span>
<span id="cb5-138"><a href="#cb5-138" aria-hidden="true" tabindex="-1"></a> plan <span class="op">=</span> <span class="bu">dict</span>(<span class="bu">zip</span>(PRODUKTE, basis.x))</span>
<span id="cb5-139"><a href="#cb5-139" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-140"><a href="#cb5-140" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-141"><a href="#cb5-141" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; CONSTRAINT ATTRIBUTION: Was kostet uns welche Bedingung?&quot;</span>)</span>
<span id="cb5-142"><a href="#cb5-142" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-143"><a href="#cb5-143" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Status: </span><span class="sc">{</span>basis<span class="sc">.</span>message<span class="sc">.</span>split(<span class="st">&#39;(&#39;</span>)[<span class="dv">0</span>]<span class="sc">.</span>strip()<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-144"><a href="#cb5-144" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Deckungsbeitrag des Plans: </span><span class="sc">{</span>euro(db_basis)<span class="sc">}</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb5-145"><a href="#cb5-145" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; &quot;</span> <span class="op">+</span> <span class="st">&quot; &quot;</span>.join(<span class="ss">f&quot;</span><span class="sc">{</span>p<span class="sc">}</span><span class="ss">: </span><span class="sc">{</span>m<span class="sc">:.1f}</span><span class="ss">&quot;</span> <span class="cf">for</span> p, m <span class="kw">in</span> plan.items()))</span>
<span id="cb5-146"><a href="#cb5-146" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-147"><a href="#cb5-147" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- 1. Wer begrenzt den Plan? -----------------------------------------</span></span>
<span id="cb5-148"><a href="#cb5-148" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-149"><a href="#cb5-149" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; (1) Welche Bedingungen binden - und was kostet die naechste Einheit?&quot;</span>)</span>
<span id="cb5-150"><a href="#cb5-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">78</span>)</span>
<span id="cb5-151"><a href="#cb5-151" 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;Bedingung&#39;</span><span class="sc">:&lt;24}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;genutzt&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Grenze&#39;</span><span class="sc">:&gt;7}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Schlupf&#39;</span><span class="sc">:&gt;8}</span><span class="ss"> &quot;</span></span>
<span id="cb5-152"><a href="#cb5-152" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Schattenpreis&#39;</span><span class="sc">:&gt;11}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-153"><a href="#cb5-153" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-154"><a href="#cb5-154" aria-hidden="true" tabindex="-1"></a> bindend <span class="op">=</span> []</span>
<span id="cb5-155"><a href="#cb5-155" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> name, links, richtung, grenze, schlupf, dual <span class="kw">in</span> auswertung(basis):</span>
<span id="cb5-156"><a href="#cb5-156" aria-hidden="true" tabindex="-1"></a> marke <span class="op">=</span> <span class="st">&quot;&quot;</span></span>
<span id="cb5-157"><a href="#cb5-157" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(schlupf) <span class="op">&lt;</span> <span class="fl">1e-6</span>:</span>
<span id="cb5-158"><a href="#cb5-158" aria-hidden="true" tabindex="-1"></a> bindend.append((name, dual))</span>
<span id="cb5-159"><a href="#cb5-159" aria-hidden="true" tabindex="-1"></a> marke <span class="op">=</span> <span class="st">&quot; &lt;-- bindend&quot;</span></span>
<span id="cb5-160"><a href="#cb5-160" 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;24}</span><span class="ss"> </span><span class="sc">{</span>links<span class="sc">:&gt;9.1f}</span><span class="ss"> </span><span class="sc">{</span>grenze<span class="sc">:&gt;7.0f}</span><span class="ss"> </span><span class="sc">{</span>schlupf<span class="sc">:&gt;8.1f}</span><span class="ss"> &quot;</span></span>
<span id="cb5-161"><a href="#cb5-161" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>dual<span class="sc">:&gt;11.2f}{</span>marke<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-162"><a href="#cb5-162" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-163"><a href="#cb5-163" aria-hidden="true" tabindex="-1"></a> ohne_preis <span class="op">=</span> [n <span class="cf">for</span> n, d <span class="kw">in</span> bindend <span class="cf">if</span> d <span class="op">&lt;</span> <span class="fl">1e-9</span>]</span>
<span id="cb5-164"><a href="#cb5-164" 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"> </span><span class="sc">{</span><span class="bu">len</span>(bindend)<span class="sc">}</span><span class="ss"> Bedingungen binden. Aber: &quot;</span></span>
<span id="cb5-165"><a href="#cb5-165" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;, &#39;</span><span class="sc">.</span>join(ohne_preis)<span class="sc">}</span><span class="ss"> bindet&quot;</span>)</span>
<span id="cb5-166"><a href="#cb5-166" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; mit einem Schattenpreis von 0,00 EUR - sie beruehrt den Plan, ohne ihn&quot;</span>)</span>
<span id="cb5-167"><a href="#cb5-167" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; zu verteuern. Bindend und kostenrelevant sind zwei verschiedene Dinge.&quot;</span>)</span>
<span id="cb5-168"><a href="#cb5-168" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-169"><a href="#cb5-169" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- 2. Der Schattenpreis als Momentaufnahme ---------------------------</span></span>
<span id="cb5-170"><a href="#cb5-170" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-171"><a href="#cb5-171" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; (2) Was der Schattenpreis verschweigt&quot;</span>)</span>
<span id="cb5-172"><a href="#cb5-172" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-173"><a href="#cb5-173" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Der Schattenpreis gilt fuer die NAECHSTE Einheit. Ein Planer kauft aber&quot;</span>)</span>
<span id="cb5-174"><a href="#cb5-174" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;keine Einheit, sondern eine ganze Schicht. Beides gegenuebergestellt:</span><span class="ch">\n</span><span class="st">&quot;</span>)</span>
<span id="cb5-175"><a href="#cb5-175" aria-hidden="true" tabindex="-1"></a> preise <span class="op">=</span> {name: dual <span class="cf">for</span> name, <span class="op">*</span>_, dual <span class="kw">in</span> auswertung(basis)}</span>
<span id="cb5-176"><a href="#cb5-176" 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;Stellschraube&#39;</span><span class="sc">:&lt;24}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Preis&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Schritt&#39;</span><span class="sc">:&gt;7}</span><span class="ss"> &quot;</span></span>
<span id="cb5-177"><a href="#cb5-177" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;hochgerechnet&#39;</span><span class="sc">:&gt;13}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;gemessen&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Abw.&#39;</span><span class="sc">:&gt;9}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-178"><a href="#cb5-178" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-179"><a href="#cb5-179" aria-hidden="true" tabindex="-1"></a> wirkung <span class="op">=</span> {}</span>
<span id="cb5-180"><a href="#cb5-180" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> name, (delta, _) <span class="kw">in</span> STELLSCHRAUBEN.items():</span>
<span id="cb5-181"><a href="#cb5-181" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> loese(geaendert(name, delta))</span>
<span id="cb5-182"><a href="#cb5-182" aria-hidden="true" tabindex="-1"></a> echt <span class="op">=</span> (<span class="op">-</span>ergebnis.fun) <span class="op">-</span> db_basis <span class="cf">if</span> ergebnis.success <span class="cf">else</span> <span class="fl">0.0</span></span>
<span id="cb5-183"><a href="#cb5-183" aria-hidden="true" tabindex="-1"></a> linear <span class="op">=</span> preise[name] <span class="op">*</span> <span class="bu">abs</span>(delta)</span>
<span id="cb5-184"><a href="#cb5-184" aria-hidden="true" tabindex="-1"></a> wirkung[name] <span class="op">=</span> echt</span>
<span id="cb5-185"><a href="#cb5-185" 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;24}</span><span class="ss"> </span><span class="sc">{</span>preise[name]<span class="sc">:&gt;9.2f}</span><span class="ss"> </span><span class="sc">{</span>delta<span class="sc">:&gt;+7.0f}</span><span class="ss"> &quot;</span></span>
<span id="cb5-186"><a href="#cb5-186" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>linear<span class="sc">:&gt;13.2f}</span><span class="ss"> </span><span class="sc">{</span>echt<span class="sc">:&gt;9.2f}</span><span class="ss"> </span><span class="sc">{</span>echt <span class="op">-</span> linear<span class="sc">:&gt;+9.2f}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-187"><a href="#cb5-187" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-188"><a href="#cb5-188" aria-hidden="true" tabindex="-1"></a> <span class="co"># Wo endet der Gueltigkeitsbereich? Schrittweise nachfahren.</span></span>
<span id="cb5-189"><a href="#cb5-189" aria-hidden="true" tabindex="-1"></a> abweichler <span class="op">=</span> <span class="bu">max</span>(STELLSCHRAUBEN,</span>
<span id="cb5-190"><a href="#cb5-190" aria-hidden="true" tabindex="-1"></a> key<span class="op">=</span><span class="kw">lambda</span> n: preise[n] <span class="op">*</span> <span class="bu">abs</span>(STELLSCHRAUBEN[n][<span class="dv">0</span>]) <span class="op">-</span> wirkung[n])</span>
<span id="cb5-191"><a href="#cb5-191" aria-hidden="true" tabindex="-1"></a> delta_max <span class="op">=</span> STELLSCHRAUBEN[abweichler][<span class="dv">0</span>]</span>
<span id="cb5-192"><a href="#cb5-192" aria-hidden="true" tabindex="-1"></a> grenze_gueltig <span class="op">=</span> <span class="fl">0.0</span></span>
<span id="cb5-193"><a href="#cb5-193" aria-hidden="true" tabindex="-1"></a> schritt <span class="op">=</span> delta_max <span class="op">/</span> <span class="fl">60.0</span></span>
<span id="cb5-194"><a href="#cb5-194" aria-hidden="true" tabindex="-1"></a> vorher <span class="op">=</span> db_basis</span>
<span id="cb5-195"><a href="#cb5-195" 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">1</span>, <span class="dv">61</span>):</span>
<span id="cb5-196"><a href="#cb5-196" aria-hidden="true" tabindex="-1"></a> d <span class="op">=</span> schritt <span class="op">*</span> i</span>
<span id="cb5-197"><a href="#cb5-197" aria-hidden="true" tabindex="-1"></a> e <span class="op">=</span> loese(geaendert(abweichler, d))</span>
<span id="cb5-198"><a href="#cb5-198" aria-hidden="true" tabindex="-1"></a> db <span class="op">=</span> <span class="op">-</span>e.fun <span class="cf">if</span> e.success <span class="cf">else</span> vorher</span>
<span id="cb5-199"><a href="#cb5-199" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> db <span class="op">-</span> vorher <span class="op">&gt;</span> <span class="fl">1e-9</span>:</span>
<span id="cb5-200"><a href="#cb5-200" aria-hidden="true" tabindex="-1"></a> grenze_gueltig <span class="op">=</span> d</span>
<span id="cb5-201"><a href="#cb5-201" aria-hidden="true" tabindex="-1"></a> vorher <span class="op">=</span> db</span>
<span id="cb5-202"><a href="#cb5-202" 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"> &#39;</span><span class="sc">{</span>abweichler<span class="sc">}</span><span class="ss">&#39;: hochgerechnet &quot;</span></span>
<span id="cb5-203"><a href="#cb5-203" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>preise[abweichler] <span class="op">*</span> <span class="bu">abs</span>(delta_max)<span class="sc">:.2f}</span><span class="ss"> EUR, tatsaechlich &quot;</span></span>
<span id="cb5-204"><a href="#cb5-204" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>wirkung[abweichler]<span class="sc">:.2f}</span><span class="ss"> EUR.&quot;</span>)</span>
<span id="cb5-205"><a href="#cb5-205" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Der Preis von </span><span class="sc">{</span>preise[abweichler]<span class="sc">:.2f}</span><span class="ss"> EUR gilt nur bis &quot;</span></span>
<span id="cb5-206"><a href="#cb5-206" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;+</span><span class="sc">{</span>grenze_gueltig<span class="sc">:.0f}</span><span class="ss"> Stunden. Jede weitere Stunde&quot;</span>)</span>
<span id="cb5-207"><a href="#cb5-207" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; dieser Sonderschicht bringt exakt nichts - der Engpass ist dann ein anderer.&quot;</span>)</span>
<span id="cb5-208"><a href="#cb5-208" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-209"><a href="#cb5-209" aria-hidden="true" tabindex="-1"></a> nach_preis <span class="op">=</span> <span class="bu">sorted</span>(STELLSCHRAUBEN, key<span class="op">=</span><span class="kw">lambda</span> n: <span class="op">-</span>preise[n])[<span class="dv">0</span>]</span>
<span id="cb5-210"><a href="#cb5-210" aria-hidden="true" tabindex="-1"></a> nach_wirkung <span class="op">=</span> <span class="bu">max</span>(wirkung, key<span class="op">=</span>wirkung.get)</span>
<span id="cb5-211"><a href="#cb5-211" 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"> Und die Ranglisten drehen sich: Nach Schattenpreis fuehrt&quot;</span>)</span>
<span id="cb5-212"><a href="#cb5-212" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; &#39;</span><span class="sc">{</span>nach_preis<span class="sc">}</span><span class="ss">&#39; (</span><span class="sc">{</span>preise[nach_preis]<span class="sc">:.2f}</span><span class="ss"> EUR je Einheit),&quot;</span>)</span>
<span id="cb5-213"><a href="#cb5-213" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; nach tatsaechlicher Wirkung &#39;</span><span class="sc">{</span>nach_wirkung<span class="sc">}</span><span class="ss">&#39; &quot;</span></span>
<span id="cb5-214"><a href="#cb5-214" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span>wirkung[nach_wirkung]<span class="sc">:.2f}</span><span class="ss"> EUR).&quot;</span>)</span>
<span id="cb5-215"><a href="#cb5-215" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Der Schattenpreis sagt, was eine Einheit wert ist - nicht, wie viele&quot;</span>)</span>
<span id="cb5-216"><a href="#cb5-216" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Einheiten zu haben sind.&quot;</span>)</span>
<span id="cb5-217"><a href="#cb5-217" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-218"><a href="#cb5-218" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- 3. Der Wunsch aus dem Vertrieb ------------------------------------</span></span>
<span id="cb5-219"><a href="#cb5-219" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-220"><a href="#cb5-220" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; (3) Was-waere-wenn: mindestens </span><span class="sc">{</span>WUNSCH_MENGE<span class="sc">:.0f}</span><span class="ss"> &quot;</span></span>
<span id="cb5-221"><a href="#cb5-221" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>WUNSCH_PRODUKT<span class="sc">}</span><span class="ss"> statt </span><span class="sc">{</span>plan[WUNSCH_PRODUKT]<span class="sc">:.1f}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-222"><a href="#cb5-222" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-223"><a href="#cb5-223" aria-hidden="true" tabindex="-1"></a> wunsch <span class="op">=</span> (<span class="ss">f&quot;Wunsch Vertrieb </span><span class="sc">{</span>WUNSCH_PRODUKT<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb5-224"><a href="#cb5-224" aria-hidden="true" tabindex="-1"></a> [<span class="dv">1</span> <span class="cf">if</span> p <span class="op">==</span> WUNSCH_PRODUKT <span class="cf">else</span> <span class="dv">0</span> <span class="cf">for</span> p <span class="kw">in</span> PRODUKTE], <span class="st">&quot;&gt;=&quot;</span>, WUNSCH_MENGE)</span>
<span id="cb5-225"><a href="#cb5-225" aria-hidden="true" tabindex="-1"></a> mit_wunsch <span class="op">=</span> loese(BEDINGUNGEN <span class="op">+</span> [wunsch])</span>
<span id="cb5-226"><a href="#cb5-226" aria-hidden="true" tabindex="-1"></a> schuldige: <span class="bu">list</span>[<span class="bu">str</span>] <span class="op">=</span> []</span>
<span id="cb5-227"><a href="#cb5-227" aria-hidden="true" tabindex="-1"></a> engpass <span class="op">=</span> noetig <span class="op">=</span> <span class="va">None</span></span>
<span id="cb5-228"><a href="#cb5-228" aria-hidden="true" tabindex="-1"></a> wunsch_gratis <span class="op">=</span> <span class="va">False</span></span>
<span id="cb5-229"><a href="#cb5-229" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-230"><a href="#cb5-230" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> mit_wunsch.success:</span>
<span id="cb5-231"><a href="#cb5-231" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Erfuellbar, kostet </span><span class="sc">{</span>euro(db_basis <span class="op">-</span> (<span class="op">-</span>mit_wunsch.fun))<span class="sc">}</span><span class="ss">.&quot;</span>)</span>
<span id="cb5-232"><a href="#cb5-232" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb5-233"><a href="#cb5-233" aria-hidden="true" tabindex="-1"></a> schuldige <span class="op">=</span> blockierer(BEDINGUNGEN, wunsch)</span>
<span id="cb5-234"><a href="#cb5-234" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Der Wunsch ist nicht teuer - er ist UNMOEGLICH.</span><span class="ch">\n</span><span class="st">&quot;</span>)</span>
<span id="cb5-235"><a href="#cb5-235" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Blockiert wird er von </span><span class="sc">{</span><span class="bu">len</span>(schuldige)<span class="sc">}</span><span class="ss"> Bedingungen gemeinsam. &quot;</span></span>
<span id="cb5-236"><a href="#cb5-236" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Faellt eine davon&quot;</span>)</span>
<span id="cb5-237"><a href="#cb5-237" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;weg, ist er erfuellbar; keine allein ist &#39;der&#39; Grund:&quot;</span>)</span>
<span id="cb5-238"><a href="#cb5-238" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> name <span class="kw">in</span> schuldige:</span>
<span id="cb5-239"><a href="#cb5-239" aria-hidden="true" tabindex="-1"></a> e <span class="op">=</span> loese([c <span class="cf">for</span> c <span class="kw">in</span> BEDINGUNGEN <span class="cf">if</span> c[<span class="dv">0</span>] <span class="op">!=</span> name] <span class="op">+</span> [wunsch])</span>
<span id="cb5-240"><a href="#cb5-240" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; ohne &#39;</span><span class="sc">{</span>name<span class="sc">}</span><span class="ss">&#39;: moeglich, Deckungsbeitrag </span><span class="sc">{</span>euro(<span class="op">-</span>e.fun)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-241"><a href="#cb5-241" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-242"><a href="#cb5-242" aria-hidden="true" tabindex="-1"></a> engpass <span class="op">=</span> <span class="bu">next</span>((n <span class="cf">for</span> n <span class="kw">in</span> schuldige <span class="cf">if</span> n <span class="kw">in</span> STELLSCHRAUBEN), schuldige[<span class="dv">0</span>])</span>
<span id="cb5-243"><a href="#cb5-243" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> schritt <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">1</span>, <span class="dv">61</span>):</span>
<span id="cb5-244"><a href="#cb5-244" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> loese(geaendert(engpass, schritt) <span class="op">+</span> [wunsch]).success:</span>
<span id="cb5-245"><a href="#cb5-245" aria-hidden="true" tabindex="-1"></a> noetig <span class="op">=</span> schritt</span>
<span id="cb5-246"><a href="#cb5-246" aria-hidden="true" tabindex="-1"></a> <span class="cf">break</span></span>
<span id="cb5-247"><a href="#cb5-247" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> noetig <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span>
<span id="cb5-248"><a href="#cb5-248" aria-hidden="true" tabindex="-1"></a> e <span class="op">=</span> loese(geaendert(engpass, noetig) <span class="op">+</span> [wunsch])</span>
<span id="cb5-249"><a href="#cb5-249" aria-hidden="true" tabindex="-1"></a> ohne <span class="op">=</span> loese(geaendert(engpass, noetig))</span>
<span id="cb5-250"><a href="#cb5-250" aria-hidden="true" tabindex="-1"></a> wunsch_gratis <span class="op">=</span> <span class="bu">abs</span>((<span class="op">-</span>e.fun) <span class="op">-</span> (<span class="op">-</span>ohne.fun)) <span class="op">&lt;</span> <span class="fl">1e-6</span></span>
<span id="cb5-251"><a href="#cb5-251" 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"> Mit +</span><span class="sc">{</span>noetig<span class="sc">}</span><span class="ss"> bei &#39;</span><span class="sc">{</span>engpass<span class="sc">}</span><span class="ss">&#39; wird der Wunsch erfuellbar:&quot;</span>)</span>
<span id="cb5-252"><a href="#cb5-252" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Deckungsbeitrag </span><span class="sc">{</span>euro(<span class="op">-</span>e.fun)<span class="sc">}</span><span class="ss"> &quot;</span></span>
<span id="cb5-253"><a href="#cb5-253" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span><span class="op">-</span>e<span class="sc">.</span>fun <span class="op">-</span> db_basis<span class="sc">:+,.2f}</span><span class="ss"> gegenueber heute).&quot;</span>)</span>
<span id="cb5-254"><a href="#cb5-254" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> wunsch_gratis:</span>
<span id="cb5-255"><a href="#cb5-255" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Und ohne den Wunsch ergaebe dieselbe Lockerung &quot;</span></span>
<span id="cb5-256"><a href="#cb5-256" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>euro(<span class="op">-</span>ohne.fun)<span class="sc">}</span><span class="ss"> bei&quot;</span>)</span>
<span id="cb5-257"><a href="#cb5-257" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>ohne<span class="sc">.</span>x[PRODUKTE.index(WUNSCH_PRODUKT)]<span class="sc">:.0f}</span><span class="ss"> &quot;</span></span>
<span id="cb5-258"><a href="#cb5-258" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>WUNSCH_PRODUKT<span class="sc">}</span><span class="ss"> - der Wunsch kostet also NICHTS.&quot;</span>)</span>
<span id="cb5-259"><a href="#cb5-259" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Er war nie das Problem. Der Engpass war es.&quot;</span>)</span>
<span id="cb5-260"><a href="#cb5-260" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-261"><a href="#cb5-261" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- 4. Der Bericht ----------------------------------------------------</span></span>
<span id="cb5-262"><a href="#cb5-262" aria-hidden="true" tabindex="-1"></a> <span class="co"># Aus denselben Zahlen, die oben in Tabellen stehen, wird hier Prosa. Kein</span></span>
<span id="cb5-263"><a href="#cb5-263" aria-hidden="true" tabindex="-1"></a> <span class="co"># Satz enthaelt eine Zahl, die nicht vorher gerechnet wurde - das ist der</span></span>
<span id="cb5-264"><a href="#cb5-264" aria-hidden="true" tabindex="-1"></a> <span class="co"># ganze Trick und zugleich die Bedingung dafuer, dass der Bericht nicht</span></span>
<span id="cb5-265"><a href="#cb5-265" aria-hidden="true" tabindex="-1"></a> <span class="co"># irgendwann leise falsch wird.</span></span>
<span id="cb5-266"><a href="#cb5-266" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-267"><a href="#cb5-267" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; (4) Und so steht es in der Vorlage fuer die Abteilungsleiterrunde&quot;</span>)</span>
<span id="cb5-268"><a href="#cb5-268" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb5-269"><a href="#cb5-269" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-270"><a href="#cb5-270" aria-hidden="true" tabindex="-1"></a> teuerste <span class="op">=</span> <span class="bu">sorted</span>(((n, d) <span class="cf">for</span> n, d <span class="kw">in</span> bindend <span class="cf">if</span> d <span class="op">&gt;</span> <span class="dv">0</span>), key<span class="op">=</span><span class="kw">lambda</span> t: <span class="op">-</span>t[<span class="dv">1</span>])</span>
<span id="cb5-271"><a href="#cb5-271" aria-hidden="true" tabindex="-1"></a> gross <span class="op">=</span> STELLSCHRAUBEN[nach_wirkung][<span class="dv">1</span>]</span>
<span id="cb5-272"><a href="#cb5-272" aria-hidden="true" tabindex="-1"></a> klein <span class="op">=</span> STELLSCHRAUBEN[abweichler][<span class="dv">1</span>]</span>
<span id="cb5-273"><a href="#cb5-273" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-274"><a href="#cb5-274" aria-hidden="true" tabindex="-1"></a> absaetze <span class="op">=</span> [</span>
<span id="cb5-275"><a href="#cb5-275" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Der Plan bringt </span><span class="sc">{</span>euro(db_basis)<span class="sc">}</span><span class="ss"> Deckungsbeitrag. Begrenzt wird er von &quot;</span></span>
<span id="cb5-276"><a href="#cb5-276" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">len</span>(bindend)<span class="sc">}</span><span class="ss"> Bedingungen, von denen </span><span class="sc">{</span><span class="bu">len</span>(teuerste)<span class="sc">}</span><span class="ss"> tatsaechlich Geld &quot;</span></span>
<span id="cb5-277"><a href="#cb5-277" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;kosten; </span><span class="ch">\&quot;</span><span class="sc">{</span>ohne_preis[<span class="dv">0</span>]<span class="sc">}</span><span class="ch">\&quot;</span><span class="ss"> beruehrt den Plan, ohne ihn zu verteuern.&quot;</span>,</span>
<span id="cb5-278"><a href="#cb5-278" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-279"><a href="#cb5-279" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Teuerste Bindung ist </span><span class="ch">\&quot;</span><span class="sc">{</span>teuerste[<span class="dv">0</span>][<span class="dv">0</span>]<span class="sc">}</span><span class="ch">\&quot;</span><span class="ss"> mit &quot;</span></span>
<span id="cb5-280"><a href="#cb5-280" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>teuerste[<span class="dv">0</span>][<span class="dv">1</span>]<span class="sc">:.2f}</span><span class="ss"> EUR je Einheit, gefolgt von </span><span class="ch">\&quot;</span><span class="sc">{</span>teuerste[<span class="dv">1</span>][<span class="dv">0</span>]<span class="sc">}</span><span class="ch">\&quot;</span><span class="ss"> &quot;</span></span>
<span id="cb5-281"><a href="#cb5-281" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;mit </span><span class="sc">{</span>teuerste[<span class="dv">1</span>][<span class="dv">1</span>]<span class="sc">:.2f}</span><span class="ss"> EUR.&quot;</span>,</span>
<span id="cb5-282"><a href="#cb5-282" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-283"><a href="#cb5-283" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Die groesste einzelne Verbesserung bringt </span><span class="sc">{</span>gross<span class="sc">}</span><span class="ss">: &quot;</span></span>
<span id="cb5-284"><a href="#cb5-284" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>euro(wirkung[nach_wirkung])<span class="sc">}</span><span class="ss"> mehr Deckungsbeitrag. </span><span class="sc">{</span>klein[<span class="dv">0</span>]<span class="sc">.</span>upper()<span class="sc">}{</span>klein[<span class="dv">1</span>:]<span class="sc">}</span><span class="ss"> &quot;</span></span>
<span id="cb5-285"><a href="#cb5-285" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;dagegen bringt nur </span><span class="sc">{</span>euro(wirkung[abweichler])<span class="sc">}</span><span class="ss"> statt der rechnerischen &quot;</span></span>
<span id="cb5-286"><a href="#cb5-286" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>euro(preise[abweichler] <span class="op">*</span> <span class="bu">abs</span>(delta_max))<span class="sc">}</span><span class="ss"> - ab +</span><span class="sc">{</span>grenze_gueltig<span class="sc">:.0f}</span><span class="ss"> &quot;</span></span>
<span id="cb5-287"><a href="#cb5-287" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Stunden begrenzt uns etwas anderes. Wer die ganze Schicht bezahlt, &quot;</span></span>
<span id="cb5-288"><a href="#cb5-288" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;bezahlt die zweite Haelfte umsonst.&quot;</span>,</span>
<span id="cb5-289"><a href="#cb5-289" aria-hidden="true" tabindex="-1"></a> ]</span>
<span id="cb5-290"><a href="#cb5-290" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-291"><a href="#cb5-291" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> schuldige <span class="kw">and</span> noetig <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span>
<span id="cb5-292"><a href="#cb5-292" aria-hidden="true" tabindex="-1"></a> namen <span class="op">=</span> <span class="st">&quot;, &quot;</span>.join(<span class="ss">f&#39;&quot;</span><span class="sc">{</span>s<span class="sc">}</span><span class="ss">&quot;&#39;</span> <span class="cf">for</span> s <span class="kw">in</span> schuldige)</span>
<span id="cb5-293"><a href="#cb5-293" aria-hidden="true" tabindex="-1"></a> satz <span class="op">=</span> (<span class="ss">f&quot;Zum Wunsch des Vertriebs (</span><span class="sc">{</span>WUNSCH_MENGE<span class="sc">:.0f}</span><span class="ss"> </span><span class="sc">{</span>WUNSCH_PRODUKT<span class="sc">}</span><span class="ss"> statt &quot;</span></span>
<span id="cb5-294"><a href="#cb5-294" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>plan[WUNSCH_PRODUKT]<span class="sc">:.1f}</span><span class="ss">): Er ist mit der heutigen Ausstattung &quot;</span></span>
<span id="cb5-295"><a href="#cb5-295" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;nicht erfuellbar - nicht aus Kostengruenden, sondern weil </span><span class="sc">{</span>namen<span class="sc">}</span><span class="ss"> &quot;</span></span>
<span id="cb5-296"><a href="#cb5-296" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;ihn gemeinsam blockieren. Mit +</span><span class="sc">{</span>noetig<span class="sc">}</span><span class="ss"> Stunden bei &quot;</span></span>
<span id="cb5-297"><a href="#cb5-297" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&#39;&quot;</span><span class="sc">{</span>engpass<span class="sc">}</span><span class="ss">&quot; wird er moeglich&#39;</span>)</span>
<span id="cb5-298"><a href="#cb5-298" aria-hidden="true" tabindex="-1"></a> satz <span class="op">+=</span> (<span class="st">&quot;, und er kostet dann nichts: Der Plan waehlt diese Menge von &quot;</span></span>
<span id="cb5-299"><a href="#cb5-299" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;sich aus.&quot;</span> <span class="cf">if</span> wunsch_gratis <span class="cf">else</span> <span class="st">&quot;.&quot;</span>)</span>
<span id="cb5-300"><a href="#cb5-300" aria-hidden="true" tabindex="-1"></a> absaetze.append(satz)</span>
<span id="cb5-301"><a href="#cb5-301" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-302"><a href="#cb5-302" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> absatz <span class="kw">in</span> absaetze:</span>
<span id="cb5-303"><a href="#cb5-303" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb5-304"><a href="#cb5-304" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(textwrap.fill(absatz, width<span class="op">=</span><span class="dv">76</span>))</span>
<span id="cb5-305"><a href="#cb5-305" 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></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>==============================================================================
CONSTRAINT ATTRIBUTION: Was kostet uns welche Bedingung?
==============================================================================
Status: Optimization terminated successfully.
Deckungsbeitrag des Plans: 3,930.00 EUR
Rahmen: 47.5 Gehaeuse: 30.0 Deckel: 97.5 Traeger: 25.0 Halter: 0.0
==============================================================================
(1) Welche Bedingungen binden - und was kostet die naechste Einheit?
==============================================================================
Bedingung genutzt Grenze Schlupf Schattenpreis
------------------------------------------------------------------------------
Kapazitaet Montage 400.0 400 0.0 9.00 &lt;-- bindend
Kapazitaet Lackieren 210.0 210 0.0 4.00 &lt;-- bindend
Kapazitaet Pruefung 200.0 300 100.0 0.00
Liefervertrag Rahmen 47.5 40 7.5 0.00
Liefervertrag Gehaeuse 30.0 30 0.0 2.00 &lt;-- bindend
Liefervertrag Deckel 97.5 20 77.5 0.00
Liefervertrag Traeger 25.0 25 0.0 18.00 &lt;-- bindend
Marktgrenze Rahmen 47.5 120 72.5 0.00
Marktgrenze Gehaeuse 30.0 90 60.0 0.00
Marktgrenze Deckel 97.5 150 52.5 0.00
Marktgrenze Halter 0.0 200 200.0 0.00
Mindestumsatz 10800.0 10000 800.0 0.00
Sortimentsbreite 122.5 100 22.5 0.00
Lackierbudget Schicht 2 55.0 55 0.0 0.00 &lt;-- bindend
5 Bedingungen binden. Aber: Lackierbudget Schicht 2 bindet
mit einem Schattenpreis von 0,00 EUR - sie beruehrt den Plan, ohne ihn
zu verteuern. Bindend und kostenrelevant sind zwei verschiedene Dinge.
==============================================================================
(2) Was der Schattenpreis verschweigt
==============================================================================
Der Schattenpreis gilt fuer die NAECHSTE Einheit. Ein Planer kauft aber
keine Einheit, sondern eine ganze Schicht. Beides gegenuebergestellt:
Stellschraube Preis Schritt hochgerechnet gemessen Abw.
------------------------------------------------------------------------------
Kapazitaet Montage 9.00 +50 450.00 450.00 +0.00
Kapazitaet Lackieren 4.00 +30 120.00 60.00 -60.00
Kapazitaet Pruefung 0.00 +50 0.00 0.00 +0.00
Lackierbudget Schicht 2 0.00 +10 0.00 0.00 +0.00
Liefervertrag Traeger 18.00 -10 180.00 180.00 +0.00
Liefervertrag Gehaeuse 2.00 -10 20.00 18.00 -2.00
&#39;Kapazitaet Lackieren&#39;: hochgerechnet 120.00 EUR, tatsaechlich 60.00 EUR.
Der Preis von 4.00 EUR gilt nur bis +15 Stunden. Jede weitere Stunde
dieser Sonderschicht bringt exakt nichts - der Engpass ist dann ein anderer.
Und die Ranglisten drehen sich: Nach Schattenpreis fuehrt
&#39;Liefervertrag Traeger&#39; (18.00 EUR je Einheit),
nach tatsaechlicher Wirkung &#39;Kapazitaet Montage&#39; (450.00 EUR).
Der Schattenpreis sagt, was eine Einheit wert ist - nicht, wie viele
Einheiten zu haben sind.
==============================================================================
(3) Was-waere-wenn: mindestens 55 Rahmen statt 47.5
==============================================================================
Der Wunsch ist nicht teuer - er ist UNMOEGLICH.
Blockiert wird er von 3 Bedingungen gemeinsam. Faellt eine davon
weg, ist er erfuellbar; keine allein ist &#39;der&#39; Grund:
ohne &#39;Kapazitaet Lackieren&#39;: moeglich, Deckungsbeitrag 3,990.00 EUR
ohne &#39;Liefervertrag Gehaeuse&#39;: moeglich, Deckungsbeitrag 3,948.00 EUR
ohne &#39;Liefervertrag Traeger&#39;: moeglich, Deckungsbeitrag 4,320.00 EUR
Mit +15 bei &#39;Kapazitaet Lackieren&#39; wird der Wunsch erfuellbar:
Deckungsbeitrag 3,990.00 EUR (+60.00 gegenueber heute).
Und ohne den Wunsch ergaebe dieselbe Lockerung 3,990.00 EUR bei
55 Rahmen - der Wunsch kostet also NICHTS.
Er war nie das Problem. Der Engpass war es.
==============================================================================
(4) Und so steht es in der Vorlage fuer die Abteilungsleiterrunde
==============================================================================
Der Plan bringt 3,930.00 EUR Deckungsbeitrag. Begrenzt wird er von 5
Bedingungen, von denen 4 tatsaechlich Geld kosten; &quot;Lackierbudget Schicht 2&quot;
beruehrt den Plan, ohne ihn zu verteuern.
Teuerste Bindung ist &quot;Liefervertrag Traeger&quot; mit 18.00 EUR je Einheit,
gefolgt von &quot;Kapazitaet Montage&quot; mit 9.00 EUR.
Die groesste einzelne Verbesserung bringt eine Sonderschicht Montage: 450.00
EUR mehr Deckungsbeitrag. Eine Sonderschicht Lackieren dagegen bringt nur
60.00 EUR statt der rechnerischen 120.00 EUR - ab +15 Stunden begrenzt uns
etwas anderes. Wer die ganze Schicht bezahlt, bezahlt die zweite Haelfte
umsonst.
Zum Wunsch des Vertriebs (55 Rahmen statt 47.5): Er ist mit der heutigen
Ausstattung nicht erfuellbar - nicht aus Kostengruenden, sondern weil
&quot;Kapazitaet Lackieren&quot;, &quot;Liefervertrag Gehaeuse&quot;, &quot;Liefervertrag Traeger&quot;
ihn gemeinsam blockieren. Mit +15 Stunden bei &quot;Kapazitaet Lackieren&quot; wird er
moeglich, und er kostet dann nichts: Der Plan waehlt diese Menge von sich
aus.
==============================================================================</code></pre>
<h3 id="drei-befunde-und-keiner-steht-im-solverergebnis">Drei Befunde, und keiner steht im Solverergebnis</h3>
<p><strong>Erstens: bindend heißt nicht teuer.</strong> Fünf Bedingungen binden, aber „Lackierbudget Schicht 2“ tut es mit einem Schattenpreis von <strong>0,00 €</strong>. Sie berührt den Plan, ohne ihn zu verteuern — wer sie nachverhandelt, gewinnt nichts. Das ist derselbe Unterschied, den <code>Erklaerbarkeit.py</code> oben an einer einzelnen Zuweisung zeigt, hier im Dualwert eines LP.</p>
<p><strong>Zweitens: Der Schattenpreis ist eine Momentaufnahme.</strong> Er gilt für die <strong>nächste</strong> Einheit. Ein Planer kauft aber keine Einheit, sondern eine Schicht. Für die Lackiererei verspricht die Hochrechnung <span class="math inline">4{,}00 \cdot 30 = 120</span> €; gemessen kommen <strong>60 €</strong>. Der Preis von 4,00 € gilt exakt bis <strong>+15 Stunden</strong>, danach begrenzt etwas anderes — <strong>die zweite Hälfte der Sonderschicht wäre bezahlt und wirkungslos.</strong></p>
<p>Und die Ranglisten drehen sich dabei um: Nach Schattenpreis führt der Trägervertrag mit 18,00 € je Stück, nach tatsächlicher Wirkung die Montage-Sonderschicht mit 450 €. Der Schattenpreis sagt, was <strong>eine</strong> Einheit wert ist — nicht, wie viele Einheiten zu haben sind. Für die Frage „welchen Hebel ziehen wir?“ ist das der entscheidende Unterschied.</p>
<p><strong>Drittens: Ein Wunsch kann unmöglich statt teuer sein.</strong> Der Vertrieb hätte gern 55 Rahmen statt 47,5. Das Modell antwortet nicht mit einem Preis, sondern mit <code>INFEASIBLE</code> — und drei Bedingungen blockieren den Wunsch <strong>gemeinsam</strong>: Fällt eine davon weg, ist er erfüllbar, keine allein ist „der“ Grund. Genau die Struktur, die <a href="anhang-fehlerdiagnose.html#anhang-fehlerdiagnose">Anhang C</a> für den unlösbaren Fall beschreibt, hier auf einen Wunsch angewandt.</p>
<p>Die Pointe steht am Ende: Mit +15 Stunden Lackierkapazität wird der Wunsch erfüllbar — und kostet dann <strong>nichts</strong>. Der Plan wählt diese Menge von sich aus, sobald der Engpass weg ist. Der Konflikt bestand nie zwischen Vertrieb und Produktion. Er bestand zwischen dem Vertrieb und der Lackiererei, und niemand wusste das.</p>
<blockquote>
<p><strong>🎯 Die drei Fragen, jetzt auf Modellebene</strong> Die Faustregel oben stellt sie für eine einzelne Zuweisung. Constraint Attribution stellt dieselben drei für den <strong>ganzen Plan</strong>: 1. <strong>Warum dieser Zielwert?</strong> — welche Bedingungen binden, und welche davon kosten Geld. 2. <strong>Warum nicht besser?</strong> — was eine realistische Lockerung wirklich bringt, gemessen statt hochgerechnet. 3. <strong>Was ist mit meinem Wunsch?</strong> — erfüllbar und zu welchem Preis, oder unmöglich und warum.</p>
</blockquote>
<h3 id="vom-zahlenwerk-zur-vorlage">Vom Zahlenwerk zur Vorlage</h3>
<p>Alle drei Antworten stehen bis hierhin in Tabellen. Tabellen liest niemand in einer Abteilungsleiterrunde. Teil (4) des Programms baut aus <strong>denselben</strong> Zahlen einen Text — und das ist weniger trivial, als es aussieht:</p>
<ul>
<li><strong>Kein Satz enthält eine Zahl, die nicht vorher gerechnet wurde.</strong> Auch nicht die Rangfolge, auch nicht die Namen der blockierenden Bedingungen. Sobald sich das Modell ändert, ändert sich der Bericht mit — er kann nicht leise falsch werden.</li>
<li><strong>Der sprechende Name jeder Bedingung ist die Voraussetzung dafür.</strong> <code>A_ub[7]</code> ergibt keinen Satz. Deshalb tragen die Bedingungen in beiden Programmen einen Namen, und deshalb ist das keine Kosmetik.</li>
</ul>
<blockquote>
<p><strong>⚠️ Was ein solcher Bericht nicht leistet</strong> Er erklärt das <strong>Modell</strong>, nicht die <strong>Wirklichkeit</strong>. „Liefervertrag Träger kostet 18 € je Stück“ heißt: <em>in diesem Modell, mit diesen Deckungsbeiträgen</em>. Ist ein Deckungsbeitrag falsch geschätzt, ist der Bericht überzeugend <strong>und</strong> falsch — die gefährlichste Kombination. Ein generierter Text macht ein Modell nicht richtiger, nur schwerer anzuzweifeln. Deshalb gehört zu jedem solchen Bericht die Angabe, worauf er beruht.</p>
</blockquote>
<hr />
<h2 id="sec:praxisfallen-architektur-einer-produktionsreifen-or-plattform">22.5 Architektur einer produktionsreifen OR-Plattform</h2>
<figure>
<img src="bilder_04/kap15_or_plattform.svg" alt="Abb. 22.2: Architektur einer produktionsreifen OR-Plattform" />
<figcaption aria-hidden="true">Abb. 22.2: Architektur einer produktionsreifen OR-Plattform</figcaption>
</figure>
<p>In einer professionellen Umgebung ist der Optimierer kein Skript, sondern ein <strong>versionierter, zustandsloser Dienst</strong>:</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Schicht</th>
<th>Aufgabe</th>
<th>Typische Werkzeuge</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Quellsysteme</strong></td>
<td>ERP, Personalplanung, Marktdaten-Feeds</td>
<td>REST, Datenbanken, Broker-APIs</td>
</tr>
<tr class="even">
<td><strong>Datenqualität</strong></td>
<td>Validierung, Plausibilität, unveränderlicher Snapshot</td>
<td>Pydantic, Pandera, Great Expectations</td>
</tr>
<tr class="odd">
<td><strong>Optimierungs-Worker</strong></td>
<td>Modell bauen, lösen, Timeout, Fallback</td>
<td>Celery/Redis, OR-Tools, HiGHS, CVXPY</td>
</tr>
<tr class="even">
<td><strong>Disposition</strong></td>
<td>Ergebnis prüfen, ändern, freigeben</td>
<td>Web-UI, Dashboard, Freigabeworkflow</td>
</tr>
<tr class="odd">
<td><strong>Ausführung</strong></td>
<td>Kalender-Sync, Benachrichtigung, Order-Routing</td>
<td>Kalender-APIs, Messaging, Broker</td>
</tr>
<tr class="even">
<td><strong>Betrieb</strong></td>
<td>Monitoring, Audit-Trail, Alarme</td>
<td>Logging, Metriken, Versionierung</td>
</tr>
</tbody>
</table>
<p><strong>Fünf Prinzipien, die sich in der Praxis bewährt haben:</strong></p>
<ol type="1">
<li><strong>Snapshot-Prinzip.</strong> Jeder Optimierungslauf arbeitet auf einem unveränderlichen Datenschnappschuss mit eigener ID. Nur so ist ein Ergebnis später reproduzierbar — und im Streitfall belegbar.</li>
<li><strong>Zustandslosigkeit.</strong> Der Worker hält keinen Zustand; Eingabe und Ausgabe sind Daten. Das macht Skalierung und Wiederholung trivial.</li>
<li><strong>Mensch in der Schleife.</strong> Der Optimierer <strong>schlägt vor</strong>, ein Mensch <strong>entscheidet</strong> — zumindest in der Einführungsphase. Das baut Vertrauen auf und fängt Modellfehler ab.</li>
<li><strong>Fallback-Strategie.</strong> Was passiert, wenn der Solver kein Ergebnis liefert? Antwort: letzter gültiger Plan, regelbasierte Notlösung, Alarm. Nie: nichts.</li>
<li><strong>Versionierung.</strong> Modellversion, Parametersatz und Solver-Version gehören ins Protokoll jedes Laufs. Ohne das lässt sich nicht klären, warum das Ergebnis von letzter Woche anders aussah.</li>
</ol>
<blockquote>
<p><strong>⚠️ Der häufigste Projektfehler</strong> Nicht das Modell scheitert, sondern die <strong>Einführung</strong>. Ein technisch überlegener Plan, den die Disponentin nicht versteht und dem sie nicht traut, wird umgangen — sie plant weiter in ihrer Tabelle. Rechnen Sie mindestens so viel Zeit für Erklärbarkeit, Schulung und schrittweise Einführung ein wie für das Modell selbst.</p>
</blockquote>
<hr />
<h2 id="sec:praxisfallen-or-kern">22.6 Der gemeinsame Unterbau: <code>or_kern.py</code></h2>
<p>Die fünf Prinzipien oben beschreiben eine Plattform. Der Weg dorthin beginnt aber viel kleiner — mit der Frage, welche Teile eines Optimierungsprogramms <strong>immer dieselben</strong> sind.</p>
<p>Sehen Sie sich die Programme dieses Buchs an: Jedes liest Daten ein, baut ein Modell, wertet einen Solverstatus aus und prüft das Ergebnis. Nur der mittlere Schritt ist wirklich problemspezifisch. Die anderen drei schreibt man in jedem Projekt neu — und macht dabei jedes Mal dieselben Fehler.</p>
<h3 id="die-vier-schichten">Die vier Schichten</h3>
<p><span class="math display">
\underbrace{\text{Rohdaten}}_{\text{Excel, CSV, ERP}}
\;\longrightarrow\;
\underbrace{\text{Domänenmodell}}_{\text{geprüft, solverfrei}}
\;\longrightarrow\;
\underbrace{\text{Modellbauer}}_{\text{solverabhängig}}
\;\longrightarrow\;
\underbrace{\text{Lösung}}_{\text{DTO, solverfrei}}
</span></p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Schicht</th>
<th>Weiß nichts von …</th>
<th>Gewinn</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Domänenmodell</strong></td>
<td>Solvern, Matrizen, Variablen</td>
<td>Datenfehler schlagen beim <strong>Einlesen</strong> zu, wo man sie noch zuordnen kann</td>
</tr>
<tr class="even">
<td><strong>Modellbauer</strong></td>
<td>Datenherkunft, Berichtsformat</td>
<td>Solverwechsel betrifft genau <strong>eine</strong> Datei</td>
</tr>
<tr class="odd">
<td><strong>Lösung (DTO)</strong></td>
<td>wie gerechnet wurde</td>
<td>Auswertung, Test und Bericht funktionieren für jeden Solver gleich</td>
</tr>
<tr class="even">
<td><strong>Abnahmeprüfung</strong></td>
<td>Modell und Solver</td>
<td>prüft gegen die <strong>Anforderung</strong>, nicht gegen sich selbst</td>
</tr>
</tbody>
</table>
<figure>
<img src="bilder_04/kap_praxisfallen_schichten.svg" alt="Abb. 22.3: Dieselben Schichten als Abhängigkeitsbild. Die Aussage steckt in der Pfeilrichtung: Alles zeigt nach innen auf die Domäne, und die Domäne zeigt auf nichts. Erzeugt von bilder_04/erzeuge_architektur_diagramme.py." />
<figcaption aria-hidden="true">Abb. 22.3: Dieselben Schichten als Abhängigkeitsbild. Die Aussage steckt in der Pfeilrichtung: Alles zeigt nach innen auf die Domäne, und die Domäne zeigt auf nichts. Erzeugt von <code>bilder_04/erzeuge_architektur_diagramme.py</code>.</figcaption>
</figure>
<blockquote>
<p><strong>🎯 Merksatz</strong> Die Trennung lohnt sich nicht wegen des Solverwechsels — den macht man selten. Sie lohnt sich, weil jede Schicht dadurch <strong>einzeln prüfbar</strong> wird. Das Domänenmodell testet man ohne Solver, die Abnahmeprüfung ohne Modell, den Bericht ohne Daten.</p>
</blockquote>
<h3 id="ein-enum-für-fünf-bibliotheken">Ein Enum für fünf Bibliotheken</h3>
<p>Der unscheinbarste, aber wirksamste Teil ist die Statusübersetzung. Dieselbe Aussage heißt in den fünf im Buch verwendeten Bibliotheken:</p>
<table style="width:100%;">
<colgroup>
<col style="width: 16%" />
<col style="width: 16%" />
<col style="width: 16%" />
<col style="width: 16%" />
<col style="width: 16%" />
<col style="width: 16%" />
</colgroup>
<thead>
<tr class="header">
<th>Bedeutung</th>
<th>pywraplp</th>
<th>CP-SAT</th>
<th>HiGHS</th>
<th>SciPy</th>
<th>CVXPY</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>beweisbar optimal</td>
<td><code>Solver.OPTIMAL</code></td>
<td><code>cp_model.OPTIMAL</code></td>
<td><code>"Optimal"</code></td>
<td><code>status == 0</code></td>
<td><code>"optimal"</code></td>
</tr>
<tr class="even">
<td>zulässig, nicht bewiesen</td>
<td><code>Solver.FEASIBLE</code></td>
<td><code>cp_model.FEASIBLE</code></td>
<td><code>"Time limit reached"</code></td>
<td></td>
<td><code>"optimal_inaccurate"</code></td>
</tr>
<tr class="odd">
<td>es gibt keine Lösung</td>
<td><code>Solver.INFEASIBLE</code></td>
<td><code>cp_model.INFEASIBLE</code></td>
<td><code>"Infeasible"</code></td>
<td><code>status == 2</code></td>
<td><code>"infeasible"</code></td>
</tr>
<tr class="even">
<td>Modell ist fehlerhaft</td>
<td><code>Solver.ABNORMAL</code></td>
<td><code>cp_model.MODEL_INVALID</code></td>
<td><code>"Model error"</code></td>
<td><code>status == 4</code></td>
<td></td>
</tr>
</tbody>
</table>
<p>Fünf Schreibweisen für dieselben vier Aussagen. Wer direkt darauf prüft, bindet seinen gesamten Auswertungscode an eine Bibliothek — und muss ihn beim Wechsel überall anfassen.</p>
<blockquote>
<p><strong>⚠️ Eine Besonderheit beim Import</strong></p>
<p><code>or_kern.py</code> importiert von sich aus <strong>keine</strong> Solverbibliothek. Die Statusübersetzer laden ihre Bibliothek erst beim Aufruf. Das ist kein Stilmittel, sondern notwendig: <code>ortools</code> und <code>highspy</code> vertragen sich nicht im selben Prozess (<a href="oekosystem.html#sec:oekosystem-ein-system-vier-programmieransaetze">Abschnitt 3.5</a>). Ein Modul, das beide importierte, wäre mit keinem von beiden benutzbar.</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"># or_kern.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 Praxisfallen: Der gemeinsame Unterbau fuer produktionsreife OR-Modelle.</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">Alle Beispiele dieses Buchs loesen dieselben vier Aufgaben immer wieder:</span></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a><span class="co">Daten einlesen und pruefen, ein Modell bauen, den Solverstatus auswerten, die</span></span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a><span class="co">Loesung gegen die Wirklichkeit kontrollieren. Dieses Modul zieht diese vier</span></span>
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a><span class="co">Aufgaben aus den Einzelprogrammen heraus.</span></span>
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a><span class="co"> Rohdaten (Excel/CSV)</span></span>
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a><span class="co"> -&gt; Domaenenmodell (Pydantic, geprueft)</span></span>
<span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a><span class="co"> -&gt; Modellbauer (solverabhaengig)</span></span>
<span id="cb7-15"><a href="#cb7-15" aria-hidden="true" tabindex="-1"></a><span class="co"> -&gt; Loesung (DTO, solverunabhaengig)</span></span>
<span id="cb7-16"><a href="#cb7-16" aria-hidden="true" tabindex="-1"></a><span class="co"> -&gt; Abnahmepruefung</span></span>
<span id="cb7-17"><a href="#cb7-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-18"><a href="#cb7-18" aria-hidden="true" tabindex="-1"></a><span class="co">Der Sinn dieser Trennung: Die Domaenenschicht weiss nichts von Solvern, die</span></span>
<span id="cb7-19"><a href="#cb7-19" aria-hidden="true" tabindex="-1"></a><span class="co">Loesungsschicht weiss nichts vom Modellaufbau. Wer den Solver wechselt, tauscht</span></span>
<span id="cb7-20"><a href="#cb7-20" aria-hidden="true" tabindex="-1"></a><span class="co">genau EINEN Baustein aus - der Rest bleibt.</span></span>
<span id="cb7-21"><a href="#cb7-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-22"><a href="#cb7-22" aria-hidden="true" tabindex="-1"></a><span class="co">WICHTIG ZUM IMPORT: Dieses Modul importiert von sich aus KEINE Solver-</span></span>
<span id="cb7-23"><a href="#cb7-23" aria-hidden="true" tabindex="-1"></a><span class="co">bibliothek. Der Grund steht im Kapitel Oekosystem: ortools und highspy vertragen sich</span></span>
<span id="cb7-24"><a href="#cb7-24" aria-hidden="true" tabindex="-1"></a><span class="co">nicht im selben Prozess. Die Statusuebersetzer laden ihre Bibliothek erst,</span></span>
<span id="cb7-25"><a href="#cb7-25" aria-hidden="true" tabindex="-1"></a><span class="co">wenn sie aufgerufen werden - so bleibt or_kern.py mit jedem Solver benutzbar.</span></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="co">Benoetigt: pydantic (v2), numpy; optional pandas und openpyxl fuer Excel.</span></span>
<span id="cb7-28"><a href="#cb7-28" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb7-29"><a href="#cb7-29" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-30"><a href="#cb7-30" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb7-31"><a href="#cb7-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-32"><a href="#cb7-32" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> enum <span class="im">import</span> Enum</span>
<span id="cb7-33"><a href="#cb7-33" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> typing <span class="im">import</span> Annotated, Any, Sequence</span>
<span id="cb7-34"><a href="#cb7-34" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-35"><a href="#cb7-35" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb7-36"><a href="#cb7-36" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> pydantic <span class="im">import</span> BaseModel, Field, model_validator</span>
<span id="cb7-37"><a href="#cb7-37" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-38"><a href="#cb7-38" aria-hidden="true" tabindex="-1"></a><span class="co"># Wiederverwendbare Feldtypen. Sie tragen die Pruefung im Typ, nicht im Code -</span></span>
<span id="cb7-39"><a href="#cb7-39" aria-hidden="true" tabindex="-1"></a><span class="co"># damit gilt sie ueberall, wo der Typ verwendet wird.</span></span>
<span id="cb7-40"><a href="#cb7-40" aria-hidden="true" tabindex="-1"></a>PositiveZahl <span class="op">=</span> Annotated[<span class="bu">float</span>, Field(gt<span class="op">=</span><span class="dv">0</span>)]</span>
<span id="cb7-41"><a href="#cb7-41" aria-hidden="true" tabindex="-1"></a>NichtNegativ <span class="op">=</span> Annotated[<span class="bu">float</span>, Field(ge<span class="op">=</span><span class="dv">0</span>)]</span>
<span id="cb7-42"><a href="#cb7-42" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-43"><a href="#cb7-43" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-44"><a href="#cb7-44" aria-hidden="true" tabindex="-1"></a><span class="co"># --- 1. Solverstatus: eine Sprache fuer fuenf Bibliotheken -------------------</span></span>
<span id="cb7-45"><a href="#cb7-45" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-46"><a href="#cb7-46" aria-hidden="true" tabindex="-1"></a><span class="kw">class</span> SolverStatus(<span class="bu">str</span>, Enum):</span>
<span id="cb7-47"><a href="#cb7-47" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Was ein Solverlauf ergeben hat - unabhaengig davon, wer gerechnet hat.</span></span>
<span id="cb7-48"><a href="#cb7-48" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-49"><a href="#cb7-49" aria-hidden="true" tabindex="-1"></a><span class="co"> Die fuenf im Buch verwendeten Bibliotheken benennen dasselbe Ergebnis</span></span>
<span id="cb7-50"><a href="#cb7-50" aria-hidden="true" tabindex="-1"></a><span class="co"> unterschiedlich: &#39;Optimal&#39;, &#39;OPTIMAL&#39;, 2, &#39;optimal&#39;. Wer darauf direkt</span></span>
<span id="cb7-51"><a href="#cb7-51" aria-hidden="true" tabindex="-1"></a><span class="co"> prueft, bindet seinen Auswertungscode an eine Bibliothek.</span></span>
<span id="cb7-52"><a href="#cb7-52" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb7-53"><a href="#cb7-53" aria-hidden="true" tabindex="-1"></a> OPTIMAL <span class="op">=</span> <span class="st">&quot;optimal&quot;</span> <span class="co"># beweisbar bestmoeglich</span></span>
<span id="cb7-54"><a href="#cb7-54" aria-hidden="true" tabindex="-1"></a> ZULAESSIG <span class="op">=</span> <span class="st">&quot;zulaessig&quot;</span> <span class="co"># brauchbare Loesung, Beweis fehlt</span></span>
<span id="cb7-55"><a href="#cb7-55" aria-hidden="true" tabindex="-1"></a> UNZULAESSIG <span class="op">=</span> <span class="st">&quot;unzulaessig&quot;</span> <span class="co"># es gibt keine Loesung (Aussage ueber das Modell)</span></span>
<span id="cb7-56"><a href="#cb7-56" aria-hidden="true" tabindex="-1"></a> UNBESCHRAENKT <span class="op">=</span> <span class="st">&quot;unbeschraenkt&quot;</span> <span class="co"># Ziel waechst ins Unendliche</span></span>
<span id="cb7-57"><a href="#cb7-57" aria-hidden="true" tabindex="-1"></a> ZEITLIMIT <span class="op">=</span> <span class="st">&quot;zeitlimit&quot;</span> <span class="co"># abgebrochen, nichts Brauchbares gefunden</span></span>
<span id="cb7-58"><a href="#cb7-58" aria-hidden="true" tabindex="-1"></a> FEHLERHAFT <span class="op">=</span> <span class="st">&quot;fehlerhaft&quot;</span> <span class="co"># das Modell ist kein gueltiges Modell</span></span>
<span id="cb7-59"><a href="#cb7-59" aria-hidden="true" tabindex="-1"></a> UNBEKANNT <span class="op">=</span> <span class="st">&quot;unbekannt&quot;</span> <span class="co"># alles Uebrige</span></span>
<span id="cb7-60"><a href="#cb7-60" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-61"><a href="#cb7-61" aria-hidden="true" tabindex="-1"></a> <span class="at">@property</span></span>
<span id="cb7-62"><a href="#cb7-62" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> brauchbar(<span class="va">self</span>) <span class="op">-&gt;</span> <span class="bu">bool</span>:</span>
<span id="cb7-63"><a href="#cb7-63" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Habe ich etwas in der Hand, das ich ausfuehren kann?&quot;&quot;&quot;</span></span>
<span id="cb7-64"><a href="#cb7-64" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">self</span> <span class="kw">in</span> (SolverStatus.OPTIMAL, SolverStatus.ZULAESSIG)</span>
<span id="cb7-65"><a href="#cb7-65" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-66"><a href="#cb7-66" aria-hidden="true" tabindex="-1"></a> <span class="at">@property</span></span>
<span id="cb7-67"><a href="#cb7-67" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> modellfehler(<span class="va">self</span>) <span class="op">-&gt;</span> <span class="bu">bool</span>:</span>
<span id="cb7-68"><a href="#cb7-68" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Liegt die Ursache im Modell (und nicht in der Rechenzeit)?&quot;&quot;&quot;</span></span>
<span id="cb7-69"><a href="#cb7-69" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">self</span> <span class="kw">in</span> (SolverStatus.UNZULAESSIG, SolverStatus.UNBESCHRAENKT,</span>
<span id="cb7-70"><a href="#cb7-70" aria-hidden="true" tabindex="-1"></a> SolverStatus.FEHLERHAFT)</span>
<span id="cb7-71"><a href="#cb7-71" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-72"><a href="#cb7-72" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-73"><a href="#cb7-73" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> status_von_pywraplp(rohstatus: <span class="bu">int</span>) <span class="op">-&gt;</span> SolverStatus:</span>
<span id="cb7-74"><a href="#cb7-74" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;OR-Tools linear_solver (GLOP, SCIP, CBC).&quot;&quot;&quot;</span></span>
<span id="cb7-75"><a href="#cb7-75" aria-hidden="true" tabindex="-1"></a> <span class="im">from</span> ortools.linear_solver <span class="im">import</span> pywraplp</span>
<span id="cb7-76"><a href="#cb7-76" aria-hidden="true" tabindex="-1"></a> zuordnung <span class="op">=</span> {</span>
<span id="cb7-77"><a href="#cb7-77" aria-hidden="true" tabindex="-1"></a> pywraplp.Solver.OPTIMAL: SolverStatus.OPTIMAL,</span>
<span id="cb7-78"><a href="#cb7-78" aria-hidden="true" tabindex="-1"></a> pywraplp.Solver.FEASIBLE: SolverStatus.ZULAESSIG,</span>
<span id="cb7-79"><a href="#cb7-79" aria-hidden="true" tabindex="-1"></a> pywraplp.Solver.INFEASIBLE: SolverStatus.UNZULAESSIG,</span>
<span id="cb7-80"><a href="#cb7-80" aria-hidden="true" tabindex="-1"></a> pywraplp.Solver.UNBOUNDED: SolverStatus.UNBESCHRAENKT,</span>
<span id="cb7-81"><a href="#cb7-81" aria-hidden="true" tabindex="-1"></a> pywraplp.Solver.ABNORMAL: SolverStatus.FEHLERHAFT,</span>
<span id="cb7-82"><a href="#cb7-82" aria-hidden="true" tabindex="-1"></a> pywraplp.Solver.NOT_SOLVED: SolverStatus.ZEITLIMIT,</span>
<span id="cb7-83"><a href="#cb7-83" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb7-84"><a href="#cb7-84" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> zuordnung.get(rohstatus, SolverStatus.UNBEKANNT)</span>
<span id="cb7-85"><a href="#cb7-85" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-86"><a href="#cb7-86" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-87"><a href="#cb7-87" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> status_von_cpsat(rohstatus: <span class="bu">int</span>) <span class="op">-&gt;</span> SolverStatus:</span>
<span id="cb7-88"><a href="#cb7-88" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;OR-Tools CP-SAT (siehe Kapitel CP-SAT, alle fuenf Faelle).&quot;&quot;&quot;</span></span>
<span id="cb7-89"><a href="#cb7-89" aria-hidden="true" tabindex="-1"></a> <span class="im">from</span> ortools.sat.python <span class="im">import</span> cp_model</span>
<span id="cb7-90"><a href="#cb7-90" aria-hidden="true" tabindex="-1"></a> zuordnung <span class="op">=</span> {</span>
<span id="cb7-91"><a href="#cb7-91" aria-hidden="true" tabindex="-1"></a> cp_model.OPTIMAL: SolverStatus.OPTIMAL,</span>
<span id="cb7-92"><a href="#cb7-92" aria-hidden="true" tabindex="-1"></a> cp_model.FEASIBLE: SolverStatus.ZULAESSIG,</span>
<span id="cb7-93"><a href="#cb7-93" aria-hidden="true" tabindex="-1"></a> cp_model.INFEASIBLE: SolverStatus.UNZULAESSIG,</span>
<span id="cb7-94"><a href="#cb7-94" aria-hidden="true" tabindex="-1"></a> cp_model.MODEL_INVALID: SolverStatus.FEHLERHAFT,</span>
<span id="cb7-95"><a href="#cb7-95" aria-hidden="true" tabindex="-1"></a> cp_model.UNKNOWN: SolverStatus.ZEITLIMIT,</span>
<span id="cb7-96"><a href="#cb7-96" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb7-97"><a href="#cb7-97" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> zuordnung.get(rohstatus, SolverStatus.UNBEKANNT)</span>
<span id="cb7-98"><a href="#cb7-98" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-99"><a href="#cb7-99" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-100"><a href="#cb7-100" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> status_von_highs(statustext: <span class="bu">str</span>) <span class="op">-&gt;</span> SolverStatus:</span>
<span id="cb7-101"><a href="#cb7-101" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;HiGHS ueber highspy - hier kommt der Status als Text.&quot;&quot;&quot;</span></span>
<span id="cb7-102"><a href="#cb7-102" aria-hidden="true" tabindex="-1"></a> zuordnung <span class="op">=</span> {</span>
<span id="cb7-103"><a href="#cb7-103" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Optimal&quot;</span>: SolverStatus.OPTIMAL,</span>
<span id="cb7-104"><a href="#cb7-104" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Time limit reached&quot;</span>: SolverStatus.ZULAESSIG, <span class="co"># meist mit Loesung</span></span>
<span id="cb7-105"><a href="#cb7-105" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Solution limit reached&quot;</span>: SolverStatus.ZULAESSIG,</span>
<span id="cb7-106"><a href="#cb7-106" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Infeasible&quot;</span>: SolverStatus.UNZULAESSIG,</span>
<span id="cb7-107"><a href="#cb7-107" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Unbounded&quot;</span>: SolverStatus.UNBESCHRAENKT,</span>
<span id="cb7-108"><a href="#cb7-108" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Primal infeasible or unbounded&quot;</span>: SolverStatus.UNZULAESSIG,</span>
<span id="cb7-109"><a href="#cb7-109" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Model error&quot;</span>: SolverStatus.FEHLERHAFT,</span>
<span id="cb7-110"><a href="#cb7-110" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb7-111"><a href="#cb7-111" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> zuordnung.get(statustext, SolverStatus.UNBEKANNT)</span>
<span id="cb7-112"><a href="#cb7-112" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-113"><a href="#cb7-113" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-114"><a href="#cb7-114" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> status_von_scipy(ergebnis: Any) <span class="op">-&gt;</span> SolverStatus:</span>
<span id="cb7-115"><a href="#cb7-115" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;scipy.optimize.linprog und milp liefern ein Ergebnisobjekt mit .status.&quot;&quot;&quot;</span></span>
<span id="cb7-116"><a href="#cb7-116" aria-hidden="true" tabindex="-1"></a> zuordnung <span class="op">=</span> {</span>
<span id="cb7-117"><a href="#cb7-117" aria-hidden="true" tabindex="-1"></a> <span class="dv">0</span>: SolverStatus.OPTIMAL,</span>
<span id="cb7-118"><a href="#cb7-118" aria-hidden="true" tabindex="-1"></a> <span class="dv">1</span>: SolverStatus.ZEITLIMIT, <span class="co"># Iterations-/Zeitgrenze</span></span>
<span id="cb7-119"><a href="#cb7-119" aria-hidden="true" tabindex="-1"></a> <span class="dv">2</span>: SolverStatus.UNZULAESSIG,</span>
<span id="cb7-120"><a href="#cb7-120" aria-hidden="true" tabindex="-1"></a> <span class="dv">3</span>: SolverStatus.UNBESCHRAENKT,</span>
<span id="cb7-121"><a href="#cb7-121" aria-hidden="true" tabindex="-1"></a> <span class="dv">4</span>: SolverStatus.FEHLERHAFT, <span class="co"># numerische Schwierigkeiten</span></span>
<span id="cb7-122"><a href="#cb7-122" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb7-123"><a href="#cb7-123" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> zuordnung.get(<span class="bu">int</span>(ergebnis.status), SolverStatus.UNBEKANNT)</span>
<span id="cb7-124"><a href="#cb7-124" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-125"><a href="#cb7-125" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-126"><a href="#cb7-126" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> status_von_cvxpy(statustext: <span class="bu">str</span>) <span class="op">-&gt;</span> SolverStatus:</span>
<span id="cb7-127"><a href="#cb7-127" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;CVXPY - Statuszeichenketten wie &#39;optimal&#39; oder &#39;infeasible&#39;.&quot;&quot;&quot;</span></span>
<span id="cb7-128"><a href="#cb7-128" aria-hidden="true" tabindex="-1"></a> zuordnung <span class="op">=</span> {</span>
<span id="cb7-129"><a href="#cb7-129" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;optimal&quot;</span>: SolverStatus.OPTIMAL,</span>
<span id="cb7-130"><a href="#cb7-130" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;optimal_inaccurate&quot;</span>: SolverStatus.ZULAESSIG,</span>
<span id="cb7-131"><a href="#cb7-131" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;infeasible&quot;</span>: SolverStatus.UNZULAESSIG,</span>
<span id="cb7-132"><a href="#cb7-132" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;infeasible_inaccurate&quot;</span>: SolverStatus.UNZULAESSIG,</span>
<span id="cb7-133"><a href="#cb7-133" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;unbounded&quot;</span>: SolverStatus.UNBESCHRAENKT,</span>
<span id="cb7-134"><a href="#cb7-134" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;unbounded_inaccurate&quot;</span>: SolverStatus.UNBESCHRAENKT,</span>
<span id="cb7-135"><a href="#cb7-135" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb7-136"><a href="#cb7-136" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> zuordnung.get(statustext, SolverStatus.UNBEKANNT)</span>
<span id="cb7-137"><a href="#cb7-137" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-138"><a href="#cb7-138" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-139"><a href="#cb7-139" aria-hidden="true" tabindex="-1"></a><span class="co"># --- 2. Die Loesung als solverunabhaengiges Datenobjekt ----------------------</span></span>
<span id="cb7-140"><a href="#cb7-140" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-141"><a href="#cb7-141" aria-hidden="true" tabindex="-1"></a><span class="kw">class</span> Loesung(BaseModel):</span>
<span id="cb7-142"><a href="#cb7-142" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Das Ergebnis eines Solverlaufs, so wie es weiterverarbeitet wird.</span></span>
<span id="cb7-143"><a href="#cb7-143" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-144"><a href="#cb7-144" aria-hidden="true" tabindex="-1"></a><span class="co"> Bewusst OHNE Referenz auf Solver, Modell oder Variablen: Ein Bericht, ein</span></span>
<span id="cb7-145"><a href="#cb7-145" aria-hidden="true" tabindex="-1"></a><span class="co"> Test oder eine Weiterverarbeitung soll nicht wissen muessen, womit</span></span>
<span id="cb7-146"><a href="#cb7-146" aria-hidden="true" tabindex="-1"></a><span class="co"> gerechnet wurde.</span></span>
<span id="cb7-147"><a href="#cb7-147" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb7-148"><a href="#cb7-148" aria-hidden="true" tabindex="-1"></a> status: SolverStatus</span>
<span id="cb7-149"><a href="#cb7-149" aria-hidden="true" tabindex="-1"></a> werte: <span class="bu">dict</span>[<span class="bu">str</span>, <span class="bu">float</span>] <span class="op">=</span> Field(default_factory<span class="op">=</span><span class="bu">dict</span>)</span>
<span id="cb7-150"><a href="#cb7-150" aria-hidden="true" tabindex="-1"></a> zielwert: <span class="bu">float</span> <span class="op">|</span> <span class="va">None</span> <span class="op">=</span> <span class="va">None</span></span>
<span id="cb7-151"><a href="#cb7-151" aria-hidden="true" tabindex="-1"></a> schranke: <span class="bu">float</span> <span class="op">|</span> <span class="va">None</span> <span class="op">=</span> <span class="va">None</span></span>
<span id="cb7-152"><a href="#cb7-152" aria-hidden="true" tabindex="-1"></a> laufzeit: <span class="bu">float</span> <span class="op">|</span> <span class="va">None</span> <span class="op">=</span> <span class="va">None</span></span>
<span id="cb7-153"><a href="#cb7-153" aria-hidden="true" tabindex="-1"></a> <span class="co"># Schattenpreise je Nebenbedingung, sofern der Solver sie liefert</span></span>
<span id="cb7-154"><a href="#cb7-154" aria-hidden="true" tabindex="-1"></a> <span class="co"># (nur bei kontinuierlichen Problemen, siehe Kapitel LP). Auch das ist</span></span>
<span id="cb7-155"><a href="#cb7-155" aria-hidden="true" tabindex="-1"></a> <span class="co"># ein solverunabhaengiger Begriff und gehoert deshalb hierher.</span></span>
<span id="cb7-156"><a href="#cb7-156" aria-hidden="true" tabindex="-1"></a> schattenpreise: <span class="bu">dict</span>[<span class="bu">str</span>, <span class="bu">float</span>] <span class="op">=</span> Field(default_factory<span class="op">=</span><span class="bu">dict</span>)</span>
<span id="cb7-157"><a href="#cb7-157" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-158"><a href="#cb7-158" aria-hidden="true" tabindex="-1"></a> <span class="at">@property</span></span>
<span id="cb7-159"><a href="#cb7-159" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> gap(<span class="va">self</span>) <span class="op">-&gt;</span> <span class="bu">float</span> <span class="op">|</span> <span class="va">None</span>:</span>
<span id="cb7-160"><a href="#cb7-160" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Relativer Abstand zwischen gefundener Loesung und bewiesener Schranke.</span></span>
<span id="cb7-161"><a href="#cb7-161" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-162"><a href="#cb7-162" aria-hidden="true" tabindex="-1"></a><span class="co"> Das ist die Zahl fuer den Bericht: keine Schaetzung, sondern eine</span></span>
<span id="cb7-163"><a href="#cb7-163" aria-hidden="true" tabindex="-1"></a><span class="co"> Zusage - schlechter als das kann die Loesung nicht sein.</span></span>
<span id="cb7-164"><a href="#cb7-164" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb7-165"><a href="#cb7-165" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="va">self</span>.zielwert <span class="kw">is</span> <span class="va">None</span> <span class="kw">or</span> <span class="va">self</span>.schranke <span class="kw">is</span> <span class="va">None</span>:</span>
<span id="cb7-166"><a href="#cb7-166" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">None</span></span>
<span id="cb7-167"><a href="#cb7-167" aria-hidden="true" tabindex="-1"></a> nenner <span class="op">=</span> <span class="bu">max</span>(<span class="bu">abs</span>(<span class="va">self</span>.zielwert), <span class="fl">1e-9</span>)</span>
<span id="cb7-168"><a href="#cb7-168" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="bu">abs</span>(<span class="va">self</span>.zielwert <span class="op">-</span> <span class="va">self</span>.schranke) <span class="op">/</span> nenner</span>
<span id="cb7-169"><a href="#cb7-169" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-170"><a href="#cb7-170" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> als_bericht(<span class="va">self</span>) <span class="op">-&gt;</span> <span class="bu">str</span>:</span>
<span id="cb7-171"><a href="#cb7-171" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Eine Zeile fuers Betriebsprotokoll - siehe Betriebsueberwachung.py.&quot;&quot;&quot;</span></span>
<span id="cb7-172"><a href="#cb7-172" aria-hidden="true" tabindex="-1"></a> teile <span class="op">=</span> [<span class="ss">f&quot;Status: </span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>status<span class="sc">.</span>value<span class="sc">}</span><span class="ss">&quot;</span>]</span>
<span id="cb7-173"><a href="#cb7-173" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="va">self</span>.zielwert <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span>
<span id="cb7-174"><a href="#cb7-174" aria-hidden="true" tabindex="-1"></a> teile.append(<span class="ss">f&quot;Zielwert: </span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>zielwert<span class="sc">:,.2f}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-175"><a href="#cb7-175" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="va">self</span>.gap <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span>
<span id="cb7-176"><a href="#cb7-176" aria-hidden="true" tabindex="-1"></a> teile.append(<span class="ss">f&quot;Gap: </span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>gap<span class="sc">:.2%}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-177"><a href="#cb7-177" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="va">self</span>.laufzeit <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span>
<span id="cb7-178"><a href="#cb7-178" aria-hidden="true" tabindex="-1"></a> teile.append(<span class="ss">f&quot;Zeit: </span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>laufzeit<span class="sc">:.2f}</span><span class="ss">s&quot;</span>)</span>
<span id="cb7-179"><a href="#cb7-179" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="st">&quot; | &quot;</span>.join(teile)</span>
<span id="cb7-180"><a href="#cb7-180" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-181"><a href="#cb7-181" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-182"><a href="#cb7-182" aria-hidden="true" tabindex="-1"></a><span class="co"># --- 3. Domaenenmodell: die Fakten, bevor ein Solver sie sieht ---------------</span></span>
<span id="cb7-183"><a href="#cb7-183" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-184"><a href="#cb7-184" aria-hidden="true" tabindex="-1"></a><span class="kw">class</span> Produkt(BaseModel):</span>
<span id="cb7-185"><a href="#cb7-185" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Ein Produkt mit Deckungsbeitrag und Ressourcenverbrauch.&quot;&quot;&quot;</span></span>
<span id="cb7-186"><a href="#cb7-186" aria-hidden="true" tabindex="-1"></a> name: <span class="bu">str</span> <span class="op">=</span> Field(min_length<span class="op">=</span><span class="dv">1</span>)</span>
<span id="cb7-187"><a href="#cb7-187" aria-hidden="true" tabindex="-1"></a> deckungsbeitrag: <span class="bu">float</span></span>
<span id="cb7-188"><a href="#cb7-188" aria-hidden="true" tabindex="-1"></a> verbrauch: <span class="bu">dict</span>[<span class="bu">str</span>, NichtNegativ]</span>
<span id="cb7-189"><a href="#cb7-189" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-190"><a href="#cb7-190" aria-hidden="true" tabindex="-1"></a> model_config <span class="op">=</span> {<span class="st">&quot;frozen&quot;</span>: <span class="va">True</span>} <span class="co"># Stammdaten aendern sich nicht im Lauf</span></span>
<span id="cb7-191"><a href="#cb7-191" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-192"><a href="#cb7-192" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-193"><a href="#cb7-193" aria-hidden="true" tabindex="-1"></a><span class="kw">class</span> Produktionsproblem(BaseModel):</span>
<span id="cb7-194"><a href="#cb7-194" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Produktionsprogrammplanung - das Modell aus Kapitel Einfuehrung.</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"> Die Pruefungen stehen hier und nicht im Solvercode. Damit gelten sie</span></span>
<span id="cb7-197"><a href="#cb7-197" aria-hidden="true" tabindex="-1"></a><span class="co"> unabhaengig davon, mit welcher Bibliothek spaeter gerechnet wird - und sie</span></span>
<span id="cb7-198"><a href="#cb7-198" aria-hidden="true" tabindex="-1"></a><span class="co"> schlagen beim EINLESEN zu, wo der Fehler noch zuzuordnen ist.</span></span>
<span id="cb7-199"><a href="#cb7-199" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb7-200"><a href="#cb7-200" aria-hidden="true" tabindex="-1"></a> produkte: <span class="bu">list</span>[Produkt] <span class="op">=</span> Field(min_length<span class="op">=</span><span class="dv">1</span>)</span>
<span id="cb7-201"><a href="#cb7-201" aria-hidden="true" tabindex="-1"></a> kapazitaeten: <span class="bu">dict</span>[<span class="bu">str</span>, PositiveZahl]</span>
<span id="cb7-202"><a href="#cb7-202" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-203"><a href="#cb7-203" aria-hidden="true" tabindex="-1"></a> <span class="at">@model_validator</span>(mode<span class="op">=</span><span class="st">&quot;after&quot;</span>)</span>
<span id="cb7-204"><a href="#cb7-204" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> pruefe_ressourcen(<span class="va">self</span>) <span class="op">-&gt;</span> <span class="st">&quot;Produktionsproblem&quot;</span>:</span>
<span id="cb7-205"><a href="#cb7-205" aria-hidden="true" tabindex="-1"></a> namen <span class="op">=</span> [p.name <span class="cf">for</span> p <span class="kw">in</span> <span class="va">self</span>.produkte]</span>
<span id="cb7-206"><a href="#cb7-206" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">len</span>(<span class="bu">set</span>(namen)) <span class="op">!=</span> <span class="bu">len</span>(namen):</span>
<span id="cb7-207"><a href="#cb7-207" aria-hidden="true" tabindex="-1"></a> doppelt <span class="op">=</span> <span class="bu">sorted</span>({n <span class="cf">for</span> n <span class="kw">in</span> namen <span class="cf">if</span> namen.count(n) <span class="op">&gt;</span> <span class="dv">1</span>})</span>
<span id="cb7-208"><a href="#cb7-208" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="ss">f&quot;Produktnamen kommen mehrfach vor: </span><span class="sc">{</span>doppelt<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-209"><a href="#cb7-209" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-210"><a href="#cb7-210" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> produkt <span class="kw">in</span> <span class="va">self</span>.produkte:</span>
<span id="cb7-211"><a href="#cb7-211" aria-hidden="true" tabindex="-1"></a> unbekannt <span class="op">=</span> <span class="bu">set</span>(produkt.verbrauch) <span class="op">-</span> <span class="bu">set</span>(<span class="va">self</span>.kapazitaeten)</span>
<span id="cb7-212"><a href="#cb7-212" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> unbekannt:</span>
<span id="cb7-213"><a href="#cb7-213" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">ValueError</span>(</span>
<span id="cb7-214"><a href="#cb7-214" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Produkt &#39;</span><span class="sc">{</span>produkt<span class="sc">.</span>name<span class="sc">}</span><span class="ss">&#39; verbraucht Ressourcen ohne &quot;</span></span>
<span id="cb7-215"><a href="#cb7-215" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Kapazitaetsangabe: </span><span class="sc">{</span><span class="bu">sorted</span>(unbekannt)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-216"><a href="#cb7-216" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">self</span></span>
<span id="cb7-217"><a href="#cb7-217" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-218"><a href="#cb7-218" aria-hidden="true" tabindex="-1"></a> <span class="at">@property</span></span>
<span id="cb7-219"><a href="#cb7-219" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> ressourcen(<span class="va">self</span>) <span class="op">-&gt;</span> <span class="bu">list</span>[<span class="bu">str</span>]:</span>
<span id="cb7-220"><a href="#cb7-220" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Feste Reihenfolge - siehe die Spaltenfalle in Kapitel Finanzdaten.&quot;&quot;&quot;</span></span>
<span id="cb7-221"><a href="#cb7-221" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="bu">sorted</span>(<span class="va">self</span>.kapazitaeten)</span>
<span id="cb7-222"><a href="#cb7-222" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-223"><a href="#cb7-223" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> verbrauchsmatrix(<span class="va">self</span>) <span class="op">-&gt;</span> np.ndarray:</span>
<span id="cb7-224"><a href="#cb7-224" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Zeilen = Ressourcen, Spalten = Produkte (Matrixform, Kapitel Fundament).&quot;&quot;&quot;</span></span>
<span id="cb7-225"><a href="#cb7-225" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> np.array([[p.verbrauch.get(r, <span class="fl">0.0</span>) <span class="cf">for</span> p <span class="kw">in</span> <span class="va">self</span>.produkte]</span>
<span id="cb7-226"><a href="#cb7-226" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> r <span class="kw">in</span> <span class="va">self</span>.ressourcen])</span>
<span id="cb7-227"><a href="#cb7-227" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-228"><a href="#cb7-228" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> kapazitaetsvektor(<span class="va">self</span>) <span class="op">-&gt;</span> np.ndarray:</span>
<span id="cb7-229"><a href="#cb7-229" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> np.array([<span class="va">self</span>.kapazitaeten[r] <span class="cf">for</span> r <span class="kw">in</span> <span class="va">self</span>.ressourcen])</span>
<span id="cb7-230"><a href="#cb7-230" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-231"><a href="#cb7-231" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> deckungsbeitragsvektor(<span class="va">self</span>) <span class="op">-&gt;</span> np.ndarray:</span>
<span id="cb7-232"><a href="#cb7-232" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> np.array([p.deckungsbeitrag <span class="cf">for</span> p <span class="kw">in</span> <span class="va">self</span>.produkte])</span>
<span id="cb7-233"><a href="#cb7-233" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-234"><a href="#cb7-234" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-235"><a href="#cb7-235" aria-hidden="true" tabindex="-1"></a><span class="co"># --- 4. Abnahmepruefung: Loesung gegen Anforderung, ohne Solver --------------</span></span>
<span id="cb7-236"><a href="#cb7-236" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-237"><a href="#cb7-237" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> pruefe_loesung(problem: Produktionsproblem, loesung: Loesung,</span>
<span id="cb7-238"><a href="#cb7-238" aria-hidden="true" tabindex="-1"></a> ganzzahlig: Sequence[<span class="bu">str</span>] <span class="op">=</span> (),</span>
<span id="cb7-239"><a href="#cb7-239" aria-hidden="true" tabindex="-1"></a> 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-240"><a href="#cb7-240" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Prueft eine Loesung gegen die Anforderungen - OHNE den Solver zu fragen.</span></span>
<span id="cb7-241"><a href="#cb7-241" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-242"><a href="#cb7-242" aria-hidden="true" tabindex="-1"></a><span class="co"> Diese Funktion darf ausdruecklich KEIN Solverobjekt und keine</span></span>
<span id="cb7-243"><a href="#cb7-243" aria-hidden="true" tabindex="-1"></a><span class="co"> Modellvariable benutzen. Eine Pruefung aus denselben Bausteinen wie das</span></span>
<span id="cb7-244"><a href="#cb7-244" aria-hidden="true" tabindex="-1"></a><span class="co"> Modell prueft das Modell gegen sich selbst und findet nichts (siehe die</span></span>
<span id="cb7-245"><a href="#cb7-245" aria-hidden="true" tabindex="-1"></a><span class="co"> Denkfehler in den Kapiteln Graphen und MILP).</span></span>
<span id="cb7-246"><a href="#cb7-246" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-247"><a href="#cb7-247" aria-hidden="true" tabindex="-1"></a><span class="co"> Liefert eine Liste von Beanstandungen; leer heisst bestanden.</span></span>
<span id="cb7-248"><a href="#cb7-248" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb7-249"><a href="#cb7-249" 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-250"><a href="#cb7-250" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-251"><a href="#cb7-251" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> loesung.status.brauchbar:</span>
<span id="cb7-252"><a href="#cb7-252" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> [<span class="ss">f&quot;Kein verwertbares Ergebnis (Status: </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-253"><a href="#cb7-253" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-254"><a href="#cb7-254" aria-hidden="true" tabindex="-1"></a> fehlend <span class="op">=</span> [p.name <span class="cf">for</span> p <span class="kw">in</span> problem.produkte <span class="cf">if</span> p.name <span class="kw">not</span> <span class="kw">in</span> loesung.werte]</span>
<span id="cb7-255"><a href="#cb7-255" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> fehlend:</span>
<span id="cb7-256"><a href="#cb7-256" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> [<span class="ss">f&quot;Loesung enthaelt keine Werte fuer: </span><span class="sc">{</span>fehlend<span class="sc">}</span><span class="ss">&quot;</span>]</span>
<span id="cb7-257"><a href="#cb7-257" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-258"><a href="#cb7-258" aria-hidden="true" tabindex="-1"></a> mengen <span class="op">=</span> np.array([loesung.werte[p.name] <span class="cf">for</span> p <span class="kw">in</span> problem.produkte])</span>
<span id="cb7-259"><a href="#cb7-259" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-260"><a href="#cb7-260" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (mengen <span class="op">&lt;</span> <span class="op">-</span>toleranz).<span class="bu">any</span>():</span>
<span id="cb7-261"><a href="#cb7-261" aria-hidden="true" tabindex="-1"></a> negativ <span class="op">=</span> [p.name <span class="cf">for</span> p, m <span class="kw">in</span> <span class="bu">zip</span>(problem.produkte, mengen) <span class="cf">if</span> m <span class="op">&lt;</span> <span class="op">-</span>toleranz]</span>
<span id="cb7-262"><a href="#cb7-262" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;negative Mengen bei: </span><span class="sc">{</span>negativ<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-263"><a href="#cb7-263" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-264"><a href="#cb7-264" aria-hidden="true" tabindex="-1"></a> verbrauch <span class="op">=</span> problem.verbrauchsmatrix() <span class="op">@</span> mengen</span>
<span id="cb7-265"><a href="#cb7-265" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> ressource, ist, grenze <span class="kw">in</span> <span class="bu">zip</span>(problem.ressourcen, verbrauch,</span>
<span id="cb7-266"><a href="#cb7-266" aria-hidden="true" tabindex="-1"></a> problem.kapazitaetsvektor()):</span>
<span id="cb7-267"><a href="#cb7-267" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> ist <span class="op">&gt;</span> grenze <span class="op">+</span> toleranz:</span>
<span id="cb7-268"><a href="#cb7-268" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(</span>
<span id="cb7-269"><a href="#cb7-269" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>ressource<span class="sc">}</span><span class="ss">: Verbrauch </span><span class="sc">{</span>ist<span class="sc">:,.3f}</span><span class="ss"> ueber Kapazitaet </span><span class="sc">{</span>grenze<span class="sc">:,.3f}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-270"><a href="#cb7-270" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-271"><a href="#cb7-271" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> name <span class="kw">in</span> ganzzahlig:</span>
<span id="cb7-272"><a href="#cb7-272" aria-hidden="true" tabindex="-1"></a> wert <span class="op">=</span> loesung.werte[name]</span>
<span id="cb7-273"><a href="#cb7-273" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(wert <span class="op">-</span> <span class="bu">round</span>(wert)) <span class="op">&gt;</span> toleranz:</span>
<span id="cb7-274"><a href="#cb7-274" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;&#39;</span><span class="sc">{</span>name<span class="sc">}</span><span class="ss">&#39; = </span><span class="sc">{</span>wert<span class="sc">!r}</span><span class="ss"> ist nicht ganzzahlig&quot;</span>)</span>
<span id="cb7-275"><a href="#cb7-275" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-276"><a href="#cb7-276" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> loesung.zielwert <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span>
<span id="cb7-277"><a href="#cb7-277" aria-hidden="true" tabindex="-1"></a> nachgerechnet <span class="op">=</span> <span class="bu">float</span>(problem.deckungsbeitragsvektor() <span class="op">@</span> mengen)</span>
<span id="cb7-278"><a href="#cb7-278" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(nachgerechnet <span class="op">-</span> loesung.zielwert) <span class="op">&gt;</span> toleranz <span class="op">*</span> <span class="bu">max</span>(<span class="fl">1.0</span>, <span class="bu">abs</span>(nachgerechnet)):</span>
<span id="cb7-279"><a href="#cb7-279" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(</span>
<span id="cb7-280"><a href="#cb7-280" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Zielwert </span><span class="sc">{</span>loesung<span class="sc">.</span>zielwert<span class="sc">:,.4f}</span><span class="ss"> passt nicht zu den Mengen &quot;</span></span>
<span id="cb7-281"><a href="#cb7-281" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(nachgerechnet </span><span class="sc">{</span>nachgerechnet<span class="sc">:,.4f}</span><span class="ss">)&quot;</span>)</span>
<span id="cb7-282"><a href="#cb7-282" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-283"><a href="#cb7-283" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> beanstandungen</span>
<span id="cb7-284"><a href="#cb7-284" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-285"><a href="#cb7-285" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-286"><a href="#cb7-286" aria-hidden="true" tabindex="-1"></a><span class="co"># --- 5. Ein Modellbauer je Solver - austauschbar ----------------------------</span></span>
<span id="cb7-287"><a href="#cb7-287" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-288"><a href="#cb7-288" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese_mit_glop(problem: Produktionsproblem) <span class="op">-&gt;</span> Loesung:</span>
<span id="cb7-289"><a href="#cb7-289" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Modellbauer fuer OR-Tools GLOP (kontinuierlich).&quot;&quot;&quot;</span></span>
<span id="cb7-290"><a href="#cb7-290" aria-hidden="true" tabindex="-1"></a> <span class="im">import</span> time</span>
<span id="cb7-291"><a href="#cb7-291" aria-hidden="true" tabindex="-1"></a> <span class="im">from</span> ortools.linear_solver <span class="im">import</span> pywraplp</span>
<span id="cb7-292"><a href="#cb7-292" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-293"><a href="#cb7-293" aria-hidden="true" tabindex="-1"></a> solver <span class="op">=</span> pywraplp.Solver.CreateSolver(<span class="st">&quot;GLOP&quot;</span>)</span>
<span id="cb7-294"><a href="#cb7-294" aria-hidden="true" tabindex="-1"></a> menge <span class="op">=</span> {p.name: solver.NumVar(<span class="dv">0</span>, solver.infinity(), p.name)</span>
<span id="cb7-295"><a href="#cb7-295" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> problem.produkte}</span>
<span id="cb7-296"><a href="#cb7-296" aria-hidden="true" tabindex="-1"></a> <span class="co"># Die Nebenbedingung wird gemerkt - ohne die Referenz gibt es spaeter</span></span>
<span id="cb7-297"><a href="#cb7-297" aria-hidden="true" tabindex="-1"></a> <span class="co"># keinen Schattenpreis (siehe Kapitel LP).</span></span>
<span id="cb7-298"><a href="#cb7-298" aria-hidden="true" tabindex="-1"></a> bedingung <span class="op">=</span> {}</span>
<span id="cb7-299"><a href="#cb7-299" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> ressource <span class="kw">in</span> problem.ressourcen:</span>
<span id="cb7-300"><a href="#cb7-300" aria-hidden="true" tabindex="-1"></a> bedingung[ressource] <span class="op">=</span> solver.Add(</span>
<span id="cb7-301"><a href="#cb7-301" aria-hidden="true" tabindex="-1"></a> <span class="bu">sum</span>(menge[p.name] <span class="op">*</span> p.verbrauch.get(ressource, <span class="fl">0.0</span>)</span>
<span id="cb7-302"><a href="#cb7-302" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> problem.produkte)</span>
<span id="cb7-303"><a href="#cb7-303" aria-hidden="true" tabindex="-1"></a> <span class="op">&lt;=</span> problem.kapazitaeten[ressource], name<span class="op">=</span>ressource)</span>
<span id="cb7-304"><a href="#cb7-304" aria-hidden="true" tabindex="-1"></a> solver.Maximize(<span class="bu">sum</span>(menge[p.name] <span class="op">*</span> p.deckungsbeitrag <span class="cf">for</span> p <span class="kw">in</span> problem.produkte))</span>
<span id="cb7-305"><a href="#cb7-305" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-306"><a href="#cb7-306" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()</span>
<span id="cb7-307"><a href="#cb7-307" aria-hidden="true" tabindex="-1"></a> rohstatus <span class="op">=</span> solver.Solve()</span>
<span id="cb7-308"><a href="#cb7-308" aria-hidden="true" tabindex="-1"></a> laufzeit <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
<span id="cb7-309"><a href="#cb7-309" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-310"><a href="#cb7-310" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> status_von_pywraplp(rohstatus)</span>
<span id="cb7-311"><a href="#cb7-311" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> status.brauchbar:</span>
<span id="cb7-312"><a href="#cb7-312" 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-313"><a href="#cb7-313" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Loesung(status<span class="op">=</span>status,</span>
<span id="cb7-314"><a href="#cb7-314" aria-hidden="true" tabindex="-1"></a> werte<span class="op">=</span>{name: v.solution_value() <span class="cf">for</span> name, v <span class="kw">in</span> menge.items()},</span>
<span id="cb7-315"><a href="#cb7-315" aria-hidden="true" tabindex="-1"></a> zielwert<span class="op">=</span>solver.Objective().Value(),</span>
<span id="cb7-316"><a href="#cb7-316" aria-hidden="true" tabindex="-1"></a> schranke<span class="op">=</span>solver.Objective().Value(),</span>
<span id="cb7-317"><a href="#cb7-317" aria-hidden="true" tabindex="-1"></a> schattenpreise<span class="op">=</span>{r: b.dual_value() <span class="cf">for</span> r, b <span class="kw">in</span> bedingung.items()},</span>
<span id="cb7-318"><a href="#cb7-318" aria-hidden="true" tabindex="-1"></a> laufzeit<span class="op">=</span>laufzeit)</span>
<span id="cb7-319"><a href="#cb7-319" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-320"><a href="#cb7-320" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-321"><a href="#cb7-321" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese_mit_scipy(problem: Produktionsproblem) <span class="op">-&gt;</span> Loesung:</span>
<span id="cb7-322"><a href="#cb7-322" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Derselbe Fall mit scipy.optimize.linprog - anderer Solver, gleiche Loesung.</span></span>
<span id="cb7-323"><a href="#cb7-323" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-324"><a href="#cb7-324" aria-hidden="true" tabindex="-1"></a><span class="co"> Beachten Sie, wie wenig hier steht: Die Daten kommen fertig geprueft aus</span></span>
<span id="cb7-325"><a href="#cb7-325" aria-hidden="true" tabindex="-1"></a><span class="co"> dem Domaenenmodell, die Auswertung geht ans gemeinsame Loesung-Objekt.</span></span>
<span id="cb7-326"><a href="#cb7-326" aria-hidden="true" tabindex="-1"></a><span class="co"> Genau das ist der Ertrag der Trennung.</span></span>
<span id="cb7-327"><a href="#cb7-327" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb7-328"><a href="#cb7-328" aria-hidden="true" tabindex="-1"></a> <span class="im">import</span> time</span>
<span id="cb7-329"><a href="#cb7-329" aria-hidden="true" tabindex="-1"></a> <span class="im">from</span> scipy.optimize <span class="im">import</span> linprog</span>
<span id="cb7-330"><a href="#cb7-330" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-331"><a href="#cb7-331" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()</span>
<span id="cb7-332"><a href="#cb7-332" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> linprog(c<span class="op">=-</span>problem.deckungsbeitragsvektor(), <span class="co"># linprog minimiert</span></span>
<span id="cb7-333"><a href="#cb7-333" aria-hidden="true" tabindex="-1"></a> A_ub<span class="op">=</span>problem.verbrauchsmatrix(),</span>
<span id="cb7-334"><a href="#cb7-334" aria-hidden="true" tabindex="-1"></a> b_ub<span class="op">=</span>problem.kapazitaetsvektor(),</span>
<span id="cb7-335"><a href="#cb7-335" aria-hidden="true" tabindex="-1"></a> bounds<span class="op">=</span>(<span class="dv">0</span>, <span class="va">None</span>), method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb7-336"><a href="#cb7-336" aria-hidden="true" tabindex="-1"></a> laufzeit <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
<span id="cb7-337"><a href="#cb7-337" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-338"><a href="#cb7-338" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> status_von_scipy(ergebnis)</span>
<span id="cb7-339"><a href="#cb7-339" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> status.brauchbar:</span>
<span id="cb7-340"><a href="#cb7-340" 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-341"><a href="#cb7-341" aria-hidden="true" tabindex="-1"></a> <span class="co"># linprog rechnet minimierend mit negierten Kosten - die Dualwerte</span></span>
<span id="cb7-342"><a href="#cb7-342" aria-hidden="true" tabindex="-1"></a> <span class="co"># muessen entsprechend zurueckgedreht werden.</span></span>
<span id="cb7-343"><a href="#cb7-343" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Loesung(status<span class="op">=</span>status,</span>
<span id="cb7-344"><a href="#cb7-344" aria-hidden="true" tabindex="-1"></a> werte<span class="op">=</span>{p.name: <span class="bu">float</span>(x)</span>
<span id="cb7-345"><a href="#cb7-345" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p, x <span class="kw">in</span> <span class="bu">zip</span>(problem.produkte, ergebnis.x)},</span>
<span id="cb7-346"><a href="#cb7-346" 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-347"><a href="#cb7-347" aria-hidden="true" tabindex="-1"></a> schranke<span class="op">=</span><span class="bu">float</span>(<span class="op">-</span>ergebnis.fun),</span>
<span id="cb7-348"><a href="#cb7-348" aria-hidden="true" tabindex="-1"></a> schattenpreise<span class="op">=</span>{r: <span class="bu">float</span>(<span class="op">-</span>m) <span class="cf">for</span> r, m <span class="kw">in</span></span>
<span id="cb7-349"><a href="#cb7-349" aria-hidden="true" tabindex="-1"></a> <span class="bu">zip</span>(problem.ressourcen,</span>
<span id="cb7-350"><a href="#cb7-350" aria-hidden="true" tabindex="-1"></a> ergebnis.ineqlin.marginals)},</span>
<span id="cb7-351"><a href="#cb7-351" aria-hidden="true" tabindex="-1"></a> laufzeit<span class="op">=</span>laufzeit)</span>
<span id="cb7-352"><a href="#cb7-352" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-353"><a href="#cb7-353" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-354"><a href="#cb7-354" aria-hidden="true" tabindex="-1"></a><span class="co"># --- 6. Ein- und Ausgabe: Tabellen sind die Wirklichkeit ---------------------</span></span>
<span id="cb7-355"><a href="#cb7-355" aria-hidden="true" tabindex="-1"></a><span class="co">#</span></span>
<span id="cb7-356"><a href="#cb7-356" aria-hidden="true" tabindex="-1"></a><span class="co"># In den meisten Betrieben liegen die Zahlen in einer Tabellenkalkulation</span></span>
<span id="cb7-357"><a href="#cb7-357" aria-hidden="true" tabindex="-1"></a><span class="co"># (Kapitel Einfuehrung). Diese beiden Funktionen sind die einzige Stelle im</span></span>
<span id="cb7-358"><a href="#cb7-358" aria-hidden="true" tabindex="-1"></a><span class="co"># Modul, die das weiss - alles andere arbeitet mit dem Domaenenmodell.</span></span>
<span id="cb7-359"><a href="#cb7-359" aria-hidden="true" tabindex="-1"></a><span class="co">#</span></span>
<span id="cb7-360"><a href="#cb7-360" aria-hidden="true" tabindex="-1"></a><span class="co"># pandas und openpyxl werden bewusst erst hier importiert: Wer or_kern nur</span></span>
<span id="cb7-361"><a href="#cb7-361" aria-hidden="true" tabindex="-1"></a><span class="co"># fuer Statusauswertung und Pruefung benutzt, soll sie nicht installieren</span></span>
<span id="cb7-362"><a href="#cb7-362" aria-hidden="true" tabindex="-1"></a><span class="co"># muessen.</span></span>
<span id="cb7-363"><a href="#cb7-363" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-364"><a href="#cb7-364" aria-hidden="true" tabindex="-1"></a>STAMMSPALTEN <span class="op">=</span> (<span class="st">&quot;Produkt&quot;</span>, <span class="st">&quot;Deckungsbeitrag&quot;</span>)</span>
<span id="cb7-365"><a href="#cb7-365" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-366"><a href="#cb7-366" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-367"><a href="#cb7-367" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> lade_produktionsproblem(pfad: <span class="bu">str</span>, blatt_produkte: <span class="bu">str</span> <span class="op">=</span> <span class="st">&quot;Produkte&quot;</span>,</span>
<span id="cb7-368"><a href="#cb7-368" aria-hidden="true" tabindex="-1"></a> blatt_kapazitaeten: <span class="bu">str</span> <span class="op">=</span> <span class="st">&quot;Kapazitaeten&quot;</span></span>
<span id="cb7-369"><a href="#cb7-369" aria-hidden="true" tabindex="-1"></a> ) <span class="op">-&gt;</span> Produktionsproblem:</span>
<span id="cb7-370"><a href="#cb7-370" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Liest eine Excel-Mappe und liefert ein geprueftes Domaenenmodell.</span></span>
<span id="cb7-371"><a href="#cb7-371" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-372"><a href="#cb7-372" aria-hidden="true" tabindex="-1"></a><span class="co"> Alle Spalten ausser &#39;Produkt&#39; und &#39;Deckungsbeitrag&#39; gelten als</span></span>
<span id="cb7-373"><a href="#cb7-373" aria-hidden="true" tabindex="-1"></a><span class="co"> Verbrauchsspalten - eine neue Ressource ist damit eine neue SPALTE in der</span></span>
<span id="cb7-374"><a href="#cb7-374" aria-hidden="true" tabindex="-1"></a><span class="co"> Mappe und erfordert keine Codeaenderung.</span></span>
<span id="cb7-375"><a href="#cb7-375" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-376"><a href="#cb7-376" aria-hidden="true" tabindex="-1"></a><span class="co"> Die eigentliche Pruefung passiert nicht hier, sondern im Konstruktor von</span></span>
<span id="cb7-377"><a href="#cb7-377" aria-hidden="true" tabindex="-1"></a><span class="co"> Produktionsproblem. Diese Funktion uebersetzt nur Tabelle in Objekt; was</span></span>
<span id="cb7-378"><a href="#cb7-378" aria-hidden="true" tabindex="-1"></a><span class="co"> ein gueltiges Problem ist, steht an genau einer Stelle.</span></span>
<span id="cb7-379"><a href="#cb7-379" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb7-380"><a href="#cb7-380" aria-hidden="true" tabindex="-1"></a> <span class="im">import</span> pandas <span class="im">as</span> pd</span>
<span id="cb7-381"><a href="#cb7-381" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-382"><a href="#cb7-382" aria-hidden="true" tabindex="-1"></a> produkte_tabelle <span class="op">=</span> pd.read_excel(pfad, sheet_name<span class="op">=</span>blatt_produkte)</span>
<span id="cb7-383"><a href="#cb7-383" aria-hidden="true" tabindex="-1"></a> kapazitaeten_tabelle <span class="op">=</span> pd.read_excel(pfad, sheet_name<span class="op">=</span>blatt_kapazitaeten)</span>
<span id="cb7-384"><a href="#cb7-384" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-385"><a href="#cb7-385" aria-hidden="true" tabindex="-1"></a> fehlende <span class="op">=</span> [s <span class="cf">for</span> s <span class="kw">in</span> STAMMSPALTEN <span class="cf">if</span> s <span class="kw">not</span> <span class="kw">in</span> produkte_tabelle.columns]</span>
<span id="cb7-386"><a href="#cb7-386" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> fehlende:</span>
<span id="cb7-387"><a href="#cb7-387" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="ss">f&quot;Blatt &#39;</span><span class="sc">{</span>blatt_produkte<span class="sc">}</span><span class="ss">&#39;: Spalten fehlen: </span><span class="sc">{</span>fehlende<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-388"><a href="#cb7-388" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> produkte_tabelle.isna().<span class="bu">any</span>().<span class="bu">any</span>():</span>
<span id="cb7-389"><a href="#cb7-389" aria-hidden="true" tabindex="-1"></a> zeilen <span class="op">=</span> produkte_tabelle[produkte_tabelle.isna().<span class="bu">any</span>(axis<span class="op">=</span><span class="dv">1</span>)].index.tolist()</span>
<span id="cb7-390"><a href="#cb7-390" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="ss">f&quot;Blatt &#39;</span><span class="sc">{</span>blatt_produkte<span class="sc">}</span><span class="ss">&#39;: leere Zellen in Zeile(n) </span><span class="sc">{</span>zeilen<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-391"><a href="#cb7-391" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-392"><a href="#cb7-392" aria-hidden="true" tabindex="-1"></a> ressourcen <span class="op">=</span> [s <span class="cf">for</span> s <span class="kw">in</span> produkte_tabelle.columns <span class="cf">if</span> s <span class="kw">not</span> <span class="kw">in</span> STAMMSPALTEN]</span>
<span id="cb7-393"><a href="#cb7-393" aria-hidden="true" tabindex="-1"></a> produkte <span class="op">=</span> [</span>
<span id="cb7-394"><a href="#cb7-394" aria-hidden="true" tabindex="-1"></a> Produkt(name<span class="op">=</span><span class="bu">str</span>(zeile.Produkt),</span>
<span id="cb7-395"><a href="#cb7-395" aria-hidden="true" tabindex="-1"></a> deckungsbeitrag<span class="op">=</span><span class="bu">float</span>(zeile.Deckungsbeitrag),</span>
<span id="cb7-396"><a href="#cb7-396" aria-hidden="true" tabindex="-1"></a> verbrauch<span class="op">=</span>{r: <span class="bu">float</span>(<span class="bu">getattr</span>(zeile, r)) <span class="cf">for</span> r <span class="kw">in</span> ressourcen})</span>
<span id="cb7-397"><a href="#cb7-397" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> zeile <span class="kw">in</span> produkte_tabelle.itertuples()</span>
<span id="cb7-398"><a href="#cb7-398" aria-hidden="true" tabindex="-1"></a> ]</span>
<span id="cb7-399"><a href="#cb7-399" aria-hidden="true" tabindex="-1"></a> kapazitaeten <span class="op">=</span> {<span class="bu">str</span>(z.Ressource): <span class="bu">float</span>(z.Verfuegbar)</span>
<span id="cb7-400"><a href="#cb7-400" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> z <span class="kw">in</span> kapazitaeten_tabelle.itertuples()}</span>
<span id="cb7-401"><a href="#cb7-401" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-402"><a href="#cb7-402" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Produktionsproblem(produkte<span class="op">=</span>produkte, kapazitaeten<span class="op">=</span>kapazitaeten)</span>
<span id="cb7-403"><a href="#cb7-403" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-404"><a href="#cb7-404" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-405"><a href="#cb7-405" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> schreibe_ergebnis(pfad: <span class="bu">str</span>, problem: Produktionsproblem,</span>
<span id="cb7-406"><a href="#cb7-406" aria-hidden="true" tabindex="-1"></a> loesung: Loesung) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb7-407"><a href="#cb7-407" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Schreibt Plan und Kennzahlen als zwei Blaetter zurueck nach Excel.</span></span>
<span id="cb7-408"><a href="#cb7-408" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-409"><a href="#cb7-409" aria-hidden="true" tabindex="-1"></a><span class="co"> Die Fachabteilung bekommt das Ergebnis im Format, das sie ohnehin benutzt.</span></span>
<span id="cb7-410"><a href="#cb7-410" aria-hidden="true" tabindex="-1"></a><span class="co"> Akzeptanz entscheidet ueber Projekterfolg (Kapitel Praxisfallen).</span></span>
<span id="cb7-411"><a href="#cb7-411" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb7-412"><a href="#cb7-412" aria-hidden="true" tabindex="-1"></a> <span class="im">import</span> pandas <span class="im">as</span> pd</span>
<span id="cb7-413"><a href="#cb7-413" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-414"><a href="#cb7-414" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> loesung.status.brauchbar:</span>
<span id="cb7-415"><a href="#cb7-415" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="ss">f&quot;Nichts zu schreiben - Status: </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-416"><a href="#cb7-416" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-417"><a href="#cb7-417" aria-hidden="true" tabindex="-1"></a> mengen <span class="op">=</span> np.array([loesung.werte[p.name] <span class="cf">for</span> p <span class="kw">in</span> problem.produkte])</span>
<span id="cb7-418"><a href="#cb7-418" aria-hidden="true" tabindex="-1"></a> plan <span class="op">=</span> pd.DataFrame({</span>
<span id="cb7-419"><a href="#cb7-419" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Produkt&quot;</span>: [p.name <span class="cf">for</span> p <span class="kw">in</span> problem.produkte],</span>
<span id="cb7-420"><a href="#cb7-420" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Menge&quot;</span>: mengen,</span>
<span id="cb7-421"><a href="#cb7-421" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Deckungsbeitrag&quot;</span>: mengen <span class="op">*</span> problem.deckungsbeitragsvektor(),</span>
<span id="cb7-422"><a href="#cb7-422" aria-hidden="true" tabindex="-1"></a> })</span>
<span id="cb7-423"><a href="#cb7-423" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-424"><a href="#cb7-424" aria-hidden="true" tabindex="-1"></a> verbrauch <span class="op">=</span> problem.verbrauchsmatrix() <span class="op">@</span> mengen</span>
<span id="cb7-425"><a href="#cb7-425" aria-hidden="true" tabindex="-1"></a> kennzahlen: <span class="bu">dict</span>[<span class="bu">str</span>, <span class="bu">float</span> <span class="op">|</span> <span class="bu">str</span>] <span class="op">=</span> {</span>
<span id="cb7-426"><a href="#cb7-426" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Status&quot;</span>: loesung.status.value,</span>
<span id="cb7-427"><a href="#cb7-427" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Gesamtdeckungsbeitrag&quot;</span>: loesung.zielwert <span class="kw">or</span> <span class="fl">0.0</span>,</span>
<span id="cb7-428"><a href="#cb7-428" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb7-429"><a href="#cb7-429" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> ressource, ist, grenze <span class="kw">in</span> <span class="bu">zip</span>(problem.ressourcen, verbrauch,</span>
<span id="cb7-430"><a href="#cb7-430" aria-hidden="true" tabindex="-1"></a> problem.kapazitaetsvektor()):</span>
<span id="cb7-431"><a href="#cb7-431" aria-hidden="true" tabindex="-1"></a> kennzahlen[<span class="ss">f&quot;</span><span class="sc">{</span>ressource<span class="sc">}</span><span class="ss">: Verbrauch&quot;</span>] <span class="op">=</span> <span class="bu">float</span>(ist)</span>
<span id="cb7-432"><a href="#cb7-432" aria-hidden="true" tabindex="-1"></a> kennzahlen[<span class="ss">f&quot;</span><span class="sc">{</span>ressource<span class="sc">}</span><span class="ss">: Auslastung %&quot;</span>] <span class="op">=</span> <span class="bu">float</span>(<span class="fl">100.0</span> <span class="op">*</span> ist <span class="op">/</span> grenze)</span>
<span id="cb7-433"><a href="#cb7-433" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> ressource <span class="kw">in</span> loesung.schattenpreise:</span>
<span id="cb7-434"><a href="#cb7-434" aria-hidden="true" tabindex="-1"></a> kennzahlen[<span class="ss">f&quot;</span><span class="sc">{</span>ressource<span class="sc">}</span><span class="ss">: Schattenpreis&quot;</span>] <span class="op">=</span> <span class="op">\</span></span>
<span id="cb7-435"><a href="#cb7-435" aria-hidden="true" tabindex="-1"></a> loesung.schattenpreise[ressource]</span>
<span id="cb7-436"><a href="#cb7-436" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-437"><a href="#cb7-437" aria-hidden="true" tabindex="-1"></a> kennzahlen_tabelle <span class="op">=</span> pd.DataFrame({</span>
<span id="cb7-438"><a href="#cb7-438" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Kennzahl&quot;</span>: <span class="bu">list</span>(kennzahlen),</span>
<span id="cb7-439"><a href="#cb7-439" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Wert&quot;</span>: [<span class="bu">round</span>(w, <span class="dv">2</span>) <span class="cf">if</span> <span class="bu">isinstance</span>(w, <span class="bu">float</span>) <span class="cf">else</span> w</span>
<span id="cb7-440"><a href="#cb7-440" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> w <span class="kw">in</span> kennzahlen.values()],</span>
<span id="cb7-441"><a href="#cb7-441" aria-hidden="true" tabindex="-1"></a> })</span>
<span id="cb7-442"><a href="#cb7-442" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-443"><a href="#cb7-443" aria-hidden="true" tabindex="-1"></a> <span class="cf">with</span> pd.ExcelWriter(pfad, engine<span class="op">=</span><span class="st">&quot;openpyxl&quot;</span>) <span class="im">as</span> mappe:</span>
<span id="cb7-444"><a href="#cb7-444" aria-hidden="true" tabindex="-1"></a> plan.<span class="bu">round</span>(<span class="dv">2</span>).to_excel(mappe, sheet_name<span class="op">=</span><span class="st">&quot;Plan&quot;</span>, index<span class="op">=</span><span class="va">False</span>)</span>
<span id="cb7-445"><a href="#cb7-445" aria-hidden="true" tabindex="-1"></a> kennzahlen_tabelle.to_excel(mappe, sheet_name<span class="op">=</span><span class="st">&quot;Kennzahlen&quot;</span>, index<span class="op">=</span><span class="va">False</span>)</span>
<span id="cb7-446"><a href="#cb7-446" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-447"><a href="#cb7-447" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-448"><a href="#cb7-448" 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-449"><a href="#cb7-449" aria-hidden="true" tabindex="-1"></a> <span class="co"># Die Schreinerei aus Kapitel Einfuehrung - jetzt als Domaenenmodell.</span></span>
<span id="cb7-450"><a href="#cb7-450" aria-hidden="true" tabindex="-1"></a> schreinerei <span class="op">=</span> Produktionsproblem(</span>
<span id="cb7-451"><a href="#cb7-451" aria-hidden="true" tabindex="-1"></a> produkte<span class="op">=</span>[</span>
<span id="cb7-452"><a href="#cb7-452" aria-hidden="true" tabindex="-1"></a> Produkt(name<span class="op">=</span><span class="st">&quot;Tisch&quot;</span>, deckungsbeitrag<span class="op">=</span><span class="fl">240.0</span>,</span>
<span id="cb7-453"><a href="#cb7-453" aria-hidden="true" tabindex="-1"></a> verbrauch<span class="op">=</span>{<span class="st">&quot;Montagestunden&quot;</span>: <span class="fl">3.0</span>, <span class="st">&quot;Plattenmaterial&quot;</span>: <span class="fl">6.0</span>}),</span>
<span id="cb7-454"><a href="#cb7-454" aria-hidden="true" tabindex="-1"></a> Produkt(name<span class="op">=</span><span class="st">&quot;Stuhl&quot;</span>, deckungsbeitrag<span class="op">=</span><span class="fl">60.0</span>,</span>
<span id="cb7-455"><a href="#cb7-455" aria-hidden="true" tabindex="-1"></a> verbrauch<span class="op">=</span>{<span class="st">&quot;Montagestunden&quot;</span>: <span class="fl">1.0</span>, <span class="st">&quot;Plattenmaterial&quot;</span>: <span class="fl">1.0</span>}),</span>
<span id="cb7-456"><a href="#cb7-456" aria-hidden="true" tabindex="-1"></a> ],</span>
<span id="cb7-457"><a href="#cb7-457" aria-hidden="true" tabindex="-1"></a> kapazitaeten<span class="op">=</span>{<span class="st">&quot;Montagestunden&quot;</span>: <span class="fl">150.0</span>, <span class="st">&quot;Plattenmaterial&quot;</span>: <span class="fl">240.0</span>},</span>
<span id="cb7-458"><a href="#cb7-458" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb7-459"><a href="#cb7-459" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-460"><a href="#cb7-460" 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="cb7-461"><a href="#cb7-461" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; DERSELBE FALL, ZWEI SOLVER, EIN AUSWERTUNGSCODE&quot;</span>)</span>
<span id="cb7-462"><a href="#cb7-462" 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="cb7-463"><a href="#cb7-463" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-464"><a href="#cb7-464" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> name, bauer <span class="kw">in</span> [(<span class="st">&quot;OR-Tools GLOP&quot;</span>, loese_mit_glop),</span>
<span id="cb7-465"><a href="#cb7-465" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;scipy / HiGHS&quot;</span>, loese_mit_scipy)]:</span>
<span id="cb7-466"><a href="#cb7-466" aria-hidden="true" tabindex="-1"></a> loesung <span class="op">=</span> bauer(schreinerei)</span>
<span id="cb7-467"><a href="#cb7-467" aria-hidden="true" tabindex="-1"></a> beanstandungen <span class="op">=</span> pruefe_loesung(schreinerei, loesung)</span>
<span id="cb7-468"><a href="#cb7-468" 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>name<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-469"><a href="#cb7-469" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </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-470"><a href="#cb7-470" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> produkt, menge <span class="kw">in</span> loesung.werte.items():</span>
<span id="cb7-471"><a href="#cb7-471" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>produkt<span class="sc">:&lt;10}</span><span class="ss"> </span><span class="sc">{</span>menge<span class="sc">:8.2f}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-472"><a href="#cb7-472" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Abnahmepruefung: &quot;</span></span>
<span id="cb7-473"><a href="#cb7-473" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;bestanden&#39;</span> <span class="cf">if</span> <span class="kw">not</span> beanstandungen <span class="cf">else</span> beanstandungen<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-474"><a href="#cb7-474" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-475"><a href="#cb7-475" 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="cb7-476"><a href="#cb7-476" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; WAS DIE PRUEFUNGEN ABFANGEN&quot;</span>)</span>
<span id="cb7-477"><a href="#cb7-477" 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="cb7-478"><a href="#cb7-478" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-479"><a href="#cb7-479" aria-hidden="true" tabindex="-1"></a> faelle <span class="op">=</span> [</span>
<span id="cb7-480"><a href="#cb7-480" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Kapazitaet 0&quot;</span>, <span class="bu">dict</span>(</span>
<span id="cb7-481"><a href="#cb7-481" aria-hidden="true" tabindex="-1"></a> produkte<span class="op">=</span>schreinerei.produkte,</span>
<span id="cb7-482"><a href="#cb7-482" aria-hidden="true" tabindex="-1"></a> kapazitaeten<span class="op">=</span>{<span class="st">&quot;Montagestunden&quot;</span>: <span class="fl">0.0</span>, <span class="st">&quot;Plattenmaterial&quot;</span>: <span class="fl">240.0</span>})),</span>
<span id="cb7-483"><a href="#cb7-483" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Ressource ohne Kapazitaet&quot;</span>, <span class="bu">dict</span>(</span>
<span id="cb7-484"><a href="#cb7-484" aria-hidden="true" tabindex="-1"></a> produkte<span class="op">=</span>[Produkt(name<span class="op">=</span><span class="st">&quot;Regal&quot;</span>, deckungsbeitrag<span class="op">=</span><span class="fl">130.0</span>,</span>
<span id="cb7-485"><a href="#cb7-485" aria-hidden="true" tabindex="-1"></a> verbrauch<span class="op">=</span>{<span class="st">&quot;Lackieren&quot;</span>: <span class="fl">2.0</span>})],</span>
<span id="cb7-486"><a href="#cb7-486" aria-hidden="true" tabindex="-1"></a> kapazitaeten<span class="op">=</span>{<span class="st">&quot;Montagestunden&quot;</span>: <span class="fl">150.0</span>})),</span>
<span id="cb7-487"><a href="#cb7-487" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;doppelter Produktname&quot;</span>, <span class="bu">dict</span>(</span>
<span id="cb7-488"><a href="#cb7-488" aria-hidden="true" tabindex="-1"></a> produkte<span class="op">=</span>[schreinerei.produkte[<span class="dv">0</span>], schreinerei.produkte[<span class="dv">0</span>]],</span>
<span id="cb7-489"><a href="#cb7-489" aria-hidden="true" tabindex="-1"></a> kapazitaeten<span class="op">=</span>schreinerei.kapazitaeten)),</span>
<span id="cb7-490"><a href="#cb7-490" aria-hidden="true" tabindex="-1"></a> ]</span>
<span id="cb7-491"><a href="#cb7-491" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> beschreibung, daten <span class="kw">in</span> faelle:</span>
<span id="cb7-492"><a href="#cb7-492" aria-hidden="true" tabindex="-1"></a> <span class="cf">try</span>:</span>
<span id="cb7-493"><a href="#cb7-493" aria-hidden="true" tabindex="-1"></a> Produktionsproblem(<span class="op">**</span>daten)</span>
<span id="cb7-494"><a href="#cb7-494" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>beschreibung<span class="sc">:&lt;28}</span><span class="ss"> NICHT erkannt (!)&quot;</span>)</span>
<span id="cb7-495"><a href="#cb7-495" aria-hidden="true" tabindex="-1"></a> <span class="cf">except</span> <span class="pp">Exception</span> <span class="im">as</span> fehler:</span>
<span id="cb7-496"><a href="#cb7-496" aria-hidden="true" tabindex="-1"></a> meldung <span class="op">=</span> <span class="bu">str</span>(fehler).splitlines()</span>
<span id="cb7-497"><a href="#cb7-497" aria-hidden="true" tabindex="-1"></a> kern <span class="op">=</span> <span class="bu">next</span>((z.strip() <span class="cf">for</span> z <span class="kw">in</span> meldung</span>
<span id="cb7-498"><a href="#cb7-498" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="st">&quot;Value error&quot;</span> <span class="kw">in</span> z <span class="kw">or</span> <span class="st">&quot;greater than&quot;</span> <span class="kw">in</span> z), meldung[<span class="op">-</span><span class="dv">1</span>])</span>
<span id="cb7-499"><a href="#cb7-499" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>beschreibung<span class="sc">:&lt;28}</span><span class="ss"> abgefangen: </span><span class="sc">{</span>kern[:<span class="dv">44</span>]<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-500"><a href="#cb7-500" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-501"><a href="#cb7-501" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">Alle drei scheitern beim EINLESEN - nicht erst beim Loesen und&quot;</span>)</span>
<span id="cb7-502"><a href="#cb7-502" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;schon gar nicht erst im Bericht. Das ist der ganze Zweck der&quot;</span>)</span>
<span id="cb7-503"><a href="#cb7-503" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Domaenenschicht.&quot;</span>)</span>
<span id="cb7-504"><a href="#cb7-504" 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>==============================================================================
DERSELBE FALL, ZWEI SOLVER, EIN AUSWERTUNGSCODE
==============================================================================
OR-Tools GLOP
Status: optimal | Zielwert: 10,800.00 | Gap: 0.00% | Zeit: 0.00s
Tisch 30.00
Stuhl 60.00
Abnahmepruefung: bestanden
scipy / HiGHS
Status: optimal | Zielwert: 10,800.00 | Gap: 0.00% | Zeit: 0.00s
Tisch 30.00
Stuhl 60.00
Abnahmepruefung: bestanden
==============================================================================
WAS DIE PRUEFUNGEN ABFANGEN
==============================================================================
Kapazitaet 0 abgefangen: Input should be greater than 0 [type=greater
Ressource ohne Kapazitaet abgefangen: Value error, Produkt &#39;Regal&#39; verbraucht Ress
doppelter Produktname abgefangen: Value error, Produktnamen kommen mehrfach vo
Alle drei scheitern beim EINLESEN - nicht erst beim Loesen und
schon gar nicht erst im Bericht. Das ist der ganze Zweck der
Domaenenschicht.
==============================================================================</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 so</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>PositiveZahl = Annotated[float, Field(gt=0)]</code></td>
<td>Prüfung im <strong>Typ</strong>, nicht im Code</td>
<td>Sie gilt damit überall, wo der Typ verwendet wird — man kann sie nicht vergessen.</td>
</tr>
<tr class="even">
<td><code>SolverStatus.brauchbar</code></td>
<td>fragt „habe ich etwas in der Hand?“</td>
<td>Nicht „war der Solver erfolgreich?“. <code>ZULAESSIG</code> ist im Betrieb meist völlig ausreichend (<a href="milp.html#sec:milp-gap">Abschnitt 6.8</a>).</td>
</tr>
<tr class="odd">
<td><code>SolverStatus.modellfehler</code></td>
<td>trennt Modell- von Zeitproblemen</td>
<td>Genau die Unterscheidung, die <a href="cpsat.html#sec:cpsat-statusfaelle">Abschnitt 7.8</a> als häufigste Verwechslung benennt.</td>
</tr>
<tr class="even">
<td><code>model_validator(mode="after")</code></td>
<td>prüft Zusammenhänge <strong>zwischen</strong> Feldern</td>
<td>Einzelne Felder prüft Pydantic selbst; dass jedes Produkt nur bekannte Ressourcen verbraucht, muss man sagen.</td>
</tr>
<tr class="odd">
<td><code>ressourcen</code> als sortierte Liste</td>
<td>feste Spaltenreihenfolge</td>
<td>Die Vertauschungsfalle aus <a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a> wird hier strukturell unmöglich.</td>
</tr>
<tr class="even">
<td><code>loese_mit_glop</code> / <code>loese_mit_scipy</code></td>
<td>zwei Modellbauer, ein Auswertungscode</td>
<td>Der Solverwechsel betrifft <strong>eine</strong> Funktion. Alles danach — Bericht, Prüfung, Test — bleibt unverändert.</td>
</tr>
<tr class="odd">
<td>die drei Fehlerfälle am Ende</td>
<td>Kapazität 0, unbekannte Ressource, doppelter Name</td>
<td>Alle drei scheitern beim <strong>Einlesen</strong>. Ohne Domänenschicht fielen sie erst beim Lösen auf — oder gar nicht.</td>
</tr>
<tr class="even">
<td><code>import pandas</code> <strong>in</strong> der Funktion</td>
<td>Excel-Schicht als optionale Abhängigkeit</td>
<td>Wer <code>or_kern</code> nur für Statusauswertung und Prüfung benutzt, soll pandas nicht installieren müssen.</td>
</tr>
<tr class="odd">
<td><code>schattenpreise</code> im DTO</td>
<td>Dualwerte gehören zur Lösung, nicht zum Solver</td>
<td>Beide Modellbauer liefern hier identische Werte (40 / 20) — der Begriff ist solverunabhängig, die Abfrage nicht.</td>
</tr>
</tbody>
</table>
</blockquote>
<blockquote>
<p><strong>🎯 Merksatz</strong> Ein Fehler, der beim Einlesen auffliegt, kostet Minuten. Derselbe Fehler, der erst im Bericht auffällt, kostet Tage — und derselbe Fehler, der <strong>nie</strong> auffällt, kostet am meisten. Validierung ist deshalb keine Fleißarbeit, sondern die günstigste Stelle im ganzen Ablauf.</p>
</blockquote>
<hr />
<h2 id="sec:praxisfallen-solverwechsel">22.7 Der Solverwechsel in der Praxis</h2>
<p>Der vorige Abschnitt endet mit einer Behauptung: Ein Solverwechsel betreffe genau <strong>einen</strong> Baustein. Das ist die Art Satz, die in jedem Architekturvortrag vorkommt und die man deshalb nicht glauben sollte, bevor sie jemand vorgeführt hat.</p>
<p>Also führen wir sie vor — und zwar am unbequemsten Paar, das dieses Buch zu bieten hat: <strong>CP-SAT gegen HiGHS</strong>. Die beiden sind grundverschieden. CP-SAT ist ein Constraint-Programming-Solver, der ausschließlich ganzzahlig rechnet und Restriktionen als Ausdrücke entgegennimmt. HiGHS ist ein klassischer Branch-and-Bound-Löser, der eine Koeffizientenmatrix erwartet. Und sie lassen sich nicht einmal gemeinsam importieren (<a href="oekosystem.html#kap-oekosystem">Kapitel 3</a>).</p>
<p><strong>Die Aufgabe</strong> ist eine kapazitierte Standortplanung: Aus sechs möglichen Lagern sind diejenigen zu eröffnen, mit denen zwölf Kunden am günstigsten beliefert werden. Jeder Kunde wird von genau einem Lager bedient, jedes eröffnete Lager kostet eine Fixgebühr, und die Kapazität reicht je Lager für 174 der insgesamt 387 Paletten — es müssen also mindestens drei Lager öffnen.</p>
<p><strong>Der Aufbau folgt dem Muster aus <code>or_kern.py</code>:</strong></p>
<table>
<thead>
<tr class="header">
<th>Baustein</th>
<th>Solverabhängig?</th>
<th>Zeilen</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>Standortproblem</code> (Pydantic-Domänenmodell)</td>
<td>nein</td>
<td>38</td>
</tr>
<tr class="even">
<td><code>baue_und_loese_mit_cpsat()</code></td>
<td><strong>ja</strong></td>
<td>39</td>
</tr>
<tr class="odd">
<td><code>baue_und_loese_mit_highs()</code></td>
<td><strong>ja</strong></td>
<td>52</td>
</tr>
<tr class="even">
<td><code>pruefe_zuordnung()</code> (Abnahmeprüfung)</td>
<td>nein</td>
<td>36</td>
</tr>
<tr class="odd">
<td>Bericht und Vergleich</td>
<td>nein</td>
<td>Rest</td>
</tr>
</tbody>
</table>
<p>Ausgetauscht wird eine Zeile: welche der beiden Funktionen aufgerufen wird. Prüfung und Bericht bekommen davon nichts mit, weil beide Funktionen dasselbe <code>Loesung</code>-Objekt liefern.</p>
<blockquote>
<p><strong>📎 Warum zwei Prozesse?</strong></p>
<p><code>ortools</code> und <code>highspy</code> bringen beide eine eigene HiGHS-Kopie mit und kollidieren beim gemeinsamen Import (<a href="oekosystem.html#kap-oekosystem">Kapitel 3</a>). Das Programm startet sich deshalb für jeden Solver selbst noch einmal als Kindprozess und lässt sich das Ergebnis als <strong>JSON</strong> zurückgeben:</p>
<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="bu">print</span>(MODELLBAUER[sys.argv[<span class="dv">1</span>]](problem).model_dump_json()) <span class="co"># im Kindprozess</span></span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a>Loesung.model_validate_json(...) <span class="co"># im Hauptprozess</span></span></code></pre></div>
<p>Das ist der zweite, weniger offensichtliche Ertrag des DTOs: Ein Lösungsobjekt ohne Solverbezug ist nicht nur eine Sprachregelung, sondern ein <strong>Datenformat</strong>. Es überlebt eine Prozessgrenze, passt in eine Warteschlange und lässt sich protokollieren. Ein <code>pywraplp.Solver</code>-Objekt kann das alles nicht.</p>
</blockquote>
<div class="sourceCode" id="cb10"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Solverwechsel_CPSAT_HiGHS.py</span></span>
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel Praxisfallen: Denselben Fall einmal mit CP-SAT und einmal mit HiGHS rechnen.</span></span>
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a><span class="co">Die Trennung aus or_kern.py behauptet, ein Solverwechsel koste genau EINEN</span></span>
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a><span class="co">Baustein. Dieses Programm loest diese Behauptung ein. Aufgabe ist eine</span></span>
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a><span class="co">Standortplanung: Welche Lager oeffnen wir, und wer beliefert welchen Kunden?</span></span>
<span id="cb10-10"><a href="#cb10-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-11"><a href="#cb10-11" aria-hidden="true" tabindex="-1"></a><span class="co"> Standortproblem (Pydantic, geprueft) &lt;- gemeinsam</span></span>
<span id="cb10-12"><a href="#cb10-12" aria-hidden="true" tabindex="-1"></a><span class="co"> baue_und_loese_mit_cpsat() &lt;- der EINE Baustein</span></span>
<span id="cb10-13"><a href="#cb10-13" aria-hidden="true" tabindex="-1"></a><span class="co"> baue_und_loese_mit_highs() &lt;- ... in zwei Ausfuehrungen</span></span>
<span id="cb10-14"><a href="#cb10-14" aria-hidden="true" tabindex="-1"></a><span class="co"> Loesung (DTO) &lt;- gemeinsam</span></span>
<span id="cb10-15"><a href="#cb10-15" aria-hidden="true" tabindex="-1"></a><span class="co"> pruefe_zuordnung() &lt;- gemeinsam</span></span>
<span id="cb10-16"><a href="#cb10-16" aria-hidden="true" tabindex="-1"></a><span class="co"> berichte() &lt;- gemeinsam</span></span>
<span id="cb10-17"><a href="#cb10-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-18"><a href="#cb10-18" aria-hidden="true" tabindex="-1"></a><span class="co">Die beiden Modellbauer sind 39 und 52 Zeilen lang - sie sind der gesamte</span></span>
<span id="cb10-19"><a href="#cb10-19" aria-hidden="true" tabindex="-1"></a><span class="co">solverabhaengige Teil des Programms. Alles andere wird zweimal benutzt und</span></span>
<span id="cb10-20"><a href="#cb10-20" aria-hidden="true" tabindex="-1"></a><span class="co">einmal geschrieben.</span></span>
<span id="cb10-21"><a href="#cb10-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-22"><a href="#cb10-22" aria-hidden="true" tabindex="-1"></a><span class="co">WARUM ZWEI PROZESSE? ortools und highspy bringen beide eine eigene</span></span>
<span id="cb10-23"><a href="#cb10-23" aria-hidden="true" tabindex="-1"></a><span class="co">HiGHS-Kopie mit und lassen sich auf vielen Systemen nicht gemeinsam</span></span>
<span id="cb10-24"><a href="#cb10-24" aria-hidden="true" tabindex="-1"></a><span class="co">importieren (Kapitel Oekosystem). Das Hauptprogramm startet deshalb fuer jeden</span></span>
<span id="cb10-25"><a href="#cb10-25" aria-hidden="true" tabindex="-1"></a><span class="co">Solver einen eigenen Python-Prozess und laesst sich die Loesung als JSON</span></span>
<span id="cb10-26"><a href="#cb10-26" aria-hidden="true" tabindex="-1"></a><span class="co">zurueckgeben - das DTO ist nicht nur eine Sprachregelung, sondern ein</span></span>
<span id="cb10-27"><a href="#cb10-27" aria-hidden="true" tabindex="-1"></a><span class="co">Datenformat, das eine Prozessgrenze ueberlebt.</span></span>
<span id="cb10-28"><a href="#cb10-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-29"><a href="#cb10-29" aria-hidden="true" tabindex="-1"></a><span class="co">Aufruf:</span></span>
<span id="cb10-30"><a href="#cb10-30" aria-hidden="true" tabindex="-1"></a><span class="co"> python3 Solverwechsel_CPSAT_HiGHS.py # beide, mit Vergleich</span></span>
<span id="cb10-31"><a href="#cb10-31" aria-hidden="true" tabindex="-1"></a><span class="co"> python3 Solverwechsel_CPSAT_HiGHS.py cpsat # nur der Kindprozess</span></span>
<span id="cb10-32"><a href="#cb10-32" aria-hidden="true" tabindex="-1"></a><span class="co"> python3 Solverwechsel_CPSAT_HiGHS.py highs</span></span>
<span id="cb10-33"><a href="#cb10-33" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-34"><a href="#cb10-34" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, pydantic, ortools, highspy (jeweils im eigenen Prozess)</span></span>
<span id="cb10-35"><a href="#cb10-35" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb10-36"><a href="#cb10-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-37"><a href="#cb10-37" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb10-38"><a href="#cb10-38" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-39"><a href="#cb10-39" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> multiprocessing</span>
<span id="cb10-40"><a href="#cb10-40" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> time</span>
<span id="cb10-41"><a href="#cb10-41" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> concurrent.futures <span class="im">import</span> ProcessPoolExecutor</span>
<span id="cb10-42"><a href="#cb10-42" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-43"><a href="#cb10-43" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb10-44"><a href="#cb10-44" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> pydantic <span class="im">import</span> BaseModel, Field, model_validator</span>
<span id="cb10-45"><a href="#cb10-45" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-46"><a href="#cb10-46" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> or_kern <span class="im">import</span> Loesung, SolverStatus, status_von_cpsat, status_von_highs</span>
<span id="cb10-47"><a href="#cb10-47" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-48"><a href="#cb10-48" aria-hidden="true" tabindex="-1"></a>ZEITLIMIT <span class="op">=</span> <span class="fl">30.0</span></span>
<span id="cb10-49"><a href="#cb10-49" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-50"><a href="#cb10-50" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-51"><a href="#cb10-51" aria-hidden="true" tabindex="-1"></a><span class="co"># --- 1. Domaenenmodell: dieselben Fakten fuer beide Solver -------------------</span></span>
<span id="cb10-52"><a href="#cb10-52" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-53"><a href="#cb10-53" aria-hidden="true" tabindex="-1"></a><span class="kw">class</span> Standortproblem(BaseModel):</span>
<span id="cb10-54"><a href="#cb10-54" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Kapazitierte Standortplanung mit Einzelbelieferung.</span></span>
<span id="cb10-55"><a href="#cb10-55" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-56"><a href="#cb10-56" aria-hidden="true" tabindex="-1"></a><span class="co"> Nach dem Muster von Produktionsproblem in or_kern.py: Die Pruefungen</span></span>
<span id="cb10-57"><a href="#cb10-57" aria-hidden="true" tabindex="-1"></a><span class="co"> stehen im Konstruktor, nicht im Solvercode - sie gelten damit fuer beide</span></span>
<span id="cb10-58"><a href="#cb10-58" aria-hidden="true" tabindex="-1"></a><span class="co"> Solver, und sie schlagen beim Einlesen zu.</span></span>
<span id="cb10-59"><a href="#cb10-59" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-60"><a href="#cb10-60" aria-hidden="true" tabindex="-1"></a><span class="co"> Alle Kosten sind ganzzahlig (Euro). Das ist keine Bequemlichkeit, sondern</span></span>
<span id="cb10-61"><a href="#cb10-61" aria-hidden="true" tabindex="-1"></a><span class="co"> Voraussetzung: CP-SAT rechnet ausschliesslich ganzzahlig.</span></span>
<span id="cb10-62"><a href="#cb10-62" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb10-63"><a href="#cb10-63" aria-hidden="true" tabindex="-1"></a> lager: <span class="bu">list</span>[<span class="bu">str</span>] <span class="op">=</span> Field(min_length<span class="op">=</span><span class="dv">1</span>)</span>
<span id="cb10-64"><a href="#cb10-64" aria-hidden="true" tabindex="-1"></a> kunden: <span class="bu">list</span>[<span class="bu">str</span>] <span class="op">=</span> Field(min_length<span class="op">=</span><span class="dv">1</span>)</span>
<span id="cb10-65"><a href="#cb10-65" aria-hidden="true" tabindex="-1"></a> fixkosten: <span class="bu">list</span>[<span class="bu">int</span>] <span class="co"># je Lager, faellt bei Eroeffnung an</span></span>
<span id="cb10-66"><a href="#cb10-66" aria-hidden="true" tabindex="-1"></a> kapazitaet: <span class="bu">list</span>[<span class="bu">int</span>] <span class="co"># je Lager, in Paletten</span></span>
<span id="cb10-67"><a href="#cb10-67" aria-hidden="true" tabindex="-1"></a> bedarf: <span class="bu">list</span>[<span class="bu">int</span>] <span class="co"># je Kunde, in Paletten</span></span>
<span id="cb10-68"><a href="#cb10-68" aria-hidden="true" tabindex="-1"></a> transport: <span class="bu">list</span>[<span class="bu">list</span>[<span class="bu">int</span>]] <span class="co"># [Lager][Kunde], Kosten der Belieferung</span></span>
<span id="cb10-69"><a href="#cb10-69" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-70"><a href="#cb10-70" aria-hidden="true" tabindex="-1"></a> <span class="at">@model_validator</span>(mode<span class="op">=</span><span class="st">&quot;after&quot;</span>)</span>
<span id="cb10-71"><a href="#cb10-71" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> pruefe_masse(<span class="va">self</span>) <span class="op">-&gt;</span> <span class="st">&quot;Standortproblem&quot;</span>:</span>
<span id="cb10-72"><a href="#cb10-72" aria-hidden="true" tabindex="-1"></a> n, m <span class="op">=</span> <span class="bu">len</span>(<span class="va">self</span>.lager), <span class="bu">len</span>(<span class="va">self</span>.kunden)</span>
<span id="cb10-73"><a href="#cb10-73" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">len</span>(<span class="va">self</span>.fixkosten) <span class="op">!=</span> n <span class="kw">or</span> <span class="bu">len</span>(<span class="va">self</span>.kapazitaet) <span class="op">!=</span> n:</span>
<span id="cb10-74"><a href="#cb10-74" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="ss">f&quot;fixkosten/kapazitaet muessen </span><span class="sc">{</span>n<span class="sc">}</span><span class="ss"> Eintraege haben&quot;</span>)</span>
<span id="cb10-75"><a href="#cb10-75" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">len</span>(<span class="va">self</span>.bedarf) <span class="op">!=</span> m:</span>
<span id="cb10-76"><a href="#cb10-76" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="ss">f&quot;bedarf muss </span><span class="sc">{</span>m<span class="sc">}</span><span class="ss"> Eintraege haben&quot;</span>)</span>
<span id="cb10-77"><a href="#cb10-77" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">len</span>(<span class="va">self</span>.transport) <span class="op">!=</span> n <span class="kw">or</span> <span class="bu">any</span>(<span class="bu">len</span>(z) <span class="op">!=</span> m <span class="cf">for</span> z <span class="kw">in</span> <span class="va">self</span>.transport):</span>
<span id="cb10-78"><a href="#cb10-78" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="ss">f&quot;transport muss </span><span class="sc">{</span>n<span class="sc">}</span><span class="ss"> x </span><span class="sc">{</span>m<span class="sc">}</span><span class="ss"> sein&quot;</span>)</span>
<span id="cb10-79"><a href="#cb10-79" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">sum</span>(<span class="va">self</span>.kapazitaet) <span class="op">&lt;</span> <span class="bu">sum</span>(<span class="va">self</span>.bedarf):</span>
<span id="cb10-80"><a href="#cb10-80" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">ValueError</span>(<span class="ss">f&quot;Gesamtkapazitaet </span><span class="sc">{</span><span class="bu">sum</span>(<span class="va">self</span>.kapazitaet)<span class="sc">}</span><span class="ss"> deckt den &quot;</span></span>
<span id="cb10-81"><a href="#cb10-81" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Gesamtbedarf </span><span class="sc">{</span><span class="bu">sum</span>(<span class="va">self</span>.bedarf)<span class="sc">}</span><span class="ss"> nicht&quot;</span>)</span>
<span id="cb10-82"><a href="#cb10-82" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">self</span></span>
<span id="cb10-83"><a href="#cb10-83" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-84"><a href="#cb10-84" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> schluessel(<span class="va">self</span>, i: <span class="bu">int</span>, j: <span class="bu">int</span>) <span class="op">-&gt;</span> <span class="bu">str</span>:</span>
<span id="cb10-85"><a href="#cb10-85" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Variablenname im Loesung-DTO - beide Modellbauer benutzen ihn.&quot;&quot;&quot;</span></span>
<span id="cb10-86"><a href="#cb10-86" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="ss">f&quot;</span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>lager[i]<span class="sc">}</span><span class="ss">-&gt;</span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>kunden[j]<span class="sc">}</span><span class="ss">&quot;</span></span>
<span id="cb10-87"><a href="#cb10-87" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-88"><a href="#cb10-88" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-89"><a href="#cb10-89" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> beispielproblem(saat: <span class="bu">int</span> <span class="op">=</span> <span class="dv">11</span>) <span class="op">-&gt;</span> Standortproblem:</span>
<span id="cb10-90"><a href="#cb10-90" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Sechs moegliche Lager, zwoelf Kunden - klein genug fuer beide Solver.&quot;&quot;&quot;</span></span>
<span id="cb10-91"><a href="#cb10-91" aria-hidden="true" tabindex="-1"></a> rng <span class="op">=</span> np.random.default_rng(saat)</span>
<span id="cb10-92"><a href="#cb10-92" aria-hidden="true" tabindex="-1"></a> lager <span class="op">=</span> [<span class="ss">f&quot;Lager_</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="st">&quot;ABCDEF&quot;</span>]</span>
<span id="cb10-93"><a href="#cb10-93" aria-hidden="true" tabindex="-1"></a> kunden <span class="op">=</span> [<span class="ss">f&quot;Kunde_</span><span class="sc">{</span>k<span class="sc">:02d}</span><span class="ss">&quot;</span> <span class="cf">for</span> k <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">1</span>, <span class="dv">13</span>)]</span>
<span id="cb10-94"><a href="#cb10-94" aria-hidden="true" tabindex="-1"></a> bedarf <span class="op">=</span> rng.integers(<span class="dv">10</span>, <span class="dv">60</span>, <span class="bu">len</span>(kunden))</span>
<span id="cb10-95"><a href="#cb10-95" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Standortproblem(</span>
<span id="cb10-96"><a href="#cb10-96" aria-hidden="true" tabindex="-1"></a> lager<span class="op">=</span>lager,</span>
<span id="cb10-97"><a href="#cb10-97" aria-hidden="true" tabindex="-1"></a> kunden<span class="op">=</span>kunden,</span>
<span id="cb10-98"><a href="#cb10-98" aria-hidden="true" tabindex="-1"></a> fixkosten<span class="op">=</span>rng.integers(<span class="dv">3000</span>, <span class="dv">9000</span>, <span class="bu">len</span>(lager)).tolist(),</span>
<span id="cb10-99"><a href="#cb10-99" aria-hidden="true" tabindex="-1"></a> kapazitaet<span class="op">=</span>[<span class="bu">int</span>(bedarf.<span class="bu">sum</span>() <span class="op">*</span> <span class="fl">0.45</span>)] <span class="op">*</span> <span class="bu">len</span>(lager),</span>
<span id="cb10-100"><a href="#cb10-100" aria-hidden="true" tabindex="-1"></a> bedarf<span class="op">=</span>bedarf.tolist(),</span>
<span id="cb10-101"><a href="#cb10-101" aria-hidden="true" tabindex="-1"></a> transport<span class="op">=</span>rng.integers(<span class="dv">200</span>, <span class="dv">1800</span>, (<span class="bu">len</span>(lager), <span class="bu">len</span>(kunden))).tolist(),</span>
<span id="cb10-102"><a href="#cb10-102" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb10-103"><a href="#cb10-103" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-104"><a href="#cb10-104" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-105"><a href="#cb10-105" aria-hidden="true" tabindex="-1"></a><span class="co"># --- 2. Der eine Baustein, der sich aendert: der Modellbauer -----------------</span></span>
<span id="cb10-106"><a href="#cb10-106" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-107"><a href="#cb10-107" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> baue_und_loese_mit_cpsat(problem: Standortproblem) <span class="op">-&gt;</span> Loesung:</span>
<span id="cb10-108"><a href="#cb10-108" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;CP-SAT: Bool-Variablen, ganzzahlige Koeffizienten, Minimize.&quot;&quot;&quot;</span></span>
<span id="cb10-109"><a href="#cb10-109" aria-hidden="true" tabindex="-1"></a> <span class="im">from</span> ortools.sat.python <span class="im">import</span> cp_model</span>
<span id="cb10-110"><a href="#cb10-110" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-111"><a href="#cb10-111" aria-hidden="true" tabindex="-1"></a> n, m <span class="op">=</span> <span class="bu">len</span>(problem.lager), <span class="bu">len</span>(problem.kunden)</span>
<span id="cb10-112"><a href="#cb10-112" aria-hidden="true" tabindex="-1"></a> modell <span class="op">=</span> cp_model.CpModel()</span>
<span id="cb10-113"><a href="#cb10-113" aria-hidden="true" tabindex="-1"></a> y <span class="op">=</span> [modell.NewBoolVar(<span class="ss">f&quot;offen_</span><span class="sc">{</span>i<span class="sc">}</span><span class="ss">&quot;</span>) <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n)]</span>
<span id="cb10-114"><a href="#cb10-114" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> {(i, j): modell.NewBoolVar(<span class="ss">f&quot;liefert_</span><span class="sc">{</span>i<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>j<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-115"><a href="#cb10-115" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n) <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(m)}</span>
<span id="cb10-116"><a href="#cb10-116" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-117"><a href="#cb10-117" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(m): <span class="co"># jeder Kunde genau einmal</span></span>
<span id="cb10-118"><a href="#cb10-118" aria-hidden="true" tabindex="-1"></a> modell.AddExactlyOne(x[i, j] <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n))</span>
<span id="cb10-119"><a href="#cb10-119" 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="cb10-120"><a href="#cb10-120" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(m): <span class="co"># nur aus offenen Lagern</span></span>
<span id="cb10-121"><a href="#cb10-121" aria-hidden="true" tabindex="-1"></a> modell.AddImplication(x[i, j], y[i])</span>
<span id="cb10-122"><a href="#cb10-122" aria-hidden="true" tabindex="-1"></a> modell.Add(<span class="bu">sum</span>(problem.bedarf[j] <span class="op">*</span> x[i, j] <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(m))</span>
<span id="cb10-123"><a href="#cb10-123" aria-hidden="true" tabindex="-1"></a> <span class="op">&lt;=</span> problem.kapazitaet[i] <span class="op">*</span> y[i]) <span class="co"># Kapazitaet</span></span>
<span id="cb10-124"><a href="#cb10-124" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-125"><a href="#cb10-125" aria-hidden="true" tabindex="-1"></a> modell.Minimize(</span>
<span id="cb10-126"><a href="#cb10-126" aria-hidden="true" tabindex="-1"></a> <span class="bu">sum</span>(problem.fixkosten[i] <span class="op">*</span> y[i] <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n))</span>
<span id="cb10-127"><a href="#cb10-127" aria-hidden="true" tabindex="-1"></a> <span class="op">+</span> <span class="bu">sum</span>(problem.transport[i][j] <span class="op">*</span> x[i, j] <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n) <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(m)))</span>
<span id="cb10-128"><a href="#cb10-128" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-129"><a href="#cb10-129" aria-hidden="true" tabindex="-1"></a> loeser <span class="op">=</span> cp_model.CpSolver()</span>
<span id="cb10-130"><a href="#cb10-130" aria-hidden="true" tabindex="-1"></a> loeser.parameters.max_time_in_seconds <span class="op">=</span> ZEITLIMIT</span>
<span id="cb10-131"><a href="#cb10-131" aria-hidden="true" tabindex="-1"></a> loeser.parameters.num_workers <span class="op">=</span> <span class="dv">1</span></span>
<span id="cb10-132"><a href="#cb10-132" aria-hidden="true" tabindex="-1"></a> loeser.parameters.random_seed <span class="op">=</span> <span class="dv">1</span></span>
<span id="cb10-133"><a href="#cb10-133" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> status_von_cpsat(loeser.Solve(modell))</span>
<span id="cb10-134"><a href="#cb10-134" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> status.brauchbar:</span>
<span id="cb10-135"><a href="#cb10-135" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Loesung(status<span class="op">=</span>status, laufzeit<span class="op">=</span>loeser.WallTime())</span>
<span id="cb10-136"><a href="#cb10-136" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-137"><a href="#cb10-137" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Loesung(</span>
<span id="cb10-138"><a href="#cb10-138" aria-hidden="true" tabindex="-1"></a> status<span class="op">=</span>status,</span>
<span id="cb10-139"><a href="#cb10-139" aria-hidden="true" tabindex="-1"></a> werte<span class="op">=</span>{problem.schluessel(i, j): <span class="bu">float</span>(loeser.Value(x[i, j]))</span>
<span id="cb10-140"><a href="#cb10-140" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n) <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(m)},</span>
<span id="cb10-141"><a href="#cb10-141" aria-hidden="true" tabindex="-1"></a> zielwert<span class="op">=</span>loeser.ObjectiveValue(),</span>
<span id="cb10-142"><a href="#cb10-142" aria-hidden="true" tabindex="-1"></a> schranke<span class="op">=</span>loeser.BestObjectiveBound(),</span>
<span id="cb10-143"><a href="#cb10-143" aria-hidden="true" tabindex="-1"></a> laufzeit<span class="op">=</span>loeser.WallTime())</span>
<span id="cb10-144"><a href="#cb10-144" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-145"><a href="#cb10-145" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-146"><a href="#cb10-146" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> baue_und_loese_mit_highs(problem: Standortproblem) <span class="op">-&gt;</span> Loesung:</span>
<span id="cb10-147"><a href="#cb10-147" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;HiGHS: dieselben Restriktionen als Ungleichungszeilen einer Matrix.&quot;&quot;&quot;</span></span>
<span id="cb10-148"><a href="#cb10-148" aria-hidden="true" tabindex="-1"></a> <span class="im">import</span> highspy</span>
<span id="cb10-149"><a href="#cb10-149" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-150"><a href="#cb10-150" aria-hidden="true" tabindex="-1"></a> n, m <span class="op">=</span> <span class="bu">len</span>(problem.lager), <span class="bu">len</span>(problem.kunden)</span>
<span id="cb10-151"><a href="#cb10-151" aria-hidden="true" tabindex="-1"></a> anzahl_x <span class="op">=</span> n <span class="op">*</span> m <span class="co"># Spalten 0..n*m-1</span></span>
<span id="cb10-152"><a href="#cb10-152" aria-hidden="true" tabindex="-1"></a> spalte_y <span class="op">=</span> <span class="kw">lambda</span> i: anzahl_x <span class="op">+</span> i <span class="co"># danach die y_i # noqa: E731</span></span>
<span id="cb10-153"><a href="#cb10-153" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-154"><a href="#cb10-154" aria-hidden="true" tabindex="-1"></a> modell <span class="op">=</span> highspy.Highs()</span>
<span id="cb10-155"><a href="#cb10-155" aria-hidden="true" tabindex="-1"></a> modell.setOptionValue(<span class="st">&quot;output_flag&quot;</span>, <span class="va">False</span>)</span>
<span id="cb10-156"><a href="#cb10-156" aria-hidden="true" tabindex="-1"></a> modell.setOptionValue(<span class="st">&quot;time_limit&quot;</span>, ZEITLIMIT)</span>
<span id="cb10-157"><a href="#cb10-157" aria-hidden="true" tabindex="-1"></a> modell.addVars(anzahl_x <span class="op">+</span> n, np.zeros(anzahl_x <span class="op">+</span> n), np.ones(anzahl_x <span class="op">+</span> n))</span>
<span id="cb10-158"><a href="#cb10-158" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> spalte <span class="kw">in</span> <span class="bu">range</span>(anzahl_x <span class="op">+</span> n):</span>
<span id="cb10-159"><a href="#cb10-159" aria-hidden="true" tabindex="-1"></a> modell.changeColIntegrality(spalte, highspy.HighsVarType.kInteger)</span>
<span id="cb10-160"><a href="#cb10-160" 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="cb10-161"><a href="#cb10-161" aria-hidden="true" tabindex="-1"></a> modell.changeColCost(spalte_y(i), <span class="bu">float</span>(problem.fixkosten[i]))</span>
<span id="cb10-162"><a href="#cb10-162" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(m):</span>
<span id="cb10-163"><a href="#cb10-163" aria-hidden="true" tabindex="-1"></a> modell.changeColCost(i <span class="op">*</span> m <span class="op">+</span> j, <span class="bu">float</span>(problem.transport[i][j]))</span>
<span id="cb10-164"><a href="#cb10-164" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-165"><a href="#cb10-165" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(m): <span class="co"># jeder Kunde genau einmal</span></span>
<span id="cb10-166"><a href="#cb10-166" aria-hidden="true" tabindex="-1"></a> index <span class="op">=</span> np.array([i <span class="op">*</span> m <span class="op">+</span> j <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n)], dtype<span class="op">=</span>np.int32)</span>
<span id="cb10-167"><a href="#cb10-167" aria-hidden="true" tabindex="-1"></a> modell.addRow(<span class="fl">1.0</span>, <span class="fl">1.0</span>, n, index, np.ones(n))</span>
<span id="cb10-168"><a href="#cb10-168" 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="cb10-169"><a href="#cb10-169" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(m): <span class="co"># x_ij - y_i &lt;= 0</span></span>
<span id="cb10-170"><a href="#cb10-170" aria-hidden="true" tabindex="-1"></a> modell.addRow(<span class="op">-</span>highspy.kHighsInf, <span class="fl">0.0</span>, <span class="dv">2</span>,</span>
<span id="cb10-171"><a href="#cb10-171" aria-hidden="true" tabindex="-1"></a> np.array([i <span class="op">*</span> m <span class="op">+</span> j, spalte_y(i)], dtype<span class="op">=</span>np.int32),</span>
<span id="cb10-172"><a href="#cb10-172" aria-hidden="true" tabindex="-1"></a> np.array([<span class="fl">1.0</span>, <span class="op">-</span><span class="fl">1.0</span>]))</span>
<span id="cb10-173"><a href="#cb10-173" aria-hidden="true" tabindex="-1"></a> index <span class="op">=</span> np.array([i <span class="op">*</span> m <span class="op">+</span> j <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(m)] <span class="op">+</span> [spalte_y(i)],</span>
<span id="cb10-174"><a href="#cb10-174" aria-hidden="true" tabindex="-1"></a> dtype<span class="op">=</span>np.int32) <span class="co"># Kapazitaet</span></span>
<span id="cb10-175"><a href="#cb10-175" aria-hidden="true" tabindex="-1"></a> werte <span class="op">=</span> np.concatenate([np.array(problem.bedarf, dtype<span class="op">=</span><span class="bu">float</span>),</span>
<span id="cb10-176"><a href="#cb10-176" aria-hidden="true" tabindex="-1"></a> [<span class="op">-</span><span class="bu">float</span>(problem.kapazitaet[i])]])</span>
<span id="cb10-177"><a href="#cb10-177" aria-hidden="true" tabindex="-1"></a> modell.addRow(<span class="op">-</span>highspy.kHighsInf, <span class="fl">0.0</span>, m <span class="op">+</span> <span class="dv">1</span>, index, werte)</span>
<span id="cb10-178"><a href="#cb10-178" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-179"><a href="#cb10-179" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()</span>
<span id="cb10-180"><a href="#cb10-180" aria-hidden="true" tabindex="-1"></a> modell.run()</span>
<span id="cb10-181"><a href="#cb10-181" aria-hidden="true" tabindex="-1"></a> laufzeit <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
<span id="cb10-182"><a href="#cb10-182" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-183"><a href="#cb10-183" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> status_von_highs(modell.modelStatusToString(modell.getModelStatus()))</span>
<span id="cb10-184"><a href="#cb10-184" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> status.brauchbar:</span>
<span id="cb10-185"><a href="#cb10-185" 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="cb10-186"><a href="#cb10-186" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-187"><a href="#cb10-187" aria-hidden="true" tabindex="-1"></a> loesungswerte <span class="op">=</span> modell.getSolution().col_value</span>
<span id="cb10-188"><a href="#cb10-188" aria-hidden="true" tabindex="-1"></a> info <span class="op">=</span> modell.getInfo()</span>
<span id="cb10-189"><a href="#cb10-189" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Loesung(</span>
<span id="cb10-190"><a href="#cb10-190" aria-hidden="true" tabindex="-1"></a> status<span class="op">=</span>status,</span>
<span id="cb10-191"><a href="#cb10-191" aria-hidden="true" tabindex="-1"></a> werte<span class="op">=</span>{problem.schluessel(i, j): <span class="bu">float</span>(loesungswerte[i <span class="op">*</span> m <span class="op">+</span> j])</span>
<span id="cb10-192"><a href="#cb10-192" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n) <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(m)},</span>
<span id="cb10-193"><a href="#cb10-193" aria-hidden="true" tabindex="-1"></a> zielwert<span class="op">=</span>info.objective_function_value,</span>
<span id="cb10-194"><a href="#cb10-194" aria-hidden="true" tabindex="-1"></a> schranke<span class="op">=</span>info.mip_dual_bound,</span>
<span id="cb10-195"><a href="#cb10-195" aria-hidden="true" tabindex="-1"></a> laufzeit<span class="op">=</span>laufzeit)</span>
<span id="cb10-196"><a href="#cb10-196" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-197"><a href="#cb10-197" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-198"><a href="#cb10-198" aria-hidden="true" tabindex="-1"></a>MODELLBAUER <span class="op">=</span> {<span class="st">&quot;cpsat&quot;</span>: baue_und_loese_mit_cpsat, <span class="st">&quot;highs&quot;</span>: baue_und_loese_mit_highs}</span>
<span id="cb10-199"><a href="#cb10-199" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-200"><a href="#cb10-200" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-201"><a href="#cb10-201" aria-hidden="true" tabindex="-1"></a><span class="co"># --- 3. Alles Weitere ist wieder gemeinsam -----------------------------------</span></span>
<span id="cb10-202"><a href="#cb10-202" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-203"><a href="#cb10-203" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> pruefe_zuordnung(problem: Standortproblem, loesung: Loesung,</span>
<span id="cb10-204"><a href="#cb10-204" aria-hidden="true" tabindex="-1"></a> 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="cb10-205"><a href="#cb10-205" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Prueft die Loesung gegen die Anforderung - ohne Solver, ohne Modell.&quot;&quot;&quot;</span></span>
<span id="cb10-206"><a href="#cb10-206" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> loesung.status.brauchbar:</span>
<span id="cb10-207"><a href="#cb10-207" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> [<span class="ss">f&quot;kein verwertbares Ergebnis (</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="cb10-208"><a href="#cb10-208" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-209"><a href="#cb10-209" aria-hidden="true" tabindex="-1"></a> n, m <span class="op">=</span> <span class="bu">len</span>(problem.lager), <span class="bu">len</span>(problem.kunden)</span>
<span id="cb10-210"><a href="#cb10-210" aria-hidden="true" tabindex="-1"></a> zuordnung <span class="op">=</span> np.array([[loesung.werte[problem.schluessel(i, j)]</span>
<span id="cb10-211"><a href="#cb10-211" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(m)] <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n)])</span>
<span id="cb10-212"><a href="#cb10-212" aria-hidden="true" tabindex="-1"></a> beanstandungen: <span class="bu">list</span>[<span class="bu">str</span>] <span class="op">=</span> []</span>
<span id="cb10-213"><a href="#cb10-213" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-214"><a href="#cb10-214" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (np.<span class="bu">abs</span>(zuordnung <span class="op">-</span> np.<span class="bu">round</span>(zuordnung)) <span class="op">&gt;</span> toleranz).<span class="bu">any</span>():</span>
<span id="cb10-215"><a href="#cb10-215" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="st">&quot;Zuordnungen sind nicht 0/1&quot;</span>)</span>
<span id="cb10-216"><a href="#cb10-216" aria-hidden="true" tabindex="-1"></a> zuordnung <span class="op">=</span> np.<span class="bu">round</span>(zuordnung)</span>
<span id="cb10-217"><a href="#cb10-217" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-218"><a href="#cb10-218" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j, kunde <span class="kw">in</span> <span class="bu">enumerate</span>(problem.kunden):</span>
<span id="cb10-219"><a href="#cb10-219" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(zuordnung[:, j].<span class="bu">sum</span>() <span class="op">-</span> <span class="fl">1.0</span>) <span class="op">&gt;</span> toleranz:</span>
<span id="cb10-220"><a href="#cb10-220" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;</span><span class="sc">{</span>kunde<span class="sc">}</span><span class="ss"> wird </span><span class="sc">{</span>zuordnung[:, j]<span class="sc">.</span><span class="bu">sum</span>()<span class="sc">:.0f}</span><span class="ss">-mal beliefert&quot;</span>)</span>
<span id="cb10-221"><a href="#cb10-221" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-222"><a href="#cb10-222" aria-hidden="true" tabindex="-1"></a> beliefert <span class="op">=</span> zuordnung <span class="op">@</span> np.array(problem.bedarf, dtype<span class="op">=</span><span class="bu">float</span>)</span>
<span id="cb10-223"><a href="#cb10-223" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i, lagername <span class="kw">in</span> <span class="bu">enumerate</span>(problem.lager):</span>
<span id="cb10-224"><a href="#cb10-224" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> beliefert[i] <span class="op">&gt;</span> problem.kapazitaet[i] <span class="op">+</span> toleranz:</span>
<span id="cb10-225"><a href="#cb10-225" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;</span><span class="sc">{</span>lagername<span class="sc">}</span><span class="ss">: </span><span class="sc">{</span>beliefert[i]<span class="sc">:.0f}</span><span class="ss"> Paletten ueber &quot;</span></span>
<span id="cb10-226"><a href="#cb10-226" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Kapazitaet </span><span class="sc">{</span>problem<span class="sc">.</span>kapazitaet[i]<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-227"><a href="#cb10-227" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-228"><a href="#cb10-228" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> loesung.zielwert <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span>
<span id="cb10-229"><a href="#cb10-229" aria-hidden="true" tabindex="-1"></a> offen <span class="op">=</span> beliefert <span class="op">&gt;</span> toleranz</span>
<span id="cb10-230"><a href="#cb10-230" aria-hidden="true" tabindex="-1"></a> nachgerechnet <span class="op">=</span> (np.array(problem.fixkosten, dtype<span class="op">=</span><span class="bu">float</span>) <span class="op">@</span> offen</span>
<span id="cb10-231"><a href="#cb10-231" aria-hidden="true" tabindex="-1"></a> <span class="op">+</span> (np.array(problem.transport, dtype<span class="op">=</span><span class="bu">float</span>) <span class="op">*</span> zuordnung).<span class="bu">sum</span>())</span>
<span id="cb10-232"><a href="#cb10-232" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(nachgerechnet <span class="op">-</span> loesung.zielwert) <span class="op">&gt;</span> <span class="fl">0.5</span>:</span>
<span id="cb10-233"><a href="#cb10-233" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;Zielwert </span><span class="sc">{</span>loesung<span class="sc">.</span>zielwert<span class="sc">:,.0f}</span><span class="ss"> passt nicht zur &quot;</span></span>
<span id="cb10-234"><a href="#cb10-234" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Zuordnung (nachgerechnet </span><span class="sc">{</span>nachgerechnet<span class="sc">:,.0f}</span><span class="ss">)&quot;</span>)</span>
<span id="cb10-235"><a href="#cb10-235" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> beanstandungen</span>
<span id="cb10-236"><a href="#cb10-236" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-237"><a href="#cb10-237" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-238"><a href="#cb10-238" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> geoeffnete_lager(problem: Standortproblem, loesung: Loesung) <span class="op">-&gt;</span> <span class="bu">list</span>[<span class="bu">str</span>]:</span>
<span id="cb10-239"><a href="#cb10-239" aria-hidden="true" tabindex="-1"></a> m <span class="op">=</span> <span class="bu">len</span>(problem.kunden)</span>
<span id="cb10-240"><a href="#cb10-240" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> [name <span class="cf">for</span> i, name <span class="kw">in</span> <span class="bu">enumerate</span>(problem.lager)</span>
<span id="cb10-241"><a href="#cb10-241" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">any</span>(loesung.werte[problem.schluessel(i, j)] <span class="op">&gt;</span> <span class="fl">0.5</span> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(m))]</span>
<span id="cb10-242"><a href="#cb10-242" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-243"><a href="#cb10-243" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-244"><a href="#cb10-244" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese_in_eigenem_prozess(name: <span class="bu">str</span>, problem: Standortproblem) <span class="op">-&gt;</span> Loesung:</span>
<span id="cb10-245"><a href="#cb10-245" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Laesst genau einen Modellbauer in einem frischen Prozess rechnen.</span></span>
<span id="cb10-246"><a href="#cb10-246" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-247"><a href="#cb10-247" aria-hidden="true" tabindex="-1"></a><span class="co"> &#39;spawn&#39; statt des Linux-Standards &#39;fork&#39;: Der Kindprozess startet mit</span></span>
<span id="cb10-248"><a href="#cb10-248" aria-hidden="true" tabindex="-1"></a><span class="co"> einem leeren Interpreter und importiert nur den Solver, den SEIN</span></span>
<span id="cb10-249"><a href="#cb10-249" aria-hidden="true" tabindex="-1"></a><span class="co"> Modellbauer braucht. max_tasks_per_child=1 sorgt dafuer, dass der Pool</span></span>
<span id="cb10-250"><a href="#cb10-250" aria-hidden="true" tabindex="-1"></a><span class="co"> seinen Arbeiter nicht wiederverwendet - sonst saessen beim zweiten Aufruf</span></span>
<span id="cb10-251"><a href="#cb10-251" aria-hidden="true" tabindex="-1"></a><span class="co"> wieder beide Bibliotheken im selben Prozess.</span></span>
<span id="cb10-252"><a href="#cb10-252" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-253"><a href="#cb10-253" aria-hidden="true" tabindex="-1"></a><span class="co"> Hin und zurueck wandert das Domaenenmodell bzw. das Loesungs-DTO. Beide</span></span>
<span id="cb10-254"><a href="#cb10-254" aria-hidden="true" tabindex="-1"></a><span class="co"> kennen keinen Solver, sind also serialisierbar - genau dafuer sind sie da.</span></span>
<span id="cb10-255"><a href="#cb10-255" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb10-256"><a href="#cb10-256" aria-hidden="true" tabindex="-1"></a> <span class="cf">with</span> ProcessPoolExecutor(</span>
<span id="cb10-257"><a href="#cb10-257" aria-hidden="true" tabindex="-1"></a> max_workers<span class="op">=</span><span class="dv">1</span>,</span>
<span id="cb10-258"><a href="#cb10-258" aria-hidden="true" tabindex="-1"></a> mp_context<span class="op">=</span>multiprocessing.get_context(<span class="st">&quot;spawn&quot;</span>),</span>
<span id="cb10-259"><a href="#cb10-259" aria-hidden="true" tabindex="-1"></a> max_tasks_per_child<span class="op">=</span><span class="dv">1</span>) <span class="im">as</span> pool:</span>
<span id="cb10-260"><a href="#cb10-260" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> pool.submit(MODELLBAUER[name], problem).result(timeout<span class="op">=</span><span class="dv">300</span>)</span>
<span id="cb10-261"><a href="#cb10-261" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-262"><a href="#cb10-262" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-263"><a href="#cb10-263" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb10-264"><a href="#cb10-264" aria-hidden="true" tabindex="-1"></a> problem <span class="op">=</span> beispielproblem()</span>
<span id="cb10-265"><a href="#cb10-265" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-266"><a href="#cb10-266" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Beide Solver anstossen und vergleichen ---------------------------</span></span>
<span id="cb10-267"><a href="#cb10-267" 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="cb10-268"><a href="#cb10-268" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; DERSELBE FALL, ZWEI SOLVER - UND EIN AUSWERTUNGSCODE&quot;</span>)</span>
<span id="cb10-269"><a href="#cb10-269" 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="cb10-270"><a href="#cb10-270" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Standortplanung: </span><span class="sc">{</span><span class="bu">len</span>(problem.lager)<span class="sc">}</span><span class="ss"> moegliche Lager, &quot;</span></span>
<span id="cb10-271"><a href="#cb10-271" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">len</span>(problem.kunden)<span class="sc">}</span><span class="ss"> Kunden, </span><span class="sc">{</span><span class="bu">sum</span>(problem.bedarf)<span class="sc">}</span><span class="ss"> Paletten Bedarf.&quot;</span>)</span>
<span id="cb10-272"><a href="#cb10-272" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Kapazitaet je Lager: </span><span class="sc">{</span>problem<span class="sc">.</span>kapazitaet[<span class="dv">0</span>]<span class="sc">}</span><span class="ss"> Paletten &quot;</span></span>
<span id="cb10-273"><a href="#cb10-273" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;-&gt; mindestens 3 Lager noetig.</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb10-274"><a href="#cb10-274" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-275"><a href="#cb10-275" aria-hidden="true" tabindex="-1"></a> loesungen: <span class="bu">dict</span>[<span class="bu">str</span>, Loesung] <span class="op">=</span> {}</span>
<span id="cb10-276"><a href="#cb10-276" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> name, beschriftung <span class="kw">in</span> [(<span class="st">&quot;cpsat&quot;</span>, <span class="st">&quot;OR-Tools CP-SAT&quot;</span>),</span>
<span id="cb10-277"><a href="#cb10-277" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;highs&quot;</span>, <span class="st">&quot;HiGHS (highspy)&quot;</span>)]:</span>
<span id="cb10-278"><a href="#cb10-278" aria-hidden="true" tabindex="-1"></a> loesung <span class="op">=</span> loesungen[name] <span class="op">=</span> loese_in_eigenem_prozess(name, problem)</span>
<span id="cb10-279"><a href="#cb10-279" aria-hidden="true" tabindex="-1"></a> beanstandungen <span class="op">=</span> pruefe_zuordnung(problem, loesung)</span>
<span id="cb10-280"><a href="#cb10-280" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-281"><a href="#cb10-281" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>beschriftung<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-282"><a href="#cb10-282" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>loesung<span class="sc">.</span>als_bericht()<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-283"><a href="#cb10-283" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; eroeffnete Lager: </span><span class="sc">{</span><span class="st">&#39;, &#39;</span><span class="sc">.</span>join(geoeffnete_lager(problem, loesung))<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-284"><a href="#cb10-284" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Abnahmepruefung: &quot;</span></span>
<span id="cb10-285"><a href="#cb10-285" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;bestanden&#39;</span> <span class="cf">if</span> <span class="kw">not</span> beanstandungen <span class="cf">else</span> beanstandungen<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-286"><a href="#cb10-286" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-287"><a href="#cb10-287" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Was der Vergleich zeigt -----------------------------------------</span></span>
<span id="cb10-288"><a href="#cb10-288" aria-hidden="true" tabindex="-1"></a> zielwerte <span class="op">=</span> [loesung.zielwert <span class="cf">for</span> loesung <span class="kw">in</span> loesungen.values()]</span>
<span id="cb10-289"><a href="#cb10-289" 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="cb10-290"><a href="#cb10-290" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Zielwertdifferenz: </span><span class="sc">{</span><span class="bu">abs</span>(zielwerte[<span class="dv">0</span>] <span class="op">-</span> zielwerte[<span class="dv">1</span>])<span class="sc">:.6f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb10-291"><a href="#cb10-291" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-292"><a href="#cb10-292" aria-hidden="true" tabindex="-1"></a> gleich_belegt <span class="op">=</span> <span class="bu">all</span>(</span>
<span id="cb10-293"><a href="#cb10-293" aria-hidden="true" tabindex="-1"></a> <span class="bu">round</span>(loesungen[<span class="st">&quot;cpsat&quot;</span>].werte[s]) <span class="op">==</span> <span class="bu">round</span>(loesungen[<span class="st">&quot;highs&quot;</span>].werte[s])</span>
<span id="cb10-294"><a href="#cb10-294" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> s <span class="kw">in</span> loesungen[<span class="st">&quot;cpsat&quot;</span>].werte)</span>
<span id="cb10-295"><a href="#cb10-295" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Identische Zuordnung: </span><span class="sc">{</span><span class="st">&#39;ja&#39;</span> <span class="cf">if</span> gleich_belegt <span class="cf">else</span> <span class="st">&#39;nein&#39;</span><span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-296"><a href="#cb10-296" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-297"><a href="#cb10-297" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> <span class="bu">abs</span>(zielwerte[<span class="dv">0</span>] <span class="op">-</span> zielwerte[<span class="dv">1</span>]) <span class="op">&lt;</span> <span class="fl">0.5</span>, <span class="st">&quot;Die Solver widersprechen sich!&quot;</span></span>
<span id="cb10-298"><a href="#cb10-298" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> <span class="bu">all</span>(l.status <span class="kw">is</span> SolverStatus.OPTIMAL <span class="cf">for</span> l <span class="kw">in</span> loesungen.values())</span>
<span id="cb10-299"><a href="#cb10-299" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-300"><a href="#cb10-300" 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">82</span>)</span>
<span id="cb10-301"><a href="#cb10-301" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; WAS DER WECHSEL GEKOSTET HAT&quot;</span>)</span>
<span id="cb10-302"><a href="#cb10-302" 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="cb10-303"><a href="#cb10-303" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Ausgetauscht wurde EINE Funktion. Domaenenmodell, Abnahmepruefung und&quot;</span>)</span>
<span id="cb10-304"><a href="#cb10-304" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Bericht sind woertlich dieselben - sie sehen den Solver nie.&quot;</span>)</span>
<span id="cb10-305"><a href="#cb10-305" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb10-306"><a href="#cb10-306" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Nicht umsonst ist der Wechsel trotzdem:&quot;</span>)</span>
<span id="cb10-307"><a href="#cb10-307" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; * CP-SAT rechnet ausschliesslich GANZZAHLIG. Alle Kosten sind hier&quot;</span>)</span>
<span id="cb10-308"><a href="#cb10-308" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; deshalb int. Wer in Euro und Cent rechnet, skaliert vorher auf Cent -&quot;</span>)</span>
<span id="cb10-309"><a href="#cb10-309" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; und muss das im Bericht wieder zuruecknehmen.&quot;</span>)</span>
<span id="cb10-310"><a href="#cb10-310" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; * HiGHS braucht die Restriktionen als Matrixzeilen, CP-SAT nimmt sie&quot;</span>)</span>
<span id="cb10-311"><a href="#cb10-311" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; als Ausdruecke. Das ist der Grund, warum der HiGHS-Modellbauer&quot;</span>)</span>
<span id="cb10-312"><a href="#cb10-312" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; laenger ist, obwohl er dasselbe Modell beschreibt.&quot;</span>)</span>
<span id="cb10-313"><a href="#cb10-313" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; * Beide Bibliotheken bringen eine eigene HiGHS-Kopie mit und lassen&quot;</span>)</span>
<span id="cb10-314"><a href="#cb10-314" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; sich nicht gemeinsam importieren - daher die zwei Prozesse.&quot;</span>)</span>
<span id="cb10-315"><a href="#cb10-315" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb10-316"><a href="#cb10-316" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Der Ertrag: Beide beweisen denselben optimalen Zielwert, und die&quot;</span>)</span>
<span id="cb10-317"><a href="#cb10-317" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Entscheidung zwischen ihnen ist eine Frage der Laufzeit geworden -&quot;</span>)</span>
<span id="cb10-318"><a href="#cb10-318" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;nicht eine Frage, wie viel Code man neu schreiben muss.&quot;</span>)</span>
<span id="cb10-319"><a href="#cb10-319" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb10-320"><a href="#cb10-320" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Verglichen wird deshalb der ZIELWERT, nicht der Plan: Gibt es mehrere&quot;</span>)</span>
<span id="cb10-321"><a href="#cb10-321" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;gleich teure Loesungen, darf jeder Solver eine andere davon liefern.&quot;</span>)</span>
<span id="cb10-322"><a href="#cb10-322" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Hier stimmen sie zufaellig ueberein - darauf zu testen waere trotzdem&quot;</span>)</span>
<span id="cb10-323"><a href="#cb10-323" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;ein unzuverlaessiger Test (siehe JobShop_Intervalle.py).&quot;</span>)</span>
<span id="cb10-324"><a href="#cb10-324" 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> (Laufzeiten hardwareabhängig):</p>
<pre><code>==================================================================================
DERSELBE FALL, ZWEI SOLVER - UND EIN AUSWERTUNGSCODE
==================================================================================
Standortplanung: 6 moegliche Lager, 12 Kunden, 387 Paletten Bedarf.
Kapazitaet je Lager: 174 Paletten -&gt; mindestens 3 Lager noetig.
OR-Tools CP-SAT
Status: optimal | Zielwert: 22,010.00 | Gap: 0.00% | Zeit: 0.07s
eroeffnete Lager: Lager_A, Lager_B, Lager_D
Abnahmepruefung: bestanden
HiGHS (highspy)
Status: optimal | Zielwert: 22,010.00 | Gap: 0.00% | Zeit: 0.02s
eroeffnete Lager: Lager_A, Lager_B, Lager_D
Abnahmepruefung: bestanden
----------------------------------------------------------------------------------
Zielwertdifferenz: 0.000000 EUR
Identische Zuordnung: ja
==================================================================================
WAS DER WECHSEL GEKOSTET HAT
==================================================================================
Ausgetauscht wurde EINE Funktion. Domaenenmodell, Abnahmepruefung und
Bericht sind woertlich dieselben - sie sehen den Solver nie.
Nicht umsonst ist der Wechsel trotzdem:
* CP-SAT rechnet ausschliesslich GANZZAHLIG. Alle Kosten sind hier
deshalb int. Wer in Euro und Cent rechnet, skaliert vorher auf Cent -
und muss das im Bericht wieder zuruecknehmen.
* HiGHS braucht die Restriktionen als Matrixzeilen, CP-SAT nimmt sie
als Ausdruecke. Das ist der Grund, warum der HiGHS-Modellbauer
laenger ist, obwohl er dasselbe Modell beschreibt.
* Beide Bibliotheken bringen eine eigene HiGHS-Kopie mit und lassen
sich nicht gemeinsam importieren - daher die zwei Prozesse.
Der Ertrag: Beide beweisen denselben optimalen Zielwert, und die
Entscheidung zwischen ihnen ist eine Frage der Laufzeit geworden -
nicht eine Frage, wie viel Code man neu schreiben muss.
Verglichen wird deshalb der ZIELWERT, nicht der Plan: Gibt es mehrere
gleich teure Loesungen, darf jeder Solver eine andere davon liefern.
Hier stimmen sie zufaellig ueberein - darauf zu testen waere trotzdem
ein unzuverlaessiger Test (siehe JobShop_Intervalle.py).
==================================================================================</code></pre>
<blockquote>
<p><strong>💻 Was dieser Vergleich belegt — und was er kostet</strong></p>
<p><strong>Belegt:</strong> Beide Solver beweisen denselben Zielwert von 22 010 €. Domänenmodell, Abnahmeprüfung und Bericht sind wörtlich dieselben; sie sehen den Solver nie. Die Entscheidung zwischen CP-SAT und HiGHS ist damit zu einer Laufzeitfrage geworden — man kann beide messen, statt sich vorher festlegen zu müssen.</p>
<p><strong>Kostet:</strong> Der Wechsel ist nicht gratis, und es wäre unehrlich, das zu verschweigen.</p>
<ul>
<li><strong>CP-SAT rechnet ausschließlich ganzzahlig.</strong> Alle Kosten im <code>Standortproblem</code> sind deshalb <code>int</code>. Wer in Euro und Cent rechnet, skaliert vor dem Modellaufbau auf Cent — und muss die Skalierung im Bericht zurücknehmen. Diese Umrechnung ist eine klassische Fehlerquelle; sie gehört an genau eine Stelle und in einen Test.</li>
<li><strong>Der HiGHS-Modellbauer ist ein Drittel länger</strong>, obwohl er dasselbe Modell beschreibt. Der Unterschied ist nicht Qualität, sondern Schnittstelle: <code>AddImplication(x, y)</code> gegen eine Matrixzeile mit den Koeffizienten <span class="math inline">+1</span> und <span class="math inline">-1</span>.</li>
<li><strong>Verglichen wird der Zielwert, nicht der Plan.</strong> Gibt es mehrere gleich teure Lösungen, darf jeder Solver eine andere davon liefern — hier stimmen sie überein, aber darauf zu testen wäre unzuverlässig. Denselben Fehler zeigt <code>JobShop_Intervalle.py</code> in <a href="cpsat.html#kap-cpsat">Kapitel 7</a>.</li>
</ul>
</blockquote>
<blockquote>
<p><strong>🎯 Merksatz</strong> Die Frage „welcher Solver ist der beste?“ ist schlecht gestellt. Die gute Frage lautet: „Wie teuer ist es, den Solver zu wechseln?“ Wer sie mit <em>eine Funktion</em> beantworten kann, muss die erste Frage nie entscheiden — er misst.</p>
</blockquote>
<hr />
<h2 id="sec:praxisfallen-eine-checkliste-vor-dem-produktivgang">22.8 Eine Checkliste vor dem Produktivgang</h2>
<p>Arbeiten Sie diese Liste ab, bevor ein OR-System in Betrieb geht:</p>
<p><strong>Modell</strong> - [ ] Alle Nebenbedingungen sind dokumentiert — mit fachlicher Begründung, nicht nur als Code. - [ ] Für jede harte Bedingung ist geprüft: Muss sie wirklich hart sein? - [ ] Es gibt eine Relaxationsstrategie für den Infeasible-Fall. - [ ] Big-M-Werte sind so klein wie möglich gewählt. - [ ] Die Zielfunktion ist in konsistenten Einheiten formuliert.</p>
<p><strong>Daten</strong> - [ ] Spaltenreihenfolge und Datentypen werden nach jedem Import geprüft. - [ ] Fehlende Werte und Ausreißer haben eine definierte Behandlung. - [ ] Kovarianzmatrizen werden auf positive Definitheit geprüft. - [ ] Datenstände sind versioniert und reproduzierbar.</p>
<p><strong>Validierung</strong> - [ ] Das Ergebnis wurde gegen eine unabhängige Rechnung geprüft (Handrechnung, zweiter Solver, Simulation). - [ ] Alle Nebenbedingungen werden nach dem Lösen per <code>assert</code> verifiziert. - [ ] Es gibt einen Vergleich gegen eine naive Referenzstrategie. - [ ] Bei Backtests: Lookahead-Selbsttest bestanden, Zahl der Versuche protokolliert.</p>
<p><strong>Betrieb</strong> - [ ] Zeitlimit und akzeptierter Gap sind festgelegt und begründet. - [ ] Es gibt eine Fallback-Strategie und Alarmierung. - [ ] Jedes Ergebnis ist mit Snapshot-ID, Modellversion und Solver-Version protokolliert. - [ ] Die Ergebnisse sind erklärbar (Kostenzerlegung, Schattenpreise, Constraint-Trace).</p>
<hr />
<h2 id="sec:praxisfallen-weiterfuehrende-literatur-und-roadmap">22.9 Weiterführende Literatur und Roadmap</h2>
<p><strong>Grundlagen und lineare/ganzzahlige Optimierung</strong> * Bertsimas &amp; Tsitsiklis: <em>Introduction to Linear Optimization</em> — Standardwerk zu Simplex, Dualität, Polyedertheorie. * Wolsey: <em>Integer Programming</em> — Branch-and-Cut, Schnittebenen, Formulierungsstärke.</p>
<p><strong>Konvexe Optimierung</strong> * Boyd &amp; Vandenberghe: <em>Convex Optimization</em> — Pflichtlektüre; frei verfügbar. Die theoretische Basis von CVXPY.</p>
<p><strong>Constraint Programming und Scheduling</strong> * Google OR-Tools Dokumentation und das CP-SAT-Primer von Laurent Perron.</p>
<p><strong>Quantitative Finanzmathematik</strong> * López de Prado: <em>Advances in Financial Machine Learning</em> — Backtest-Overfitting, Denoising, Deflated Sharpe Ratio. * Cornuéjols, Peña &amp; Tütüncü: <em>Optimization Methods in Finance</em>.</p>
<p><strong>Wie es weitergeht</strong></p>
<p>Der nächste Schritt nach diesem Kurs ist selten ein weiteres Verfahren, sondern die <strong>Verankerung im Betrieb</strong>: reproduzierbare Datenpipelines, versionierte Modelle, Monitoring der Lösungsqualität, bewusster Umgang mit Zeitlimits.</p>
<p>Wenn Sie doch tiefer in die Methodik wollen, sind das die lohnendsten Richtungen:</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Richtung</th>
<th>Wofür</th>
<th>Einstieg</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Column Generation / Branch-and-Price</strong></td>
<td>sehr große Zuschnitt- und Dienstplanprobleme</td>
<td><a href="dekomposition.html#kap-dekomposition">Kapitel 10</a>, Wolsey</td>
</tr>
<tr class="even">
<td><strong>Benders-Zerlegung</strong></td>
<td>zweistufige stochastische Modelle mit vielen Szenarien</td>
<td>Birge &amp; Louveaux</td>
</tr>
<tr class="odd">
<td><strong>Approximate Dynamic Programming / RL</strong></td>
<td>hochdimensionale Zustandsräume</td>
<td>Powell: <em>Reinforcement Learning and Stochastic Optimization</em></td>
</tr>
<tr class="even">
<td><strong>Konische Optimierung (SOCP, SDP)</strong></td>
<td>robuste Portfolios, Ellipsoid-Unsicherheit</td>
<td>Boyd &amp; Vandenberghe, <a href="lp.html#kap-lp">Kapitel 5</a></td>
</tr>
<tr class="odd">
<td><strong>Multi-Objective Optimization</strong></td>
<td>echte Zielkonflikte ohne Gewichtung</td>
<td>Ehrgott: <em>Multicriteria Optimization</em></td>
</tr>
</tbody>
</table>
<hr />
<h2 id="sec:praxisfallen-uebungsaufgaben">22.10 Übungsaufgaben</h2>
<blockquote>
<p>Lösungen: <a href="anhang-loesungen.html#sec:loesungen-praxisfallen">Abschnitt A.22</a>.</p>
</blockquote>
<p><strong>Aufgabe 22.1 ⭐ — Hart oder weich, revisited.</strong> Nennen Sie für ein Vertretungsplanungssystem je zwei Bedingungen, die (a) zwingend hart bleiben müssen, (b) unbedingt weich sein sollten, (c) diskutabel sind. Begründen Sie.</p>
<p><strong>Aufgabe 22.2 ⭐ — Zeitlimit wählen.</strong> Für welche Szenarien setzen Sie welches Zeitlimit und welchen Gap? (a) Taxi-Disposition in Echtzeit, (b) Wochendienstplan, freitags erstellt, (c) Jahresproduktionsplanung, (d) Portfolio-Rebalancing monatlich.</p>
<p><strong>Aufgabe 22.3 ⭐⭐ — Relaxation einbauen.</strong> Nehmen Sie Ihr Modell aus der Aufgabe <em>Eigener Dienstplan</em> (<a href="cpsat.html#sec:cpsat-uebungsaufgaben">Abschnitt 7.9</a>) und machen Sie es <code>INFEASIBLE</code>-sicher: Schlupfvariablen für unbesetzte Schichten, gestaffelte Strafen. Testen Sie mit einem bewusst überlasteten Szenario.</p>
<p><strong>Aufgabe 22.4 ⭐⭐ — Erklärbarkeit implementieren.</strong> Erweitern Sie ein beliebiges Modell aus dem Kurs um einen <code>erklaere(loesung)</code>-Report, der die drei Fragen aus <a href="#sec:praxisfallen-die-fuenf-typischen-praxisfallen">Abschnitt 22.3</a> beantwortet.</p>
<p><strong>Aufgabe 22.5 ⭐⭐ — Den Bericht in die Irre führen.</strong> <code>Constraint_Attribution.py</code> erzeugt seinen Bericht aus gerechneten Zahlen — richtig ist er deshalb noch nicht. (a) Setzen Sie den Deckungsbeitrag von <em>Deckel</em> von 9 € auf 20 €. Wie ändern sich Plan, Rangliste und Bericht? Welche Sätze bleiben wörtlich stehen, obwohl sie jetzt etwas anderes bedeuten? (b) Der Bericht nennt „Liefervertrag Träger” als teuerste Bindung. Formulieren Sie den Satz so um, dass ein Leser erkennt, <strong>worauf</strong> die Aussage beruht. (c) Welche Angabe müsste das Programm zusätzlich ausgeben, damit ein Empfänger die Belastbarkeit selbst einschätzen kann?</p>
<p><strong>Aufgabe 22.6 ⭐⭐⭐ — Gap gegen Laufzeit.</strong> Nehmen Sie das MILP-Portfolio aus <a href="milp.html#kap-milp">Kapitel 6</a> und vergrößern Sie es auf 50 Anlagen. Messen Sie Laufzeit und Zielwert bei Gap-Vorgaben von 10 %, 5 %, 2 %, 1 %, 0,1 % und 0 %. Stellen Sie den Zusammenhang grafisch dar und leiten Sie eine Empfehlung ab.</p>
<p><strong>Aufgabe 22.7 ⭐⭐⭐ — Post-Mortem schreiben.</strong> Suchen Sie sich einen der im Buch besprochenen Fehler aus (z. B. die Vorzeichenfalle in <a href="lp.html#sec:lp-die-vorzeichenfalle-bei-schattenpreisen">Abschnitt 5.7</a>, die ungültige Kovarianzmatrix in <a href="qp-nlp.html#kap-qp-nlp">Kapitel 11</a>, die Sektor-Positionsindizes in <a href="markowitz.html#kap-markowitz">Kapitel 19</a>, die vermischten Einheiten in <a href="cvar.html#sec:cvar-implementierung-cvar-portfolio-mit-reibung">Abschnitt 20.6</a> oder die Rebalancing-Termine in <a href="handelsmaschine.html#sec:handelsmaschine-rebalancing-termine-richtig-bestimmen">Abschnitt 21.4</a>) und schreiben Sie ein einseitiges Post-Mortem: Was war der Fehler? Warum fiel er nicht auf? Welche Prüfung hätte ihn gefunden? Welche Regel leiten Sie daraus für eigene Projekte ab?</p>
<hr />
<h2 id="sec:praxisfallen-denkfehler">22.11 Finde den Denkfehler</h2>
<p>Falle 5 — die Laufzeitexplosion — zeigt sich im Betrieb selten als Absturz. Sie zeigt sich als <strong>gar nichts</strong>.</p>
<div class="card card-denkfehler">
<blockquote>
<p><strong>🐛 Finde den Denkfehler: Das Modell, das seit einem Jahr nicht mehr optimiert</strong></p>
<p>Ein Standortplanungsmodell geht 2024 in Betrieb. Es läuft jede Nacht, hat drei Sekunden Zeitbudget und liefert zuverlässig die Kosten des Tagesplans. Das Betriebsteam überwacht Abstürze und Laufzeitspitzen — beides tritt nie auf.</p>
<p>Zwei Jahre später ist das Unternehmen von 15 auf 80 Lager gewachsen. Am Code hat niemand etwas geändert; es gab ja keinen Anlass. Das Protokoll sieht so aus:</p>
<table>
<thead>
<tr class="header">
<th>Zeitpunkt</th>
<th style="text-align: right;">Kosten</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>2024-Q1</td>
<td style="text-align: right;">40 484</td>
</tr>
<tr class="even">
<td>2024-Q3</td>
<td style="text-align: right;">51 042</td>
</tr>
<tr class="odd">
<td>2025-Q1</td>
<td style="text-align: right;">64 053</td>
</tr>
<tr class="even">
<td>2025-Q3</td>
<td style="text-align: right;">85 791</td>
</tr>
<tr class="odd">
<td>2026-Q1</td>
<td style="text-align: right;">108 582</td>
</tr>
</tbody>
</table>
<p>Die Kosten steigen — das Unternehmen wächst schließlich. Alles unauffällig.</p>
<p>Tatsächlich liefert das Modell seit <strong>Anfang 2025</strong> nicht mehr das Optimum, sondern eine Lösung, die zuletzt <strong>17,2 %</strong> davon entfernt ist.</p>
<p><strong>Ihre Aufgabe:</strong> (a) Warum ist die gleichbleibende Laufzeit hier kein Zeichen von Stabilität, sondern das Symptom? (b) Welche drei Größen fehlen im Protokoll? (c) Welche der drei warnt am <strong>frühesten</strong> — und warum ist gerade sie so wertvoll? (d) Das Team bemerkt den Fehler 2026. Wie hoch waren die Mehrkosten, und warum lässt sich das nachträglich nicht mehr genau beziffern?</p>
<p><em>Auflösung: <a href="anhang-loesungen.html#sec:loesungen-praxisfallen">Abschnitt A.22</a>.</em></p>
</blockquote>
</div>
<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"># Betriebsueberwachung.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 Praxisfallen: Das Modell, das still aufgehoert hat zu optimieren.</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">Ein Optimierungsmodell geht in Betrieb und laeuft jede Nacht. Es liefert</span></span>
<span id="cb12-8"><a href="#cb12-8" aria-hidden="true" tabindex="-1"></a><span class="co">zuverlaessig eine Zahl, die Job-Dauer bleibt konstant, es gibt keine</span></span>
<span id="cb12-9"><a href="#cb12-9" aria-hidden="true" tabindex="-1"></a><span class="co">Fehlermeldung. Alles sieht gut aus.</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">Waehrenddessen waechst das Unternehmen. Aus 15 Lagern werden 40, dann 80. Das</span></span>
<span id="cb12-12"><a href="#cb12-12" aria-hidden="true" tabindex="-1"></a><span class="co">Zeitlimit von drei Sekunden - vor zwei Jahren grosszuegig bemessen - reicht</span></span>
<span id="cb12-13"><a href="#cb12-13" aria-hidden="true" tabindex="-1"></a><span class="co">irgendwann nicht mehr. Der Solver bricht ab und liefert die beste Loesung, die</span></span>
<span id="cb12-14"><a href="#cb12-14" aria-hidden="true" tabindex="-1"></a><span class="co">er bis dahin gefunden hat.</span></span>
<span id="cb12-15"><a href="#cb12-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-16"><a href="#cb12-16" aria-hidden="true" tabindex="-1"></a><span class="co">Das ist nicht falsch. Es ist sogar richtig so. Aber es ist etwas anderes als</span></span>
<span id="cb12-17"><a href="#cb12-17" aria-hidden="true" tabindex="-1"></a><span class="co">das, was das Modell einmal geleistet hat - und niemand merkt es, weil im</span></span>
<span id="cb12-18"><a href="#cb12-18" aria-hidden="true" tabindex="-1"></a><span class="co">Protokoll nur die Kosten stehen.</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">Dieses Programm zeigt die Verschlechterung und die Ueberwachung, die sie</span></span>
<span id="cb12-21"><a href="#cb12-21" aria-hidden="true" tabindex="-1"></a><span class="co">sichtbar macht.</span></span>
<span id="cb12-22"><a href="#cb12-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-23"><a href="#cb12-23" aria-hidden="true" tabindex="-1"></a><span class="co">WICHTIG: ortools wird hier nicht importiert (Konflikt mit highspy, siehe</span></span>
<span id="cb12-24"><a href="#cb12-24" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel Oekosystem).</span></span>
<span id="cb12-25"><a href="#cb12-25" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-26"><a href="#cb12-26" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, highspy</span></span>
<span id="cb12-27"><a href="#cb12-27" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb12-28"><a href="#cb12-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-29"><a href="#cb12-29" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb12-30"><a href="#cb12-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-31"><a href="#cb12-31" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> time</span>
<span id="cb12-32"><a href="#cb12-32" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> dataclasses <span class="im">import</span> dataclass</span>
<span id="cb12-33"><a href="#cb12-33" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-34"><a href="#cb12-34" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb12-35"><a href="#cb12-35" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> highspy</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>ZEITLIMIT <span class="op">=</span> <span class="fl">3.0</span> <span class="co"># das Budget, das der Nachtjob hat</span></span>
<span id="cb12-38"><a href="#cb12-38" aria-hidden="true" tabindex="-1"></a>GAP_WARNSCHWELLE <span class="op">=</span> <span class="fl">0.02</span> <span class="co"># ab 2 % Gap wollen wir es wissen</span></span>
<span id="cb12-39"><a href="#cb12-39" aria-hidden="true" tabindex="-1"></a>ZEITAUSLASTUNG_WARNSCHWELLE <span class="op">=</span> <span class="fl">0.9</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>
<span id="cb12-42"><a href="#cb12-42" aria-hidden="true" tabindex="-1"></a><span class="at">@dataclass</span></span>
<span id="cb12-43"><a href="#cb12-43" aria-hidden="true" tabindex="-1"></a><span class="kw">class</span> Laufprotokoll:</span>
<span id="cb12-44"><a href="#cb12-44" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Was nach JEDEM Produktivlauf protokolliert gehoert.</span></span>
<span id="cb12-45"><a href="#cb12-45" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-46"><a href="#cb12-46" aria-hidden="true" tabindex="-1"></a><span class="co"> Die meisten Systeme schreiben nur &#39;kosten&#39;. Genau deshalb faellt eine</span></span>
<span id="cb12-47"><a href="#cb12-47" aria-hidden="true" tabindex="-1"></a><span class="co"> schleichende Verschlechterung ueber Monate nicht auf.</span></span>
<span id="cb12-48"><a href="#cb12-48" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb12-49"><a href="#cb12-49" aria-hidden="true" tabindex="-1"></a> zeitpunkt: <span class="bu">str</span></span>
<span id="cb12-50"><a href="#cb12-50" aria-hidden="true" tabindex="-1"></a> lager: <span class="bu">int</span></span>
<span id="cb12-51"><a href="#cb12-51" aria-hidden="true" tabindex="-1"></a> kunden: <span class="bu">int</span></span>
<span id="cb12-52"><a href="#cb12-52" aria-hidden="true" tabindex="-1"></a> status: <span class="bu">str</span></span>
<span id="cb12-53"><a href="#cb12-53" aria-hidden="true" tabindex="-1"></a> kosten: <span class="bu">float</span></span>
<span id="cb12-54"><a href="#cb12-54" aria-hidden="true" tabindex="-1"></a> gap: <span class="bu">float</span></span>
<span id="cb12-55"><a href="#cb12-55" aria-hidden="true" tabindex="-1"></a> laufzeit: <span class="bu">float</span></span>
<span id="cb12-56"><a href="#cb12-56" aria-hidden="true" tabindex="-1"></a> knoten: <span class="bu">int</span></span>
<span id="cb12-57"><a href="#cb12-57" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-58"><a href="#cb12-58" aria-hidden="true" tabindex="-1"></a> <span class="at">@property</span></span>
<span id="cb12-59"><a href="#cb12-59" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> warnungen(<span class="va">self</span>) <span class="op">-&gt;</span> <span class="bu">list</span>[<span class="bu">str</span>]:</span>
<span id="cb12-60"><a href="#cb12-60" aria-hidden="true" tabindex="-1"></a> meldungen <span class="op">=</span> []</span>
<span id="cb12-61"><a href="#cb12-61" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="va">self</span>.status <span class="op">!=</span> <span class="st">&quot;Optimal&quot;</span>:</span>
<span id="cb12-62"><a href="#cb12-62" aria-hidden="true" tabindex="-1"></a> meldungen.append(<span class="ss">f&quot;nicht beweisbar optimal (</span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>status<span class="sc">}</span><span class="ss">)&quot;</span>)</span>
<span id="cb12-63"><a href="#cb12-63" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="va">self</span>.gap <span class="op">&gt;</span> GAP_WARNSCHWELLE:</span>
<span id="cb12-64"><a href="#cb12-64" aria-hidden="true" tabindex="-1"></a> meldungen.append(<span class="ss">f&quot;Gap </span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>gap<span class="sc">:.1%}</span><span class="ss"> ueber Schwelle &quot;</span></span>
<span id="cb12-65"><a href="#cb12-65" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>GAP_WARNSCHWELLE<span class="sc">:.0%}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-66"><a href="#cb12-66" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="va">self</span>.laufzeit <span class="op">&gt;</span> ZEITAUSLASTUNG_WARNSCHWELLE <span class="op">*</span> ZEITLIMIT:</span>
<span id="cb12-67"><a href="#cb12-67" aria-hidden="true" tabindex="-1"></a> meldungen.append(<span class="ss">f&quot;Zeitbudget zu </span><span class="sc">{</span><span class="va">self</span><span class="sc">.</span>laufzeit <span class="op">/</span> ZEITLIMIT<span class="sc">:.0%}</span><span class="ss"> &quot;</span></span>
<span id="cb12-68"><a href="#cb12-68" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;ausgeschoepft&quot;</span>)</span>
<span id="cb12-69"><a href="#cb12-69" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> meldungen</span>
<span id="cb12-70"><a href="#cb12-70" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-71"><a href="#cb12-71" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-72"><a href="#cb12-72" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> plane_netzwerk(n_lager: <span class="bu">int</span>, n_kunden: <span class="bu">int</span>, zeitlimit: <span class="bu">float</span>,</span>
<span id="cb12-73"><a href="#cb12-73" aria-hidden="true" tabindex="-1"></a> saat: <span class="bu">int</span> <span class="op">=</span> <span class="dv">7</span>) <span class="op">-&gt;</span> <span class="bu">tuple</span>[<span class="bu">str</span>, <span class="bu">float</span>, <span class="bu">float</span>, <span class="bu">float</span>, <span class="bu">int</span>]:</span>
<span id="cb12-74"><a href="#cb12-74" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Standortplanung wie in Kapitel MILP, nur mit wachsender Groesse.&quot;&quot;&quot;</span></span>
<span id="cb12-75"><a href="#cb12-75" aria-hidden="true" tabindex="-1"></a> rng <span class="op">=</span> np.random.default_rng(saat)</span>
<span id="cb12-76"><a href="#cb12-76" 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="cb12-77"><a href="#cb12-77" 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_kunden))</span>
<span id="cb12-78"><a href="#cb12-78" aria-hidden="true" tabindex="-1"></a> bedarf <span class="op">=</span> rng.uniform(<span class="dv">10</span>, <span class="dv">60</span>, n_kunden)</span>
<span id="cb12-79"><a href="#cb12-79" 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="cb12-80"><a href="#cb12-80" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-81"><a href="#cb12-81" aria-hidden="true" tabindex="-1"></a> modell <span class="op">=</span> highspy.Highs()</span>
<span id="cb12-82"><a href="#cb12-82" aria-hidden="true" tabindex="-1"></a> modell.setOptionValue(<span class="st">&quot;output_flag&quot;</span>, <span class="va">False</span>)</span>
<span id="cb12-83"><a href="#cb12-83" aria-hidden="true" tabindex="-1"></a> modell.setOptionValue(<span class="st">&quot;time_limit&quot;</span>, zeitlimit)</span>
<span id="cb12-84"><a href="#cb12-84" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-85"><a href="#cb12-85" aria-hidden="true" tabindex="-1"></a> anzahl_x <span class="op">=</span> n_lager <span class="op">*</span> n_kunden</span>
<span id="cb12-86"><a href="#cb12-86" aria-hidden="true" tabindex="-1"></a> unendlich <span class="op">=</span> highspy.kHighsInf</span>
<span id="cb12-87"><a href="#cb12-87" aria-hidden="true" tabindex="-1"></a> modell.addVars(anzahl_x, np.zeros(anzahl_x), np.full(anzahl_x, unendlich))</span>
<span id="cb12-88"><a href="#cb12-88" aria-hidden="true" tabindex="-1"></a> modell.addVars(n_lager, np.zeros(n_lager), np.ones(n_lager))</span>
<span id="cb12-89"><a href="#cb12-89" 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="cb12-90"><a href="#cb12-90" aria-hidden="true" tabindex="-1"></a> modell.changeColIntegrality(anzahl_x <span class="op">+</span> i, highspy.HighsVarType.kInteger)</span>
<span id="cb12-91"><a href="#cb12-91" aria-hidden="true" tabindex="-1"></a> modell.changeColCost(anzahl_x <span class="op">+</span> i, fixkosten[i])</span>
<span id="cb12-92"><a href="#cb12-92" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(n_kunden):</span>
<span id="cb12-93"><a href="#cb12-93" aria-hidden="true" tabindex="-1"></a> modell.changeColCost(i <span class="op">*</span> n_kunden <span class="op">+</span> j, transport[i, j])</span>
<span id="cb12-94"><a href="#cb12-94" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-95"><a href="#cb12-95" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(n_kunden):</span>
<span id="cb12-96"><a href="#cb12-96" aria-hidden="true" tabindex="-1"></a> index <span class="op">=</span> np.array([i <span class="op">*</span> n_kunden <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="cb12-97"><a href="#cb12-97" aria-hidden="true" tabindex="-1"></a> modell.addRow(bedarf[j], bedarf[j], <span class="bu">len</span>(index), index, np.ones(<span class="bu">len</span>(index)))</span>
<span id="cb12-98"><a href="#cb12-98" 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="cb12-99"><a href="#cb12-99" aria-hidden="true" tabindex="-1"></a> index <span class="op">=</span> np.array([i <span class="op">*</span> n_kunden <span class="op">+</span> j <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(n_kunden)] <span class="op">+</span> [anzahl_x <span class="op">+</span> i],</span>
<span id="cb12-100"><a href="#cb12-100" aria-hidden="true" tabindex="-1"></a> dtype<span class="op">=</span>np.int32)</span>
<span id="cb12-101"><a href="#cb12-101" aria-hidden="true" tabindex="-1"></a> werte <span class="op">=</span> np.concatenate([np.ones(n_kunden), [<span class="op">-</span>kapazitaet[i]]])</span>
<span id="cb12-102"><a href="#cb12-102" aria-hidden="true" tabindex="-1"></a> modell.addRow(<span class="op">-</span>unendlich, <span class="fl">0.0</span>, <span class="bu">len</span>(index), index, werte)</span>
<span id="cb12-103"><a href="#cb12-103" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-104"><a href="#cb12-104" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()</span>
<span id="cb12-105"><a href="#cb12-105" aria-hidden="true" tabindex="-1"></a> modell.run()</span>
<span id="cb12-106"><a href="#cb12-106" aria-hidden="true" tabindex="-1"></a> laufzeit <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
<span id="cb12-107"><a href="#cb12-107" aria-hidden="true" tabindex="-1"></a> info <span class="op">=</span> modell.getInfo()</span>
<span id="cb12-108"><a href="#cb12-108" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> (modell.modelStatusToString(modell.getModelStatus()),</span>
<span id="cb12-109"><a href="#cb12-109" aria-hidden="true" tabindex="-1"></a> info.objective_function_value, info.mip_gap, laufzeit,</span>
<span id="cb12-110"><a href="#cb12-110" aria-hidden="true" tabindex="-1"></a> info.mip_node_count)</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>
<span id="cb12-113"><a href="#cb12-113" 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-114"><a href="#cb12-114" aria-hidden="true" tabindex="-1"></a> <span class="co"># So ist das Unternehmen ueber zwei Jahre gewachsen.</span></span>
<span id="cb12-115"><a href="#cb12-115" aria-hidden="true" tabindex="-1"></a> entwicklung <span class="op">=</span> [(<span class="st">&quot;2024-Q1&quot;</span>, <span class="dv">15</span>, <span class="dv">40</span>), (<span class="st">&quot;2024-Q3&quot;</span>, <span class="dv">25</span>, <span class="dv">70</span>),</span>
<span id="cb12-116"><a href="#cb12-116" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;2025-Q1&quot;</span>, <span class="dv">40</span>, <span class="dv">110</span>), (<span class="st">&quot;2025-Q3&quot;</span>, <span class="dv">60</span>, <span class="dv">160</span>),</span>
<span id="cb12-117"><a href="#cb12-117" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;2026-Q1&quot;</span>, <span class="dv">80</span>, <span class="dv">220</span>)]</span>
<span id="cb12-118"><a href="#cb12-118" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-119"><a href="#cb12-119" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span>
<span id="cb12-120"><a href="#cb12-120" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; WAS DAS PROTOKOLL ZEIGT - UND WAS ES ZEIGEN SOLLTE&quot;</span>)</span>
<span id="cb12-121"><a href="#cb12-121" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">84</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;Derselbe Nachtjob, unveraendert, mit </span><span class="sc">{</span>ZEITLIMIT<span class="sc">:.0f}</span><span class="ss"> Sekunden Zeitlimit.</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb12-123"><a href="#cb12-123" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-124"><a href="#cb12-124" aria-hidden="true" tabindex="-1"></a> protokolle <span class="op">=</span> []</span>
<span id="cb12-125"><a href="#cb12-125" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> zeitpunkt, lager, kunden <span class="kw">in</span> entwicklung:</span>
<span id="cb12-126"><a href="#cb12-126" aria-hidden="true" tabindex="-1"></a> status, kosten, gap, laufzeit, knoten <span class="op">=</span> plane_netzwerk(</span>
<span id="cb12-127"><a href="#cb12-127" aria-hidden="true" tabindex="-1"></a> lager, kunden, ZEITLIMIT)</span>
<span id="cb12-128"><a href="#cb12-128" aria-hidden="true" tabindex="-1"></a> protokolle.append(Laufprotokoll(zeitpunkt, lager, kunden, status,</span>
<span id="cb12-129"><a href="#cb12-129" aria-hidden="true" tabindex="-1"></a> kosten, gap, laufzeit, knoten))</span>
<span id="cb12-130"><a href="#cb12-130" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-131"><a href="#cb12-131" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;So sieht das ueblich gefuehrte Protokoll aus:</span><span class="ch">\n</span><span class="st">&quot;</span>)</span>
<span id="cb12-132"><a href="#cb12-132" 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;Zeitpunkt&#39;</span><span class="sc">:&lt;10}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Kosten&#39;</span><span class="sc">:&gt;12}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-133"><a href="#cb12-133" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; &quot;</span> <span class="op">+</span> <span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">24</span>)</span>
<span id="cb12-134"><a href="#cb12-134" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> protokolle:</span>
<span id="cb12-135"><a href="#cb12-135" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>p<span class="sc">.</span>zeitpunkt<span class="sc">:&lt;10}</span><span class="ss"> </span><span class="sc">{</span>p<span class="sc">.</span>kosten<span class="sc">:&gt;12,.0f}</span><span class="ss">&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;</span><span class="ch">\n</span><span class="st"> -&gt; Die Kosten steigen. Das Unternehmen waechst ja auch.&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; Nichts an dieser Tabelle deutet auf ein Problem hin.&quot;</span>)</span>
<span id="cb12-138"><a href="#cb12-138" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-139"><a href="#cb12-139" 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">84</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;So sieht ein vollstaendiges Protokoll aus:</span><span class="ch">\n</span><span class="st">&quot;</span>)</span>
<span id="cb12-141"><a href="#cb12-141" 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;Zeitpunkt&#39;</span><span class="sc">:&lt;10}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Groesse&#39;</span><span class="sc">:&gt;10}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Status&#39;</span><span class="sc">:&gt;20}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Kosten&#39;</span><span class="sc">:&gt;11}</span><span class="ss"> &quot;</span></span>
<span id="cb12-142"><a href="#cb12-142" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Gap&#39;</span><span class="sc">:&gt;8}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Zeit&#39;</span><span class="sc">:&gt;7}</span><span class="ss">&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; &quot;</span> <span class="op">+</span> <span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">80</span>)</span>
<span id="cb12-144"><a href="#cb12-144" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> protokolle:</span>
<span id="cb12-145"><a href="#cb12-145" aria-hidden="true" tabindex="-1"></a> groesse <span class="op">=</span> <span class="ss">f&quot;</span><span class="sc">{</span>p<span class="sc">.</span>lager<span class="sc">}</span><span class="ss">x</span><span class="sc">{</span>p<span class="sc">.</span>kunden<span class="sc">}</span><span class="ss">&quot;</span></span>
<span id="cb12-146"><a href="#cb12-146" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>p<span class="sc">.</span>zeitpunkt<span class="sc">:&lt;10}</span><span class="ss"> </span><span class="sc">{</span>groesse<span class="sc">:&gt;10}</span><span class="ss"> </span><span class="sc">{</span>p<span class="sc">.</span>status<span class="sc">:&gt;20}</span><span class="ss"> &quot;</span></span>
<span id="cb12-147"><a href="#cb12-147" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>p<span class="sc">.</span>kosten<span class="sc">:&gt;11,.0f}</span><span class="ss"> </span><span class="sc">{</span>p<span class="sc">.</span>gap <span class="op">*</span> <span class="dv">100</span><span class="sc">:7.2f}</span><span class="ss"> % </span><span class="sc">{</span>p<span class="sc">.</span>laufzeit<span class="sc">:6.2f}</span><span class="ss">s&quot;</span>)</span>
<span id="cb12-148"><a href="#cb12-148" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-149"><a href="#cb12-149" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span>
<span id="cb12-150"><a href="#cb12-150" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Und so sehen die Warnungen aus, die daraus folgen:</span><span class="ch">\n</span><span class="st">&quot;</span>)</span>
<span id="cb12-151"><a href="#cb12-151" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> protokolle:</span>
<span id="cb12-152"><a href="#cb12-152" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> p.warnungen:</span>
<span id="cb12-153"><a href="#cb12-153" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>p<span class="sc">.</span>zeitpunkt<span class="sc">}</span><span class="ss">: &quot;</span> <span class="op">+</span> <span class="st">&quot;; &quot;</span>.join(p.warnungen))</span>
<span id="cb12-154"><a href="#cb12-154" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb12-155"><a href="#cb12-155" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>p<span class="sc">.</span>zeitpunkt<span class="sc">}</span><span class="ss">: in Ordnung&quot;</span>)</span>
<span id="cb12-156"><a href="#cb12-156" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-157"><a href="#cb12-157" aria-hidden="true" tabindex="-1"></a> letzte <span class="op">=</span> protokolle[<span class="op">-</span><span class="dv">1</span>]</span>
<span id="cb12-158"><a href="#cb12-158" aria-hidden="true" tabindex="-1"></a> erste_warnung <span class="op">=</span> <span class="bu">next</span>(p <span class="cf">for</span> p <span class="kw">in</span> protokolle <span class="cf">if</span> p.warnungen)</span>
<span id="cb12-159"><a href="#cb12-159" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-160"><a href="#cb12-160" 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">84</span>)</span>
<span id="cb12-161"><a href="#cb12-161" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; WAS DA PASSIERT IST&quot;</span>)</span>
<span id="cb12-162"><a href="#cb12-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">84</span>)</span>
<span id="cb12-163"><a href="#cb12-163" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Seit </span><span class="sc">{</span>erste_warnung<span class="sc">.</span>zeitpunkt<span class="sc">}</span><span class="ss"> erreicht der Job das Zeitlimit und liefert&quot;</span>)</span>
<span id="cb12-164"><a href="#cb12-164" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;nicht mehr das Optimum, sondern die beste bis dahin gefundene Loesung.&quot;</span>)</span>
<span id="cb12-165"><a href="#cb12-165" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Im letzten Lauf betraegt der Abstand zum Bestmoeglichen &quot;</span></span>
<span id="cb12-166"><a href="#cb12-166" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>letzte<span class="sc">.</span>gap<span class="sc">:.1%}</span><span class="ss">.&quot;</span>)</span>
<span id="cb12-167"><a href="#cb12-167" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb12-168"><a href="#cb12-168" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Der Job ist nicht abgestuerzt. Er hat keine Fehlermeldung erzeugt.&quot;</span>)</span>
<span id="cb12-169"><a href="#cb12-169" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Die Laufzeit ist sogar bemerkenswert STABIL geblieben - genau deshalb,&quot;</span>)</span>
<span id="cb12-170"><a href="#cb12-170" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;weil das Zeitlimit greift. Ein Ueberwachungssystem, das auf Abstuerze&quot;</span>)</span>
<span id="cb12-171"><a href="#cb12-171" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;und Laufzeitspitzen achtet, sieht hier nichts.&quot;</span>)</span>
<span id="cb12-172"><a href="#cb12-172" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb12-173"><a href="#cb12-173" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Drei Zahlen gehoeren deshalb in jedes Protokoll eines Optimierungsjobs:&quot;</span>)</span>
<span id="cb12-174"><a href="#cb12-174" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; * der STATUS - &#39;Optimal&#39; oder etwas anderes?&quot;</span>)</span>
<span id="cb12-175"><a href="#cb12-175" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; * der GAP - wie weit ist die Loesung vom Bestmoeglichen entfernt?&quot;</span>)</span>
<span id="cb12-176"><a href="#cb12-176" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; * die LAUFZEIT im Verhaeltnis zum Limit - wie nah am Anschlag?&quot;</span>)</span>
<span id="cb12-177"><a href="#cb12-177" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb12-178"><a href="#cb12-178" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Die dritte ist die frueheste Warnung: Sie steigt, lange bevor der Gap&quot;</span>)</span>
<span id="cb12-179"><a href="#cb12-179" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;sichtbar wird, und gibt Zeit zum Handeln.&quot;</span>)</span>
<span id="cb12-180"><a href="#cb12-180" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe (Zeiten hardwareabhängig):</strong></p>
<pre><code>====================================================================================
WAS DAS PROTOKOLL ZEIGT - UND WAS ES ZEIGEN SOLLTE
====================================================================================
Derselbe Nachtjob, unveraendert, mit 3 Sekunden Zeitlimit.
So sieht das ueblich gefuehrte Protokoll aus:
Zeitpunkt Kosten
------------------------
2024-Q1 40,484
2024-Q3 51,042
2025-Q1 64,053
2025-Q3 85,791
2026-Q1 108,582
-&gt; Die Kosten steigen. Das Unternehmen waechst ja auch.
Nichts an dieser Tabelle deutet auf ein Problem hin.
------------------------------------------------------------------------------------
So sieht ein vollstaendiges Protokoll aus:
Zeitpunkt Groesse Status Kosten Gap Zeit
--------------------------------------------------------------------------------
2024-Q1 15x40 Optimal 40,484 0.00 % 0.57s
2024-Q3 25x70 Optimal 51,042 0.00 % 1.50s
2025-Q1 40x110 Time limit reached 64,053 8.41 % 3.00s
2025-Q3 60x160 Time limit reached 85,791 14.99 % 3.01s
2026-Q1 80x220 Time limit reached 108,582 17.16 % 3.01s
------------------------------------------------------------------------------------
Und so sehen die Warnungen aus, die daraus folgen:
2024-Q1: in Ordnung
2024-Q3: in Ordnung
2025-Q1: nicht beweisbar optimal (Time limit reached); Gap 8.4% ueber Schwelle 2%; Zeitbudget zu 100% ausgeschoepft
2025-Q3: nicht beweisbar optimal (Time limit reached); Gap 15.0% ueber Schwelle 2%; Zeitbudget zu 100% ausgeschoepft
2026-Q1: nicht beweisbar optimal (Time limit reached); Gap 17.2% ueber Schwelle 2%; Zeitbudget zu 100% ausgeschoepft
====================================================================================
WAS DA PASSIERT IST
====================================================================================
Seit 2025-Q1 erreicht der Job das Zeitlimit und liefert
nicht mehr das Optimum, sondern die beste bis dahin gefundene Loesung.
Im letzten Lauf betraegt der Abstand zum Bestmoeglichen 17.2%.
Der Job ist nicht abgestuerzt. Er hat keine Fehlermeldung erzeugt.
Die Laufzeit ist sogar bemerkenswert STABIL geblieben - genau deshalb,
weil das Zeitlimit greift. Ein Ueberwachungssystem, das auf Abstuerze
und Laufzeitspitzen achtet, sieht hier nichts.
Drei Zahlen gehoeren deshalb in jedes Protokoll eines Optimierungsjobs:
* der STATUS - &#39;Optimal&#39; oder etwas anderes?
* der GAP - wie weit ist die Loesung vom Bestmoeglichen entfernt?
* die LAUFZEIT im Verhaeltnis zum Limit - wie nah am Anschlag?
Die dritte ist die frueheste Warnung: Sie steigt, lange bevor der Gap
sichtbar wird, und gibt Zeit zum Handeln.
====================================================================================</code></pre>
<blockquote>
<p><strong>🎯 Merksatz</strong> Ein Optimierungsjob, der ein Zeitlimit hat, wird bei wachsenden Daten <strong>nicht langsamer</strong> — er wird <strong>schlechter</strong>. Genau deshalb greift die übliche Betriebsüberwachung nicht: Sie achtet auf Laufzeit und Abstürze, und beides bleibt unauffällig. Protokollieren Sie <strong>Status, Gap und Zeitausschöpfung</strong> bei jedem Lauf, oder Sie erfahren nie, wann Ihr Modell aufgehört hat zu optimieren.</p>
</blockquote>
<hr />
<h2 id="sec:praxisfallen-quiz">22.12 Micro-Quiz</h2>
<div class="card card-quiz">
<blockquote>
<p><strong>❓ Micro-Quiz 22: Drei Fragen zum Selbstcheck</strong></p>
<p>Genau eine Antwort ist jeweils richtig. Auflösung in <a href="anhang-loesungen.html#quiz-loesung-praxisfallen">Anhang A</a>.</p>
<p><strong>1. Ihr Nachtjob läuft seit Monaten konstant 3,0 Sekunden bei einem Zeitlimit von 3 Sekunden. Was schließen Sie daraus?</strong> (a) Das System ist bemerkenswert stabil. (b) Das Zeitlimit greift jedes Mal. Der Solver bricht ab, statt fertig zu werden — die konstante Laufzeit ist das Symptom, nicht die Beruhigung. (c) Der Rechner ist ausgelastet und sollte aufgerüstet werden.</p>
<p><strong>2. Eine Prüffunktion soll kontrollieren, ob ein Tourenplan die Fahrzeugkapazitäten einhält. Woher nimmt sie die geladenen Mengen?</strong> (a) Aus der Ladungsdimension des Routing-Modells — dort stehen sie ja bereits. (b) Sie summiert die Bedarfe der Kunden aus dem <strong>ausgegebenen Tourenplan</strong> auf. Alles andere fragt das Modell, ob es sich an sich selbst hält. (c) Aus der Zielfunktion, sofern die Kapazität dort bepreist ist.</p>
<p><strong>3. Ein Modell meldet <code>INFEASIBLE</code>, nachdem eine neue Regel aufgenommen wurde. Was ist der produktivste erste Schritt?</strong> (a) Das Zeitlimit erhöhen. (b) Die neue Regel wieder entfernen und in Ruhe abwarten. (c) Die harten Regeln hierarchisch lockern — mit teuren Strafkosten statt Verboten —, damit der Solver benennt, <strong>welche</strong> Regel den Widerspruch erzeugt, statt nur zu sagen, dass es einen gibt.</p>
</blockquote>
</div>
<hr />
<h2 id="sec:praxisfallen-selbsttest">22.13 Selbsttest</h2>
<blockquote>
<p>Antworten: <a href="anhang-loesungen.html#selbsttest-loesung-praxisfallen">Anhang A</a>.</p>
</blockquote>
<ol type="1">
<li>Wie verwandelt man ein unlösbares Modell in einen brauchbaren Notfallplan?</li>
<li>Welche drei Fragen muss ein erklärbares OR-System beantworten können?</li>
<li>Warum ist ein MIP-Gap von 2 % in der Praxis meist ausreichend?</li>
<li>Was ist das Snapshot-Prinzip und wozu dient es?</li>
<li>Was ist der häufigste Grund, aus dem OR-Projekte scheitern?</li>
<li>Eine Bedingung bindet, ihr Schattenpreis ist 0. Was heißt das — und was folgt daraus für eine Nachverhandlung?</li>
<li>Warum kann die Rangfolge nach Schattenpreis eine andere sein als die Rangfolge nach tatsächlichem Nutzen einer Lockerung?</li>
</ol>
<hr />
<h2 id="sec:praxisfallen-zusammenfassung">22.14 Zusammenfassung</h2>
<ul>
<li><strong>Infeasibility ist ein Entwurfsproblem, kein Solverproblem.</strong> Wer alles hart formuliert, erhält irgendwann eine Fehlermeldung statt eines Plans.</li>
<li><strong>Erklärbarkeit entscheidet über Akzeptanz.</strong> Schattenpreise, Kostenzerlegung und Constraint-Traces sind keine Kür.</li>
<li><strong>Bindend heißt nicht teuer.</strong> Eine Bedingung kann den Plan berühren und trotzdem 0,00 € kosten. Nur die kostenrelevanten lohnen eine Nachverhandlung.</li>
<li><strong>Der Schattenpreis gilt für die nächste Einheit, nicht für die nächste Schicht.</strong> Hochgerechnet auf einen realistischen Schritt überschätzt er den Nutzen — im Beispiel um das Doppelte. Wer wissen will, was ein Hebel bringt, <strong>misst</strong> die Lockerung, statt zu multiplizieren; die beiden Ranglisten können sich dabei umdrehen.</li>
<li><strong>Ein Wunsch kann unmöglich statt teuer sein</strong> — und dann ist die richtige Antwort nicht ein Preis, sondern die Liste der Bedingungen, die ihn gemeinsam blockieren.</li>
<li><strong>Ein generierter Bericht macht ein Modell nicht richtiger, nur schwerer anzuzweifeln.</strong> Er erklärt das Modell, nicht die Wirklichkeit — deshalb gehört dazu, worauf er beruht.</li>
<li><strong>Optimieren Sie nicht genauer als Ihre Daten.</strong> Ein Gap von 2 % ist bei ±10 % Datenunsicherheit irrelevant.</li>
<li><strong>Der Optimierer schlägt vor, der Mensch entscheidet</strong> — jedenfalls anfangs.</li>
<li><strong>Reproduzierbarkeit braucht Snapshots und Versionierung</strong>, nicht guten Willen.</li>
<li><strong>Projekte scheitern an der Einführung, nicht am Modell.</strong></li>
<li><strong>Schreiben Sie die Abnahmeprüfung als eigenen Baustein</strong> — sie darf keinen Solver und keine Modellvariable anfassen, sonst prüft sie das Modell gegen sich selbst. Sie belegt Zulässigkeit, nicht Optimalität; für Letztere braucht es ein zweites Verfahren oder eine bekannte Schranke.</li>
<li><strong>Trennen Sie Domäne, Modellbau und Lösung</strong> (<code>or_kern.py</code>). Der Gewinn liegt nicht im Solverwechsel, sondern darin, dass jede Schicht einzeln prüfbar wird — und dass Datenfehler beim <strong>Einlesen</strong> auffliegen statt im Bericht.</li>
<li><strong>Protokollieren Sie Status, Gap und Zeitausschöpfung bei jedem Lauf.</strong> Ein Job mit Zeitlimit wird bei wachsenden Daten nicht langsamer, sondern schlechter — und eine konstante Laufzeit am Limit ist deshalb ein Warnsignal, keine Beruhigung.</li>
</ul>
</article>
<button type="button" class="fortschritt-knopf" data-kapitel="praxisfallen.html"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg> <span>Als gelesen markieren</span></button>
<nav class="prev-next"><a class="prev-next-knopf prev-next-prev" href="synthese-anwendungen.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Synthese Teil IV</span></a><a class="prev-next-knopf prev-next-next" href="testing.html"><span><small>Weiter</small>Kapitel 23: Testen, Messen, Ausliefern</span><svg class="icon" aria-hidden="true"><use href="#icon-chevron-right"></use></svg></a></nav>
</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>