<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><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</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><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: Nichtlinearität, Unsicherheit und mehrperiodige D
<navclass="breadcrumb"aria-label="Breadcrumb"><ahref="index.html">Start</a>›<span>Anhang D: Spickzettel der Solver</span></nav>
<navclass="prev-next"><aclass="prev-next-knopf prev-next-prev"href="anhang-fehlerdiagnose.html"><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-left"></use></svg><span><small>Zurück</small>Anhang C: Fehlerdiagnose-Handbuch</span></a><aclass="prev-next-knopf prev-next-next"href="anhang-glossar-literatur.html"><span><small>Weiter</small>Anhang E: Glossar und Literatur</span><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-right"></use></svg></a></nav>
<article>
<h1id="anhang-spickzettel">Anhang D: Spickzettel der Solver</h1>
<blockquote>
<p><strong>Wofür dieser Anhang gedacht ist:</strong> Sie wissen, was Sie modellieren wollen, und suchen nur noch, wie die gewählte Bibliothek es schreibt. Jede Seite hat denselben Aufbau — Modell, Variablen, Nebenbedingungen, Lösen, <strong>alle</strong> Statusfälle, Lösung auslesen, Stolpersteine. So lassen sich die Seiten nebeneinanderlegen.</p>
</blockquote>
<p><strong>Alle Schnipsel lösen dasselbe Problem</strong> — das Produktionsprogramm aus <ahref="oekosystem.html#sec:oekosystem-schnellstart">Abschnitt 3.1</a> mit dem bekannten Optimum <strong>530</strong> und den Schattenpreisen <strong>12</strong> und <strong>1</strong>:</p>
<p>Damit ist jeder Schnipsel selbstprüfend: Kommt bei Ihnen etwas anderes als 530 heraus, liegt es an der Übertragung, nicht am Modell. (Die Ausnahme ist CP-SAT — ein rein stetiges LP ist dort das falsche Werkzeug; die Seite zeigt stattdessen die CP-SAT-eigenen Bausteine.)</p>
<blockquote>
<p><strong>Was dieser Anhang <em>nicht</em> ist.</strong> Kein Vergleich und keine Empfehlung. Welche Bibliothek für welche Aufgabe taugt, steht in <ahref="oekosystem.html#sec:oekosystem-wann-lohnt-sich-welche-ebene">Abschnitt 3.6</a>; denselben Fall in vier Bibliotheken <em>nebeneinander</em> zeigt <code>Ein_System_Vier_Ansaetze.py</code>, die beiden Modellierungssprachen <code>Modellierungsschichten.py</code>. Hier geht es allein ums Nachschlagen.</p>
</blockquote>
<hr/>
<h2id="d1-scipy-linprog-und-milp">D1 — SciPy: <code>linprog</code> und <code>milp</code></h2>
<p><strong>Wofür.</strong> Die Einstiegsschicht: keine zusätzliche Installation, HiGHS als Unterbau, ideal für lineare und gemischt-ganzzahlige Probleme in Matrixform. <strong>Wofür nicht:</strong> alles, was sich nicht als Matrix schreiben lässt, und jede nichtlineare Zielfunktion.</p>
<spanid="cb1-4"><ahref="#cb1-4"aria-hidden="true"tabindex="-1"></a><spanclass="co"># linprog MINIMIERT immer -> zum Maximieren die Zielfunktion negieren</span></span>
<spanid="cb1-17"><ahref="#cb1-17"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"INFEASIBLE - kein zulaessiger Punkt"</span>)</span>
<spanid="cb1-21"><ahref="#cb1-21"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Iterations- oder Zeitlimit erreicht"</span>)</span>
<spanid="cb1-23"><ahref="#cb1-23"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"numerisches Problem (status </span><spanclass="sc">{</span>ergebnis<spanclass="sc">.</span>status<spanclass="sc">}</span><spanclass="ss">): </span><spanclass="sc">{</span>ergebnis<spanclass="sc">.</span>message<spanclass="sc">}</span><spanclass="ss">"</span>)</span></code></pre></div>
<spanid="cb2-4"><ahref="#cb2-4"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Ganzzahlig: dasselbe Problem, aber x muss ganzzahlig sein</span></span>
<spanid="cb2-12"><ahref="#cb2-12"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"optimal: </span><spanclass="sc">{</span><spanclass="op">-</span>ergebnis<spanclass="sc">.</span>fun<spanclass="sc">:.2f}</span><spanclass="ss"> x = </span><spanclass="sc">{</span>np<spanclass="sc">.</span><spanclass="bu">round</span>(ergebnis.x)<spanclass="sc">.</span>astype(<spanclass="bu">int</span>)<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb2-15"><ahref="#cb2-15"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Zeitlimit - beste gefundene Loesung nutzen, Gap pruefen"</span>)</span>
<h3id="die-drei-häufigsten-stolpersteine">Die drei häufigsten Stolpersteine</h3>
<oltype="1">
<li><strong><code>linprog</code> minimiert immer.</strong> Zum Maximieren <code>c</code> negieren — und beim Ausgeben des Zielwerts das Vorzeichen wieder zurücknehmen. Dieselbe Negation dreht auch die Schattenpreise (<ahref="lp.html#sec:lp-die-vorzeichenfalle-bei-schattenpreisen">Abschnitt 5.7</a>).</li>
<li><strong><code>status == 0</code> prüfen, nicht <code>res.success</code> allein.</strong><code>success</code> ist bei <code>status == 1</code> (Limit erreicht) <code>False</code>, obwohl eine brauchbare Lösung vorliegen kann.</li>
<li><strong><code>bounds</code> gilt je Variable.</strong><code>bounds=(0, None)</code> setzt alle Variablen gleich; <code>bounds=[(0, None), (0, 10), ...]</code> einzeln. Wer die Liste vergisst, bekommt stillschweigend überall dieselbe Schranke.</li>
</ol>
<hr/>
<h2id="d2-highs-über-highspy">D2 — HiGHS über <code>highspy</code></h2>
<p><strong>Wofür.</strong> Derselbe Solver wie unter SciPy, aber direkt gesteuert: Optionen, Warm-Starts, inkrementelles Ändern eines bestehenden Modells. <strong>Wofür nicht:</strong> schnelles Hinschreiben — die CSR-Matrixübergabe ist fehleranfällig.</p>
<spanid="cb3-15"><ahref="#cb3-15"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Zeilen im CSR-Format: starts[i] = Beginn von Zeile i in indices/values</span></span>
<spanid="cb3-20"><ahref="#cb3-20"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Ganzzahligkeit: h.changeColsIntegrality(...) mit highspy.HighsVarType.kInteger</span></span>
<spanid="cb3-24"><ahref="#cb3-24"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> status <spanclass="op">==</span> highspy.HighsModelStatus.kOptimal:</span>
<spanid="cb3-28"><ahref="#cb3-28"aria-hidden="true"tabindex="-1"></a><spanclass="cf">elif</span> status <spanclass="op">==</span> highspy.HighsModelStatus.kInfeasible:</span>
<spanid="cb3-30"><ahref="#cb3-30"aria-hidden="true"tabindex="-1"></a><spanclass="cf">elif</span> status <spanclass="op">==</span> highspy.HighsModelStatus.kUnbounded:</span>
<spanid="cb3-32"><ahref="#cb3-32"aria-hidden="true"tabindex="-1"></a><spanclass="cf">elif</span> status <spanclass="op">==</span> highspy.HighsModelStatus.kTimeLimit:</span>
<spanid="cb3-33"><ahref="#cb3-33"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Zeitlimit, Gap </span><spanclass="sc">{</span>h<spanclass="sc">.</span>getInfo()<spanclass="sc">.</span>mip_gap<spanclass="sc">:.3f}</span><spanclass="ss">"</span>)</span>
<h3id="die-drei-häufigsten-stolpersteine-1">Die drei häufigsten Stolpersteine</h3>
<oltype="1">
<li><strong>Nicht zusammen mit <code>ortools</code> importieren.</strong> Beide bringen eine eigene HiGHS-Kopie mit; im selben Prozess endet das in <code>undefined symbol</code> (<ahref="anhang-fehlerdiagnose.html#anhang-fehlerdiagnose">Anhang C</a>, C9). Auch <code>cvxpy</code> zieht <code>highspy</code> bei der Solver-Erkennung mit hinein.</li>
<li><strong>Das CSR-Format stimmt oder es stimmt still nicht.</strong><code>starts</code> hat so viele Einträge wie Zeilen, <code>indices</code> und <code>values</code> so viele wie Nichtnullen. Ein falscher <code>starts</code>-Eintrag erzeugt ein <em>anderes</em>, aber lösbares Modell — es fällt nur durch ein falsches Ergebnis auf.</li>
<li><strong><code>output_flag</code> abschalten</strong>, sonst überschwemmt das Solverprotokoll jede Ausgabe.</li>
<p><strong>Wofür.</strong> Bequeme algebraische Schreibweise für LP und MILP mit umschaltbarem Backend (<code>GLOP</code>, <code>SCIP</code>, <code>CBC</code>, <code>SAT</code>). <strong>Wofür nicht:</strong> Scheduling und kombinatorische Bedingungen — dafür ist CP-SAT (D4) da.</p>
<spanid="cb4-6"><ahref="#cb4-6"aria-hidden="true"tabindex="-1"></a><spanclass="cf">raise</span><spanclass="pp">SystemExit</span>(<spanclass="st">"Solver nicht verfuegbar"</span>)</span>
<spanid="cb4-18"><ahref="#cb4-18"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> status <spanclass="op">==</span> pywraplp.Solver.OPTIMAL:</span>
<spanid="cb4-20"><ahref="#cb4-20"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"x = </span><spanclass="sc">{</span>[<spanclass="bu">round</span>(v.solution_value(), <spanclass="dv">4</span>) <spanclass="cf">for</span> v <spanclass="kw">in</span> x]<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb4-21"><ahref="#cb4-21"aria-hidden="true"tabindex="-1"></a><spanclass="cf">elif</span> status <spanclass="op">==</span> pywraplp.Solver.FEASIBLE:</span>
<spanid="cb4-22"><ahref="#cb4-22"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"zulaessig, nicht bewiesen optimal (Zeitlimit)"</span>)</span>
<spanid="cb4-23"><ahref="#cb4-23"aria-hidden="true"tabindex="-1"></a><spanclass="cf">elif</span> status <spanclass="op">==</span> pywraplp.Solver.INFEASIBLE:</span>
<spanid="cb4-25"><ahref="#cb4-25"aria-hidden="true"tabindex="-1"></a><spanclass="cf">elif</span> status <spanclass="op">==</span> pywraplp.Solver.UNBOUNDED:</span>
<spanid="cb4-28"><ahref="#cb4-28"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"ABNORMAL / NOT_SOLVED - Modell oder Solver pruefen"</span>)</span></code></pre></div>
<h3id="die-drei-häufigsten-stolpersteine-2">Die drei häufigsten Stolpersteine</h3>
<oltype="1">
<li><strong><code>CreateSolver</code> liefert <code>None</code></strong>, wenn der Backend-Name unbekannt oder nicht gebaut ist — immer prüfen, statt am <code>None</code> später zu scheitern.</li>
<li><strong><code>SetTimeLimit</code> erwartet Millisekunden</strong>, nicht Sekunden. Ein <code>SetTimeLimit(60)</code> bricht nach einer sechzigstel Sekunde ab.</li>
<li><strong><code>FEASIBLE</code> ist kein <code>OPTIMAL</code>.</strong> Bei Zeitlimit liefert der Solver eine gültige, aber möglicherweise schlechte Lösung — den Gap mitberichten, nicht die Zahl allein.</li>
<spanid="cb5-11"><ahref="#cb5-11"aria-hidden="true"tabindex="-1"></a>modell.Add(<spanclass="dv">2</span><spanclass="op">*</span> x <spanclass="op">+</span><spanclass="dv">3</span><spanclass="op">*</span> y <spanclass="op"><=</span><spanclass="dv">50</span>)</span>
<spanid="cb5-12"><ahref="#cb5-12"aria-hidden="true"tabindex="-1"></a>modell.Add(x <spanclass="op">>=</span><spanclass="dv">5</span>).OnlyEnforceIf(b) <spanclass="co"># gilt nur, wenn b wahr ist</span></span>
<spanid="cb5-16"><ahref="#cb5-16"aria-hidden="true"tabindex="-1"></a>modell.Maximize(<spanclass="dv">10</span><spanclass="op">*</span> x <spanclass="op">+</span><spanclass="dv">15</span><spanclass="op">*</span> y <spanclass="op">-</span><spanclass="dv">3</span><spanclass="op">*</span> z)</span>
<spanid="cb5-24"><ahref="#cb5-24"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> status <spanclass="op">==</span> cp_model.OPTIMAL:</span>
<spanid="cb5-26"><ahref="#cb5-26"aria-hidden="true"tabindex="-1"></a><spanclass="cf">elif</span> status <spanclass="op">==</span> cp_model.FEASIBLE:</span>
<spanid="cb5-27"><ahref="#cb5-27"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"zulaessig, nicht bewiesen optimal - Gap-Schranke: </span><spanclass="sc">{</span>loeser<spanclass="sc">.</span>BestObjectiveBound()<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb5-28"><ahref="#cb5-28"aria-hidden="true"tabindex="-1"></a><spanclass="cf">elif</span> status <spanclass="op">==</span> cp_model.INFEASIBLE:</span>
<spanid="cb5-30"><ahref="#cb5-30"aria-hidden="true"tabindex="-1"></a><spanclass="cf">elif</span> status <spanclass="op">==</span> cp_model.MODEL_INVALID:</span>
<spanid="cb5-33"><ahref="#cb5-33"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"UNKNOWN - Zeit abgelaufen, ohne eine Loesung zu finden"</span>)</span>
<h3id="die-drei-häufigsten-stolpersteine-3">Die drei häufigsten Stolpersteine</h3>
<oltype="1">
<li><strong>Alles ist ganzzahlig.</strong><code>0.5 * x</code> gibt es nicht. Skalieren Sie das ganze Modell auf eine feinere Einheit, statt zu runden.</li>
<li><strong><code>UNKNOWN</code> heißt nicht <code>INFEASIBLE</code>.</strong> Es heißt: Die Zeit war zu knapp, um überhaupt etwas zu finden. Die beiden zu verwechseln ist einer der teuersten Fehler in Produktion — das Modell wird für widersprüchlich erklärt, obwohl es lösbar ist.</li>
<li><strong>Ohne <code>num_workers = 1</code> ist der Lauf nicht reproduzierbar.</strong> Mehrere Suchstränge finden je nach Zeitverlauf verschiedene, gleich gute Lösungen. Für Tests und für abgedruckte Ausgaben Worker und Seed festnageln.</li>
</ol>
<hr/>
<h2id="d5-cvxpy">D5 — CVXPY</h2>
<p><strong>Wofür.</strong> Konvexe Probleme: quadratische Ziele, Normen, CVaR, alles mit Regularisierungstermen. <strong>Wofür nicht:</strong> große kombinatorische Modelle — der Aufbau der Ausdrücke wird dann selbst zum Engpass.</p>
<spanid="cb6-12"><ahref="#cb6-12"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span><spanclass="kw">not</span> problem.is_dcp(): <spanclass="co"># VOR dem Loesen pruefen</span></span>
<spanid="cb6-13"><ahref="#cb6-13"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"nicht DCP:"</span>, [c <spanclass="cf">for</span> c <spanclass="kw">in</span> bedingungen <spanclass="cf">if</span><spanclass="kw">not</span> c.is_dcp()])</span>
<h3id="die-drei-häufigsten-stolpersteine-4">Die drei häufigsten Stolpersteine</h3>
<oltype="1">
<li><strong>DCP-Regeln vor dem Lösen prüfen.</strong><code>problem.is_dcp()</code> und dann die einzelnen Bedingungen — das nennt die Schuldige, statt einen <code>DCPError</code> ohne Ort zu werfen (<ahref="anhang-fehlerdiagnose.html#anhang-fehlerdiagnose">Anhang C</a>, C8).</li>
<li><strong><code>OPTIMAL_INACCURATE</code> ist kein Erfolg.</strong> Der Solver hat aufgegeben und meldet das leise. Diesen Fall immer eigens behandeln.</li>
<li><strong>Der Aufbau kann teurer sein als das Lösen.</strong> Schleifen über Szenarien durch Vektorausdrücke ersetzen; <code>Parameter</code> statt Neuaufbau, wenn sich nur Zahlen ändern.</li>
</ol>
<hr/>
<h2id="d6-modellierungssprachen-pyomo-und-linopy">D6 — Modellierungssprachen: Pyomo und Linopy</h2>
<p><strong>Wofür.</strong> Beide trennen <em>Modell</em> von <em>Solver</em>: dasselbe Modell läuft ohne Änderung unter HiGHS, CBC, Gurobi. Pyomo denkt in <strong>Mengen und Indizes</strong> wie eine mathematische Formulierung; Linopy denkt in <strong>beschrifteten Arrays</strong> und baut Nebenbedingungen als Matrixoperation statt in Python-Schleifen. <strong>Wofür nicht:</strong> ein Modell mit zehn Nebenbedingungen — dort ist der Aufwand höher als der Nutzen.</p>
<spanid="cb8-21"><ahref="#cb8-21"aria-hidden="true"tabindex="-1"></a><spanclass="co"># EINE Zeile, so viele Nebenbedingungen wie 'ressource' Eintraege hat:</span></span>
<h3id="die-drei-häufigsten-stolpersteine-5">Die drei häufigsten Stolpersteine</h3>
<oltype="1">
<li><strong>Der Solver ist ein eigenes Programm.</strong><code>SolverFactory("appsi_highs")</code> scheitert, wenn HiGHS nicht auffindbar ist — die Fehlermeldung nennt dann das Modell, nicht die fehlende Installation.</li>
<li><strong>Schattenpreise kommen nur auf Anforderung.</strong> Bei Pyomo braucht es <code>Suffix(direction=IMPORT)</code><em>vor</em> dem Lösen; wer ihn vergisst, bekommt einen <code>KeyError</code> statt einer Warnung.</li>
<li><strong>Bei Linopy die Indizes benennen</strong> (<code>pd.Index(..., name="produkt")</code>). Ohne Namen heißen die Achsen <code>dim_0</code>, und jede spätere Zuordnung wird zum Ratespiel.</li>
<p>Alle sechs Seiten haben denselben längsten Abschnitt: die <strong>Statusauswertung</strong>. Das ist kein Zufall. Ein Solveraufruf hat nie zwei Ausgänge, sondern mindestens fünf — optimal, zulässig ohne Beweis, unlösbar, unbeschränkt, abgebrochen. Code, der nur <code>if erfolgreich:</code> prüft, verwechselt früher oder später „keine Lösung gefunden“ mit „es gibt keine Lösung“, und diese Verwechslung merkt niemand, bis sie teuer wird.</p>