<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"class="aktiv"><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
<navclass="breadcrumb"aria-label="Breadcrumb"><ahref="index.html">Start</a>›<span>Teil I</span>›<span>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</span></nav>
<navclass="prev-next"><aclass="prev-next-knopf prev-next-prev"href="einfuehrung.html"><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></a><aclass="prev-next-knopf prev-next-next"href="oekosystem.html"><span><small>Weiter</small>Kapitel 3: Das Python-Ökosystem für OR — Solver, Bindings und Modellierungsschichten</span><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-right"></use></svg></a></nav>
<article>
<h1id="kap-fundament">Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</h1>
<divclass="card card-blick">
<blockquote>
<p><strong>📌 Kapitel auf einen Blick</strong></p>
<p><strong>Worum geht es?</strong> Um die Sprache, in der Optimierungsmodelle aufgeschrieben werden, und um die eine Eigenschaft, die darüber entscheidet, ob ein Problem verlässlich lösbar ist: Konvexität.</p>
<p><strong>Voraussetzungen:</strong><ahref="einfuehrung.html#kap-einfuehrung">Kapitel 1</a>. Lineare Algebra wird hier von Grund auf wiederholt.</p>
<p><strong>Danach können Sie:</strong> Ein Modell in Matrixform aufschreiben, den zulässigen Bereich geometrisch deuten, beurteilen ob ein Problem konvex ist — und einschätzen, wie viele Stellen Ihres Ergebnisses überhaupt belastbar sind.</p>
<p><strong>Zeitbedarf:</strong> ca. 5 Stunden.</p>
<p><strong>Notebook:</strong><ahref="Notebooks_04/fundament.ipynb">fundament.ipynb</a> — herunterladen und in Jupyter öffnen, in Colab hochladen oder mit dem Kurs-Image starten</p>
<h2id="sec:fundament-schnellstart">2.1 In 5 Minuten gelöst</h2>
<p>Dieses Kapitel handelt von Geometrie. Bevor wir Vektoren und Matrizen einführen, sehen Sie die zentrale Einsicht in Aktion — ganz ohne Solver, mit acht Zeilen NumPy.</p>
<divclass="card card-schnellstart">
<blockquote>
<p><strong>🚀 In 5 Minuten gelöst: Das Optimum sitzt immer in einer Ecke</strong></p>
<p>Ein Betrieb fertigt zwei Produkte. Zwei Ressourcen begrenzen ihn:</p>
<p>Der zulässige Bereich enthält <strong>unendlich viele</strong> Punkte. Trotzdem müssen wir nur eine Handvoll prüfen — nämlich die <strong>Ecken</strong>, an denen sich je zwei Begrenzungslinien schneiden:</p>
<spanid="cb1-12"><ahref="#cb1-12"aria-hidden="true"tabindex="-1"></a><spanclass="cf">continue</span><spanclass="co"># Geraden parallel: keine Ecke</span></span>
<spanid="cb1-14"><ahref="#cb1-14"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> np.<spanclass="bu">all</span>(A <spanclass="op">@</span> ecke <spanclass="op"><=</span> b <spanclass="op">+</span><spanclass="fl">1e-9</span>): <spanclass="co"># liegt die Ecke im Bereich?</span></span>
<spanid="cb1-15"><ahref="#cb1-15"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Ecke (</span><spanclass="sc">{</span>ecke[<spanclass="dv">0</span>]<spanclass="sc">:.1f}</span><spanclass="ss">, </span><spanclass="sc">{</span>ecke[<spanclass="dv">1</span>]<spanclass="sc">:.1f}</span><spanclass="ss">) -> Z = </span><spanclass="sc">{</span>c <spanclass="op">@</span> ecke<spanclass="sc">:6.2f}</span><spanclass="ss">"</span>)</span></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>Ecke (4.0, 4.0) -> Z = 32.00
Ecke (0.0, 8.0) -> Z = 40.00
Ecke (6.0, 0.0) -> Z = 18.00
Ecke (0.0, 0.0) -> Z = 0.00</code></pre>
</blockquote>
</div>
<p><strong>Das Optimum lautet <spanclass="math inline">(0, 8)</span> mit <spanclass="math inline">Z = 40</span></strong> — und Sie haben es gefunden, ohne einen einzigen Solver zu starten. Vier Kandidaten statt unendlich vieler Punkte: Das ist der <strong>Fundamentalsatz der linearen Optimierung</strong>, und er ist der Grund, warum sich lineare Programme überhaupt zuverlässig lösen lassen.</p>
<p><strong>Warum funktioniert das?</strong> Weil der zulässige Bereich ein <strong>konvexes Polyeder</strong> ist — ein Vielflächner ohne Diagonalen nach innen — und weil eine lineare Zielfunktion darauf ihren größten Wert immer am Rand annimmt, genauer: in einer Ecke. Beides begründet dieses Kapitel. Der Simplex-Algorithmus in <ahref="lp.html#kap-lp">Kapitel 5</a> tut im Kern nichts anderes als das Programm oben, nur klüger: Er probiert nicht alle Ecken durch, sondern läuft gezielt von Ecke zu Ecke bergauf.</p>
<blockquote>
<p><strong>⚠️ Und wo ist der Haken?</strong> Bei zwei Variablen gibt es 6 Eckenkandidaten, bei 50 Variablen und 50 Bedingungen sind es <spanclass="math inline">\binom{100}{50} \approx 10^{29}</span>. Der Fundamentalsatz sagt uns <em>wo</em> wir suchen müssen — nicht, dass die Suche leicht wird.</p>
<li>… ein Optimierungsmodell von der „Zeile-für-Zeile“-Schreibweise in die kompakte Matrixform <spanclass="math inline">\max \mathbf{c}^\top\mathbf{x}</span> u. d.N. <spanclass="math inline">\mathbf{A}\mathbf{x} \le \mathbf{b}</span> übersetzen — und zurück.</li>
<li>… erklären, warum der zulässige Bereich eines linearen Programms ein <strong>Polyeder</strong> ist.</li>
<li>… den Fundamentalsatz der linearen Optimierung anwenden, um Kandidaten für das Optimum zu finden, ohne alles durchzuprobieren.</li>
<li>… prüfen, ob eine Menge bzw. eine Funktion konvex ist, und begründen, warum das für die Lösbarkeit entscheidend ist.</li>
<li>… einen zweidimensionalen Lösungsraum mit <code>matplotlib</code> zeichnen und daraus die optimale Ecke ablesen.</li>
<li>… die <strong>Konditionszahl</strong><spanclass="math inline">\kappa(\mathbf{A})</span> berechnen, ihren Wert deuten und ein schlecht skaliertes Modell mit Ruiz-Equilibrierung wieder rechenbar machen.</li>
<li>… erklären, warum ein Solver-Ergebnis von <code>0.99999998</code> niemals mit <code>int()</code> in eine ganze Zahl verwandelt werden darf.</li>
</ol>
<hr/>
<h2id="sec:fundament-warum-ueberhaupt-vektoren-und-matrizen">2.3 Warum überhaupt Vektoren und Matrizen?</h2>
<p>In <ahref="einfuehrung.html#kap-einfuehrung">Kapitel 1</a> hatten wir zwei Variablen und drei Nebenbedingungen. Das ließ sich bequem ausschreiben. Reale Modelle haben hunderte bis hunderttausende Variablen — dort wäre Ausschreiben nicht nur unpraktisch, sondern unmöglich.</p>
<p>Die lineare Algebra löst dieses Problem, indem sie <strong>viele gleichartige Zahlen zu einem Objekt bündelt</strong>. Das ist derselbe Gedanke wie eine Liste in Python: Statt <code>preis_1</code>, <code>preis_2</code>, …, <code>preis_1000</code> schreibt man <code>preise</code> und arbeitet mit dem Ganzen.</p>
<h3id="die-bausteine">Die Bausteine</h3>
<p>Sei <spanclass="math inline">n</span> die Anzahl der Entscheidungsvariablen und <spanclass="math inline">m</span> die Anzahl der Nebenbedingungen.</p>
<p><strong>Entscheidungsvektor</strong> — was wir festlegen:</p>
<p><strong>📐 Formel-Lesehilfe</strong> * <spanclass="math inline">\mathbf{c}^\top</span> — der Ertragsvektor, „umgekippt“ zu einer Zeile. * <spanclass="math inline">\mathbf{c}^\top \mathbf{x}</span> — <strong>Skalarprodukt</strong>: Multipliziere jedes <spanclass="math inline">c_j</span> mit dem zugehörigen <spanclass="math inline">x_j</span> und addiere alles. Das Ergebnis ist <strong>eine einzige Zahl</strong>. * <spanclass="math inline">\sum_{j=1}^n</span> — „addiere für <spanclass="math inline">j = 1</span> bis <spanclass="math inline">j = n</span>“.</p>
<p><strong>Ohne Formel gesagt:</strong> „Nimm von jedem Produkt die hergestellte Menge mal den Gewinn pro Stück und zähle alles zusammen.“ Im Bot-Beispiel: <spanclass="math inline">\mathbf{c} = (150, 250)^\top</span>, <spanclass="math inline">\mathbf{x} = (7, 5)^\top</span>, also <spanclass="math inline">\mathbf{c}^\top\mathbf{x} = 150 \cdot 7 + 250 \cdot 5 = 2300</span>.</p>
</blockquote>
<p><strong>Technologiematrix</strong> — wer verbraucht wie viel wovon:</p>
<p><spanclass="math display">
\mathbf{A} = \begin{pmatrix}
a_{11} & a_{12} & \cdots & a_{1n} \\
a_{21} & a_{22} & \cdots & a_{2n} \\
\vdots & \vdots & \ddots & \vdots \\
a_{m1} & a_{m2} & \cdots & a_{mn}
\end{pmatrix} \in \mathbb{R}^{m \times n}
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong><strong>Jede Zeile ist eine Ressource, jede Spalte eine Variable.</strong> Der Eintrag <spanclass="math inline">a_{ij}</span> beantwortet: „Wie viel von Ressource <spanclass="math inline">i</span> verbraucht eine Einheit von Variable <spanclass="math inline">j</span>?“</p>
<p>Im Bot-Beispiel: <spanclass="math display">\mathbf{A} = \begin{pmatrix} 2 & 5 \\ 4 & 6 \\ 1 & 0 \end{pmatrix} \begin{matrix} \leftarrow \text{vCPU} \\ \leftarrow \text{RAM} \\ \leftarrow \text{Liquidität} \end{matrix}</span> Die erste Zeile <spanclass="math inline">(2, 5)</span> heißt: Ein A-Bot braucht 2 vCPUs, ein B-Bot braucht 5. Die dritte Zeile <spanclass="math inline">(1, 0)</span> heißt: Die Liquiditätsgrenze zählt nur A-Bots, B-Bots gar nicht.</p>
<p><strong>📐 Formel-Lesehilfe</strong> * <spanclass="math inline">\mathbf{A}\mathbf{x}</span> — <strong>Matrix-Vektor-Produkt</strong>: berechnet auf einen Schlag den Verbrauch <em>aller</em> Ressourcen. Zeile <spanclass="math inline">i</span> des Ergebnisses ist <spanclass="math inline">a_{i1}x_1 + a_{i2}x_2 + \dots + a_{in}x_n</span>. * <spanclass="math inline">\mathbf{A}\mathbf{x} \le \mathbf{b}</span> — die Ungleichung gilt <strong>komponentenweise</strong>: jede einzelne Zeile muss ihre Kapazität einhalten. * <spanclass="math inline">\mathbf{x} \ge \mathbf{0}</span> — alle Variablen sind nichtnegativ.</p>
<p><strong>Ohne Formel gesagt:</strong> „Minimiere die Gesamtkosten, ohne bei irgendeiner Ressource über die Kapazität zu gehen, und ohne negative Mengen zu produzieren.“</p>
</blockquote>
<blockquote>
<p><strong>⚠️ Typische Fehler</strong></p>
<ul>
<li><strong>Maximieren statt minimieren.</strong> Die Standardform <em>minimiert</em>. Eine Maximierung wird durch Vorzeichenwechsel überführt: <spanclass="math inline">\max\ \mathbf{c}^\top\mathbf{x}</span> ist dasselbe wie <spanclass="math inline">-\min\ (-\mathbf{c})^\top\mathbf{x}</span>. Wer das vergisst, erhält systematisch die <em>schlechteste</em> statt der besten Lösung — siehe <ahref="lp.html#sec:lp-die-vorzeichenfalle-bei-schattenpreisen">Abschnitt 5.7</a>.</li>
<li><strong>„<spanclass="math inline">\ge</span>“-Bedingungen direkt einsetzen.</strong> Die Standardform kennt nur „<spanclass="math inline">\le</span>“. Aus <spanclass="math inline">3x_1 + 2x_2 \ge 12</span> wird durch Multiplikation mit <spanclass="math inline">-1</span>: <spanclass="math inline">-3x_1 - 2x_2 \le -12</span>. <strong>Beim Multiplizieren mit einer negativen Zahl dreht sich das Ungleichheitszeichen um</strong> — der häufigste Vorzeichenfehler überhaupt.</li>
<li><strong>Gleichungen vergessen.</strong><spanclass="math inline">h(\mathbf{x}) = b</span> lässt sich als zwei Ungleichungen schreiben: <spanclass="math inline">h(\mathbf{x}) \le b</span><strong>und</strong><spanclass="math inline">-h(\mathbf{x}) \le -b</span>. Die meisten Solver nehmen Gleichungen aber direkt entgegen — man muss es nicht von Hand machen.</li>
</ul>
</blockquote>
<blockquote>
<p><strong>✏️ Handrechnung 2.1: Modell in Matrixform übersetzen</strong></p>
<p>Schritt 1 — Vektoren und Matrix ablesen: <spanclass="math display">\mathbf{c} = \begin{pmatrix}3\\5\end{pmatrix},\quad \mathbf{A} = \begin{pmatrix}1 & 0\\ 0 & 2\\ 3 & 2\end{pmatrix},\quad \mathbf{b} = \begin{pmatrix}4\\12\\18\end{pmatrix}</span></p>
<p>Achten Sie auf die <strong>Nullen</strong>: Die erste Bedingung enthält <spanclass="math inline">x_2</span> gar nicht, also steht dort eine 0. Das Weglassen der Nullen ist der häufigste Anfängerfehler beim Aufstellen von <spanclass="math inline">\mathbf{A}</span>.</p>
<p>Schritt 2 — Für die Standardform (Minimierung) negieren: <spanclass="math inline">\tilde{\mathbf{c}} = (-3, -5)^\top</span>. Der optimale Zielwert der Minimierung ist dann das Negative des gesuchten Maximums.</p>
<spanid="cb3-5"><ahref="#cb3-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel Fundament: Von der ausgeschriebenen Form zur Matrixform - und zurück.</span></span>
<spanid="cb3-6"><ahref="#cb3-6"aria-hidden="true"tabindex="-1"></a><spanclass="co">Zeigt, dass beide Schreibweisen dasselbe Modell beschreiben.</span></span>
<spanid="cb3-12"><ahref="#cb3-12"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Modell in Matrixform -------------------------------------------------</span></span>
<spanid="cb3-13"><ahref="#cb3-13"aria-hidden="true"tabindex="-1"></a><spanclass="co"># max 3*x1 + 5*x2 u.d.N. x1 <= 4, 2*x2 <= 12, 3*x1 + 2*x2 <= 18, x >= 0</span></span>
<spanid="cb3-15"><ahref="#cb3-15"aria-hidden="true"tabindex="-1"></a>A <spanclass="op">=</span> np.array([[<spanclass="fl">1.0</span>, <spanclass="fl">0.0</span>], <spanclass="co"># Zeile 1: nur x1 kommt vor</span></span>
<spanid="cb3-16"><ahref="#cb3-16"aria-hidden="true"tabindex="-1"></a> [<spanclass="fl">0.0</span>, <spanclass="fl">2.0</span>], <spanclass="co"># Zeile 2: nur x2 kommt vor</span></span>
<spanid="cb3-21"><ahref="#cb3-21"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Ausgeschriebene Form maschinell erzeugen ------------------------------</span></span>
<spanid="cb3-22"><ahref="#cb3-22"aria-hidden="true"tabindex="-1"></a><spanclass="kw">def</span> zeige_ausgeschrieben(c, A, b, namen):</span>
<spanid="cb3-23"><ahref="#cb3-23"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Druckt die Matrixform als lesbares Ungleichungssystem."""</span></span>
<spanid="cb3-24"><ahref="#cb3-24"aria-hidden="true"tabindex="-1"></a> terme <spanclass="op">=</span><spanclass="st">" + "</span>.join(<spanclass="ss">f"</span><spanclass="sc">{</span>c[j]<spanclass="sc">:g}</span><spanclass="ss">*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="bu">len</span>(c)))</span>
<spanid="cb3-27"><ahref="#cb3-27"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(A.shape[<spanclass="dv">0</span>]):</span>
<spanid="cb3-35"><ahref="#cb3-35"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Zulässigkeit eines Punktes prüfen ------------------------------------</span></span>
<spanid="cb3-36"><ahref="#cb3-36"aria-hidden="true"tabindex="-1"></a><spanclass="kw">def</span> ist_zulaessig(x, A, b, toleranz<spanclass="op">=</span><spanclass="fl">1e-9</span>):</span>
<spanid="cb3-37"><ahref="#cb3-37"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Prüft A x <= b und x >= 0 komponentenweise."""</span></span>
<spanid="cb3-38"><ahref="#cb3-38"aria-hidden="true"tabindex="-1"></a> verbrauch <spanclass="op">=</span> A <spanclass="op">@</span> x <spanclass="co"># Matrix-Vektor-Produkt: alle Zeilen auf einmal</span></span>
<spanid="cb3-39"><ahref="#cb3-39"aria-hidden="true"tabindex="-1"></a><spanclass="cf">return</span><spanclass="bu">bool</span>(np.<spanclass="bu">all</span>(verbrauch <spanclass="op"><=</span> b <spanclass="op">+</span> toleranz) <spanclass="kw">and</span> np.<spanclass="bu">all</span>(x <spanclass="op">>=</span><spanclass="op">-</span>toleranz))</span>
<spanid="cb3-42"><ahref="#cb3-42"aria-hidden="true"tabindex="-1"></a> zulaessig <spanclass="op">=</span> ist_zulaessig(kandidat, A, b)</span>
<spanid="cb3-43"><ahref="#cb3-43"aria-hidden="true"tabindex="-1"></a> zielwert <spanclass="op">=</span> c <spanclass="op">@</span> kandidat</span>
<spanid="cb3-44"><ahref="#cb3-44"aria-hidden="true"tabindex="-1"></a> verbrauch <spanclass="op">=</span> A <spanclass="op">@</span> kandidat</span>
<spanid="cb3-45"><ahref="#cb3-45"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="ch">\n</span><spanclass="ss">x = </span><spanclass="sc">{</span>kandidat<spanclass="sc">}</span><spanclass="ss"> -> A x = </span><spanclass="sc">{</span>verbrauch<spanclass="sc">}</span><spanclass="ss">"</span></span>
<spanid="cb3-46"><ahref="#cb3-46"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"</span><spanclass="sc">{</span><spanclass="st">'zulaessig'</span><spanclass="cf">if</span> zulaessig <spanclass="cf">else</span><spanclass="st">'UNZULAESSIG'</span><spanclass="sc">}</span><spanclass="ss">, Z = </span><spanclass="sc">{</span>zielwert<spanclass="sc">:g}</span><spanclass="ss">"</span>)</span>
<spanid="cb3-48"><ahref="#cb3-48"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Lösen: linprog minimiert, also c negieren -----------------------------</span></span>
<spanid="cb3-53"><ahref="#cb3-53"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Hinweis: linprog minimiert, deshalb wurde c negiert und das"</span>)</span>
<spanid="cb3-54"><ahref="#cb3-54"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Ergebnis am Ende wieder mit -1 multipliziert."</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>max 3*x1 + 5*x2
u.d.N.
1*x1 <= 4 (Rohstoff A)
2*x2 <= 12 (Rohstoff B)
3*x1 + 2*x2 <= 18 (Maschinenzeit)
x1, ..., x2 >= 0
x = [2. 6.] -> A x = [ 2. 12. 18.] zulaessig, Z = 36
x = [4. 3.] -> A x = [ 4. 6. 18.] zulaessig, Z = 27
x = [4. 6.] -> A x = [ 4. 12. 24.] UNZULAESSIG, Z = 42
Hinweis: linprog minimiert, deshalb wurde c negiert und das
Ergebnis am Ende wieder mit -1 multipliziert.</code></pre>
<p>Der dritte Kandidat <spanclass="math inline">(4, 6)</span> zeigt die Falle: Er hätte mit <spanclass="math inline">Z = 42</span> den höchsten Zielwert — ist aber <strong>unzulässig</strong>, weil die Maschinenzeit mit 24 über der Kapazität von 18 liegt. Ein hoher Zielwert allein bedeutet nichts.</p>
<hr/>
<h2id="sec:fundament-der-zulaessige-loesungsraum-und-das-polyeder">2.4 Der zulässige Lösungsraum und das Polyeder</h2>
<p>Jede lineare Ungleichung <spanclass="math inline">a_{i1}x_1 + a_{i2}x_2 \le b_i</span> definiert geometrisch eine <strong>Halbebene</strong> (im <spanclass="math inline">\mathbb{R}^2</span>) bzw. einen <strong>Halbraum</strong> (im <spanclass="math inline">\mathbb{R}^n</span>): Die zugehörige Gleichung ist eine Gerade (bzw. Hyperebene), und die Ungleichung wählt eine der beiden Seiten aus.</p>
<p>Der Schnitt aller Halbräume bildet den zulässigen Bereich <spanclass="math inline">\mathcal{F}</span>:</p>
<p><strong>📐 Formel-Lesehilfe</strong> Die geschweiften Klammern beschreiben eine <strong>Menge</strong>. Der senkrechte Strich <spanclass="math inline">\mid</span> heißt „für die gilt“.</p>
<p><strong>Ohne Formel gesagt:</strong> „<spanclass="math inline">\mathcal{F}</span> ist die Menge aller Punkte, die gleichzeitig alle Kapazitätsgrenzen einhalten und nicht negativ sind.“ Anschaulich: der Bereich, in dem man überhaupt landen darf.</p>
</blockquote>
<p>Geometrisch ist <spanclass="math inline">\mathcal{F}</span> ein <strong>konvexes Polyeder</strong> — ein von ebenen Flächen begrenzter Körper ohne Diese-Eindellungen. Im Zweidimensionalen ein Vieleck, im Dreidimensionalen ein Körper wie ein geschliffener Diamant.</p>
<figure>
<imgsrc="bilder_04/kap_fundament_polyeder.svg"alt="Abb. 2.1: Der zulässige Bereich des Bot-Allokationsproblems aus Kapitel 1, das dieses Kapitel weiterrechnet. Fünf Ecken, jede mit ihrem Zielwert — der Fundamentalsatz sagt, dass das Optimum unter ihnen sein muss, und hier ist es (7,5; 5) mit 2 375 €. Die Koordinaten sind nicht eingetragen, sondern als zulässige Schnittpunkte der Begrenzungsgeraden berechnet; erzeugt von bilder_04/erzeuge_polyeder.py."/>
<figcaptionaria-hidden="true">Abb. 2.1: Der zulässige Bereich des Bot-Allokationsproblems aus <ahref="einfuehrung.html#kap-einfuehrung">Kapitel 1</a>, das dieses Kapitel weiterrechnet. Fünf Ecken, jede mit ihrem Zielwert — der Fundamentalsatz sagt, dass das Optimum unter ihnen sein muss, und hier ist es (7,5; 5) mit 2 375 €. Die Koordinaten sind nicht eingetragen, sondern als zulässige Schnittpunkte der Begrenzungsgeraden berechnet; erzeugt von <code>bilder_04/erzeuge_polyeder.py</code>.</figcaption>
</figure>
<h3id="der-fundamentalsatz-der-linearen-optimierung">Der Fundamentalsatz der linearen Optimierung</h3>
<blockquote>
<p><strong>Satz.</strong> Besitzt ein lineares Optimierungsproblem eine optimale Lösung, dann liegt mindestens ein optimaler Punkt auf einer <strong>Ecke (Extrempunkt)</strong> des Polyeders <spanclass="math inline">\mathcal{F}</span>.</p>
</blockquote>
<p><strong>Warum das gilt — die Anschauung:</strong> Die Zielfunktion <spanclass="math inline">\mathbf{c}^\top\mathbf{x}</span> hat überall dieselbe Steigungsrichtung; ihre Höhenlinien sind parallele Geraden. Stellen Sie sich vor, Sie schieben eine solche Gerade in Richtung wachsender Zielwerte über das Polyeder. Der letzte Punkt, den sie berührt, bevor sie das Polyeder verlässt, ist eine <strong>Ecke</strong> — oder, wenn die Gerade zufällig parallel zu einer Kante liegt, eine ganze Kante, deren Endpunkte wiederum Ecken sind.</p>
<p><strong>Warum das praktisch so wichtig ist:</strong> Das Polyeder enthält unendlich viele Punkte, aber nur <strong>endlich viele Ecken</strong>. Der Simplex-Algorithmus (<ahref="lp.html#kap-lp">Kapitel 5</a>) muss deshalb nicht unendlich viel absuchen, sondern wandert gezielt von Ecke zu Ecke.</p>
<blockquote>
<p><strong>✏️ Handrechnung 2.2: Optimum über Ecken finden</strong></p>
<p>Beachten Sie: Wir haben <strong>fünf</strong> Punkte geprüft statt unendlich viele. Genau das ist der Gewinn des Fundamentalsatzes. Für <spanclass="math inline">n = 2</span> geht das von Hand; bei <spanclass="math inline">n = 50</span> gibt es zu viele Ecken — dann übernimmt der Simplex-Algorithmus, der nur die <em>verbessernden</em> Ecken besucht.</p>
</blockquote>
<hr/>
<h2id="sec:fundament-konvexitaet-die-grenze-zwischen-leicht-und">2.5 Konvexität: die Grenze zwischen leicht und schwer</h2>
<p>Hier kommt die vielleicht wichtigste Einsicht des ganzen Buches. In der Optimierungstheorie verläuft die Trennlinie zwischen „zuverlässig lösbar“ und „im Allgemeinen hoffnungslos“ <strong>nicht</strong> zwischen linear und nichtlinear — sondern zwischen <strong>konvex</strong> und <strong>nicht-konvex</strong>.</p>
<p>Eine Menge <spanclass="math inline">\mathcal{C} \subseteq \mathbb{R}^n</span> heißt <strong>konvex</strong>, wenn für alle Punkte <spanclass="math inline">\mathbf{x}, \mathbf{y} \in \mathcal{C}</span> und jedes <spanclass="math inline">\theta \in [0,1]</span> gilt:</p>
<p><strong>📐 Formel-Lesehilfe</strong> * <spanclass="math inline">\theta</span> („theta“) ist eine Zahl zwischen 0 und 1. * <spanclass="math inline">\theta\mathbf{x} + (1-\theta)\mathbf{y}</span> durchläuft für <spanclass="math inline">\theta</span> von 1 bis 0 genau die <strong>Verbindungsstrecke</strong> von <spanclass="math inline">\mathbf{x}</span> nach <spanclass="math inline">\mathbf{y}</span>. Bei <spanclass="math inline">\theta=1</span> sind wir in <spanclass="math inline">\mathbf{x}</span>, bei <spanclass="math inline">\theta=0</span> in <spanclass="math inline">\mathbf{y}</span>, bei <spanclass="math inline">\theta=0{,}5</span> genau in der Mitte.</p>
<p><strong>Ohne Formel gesagt:</strong> Eine Menge ist konvex, wenn man zwischen zwei beliebigen ihrer Punkte eine gerade Linie ziehen kann, ohne die Menge zu verlassen. Ein Kreis, ein Quadrat und jedes Polyeder sind konvex. Ein Halbmond, ein Ring und ein Stern sind es nicht.</p>
</blockquote>
<h3id="konvexe-funktion">Konvexe Funktion</h3>
<p>Eine Funktion <spanclass="math inline">f: \mathcal{C} \to \mathbb{R}</span> heißt <strong>konvex</strong>, wenn für alle <spanclass="math inline">\mathbf{x}, \mathbf{y} \in \mathcal{C}</span> und <spanclass="math inline">\theta \in [0,1]</span> gilt:</p>
<p><strong>📐 Formel-Lesehilfe</strong> * Links: der Funktionswert <strong>auf</strong> der Verbindungsstrecke. * Rechts: der entsprechende Wert <strong>auf der Sehne</strong> zwischen den beiden Funktionswerten.</p>
<p><strong>Ohne Formel gesagt:</strong> Der Funktionsgraph liegt zwischen je zwei Punkten immer <strong>unterhalb</strong> der geraden Verbindungslinie. Eine konvexe Funktion ist „nach oben offen geschüsselt“ — wie eine Parabel <spanclass="math inline">x^2</span> oder eine Suppenschüssel. Kippen Sie eine Kugel hinein, rollt sie zum tiefsten Punkt, und der ist eindeutig.</p>
</blockquote>
<h3id="der-zentrale-satz">Der zentrale Satz</h3>
<blockquote>
<p><strong>Satz.</strong> Bei einem <strong>konvexen Optimierungsproblem</strong> (konvexe Zielfunktion über konvexem zulässigen Bereich) ist jedes <strong>lokale</strong> Minimum automatisch auch das <strong>globale</strong> Minimum.</p>
</blockquote>
<p><strong>Warum das den Unterschied macht:</strong> Ein Algorithmus, der nur lokal sucht — „gehe bergab, bis es nicht mehr bergab geht“ — findet bei konvexen Problemen garantiert das absolut beste Ergebnis. Bei nicht-konvexen Problemen bleibt derselbe Algorithmus im nächstbesten Tal stecken, und niemand kann ihm ansehen, ob es das tiefste war. Man müsste alle Täler prüfen — und deren Zahl wächst wieder kombinatorisch.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Konvex heißt: Wer bergab geht, kommt am tiefsten Punkt an. Nicht-konvex heißt: Wer bergab geht, kommt <em>irgendwo</em> an — und weiß nicht, ob es der tiefste Punkt war.</p>
<imgsrc="bilder_04/kap02_konvexitaet_landschaft.svg"alt="Abb. 2.3: Die Funktion f(x) = x^2 + 3\sin(3x), links eindimensional mit den fünf Startpunkten aus Konvexitaet_Demo.py, rechts als separable Fortsetzung f(x_1,x_2) = f(x_1) + f(x_2) — auf jeder Achse liegt genau die Kurve von links. Erzeugt von bilder_04/erzeuge_3d_konvexitaet.py."/>
<figcaptionaria-hidden="true">Abb. 2.3: Die Funktion <spanclass="math inline">f(x) = x^2 + 3\sin(3x)</span>, links eindimensional mit den fünf Startpunkten aus <code>Konvexitaet_Demo.py</code>, rechts als separable Fortsetzung <spanclass="math inline">f(x_1,x_2) = f(x_1) + f(x_2)</span> — auf jeder Achse liegt genau die Kurve von links. Erzeugt von <code>bilder_04/erzeuge_3d_konvexitaet.py</code>.</figcaption>
</figure>
<p><strong>Was Sie in der Abbildung sehen.</strong> Links sind es fünf Läufe desselben Verfahrens, die in vier verschiedenen Tälern enden — dieselben vier, die Teil 2 von <code>Konvexitaet_Demo.py</code> weiter unten auflistet. In zwei Dimensionen werden aus den Tälern Mulden in einem Gitter, und der Unterschied zwischen Start A und Start B beträgt hier bereits mehr als 24 Einheiten. Keiner der beiden Läufe hat etwas falsch gemacht: Beide enden dort, wo es nicht mehr bergab geht.</p>
<td>Der zulässige Bereich ist ein Punktgitter, keine zusammenhängende Menge — NP-schwer (<ahref="milp.html#kap-milp">Kapitel 6</a>)</td>
</tr>
<trclass="even">
<td>Quadratisches Programm mit indefinitem <spanclass="math inline">\mathbf{P}</span></td>
<td><strong>nein</strong></td>
<td>Nur lokale Optima; eine ungültige Kovarianzmatrix ist ein typischer Auslöser (siehe <ahref="qp-nlp.html#sec:qp-nlp-nichtlineare-optimierung-mit-scipy-optimize-minimize">Abschnitt 11.5</a>)</td>
</tr>
<trclass="odd">
<td>Allgemeines NLP mit beliebigen Funktionen</td>
<td>meist <strong>nein</strong></td>
<td><code>scipy.optimize</code> liefert nur ein lokales Optimum, abhängig vom Startpunkt</td>
<spanid="cb5-6"><ahref="#cb5-6"aria-hidden="true"tabindex="-1"></a><spanclass="co">Teil 1: Die Sehnen-Bedingung numerisch nachprüfen.</span></span>
<spanid="cb5-7"><ahref="#cb5-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Teil 2: Zeigen, warum lokale Suche bei nicht-konvexen Funktionen scheitert.</span></span>
<spanid="cb5-16"><ahref="#cb5-16"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Prüft die Konvexitätsdefinition an zufälligen Punktepaaren:</span></span>
<spanid="cb5-18"><ahref="#cb5-18"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Findet Gegenbeispiele - beweist aber KEINE Konvexität.</span></span>
<spanid="cb5-40"><ahref="#cb5-40"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" TEIL 1: SEHNEN-TEST (positive Zahl = Konvexität verletzt)"</span>)</span>
<spanid="cb5-42"><ahref="#cb5-42"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> name, f <spanclass="kw">in</span> funktionen.items():</span>
<spanid="cb5-45"><ahref="#cb5-45"aria-hidden="true"tabindex="-1"></a><spanclass="cf">else</span><spanclass="ss">f"NICHT konvex (Verletzung bis </span><spanclass="sc">{</span>verletzung<spanclass="sc">:.3f}</span><spanclass="ss">)"</span></span>
<spanid="cb5-48"><ahref="#cb5-48"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Teil 2: Lokale Suche von verschiedenen Startpunkten -------------------</span></span>
<spanid="cb5-50"><ahref="#cb5-50"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Nicht-konvex: eine Parabel mit aufmodulierter Welle -> viele lokale Minima."""</span></span>
<spanid="cb5-54"><ahref="#cb5-54"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" TEIL 2: LOKALE SUCHE BEI NICHT-KONVEXER FUNKTION"</span>)</span>
<spanid="cb5-67"><ahref="#cb5-67"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Je nach Startpunkt landet derselbe Algorithmus in "</span></span>
<spanid="cb5-68"><ahref="#cb5-68"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"</span><spanclass="sc">{</span><spanclass="bu">len</span>({<spanclass="bu">round</span>(e[<spanclass="dv">1</span>], <spanclass="dv">3</span>) <spanclass="cf">for</span> e <spanclass="kw">in</span> ergebnisse})<spanclass="sc">}</span><spanclass="ss"> verschiedenen Minima."</span>)</span>
<spanid="cb5-69"><ahref="#cb5-69"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Das beste gefundene: x = </span><spanclass="sc">{</span>bester[<spanclass="dv">1</span>]<spanclass="sc">:.4f}</span><spanclass="ss"> mit f = </span><spanclass="sc">{</span>bester[<spanclass="dv">2</span>]<spanclass="sc">:.4f}</span><spanclass="ss">"</span></span>
<spanid="cb5-70"><ahref="#cb5-70"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"(Start bei </span><spanclass="sc">{</span>bester[<spanclass="dv">0</span>]<spanclass="sc">:.1f}</span><spanclass="ss">)"</span>)</span>
<spanid="cb5-71"><ahref="#cb5-71"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Bei einer KONVEXEN Funktion waeren alle Zeilen identisch --"</span>)</span>
<td>Ein einziges Gegenbeispiel <strong>widerlegt</strong> Konvexität; kein noch so großer Test <strong>beweist</strong> sie</td>
</tr>
<trclass="even">
<td><code>links - rechts</code></td>
<td>Abstand zur Sehne</td>
<td>Positiv = Graph liegt über der Sehne = Verletzung</td>
</tr>
<trclass="odd">
<td>Teil 2, Schleife über Startpunkte</td>
<td>dieselbe Funktion, fünf Starts</td>
<td>Zeigt die praktische Konsequenz: das Ergebnis hängt vom Zufall des Startpunkts ab</td>
</tr>
<trclass="even">
<td><code>method="BFGS"</code></td>
<td>klassisches lokales Gradientenverfahren</td>
<td>Genau der Verfahrenstyp, den <code>scipy.optimize</code> in <ahref="qp-nlp.html#kap-qp-nlp">Kapitel 11</a> verwendet</td>
</tr>
</tbody>
</table>
<p><strong>Die Lektion in einer Zeile:</strong> Wenn Ihr Optimierungsergebnis vom Startwert abhängt, ist Ihr Problem nicht konvex — und Sie haben keine Garantie, das Beste gefunden zu haben.</p>
</blockquote>
<p>Achten Sie besonders auf die erste Zeile der Tabelle: Vom Startpunkt <spanclass="math inline">-3{,}0</span> aus landet das Verfahren bei <spanclass="math inline">f = 3{,}37</span> — einem Wert, der um mehr als <strong>6 Einheiten schlechter</strong> ist als das beste gefundene Minimum. Der Algorithmus meldet dabei keinen Fehler und keine Warnung. Er hat korrekt gearbeitet, ein lokales Minimum gefunden und ist stehen geblieben. Genau das ist die Gefahr: <strong>Nicht-Konvexität erzeugt keine Fehlermeldungen, sondern stille Fehlentscheidungen.</strong></p>
<blockquote>
<p><strong>⚠️ Typische Fehler</strong></p>
<ul>
<li><strong>Konvexität mit Linearität verwechseln.</strong><spanclass="math inline">f(x) = x^2</span> ist nichtlinear, aber konvex und damit bequem lösbar. <spanclass="math inline">f(x) = x^3</span> ist ebenfalls nichtlinear, aber nicht konvex und deshalb problematisch. Nichtlinear ist kein Urteil, konvex schon.</li>
<li><strong>Konvexität nur der Zielfunktion prüfen.</strong> Auch der zulässige Bereich muss konvex sein. Ganzzahligkeitsbedingungen zerstören genau diese Eigenschaft — deshalb ist MILP schwer, obwohl alles daran linear ist.</li>
<li><strong>Eine Matrix ungeprüft als Kovarianzmatrix verwenden.</strong> Nur wenn alle Eigenwerte <spanclass="math inline">\ge 0</span> sind, ist <spanclass="math inline">\mathbf{x}^\top\mathbf{P}\mathbf{x}</span> konvex. Ein einziger negativer Eigenwert kippt das ganze Problem — mehr dazu in <ahref="qp-nlp.html#sec:qp-nlp-nichtlineare-optimierung-mit-scipy-optimize-minimize">Abschnitt 11.5</a>.</li>
</ul>
</blockquote>
<hr/>
<h2id="sec:fundament-geometrische-visualisierung-des-loesungsraums">2.6 Geometrische Visualisierung des Lösungsraums</h2>
<p>Wir zeichnen nun den Lösungsraum aus <ahref="einfuehrung.html#kap-einfuehrung">Kapitel 1</a> und lesen das Optimum ab. Zweidimensionale Bilder sind der schnellste Weg, Intuition für höhere Dimensionen aufzubauen — auch wenn man sie dort nicht mehr zeichnen kann.</p>
<spanid="cb7-7"><ahref="#cb7-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Ecken werden berechnet, auf Zulässigkeit geprüft, bewertet und eingezeichnet.</span></span>
<spanid="cb7-8"><ahref="#cb7-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">Zusätzlich wird das ganzzahlige Optimum systematisch bestimmt statt behauptet.</span></span>
<spanid="cb7-16"><ahref="#cb7-16"aria-hidden="true"tabindex="-1"></a>matplotlib.use(<spanclass="st">"Agg"</span>) <spanclass="co"># kein Bildschirm nötig</span></span>
<spanid="cb7-22"><ahref="#cb7-22"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Modell (identisch zum Kapitel Einfuehrung) ----------------------------</span></span>
<spanid="cb7-23"><ahref="#cb7-23"aria-hidden="true"tabindex="-1"></a><spanclass="co"># max 150*xA + 250*xB</span></span>
<spanid="cb7-39"><ahref="#cb7-39"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Ecken = Schnittpunkte je zweier Begrenzungsgeraden, die zulässig sind.</span></span>
<spanid="cb7-40"><ahref="#cb7-40"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Begrenzungen sind die 3 Restriktionen plus die beiden Achsen xA=0, xB=0.</span></span>
<spanid="cb7-58"><ahref="#cb7-58"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Vollständige Suche über das kleine Gitter - hier zulässig, weil winzig."""</span></span>
<spanid="cb7-60"><ahref="#cb7-60"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> xa <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="dv">0</span>, <spanclass="dv">21</span>):</span>
<spanid="cb7-62"><ahref="#cb7-62"aria-hidden="true"tabindex="-1"></a> p <spanclass="op">=</span> np.array([<spanclass="bu">float</span>(xa), <spanclass="bu">float</span>(xb)])</span>
<spanid="cb7-63"><ahref="#cb7-63"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> ist_zulaessig(p) <spanclass="kw">and</span> c <spanclass="op">@</span> p <spanclass="op">></span> bester_wert:</span>
<spanid="cb7-64"><ahref="#cb7-64"aria-hidden="true"tabindex="-1"></a> bester_wert, bester_punkt <spanclass="op">=</span> c <spanclass="op">@</span> p, p</span>
<spanid="cb7-118"><ahref="#cb7-118"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Ecken einzeichnen - jetzt werden sie tatsächlich benutzt</span></span>
<spanid="cb7-120"><ahref="#cb7-120"aria-hidden="true"tabindex="-1"></a> edgecolors<spanclass="op">=</span><spanclass="st">"black"</span>, zorder<spanclass="op">=</span><spanclass="dv">4</span>, label<spanclass="op">=</span><spanclass="st">"Ecken des Polyeders"</span>)</span>
<spanid="cb7-121"><ahref="#cb7-121"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> e, w <spanclass="kw">in</span><spanclass="bu">zip</span>(ecken, werte):</span>
<spanid="cb7-134"><ahref="#cb7-134"aria-hidden="true"tabindex="-1"></a>plt.title(<spanclass="st">"Polyeder des zulässigen Bereichs mit Niveaulinien der Zielfunktion"</span>, fontsize<spanclass="op">=</span><spanclass="dv">13</span>)</span>
<spanid="cb7-140"><ahref="#cb7-140"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Visualisierung gespeichert unter '</span><spanclass="sc">{</span>ziel<spanclass="sc">}</span><spanclass="ss">'"</span>)</span></code></pre></div>
<p>Zwei Beobachtungen, die <ahref="milp.html#kap-milp">Kapitel 6</a> vorbereiten:</p>
<oltype="1">
<li>Das LP-Optimum liegt bei <spanclass="math inline">(7{,}5;\ 5{,}0)</span> — <strong>kein zulässiger Betriebszustand</strong>, denn ein halber Bot existiert nicht.</li>
<li>Das ganzzahlige Optimum <spanclass="math inline">(7, 5)</span> ist <strong>nicht</strong> durch Runden entstanden: Aufrunden auf <spanclass="math inline">(8, 5)</span> wäre unzulässig (<spanclass="math inline">2\cdot8+5\cdot5=41>40</span>). Hier ging Abrunden gut — <ahref="milp.html#kap-milp">Kapitel 6</a> zeigt Fälle, in denen Runden dramatisch scheitert.</li>
</ol>
<hr/>
<h2id="sec:fundament-kondition">2.7 Wenn die Zahlen nicht zusammenpassen: Kondition und Skalierung</h2>
<p>Bis hierher war die lineare Algebra exakt. Ein Rechner rechnet aber nicht exakt, sondern mit rund 16 signifikanten Stellen — und Ihre Betriebsdaten sind ohnehin nur auf wenige Stellen genau bekannt. Dieser Abschnitt beantwortet die Frage, die genau daraus entsteht:</p>
<blockquote>
<p><strong>Wenn meine Eingabedaten leicht ungenau sind — wie ungenau ist dann meine Lösung?</strong></p>
<imgsrc="bilder_04/kap_fundament_kondition.svg"alt="Abb. 2.4: Was \kappa(\mathbf{A}) praktisch bedeutet. Links: Über zwölf Größenordnungen hinweg liegt die gemessene größte Fehlerverstärkung genau auf der theoretischen Schranke — \kappa ist keine grobe Faustregel. Rechts: Was die Ruiz-Equilibrierung an einem Modell mit unverträglichen Einheiten ausrichtet. Erzeugt von bilder_04/erzeuge_kondition.py, gerechnet mit derselben Instanz wie Skalierung_Kondition.py."/>
<figcaptionaria-hidden="true">Abb. 2.4: Was <spanclass="math inline">\kappa(\mathbf{A})</span> praktisch bedeutet. Links: Über zwölf Größenordnungen hinweg liegt die gemessene größte Fehlerverstärkung genau auf der theoretischen Schranke — <spanclass="math inline">\kappa</span> ist keine grobe Faustregel. Rechts: Was die Ruiz-Equilibrierung an einem Modell mit unverträglichen Einheiten ausrichtet. Erzeugt von <code>bilder_04/erzeuge_kondition.py</code>, gerechnet mit derselben Instanz wie <code>Skalierung_Kondition.py</code>.</figcaption>
<p>Die Antwort trägt einen Namen: die <strong>Konditionszahl</strong><spanclass="math inline">\kappa(\mathbf{A})</span>. Sie ist der wichtigste Begriff dieses Kapitels für den Produktivbetrieb, weil er erklärt, warum ein mathematisch korrektes Modell trotzdem unbrauchbare Ergebnisse liefern kann — oder sich mit einem sachlich falschen <code>INFEASIBLE</code> verabschiedet.</p>
<h3id="die-geometrische-vorstellung-zwei-fast-parallele-linien">Die geometrische Vorstellung: zwei fast parallele Linien</h3>
<p>Zwei Nebenbedingungen legen im Zweidimensionalen einen Schnittpunkt fest. Stehen die zugehörigen Geraden <strong>kreuzweise</strong> aufeinander, ist der Schnittpunkt robust: Verschiebt man eine Gerade um einen Millimeter, wandert der Schnittpunkt um einen Millimeter.</p>
<p>Verlaufen die Geraden dagegen <strong>fast parallel</strong>, entsteht ein spitzer, langgezogener Keil. Derselbe Millimeter Verschiebung schickt den Schnittpunkt jetzt meterweit den Keil entlang. Genau das misst <spanclass="math inline">\kappa(\mathbf{A})</span>: <strong>den Verstärkungsfaktor zwischen Datenfehler und Lösungsfehler.</strong></p>
<td>Faustregel: Sie verlieren <spanclass="math inline">k</span> signifikante Stellen. Bei <spanclass="math inline">\kappa = 10^{8}</span> bleiben von 16 Stellen noch 8.</td>
</tr>
</tbody>
</table>
<p><strong>In einem Satz:</strong><em><spanclass="math inline">\kappa</span> sagt Ihnen, wie viele Nachkommastellen Ihres Ergebnisses Sie noch glauben dürfen.</em></p>
</blockquote>
</div>
<blockquote>
<p><strong>🎯 Merksatz</strong><spanclass="math inline">\kappa < 10^3</span> ist unbedenklich, <spanclass="math inline">\kappa > 10^6</span> verdient eine Prüfung, <spanclass="math inline">\kappa > 10^{10}</span> heißt: Das Ergebnis ist Rauschen mit Nachkommastellen. Rechnen Sie <spanclass="math inline">\kappa</span> mit <code>np.linalg.cond(A)</code> aus — es kostet eine Zeile und beantwortet die Frage, die sonst monatelang niemand stellt.</p>
</blockquote>
<h3id="woher-schlechte-kondition-im-alltag-kommt">Woher schlechte Kondition im Alltag kommt</h3>
<p>Fast nie aus exotischer Mathematik — fast immer aus <strong>Einheiten</strong>:</p>
<table>
<colgroup>
<colstyle="width: 33%"/>
<colstyle="width: 33%"/>
<colstyle="width: 33%"/>
</colgroup>
<thead>
<trclass="header">
<th>Ursache</th>
<th>Beispiel aus der Praxis</th>
<th>Abhilfe</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td>Gemischte Größenordnungen</td>
<td>Kapitalbindung in Euro (<spanclass="math inline">10^7</span>) neben Ausschussquote als Anteil (<spanclass="math inline">10^{-2}</span>) in derselben Matrix</td>
<td>In Tsd. Euro und Prozent rechnen — oder automatisch skalieren</td>
</tr>
<trclass="even">
<td>Fast redundante Regeln</td>
<td>„Mindestens 30 % Anteil A“ und „höchstens 70 % Anteil B“ bei nur zwei Sorten</td>
<td>Doppelung erkennen und eine Regel streichen</td>
<td>Kleinstmögliches <spanclass="math inline">M</span> aus den Daten herleiten</td>
</tr>
<trclass="even">
<td>Mengen in Stück <em>und</em> Tonnen</td>
<td>Schüttgut und Einzelteile im selben Modell</td>
<td>Eine Einheit je Größe, konsequent</td>
</tr>
</tbody>
</table>
<p>Die dritte Zeile ist der häufigste selbstgemachte Fall: Ein unnötig großes Big-M bläht <spanclass="math inline">\kappa</span> auf und lässt Solver bei völlig harmlosen Modellen stundenlang suchen oder falsche Ergebnisse melden.</p>
<h3id="ruiz-equilibrierung-das-modell-gesundrechnen">Ruiz-Equilibrierung: das Modell gesundrechnen</h3>
<p>Die Gegenmaßnahme ist erstaunlich schlicht. Man multipliziert jede <strong>Zeile</strong> und jede <strong>Spalte</strong> der Matrix mit einem Faktor, sodass die Beträge überall in derselben Größenordnung landen:</p>
<p>mit Diagonalmatrizen <spanclass="math inline">\mathbf{D}_r</span> (Zeilen) und <spanclass="math inline">\mathbf{D}_c</span> (Spalten). Das <strong>Ruiz-Verfahren</strong> bestimmt diese Faktoren iterativ: In jedem Durchlauf wird jede Zeile durch die Wurzel ihres größten Betrags geteilt, danach jede Spalte. Nach wenigen Durchläufen liegen alle Zeilen- und Spaltenmaxima bei 1.</p>
<p>Wirtschaftlich passiert dabei nichts: Eine Zeilenskalierung heißt „diese Nebenbedingung in einer anderen Einheit messen“, eine Spaltenskalierung „diese Variable in einer anderen Einheit messen“. Die Lösung rechnet man mit <spanclass="math inline">\mathbf{x} = \mathbf{D}_c\,\tilde{\mathbf{x}}</span> zurück.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Jeder ernsthafte Solver skaliert intern selbst — HiGHS, Gurobi, CP-SAT alle. Verlassen Sie sich trotzdem nicht darauf: Die interne Skalierung repariert die Rechnung, nicht die Modellierung. Wenn Ihre Koeffizienten zwölf Größenordnungen überspannen, sagt Ihnen das etwas über Ihr Modell, nicht über den Solver.</p>
</blockquote>
<h3id="das-experiment">Das Experiment</h3>
<p>Das folgende Programm führt beides vor — die Fehlerverstärkung und ihre Behebung — und schließt mit der praktischen Konsequenz, die Sie in <ahref="milp.html#kap-milp">Kapitel 6</a> wieder brauchen werden: warum eine Binärvariable mit dem Wert <code>0.99999998</code> niemals mit <code>int()</code> gerundet werden darf.</p>
<spanid="cb9-5"><ahref="#cb9-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel Fundament: Was die Konditionszahl kappa(A) praktisch bedeutet - und wie man</span></span>
<spanid="cb9-6"><ahref="#cb9-6"aria-hidden="true"tabindex="-1"></a><spanclass="co">ein schlecht skaliertes Modell wieder gesund rechnet.</span></span>
<spanid="cb9-10"><ahref="#cb9-10"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 1. Fehlerverstaerkung: Wie stark schlaegt eine winzige Datenunsicherheit auf</span></span>
<spanid="cb9-11"><ahref="#cb9-11"aria-hidden="true"tabindex="-1"></a><spanclass="co"> die Loesung durch? kappa(A) ist genau die Obergrenze dieses Faktors.</span></span>
<spanid="cb9-12"><ahref="#cb9-12"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 2. Ruiz-Equilibrierung: Ein Modell, in dem Euro-Betraege (1e7) und</span></span>
<spanid="cb9-13"><ahref="#cb9-13"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Tonnen-Angaben (1e-3) in derselben Matrix stehen, wird durch Zeilen- und</span></span>
<spanid="cb9-14"><ahref="#cb9-14"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Spaltenskalierung um Groessenordnungen besser konditioniert.</span></span>
<spanid="cb9-15"><ahref="#cb9-15"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 3. Toleranzen: Warum eine Binaervariable mit dem Wert 0.99999998 niemals</span></span>
<spanid="cb9-16"><ahref="#cb9-16"aria-hidden="true"tabindex="-1"></a><spanclass="co"> mit int() gerundet werden darf.</span></span>
<spanid="cb9-33"><ahref="#cb9-33"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Stoert die rechte Seite b relativ um 'stoerung' in zufaellige Richtungen</span></span>
<spanid="cb9-34"><ahref="#cb9-34"aria-hidden="true"tabindex="-1"></a><spanclass="co"> und misst, um welchen Faktor sich der Fehler in der Loesung x vergroessert.</span></span>
<spanid="cb9-36"><ahref="#cb9-36"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Liefert (Median, Maximum) der Verstaerkung. Die Theorie sagt: Das Maximum</span></span>
<spanid="cb9-37"><ahref="#cb9-37"aria-hidden="true"tabindex="-1"></a><spanclass="co"> kann bis kappa(A) betragen - und nur bis dahin.</span></span>
<spanid="cb9-43"><ahref="#cb9-43"aria-hidden="true"tabindex="-1"></a> richtung <spanclass="op">=</span> RNG.normal(size<spanclass="op">=</span>b.size)</span>
<spanid="cb9-44"><ahref="#cb9-44"aria-hidden="true"tabindex="-1"></a> richtung <spanclass="op">/=</span> np.linalg.norm(richtung)</span>
<spanid="cb9-45"><ahref="#cb9-45"aria-hidden="true"tabindex="-1"></a> b_gestoert <spanclass="op">=</span> b <spanclass="op">+</span> stoerung <spanclass="op">*</span> np.linalg.norm(b) <spanclass="op">*</span> richtung</span>
<spanid="cb9-54"><ahref="#cb9-54"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" 1. KONDITIONSZAHL ALS FEHLERVERSTAERKER"</span>)</span>
<spanid="cb9-56"><ahref="#cb9-56"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Zwei Gleichungssysteme, beide exakt loesbar mit x = (1, 1)."</span>)</span>
<spanid="cb9-57"><ahref="#cb9-57"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Die rechte Seite wird um relativ 1e-10 gestoert - so viel Unsicherheit"</span>)</span>
<spanid="cb9-58"><ahref="#cb9-58"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"steckt in JEDER gemessenen Betriebszahl allemal.</span><spanclass="ch">\n</span><spanclass="st">"</span>)</span>
<spanid="cb9-66"><ahref="#cb9-66"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> name, A <spanclass="kw">in</span> modelle.items():</span>
<spanid="cb9-67"><ahref="#cb9-67"aria-hidden="true"tabindex="-1"></a> median, maximum <spanclass="op">=</span> fehlerverstaerkung(A)</span>
<spanid="cb9-70"><ahref="#cb9-70"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st">Lesart: Bei der zweiten Matrix wird aus einem Datenfehler in der"</span>)</span>
<spanid="cb9-71"><ahref="#cb9-71"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"10. Nachkommastelle ein Loesungsfehler in der 2. Nachkommastelle."</span>)</span>
<spanid="cb9-72"><ahref="#cb9-72"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Das Modell ist mathematisch korrekt - und praktisch wertlos."</span>)</span>
<spanid="cb9-79"><ahref="#cb9-79"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Skaliert A iterativ so, dass alle Zeilen- und Spaltenmaxima nahe 1</span></span>
<spanid="cb9-80"><ahref="#cb9-80"aria-hidden="true"tabindex="-1"></a><spanclass="co"> liegen (Ruiz 2001).</span></span>
<spanid="cb9-82"><ahref="#cb9-82"aria-hidden="true"tabindex="-1"></a><spanclass="co"> In jedem Durchlauf wird jede Zeile durch die Wurzel ihres Betragsmaximums</span></span>
<spanid="cb9-83"><ahref="#cb9-83"aria-hidden="true"tabindex="-1"></a><spanclass="co"> geteilt, danach jede Spalte. Das Verfahren konvergiert schnell und braucht</span></span>
<spanid="cb9-84"><ahref="#cb9-84"aria-hidden="true"tabindex="-1"></a><spanclass="co"> keinerlei Wissen ueber die Bedeutung der Zahlen - genau deshalb steckt es</span></span>
<spanid="cb9-85"><ahref="#cb9-85"aria-hidden="true"tabindex="-1"></a><spanclass="co"> in jedem ernsthaften Solver als Vorverarbeitung.</span></span>
<spanid="cb9-115"><ahref="#cb9-115"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Ein Produktionsmodell, in dem vier Ressourcen in voellig"</span>)</span>
<spanid="cb9-117"><ahref="#cb9-117"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Zeile 1: Kapitalbindung in Euro (Groessenordnung 1e7)"</span>)</span>
<spanid="cb9-118"><ahref="#cb9-118"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Zeile 2: Katalysatorverbrauch in Tonnen (Groessenordnung 1e-3)"</span>)</span>
<spanid="cb9-119"><ahref="#cb9-119"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Zeile 3: Energie in Wattsekunden (Groessenordnung 1e5)"</span>)</span>
<spanid="cb9-120"><ahref="#cb9-120"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Zeile 4: Ausschussquote als Anteil (Groessenordnung 1e-2)</span><spanclass="ch">\n</span><spanclass="st">"</span>)</span>
<spanid="cb9-122"><ahref="#cb9-122"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Quadratisch gewaehlt, damit die Loesung eindeutig ist und die</span></span>
<spanid="cb9-123"><ahref="#cb9-123"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Ruecktransformation unten wirklich etwas beweist.</span></span>
<spanid="cb9-124"><ahref="#cb9-124"aria-hidden="true"tabindex="-1"></a> n <spanclass="op">=</span><spanclass="dv">4</span></span>
<spanid="cb9-139"><ahref="#cb9-139"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Gegenprobe: Das skalierte Modell beschreibt dasselbe Problem. Wer x_s</span></span>
<spanid="cb9-140"><ahref="#cb9-140"aria-hidden="true"tabindex="-1"></a><spanclass="co"># loest, erhaelt die urspruengliche Loesung durch x = c * x_s.</span></span>
<spanid="cb9-145"><ahref="#cb9-145"aria-hidden="true"tabindex="-1"></a> x_zurueck <spanclass="op">=</span> c <spanclass="op">*</span> x_s</span>
<spanid="cb9-146"><ahref="#cb9-146"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="ch">\n</span><spanclass="ss">Ruecktransformation x = c * x_s: groesste Abweichung zur wahren "</span></span>
<spanid="cb9-148"><ahref="#cb9-148"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Die Skalierung ist also verlustfrei - sie aendert nur die Zahlen,"</span>)</span>
<spanid="cb9-149"><ahref="#cb9-149"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"nicht das Problem."</span>)</span>
<spanid="cb9-152"><ahref="#cb9-152"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Experiment 3: Toleranzen und der int()-Fehler --------------------------</span></span>
<spanid="cb9-159"><ahref="#cb9-159"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Ein LP, dessen Optimum bei x = 1 liegt, aber vom Solver nur bis auf</span></span>
<spanid="cb9-160"><ahref="#cb9-160"aria-hidden="true"tabindex="-1"></a><spanclass="co"># seine Toleranz getroffen wird.</span></span>
<spanid="cb9-164"><ahref="#cb9-164"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Solver liefert x = </span><spanclass="sc">{</span>wert<spanclass="sc">!r}</span><spanclass="ss">"</span>)</span>
<spanid="cb9-166"><ahref="#cb9-166"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Typische Werte, wie sie aus MILP-Solvern zurueckkommen.</span></span>
<spanid="cb9-171"><ahref="#cb9-171"aria-hidden="true"tabindex="-1"></a><spanclass="fl">0.99999998</span>: <spanclass="st">"int() macht aus einer JA- eine NEIN-Entscheidung"</span>,</span>
<spanid="cb9-172"><ahref="#cb9-172"aria-hidden="true"tabindex="-1"></a><spanclass="fl">1.00000002</span>: <spanclass="st">"hier ginge int() zufaellig gut - Verlass ist keiner"</span>,</span>
<spanid="cb9-173"><ahref="#cb9-173"aria-hidden="true"tabindex="-1"></a><spanclass="fl">0.49999999</span>: <spanclass="st">"echt unentschieden: Modell oder Toleranz pruefen!"</span>,</span>
<spanid="cb9-174"><ahref="#cb9-174"aria-hidden="true"tabindex="-1"></a><spanclass="fl">2.9999999</span>: <spanclass="st">"3 Maschinen werden zu 2 - der Plan geht nicht auf"</span>,</span>
<spanid="cb9-180"><ahref="#cb9-180"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st">Richtige Vorgehensweise: gegen die Solver-Toleranz pruefen,"</span>)</span>
<spanid="cb9-181"><ahref="#cb9-181"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"dann erst runden - und den Zweifelsfall melden statt still zu raten."</span>)</span>
<spanid="cb9-187"><ahref="#cb9-187"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"</span><spanclass="sc">{</span>wert<spanclass="sc">}</span><spanclass="ss"> ist </span><spanclass="sc">{</span><spanclass="bu">abs</span>(wert <spanclass="op">-</span> naechste)<spanclass="sc">:.2e}</span><spanclass="ss"> von der naechsten ganzen "</span></span>
<spanid="cb9-188"><ahref="#cb9-188"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"Zahl entfernt - das ist mehr als die Toleranz </span><spanclass="sc">{</span>toleranz<spanclass="sc">}</span><spanclass="ss">. "</span></span>
<spanid="cb9-189"><ahref="#cb9-189"aria-hidden="true"tabindex="-1"></a><spanclass="st">"Ganzzahligkeit im Modell pruefen."</span>)</span>
<spanid="cb9-204"><ahref="#cb9-204"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Merksatz: Skalieren Sie Ihre Daten, BEVOR der Solver sie sieht -"</span>)</span>
<spanid="cb9-205"><ahref="#cb9-205"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"und runden Sie Solver-Ergebnisse NIE ohne Toleranzpruefung."</span>)</span>
0.99999998 0 1 int() macht aus einer JA- eine NEIN-Entscheidung
1.00000002 1 1 hier ginge int() zufaellig gut - Verlass ist keiner
0.49999999 0 0 echt unentschieden: Modell oder Toleranz pruefen!
2.99999990 2 3 3 Maschinen werden zu 2 - der Plan geht nicht auf
Richtige Vorgehensweise: gegen die Solver-Toleranz pruefen,
dann erst runden - und den Zweifelsfall melden statt still zu raten.
sichere_ganzzahl(0.99999998) = 1
sichere_ganzzahl(1.00000002) = 1
sichere_ganzzahl(0.49999999) -> ValueError: 0.49999999 ist 5.00e-01 von der naechsten ganzen Zahl entfernt - das ist mehr als die Toleranz 1e-06. Ganzzahligkeit im Modell pruefen.
<td>Störung in <strong>zufälliger Richtung</strong></td>
<td>200 Versuche statt einer festen Störung</td>
<td>Eine proportionale Störung (<spanclass="math inline">b \cdot 1{,}0000000001</span>) trifft die empfindliche Richtung meist gar nicht — man sähe fälschlich gar keinen Effekt. Die Schwäche zeigt sich nur, wenn man in alle Richtungen stößt.</td>
<td>Die Schranke aus der Formel oben ist <strong>scharf</strong>: Es gibt eine Richtung, in der die volle Verstärkung eintritt. Sie ist keine pessimistische Abschätzung.</td>
<td>Nur bei eindeutiger Lösung beweist die Rückrechnung etwas. Bei unterbestimmten Systemen liefert <code>lstsq</code> die Minimum-Norm-Lösung, und die ist unter Skalierung <em>nicht</em> invariant — eine Falle, in die man leicht tappt.</td>
</tr>
<trclass="even">
<td>Abweichung <code>2.35e-14</code></td>
<td>Rückrechnung trifft die wahre Lösung</td>
<td>Der Beleg, dass Skalieren nichts kaputt macht: Es ändert die Zahlen, nicht das Problem.</td>
</tr>
<trclass="odd">
<td><code>sichere_ganzzahl()</code></td>
<td>prüft Abstand zur nächsten ganzen Zahl</td>
<td>Der Zweifelsfall (<code>0.49999999</code>) wird <strong>gemeldet</strong>, nicht stillschweigend geraten. Genau diese Funktion gehört in jedes MILP-Auswertungsskript.</td>
</tr>
</tbody>
</table>
</blockquote>
<blockquote>
<p><strong>⚠️ Typische Fehler</strong></p>
<ul>
<li><strong><code>int()</code> auf Solver-Ergebnisse.</strong><code>int(0.99999998) == 0</code> — aus einem „ja, Standort eröffnen“ wird ein „nein“. <code>int()</code> schneidet ab, es rundet nicht. Und selbst <code>round()</code> ist nur mit vorheriger Toleranzprüfung vertretbar.</li>
<li><strong>Große Zahlen für „Sicherheit“.</strong> Ein Big-M von <spanclass="math inline">10^9</span> „damit es garantiert reicht“ verschlechtert <spanclass="math inline">\kappa</span> um neun Größenordnungen und erzeugt genau die Instabilität, die man vermeiden wollte.</li>
<li><strong>Auf die interne Solver-Skalierung vertrauen.</strong> Sie hilft der Rechnung, aber sie kann Ihnen nicht sagen, dass Sie Stück und Tonnen vermischt haben.</li>
<li><strong><spanclass="math inline">\kappa</span> nie nachschauen.</strong> Eine Zeile <code>np.linalg.cond(A)</code> beim Modellaufbau, geloggt neben der Laufzeit, hätte in vielen Projekten Wochen Fehlersuche gespart.</li>
<p><strong>Aufgabe 2.1 ⭐ — Matrixform lesen.</strong> Gegeben <spanclass="math inline">\mathbf{c} = (4, 1, 6)^\top</span>, <spanclass="math inline">\mathbf{A} = \begin{pmatrix} 1 & 2 & 0 \\ 0 & 1 & 3 \end{pmatrix}</span>, <spanclass="math inline">\mathbf{b} = (10, 12)^\top</span>. Schreiben Sie das Modell ausgeschrieben hin. Wie viele Variablen und wie viele Nebenbedingungen hat es?</p>
<p><strong>Aufgabe 2.2 ⭐ — Standardform herstellen.</strong> Bringen Sie in die Form <spanclass="math inline">\min \mathbf{c}^\top\mathbf{x}</span> u. d.N. <spanclass="math inline">\mathbf{A}\mathbf{x} \le \mathbf{b}</span>: <spanclass="math display">\max\ 7x_1 - 2x_2 \quad \text{u. d. N.}\quad 4x_1 + x_2 \ge 20,\quad x_1 - x_2 = 3,\quad x_1, x_2 \ge 0</span></p>
<p><strong>Aufgabe 2.3 ⭐⭐ — Ecken von Hand.</strong> Für <spanclass="math inline">\max 2x_1 + 3x_2</span> u. d.N. <spanclass="math inline">x_1 + x_2 \le 8</span>, <spanclass="math inline">2x_1 + x_2 \le 12</span>, <spanclass="math inline">x_1, x_2 \ge 0</span>: (a) Zeichnen Sie den zulässigen Bereich auf kariertes Papier. (b) Bestimmen Sie alle Ecken. (c) Werten Sie <spanclass="math inline">Z</span> in jeder Ecke aus und geben Sie das Optimum an. (d) Was passiert, wenn die Zielfunktion zu <spanclass="math inline">\max 2x_1 + 2x_2</span> wird? Wie viele optimale Lösungen gibt es dann?</p>
<p><strong>Aufgabe 2.5 ⭐⭐ — Positive Semidefinitheit prüfen.</strong> Prüfen Sie mit NumPy, welche dieser Matrizen als Kovarianzmatrix taugen (alle Eigenwerte <spanclass="math inline">\ge 0</span>), und geben Sie bei Nichteignung die implizierte Korrelation an: <spanclass="math display">\mathbf{P}_1=\begin{pmatrix}4&1\\1&9\end{pmatrix},\quad \mathbf{P}_2=\begin{pmatrix}4&7\\7&9\end{pmatrix},\quad \mathbf{P}_3=\begin{pmatrix}1&0.5&0.5\\0.5&1&0.5\\0.5&0.5&1\end{pmatrix}</span></p>
<p><strong>Aufgabe 2.6 ⭐⭐⭐ — Eigene Visualisierung.</strong> Passen Sie <code>Visualisierung_Loesungsraum.py</code> an das Bäckerei-Problem aus <ahref="einfuehrung.html#kap-einfuehrung">Kapitel 1</a> (die Bäckerei-Handrechnung aus <ahref="einfuehrung.html#kap-einfuehrung">Kapitel 1</a>) an: <spanclass="math inline">\max 2{,}5x_1 + 3{,}0x_2</span> u. d.N. <spanclass="math inline">0{,}5x_1+0{,}6x_2 \le 90</span>, <spanclass="math inline">4x_1+3x_2 \le 600</span>, <spanclass="math inline">x_1 \ge 40</span>. Geben Sie alle Ecken mit Zielwerten aus und zeichnen Sie das Polyeder.</p>
<p><strong>Aufgabe 2.7 ⭐⭐⭐ — Konvexität einer Kovarianzmatrix reparieren.</strong> Die Matrix <spanclass="math inline">\mathbf{P}_2</span> aus der Aufgabe <em>Positive Semidefinitheit prüfen</em> ist nicht positiv semidefinit. Schreiben Sie eine Funktion <code>repariere(P)</code>, die die negativen Eigenwerte auf 0 setzt und die Matrix rekonstruiert (<em>Eigenwert-Clipping</em>): <spanclass="math inline">\mathbf{P}_{\text{rep}} = \mathbf{V}\max(\boldsymbol{\Lambda},0)\mathbf{V}^\top</span>. Prüfen Sie das Ergebnis und vergleichen Sie es mit dem Original.</p>
<hr/>
<h2id="sec:fundament-denkfehler">2.9 Finde den Denkfehler</h2>
<spanid="cb11-10"><ahref="#cb11-10"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"x ="</span>, np.<spanclass="bu">round</span>(r.x, <spanclass="dv">2</span>), <spanclass="st">" Z ="</span>, <spanclass="bu">round</span>(<spanclass="op">-</span>r.fun, <spanclass="dv">2</span>))</span></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>x = [5.6 0.8] Z = 20.8</code></pre>
<p>Kein Fehler, kein Warnhinweis, ein völlig unauffälliger Zielwert. Das korrekte Optimum wäre <spanclass="math inline">\mathbf{x}^* = (3{,}2;\ 2{,}4)</span> mit <spanclass="math inline">Z^* = 21{,}6</span> gewesen.</p>
<p><strong>Ihre Aufgabe:</strong> (a) Warum fällt dieser Fehler weder Python noch dem Solver auf? (b) Prüfen Sie den Plan <spanclass="math inline">(5{,}6;\ 0{,}8)</span> gegen die <strong>echten</strong> Nebenbedingungen — was stellen Sie fest? (c) Wieso ist es besonders gefährlich, dass der falsche Zielwert <em>kleiner</em> ist als der richtige? (d) Welche zwei Zeilen Code hätten den Fehler beim Einlesen sofort aufgedeckt?</p>
<p><strong>❓ Micro-Quiz 2: Drei Fragen zum Selbstcheck</strong></p>
<p>Genau eine Antwort ist jeweils richtig. Auflösung in <ahref="anhang-loesungen.html#quiz-loesung-fundament">Anhang A</a>.</p>
<p><strong>1. Ein lineares Programm hat 6 Variablen und 4 Ungleichungen (plus Nichtnegativität). Was garantiert der Fundamentalsatz der linearen Optimierung?</strong> (a) Dass es genau eine optimale Lösung gibt. (b) Dass es — sofern ein Optimum existiert — mindestens eine optimale Lösung in einer Ecke des zulässigen Polyeders gibt. (c) Dass sich das Optimum durch Ausprobieren aller Ecken in vertretbarer Zeit finden lässt.</p>
<p><strong>2. <code>np.linalg.cond(A)</code> liefert für Ihre Technologiematrix den Wert <spanclass="math inline">3 \cdot 10^{11}</span>. Was folgt daraus?</strong> (a) Das Modell ist unlösbar und muss neu formuliert werden. (b) Von den rund 16 Stellen Rechengenauigkeit bleiben etwa 5 übrig — die Ergebnisse sind mit Vorsicht zu genießen, und die Ursache liegt meist in gemischten Einheiten. (c) Der Solver braucht <spanclass="math inline">3 \cdot 10^{11}</span> Iterationen.</p>
<p><strong>3. Ein MILP-Solver liefert für eine Binärvariable den Wert <code>0.99999998</code>. Wie werten Sie ihn aus?</strong> (a) <code>int(wert)</code> — das ist die Standardumwandlung in Python. (b) Wert unverändert weiterreichen, der Solver wird schon recht haben. (c) Abstand zur nächsten ganzen Zahl gegen die Solver-Toleranz prüfen, dann runden — und einen Wert wie <code>0.5</code> als Fehler melden statt zu raten.</p>
<li><strong>Matrixform</strong> bündelt beliebig viele Variablen und Bedingungen in drei Zeilen: <spanclass="math inline">\min \mathbf{c}^\top\mathbf{x}</span> u. d.N. <spanclass="math inline">\mathbf{A}\mathbf{x} \le \mathbf{b}</span>, <spanclass="math inline">\mathbf{x} \ge \mathbf{0}</span>. Jede Zeile von <spanclass="math inline">\mathbf{A}</span> ist eine Ressource, jede Spalte eine Variable.</li>
<li><strong>Der zulässige Bereich</strong> ist der Schnitt aller Halbräume: ein konvexes Polyeder.</li>
<li><strong>Der Fundamentalsatz</strong> garantiert, dass ein Optimum in einer Ecke liegt — endlich viele Kandidaten statt unendlich vieler Punkte.</li>
<li><strong>Kondition und Skalierung</strong> entscheiden darüber, ob aus mathematisch korrekten Zahlen auch brauchbare werden: <spanclass="math inline">\kappa(\mathbf{A})</span> ist der Verstärkungsfaktor zwischen Daten- und Lösungsfehler, gemischte Einheiten sind seine häufigste Ursache, Ruiz-Equilibrierung die Gegenmaßnahme — und Solver-Ergebnisse werden nur nach Toleranzprüfung gerundet.</li>
<li><strong>Konvexität</strong> ist die eigentliche Trennlinie zwischen leicht und schwer. Bei konvexen Problemen ist jedes lokale Optimum global; bei nicht-konvexen hängt das Ergebnis vom Startpunkt ab.</li>
<li><strong>Prüfen Sie Matrizen</strong>, bevor Sie sie als Kovarianzmatrix verwenden: alle Eigenwerte müssen <spanclass="math inline">\ge 0</span> sein.</li>
</ul>
<p><strong>Ausblick.</strong><ahref="oekosystem.html#kap-oekosystem">Kapitel 3</a> ordnet das Python-Ökosystem: Welche Bibliothek löst welche Problemklasse, und warum gibt es überhaupt mehrere?</p>
<navclass="prev-next"><aclass="prev-next-knopf prev-next-prev"href="einfuehrung.html"><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></a><aclass="prev-next-knopf prev-next-next"href="oekosystem.html"><span><small>Weiter</small>Kapitel 3: Das Python-Ökosystem für OR — Solver, Bindings und Modellierungsschichten</span><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-right"></use></svg></a></nav>