<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{idx: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><lidata-kapitel="synthese-grundlagen.html"><ahref="synthese-grundlagen.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Synthese Teil I</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{idx: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{idx: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><lidata-kapitel="synthese-kernverfahren.html"><ahref="synthese-kernverfahren.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Synthese Teil II</span></a></li></ul></details><detailsclass="sidebar-gruppe"><summary>Teil III:
<navclass="prev-next"><aclass="prev-next-knopf prev-next-prev"href="praxisfallen.html"><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 22: Praxisfallen und der Weg zum produktiven Einsatz</span></a><aclass="prev-next-knopf prev-next-next"href="synthese-praxis.html"><span><small>Weiter</small>Synthese Teil V</span><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-right"></use></svg></a></nav>
<p><strong>Worum geht es?</strong> Um die drei Fragen, die zwischen einem funktionierenden Modell und einem System stehen, auf das sich jemand verlässt: <em>Woher weiß ich, dass es stimmt? Woher weiß ich, dass es schnell genug ist? Wie kommt es zu den Leuten, die es brauchen?</em></p>
<p><strong>Voraussetzungen:</strong><ahref="praxisfallen.html#kap-praxisfallen">Kapitel 22</a>, insbesondere <code>or_kern.py</code> aus <ahref="praxisfallen.html#sec:praxisfallen-or-kern">Abschnitt 22.6</a> — dieses Kapitel testet genau dieses Modul.</p>
<p><strong>Danach können Sie:</strong> ein Optimierungsmodell testen, obwohl Sie die richtige Antwort nicht kennen; Ihre eigene Testsuite auf Lücken prüfen; einen Solververgleich aufsetzen, dem man glauben kann; und das Modell als Dienst betreiben, ohne dass die erste große Anfrage ihn blockiert.</p>
<p><strong>Zeitbedarf:</strong> ca. 5 Stunden.</p>
<p><strong>Notebook:</strong><ahref="Notebooks_04/testing.ipynb">testing.ipynb</a> — herunterladen und in Jupyter öffnen, in Colab hochladen oder mit dem Kurs-Image starten</p>
<p>Die Schreinerei aus <ahref="einfuehrung.html#kap-einfuehrung">Kapitel 1</a>, nur mit etwas knapperen Kapazitäten. Der <ahref="anhang-glossar.md#gloss:solver"class="glossar-link">Solver</a> liefert einen Plan, halbe Tische gibt es nicht — also wird gerundet.</p>
<spanid="cb1-11"><ahref="#cb1-11"aria-hidden="true"tabindex="-1"></a>plan <spanclass="op">=</span> np.<spanclass="bu">round</span>(loesung.x) <spanclass="co"># "Halbe Tische gibt es nicht"</span></span>
<spanid="cb1-13"><ahref="#cb1-13"aria-hidden="true"tabindex="-1"></a>verbrauch <spanclass="op">=</span> verbrauch_je_stueck <spanclass="op">@</span> plan <spanclass="co"># DER </span><spanclass="al">TEST</span></span>
<p><strong>Und jetzt der Punkt.</strong> Der Solver hat richtig gerechnet: 30,67 Tische und 48 Stühle sind das Optimum des <ahref="anhang-glossar.md#gloss:lineare-programmierung-lp"class="glossar-link">LP (Lineare Programmierung)</a>. Das Runden hat den Plan kaputt gemacht — 31 Tische brauchen eine Montagestunde mehr, als es gibt. Ohne die drei Zeilen wäre dieser Plan in die Fertigung gegangen, und dort hätte am Monatsende eine Stunde gefehlt, die niemand einer Zeile Python zugeordnet hätte.</p>
<p>Bemerkenswert ist, <strong>was</strong> der Test prüft. Er prüft nicht, ob 30,67 das Optimum ist — das könnte er gar nicht, dafür bräuchte er einen zweiten Solver. Er prüft die Eigenschaft, die jede brauchbare Lösung haben muss: <em>Sie hält die Kapazität ein.</em> Diese Verschiebung — <strong>von der Frage nach dem richtigen Wert zur Frage nach den notwendigen Eigenschaften</strong> — ist der Schlüssel zum ganzen Kapitel.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Ein Optimierungsmodell lässt sich fast nie gegen den richtigen Wert testen. Gegen seine Eigenschaften lässt es sich immer testen.</p>
</blockquote>
<p><strong>Warum funktioniert das?</strong> Weil die Prüfung aus einer <strong>anderen Richtung</strong> kommt als das Modell. Der Solver bekam die Matrix als Nebenbedingung, der Test benutzt sie als Nachrechnung des fertigen Plans. Dass beide dieselbe Matrix verwenden, ist dabei die Grenze des Verfahrens — darauf kommt <ahref="#sec:testing-denkfehler">Abschnitt 23.9</a> zurück.</p>
<hr/>
<h2id="sec:testing-lernziele">23.2 Lernziele</h2>
<p>Nach diesem Kapitel können Sie …</p>
<oltype="1">
<li>… die vier Testarten benennen, die bei Optimierungsmodellen tragen, wenn der Sollwert unbekannt ist.</li>
<li>… eine Testsuite so aufbauen, dass sie beide Modellbauer durchläuft statt nur einen.</li>
<li>… mit einem <strong>Mutationstest</strong> herausfinden, welche Fehler Ihre Suite durchgehen lässt.</li>
<li>… einen Solververgleich aufsetzen, der Aufbau und Lösen trennt, Zielwerte gegeneinander prüft und seine eigene Reichweite benennt.</li>
<li>… begründen, warum ein Optimierungsdienst zweistufig sein muss, und messen, ob Threads oder Prozesse die richtige Wahl sind.</li>
</ol>
<hr/>
<h2id="sec:testing-warum-schwer">23.3 Warum Optimierungsmodelle schwer zu testen sind</h2>
<p>Bei einer gewöhnlichen Funktion schreibt man den erwarteten Wert hin:</p>
<p>Bei „der beste Produktionsplan für 400 Aufträge” gibt es niemanden, der die Antwort unabhängig ausrechnen könnte. Gäbe es ihn, bräuchte man den Solver nicht. Damit fällt die naheliegende Testform aus — und mit ihr die Illusion, man könne Optimierungscode wie Geschäftslogik testen.</p>
<p>Was bleibt, sind vier Ersatzformen:</p>
<table>
<colgroup>
<colstyle="width: 33%"/>
<colstyle="width: 33%"/>
<colstyle="width: 33%"/>
</colgroup>
<thead>
<trclass="header">
<th>Art</th>
<th>Was geprüft wird</th>
<th>Beispiel aus diesem Kapitel</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td><strong>Eigenschaften</strong></td>
<td>Was jede zulässige Lösung erfüllen muss</td>
<td>Kapazitäten eingehalten, Zielwert passt zu den Mengen</td>
</tr>
<trclass="even">
<td><strong>Invarianten</strong></td>
<td>Was das Ergebnis <em>nicht</em> ändern darf</td>
<td>Produktreihenfolge, Währungseinheit, ein Produkt mit Deckungsbeitrag 0</td>
</tr>
<trclass="odd">
<td><strong>Regression</strong></td>
<td>Eine kleine Instanz mit von Hand belegtem Optimum</td>
<td>Dass Unsinn abgewiesen wird — und die Prüfung wirklich anschlägt</td>
<td>kaputte Lösungen an <code>pruefe_loesung()</code> verfüttern</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>⚠️ Die Testart, die am häufigsten fehlt</strong></p>
<p>Die vierte. Fast jede Suite prüft, dass gute Eingaben gute Ergebnisse liefern; kaum eine prüft, dass <strong>schlechte Eingaben</strong> auffliegen.</p>
<p>Das ist besonders bei Optimierungsmodellen fatal, weil die Abnahmeprüfung selbst Code ist, der falsch sein kann. Eine <code>pruefe_loesung()</code>, die versehentlich immer eine leere Liste zurückgibt, sieht in einer grünen Suite genauso aus wie eine, die funktioniert. Man merkt es erst, wenn sie etwas hätte finden sollen — also im Betrieb.</p>
<p>Die Tests unten füttern die Prüfung deshalb mit Lösungen, die absichtlich falsch sind: zu viel verbraucht, negative Mengen, ein Zielwert, der nicht zu den Mengen passt. Jeder dieser Tests verlangt, dass die Prüfung <strong>anschlägt</strong>.</p>
<spanid="cb4-5"><ahref="#cb4-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel Testen: Eine Testsuite fuer ein Optimierungsmodell.</span></span>
<spanid="cb4-7"><ahref="#cb4-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Das Grundproblem beim Testen von Optimierungsmodellen: Man kennt die richtige</span></span>
<spanid="cb4-8"><ahref="#cb4-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">Antwort nicht. Bei einer Funktion `steuer(brutto)` schreibt man den erwarteten</span></span>
<spanid="cb4-9"><ahref="#cb4-9"aria-hidden="true"tabindex="-1"></a><spanclass="co">Wert hin. Bei "der beste Produktionsplan fuer 400 Auftraege" gibt es niemanden,</span></span>
<spanid="cb4-10"><ahref="#cb4-10"aria-hidden="true"tabindex="-1"></a><spanclass="co">der ihn unabhaengig ausrechnen koennte - sonst braeuchte man den Solver nicht.</span></span>
<spanid="cb4-12"><ahref="#cb4-12"aria-hidden="true"tabindex="-1"></a><spanclass="co">Deshalb testet man nicht den WERT, sondern vier andere Dinge:</span></span>
<spanid="cb4-14"><ahref="#cb4-14"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 1. EIGENSCHAFTEN Die Loesung haelt jede Nebenbedingung ein, und der</span></span>
<spanid="cb4-15"><ahref="#cb4-15"aria-hidden="true"tabindex="-1"></a><spanclass="co"> ausgewiesene Zielwert passt zu den Mengen.</span></span>
<spanid="cb4-16"><ahref="#cb4-16"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 2. INVARIANTEN Was das Ergebnis NICHT aendern darf: die Reihenfolge der</span></span>
<spanid="cb4-17"><ahref="#cb4-17"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Produkte, die Waehrungseinheit, ein zusaetzliches Produkt</span></span>
<spanid="cb4-18"><ahref="#cb4-18"aria-hidden="true"tabindex="-1"></a><spanclass="co"> mit Deckungsbeitrag 0.</span></span>
<spanid="cb4-19"><ahref="#cb4-19"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 3. REGRESSION Eine kleine Instanz mit von Hand nachgerechnetem Optimum.</span></span>
<spanid="cb4-20"><ahref="#cb4-20"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 4. FEHLERFAELLE Unsinnige Eingaben werden abgewiesen, unloesbare Modelle</span></span>
<spanid="cb4-21"><ahref="#cb4-21"aria-hidden="true"tabindex="-1"></a><spanclass="co"> als unloesbar erkannt - und die Abnahmepruefung schlaegt</span></span>
<spanid="cb4-22"><ahref="#cb4-22"aria-hidden="true"tabindex="-1"></a><spanclass="co"> tatsaechlich an, wenn man ihr eine kaputte Loesung gibt.</span></span>
<spanid="cb4-24"><ahref="#cb4-24"aria-hidden="true"tabindex="-1"></a><spanclass="co">Der vierte Punkt ist der wichtigste und wird am haeufigsten vergessen: Eine</span></span>
<spanid="cb4-25"><ahref="#cb4-25"aria-hidden="true"tabindex="-1"></a><spanclass="co">Pruefung, die noch nie etwas gefunden hat, ist keine Pruefung, sondern eine</span></span>
<spanid="cb4-26"><ahref="#cb4-26"aria-hidden="true"tabindex="-1"></a><spanclass="co">Vermutung. Die Tests unten fuettern `pruefe_loesung()` deshalb absichtlich mit</span></span>
<spanid="cb4-27"><ahref="#cb4-27"aria-hidden="true"tabindex="-1"></a><spanclass="co">falschen Loesungen und verlangen, dass sie anschlaegt.</span></span>
<spanid="cb4-45"><ahref="#cb4-45"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Die Instanz, gegen die getestet wird ----------------------------------</span></span>
<spanid="cb4-48"><ahref="#cb4-48"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Der Fall aus Kapitel Einfuehrung - klein genug fuer die Handrechnung.</span></span>
<spanid="cb4-65"><ahref="#cb4-65"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Beide Modellbauer durchlaufen JEDEN Test. Ein Test, der nur mit einem Solver</span></span>
<spanid="cb4-66"><ahref="#cb4-66"aria-hidden="true"tabindex="-1"></a><spanclass="co"># laeuft, prueft die Bibliothek mit - nicht das Modell.</span></span>
<spanid="cb4-89"><ahref="#cb4-89"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Klingt trivial, ist es nicht: Wer die Zielfunktion im Modell anders</span></span>
<spanid="cb4-90"><ahref="#cb4-90"aria-hidden="true"tabindex="-1"></a><spanclass="co"> zusammensetzt als im Bericht (etwa Gebuehren einmal abgezogen, einmal</span></span>
<spanid="cb4-91"><ahref="#cb4-91"aria-hidden="true"tabindex="-1"></a><spanclass="co"> nicht), merkt es sonst nie.</span></span>
<spanid="cb4-101"><ahref="#cb4-101"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Der Kern der Architektur aus Kapitel Praxisfallen, als Test."""</span></span>
<spanid="cb4-102"><ahref="#cb4-102"aria-hidden="true"tabindex="-1"></a> problem <spanclass="op">=</span> schreinerei()</span>
<spanid="cb4-115"><ahref="#cb4-115"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Das faengt die Spaltenvertauschungsfalle aus Kapitel Finanzdaten ab: ein</span></span>
<spanid="cb4-116"><ahref="#cb4-116"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Modell, das Positionen statt Namen benutzt, faellt hier durch.</span></span>
<spanid="cb4-125"><ahref="#cb4-125"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Deckungsbeitraege in Cent statt Euro: Zielwert mal 100, Mengen gleich.</span></span>
<spanid="cb4-127"><ahref="#cb4-127"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Der Test prueft nicht die Mathematik - die ist offensichtlich -, sondern</span></span>
<spanid="cb4-128"><ahref="#cb4-128"aria-hidden="true"tabindex="-1"></a><spanclass="co"> die NUMERIK. Wer schlecht skalierte Modelle baut (Kapitel Fundament),</span></span>
<spanid="cb4-129"><ahref="#cb4-129"aria-hidden="true"tabindex="-1"></a><spanclass="co"> bekommt hier Abweichungen weit ueber der Toleranz.</span></span>
<spanid="cb4-144"><ahref="#cb4-144"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Ein Produkt, das nichts einbringt, darf den Zielwert nicht heben."""</span></span>
<spanid="cb4-145"><ahref="#cb4-145"aria-hidden="true"tabindex="-1"></a> problem <spanclass="op">=</span> schreinerei()</span>
<spanid="cb4-159"><ahref="#cb4-159"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Das ist der einzige Test, der einen ZAHLENWERT festschreibt - und er darf</span></span>
<spanid="cb4-160"><ahref="#cb4-160"aria-hidden="true"tabindex="-1"></a><spanclass="co"> es, weil dieser Wert von Hand belegt ist. Fuer grosse Instanzen gibt es</span></span>
<spanid="cb4-161"><ahref="#cb4-161"aria-hidden="true"tabindex="-1"></a><spanclass="co"> diesen Test nicht; dort tragen die Eigenschaften und Invarianten.</span></span>
<spanid="cb4-170"><ahref="#cb4-170"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""40 EUR je Montagestunde, 20 EUR je Einheit Plattenmaterial."""</span></span>
<spanid="cb4-179"><ahref="#cb4-179"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Eine Loesung, die zu viel verbraucht, MUSS beanstandet werden."""</span></span>
<spanid="cb4-180"><ahref="#cb4-180"aria-hidden="true"tabindex="-1"></a> problem <spanclass="op">=</span> schreinerei()</span>
<spanid="cb4-185"><ahref="#cb4-185"aria-hidden="true"tabindex="-1"></a><spanclass="cf">assert</span><spanclass="bu">any</span>(<spanclass="st">"Montagestunden"</span><spanclass="kw">in</span> b <spanclass="cf">for</span> b <spanclass="kw">in</span> beanstandungen)</span>
<spanid="cb4-189"><ahref="#cb4-189"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Mengen und Zielwert passen nicht zusammen - der haeufigste stille Fehler."""</span></span>
<spanid="cb4-190"><ahref="#cb4-190"aria-hidden="true"tabindex="-1"></a> problem <spanclass="op">=</span> schreinerei()</span>
<spanid="cb4-194"><ahref="#cb4-194"aria-hidden="true"tabindex="-1"></a><spanclass="cf">assert</span><spanclass="bu">any</span>(<spanclass="st">"Zielwert"</span><spanclass="kw">in</span> b <spanclass="cf">for</span> b <spanclass="kw">in</span> pruefe_loesung(problem, kaputt))</span>
<spanid="cb4-202"><ahref="#cb4-202"aria-hidden="true"tabindex="-1"></a><spanclass="cf">assert</span><spanclass="bu">any</span>(<spanclass="st">"negativ"</span><spanclass="kw">in</span> b <spanclass="cf">for</span> b <spanclass="kw">in</span> pruefe_loesung(problem, kaputt))</span>
<spanid="cb4-206"><ahref="#cb4-206"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Die Falle aus Kapitel MILP: 0,99999998 ist nicht 1."""</span></span>
<spanid="cb4-207"><ahref="#cb4-207"aria-hidden="true"tabindex="-1"></a> problem <spanclass="op">=</span> schreinerei()</span>
<spanid="cb4-212"><ahref="#cb4-212"aria-hidden="true"tabindex="-1"></a><spanclass="cf">assert</span><spanclass="bu">any</span>(<spanclass="st">"ganzzahlig"</span><spanclass="kw">in</span> b <spanclass="cf">for</span> b <spanclass="kw">in</span> beanstandungen)</span>
<spanid="cb4-216"><ahref="#cb4-216"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Ohne verwertbares Ergebnis wird gar nicht erst gerechnet."""</span></span>
<spanid="cb4-217"><ahref="#cb4-217"aria-hidden="true"tabindex="-1"></a> problem <spanclass="op">=</span> schreinerei()</span>
<spanid="cb4-218"><ahref="#cb4-218"aria-hidden="true"tabindex="-1"></a> ohne <spanclass="op">=</span> Loesung(status<spanclass="op">=</span>SolverStatus.UNZULAESSIG)</span>
<spanid="cb4-233"><ahref="#cb4-233"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Drei Fehler, die NICHT erst beim Loesen auffallen duerfen."""</span></span>
<spanid="cb4-257"><ahref="#cb4-257"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Die Uebersetzertabelle selbst - inklusive des unbekannten Falls."""</span></span>
<spanid="cb4-264"><ahref="#cb4-264"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""brauchbar und modellfehler duerfen sich nie ueberschneiden."""</span></span>
<spanid="cb4-265"><ahref="#cb4-265"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> status <spanclass="kw">in</span> SolverStatus:</span>
<spanid="cb4-269"><ahref="#cb4-269"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Die beiden folgenden Tests gab es zuerst nicht. Sie sind entstanden, weil</span></span>
<spanid="cb4-270"><ahref="#cb4-270"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Mutationstest.py zwei eingebaute Fehler UEBERLEBEN liess - siehe den</span></span>
<spanid="cb4-271"><ahref="#cb4-271"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Abschnitt "Wer testet die Tests?".</span></span>
<spanid="cb4-274"><ahref="#cb4-274"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""ZEITLIMIT ist NICHT brauchbar - der Solver hat nichts gefunden.</span></span>
<spanid="cb4-276"><ahref="#cb4-276"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Der Unterschied entscheidet, ob ein Nachtjob einen Plan ausliefert oder</span></span>
<spanid="cb4-277"><ahref="#cb4-277"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Alarm schlaegt. Ein aufgeweichtes 'brauchbar' faellt sonst nirgends auf.</span></span>
<spanid="cb4-281"><ahref="#cb4-281"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> status <spanclass="kw">in</span> (SolverStatus.ZEITLIMIT, SolverStatus.UNZULAESSIG,</span>
<spanid="cb4-288"><ahref="#cb4-288"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Zwei Mappings mit gleichem Inhalt, andere Einfuegereihenfolge.</span></span>
<spanid="cb4-290"><ahref="#cb4-290"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Die Verbrauchsmatrix muss zeilenweise identisch sein. Ohne diese</span></span>
<spanid="cb4-291"><ahref="#cb4-291"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Zusicherung passt die Matrix irgendwann nicht mehr zum</span></span>
<spanid="cb4-292"><ahref="#cb4-292"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Kapazitaetsvektor - die Vertauschungsfalle aus Kapitel Finanzdaten,</span></span>
<spanid="cb4-293"><ahref="#cb4-293"aria-hidden="true"tabindex="-1"></a><spanclass="co"> nur eine Ebene tiefer.</span></span>
<td>Jeder Test läuft <strong>zweimal</strong> — einmal mit <ahref="anhang-glossar.md#gloss:glop"class="glossar-link">GLOP</a>, einmal mit <ahref="anhang-glossar.md#gloss:scipy"class="glossar-link">SciPy</a>. Ein Test, der nur einen Solver sieht, prüft die Bibliothek mit statt das Modell</td>
<td>prüft nicht Mathematik, sondern <strong>Numerik</strong> — schlecht skalierte Modelle (<ahref="fundament.html#kap-fundament">Kapitel 2</a>) fallen hier durch</td>
<td>damit die Datei auch ohne <code>pytest</code>-Aufruf läuft — sie gehört zur Programmsammlung des Buchs</td>
</tr>
</tbody>
</table>
</blockquote>
<p>Der Lauf ist unspektakulär, und das ist der Punkt:</p>
<pre><code>$ pytest test_or_kern.py -q
................................. [100%]
33 passed in 0.5s</code></pre>
<hr/>
<h2id="sec:testing-mutation">23.5 Wer testet die Tests?</h2>
<p>33 grüne Tests. Das ist noch kein Ergebnis.</p>
<p>Eine Testsuite, die nie rot war, ist keine Prüfung, sondern eine Vermutung — sie könnte aus lauter <code>assert True</code> bestehen und sähe genauso aus. Die Frage ist nicht, ob die Tests laufen, sondern ob sie etwas <strong>fangen</strong> würden.</p>
<p>Das lässt sich messen. Man baut absichtlich Fehler in den Code ein und schaut, ob die Suite rot wird. Jede Mutation hat genau zwei mögliche Ausgänge:</p>
<table>
<colgroup>
<colstyle="width: 50%"/>
<colstyle="width: 50%"/>
</colgroup>
<thead>
<trclass="header">
<th>Ausgang</th>
<th>Bedeutung</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td><strong>getötet</strong></td>
<td>Mindestens ein Test wird rot — diesen Fehler hätte die Suite gefunden.</td>
</tr>
<trclass="even">
<td><strong>überlebt</strong></td>
<td>Alle Tests bleiben grün — diesen Fehler hätte sie <strong>durchgehen lassen</strong>.</td>
<spanid="cb6-7"><ahref="#cb6-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">`test_or_kern.py` besteht aus 31 Tests, und alle sind gruen. Das ist noch kein</span></span>
<spanid="cb6-8"><ahref="#cb6-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">Ergebnis. Eine Testsuite, die nie rot war, ist keine Pruefung, sondern eine</span></span>
<spanid="cb6-9"><ahref="#cb6-9"aria-hidden="true"tabindex="-1"></a><spanclass="co">Vermutung - sie koennte aus lauter `assert True` bestehen und saehe genauso aus.</span></span>
<spanid="cb6-11"><ahref="#cb6-11"aria-hidden="true"tabindex="-1"></a><spanclass="co">Dieses Programm prueft die Pruefung. Es baut nacheinander KLEINE, gezielte</span></span>
<spanid="cb6-12"><ahref="#cb6-12"aria-hidden="true"tabindex="-1"></a><spanclass="co">Fehler in `or_kern.py` ein - jeder einzelne ist ein Fehler, den man beim</span></span>
<spanid="cb6-13"><ahref="#cb6-13"aria-hidden="true"tabindex="-1"></a><spanclass="co">Programmieren tatsaechlich macht - und laesst die Testsuite darauf los. Fuer</span></span>
<spanid="cb6-14"><ahref="#cb6-14"aria-hidden="true"tabindex="-1"></a><spanclass="co">jede Mutation gibt es genau zwei moegliche Ausgaenge:</span></span>
<spanid="cb6-16"><ahref="#cb6-16"aria-hidden="true"tabindex="-1"></a><spanclass="co"> GETOETET Mindestens ein Test wird rot. Die Suite haette diesen Fehler</span></span>
<spanid="cb6-17"><ahref="#cb6-17"aria-hidden="true"tabindex="-1"></a><spanclass="co"> in der Praxis gefunden.</span></span>
<spanid="cb6-18"><ahref="#cb6-18"aria-hidden="true"tabindex="-1"></a><spanclass="co"> UEBERLEBT Alle Tests bleiben gruen. Die Suite haette diesen Fehler</span></span>
<spanid="cb6-19"><ahref="#cb6-19"aria-hidden="true"tabindex="-1"></a><spanclass="co"> DURCHGEHEN LASSEN - hier fehlt ein Test.</span></span>
<spanid="cb6-21"><ahref="#cb6-21"aria-hidden="true"tabindex="-1"></a><spanclass="co">Ueberlebende Mutationen sind das eigentliche Ergebnis: Sie zeigen die Luecken,</span></span>
<spanid="cb6-22"><ahref="#cb6-22"aria-hidden="true"tabindex="-1"></a><spanclass="co">und zwar ohne dass man sie erraten muss.</span></span>
<spanid="cb6-24"><ahref="#cb6-24"aria-hidden="true"tabindex="-1"></a><spanclass="co">Das Verfahren heisst Mutationstest und ist alt; neu ist nur, wie gut es zu</span></span>
<spanid="cb6-25"><ahref="#cb6-25"aria-hidden="true"tabindex="-1"></a><spanclass="co">Optimierungsmodellen passt. Dort kann man den Zielwert meist nicht gegen einen</span></span>
<spanid="cb6-26"><ahref="#cb6-26"aria-hidden="true"tabindex="-1"></a><spanclass="co">bekannten Sollwert pruefen - also weiss man ohne so ein Experiment nicht, ob</span></span>
<spanid="cb6-29"><ahref="#cb6-29"aria-hidden="true"tabindex="-1"></a><spanclass="co">WICHTIG: Es wird nichts veraendert. Jede Mutation laeuft in einem eigenen</span></span>
<spanid="cb6-30"><ahref="#cb6-30"aria-hidden="true"tabindex="-1"></a><spanclass="co">temporaeren Verzeichnis mit einer Kopie von or_kern.py.</span></span>
<spanid="cb6-32"><ahref="#cb6-32"aria-hidden="true"tabindex="-1"></a><spanclass="co">Benoetigt: pytest; im selben Verzeichnis or_kern.py und test_or_kern.py</span></span>
<spanid="cb6-53"><ahref="#cb6-53"aria-hidden="true"tabindex="-1"></a> (<spanclass="st">"Vorzeichen der Dualwerte vergessen"</span>,</span>
<spanid="cb6-54"><ahref="#cb6-54"aria-hidden="true"tabindex="-1"></a><spanclass="st">"schattenpreise={r: float(-m) for r, m in"</span>,</span>
<spanid="cb6-55"><ahref="#cb6-55"aria-hidden="true"tabindex="-1"></a><spanclass="st">"schattenpreise={r: float(m) for r, m in"</span>),</span>
<spanid="cb6-77"><ahref="#cb6-77"aria-hidden="true"tabindex="-1"></a> (<spanclass="st">"ZEITLIMIT gilt faelschlich als brauchbar"</span>,</span>
<spanid="cb6-78"><ahref="#cb6-78"aria-hidden="true"tabindex="-1"></a><spanclass="st">"return self in (SolverStatus.OPTIMAL, SolverStatus.ZULAESSIG)"</span>,</span>
<spanid="cb6-79"><ahref="#cb6-79"aria-hidden="true"tabindex="-1"></a><spanclass="st">"return self in (SolverStatus.OPTIMAL, SolverStatus.ZULAESSIG,</span><spanclass="ch">\n</span><spanclass="st">"</span></span>
<spanid="cb6-82"><ahref="#cb6-82"aria-hidden="true"tabindex="-1"></a> (<spanclass="st">"Ressourcenreihenfolge nicht mehr stabil"</span>,</span>
<spanid="cb6-94"><ahref="#cb6-94"aria-hidden="true"tabindex="-1"></a> letzte <spanclass="op">=</span> [z <spanclass="cf">for</span> z <spanclass="kw">in</span> ergebnis.stdout.strip().splitlines() <spanclass="cf">if</span> z.strip()]</span>
<spanid="cb6-95"><ahref="#cb6-95"aria-hidden="true"tabindex="-1"></a> zeile <spanclass="op">=</span> letzte[<spanclass="op">-</span><spanclass="dv">1</span>] <spanclass="cf">if</span> letzte <spanclass="cf">else</span><spanclass="st">""</span></span>
<spanid="cb6-97"><ahref="#cb6-97"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> teil <spanclass="kw">in</span> zeile.replace(<spanclass="st">"="</span>, <spanclass="st">""</span>).split(<spanclass="st">","</span>):</span>
<spanid="cb6-110"><ahref="#cb6-110"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Legt eine mutierte Kopie an und laesst die Suite darauf laufen."""</span></span>
<spanid="cb6-112"><ahref="#cb6-112"aria-hidden="true"tabindex="-1"></a> text <spanclass="op">=</span><spanclass="bu">open</span>(quelle, encoding<spanclass="op">=</span><spanclass="st">"utf-8"</span>).read()</span>
<spanid="cb6-114"><ahref="#cb6-114"aria-hidden="true"tabindex="-1"></a><spanclass="cf">return</span><spanclass="va">None</span><spanclass="co"># Muster passt nicht (mehr)</span></span>
<spanid="cb6-122"><ahref="#cb6-122"aria-hidden="true"tabindex="-1"></a> hier <spanclass="op">=</span> os.path.dirname(os.path.abspath(<spanclass="va">__file__</span>))</span>
<spanid="cb6-123"><ahref="#cb6-123"aria-hidden="true"tabindex="-1"></a> quelle <spanclass="op">=</span> os.path.join(hier, <spanclass="st">"or_kern.py"</span>)</span>
<spanid="cb6-124"><ahref="#cb6-124"aria-hidden="true"tabindex="-1"></a> suite <spanclass="op">=</span> os.path.join(hier, <spanclass="st">"test_or_kern.py"</span>)</span>
<spanid="cb6-128"><ahref="#cb6-128"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"Beide Dateien muessen im selben Verzeichnis liegen."</span>)</span>
<spanid="cb6-131"><ahref="#cb6-131"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" MUTATIONSTEST: WUERDE DIE SUITE DIESE FEHLER FINDEN?"</span>)</span>
<spanid="cb6-140"><ahref="#cb6-140"aria-hidden="true"tabindex="-1"></a><spanclass="cf">raise</span><spanclass="pp">SystemExit</span>(<spanclass="st">"Die Suite ist schon ohne Mutation rot - erst das reparieren."</span>)</span>
<spanid="cb6-158"><ahref="#cb6-158"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Getoetet: </span><spanclass="sc">{</span><spanclass="bu">len</span>(getoetet)<spanclass="sc">}</span><spanclass="ss"> von </span><spanclass="sc">{</span><spanclass="bu">len</span>(getoetet) <spanclass="op">+</span><spanclass="bu">len</span>(ueberlebt)<spanclass="sc">}</span><spanclass="ss">"</span></span>
<spanid="cb6-162"><ahref="#cb6-162"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" WAS DAS ERGEBNIS BEDEUTET"</span>)</span>
<spanid="cb6-165"><ahref="#cb6-165"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Diese Fehler haette die Suite DURCHGEHEN LASSEN:</span><spanclass="ch">\n</span><spanclass="st">"</span>)</span>
<spanid="cb6-166"><ahref="#cb6-166"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> name <spanclass="kw">in</span> ueberlebt:</span>
<spanid="cb6-168"><ahref="#cb6-168"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st">Jeder ueberlebende Eintrag ist eine Testluecke - und zwar eine"</span>)</span>
<spanid="cb6-169"><ahref="#cb6-169"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"gefundene, keine vermutete. Das ist der ganze Zweck des Verfahrens:"</span>)</span>
<spanid="cb6-170"><ahref="#cb6-170"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Es sagt einem, welchen Test man als naechstes schreiben sollte,"</span>)</span>
<spanid="cb6-171"><ahref="#cb6-171"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"statt dass man raten muss."</span>)</span>
<spanid="cb6-173"><ahref="#cb6-173"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Alle eingebauten Fehler wurden gefunden. Das heisst nicht, dass die"</span>)</span>
<spanid="cb6-174"><ahref="#cb6-174"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Suite vollstaendig ist - nur, dass sie diese zehn Fehler faengt."</span>)</span>
<spanid="cb6-176"><ahref="#cb6-176"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Die Quote selbst ist keine Kennzahl fuer ein Dashboard. Zehn von Hand"</span>)</span>
<spanid="cb6-177"><ahref="#cb6-177"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"gewaehlte Mutationen sind keine Stichprobe aus der Menge aller"</span>)</span>
<spanid="cb6-178"><ahref="#cb6-178"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"moeglichen Fehler. Was zaehlt, ist die LISTE der Ueberlebenden."</span>)</span>
Pruefung auf doppelte Produktnamen entfernt 1 GETOETET
Ganzzahligkeitspruefung entfernt 1 GETOETET
Zielwertabgleich entfernt 1 GETOETET
ZEITLIMIT gilt faelschlich als brauchbar 0 UEBERLEBT
Ressourcenreihenfolge nicht mehr stabil 0 UEBERLEBT</code></pre>
<p><strong>Zwei Lücken, und beide waren ernst.</strong></p>
<p>Die erste: Ändert man <code>SolverStatus.brauchbar</code> so, dass auch <code>ZEITLIMIT</code> als brauchbar gilt, merkt es keiner der 31 Tests. Genau diese Unterscheidung entscheidet aber, ob ein Nachtjob einen Plan ausliefert oder Alarm schlägt (<ahref="praxisfallen.html#kap-praxisfallen">Kapitel 22</a>, <code>Betriebsueberwachung.py</code>).</p>
<p>Die zweite: Ersetzt man <code>sorted(self.kapazitaeten)</code> durch <code>list(...)</code>, hängt die Zeilenreihenfolge der Verbrauchsmatrix wieder an der Einfügereihenfolge des Mappings — die Falle aus <ahref="finanzdaten.html#kap-finanzdaten">Kapitel 18</a>, eine Ebene tiefer. Auch das fiel niemandem auf.</p>
<p>Beide Tests stehen jetzt in <code>test_or_kern.py</code>; sie tragen einen Kommentar, der sagt, woher sie kommen. Danach:</p>
<p><strong>⚠️ Was die Quote nicht ist</strong></p>
<p>„100 %” ist keine Kennzahl fürs Dashboard. Zehn von Hand gewählte Mutationen sind keine Stichprobe aus der Menge aller möglichen Fehler — sie sind eine Liste von Fehlern, die <em>mir</em> eingefallen sind. Eine Mutation, an die niemand gedacht hat, taucht auch nicht als Lücke auf.</p>
<p>Der Wert des Verfahrens liegt nicht in der Zahl, sondern in der <strong>Liste der Überlebenden</strong>. Sie sagt einem, welchen Test man als nächstes schreiben sollte — statt dass man raten muss.</p>
</blockquote>
<hr/>
<h2id="sec:testing-benchmark">23.6 Ein Vergleich, dem man glauben kann</h2>
<p>Solververgleiche stehen in jedem Blog, und die meisten sind wertlos. Nicht, weil falsch gemessen wurde, sondern weil zu wenig dazugesagt wird.</p>
<spanid="cb9-7"><ahref="#cb9-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Solververgleiche stehen in jedem Blog, und die meisten sind wertlos - nicht</span></span>
<spanid="cb9-8"><ahref="#cb9-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">weil falsch gemessen wurde, sondern weil zu wenig dazugesagt wird. Dieses</span></span>
<spanid="cb9-9"><ahref="#cb9-9"aria-hidden="true"tabindex="-1"></a><spanclass="co">Programm misst dasselbe Transportproblem in drei Groessen mit vier</span></span>
<spanid="cb9-10"><ahref="#cb9-10"aria-hidden="true"tabindex="-1"></a><spanclass="co">Bibliotheken und haelt sich dabei an fuenf Regeln, die den Unterschied machen:</span></span>
<spanid="cb9-12"><ahref="#cb9-12"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 1. EIGENER PROZESS je Bibliothek. Nicht nur wegen des Importkonflikts</span></span>
<spanid="cb9-13"><ahref="#cb9-13"aria-hidden="true"tabindex="-1"></a><spanclass="co"> zwischen ortools und highspy (Kapitel Oekosystem) - auch, damit der</span></span>
<spanid="cb9-14"><ahref="#cb9-14"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Speicherverbrauch der einen nicht in der Messung der anderen auftaucht.</span></span>
<spanid="cb9-15"><ahref="#cb9-15"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 2. AUFBAU UND LOESEN GETRENNT messen. Bei grossen Instanzen ist der Aufbau</span></span>
<spanid="cb9-16"><ahref="#cb9-16"aria-hidden="true"tabindex="-1"></a><spanclass="co"> des Modells in Python regelmaessig teurer als das Loesen. Wer nur die</span></span>
<spanid="cb9-17"><ahref="#cb9-17"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Gesamtzeit misst, optimiert am Ende die falsche Haelfte.</span></span>
<spanid="cb9-18"><ahref="#cb9-18"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 3. ZIELWERTE GEGENEINANDER PRUEFEN. Eine Bibliothek, die schneller ist und</span></span>
<spanid="cb9-19"><ahref="#cb9-19"aria-hidden="true"tabindex="-1"></a><spanclass="co"> etwas anderes ausrechnet, hat den Vergleich nicht gewonnen. Diese Pruefung</span></span>
<spanid="cb9-20"><ahref="#cb9-20"aria-hidden="true"tabindex="-1"></a><spanclass="co"> ist der wichtigste Teil des Programms.</span></span>
<spanid="cb9-21"><ahref="#cb9-21"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 4. SPEICHER MITMESSEN. Bei 10.000 Variablen entscheidet oft er und nicht die</span></span>
<spanid="cb9-22"><ahref="#cb9-22"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Zeit darueber, was auf einer Maschine noch laeuft.</span></span>
<spanid="cb9-23"><ahref="#cb9-23"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 5. DIESELBE INSTANZ fuer alle. Feste Saat, kein Neuwuerfeln zwischendurch.</span></span>
<spanid="cb9-25"><ahref="#cb9-25"aria-hidden="true"tabindex="-1"></a><spanclass="co">Und die Einschraenkung, die dazugehoert: Gemessen wird EIN Problemtyp in EINER</span></span>
<spanid="cb9-26"><ahref="#cb9-26"aria-hidden="true"tabindex="-1"></a><spanclass="co">Formulierung auf EINER Maschine. Das Ergebnis ist keine Rangliste der Solver,</span></span>
<spanid="cb9-27"><ahref="#cb9-27"aria-hidden="true"tabindex="-1"></a><spanclass="co">sondern eine Entscheidungshilfe fuer genau diesen Fall. Wer es verallgemeinert,</span></span>
<spanid="cb9-28"><ahref="#cb9-28"aria-hidden="true"tabindex="-1"></a><spanclass="co">macht denselben Fehler wie jemand, der aus einem Backtest auf die Zukunft</span></span>
<spanid="cb9-31"><ahref="#cb9-31"aria-hidden="true"tabindex="-1"></a><spanclass="co">Benoetigt: numpy; in den Kindprozessen scipy, highspy, ortools, cvxpy</span></span>
<spanid="cb9-46"><ahref="#cb9-46"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Instanz und Speichermessung stehen als gewoehnliche Funktionen hier - nicht</span></span>
<spanid="cb9-47"><ahref="#cb9-47"aria-hidden="true"tabindex="-1"></a><spanclass="co"># in einem String, den ein Kindprozess ausfuehrt. Jede Messfunktion baut die</span></span>
<spanid="cb9-48"><ahref="#cb9-48"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Instanz aus derselben Saat neu auf, damit ueber die Prozessgrenze nichts</span></span>
<spanid="cb9-49"><ahref="#cb9-49"aria-hidden="true"tabindex="-1"></a><spanclass="co"># reist, was das Ergebnis verfaelschen koennte.</span></span>
<spanid="cb9-60"><ahref="#cb9-60"aria-hidden="true"tabindex="-1"></a><spanclass="co"># ru_maxrss ist unter Linux in Kilobyte. Gemessen wird der Kindprozess -</span></span>
<spanid="cb9-61"><ahref="#cb9-61"aria-hidden="true"tabindex="-1"></a><spanclass="co"># deshalb muss jede Messung einen eigenen bekommen.</span></span>
<spanid="cb9-69"><ahref="#cb9-69"aria-hidden="true"tabindex="-1"></a> c <spanclass="op">=</span> kosten.reshape(<spanclass="op">-</span><spanclass="dv">1</span>)</span>
<spanid="cb9-70"><ahref="#cb9-70"aria-hidden="true"tabindex="-1"></a> A_ub <spanclass="op">=</span> np.zeros((m, m <spanclass="op">*</span> n))<spanclass="op">;</span> A_eq <spanclass="op">=</span> np.zeros((n, m <spanclass="op">*</span> n))</span>
<spanid="cb9-71"><ahref="#cb9-71"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(m):</span>
<spanid="cb9-72"><ahref="#cb9-72"aria-hidden="true"tabindex="-1"></a> A_ub[i, i <spanclass="op">*</span> n:(i <spanclass="op">+</span><spanclass="dv">1</span>) <spanclass="op">*</span> n] <spanclass="op">=</span><spanclass="fl">1.0</span></span>
<spanid="cb9-87"><ahref="#cb9-87"aria-hidden="true"tabindex="-1"></a> h <spanclass="op">=</span> highspy.Highs()<spanclass="op">;</span> h.setOptionValue(<spanclass="st">"output_flag"</span>, <spanclass="va">False</span>)</span>
<spanid="cb9-88"><ahref="#cb9-88"aria-hidden="true"tabindex="-1"></a> h.addVars(m <spanclass="op">*</span> n, np.zeros(m <spanclass="op">*</span> n), np.full(m <spanclass="op">*</span> n, highspy.kHighsInf))</span>
<spanid="cb9-89"><ahref="#cb9-89"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> k <spanclass="kw">in</span><spanclass="bu">range</span>(m <spanclass="op">*</span> n):</span>
<spanid="cb9-91"><ahref="#cb9-91"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(m):</span>
<spanid="cb9-92"><ahref="#cb9-92"aria-hidden="true"tabindex="-1"></a> idx <spanclass="op">=</span> np.arange(i <spanclass="op">*</span> n, (i <spanclass="op">+</span><spanclass="dv">1</span>) <spanclass="op">*</span> n, dtype<spanclass="op">=</span>np.int32)</span>
<spanid="cb9-93"><ahref="#cb9-93"aria-hidden="true"tabindex="-1"></a> h.addRow(<spanclass="op">-</span>highspy.kHighsInf, <spanclass="bu">float</span>(angebot[i]), n, idx, np.ones(n))</span>
<spanid="cb9-95"><ahref="#cb9-95"aria-hidden="true"tabindex="-1"></a> idx <spanclass="op">=</span> np.arange(j, m <spanclass="op">*</span> n, n, dtype<spanclass="op">=</span>np.int32)</span>
<spanid="cb9-96"><ahref="#cb9-96"aria-hidden="true"tabindex="-1"></a> h.addRow(<spanclass="bu">float</span>(bedarf[j]), <spanclass="bu">float</span>(bedarf[j]), m, idx, np.ones(m))</span>
<spanid="cb9-106"><ahref="#cb9-106"aria-hidden="true"tabindex="-1"></a> s <spanclass="op">=</span> pywraplp.Solver.CreateSolver(<spanclass="st">"GLOP"</span>)</span>
<spanid="cb9-107"><ahref="#cb9-107"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> [[s.NumVar(<spanclass="dv">0</span>, s.infinity(), <spanclass="ss">f"x</span><spanclass="sc">{</span>i<spanclass="sc">}</span><spanclass="ss">_</span><spanclass="sc">{</span>j<spanclass="sc">}</span><spanclass="ss">"</span>) <spanclass="cf">for</span> j <spanclass="kw">in</span><spanclass="bu">range</span>(n)]</span>
<spanid="cb9-108"><ahref="#cb9-108"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(m)]</span>
<spanid="cb9-109"><ahref="#cb9-109"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(m):</span>
<spanid="cb9-124"><ahref="#cb9-124"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> cp.Variable((m, n), nonneg<spanclass="op">=</span><spanclass="va">True</span>)</span>
<spanid="cb9-125"><ahref="#cb9-125"aria-hidden="true"tabindex="-1"></a> problem <spanclass="op">=</span> cp.Problem(cp.Minimize(cp.<spanclass="bu">sum</span>(cp.multiply(kosten, x))),</span>
<spanid="cb9-138"><ahref="#cb9-138"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Fuehrt eine Messfunktion in einem FRISCHEN Prozess aus.</span></span>
<spanid="cb9-140"><ahref="#cb9-140"aria-hidden="true"tabindex="-1"></a><spanclass="co">'spawn' und max_tasks_per_child=1 zusammen garantieren, was Regel 1</span></span>
<spanid="cb9-141"><ahref="#cb9-141"aria-hidden="true"tabindex="-1"></a><spanclass="co"> verlangt: Jede Messung sieht einen leeren Interpreter. Ohne das</span></span>
<spanid="cb9-142"><ahref="#cb9-142"aria-hidden="true"tabindex="-1"></a><spanclass="co"> zweite wuerde der Pool seinen Arbeiter wiederverwenden - dann waere</span></span>
<spanid="cb9-143"><ahref="#cb9-143"aria-hidden="true"tabindex="-1"></a><spanclass="co"> der Speicherwert der zweiten Bibliothek um die erste zu hoch, und</span></span>
<spanid="cb9-144"><ahref="#cb9-144"aria-hidden="true"tabindex="-1"></a><spanclass="co"> ortools und highspy saessen im selben Prozess.</span></span>
<spanid="cb9-151"><ahref="#cb9-151"aria-hidden="true"tabindex="-1"></a><spanclass="cf">return</span> pool.submit(funktion, m, n).result(timeout<spanclass="op">=</span><spanclass="dv">600</span>), <spanclass="va">None</span></span>
<spanid="cb9-158"><ahref="#cb9-158"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" SKALIERUNGSVERGLEICH: TRANSPORTPROBLEM, VIER BIBLIOTHEKEN"</span>)</span>
<spanid="cb9-160"><ahref="#cb9-160"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Jede Zeile ein eigener Prozess. Zeiten und Speicher sind "</span></span>
<spanid="cb9-162"><ahref="#cb9-162"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"die Zielwerte und ihr Verhaeltnis zueinander nicht.</span><spanclass="ch">\n</span><spanclass="st">"</span>)</span>
<spanid="cb9-174"><ahref="#cb9-174"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span>name<spanclass="sc">:<16}</span><spanclass="ss"> nicht verfuegbar: </span><spanclass="sc">{</span>fehler<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb9-182"><ahref="#cb9-182"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Die wichtigste Zeile: Rechnen alle dasselbe aus?</span></span>
<spanid="cb9-184"><ahref="#cb9-184"aria-hidden="true"tabindex="-1"></a> bezug <spanclass="op">=</span><spanclass="bu">max</span>(<spanclass="bu">abs</span>(v) <spanclass="cf">for</span> v <spanclass="kw">in</span> zielwerte.values())</span>
<spanid="cb9-185"><ahref="#cb9-185"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span><spanclass="st">''</span><spanclass="sc">:16}</span><spanclass="ss"> Spannweite der Zielwerte: </span><spanclass="sc">{</span>spanne<spanclass="sc">:.2e}</span><spanclass="ss">"</span></span>
<spanid="cb9-188"><ahref="#cb9-188"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" ACHTUNG: Die Bibliotheken widersprechen sich - "</span></span>
<spanid="cb9-189"><ahref="#cb9-189"aria-hidden="true"tabindex="-1"></a><spanclass="st">"der Zeitvergleich ist wertlos."</span>)</span>
<spanid="cb9-193"><ahref="#cb9-193"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" WAS MAN AUS SO EINER TABELLE ABLESEN DARF - UND WAS NICHT"</span>)</span>
<spanid="cb9-195"><ahref="#cb9-195"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"DARF man ablesen:"</span>)</span>
<spanid="cb9-196"><ahref="#cb9-196"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * Die Spalte 'Anteil' - wie viel der Zeit in den AUFBAU geht statt"</span>)</span>
<spanid="cb9-197"><ahref="#cb9-197"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" ins Loesen. Wenn dort 80 </span><spanclass="sc">% s</span><spanclass="st">tehen, ist ein schnellerer Solver die"</span>)</span>
<spanid="cb9-198"><ahref="#cb9-198"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" falsche Antwort; dann gehoert das Modell vektorisiert aufgebaut"</span>)</span>
<spanid="cb9-200"><ahref="#cb9-200"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * Die Groessenordnung des Speicherbedarfs. Sie entscheidet, was auf"</span>)</span>
<spanid="cb9-201"><ahref="#cb9-201"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" einer bestimmten Maschine ueberhaupt laeuft."</span>)</span>
<spanid="cb9-202"><ahref="#cb9-202"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * Wie sich beides mit der Groesse ENTWICKELT. Der Trend ist"</span>)</span>
<spanid="cb9-203"><ahref="#cb9-203"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" uebertragbarer als der Absolutwert."</span>)</span>
<spanid="cb9-206"><ahref="#cb9-206"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * 'Bibliothek X ist schneller als Y.' Gemessen wurde EIN"</span>)</span>
<spanid="cb9-207"><ahref="#cb9-207"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Problemtyp in EINER Formulierung. Ein MILP, ein QP oder eine"</span>)</span>
<spanid="cb9-208"><ahref="#cb9-208"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" andere Modellierung desselben Problems koennen die Reihenfolge"</span>)</span>
<spanid="cb9-210"><ahref="#cb9-210"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * Etwas ueber Ihre Maschine. Diese Zahlen stammen von einer"</span>)</span>
<spanid="cb9-211"><ahref="#cb9-211"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" anderen. Der Sinn des Programms ist, dass Sie es auf Ihrer"</span>)</span>
<p>Bei <ahref="anhang-glossar.md#gloss:or-tools"class="glossar-link">OR-Tools</a> gehen drei von vier Sekunden dafür drauf, in Python 10 000 Variablenobjekte anzulegen und Nebenbedingungen daraus zusammenzusetzen. Wer an dieser Stelle einen schnelleren <em>Solver</em> sucht, sucht in der falschen Hälfte — die Antwort ist ein vektorisierter Modellaufbau (<ahref="oekosystem.html#kap-oekosystem">Kapitel 3</a>).</p>
<p>Die zweite unbequeme Zahl steht ganz rechts: <strong><ahref="anhang-glossar.md#gloss:cvxpy"class="glossar-link">CVXPY</a> braucht rund 240 MB</strong>, unabhängig von der Problemgröße — das ist der Preis seiner Modellierungsschicht. Auf einem Rechner mit knappem Speicher entscheidet diese Spalte und nicht die Laufzeit darüber, was überhaupt läuft.</p>
<p>Sie sagt <strong>nicht</strong> „ortools ist langsamer als scipy”. Gemessen wurde <em>ein</em> Problemtyp in <em>einer</em> Formulierung auf <em>einer</em> Maschine. Ein <ahref="anhang-glossar.md#gloss:gemischt-ganzzahlige-optimierung-milp"class="glossar-link">MILP (Gemischt-ganzzahlige Optimierung)</a> statt eines LP, eine andere Modellierung desselben Problems oder ein anderer Rechner können die Reihenfolge umdrehen — und bei MILPs tun sie es regelmäßig.</p>
<p>Übertragbar ist der <strong>Trend</strong>, nicht der Absolutwert: dass der Aufbauanteil mit der Größe wächst, dass CVXPY einen konstanten Speichersockel hat. Der Zweck des Programms ist, dass Sie es auf Ihrer Maschine mit Ihrem Problem laufen lassen.</p>
</blockquote>
<hr/>
<h2id="sec:testing-dienst">23.7 Das Modell als Dienst</h2>
<p>Ein Modell hinter eine HTTP-Schnittstelle zu hängen sieht nach einer Fingerübung aus. Es gibt aber einen Unterschied, der die ganze Bauform bestimmt:</p>
<blockquote>
<p>Eine gewöhnliche Anfrage dauert Millisekunden. Eine Optimierung dauert Sekunden bis Minuten — und manchmal länger, als jemand warten will.</p>
</blockquote>
<p>Damit scheidet die naheliegende Lösung aus. Wer den Solver direkt im Request-Handler aufruft, baut einen Dienst, der bei der ersten großen Instanz in den Timeout des Reverse Proxy läuft und bei zehn gleichzeitigen Anfragen alle Arbeiter blockiert.</p>
<figure>
<imgsrc="bilder_04/kap_testing_dienst.svg"alt="Abb. 23.1: Der Ablauf des Optimierungsdienstes. Die Validierung weist eine fehlerhafte Eingabe ab, bevor überhaupt ein Solver startet; die Rechnung selbst läuft im Hintergrund, und der Aufrufer bekommt sofort eine Auftrags-ID. Erzeugt von bilder_04/erzeuge_architektur_diagramme.py."/>
<figcaptionaria-hidden="true">Abb. 23.1: Der Ablauf des Optimierungsdienstes. Die Validierung weist eine fehlerhafte Eingabe ab, bevor überhaupt ein Solver startet; die Rechnung selbst läuft im Hintergrund, und der Aufrufer bekommt sofort eine Auftrags-ID. Erzeugt von <code>bilder_04/erzeuge_architektur_diagramme.py</code>.</figcaption>
</figure>
<p>Die tragfähige Bauform ist <strong>zweistufig</strong>:</p>
<table>
<colgroup>
<colstyle="width: 50%"/>
<colstyle="width: 50%"/>
</colgroup>
<thead>
<trclass="header">
<th>Aufruf</th>
<th>Was passiert</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td><code>POST /plaene</code></td>
<td>nimmt an, <strong>prüft</strong>, gibt sofort <code>202 Accepted</code> und eine Auftragsnummer zurück</td>
</tr>
<trclass="even">
<td><code>GET /plaene/{id}</code></td>
<td>sagt, wie weit es ist — und liefert am Ende die Lösung samt Status und Gap</td>
<spanid="cb11-5"><ahref="#cb11-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel Testen: Das Modell als Dienst - und warum es kein normaler Endpunkt ist.</span></span>
<spanid="cb11-7"><ahref="#cb11-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Ein Optimierungsmodell hinter eine HTTP-Schnittstelle zu haengen sieht nach</span></span>
<spanid="cb11-8"><ahref="#cb11-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">einer Fingeruebung aus. Es gibt aber einen Unterschied, der alles bestimmt:</span></span>
<spanid="cb11-10"><ahref="#cb11-10"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Eine gewoehnliche Anfrage dauert Millisekunden.</span></span>
<spanid="cb11-11"><ahref="#cb11-11"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Eine Optimierung dauert Sekunden bis Minuten - und manchmal laenger,</span></span>
<spanid="cb11-12"><ahref="#cb11-12"aria-hidden="true"tabindex="-1"></a><spanclass="co"> als jemand warten will.</span></span>
<spanid="cb11-14"><ahref="#cb11-14"aria-hidden="true"tabindex="-1"></a><spanclass="co">Damit scheidet die naheliegende Bauform aus. Wer den Solver direkt im</span></span>
<spanid="cb11-15"><ahref="#cb11-15"aria-hidden="true"tabindex="-1"></a><spanclass="co">Request-Handler aufruft, baut einen Dienst, der bei der ersten grossen Instanz</span></span>
<spanid="cb11-16"><ahref="#cb11-16"aria-hidden="true"tabindex="-1"></a><spanclass="co">in einen Timeout des Reverse Proxy laeuft, und bei zehn gleichzeitigen Anfragen</span></span>
<spanid="cb11-21"><ahref="#cb11-21"aria-hidden="true"tabindex="-1"></a><spanclass="co"> POST /plaene nimmt den Auftrag an, prueft ihn, gibt sofort</span></span>
<spanid="cb11-22"><ahref="#cb11-22"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 202 Accepted und eine Auftragsnummer zurueck</span></span>
<spanid="cb11-23"><ahref="#cb11-23"aria-hidden="true"tabindex="-1"></a><spanclass="co"> GET /plaene/{id} sagt, wie weit es ist - und liefert am Ende die</span></span>
<spanid="cb11-24"><ahref="#cb11-24"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Loesung samt Status und Gap</span></span>
<spanid="cb11-26"><ahref="#cb11-26"aria-hidden="true"tabindex="-1"></a><spanclass="co">Drei Dinge, die dabei leicht untergehen und hier bewusst sichtbar sind:</span></span>
<spanid="cb11-28"><ahref="#cb11-28"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 1. Das Domaenenmodell aus or_kern.py ist zugleich das API-Schema. Eine</span></span>
<spanid="cb11-29"><ahref="#cb11-29"aria-hidden="true"tabindex="-1"></a><spanclass="co"> unsinnige Anfrage wird von FastAPI mit 422 abgewiesen, bevor irgendein</span></span>
<spanid="cb11-30"><ahref="#cb11-30"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Solver startet - dieselbe Pruefung wie beim Excel-Import.</span></span>
<spanid="cb11-31"><ahref="#cb11-31"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 2. Die Antwort enthaelt STATUS und GAP, nicht nur Zahlen. Ein Aufrufer, der</span></span>
<spanid="cb11-32"><ahref="#cb11-32"aria-hidden="true"tabindex="-1"></a><spanclass="co"> nur die Mengen bekommt, kann nicht unterscheiden, ob er ein bewiesenes</span></span>
<spanid="cb11-33"><ahref="#cb11-33"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Optimum oder einen Zeitlimit-Abbruch in der Hand haelt.</span></span>
<spanid="cb11-34"><ahref="#cb11-34"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 3. Jeder Auftrag hat ein ZEITLIMIT. Ohne das belegt eine einzige unguenstige</span></span>
<spanid="cb11-35"><ahref="#cb11-35"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Instanz einen Arbeiter auf unbestimmte Zeit.</span></span>
<spanid="cb11-37"><ahref="#cb11-37"aria-hidden="true"tabindex="-1"></a><spanclass="co">Dieses Programm laeuft ohne Server: Der Selbsttest unten benutzt den</span></span>
<spanid="cb11-38"><ahref="#cb11-38"aria-hidden="true"tabindex="-1"></a><spanclass="co">TestClient von FastAPI und spricht die Anwendung direkt an. Fuer den echten</span></span>
<spanid="cb11-39"><ahref="#cb11-39"aria-hidden="true"tabindex="-1"></a><spanclass="co">Betrieb steht am Ende, was sich aendert.</span></span>
<spanid="cb11-42"><ahref="#cb11-42"aria-hidden="true"tabindex="-1"></a><spanclass="co"> python3 Optimierungsdienst.py # Selbsttest, kein Server noetig</span></span>
<spanid="cb11-43"><ahref="#cb11-43"aria-hidden="true"tabindex="-1"></a><spanclass="co"> uvicorn Optimierungsdienst:app # echter Server auf Port 8000</span></span>
<spanid="cb11-73"><ahref="#cb11-73"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Was der Aufrufer beim Abholen bekommt.</span></span>
<spanid="cb11-75"><ahref="#cb11-75"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Bewusst NICHT nur die Mengen: 'stand' und die Felder aus 'loesung'</span></span>
<spanid="cb11-76"><ahref="#cb11-76"aria-hidden="true"tabindex="-1"></a><spanclass="co"> (Status, Gap) sind der Unterschied zwischen einer Zahl und einer</span></span>
<spanid="cb11-89"><ahref="#cb11-89"aria-hidden="true"tabindex="-1"></a> summary<spanclass="op">=</span><spanclass="st">"Produktionsplanung als Auftrag, nicht als Abfrage"</span>)</span>
<spanid="cb11-91"><ahref="#cb11-91"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Fuer das Buchbeispiel: Auftragsbuch im Speicher, Arbeiter im selben Prozess.</span></span>
<spanid="cb11-92"><ahref="#cb11-92"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Was daran im echten Betrieb nicht reicht, steht unten.</span></span>
<spanid="cb11-98"><ahref="#cb11-98"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Laeuft im Arbeiterthread - nie im Request-Handler."""</span></span>
<spanid="cb11-105"><ahref="#cb11-105"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Dieselbe Abnahmepruefung wie ueberall sonst. Ein Dienst, der sie</span></span>
<spanid="cb11-106"><ahref="#cb11-106"aria-hidden="true"tabindex="-1"></a><spanclass="co"># weglaesst, liefert Fehler schneller aus als ein Mensch sie faende.</span></span>
<spanid="cb11-124"><ahref="#cb11-124"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Nimmt an, prueft, gibt sofort zurueck.</span></span>
<spanid="cb11-126"><ahref="#cb11-126"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Der Typ 'Produktionsproblem' im Parameter ist der ganze Trick: FastAPI</span></span>
<spanid="cb11-127"><ahref="#cb11-127"aria-hidden="true"tabindex="-1"></a><spanclass="co"> validiert die Anfrage damit gegen das Domaenenmodell und antwortet bei</span></span>
<spanid="cb11-128"><ahref="#cb11-128"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Unsinn mit 422, ohne dass hier eine Zeile Pruefcode steht.</span></span>
<spanid="cb11-153"><ahref="#cb11-153"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Selbsttest ohne Server -------------------------------------------------</span></span>
<spanid="cb11-171"><ahref="#cb11-171"aria-hidden="true"tabindex="-1"></a><spanclass="st"># Ein Arbeiter je CPU-Kern - der Solver rechnet selbst schon parallel,</span></span>
<spanid="cb11-172"><ahref="#cb11-172"aria-hidden="true"tabindex="-1"></a><spanclass="st"># mehr Prozesse machen ihn nicht schneller, sondern langsamer.</span></span>
<spanid="cb11-184"><ahref="#cb11-184"aria-hidden="true"tabindex="-1"></a><spanclass="cf">raise</span><spanclass="pp">TimeoutError</span>(<spanclass="st">"Auftrag wurde nicht fertig"</span>)</span>
<spanid="cb11-191"><ahref="#cb11-191"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" DAS MODELL ALS DIENST"</span>)</span>
<spanid="cb11-199"><ahref="#cb11-199"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" POST /plaene -></span><spanclass="sc">{</span>angenommen<spanclass="sc">.</span>status_code<spanclass="sc">}</span><spanclass="ss">"</span></span>
<spanid="cb11-202"><ahref="#cb11-202"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Der entscheidende Punkt, und er ist pruefbar: Die Antwort ist da,</span></span>
<spanid="cb11-203"><ahref="#cb11-203"aria-hidden="true"tabindex="-1"></a><spanclass="co"># BEVOR es eine Loesung gibt. Genau dafuer ist 202 gedacht.</span></span>
<spanid="cb11-204"><ahref="#cb11-204"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Loesung schon dabei? ->"</span></span>
<spanid="cb11-205"><ahref="#cb11-205"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"</span><spanclass="sc">{</span><spanclass="st">'ja'</span><spanclass="cf">if</span> angenommen<spanclass="sc">.</span>json()[<spanclass="st">'loesung'</span>] <spanclass="cf">else</span><spanclass="st">'nein - genau so soll es sein'</span><spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb11-210"><ahref="#cb11-210"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Stand -></span><spanclass="sc">{</span>fertig[<spanclass="st">'stand'</span>]<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb11-220"><ahref="#cb11-220"aria-hidden="true"tabindex="-1"></a><spanclass="co"># 2. Unsinnige Anfrage - muss abgewiesen werden, BEVOR gerechnet wird</span></span>
<spanid="cb11-221"><ahref="#cb11-221"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st">3. Unsinnige Anfragen (der Solver startet gar nicht erst)"</span>)</span>
<spanid="cb11-234"><ahref="#cb11-234"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" GET /plaene/gibtsnicht ->"</span></span>
<spanid="cb11-235"><ahref="#cb11-235"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"</span><spanclass="sc">{</span>klient<spanclass="sc">.</span>get(<spanclass="st">'/plaene/gibtsnicht'</span>)<spanclass="sc">.</span>status_code<spanclass="sc">}</span><spanclass="ss"> Not Found"</span>)</span>
<spanid="cb11-238"><ahref="#cb11-238"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" GET /gesundheit -></span><spanclass="sc">{</span>klient<spanclass="sc">.</span>get(<spanclass="st">'/gesundheit'</span>)<spanclass="sc">.</span>json()<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb11-240"><ahref="#cb11-240"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Warum ein Thread je Auftrag genuegt -----------------------------</span></span>
<spanid="cb11-242"><ahref="#cb11-242"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"6. Warum Threads hier reichen (und wann nicht)</span><spanclass="ch">\n</span><spanclass="st">"</span>)</span>
<spanid="cb11-245"><ahref="#cb11-245"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Wichtig: eine Rechnung mit ECHTER Last. Ein Modell, das in zehn</span></span>
<spanid="cb11-246"><ahref="#cb11-246"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Millisekunden fertig ist, misst nur den Aufwand fuers Threadstarten -</span></span>
<spanid="cb11-247"><ahref="#cb11-247"aria-hidden="true"tabindex="-1"></a><spanclass="co"># der erste Entwurf dieser Messung ist genau daran gescheitert und zeigte</span></span>
<spanid="cb11-248"><ahref="#cb11-248"aria-hidden="true"tabindex="-1"></a><spanclass="co"># eine Verlangsamung, wo in Wirklichkeit eine Beschleunigung steht.</span></span>
<spanid="cb11-255"><ahref="#cb11-255"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> [solver.IntVar(<spanclass="dv">0</span>, <spanclass="dv">1</span>, <spanclass="ss">f"x</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="cb11-275"><ahref="#cb11-275"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" vier Rechnungen nacheinander -></span><spanclass="sc">{</span>seriell<spanclass="sc">:6.2f}</span><spanclass="ss"> s"</span>)</span>
<spanid="cb11-276"><ahref="#cb11-276"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" vier Rechnungen in Threads -></span><spanclass="sc">{</span>parallel<spanclass="sc">:6.2f}</span><spanclass="ss"> s "</span></span>
<spanid="cb11-280"><ahref="#cb11-280"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Die Laeufe ueberlappen sich. ortools rechnet in C++ und gibt"</span>)</span>
<spanid="cb11-281"><ahref="#cb11-281"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" den GIL waehrend Solve() frei - deshalb genuegt hier ein"</span>)</span>
<spanid="cb11-282"><ahref="#cb11-282"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Threadpool, es braucht keine eigenen Prozesse."</span>)</span>
<spanid="cb11-284"><ahref="#cb11-284"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Keine Ueberlappung - hier waeren Prozesse noetig."</span>)</span>
<spanid="cb11-286"><ahref="#cb11-286"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" ACHTUNG, das gilt nicht allgemein: Eine in reinem Python"</span>)</span>
<spanid="cb11-287"><ahref="#cb11-287"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" geschriebene Heuristik (Kapitel Metaheuristiken) haelt den GIL"</span>)</span>
<spanid="cb11-288"><ahref="#cb11-288"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" die ganze Zeit. Fuer sie braucht derselbe Dienst einen"</span>)</span>
<spanid="cb11-289"><ahref="#cb11-289"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" ProcessPoolExecutor statt eines Threadpools."</span>)</span>
<spanid="cb11-292"><ahref="#cb11-292"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" WAS SICH IM ECHTEN BETRIEB AENDERT"</span>)</span>
<spanid="cb11-294"><ahref="#cb11-294"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Dieses Beispiel haelt das Auftragsbuch im Speicher und rechnet in"</span>)</span>
<spanid="cb11-295"><ahref="#cb11-295"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Threads desselben Prozesses. Das reicht zum Zeigen und fuer einen"</span>)</span>
<spanid="cb11-296"><ahref="#cb11-296"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"einzelnen Rechner - nicht darueber hinaus:"</span>)</span>
<spanid="cb11-298"><ahref="#cb11-298"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * NEUSTART LOESCHT ALLES. Auftragsbuch in eine Datenbank oder eine"</span>)</span>
<spanid="cb11-299"><ahref="#cb11-299"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Warteschlange (Redis, RabbitMQ), nicht in ein dict."</span>)</span>
<spanid="cb11-300"><ahref="#cb11-300"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * ZWEI INSTANZEN KENNEN EINANDER NICHT. Sobald der Dienst mehr als"</span>)</span>
<spanid="cb11-301"><ahref="#cb11-301"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" einmal laeuft, muss die Warteschlange ausserhalb liegen -"</span>)</span>
<spanid="cb11-302"><ahref="#cb11-302"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" typischerweise Celery mit Redis als Vermittler."</span>)</span>
<spanid="cb11-303"><ahref="#cb11-303"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * DIE WAHL THREADS/PROZESSE HAENGT AM SOLVER - siehe die Messung"</span>)</span>
<spanid="cb11-304"><ahref="#cb11-304"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" unter Punkt 6. Sie gehoert gemessen, nicht angenommen."</span>)</span>
<spanid="cb11-305"><ahref="#cb11-305"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * OHNE ZEITLIMIT KEIN DIENST. Jede Instanz bekommt eines, und der"</span>)</span>
<spanid="cb11-306"><ahref="#cb11-306"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Aufrufer erfaehrt im Status, ob es gegriffen hat."</span>)</span>
<spanid="cb11-308"><ahref="#cb11-308"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Das zugehoerige Dockerfile ist kurz genug, um es ganz zu zeigen:"</span>)</span>
<spanid="cb11-313"><ahref="#cb11-313"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"(Es wird hier nicht gebaut - das Buch setzt keine laufende"</span>)</span>
<p><strong>1. Das Domänenmodell ist das API-Schema.</strong> Der <ahref="anhang-glossar.md#gloss:parameter"class="glossar-link">Parameter</a> heißt <code>problem: Produktionsproblem</code> — mehr steht nicht da. FastAPI validiert die Anfrage damit gegen dieselben Pydantic-Regeln wie der Excel-Import aus <ahref="einfuehrung.html#kap-einfuehrung">Kapitel 1</a> und antwortet bei Unsinn mit <code>422</code>, <strong>bevor</strong> ein Solver startet. Die drei abgewiesenen Anfragen im Selbsttest sind genau die drei Fälle, die <code>or_kern.py</code> schon beim Einlesen abfängt.</p>
<p><strong>2. Die Antwort enthält Status und Gap, nicht nur Zahlen.</strong> Ein Aufrufer, der nur die Mengen bekommt, kann nicht unterscheiden, ob er ein bewiesenes Optimum oder einen Zeitlimit-Abbruch in der Hand hält. Das <code>Loesung</code>-Objekt geht deshalb vollständig durch die Schnittstelle — dasselbe <ahref="anhang-glossar.md#gloss:dto-data-transfer-object"class="glossar-link">DTO (Data Transfer Object)</a>, das in <ahref="praxisfallen.html#sec:praxisfallen-solverwechsel">Abschnitt 22.7</a> schon eine Prozessgrenze überlebt hat.</p>
<p><strong>3. Die Abnahmeprüfung läuft auch hier.</strong><code>pruefe_loesung()</code> steht im Arbeiter, nicht nur im Skript. Ein Dienst ohne sie liefert Fehler schneller aus, als ein Mensch sie finden könnte.</p>
</blockquote>
<h3id="threads-oder-prozesse-eine-messung-keine-meinung">Threads oder Prozesse? Eine Messung, keine Meinung</h3>
<p>Punkt 6 des Selbsttests misst, ob vier Solverläufe in Threads sich überlappen. Das Ergebnis ist ein Faktor von rund 3,5 bei vier Threads: <strong>OR-Tools rechnet in C++ und gibt den <ahref="anhang-glossar.md#gloss:gil-global-interpreter-lock"class="glossar-link">GIL (Global Interpreter Lock)</a> während <code>Solve()</code> frei.</strong> Ein Threadpool genügt also, es braucht keine eigenen Prozesse.</p>
<p>Das gilt aber nicht allgemein. Eine in reinem Python geschriebene Heuristik (<ahref="metaheuristiken.html#kap-metaheuristiken">Kapitel 9</a>) hält den GIL die ganze Zeit — für sie braucht derselbe Dienst einen <code>ProcessPoolExecutor</code>. Die Entscheidung gehört gemessen, nicht angenommen.</p>
<blockquote>
<p><strong>⚠️ Der erste Entwurf dieser Messung war falsch</strong></p>
<p>Sie lief zunächst gegen ein LP mit 120 Produkten, das in zehn Millisekunden fertig war. Ergebnis: Faktor 0,7 — die Threads schienen zu <em>bremsen</em>. Gemessen wurde aber nicht der GIL, sondern der Aufwand fürs Threadstarten, der bei zehn Millisekunden Rechenzeit alles überdeckt.</p>
<p>Das ist die häufigste Art, eine Parallelisierungsmessung zu verderben: <strong>Die Arbeitseinheit muss groß genug sein, dass der Verwaltungsaufwand daneben verschwindet.</strong> Im Programm steht deshalb jetzt ein MILP mit knapp einer halben Sekunde Rechenzeit — und der Kommentar, warum.</p>
</blockquote>
<h3id="was-im-echten-betrieb-dazukommt">Was im echten Betrieb dazukommt</h3>
<p>Das Beispiel hält das Auftragsbuch in einem <code>dict</code> und rechnet in Threads desselben Prozesses. Für einen einzelnen Rechner reicht das; darüber hinaus nicht:</p>
<table>
<colgroup>
<colstyle="width: 50%"/>
<colstyle="width: 50%"/>
</colgroup>
<thead>
<trclass="header">
<th>Grenze</th>
<th>Was stattdessen</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td>Neustart löscht alle Aufträge</td>
<td>Auftragsbuch in eine Datenbank oder Warteschlange</td>
</tr>
<trclass="even">
<td>Zwei Instanzen kennen einander nicht</td>
<td>Warteschlange <strong>außerhalb</strong> des Prozesses — Celery mit Redis als Vermittler</td>
</tr>
<trclass="odd">
<td>Ein Auftrag kann ewig laufen</td>
<td>Zeitlimit je Instanz, und der Aufrufer erfährt im Status, ob es gegriffen hat</td>
</tr>
<trclass="even">
<td>Kein Ort für das Ergebnis</td>
<td>Ergebnisspeicher mit Verfallsdatum statt eines wachsenden <code>dict</code></td>
</tr>
</tbody>
</table>
<p>Das Dockerfile ist kurz genug, um es ganz zu zeigen — es steht im Programm und wird beim Lauf mit ausgegeben. Gebaut wird es hier nicht: Das Buch setzt keine laufende Docker-Installation voraus.</p>
<p><strong>Aufgabe 23.1 ⭐ — Den Schnellstart reparieren.</strong> Der gerundete Plan verletzt die Kapazität. Nennen Sie zwei Wege, das zu beheben, und sagen Sie, welcher der richtige ist — und warum <ahref="milp.html#kap-milp">Kapitel 6</a> das ausführlich behandelt.</p>
<p><strong>Aufgabe 23.2 ⭐ — Eine Invariante mehr.</strong> Schreiben Sie einen Test: Verdoppelt man <strong>alle</strong> Kapazitäten, muss sich der Zielwert verdoppeln und die Mengen ebenfalls. Warum gilt das bei einem LP, aber nicht bei einem MILP?</p>
<p><strong>Aufgabe 23.3 ⭐⭐ — Eine eigene Mutation.</strong> Ergänzen Sie <code>MUTATIONEN</code> um zwei weitere realistische Fehler — zum Beispiel ein vertauschtes Ungleichheitszeichen in <code>pruefe_loesung()</code> oder eine Toleranz mit falschem Vorzeichen. Überleben sie?</p>
<p><strong>Aufgabe 23.4 ⭐⭐ — Der Benchmark mit MILP.</strong> Ersetzen Sie im Benchmark das Transportproblem durch dieselbe Aufgabe mit ganzzahligen Mengen. Bleibt die Reihenfolge der Bibliotheken gleich? Was passiert mit der Spalte „Anteil”?</p>
<p><strong>Aufgabe 23.5 ⭐⭐⭐ — Der Dienst mit Zeitlimit.</strong><code>ZEITLIMIT_SEKUNDEN</code> steht im Programm, wird aber nirgends benutzt. Reichen Sie es an den Modellbauer durch, geben Sie im Ergebnis aus, ob es gegriffen hat, und schreiben Sie einen Test, der eine Instanz einreicht, die das Limit überschreitet.</p>
<hr/>
<h2id="sec:testing-denkfehler">23.9 Finde den Denkfehler</h2>
<divclass="card card-denkfehler">
<blockquote>
<p><strong>🐛 „Die Suite ist grün, das Modell stimmt”</strong></p>
<p>Ein Team liefert ein Produktionsplanungsmodell aus. Die Testsuite hat 33 Tests, der Mutationstest tötet alle zehn eingebauten Fehler, und die Abnahmeprüfung läuft in jedem Nachtlauf mit. Nach drei Wochen im Betrieb meldet die Fertigung, dass ihr regelmäßig Lackierkapazität fehlt.</p>
<p>Die Untersuchung zeigt: In der Stammdatentabelle steht der Lackierverbrauch für Regale mit <strong>0,4 Stunden je Stück</strong>. Tatsächlich sind es 4,0 — beim Anlegen ist die Kommastelle verrutscht.</p>
<p>Sämtliche Tests waren grün. Der Mutationstest tötete alles. Die Abnahmeprüfung hat nie etwas beanstandet.</p>
<p><strong>Warum konnte keine dieser Prüfungen den Fehler finden — und was hätte geholfen?</strong></p>
<p>Ein Hinweis: Die Antwort steht schon im Schnellstart dieses Kapitels, im Absatz „Warum funktioniert das?“.</p>
</blockquote>
</div>
<hr/>
<h2id="sec:testing-quiz">23.10 Micro-Quiz</h2>
<divclass="card card-quiz">
<blockquote>
<p><strong>❓ Drei Fragen</strong></p>
<p><strong>1. Warum lässt sich ein Optimierungsmodell selten gegen einen Sollwert testen?</strong> a) Weil Solver nicht deterministisch sind. b) Weil niemand die richtige Antwort unabhängig ausrechnen kann — sonst bräuchte man den Solver nicht. c) Weil Gleitkommazahlen keinen exakten Vergleich erlauben.</p>
<p><strong>2. Eine Mutation „überlebt”. Was heißt das?</strong> a) Der eingebaute Fehler war harmlos. b) Die Testsuite hätte diesen Fehler durchgehen lassen — hier fehlt ein Test. c) Der Test war zu streng eingestellt.</p>
<p><strong>3. Im Benchmark stehen bei einer Bibliothek 77 % in der Spalte „Anteil”. Was folgt daraus?</strong> a) Diese Bibliothek hat den langsamsten Solver. b) Der Modellaufbau in Python dominiert; ein schnellerer Solver würde wenig ändern. c) Die Messung ist fehlerhaft, Aufbau darf nie so lange dauern.</p>
<li>Ein Optimierungsmodell lässt sich fast nie gegen den <strong>richtigen Wert</strong> testen, aber immer gegen seine <strong>Eigenschaften</strong>: Zulässigkeit, Invarianten, eine kleine Regressionsinstanz, Fehlerfälle.</li>
<li>Die am häufigsten fehlende Testart ist die vierte. Eine Abnahmeprüfung, die nie etwas gefunden hat, könnte kaputt sein — man muss sie mit falschen Lösungen füttern, um es zu wissen.</li>
<li>Jeder Test sollte <strong>beide Modellbauer</strong> durchlaufen. Sonst prüft man die Bibliothek mit statt das Modell.</li>
<li><strong>Mutationstests</strong> beantworten die Frage, die eine grüne Suite offenlässt: Würde sie einen Fehler überhaupt bemerken? Wertvoll ist nicht die Quote, sondern die Liste der Überlebenden — hier waren es zwei, und beide führten zu einem neuen Test.</li>
<li>Ein <strong>Benchmark</strong> muss Aufbau und Lösen trennen, Zielwerte gegeneinander prüfen, in getrennten Prozessen laufen und seine eigene Reichweite benennen. Der Aufbauanteil ist regelmäßig die überraschendere Zahl.</li>
<li>Ein <strong>Optimierungsdienst</strong> ist zweistufig: annehmen und prüfen sofort, rechnen im Hintergrund, Ergebnis mit Status und Gap abholen. Die Frage Threads oder Prozesse gehört gemessen — bei C++-Solvern genügen Threads, bei Python-Heuristiken nicht.</li>
<navclass="prev-next"><aclass="prev-next-knopf prev-next-prev"href="praxisfallen.html"><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 22: Praxisfallen und der Weg zum produktiven Einsatz</span></a><aclass="prev-next-knopf prev-next-next"href="synthese-praxis.html"><span><small>Weiter</small>Synthese Teil V</span><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-right"></use></svg></a></nav>