<aclass="brand"href="index.html"><svgclass="icon"aria-hidden="true"><usehref="#icon-book"></use></svg><span>Optimierte Entscheidungsfindung mit Python</span></a>
<navclass="sidebar"id="sidebar"aria-label="Kapitelnavigation"><divclass="sidebar-inhalt"><detailsclass="sidebar-gruppe"><summary>Einstieg</summary><ul><lidata-kapitel="vorwort.html"><ahref="vorwort.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Vorwort & Lesehilfe</span></a></li><lidata-kapitel="notation.html"><ahref="notation.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Notation & Abkürzungen</span></a></li></ul></details><detailsclass="sidebar-gruppe"><summary>Teil I: Grundlagen des Operations Research</summary><ul><lidata-kapitel="einfuehrung.html"><ahref="einfuehrung.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></a></li><lidata-kapitel="fundament.html"><ahref="fundament.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</span></a></li><lidata-kapitel="oekosystem.html"><ahref="oekosystem.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 3: Das Python-Ökosystem für OR — Solver, Bindings und Modellierungsschichten</span></a></li><lidata-kapitel="modellierung.html"><ahref="modellierung.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 4: Vom Management-Wunsch zum Modell</span></a></li></ul></details><detailsclass="sidebar-gruppe"><summary>Teil II: Die Kernverfahren der deterministischen Optimierung</summary><ul><lidata-kapitel="lp.html"><ahref="lp.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></a></li><lidata-kapitel="milp.html"><ahref="milp.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound</span></a></li><lidata-kapitel="cpsat.html"><ahref="cpsat.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</span></a></li><lidata-kapitel="graphen.html"><ahref="graphen.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</span></a></li><lidata-kapitel="metaheuristiken.html"><ahref="metaheuristiken.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 9: Metaheuristiken — wenn der exakte Solver aussteigt</span></a></li><lidata-kapitel="dekomposition.html"><ahref="dekomposition.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 10: Spaltengenerierung — das Modell umbauen statt die Lösung raten</span></a></li></ul></details><detailsclass="sidebar-gruppe"><summary>Teil III: Nichtlinearität, Unsicherheit und mehrperiodige Dynamik</summary><ul><lidata-kapitel="qp-nlp.html"><ahref="qp-nlp.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 11: Quadratische und nichtlineare Optimierung — KKT, Lagrange, Konvexität</span></a></li><lidata-kapitel="unsicherheit.html"><ahref="unsicherheit.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></
<mainclass="content">
<navclass="breadcrumb"aria-label="Breadcrumb"><ahref="index.html">Start</a>›<span>Teil V</span>›<span>Kapitel 22: Praxisfallen und der Weg zum produktiven Einsatz</span></nav>
<h1id="kap-praxisfallen">Kapitel 22: Praxisfallen und der Weg zum produktiven Einsatz</h1>
<divclass="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>
<ahref="https://colab.research.google.com/github/dschlueter/or-mit-python/blob/main/Notebooks_04/praxisfallen.ipynb">In Google Colab öffnen</a></p>
</blockquote>
</div>
<hr/>
<h2id="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>
<divclass="card card-schnellstart">
<blockquote>
<p><strong>🚀 In 5 Minuten gelöst: Die Abnahmeprüfung, die in jedes Modell gehört</strong></p>
<divclass="sourceCode"id="cb1"><preclass="sourceCode python"><codeclass="sourceCode python"><spanid="cb1-1"><ahref="#cb1-1"aria-hidden="true"tabindex="-1"></a><spanclass="kw">def</span> pruefe_loesung(x, A, b, kosten, zielwert, ganzzahlig<spanclass="op">=</span>(), toleranz<spanclass="op">=</span><spanclass="fl">1e-6</span>):</span>
<spanid="cb1-2"><ahref="#cb1-2"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Prueft eine Loesung gegen die Anforderungen - OHNE den Solver zu fragen."""</span></span>
<spanid="cb1-5"><ahref="#cb1-5"aria-hidden="true"tabindex="-1"></a> verbrauch <spanclass="op">=</span> A <spanclass="op">@</span> x <spanclass="co"># 1. Werden Grenzen eingehalten?</span></span>
<spanid="cb1-6"><ahref="#cb1-6"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i, (ist, grenze) <spanclass="kw">in</span><spanclass="bu">enumerate</span>(<spanclass="bu">zip</span>(verbrauch, b)):</span>
<spanid="cb1-7"><ahref="#cb1-7"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> ist <spanclass="op">></span> grenze <spanclass="op">+</span> toleranz:</span>
<spanid="cb1-10"><ahref="#cb1-10"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> j <spanclass="kw">in</span> ganzzahlig: <spanclass="co"># 2. Ist ganzzahlig auch ganzzahlig?</span></span>
<spanid="cb1-12"><ahref="#cb1-12"aria-hidden="true"tabindex="-1"></a> beanstandungen.append(<spanclass="ss">f"x[</span><spanclass="sc">{</span>j<spanclass="sc">}</span><spanclass="ss">] = </span><spanclass="sc">{</span>x[j]<spanclass="sc">!r}</span><spanclass="ss"> ist nicht ganzzahlig"</span>)</span>
<spanid="cb1-17"><ahref="#cb1-17"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"Zielwert </span><spanclass="sc">{</span>zielwert<spanclass="sc">:.4f}</span><spanclass="ss"> passt nicht zu den Daten (</span><spanclass="sc">{</span>nachgerechnet<spanclass="sc">:.4f}</span><spanclass="ss">)"</span>)</span>
<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 <ahref="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>
<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/>
<h2id="sec:praxisfallen-die-fuenf-typischen-praxisfallen">22.3 Die fünf typischen Praxisfallen</h2>
<figure>
<imgsrc="bilder_04/kap15_praxisfallen.svg"alt="Abb. 22.1: Die fünf typischen Praxisfallen"/>
<figcaptionaria-hidden="true">Abb. 22.1: Die fünf typischen Praxisfallen</figcaption>
<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>
<spanid="cb2-5"><ahref="#cb2-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel Praxisfallen: Aus INFEASIBLE einen Notfallplan machen.</span></span>
<spanid="cb2-7"><ahref="#cb2-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Demonstriert die hierarchische Relaxation an einem Dienstplan, der in der</span></span>
<spanid="cb2-8"><ahref="#cb2-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">harten Fassung unloesbar ist, und zeigt, wie der Solver die Ursache benennt.</span></span>
<spanid="cb2-22"><ahref="#cb2-22"aria-hidden="true"tabindex="-1"></a>STRAFE_UNBESETZT <spanclass="op">=</span><spanclass="dv">10_000</span><spanclass="co"># sehr teuer, aber nicht unmoeglich</span></span>
<spanid="cb2-23"><ahref="#cb2-23"aria-hidden="true"tabindex="-1"></a>STRAFE_UEBERLAST <spanclass="op">=</span><spanclass="dv">500</span><spanclass="co"># teuer, aber billiger als Ausfall</span></span>
<spanid="cb2-28"><ahref="#cb2-28"aria-hidden="true"tabindex="-1"></a><spanclass="co"> harte_fassung=False: mit Schlupfvariablen (immer loesbar)."""</span></span>
<spanid="cb2-29"><ahref="#cb2-29"aria-hidden="true"tabindex="-1"></a> modell <spanclass="op">=</span> cp_model.CpModel()</span>
<spanid="cb2-30"><ahref="#cb2-30"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> {(p, s): modell.NewBoolVar(<spanclass="ss">f"x_</span><spanclass="sc">{</span>p<spanclass="sc">}</span><spanclass="ss">_</span><spanclass="sc">{</span>s<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb2-31"><ahref="#cb2-31"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL <spanclass="cf">for</span> s <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(SLOTS))}</span>
<spanid="cb2-37"><ahref="#cb2-37"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> s <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(SLOTS)):</span>
<spanid="cb2-41"><ahref="#cb2-41"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Schlupfvariable: der Slot DARF unbesetzt bleiben - gegen hohe Strafe</span></span>
<spanid="cb2-46"><ahref="#cb2-46"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Qualifikation bleibt IMMER hart - fachfremder Unterricht ist keine Option</span></span>
<spanid="cb2-47"><ahref="#cb2-47"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> s, fach <spanclass="kw">in</span><spanclass="bu">enumerate</span>(FAECHER):</span>
<spanid="cb2-48"><ahref="#cb2-48"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL:</span>
<spanid="cb2-49"><ahref="#cb2-49"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> fach <spanclass="kw">not</span><spanclass="kw">in</span> QUALIFIKATION[p]:</span>
<spanid="cb2-52"><ahref="#cb2-52"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL:</span>
<spanid="cb2-53"><ahref="#cb2-53"aria-hidden="true"tabindex="-1"></a> last <spanclass="op">=</span><spanclass="bu">sum</span>(x[p, s] <spanclass="cf">for</span> s <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(SLOTS)))</span>
<spanid="cb2-77"><ahref="#cb2-77"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> status <spanclass="op">==</span> cp_model.INFEASIBLE:</span>
<spanid="cb2-78"><ahref="#cb2-78"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Das System kann keinen Plan liefern. Der Anwender erfaehrt NICHT,"</span>)</span>
<spanid="cb2-79"><ahref="#cb2-79"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"welche Regel das Problem verursacht - nur, dass es nicht geht.</span><spanclass="ch">\n</span><spanclass="st">"</span>)</span>
<spanid="cb2-93"><ahref="#cb2-93"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"Kein qualifiziertes Personal verfuegbar"</span>)</span>
<spanid="cb2-95"><ahref="#cb2-95"aria-hidden="true"tabindex="-1"></a> person <spanclass="op">=</span><spanclass="bu">next</span>(p <spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL <spanclass="cf">if</span> loeser.Value(x[p, s]))</span>
<spanid="cb2-101"><ahref="#cb2-101"aria-hidden="true"tabindex="-1"></a> fach <spanclass="op">=</span> FAECHER[s]</span>
<spanid="cb2-102"><ahref="#cb2-102"aria-hidden="true"tabindex="-1"></a> qualifiziert <spanclass="op">=</span> [p <spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL <spanclass="cf">if</span> fach <spanclass="kw">in</span> QUALIFIKATION[p]]</span>
<spanid="cb2-103"><ahref="#cb2-103"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"'</span><spanclass="sc">{</span>slot<spanclass="sc">}</span><spanclass="ss">' (</span><spanclass="sc">{</span>fach<spanclass="sc">}</span><spanclass="ss">): </span><spanclass="sc">{</span><spanclass="bu">len</span>(qualifiziert)<spanclass="sc">}</span><spanclass="ss"> qualifizierte Personen "</span></span>
<spanid="cb2-104"><ahref="#cb2-104"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"</span><spanclass="sc">{</span>qualifiziert <spanclass="cf">if</span> qualifiziert <spanclass="cf">else</span><spanclass="st">'-> URSACHE: niemand kann dieses Fach'</span><spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb2-105"><ahref="#cb2-105"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL:</span>
<spanid="cb2-109"><ahref="#cb2-109"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st">Der Anwender bekommt jetzt einen Plan PLUS eine konkrete Ursache -"</span>)</span>
<spanid="cb2-110"><ahref="#cb2-110"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"und kann handeln: Vertretung von aussen holen, Stunde verlegen,"</span>)</span>
<spanid="cb2-111"><ahref="#cb2-111"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"oder die Klasse zusammenlegen."</span>)</span>
<h3id="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 <ahref="cpsat.html#kap-cpsat">Kapitel 7</a> und <ahref="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><ahref="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>
<spanid="cb3-5"><ahref="#cb3-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel Praxisfallen: Ein Constraint-Trace, der die drei Fragen der Faustregel fuer</span></span>
<spanid="cb3-8"><ahref="#cb3-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">Warum diese Loesung? Warum nicht die Alternative? Was wuerde sie verbessern?</span></span>
<spanid="cb3-16"><ahref="#cb3-16"aria-hidden="true"tabindex="-1"></a>STRAFE_VORBELASTUNG <spanclass="op">=</span><spanclass="dv">50</span><spanclass="co"># pro bereits geleisteter Stunde</span></span>
<spanid="cb3-21"><ahref="#cb3-21"aria-hidden="true"tabindex="-1"></a> modell <spanclass="op">=</span> cp_model.CpModel()</span>
<spanid="cb3-22"><ahref="#cb3-22"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> {p: modell.NewBoolVar(<spanclass="ss">f"x_</span><spanclass="sc">{</span>p<spanclass="sc">}</span><spanclass="ss">"</span>) <spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL}</span>
<spanid="cb3-34"><ahref="#cb3-34"aria-hidden="true"tabindex="-1"></a> status <spanclass="op">=</span> loeser.Solve(modell)</span>
<spanid="cb3-35"><ahref="#cb3-35"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> status <spanclass="kw">not</span><spanclass="kw">in</span> (cp_model.OPTIMAL, cp_model.FEASIBLE):</span>
<spanid="cb3-37"><ahref="#cb3-37"aria-hidden="true"tabindex="-1"></a> gewinner <spanclass="op">=</span><spanclass="bu">next</span>(p <spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL <spanclass="cf">if</span> loeser.Value(x[p]))</span>
<spanid="cb3-52"><ahref="#cb3-52"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st">1. WARUM DIESE LOESUNG?"</span>)</span>
<spanid="cb3-53"><ahref="#cb3-53"aria-hidden="true"tabindex="-1"></a> v, w <spanclass="op">=</span> BEREITS_STUNDEN[gewinner] <spanclass="op">*</span> STRAFE_VORBELASTUNG, STRAFKOSTEN_WUNSCH[gewinner]</span>
<spanid="cb3-57"><ahref="#cb3-57"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st">2. WARUM NICHT DIE NAHELIEGENDEN ALTERNATIVEN?"</span>)</span>
<spanid="cb3-58"><ahref="#cb3-58"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL:</span>
<spanid="cb3-59"><ahref="#cb3-59"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> p <spanclass="op">==</span> gewinner:</span>
<spanid="cb3-62"><ahref="#cb3-62"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span>p<spanclass="sc">}</span><spanclass="ss">: AUSGESCHLOSSEN - hat bereits </span><spanclass="sc">{</span>BEREITS_STUNDEN[p]<spanclass="sc">}</span><spanclass="ss"> Stunden, "</span></span>
<spanid="cb3-63"><ahref="#cb3-63"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"eine dritte würde die Obergrenze von </span><spanclass="sc">{</span>MAX_STUNDEN<spanclass="sc">}</span><spanclass="ss"> Stunden verletzen "</span></span>
<spanid="cb3-64"><ahref="#cb3-64"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"(harte Bedingung, nicht verhandelbar)."</span>)</span>
<spanid="cb3-67"><ahref="#cb3-67"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span>p<spanclass="sc">}</span><spanclass="ss">: zulässig, aber </span><spanclass="sc">{</span>differenz<spanclass="sc">:.0f}</span><spanclass="ss"> Strafpunkte teurer als "</span></span>
<spanid="cb3-70"><ahref="#cb3-70"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st">3. WAS WUERDE DIE LOESUNG VERBESSERN?"</span>)</span>
<spanid="cb3-73"><ahref="#cb3-73"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Obergrenze auf </span><spanclass="sc">{</span>MAX_STUNDEN <spanclass="op">+</span><spanclass="dv">1</span><spanclass="sc">}</span><spanclass="ss"> Stunden gelockert: </span><spanclass="sc">{</span>gewinner_gelockert<spanclass="sc">}</span><spanclass="ss">"</span></span>
<spanid="cb3-74"><ahref="#cb3-74"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"uebernimmt jetzt zu </span><spanclass="sc">{</span>kosten_gelockert<spanclass="sc">:.0f}</span><spanclass="ss"> Strafpunkten "</span></span>
<spanid="cb3-75"><ahref="#cb3-75"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"(</span><spanclass="sc">{</span>kosten_opt <spanclass="op">-</span> kosten_gelockert<spanclass="sc">:.0f}</span><spanclass="ss"> weniger als heute)."</span>)</span>
<spanid="cb3-76"><ahref="#cb3-76"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" -> Diese Bedingung hat tatsaechlich einen Preis."</span>)</span>
<spanid="cb3-78"><ahref="#cb3-78"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Obergrenze auf </span><spanclass="sc">{</span>MAX_STUNDEN <spanclass="op">+</span><spanclass="dv">1</span><spanclass="sc">}</span><spanclass="ss"> Stunden gelockert: Ergebnis bleibt bei "</span></span>
<spanid="cb3-79"><ahref="#cb3-79"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"</span><spanclass="sc">{</span>gewinner<spanclass="sc">}</span><spanclass="ss"> mit </span><spanclass="sc">{</span>kosten_gelockert<spanclass="sc">:.0f}</span><spanclass="ss"> Strafpunkten (unveraendert)."</span>)</span>
<spanid="cb3-80"><ahref="#cb3-80"aria-hidden="true"tabindex="-1"></a> blockiert <spanclass="op">=</span><spanclass="st">", "</span>.join(p <spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL</span>
<spanid="cb3-81"><ahref="#cb3-81"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> BEREITS_STUNDEN[p] <spanclass="op">+</span><spanclass="dv">1</span><spanclass="op">></span> MAX_STUNDEN <spanclass="kw">and</span> p <spanclass="op">!=</span> gewinner)</span>
<spanid="cb3-82"><ahref="#cb3-82"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" -> Die Obergrenze blockiert zwar </span><spanclass="sc">{</span>blockiert<spanclass="sc">}</span><spanclass="ss">, kostet aber gerade NICHTS:"</span>)</span>
<spanid="cb3-83"><ahref="#cb3-83"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Selbst ohne sie waere die Loesung dieselbe."</span>)</span>
<spanid="cb3-84"><ahref="#cb3-84"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Blockierend und kostenrelevant sind zwei verschiedene Dinge."</span>)</span>
<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>
<h3id="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>
<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>
<h3id="falle-4-lookahead--und-survivorship-bias">Falle 4 — Lookahead- und Survivorship-Bias</h3>
<p>Ausführlich in <ahref="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 <ahref="handelsmaschine.html#sec:handelsmaschine-die-engine">Abschnitt 21.5</a> findet den ersten automatisch.</p>
<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>
<trclass="header">
<th>Einsatzszenario</th>
<th>Empfohlenes Zeitlimit</th>
<th>Akzeptabler MIP-Gap</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td>Interaktive Oberfläche</td>
<td>5–10 Sekunden</td>
<td>2–5 %</td>
</tr>
<trclass="even">
<td>Batch tagsüber</td>
<td>1–5 Minuten</td>
<td>1–2 %</td>
</tr>
<trclass="odd">
<td>Nachtlauf</td>
<td>30–60 Minuten</td>
<td>0,1–1 %</td>
</tr>
<trclass="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/>
<h2id="sec:praxisfallen-architektur-einer-produktionsreifen-or-plattform">22.4 Architektur einer produktionsreifen OR-Plattform</h2>
<figure>
<imgsrc="bilder_04/kap15_or_plattform.svg"alt="Abb. 22.2: Architektur einer produktionsreifen OR-Plattform"/>
<figcaptionaria-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>
<p><strong>Fünf Prinzipien, die sich in der Praxis bewährt haben:</strong></p>
<oltype="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/>
<h2id="sec:praxisfallen-or-kern">22.5 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>
<h3id="die-vier-schichten">Die vier Schichten</h3>
<td>Auswertung, Test und Bericht funktionieren für jeden Solver gleich</td>
</tr>
<trclass="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>
<imgsrc="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."/>
<figcaptionaria-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>
<h3id="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>
<tablestyle="width:100%;">
<colgroup>
<colstyle="width: 16%"/>
<colstyle="width: 16%"/>
<colstyle="width: 16%"/>
<colstyle="width: 16%"/>
<colstyle="width: 16%"/>
<colstyle="width: 16%"/>
</colgroup>
<thead>
<trclass="header">
<th>Bedeutung</th>
<th>pywraplp</th>
<th>CP-SAT</th>
<th>HiGHS</th>
<th>SciPy</th>
<th>CVXPY</th>
</tr>
</thead>
<tbody>
<trclass="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>
<trclass="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>
<trclass="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>
<trclass="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 (<ahref="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>
<spanid="cb5-7"><ahref="#cb5-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Alle Beispiele dieses Buchs loesen dieselben vier Aufgaben immer wieder:</span></span>
<spanid="cb5-8"><ahref="#cb5-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">Daten einlesen und pruefen, ein Modell bauen, den Solverstatus auswerten, die</span></span>
<spanid="cb5-9"><ahref="#cb5-9"aria-hidden="true"tabindex="-1"></a><spanclass="co">Loesung gegen die Wirklichkeit kontrollieren. Dieses Modul zieht diese vier</span></span>
<spanid="cb5-10"><ahref="#cb5-10"aria-hidden="true"tabindex="-1"></a><spanclass="co">Aufgaben aus den Einzelprogrammen heraus.</span></span>
<spanid="cb5-18"><ahref="#cb5-18"aria-hidden="true"tabindex="-1"></a><spanclass="co">Der Sinn dieser Trennung: Die Domaenenschicht weiss nichts von Solvern, die</span></span>
<spanid="cb5-19"><ahref="#cb5-19"aria-hidden="true"tabindex="-1"></a><spanclass="co">Loesungsschicht weiss nichts vom Modellaufbau. Wer den Solver wechselt, tauscht</span></span>
<spanid="cb5-20"><ahref="#cb5-20"aria-hidden="true"tabindex="-1"></a><spanclass="co">genau EINEN Baustein aus - der Rest bleibt.</span></span>
<spanid="cb5-22"><ahref="#cb5-22"aria-hidden="true"tabindex="-1"></a><spanclass="co">WICHTIG ZUM IMPORT: Dieses Modul importiert von sich aus KEINE Solver-</span></span>
<spanid="cb5-23"><ahref="#cb5-23"aria-hidden="true"tabindex="-1"></a><spanclass="co">bibliothek. Der Grund steht im Kapitel Oekosystem: ortools und highspy vertragen sich</span></span>
<spanid="cb5-24"><ahref="#cb5-24"aria-hidden="true"tabindex="-1"></a><spanclass="co">nicht im selben Prozess. Die Statusuebersetzer laden ihre Bibliothek erst,</span></span>
<spanid="cb5-25"><ahref="#cb5-25"aria-hidden="true"tabindex="-1"></a><spanclass="co">wenn sie aufgerufen werden - so bleibt or_kern.py mit jedem Solver benutzbar.</span></span>
<spanid="cb5-38"><ahref="#cb5-38"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Wiederverwendbare Feldtypen. Sie tragen die Pruefung im Typ, nicht im Code -</span></span>
<spanid="cb5-39"><ahref="#cb5-39"aria-hidden="true"tabindex="-1"></a><spanclass="co"># damit gilt sie ueberall, wo der Typ verwendet wird.</span></span>
<spanid="cb5-44"><ahref="#cb5-44"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- 1. Solverstatus: eine Sprache fuer fuenf Bibliotheken -------------------</span></span>
<spanid="cb5-47"><ahref="#cb5-47"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Was ein Solverlauf ergeben hat - unabhaengig davon, wer gerechnet hat.</span></span>
<spanid="cb5-49"><ahref="#cb5-49"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Die fuenf im Buch verwendeten Bibliotheken benennen dasselbe Ergebnis</span></span>
<spanid="cb5-50"><ahref="#cb5-50"aria-hidden="true"tabindex="-1"></a><spanclass="co"> unterschiedlich: 'Optimal', 'OPTIMAL', 2, 'optimal'. Wer darauf direkt</span></span>
<spanid="cb5-51"><ahref="#cb5-51"aria-hidden="true"tabindex="-1"></a><spanclass="co"> prueft, bindet seinen Auswertungscode an eine Bibliothek.</span></span>
<spanid="cb5-55"><ahref="#cb5-55"aria-hidden="true"tabindex="-1"></a> UNZULAESSIG <spanclass="op">=</span><spanclass="st">"unzulaessig"</span><spanclass="co"># es gibt keine Loesung (Aussage ueber das Modell)</span></span>
<spanid="cb5-56"><ahref="#cb5-56"aria-hidden="true"tabindex="-1"></a> UNBESCHRAENKT <spanclass="op">=</span><spanclass="st">"unbeschraenkt"</span><spanclass="co"># Ziel waechst ins Unendliche</span></span>
<spanid="cb5-58"><ahref="#cb5-58"aria-hidden="true"tabindex="-1"></a> FEHLERHAFT <spanclass="op">=</span><spanclass="st">"fehlerhaft"</span><spanclass="co"># das Modell ist kein gueltiges Modell</span></span>
<spanid="cb5-59"><ahref="#cb5-59"aria-hidden="true"tabindex="-1"></a> UNBEKANNT <spanclass="op">=</span><spanclass="st">"unbekannt"</span><spanclass="co"># alles Uebrige</span></span>
<spanid="cb5-63"><ahref="#cb5-63"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Habe ich etwas in der Hand, das ich ausfuehren kann?"""</span></span>
<spanid="cb5-68"><ahref="#cb5-68"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Liegt die Ursache im Modell (und nicht in der Rechenzeit)?"""</span></span>
<spanid="cb5-101"><ahref="#cb5-101"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""HiGHS ueber highspy - hier kommt der Status als Text."""</span></span>
<spanid="cb5-104"><ahref="#cb5-104"aria-hidden="true"tabindex="-1"></a><spanclass="st">"Time limit reached"</span>: SolverStatus.ZULAESSIG, <spanclass="co"># meist mit Loesung</span></span>
<spanid="cb5-108"><ahref="#cb5-108"aria-hidden="true"tabindex="-1"></a><spanclass="st">"Primal infeasible or unbounded"</span>: SolverStatus.UNZULAESSIG,</span>
<spanid="cb5-115"><ahref="#cb5-115"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""scipy.optimize.linprog und milp liefern ein Ergebnisobjekt mit .status."""</span></span>
<spanid="cb5-127"><ahref="#cb5-127"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""CVXPY - Statuszeichenketten wie 'optimal' oder 'infeasible'."""</span></span>
<spanid="cb5-139"><ahref="#cb5-139"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- 2. Die Loesung als solverunabhaengiges Datenobjekt ----------------------</span></span>
<spanid="cb5-142"><ahref="#cb5-142"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Das Ergebnis eines Solverlaufs, so wie es weiterverarbeitet wird.</span></span>
<spanid="cb5-144"><ahref="#cb5-144"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Bewusst OHNE Referenz auf Solver, Modell oder Variablen: Ein Bericht, ein</span></span>
<spanid="cb5-145"><ahref="#cb5-145"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Test oder eine Weiterverarbeitung soll nicht wissen muessen, womit</span></span>
<spanid="cb5-153"><ahref="#cb5-153"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Schattenpreise je Nebenbedingung, sofern der Solver sie liefert</span></span>
<spanid="cb5-154"><ahref="#cb5-154"aria-hidden="true"tabindex="-1"></a><spanclass="co"># (nur bei kontinuierlichen Problemen, siehe Kapitel LP). Auch das ist</span></span>
<spanid="cb5-155"><ahref="#cb5-155"aria-hidden="true"tabindex="-1"></a><spanclass="co"># ein solverunabhaengiger Begriff und gehoert deshalb hierher.</span></span>
<spanid="cb5-160"><ahref="#cb5-160"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Relativer Abstand zwischen gefundener Loesung und bewiesener Schranke.</span></span>
<spanid="cb5-162"><ahref="#cb5-162"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Das ist die Zahl fuer den Bericht: keine Schaetzung, sondern eine</span></span>
<spanid="cb5-163"><ahref="#cb5-163"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Zusage - schlechter als das kann die Loesung nicht sein.</span></span>
<spanid="cb5-172"><ahref="#cb5-172"aria-hidden="true"tabindex="-1"></a> teile <spanclass="op">=</span> [<spanclass="ss">f"Status: </span><spanclass="sc">{</span><spanclass="va">self</span><spanclass="sc">.</span>status<spanclass="sc">.</span>value<spanclass="sc">}</span><spanclass="ss">"</span>]</span>
<spanid="cb5-182"><ahref="#cb5-182"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- 3. Domaenenmodell: die Fakten, bevor ein Solver sie sieht ---------------</span></span>
<spanid="cb5-185"><ahref="#cb5-185"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Ein Produkt mit Deckungsbeitrag und Ressourcenverbrauch."""</span></span>
<spanid="cb5-190"><ahref="#cb5-190"aria-hidden="true"tabindex="-1"></a> model_config <spanclass="op">=</span> {<spanclass="st">"frozen"</span>: <spanclass="va">True</span>} <spanclass="co"># Stammdaten aendern sich nicht im Lauf</span></span>
<spanid="cb5-194"><ahref="#cb5-194"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Produktionsprogrammplanung - das Modell aus Kapitel Einfuehrung.</span></span>
<spanid="cb5-196"><ahref="#cb5-196"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Die Pruefungen stehen hier und nicht im Solvercode. Damit gelten sie</span></span>
<spanid="cb5-197"><ahref="#cb5-197"aria-hidden="true"tabindex="-1"></a><spanclass="co"> unabhaengig davon, mit welcher Bibliothek spaeter gerechnet wird - und sie</span></span>
<spanid="cb5-198"><ahref="#cb5-198"aria-hidden="true"tabindex="-1"></a><spanclass="co"> schlagen beim EINLESEN zu, wo der Fehler noch zuzuordnen ist.</span></span>
<spanid="cb5-205"><ahref="#cb5-205"aria-hidden="true"tabindex="-1"></a> namen <spanclass="op">=</span> [p.name <spanclass="cf">for</span> p <spanclass="kw">in</span><spanclass="va">self</span>.produkte]</span>
<spanid="cb5-207"><ahref="#cb5-207"aria-hidden="true"tabindex="-1"></a> doppelt <spanclass="op">=</span><spanclass="bu">sorted</span>({n <spanclass="cf">for</span> n <spanclass="kw">in</span> namen <spanclass="cf">if</span> namen.count(n) <spanclass="op">></span><spanclass="dv">1</span>})</span>
<spanid="cb5-208"><ahref="#cb5-208"aria-hidden="true"tabindex="-1"></a><spanclass="cf">raise</span><spanclass="pp">ValueError</span>(<spanclass="ss">f"Produktnamen kommen mehrfach vor: </span><spanclass="sc">{</span>doppelt<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb5-220"><ahref="#cb5-220"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Feste Reihenfolge - siehe die Spaltenfalle in Kapitel Finanzdaten."""</span></span>
<spanid="cb5-225"><ahref="#cb5-225"aria-hidden="true"tabindex="-1"></a><spanclass="cf">return</span> np.array([[p.verbrauch.get(r, <spanclass="fl">0.0</span>) <spanclass="cf">for</span> p <spanclass="kw">in</span><spanclass="va">self</span>.produkte]</span>
<spanid="cb5-226"><ahref="#cb5-226"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> r <spanclass="kw">in</span><spanclass="va">self</span>.ressourcen])</span>
<spanid="cb5-229"><ahref="#cb5-229"aria-hidden="true"tabindex="-1"></a><spanclass="cf">return</span> np.array([<spanclass="va">self</span>.kapazitaeten[r] <spanclass="cf">for</span> r <spanclass="kw">in</span><spanclass="va">self</span>.ressourcen])</span>
<spanid="cb5-232"><ahref="#cb5-232"aria-hidden="true"tabindex="-1"></a><spanclass="cf">return</span> np.array([p.deckungsbeitrag <spanclass="cf">for</span> p <spanclass="kw">in</span><spanclass="va">self</span>.produkte])</span>
<spanid="cb5-235"><ahref="#cb5-235"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- 4. Abnahmepruefung: Loesung gegen Anforderung, ohne Solver --------------</span></span>
<spanid="cb5-240"><ahref="#cb5-240"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Prueft eine Loesung gegen die Anforderungen - OHNE den Solver zu fragen.</span></span>
<spanid="cb5-242"><ahref="#cb5-242"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Diese Funktion darf ausdruecklich KEIN Solverobjekt und keine</span></span>
<spanid="cb5-243"><ahref="#cb5-243"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Modellvariable benutzen. Eine Pruefung aus denselben Bausteinen wie das</span></span>
<spanid="cb5-244"><ahref="#cb5-244"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Modell prueft das Modell gegen sich selbst und findet nichts (siehe die</span></span>
<spanid="cb5-245"><ahref="#cb5-245"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Denkfehler in den Kapiteln Graphen und MILP).</span></span>
<spanid="cb5-247"><ahref="#cb5-247"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Liefert eine Liste von Beanstandungen; leer heisst bestanden.</span></span>
<spanid="cb5-265"><ahref="#cb5-265"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> ressource, ist, grenze <spanclass="kw">in</span><spanclass="bu">zip</span>(problem.ressourcen, verbrauch,</span>
<spanid="cb5-267"><ahref="#cb5-267"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> ist <spanclass="op">></span> grenze <spanclass="op">+</span> toleranz:</span>
<spanid="cb5-274"><ahref="#cb5-274"aria-hidden="true"tabindex="-1"></a> beanstandungen.append(<spanclass="ss">f"'</span><spanclass="sc">{</span>name<spanclass="sc">}</span><spanclass="ss">' = </span><spanclass="sc">{</span>wert<spanclass="sc">!r}</span><spanclass="ss"> ist nicht ganzzahlig"</span>)</span>
<spanid="cb5-280"><ahref="#cb5-280"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"Zielwert </span><spanclass="sc">{</span>loesung<spanclass="sc">.</span>zielwert<spanclass="sc">:,.4f}</span><spanclass="ss"> passt nicht zu den Mengen "</span></span>
<spanid="cb5-286"><ahref="#cb5-286"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- 5. Ein Modellbauer je Solver - austauschbar ----------------------------</span></span>
<spanid="cb5-295"><ahref="#cb5-295"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p <spanclass="kw">in</span> problem.produkte}</span>
<spanid="cb5-296"><ahref="#cb5-296"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Die Nebenbedingung wird gemerkt - ohne die Referenz gibt es spaeter</span></span>
<spanid="cb5-322"><ahref="#cb5-322"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Derselbe Fall mit scipy.optimize.linprog - anderer Solver, gleiche Loesung.</span></span>
<spanid="cb5-324"><ahref="#cb5-324"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Beachten Sie, wie wenig hier steht: Die Daten kommen fertig geprueft aus</span></span>
<spanid="cb5-325"><ahref="#cb5-325"aria-hidden="true"tabindex="-1"></a><spanclass="co"> dem Domaenenmodell, die Auswertung geht ans gemeinsame Loesung-Objekt.</span></span>
<spanid="cb5-326"><ahref="#cb5-326"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Genau das ist der Ertrag der Trennung.</span></span>
<spanid="cb5-341"><ahref="#cb5-341"aria-hidden="true"tabindex="-1"></a><spanclass="co"># linprog rechnet minimierend mit negierten Kosten - die Dualwerte</span></span>
<spanid="cb5-345"><ahref="#cb5-345"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p, x <spanclass="kw">in</span><spanclass="bu">zip</span>(problem.produkte, ergebnis.x)},</span>
<spanid="cb5-354"><ahref="#cb5-354"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- 6. Ein- und Ausgabe: Tabellen sind die Wirklichkeit ---------------------</span></span>
<spanid="cb5-356"><ahref="#cb5-356"aria-hidden="true"tabindex="-1"></a><spanclass="co"># In den meisten Betrieben liegen die Zahlen in einer Tabellenkalkulation</span></span>
<spanid="cb5-357"><ahref="#cb5-357"aria-hidden="true"tabindex="-1"></a><spanclass="co"># (Kapitel Einfuehrung). Diese beiden Funktionen sind die einzige Stelle im</span></span>
<spanid="cb5-358"><ahref="#cb5-358"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Modul, die das weiss - alles andere arbeitet mit dem Domaenenmodell.</span></span>
<spanid="cb5-360"><ahref="#cb5-360"aria-hidden="true"tabindex="-1"></a><spanclass="co"># pandas und openpyxl werden bewusst erst hier importiert: Wer or_kern nur</span></span>
<spanid="cb5-361"><ahref="#cb5-361"aria-hidden="true"tabindex="-1"></a><spanclass="co"># fuer Statusauswertung und Pruefung benutzt, soll sie nicht installieren</span></span>
<spanid="cb5-370"><ahref="#cb5-370"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Liest eine Excel-Mappe und liefert ein geprueftes Domaenenmodell.</span></span>
<spanid="cb5-372"><ahref="#cb5-372"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Alle Spalten ausser 'Produkt' und 'Deckungsbeitrag' gelten als</span></span>
<spanid="cb5-373"><ahref="#cb5-373"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Verbrauchsspalten - eine neue Ressource ist damit eine neue SPALTE in der</span></span>
<spanid="cb5-374"><ahref="#cb5-374"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Mappe und erfordert keine Codeaenderung.</span></span>
<spanid="cb5-376"><ahref="#cb5-376"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Die eigentliche Pruefung passiert nicht hier, sondern im Konstruktor von</span></span>
<spanid="cb5-377"><ahref="#cb5-377"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Produktionsproblem. Diese Funktion uebersetzt nur Tabelle in Objekt; was</span></span>
<spanid="cb5-378"><ahref="#cb5-378"aria-hidden="true"tabindex="-1"></a><spanclass="co"> ein gueltiges Problem ist, steht an genau einer Stelle.</span></span>
<spanid="cb5-400"><ahref="#cb5-400"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> z <spanclass="kw">in</span> kapazitaeten_tabelle.itertuples()}</span>
<spanid="cb5-407"><ahref="#cb5-407"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Schreibt Plan und Kennzahlen als zwei Blaetter zurueck nach Excel.</span></span>
<spanid="cb5-409"><ahref="#cb5-409"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Die Fachabteilung bekommt das Ergebnis im Format, das sie ohnehin benutzt.</span></span>
<spanid="cb5-429"><ahref="#cb5-429"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> ressource, ist, grenze <spanclass="kw">in</span><spanclass="bu">zip</span>(problem.ressourcen, verbrauch,</span>
<spanid="cb5-440"><ahref="#cb5-440"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> w <spanclass="kw">in</span> kennzahlen.values()],</span>
<spanid="cb5-449"><ahref="#cb5-449"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Die Schreinerei aus Kapitel Einfuehrung - jetzt als Domaenenmodell.</span></span>
<spanid="cb5-461"><ahref="#cb5-461"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" DERSELBE FALL, ZWEI SOLVER, EIN AUSWERTUNGSCODE"</span>)</span>
<spanid="cb5-476"><ahref="#cb5-476"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" WAS DIE PRUEFUNGEN ABFANGEN"</span>)</span>
<spanid="cb5-483"><ahref="#cb5-483"aria-hidden="true"tabindex="-1"></a> (<spanclass="st">"Ressource ohne Kapazitaet"</span>, <spanclass="bu">dict</span>(</span>
<spanid="cb5-494"><ahref="#cb5-494"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span>beschreibung<spanclass="sc">:<28}</span><spanclass="ss"> NICHT erkannt (!)"</span>)</span>
<spanid="cb5-501"><ahref="#cb5-501"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st">Alle drei scheitern beim EINLESEN - nicht erst beim Loesen und"</span>)</span>
<spanid="cb5-502"><ahref="#cb5-502"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"schon gar nicht erst im Bericht. Das ist der ganze Zweck der"</span>)</span>
<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>
<trclass="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 (<ahref="milp.html#sec:milp-gap">Abschnitt 6.8</a>).</td>
</tr>
<trclass="odd">
<td><code>SolverStatus.modellfehler</code></td>
<td>trennt Modell- von Zeitproblemen</td>
<td>Genau die Unterscheidung, die <ahref="cpsat.html#sec:cpsat-statusfaelle">Abschnitt 7.7</a> als häufigste Verwechslung benennt.</td>
<td>Alle drei scheitern beim <strong>Einlesen</strong>. Ohne Domänenschicht fielen sie erst beim Lösen auf — oder gar nicht.</td>
</tr>
<trclass="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>
<trclass="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/>
<h2id="sec:praxisfallen-solverwechsel">22.6 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 (<ahref="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>
<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 (<ahref="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>
<divclass="sourceCode"id="cb7"><preclass="sourceCode python"><codeclass="sourceCode python"><spanid="cb7-1"><ahref="#cb7-1"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(MODELLBAUER[sys.argv[<spanclass="dv">1</span>]](problem).model_dump_json()) <spanclass="co"># im Kindprozess</span></span>
<spanid="cb7-2"><ahref="#cb7-2"aria-hidden="true"tabindex="-1"></a>Loesung.model_validate_json(...) <spanclass="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>
<spanid="cb8-5"><ahref="#cb8-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel Praxisfallen: Denselben Fall einmal mit CP-SAT und einmal mit HiGHS rechnen.</span></span>
<spanid="cb8-7"><ahref="#cb8-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Die Trennung aus or_kern.py behauptet, ein Solverwechsel koste genau EINEN</span></span>
<spanid="cb8-8"><ahref="#cb8-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">Baustein. Dieses Programm loest diese Behauptung ein. Aufgabe ist eine</span></span>
<spanid="cb8-9"><ahref="#cb8-9"aria-hidden="true"tabindex="-1"></a><spanclass="co">Standortplanung: Welche Lager oeffnen wir, und wer beliefert welchen Kunden?</span></span>
<spanid="cb8-12"><ahref="#cb8-12"aria-hidden="true"tabindex="-1"></a><spanclass="co"> baue_und_loese_mit_cpsat() <- der EINE Baustein</span></span>
<spanid="cb8-13"><ahref="#cb8-13"aria-hidden="true"tabindex="-1"></a><spanclass="co"> baue_und_loese_mit_highs() <- ... in zwei Ausfuehrungen</span></span>
<spanid="cb8-18"><ahref="#cb8-18"aria-hidden="true"tabindex="-1"></a><spanclass="co">Die beiden Modellbauer sind 39 und 52 Zeilen lang - sie sind der gesamte</span></span>
<spanid="cb8-19"><ahref="#cb8-19"aria-hidden="true"tabindex="-1"></a><spanclass="co">solverabhaengige Teil des Programms. Alles andere wird zweimal benutzt und</span></span>
<spanid="cb8-22"><ahref="#cb8-22"aria-hidden="true"tabindex="-1"></a><spanclass="co">WARUM ZWEI PROZESSE? ortools und highspy bringen beide eine eigene</span></span>
<spanid="cb8-23"><ahref="#cb8-23"aria-hidden="true"tabindex="-1"></a><spanclass="co">HiGHS-Kopie mit und lassen sich auf vielen Systemen nicht gemeinsam</span></span>
<spanid="cb8-24"><ahref="#cb8-24"aria-hidden="true"tabindex="-1"></a><spanclass="co">importieren (Kapitel Oekosystem). Das Hauptprogramm startet deshalb fuer jeden</span></span>
<spanid="cb8-25"><ahref="#cb8-25"aria-hidden="true"tabindex="-1"></a><spanclass="co">Solver einen eigenen Python-Prozess und laesst sich die Loesung als JSON</span></span>
<spanid="cb8-26"><ahref="#cb8-26"aria-hidden="true"tabindex="-1"></a><spanclass="co">zurueckgeben - das DTO ist nicht nur eine Sprachregelung, sondern ein</span></span>
<spanid="cb8-27"><ahref="#cb8-27"aria-hidden="true"tabindex="-1"></a><spanclass="co">Datenformat, das eine Prozessgrenze ueberlebt.</span></span>
<spanid="cb8-30"><ahref="#cb8-30"aria-hidden="true"tabindex="-1"></a><spanclass="co"> python3 Solverwechsel_CPSAT_HiGHS.py # beide, mit Vergleich</span></span>
<spanid="cb8-31"><ahref="#cb8-31"aria-hidden="true"tabindex="-1"></a><spanclass="co"> python3 Solverwechsel_CPSAT_HiGHS.py cpsat # nur der Kindprozess</span></span>
<spanid="cb8-34"><ahref="#cb8-34"aria-hidden="true"tabindex="-1"></a><spanclass="co">Benoetigt: numpy, pydantic, ortools, highspy (jeweils im eigenen Prozess)</span></span>
<spanid="cb8-54"><ahref="#cb8-54"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Kapazitierte Standortplanung mit Einzelbelieferung.</span></span>
<spanid="cb8-56"><ahref="#cb8-56"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Nach dem Muster von Produktionsproblem in or_kern.py: Die Pruefungen</span></span>
<spanid="cb8-57"><ahref="#cb8-57"aria-hidden="true"tabindex="-1"></a><spanclass="co"> stehen im Konstruktor, nicht im Solvercode - sie gelten damit fuer beide</span></span>
<spanid="cb8-58"><ahref="#cb8-58"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Solver, und sie schlagen beim Einlesen zu.</span></span>
<spanid="cb8-60"><ahref="#cb8-60"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Alle Kosten sind ganzzahlig (Euro). Das ist keine Bequemlichkeit, sondern</span></span>
<spanid="cb8-65"><ahref="#cb8-65"aria-hidden="true"tabindex="-1"></a> fixkosten: <spanclass="bu">list</span>[<spanclass="bu">int</span>] <spanclass="co"># je Lager, faellt bei Eroeffnung an</span></span>
<spanid="cb8-66"><ahref="#cb8-66"aria-hidden="true"tabindex="-1"></a> kapazitaet: <spanclass="bu">list</span>[<spanclass="bu">int</span>] <spanclass="co"># je Lager, in Paletten</span></span>
<spanid="cb8-67"><ahref="#cb8-67"aria-hidden="true"tabindex="-1"></a> bedarf: <spanclass="bu">list</span>[<spanclass="bu">int</span>] <spanclass="co"># je Kunde, in Paletten</span></span>
<spanid="cb8-68"><ahref="#cb8-68"aria-hidden="true"tabindex="-1"></a> transport: <spanclass="bu">list</span>[<spanclass="bu">list</span>[<spanclass="bu">int</span>]] <spanclass="co"># [Lager][Kunde], Kosten der Belieferung</span></span>
<spanid="cb8-72"><ahref="#cb8-72"aria-hidden="true"tabindex="-1"></a> n, m <spanclass="op">=</span><spanclass="bu">len</span>(<spanclass="va">self</span>.lager), <spanclass="bu">len</span>(<spanclass="va">self</span>.kunden)</span>
<spanid="cb8-73"><ahref="#cb8-73"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span><spanclass="bu">len</span>(<spanclass="va">self</span>.fixkosten) <spanclass="op">!=</span> n <spanclass="kw">or</span><spanclass="bu">len</span>(<spanclass="va">self</span>.kapazitaet) <spanclass="op">!=</span> n:</span>
<spanid="cb8-76"><ahref="#cb8-76"aria-hidden="true"tabindex="-1"></a><spanclass="cf">raise</span><spanclass="pp">ValueError</span>(<spanclass="ss">f"bedarf muss </span><spanclass="sc">{</span>m<spanclass="sc">}</span><spanclass="ss"> Eintraege haben"</span>)</span>
<spanid="cb8-77"><ahref="#cb8-77"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span><spanclass="bu">len</span>(<spanclass="va">self</span>.transport) <spanclass="op">!=</span> n <spanclass="kw">or</span><spanclass="bu">any</span>(<spanclass="bu">len</span>(z) <spanclass="op">!=</span> m <spanclass="cf">for</span> z <spanclass="kw">in</span><spanclass="va">self</span>.transport):</span>
<spanid="cb8-78"><ahref="#cb8-78"aria-hidden="true"tabindex="-1"></a><spanclass="cf">raise</span><spanclass="pp">ValueError</span>(<spanclass="ss">f"transport muss </span><spanclass="sc">{</span>n<spanclass="sc">}</span><spanclass="ss"> x </span><spanclass="sc">{</span>m<spanclass="sc">}</span><spanclass="ss"> sein"</span>)</span>
<spanid="cb8-80"><ahref="#cb8-80"aria-hidden="true"tabindex="-1"></a><spanclass="cf">raise</span><spanclass="pp">ValueError</span>(<spanclass="ss">f"Gesamtkapazitaet </span><spanclass="sc">{</span><spanclass="bu">sum</span>(<spanclass="va">self</span>.kapazitaet)<spanclass="sc">}</span><spanclass="ss"> deckt den "</span></span>
<spanid="cb8-85"><ahref="#cb8-85"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Variablenname im Loesung-DTO - beide Modellbauer benutzen ihn."""</span></span>
<spanid="cb8-105"><ahref="#cb8-105"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- 2. Der eine Baustein, der sich aendert: der Modellbauer -----------------</span></span>
<spanid="cb8-111"><ahref="#cb8-111"aria-hidden="true"tabindex="-1"></a> n, m <spanclass="op">=</span><spanclass="bu">len</span>(problem.lager), <spanclass="bu">len</span>(problem.kunden)</span>
<spanid="cb8-112"><ahref="#cb8-112"aria-hidden="true"tabindex="-1"></a> modell <spanclass="op">=</span> cp_model.CpModel()</span>
<spanid="cb8-113"><ahref="#cb8-113"aria-hidden="true"tabindex="-1"></a> y <spanclass="op">=</span> [modell.NewBoolVar(<spanclass="ss">f"offen_</span><spanclass="sc">{</span>i<spanclass="sc">}</span><spanclass="ss">"</span>) <spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n)]</span>
<spanid="cb8-114"><ahref="#cb8-114"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> {(i, j): modell.NewBoolVar(<spanclass="ss">f"liefert_</span><spanclass="sc">{</span>i<spanclass="sc">}</span><spanclass="ss">_</span><spanclass="sc">{</span>j<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb8-115"><ahref="#cb8-115"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n) <spanclass="cf">for</span> j <spanclass="kw">in</span><spanclass="bu">range</span>(m)}</span>
<spanid="cb8-117"><ahref="#cb8-117"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> j <spanclass="kw">in</span><spanclass="bu">range</span>(m): <spanclass="co"># jeder Kunde genau einmal</span></span>
<spanid="cb8-118"><ahref="#cb8-118"aria-hidden="true"tabindex="-1"></a> modell.AddExactlyOne(x[i, j] <spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n))</span>
<spanid="cb8-119"><ahref="#cb8-119"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n):</span>
<spanid="cb8-120"><ahref="#cb8-120"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> j <spanclass="kw">in</span><spanclass="bu">range</span>(m): <spanclass="co"># nur aus offenen Lagern</span></span>
<spanid="cb8-147"><ahref="#cb8-147"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""HiGHS: dieselben Restriktionen als Ungleichungszeilen einer Matrix."""</span></span>
<spanid="cb8-150"><ahref="#cb8-150"aria-hidden="true"tabindex="-1"></a> n, m <spanclass="op">=</span><spanclass="bu">len</span>(problem.lager), <spanclass="bu">len</span>(problem.kunden)</span>
<spanid="cb8-151"><ahref="#cb8-151"aria-hidden="true"tabindex="-1"></a> anzahl_x <spanclass="op">=</span> n <spanclass="op">*</span> m <spanclass="co"># Spalten 0..n*m-1</span></span>
<spanid="cb8-152"><ahref="#cb8-152"aria-hidden="true"tabindex="-1"></a> spalte_y <spanclass="op">=</span><spanclass="kw">lambda</span> i: anzahl_x <spanclass="op">+</span> i <spanclass="co"># danach die y_i # noqa: E731</span></span>
<spanid="cb8-160"><ahref="#cb8-160"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n):</span>
<spanid="cb8-165"><ahref="#cb8-165"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> j <spanclass="kw">in</span><spanclass="bu">range</span>(m): <spanclass="co"># jeder Kunde genau einmal</span></span>
<spanid="cb8-166"><ahref="#cb8-166"aria-hidden="true"tabindex="-1"></a> index <spanclass="op">=</span> np.array([i <spanclass="op">*</span> m <spanclass="op">+</span> j <spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n)], dtype<spanclass="op">=</span>np.int32)</span>
<spanid="cb8-167"><ahref="#cb8-167"aria-hidden="true"tabindex="-1"></a> modell.addRow(<spanclass="fl">1.0</span>, <spanclass="fl">1.0</span>, n, index, np.ones(n))</span>
<spanid="cb8-168"><ahref="#cb8-168"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n):</span>
<spanid="cb8-183"><ahref="#cb8-183"aria-hidden="true"tabindex="-1"></a> status <spanclass="op">=</span> status_von_highs(modell.modelStatusToString(modell.getModelStatus()))</span>
<spanid="cb8-201"><ahref="#cb8-201"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- 3. Alles Weitere ist wieder gemeinsam -----------------------------------</span></span>
<spanid="cb8-205"><ahref="#cb8-205"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Prueft die Loesung gegen die Anforderung - ohne Solver, ohne Modell."""</span></span>
<spanid="cb8-209"><ahref="#cb8-209"aria-hidden="true"tabindex="-1"></a> n, m <spanclass="op">=</span><spanclass="bu">len</span>(problem.lager), <spanclass="bu">len</span>(problem.kunden)</span>
<spanid="cb8-215"><ahref="#cb8-215"aria-hidden="true"tabindex="-1"></a> beanstandungen.append(<spanclass="st">"Zuordnungen sind nicht 0/1"</span>)</span>
<spanid="cb8-218"><ahref="#cb8-218"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> j, kunde <spanclass="kw">in</span><spanclass="bu">enumerate</span>(problem.kunden):</span>
<spanid="cb8-223"><ahref="#cb8-223"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i, lagername <spanclass="kw">in</span><spanclass="bu">enumerate</span>(problem.lager):</span>
<spanid="cb8-233"><ahref="#cb8-233"aria-hidden="true"tabindex="-1"></a> beanstandungen.append(<spanclass="ss">f"Zielwert </span><spanclass="sc">{</span>loesung<spanclass="sc">.</span>zielwert<spanclass="sc">:,.0f}</span><spanclass="ss"> passt nicht zur "</span></span>
<spanid="cb8-239"><ahref="#cb8-239"aria-hidden="true"tabindex="-1"></a> m <spanclass="op">=</span><spanclass="bu">len</span>(problem.kunden)</span>
<spanid="cb8-240"><ahref="#cb8-240"aria-hidden="true"tabindex="-1"></a><spanclass="cf">return</span> [name <spanclass="cf">for</span> i, name <spanclass="kw">in</span><spanclass="bu">enumerate</span>(problem.lager)</span>
<spanid="cb8-245"><ahref="#cb8-245"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Startet dieses Programm noch einmal - mit genau einem Solverimport."""</span></span>
<spanid="cb8-250"><ahref="#cb8-250"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Das DTO als JSON - genau dafuer ist ein Datenobjekt ohne Solverbezug gut.</span></span>
<spanid="cb8-257"><ahref="#cb8-257"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Kindprozess: rechnen und das DTO als JSON ausgeben ---------------</span></span>
<spanid="cb8-262"><ahref="#cb8-262"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Hauptprozess: beide Solver anstossen und vergleichen -------------</span></span>
<spanid="cb8-264"><ahref="#cb8-264"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" DERSELBE FALL, ZWEI SOLVER - UND EIN AUSWERTUNGSCODE"</span>)</span>
<spanid="cb8-283"><ahref="#cb8-283"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Was der Vergleich zeigt -----------------------------------------</span></span>
<spanid="cb8-290"><ahref="#cb8-290"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> s <spanclass="kw">in</span> loesungen[<spanclass="st">"cpsat"</span>].werte)</span>
<spanid="cb8-297"><ahref="#cb8-297"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" WAS DER WECHSEL GEKOSTET HAT"</span>)</span>
<spanid="cb8-299"><ahref="#cb8-299"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Ausgetauscht wurde EINE Funktion. Domaenenmodell, Abnahmepruefung und"</span>)</span>
<spanid="cb8-300"><ahref="#cb8-300"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Bericht sind woertlich dieselben - sie sehen den Solver nie."</span>)</span>
<spanid="cb8-302"><ahref="#cb8-302"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Nicht umsonst ist der Wechsel trotzdem:"</span>)</span>
<spanid="cb8-303"><ahref="#cb8-303"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * CP-SAT rechnet ausschliesslich GANZZAHLIG. Alle Kosten sind hier"</span>)</span>
<spanid="cb8-304"><ahref="#cb8-304"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" deshalb int. Wer in Euro und Cent rechnet, skaliert vorher auf Cent -"</span>)</span>
<spanid="cb8-305"><ahref="#cb8-305"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" und muss das im Bericht wieder zuruecknehmen."</span>)</span>
<spanid="cb8-306"><ahref="#cb8-306"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * HiGHS braucht die Restriktionen als Matrixzeilen, CP-SAT nimmt sie"</span>)</span>
<spanid="cb8-307"><ahref="#cb8-307"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" als Ausdruecke. Das ist der Grund, warum der HiGHS-Modellbauer"</span>)</span>
<spanid="cb8-308"><ahref="#cb8-308"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" laenger ist, obwohl er dasselbe Modell beschreibt."</span>)</span>
<spanid="cb8-309"><ahref="#cb8-309"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * Beide Bibliotheken bringen eine eigene HiGHS-Kopie mit und lassen"</span>)</span>
<spanid="cb8-310"><ahref="#cb8-310"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" sich nicht gemeinsam importieren - daher die zwei Prozesse."</span>)</span>
<spanid="cb8-312"><ahref="#cb8-312"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Der Ertrag: Beide beweisen denselben optimalen Zielwert, und die"</span>)</span>
<spanid="cb8-313"><ahref="#cb8-313"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Entscheidung zwischen ihnen ist eine Frage der Laufzeit geworden -"</span>)</span>
<spanid="cb8-314"><ahref="#cb8-314"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"nicht eine Frage, wie viel Code man neu schreiben muss."</span>)</span>
<spanid="cb8-316"><ahref="#cb8-316"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Verglichen wird deshalb der ZIELWERT, nicht der Plan: Gibt es mehrere"</span>)</span>
<spanid="cb8-317"><ahref="#cb8-317"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"gleich teure Loesungen, darf jeder Solver eine andere davon liefern."</span>)</span>
<spanid="cb8-318"><ahref="#cb8-318"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Hier stimmen sie zufaellig ueberein - darauf zu testen waere trotzdem"</span>)</span>
<spanid="cb8-319"><ahref="#cb8-319"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"ein unzuverlaessiger Test (siehe JobShop_Intervalle.py)."</span>)</span>
<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 <spanclass="math inline">+1</span> und <spanclass="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 <ahref="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/>
<h2id="sec:praxisfallen-eine-checkliste-vor-dem-produktivgang">22.7 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/>
<h2id="sec:praxisfallen-weiterfuehrende-literatur-und-roadmap">22.8 Weiterführende Literatur und Roadmap</h2>
<p><strong>Grundlagen und lineare/ganzzahlige Optimierung</strong> * Bertsimas & 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 & 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 & 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>
<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 Übung 6.7 (Wochendienstplan) 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 <ahref="#sec:praxisfallen-die-fuenf-typischen-praxisfallen">Abschnitt 22.3</a> beantwortet.</p>
<p><strong>Aufgabe 22.5 ⭐⭐⭐ — Gap gegen Laufzeit.</strong> Nehmen Sie das MILP-Portfolio aus <ahref="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.6 ⭐⭐⭐ — Post-Mortem schreiben.</strong> Suchen Sie sich einen der im Buch besprochenen Fehler aus (z. B. die Vorzeichenfalle in <ahref="lp.html#sec:lp-die-vorzeichenfalle-bei-schattenpreisen">Abschnitt 5.7</a>, die ungültige Kovarianzmatrix in <ahref="qp-nlp.html#kap-qp-nlp">Kapitel 11</a>, die Sektor-Positionsindizes in <ahref="markowitz.html#kap-markowitz">Kapitel 19</a>, die vermischten Einheiten in <ahref="cvar.html#sec:cvar-implementierung-cvar-portfolio-mit-reibung">Abschnitt 20.6</a> oder die Rebalancing-Termine in <ahref="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/>
<h2id="sec:praxisfallen-denkfehler">22.10 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>
<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>
<trclass="header">
<th>Zeitpunkt</th>
<thstyle="text-align: right;">Kosten</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td>2024-Q1</td>
<tdstyle="text-align: right;">40 484</td>
</tr>
<trclass="even">
<td>2024-Q3</td>
<tdstyle="text-align: right;">51 042</td>
</tr>
<trclass="odd">
<td>2025-Q1</td>
<tdstyle="text-align: right;">64 053</td>
</tr>
<trclass="even">
<td>2025-Q3</td>
<tdstyle="text-align: right;">85 791</td>
</tr>
<trclass="odd">
<td>2026-Q1</td>
<tdstyle="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>
<spanid="cb10-5"><ahref="#cb10-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel Praxisfallen: Das Modell, das still aufgehoert hat zu optimieren.</span></span>
<spanid="cb10-7"><ahref="#cb10-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Ein Optimierungsmodell geht in Betrieb und laeuft jede Nacht. Es liefert</span></span>
<spanid="cb10-8"><ahref="#cb10-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">zuverlaessig eine Zahl, die Job-Dauer bleibt konstant, es gibt keine</span></span>
<spanid="cb10-9"><ahref="#cb10-9"aria-hidden="true"tabindex="-1"></a><spanclass="co">Fehlermeldung. Alles sieht gut aus.</span></span>
<spanid="cb10-11"><ahref="#cb10-11"aria-hidden="true"tabindex="-1"></a><spanclass="co">Waehrenddessen waechst das Unternehmen. Aus 15 Lagern werden 40, dann 80. Das</span></span>
<spanid="cb10-12"><ahref="#cb10-12"aria-hidden="true"tabindex="-1"></a><spanclass="co">Zeitlimit von drei Sekunden - vor zwei Jahren grosszuegig bemessen - reicht</span></span>
<spanid="cb10-13"><ahref="#cb10-13"aria-hidden="true"tabindex="-1"></a><spanclass="co">irgendwann nicht mehr. Der Solver bricht ab und liefert die beste Loesung, die</span></span>
<spanid="cb10-14"><ahref="#cb10-14"aria-hidden="true"tabindex="-1"></a><spanclass="co">er bis dahin gefunden hat.</span></span>
<spanid="cb10-16"><ahref="#cb10-16"aria-hidden="true"tabindex="-1"></a><spanclass="co">Das ist nicht falsch. Es ist sogar richtig so. Aber es ist etwas anderes als</span></span>
<spanid="cb10-17"><ahref="#cb10-17"aria-hidden="true"tabindex="-1"></a><spanclass="co">das, was das Modell einmal geleistet hat - und niemand merkt es, weil im</span></span>
<spanid="cb10-18"><ahref="#cb10-18"aria-hidden="true"tabindex="-1"></a><spanclass="co">Protokoll nur die Kosten stehen.</span></span>
<spanid="cb10-20"><ahref="#cb10-20"aria-hidden="true"tabindex="-1"></a><spanclass="co">Dieses Programm zeigt die Verschlechterung und die Ueberwachung, die sie</span></span>
<spanid="cb10-23"><ahref="#cb10-23"aria-hidden="true"tabindex="-1"></a><spanclass="co">WICHTIG: ortools wird hier nicht importiert (Konflikt mit highspy, siehe</span></span>
<spanid="cb10-37"><ahref="#cb10-37"aria-hidden="true"tabindex="-1"></a>ZEITLIMIT <spanclass="op">=</span><spanclass="fl">3.0</span><spanclass="co"># das Budget, das der Nachtjob hat</span></span>
<spanid="cb10-38"><ahref="#cb10-38"aria-hidden="true"tabindex="-1"></a>GAP_WARNSCHWELLE <spanclass="op">=</span><spanclass="fl">0.02</span><spanclass="co"># ab 2 % Gap wollen wir es wissen</span></span>
<spanid="cb10-46"><ahref="#cb10-46"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Die meisten Systeme schreiben nur 'kosten'. Genau deshalb faellt eine</span></span>
<spanid="cb10-47"><ahref="#cb10-47"aria-hidden="true"tabindex="-1"></a><spanclass="co"> schleichende Verschlechterung ueber Monate nicht auf.</span></span>
<spanid="cb10-74"><ahref="#cb10-74"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Standortplanung wie in Kapitel MILP, nur mit wachsender Groesse."""</span></span>
<spanid="cb10-89"><ahref="#cb10-89"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n_lager):</span>
<spanid="cb10-90"><ahref="#cb10-90"aria-hidden="true"tabindex="-1"></a> modell.changeColIntegrality(anzahl_x <spanclass="op">+</span> i, highspy.HighsVarType.kInteger)</span>
<spanid="cb10-91"><ahref="#cb10-91"aria-hidden="true"tabindex="-1"></a> modell.changeColCost(anzahl_x <spanclass="op">+</span> i, fixkosten[i])</span>
<spanid="cb10-98"><ahref="#cb10-98"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n_lager):</span>
<spanid="cb10-114"><ahref="#cb10-114"aria-hidden="true"tabindex="-1"></a><spanclass="co"># So ist das Unternehmen ueber zwei Jahre gewachsen.</span></span>
<spanid="cb10-115"><ahref="#cb10-115"aria-hidden="true"tabindex="-1"></a> entwicklung <spanclass="op">=</span> [(<spanclass="st">"2024-Q1"</span>, <spanclass="dv">15</span>, <spanclass="dv">40</span>), (<spanclass="st">"2024-Q3"</span>, <spanclass="dv">25</span>, <spanclass="dv">70</span>),</span>
<spanid="cb10-120"><ahref="#cb10-120"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" WAS DAS PROTOKOLL ZEIGT - UND WAS ES ZEIGEN SOLLTE"</span>)</span>
<spanid="cb10-136"><ahref="#cb10-136"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st"> -> Die Kosten steigen. Das Unternehmen waechst ja auch."</span>)</span>
<spanid="cb10-137"><ahref="#cb10-137"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Nichts an dieser Tabelle deutet auf ein Problem hin."</span>)</span>
<spanid="cb10-150"><ahref="#cb10-150"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Und so sehen die Warnungen aus, die daraus folgen:</span><spanclass="ch">\n</span><spanclass="st">"</span>)</span>
<spanid="cb10-151"><ahref="#cb10-151"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p <spanclass="kw">in</span> protokolle:</span>
<spanid="cb10-155"><ahref="#cb10-155"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span>p<spanclass="sc">.</span>zeitpunkt<spanclass="sc">}</span><spanclass="ss">: in Ordnung"</span>)</span>
<spanid="cb10-157"><ahref="#cb10-157"aria-hidden="true"tabindex="-1"></a> letzte <spanclass="op">=</span> protokolle[<spanclass="op">-</span><spanclass="dv">1</span>]</span>
<spanid="cb10-158"><ahref="#cb10-158"aria-hidden="true"tabindex="-1"></a> erste_warnung <spanclass="op">=</span><spanclass="bu">next</span>(p <spanclass="cf">for</span> p <spanclass="kw">in</span> protokolle <spanclass="cf">if</span> p.warnungen)</span>
<spanid="cb10-161"><ahref="#cb10-161"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" WAS DA PASSIERT IST"</span>)</span>
<spanid="cb10-163"><ahref="#cb10-163"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Seit </span><spanclass="sc">{</span>erste_warnung<spanclass="sc">.</span>zeitpunkt<spanclass="sc">}</span><spanclass="ss"> erreicht der Job das Zeitlimit und liefert"</span>)</span>
<spanid="cb10-164"><ahref="#cb10-164"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"nicht mehr das Optimum, sondern die beste bis dahin gefundene Loesung."</span>)</span>
<spanid="cb10-165"><ahref="#cb10-165"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Im letzten Lauf betraegt der Abstand zum Bestmoeglichen "</span></span>
<spanid="cb10-168"><ahref="#cb10-168"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Der Job ist nicht abgestuerzt. Er hat keine Fehlermeldung erzeugt."</span>)</span>
<spanid="cb10-169"><ahref="#cb10-169"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Die Laufzeit ist sogar bemerkenswert STABIL geblieben - genau deshalb,"</span>)</span>
<spanid="cb10-170"><ahref="#cb10-170"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"weil das Zeitlimit greift. Ein Ueberwachungssystem, das auf Abstuerze"</span>)</span>
<spanid="cb10-171"><ahref="#cb10-171"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"und Laufzeitspitzen achtet, sieht hier nichts."</span>)</span>
<spanid="cb10-173"><ahref="#cb10-173"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Drei Zahlen gehoeren deshalb in jedes Protokoll eines Optimierungsjobs:"</span>)</span>
<spanid="cb10-174"><ahref="#cb10-174"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * der STATUS - 'Optimal' oder etwas anderes?"</span>)</span>
<spanid="cb10-175"><ahref="#cb10-175"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * der GAP - wie weit ist die Loesung vom Bestmoeglichen entfernt?"</span>)</span>
<spanid="cb10-176"><ahref="#cb10-176"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * die LAUFZEIT im Verhaeltnis zum Limit - wie nah am Anschlag?"</span>)</span>
<spanid="cb10-178"><ahref="#cb10-178"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Die dritte ist die frueheste Warnung: Sie steigt, lange bevor der Gap"</span>)</span>
<spanid="cb10-179"><ahref="#cb10-179"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"sichtbar wird, und gibt Zeit zum Handeln."</span>)</span>
<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>
<p>Genau eine Antwort ist jeweils richtig. Auflösung in <ahref="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>
<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>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>