<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"open><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"class="aktiv"><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
<navclass="breadcrumb"aria-label="Breadcrumb"><ahref="index.html">Start</a>›<span>Teil I</span>›<span>Kapitel 3: Das Python-Ökosystem für OR — Solver, Bindings und Modellierungsschichten</span></nav>
<navclass="prev-next"><aclass="prev-next-knopf prev-next-prev"href="fundament.html"><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</span></a><aclass="prev-next-knopf prev-next-next"href="modellierung.html"><span><small>Weiter</small>Kapitel 4: Vom Management-Wunsch zum Modell</span><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-right"></use></svg></a></nav>
<article>
<h1id="kap-oekosystem">Kapitel 3: Das Python-Ökosystem für OR — Solver, Bindings und Modellierungsschichten</h1>
<divclass="card card-blick">
<blockquote>
<p><strong>📌 Kapitel auf einen Blick</strong></p>
<p><strong>Worum geht es?</strong> Warum es für Optimierung in Python mehrere konkurrierende Bibliotheken gibt, was sie unterscheidet, und wie Sie in unter einer Minute die richtige auswählen.</p>
<p><strong>Voraussetzungen:</strong><ahref="einfuehrung.html#kap-einfuehrung">Kapitel 1</a> und <ahref="fundament.html#kap-fundament">Kapitel 2</a>.</p>
<p><strong>Danach können Sie:</strong> Für ein gegebenes Problem begründet einen Solver wählen, dasselbe Modell in verschiedenen Bibliotheken formulieren, die Ergebnisse gegeneinander prüfen — und messen, ob Ihre Laufzeit überhaupt im Solver entsteht oder schon davor.</p>
<p><strong>Zeitbedarf:</strong> ca. 4 Stunden.</p>
<p><strong>Notebook:</strong><ahref="Notebooks_04/oekosystem.ipynb">oekosystem.ipynb</a> — herunterladen und in Jupyter öffnen, in Colab hochladen oder mit dem Kurs-Image starten</p>
<h2id="sec:oekosystem-schnellstart">3.1 In 5 Minuten gelöst</h2>
<divclass="card card-schnellstart">
<blockquote>
<p><strong>🚀 In 5 Minuten gelöst: Ein LP ohne jede Installation</strong></p>
<p>Ein Futtermittelhersteller mischt zwei Rohstoffe zu möglichst geringen Kosten. Jeder Kilogramm Mischung muss mindestens 20 g Protein und 5 g Fett enthalten.</p>
<table>
<thead>
<trclass="header">
<th>Rohstoff</th>
<thstyle="text-align: right;">Preis je kg</th>
<thstyle="text-align: right;">Protein je kg</th>
<thstyle="text-align: right;">Fett je kg</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td>Weizenschrot</td>
<tdstyle="text-align: right;">0,42 €</td>
<tdstyle="text-align: right;">12 g</td>
<tdstyle="text-align: right;">2 g</td>
</tr>
<trclass="even">
<td>Sojaschrot</td>
<tdstyle="text-align: right;">0,88 €</td>
<tdstyle="text-align: right;">44 g</td>
<tdstyle="text-align: right;">15 g</td>
</tr>
</tbody>
</table>
<p>Sie brauchen dafür <strong>keine</strong> zusätzliche Bibliothek — SciPy genügt, und SciPy ist in jeder wissenschaftlichen Python-Installation schon da:</p>
<spanid="cb1-3"><ahref="#cb1-3"aria-hidden="true"tabindex="-1"></a><spanclass="co"># linprog MINIMIERT und kennt nur "<=". Mindestgehalte werden also negiert.</span></span>
<spanid="cb1-4"><ahref="#cb1-4"aria-hidden="true"tabindex="-1"></a>res <spanclass="op">=</span> linprog(c<spanclass="op">=</span>[<spanclass="fl">0.42</span>, <spanclass="fl">0.88</span>], <spanclass="co"># Kosten je kg</span></span>
<spanid="cb1-11"><ahref="#cb1-11"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Weizen </span><spanclass="sc">{</span>res<spanclass="sc">.</span>x[<spanclass="dv">0</span>]<spanclass="sc">:.3f}</span><spanclass="ss"> kg, Soja </span><spanclass="sc">{</span>res<spanclass="sc">.</span>x[<spanclass="dv">1</span>]<spanclass="sc">:.3f}</span><spanclass="ss"> kg -></span><spanclass="sc">{</span>res<spanclass="sc">.</span>fun<spanclass="sc">:.4f}</span><spanclass="ss"> EUR/kg"</span>)</span></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>Optimization terminated successfully. (HiGHS Status 7: Optimal)
Weizen 0.750 kg, Soja 0.250 kg -> 0.5350 EUR/kg</code></pre>
</blockquote>
</div>
<p><strong>Sie haben soeben HiGHS benutzt</strong> — denselben C++-Solver, der auch hinter <code>highspy</code>, hinter CVXPY und in vielen kommerziellen Systemen steckt. <code>scipy.optimize.linprog</code> ist nur die dünnste denkbare Hülle darum.</p>
<p>Das ist die zentrale Botschaft dieses Kapitels: <strong>Modellierungsschicht und Solver sind zwei verschiedene Dinge.</strong> Die Bibliothek, in der Sie Ihr Modell hinschreiben, bestimmt, wie angenehm die Arbeit ist. Der Solver dahinter bestimmt, wie schnell gerechnet wird. Beide lassen sich unabhängig voneinander tauschen — und genau davon handelt der Rest des Kapitels.</p>
<blockquote>
<p><strong>⚠️ Zwei Stolpersteine stecken schon in diesen sechs Zeilen</strong></p>
<ul>
<li><code>linprog</code><strong>minimiert immer</strong>. Wer maximieren will, negiert die Zielfunktion — und darf nicht vergessen, das Ergebnis zurückzudrehen.</li>
<li><code>linprog</code> kennt <strong>nur <code><=</code></strong>. Ein Mindestgehalt „<spanclass="math inline">\ge 20</span>“ wird zu „<spanclass="math inline">-12x_1 - 44x_2 \le -20</span>“. Ein Vorzeichenfehler an dieser Stelle liefert eine perfekt aussehende Lösung, die das Gegenteil des Gewollten erfüllt.</li>
</ul>
<p>Beide Fallen verschwinden, sobald man eine Modellierungsschicht wie CVXPY oder Pyomo benutzt, in der <code>>=</code> einfach <code>>=</code> heißt. Das ist ein Hauptgrund, warum es sie gibt.</p>
<li>… einschätzen, wann sich der Aufwand einer Low-Level-Schnittstelle lohnt und wann nicht.</li>
<li>… Pyomo und Linopy einordnen und begründen, wann sich der Umstieg auf sie lohnt.</li>
<li>… Aufbau- und Lösezeit getrennt messen und einen Modellaufbau mit NumPy oder Polars vektorisieren, statt ihn in Python-Schleifen zu erzeugen.</li>
</ol>
<hr/>
<h2id="sec:oekosystem-die-zwei-schichten-architektur">3.3 Die Zwei-Schichten-Architektur</h2>
<p>Im modernen Operations Research schreibt man Optimierungsalgorithmen nicht selbst. Man nutzt eine <strong>zweischichtige Architektur</strong>:</p>
<oltype="1">
<li><strong>Modellierungsschicht (<em>Frontend</em>, DSL):</strong> Python-Bibliotheken, mit denen Entscheidungsvariablen, Zielfunktion und Nebenbedingungen in mathematiknaher Syntax formuliert werden. Hier arbeiten Sie.</li>
<li><strong>Solver-Schicht (<em>Backend</em>, Engine):</strong> Hochoptimierte C++- oder Fortran-Bibliotheken, die das Modell in Matrixstrukturen übersetzen und mit spezialisierten Algorithmen lösen (Dual Simplex, Interior-Point, Branch-and-Cut, CDCL-SAT-Suche).</li>
</ol>
<figure>
<imgsrc="bilder_04/kap03_solver_architektur.svg"alt="Abb. 3.1: Modellierungsschicht und Solver-Engines"/>
<figcaptionaria-hidden="true">Abb. 3.1: Modellierungsschicht und Solver-Engines</figcaption>
</figure>
<blockquote>
<p><strong>🎯 Merksatz</strong> Die Modellierungsschicht bestimmt, wie angenehm Sie arbeiten. Die Solver-Schicht bestimmt, wie schnell gerechnet wird. Beides ist entkoppelt — man kann dieselbe CVXPY-Formulierung mit fünf verschiedenen Solvern lösen.</p>
</blockquote>
<p><strong>Warum diese Trennung nützlich ist:</strong> Ein CVXPY-Modell, das heute mit dem freien Solver Clarabel läuft, läuft morgen ohne Codeänderung mit dem kommerziellen Gurobi — man tauscht ein Argument. Das schützt vor Herstellerbindung und erlaubt, im Projekt klein anzufangen.</p>
<hr/>
<h2id="sec:oekosystem-die-werkzeuge-im-vergleich">3.4 Die Werkzeuge im Vergleich</h2>
<td>Industrielle Großmodelle mit strikter Trennung von Daten und Modell</td>
<td>HiGHS, Gurobi, CPLEX, SCIP, IPOPT</td>
</tr>
<trclass="even">
<td><code>Linopy</code></td>
<td>LP/MILP über beschriftete Arrays (<code>xarray</code>)</td>
<td>Modelle mit zehntausenden gleichartigen Nebenbedingungen (Energie, Netze, Zeitreihen)</td>
<td>HiGHS, GLPK, CBC, Gurobi</td>
</tr>
</tbody>
</table>
<h3id="die-entscheidung-in-drei-fragen">Die Entscheidung in drei Fragen</h3>
<p>Statt die Tabelle auswendig zu lernen, beantworten Sie drei Fragen:</p>
<blockquote>
<p><strong>Frage 1: Gibt es Ja/Nein-Entscheidungen oder Reihenfolgen?</strong> → <strong>Ja:</strong> OR-Tools (CP-SAT) bei Zuweisung/Scheduling, oder MILP über HiGHS bei ökonomischen Fixkostenmodellen. → <strong>Nein:</strong> weiter zu Frage 2.</p>
<p><strong>Frage 2: Ist die Zielfunktion linear?</strong> → <strong>Ja:</strong><code>scipy.optimize.linprog</code> (klein) oder <code>highspy</code> (groß, wiederholt). → <strong>Nein, aber konvex</strong> (Quadrate, Normen, <code>log</code>, <code>exp</code> in konvexer Kombination): <strong>CVXPY</strong>. → <strong>Nein und nicht konvex:</strong><code>scipy.optimize.minimize</code> — im Bewusstsein, dass nur ein lokales Optimum herauskommt.</p>
<p><strong>Frage 3: Ist es ein Routing-Problem mit Fahrzeugen, Depots und Zeitfenstern?</strong> → <strong>Ja:</strong> OR-Tools <strong>Routing Library</strong> (nicht CP-SAT von Hand nachbauen).</p>
</blockquote>
<p>Das folgende kleine Programm gießt diese Logik in Code — als Nachschlagehilfe für den eigenen Gebrauch.</p>
<spanid="cb3-5"><ahref="#cb3-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel Oekosystem: Entscheidungshilfe zur Solverwahl.</span></span>
<spanid="cb3-6"><ahref="#cb3-6"aria-hidden="true"tabindex="-1"></a><spanclass="co">Beantwortet drei Fragen und empfiehlt Bibliothek + Backend.</span></span>
<spanid="cb3-31"><ahref="#cb3-31"aria-hidden="true"tabindex="-1"></a><spanclass="st">"MILP mit ökonomischer Struktur (Fixkosten, Kardinalität)."</span>)</span>
<spanid="cb3-33"><ahref="#cb3-33"aria-hidden="true"tabindex="-1"></a><spanclass="st">"Logische Regeln und Zuweisungen: CP-SAT propagiert sehr effizient."</span>)</span>
<spanid="cb3-35"><ahref="#cb3-35"aria-hidden="true"tabindex="-1"></a><spanclass="st">"Diskrete Struktur dominiert; CP-SAT verarbeitet auch nichtlineare Logik."</span>)</span>
<spanid="cb3-42"><ahref="#cb3-42"aria-hidden="true"tabindex="-1"></a><spanclass="st">"Kleinstes Setup, keine zusätzliche Abhängigkeit."</span>)</span>
<spanid="cb3-46"><ahref="#cb3-46"aria-hidden="true"tabindex="-1"></a><spanclass="st">"Konvexität wird automatisch geprüft; Portfolio-Standard."</span>)</span>
<spanid="cb3-49"><ahref="#cb3-49"aria-hidden="true"tabindex="-1"></a><spanclass="st">"Nicht konvex: nur lokales Optimum, Startpunkt variieren und vergleichen!"</span>)</span>
<spanid="cb3-67"><ahref="#cb3-67"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> name, p <spanclass="kw">in</span> BEISPIELE.items():</span>
<spanid="cb3-68"><ahref="#cb3-68"aria-hidden="true"tabindex="-1"></a> lib, backend, grund <spanclass="op">=</span> empfehle(p)</span>
<p><strong>✏️ Handrechnung 3.1: Erwarten Sie das Ergebnis</strong></p>
<p>Bevor Sie das Programm laufen lassen: Welches Produkt ist am attraktivsten?</p>
<p>Produkt 3 bringt den höchsten Ertrag (25) und verbraucht wenig von Ressource 2 (nur 1). Es verbraucht aber doppelt so viel von Ressource 1. Rechnen wir den Ertrag <strong>pro Einheit Engpassressource</strong>:</p>
<p><strong>⚠️ Vorab: eine Stolperfalle der Installation</strong></p>
<p>Auf vielen Systemen lassen sich <strong><code>ortools</code> und <code>highspy</code> nicht im selben Python-Prozess importieren</strong>. Beide Pakete bringen ihre eigene, unterschiedlich kompilierte Kopie des HiGHS-Solvers mit; der dynamische Linker löst die Symbole dann falsch auf. Die Fehlermeldung sieht so aus:</p>
<p>Das ist <strong>kein Fehler in Ihrem Code</strong>, und bei <code>ortools</code> und <code>highspy</code> direkt hilft auch die Reihenfolge der Importe nicht — es trifft beide Richtungen. <code>cvxpy</code> verträgt sich mit beiden, importiert aber ein installiertes <code>highspy</code> bei der Solver-Erkennung selbst mit (siehe <code>*</code>).</p>
<p>* ist <code>highspy</code> installiert, gilt die <code>ortools</code>+<code>highspy</code>-Zeile entsprechend: erst <code>cvxpy</code>, dann <code>ortools</code> importiert → Crash; erst <code>ortools</code>, dann <code>cvxpy</code> → läuft, CVXPY nur ohne HIGHS-Interface.</p>
<p><strong>Abhilfe:</strong> Jeden Solver in einem <strong>eigenen Prozess</strong> ausführen — genau das tut das folgende Programm. Alternativ: getrennte virtuelle Umgebungen, oder auf <code>highspy</code> verzichten und HiGHS über <code>scipy.optimize.linprog</code> bzw. CVXPY ansprechen (dort ist es ohnehin als Backend verfügbar).</p>
<p>Der Installationstest im Vorspann umgeht die Falle bereits: Er lädt <code>ortools</code> zuerst, prüft <code>highspy</code> und <code>cvxpy</code> in der Paketübersicht nur auf Anwesenheit (<code>importlib.util.find_spec</code>) und importiert CVXPY erst im Funktionstest.</p>
<h3id="wie-die-isolation-aussieht-wenn-sie-tragen-soll">Wie die Isolation aussieht, wenn sie tragen soll</h3>
<p>„Eigener Prozess” ist schnell gesagt. Die naheliegende Umsetzung — ein Codeschnipsel als Zeichenkette an <code>python -c</code> übergeben — funktioniert und ist trotzdem die schlechteste: Der Schnipsel ist für Editor, Linter und Testwerkzeug unsichtbar, ein Tippfehler darin fällt erst zur Laufzeit auf, und übergeben lassen sich nur Zeichenketten.</p>
<p>Tragfähig ist stattdessen: <strong>jeder Solver eine gewöhnliche Funktion mit lokalem Import</strong>, ausgeführt von einem <code>ProcessPoolExecutor</code> mit zwei Einstellungen, die zusammen die Garantie ergeben:</p>
<td>Der Kindprozess startet mit einem <strong>frischen</strong> Interpreter, statt den Speicher des Elternprozesses zu erben. Unter Linux ist <code>fork</code> der Standard — und damit wäre alles, was hier schon importiert ist, auch dort importiert.</td>
</tr>
<trclass="even">
<td><code>max_tasks_per_child=1</code></td>
<td>Jede Aufgabe bekommt einen <strong>neuen</strong> Prozess. Ohne das verwendet der Pool seinen Arbeiter wieder, und beim zweiten Solver ist der Konflikt zurück. Genau dieser Fehler ist leicht zu machen und schwer zu finden.</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>⚠️ <code>max_tasks_per_child=1</code> ist nicht optional</strong> Ein Pool ohne diese Angabe ist der <strong>Normalfall</strong> — er soll seine Arbeiter ja wiederverwenden. Wer die Isolation über einen Pool herstellt und das vergisst, hat einen Prozesswechsel programmiert, aber keine Isolation gewonnen: Die zweite Aufgabe landet im selben Interpreter wie die erste. Der Absturz kommt dann nicht beim ersten Solver, sondern beim zweiten — und sieht aus wie ein Problem des zweiten.</p>
</blockquote>
<p>Denselben Aufbau verwenden <code>Solverwechsel_CPSAT_HiGHS.py</code> (<ahref="praxisfallen.html#kap-praxisfallen">Kapitel 22</a>) und <code>Benchmark_Skalierung.py</code> (<ahref="testing.html#kap-testing">Kapitel 23</a>). Dort wandern zusätzlich <strong>Datenobjekte</strong> über die Prozessgrenze statt Zeichenketten — möglich, weil Domänenmodell und Lösungs-DTO keinen Solver kennen (<ahref="praxisfallen.html#sec:praxisfallen-or-kern">Abschnitt 22.6</a>).</p>
<spanid="cb6-11"><ahref="#cb6-11"aria-hidden="true"tabindex="-1"></a><spanclass="co">Deckt scipy.optimize, highspy, CVXPY und OR-Tools/GLOP ab, mit Kreuzvergleich</span></span>
<spanid="cb6-14"><ahref="#cb6-14"aria-hidden="true"tabindex="-1"></a><spanclass="co">WICHTIG: Jeder Solver laeuft in einem EIGENEN Prozess, weil sich ortools und</span></span>
<spanid="cb6-15"><ahref="#cb6-15"aria-hidden="true"tabindex="-1"></a><spanclass="co">highspy auf vielen Systemen nicht gemeinsam importieren lassen (beide bringen</span></span>
<spanid="cb6-16"><ahref="#cb6-16"aria-hidden="true"tabindex="-1"></a><spanclass="co">eine eigene HiGHS-Kopie mit -> Symbolkonflikt).</span></span>
<spanid="cb6-18"><ahref="#cb6-18"aria-hidden="true"tabindex="-1"></a><spanclass="co">Die Isolation besorgt ein ProcessPoolExecutor. Drei Einstellungen ergeben</span></span>
<spanid="cb6-19"><ahref="#cb6-19"aria-hidden="true"tabindex="-1"></a><spanclass="co">zusammen die Garantie:</span></span>
<spanid="cb6-21"><ahref="#cb6-21"aria-hidden="true"tabindex="-1"></a><spanclass="co"> mp_context "spawn" Der Kindprozess startet mit einem FRISCHEN</span></span>
<spanid="cb6-22"><ahref="#cb6-22"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Interpreter, statt den Speicher des Elternprozesses</span></span>
<spanid="cb6-23"><ahref="#cb6-23"aria-hidden="true"tabindex="-1"></a><spanclass="co"> zu erben. Was hier schon importiert ist, ist dort</span></span>
<spanid="cb6-24"><ahref="#cb6-24"aria-hidden="true"tabindex="-1"></a><spanclass="co"> nicht importiert. Mit dem Standard "fork" auf Linux</span></span>
<spanid="cb6-25"><ahref="#cb6-25"aria-hidden="true"tabindex="-1"></a><spanclass="co"> waere das nicht so.</span></span>
<spanid="cb6-26"><ahref="#cb6-26"aria-hidden="true"tabindex="-1"></a><spanclass="co"> max_tasks_per_child=1 Jede Aufgabe bekommt einen NEUEN Prozess. Ohne das</span></span>
<spanid="cb6-27"><ahref="#cb6-27"aria-hidden="true"tabindex="-1"></a><spanclass="co"> wuerde der Pool seinen Arbeiter wiederverwenden - und</span></span>
<spanid="cb6-28"><ahref="#cb6-28"aria-hidden="true"tabindex="-1"></a><spanclass="co"> beim zweiten Solver waere der Konflikt zurueck.</span></span>
<spanid="cb6-29"><ahref="#cb6-29"aria-hidden="true"tabindex="-1"></a><spanclass="co"> max_workers=1 Haelt die vier Laeufe nacheinander. Nicht aus</span></span>
<spanid="cb6-30"><ahref="#cb6-30"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Vorsicht, sondern damit die gemessenen Zeiten</span></span>
<spanid="cb6-33"><ahref="#cb6-33"aria-hidden="true"tabindex="-1"></a><spanclass="co">Jeder Solver steht in einer eigenen Funktion mit LOKALEM Import. Das ist der</span></span>
<spanid="cb6-34"><ahref="#cb6-34"aria-hidden="true"tabindex="-1"></a><spanclass="co">Unterschied zu einem Codestring, den man an 'python -c' uebergibt: Die</span></span>
<spanid="cb6-35"><ahref="#cb6-35"aria-hidden="true"tabindex="-1"></a><spanclass="co">Funktion laesst sich einzeln aufrufen, testen und vom Editor pruefen - ein</span></span>
<spanid="cb6-45"><ahref="#cb6-45"aria-hidden="true"tabindex="-1"></a>ERWARTET <spanclass="op">=</span><spanclass="fl">530.0</span><spanclass="co"># Ergebnis der Handrechnung zum Produktionsprogramm</span></span>
<spanid="cb6-47"><ahref="#cb6-47"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Die Instanz - einmal notiert, von allen vier Funktionen benutzt.</span></span>
<spanid="cb6-70"><ahref="#cb6-70"aria-hidden="true"tabindex="-1"></a><spanclass="co"># CSR-Format: starts[i] = Beginn von Zeile i in indices/values</span></span>
<spanid="cb6-83"><ahref="#cb6-83"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> cp.Variable(<spanclass="dv">3</span>, nonneg<spanclass="op">=</span><spanclass="va">True</span>)</span>
<spanid="cb6-84"><ahref="#cb6-84"aria-hidden="true"tabindex="-1"></a> problem <spanclass="op">=</span> cp.Problem(cp.Maximize(np.array(ZIEL) <spanclass="op">@</span> x),</span>
<spanid="cb6-85"><ahref="#cb6-85"aria-hidden="true"tabindex="-1"></a> [np.array(MATRIX) <spanclass="op">@</span> x <spanclass="op"><=</span> np.array(KAPAZITAET)])</span>
<spanid="cb6-92"><ahref="#cb6-92"aria-hidden="true"tabindex="-1"></a> s <spanclass="op">=</span> pywraplp.Solver.CreateSolver(<spanclass="st">"GLOP"</span>)</span>
<spanid="cb6-93"><ahref="#cb6-93"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>j<spanclass="op">+</span><spanclass="dv">1</span><spanclass="sc">}</span><spanclass="ss">"</span>) <spanclass="cf">for</span> j <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="dv">3</span>)]</span>
<spanid="cb6-94"><ahref="#cb6-94"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i, kapazitaet <spanclass="kw">in</span><spanclass="bu">enumerate</span>(KAPAZITAET):</span>
<spanid="cb6-111"><ahref="#cb6-111"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" EIN SYSTEM - VIER ANSAETZE (je eigener Prozess)"</span>)</span>
<spanid="cb6-117"><ahref="#cb6-117"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Ein Pool, vier Aufgaben, vier frische Prozesse. Der Kontext muss</span></span>
<spanid="cb6-118"><ahref="#cb6-118"aria-hidden="true"tabindex="-1"></a><spanclass="co"># "spawn" sein - siehe Modulkommentar.</span></span>
<spanid="cb6-126"><ahref="#cb6-126"aria-hidden="true"tabindex="-1"></a> wert, x <spanclass="op">=</span> pool.submit(funktion).result(timeout<spanclass="op">=</span><spanclass="dv">120</span>)</span>
<spanid="cb6-127"><ahref="#cb6-127"aria-hidden="true"tabindex="-1"></a><spanclass="cf">except</span><spanclass="pp">Exception</span><spanclass="im">as</span> fehler: <spanclass="co"># Bibliothek fehlt o. Ae.</span></span>
<spanid="cb6-128"><ahref="#cb6-128"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span>name<spanclass="sc">:<26}</span><spanclass="ss"> nicht verfuegbar: </span><spanclass="sc">{</span><spanclass="bu">str</span>(fehler)[:<spanclass="dv">40</span>]<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb6-137"><ahref="#cb6-137"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Spannweite zwischen den Bibliotheken: </span><spanclass="sc">{</span>spanne<spanclass="sc">:.2e}</span><spanclass="ss">"</span>)</span>
<spanid="cb6-138"><ahref="#cb6-138"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Abweichung zur Handrechnung (</span><spanclass="sc">{</span>ERWARTET<spanclass="sc">:.0f}</span><spanclass="ss">): "</span></span>
<spanid="cb6-141"><ahref="#cb6-141"aria-hidden="true"tabindex="-1"></a><spanclass="cf">assert</span><spanclass="bu">abs</span>(werte[<spanclass="dv">0</span>] <spanclass="op">-</span> ERWARTET) <spanclass="op"><</span><spanclass="fl">1e-6</span>, <spanclass="st">"Ergebnis weicht von der Handrechnung ab!"</span></span>
<spanid="cb6-142"><ahref="#cb6-142"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Alle Wege fuehren zum selben, von Hand bestaetigten Optimum."</span>)</span>
<spanid="cb6-143"><ahref="#cb6-143"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"(Die Zeiten enthalten Prozessstart und Import - sie messen NICHT die"</span>)</span>
<spanid="cb6-144"><ahref="#cb6-144"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" reine Solverleistung. Die Uebungsaufgabe 'Laufzeitvergleich' trennt beides.)"</span>)</span>
<p><strong>🎯 Merksatz zur Spannweite</strong> Die vier Bibliotheken stimmen <strong>nicht auf die letzte Stelle</strong> überein, sondern nur bis auf <spanclass="math inline">2{,}4 \times 10^{-8}</span>. Das ist normal: Solver arbeiten mit endlicher Genauigkeit und brechen ab, sobald ihre eigene Toleranz erreicht ist. <strong>Vergleichen Sie Solver-Ergebnisse deshalb nie mit <code>==</code></strong>, sondern immer mit einer Toleranz — <code>abs(a - b) < 1e-6</code> oder <code>np.isclose()</code>. Wer auf exakte Gleichheit prüft, baut sich Tests, die zufällig mal bestehen und mal nicht.</p>
</blockquote>
<blockquote>
<p><strong>💻 Code-Durchgang</strong></p>
<table>
<colgroup>
<colstyle="width: 33%"/>
<colstyle="width: 33%"/>
<colstyle="width: 33%"/>
</colgroup>
<thead>
<trclass="header">
<th>Ansatz</th>
<th>Zeilen für das Modell</th>
<th>Charakter</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td><code>linprog</code></td>
<td>3</td>
<td>Matrizen direkt übergeben. Kürzeste Variante, aber man muss selbst negieren und die Matrixform von Hand herstellen.</td>
</tr>
<trclass="even">
<td><code>highspy</code></td>
<td>~15</td>
<td>Alles explizit, inklusive CSR-Format der dünnbesetzten Matrix. Aufwendig — dafür volle Kontrolle und kein Overhead beim wiederholten Lösen.</td>
</tr>
<trclass="odd">
<td><code>cvxpy</code></td>
<td>4</td>
<td>Liest sich wie die mathematische Formulierung. Prüft zusätzlich automatisch, ob das Problem konvex ist. Höchster Startaufwand pro Lauf (Kompilierung des Ausdrucksbaums).</td>
</tr>
<trclass="even">
<td><code>ortools</code>/GLOP</td>
<td>~6</td>
<td>Bedingungen einzeln mit <code>Add()</code> — gut lesbar bei wenigen, mühsam bei vielen Restriktionen.</td>
</tr>
</tbody>
</table>
<p><strong>Der wichtigste Teil des Programms sind die letzten fünf Zeilen:</strong> der Kreuzvergleich. Vier unabhängige Implementierungen, die auf 13 Nachkommastellen übereinstimmen und mit einer Handrechnung zusammenpassen, sind ein starkes Indiz für Korrektheit. Bei einem einzelnen Solver-Ergebnis haben Sie diese Sicherheit nicht.</p>
</blockquote>
<h3id="was-das-csr-format-bedeutet">Was das CSR-Format bedeutet</h3>
<p><code>highspy</code> erwartet die Nebenbedingungsmatrix im <strong>CSR-Format</strong> (<em>Compressed Sparse Row</em>, komprimierte Zeilendarstellung). Statt der vollen Matrix speichert man nur die Einträge ungleich null:</p>
<p><spanclass="math display">
\mathbf{A} = \begin{pmatrix} 1 & 1 & 2 \\ 2 & 3 & 1 \end{pmatrix}
</span></p>
<table>
<colgroup>
<colstyle="width: 33%"/>
<colstyle="width: 33%"/>
<colstyle="width: 33%"/>
</colgroup>
<thead>
<trclass="header">
<th>Array</th>
<th>Inhalt</th>
<th>Bedeutung</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td><code>values</code></td>
<td><code>[1, 1, 2, 2, 3, 1]</code></td>
<td>die Zahlen selbst, zeilenweise</td>
</tr>
<trclass="even">
<td><code>indices</code></td>
<td><code>[0, 1, 2, 0, 1, 2]</code></td>
<td>zu welcher <strong>Spalte</strong> gehört jeder Wert</td>
</tr>
<trclass="odd">
<td><code>starts</code></td>
<td><code>[0, 3]</code></td>
<td>Zeile 0 beginnt bei Position 0, Zeile 1 bei Position 3</td>
</tr>
</tbody>
</table>
<p>Bei kleinen Modellen wirkt das umständlich. Bei realen Modellen mit 100 000 Variablen und nur 0,1 % Nicht-Null-Einträgen spart es Faktor 1000 an Speicher — und ist der Grund, warum große LPs überhaupt lösbar sind.</p>
<blockquote>
<p><strong>⚠️ Typische Fehler</strong></p>
<ul>
<li><strong>Vergessen, dass <code>linprog</code> minimiert.</strong> Der häufigste Fehler überhaupt. Symptom: Der „optimale“ Gewinn ist erstaunlich niedrig oder null.</li>
<li><strong>CVXPY für ein nicht-konvexes Problem verwenden.</strong> CVXPY lehnt das ab mit <code>DCPError: Problem does not follow DCP rules</code>. Das ist ein <strong>Feature</strong>, keine Einschränkung: Der Fehler sagt Ihnen, dass Ihre Formulierung keine Optimalitätsgarantie hätte.</li>
<li><strong>CP-SAT mit kontinuierlichen Variablen füttern.</strong> CP-SAT kennt nur ganze Zahlen. Wer Euro-Beträge modelliert, rechnet in Cent (Ganzzahl) — oder nimmt einen LP-Solver.</li>
<li><strong>Für jeden Lauf ein neues Modell bauen.</strong> Bei 60 Backtest-Rebalancings kostet das Aufbauen mehr Zeit als das Lösen. <code>highspy</code> und CVXPY-<code>Parameter</code> erlauben es, das Modell einmal zu bauen und nur Daten zu tauschen (siehe <ahref="markowitz.html#kap-markowitz">Kapitel 19</a> und <ahref="handelsmaschine.html#kap-handelsmaschine">Kapitel 21</a>).</li>
</ul>
</blockquote>
<hr/>
<h2id="sec:oekosystem-wann-lohnt-sich-welche-ebene">3.6 Wann lohnt sich welche Ebene?</h2>
<td>Bewusst mit mehreren Startpunkten arbeiten</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>🎯 Merksatz</strong> Wählen Sie den Solver nach der <strong>Struktur des Modells</strong>, nicht nach Gewohnheit. Ein Zuweisungsproblem in CVXPY zu quälen oder ein Portfolio mit CP-SAT nachzubauen kostet Laufzeit und Nerven — und meist auch Lösungsqualität.</p>
</blockquote>
<hr/>
<h2id="sec:oekosystem-pyomo-linopy">3.7 Modellierungsschichten für große Modelle: Pyomo und Linopy</h2>
<p>Die vier Bibliotheken aus dem Vierfach-Vergleich decken den Alltag weitgehend ab. Sobald Modelle industrielle Größe erreichen — zehntausende Variablen, Daten aus mehreren Systemen, mehrere Jahre Lebensdauer — treten zwei weitere Werkzeuge in den Vordergrund.</p>
<h3id="pyomo-die-algebraische-denkweise">Pyomo: die algebraische Denkweise</h3>
<p><strong>Pyomo</strong> ist im deutschsprachigen Raum der De-facto-Standard für große LP- und MILP-Modelle in Energiewirtschaft, Chemie und Logistik. Sein Kennzeichen: Es denkt in <strong>Mengen und Indizes</strong>, so wie die mathematische Formulierung selbst.</p>
<p>Das ist das <spanclass="math inline">\forall i \in I</span> aus <ahref="einfuehrung.html#kap-einfuehrung">Kapitel 1</a>, unmittelbar in Code übersetzt: eine Regel, angewandt auf jedes Element einer Menge. Pyomo kann außerdem, was CVXPY und OR-Tools nicht können — <strong>nichtlineare und gemischt-ganzzahlig-nichtlineare</strong> Modelle (MINLP) an Solver wie Ipopt oder BONMIN übergeben (siehe <ahref="qp-nlp.html#kap-qp-nlp">Kapitel 11</a>).</p>
<h3id="linopy-eine-zeile-zehntausend-nebenbedingungen">Linopy: eine Zeile, zehntausend Nebenbedingungen</h3>
<p><strong>Linopy</strong> verfolgt einen anderen Ansatz: Variablen sind <strong>beschriftete Arrays</strong> (<code>xarray</code>), keine indizierten Einzelobjekte. Damit wird aus</p>
<p>nicht eine Nebenbedingung, sondern <strong>so viele, wie die Achse <code>ressource</code> Einträge hat</strong> — erzeugt als Matrixoperation, ohne dass je eine Python-Schleife läuft. In Energiesystem- und Netzmodellen mit den Achsen <em>Region × Technologie × Stunde des Jahres</em> ist das der Unterschied zwischen Minuten und Sekunden beim Modellaufbau.</p>
<h3id="die-schichten-im-überblick">Die Schichten im Überblick</h3>
<td>extrem schneller Aufbau bei Millionen Nebenbedingungen</td>
<td>nur LP/MILP; Daten müssen zu Arrays passen</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>🎯 Merksatz</strong> Keine dieser Schichten rechnet selbst. Alle sechs geben dasselbe Modell am Ende an dieselbe Handvoll C++-Solver weiter — hier fast immer an HiGHS. Die Wahl der Schicht entscheidet über <strong>Ihre</strong> Produktivität, nicht über die des Rechners.</p>
</blockquote>
<p>Das folgende Programm löst mit beiden Schichten dasselbe Produktionsproblem wie der Vierfach-Vergleich und prüft das Ergebnis gegen dieselbe Handrechnung.</p>
<spanid="cb10-7"><ahref="#cb10-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Geloest wird dasselbe Produktionsproblem wie im Vierfach-Vergleich:</span></span>
<spanid="cb10-18"><ahref="#cb10-18"aria-hidden="true"tabindex="-1"></a><spanclass="co"> trennt Modellstruktur sauber von den Daten (AbstractModel).</span></span>
<spanid="cb10-20"><ahref="#cb10-20"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Nebenbedingungen auf einmal, ohne Python-Schleife.</span></span>
<spanid="cb10-22"><ahref="#cb10-22"aria-hidden="true"tabindex="-1"></a><spanclass="co">Beide bringen KEINEN eigenen Solver mit; hier rechnet in beiden Faellen HiGHS.</span></span>
<spanid="cb10-24"><ahref="#cb10-24"aria-hidden="true"tabindex="-1"></a><spanclass="co">WICHTIG: ortools wird in diesem Programm bewusst NICHT importiert - es</span></span>
<spanid="cb10-25"><ahref="#cb10-25"aria-hidden="true"tabindex="-1"></a><spanclass="co">vertraegt sich nicht mit der HiGHS-Kopie, die Pyomo und Linopy laden</span></span>
<spanid="cb10-26"><ahref="#cb10-26"aria-hidden="true"tabindex="-1"></a><spanclass="co">(siehe die Stolperfalle im Abschnitt 'Ein System - vier Programmieransaetze').</span></span>
<spanid="cb10-41"><ahref="#cb10-41"aria-hidden="true"tabindex="-1"></a>ERWARTET <spanclass="op">=</span><spanclass="fl">530.0</span><spanclass="co"># Ergebnis der Handrechnung</span></span>
<spanid="cb10-47"><ahref="#cb10-47"aria-hidden="true"tabindex="-1"></a>VERBRAUCH <spanclass="op">=</span> np.array([[<spanclass="fl">1.0</span>, <spanclass="fl">1.0</span>, <spanclass="fl">2.0</span>], <spanclass="co"># Material je Produkt</span></span>
<spanid="cb10-48"><ahref="#cb10-48"aria-hidden="true"tabindex="-1"></a> [<spanclass="fl">2.0</span>, <spanclass="fl">3.0</span>, <spanclass="fl">1.0</span>]]) <spanclass="co"># Montage je Produkt</span></span>
<spanid="cb10-55"><ahref="#cb10-55"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Pyomo denkt in Mengen und Indizes, wie ein Mathematiker es aufschreibt.</span></span>
<spanid="cb10-57"><ahref="#cb10-57"aria-hidden="true"tabindex="-1"></a><spanclass="co"> `Constraint(RESSOURCEN, rule=...)` erzeugt fuer JEDES Element der Menge</span></span>
<spanid="cb10-58"><ahref="#cb10-58"aria-hidden="true"tabindex="-1"></a><spanclass="co"> eine Nebenbedingung - das ist das 'fuer alle i' der Formelsprache,</span></span>
<spanid="cb10-59"><ahref="#cb10-59"aria-hidden="true"tabindex="-1"></a><spanclass="co"> unmittelbar in Code uebersetzt.</span></span>
<spanid="cb10-63"><ahref="#cb10-63"aria-hidden="true"tabindex="-1"></a> modell <spanclass="op">=</span> pyo.ConcreteModel(name<spanclass="op">=</span><spanclass="st">"Produktionsprogramm"</span>)</span>
<spanid="cb10-70"><ahref="#cb10-70"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i, r <spanclass="kw">in</span><spanclass="bu">enumerate</span>(RESSOURCEN) <spanclass="cf">for</span> j, p <spanclass="kw">in</span><spanclass="bu">enumerate</span>(PRODUKTE)})</span>
<spanid="cb10-87"><ahref="#cb10-87"aria-hidden="true"tabindex="-1"></a> status <spanclass="op">=</span> ergebnis.solver.termination_condition</span>
<spanid="cb10-88"><ahref="#cb10-88"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> status <spanclass="op">!=</span> pyo.TerminationCondition.optimal:</span>
<spanid="cb10-101"><ahref="#cb10-101"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Der entscheidende Unterschied: `(verbrauch * x).sum("produkt") <= vorrat`</span></span>
<spanid="cb10-102"><ahref="#cb10-102"aria-hidden="true"tabindex="-1"></a><spanclass="co"> ist EINE Zeile und erzeugt so viele Nebenbedingungen, wie die Dimension</span></span>
<spanid="cb10-103"><ahref="#cb10-103"aria-hidden="true"tabindex="-1"></a><spanclass="co">'ressource' Eintraege hat. Bei 2 Ressourcen faellt das nicht auf, bei</span></span>
<spanid="cb10-104"><ahref="#cb10-104"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 200 000 schon - dort entstehen sie als Matrixoperation statt in einer</span></span>
<spanid="cb10-109"><ahref="#cb10-109"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Benannte Indizes statt blosser Listen: Dadurch heissen die Achsen</span></span>
<spanid="cb10-110"><ahref="#cb10-110"aria-hidden="true"tabindex="-1"></a><spanclass="co"># 'produkt' und 'ressource', und xarray fuehrt sie beim Rechnen von allein</span></span>
<spanid="cb10-111"><ahref="#cb10-111"aria-hidden="true"tabindex="-1"></a><spanclass="co"># richtig zusammen. Ohne Namen vergibt linopy 'dim_0', und man muss</span></span>
<spanid="cb10-112"><ahref="#cb10-112"aria-hidden="true"tabindex="-1"></a><spanclass="co"># spaeter umbenennen - eine haeufige Stolperstelle.</span></span>
<spanid="cb10-118"><ahref="#cb10-118"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> modell.variables[<spanclass="st">"menge"</span>]</span>
<spanid="cb10-120"><ahref="#cb10-120"aria-hidden="true"tabindex="-1"></a> db <spanclass="op">=</span> xr.DataArray(DECKUNGSBEITRAG, coords<spanclass="op">=</span>[produkt])</span>
<spanid="cb10-124"><ahref="#cb10-124"aria-hidden="true"tabindex="-1"></a><spanclass="co"># EINE Zeile - sie erzeugt so viele Nebenbedingungen, wie die Achse</span></span>
<spanid="cb10-125"><ahref="#cb10-125"aria-hidden="true"tabindex="-1"></a><spanclass="co"># 'ressource' Eintraege hat. Genau das ist der Punkt.</span></span>
<spanid="cb10-160"><ahref="#cb10-160"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"Abweichung von der Handrechnung: </span><spanclass="sc">{</span>ziel<spanclass="sc">}</span><spanclass="ss"> statt </span><spanclass="sc">{</span>ERWARTET<spanclass="sc">}</span><spanclass="ss">"</span></span>
<spanid="cb10-161"><ahref="#cb10-161"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Beide stimmen mit der Handrechnung ueberein (Z* = </span><spanclass="sc">{</span>ERWARTET<spanclass="sc">:.0f}</span><spanclass="ss">)."</span>)</span>
<spanid="cb10-163"><ahref="#cb10-163"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Wann welche Schicht?"</span>)</span>
<spanid="cb10-164"><ahref="#cb10-164"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Pyomo -> wenn Modellstruktur und Daten getrennt bleiben sollen,"</span>)</span>
<spanid="cb10-165"><ahref="#cb10-165"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" wenn nichtlineare Terme oder MINLP dazukommen koennen,"</span>)</span>
<spanid="cb10-166"><ahref="#cb10-166"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" wenn spaeter ein kommerzieller Solver angebunden wird."</span>)</span>
<spanid="cb10-167"><ahref="#cb10-167"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Linopy -> wenn die Daten ohnehin als beschriftete Arrays vorliegen"</span>)</span>
<spanid="cb10-168"><ahref="#cb10-168"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" (Energiesystem-, Netz- und Zeitreihenmodelle) und das"</span>)</span>
<spanid="cb10-169"><ahref="#cb10-169"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Modell zehntausende gleichartige Nebenbedingungen hat."</span>)</span>
<p><strong>⚠️ Lassen Sie sich von den 0,27 s bei Linopy nicht täuschen.</strong> Bei drei Variablen misst man ausschließlich Startkosten; Linopys Stärke liegt naturgemäß dort, wo es viele gleichartige Nebenbedingungen auf einmal erzeugt. Ein Werkzeug an einem Spielzeugmodell zu bewerten ist einer der häufigsten Benchmark-Fehler — <ahref="praxisfallen.html#kap-praxisfallen">Kapitel 22</a> zeigt, wie man es richtig macht.</p>
</blockquote>
<hr/>
<h2id="sec:oekosystem-vektorisierung">3.8 Wo die Zeit wirklich hingeht: vektorisierte Modellgenerierung</h2>
<p>Eine der hartnäckigsten Fehlannahmen in Optimierungsprojekten lautet: <em>„Wenn es zu langsam ist, brauchen wir einen besseren Solver.“</em> Messen Sie erst — oft stimmt das nicht.</p>
<p>Der Grund ist strukturell. Bevor der Solver auch nur eine Iteration rechnet, muss das Modell <strong>aufgebaut</strong> werden: Variablen anlegen, Ausdrücke zusammensetzen, Nebenbedingungen an die C++-Schicht übergeben. Dieser Aufbau läuft in <strong>Python</strong>, der Solver läuft in <strong>C++</strong> — und zwischen beiden liegen leicht zwei Größenordnungen Geschwindigkeit.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Bei jedem Optimierungsproblem gibt es zwei Laufzeiten: die zum <strong>Aufbauen</strong> und die zum <strong>Lösen</strong>. Messen Sie beide getrennt, bevor Sie irgendetwas optimieren. Wer den kleineren Anteil beschleunigt, hat viel Arbeit für wenig Wirkung.</p>
</blockquote>
<h3id="vier-stufen-an-einem-transportproblem">Vier Stufen an einem Transportproblem</h3>
<p>Wir bauen dasselbe Transportproblem (<spanclass="math inline">m</span> Werke, <spanclass="math inline">n</span> Kunden, <spanclass="math inline">m \cdot n</span> Variablen) auf vier Arten auf:</p>
<table>
<colgroup>
<colstyle="width: 33%"/>
<colstyle="width: 33%"/>
<colstyle="width: 33%"/>
</colgroup>
<thead>
<trclass="header">
<th>Stufe</th>
<th>Wie das Modell entsteht</th>
<th>Was daran teuer ist</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td><strong>A</strong></td>
<td>OR-Tools, ein <code>Add()</code> je Nebenbedingung</td>
<td>Je Aufruf entsteht in Python ein Ausdrucksbaum aus <spanclass="math inline">n</span> Termen</td>
</tr>
<trclass="even">
<td><strong>B</strong></td>
<td>Nebenbedingungsmatrix als COO-Tripel, in Python-Schleifen</td>
<td>Kein Ausdrucksbaum mehr — aber die Schleife bleibt Python</td>
</tr>
<trclass="odd">
<td><strong>C</strong></td>
<td>dieselbe Matrix über Kronecker-Produkte (NumPy/SciPy)</td>
<td>nichts: eine Handvoll Array-Operationen</td>
</tr>
<trclass="even">
<td><strong>D</strong></td>
<td>Daten kommen als lange Tabelle, aufbereitet mit <strong>Polars</strong></td>
<td>nichts: ein Spaltenausdruck statt einer Zeilenschleife</td>
</tr>
</tbody>
</table>
<p>Stufe D ist der realistische Fall: Kostenmatrizen liegen in der Praxis selten als <spanclass="math inline">m \times n</span>-Array vor, sondern als lange Tabelle <code>(werk, kunde, kosten)</code> aus Datenbank oder Data Lake. Polars berechnet daraus die Spaltenindizes der dünnbesetzten Matrix in einem einzigen Ausdruck.</p>
<spanid="cb12-5"><ahref="#cb12-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel Oekosystem: Warum der Solver oft gar nicht der Engpass ist.</span></span>
<spanid="cb12-7"><ahref="#cb12-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">In realen Projekten geht ein grosser Teil der Rechenzeit nicht ins Loesen,</span></span>
<spanid="cb12-8"><ahref="#cb12-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">sondern ins AUFBAUEN des Modells. Dieses Programm misst das an einem</span></span>
<spanid="cb12-9"><ahref="#cb12-9"aria-hidden="true"tabindex="-1"></a><spanclass="co">Transportproblem wachsender Groesse in vier Stufen:</span></span>
<spanid="cb12-11"><ahref="#cb12-11"aria-hidden="true"tabindex="-1"></a><spanclass="co"> A Modellierungsschicht, Nebenbedingung fuer Nebenbedingung (OR-Tools)</span></span>
<spanid="cb12-12"><ahref="#cb12-12"aria-hidden="true"tabindex="-1"></a><spanclass="co"> B Matrix direkt, aber mit Python-Schleifen ueber die Eintraege (COO)</span></span>
<spanid="cb12-13"><ahref="#cb12-13"aria-hidden="true"tabindex="-1"></a><spanclass="co"> C Matrix vektorisiert ueber Kronecker-Produkte (NumPy/SciPy)</span></span>
<spanid="cb12-14"><ahref="#cb12-14"aria-hidden="true"tabindex="-1"></a><spanclass="co"> D Daten kommen als lange Tabelle, aufbereitet mit Polars</span></span>
<spanid="cb12-16"><ahref="#cb12-16"aria-hidden="true"tabindex="-1"></a><spanclass="co">Alle Varianten loesen dasselbe Problem und muessen denselben Zielwert</span></span>
<spanid="cb12-17"><ahref="#cb12-17"aria-hidden="true"tabindex="-1"></a><spanclass="co">liefern - das wird am Ende geprueft.</span></span>
<spanid="cb12-37"><ahref="#cb12-37"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Transportproblem: m Werke, n Kunden.</span></span>
<spanid="cb12-39"><ahref="#cb12-39"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Liefert (kosten[m, n], angebot[m], bedarf[n]). Das Gesamtangebot liegt</span></span>
<spanid="cb12-40"><ahref="#cb12-40"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 20 % ueber dem Gesamtbedarf, damit das Modell sicher loesbar ist.</span></span>
<spanid="cb12-53"><ahref="#cb12-53"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""So schreibt man ein Transportproblem zuerst hin - gut lesbar, nah an</span></span>
<spanid="cb12-54"><ahref="#cb12-54"aria-hidden="true"tabindex="-1"></a><spanclass="co"> der mathematischen Formulierung, jede Nebenbedingung ein eigener Aufruf.</span></span>
<spanid="cb12-56"><ahref="#cb12-56"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Jedes `s.Add(sum(...))` baut in Python einen Ausdrucksbaum aus m bzw. n</span></span>
<spanid="cb12-57"><ahref="#cb12-57"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Termen auf und uebergibt ihn einzeln an die C++-Schicht. Das ist der</span></span>
<spanid="cb12-58"><ahref="#cb12-58"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Preis der Bequemlichkeit - und er waechst linear mit der Modellgroesse.</span></span>
<spanid="cb12-63"><ahref="#cb12-63"aria-hidden="true"tabindex="-1"></a> s <spanclass="op">=</span> pywraplp.Solver.CreateSolver(<spanclass="st">"GLOP"</span>)</span>
<spanid="cb12-64"><ahref="#cb12-64"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="cb12-65"><ahref="#cb12-65"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(m)]</span>
<spanid="cb12-66"><ahref="#cb12-66"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(m):</span>
<spanid="cb12-76"><ahref="#cb12-76"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> status <spanclass="op">!=</span> pywraplp.Solver.OPTIMAL:</span>
<spanid="cb12-81"><ahref="#cb12-81"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Varianten B bis D: Matrix selbst bauen, dann SciPy/HiGHS ---------------</span></span>
<spanid="cb12-88"><ahref="#cb12-88"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Schon deutlich naeher am Blech als Variante A - es entsteht kein</span></span>
<spanid="cb12-89"><ahref="#cb12-89"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Ausdrucksbaum mehr. Die Schleife selbst bleibt aber Python.</span></span>
<spanid="cb12-97"><ahref="#cb12-97"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(m): <spanclass="co"># Angebot je Werk</span></span>
<spanid="cb12-104"><ahref="#cb12-104"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> j <spanclass="kw">in</span><spanclass="bu">range</span>(n): <spanclass="co"># Bedarf je Kunde, als -x <= -bedarf</span></span>
<spanid="cb12-105"><ahref="#cb12-105"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(m):</span>
<spanid="cb12-117"><ahref="#cb12-117"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Dieselbe Matrix ohne eine einzige Schleife - ueber Kronecker-Produkte.</span></span>
<spanid="cb12-119"><ahref="#cb12-119"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Die Angebotsmatrix ist kron(I_m, 1_n^T): je Werk eine Zeile mit Einsen</span></span>
<spanid="cb12-120"><ahref="#cb12-120"aria-hidden="true"tabindex="-1"></a><spanclass="co"> an genau den n Spalten dieses Werks. Die Bedarfsmatrix ist</span></span>
<spanid="cb12-121"><ahref="#cb12-121"aria-hidden="true"tabindex="-1"></a><spanclass="co"> kron(1_m^T, I_n). Beide entstehen in je einem Aufruf und sind sofort</span></span>
<spanid="cb12-134"><ahref="#cb12-134"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Der realistische Fall: Die Kosten kommen als LANGE Tabelle</span></span>
<spanid="cb12-135"><ahref="#cb12-135"aria-hidden="true"tabindex="-1"></a><spanclass="co"> (werk, kunde, kosten) aus Datenbank, Data Lake oder CSV-Datei.</span></span>
<spanid="cb12-137"><ahref="#cb12-137"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Polars berechnet den Spaltenindex jeder Variablen in einem einzigen</span></span>
<spanid="cb12-138"><ahref="#cb12-138"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Spaltenausdruck - ohne Python-Schleife ueber die Zeilen. Genau so baut man</span></span>
<spanid="cb12-139"><ahref="#cb12-139"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Modelle aus Millionen Tabellenzeilen.</span></span>
<spanid="cb12-153"><ahref="#cb12-153"aria-hidden="true"tabindex="-1"></a> werk <spanclass="op">=</span> tabelle[<spanclass="st">"werk"</span>].to_numpy()</span>
<spanid="cb12-154"><ahref="#cb12-154"aria-hidden="true"tabindex="-1"></a> kunde <spanclass="op">=</span> tabelle[<spanclass="st">"kunde"</span>].to_numpy()</span>
<spanid="cb12-155"><ahref="#cb12-155"aria-hidden="true"tabindex="-1"></a> eins <spanclass="op">=</span> np.ones(var_index.size)</span>
<spanid="cb12-167"><ahref="#cb12-167"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Liefert (Aufbauzeit, Loesezeit, Zielwert) fuer die Varianten B bis D."""</span></span>
<spanid="cb12-183"><ahref="#cb12-183"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" MODELLAUFBAU: WO DIE ZEIT WIRKLICH HINGEHT"</span>)</span>
<spanid="cb12-185"><ahref="#cb12-185"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Transportproblem mit m Werken und n Kunden -> m*n Variablen.</span><spanclass="ch">\n</span><spanclass="st">"</span>)</span>
<spanid="cb12-195"><ahref="#cb12-195"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Hinweis: polars nicht installiert - Variante D wird uebersprungen.</span><spanclass="ch">\n</span><spanclass="st">"</span>)</span>
<spanid="cb12-197"><ahref="#cb12-197"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Aufwaermlauf: Der erste Aufruf bezahlt Importe und einmalige</span></span>
<spanid="cb12-198"><ahref="#cb12-198"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Initialisierungen. Wer den mitmisst, vergleicht Startkosten statt</span></span>
<spanid="cb12-199"><ahref="#cb12-199"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Rechenarbeit - ein klassischer Benchmark-Fehler.</span></span>
<spanid="cb12-209"><ahref="#cb12-209"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> m, n <spanclass="kw">in</span> [(<spanclass="dv">40</span>, <spanclass="dv">40</span>), (<spanclass="dv">120</span>, <spanclass="dv">120</span>), (<spanclass="dv">250</span>, <spanclass="dv">250</span>)]:</span>
<spanid="cb12-225"><ahref="#cb12-225"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Alle Varianten muessen dasselbe Problem beschreiben.</span></span>
<spanid="cb12-233"><ahref="#cb12-233"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st">Zwei Lehren aus der Tabelle:"</span>)</span>
<spanid="cb12-234"><ahref="#cb12-234"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"1. Bei Variante A geht mehr Zeit in den AUFBAU als ins Loesen. Wer hier"</span>)</span>
<spanid="cb12-235"><ahref="#cb12-235"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" einen schnelleren Solver kauft, beschleunigt den kleineren Teil."</span>)</span>
<spanid="cb12-236"><ahref="#cb12-236"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"2. Zwischen B und C liegt keine andere Mathematik, nur eine andere"</span>)</span>
<spanid="cb12-237"><ahref="#cb12-237"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Schreibweise derselben Matrix - Schleife gegen Kronecker-Produkt."</span>)</span>
<spanid="cb12-238"><ahref="#cb12-238"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st">Die Lesbarkeit von Variante A ist trotzdem viel wert: Fangen Sie dort an,"</span>)</span>
<spanid="cb12-239"><ahref="#cb12-239"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"und vektorisieren Sie erst, wenn die Messung es verlangt."</span>)</span>
<p><strong>Lesen Sie die letzte Spalte.</strong> Bei 62 500 Variablen verbringt Variante A <strong>69 % der Gesamtzeit damit, das Modell überhaupt aufzuschreiben</strong> — und nur 31 % mit Rechnen. Die vektorisierten Varianten kehren das Verhältnis um: 1 % Aufbau, 99 % Solver. Erst dort ist ein schnellerer Solver überhaupt die richtige Stellschraube.</p>
<imgsrc="bilder_04/kap_oekosystem_laufzeit.svg"alt="Abb. 3.2: Dieselbe Aufgabe, zwei Arten sie aufzuschreiben — aufgeteilt in Aufbauzeit (Python) und Lösezeit (C++). Werte aus der Messung oben. Erzeugt von bilder_04/erzeuge_laufzeit_aufbau.py."/>
<figcaptionaria-hidden="true">Abb. 3.2: Dieselbe Aufgabe, zwei Arten sie aufzuschreiben — aufgeteilt in Aufbauzeit (Python) und Lösezeit (C++). Werte aus der Messung oben. Erzeugt von <code>bilder_04/erzeuge_laufzeit_aufbau.py</code>.</figcaption>
</figure>
<p><strong>Was Sie in der Abbildung sehen.</strong> Der rote Anteil ist die Zeit, in der der Solver noch gar nicht rechnet. Bei Variante A wächst er mit dem Modell, bei Variante C verschwindet er — dieselbe Mathematik, nur anders aufgeschrieben.</p>
<td>jede Variante einmal mit 10×10 laufen lassen</td>
<td>Der erste Aufruf bezahlt Importe und einmalige Initialisierungen. Wer den mitmisst, vergleicht Startkosten statt Rechenarbeit — der häufigste Benchmark-Fehler überhaupt.</td>
<td>erzeugt die Angebotsmatrix in einem Aufruf</td>
<td>Kronecker-Produkte sind das Werkzeug für „jede Gruppe bekommt eine Zeile“. Ein zweiter Blick lohnt: Genau dieses Muster deckt Zuordnungs-, Transport- und Schichtmodelle ab.</td>
</tr>
<trclass="odd">
<td>Variante B als COO-Tripel</td>
<td>nicht als dichte Matrix</td>
<td>Wäre B eine dichte Matrix, würde sie bei 62 500 Variablen 250 MB belegen — der Vergleich wüde dann Speicher statt Schleifen messen. Fairness im Benchmark heißt: nur eine Sache verändern.</td>
<td>Ohne diese Zeile misst man womöglich die Laufzeit von vier <em>verschiedenen</em> Modellen. Ein Benchmark ohne Korrektheitsprüfung ist wertlos.</td>
<td>Absolute Zeiten hängen von der Hardware ab, das Verhältnis nicht. Es ist die Zahl, die die Entscheidung trägt.</td>
</tr>
</tbody>
</table>
</blockquote>
<blockquote>
<p><strong>🎯 Merksatz</strong> Fangen Sie mit der lesbarsten Variante an. Vektorisieren Sie erst, wenn Sie <strong>gemessen</strong> haben, dass der Aufbau der Engpass ist — und messen Sie Aufbau und Lösen immer getrennt. Lesbarer Code, der schnell genug ist, schlägt schnellen Code, den niemand mehr versteht.</p>
<p><strong>Aufgabe 3.1 ⭐ — Solverwahl begründen.</strong> Welche Bibliothek würden Sie wählen? Begründen Sie mit den drei Fragen aus <ahref="#sec:oekosystem-die-werkzeuge-im-vergleich">Abschnitt 3.4</a>: (a) Zuteilung von 300 Prüfungen auf 40 Räume und 12 Zeitfenster. (b) Mischungsproblem: günstigstes Tierfutter aus 8 Rohstoffen, Nährwertgrenzen. (c) Portfolio aus 200 Aktien, Risiko minimieren bei Mindestrendite. (d) Wie (c), aber höchstens 15 Titel im Depot. (e) Standortwahl: Welche 5 von 40 möglichen Lagern eröffnen? (f) Kalibrierung eines Modells mit 4 Parametern an Messdaten (Fehlerquadratsumme, nicht konvex).</p>
<p><strong>Aufgabe 3.2 ⭐⭐ — Modell übersetzen.</strong> Formulieren Sie das Bäckerei-Problem aus <ahref="einfuehrung.html#kap-einfuehrung">Kapitel 1</a> in <strong>allen vier</strong> Bibliotheken aus <ahref="#sec:oekosystem-ein-system-vier-programmieransaetze">Abschnitt 3.5</a> und prüfen Sie mit <code>assert</code>, dass alle dasselbe Ergebnis liefern.</p>
<p><strong>Aufgabe 3.3 ⭐⭐ — CSR-Format von Hand.</strong> Geben Sie <code>values</code>, <code>indices</code> und <code>starts</code> für folgende Matrix an: <spanclass="math display">\mathbf{A}=\begin{pmatrix} 3 & 0 & 0 & 1 \\ 0 & 0 & 2 & 0 \\ 5 & 4 & 0 & 6\end{pmatrix}</span> Wie viele Zahlen speichert CSR, wie viele die volle Matrix?</p>
<p><strong>Aufgabe 3.4 ⭐⭐ — Konvexitätsprüfung erleben.</strong> Versuchen Sie, in CVXPY das Problem <spanclass="math inline">\min x^3</span> u. d.N. <spanclass="math inline">-2 \le x \le 2</span> zu lösen. Was passiert? Formulieren Sie anschließend <spanclass="math inline">\min x^2</span> mit denselben Schranken. Erklären Sie den Unterschied in eigenen Worten.</p>
<p><strong>Aufgabe 3.5 ⭐⭐⭐ — Laufzeitvergleich.</strong> Erzeugen Sie zufällige LPs wachsender Größe (<spanclass="math inline">n = 10, 50, 100, 500, 1000</span> Variablen, <spanclass="math inline">m = n/2</span> Nebenbedingungen) und messen Sie die Laufzeit von <code>linprog</code>, <code>highspy</code> und <code>cvxpy</code>. Stellen Sie die Ergebnisse in einer Tabelle dar. (a) Ab welcher Größe zahlt sich <code>highspy</code> aus? (b) Wie viel Zeit entfällt bei CVXPY auf den Modellaufbau, wie viel auf das eigentliche Lösen? (Tipp: <code>problem.solver_stats.solve_time</code>.)</p>
<p><strong>Aufgabe 3.6 ⭐⭐⭐ — Eigene Entscheidungshilfe.</strong> Erweitern Sie <code>Solver_Wahl.py</code> um zwei weitere Kriterien: „Ist eine kommerzielle Lizenz verfügbar?“ und „Muss das Modell für Nicht-Programmierer lesbar sein?“. Ergänzen Sie passende Empfehlungen.</p>
<hr/>
<h2id="sec:oekosystem-denkfehler">3.10 Finde den Denkfehler</h2>
<p>Ein Team vergleicht zwei Bibliotheken für sein Zuordnungsproblem und schreibt ins Protokoll: <em>„CVXPY braucht 1,9 s, SciPy nur 0,6 s — wir setzen auf SciPy.“</em> Gemessen wurde so:</p>
<p>Beide finden dieselbe Lösung. Die Zeiten stimmen — auf diesem Rechner reproduzierbar. Trotzdem trägt die Entscheidung nicht.</p>
<p><strong>Ihre Aufgabe:</strong> (a) Nennen Sie <strong>drei</strong> Dinge, die diese Messung mitmisst, obwohl sie nichts mit der Lösegeschwindigkeit zu tun haben. (b) Warum ist gerade bei CVXPY der gemessene Wert besonders irreführend, wenn das Modell später in einer Schleife 60-mal mit wechselnden Daten gelöst wird? (c) Wie sähe eine Messung aus, die die Frage des Teams tatsächlich beantwortet?</p>
<p><strong>❓ Micro-Quiz 3: Drei Fragen zum Selbstcheck</strong></p>
<p>Genau eine Antwort ist jeweils richtig. Auflösung in <ahref="anhang-loesungen.html#quiz-loesung-oekosystem">Anhang A</a>.</p>
<p><strong>1. Sie sollen 300 Prüfungen auf 40 Räume und 12 Zeitfenster verteilen, mit Regeln wie „diese beiden Klausuren nicht gleichzeitig“. Welches Werkzeug passt?</strong> (a) CVXPY — es prüft die Konvexität automatisch. (b) OR-Tools CP-SAT — diskrete Zuweisung mit logischen Regeln ist genau sein Gebiet. (c) <code>scipy.optimize.minimize</code> — es kann beliebige Zielfunktionen.</p>
<p><strong>2. Ihr Modell mit 80 000 Variablen braucht 40 Sekunden, davon 32 Sekunden bis zum ersten Solver-Aufruf. Was ist die wirksamste Maßnahme?</strong> (a) Einen kommerziellen Solver lizenzieren. (b) Das Zeitlimit des Solvers heraufsetzen. (c) Den Modellaufbau vektorisieren — der Solver ist mit 8 Sekunden gar nicht der Engpass.</p>
<p><strong>3. Was haben <code>scipy.optimize.linprog</code>, <code>highspy</code>, CVXPY (mit Standardeinstellung) und Pyomo (mit <code>appsi_highs</code>) gemeinsam?</strong> (a) Sie implementieren jeweils einen eigenen Simplex-Algorithmus in Python. (b) Sie sind Modellierungsschichten über demselben C++-Solver HiGHS — die Rechenleistung ist dieselbe, nur die Schreibweise unterscheidet sich. (c) Sie können alle sowohl konvexe als auch nicht-konvexe Probleme global lösen.</p>
<li>Welche zwei Schichten hat eine moderne OR-Architektur, und wozu dient die Trennung?</li>
<li>Warum wirft CVXPY bei <spanclass="math inline">\min x^3</span> einen Fehler, <code>scipy.optimize.minimize</code> aber nicht?</li>
<li>Was speichert das CSR-Format, und warum ist es bei großen Modellen entscheidend?</li>
<li>Warum ist CVXPY im Laufzeitvergleich (<ahref="#sec:oekosystem-ein-system-vier-programmieransaetze">Abschnitt 3.5</a>) das langsamste Werkzeug — und warum ist das trotzdem kein Argument gegen seinen Einsatz?</li>
<li>Nennen Sie zwei Situationen, in denen CP-SAT die falsche Wahl wäre.</li>
<li><strong>Zwei Schichten:</strong> Sie formulieren in Python, ein C++-Solver rechnet. Beides ist austauschbar.</li>
<li><strong>Drei Fragen</strong> genügen zur Solverwahl: diskrete Entscheidungen? lineare Zielfunktion? Routing?</li>
<li><strong>Dasselbe Modell, vier Wege, ein Ergebnis</strong> — nutzen Sie das als Prüfmittel: Wenn zwei Bibliotheken verschiedene Optima melden, ist eine Ihrer Formulierungen falsch.</li>
<li><strong>CVXPYs Konvexitätsprüfung ist ein Schutzmechanismus</strong>, keine Schikane.</li>
<li><strong>Sechs Modellierungsschichten, eine Handvoll Solver.</strong> Pyomo (Mengen und Indizes) und Linopy (beschriftete Arrays) ergänzen die vier Grundwerkzeuge, sobald Modelle industrielle Größe erreichen — sie rechnen aber alle mit denselben C++-Engines.</li>
<li><strong>Messen Sie Aufbau- und Lösezeit getrennt.</strong> Bei 62 500 Variablen entfallen in der bequemen Schreibweise rund zwei Drittel der Zeit auf den Modellaufbau. Vektorisierung mit NumPy oder Polars dreht das Verhältnis um — ein schnellerer Solver hätte es nicht getan.</li>
<li><strong>Rechnen Sie das Ergebnis wenn möglich von Hand nach</strong> — bei kleinen Instanzen ist das in Minuten erledigt und deckt Modellfehler auf, die kein Solver melden kann.</li>
</ul>
<p><strong>Ausblick.</strong><ahref="lp.html#teil-kernverfahren">Teil II</a> beginnt mit dem Kernverfahren des Operations Research: der linearen Programmierung. Wir bauen den Simplex-Algorithmus selbst, um zu verstehen, woher Schattenpreise kommen — und was sie wirtschaftlich bedeuten.</p>